/*
 * 321 Peptides — design tokens and base styles.
 * Variable names mirror docs/02-DESIGN-SYSTEM.md and the values baked into the .dc.html
 * prototypes verbatim, so template markup can reference var(--token) directly.
 */

:root {
	/* dark surfaces */
	--ink: #08171c;
	--ink-2: #0f2830;
	--ink-3: #17333c;
	--hair-dk: #1e3b45;

	/* light surfaces */
	--page: #f4f7f8;
	--panel: #ffffff;
	--band: #e5edef;
	--hair: #d2dde0;

	/* brand */
	--brand: #02aecb;
	--brand-dk: #01758a;
	--brand-lt: #7ed2db;

	/* alert -- the theme's error red, already the colour of .woocommerce-error and
	   .p321-checkout-error. Promoted to a token on its third use (2026-08-26). 4.98:1 on white,
	   so it clears AA for text as well as the 3:1 floor for a border. */
	--alert: #c2453f;

	/*
	 * The rest of the error pair. page-contact.php hard-coded an entire error card -- #C2413B
	 * (a four-digit near-miss of --alert, and guaranteed to drift away from it), #FBEDEC and
	 * #7A2A26 -- because the site had a danger *accent* but no danger *surface* or *text*
	 * (14-SITE-AUDIT.md D9). Added 2026-09-03 so the card can be a class instead of an inline
	 * style. Measured: --alert-ink on --alert-bg is 8.43:1 (AAA), and --alert as a 1px border
	 * on --alert-bg is 4.37:1, comfortably over the 3:1 floor for a non-text UI component.
	 */
	--alert-bg: #fbedec;
	--alert-ink: #7a2a26;

	/* text */
	--t1: #08171c;
	--t2: #53676e;

	/*
	 * Text on dark, primary. The series ran od2/od3/od4 with no od1, so the one colour it was
	 * missing -- plain white -- had nowhere to live, and the templates hard-coded
	 * `style="color: #fff"` instead (14-SITE-AUDIT.md D9). Added 2026-09-02.
	 *
	 * Conversion done 2026-09-03: all sixteen (the audit said fourteen; it undercounted) were
	 * `.display-1`/`.display-2` headings inside a `.band-dark`, with no exceptions, so they are
	 * now covered by one descendant rule below rather than sixteen inline styles.
	 */
	--od1: #ffffff;
	--od2: #b4c7cd;
	--od3: #9db4bb;
	--od4: #7e959c;

	/* radii */
	--rb: 999px;
	--rc: 18px;
	--ri: 12px;

	/*
	 * One phone/not-phone boundary: 895 / 896 (2026-08-26).
	 *
	 * The site had two, 5px apart. Eight blocks collapsed at `max-width: 900px` -- the header nav,
	 * the cart, checkout, legal and account shells, and three grids -- while the sticky bars and
	 * the PDP hero reorder used 895/896. That left 896-900 as a band belonging to neither: the
	 * sticky bar was already suppressed (`min-width: 896px`) while the cart was still a single
	 * column, so Proceed to checkout sat at y=862 against a 414px landscape fold with nothing
	 * standing in for it. 896px is not a hypothetical width -- it is the iPhone XR / 11 / 11 Pro
	 * Max in landscape.
	 *
	 * 895/896 rather than 900/901 because the boundary then falls on a real device edge rather
	 * than 4px above one, and because the sticky-bar and hero values were the device-verified pair.
	 * CLAUDE.md rule 9 is also explicit that 900 is nobody's device.
	 *
	 * These are documentation, not switches: media queries cannot read a custom property, so the
	 * literals below still have to be edited by hand. Search for both numbers when changing them.
	 */

	/* layout */
	--content-max: 1320px;
	--gutter: clamp(16px, 4vw, 26px);
	--section-pad: clamp(56px, 8vw, 96px);
	--card-pad: clamp(18px, 3vw, 30px);
}

/*
 * Metric-matched fallbacks for the two self-hosted variable fonts, computed from
 * each font's own OS/2/hhea tables against Arial as the reference. Without these,
 * the swap from system-ui/Arial to the real webfont reflows the page (measured
 * ~0.15 CLS on the About page) because Archivo and Instrument Sans both run
 * noticeably wider/taller than Arial at the same font-size.
 */
@font-face {
	font-family: "Archivo Fallback";
	src: local("Arial");
	size-adjust: 130.27%;
	ascent-override: 67.4%;
	descent-override: 16.12%;
	line-gap-override: 0%;
}

@font-face {
	font-family: "Instrument Sans Fallback";
	src: local("Arial");
	size-adjust: 127.77%;
	ascent-override: 75.92%;
	descent-override: 19.57%;
	line-gap-override: 0%;
}

/*
 * The third face needed the same treatment and had been missed (owner, 2026-08-26). IBM Plex Mono
 * is declared `font-display: swap` like the other two but is deliberately not preloaded
 * (functions.php: "micro-labels only"), so its fallback window is the *longest* of the three --
 * and it was the only stack with no matched fallback in it. Every mono element therefore grew when
 * the webfont landed: a 13px micro-label measured 212.9x15 in the fallback against 229.8x17 real,
 * so 2px of extra line height across prices, eyebrows, spec tables, breadcrumbs and lot codes.
 *
 * Courier New rather than one of the nicer monospaces, and alone rather than in a `local()` list:
 * monospace faces do *not* all share an advance width, which was the assumption worth checking.
 * Measured per 100px -- IBM Plex Mono 0.6000, Courier New 0.6001, Consolas 0.5498. Only Courier
 * matches, so only Courier can carry these overrides; listing Consolas or Menlo alongside it would
 * hand them a size-adjust computed for a font 9% wider than they are.
 *
 * That near-exact advance means this fallback matches width as well as height, so mono text does
 * not re-wrap on swap either -- better than the two above, which are height-matched only.
 *
 * Where `local("Courier New")` does not resolve, this @font-face is simply unused and the stack
 * falls through to `monospace` exactly as it did before.
 */
@font-face {
	font-family: "IBM Plex Mono Fallback";
	src: local("Courier New");
	size-adjust: 99.98%;
	ascent-override: 103.02%;
	descent-override: 28%;
	line-gap-override: 0%;
}

*,
*::before,
*::after {
	box-sizing: border-box;
}

/*
 * The native `hidden` attribute's implicit display:none is a UA-stylesheet rule, which author
 * rules always beat regardless of source order or specificity -- any class that sets its own
 * `display` (`.p321-product-grid`, `.p321-btn`, etc.) silently cancels `hidden` on the same
 * element. Toggling `.hidden = true/false` from JS is already the established pattern
 * (initFilterSheet(), initCoaLotSelect(), initExpandableRails()), so make it actually work
 * everywhere rather than relying on no class ever setting `display` on a hideable element.
 */
[hidden] {
	display: none !important;
}

html {
	scroll-behavior: smooth;
	-webkit-text-size-adjust: 100%;
}

body {
	background: var(--page);
	color: var(--t1);
	font-family: "Instrument Sans", "Instrument Sans Fallback", system-ui, sans-serif;
	margin: 0;
}

a {
	color: var(--brand-dk);
	text-decoration: none;
}

a:hover {
	color: var(--brand);
}

/* WordPress's standard visually-hidden class. The theme uses it itself (the header search label,
   the Reviews section heading) and WooCommerce emits it too -- on every sale price, as
   "Original price was: ..." / "Current price is: ...". It was working only because WooCommerce's
   own stylesheets happen to define it: nothing in this theme did, so dequeuing Woo's CSS (a very
   likely Milestone F performance move) would have made all of that text visible. The theme uses
   the class, so the theme should own the rule. */
.screen-reader-text {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* WooCommerce marks a sale price up as <del>old</del> <ins>new</ins>, and <ins> carries a browser
   default underline, so every sale price rendered as if the new price were a link. The design uses
   a plain line-through on the old price and no decoration on the new one. */
ins {
	text-decoration: none;
	background: none;
}

.screen-reader-text:focus {
	position: static;
	width: auto;
	height: auto;
	margin: 0;
	overflow: visible;
	clip-path: none;
	white-space: normal;
}

/* Buttons carry their own colour scheme, so they have to be excluded here. Without the :not()s
   this generic descendant rule (specificity 0,1,1) silently beats .p321-btn's own colour (0,1,0):
   a solid teal button inside a dark band got a teal label on a teal background, 1.53:1, which axe
   caught on the homepage hero CTA. The design has these at #03242B on brand and #fff for the
   outline variant, which is what the component rules already say. */
.band-dark a:not(.p321-btn):not(.wp-block-button__link) {
	color: var(--brand-lt);
}

.band-dark a:not(.p321-btn):not(.wp-block-button__link):hover {
	color: var(--brand);
}

img {
	max-width: 100%;
	display: block;
}

:focus-visible {
	/* 3px, not the rounded 2px in 02-DESIGN-SYSTEM.md -- every .dc.html source file (Collection,
	   PDP, Cart, Checkout) consistently uses outline:3px, so the source wins per CLAUDE.md rule 2. */
	outline: 3px solid var(--brand);
	outline-offset: 2px;
}

/*
 * Skip link -- WCAG 2.4.1 Bypass Blocks, Level A (14-SITE-AUDIT.md D7).
 *
 * The header puts the logo, the primary nav, search, account, cart and the menu toggle in front of
 * <main> on every template, so a sighted keyboard user tabbed through all of it on every page.
 *
 * This never showed in axe because a <main> landmark and a heading structure already exist, and
 * those satisfy bypass-blocks for screen readers -- landmark jumping is the bypass. It does nothing
 * for someone driving the page from the Tab key, which is why "axe: 0 violations" and this gap
 * coexisted for the whole build.
 *
 * Moved off-canvas rather than `display: none` or `visibility: hidden`: both would make it
 * unfocusable and defeat the point entirely.
 *
 * z-index 100 clears the sticky header (60) and the highest layer in this sheet (90), so the
 * focused control is never covered -- WCAG 2.2 focus-not-obscured. `top` is 0 rather than an offset
 * below the header for the same reason: it must sit over the header, not under it.
 */
.p321-skip-link {
	position: absolute;
	top: 0;
	left: -9999px;
	z-index: 100;
	padding: 14px 22px;
	border-radius: 0 0 var(--ri) 0;
	background: var(--ink);
	color: var(--od1);
	font-family: 'IBM Plex Mono', 'IBM Plex Mono Fallback', monospace;
	font-size: 14px;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	text-decoration: none;
}

.p321-skip-link:focus {
	left: 0;
}

@media (prefers-reduced-motion: reduce) {
	*,
	*::before,
	*::after {
		animation-duration: 0.01ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.01ms !important;
		scroll-behavior: auto !important;
	}
}

/* Layout */

.container {
	max-width: var(--content-max);
	margin: 0 auto;
	padding: 0 var(--gutter);
}

/*
 * Vertical longhands, not the `padding` shorthand. 29 elements across nine templates carry
 * `class="container section"`, and the shorthand here silently reset the horizontal gutter
 * `.container` had just set -- same specificity (0,1,0), and this rule comes later. Desktop hid it,
 * because --content-max centres the block and supplies visual margin anyway; at 390px there is no
 * slack, so text and cards ran flush to both screen edges and clipped on the right.
 */
.section {
	padding-top: var(--section-pad);
	padding-bottom: var(--section-pad);
}

.band-light {
	background: var(--page);
	color: var(--t1);
}

.band-dark {
	background: var(--ink);
	color: var(--page);
}

.band-tint {
	background: var(--band);
	border-top: 1px solid var(--hair);
	border-bottom: 1px solid var(--hair);
}

/* Typography */

/*
 * Owner, 2026-08-23: 14px, up from 12.5px. Legibility only -- a mono label at 12.5px with 0.14em
 * tracking is small on a phone, and ~80% of this store's traffic is one.
 *
 * It does NOT change any contrast requirement, despite an earlier claim in session that it would.
 * WCAG "large text" is 18pt (24px), or 14pt bold (~18.7px bold) -- 14pt is not 14px. At 14px/400
 * this is still normal text, so AAA still asks 7:1 and the dark-band teal still sits at 6.89:1.
 * The levers that would actually reach AAA are 24px, 18.7px+bold, reverting to --brand-lt, or
 * lightening the teal a couple of points of green (#02b0cb measures ~7.02:1).
 */
.eyebrow {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 14px;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	margin: 0 0 14px;
}

.band-light .eyebrow,
.band-tint .eyebrow {
	color: var(--brand-dk);
}

/*
 * Owner, 2026-08-23: the brand teal, not the pale tint this used to carry. Contact and the FAQ both
 * use the literal word "Support" as their eyebrow and were rendering it in two different colours,
 * which was drift rather than a decision.
 *
 * Contrast on --ink drops from 10.55:1 to 6.89:1 -- comfortably AA at 12.5px, 0.11 short of AAA.
 * For scale, the light-band eyebrows above already ship at 4.99:1, so this is still the
 * higher-contrast half of the site.
 *
 * Twelve surfaces move with this: Contact, Reviews, the COA library, the bundles index, the
 * "Analytical data" band on every product and bundle page, the legal index, the eight legal and
 * company documents, and 404. See docs/11-SESSION-HANDOFF.md 5b-6 for the measured inventory.
 */
.band-dark .eyebrow {
	color: var(--brand);
}

/*
 * The one carve-out. This element wears .eyebrow but is overridden inline to 16.5px Instrument Sans
 * in sentence case, so it renders as a full sentence of running copy rather than a four-word mono
 * label. Recolouring the labels is a brand-consistency move; turning a sentence of body text teal
 * is a different decision, and the owner kept this one as it was.
 */
.band-dark .eyebrow--sentence {
	color: var(--brand-lt);
}

.display-1,
.display-2,
.display-3 {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	text-transform: uppercase;
	letter-spacing: -0.028em;
	margin: 0;
}

.display-1 {
	font-variation-settings: "wdth" 112, "wght" 800;
	font-size: clamp(36px, 6vw, 76px);
	line-height: 0.98;
}

.display-2 {
	font-variation-settings: "wdth" 108, "wght" 800;
	font-size: clamp(26px, 3.4vw, 48px);
	line-height: 1.06;
}

.display-3 {
	font-variation-settings: "wdth" 106, "wght" 750;
	font-size: 19px;
	line-height: 1.3;
	text-transform: none;
	letter-spacing: normal;
}

/*
 * Display headings on a dark band are white, not the band's inherited body colour.
 *
 * `.band-dark` sets `color: var(--page)` (#f4f7f8) for body copy, which is deliberately off-white.
 * A display heading wants the full-strength --od1 (#ffffff) against it. Every one of the sixteen
 * such headings in the theme previously carried `style="color: #fff"` to say so -- with no
 * exceptions, which is what made this safe to lift into one rule (14-SITE-AUDIT.md D9).
 *
 * .display-3 is included for consistency even though none currently sits on a dark band: the next
 * one to be added should not have to rediscover this.
 */
.band-dark .display-1,
.band-dark .display-2,
.band-dark .display-3 {
	color: var(--od1);
}

/*
 * Section headline + sub-headline pair. There was a real gap in the scale: `.display-2` tops out
 * at 48px and `.display-3` is a 19px card title, so a section that wanted a headline with a
 * smaller supporting line under it had nothing to reach for and used two `.display-2`s -- which
 * read as two competing headlines rather than a hierarchy. Used across the About page's sections.
 */
.p321-sec-title {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 108, "wght" 800;
	font-size: clamp(23px, 2.8vw, 38px);
	line-height: 1.08;
	letter-spacing: -0.028em;
	text-transform: uppercase;
	text-wrap: balance;
	margin: 0;
}

.p321-sec-sub {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 106, "wght" 750;
	font-size: clamp(16px, 1.9vw, 23px);
	line-height: 1.2;
	letter-spacing: -0.01em;
	text-transform: uppercase;
	color: var(--brand-dk);
	margin: 12px 0 0;
}

/* On a dark band the mid-tone brand blue does not carry; the light tint is the readable one. */
.band-dark .p321-sec-sub {
	color: var(--brand-lt);
}

/* A lead paragraph that stays at body size but keeps the weight -- for openers that introduce a
   block of copy without competing with the headline above them. */
.body-copy--lead {
	font-weight: 700;
	color: var(--t1);
}

.band-dark .body-copy--lead {
	color: var(--page);
}

/*
 * Cards in a columns row share a height. Core stretches the *columns* but not what sits inside
 * them, so a row of cards ended up ragged -- each one only as tall as its own copy, which reads as
 * a mistake when they are visually identical panels sitting side by side.
 */
.wp-block-columns .wp-block-column > .is-style-p321-card {
	height: 100%;
}

.lede {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 106, "wght" 700;
	font-size: clamp(21px, 2.5vw, 28px);
	line-height: 1.25;
	color: var(--t1);
	margin: 0 0 24px;
}

.body-copy {
	font-size: 17px;
	line-height: 1.7;
	color: var(--t2);
	max-width: 66ch;
	margin: 0 0 18px;
}

.band-dark .body-copy {
	color: var(--od2);
}

/* Inline links within a block of prose need a non-color distinguishing feature (WCAG 1.4.1) --
   the sitewide a{} rule alone (color only) fails this specifically where links sit inline among
   plain text, confirmed by axe-core's link-in-text-block on the My Account dashboard's welcome
   copy (the first page on this site with inline prose links, not a pre-existing gap elsewhere).

   .body-copy-sm was added to the same rule during Phase 5 Milestone A: the "Questions" callout
   shared by all eight reference documents uses it, and axe flagged the same violation on all eight
   (brand teal against body-copy grey is 1.1:1, against a 3:1 minimum). The document article's own
   prose is included too -- it has no inline links today, but it is owner-editable block content,
   so the next one added should not reintroduce this. */
.body-copy a,
.body-copy-sm a,
.p321-legal-article p a,
.p321-legal-article li a {
	text-decoration: underline;
	text-underline-offset: 2px;
}

.body-copy-sm {
	font-size: 15.5px;
	line-height: 1.62;
	color: var(--t2);
	margin: 0 0 12px;
}

.band-dark .body-copy-sm {
	color: var(--od3);
}

/* Cards */

.card {
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	background: var(--panel);
	padding: var(--card-pad);
}

.band-dark .card,
.card-dark {
	background: var(--ink-2);
	border-color: var(--hair-dk);
}

/* Lists: checkmark bullets, shared by hand-authored markup (.check-list) and the
   core/list "Checklist" block style so editors get the same look from the standard
   list block, without a dedicated custom block for it. */

.check-list,
.wp-block-list.is-style-p321-checklist {
	list-style: none;
	padding: 0;
	margin: 0;
	display: flex;
	flex-direction: column;
	gap: 13px;
}

.check-list li,
.wp-block-list.is-style-p321-checklist li {
	position: relative;
	padding-left: 30px;
	font-size: 16px;
	line-height: 1.55;
	color: var(--t2);
}

/*
 * One check mark, everywhere.
 *
 * The About page's three tick lists -- section 1's "you receive" card, section 3's outcome strip
 * and section 6's commitment list -- are the same semantic mark and the design draws all three
 * with the same 18px SVG. They had drifted into three implementations at two sizes and three
 * colours: this masked path at 18px, and two hand-drawn border-corner checks at 11x6 in --brand-dk
 * and --brand. Side by side on one page that reads as carelessness.
 *
 * Masked rather than 26 inline copies of the same `<svg>`. It is the design's own path either way,
 * but as a mask it stays in the stylesheet where a colour or weight change is one edit, instead of
 * bloating post_content the owner is meant to be able to edit.
 */
.check-list li::before,
.wp-block-list.is-style-p321-checklist li::before,
.p321-route-outcome__list li::before,
.p321-commit__list li::before {
	content: "";
	position: absolute;
	left: 0;
	top: 3px;
	width: 18px;
	height: 18px;
	border: 0;
	border-radius: 0;
	transform: none;
	background-color: var(--brand-dk);
	-webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="%23000" stroke-width="2.6" stroke-linecap="round" d="M4 12.5l5 5L20 6.5"/></svg>') no-repeat center / contain;
	mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="%23000" stroke-width="2.6" stroke-linecap="round" d="M4 12.5l5 5L20 6.5"/></svg>') no-repeat center / contain;
}

.band-dark .check-list li,
.band-dark .wp-block-list.is-style-p321-checklist li {
	color: var(--od2);
}

.band-dark .check-list li::before,
.band-dark .wp-block-list.is-style-p321-checklist li::before {
	background-color: var(--brand-lt);
}

/*
 * Logged-out My Account -- sign in / create an account.
 *
 * WooCommerce's own `.col2-set` floats its two forms at 48% and never stacks them, so the columns
 * measured 187px at 390, 192px at 834 and *158px at 1440* -- narrower as the screen got wider --
 * leaving a 48px-wide login field on desktop. A real grid instead: two-up only where two will fit,
 * one column otherwise, and a measure cap so the pair sits together rather than stranded either
 * side of an empty page.
 *
 * The fields themselves carry `.p321-form-row` in the template, so they inherit the contact form's
 * treatment rather than getting a second, parallel set of form styles.
 */
.p321-account-intro {
	margin-top: 14px;
	max-width: 56ch;
}

.p321-auth-grid {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: clamp(18px, 2.6vw, 28px);
	align-items: start;
	max-width: 880px;
	margin-top: clamp(26px, 3.4vw, 38px);
}

.p321-auth-grid--single {
	grid-template-columns: minmax(0, 1fr);
	max-width: 460px;
}

@media (max-width: 780px) {
	.p321-auth-grid {
		grid-template-columns: minmax(0, 1fr);
	}
}

.p321-auth-card {
	background: var(--panel);
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	padding: clamp(22px, 3vw, 30px);
}

.p321-auth-card__title {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 106, "wght" 750;
	font-size: 20px;
	text-transform: uppercase;
	letter-spacing: -0.01em;
	margin: 0 0 20px;
}

/* WooCommerce draws its own bordered, padded box around these two forms, which nested a second
   border inside the card. The card is the container; the form is just the fields. */
.p321-auth-card form.login,
.p321-auth-card form.register {
	border: 0;
	border-radius: 0;
	padding: 0;
	margin: 0;
}

/* Woo emits each row as a <p>; the grid gives them their rhythm so the margins do not double. */
.p321-auth-card .form-row {
	margin: 0 0 16px;
	display: grid;
	gap: 7px;
}

.p321-auth-card__note {
	margin: 0 0 18px;
	color: var(--t2);
}

.p321-auth-card__actions {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 14px;
	margin-top: 22px;
}

.p321-auth-card__actions .p321-btn {
	flex: 0 0 auto;
}

.p321-auth-remember {
	display: inline-flex;
	align-items: center;
	gap: 10px;
	min-height: 44px;
	font-size: 15px;
	color: var(--t2);
	cursor: pointer;
	margin: 0;
}

.p321-auth-remember input[type="checkbox"] {
	width: 24px;
	height: 24px;
	accent-color: var(--brand);
	flex: none;
	margin: 0;
}

.woocommerce-LostPassword {
	margin: 6px 0 0;
}

.woocommerce-LostPassword a {
	display: inline-flex;
	align-items: center;
	min-height: 44px;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12.5px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
}

/* Woo prints a `required` asterisk inside the label; keep it from inheriting the mono micro-label
   letter-spacing and drifting away from the word it marks. */
.p321-auth-card .required {
	/* Same red the theme already uses for form and checkout errors, rather than a second one. */
	color: var(--alert);
	letter-spacing: 0;
	text-decoration: none;
}

/* The quiet half of the cart's action row -- destructive, so it should be findable but never the
   loudest thing there. Keeps a 44px target despite having no button chrome. */
.p321-cart-clear {
	display: inline-flex;
	align-items: center;
	min-height: 44px;
	padding: 0;
	border: 0;
	background: none;
	cursor: pointer;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12.5px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--t2);
}

.p321-cart-clear:hover {
	color: var(--alert);
}

/*
 * Logged-in account screens: account details, addresses, downloads.
 *
 * These render inside `my-account.php`, which already supplies the breadcrumb, container, heading
 * and nav -- so what is needed here is the inner treatment WooCommerce's own templates never had:
 * a real grid instead of `.form-row-first`/`--last` floats, a legible password fieldset, and
 * address cards rather than Woo's `.col2-set`.
 */
.p321-account-title {
	margin-bottom: clamp(20px, 2.6vw, 28px);
}

.p321-account-subhead {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 106, "wght" 750;
	font-size: 20px;
	text-transform: uppercase;
	letter-spacing: -0.01em;
	margin: 0 0 20px;
}

.p321-account-form {
	max-width: 620px;
}

/* Woo emits each field as a <p>; give them one rhythm so their margins cannot double up. */
.p321-account-form .form-row,
.p321-address-fields .form-row {
	margin: 0 0 16px;
	display: grid;
	gap: 7px;
}

/* The one place two fields genuinely pair. Woo does this with floats; this is the same intent
   without the clearfix. */
.p321-account-form__grid {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 0 16px;
}

/*
 * Both class names land on the same element, and the older rule wins on source order.
 *
 * An earlier milestone reskinned these WooCommerce forms without template overrides --
 * `.woocommerce-address-fields__field-wrapper` is set to `display: flex; flex-direction: column`
 * further down this file. Same specificity as a bare `.p321-address-fields`, declared later, so the
 * grid below never applied and every field stacked full width including the name pair. Qualifying
 * with both class names states the intent plainly: when the wrapper carries our class, it is a
 * grid. Leaving the two rules to fight on source order is how this broke silently in the first
 * place.
 */
.woocommerce-address-fields__field-wrapper.p321-address-fields {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 0 16px;
}

/* Anything that is not a paired name field spans the row -- street, company, and Woo's own
   full-width rows carry `.form-row-wide`. */
.p321-address-fields .form-row-wide,
.p321-address-fields .form-row:only-child {
	grid-column: 1 / -1;
}

/* Matches the two-class selector above, or this would lose to it and stay 2-up on a phone. */
@media (max-width: 640px) {
	.p321-account-form__grid,
	.woocommerce-address-fields__field-wrapper.p321-address-fields {
		grid-template-columns: minmax(0, 1fr);
	}
}

.p321-field-hint {
	font-size: 13.5px;
	line-height: 1.5;
	color: var(--t2);
}

.p321-account-fieldset {
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	padding: clamp(18px, 2.4vw, 24px);
	margin: clamp(24px, 3vw, 32px) 0 0;
	background: var(--panel);
}

.p321-account-fieldset__legend {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.12em;
	text-transform: uppercase;
	color: var(--brand-dk);
	padding: 0 8px;
	margin-left: -8px;
}

.p321-account-fieldset__hint {
	margin: 0 0 18px;
}

.p321-account-form__actions {
	display: flex;
	flex-wrap: wrap;
	gap: 12px;
	align-items: center;
	margin: clamp(24px, 3vw, 32px) 0 0;
}

/* Saved addresses -- two cards, or one where shipping is not collected. */
.p321-address-grid {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: clamp(16px, 2.4vw, 24px);
	margin-top: clamp(20px, 2.6vw, 28px);
	align-items: start;
}

.p321-address-grid--single {
	grid-template-columns: minmax(0, 1fr);
	max-width: 460px;
}

@media (max-width: 700px) {
	.p321-address-grid {
		grid-template-columns: minmax(0, 1fr);
	}
}

.p321-address-card {
	background: var(--panel);
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	padding: clamp(20px, 2.6vw, 26px);
	display: flex;
	flex-direction: column;
	gap: 14px;
	height: 100%;
}

.p321-address-card__title {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--brand-dk);
	margin: 0;
}

.p321-address-card__body {
	font-style: normal;
	font-size: 15.5px;
	line-height: 1.6;
	color: var(--t1);
	margin: 0;
	flex: 1;
}

.p321-address-card__empty {
	color: var(--t2);
}

/* Pinned to the card's bottom so both cards' buttons sit on one line regardless of address
   length -- the same reason .p321-product-card__buy exists. */
.p321-address-card__edit {
	align-self: flex-start;
	margin-top: auto;
}

/*
 * WooCommerce's own my-account controls, which the theme had never styled.
 *
 * Measured on the logged-out page: the Log in and Register buttons at 36px tall, the show-password
 * eye at 22x22 and the "Remember me" checkbox at 13x13 -- the last two below WCAG 2.2 target-size
 * (minimum) AA at 24x24, not merely short of the 44px guidance.
 *
 * The checkbox is taken to 24 rather than 44 deliberately: a 44px checkbox is not a real pattern,
 * and its `<label>` already makes the practical target the full row of text beside it. The eye
 * keeps its drawn size and gains hit area from a transparent pseudo-element, the same technique the
 * wishlist disc uses.
 *
 * This page still has no design of its own and needs the owner to spec one (see section 11); these
 * are target-size corrections to what WooCommerce ships, not an attempt to design it.
 */
.woocommerce-form button.woocommerce-button,
.woocommerce-form button.woocommerce-Button,
.woocommerce button.woocommerce-button,
.woocommerce button.woocommerce-Button {
	min-height: 44px;
}

.woocommerce-form .show-password-input,
.woocommerce .show-password-input {
	position: relative;
}

.woocommerce-form .show-password-input::before,
.woocommerce .show-password-input::before {
	content: "";
	position: absolute;
	inset: -11px;
}

.woocommerce-form input[type="checkbox"],
.woocommerce input[type="checkbox"] {
	width: 24px;
	height: 24px;
	accent-color: var(--brand);
}

/*
 * Form controls never drop below 16px.
 *
 * iOS Safari zooms the page whenever a focused field computes under 16px, and every form on this
 * site sat at 15.5px -- contact, header search, FAQ search, my-account, and the PDP quantity at
 * 15px. Half a pixel, on every field, on the 80% of traffic that arrives on a phone. WooCommerce's
 * checkout was worse still: its inputs were never styled at all and inherited the UA default of
 * 13.33px.
 *
 * Set once, on the controls themselves, so nothing can drift under the threshold again. Component
 * rules may raise the size but must not lower it.
 */
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
select,
textarea {
	font-size: max(16px, 1rem);
}

/*
 * WooCommerce checkout fields, which the theme had never targeted.
 *
 * Measured before this rule: every checkout input 31px tall at 13.33px -- browser defaults -- while
 * the State <select> beside them was a properly styled 52px. Styled and unstyled controls sat on
 * the same row of the page where people pay.
 *
 * Matches .p321-form-row, which is the contact form's already-correct treatment, so checkout stops
 * being the one form built to a different standard.
 */
.woocommerce form .form-row input.input-text,
.woocommerce form .form-row textarea,
.woocommerce form .form-row select,
.woocommerce-checkout .form-row input.input-text,
.woocommerce-checkout .form-row textarea,
.woocommerce-checkout .form-row select {
	width: 100%;
	min-height: 52px;
	padding: 0 16px;
	border: 1.5px solid var(--hair);
	border-radius: 14px;
	background: var(--panel);
	color: var(--t1);
	font-family: "Instrument Sans", "Instrument Sans Fallback", system-ui, sans-serif;
	box-sizing: border-box;
}

.woocommerce-checkout .form-row textarea {
	min-height: 120px;
	padding: 14px 16px;
	line-height: 1.6;
}

/*
 * Native selects, since selectWoo is dequeued on the checkout
 * (p321_dequeue_select2_on_checkout in inc/checkout.php).
 *
 * `appearance: none` only changes the closed-state rendering -- iOS still opens its native wheel
 * picker, which is the point of dropping selectWoo in the first place. The chevron is drawn from
 * two borders on the field wrapper rather than an inline SVG so it stays on the tokens; a data URI
 * would need a literal hex, which `currentColor` cannot supply.
 *
 * `:has(select)` keeps the chevron off text inputs, including the case where country-select.js
 * swaps the state field to an <input> for a country with no states. Where `:has()` is unsupported
 * the rule is dropped and the field keeps the UA's own arrow -- no worse than before.
 *
 * Scoped to `.form-row`, not `.p321-form-row`: the checkout renders through
 * woocommerce_form_field(), so its fields carry WooCommerce's class and are styled by the block
 * above. `.p321-form-row` is the contact page's own markup and has no selects.
 */
.woocommerce-checkout .form-row select,
.p321-form-row select {
	-webkit-appearance: none;
	   -moz-appearance: none;
	        appearance: none;
}

/*
 * Split out, and more specific than it looks like it needs to be. This is not defensive padding --
 * without it the declaration loses.
 *
 * The `padding: 0 16px` block above reaches this element through
 * `.woocommerce form .form-row select`, which is **(0,2,2)** -- two classes and *two* type
 * selectors, because of the `form`. `.woocommerce-checkout .form-row select` is only (0,2,1), so it
 * loses despite coming later, and the 44px that reserves room for the chevron never applied on
 * checkout at all.
 *
 * `appearance: none` above is what hid this: it survives because WooCommerce sets it too, so the
 * field looked correctly styled while its padding silently came from a different rule.
 *
 * Measured at 390 before the fix, with "District of Columbia" selected: the select's text box ran to
 * x=336 while the chevron starts at x=323 -- **13px of overlap**, so long state names ran underneath
 * the arrow.
 */
.woocommerce-checkout .p321-form-grid .form-row select,
.woocommerce-checkout form .form-row select,
.p321-form-row select {
	padding-right: 44px;
}

.woocommerce-checkout .form-row .woocommerce-input-wrapper,
.p321-form-row .woocommerce-input-wrapper {
	display: block;
	position: relative;
}

.woocommerce-checkout .form-row .woocommerce-input-wrapper:has(select)::after,
.p321-form-row .woocommerce-input-wrapper:has(select)::after {
	content: "";
	position: absolute;
	top: 50%;
	right: 20px;
	width: 9px;
	height: 9px;
	margin-top: -7px;
	border-right: 2px solid var(--t2);
	border-bottom: 2px solid var(--t2);
	transform: rotate(45deg);
	pointer-events: none;
}

.woocommerce form .form-row input.input-text:focus,
.woocommerce form .form-row textarea:focus,
.woocommerce form .form-row select:focus,
.woocommerce-checkout .form-row input.input-text:focus,
.woocommerce-checkout .form-row textarea:focus,
.woocommerce-checkout .form-row select:focus {
	border-color: var(--brand);
	outline: 3px solid var(--brand);
	outline-offset: 1px;
}

/*
 * Payment method radios were 13x13 -- below the WCAG 2.2 target-size (minimum) AA floor of 24x24.
 * The row has an associated label so it was operable, but the control itself failed.
 *
 * Raised to 24 in an earlier pass, which cleared the floor and stopped there. CLAUDE.md is explicit
 * that 24x24 is the floor and 44x44 is the goal, and the owner reported these as still too small to
 * tap on a phone (2026-09-05). Measured at 390 before this change: the radio was 24x24, the label
 * 98x44 starting 17px to its right, and the remaining ~149px of a 288px row was dead. Three
 * separate hit zones with dead space between them, on a control where a mis-tap silently changes
 * how the customer pays.
 *
 * The radio stays 24 visually -- a 44px native radio looks like a bug. The *row* becomes the target
 * instead: the li is a flex row and the label flexes to fill it, so everything from the radio to the
 * right edge is one 44px-tall target.
 */
.woocommerce-checkout ul.wc_payment_methods > li {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
}

.woocommerce-checkout ul.wc_payment_methods input[type="radio"] {
	width: 24px;
	height: 24px;
	accent-color: var(--brand);
	flex: none;
}

/*
 * The space between the radio and its text belongs to the label's padding, not the radio's margin,
 * so it is inside the tap target instead of a dead strip between two of them.
 *
 * Needs an ID to land. WooCommerce sets `margin: 1em` through
 * `.woocommerce-checkout #payment ul.payment_methods li input` -- (1,2,2), and an ID beats any
 * number of classes, so the (0,3,1) rule above cannot reach it. This selector is (1,3,2).
 *
 * Caught by measuring after deploy: `padding-left` on the label applied, `margin-right: 0` did not,
 * so the gap went from 13px to 26px while the dead strip stayed exactly where it was.
 */
.woocommerce-checkout #payment ul.wc_payment_methods li input[type="radio"] {
	margin-right: 0;
}

.woocommerce-checkout ul.wc_payment_methods label {
	display: flex;
	align-items: center;
	flex: 1;
	min-height: 44px;
	padding-left: 13px;
	cursor: pointer;
}

/*
 * Anything that is not the radio or its label -- the gateway's own `.payment_box` description today,
 * and whatever a real gateway renders once C2 lands -- drops to its own line instead of being pulled
 * onto the flex row beside the label. Written as a negation rather than `.payment_box` so a hosted
 * -field or redirect gateway does not silently land in the wrong place.
 */
.woocommerce-checkout ul.wc_payment_methods > li > *:not(input):not(label) {
	flex-basis: 100%;
}

/*
 * Buttons do not inherit font-family -- the UA stylesheet sets Arial on them -- and .p321-btn never
 * set one. So every <button>-based control rendered in Arial while every <a>-based one used
 * Instrument Sans: "Select Options" (a link) and "Add To Cart" (a button) sat in the same product
 * card row in two different typefaces. Confirmed by measurement rather than eye -- "Add To Cart" at
 * 16px measures 87.5px in the body face and 84.2px in Arial, and the button measured 84.2px.
 *
 * Also caught: Filters, the quantity stepper, Send Message, and WooCommerce's own buttons.
 */
button,
input[type="submit"],
input[type="button"],
input[type="reset"] {
	font-family: inherit;
}

/* Touch targets */

.tap-target {
	min-height: 44px;
	min-width: 44px;
}

/* Header */

.site-header {
	position: sticky;
	top: 0;
	z-index: 60;
	background: color-mix(in srgb, var(--page) 95%, transparent);
	backdrop-filter: blur(16px);
	border-bottom: 1px solid var(--hair);
}

.site-header__inner {
	max-width: var(--content-max);
	margin: 0 auto;
	padding: 0 var(--gutter);
	display: flex;
	align-items: center;
	gap: clamp(10px, 3vw, 32px);
	height: clamp(68px, 11vw, 96px);
}

.site-logo {
	display: flex;
	align-items: center;
	margin-right: auto;
	flex: 1 1 auto;
	min-width: 0;
	min-height: 44px;
	padding: 6px 0;
}

/*
 * Sized to fill the header on a phone rather than sit in the middle of it.
 *
 * The old `clamp(30px, 7vw, 58px)` did not reach 44px until a 628px viewport, so every phone got
 * the 30px floor. Measured at 390 on 2026-09-03: the mark rendered 95x30 with 19px of dead space
 * above and below it, while the icon buttons beside it are 44x44 with 12px. The logo was the
 * smallest thing in a row it should lead.
 *
 * 11.3vw puts it at 44.1px at 390 -- the same height as the icon buttons, so the row reads as one
 * optical line instead of a small mark next to big circles. Nothing grows: `.site-header__inner`
 * is a fixed `clamp(68px, 11vw, 96px)`, which is 68px on every phone, so this fills space that was
 * already reserved.
 *
 * The ceiling is horizontal, not vertical. At 390 the button group starts at x=175 and the logo
 * starts at x=16, so beyond ~145px wide (about 45px tall, at the mark's 3.179 ratio) it would
 * crowd them. `max-width` plus `object-fit: contain` is the guard for narrower phones: the mark
 * scales down inside its box instead of shoving the icons off-screen, and never distorts.
 */
.site-logo img {
	height: clamp(38px, 11.3vw, 58px);
	width: auto;
	max-width: 100%;
	-o-object-fit: contain;
	   object-fit: contain;
}

.site-logo-text {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 108, "wght" 800;
	text-transform: uppercase;
	letter-spacing: -0.02em;
	/*
	 * 17px floor, not 18px: at 390px the wordmark needed 137px and had 135px, so "321 Peptides"
	 * ellipsed to "321 PEPTID…" on every mobile page. The ellipsis rules below stay as the guard
	 * against a longer site name shoving the header icons off-screen -- this just stops the real
	 * one tripping it. Moot once a logo image is supplied, which replaces this text entirely.
	 */
	font-size: clamp(17px, 3vw, 24px);
	color: var(--t1);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	min-width: 0;
}

.nav-primary {
	display: flex;
	gap: clamp(14px, 1.8vw, 24px);
	font-size: 15px;
	font-weight: 600;
	list-style: none;
	margin: 0;
	padding: 0;
}

.nav-primary a {
	color: var(--t1);
	padding: 14px 2px;
	display: inline-flex;
	align-items: center;
	white-space: nowrap;
	border-bottom: 2px solid transparent;
}

.nav-primary a:hover,
.nav-primary .current-menu-item a {
	border-color: var(--brand);
	color: var(--t1);
}

.header-icons {
	display: flex;
	gap: clamp(6px, 2vw, 10px);
	flex: none;
}

.icon-btn {
	width: clamp(44px, 11vw, 48px);
	height: clamp(44px, 11vw, 48px);
	flex: none;
	display: grid;
	place-items: center;
	border: 1px solid var(--hair);
	border-radius: 50%;
	background: var(--panel);
	cursor: pointer;
	color: var(--t1);
}

.icon-btn:hover {
	border-color: var(--brand);
	color: var(--brand-dk);
}

.cart-icon-wrap {
	position: relative;
}

.cart-badge {
	position: absolute;
	top: -6px;
	right: -6px;
	background: var(--brand);
	color: #062229;
	font-size: 12px;
	font-weight: 600;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	min-width: 20px;
	height: 20px;
	border-radius: 10px;
	display: grid;
	place-items: center;
	padding: 0 5px;
}

.mobile-menu-toggle {
	display: none;
}

.mobile-nav {
	display: none;
	border-top: 1px solid var(--hair);
	background: var(--page);
	padding: 10px 20px 18px;
	flex-direction: column;
	list-style: none;
	margin: 0;
}

.mobile-nav a {
	color: var(--t1);
	font-size: 17px;
	font-weight: 600;
	min-height: 54px;
	display: flex;
	align-items: center;
	border-bottom: 1px solid var(--hair);
}

@media (max-width: 895px) {
	.nav-primary {
		display: none;
	}

	.mobile-menu-toggle {
		display: grid;
	}

	.site-header[data-menu-open="true"] .mobile-nav {
		display: flex;
	}
}

/* Footer */

.site-footer {
	background: var(--ink);
	color: var(--od2);
}

/*
 * Five blocks -- the brand column plus four link columns -- so `auto-fit` had nowhere good to land.
 * It packed four columns from ~1030 to 1300px and three from ~800 to 1030, stranding Legal on its
 * own row with three empty cells beside it. On every page of the site, at the width a laptop
 * actually uses.
 *
 * Explicit counts instead, with the brand block spanning the full row once the five no longer fit.
 * That keeps the four link columns as one complete row at every step: 5, then brand + 4, then
 * brand + 2 x 2, then stacked.
 */
.footer-grid {
	max-width: var(--content-max);
	margin: 0 auto;
	padding: clamp(44px, 7vw, 64px) var(--gutter) 34px;
	display: grid;
	grid-template-columns: repeat(5, minmax(0, 1fr));
	gap: clamp(28px, 4vw, 40px);
}

@media (max-width: 1100px) {
	.footer-grid {
		grid-template-columns: repeat(4, minmax(0, 1fr));
	}

	.footer-grid > .footer-brand {
		grid-column: 1 / -1;
	}
}

@media (max-width: 700px) {
	.footer-grid {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

@media (max-width: 430px) {
	.footer-grid {
		grid-template-columns: 1fr;
	}
}

.footer-brand img {
	height: 52px;
	width: auto;
	margin-bottom: 16px;
}

.footer-brand p {
	font-size: 14.5px;
	line-height: 1.6;
	margin: 0 0 18px;
	max-width: 32ch;
}

.footer-social {
	display: flex;
	gap: 10px;
	margin-top: 18px;
}

/*
 * Matches both the <a> (URL set) and the <span> (URL still outstanding) forms -- the icon is
 * always visible, only its interactivity waits on the theme mod.
 */
.footer-social__item {
	width: 42px;
	height: 42px;
	display: grid;
	place-items: center;
	border: 1px solid var(--hair-dk);
	border-radius: 50%;
	color: var(--hair-dk);
}

a.footer-social__item:hover,
a.footer-social__item:focus-visible {
	border-color: var(--brand);
	color: var(--brand);
}

.footer-col__heading {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.17em;
	text-transform: uppercase;
	color: var(--panel);
	margin: 0 0 14px;
	font-weight: 500;
}

/*
 * Footer groups as disclosure widgets.
 *
 * Desktop keeps exactly what it had: the summary is styled back into a plain heading and made
 * inert, so the only trace of `<details>` is in the markup. The accordion is a phone-only measure
 * against a footer that measured 1,413px at 390 -- 1.7 screens -- because 20 links at the 44px tap
 * target stack into one column.
 */
.footer-col__summary {
	list-style: none;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
}

.footer-col__summary::-webkit-details-marker {
	display: none;
}

/* The chevron only means anything where the control is operable. */
.footer-col__chevron {
	display: none;
	flex: none;
	color: var(--od4);
	transition: transform 0.18s ease;
}

@media (prefers-reduced-motion: reduce) {
	.footer-col__chevron {
		transition: none;
	}
}

@media (max-width: 560px) {
	.footer-col {
		border-top: 1px solid var(--hair-dk);
	}

	.footer-col__summary {
		min-height: 52px;
		cursor: pointer;
	}

	/* The heading's own bottom margin would push the chevron off-centre in a flex row. */
	.footer-col__summary .footer-col__heading {
		margin: 0;
	}

	.footer-col__chevron {
		display: block;
	}

	.footer-col[open] .footer-col__chevron {
		transform: rotate(180deg);
	}

	.footer-col > ul {
		padding-bottom: 8px;
	}
}

/* Above the breakpoint the summary is a heading again, not a control: no pointer, no focus ring,
   nothing to click. The group is forced open by JS, and stays open without it. */
@media (min-width: 561px) {
	.footer-col__summary {
		cursor: default;
		pointer-events: none;
	}
}

.footer-col ul {
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: 4px;
	font-size: 15px;
	padding: 0;
	margin: 0;
	line-height: 1.2;
}

.footer-col a {
	color: var(--od2);
	display: flex;
	align-items: center;
	min-height: 44px;
}

.footer-col a:hover {
	color: var(--brand-lt);
}

.footer-bottom {
	border-top: 1px solid var(--hair-dk);
}

.footer-bottom__inner {
	max-width: var(--content-max);
	margin: 0 auto;
	padding: 22px var(--gutter);
	display: flex;
	flex-wrap: wrap;
	gap: 14px;
	justify-content: space-between;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.05em;
	color: var(--od4);
}

/*
 * Tighter on phones only. Of the bar's 119px at 390, 58 was padding and gap around 60 of text --
 * this recovers about 20 of it.
 *
 * Spacing only. The notice is the research-use-only compliance line, so under rule 1 the wording
 * does not change, and at 12px it is already at the readable floor so the size does not either.
 * Desktop keeps the original 22px, where the bar is a single line and the room reads as deliberate
 * rather than loose.
 */
@media (max-width: 560px) {
	.footer-bottom__inner {
		padding-block: 14px;
		gap: 8px;
	}
}

.rou-notice {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.05em;
	color: var(--od4);
}

/* Block: Section Header */

.p321-section-header {
	margin-bottom: clamp(28px, 4vw, 42px);
}

/* Block: Stat Row */

.p321-stat-row {
	display: flex;
	gap: clamp(20px, 4vw, 48px);
	flex-wrap: wrap;
}

.p321-stat__value {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 110, "wght" 800;
	font-size: clamp(26px, 3.2vw, 40px);
	line-height: 1;
}

.p321-stat__label {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--od3);
	margin-top: 6px;
}

.band-light .p321-stat__label {
	color: var(--t2);
}

/* ---------------------------------------------------------------------------------------------
   Homepage assurance strip (owner, 2026-08-23) -- six labelled claims under the hero, replacing the
   four-figure stat band.

   Built as a hairline grid rather than six bordered cards: one `--hair` background showing through
   1px gaps between `--panel` cells. That is the site's own idiom -- the spec tables and the FAQ
   accordion are constructed the same way -- and it reads as a certificate spec block rather than a
   row of marketing tiles, which is the right register for a lab supplier.

   Icon sits left of the text at every width, not above it. Stacked icons would make these read as
   feature cards; alongside, they read as a credentials list, which is what they are.
   --------------------------------------------------------------------------------------------- */

/*
 * Full-bleed band whose text still lands on the page's real content edge.
 *
 * That edge is NOT `(100% - content-max) / 2` -- that is where `.container`'s *box* starts, and the
 * container then adds `--gutter` of padding inside it. Padding to the box edge leaves the band 26px
 * left of every heading on the page at 1440. The `+ var(--gutter)` is the whole fix. (The stat band
 * this replaced had the same bug inverted: its own 26px cell padding made it line up at 1440 and
 * sit 26px off at 768, where the max() resolves to the gutter instead.)
 */
.p321-assurance {
	background: var(--panel);
	border-bottom: 1px solid var(--hair);
	padding-inline: max(var(--gutter), (100% - var(--content-max)) / 2 + var(--gutter));
}

/*
 * Explicit column counts per item count, never auto-fit (CLAUDE.md conventions). The count is on
 * the wrapper as `--4` / `--6` because the two want different grids to fill every row: six divides
 * by 1, 2 and 3; four divides by 1, 2 and 4. Running four through the six-item 3-column rule would
 * strand the fourth item alone on a second row -- the exact defect the convention exists to stop.
 */
.p321-assurance__grid {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	grid-template-columns: minmax(0, 1fr);
}

.p321-assurance__cell {
	display: flex;
	align-items: flex-start;
	gap: 14px;
	background: var(--panel);
	/* No inline padding on a phone, so the text lines up with the hero directly above it. */
	padding: 17px 0;
}

/*
 * The dividers are borders on the cells, NOT `gap: 1px` over a `--hair` grid background.
 *
 * The background technique drew a stray vertical hairline down the right edge of the band at wide
 * viewports. Cause, measured at 1920: three columns resolve to 436.656 / 436.672 / 436.656, which
 * with two 1px gaps sum to 1311.984 against a 1312px grid -- so 0.016px of the grid's background
 * was left uncovered at the right edge and the browser antialiased that sliver into a visible 1px
 * line. Any fractional column width can do this, so it was never going to stay fixed by nudging a
 * number; borders remove the exposed background entirely.
 *
 * Borders are safe for the layout because `box-sizing: border-box` is universal here (top of file).
 * Each breakpoint re-declares both borders and then clears them on the first row and first column,
 * so the rules never accumulate from a narrower breakpoint.
 */
.p321-assurance__cell + .p321-assurance__cell {
	border-top: 1px solid var(--hair);
}

.p321-assurance__icon {
	flex: none;
	display: grid;
	place-items: center;
	width: 42px;
	height: 42px;
	border-radius: 12px;
	background: var(--band);
	/* --brand-dk, not --brand: on the --band tint the mid-tone teal falls under the 3:1 that WCAG
	   1.4.11 asks of a meaningful graphic. */
	color: var(--brand-dk);
}

.p321-assurance__title {
	display: block;
	font-family: Archivo, sans-serif;
	font-weight: 700;
	font-size: 16.5px;
	line-height: 1.25;
	letter-spacing: -0.01em;
}

.p321-assurance__desc {
	display: block;
	margin-top: 3px;
	color: var(--t2);
	font-size: 14.5px;
	line-height: 1.5;
}

@media (min-width: 600px) {

	/*
	 * The negative inline margin is what keeps the first column's icon on the same left edge as the
	 * hero headline above it. Without it, the cell's own 22px padding indents the whole strip and
	 * the band visibly fails to line up with the rest of the page. Safe to overhang: the cells and
	 * the band are both --panel, so the extra 22px each side is white on white, and the grid's
	 * --hair background never shows because the cells cover the full grid area.
	 */
	.p321-assurance__grid {
		grid-template-columns: repeat(2, minmax(0, 1fr));
		margin-inline: -22px;
	}

	.p321-assurance__cell {
		padding: 22px;
		border-top: 1px solid var(--hair);
		border-left: 1px solid var(--hair);
	}

	/* Top row and left column carry no rule -- the band's own edges are not drawn. */
	.p321-assurance__cell:nth-child(-n+2) {
		border-top: 0;
	}

	.p321-assurance__cell:nth-child(2n+1) {
		border-left: 0;
	}
}

@media (min-width: 1000px) {

	.p321-assurance--6 .p321-assurance__grid {
		grid-template-columns: repeat(3, minmax(0, 1fr));
	}

	.p321-assurance--4 .p321-assurance__grid {
		grid-template-columns: repeat(4, minmax(0, 1fr));
	}

	/* Re-declared, not inherited from the 2-up block: at three columns the first row is cells 1-3
	   and the first column is every third cell, so the 2-up exceptions above are simply wrong here
	   and would leave a rule above cell 3 and none down the middle. */
	.p321-assurance--6 .p321-assurance__cell {
		border-top: 1px solid var(--hair);
		border-left: 1px solid var(--hair);
	}

	.p321-assurance--6 .p321-assurance__cell:nth-child(-n+3) {
		border-top: 0;
	}

	.p321-assurance--6 .p321-assurance__cell:nth-child(3n+1) {
		border-left: 0;
	}

	/* Four across is a single row, so every top rule goes. */
	.p321-assurance--4 .p321-assurance__cell {
		border-top: 0;
		border-left: 1px solid var(--hair);
	}

	.p321-assurance--4 .p321-assurance__cell:first-child {
		border-left: 0;
	}
}

/*
 * About page proof band. The section group around it is a constrained layout that already supplies
 * the content column, so the band must not add its own a second time -- that is what pushed the
 * stat row it replaced out to 85px while every heading on the page sat at 60. The hairline goes for
 * the same reason it went there: the band is white against the page's off-white, and that colour
 * change is the section break; a rule on top of it was the stray line the owner asked to remove.
 *
 * The grid's negative inline margin is deliberately left alone -- it still pulls the cells out by
 * exactly their own padding, so the text lands on the column edge here just as it does on the
 * homepage, and the overhang is --panel on --panel either way.
 */
.p321-about-proof .p321-assurance {
	padding-inline: 0;
	border-bottom: 0;
}

/* ---------------------------------------------------------------------------------------------
   PDP hero: two columns on a desk, one reordered column on a phone (owner, 2026-08-24)

   Measured at 393x852 before this: Add to cart sat 1,514px down -- 1.78 screens. The order was
   photo, what's-in-the-box, title, ticks, bundle math, price, buy. A shopper had to scroll past
   every specification to reach the decision.

   On a phone the two column wrappers become `display: contents`, which promotes their children to
   grid items so they can be ordered against each other across the original column boundary. Order
   is: photograph, what it is, what it costs, buy -- then everything that supports the decision
   rather than making it.
   --------------------------------------------------------------------------------------------- */

.p321-pdp-hero {
	padding-top: 4px;
	padding-bottom: var(--section-pad);
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 430px), 1fr));
	gap: clamp(26px, 4vw, 64px);
	align-items: start;
}

.p321-pdp-hero--single {
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr));
}

.p321-pdp-col {
	display: flex;
	flex-direction: column;
	min-width: 0;
}

.p321-pdp-col--media {
	gap: 12px;
}

.p321-pdp-col--info {
	gap: clamp(18px, 2.6vw, 24px);
}

/*
 * 895px, not 560: the grid resolves to one column at roughly 896px (two 430px tracks plus a 4vw
 * gap), so this is exactly the range where the two columns are already stacked. Above it the
 * side-by-side layout is untouched.
 */
@media (max-width: 895px) {

	.p321-pdp-col {
		display: contents;
	}

	.p321-pdp-hero {
		grid-template-columns: minmax(0, 1fr);
		gap: clamp(18px, 2.6vw, 24px);
	}

	/*
	 * Every promoted child gets a default rank FIRST. Without it an unnamed block would fall to
	 * `order: 0` and sort *above* the photograph -- the size chips and the variation form are not
	 * named individually on the simple-product template, and they would have jumped to the top.
	 * The named rules below all out-specify this one.
	 */
	.p321-pdp-col > * {
		order: 9;
	}

	.p321-pdp-hero > .p321-pdp-col > .p321-bundle-hero-media,
	.p321-pdp-hero > .p321-pdp-col > .p321-product-card__media {
		order: 1;
	}

	.p321-pdp-hero .p321-pdp-identity {
		order: 2;
	}

	/*
	 * Size before price, deliberately. The price is a function of the vial size here -- each chip
	 * carries its own figure and WooCommerce only resolves a single price once one is chosen. Put
	 * the price first and the shopper reads a range, then has to re-read it after choosing. This
	 * way the sequence is: what it is, which one, what that costs, buy.
	 */
	.p321-pdp-hero .p321-size-select {
		order: 3;
	}

	.p321-pdp-hero .p321-price-row,
	.p321-pdp-hero .woocommerce-variation-price {
		order: 4;
	}

	.p321-pdp-hero .p321-buy-form {
		order: 5;
	}

	.p321-pdp-hero .p321-bundle-line-items {
		order: 6;
	}

	.p321-pdp-hero .p321-bundle-math {
		order: 7;
	}

	.p321-pdp-hero .check-list {
		order: 8;
	}

	.p321-pdp-hero .p321-trust-list {
		order: 10;
	}

	.p321-pdp-hero .p321-callout {
		order: 11;
	}
}

/*
 * PDP wishlist heart: bottom-right of the photograph (owner, 2026-08-24), matching every grid card.
 *
 * The card version uses `top: calc(100cqw - 50px)` because a card's media is a square at full card
 * width. The PDP boxes are not, so this anchors to `bottom` directly. `top: auto` is required --
 * the base rule sets `top: 12px`, and with both set `top` wins.
 */
.p321-pdp-hero .p321-wishlist-toggle {
	top: auto;
	bottom: 12px;
	right: 12px;
}

/*
 * Buy row centred on a phone (owner, 2026-08-24). Above that the row sits in the right-hand column
 * beside the photograph, where left-aligned is correct; stacked under the photograph on a phone it
 * reads as the page's primary action and belongs on the page's centre line.
 */
@media (max-width: 895px) {

	/*
	 * The row wraps here -- the stepper plus a 200px-minimum button exceeds a 393px column -- so the
	 * stepper sat alone and left-aligned above a full-width button. Centring the flex container puts
	 * the stepper on the page's centre line; the button keeps `flex: 1` and stays full width, which
	 * is right for the primary action on a phone.
	 */
	.p321-buy-form form.cart,
	.p321-buy-form .woocommerce-variation-add-to-cart {
		justify-content: center;
	}
}

/* ---------------------------------------------------------------------------------------------
   Sticky action bar, phone only (owner, 2026-08-24; cart variant 2026-08-25)

   Shown once the real control has scrolled out of view, so it never covers the control it points
   at. Desktop never sees it -- there the buy row is beside the photograph and always near.

   Two consumers share this block: template-parts/sticky-buy.php on the two product templates, and
   template-parts/sticky-checkout.php on the cart. The `-buy` in the class name predates the second
   one; the frame, safe-area handling and >=896px suppression are common to both, so they were not
   worth forking. Only the cart modifier below and the two init functions differ.
   --------------------------------------------------------------------------------------------- */

.p321-sticky-buy {
	position: fixed;
	z-index: 55;
	inset: auto 0 0 0;
	background: var(--panel);
	border-top: 1px solid var(--hair);
	box-shadow: 0 -10px 30px rgba(8, 23, 28, 0.13);
	transform: translateY(101%);
	transition: transform 0.2s ease;
	/* Clear of the iOS home indicator. */
	padding-bottom: env(safe-area-inset-bottom, 0);
}

.p321-sticky-buy.is-visible {
	transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {

	.p321-sticky-buy {
		transition: none;
	}
}

.p321-sticky-buy__inner {
	display: flex;
	align-items: center;
	gap: 14px;
	padding: 10px var(--gutter);
	max-width: var(--content-max);
	margin: 0 auto;
}

.p321-sticky-buy__price {
	flex: 1;
	min-width: 0;
}

.p321-sticky-buy__name {
	display: block;
	font-size: 12.5px;
	color: var(--t2);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

.p321-sticky-buy__figure {
	display: block;
	font-family: Archivo, sans-serif;
	font-weight: 800;
	font-size: 18px;
	line-height: 1.2;
}

/* WooCommerce prints a <del>/<ins> pair inside get_price_html(); only the live figure belongs on a
   bar this small. */
.p321-sticky-buy__figure del {
	display: none;
}

.p321-sticky-buy__figure ins {
	text-decoration: none;
}

.p321-sticky-buy__cta {
	flex: none;
	min-height: 46px;
	padding: 0 20px;
	font-size: 14.5px;
	text-decoration: none;
}

/* The cart bar carries a total rather than a single price, so the figure gets the room and the
   count sits above it as the caption. */
.p321-sticky-buy--cart .p321-sticky-buy__figure {
	font-size: 19px;
}

.p321-sticky-buy--cart .p321-sticky-buy__cta {
	font-size: 14px;
	padding: 0 18px;
}

/* Above the phone range the bar is never shown, whatever the script does. */
@media (min-width: 896px) {

	.p321-sticky-buy {
		display: none;
	}
}

/*
 * Clearance so the bar never permanently covers the end of the page. The cart bar is present for
 * the whole scroll (it hides only while the real button is in view), so the last footer row would
 * otherwise sit under it with no way to reveal it. Scoped with :has() so the empty-cart page --
 * same body class, no bar -- does not gain the padding; where :has() is unsupported the rule is
 * dropped and the page is simply as it was.
 */
@media (max-width: 895px) {

	body:has(.p321-sticky-buy--cart) footer {
		padding-bottom: calc(76px + env(safe-area-inset-bottom, 0px));
	}
}

/* ---------------------------------------------------------------------------------------------
   Lot history disclosure (owner, 2026-08-24)

   The table stays in the markup; only its default state changes. A three-vial bundle listed
   fifteen rows and roughly 1,400px of a phone screen between the FAQ and the related bundles.
   --------------------------------------------------------------------------------------------- */

.p321-lot-history__summary {
	cursor: pointer;
	list-style: none;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	min-height: 56px;
	padding: 6px 0;
}

.p321-lot-history__summary::-webkit-details-marker {
	display: none;
}

.p321-lot-history__title {
	display: block;
	font-family: Archivo, sans-serif;
	font-weight: 800;
	font-variation-settings: "wdth" 110, "wght" 800;
	font-size: clamp(24px, 3.4vw, 38px);
	line-height: 1.05;
	letter-spacing: -0.02em;
	text-transform: uppercase;
}

.p321-lot-history__hint {
	display: block;
	margin-top: 6px;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--t2);
}

.p321-lot-history__chevron {
	flex: none;
	color: var(--t2);
	transition: transform 0.18s ease;
}

.p321-lot-history[open] .p321-lot-history__chevron {
	transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {

	.p321-lot-history__chevron {
		transition: none;
	}
}

/* The heading is inside the summary now, so the paragraph needs its own top gap when opened. */
.p321-lot-history[open] > .body-copy {
	margin-top: 14px;
}

/* Block: Callout */

/*
 * Homepage stat band. The design draws the light variant quite differently from the dark one:
 * a full-bleed panel band with a hairline under it, four equal columns divided by vertical
 * rules, teal 36px figures and sentence-case 14.5px labels -- not the mono uppercase labels the
 * dark variant uses. Scoped to `.band-light` so About's dark stat row, whose own design file
 * does specify mono uppercase, is untouched.
 *
 * The padding-inline max() keeps the band full width while its columns line up with every other
 * container on the page, without needing a second wrapper element the block does not render.
 */
.p321-stat-row.band-light {
	background: var(--panel);
	border-bottom: 1px solid var(--hair);
	gap: 0;
	padding-inline: max(var(--gutter), (100% - var(--content-max)) / 2);
}

.p321-stat-row.band-light .p321-stat {
	flex: 1 1 220px;
	padding: 34px 26px;
	border-right: 1px solid var(--hair);
}

.p321-stat-row.band-light .p321-stat:last-child {
	border-right: 0;
}

.p321-stat-row.band-light .p321-stat__value {
	font-size: 36px;
	color: var(--brand-dk);
}

.p321-stat-row.band-light .p321-stat__label {
	font-family: "Instrument Sans", system-ui, sans-serif;
	font-size: 14.5px;
	letter-spacing: normal;
	text-transform: none;
	line-height: 1.5;
	margin-top: 8px;
	color: var(--t2);
}

.p321-callout {
	border-radius: var(--rc);
	padding: var(--card-pad);
}

.p321-callout--bordered {
	border: 1px solid var(--hair);
	background: var(--panel);
}

.p321-callout--tinted {
	background: var(--band);
}

.p321-callout--brand-outline {
	border: 1px solid var(--brand);
	background: var(--panel);
}

.p321-callout--quote {
	border: none;
	border-left: 4px solid var(--brand);
	padding: 0 0 0 clamp(20px, 3vw, 32px);
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 104, "wght" 650;
	font-size: clamp(19px, 2.4vw, 28px);
	line-height: 1.35;
	max-width: 44ch;
}

/* Block: Spec Table */

.p321-spec-table-wrap {
	/* Positioned for the same reason .p321-coa-infographic__results-wrap is: an absolutely
	   positioned descendant (a visually-hidden label) would otherwise be laid out against the page
	   and escape this element's overflow clip, widening the document. */
	position: relative;
	overflow-x: auto;
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	background: var(--panel);
	-webkit-overflow-scrolling: touch;
}

.p321-spec-table {
	width: 100%;
	border-collapse: collapse;
	font-size: 15px;
	min-width: 560px;
}

.p321-spec-table th,
.p321-spec-table td {
	text-align: left;
	padding: 16px 20px;
	vertical-align: top;
	line-height: 1.55;
}

.p321-spec-table--comparison thead tr {
	background: var(--ink);
	color: #fff;
}

.p321-spec-table--comparison thead th {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-weight: 500;
	letter-spacing: 0.08em;
	font-size: 12px;
	text-transform: uppercase;
}

.p321-spec-table tbody tr {
	border-top: 1px solid var(--hair);
}

.p321-spec-table tbody th {
	font-weight: 600;
}

.p321-spec-table tbody td {
	color: var(--t2);
}

/* Block: Two Column Text + Image */

.p321-two-col {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 400px), 1fr));
	gap: clamp(32px, 5vw, 64px);
	align-items: center;
}

.p321-two-col--reverse {
	direction: rtl;
}

.p321-two-col--reverse > * {
	direction: ltr;
}

.p321-two-col__aside img {
	border-radius: var(--rc);
	width: 100%;
}

/* Block: FAQ Accordion */

.p321-faq-accordion {
	display: flex;
	flex-direction: column;
	gap: 1px;
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	overflow: hidden;
	background: var(--hair);
}

.p321-faq-item {
	background: var(--panel);
	/* Was 18px. The summary below now carries 44px of its own height, so the row keeps very close to
	   the height it had -- 8 + 44 + 8 against the old 18 + 20 + 18. */
	padding: 8px var(--card-pad);
}

/*
 * The clickable element was 20px tall -- below WCAG 2.2 target-size (minimum) AA at 24x24, not just
 * short of the 44px guidance. The visible row looked bigger only because the item's padding sat
 * *outside* the summary, where a pointer press does nothing.
 *
 * `align-items: center` rather than the previous `baseline`: at 20px tall baseline and centre were
 * indistinguishable, but in a 44px box baseline pins the text to the top of its own row.
 */
.p321-faq-item__question {
	cursor: pointer;
	font-weight: 600;
	font-size: 16px;
	list-style: none;
	display: flex;
	align-items: center;
	min-height: 44px;
	gap: 14px;
}

.p321-faq-item__question span:first-child {
	flex: 1;
}

.p321-faq-item__group {
	flex: none;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 11px;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--t2);
	font-weight: 500;
}

/* Also below the 24x24 AA floor at 20px tall. */
.p321-faq-item__link {
	display: inline-flex;
	align-items: center;
	min-height: 44px;
	margin-top: 2px;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12.5px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
}

.p321-faq-item__answer {
	margin-top: 12px;
	color: var(--t2);
	font-size: 15.5px;
	line-height: 1.6;
}

/* ---------------------------------------------------------------------------------------------
   FAQ page: two views + compound index (Phase 5 Milestone G)

   The flat list was 76 items and 7.3 screens at 390px, but 54 of the 76 were three questions
   repeated across 18 products. Folding those into an 18-row index is the whole of the fix: the
   questions are unchanged and still in the markup, they are just no longer eighteen copies of the
   same shape stacked end to end.

   Owner, 2026-08-22: compounds is the default view, both lists alphabetical, "All" pinned first.
   --------------------------------------------------------------------------------------------- */

/* One measure for the whole page: the search box, the switcher and both lists share a left and
   right edge instead of the controls running to 1320px over an 82ch list. */
.p321-search-field[data-p321-faq-search-wrap],
.p321-faq-views,
.p321-faq-panel,
[data-p321-faq-empty] {
	max-width: 82ch;
}

.p321-faq-accordion,
.p321-cx {
	max-width: 100%;
}

/* --- view switcher ---------------------------------------------------------------------------
   Two links in a track, not a tab widget: see the note in page-faq.php. Explicit `1fr 1fr` and not
   auto-fit -- a fixed set of two (conventions, CLAUDE.md). */

.p321-faq-views {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: 6px;
	margin-bottom: 18px;
	padding: 5px;
	background: var(--band);
	border-radius: var(--rb);
}

.p321-faq-view {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 8px;
	min-height: 46px;
	padding: 0 12px;
	border-radius: var(--rb);
	font-weight: 650;
	font-size: 15px;
	color: var(--t2);
	text-decoration: none;
	transition: background 0.15s ease, color 0.15s ease;
}

.p321-faq-view:hover {
	color: var(--t1);
}

.p321-faq-view[aria-current] {
	background: var(--panel);
	color: var(--t1);
	box-shadow: 0 1px 3px rgba(8, 23, 28, 0.12);
}

.p321-faq-view__count {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 11.5px;
	font-weight: 500;
	color: var(--t2);
}

.p321-faq-view[aria-current] .p321-faq-view__count {
	color: var(--brand-dk);
}

/* --- compound index --------------------------------------------------------------------------
   Same hairline-gap construction as .p321-faq-accordion so the two views read as one component in
   two states rather than two different lists. */

.p321-cx {
	display: flex;
	flex-direction: column;
	gap: 1px;
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	overflow: hidden;
	background: var(--hair);
}

.p321-cx__item {
	background: var(--panel);
}

.p321-cx__summary {
	cursor: pointer;
	list-style: none;
	display: flex;
	align-items: center;
	gap: 14px;
	min-height: 72px;
	padding: 13px var(--card-pad);
}

.p321-cx__summary::-webkit-details-marker {
	display: none;
}

.p321-cx__id {
	flex: 1;
	min-width: 0;
}

.p321-cx__name {
	display: block;
	font-family: Archivo, sans-serif;
	font-weight: 750;
	font-size: 17px;
	letter-spacing: -0.01em;
	line-height: 1.25;
}

/* The strength/descriptor half of the product name, set quieter so the compound leads. Split, never
   rewritten -- the two halves recombine to the exact product title (p321_split_product_name()). */
.p321-cx__name em {
	font-style: normal;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 11.5px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--t2);
	margin-left: 8px;
	white-space: nowrap;
}

/* First sentence of the compound's own "what is" answer. Clamped rather than cut in PHP, so a long
   one ends in an ellipsis instead of a mangled sentence, and the full text is one tap below. */
.p321-cx__gist {
	display: -webkit-box;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 2;
	line-clamp: 2;
	overflow: hidden;
	color: var(--t2);
	font-size: 14.5px;
	line-height: 1.45;
	margin-top: 3px;
}

.p321-cx__chevron {
	flex: none;
	color: var(--t2);
	transition: transform 0.18s ease;
}

.p321-cx__item[open] .p321-cx__chevron,
.p321-faq-item[open] .p321-faq-item__chevron {
	transform: rotate(180deg);
}

.p321-faq-item__chevron {
	flex: none;
	color: var(--t2);
	transition: transform 0.18s ease;
}

@media (prefers-reduced-motion: reduce) {

	.p321-faq-view,
	.p321-cx__chevron,
	.p321-faq-item__chevron {
		transition: none;
	}
}

.p321-cx__body {
	padding: 0 var(--card-pad) 20px;
}

/* Mobile first: label above its question. The two-column form needs ~112px of gutter before it
   stops squeezing the question text, which is more than a 390px screen has to give. */
.p321-qa {
	display: grid;
	gap: 5px;
	padding: 15px 0;
	border-top: 1px solid var(--hair);
}

.p321-qa__label {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 10.5px;
	letter-spacing: 0.13em;
	text-transform: uppercase;
	color: var(--brand-dk);
	margin: 0;
	padding-top: 2px;
}

/* p321_faq_question_label() returns '' when a question does not classify -- an empty label would
   otherwise leave a blank row in the grid. */
.p321-qa__label:empty {
	display: none;
}

.p321-qa__q {
	font-weight: 650;
	font-size: 15.5px;
	margin: 0;
}

.p321-qa__a {
	color: var(--t2);
	font-size: 15px;
	line-height: 1.6;
	margin: 6px 0 0;
	max-width: 68ch;
}

.p321-cx__links {
	margin: 18px 0 0;
}

.p321-cx__cta {
	width: 100%;
}

@media (min-width: 641px) {

	.p321-qa {
		grid-template-columns: 112px minmax(0, 1fr);
		gap: 6px 18px;
	}

	.p321-qa__label:empty {
		display: block;
	}

	.p321-cx__cta {
		width: auto;
	}
}

/* --- search across both views ----------------------------------------------------------------
   A query shows both panels at once (main.js), so each needs a heading to say which list is which.
   Hidden in browse mode, where the switcher already says it. */

.p321-faq-panel__label {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 11px;
	letter-spacing: 0.12em;
	text-transform: uppercase;
	color: var(--t2);
	margin: 0 0 10px;
}

.p321-faq-panel__label[hidden] {
	display: none;
}

/* Both `:not([hidden])`: in browse mode one panel is hidden but still a sibling, and the bare
   adjacent selector would push the visible panel down by a gap with nothing above it. */
.p321-faq-panel:not([hidden]) + .p321-faq-panel:not([hidden]) {
	margin-top: 30px;
}

/* Core block style variation: Card (for checklists and similar boxed content) */

.wp-block-group.is-style-p321-card {
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	background: var(--panel);
	padding: var(--card-pad);
}

.band-dark .wp-block-group.is-style-p321-card {
	background: var(--ink-2);
	border-color: var(--hair-dk);
}

/* core/button: filled brand pill by default, outline variant on dark sections */

.wp-block-button__link {
	border-radius: var(--rb);
	min-height: 56px;
	padding: 0 30px;
	display: inline-flex;
	align-items: center;
	gap: 10px;
	font-size: 16px;
	font-weight: 700;
	background: var(--brand);
	color: var(--ink);
}

.wp-block-button__link:hover {
	background: var(--brand-lt);
	color: var(--ink);
}

/* .band-dark a (class+tag) otherwise beats .wp-block-button__link (class only)
   on specificity and would recolor the filled button's own text to brand-lt,
   which fails contrast against the brand teal background. */
.band-dark .wp-block-button__link {
	color: var(--ink);
}

.band-dark .wp-block-button__link:hover {
	color: var(--ink);
}

.wp-block-button.is-style-outline .wp-block-button__link {
	background: transparent;
	border: 1.5px solid var(--hair-dk);
	color: inherit;
	font-weight: 600;
}

.wp-block-button.is-style-outline .wp-block-button__link:hover {
	border-color: var(--brand);
	color: var(--brand-lt);
}

/* Plain-markup buttons (Shop archive / PDP) -- same visual language as .wp-block-button__link
   but usable on hand-templated <button>/<a> without depending on Gutenberg block markup. */

.p321-btn {
	border-radius: var(--rb);
	min-height: 48px;
	padding: 0 24px;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 9px;
	font-size: 15px;
	font-weight: 700;
	cursor: pointer;
	border: 0;
	background: var(--brand);
	color: #04252c;
	box-shadow: 0 6px 18px rgba(2, 174, 203, 0.26), inset 0 1px 0 rgba(255, 255, 255, 0.35);
}

.p321-btn:hover {
	background: #1cc3dc;
	color: #04252c;
	transform: translateY(-1px);
}

.p321-btn--lg {
	min-height: 56px;
	padding: 0 30px;
	font-size: 16.5px;
	flex: 1;
	min-width: min(100%, 220px);
}

.p321-btn--outline {
	background: transparent;
	color: var(--ink);
	border: 1.5px solid var(--ink);
	box-shadow: none;
	font-weight: 600;
}

.p321-btn--outline:hover {
	background: var(--ink);
	color: #fff;
}

.p321-btn:disabled {
	opacity: 0.4;
	cursor: not-allowed;
	transform: none;
}

/* Category / sort chip row, shared by the Shop archive and (Milestone C) Bundles index */

/*
 * Chip rows scroll sideways on one line, and SAY SO.
 *
 * Owner's call 2026-09-03: keep the row compact. Wrapping showed all nine homepage categories but
 * cost 284px at 390 -- about a quarter of a phone screen -- pushing the product rail down.
 *
 * The defect being fixed is not the scrolling, it is that the scrolling was invisible. Measured
 * 2026-09-03: the homepage Category row was a 366px box holding 1231px of chips, so 7 of its 9
 * categories sat off-screen at 390 with no signal whatsoever. `scrollbar-width: thin` reads like an
 * affordance in a stylesheet and is nothing on a phone -- touch platforms draw no persistent
 * scrollbar -- and ~80% of this site's traffic is a phone. The COA archive hid 2 of 4 the same way.
 *
 * So: a fade on whichever edge still has content behind it, driven by `data-p321-scroll` from
 * main.js. Not CSS-only, deliberately. The CSS-only routes both fail here: `animation-timeline:
 * scroll()` is Chrome-and-Firefox, and iOS Safari is the browser that matters (trap #2); and the
 * `background-attachment: local` scroll-shadow trick paints behind the chips, which are opaque
 * pills, so it would be invisible exactly where it is needed.
 *
 * Without JS the row scrolls with no fade -- i.e. today's behaviour, degraded but not broken.
 */
.p321-chip-row {
	display: flex;
	gap: 8px;
	overflow-x: auto;
	padding: 8px 4px 14px;
	margin: -8px -4px 0;
	-webkit-overflow-scrolling: touch;
	scrollbar-width: thin;
	scroll-snap-type: x proximity;
}

.p321-chip-row .p321-chip {
	scroll-snap-align: start;
}

/* `proximity`, not `mandatory`: a mandatory snap fights a short flick and can strand a half-chip. */
.p321-chip-row[data-p321-scroll="right"] {
	-webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 56px), transparent 100%);
	mask-image: linear-gradient(90deg, #000 calc(100% - 56px), transparent 100%);
}

.p321-chip-row[data-p321-scroll="left"] {
	-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 56px);
	mask-image: linear-gradient(90deg, transparent 0, #000 56px);
}

.p321-chip-row[data-p321-scroll="both"] {
	-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 56px, #000 calc(100% - 56px), transparent 100%);
	mask-image: linear-gradient(90deg, transparent 0, #000 56px, #000 calc(100% - 56px), transparent 100%);
}

/*
 * A mask clips whatever it covers, including a focus ring. Tabbing through the chips scrolls them
 * into view, and a chip landing at the edge would otherwise show a faded focus ring -- WCAG 2.4.11
 * focus-not-obscured. Drop the mask entirely while anything inside has focus.
 */
.p321-chip-row:focus-within {
	-webkit-mask-image: none;
	mask-image: none;
}

.p321-chip {
	flex: none;
	display: inline-flex;
	align-items: center;
	gap: 9px;
	min-height: 46px;
	padding: 0 20px;
	border-radius: var(--rb);
	cursor: pointer;
	font-size: 14.5px;
	font-weight: 600;
	white-space: nowrap;
	border: 1px solid var(--hair);
	background: var(--panel);
	color: var(--t1);
}

.p321-chip:hover {
	border-color: var(--brand);
}

/*
 * `aria-current`, not `aria-selected`. Every chip row is a set of links to filtered addresses, so
 * the state being drawn is "this is the view you are on" -- which is what aria-current means and
 * what the mobile sheet and the FAQ view switcher already used. `aria-selected` is only valid on a
 * handful of roles, which is why these links were carrying `role="tab"` to make it legal; see the
 * note in page-faq.php. The attribute is emitted only on the active chip, so the bare
 * `[aria-current]` is enough and stays at the same (0,2,0) the old selector had.
 */
.p321-chip[aria-current] {
	border-color: var(--ink);
	background: var(--ink);
	color: #fff;
}

.p321-chip__count {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	opacity: 0.65;
}

/* Search + sort bar */

.p321-search-sort {
	display: flex;
	gap: 14px;
	align-items: center;
	justify-content: space-between;
	flex-wrap: wrap;
	margin-bottom: 22px;
}

.p321-search-field {
	flex: 1;
	min-width: min(100%, 260px);
	display: flex;
	align-items: center;
	gap: 10px;
	border: 1.5px solid var(--hair);
	background: var(--panel);
	border-radius: var(--rb);
	padding: 0 18px;
	min-height: 52px;
}

.p321-search-field input {
	flex: 1;
	min-width: 0;
	border: 0;
	outline: none;
	background: transparent;
	font-family: "Instrument Sans", "Instrument Sans Fallback", sans-serif;
	font-size: 15.5px;
	color: var(--t1);
	height: 48px;
}

.p321-sort-select {
	min-height: 52px;
	border: 1.5px solid var(--hair);
	background: var(--panel);
	border-radius: var(--rb);
	padding: 0 16px;
	font-family: "Instrument Sans", "Instrument Sans Fallback", sans-serif;
	/* 16px, not 15: a select under 16px makes iOS zoom the page on focus. */
	font-size: 16px;
	font-weight: 600;
	color: var(--t1);
	cursor: pointer;
}

.p321-result-line {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 13px;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--t2);
	/* margin-top is a no-op for the <div> consumers (Shop, Bundles, COA, Reviews) and stops the FAQ
	   page's <p> from picking up a default paragraph margin above the count. */
	margin-top: 0;
	margin-bottom: 20px;
}

/* Mobile sticky filter bar + bottom sheet -- shared by Shop archive and (Milestone C) Bundles */

.p321-filter-bar {
	position: sticky;
	top: 68px;
	z-index: 44;
	background: color-mix(in srgb, var(--page) 96%, transparent);
	backdrop-filter: blur(14px);
	border-top: 1px solid var(--hair);
	border-bottom: 1px solid var(--hair);
	margin-top: 18px;
	padding: 9px 16px;
	display: flex;
	gap: 11px;
	align-items: center;
}

.p321-filter-bar__trigger {
	flex: none;
	min-height: 44px;
	padding: 0 16px;
	display: inline-flex;
	align-items: center;
	gap: 9px;
	border: 1.5px solid var(--ink);
	border-radius: var(--rb);
	background: var(--ink);
	color: #fff;
	font-size: 14.5px;
	font-weight: 600;
	cursor: pointer;
}

.p321-filter-bar__label {
	flex: 1;
	min-width: 0;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 11.5px;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--t2);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.p321-sheet-overlay {
	position: fixed;
	inset: 0;
	z-index: 90;
	background: rgba(8, 23, 28, 0.58);
	display: none;
	align-items: flex-end;
}

.p321-sheet-overlay[data-open="true"] {
	display: flex;
}

.p321-sheet {
	width: 100%;
	max-height: 86vh;
	overflow-y: auto;
	background: var(--page);
	border-radius: 22px 22px 0 0;
	padding: 14px 16px 22px;
	box-shadow: 0 -22px 54px rgba(0, 0, 0, 0.34);
	-webkit-overflow-scrolling: touch;
}

.p321-sheet__grabber {
	width: 44px;
	height: 4px;
	border-radius: 2px;
	background: var(--hair);
	margin: 0 auto 14px;
}

.p321-sheet__header {
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: 14px;
	margin-bottom: 18px;
}

.p321-sheet__group-label {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 11.5px;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--t2);
	margin-bottom: 10px;
}

.p321-sheet__list {
	display: flex;
	flex-direction: column;
	gap: 1px;
	background: var(--hair);
	border: 1px solid var(--hair);
	border-radius: 14px;
	overflow: hidden;
	margin-bottom: 22px;
}

.p321-sheet__row {
	text-align: left;
	min-height: 52px;
	padding: 0 16px;
	display: flex;
	align-items: center;
	gap: 12px;
	border: 0;
	background: var(--panel);
	cursor: pointer;
	font-size: 15.5px;
	font-weight: 600;
	color: var(--t1);
	width: 100%;
}

.p321-sheet__row[aria-current="page"] {
	background: rgba(2, 174, 203, 0.1);
}

.p321-sheet__row-label {
	flex: 1;
	min-width: 0;
}

.p321-sheet__row-count {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12.5px;
	color: var(--t2);
}

.p321-sheet__row-tick {
	flex: none;
	color: transparent;
}

.p321-sheet__row[aria-current="page"] .p321-sheet__row-tick {
	color: var(--brand-dk);
}

.p321-sheet__actions {
	display: flex;
	gap: 10px;
	margin-top: 22px;
}

/* Product card (Shop archive grid, PDP related rail) */

/*
 * One card per row on a phone, which is what the design specifies.
 *
 * The floor was `min(47%, 255px)`. The 47% is what forced two columns at any width: at 390 it
 * resolved to ~168px, so two cards shared 358px and each got 174px. At that width the buy button
 * wrapped to two lines ("Select / Options", "Add To / Cart"), titles ran to three, and the purity
 * string to three more. The design source uses `min(100%, 255px)`, which is 1-up there.
 *
 * `min(100%, 255px)` restores the intended progression -- 1 / 2 / 3 / 4 as the container grows --
 * and matters more here than on most sites: about 80% of this store's traffic is mobile, so the
 * phone layout is the primary one, not the fallback.
 */
.p321-product-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(min(100%, 255px), 1fr));
	gap: clamp(10px, 2vw, 20px);
}

/*
 * The PDP's "More from the catalog" rail. `wc_get_related_products( $id, 4 )` caps it at four, so
 * it is a fixed set like the homepage rails and wants the same treatment -- it was going 3 + 1 from
 * ~780 to 1300px on every product page.
 *
 * A class rather than the homepage's `[data-p321-rail]` hook, because that attribute drives the
 * expand/collapse JS and this rail has no toggle.
 */
.p321-rail-grid {
	grid-template-columns: repeat(4, minmax(0, 1fr));
}

@media (max-width: 1000px) {
	.p321-rail-grid {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

/* 1-up on a phone, like every other product grid. Four cards divide evenly at 4, 2 and 1, so no
   step here strands a card. */
@media (max-width: 560px) {
	.p321-rail-grid {
		grid-template-columns: minmax(0, 1fr);
	}
}

/*
 * Homepage rails hold exactly four cards, so only 4 / 2 / 1 divide evenly -- and `auto-fill`
 * resolved to three tracks from roughly 780 to 1300px, stranding the fourth card alone with two
 * thirds of the row empty. Measured at both 900 and 1100.
 *
 * Scoped to `[data-p321-rail]`, which exists only on the homepage: the Shop archive, bundles
 * taxonomy, search and wishlist all share `.p321-product-grid` but hold a full page of products
 * where auto-fill's packing is right and rows fill themselves.
 *
 * Four cards divide evenly at 4, 2 and 1, so no step here can strand one.
 */
[data-p321-rail] .p321-product-grid {
	grid-template-columns: repeat(4, minmax(0, 1fr));
}

@media (max-width: 1000px) {
	[data-p321-rail] .p321-product-grid {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

/* 1-up on a phone. Without this the rails would keep their own two-column step and stay 2-up at
   390 regardless of the base grid's floor. */
@media (max-width: 560px) {
	[data-p321-rail] .p321-product-grid {
		grid-template-columns: minmax(0, 1fr);
	}
}

.p321-product-card {
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	overflow: hidden;
	background: var(--panel);
	display: flex;
	flex-direction: column;
	/* A grid item's default `min-width: auto` lets it be pushed wider than its column by its own
	   content; the card would then run past the container and the badge past the screen. */
	min-width: 0;
	max-width: 100%;
	transition: 0.22s;
	color: inherit;
	text-decoration: none;
}

.p321-product-card:hover {
	border-color: var(--brand);
	transform: translateY(-4px);
	box-shadow: 0 20px 44px rgba(8, 23, 28, 0.11);
	color: inherit;
}

.p321-product-card__media {
	background: linear-gradient(170deg, #fff, #e2ebee);
	padding: clamp(12px, 3vw, 22px);
	/* Same pinning as `.p321-bundle-card__media` -- this is the badges' containing block. */
	max-width: 100%;
	min-width: 0;
	display: grid;
	place-items: center;
	aspect-ratio: 1 / 1;
	flex: none;
	position: relative;
	box-sizing: border-box;
}

.p321-product-card__media img {
	position: absolute;
	inset: 10%;
	width: 80%;
	height: 80%;
	-o-object-fit: contain;
	   object-fit: contain;
	filter: drop-shadow(0 11px 15px rgba(8, 23, 28, 0.2));
}

/*
 * Owner, 2026-08-24: the "Save $X" pill was being cut off at the right edge of the card on an
 * iPhone -- reported on the homepage rail, the bundles index and the related rail on a PDP.
 *
 * It could not be reproduced in headless Chrome at 320/360/375/390/393/414/430: the pill measured
 * ~10px clear of the card at every one of them. What differs on the device is the containing
 * block. These pills are positioned against `.p321-*__media`, and that box is sized by
 * `aspect-ratio` with percentage-sized children -- the same construction that made the PDP hero
 * image mis-size on Safari and not on Chrome. If the media resolves even slightly wider than the
 * card, `right: 9px` lands outside it and the card's `overflow: hidden` shears the pill.
 *
 * So the media boxes are pinned to the card width below, and the pill is additionally capped so it
 * can never be wider than the space it is positioned in. `nowrap` keeps a long saving on one line
 * rather than letting it wrap into the photograph.
 */
.p321-badge {
	position: absolute;
	z-index: 2;
	border-radius: var(--rb);
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 10.5px;
	letter-spacing: 0.06em;
	white-space: nowrap;
	max-width: calc(100% - 18px);
}

/*
 * Owner, 2026-08-24: the two corner badges must line up exactly.
 *
 * They shared a `top` but not a box: the lot pill measured 22px tall against the saving's 24px
 * (different font-size and padding), so their tops agreed and their baselines and bottoms did not.
 * Both now sit in the same 24px box with the same type size, so the pair reads as one row however
 * long either label is.
 */
.p321-badge--lot,
.p321-badge--save {
	display: inline-flex;
	align-items: center;
	min-height: 24px;
	font-size: 11px;
	padding: 0 9px;
}

.p321-badge--lot {
	top: 9px;
	left: 9px;
	background: var(--ink);
	color: var(--brand-lt);
}

/* Matches --lot-pdp: the PDP media box is larger and padded, so the pair sits further in and reads
   a size up. Same shared box as the grid-card pair above, so they still align with each other. */
.p321-badge--save-pdp {
	top: 16px;
	right: 16px;
	min-height: 32px;
	font-size: 11.5px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	padding: 0 11px;
}

/* The PDP's media box is larger and padded, so the badge sits further in than on a grid card. */
.p321-badge--lot-pdp {
	top: 16px;
	left: 16px;
	min-height: 32px;
	font-size: 11.5px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	padding: 0 11px;
}

.p321-badge--save {
	top: 9px;
	right: 9px;
	font-weight: 700;
	color: #04252c;
	background: var(--brand);
}

/*
 * The design's card anchor sets `color: inherit`; without it the global `a { color: var(--brand-dk) }`
 * turned every product title teal instead of the design's near-black.
 */
.p321-product-card__link {
	color: inherit;
	display: flex;
	flex-direction: column;
	flex: 1;
	text-decoration: none;
}

.p321-product-card__body {
	padding: clamp(12px, 2.6vw, 18px);
	display: flex;
	flex-direction: column;
	gap: 7px;
	flex: 1;
}

/*
 * The add-to-cart row is a second .p321-product-card__body, a sibling of the card's anchor -- the
 * button has to live outside the <a> to be clickable at all. It must not take a share of the
 * card's spare height: with `flex: 1` on both, the grid's equal-height cards split their leftover
 * space between the anchor and this row, so the split point -- and with it the top of every
 * button -- moved with each card's content, and a row of cards had its buttons at four different
 * heights. Pinning this row to its content height sends all the slack to the anchor above, which
 * already bottom-aligns its own price via .p321-price-row's `margin-top: auto`.
 */
.p321-product-card__buy {
	flex: none;
	padding-top: 0;
}

.p321-product-card__title {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 106, "wght" 780;
	font-size: clamp(15px, 1.4vw, 18px);
	margin: 0;
	line-height: 1.2;
	text-wrap: pretty;
}

.p321-product-card__meta {
	display: flex;
	gap: 10px;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	color: var(--t2);
	flex-wrap: wrap;
}

.p321-product-card__meta strong {
	color: var(--brand-dk);
	font-weight: 600;
}

.p321-price-row {
	display: flex;
	align-items: baseline;
	gap: 9px;
	margin-top: auto;
	padding-top: 14px;
	flex-wrap: wrap;
}

.p321-price-row__from {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12.5px;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--t2);
}

.p321-price-row__price {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: clamp(19px, 2vw, 23px);
	font-weight: 600;
	letter-spacing: -0.02em;
}

.p321-price-row__price--lg {
	font-size: clamp(30px, 4.6vw, 38px);
}

.p321-price-row__was {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 13px;
	color: var(--t2);
	text-decoration: line-through;
}

.p321-price-row__save {
	font-size: 12.5px;
	font-weight: 700;
	color: #04252c;
	background: var(--brand);
	padding: 7px 12px;
	border-radius: var(--rb);
}

/* PDP: size chips, qty stepper */

/*
 * Vial-size chips wrap; they used to scroll (14-SITE-AUDIT.md D8).
 *
 * `display: flex` with no `flex-wrap` plus `overflow-x: auto` meant the row scrolled sideways
 * instead of reflowing, and the chips are `flex: none; white-space: nowrap`, so nothing shrank
 * either. Measured at 390: the chip is 157px in a 366px container, so exactly two fit per row --
 * Retatrutide hid its 30 MG, and Tirzepatide hid both 30 MG and 40 MG, the 40 MG starting 126px
 * past the container edge. Not clipped, absent. Two purchasable sizes invisible on the viewport
 * that carries ~80% of traffic, with no fade, chevron, scrollbar or "+n" to suggest a swipe.
 *
 * This is why "no horizontal overflow" is a floor and not a pass: the *page* never scrolled
 * sideways, so every automated overflow check reported the PDP clean. It was found by looking.
 *
 * Wrapping rather than shrinking is deliberate: the labels carry the size and the price, so
 * compressing them to fit would trade a hidden option for an unreadable one. Two rows at 390 is
 * the worst case in the current catalogue (four sizes), costing ~62px.
 *
 * `overflow-x: auto` stays as a fallback for a chip wider than its container -- impossible today at
 * 157px against 366px, but it degrades to a scroll rather than a clip if a label ever grows. That
 * also keeps this a scroll container, which clips the painted focus ring, so the padding carries
 * the 5px an `outline: 3px` at `outline-offset: 2px` needs; the negative margin cancels it so
 * surrounding spacing is unchanged.
 */
.p321-size-chips {
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
	overflow-x: auto;
	padding: 6px 6px 8px;
	margin: -6px -6px 0;
	-webkit-overflow-scrolling: touch;
	scrollbar-width: thin;
}

.p321-size-chip {
	flex: none;
	display: inline-flex;
	align-items: center;
	gap: 10px;
	min-height: 54px;
	padding: 0 22px;
	border-radius: var(--rb);
	cursor: pointer;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 15.5px;
	font-weight: 600;
	white-space: nowrap;
	border: 1.5px solid var(--hair);
	background: var(--panel);
	color: var(--t1);
}

.p321-size-chip:hover {
	border-color: var(--brand);
}

/*
 * `aria-pressed`, not `aria-selected`. These chips are the only control for the vial size -- Woo's
 * own <select> is display:none a few rules down, deliberately -- so they are toggle buttons in a
 * labelled group, not tabs: there is no tabpanel for a tab to control. aria-pressed is valid on a
 * <button> and owes no keyboard contract beyond the one a button already has.
 */
.p321-size-chip[aria-pressed="true"] {
	border-color: var(--brand);
	background: var(--ink);
	color: #fff;
}

.p321-size-chip__price {
	font-size: 13px;
	opacity: 0.7;
}

/*
 * The chips above ARE the vial-size control -- initSizeChips() drives WooCommerce's own
 * <select id="pa_vial-size"> so price/stock/add-to-cart stay WC's tested mechanism. That select
 * has to remain in the DOM for the variation form to work, but rendering it as well leaves a raw
 * "Vial size / Choose an option" dropdown sitting under the chips: the same control twice.
 *
 * display:none rather than a visually-hidden clip, deliberately -- it also drops the select from
 * the accessibility tree and the tab order, so keyboard and screen-reader users get one control
 * (the chips) instead of two that silently fight each other.
 *
 * Scoped to the vial-size row, not the whole .variations table, so a product that ever gains a
 * second attribute still renders that one normally. All five variable products carry only
 * pa_vial-size today.
 */
.p321-buy-form .variations tr:has(#pa_vial-size) {
	display: none;
}

.p321-buy-row {
	display: flex;
	gap: 12px;
	flex-wrap: wrap;
	align-items: center;
}

.p321-qty-stepper {
	display: flex;
	align-items: center;
	border: 1.5px solid var(--hair);
	border-radius: var(--rb);
	background: var(--panel);
	height: 56px;
	flex: none;
}

.p321-qty-stepper button {
	width: 52px;
	height: 52px;
	border: 0;
	background: transparent;
	cursor: pointer;
	font-size: 22px;
	color: var(--t1);
	border-radius: 50%;
}

.p321-qty-stepper__value {
	min-width: 34px;
	text-align: center;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 17px;
}

/* PDP: trust list */

.p321-trust-list {
	display: grid;
	gap: 1px;
	background: var(--hair);
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	overflow: hidden;
}

.p321-trust-list__row {
	background: var(--panel);
	padding: 16px 18px;
	display: flex;
	gap: 13px;
	align-items: center;
	font-size: 15px;
}

.p321-trust-list__row svg {
	flex: none;
	color: var(--brand-dk);
}

/* PDP: dark spec stat cards */

/*
 * The 290px floor was mistuned for the column these sit in: at 900px there is room for all three
 * cards, but the floor forced two and stranded the third. The count is data-driven, so this is a
 * floor correction rather than a fixed column count -- 210px still holds a label, a value and a
 * note comfortably, and lets the usual three sit on one row wherever they fit.
 */
.p321-stat-cards {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr));
	gap: 16px;
	margin-bottom: 16px;
}

.p321-stat-card {
	background: var(--ink-2);
	border: 1px solid var(--hair-dk);
	border-radius: var(--rc);
	padding: clamp(20px, 3vw, 26px);
}

.p321-stat-card__label {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 11.5px;
	letter-spacing: 0.18em;
	text-transform: uppercase;
	color: var(--od4);
	margin-bottom: 14px;
}

.p321-stat-card__value {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 112, "wght" 800;
	font-size: clamp(32px, 4.5vw, 52px);
	line-height: 1;
	color: #fff;
	overflow-wrap: anywhere;
}

.p321-stat-card__note {
	font-size: 14.5px;
	color: var(--od3);
	margin-top: 12px;
	line-height: 1.5;
}

/* PDP: dark sequence + spec dl panel */

.p321-dark-panel {
	background: var(--ink-2);
	border: 1px solid var(--hair-dk);
	border-radius: var(--rc);
	overflow: hidden;
}

.p321-dark-panel__seq {
	padding: clamp(20px, 3vw, 24px) clamp(18px, 3vw, 26px);
	border-bottom: 1px solid var(--hair-dk);
}

.p321-dark-panel__seq-label {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 11.5px;
	letter-spacing: 0.18em;
	text-transform: uppercase;
	color: var(--od4);
	margin-bottom: 12px;
}

.p321-dark-panel__seq-value {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: clamp(14px, 2vw, 22px);
	color: var(--brand-lt);
	letter-spacing: 0.04em;
	overflow-wrap: anywhere;
	line-height: 1.45;
}

/*
 * Two columns at most. A 320px floor let `auto-fit` reach three columns at 1100 and above, which
 * put a four-row spec list into 3 + 1 -- and three columns of key/value rows reads as a table that
 * lost its shape rather than as a spec list. The row count is data-driven, so a short last column
 * is still possible; two columns just halves what can be stranded and keeps the pairs legible.
 */
.p321-dl-dark {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 0;
	margin: 0;
}

@media (max-width: 760px) {
	.p321-dl-dark {
		grid-template-columns: 1fr;
	}
}

.p321-dl-dark__row {
	display: flex;
	justify-content: space-between;
	gap: 18px;
	padding: 16px clamp(18px, 3vw, 26px);
	border-bottom: 1px solid var(--hair-dk);
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 13.5px;
}

.p321-dl-dark__row dt {
	color: var(--od4);
	letter-spacing: 0.05em;
	flex: none;
}

.p321-dl-dark__row dd {
	margin: 0;
	text-align: right;
	color: #fff;
	overflow-wrap: anywhere;
}

/* PDP: "How it works" numbered steps */

.p321-step-list {
	display: flex;
	flex-direction: column;
	gap: 1px;
	background: var(--hair);
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	overflow: hidden;
}

.p321-step {
	background: var(--panel);
	padding: 22px clamp(18px, 3vw, 24px);
}

.p321-step__num {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 11.5px;
	letter-spacing: 0.16em;
	color: var(--brand-dk);
	margin-bottom: 9px;
}

.p321-step__body {
	margin: 0;
	font-size: 15.5px;
	line-height: 1.6;
	color: var(--t2);
	text-wrap: pretty;
}

.p321-step__body b {
	color: var(--t1);
	font-weight: 600;
}

/* PDP: COA panel (analytics section) */

.p321-lot-list {
	display: flex;
	flex-direction: column;
	gap: 10px;
	margin-top: 26px;
}

.p321-lot-list__row {
	text-align: left;
	display: flex;
	align-items: center;
	gap: 14px;
	flex-wrap: wrap;
	min-height: 60px;
	padding: 12px clamp(14px, 3vw, 20px);
	border: 1px solid var(--hair-dk);
	background: transparent;
	border-radius: var(--rc);
	cursor: pointer;
	color: #fff;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 13.5px;
}

.p321-lot-list__row[aria-pressed="true"] {
	border-color: var(--brand);
	background: rgba(2, 174, 203, 0.12);
}

.p321-lot-list__code {
	color: var(--brand-lt);
	letter-spacing: 0.06em;
}

.p321-lot-list__date {
	color: var(--od4);
}

.p321-lot-list__purity {
	margin-left: auto;
	color: var(--brand-lt);
}

.p321-coa-card {
	background: var(--ink-2);
	border: 1px solid var(--hair-dk);
	border-radius: var(--rc);
	padding: clamp(18px, 3vw, 30px);
}

.p321-coa-card__header {
	display: flex;
	justify-content: space-between;
	align-items: flex-start;
	border-bottom: 1px solid var(--hair-dk);
	padding-bottom: 15px;
	margin-bottom: 18px;
	gap: 14px;
	flex-wrap: wrap;
}

.p321-coa-card__title {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 108, "wght" 750;
	font-size: 20px;
	text-transform: uppercase;
	/* Explicit rather than inherited -- this card is now also used standalone on single-coa.php's
	   light page background (Phase 4), not only inside a .band-dark section like before, where an
	   inherited color would leave it dark-on-dark and unreadable. */
	color: #fff;
}

.p321-coa-card__sub {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 13px;
	color: var(--od3);
	margin-top: 5px;
}

.p321-verified-badge {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.12em;
	color: var(--brand-lt);
	border: 1px solid var(--brand);
	padding: 6px 10px;
	white-space: nowrap;
}

.p321-download-coa {
	display: inline-flex;
	align-items: center;
	gap: 9px;
	margin-top: 20px;
	min-height: 44px;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 13px;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--brand-lt);
}

/* Breadcrumb */

/*
 * `padding-block`, NOT the `padding` shorthand. This element also carries `.container`, whose
 * `padding: 0 var(--gutter)` is the page's left/right gutter -- and the shorthand here silently
 * reset it to 0, so on a phone the trail sat flush against the screen edge with no margin at all
 * while every other line on the page was inset. Exactly the bug already recorded against
 * `.container.section` further up this file; second occurrence, same cause.
 */
.p321-breadcrumb {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12.5px;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--t2);
	display: flex;
	gap: 9px;
	flex-wrap: wrap;
	align-items: center;
	padding-block: 8px 0;
}

.p321-breadcrumb a {
	color: var(--t2);
	display: inline-flex;
	align-items: center;
	min-height: 44px;
}

.p321-breadcrumb__current {
	color: var(--t1);
}

/* Empty state */

.p321-empty-state {
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	background: var(--panel);
	padding: clamp(34px, 6vw, 60px);
	text-align: center;
}

.p321-empty-state__title {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 106, "wght" 780;
	font-size: 22px;
	margin-bottom: 10px;
}

.p321-empty-state p {
	color: var(--t2);
	font-size: 16px;
	margin: 0 0 20px;
}

/* WooCommerce's own variation-form price/qty/button markup (see content-single-product.php's
   note on why this isn't a full custom rebuild) -- restyled with our tokens so it doesn't look
   like an unstyled WC default, and so its disabled state clears contrast requirements (WC's own
   default disabled-button color failed axe: 2.13:1 against the required 4.5:1). */

.single_add_to_cart_button {
	font-family: "Instrument Sans", "Instrument Sans Fallback", sans-serif;
	border-radius: var(--rb);
	min-height: 56px;
	padding: 0 30px;
	font-size: 16.5px;
	font-weight: 700;
	border: 0;
	background: var(--brand);
	color: #04252c;
	cursor: pointer;
}

.single_add_to_cart_button:hover {
	background: #1cc3dc;
}

/* !important: WooCommerce's own .button.alt rule outranks a same-weight override here, and
   its default disabled-state color (#fafbfc on #baa6d6) fails contrast at 2.13:1 against the
   4.5:1 requirement -- this is a targeted, known exception, not a general pattern to repeat.
   --t1 (not --t2, which only measures ~4.22:1 against --hair at this bold 16px size -- close
   but short of AA) so the fix actually clears the threshold, not just moves it. */
button.single_add_to_cart_button.disabled {
	background: var(--hair) !important;
	color: var(--t1) !important;
	opacity: 1 !important;
	cursor: not-allowed;
}

.woocommerce-variation-price {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: clamp(24px, 3.6vw, 32px);
	font-weight: 600;
	margin: 10px 0;
}

.woocommerce-variation-availability {
	font-size: 14px;
	color: var(--t2);
}

.quantity .qty {
	height: 56px;
	border: 1.5px solid var(--hair);
	border-radius: var(--rb);
	text-align: center;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 16px;
	width: 70px;
}

.p321-mobile-filter-bar {
	display: none;
}

@media (max-width: 700px) {
	.p321-search-sort select {
		display: none;
	}

	.p321-desktop-chips {
		display: none;
	}

	.p321-mobile-filter-bar {
		display: flex;
	}
}

/* Cart */

.p321-cart-shell {
	display: grid;
	grid-template-columns: 1fr min(100%, 380px);
	gap: clamp(26px, 4vw, 48px);
	align-items: start;
}

.p321-cart-lines {
	display: flex;
	flex-direction: column;
	gap: 18px;
}

.p321-cart-line {
	display: flex;
	gap: 16px;
	align-items: flex-start;
	padding-bottom: 18px;
	border-bottom: 1px solid var(--hair);
}

.p321-cart-line__media {
	flex: none;
	width: 84px;
	height: 84px;
	border-radius: var(--ri);
	overflow: hidden;
	background: linear-gradient(170deg, #fff, #e2ebee);
	border: 1px solid var(--hair);
	display: block;
}

.p321-cart-line__media img {
	width: 100%;
	height: 100%;
	-o-object-fit: contain;
	   object-fit: contain;
}

.p321-cart-line__body {
	flex: 1;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.p321-cart-line__name {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 106, "wght" 700;
	font-size: 16px;
	color: var(--t1);
}

.p321-cart-line__meta {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 13px;
	color: var(--t2);
}

.p321-cart-line__controls {
	display: flex;
	align-items: center;
	gap: 16px;
	flex-wrap: wrap;
}

/*
 * WooCommerce's own `a.remove` is `font-size: 1.5em`, a 1em square, and `color: red !important`,
 * which beat the theme's styling and rendered the cart's "Remove" as 24px red text. woocommerce.css
 * only loads on cart/checkout/account (see inc/performance.php), which is exactly where this line
 * appears. Matching the design's treatment (Cart.dc.html:112) needs a selector that outranks Woo's
 * `.woocommerce a.remove` (0,2,1) -- a doubled class alone (0,2,0) loses, and did: the font and
 * letter-spacing applied while colour and size stayed Woo's.
 */
.woocommerce a.p321-cart-line__remove.remove {
	display: inline-flex;
	align-items: center;
	width: auto;
	height: auto;
	min-height: 44px;
	padding: 0 2px;
	border: 0;
	border-bottom: 1px solid var(--hair);
	border-radius: 0;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12.5px;
	font-weight: 400;
	letter-spacing: 0.08em;
	line-height: 1;
	text-transform: uppercase;
	color: var(--t2) !important;
}

.woocommerce a.p321-cart-line__remove.remove:hover {
	border-bottom-color: var(--ink);
	color: var(--ink) !important;
}

.p321-cart-line__total {
	flex: none;
	text-align: right;
	padding-top: 4px;
}

/*
 * The cart's compact stepper. `--sm` shrinks the control, but it was shrinking the *targets* too:
 * 40x40 buttons at every width, against 52x52 for the identical control on the PDP. Same job, two
 * sizes, and the smaller one on the page where changing quantity is the main thing you do.
 *
 * 44x44 is CLAUDE.md's goal rather than the 24x24 floor. The container grows to 48 so the buttons
 * are not flush against its border -- at 44 in a 44-tall box the round button edge meets the
 * rounded container edge and reads as a rendering fault.
 */
.p321-qty-stepper--sm {
	height: 48px;
}

.p321-qty-stepper--sm button {
	width: 44px;
	height: 44px;
	font-size: 18px;
}

.p321-qty-stepper input[type="number"] {
	border: 0;
	background: transparent;
	width: 34px;
	text-align: center;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 15px;
	-moz-appearance: textfield;
}

.p321-qty-stepper input[type="number"]::-webkit-inner-spin-button,
.p321-qty-stepper input[type="number"]::-webkit-outer-spin-button {
	-webkit-appearance: none;
	margin: 0;
}

.p321-cart-summary {
	position: sticky;
	top: clamp(80px, 12vw, 110px);
}

.p321-cart-summary__panel {
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	background: var(--panel);
	padding: var(--card-pad);
}

.p321-summary-dl {
	display: flex;
	flex-direction: column;
	gap: 10px;
	margin: 0;
}

.p321-summary-dl__row {
	display: flex;
	justify-content: space-between;
	gap: 12px;
	font-size: 15px;
}

.p321-summary-dl__row dt {
	color: var(--t2);
}

.p321-summary-dl__row dd {
	margin: 0;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
}

.p321-summary-dl__row--save dd {
	color: var(--brand-dk);
}

/* The free-shipping nudge is a second <dd> on the Shipping row, so the row has to wrap it onto its
   own full-width line beneath the figure rather than squeezing three children across. */
.p321-summary-dl__row--stack {
	flex-wrap: wrap;
}

/* Selector carries `dd` deliberately: `.p321-summary-dl__row dd` is (0,1,1) and sets the mono face
   for the figures, so a lone class at (0,1,0) loses and the sentence rendered as monospace -- which
   read as another data value rather than a line of prose. */
.p321-summary-dl__row dd.p321-summary-dl__note {
	flex-basis: 100%;
	margin: 2px 0 0;
	font-family: "Instrument Sans", "Instrument Sans Fallback", system-ui, sans-serif;
	font-size: 13px;
	line-height: 1.45;
	color: var(--t2);
}

.p321-summary-total {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	margin-top: 16px;
	padding-top: 16px;
	border-top: 1px solid var(--hair);
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 27px;
	font-weight: 600;
}

/* !important: WooCommerce's own .button styling (a purple default) outranks a same-weight
   override here -- same class of issue as the single_add_to_cart_button fix. */
a.checkout-button.wc-forward {
	display: flex;
	width: 100%;
	min-height: 56px;
	align-items: center;
	justify-content: center;
	border-radius: var(--rb);
	background: var(--brand) !important;
	color: #04252c !important;
	font-size: 16px;
	font-weight: 700;
}

a.checkout-button.wc-forward:hover {
	background: #1cc3dc !important;
	color: #04252c !important;
}

@media (max-width: 895px) {
	.p321-cart-shell {
		grid-template-columns: 1fr;
	}
}

/* The checkout coupon toggle/form rules lived here. The form is no longer rendered
   (p321_remove_coupon_form() in inc/checkout.php), so these matched nothing and are gone.
   Note for anyone restoring a coupon field: the `.woocommerce-form-coupon-toggle .woocommerce-info`
   rule zeroed the padding that the generic `.woocommerce-info` box below still needs for its
   border, background and 3px left bar, so the toggle's text sat on its own border. Restyle from
   the box in, not by resetting padding to 0. */

/*
 * "Your Order" and its Edit link.
 *
 * Edit was a bare 25x16 inline link under the heading -- below the WCAG 2.2 target-size (minimum)
 * AA floor of 24x24, and it is one of only two ways back to the cart from the checkout. Pairing it
 * with the heading on one row buys the 44px box without adding 44px of page height.
 */
.p321-order-head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 16px;
	margin-bottom: 16px;
}

.p321-order-head h2 {
	text-transform: uppercase;
	font-size: 18px;
	margin: 0;
}

.p321-order-head__edit {
	flex: none;
	display: inline-flex;
	align-items: center;
	min-height: 44px;
	padding: 0 2px;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12.5px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--t2);
	border-bottom: 1px solid var(--hair);
	line-height: 1;
}

.p321-order-head__edit:hover {
	color: var(--ink);
	border-bottom-color: var(--ink);
}

/*
 * "Your Order" line items as a disclosure, closed by default. Same treatment as the PDP's Lot
 * history, sized for a summary panel rather than a full-width section.
 */
.p321-order-items {
	border-top: 1px solid var(--hair);
	border-bottom: 1px solid var(--hair);
}

.p321-order-items__summary {
	cursor: pointer;
	list-style: none;
	display: flex;
	align-items: center;
	gap: 12px;
	min-height: 52px;
}

.p321-order-items__summary::-webkit-details-marker {
	display: none;
}

.p321-order-items__summary:focus-visible {
	outline: 3px solid var(--brand);
	outline-offset: -3px;
}

.p321-order-items__label {
	flex: 1;
	min-width: 0;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 13px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--t2);
}

.p321-order-items__figure {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 15px;
	color: var(--t1);
}

.p321-order-items__chevron {
	flex: none;
	color: var(--t2);
	transition: transform 0.18s ease;
}

.p321-order-items[open] .p321-order-items__chevron {
	transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {

	.p321-order-items__chevron {
		transition: none;
	}
}

/* The list carries its own top margin; the closing border needs the matching gap below it. */
.p321-order-items[open] .p321-cart-lines {
	margin-bottom: 16px;
}

/*
 * Review lines are tighter than the cart's own and centred on their thumbnail. The last one drops
 * its rule so it does not sit 16px above the disclosure's closing border as a second hairline.
 * This was three inline declarations on the div; as a class it can address :last-child at all.
 */
.p321-cart-line--review {
	align-items: center;
	padding-bottom: 12px;
}

.p321-cart-line--review:last-child {
	border-bottom: 0;
	padding-bottom: 0;
}

/* Checkout */

.p321-checkout-shell {
	display: grid;
	grid-template-columns: 1fr min(100%, 380px);
	gap: clamp(26px, 4vw, 48px);
	align-items: start;
}

.p321-checkout-sections {
	display: flex;
	flex-direction: column;
	gap: 18px;
}

.p321-checkout-section {
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	background: var(--panel);
	padding: var(--card-pad);
}

.p321-checkout-section__title {
	display: flex;
	align-items: center;
	gap: 12px;
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 106, "wght" 750;
	font-size: 18px;
	text-transform: uppercase;
	margin: 0 0 18px;
}

/* Section 04 sits directly above WooCommerce's own payment block rather than inside one of our
   section cards, so it supplies the top separation the card padding would otherwise give it. */
.p321-checkout-section__title--payment {
	margin-top: clamp(26px, 3.4vw, 34px);
	padding-top: clamp(20px, 2.6vw, 26px);
	border-top: 1px solid var(--hair);
}

.p321-checkout-section__num {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 13px;
	color: var(--brand-dk);
	border: 1px solid var(--brand);
	border-radius: 50%;
	width: 26px;
	height: 26px;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: none;
}

.p321-form-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
	gap: 14px;
}

/*
 * A row whose only control is a hidden input still takes a grid cell. WooCommerce collapses
 * `billing_country` to `<input type="hidden" value="US">` on a single-country store, but keeps the
 * wrapping `<p class="form-row">` -- which measured 18px plus a 14px gap at the foot of Shipping
 * Address at 390, and an empty 250x100 cell beside State and Zip code at 1440.
 *
 * Written as "no visible control" rather than `#billing_country_field`, so it also covers any other
 * field Woo collapses later -- shipping country on the same store, or state for a country with no
 * states. `:has()` is already relied on above for the select chevron; where it is unsupported this
 * rule drops and the empty cell comes back, which is exactly the behaviour being fixed and no worse
 * than before.
 */
.p321-form-grid > .form-row:has(input[type="hidden"]):not(:has(input:not([type="hidden"]), select, textarea)) {
	display: none;
}

/*
 * WooCommerce sizes the name pair as 47% floats:
 * `.woocommerce form .form-row-first, ... { width: 47%; overflow: visible }` plus `float: left/right`.
 * Inside `.p321-form-grid` those rows are **grid items**, and a grid item ignores `float` entirely --
 * so the 47% bought nothing but a field cut in half inside a cell that was already the right width.
 *
 * Measured on the deployed checkout before the fix: at 390 the grid is one column and the name
 * inputs rendered **144px against 314px** for email, phone, street and city; at 1440 the grid is
 * three columns and they rendered **112px inside 242px cells**. Same defect at every width -- it
 * only looked deliberate on desktop because the shortened pair still lined up with each other.
 *
 * `width: auto` rather than `100%`: these are grid items, so the cell decides. The margin reset is
 * because Woo's float layout leaves the pair with a gutter the grid's own `gap` already provides.
 *
 * Three classes to beat Woo's `(0,2,1)`. Do not drop `.woocommerce-checkout` -- without it this is
 * `(0,2,0)` and loses, silently, at every width.
 */
.woocommerce-checkout .p321-form-grid > .form-row-first,
.woocommerce-checkout .p321-form-grid > .form-row-last {
	float: none;
	width: auto;
	margin-right: 0;
	margin-left: 0;
}

.p321-form-row {
	display: flex;
	flex-direction: column;
	gap: 6px;
}

.p321-form-row--full {
	grid-column: 1 / -1;
}

/*
 * The contact form's validation summary. Was an inline `style` carrying three raw hex values, the
 * site's only freehand error UI (14-SITE-AUDIT.md D9). A class rather than an inline style because
 * inline styles outrank every stylesheet rule, so the old card could not be themed or overridden
 * at all -- which was half the defect.
 *
 * The left border is 4px against the 1px box: the weight is what carries "error" at a glance, so
 * it does not rely on the red alone (ui-ux-pro-max: never convey meaning by colour only). The
 * element also carries role="alert" in the template, which is what announces it to a screen reader.
 */
.p321-form-error {
	border: 1px solid var(--alert);
	border-left: 4px solid var(--alert);
	border-radius: 12px;
	background: var(--alert-bg);
	padding: 14px 16px;
	font-size: 15px;
	color: var(--alert-ink);
}

.p321-form-row label {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--t2);
}

.p321-form-row input,
.p321-form-row select,
.p321-form-row textarea {
	min-height: 52px;
	border: 1.5px solid var(--hair);
	border-radius: 14px;
	padding: 0 16px;
	font-family: "Instrument Sans", "Instrument Sans Fallback", sans-serif;
	/* 16px, not 15.5: under 16 iOS zooms the page on focus. Set here rather than relying on the
	   global floor, which this class selector would outrank -- as the contact textarea proved. */
	font-size: 16px;
	color: var(--t1);
	background: var(--panel);
	box-sizing: border-box;
	width: 100%;
}

.p321-form-row textarea {
	padding: 14px 16px;
	line-height: 1.6;
	resize: vertical;
	min-height: 150px;
}


.p321-form-row input:focus,
.p321-form-row select:focus,
.p321-form-row textarea:focus {
	border-color: var(--brand);
	outline: 3px solid var(--brand);
	outline-offset: 1px;
}

/* Contact page */

/*
 * Four topics, so two columns or one -- never three.
 *
 * The 180px floor let `auto-fit` reach three columns in the form's own column at 1440, which put
 * "Privacy request" alone on a second row beside two empty tracks. 900 and 390 were already
 * correct at two and one. Two columns also gives each label ~295px at 1440 instead of 195, so the
 * longer ones ("Order or shipping") stop sitting tight against their padding.
 *
 * Note for anyone measuring this row: it has eight children, not four. Each topic is a
 * visually-hidden radio plus its label, and the radios are `position: absolute`, so they are not
 * grid items -- a naive child count reads this as eight and gets the column maths wrong.
 */
.p321-topic-row {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 10px;
}

@media (max-width: 560px) {
	.p321-topic-row {
		grid-template-columns: 1fr;
	}
}

.p321-topic-row input[type="radio"] {
	position: absolute;
	opacity: 0;
	pointer-events: none;
}

.p321-topic-row label {
	min-height: 54px;
	padding: 0 18px;
	border-radius: 12px;
	cursor: pointer;
	font-size: 15px;
	font-weight: 600;
	display: flex;
	align-items: center;
	border: 1.5px solid var(--hair);
	background: var(--panel);
	color: var(--t1);
}

.p321-topic-row input[type="radio"]:checked + label {
	border-color: var(--ink);
	background: var(--ink);
	color: #fff;
}

.p321-topic-row input[type="radio"]:focus-visible + label {
	outline: 3px solid var(--brand);
	outline-offset: 2px;
}

.p321-wholesale-fields {
	display: none;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
	gap: 16px;
	border: 1px solid var(--hair);
	border-radius: 14px;
	background: var(--band);
	padding: 18px;
	box-sizing: border-box;
}

.p321-contact-form:has(#p321-topic-wholesale:checked) .p321-wholesale-fields {
	display: grid;
}

.p321-info-card {
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	background: var(--panel);
	padding: clamp(22px, 3vw, 28px);
}

.p321-contact-success {
	border: 1px solid var(--brand);
	border-radius: var(--rc);
	background: var(--panel);
	padding: clamp(28px, 4vw, 44px);
}

/* Reviews page */

.p321-reviews-columns {
	-moz-columns: 3 320px;
	     columns: 3 320px;
	-moz-column-gap: clamp(14px, 2vw, 20px);
	     column-gap: clamp(14px, 2vw, 20px);
}

.p321-review-card {
	-moz-column-break-inside: avoid;
	     break-inside: avoid;
	margin: 0 0 clamp(14px, 2vw, 20px);
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	background: var(--panel);
	padding: clamp(20px, 2.6vw, 26px);
}

/* Legal pages */

.p321-legal-layout {
	padding: clamp(40px, 6vw, 72px) var(--gutter) clamp(56px, 8vw, 96px);
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
	gap: clamp(30px, 4vw, 64px);
	align-items: start;
}

.p321-legal-toc {
	position: sticky;
	top: 110px;
	max-width: 320px;
}

.p321-legal-toc__label {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 11.5px;
	letter-spacing: 0.18em;
	text-transform: uppercase;
	color: var(--t2);
	margin-bottom: 8px;
}

.p321-legal-toc a {
	color: var(--t2);
	font-size: 14.5px;
	line-height: 1.35;
	padding: 9px 0;
	display: flex;
	align-items: center;
	min-height: 44px;
	border-bottom: 1px solid var(--hair);
}

.p321-legal-toc a:hover {
	color: var(--brand-dk);
}

.p321-legal-article {
	grid-column: span 2;
	min-width: 0;
	max-width: 74ch;
}

.p321-legal-article .p321-legal-heading {
	scroll-margin-top: 110px;
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 106, "wght" 780;
	text-transform: uppercase;
	letter-spacing: -0.02em;
	font-size: clamp(21px, 2.4vw, 28px);
	line-height: 1.15;
	margin: 40px 0 16px;
}

.p321-legal-article > *:first-child .p321-legal-heading,
.p321-legal-article .p321-legal-heading:first-child {
	margin-top: 0;
}

.p321-legal-article p,
.p321-legal-article li {
	font-size: 16.5px;
	line-height: 1.7;
	color: var(--t2);
}

.p321-legal-article ul {
	display: flex;
	flex-direction: column;
	gap: 9px;
	padding-left: 20px;
	margin: 0 0 18px;
}

@media (max-width: 895px) {
	/* Collapse to a single column alongside dropping the article's span. Changing the span alone
	   is not enough: auto-fit still resolves to three 260px columns at this width, so the article
	   would sit in one narrow column with a dead third column beside it. */
	.p321-legal-layout {
		grid-template-columns: 1fr;
	}

	.p321-legal-toc {
		position: static;
		max-width: none;
		display: flex;
		flex-wrap: wrap;
		gap: 4px 18px;
		margin-bottom: 8px;
	}

	.p321-legal-toc a {
		border-bottom: 0;
		padding: 4px 0;
	}

	.p321-legal-article {
		grid-column: span 1;
	}
}

/* Legal index (/legal/) -- a structural page with no design prototype, see page-legal.php */

.p321-legal-index__intro p {
	color: var(--od2);
	font-size: 17px;
	line-height: 1.65;
	margin: 18px 0 0;
	max-width: 62ch;
}

.p321-legal-index {
	padding: clamp(40px, 6vw, 72px) var(--gutter) clamp(56px, 8vw, 96px);
}

.p321-legal-index__list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
	gap: 18px;
	/* Two columns at desktop, so the four current documents sit as a balanced 2x2 rather than
	   3 + 1. Still auto-fits, so a fifth document simply extends the second column. */
	max-width: 720px;
}

.p321-legal-index__list a {
	display: flex;
	flex-direction: column;
	gap: 7px;
	height: 100%;
	border: 1px solid var(--hair);
	border-left: 4px solid var(--ink);
	border-radius: var(--rc);
	background: var(--panel);
	padding: clamp(20px, 2.6vw, 26px);
	color: inherit;
	text-decoration: none;
}

.p321-legal-index__list a:hover {
	border-color: var(--hair-dk);
	border-left-color: var(--brand-dk);
}

.p321-legal-index__title {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 106, "wght" 780;
	text-transform: uppercase;
	letter-spacing: -0.02em;
	font-size: 19px;
	line-height: 1.2;
}

.p321-legal-index__desc {
	font-size: 15.5px;
	line-height: 1.6;
	color: var(--t2);
}

.p321-legal-index__meta {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--t2);
	margin-top: auto;
	padding-top: 6px;
}

/* Wraps WooCommerce's own wc_cart_totals_shipping_html() output, which always renders a
   <tr><th>/<td> pair (shared with the cart/checkout table-based defaults) regardless of
   context -- table/tbody/tr/td stay as valid, un-invented markup for correct HTML parsing,
   restyled below into the design's toggle-card list rather than a literal table. */
.p321-delivery-options,
.p321-delivery-options tbody,
.p321-delivery-options tr,
.p321-delivery-options th {
	display: block;
	width: 100%;
}

.p321-delivery-options th {
	display: none;
}

.p321-delivery-options td {
	display: block;
	padding: 0;
}

.p321-delivery-options ul#shipping_method {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 10px;
}

.p321-delivery-options ul#shipping_method li {
	position: relative;
}

/*
 * The `li` and the leading `.woocommerce` are load-bearing, not padding.
 *
 * This rule was `.p321-delivery-options ul#shipping_method label` -- specificity (1,1,2).
 * woocommerce.css ships `.woocommerce ul#shipping_method li label { display: inline }` at (1,1,3),
 * which won, so `display: flex` never applied while every other declaration here did. The delivery
 * options rendered as shrink-to-fit inline pills instead of full-width cards: the price was not
 * right-aligned, the row was not a tap target, and the radio fell back to its native 13x13.
 * Everything else about the rule looked correct, which is why it read as fine in the source.
 *
 * Adding `li` only ties at (1,1,3) and leaves the outcome resting on stylesheet order. The
 * `.woocommerce` prefix takes it to (1,2,3) and settles it -- the same fix already applied to
 * `.woocommerce a.p321-cart-line__remove.remove` and the payment radios.
 */
.woocommerce .p321-delivery-options ul#shipping_method li label {
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: 14px;
	min-height: 54px;
	padding: 12px 18px;
	border: 1.5px solid var(--hair);
	border-radius: var(--rc);
	cursor: pointer;
	font-size: 15px;
	font-weight: 600;
}

/* 24x24 for the WCAG 2.2 target-size (minimum) AA floor, matching the payment radios above. The
   card label is the real tap target at 323x54; this is about the control itself. */
.woocommerce .p321-delivery-options ul#shipping_method li input[type="radio"] {
	position: absolute;
	left: 18px;
	top: 50%;
	width: 24px;
	height: 24px;
	transform: translateY(-50%);
	accent-color: var(--brand);
}

.woocommerce .p321-delivery-options ul#shipping_method li input[type="radio"] ~ label {
	padding-left: 54px;
}

.woocommerce .p321-delivery-options ul#shipping_method li input[type="radio"]:checked ~ label {
	border-color: var(--brand);
	background: rgba(2, 174, 203, 0.06);
}

.p321-delivery-options .woocommerce-shipping-destination {
	font-size: 13px;
	color: var(--t2);
	margin: 8px 0 0;
}

/*
 * A card inside a card. Sections 01-03 are top-level cards in `.p321-checkout-sections`, but
 * WooCommerce fires `woocommerce_review_order_before_submit` from inside its own
 * `.form-row.place-order`, so section 05 gets the card treatment a second time and its padding
 * stacks on everything above it:
 *
 *   .p321-cart-summary__panel   358px, 18px padding
 *     .form-row.place-order     320px, 16px padding
 *       .p321-checkout-section  288px, 18px padding   <- the second card
 *         .p321-attestation     250px
 *
 * 54px of padding per side, so the one sentence on this site that has to be read and understood --
 * 21+, research use only, not for human consumption -- was set in a 203px column, seven lines deep,
 * on the screen where the customer commits. An earlier pass measured that same 203px column and
 * fixed only the typography.
 *
 * Section 04 (Payment) is already not a card for exactly this reason -- see
 * `.p321-checkout-section__title--payment`. This makes 05 consistent with it, so the attestation
 * gets the same width as its actual siblings: the privacy notice and the Place Order button.
 */
.form-row.place-order .p321-checkout-section {
	border: 0;
	background: none;
	padding: 0;
}

/*
 * `gap: 0` and the label's `padding-left` below carry the spacing instead, for the same reason as
 * the payment rows: a flex `gap` is a dead strip between two tap targets, and this checkbox is
 * required to place an order. Measured at 390 before the change: a 17px strip -- the 14px gap plus
 * the UA's own 3px right margin on a checkbox, which is why `margin-right` is reset explicitly.
 */
.p321-attestation {
	display: flex;
	gap: 0;
	align-items: flex-start;
}

.p321-attestation input[type="checkbox"] {
	flex: none;
	width: 26px;
	height: 26px;
	margin-top: 2px;
	margin-right: 0;
	accent-color: var(--brand);
	border-radius: 8px;
}

/*
 * The consent, set to be read.
 *
 * WooCommerce fires `woocommerce_review_order_before_submit` from inside its own
 * `.form-row.place-order`, so `.form-row label` -- the mono 12px uppercase micro-label meant for
 * field captions -- was matching this and winning on source order. The single most important
 * statement on the site (21+, research use only) was therefore rendering as ~8 lines of 12px
 * all-caps monospace in a 203px column: the least readable setting available, for the one piece of
 * copy that has to be understood rather than skimmed.
 *
 * Body face, sentence case, near-black. The wording is untouched -- it was always sentence case in
 * the PHP; only the CSS was shouting.
 *
 * Specificity is deliberate: `.form-row label` is 0-1-1 and sits later in the file, so the plain
 * `.p321-attestation label` this replaces could never have won.
 */
.p321-checkout-section .p321-attestation label {
	font-family: "Instrument Sans", "Instrument Sans Fallback", system-ui, sans-serif;
	font-size: 15px;
	line-height: 1.6;
	letter-spacing: normal;
	text-transform: none;
	color: var(--t1);
	/* The gap the wrapper used to own, so the strip between box and text is inside the target. */
	padding-left: 17px;
}

/* !important: WooCommerce's own --wc-primary (purple) button styling outranks a same-weight
   override -- same class of fix as the add-to-cart/checkout-proceed buttons. */
button#place_order {
	width: 100%;
	min-height: 56px;
	border-radius: var(--rb);
	background: var(--brand) !important;
	color: #04252c !important;
	font-size: 16px;
	font-weight: 700;
	border: 0;
	margin-top: 16px;
}

button#place_order:hover {
	background: #1cc3dc !important;
	color: #04252c !important;
}

/*
 * WooCommerce tints `#payment` itself with a --wc-primary-derived lavender,
 * rgba(129, 110, 153, 0.14). At 849px it was the largest single block on the checkout and the only
 * untokenised colour on the page. The panel it sits in already supplies the surface, so this is
 * transparent rather than re-tinted -- .payment_box below keeps var(--band) so the method
 * description still reads as a nested note.
 */
.woocommerce-checkout #payment {
	background: transparent;
	border-radius: var(--rc);
}

/* WC's default payment_box has a light-purple tint from --wc-primary; restyle with tokens. */
.woocommerce-checkout .payment_box {
	background: var(--band) !important;
	color: var(--t2);
	border-radius: var(--ri);
}

.woocommerce-checkout .payment_box::before {
	border-bottom-color: var(--band) !important;
}

/*
 * WooCommerce's validated/invalid field colours, on the two pages this theme owns.
 *
 * Woo ships `--wc-green: #007518` and `--wc-red: #a00`, neither of which is in this palette.
 * Redefining the two custom properties beats out-specifying the rules that consume them: it is two
 * declarations instead of four selectors, and it avoids another specificity contest with
 * woocommerce.css of the kind that has already cost this build twice.
 *
 * Scoped to the cart and checkout body classes deliberately. `--wc-red` feeds 16 rules across
 * WooCommerce, including out-of-stock text on the PDP and the shop's layered-nav filter icons.
 * Those are not this change's business, and the PDP is mid-verification on device.
 *
 * `--wc-green` resolves to `--hair` on purpose -- it is not a green any more. Owner, 2026-08-26:
 * valid fields stay neutral and only errors carry colour. On a ten-field checkout every field
 * lighting up as it is filled is noise, and it avoids signalling state by colour alone. The
 * variable is redefined rather than the rule overridden so the single place Woo uses it keeps
 * working without a second selector to maintain.
 */
.woocommerce-cart,
.woocommerce-checkout {
	--wc-green: var(--hair);
	--wc-red: var(--alert);
}

.p321-checkout-error {
	border: 1px solid var(--alert);
	background: #fbecea;
	color: #7e2620;
	border-radius: var(--ri);
	padding: 14px 18px;
	font-size: 14.5px;
}

@media (max-width: 895px) {
	.p321-checkout-shell {
		grid-template-columns: 1fr;
	}
}

/* Bundles index: hero stat band */

.p321-hero-stats {
	display: flex;
	gap: clamp(20px, 4vw, 48px);
	flex-wrap: wrap;
	margin-top: 30px;
}

.p321-hero-stat__value {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 110, "wght" 800;
	font-size: clamp(26px, 3.4vw, 40px);
	color: #fff;
	line-height: 1;
}

.p321-hero-stat__value--brand {
	color: var(--brand-lt);
}

.p321-hero-stat__label {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--od4);
	margin-top: 6px;
}

/* Bundle grid card (Bundles index + PDP related rail) -- shares .p321-product-card's frame,
   card-level bits below cover the multi-item media strip and itemized retail rows. */

.p321-badge--count {
	top: 9px;
	left: 9px;
	background: var(--ink);
	color: var(--brand-lt);
	padding: 4px 7px;
}

/* Pinned to the card so the absolutely-positioned badges above can never be positioned outside it.
   See the note on `.p321-badge`. */
.p321-bundle-card__media {
	display: flex;
	gap: clamp(4px, 1.4vw, 12px);
	padding: clamp(12px, 3vw, 22px);
	max-width: 100%;
	min-width: 0;
	overflow: visible;
}

.p321-bundle-card__media-slot {
	flex: 1 1 0;
	min-width: 0;
	height: 100%;
	position: relative;
}

.p321-bundle-card__media-slot img {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	-o-object-fit: contain;
	   object-fit: contain;
	filter: drop-shadow(0 12px 18px rgba(8, 23, 28, 0.22));
}

.p321-bundle-card__items {
	list-style: none;
	padding: 0;
	margin: 0;
	display: flex;
	flex-direction: column;
	gap: 5px;
}

.p321-bundle-card__items li {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12.5px;
	color: var(--t2);
	display: flex;
	justify-content: space-between;
	gap: 10px;
	border-bottom: 1px dotted var(--hair);
	padding-bottom: 5px;
}

.p321-bundle-card__items li span:last-child {
	color: var(--t1);
	flex: none;
}

.p321-bundle-card__cta {
	width: 100%;
	margin-top: 8px;
}

/* Bundle PDP */

.p321-bundle-hero-media {
	position: relative;
	background: linear-gradient(170deg, #fff, #e2ebee);
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	aspect-ratio: 1 / 1;
	box-sizing: border-box;
	display: flex;
	align-items: center;
	justify-content: center;
	gap: clamp(6px, 2vw, 20px);
	padding: clamp(20px, 4vw, 42px);
	overflow: hidden;
}

.p321-bundle-hero-media__slot {
	flex: 1 1 0;
	min-width: 0;
	min-height: 0;
	align-self: stretch;
	display: grid;
	place-items: center;
}

/*
 * Owner, 2026-08-24: the photograph was not filling the box on an iPhone.
 *
 * The old rule sized the image with `max-width/max-height: 100%` and `width/height: auto`. Both
 * percentages resolve against the slot, whose own `height: 100%` resolves against a parent sized
 * only by `aspect-ratio`. Chrome treats that as a definite height and the image fills; Safari
 * resolves it to auto, `max-height: 100%` then means "no limit", and the image falls back to its
 * intrinsic size -- which is why it rendered around half the box on the device and full size in
 * headless Chrome. Measuring in one engine is what hid this.
 *
 * Explicit `width`/`height` with `object-fit: contain` needs no percentage-height resolution at
 * all, so both engines agree. `min-height: 0` on the slot stops a flex item refusing to shrink
 * below its content.
 */
.p321-bundle-hero-media__slot img {
	width: 100%;
	height: 100%;
	max-width: none;
	max-height: none;
	-o-object-fit: contain;
	   object-fit: contain;
	filter: drop-shadow(0 20px 28px rgba(8, 23, 28, 0.22));
}

.p321-bundle-line-items {
	display: grid;
	gap: 1px;
	background: var(--hair);
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	overflow: hidden;
}

.p321-bundle-line-items__row {
	background: var(--panel);
	padding: 14px clamp(14px, 3vw, 18px);
	display: flex;
	align-items: center;
	gap: 14px;
	flex-wrap: wrap;
}

.p321-bundle-line-items__img {
	width: 38px;
	height: 52px;
	-o-object-fit: contain;
	   object-fit: contain;
	flex: none;
}

.p321-bundle-line-items__body {
	min-width: 0;
	flex: 1;
}

.p321-bundle-line-items__name {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 106, "wght" 750;
	font-size: 16px;
	text-wrap: pretty;
}

.p321-bundle-line-items__detail {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	color: var(--t2);
	margin-top: 3px;
}

.p321-bundle-line-items__retail {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 14px;
	color: var(--t2);
	text-decoration: line-through;
	flex: none;
}

.p321-bundle-math {
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	background: var(--panel);
	overflow: hidden;
}

.p321-bundle-math__header {
	padding: 16px 20px;
	border-bottom: 1px solid var(--hair);
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 11.5px;
	letter-spacing: 0.16em;
	text-transform: uppercase;
	color: var(--t2);
}

.p321-bundle-math__body {
	padding: 6px 20px 14px;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 14px;
}

.p321-bundle-math__row {
	display: flex;
	justify-content: space-between;
	gap: 18px;
	padding: 11px 0;
	border-bottom: 1px solid var(--hair);
}

.p321-bundle-math__row span:first-child {
	color: var(--t2);
	min-width: 0;
}

.p321-bundle-math__row--save {
	border-bottom: 0;
	color: var(--brand-dk);
	font-weight: 600;
}

.p321-dark-panel__title {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 108, "wght" 780;
	font-size: 21px;
	color: #fff;
	line-height: 1.18;
	text-wrap: pretty;
	margin-top: 10px;
}

/* Certificate count is genuinely variable -- 1 to 3 depending on which components have a published
   COA for their exact vial size, and the section does not render at all below that (see
   single-product-bundle.php) -- so `auto-fit` is the right tool here, not a fixed per-breakpoint
   count. What auto-fit does not solve is a short row: one card alone resolves to the full ~1265px
   track at 1440 and strands its labels and values at opposite ends of a very wide definition list.
   Measured after the certificate gate landed: 8 of 20 bundles render exactly one card, 6 render two.

   Cap the *container*, not the track. Capping the track to a fixed max is the obvious move and it is
   wrong: at 1440 only two 420px columns then fit, so a three-vial bundle strands its third card on
   a row of its own -- exactly the auto-fit failure the CLAUDE.md rule exists to prevent. Capping the
   container leaves the three-card case untouched at its natural ~411px.

   `:has()` degrades to the previous full-width behaviour where unsupported, as with the other uses
   in this file. */
.p321-bundle-spec-grid {
	--p321-coa-col: 420px;
	--p321-coa-gap: 16px;

	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
	gap: var(--p321-coa-gap);
}

.p321-bundle-spec-grid:has(> .p321-coa-card:only-child) {
	max-width: var(--p321-coa-col);
}

.p321-bundle-spec-grid:has(> .p321-coa-card:nth-child(2):last-child) {
	max-width: calc(var(--p321-coa-col) * 2 + var(--p321-coa-gap));
}

/* Three cards is the one count auto-fit cannot resolve on its own. Between roughly 700px and
   1050px the 320px floor admits exactly two columns, so the third card strands on a row of its
   own with an empty half beside it -- measured at 1024 before this rule, and it is the failure
   the "explicit counts per breakpoint" convention exists to prevent. Give this case explicit
   counts instead, chosen so every row fills: 3-up once three columns clear ~300px, 1-up below,
   never 2-up. The 1-up band keeps the same container cap as a lone card, so stacking does not
   reintroduce the over-wide card this block set out to fix. */
.p321-bundle-spec-grid:has(> .p321-coa-card:nth-child(3)) {
	grid-template-columns: 1fr;
	max-width: var(--p321-coa-col);
}

@media (min-width: 1000px) {
	.p321-bundle-spec-grid:has(> .p321-coa-card:nth-child(3)) {
		grid-template-columns: repeat(3, minmax(0, 1fr));
		max-width: none;
	}
}

/* Bundle cards carry two corner badges (vial count + save amount) at once, unlike regular
   product cards -- the Shop grid's narrower per-card minimum (min(47%, 255px)) crowds them at
   phone widths. Matches Bundles.dc.html's own grid, which uses a taller per-card floor. */
/*
 * No column override any more -- bundle cards use .p321-product-grid's track sizing so the
 * homepage's bundle rail lays out identically to Best Sellers directly above it (owner,
 * 2026-08-19: "It need to match the best sellers section"). The old 300px minimum capped the
 * homepage rail at three columns where Best Sellers fits four.
 */

/* Milestone D: announcement bar. Not in the design -- built to the ink/brand-lt dark-strip
   pairing already used throughout the site's dark bands. */

.p321-announcement-bar {
	background: var(--ink);
	color: var(--brand-lt);
	text-align: center;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 13px;
	letter-spacing: 0.04em;
	padding: 10px 16px;
}

.p321-announcement-bar a {
	color: inherit;
	text-decoration: underline;
	text-underline-offset: 3px;
}

.p321-announcement-bar a:hover {
	color: #fff;
}

/* Milestone D: header search -- expands a real GET form under the search icon, no design spec
   for the interaction so kept to the simplest working pattern (same class-toggle style as the
   mobile menu / filter sheet, not a new mechanism). */

.p321-header-search {
	position: relative;
}

.p321-header-search__form {
	position: absolute;
	top: calc(100% + 8px);
	right: 0;
	z-index: 70;
	display: none;
	width: min(88vw, 340px);
}

.p321-header-search__form[data-open="true"] {
	display: block;
}

.p321-header-search__form input {
	width: 100%;
	min-height: 50px;
	border: 1.5px solid var(--hair);
	background: var(--panel);
	border-radius: var(--rb);
	padding: 0 20px;
	font-family: "Instrument Sans", "Instrument Sans Fallback", sans-serif;
	font-size: 15px;
	color: var(--t1);
	box-shadow: 0 12px 28px rgba(8, 23, 28, 0.14);
}

.p321-header-search__form input:focus-visible {
	outline: 2px solid var(--brand);
	outline-offset: 2px;
}

/* Milestone D: My Account. Not in the design -- built to docs/02-DESIGN-SYSTEM.md tokens. */

.p321-account-shell {
	display: grid;
	grid-template-columns: 240px 1fr;
	gap: clamp(24px, 4vw, 40px);
	align-items: start;
}

@media (max-width: 895px) {
	.p321-account-shell {
		grid-template-columns: 1fr;
	}
}

/* WooCommerce's own .woocommerce-account .woocommerce-MyAccount-navigation rule
   (float: left; width: 30%) is a two-class compound selector that outranks a plain
   .p321-account-nav rule regardless of source order -- confirmed via CSS.getMatchedStylesForNode,
   not guessed. Match its exact selector prefix to win on specificity, same pattern already used
   elsewhere in this theme to override WooCommerce defaults. */
.woocommerce-account .woocommerce-MyAccount-navigation.p321-account-nav {
	float: none;
	width: 100%;
	position: sticky;
	top: 84px;
}

.p321-account-content {
	min-width: 0;
}

.p321-account-content h2 {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 106, "wght" 780;
	font-size: 20px;
	text-transform: uppercase;
	margin: 0 0 16px;
}

/* Order list (Orders endpoint) */

.p321-order-list {
	display: flex;
	flex-direction: column;
	gap: 1px;
	background: var(--hair);
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	overflow: hidden;
}

.p321-order-row {
	background: var(--panel);
	padding: var(--card-pad);
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: 16px;
	flex-wrap: wrap;
}

.p321-order-row__main {
	display: flex;
	align-items: center;
	gap: 16px;
	flex-wrap: wrap;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 13.5px;
}

.p321-order-row__number {
	font-weight: 600;
	color: var(--t1);
}

.p321-order-row__date,
.p321-order-row__total {
	color: var(--t2);
}

.p321-order-row__status {
	color: var(--brand-dk);
	font-weight: 500;
}

.p321-order-row__actions {
	display: flex;
	gap: 10px;
	flex-wrap: wrap;
}

.p321-order-row__actions .p321-btn,
.p321-order-row__actions .p321-btn--outline {
	min-height: 42px;
	padding: 0 18px;
	font-size: 13.5px;
}

/* Native WooCommerce forms this milestone restyles without a template override (address book,
   account details, login/register) -- reskinning WooCommerce's own tested markup/class names
   rather than rebuilding it, same discipline as the shipping-method table and variation form in
   Milestones A/B. */

.woocommerce-address-fields__field-wrapper,
.woocommerce-EditAccountForm {
	display: flex;
	flex-direction: column;
	gap: 14px;
}

.form-row,
.woocommerce-form-row {
	display: flex;
	flex-direction: column;
	gap: 6px;
}

.form-row label {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--t2);
}

.form-row .woocommerce-Input,
.form-row select {
	min-height: 52px;
	border: 1.5px solid var(--hair);
	border-radius: var(--ri);
	padding: 0 16px;
	font-family: "Instrument Sans", "Instrument Sans Fallback", sans-serif;
	/* 16px, not 15.5: under 16 iOS zooms the page on focus. */
	font-size: 16px;
	color: var(--t1);
	background: var(--panel);
}

.form-row .woocommerce-Input:focus-visible,
.form-row select:focus-visible {
	outline: 2px solid var(--brand);
	outline-offset: 2px;
}

.woocommerce-EditAccountForm fieldset {
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	padding: var(--card-pad);
	margin: 0;
}

.woocommerce-EditAccountForm legend {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--t2);
	padding: 0 6px;
}

.woocommerce-Address {
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	background: var(--panel);
	padding: var(--card-pad);
	max-width: 480px;
}

.woocommerce-Address-title {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	gap: 14px;
	margin-bottom: 12px;
}

.woocommerce-Address-title h2 {
	margin: 0;
}

.woocommerce-Address address {
	font-style: normal;
	color: var(--t2);
	line-height: 1.6;
}

#customer_login {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
	gap: clamp(24px, 4vw, 40px);
}

#customer_login .u-column1,
#customer_login .u-column2 {
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	background: var(--panel);
	padding: var(--card-pad);
}

/* Native order-details table (fired by the woocommerce_view_order hook, view-order.php) */

.woocommerce-order-details {
	margin-top: 22px;
}

.woocommerce-order-details__title {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 106, "wght" 780;
	font-size: 20px;
	text-transform: uppercase;
	margin: 0 0 16px;
}

table.shop_table {
	width: 100%;
	border-collapse: collapse;
	font-size: 14px;
}

table.shop_table th,
table.shop_table td {
	padding: 14px 16px;
	border-bottom: 1px solid var(--hair);
	text-align: left;
}

table.shop_table tfoot th,
table.shop_table tfoot td {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
}

/* Wishlist heart toggle -- absolutely positioned on grid cards, inline on PDPs. */

.p321-product-card {
	position: relative;
	container-type: inline-size;
}

.p321-wishlist-toggle {
	position: absolute;
	top: 12px;
	right: 12px;
	z-index: 3;
}

/*
 * On the product card the heart moves out of the media's top-right -- where the design puts the
 * "Save $X" badge -- down to the bottom-right of the same square, clear of the lot badge, the
 * image (inset 10%) and every line of detail below.
 *
 * The offset is measured from the top because the card, not the media, is the positioning context
 * (the form has to stay outside the anchor -- a <form> inside an <a> is invalid and would swallow
 * the click). The media is `aspect-ratio: 1/1` at full card width, so its height is exactly one
 * container-inline-size, and 100cqw - 50px lands the 38px control 12px above its bottom edge.
 */
.p321-product-card .p321-wishlist-toggle {
	top: calc(100cqw - 50px);
}

/*
 * The button box is 44; the drawn disc stays 38.
 *
 * Target size is the pointer area, not the ink, so the control should not grow to 44 visually --
 * on a 390px screen that would be a disc a quarter of the card's width. The first attempt kept the
 * box at 38 and pushed the hit area out with `inset: -3px`, which failed: `elementFromPoint` two
 * pixels past the right and bottom edges returned the card, not the button, because an ancestor
 * clips it there. Growing the box and insetting the disc has no such dependency.
 *
 * The disc is a pseudo-element, so the icon needs `position: relative` to paint above it -- a
 * positioned ::before otherwise renders over non-positioned children.
 */
.p321-wishlist-toggle__btn {
	position: relative;
	width: 44px;
	height: 44px;
	border: 0;
	border-radius: 50%;
	background: transparent;
	color: var(--t1);
	display: grid;
	place-items: center;
	cursor: pointer;
}

.p321-wishlist-toggle__btn::before {
	content: "";
	position: absolute;
	inset: 3px;
	border-radius: 50%;
	border: 1px solid var(--hair);
	background: rgba(255, 255, 255, 0.9);
}

.p321-wishlist-toggle__btn > * {
	position: relative;
}

.p321-wishlist-toggle__btn:hover {
	color: var(--brand-dk);
}

.p321-wishlist-toggle__btn:hover::before {
	border-color: var(--brand);
}

.p321-wishlist-toggle__btn[aria-pressed="true"] {
	color: #e0405a;
}

.p321-wishlist-toggle__btn[aria-pressed="true"]::before {
	border-color: #e0405a;
}

/* `.p321-buy-form--with-wishlist` and its heart rules were removed 2026-08-24: the PDP heart moved
   onto the photograph, so the buy row no longer contains one. */

/*
 * The design's buy row: quantity stepper, then an Add To Cart button that takes the rest of the
 * width, then the wishlist heart. WooCommerce stacks its bare number input above a
 * content-width button instead, which is what shipped.
 *
 * `.cart` is Woo's own form (simple products); `.woocommerce-variation-add-to-cart` is the
 * equivalent inside the variation wrapper, so both product types lay out identically.
 */
.p321-buy-form form.cart,
.p321-buy-form .woocommerce-variation-add-to-cart {
	display: flex;
	align-items: center;
	gap: 12px;
	flex: 1;
	flex-wrap: wrap;
}

.p321-buy-form .single_add_to_cart_button {
	flex: 1;
	min-width: 200px;
	min-height: 56px;
	justify-content: center;
}

/* Phase 4: certificate infographic (render_coa_infographic(), single-coa.php, PDP batch
   selector). Extends .p321-coa-card rather than a new wrapper class. */

.p321-coa-infographic {
	display: flex;
	flex-direction: column;
	gap: 18px;
}

/*
 * Three equal columns suit three short figures. A blend's net content is a four-line component
 * list, and forcing it into a third of the panel squeezed the reported date until it broke across
 * two lines. `auto` lets the net column take the width its longest component needs and leaves the
 * rest to purity and the date, which are single tokens that must not wrap.
 */
.p321-coa-infographic__stats {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 14px;
}

/*
 * Wraps rather than holding three columns, because the constraint here is the *panel's* width and
 * that does not track the viewport's.
 *
 * Three `auto` columns whose values and labels are deliberately `nowrap` (see below) need about
 * 542px. The homepage certificate panel is a flex item beside the batch selector, so it is ~496px
 * at a 1100px viewport -- and the third stat spilled 51px past the right edge of the *page*, since
 * nothing in the chain clips it. It came back at 900 because the lookup stacks there and hands the
 * panel the full width again, so the failure is a window around 1000-1200 rather than "narrow".
 * That is exactly what a viewport media query cannot express, which is why the existing
 * `max-width: 600px` stacking rule never caught it.
 *
 * Wrapping needs no breakpoint and no knowledge of the panel's width: the stats sit in a row while
 * they fit and one drops to a second line when they do not, at any width, in either layout.
 */
.p321-coa-infographic__stats--has-list {
	display: flex;
	flex-wrap: wrap;
	justify-content: space-between;
	gap: clamp(14px, 2vw, 30px);
}

/* `auto` columns still shrink below their content, and the base rule's `overflow-wrap: anywhere`
   then breaks a date mid-token ("2026-06-" / "01"). Purity and the reported date are single tokens
   with nothing to gain from wrapping, so they hold their width and the component list -- which
   wraps between components perfectly well -- gives way instead. */
.p321-coa-infographic__stats--has-list .p321-coa-infographic__stat-value:not(.p321-coa-infographic__stat-value--list) {
	white-space: nowrap;
	overflow-wrap: normal;
}

/* Same reason as the values: "Purity (HPLC-UV)" is what sets that column's width, and letting it
   fold onto a second line while its neighbours stay single-line looks like a mistake. */
.p321-coa-infographic__stats--has-list .p321-coa-infographic__stat-label {
	white-space: nowrap;
}

/* Three side-by-side stats plus a component list does not fit a phone panel; stack them there.
   Wrapping already prevents overflow -- this is about not leaving two stranded stats on one line
   and the list on another. */
@media (max-width: 600px) {
	.p321-coa-infographic__stats--has-list {
		flex-direction: column;
		gap: 16px;
	}
}

.p321-coa-infographic__stat-label {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 11px;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--od4);
	margin-bottom: 6px;
}

.p321-coa-infographic__stat-value {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 110, "wght" 800;
	font-size: clamp(18px, 2.4vw, 26px);
	color: #fff;
	overflow-wrap: anywhere;
}

/* A blend reports a net content per component. Stacked and stepped down to mono body size, so four
   of them sit in the space one figure would and the three stats keep their shared baseline. */
.p321-coa-infographic__stat-value--list {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-variation-settings: normal;
	font-size: 13px;
	font-weight: 500;
	line-height: 1.5;
}

.p321-coa-infographic__stat-value--list span {
	display: block;
	white-space: nowrap;
}

.p321-coa-infographic__pills {
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
}

.p321-pill {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.06em;
	color: var(--brand-lt);
	border: 1px solid var(--hair-dk);
	border-radius: var(--rb);
	padding: 6px 12px;
}

/*
 * `position: relative` is load-bearing, not decoration. The results table's last column header
 * holds a visually-hidden `.screen-reader-text` span, which is `position: absolute` and therefore
 * laid out against its nearest *positioned* ancestor. With this wrapper static, that ancestor was
 * the page, so the span sat at its static position ~503px in -- outside the wrapper's clip -- and
 * dragged the whole document's scroll width past the viewport at 390px. Positioning the wrapper
 * makes it the span's containing block, so `overflow-x` clips it like everything else.
 *
 * This only started biting when real certificates arrived: before that no COA had result rows, so
 * the table never rendered and the escaping span had nothing to escape from.
 */
.p321-coa-infographic__results-wrap {
	position: relative;
	overflow-x: auto;
	-webkit-overflow-scrolling: touch;
}

.p321-coa-infographic__results {
	width: 100%;
	min-width: 480px;
	border-collapse: collapse;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 13px;
}

.p321-coa-infographic__results th,
.p321-coa-infographic__results td {
	text-align: left;
	padding: 10px 12px;
	border-bottom: 1px solid var(--hair-dk);
	color: #fff;
}

.p321-coa-infographic__results th {
	color: var(--od4);
	font-weight: 500;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	font-size: 11px;
}

.p321-coa-infographic__pass-badge {
	display: inline-block;
	font-size: 11px;
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	padding: 3px 9px;
	border-radius: var(--rb);
}

.p321-coa-infographic__results tr.is-pass .p321-coa-infographic__pass-badge {
	background: rgba(2, 174, 203, 0.16);
	color: var(--brand-lt);
}

.p321-coa-infographic__results tr.is-fail .p321-coa-infographic__pass-badge {
	background: rgba(224, 64, 90, 0.16);
	color: #ff8fa0;
}

.p321-coa-infographic__graphs {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
	gap: 12px;
}

.p321-coa-infographic__graph {
	border: 1px solid var(--hair-dk);
	border-radius: var(--ri);
	padding: 12px;
}

.p321-coa-infographic__graph summary {
	display: flex;
	align-items: center;
	min-height: 44px;
	gap: 10px;
	cursor: pointer;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12.5px;
	color: var(--brand-lt);
	list-style: none;
}

.p321-coa-infographic__graph summary::-webkit-details-marker {
	display: none;
}

.p321-coa-infographic__graph summary img {
	width: 40px;
	height: 40px;
	-o-object-fit: cover;
	   object-fit: cover;
	border-radius: 6px;
	flex: none;
}

.p321-coa-infographic__graph[open] summary {
	margin-bottom: 12px;
}

.p321-coa-infographic__graph > img {
	width: 100%;
	height: auto;
	border-radius: 8px;
}

/* Phase 4: COA library archive -- reuses .p321-chip-row/.p321-search-field/.p321-hero-stats/
   .p321-result-line/.p321-empty-state exactly as the Shop and Bundles index already do. */

.p321-coa-table-wrap {
	overflow-x: auto;
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	background: var(--panel);
	-webkit-overflow-scrolling: touch;
}

.p321-coa-table {
	width: 100%;
	border-collapse: collapse;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 14px;
	min-width: 820px;
}

.p321-coa-table th {
	text-align: left;
	padding: 15px 20px;
	font-weight: 500;
	letter-spacing: 0.08em;
	font-size: 12px;
	text-transform: uppercase;
	background: var(--ink);
	color: var(--brand-lt);
}

.p321-coa-table th:last-child,
.p321-coa-table td:last-child,
.p321-coa-table th:nth-child(4),
.p321-coa-table td:nth-child(4),
.p321-coa-table th:nth-child(5),
.p321-coa-table td:nth-child(5) {
	text-align: right;
}

.p321-coa-table td {
	padding: 15px 20px;
	border-top: 1px solid var(--hair);
}

.p321-coa-table__product {
	color: var(--t1);
	font-weight: 600;
	font-family: "Instrument Sans", "Instrument Sans Fallback", sans-serif;
}

/*
 * Superseded marker, used both in the COA library table and in the PDP's lot history. A quiet
 * outline rather than an alert treatment: a superseded certificate is still a real, valid record of
 * the batch it describes, it is simply no longer the current one.
 */
.p321-coa-table__superseded {
	display: inline-block;
	margin-left: 8px;
	padding: 2px 8px;
	border: 1px solid var(--hair);
	border-radius: var(--rb);
	color: var(--t2);
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 10.5px;
	font-weight: 500;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	white-space: nowrap;
	vertical-align: middle;
}

.p321-coa-superseded-notice {
	max-width: 62ch;
	padding: 14px 18px;
	border-left: 3px solid var(--hair);
	background: var(--panel);
	border-radius: var(--ri);
	color: var(--t2);
	font-size: 15px;
	line-height: 1.6;
}

/* Underlined, not just brand-coloured: the link sits inside a paragraph, so colour alone is not a
   distinguishable cue (axe: link-in-text-block). `--brand-dk` is the on-light variant -- this notice
   sits on a white panel, where `--brand-lt` fails contrast. */
.p321-coa-superseded-notice a {
	color: var(--brand-dk);
	text-decoration: underline;
	text-underline-offset: 2px;
}

.p321-coa-superseded-notice strong {
	color: var(--t1);
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 11.5px;
	letter-spacing: 0.12em;
	text-transform: uppercase;
	margin-right: 6px;
}

/*
 * Lot codes and dates are single tokens and must never wrap. They used to fit on their own, but the
 * blend certificates put a full per-component breakdown in the Net column ("GHK-Cu - 55.24 mg, KPV
 * - 10.78 mg, ...") which is wide enough to squeeze the neighbouring columns until "2026-06-01"
 * broke across two lines. The Net column takes the wrapping instead -- it is the one value here
 * that is genuinely a list.
 */
.p321-coa-table__date,
.p321-coa-table__lot {
	white-space: nowrap;
}

.p321-coa-table__date {
	color: var(--t2);
}

.p321-coa-table__purity {
	color: var(--brand-dk);
	font-weight: 500;
}

.p321-coa-table__net {
	color: var(--t2);
	min-width: 22ch;
}

.p321-coa-table__pdf-btn {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	min-height: 44px;
	padding: 0 14px;
	border: 1px solid var(--brand);
	border-radius: var(--rb);
	color: var(--brand-dk);
	font-size: 13px;
	letter-spacing: 0.06em;
	text-transform: uppercase;
}

.p321-coa-table__pdf-btn:hover {
	background: var(--brand);
	color: #04252c;
}

.p321-coa-table__pending {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	min-height: 44px;
	padding: 0 14px;
	border: 1px dashed var(--hair);
	border-radius: var(--rb);
	color: var(--t2);
	font-size: 13px;
	letter-spacing: 0.06em;
	text-transform: uppercase;
}

/* Homepage (front-page.php) */

.p321-btn--outline-dark {
	background: transparent;
	color: #fff;
	border: 2px solid var(--hair-dk);
	box-shadow: none;
	font-weight: 600;
}

.p321-btn--outline-dark:hover {
	border-color: var(--brand-lt);
	color: var(--brand-lt);
}

.p321-hero {
	position: relative;
	overflow: hidden;
	min-height: clamp(520px, 62vh, 700px);
	display: flex;
	align-items: center;
}

/*
 * `width: 100%` is the load-bearing line. .p321-hero is a flex container, so without it the
 * .container child sizes to its content (692px) and `margin: 0 auto` then centres that, which
 * pushed the whole hero into the middle of the screen. The design has it hard left against the
 * container edge.
 */
.p321-hero__inner {
	position: relative;
	z-index: 2;
	width: 100%;
	padding-top: 56px;
	padding-bottom: 56px;
}

/* Hero background photograph and the two overlays that keep the headline readable over it. */

.p321-hero__bg {
	position: absolute;
	inset: 0;
}

.p321-hero__bg img {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	-o-object-fit: cover;
	   object-fit: cover;
	-o-object-position: 72% 55%;
	   object-position: 72% 55%;
	filter: grayscale(1) contrast(1.05) brightness(0.85);
}

/* Pulls the photograph towards the brand hue instead of leaving it neutral grey. */
.p321-hero__wash {
	position: absolute;
	inset: 0;
	background: linear-gradient(155deg, var(--ink), var(--brand-dk) 150%);
	mix-blend-mode: color;
	opacity: 0.8;
}

/* Darkest at the left, where the headline sits; lets the picture through on the right. */
.p321-hero__scrim {
	position: absolute;
	inset: 0;
	background:
		linear-gradient(90deg, rgba(8, 23, 28, 0.96) 0%, rgba(8, 23, 28, 0.88) 30%, rgba(8, 23, 28, 0.55) 56%, rgba(8, 23, 28, 0.3) 78%, rgba(8, 23, 28, 0.55) 100%),
		linear-gradient(180deg, rgba(8, 23, 28, 0.45), transparent 26%, transparent 70%, rgba(8, 23, 28, 0.55));
}

/*
 * The scrim above is built for the wide layout, where the copy occupies the left of the hero and
 * the gradient thins to 0.3 at 78% on purpose so the vials show through on the right.
 *
 * Below the two-column width there is no "right side" -- the copy spans the full container, so the
 * back half of every line lands in the zone deliberately made transparent. At 390 the paragraph
 * runs x=16..374 and the thin zone starts at 218px.
 *
 * Measured 2026-09-03 against the worst-case background pixel behind that paragraph, sampled with
 * the text hidden so glyph antialiasing could not pollute the reading:
 *
 *              old scrim      this scrim
 *   --od2       2.33:1          8.39:1
 *   white       4.08:1         14.69:1
 *
 * Two things follow. The old state was genuinely unreadable, and **recolouring alone would not have
 * fixed it** -- white on the old scrim is 4.08:1, still under the 4.5:1 floor. The scrim is the
 * load-bearing half of this fix; the white is what the owner asked for and buys the rest.
 *
 * The photograph is still visible below 1024px, just never at the expense of the copy over it.
 */
@media (max-width: 1023px) {
	.p321-hero__scrim {
		background:
			linear-gradient(90deg, rgba(8, 23, 28, 0.95) 0%, rgba(8, 23, 28, 0.91) 55%, rgba(8, 23, 28, 0.86) 100%),
			linear-gradient(180deg, rgba(8, 23, 28, 0.5), transparent 30%, transparent 64%, rgba(8, 23, 28, 0.6));
	}
}

/*
 * Hero copy is --od1, not the --od2 the rest of the dark bands use. --od2 is the right secondary
 * on a flat --ink band; this one sits on a photograph, and it is the only body copy on the site
 * that does. Owner's call 2026-09-03 after finding it hard to read on a phone.
 */
.p321-hero .body-copy {
	color: var(--od1);
}

/*
 * About page -- hero backdrop, vial line-up card. Design: docs/design/321 Peptides About.dc.html.
 * Built as classes rather than the mockup's inline styles per CLAUDE.md rule 2 (recreate the
 * design, never port the .dc.html runtime) and the no-hard-coded-hex convention.
 *
 * The About hero is block content, not a PHP template, so it cannot reuse .p321-hero directly --
 * that one is a flex container sized by front-page.php's own markup. Same technique, own class.
 */
.p321-about-hero {
	position: relative;
	overflow: hidden;
	/* The band carries no padding of its own, so without this the eyebrow starts flush against the
	   header. The design gives the hero noticeably more room than a standard section band. */
	padding-block: clamp(64px, 9vw, 110px);
}

/* Core wraps an image block in <figure>; this is that figure, pinned behind the content. */
.p321-about-hero__bg {
	position: absolute;
	inset: 0;
	margin: 0;
}

.p321-about-hero__bg img {
	width: 100%;
	height: 100%;
	-o-object-fit: cover;
	   object-fit: cover;
	opacity: 0.9;
}

/* Scrim: dark enough on the left for the headline, lets the photograph through on the right --
   the same reading-order logic as the homepage hero, over a much softer source image. */
.p321-about-hero::after {
	content: "";
	position: absolute;
	inset: 0;
	background:
		linear-gradient(150deg, rgba(8, 23, 28, 0.94) 0%, rgba(8, 23, 28, 0.78) 45%, rgba(8, 23, 28, 0.62) 100%),
		linear-gradient(180deg, rgba(8, 23, 28, 0.5), transparent 30%, transparent 68%, rgba(8, 23, 28, 0.6));
	pointer-events: none;
}

/* Everything except the backdrop sits above both the image and the scrim. */
.p321-about-hero > *:not(.p321-about-hero__bg) {
	position: relative;
	z-index: 1;
}

/*
 * The hero's sub-line. Deliberately NOT .lede: the design sets it smaller (16-22px against the
 * lede's 21-28px), uppercase, and in brand-light. It was marked up as .lede, which hard-sets
 * `color: var(--t1)` -- near-black -- and so rendered invisible on the near-black band.
 */
.p321-about-hero__sub {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 104, "wght" 600;
	font-size: clamp(16px, 1.9vw, 22px);
	line-height: 1.3;
	letter-spacing: 0.01em;
	text-transform: uppercase;
	color: var(--brand-lt);
	max-width: 34ch;
	margin: 20px 0 0;
}

/* With the proof stats lifted out into their own band, the hero's text column is much shorter than
   the vial card beside it. Centring the two stops the copy sitting in the top corner of a tall
   band with dead space beneath it. */
.p321-about-hero .wp-block-columns {
	align-items: center;
}

/*
 * The proof band under the hero -- the same four-across treatment the homepage uses.
 *
 * A grid rather than the block's default flex wrap, because these labels are full sentences and
 * these values are words rather than the homepage's short tokens. Under flex the columns sized to
 * their content and the four cells came out visibly uneven. `margin-top: auto` on the label pins
 * every label to a shared baseline, so a value that wraps to two lines no longer pushes its own
 * label out of line with its neighbours'.
 */
/* No bottom rule: the band is white against the page's off-white, and that colour change is
   already the section break. The hairline on top of it read as a second, redundant line. */
.p321-about-proof {
	background: var(--panel);
	padding-block: clamp(26px, 3.4vw, 40px);
}

/*
 * `.p321-stat-row.band-light` is shared with the homepage, where it is a standalone full-bleed
 * band that supplies its own content column and closes with a hairline. Inside the About proof
 * band neither applies: the section group already supplies the column, so its padding was being
 * added a second time and pushed the stats to 85px while every heading on the page sat at 60; and
 * its hairline was the stray line directly above the section break. The band is white against the
 * page's off-white, and that colour change is the break.
 */
.p321-about-proof .p321-stat-row {
	display: grid;
	grid-template-columns: repeat(4, 1fr);
	gap: 0;
	padding-inline: 0;
	border-bottom: 0;
}

.p321-about-proof .p321-stat {
	display: flex;
	flex-direction: column;
	padding-inline: clamp(14px, 2.2vw, 32px);
	border-left: 1px solid var(--hair);
}

.p321-about-proof .p321-stat:first-child {
	border-left: 0;
	padding-left: 0;
}

.p321-about-proof .p321-stat__value {
	font-size: clamp(21px, 2.2vw, 30px);
	color: var(--brand-dk);
	text-wrap: balance;
}

.p321-about-proof .p321-stat__label {
	margin-top: auto;
	padding-top: 12px;
}

@media (max-width: 895px) {
	.p321-about-proof .p321-stat-row {
		grid-template-columns: repeat(2, 1fr);
		row-gap: clamp(22px, 4vw, 32px);
	}

	.p321-about-proof .p321-stat:nth-child(odd) {
		border-left: 0;
		padding-left: 0;
	}
}

@media (max-width: 560px) {
	.p321-about-proof .p321-stat-row {
		grid-template-columns: 1fr;
	}

	.p321-about-proof .p321-stat {
		border-left: 0;
		padding-left: 0;
	}
}

/* The sub-headline sits close under its title by design; the copy that follows needs air. */
.p321-sec-sub + * {
	margin-top: clamp(22px, 3vw, 34px);
}

/*
 * The same latent bug, fixed generally: `.lede` sets an explicit near-black, which beats the light
 * colour `.band-dark` sets on its children. Any lede on any dark band was invisible, not just this
 * one. Scoped to .band-dark so light-band ledes are untouched.
 */
.band-dark .lede {
	color: var(--page);
}

/*
 * Vial line-up card -- the light panel of product shots in the hero and in the commitment section.
 * The vials sit on a shared baseline at staggered heights (78/94/100/94/78), which is what gives
 * the design's shallow arc with the tallest vial centred.
 */
.p321-vial-card {
	background: linear-gradient(170deg, rgba(255, 255, 255, 0.97), rgba(226, 235, 238, 0.93));
	border: 1px solid rgba(255, 255, 255, 0.5);
	border-radius: var(--rc);
	padding: clamp(22px, 3vw, 34px);
	display: flex;
	flex-direction: column;
	gap: 18px;
	box-shadow: 0 30px 70px rgba(0, 0, 0, 0.35);
}

.p321-vial-card__row {
	display: flex;
	align-items: flex-end;
	justify-content: center;
	gap: clamp(3px, 1vw, 12px);
	height: clamp(190px, 26vw, 280px);
	width: 100%;
}

.p321-vial-card__slot {
	position: relative;
	flex: 1 1 0;
	min-width: 0;
	align-self: flex-end;
	display: grid;
	place-items: end center;
	margin: 0;
	height: 100%;
}

/*
 * Fit to the slot box, not to the slot's width.
 *
 * This was `max-width/max-height: 100%` on an auto-sized image, and the max-height never bound:
 * the width cap set the size and the 307x700 aspect ratio decided the height. Five narrow slots in
 * the hero happened to come out shorter than their slot, so it looked correct; three wide slots in
 * the closing card resolved to 412px inside a 210px slot and the vials hung out of the card and
 * over the footer. Sizing the box and letting `object-fit` letterbox inside it cannot overflow at
 * any slot width or count.
 *
 * `object-position: bottom` keeps every vial on the shared baseline, which is what makes the
 * staggered `is-hNN` heights read as an arc. drop-shadow still follows the vial's own silhouette,
 * because the letterboxed area is transparent.
 *
 * Absolutely positioned rather than a grid item with `height: 100%`: as a grid item the percentage
 * height did not resolve against the grid area at all (measured 412px against a 210px slot, the
 * aspect ratio winning as if height were auto), while `width: 100%` resolved fine. Out of flow,
 * `inset: 0` makes the slot's padding box the containing block, so the box is definite in both
 * axes and there is nothing left to resolve. The slot carries its own height, so it does not
 * collapse without an in-flow child.
 */
.p321-vial-card__slot img {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	-o-object-fit: contain;
	   object-fit: contain;
	-o-object-position: bottom center;
	   object-position: bottom center;
	filter: drop-shadow(0 18px 26px rgba(8, 23, 28, 0.4));
}

.p321-vial-card__slot.is-h78 { height: 78%; }
.p321-vial-card__slot.is-h84 { height: 84%; }
.p321-vial-card__slot.is-h94 { height: 94%; }
.p321-vial-card__slot.is-h100 { height: 100%; }


.p321-vial-card__footer {
	border-top: 1px solid var(--hair);
	padding-top: 16px;
	display: flex;
	justify-content: space-between;
	gap: 14px;
	flex-wrap: wrap;
}

.p321-vial-card__footer p {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--t2);
	margin: 0;
}

.p321-vial-card__footer p.is-brand {
	color: var(--brand-dk);
}

/*
 * Supply-chain route comparison -- the About page's signature block.
 *
 * The three plain cards this replaces stated the two routes as bulleted lists, which made an
 * argument about *provenance* look like an argument about feature counts. Provenance is a
 * sequence, so it is drawn as one: a connector runs down each column and every hand the product
 * passes through is a node on it. The unverified column's connector is dashed and its nodes are
 * hollow -- the line literally does not join up. Ours is solid and filled.
 *
 * This is the one place on the page allowed to be loud; everything around it stays quiet.
 *
 * The distinction is never carried by colour alone: each route has a text label, each step has a
 * written consequence, and the hollow/filled node differs in shape as well as hue.
 */
.p321-routes {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: clamp(18px, 2.6vw, 34px);
	margin-top: clamp(30px, 4vw, 46px);
}

.p321-route {
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	background: var(--panel);
	padding: clamp(20px, 2.6vw, 32px);
}

.p321-route--verified {
	border-color: var(--brand);
	box-shadow: 0 18px 44px rgba(2, 174, 203, 0.1);
}

.p321-route__label {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--t2);
	margin: 0;
}

.p321-route--verified .p321-route__label {
	color: var(--brand-dk);
}

.p321-route__summary {
	font-size: 15.5px;
	line-height: 1.55;
	color: var(--t2);
	margin: 10px 0 0;
	max-width: 42ch;
}

.p321-route__steps {
	list-style: none;
	margin: clamp(18px, 2.2vw, 26px) 0 0;
	padding: 0;
}

/* Each step owns the connector segment above it, so the line ends with the last node rather than
   running off into the padding. */
.p321-route__step {
	position: relative;
	padding: 0 0 22px 30px;
}

.p321-route__step:last-child {
	padding-bottom: 0;
}

.p321-route__step::before {
	content: "";
	position: absolute;
	left: 0;
	top: 4px;
	width: 13px;
	height: 13px;
	border-radius: 50%;
	border: 2px solid var(--hair);
	background: var(--panel);
	box-sizing: border-box;
}

.p321-route__step:not(:last-child)::after {
	content: "";
	position: absolute;
	left: 6px;
	top: 19px;
	bottom: 2px;
	width: 0;
	border-left: 2px dashed var(--hair);
}

.p321-route--verified .p321-route__step::before {
	border-color: var(--brand);
	background: var(--brand);
}

.p321-route--verified .p321-route__step:not(:last-child)::after {
	border-left-style: solid;
	border-left-color: var(--brand);
}

/* The independent-test step is the whole argument, so it gets a ring the other nodes don't. */
.p321-route--verified .p321-route__step--tested::before {
	box-shadow: 0 0 0 4px rgba(2, 174, 203, 0.22);
}

.p321-route__step--end .p321-route__step-name {
	color: var(--t1);
}

.p321-route__step-name {
	display: block;
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 106, "wght" 700;
	font-size: 16px;
	line-height: 1.3;
	color: var(--t1);
}

.p321-route__step-note {
	display: block;
	font-size: 14.5px;
	line-height: 1.6;
	color: var(--t2);
	margin-top: 4px;
	max-width: 46ch;
}

/*
 * The outcome strip is a conclusion drawn from both routes, not a third peer. Squeezed into a
 * third column it left a large empty block; full width under the comparison it reads as the
 * summary it is, and its list can breathe into however many columns fit.
 */
.p321-route-outcome {
	margin-top: clamp(18px, 2.6vw, 34px);
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	background: var(--band);
	padding: clamp(20px, 2.6vw, 32px);
}

.p321-route-outcome__label {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--brand-dk);
	margin: 0 0 16px;
}

.p321-route-outcome__list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
	gap: 10px clamp(18px, 2.4vw, 32px);
}

.p321-route-outcome__list li {
	position: relative;
	padding-left: 30px;
	font-size: 15.5px;
	line-height: 1.55;
	color: var(--t2);
}

@media (max-width: 860px) {
	.p321-routes {
		grid-template-columns: 1fr;
	}
}

/*
 * Shared left edge for the seeded block sections.
 *
 * WordPress's constrained layout gives every direct child of a group `margin-inline: auto`, and
 * `.body-copy` carries a 66ch measure -- so each paragraph was centred inside a 1320px container
 * while the heading, table or card grid beside it started at the gutter. Measured: copy at 346px
 * against a heading at 60px, in every section on the page. Nothing shared a left edge, which is
 * what made the page read as unfinished more than any single section did.
 *
 * The measure is what should cap line length; the centring was never wanted. Scoped to direct
 * children of a block group, so the PHP templates that use `.body-copy` in their own markup are
 * untouched.
 *
 * `!important` is required, not laziness: core emits the constrained-layout rule as
 * `margin-left: auto !important; margin-right: auto !important` on
 * `.wp-container-core-group-is-layout-N > :where(:not(.alignleft):not(.alignright):not(.alignfull))`.
 * No amount of specificity beats it, and the alternative -- dropping the measure off `.body-copy`
 * and re-adding it on a wrapper in every section -- would put layout back into the seeded content
 * the owner is meant to be able to edit.
 *
 * This works because the section groups now carry the content column as padding (below), so a
 * child's containing block already *is* the column -- zero is the column's left edge, not the
 * viewport's.
 */
.wp-block-group > .body-copy,
.wp-block-group > .p321-callout,
.wp-block-group > .p321-statement {
	margin-inline: 0 !important;
}

/*
 * The seeded sections had no side gutter at all.
 *
 * Every group here is full-bleed -- `alignwide` and `alignfull` do nothing on this template, since
 * `page-longform.php` renders a plain `<main>` with no layout of its own -- and the 1320px column
 * came only from constrained layout centring each child. That works at 1440 and collapses below
 * 1320: measured at 900px, the hero headline, the comparison table, the route diagram and every
 * paragraph all sat hard against x=0, touching the screen edge. Pre-existing and page-wide, on the
 * finished sections as much as the new ones.
 *
 * Same idiom the homepage stat band already uses: hold the column while there is room for it, fall
 * back to the gutter once there is not. Padding rather than a wrapper, because the block does not
 * render one.
 */
.page-template-page-longform main > .wp-block-group {
	padding-inline: max(var(--gutter), (100% - var(--content-max)) / 2);
}

/*
 * Section rhythm.
 *
 * The seeded groups carried no vertical padding at all -- every section was separated only by
 * whatever margin its first and last block happened to have, so a heading sat almost on the
 * previous section's last line and the bands read as one continuous run of copy instead of
 * distinct sections. `--section-pad` is the same 56-96px the design gives every section.
 *
 * The hero, the proof band and the closing band set their own and are excluded rather than
 * overridden, so their spacing stays where their own rules put it.
 */
.page-template-page-longform main > .wp-block-group:not(.p321-about-hero):not(.p321-about-proof):not(.p321-commit) {
	padding-block: var(--section-pad);
}

/*
 * Section 4 -- "Transparency you can verify".
 *
 * The comparison table rendered both value columns in the same grey `--t2`, so a table whose whole
 * job is to take a side read as a neutral spec sheet: a reader had to parse ten rows of prose to
 * work out which column was the argument. The design source always differentiated them and the
 * theme's generic table styling had flattened it.
 *
 * The distinction reuses the grammar section 3 already teaches rather than inventing a second one:
 * solid and brand-ruled is the verified route, hollow is the one whose record does not join up. So
 * the page states its visual vocabulary once and then trades on it.
 *
 * Never colour alone (WCAG 1.4.1): each column carries its own header text, the marks differ in
 * shape (drawn check against hollow ring), and the two cell backgrounds differ in lightness.
 */
.wp-block-group > .p321-spec-table-wrap {
	margin-block: clamp(24px, 3.4vw, 34px);
}

.p321-spec-table--comparison thead th:nth-child(2) {
	color: var(--brand-lt);
}

.p321-spec-table--comparison thead th:nth-child(3) {
	color: var(--od3);
}

.p321-spec-table--comparison tbody th {
	color: var(--t1);
}

/* Room for the mark, which is drawn rather than set as a glyph so it holds its weight at any size
   and needs no icon font present. */
.p321-spec-table--comparison tbody td {
	position: relative;
	padding-left: 46px;
}

.p321-spec-table--comparison tbody td::before {
	content: "";
	position: absolute;
	left: 20px;
	top: 21px;
}

/* Column 2 -- ours. Lifted white, near-black text, a continuous brand rule down the column edge. */
.p321-spec-table--comparison tbody td:nth-of-type(1) {
	background: var(--panel);
	color: var(--t1);
	box-shadow: inset 2px 0 0 var(--brand);
}

.p321-spec-table--comparison tbody td:nth-of-type(1)::before {
	width: 11px;
	height: 6px;
	border-left: 2px solid var(--brand-dk);
	border-bottom: 2px solid var(--brand-dk);
	transform: rotate(-45deg);
}

/* Column 3 -- theirs. Recessed tint, and the hollow node from the unverified route. */
.p321-spec-table--comparison tbody td:nth-of-type(2) {
	background: var(--band);
	color: var(--t2);
}

/* --t2 rather than the lighter --od4: on the tinted cell that only reached ~2.5:1, which reads as
   a smudge beside the check rather than as the deliberate opposite of it. */
.p321-spec-table--comparison tbody td:nth-of-type(2)::before {
	width: 11px;
	height: 11px;
	border: 2px solid var(--t2);
	border-radius: 50%;
}

/*
 * The page's sharpest sentence, set one typographic step above body copy.
 *
 * Deliberately not a bordered pull-quote: section 5 closes on one, and two quote devices in
 * adjacent sections cancel each other out. This is the quieter of the two by design.
 */
.p321-statement {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 104, "wght" 650;
	font-size: clamp(18px, 2vw, 24px);
	line-height: 1.32;
	color: var(--t1);
	max-width: 44ch;
	margin: clamp(26px, 3.4vw, 38px) 0 20px;
	text-wrap: balance;
}

.band-dark .p321-statement {
	color: #fff;
}

/*
 * Section 2's four-card row.
 *
 * `wp:columns` is flex with `flex-wrap: nowrap`, and core only stacks it at 781px -- so at the
 * 900px review breakpoint all four cards were still side by side with about 132px of content
 * each. That is a ~17-character measure: "Transparency" was hyphenating mid-word, and the
 * chromatogram's labels rendered at ~5px. The same failure section 5 had at five-across, at a
 * width nobody had checked.
 *
 * Explicit steps rather than `auto-fit`, for the reason the homepage tiles were changed in 5a:
 * auto-fit packs as many columns as fit and leaves a ragged last row -- 848px of content divides
 * into three 260px columns, giving 3 + 1.
 */
.wp-block-columns.p321-card-row {
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
	gap: 16px;
}

@media (max-width: 1100px) {
	.wp-block-columns.p321-card-row {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

@media (max-width: 600px) {
	.wp-block-columns.p321-card-row {
		grid-template-columns: 1fr;
	}
}

/*
 * Illustrative HPLC chromatogram -- section 2's "HPLC Purity Analysis" card.
 *
 * The one real graphic the design puts in this section, and the only place on the page that shows
 * the instrument the copy keeps naming. A card that talks about chromatography and shows nothing
 * is the weakest of the four; the trace makes the claim legible at a glance -- one dominant peak,
 * one small impurity, a baseline.
 *
 * Colours come from tokens rather than the mock's inline hex (CLAUDE.md: no hard-coded hex), so it
 * sits correctly on the dark band and would follow any future retint.
 *
 * "Illustrative" is load-bearing in both the accessible name and the visible caption, and must not
 * be trimmed from either: this is a drawn example, not a plot of any batch's data, and under rule 1
 * it must not be mistakeable for one. The real per-lot plots live on the COA pages and are
 * rasterised from actual results.
 *
 * Two deviations from the mock, both because its own numbers do not survive the real card width.
 * The card is ~244px across, so the 420-unit viewBox renders at 0.58 scale and the mock's 9px and
 * 8px labels come out at 5.2px and 4.6px -- illegible, and a 1.8 stroke lands near a single pixel.
 * The type is scaled so it reads at roughly 10px, and the two axis-end labels ("0.0", "retention
 * time") are dropped in favour of a real HTML caption underneath, which can be set at a size a
 * person can actually read and is translatable besides.
 */
.p321-chromatogram {
	margin: 0 0 16px;
}

.p321-chromatogram__plot {
	width: 100%;
	display: block;
}

.p321-chromatogram__axis {
	stroke: var(--hair-dk);
}

.p321-chromatogram__trace {
	fill: none;
	stroke: var(--brand);
	stroke-width: 2.6;
	stroke-linejoin: round;
}

.p321-chromatogram text {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	fill: var(--od2);
	font-size: 17px;
}

.p321-chromatogram .p321-chromatogram__minor-label {
	font-size: 15px;
	fill: var(--od4);
}

.p321-chromatogram__caption {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 11.5px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--od4);
	margin: 8px 0 0;
}

/*
 * Standalone card link -- mono, uppercase, arrow. Not an inline prose link, so colour alone is
 * fine here; the 44px minimum height is the touch target, and the arrow is what marks it as an
 * action rather than a label.
 *
 * No colour on the dark band: `.band-dark a:not(.p321-btn):not(.wp-block-button__link)` already
 * sets brand-light there and outranks any sane class selector. This base colour is for the light
 * bands, where that rule does not apply.
 */
.p321-card-link {
	display: inline-flex;
	align-items: center;
	gap: 9px;
	min-height: 44px;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12.5px;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--brand-dk);
}

.p321-card-link svg {
	flex: none;
}

/*
 * Section 5 -- the quality principles.
 *
 * Five `wp:columns` cards forced a five-across row: ~207px per column, a 20-character measure, and
 * one card running nine lines beside another's three. Equal *heights* were already in place; the
 * widths were the problem, and no amount of height matching fixes a 20-character line.
 *
 * A sixth principle and a 3 x 2 grid give every principle an identical cell by construction, which
 * is what "all equal size" actually asks for. The design source draws this as one slab divided by
 * hairlines rather than six floating cards, and that is the truer reading: these are one company's
 * standards, not six separate offers.
 *
 * The mockup numbers them 01-05. Dropped deliberately -- numbering encodes a sequence, and these
 * are simultaneous: nothing happens "after" being transparent. Each cell takes a drawn glyph in
 * the same dark disc the homepage's COA boxes already use, so the device is reused, not invented.
 */
/*
 * Dividers are borders on the cells, NOT `gap: 1px` over a `--hair` background -- the same fix
 * `.p321-assurance__cell` already carries, and for the same reason (trap #8).
 *
 * Measured on the deployed page 2026-08-30 by sweeping this grid's container width 320-1400px in
 * 0.5px steps: at THREE columns the tracks plus the two gaps fail to cover the content box at
 * 33.3% of widths, worst 0.0156px -- which is the same order as the 0.016px trap #8 caught being
 * antialiased into a visible line. The same sweep at 1, 2 and 4 columns exposed nothing at any
 * width, which is why `.p321-feature-grid` (4 / 2 / 1) was left on the gap technique and only this
 * grid was converted. Three columns is the case that bites: two gaps and a remainder that thirds
 * cannot absorb.
 *
 * Each breakpoint re-declares both borders before applying its own exceptions. It cannot inherit
 * them: at three columns the first row is cells 1-3 and the first column is every third cell, so
 * the three-up exceptions are simply wrong at two-up and would leave a rule above cell 3 and none
 * down the middle.
 */
.p321-principles {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	/* --panel, not --hair: with the dividers drawn as borders there is no grid background left to
	   show through, and the only place the container's own colour still surfaces is the antialiased
	   arc inside each rounded corner. White there, not hairline grey. */
	background: var(--panel);
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	overflow: hidden;
	margin-top: clamp(26px, 4vw, 38px);
}

.p321-principle {
	background: var(--panel);
	padding: clamp(24px, 3vw, 32px);
	border-top: 1px solid var(--hair);
	border-left: 1px solid var(--hair);
}

/* Top row and left column carry no rule -- the slab's own 1px border is that edge. */
.p321-principle:nth-child(-n + 3) {
	border-top: 0;
}

.p321-principle:nth-child(3n + 1) {
	border-left: 0;
}

.p321-principle__icon {
	width: 44px;
	height: 44px;
	border-radius: 50%;
	display: grid;
	place-items: center;
	background: var(--ink);
	color: var(--brand-lt);
	margin-bottom: 18px;
}

.p321-principle__title {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 104, "wght" 750;
	font-size: 19px;
	line-height: 1.3;
	color: var(--t1);
	margin: 0 0 10px;
}

.p321-principle__body {
	font-size: 15.5px;
	line-height: 1.6;
	color: var(--t2);
	margin: 0;
}

.wp-block-group > .p321-callout--quote {
	margin-top: clamp(34px, 5vw, 56px);
}

@media (max-width: 895px) {
	.p321-principles {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}

	.p321-principle {
		border-top: 1px solid var(--hair);
		border-left: 1px solid var(--hair);
	}

	.p321-principle:nth-child(-n + 2) {
		border-top: 0;
	}

	.p321-principle:nth-child(2n + 1) {
		border-left: 0;
	}
}

@media (max-width: 560px) {
	.p321-principles {
		grid-template-columns: 1fr;
	}

	.p321-principle {
		border-top: 1px solid var(--hair);
		/* One column: every cell is the first column, so no vertical rule anywhere. */
		border-left: 0;
	}

	.p321-principle:first-child {
		border-top: 0;
	}
}

/*
 * Closing commitment section.
 *
 * What was here read as a mistake for three reasons, all fixed below. The headline sat at the
 * *bottom* as an afterthought under a two-column block whose short left column left a large void
 * beside a tall card. The trailing paragraphs escaped that block and started on a different left
 * edge from the heading above them. And it used the footer's own near-black, so the page simply
 * dissolved into the footer with no boundary.
 *
 * Now: the headline leads, the copy and the list sit on one baseline grid, and the band is a
 * lighter ink than the footer with a brand hairline on top -- so it reads as the page's last
 * statement rather than the first row of the footer.
 */
.p321-commit {
	background: var(--ink-2);
	border-top: 3px solid var(--brand);
	padding-block: clamp(52px, 7vw, 88px);
	color: var(--page);
}

.p321-commit__inner {
	display: flex;
	flex-direction: column;
	gap: clamp(30px, 4vw, 48px);
}

/* No measure cap on the headline: at this size 40ch broke a 57-character statement across five
   lines and left the right half of the band empty. It is a single statement and should read as
   one, so it runs the container and wraps naturally at two. */
.p321-commit__head {
	max-width: none;
}

.p321-commit__title {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 108, "wght" 800;
	font-size: clamp(26px, 3.4vw, 46px);
	line-height: 1.06;
	letter-spacing: -0.028em;
	text-transform: uppercase;
	text-wrap: balance;
	color: #fff;
	margin: 0;
}

.p321-commit__title span {
	color: var(--brand-lt);
}

.p321-commit__sub {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: clamp(12.5px, 1.15vw, 14px);
	line-height: 1.6;
	letter-spacing: 0.06em;
	color: var(--od2);
	margin: 18px 0 0;
	max-width: 62ch;
}

/*
 * One stack, not two columns.
 *
 * Side by side, three short paragraphs sat beside a ten-item list and left a visible void down the
 * left half -- the same fault this section was rebuilt to fix, which came back the moment the vial
 * card between them was removed. Stacked, the copy keeps its measure, the list spreads across the
 * full width in two columns, and the two read as one statement instead of two unequal panels.
 *
 * The hairline echoes the design's own divider between the headline and the commitment block.
 */
.p321-commit__body {
	display: flex;
	flex-direction: column;
	gap: clamp(24px, 3.4vw, 38px);
	border-top: 1px solid var(--hair-dk);
	padding-top: clamp(28px, 4vw, 44px);
}

.p321-commit__subtitle {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 106, "wght" 750;
	font-size: clamp(18px, 1.9vw, 23px);
	line-height: 1.2;
	text-transform: uppercase;
	letter-spacing: -0.01em;
	color: var(--brand-lt);
	margin: 0 0 18px;
}

.p321-commit__copy p {
	font-size: 16.5px;
	line-height: 1.7;
	color: var(--od2);
	margin: 0 0 16px;
	max-width: 54ch;
}

.p321-commit__copy p:last-child {
	margin-bottom: 0;
}

.p321-commit__card {
	background: rgba(255, 255, 255, 0.04);
	border: 1px solid var(--hair-dk);
	border-radius: var(--rc);
	padding: clamp(22px, 2.8vw, 34px);
}

.p321-commit__card-label {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12px;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--brand-lt);
	margin: 0 0 18px;
}

.p321-commit__list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 13px clamp(20px, 3vw, 48px);
}

@media (max-width: 700px) {
	.p321-commit__list {
		grid-template-columns: 1fr;
	}
}

.p321-commit__list li {
	position: relative;
	padding-left: 30px;
	font-size: 15.5px;
	line-height: 1.55;
	color: var(--page);
}

/* The commitment band is dark ink but carries no `.band-dark` class, so it misses the light-tint
   override the shared rule relies on. The design sets this list's ticks in brand-light. */
.p321-commit__list li::before {
	background-color: var(--brand-lt);
}


/* The accented closing sentence of a headline (p321_accent_last_sentence()). */
.p321-accent {
	color: var(--brand-lt);
}

.p321-hero__mark {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 118, "wght" 800;
	font-size: clamp(28px, 3.6vw, 48px);
	line-height: 1;
	color: var(--brand-lt);
	letter-spacing: 0.02em;
	display: flex;
	align-items: center;
	gap: 0.12em;
	margin-bottom: 8px;
}

.p321-hero__mark span {
	color: var(--brand);
	font-size: 0.5em;
}

.p321-hero__content {
	max-width: 640px;
}

.p321-hero__ctas {
	display: flex;
	gap: 14px;
	margin-top: 30px;
	flex-wrap: wrap;
}

.p321-section-intro {
	margin-bottom: clamp(28px, 4vw, 46px);
}

/*
 * Three across on desktop, per the design -- six category tiles read as two clean rows of three.
 * This was `repeat(auto-fit, minmax(280px, 1fr))`, which packs a fourth column in above ~1300px
 * and leaves a ragged 4 + 2. Explicit counts rather than auto-fit so the row length is the same
 * at every desktop width instead of depending on how wide the viewport happens to be.
 */
.p321-tile-grid {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	gap: 20px;
}

@media (max-width: 895px) {
	.p321-tile-grid {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

@media (max-width: 700px) {
	.p321-tile-grid {
		grid-template-columns: minmax(0, 1fr);
	}
}

.p321-tile {
	position: relative;
	aspect-ratio: 4 / 3;
	min-height: 200px;
	overflow: hidden;
	border-radius: var(--rc);
	background: linear-gradient(155deg, var(--ink), var(--brand-dk) 150%);
	display: flex;
	align-items: flex-end;
	text-decoration: none;
	color: #fff;
}

.p321-tile:hover {
	color: #fff;
}

/*
 * Category tile photography, per the design. Three stacked layers under the text: the photograph
 * desaturated, a brand gradient blended in `color` so the grey picture takes the teal hue, and a
 * bottom-up scrim carrying the title and count. The tile keeps its flat gradient background, so a
 * category with no image set simply renders as it did before -- these layers only exist when
 * front-page.php has an attachment to place.
 */
.p321-tile__img {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	-o-object-fit: cover;
	   object-fit: cover;
	filter: grayscale(1) contrast(1.05);
	opacity: 0.85;
	transition: transform 0.65s cubic-bezier(0.2, 0.7, 0.3, 1);
}

.p321-tile__wash {
	position: absolute;
	inset: 0;
	background: linear-gradient(155deg, var(--ink), var(--brand-dk) 150%);
	mix-blend-mode: color;
	opacity: 0.78;
}

.p321-tile__scrim {
	position: absolute;
	inset: 0;
	background: linear-gradient(0deg, rgba(8, 23, 28, 0.94) 6%, rgba(8, 23, 28, 0.35) 58%, rgba(8, 23, 28, 0.1));
}

.p321-tile:hover .p321-tile__img,
.p321-tile:focus-visible .p321-tile__img {
	transform: scale(1.045);
}

@media (prefers-reduced-motion: reduce) {
	.p321-tile__img {
		transition: none;
	}

	.p321-tile:hover .p321-tile__img,
	.p321-tile:focus-visible .p321-tile__img {
		transform: none;
	}
}

.p321-tile__body {
	position: relative;
	z-index: 2;
	padding: 26px;
}

.p321-tile__title {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 106, "wght" 750;
	font-size: 22px;
	margin: 0;
	color: #fff;
}

.p321-tile__desc {
	font-size: 14.5px;
	color: var(--od2);
	margin-top: 7px;
	max-width: 32ch;
}

.p321-tile__count {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12.5px;
	color: var(--brand-lt);
	letter-spacing: 0.09em;
	text-transform: uppercase;
	margin-top: 11px;
}

.p321-rail-header {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	gap: 24px;
	flex-wrap: wrap;
	margin-bottom: clamp(28px, 4vw, 46px);
}

.p321-rail-header__link {
	display: inline-flex;
	align-items: center;
	min-height: 44px;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 13px;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--brand-dk);
	border-bottom: 2px solid var(--brand);
	padding: 12px 0 6px;
	white-space: nowrap;
	font-weight: 500;
}

.p321-coa-lookup {
	padding: var(--section-pad) var(--gutter);
	display: flex;
	flex-wrap: wrap;
	gap: 56px;
	align-items: center;
}

.p321-coa-lookup__intro {
	flex: 1 1 420px;
}

.p321-coa-lookup__form {
	display: flex;
	gap: 10px;
	margin-top: 26px;
	flex-wrap: wrap;
}

.p321-coa-lookup__form input {
	flex: 1;
	min-width: 200px;
	min-height: 52px;
	padding: 0 17px;
	background: var(--ink-2);
	border: 1px solid var(--hair-dk);
	color: #fff;
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 15px;
	border-radius: var(--rb);
}

.p321-coa-lookup__form input:focus-visible {
	outline: 3px solid var(--brand);
	outline-offset: 1px;
}

/*
 * `min-width: 0` overrides the automatic minimum size a flex item gets, which is its content's
 * min-content width. The certificate inside carries a results table with `min-width: 480px`, and
 * that figure was propagating all the way up to this item: at 390px the panel refused to go below
 * 518px and took the homepage into a 159px horizontal scroll. The table has its own scroll
 * container -- it just needed this item to stop treating the table's width as a floor.
 */
.p321-coa-lookup__panel {
	flex: 1 1 420px;
	min-width: 0;
}

/* Was a 147x18 hit area -- below WCAG 2.2 target-size (minimum) AA, which wants 24x24. Taken to the
   44px the rest of the theme uses for a tapped control (.tap-target, .p321-card-link). */
.p321-coa-lookup__view-link {
	display: inline-flex;
	align-items: center;
	min-height: 44px;
	margin-top: 6px;
	color: var(--brand-lt);
	font-weight: 600;
	font-size: 14px;
}

.p321-coa-lookup__dots {
	display: flex;
	align-items: center;
	gap: 10px;
	margin-top: 12px;
}

/* 44 tall, but the visible mark is still the 3px bar its ::after centres -- the extra height is
   invisible hit area, so the dot row looks unchanged and is twice as easy to hit. */
.p321-coa-lookup__dot {
	flex: 1;
	height: 44px;
	border: 0;
	padding: 0;
	cursor: pointer;
	background: transparent;
	position: relative;
}

.p321-coa-lookup__dot::after {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	top: 50%;
	transform: translateY(-50%);
	height: 3px;
	background: var(--hair-dk);
	transition: background 0.3s;
}

.p321-coa-lookup__dot[aria-pressed="true"]::after {
	background: var(--brand);
}

.p321-mfg {
	display: flex;
	flex-wrap: wrap;
	gap: 56px;
	align-items: center;
}

.p321-mfg__copy {
	flex: 1.1 1 420px;
}

.p321-mfg__steps {
	flex: 1 1 400px;
	min-width: 0;
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 14px;
	align-items: stretch;
}

.p321-mfg-step {
	min-width: 0;
	background: rgba(8, 23, 28, 0.6);
	border: 1px solid var(--hair-dk);
	border-radius: var(--rc);
	padding: clamp(16px, 4vw, 24px) clamp(13px, 3.4vw, 20px);
	display: flex;
	flex-direction: column;
	gap: 10px;
	overflow-wrap: anywhere;
}

.p321-mfg-step__num {
	display: grid;
	place-items: center;
	width: 28px;
	height: 28px;
	border-radius: 50%;
	border: 1px solid var(--brand);
	color: var(--brand);
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 11.5px;
}

.p321-mfg-step__label {
	font-family: "Archivo", "Archivo Fallback", sans-serif;
	font-variation-settings: "wdth" 104, "wght" 780;
	font-size: clamp(15.5px, 4vw, 20px);
	text-transform: uppercase;
	color: #fff;
	letter-spacing: -0.01em;
}

.p321-mfg-step__body {
	font-size: 14.5px;
	color: var(--od3);
	line-height: 1.5;
}

/*
 * Four boxes, so only 4 / 2 / 1 divide evenly.
 *
 * `auto-fit` resolved to three tracks between about 780 and 1030px, leaving the fourth box alone on
 * a second row. Worse here than in a normal card grid: the 1px gap over a hairline background with
 * `overflow: hidden` means the empty two thirds of that row is not whitespace, it paints as a solid
 * grey block in the middle of the section. Measured at 900px, where it reads as broken rather than
 * sparse.
 */
.p321-feature-grid {
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
	gap: 1px;
	background: var(--hair);
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	overflow: hidden;
}

@media (max-width: 1030px) {
	.p321-feature-grid {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

@media (max-width: 600px) {
	.p321-feature-grid {
		grid-template-columns: 1fr;
	}
}

.p321-feature-card {
	background: var(--panel);
	padding: 32px 26px;
}

/* The design's icon disc: dark circle, brand-light glyph, left-aligned above the card title. */
.p321-feature-card__icon {
	width: 48px;
	height: 48px;
	border-radius: 50%;
	display: grid;
	place-items: center;
	background: var(--ink);
	color: var(--brand-lt);
	margin-bottom: 18px;
}

/*
 * Three reviews, so 3 or 1 -- two columns always strands the third.
 *
 * `auto-fit` gave two tracks below about 900px and left one review alone on its own row. There is
 * no honest 2-up state for an odd count, so the row goes straight from three to one.
 */
.p321-testimonial-grid {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	gap: 20px;
}

@media (max-width: 860px) {
	.p321-testimonial-grid {
		grid-template-columns: 1fr;
	}
}

.p321-testimonial-card {
	background: var(--panel);
	border: 1px solid var(--hair);
	border-radius: var(--rc);
	padding: 26px;
	display: flex;
	flex-direction: column;
	gap: 14px;
}

.p321-testimonial-card__stars {
	color: var(--brand);
	letter-spacing: 2px;
	font-size: 15px;
}

.p321-testimonial-card__attribution {
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 12.5px;
	color: var(--t2);
	margin-top: auto;
}

.p321-trust-strip {
	border-top: 1px solid var(--hair);
	border-bottom: 1px solid var(--hair);
	padding: 30px 0;
	background: var(--panel);
}

.p321-trust-strip__inner {
	display: flex;
	flex-wrap: wrap;
	gap: 28px;
	justify-content: space-between;
}

.p321-trust-strip__item {
	display: flex;
	align-items: center;
	gap: 12px;
	flex: 1 1 220px;
	color: var(--brand-dk);
}

.p321-trust-strip__item span {
	font-size: 14.5px;
	font-weight: 600;
	color: var(--t1);
}

.p321-newsletter {
	padding: 80px var(--gutter);
	display: flex;
	flex-wrap: wrap;
	gap: 40px;
	align-items: center;
	justify-content: space-between;
}

.p321-newsletter__copy {
	flex: 1.12 1 420px;
}

.p321-newsletter__form {
	flex: 0.88 1 380px;
}

.p321-newsletter__row {
	display: flex;
	gap: 11px;
	flex-wrap: wrap;
}

.p321-newsletter__row input {
	flex: 1;
	min-width: 210px;
	min-height: 52px;
	padding: 0 17px;
	border: 1px solid var(--hair);
	background: var(--panel);
	font-family: "Instrument Sans", sans-serif;
	font-size: 16px;
	border-radius: var(--rb);
}

.p321-newsletter__row input:focus-visible {
	outline: 3px solid var(--brand);
	outline-offset: 1px;
}

@media (max-width: 760px) {
	.p321-coa-lookup,
	.p321-mfg,
	.p321-newsletter {
		padding-left: var(--gutter);
		padding-right: var(--gutter);
	}
}

/* WooCommerce notices
 *
 * Woo prints its notice wrapper outside this theme's .container, so on the cart it rendered as a
 * full-bleed 1905px band above the breadcrumb. These rules constrain it to the same measure as the
 * rest of the page and restyle the three notice types with theme tokens instead of Woo's defaults.
 * template-parts/wc-notices.php adds the missing region on the PDP and archives.
 */

.woocommerce-notices-wrapper {
	margin: 0 auto;
	max-width: var(--content-max);
	padding: 0 var(--gutter);
}

.woocommerce-notices-wrapper:not(:empty) {
	padding-top: 20px;
}

.p321-wc-notices {
	padding-top: 20px;
}

.woocommerce-message,
.woocommerce-error,
.woocommerce-info {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 10px 16px;
	margin: 0 0 14px;
	padding: 15px 20px;
	border: 1px solid var(--hair);
	border-left: 3px solid var(--brand);
	border-radius: var(--ri);
	background: var(--panel);
	color: var(--t1);
	font-size: 15px;
	line-height: 1.5;
	list-style: none;
}

.woocommerce-error {
	border-left-color: var(--alert);
}

.woocommerce-info {
	border-left-color: var(--t2);
}

.woocommerce-message::before,
.woocommerce-error::before,
.woocommerce-info::before {
	content: none;
}

.woocommerce-message .button,
.woocommerce-error .button,
.woocommerce-info .button {
	/* Woo right-aligns this with `.woocommerce .woocommerce-message .button { float: right }`,
	   which outranks this rule and is inert inside a flex container either way. */
	margin-left: auto !important;
	min-height: 44px;
	display: inline-flex;
	align-items: center;
	padding: 0 20px;
	border: 1px solid var(--ink);
	border-radius: var(--rb);
	background: transparent;
	color: var(--ink);
	font-family: "IBM Plex Mono", "IBM Plex Mono Fallback", monospace;
	font-size: 13px;
	letter-spacing: 0.06em;
	text-transform: uppercase;
}

.woocommerce-message .button:hover,
.woocommerce-error .button:hover,
.woocommerce-info .button:hover {
	background: var(--ink);
	color: #fff;
}

/*
 * COA library toolbar: the design keeps the lot search and the status chips on one row, search
 * taking the free space (COA Library.dc.html). They were stacked.
 */
.p321-coa-toolbar {
	display: flex;
	gap: 14px;
	align-items: center;
	flex-wrap: wrap;
	margin-bottom: 18px;
}

.p321-coa-toolbar .p321-search-sort {
	flex: 1 1 320px;
}

.p321-coa-toolbar .p321-chip-row {
	flex: 0 1 auto;
	margin: 0;
}
