/* =====================================================================
 * zzf-base — glass.css
 * Glassmorphism utilities + aurora field for the "Pastel Glass" preset.
 * ---------------------------------------------------------------------
 * WHY A STYLESHEET AND NOT theme.json:
 * theme.json cannot express backdrop-filter, multi-stop radial blooms,
 * pseudo-element specular edges, or keyframes. These live here so they
 * are versionable in the kit and revocable in one file.
 *
 * INERT BY DEFAULT: nothing in this file applies until a block carries
 * a .zzf-glass / .zzf-aurora class, so it is safe to enqueue globally
 * across every preset. Onyx and High Desert are unaffected.
 *
 * THE MESH RULE (do not break):
 *   Glass frames.  Clinical states.
 *   Marketing surfaces (hero, tiles, steps, COA panel, assurance) = glass.
 *   Data surfaces (product cards, spec tables, checkout) = flat white.
 *   If a CAS number glows it stops reading as a CAS number.
 *   This is also the performance rule — see PERFORMANCE below.
 *
 * PERFORMANCE: backdrop-filter is GPU compositing work. Budget roughly
 * 8-12 glass surfaces per viewport. Never apply .zzf-glass to a product
 * grid card — 40 of them destroys scroll on mid-range Android, which is
 * the majority of social-sourced traffic in this niche.
 * ===================================================================== */

:root {
	/* Aurora bloom stops — the "glow". Override per client in the child theme. */
	--zzf-glow-1: #BBD4F7;   /* sky */
	--zzf-glow-2: #D9E4FB;   /* periwinkle haze */
	--zzf-glow-3: #F7DCE4;   /* dawn blush — used once, sparingly */
	--zzf-glow-4: #CDEBEA;   /* pale saline */

	/* Glass material */
	--zzf-glass-fill:   rgba(255, 255, 255, .58);
	--zzf-glass-strong: rgba(255, 255, 255, .76);
	--zzf-glass-line:   rgba(255, 255, 255, .85);
	--zzf-glass-blur:   26px;
	--zzf-glass-sat:    180%;
	--zzf-glass-shadow: 0 28px 70px -18px rgba(11, 27, 54, .22);
	--zzf-glass-inset:  inset 0 1px 0 rgba(255, 255, 255, .9);
	--zzf-glass-radius: 26px;
}

/* ---------------------------------------------------------------------
 * 1. GLASS SURFACE
 * ------------------------------------------------------------------ */
.zzf-glass {
	position: relative;
	background: var(--zzf-glass-fill);
	-webkit-backdrop-filter: blur(var(--zzf-glass-blur)) saturate(var(--zzf-glass-sat));
	backdrop-filter: blur(var(--zzf-glass-blur)) saturate(var(--zzf-glass-sat));
	border: 1px solid var(--zzf-glass-line);
	border-radius: var(--zzf-glass-radius);
	box-shadow: var(--zzf-glass-shadow), var(--zzf-glass-inset);
}

/* Specular edge — the diagonal light-catch that makes it read as
   material rather than a translucent div. Remove this and the whole
   effect collapses into "semi-transparent box". */
.zzf-glass::after {
	content: "";
	position: absolute;
	inset: 0;
	border-radius: inherit;
	pointer-events: none;
	background: linear-gradient(
		135deg,
		rgba(255,255,255,.55) 0%,
		rgba(255,255,255,0)  34%,
		rgba(255,255,255,0)  66%,
		rgba(255,255,255,.28) 100%
	);
}

.zzf-glass > * { position: relative; z-index: 1; }

.zzf-glass--strong { background: var(--zzf-glass-strong); }
.zzf-glass--flat   { box-shadow: none; }
.zzf-glass--tight  { --zzf-glass-radius: 14px; }

/* Hover lift — opt-in, for linked cards only. */
.zzf-glass--lift { transition: transform .26s ease, box-shadow .26s ease; }
.zzf-glass--lift:hover,
.zzf-glass--lift:focus-within {
	transform: translateY(-4px);
	box-shadow: 0 36px 80px -20px rgba(11,27,54,.28), var(--zzf-glass-inset);
}

/* Graceful degradation: no backdrop-filter (older Firefox, some Android
   WebViews) → fall back to an opaque panel rather than unreadable text. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
	.zzf-glass { background: rgba(255,255,255,.94); }
}

/* ---------------------------------------------------------------------
 * 2. AURORA FIELD
 * Requires a positioned ancestor. Pair with .zzf-has-aurora on the
 * section wrapper so the orbs cannot escape their section.
 * ------------------------------------------------------------------ */
.zzf-has-aurora { position: relative; isolation: isolate; overflow: hidden; }

.zzf-aurora {
	position: absolute;
	inset: 0;
	z-index: 0;
	overflow: hidden;
	pointer-events: none;
}

.zzf-aurora__orb {
	position: absolute;
	border-radius: 50%;
	filter: blur(64px);
	opacity: .85;
	will-change: transform;
}

.zzf-aurora__orb--1 {
	width: 640px; height: 640px; background: var(--zzf-glow-1);
	top: -190px; right: -130px;
	animation: zzf-drift-a 26s ease-in-out infinite alternate;
}
.zzf-aurora__orb--2 {
	width: 520px; height: 520px; background: var(--zzf-glow-2);
	top: 120px; left: -180px;
	animation: zzf-drift-b 32s ease-in-out infinite alternate;
}
.zzf-aurora__orb--3 {
	width: 400px; height: 400px; background: var(--zzf-glow-3);
	bottom: -140px; left: 34%; opacity: .6;
	animation: zzf-drift-c 29s ease-in-out infinite alternate;
}
.zzf-aurora__orb--4 {
	width: 380px; height: 380px; background: var(--zzf-glow-4);
	top: 280px; right: 26%; opacity: .5;
	animation: zzf-drift-b 35s ease-in-out infinite alternate;
}

@keyframes zzf-drift-a { to { transform: translate3d(-70px,  60px, 0) scale(1.09); } }
@keyframes zzf-drift-b { to { transform: translate3d( 80px, -50px, 0) scale(1.06); } }
@keyframes zzf-drift-c { to { transform: translate3d(-50px, -40px, 0) scale(1.12); } }

/* Legibility veil. Non-negotiable: without it, body copy over the bloom
   fails contrast. Raise --zzf-veil-top if copy reads light on a wide
   gamut display. */
.zzf-aurora__veil {
	position: absolute;
	inset: 0;
	z-index: 1;
	pointer-events: none;
	background: linear-gradient(
		180deg,
		rgba(255,255,255,.42)   0%,
		rgba(255,255,255,.72)  62%,
		#FFFFFF               100%
	);
}

/* Section content must clear the aurora stack. */
.zzf-has-aurora > .wp-block-group,
.zzf-has-aurora > .wp-block-cover__inner-container,
.zzf-above-aurora { position: relative; z-index: 2; }

/* ---------------------------------------------------------------------
 * 3. PROOF MARK
 * The SG star is a citation mark, not decoration: it appears ONLY next
 * to a claim that has a document behind it. "≥99% purity" earns one;
 * "ships from Texas" does not. Keep the rule or the mark means nothing.
 * ------------------------------------------------------------------ */
.zzf-proof { display: inline-flex; align-items: center; gap: 7px; }
.zzf-proof::before {
	content: "";
	width: 11px; height: 11px; flex: none;
	background-color: var(--wp--preset--color--accent-2, #C8102E);
	-webkit-mask: var(--zzf-star-mask) center / contain no-repeat;
	mask: var(--zzf-star-mask) center / contain no-repeat;
}
:root {
	--zzf-star-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 1.6l2.9 6.6 7.1.7-5.3 4.8 1.5 7-6.2-3.6-6.2 3.6 1.5-7L2 8.9l7.1-.7z'/%3E%3C/svg%3E");
}

/* ---------------------------------------------------------------------
 * 4. CLINICAL SURFACES — the counterweight
 * Explicitly flat. Apply to product grids and spec tables so a future
 * editor cannot accidentally glass a data surface.
 * ------------------------------------------------------------------ */
.zzf-clinical {
	background: var(--wp--preset--color--base, #FFFFFF);
	border: 1px solid var(--wp--preset--color--line, #E3EBF6);
	border-radius: 14px;
	box-shadow: none;
	-webkit-backdrop-filter: none;
	backdrop-filter: none;
}
.zzf-spec {
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 11px;
	line-height: 1.95;
	color: var(--wp--preset--color--muted, #5A6C89);
}
.zzf-spec b { color: var(--wp--preset--color--contrast, #0B1B36); font-weight: 400; }

/* ---------------------------------------------------------------------
 * 5. ACCESSIBILITY + MOTION
 * ------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
	.zzf-aurora__orb { animation: none !important; }
	.zzf-glass--lift { transition: none !important; }
	.zzf-glass--lift:hover { transform: none !important; }
}

/* High contrast / forced colors: drop the effect entirely. */
@media (forced-colors: active) {
	.zzf-glass { background: Canvas; border-color: CanvasText; box-shadow: none; }
	.zzf-glass::after,
	.zzf-aurora { display: none; }
}

/* Mobile: lighter blur radius. Large blurs are the expensive part. */
@media (max-width: 620px) {
	.zzf-aurora__orb { filter: blur(48px); }
	:root { --zzf-glass-blur: 18px; }
}

/* =====================================================================
 * 6. SITE CHROME — ticker + header
 * ---------------------------------------------------------------------
 * Driven by overridable custom properties so a dark preset (Onyx) can
 * retint the same markup instead of forking the template part.
 * ================================================================== */
:root {
	--zzf-header-bg:        rgba(255, 255, 255, .66);
	--zzf-header-bg-solid:  rgba(255, 255, 255, .92);
	--zzf-header-text:      var(--wp--preset--color--contrast, #0B1B36);
	--zzf-header-muted:     var(--wp--preset--color--muted, #5A6C89);
	--zzf-header-accent:    var(--wp--preset--color--accent, #14346E);
	--zzf-ticker-bg:        var(--wp--preset--color--contrast, #0B1B36);
	--zzf-ticker-text:      #D6E3F6;
	--zzf-header-h:         76px;
}

/* ---------- ticker ---------- */
.zzf-ticker {
	background: var(--zzf-ticker-bg);
	color: var(--zzf-ticker-text);
	position: relative;
	/* BELOW the header (50), not above it. This was 60, and that broke two
	   things:

	   1. The mobile menu. .zzf-header is sticky WITH a z-index, so it forms
	      a stacking context — the nav overlay inside it is capped at 50 no
	      matter what z-index the overlay itself carries. At 60 the ticker
	      painted over the open panel's top ~40px, clipping the logo and the
	      close button. Scrolling moved the ticker out of view, which is why
	      the menu looked correct once the page had scrolled and broken at
	      the top: the bug was never about sticky, it was about what was
	      painting on top.
	   2. Scroll transition. The ticker is above the header in flow but the
	      header pins at top:0, so their boxes overlap while the ticker
	      scrolls past — at 60 the ticker drew over the pinned header
	      through that band. */
	z-index: 40;
}
.zzf-ticker__row { gap: 34px !important; padding: 9px 0; }
.zzf-ticker__item {
	margin: 0;
	letter-spacing: .04em;
	display: inline-flex;
	align-items: center;
	gap: 9px;
	color: var(--zzf-ticker-text);
}
/* accent pip — same "one small spark" discipline as the proof mark */
.zzf-ticker__item::before {
	content: "";
	width: 3px; height: 3px; border-radius: 50%;
	background: var(--wp--preset--color--accent-2, #C8102E);
	flex: none;
}
@media (max-width: 780px) {
	/* keep only the strongest signal on small screens */
	.zzf-ticker__row { gap: 0 !important; }
	.zzf-ticker__item:not(:first-child) { display: none; }
}

/* ---------- header ---------- */

/* Sticky needs the containing block to be the page, not the wrapper.
   WP wraps the header part in <header class="wp-block-template-part">,
   a direct child of .wp-site-blocks whose box is only as tall as the
   header itself — so a sticky child has zero travel inside it and
   scrolls away with the page. display:contents drops the wrapper's box
   while leaving the element in the DOM, so selector matching and the
   root-padding geometry are unchanged and .zzf-header inherits
   .wp-site-blocks as its containing block: the full page height.

   Do not "tidy" this away. It was applied once before and lost when
   this file was rewritten for the aurora system; the header silently
   stopped sticking. See the header-sticky note in CLAUDE.md. */
.wp-site-blocks > header.wp-block-template-part { display: contents; }

/* NO backdrop-filter on .zzf-header itself — see ::before below.
   A non-`none` backdrop-filter makes an element a CONTAINING BLOCK for
   position:fixed descendants. WP's mobile nav overlay is
   `position: fixed; inset: 0`, so with the glass on the header the overlay
   was contained by the ~64px header box instead of the viewport: tapping
   the hamburger opened a clipped, internally-scrollable sliver showing one
   and a half menu items. Moving the filter to a pseudo-element keeps the
   glass and lets the overlay escape to the viewport where it belongs.
   transform, filter, perspective and will-change do the same thing — do
   not add any of them to this element either. */
.zzf-header {
	position: sticky;
	top: 0;
	z-index: 50;
	background: transparent;
	transition: box-shadow .3s ease;
}
/* Belt and braces: while the menu is open the header's stacking context must
   beat anything else in the chrome. The ticker fix above already handles the
   known case; this makes the open panel win regardless of what any future
   sibling sets. Nothing changes when the menu is closed. */
.zzf-header:has(.wp-block-navigation__responsive-container.is-menu-open) {
	z-index: 70;
}

.zzf-header::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	background: var(--zzf-header-bg);
	-webkit-backdrop-filter: blur(22px) saturate(180%);
	backdrop-filter: blur(22px) saturate(180%);
	transition: background .3s ease;
	pointer-events: none;
}
/* Pastel hairline instead of a hard border — the "kind" detail. A flat
   1px grey rule reads administrative; a soft gradient reads considered. */
.zzf-header::after {
	content: "";
	position: absolute;
	left: 0; right: 0; bottom: 0;
	height: 1px;
	pointer-events: none;
	background: linear-gradient(
		90deg,
		rgba(187,212,247,0) 0%,
		rgba(187,212,247,.9) 18%,
		rgba(217,228,251,.9) 50%,
		rgba(205,235,234,.9) 82%,
		rgba(205,235,234,0) 100%
	);
}
/* Scrolled state — JS-free via a scroll-driven fallback is not reliable,
   so the solid variant is toggled by the tiny inline script in the part's
   sibling pattern if enabled. Absent JS the translucent state is correct. */
/* Background now lives on ::before, so the scrolled state must target it too. */
.zzf-header.is-scrolled { box-shadow: 0 10px 30px -18px rgba(11,27,54,.30); }
.zzf-header.is-scrolled::before { background: var(--zzf-header-bg-solid); }

.zzf-header__bar { min-height: var(--zzf-header-h); gap: 28px !important; padding: 12px 0; }

/* brand lockup — the horizontal logo replaced the icon + text wordmark */
.zzf-header__brand { gap: 12px !important; }
.zzf-header__logo { line-height: 0; }
.zzf-header__logo img,
.zzf-header .wp-block-site-logo img {
	width: clamp(190px, 17vw, 260px);
	height: auto;
	border-radius: 0;   /* overrides the 6px rounding meant for a square icon */
	display: block;
}
@media (max-width: 900px) {
	.zzf-header__logo img,
	.zzf-header .wp-block-site-logo img { width: clamp(150px, 34vw, 200px); }
}
.zzf-header__lockup { gap: 1px !important; }
.zzf-header__name a,
.zzf-header__name {
	font-size: 19px;
	letter-spacing: .07em;
	text-transform: uppercase;
	color: var(--zzf-header-text);
	text-decoration: none;
	line-height: 1.1;
}
.zzf-header__descriptor {
	margin: 0;
	font-size: 9.5px !important;
	letter-spacing: .3em;
	text-transform: uppercase;
	color: var(--zzf-header-muted);
	line-height: 1.2;
}
.zzf-header .wp-block-site-logo img { border-radius: 6px; }

/* nav — pill hover, generous target, no underline jitter */
.zzf-header__nav .wp-block-navigation-item__content {
	color: var(--zzf-header-muted);
	text-decoration: none;
	padding: 9px 14px;
	border-radius: 999px;
	transition: background .22s ease, color .22s ease;
	white-space: nowrap;
}
.zzf-header__nav .wp-block-navigation-item__content:hover,
.zzf-header__nav .wp-block-navigation-item__content:focus-visible,
.zzf-header__nav .current-menu-item > .wp-block-navigation-item__content {
	color: var(--zzf-header-accent);
	background: rgba(187, 212, 247, .32);
}

/* actions — cart as a soft pastel pill, the one warm affordance up here */
.zzf-header__actions { gap: 10px !important; align-items: center; }
.zzf-header__actions > * { flex: 0 0 auto; margin: 0; }

/* THE ACCOUNT CONTROL — WooCommerce's Customer Account block, which Block
   Hooks auto-inserts beside the nav (Mini-Cart is unhooked, this one is not).
   Its <svg> ships with a viewBox and NO width/height attributes, so inside a
   flex pill it resolved to 0x0 and the whole control measured 30x20 with
   nothing drawn in it — a blank white oval next to a 46px cart button.
   Measured on the live header 2026-08-20.

   Sized explicitly here, and given a visible label: an icon-only account
   control that renders as an empty pill is not a control anyone will click.
   The label is CSS-generated because the markup belongs to Woo's block; the
   anchor keeps its own aria-label, so nothing is lost to a screen reader. */
.zzf-header__account a svg,
.zzf-header__account a .icon {
	width: 19px; height: 19px; flex: 0 0 auto; display: block;
}
.zzf-header__account a::after {
	content: "Account";
	font-size: 13px;
	font-weight: 500;
	line-height: 1;
	letter-spacing: -.01em;
	white-space: nowrap;
}
@media (max-width: 880px) {
	/* Below the search's own collapse point the label is the first thing to
	   go — the icon still reads, and the row has no width to spare. */
	.zzf-header__account a::after { display: none; }
}
/* The shortcode block can arrive wrapped; keep the wrapper out of the flex row. */
.zzf-header__actions .wp-block-shortcode { margin: 0; line-height: 0; }
.zzf-header__actions .zzf-cartbtn,
.zzf-header__actions .wc-block-mini-cart__button,
.zzf-header__account a {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	box-sizing: border-box;
	/* One height for every control in this row. Declared rather than inherited
	   from content: the cart measured 46px and the account 20px, which is what
	   made the row read as crooked even though all three were centred. */
	min-height: 46px;
	gap: 8px;
	padding: 9px 14px;
	border-radius: 999px;
	border: 1px solid rgba(255, 255, 255, .9);
	background: rgba(255, 255, 255, .62);
	-webkit-backdrop-filter: blur(14px);
	backdrop-filter: blur(14px);
	box-shadow: 0 8px 20px -14px rgba(11, 27, 54, .38);
	color: var(--zzf-header-accent);
	transition: background .22s ease, transform .22s ease;
}
.zzf-header__actions .zzf-cartbtn:hover,
.zzf-header__actions .wc-block-mini-cart__button:hover,
.zzf-header__account a:hover {
	background: rgba(255, 255, 255, .9);
	transform: translateY(-1px);
}
.zzf-header__actions .zzf-cartbtn { cursor: pointer; font: inherit; }
.zzf-header__actions .zzf-cartbtn__count {
	display: inline-flex; align-items: center; justify-content: center;
	min-width: 18px; height: 18px; padding: 0 5px;
	border-radius: 999px;
	background: var(--wp--preset--color--accent-2, #C8102E);
	color: #fff;
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 10.5px; line-height: 1;
}
.zzf-header__actions .zzf-cartbtn__total {
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 12.5px;
	white-space: nowrap;
}
/* The subtotal is the first thing to go when space is tight; the icon and
   count still communicate state. */
@media (max-width: 560px) {
	.zzf-header__actions .zzf-cartbtn__total { display: none; }
}
.zzf-header__actions .wc-block-mini-cart__badge {
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 11px;
}

/* ---------- mobile menu ----------
   The hamburger already existed (overlayMenu:"mobile" on the nav block);
   what was broken was the panel it opens — see the containing-block note on
   .zzf-header above. With that fixed the overlay covers the viewport, so it
   is worth dressing properly: this is the only navigation a phone user
   gets. Big targets, one item per line, hairlines instead of boxes. */
.zzf-header__nav .wp-block-navigation__responsive-container-open,
.zzf-header__nav .wp-block-navigation__responsive-container-close {
	color: var(--zzf-header-accent);
}
/* 44px is the minimum comfortable touch target; the default icon is ~24px.
   NO `display` here — deliberately. WP hides this button above the overlay
   breakpoint with its own display rule, and setting display on a selector
   this specific overrode that, leaving the hamburger visible at desktop
   width NEXT TO the full nav. Size and pad it; let WP decide when it shows. */
.zzf-header__nav .wp-block-navigation__responsive-container-open {
	padding: 10px;
	min-width: 44px;
	min-height: 44px;
	align-items: center;
	justify-content: center;
}

/* The panel is the same material as the header: translucent fill over a
   blurred backdrop, so the page reads through it rather than being replaced
   by a flat white card.

   Putting backdrop-filter HERE is safe, unlike on .zzf-header. The overlay
   is itself the position:fixed element, so it making a containing block
   traps nothing — there is no fixed descendant left to escape.

   The fill is deliberately strong (.86, not the .58 used on glass panels).
   This panel can open over the navy brand stripe as well as the pale field,
   and at .58 over navy the 22px links land near 4:1 — technically passing
   for large text, but thin display type on a moving blur is exactly where
   "technically passing" stops being readable. At .86 it clears comfortably
   on both. Tunable per client. */
:root {
	--zzf-menu-fill: rgba(255, 255, 255, .86);
	--zzf-menu-blur: 26px;

	/* CART DRAWER — same material as the header and the mobile menu.
	   These are consumed by zzf-core's cart-drawer.css, which ships solid
	   by default; setting them here is how the pastel-glass preset opts in,
	   so Onyx and High Desert keep their solid drawer untouched.

	   The scrim is deliberately lighter than the plugin's 0.55 black. The
	   panel is translucent now, so the scrim sits BEHIND the glass and gets
	   read through it — at 0.55 the frosted panel turned grey and muddy.
	   Navy at .28 dims the page enough to signal modality while leaving the
	   glass reading as glass. */
	--zzf-drawer-fill:   rgba(255, 255, 255, .88);
	--zzf-drawer-filter: blur(26px) saturate(180%);
	--zzf-drawer-line:   rgba(20, 52, 110, .12);
	--zzf-drawer-scrim:  rgba(11, 27, 54, .28);
	--zzf-drawer-shadow: -28px 0 70px -18px rgba(11, 27, 54, .34);
}

/* Drawer type, on light glass rather than the dark default. The plugin's
   fallbacks assume a dark panel (Onyx), so muted/contrast land wrong here. */
.zzf-drawer__title { color: var(--wp--preset--color--muted, #5A6C89) !important; }
.zzf-drawer__close {
	border-color: rgba(20, 52, 110, .18) !important;
	color: var(--wp--preset--color--accent, #14346E) !important;
	background: rgba(255, 255, 255, .6) !important;
}
.zzf-drawer__close:hover,
.zzf-drawer__close:focus {
	background: rgba(255, 255, 255, .95) !important;
	border-color: var(--wp--preset--color--accent, #14346E) !important;
}

/* No backdrop-filter support: fall back to a near-opaque panel rather than
   cart contents floating over a blurred-nothing background. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
	:root { --zzf-drawer-fill: rgba(255, 255, 255, .97); }
}
.zzf-header__nav .wp-block-navigation__responsive-container.is-menu-open {
	padding: 0;
	background: var(--zzf-menu-fill) !important;
	-webkit-backdrop-filter: blur(var(--zzf-menu-blur)) saturate(180%);
	backdrop-filter: blur(var(--zzf-menu-blur)) saturate(180%);
}

/* BRAND MARK IN THE PANEL.
   The overlay covers the header, so without this the brand disappears the
   moment the menu opens — the one screen where a phone user is deciding
   where to go carries no identity at all. Placed at the same left gutter
   and on the same line as the close button, so it reads as the header
   staying put rather than a new element sliding in.

   The URL is NOT hardcoded here — glass.css is agency-wide. Each client
   supplies it via settings.custom in their child theme.json, which WP
   emits as --wp--custom--zzf--menu-logo. Unset, this renders nothing and
   costs a client theme exactly zero. Decorative only: the accessible brand
   link lives in the header markup, so there is no alt text to lose. */
.zzf-header__nav .is-menu-open::before {
	content: "";
	position: absolute;
	top: clamp(16px, 4vw, 26px);
	left: var(--zzf-gutter, 28px);
	width: clamp(150px, 42vw, 200px);
	height: 48px;                 /* matches the close button, so they align */
	background-image: var(--wp--custom--zzf--menu-logo, none);
	background-repeat: no-repeat;
	background-position: left center;
	background-size: contain;
	z-index: 2;
	pointer-events: none;
}

/* Specular edge, same detail that makes .zzf-glass read as material rather
   than a semi-transparent div. Sits behind the content, never catches taps. */
.zzf-header__nav .is-menu-open::after {
	content: "";
	position: absolute;
	inset: 0;
	pointer-events: none;
	z-index: 0;
	background: linear-gradient(
		160deg,
		rgba(255,255,255,.55) 0%,
		rgba(255,255,255,0)  38%,
		rgba(255,255,255,0)  70%,
		rgba(255,255,255,.30) 100%
	);
}
.zzf-header__nav .is-menu-open .wp-block-navigation__responsive-container-content,
.zzf-header__nav .is-menu-open .wp-block-navigation__responsive-container-close {
	position: relative;
	z-index: 1;
}

/* No backdrop-filter support (older Android WebViews): fall back to an
   opaque panel rather than unreadable links over raw page content. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
	.zzf-header__nav .wp-block-navigation__responsive-container.is-menu-open {
		background: rgba(255, 255, 255, .97) !important;
	}
}
.zzf-header__nav .is-menu-open .wp-block-navigation__responsive-container-content {
	padding: clamp(84px, 14vw, 120px) var(--zzf-gutter, 28px) 48px;
	align-items: stretch !important;
	justify-content: flex-start !important;
}
.zzf-header__nav .is-menu-open .wp-block-navigation__container {
	gap: 0 !important;
	width: 100%;
}
.zzf-header__nav .is-menu-open .wp-block-navigation-item {
	width: 100%;
	/* A translucent navy hairline, NOT the solid --line (#E3EBF6). On glass
	   a flat opaque rule sits on top of the material instead of in it and
	   reads as a border drawn over frosted plastic. */
	border-bottom: 1px solid rgba(20, 52, 110, .12);
}
.zzf-header__nav .is-menu-open .wp-block-navigation-item:last-child { border-bottom: 0; }
.zzf-header__nav .is-menu-open .wp-block-navigation-item__content {
	display: block;
	width: 100%;
	padding: 20px 12px !important;
	margin: 0 -12px;              /* bleed the tap/press area past the text */
	border-radius: 10px !important;
	background: transparent !important;
	font-family: var(--wp--preset--font-family--display, serif);
	font-size: 22px;
	letter-spacing: .01em;
	color: var(--wp--preset--color--contrast, #0B1B36) !important;
	transition: background .18s ease, color .18s ease;
	-webkit-tap-highlight-color: transparent;   /* replaced by :active below */
}
/* Touch has no hover, so the press state is what confirms the tap landed —
   without it a link feels unresponsive during page load. */
.zzf-header__nav .is-menu-open .wp-block-navigation-item__content:active {
	background: rgba(187, 212, 247, .38) !important;
	color: var(--wp--preset--color--accent, #14346E) !important;
}
.zzf-header__nav .is-menu-open .current-menu-item > .wp-block-navigation-item__content {
	color: var(--wp--preset--color--accent, #14346E) !important;
}
/* CLOSE CONTROL — top right, as a soft square.
   `position: absolute` is load-bearing. WP leaves this button static, so
   setting top/right alone did nothing and it flowed as the first item in
   the column: a bare glyph jammed against the top-LEFT edge, half clipped,
   reading as a stray mark rather than a control. left:auto is belt-and-
   braces in case a WP default or an RTL rule pins it left.

   Given a frame it becomes an obvious affordance: same glass material as
   the header pill, 48px (comfortably over the 44px touch minimum), with a
   14px radius — a soft square rather than a circle, so it sits with the
   panel's hairline rectangles instead of competing with the round cart
   pill in the header behind it. */
.zzf-header__nav .is-menu-open .wp-block-navigation__responsive-container-close {
	position: absolute !important;
	top: clamp(16px, 4vw, 26px) !important;
	right: clamp(16px, 4vw, 26px) !important;
	left: auto !important;
	width: 48px;
	height: 48px;
	padding: 0 !important;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	border-radius: 14px;
	background: rgba(255, 255, 255, .72);
	border: 1px solid rgba(255, 255, 255, .92);
	box-shadow:
		0 10px 26px -14px rgba(11, 27, 54, .42),
		inset 0 1px 0 rgba(255, 255, 255, .9);
	color: var(--zzf-header-accent, #14346E) !important;
	z-index: 2;
	transition: background .18s ease, transform .12s ease;
	-webkit-tap-highlight-color: transparent;
}
.zzf-header__nav .is-menu-open .wp-block-navigation__responsive-container-close svg {
	width: 22px;
	height: 22px;
	fill: currentColor;
}
.zzf-header__nav .is-menu-open .wp-block-navigation__responsive-container-close:active {
	background: rgba(255, 255, 255, .96);
	transform: scale(.95);
}
@media (prefers-reduced-motion: reduce) {
	.zzf-header__nav .is-menu-open .wp-block-navigation__responsive-container-close {
		transition: none;
	}
	.zzf-header__nav .is-menu-open .wp-block-navigation__responsive-container-close:active {
		transform: none;
	}
}

@media (max-width: 900px) {
	.zzf-header__bar { gap: 14px !important; }
	.zzf-header__descriptor { display: none; }
	:root { --zzf-header-h: 64px; }
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
	.zzf-header::before { background: var(--zzf-header-bg-solid); }
}




/* =====================================================================
 * 7. AURORA BACKGROUND SYSTEM  (Southern Glow signature)
 * ---------------------------------------------------------------------
 * Flat 2D. Layer order, back to front:
 *   1. hero background gradient        .zzf-hero
 *   2. atmosphere bloom                .zzf-field__atmosphere
 *   3. aurora veils                    .zzf-veil
 *   4. particle field                  .zzf-field__particles
 *   5. grain overlay                   .zzf-field__grain
 *   6. BACK waves (silver, light blue) .zzf-waves--back
 *   7. hero content                    .zzf-above-ribbons
 *   8. FRONT waves (royal, red, navy)  .zzf-waves--front
 *
 * DESIGN RULES
 *   - Flat, not 3D. No metallic gradients, no glossy highlights, no
 *     large glow halos, no heavy shadows.
 *   - The atmosphere and veils provide the Aurora/Borealis depth; they
 *     are soft, slow, and never sharp.
 *   - Waves occupy the lower ~30% only. Copy and buttons never sit in
 *     that band.
 *   - Paths run -120 to 1560 on a 1440 viewBox so curves leave the frame
 *     rather than ending at a visible point.
 *
 * PERFORMANCE
 *   Everything animates transform / opacity only. NO animated filters —
 *   an SVG or CSS blur forces re-rasterisation every frame, which is the
 *   single biggest scroll cost on mid-range Android. Veils and the
 *   atmosphere are CSS gradients precisely so they cost nothing to move.
 *
 * WHY NO DECORATIVE STARS
 *   The star is the proof mark (.zzf-proof): it appears ONLY beside a
 *   claim backed by a document. Floating decorative stars would strip
 *   that mark of meaning. Particles are plain circles. Keep it that way.
 * ================================================================== */
:root {
	/* ---- colour ramp ----
	   Each hue reads a theme.json override first, exactly like the tuning
	   knobs below. Until 2026-08-24 these nine were plain literals, which
	   meant a client whose brand is not blue could not recolour the field
	   from clients/<slug>/theme.json at all — the only way was editing this
	   agency-wide file, which is the one thing Rule 2 forbids. It is the
	   same trap the knobs sat in until 2026-08-20: fixed there, missed here.
	   Found while stamping the first non-blue client (GemmAminos, emerald).

	   Slot names stay literal (navy / royal / red) because the RULES below
	   reference them by role, not by colour — on an emerald client
	   --zzf-au-navy simply holds the deepest shade. Renaming them would be a
	   rewrite of the whole file for no gain.

	   Override from a client theme.json as settings.custom.zzf.au-navy etc.
	   If you add a hue, wire it the same way or you reintroduce the trap. */
	--zzf-au-navy:      var(--wp--custom--zzf--au-navy,      #08265D);
	--zzf-au-royal:     var(--wp--custom--zzf--au-royal,     #2476C7);
	--zzf-au-lightblue: var(--wp--custom--zzf--au-lightblue, #93CDF4);
	--zzf-au-aqua:      var(--wp--custom--zzf--au-aqua,      #57D9D0);   /* borealis green-teal: the saline note, saturated */
	--zzf-au-violet:    var(--wp--custom--zzf--au-violet,    #8E7BE0);   /* bridges royal to red - the hero gradient already travels it */
	--zzf-au-red:       var(--wp--custom--zzf--au-red,       #D71935);
	--zzf-au-silver:    var(--wp--custom--zzf--au-silver,    #DCE4EC);
	--zzf-au-bg:        var(--wp--custom--zzf--au-bg,        #EEF7FF);
	--zzf-au-white:     var(--wp--custom--zzf--au-white,     #FFFFFF);

	/* ---- tuning knobs: change these, not the rules below ----
	   Each knob reads a theme.json override first, so a client can dial the
	   field back from clients/<slug>/theme.json under settings.custom.zzf.*
	   without this agency-wide file being edited. CLAUDE.md has claimed that
	   was possible since the v4 aurora; until now nothing actually read the
	   override and editing this file was the only way. It is wired now.

	   Defaults below are the 2026-08-20 dramatic field. A client that wants
	   the old soft one sets veil-opacity .42 and atmos-opacity .85.

	   The *-scale knobs are how the breakpoints thin the field. They are
	   multipliers rather than absolute values, so a client override still
	   applies on tablet and mobile instead of being overwritten there. */
	--zzf-au-speed:         var(--wp--custom--zzf--au-speed, 1);
	--zzf-veil-base:        var(--wp--custom--zzf--veil-opacity, .66);
	--zzf-veil-scale:       1;
	--zzf-veil-opacity:     calc(var(--zzf-veil-base) * var(--zzf-veil-scale));
	--zzf-particle-opacity: var(--wp--custom--zzf--particle-opacity, .88);
	--zzf-atmos-base:       var(--wp--custom--zzf--atmos-opacity, 1);
	--zzf-atmos-scale:      1;
	--zzf-atmos-opacity:    calc(var(--zzf-atmos-base) * var(--zzf-atmos-scale));
	--zzf-grain-opacity:    var(--wp--custom--zzf--grain-opacity, .042);
	--zzf-sweep-opacity:    var(--wp--custom--zzf--sweep-opacity, .55);
	--zzf-breathe:          var(--wp--custom--zzf--breathe, 1);   /* 0 parks the curtains */
	--zzf-wave-height:  clamp(150px, 24vw, 300px);
}

/* ---------- 1. section shells ----------
   NO overflow:hidden and NO isolation here. Both used to be on this rule,
   and together they made every section a sealed box: a hard clip plus its
   own stacking context, so nothing could ever bleed across a boundary.
   Continuity was impossible by construction — the page could only ever
   read as stacked bricks. Sections are transparent panes now; the sky
   they sit on is .zzf-field, rendered once per page.

   Clipping is applied per-section ONLY where a section actually contains
   a wave band that must not escape (see .zzf-has-waveband below). */
.zzf-has-ribbons { position: relative; }

/* A section that carries a wave band still has to clip it. */
.zzf-hero, .zzf-has-waveband { overflow: hidden; isolation: isolate; }

/* .zzf-hero deliberately has NO background of its own. It used to carry
   linear-gradient(180deg, white 0%, #F7FBFF 46%, white 100%) — a ramp that
   starts and ends white. Repeated down the page that produced a white seam
   at every joint: the mortar line between the bricks. The tint now lives on
   .zzf-field and runs the whole document. */

.zzf-aurora { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.zzf-above-ribbons { position: relative; z-index: 3; }

/* ---------- 2. atmosphere + aurora veils ---------- */
/* One huge, slow color bloom that sits behind the veils. This is the
   Aurora/Borealis depth: soft, atmospheric, and never sharp. */
.zzf-field__atmosphere {
	position: absolute;
	inset: -20% -10%;
	background:
		radial-gradient(44% 28% at 10%  2%, rgba(87,217,208,.50) 0%, rgba(87,217,208,0) 70%),
		radial-gradient(40% 26% at 42% -2%, rgba(36,118,199,.44) 0%, rgba(36,118,199,0) 70%),
		radial-gradient(38% 22% at 72% -4%, rgba(142,123,224,.28) 0%, rgba(142,123,224,0) 70%),
		radial-gradient(36% 21% at 96%  0%, rgba(215,25,53,.12) 0%, rgba(215,25,53,0) 72%),
		radial-gradient(56% 30% at 50% 104%, rgba(8,38,93,.34) 0%, rgba(8,38,93,0) 70%);
	animation: zzf-atmosphere 90s ease-in-out infinite alternate;
	will-change: transform;
	opacity: var(--zzf-atmos-opacity);
	pointer-events: none;
}
@keyframes zzf-atmosphere {
	0%   { transform: translate3d(0, 0, 0) scale(1); }
	100% { transform: translate3d(3%, -2.5%, 0) scale(1.05); }
}

.zzf-field__veils { position: absolute; inset: -12% -8% 22%; }
.zzf-veil {
	position: absolute; top: -20%; height: 140%;
	border-radius: 50%;
	opacity: var(--zzf-veil-opacity);
	will-change: transform;
	mix-blend-mode: multiply;
}
/* Four curtains, four hues, tight cores. Each gradient front-loads its
   colour in the top third and falls away fast, so a column reads as a
   curtain with an edge instead of the soft smudge this used to be. That
   definition is where the drama comes from - not from opacity alone, which
   only ever made the old columns muddier. */
/* The drift lives on the column; the colour inside it breathes on ::before.
   One element cannot carry two transforms, and that is the whole reason for
   the pseudo — not decoration. Splitting them is what turns four sliding
   panels into four curtains that ripple while they move.

   Breathe periods are 23/29/31/37s against drift periods of 34/41/47/38s and
   a 90s atmosphere. All coprime on purpose: with harmonic periods the whole
   field visibly re-syncs every couple of minutes and the eye catches the loop.
   If you retune one, keep it coprime with the rest or the composition starts
   breathing in unison, which reads as a pulse rather than as weather. */
.zzf-veil::before {
	content: "";
	position: absolute;
	inset: 0;
	border-radius: 50%;
	transform-origin: top center;   /* curtains hang from the top of the field */
	will-change: transform;
}

.zzf-veil--1 { left: 4%;  width: 28vw; min-width: 240px;
	animation: zzf-veil-a calc(34s / var(--zzf-au-speed)) ease-in-out infinite alternate; }
.zzf-veil--1::before { background: linear-gradient(180deg, rgba(87,217,208,.90) 0%, rgba(87,217,208,.38) 28%, rgba(87,217,208,.10) 56%, rgba(87,217,208,0) 84%);
	animation: zzf-breathe-a calc(23s / var(--zzf-au-speed)) ease-in-out infinite alternate; }

.zzf-veil--2 { left: 29%; width: 22vw; min-width: 190px;
	animation: zzf-veil-b calc(41s / var(--zzf-au-speed)) ease-in-out infinite alternate; }
.zzf-veil--2::before { background: linear-gradient(180deg, rgba(147,205,244,.92) 0%, rgba(147,205,244,.40) 28%, rgba(147,205,244,.11) 56%, rgba(147,205,244,0) 84%);
	animation: zzf-breathe-b calc(29s / var(--zzf-au-speed)) ease-in-out infinite alternate; }

.zzf-veil--3 { left: 55%; width: 31vw; min-width: 260px;
	animation: zzf-veil-a calc(47s / var(--zzf-au-speed)) ease-in-out infinite alternate; }
.zzf-veil--3::before { background: linear-gradient(180deg, rgba(142,123,224,.56) 0%, rgba(142,123,224,.23) 28%, rgba(142,123,224,.07) 56%, rgba(142,123,224,0) 84%);
	animation: zzf-breathe-c calc(31s / var(--zzf-au-speed)) ease-in-out infinite alternate; }

/* Still the single note of red, and still the only one. Raised from .14 to
   .30 at the core for the dramatic field, where it lands beside the violet
   curtain and reads as the warm end of an aurora rather than as a flag.
   This is the most arguable value in this section - pull it back first. */
.zzf-veil--4 { left: 80%; width: 19vw; min-width: 160px;
	animation: zzf-veil-c calc(38s / var(--zzf-au-speed)) ease-in-out infinite alternate; }
.zzf-veil--4::before { background: linear-gradient(180deg, rgba(215,25,53,.20) 0%, rgba(215,25,53,.09) 28%, rgba(215,25,53,.03) 56%, rgba(215,25,53,0) 84%);
	animation: zzf-breathe-d calc(37s / var(--zzf-au-speed)) ease-in-out infinite alternate; }

@keyframes zzf-veil-a { to { transform: translate3d(3.4vw, 1.6%, 0) scaleX(1.16) rotate(2.2deg); } }
@keyframes zzf-veil-b { to { transform: translate3d(-2.8vw, -1.8%, 0) scaleX(.86) rotate(-1.8deg); } }
@keyframes zzf-veil-c { to { transform: translate3d(2.2vw, 2.4%, 0) scaleX(1.24) rotate(1.4deg); } }

/* scaleY on a top-anchored gradient lengthens and shortens the curtain;
   the skew leans it. Both are compositor-only. --zzf-breathe scales the
   whole effect, so 0 parks every curtain without touching these rules. */
@keyframes zzf-breathe-a { to { transform: scaleY(calc(1 + .06 * var(--zzf-breathe))) skewX(calc(1.5deg * var(--zzf-breathe))); } }
@keyframes zzf-breathe-b { to { transform: scaleY(calc(1 - .10 * var(--zzf-breathe))) skewX(calc(-1.4deg * var(--zzf-breathe))); } }
@keyframes zzf-breathe-c { to { transform: scaleY(calc(1 + .05 * var(--zzf-breathe))) skewX(calc(1.1deg * var(--zzf-breathe))); } }
@keyframes zzf-breathe-d { to { transform: scaleY(calc(1 - .07 * var(--zzf-breathe))) skewX(calc(-1.8deg * var(--zzf-breathe))); } }

/* ---------- 3. particle field (circles only) ---------- */
/* Large enough to read as drifting light, small enough not to read as
   debris. Two size groups plus staggered delays keep the field from
   feeling uniform. */
.zzf-field__particles { position: absolute; inset: 0 0 26% 0; }
.zzf-field__particles span {
	position: absolute; border-radius: 50%;
	opacity: 0; will-change: transform, opacity;
	animation: zzf-float 19s linear infinite;
}
.zzf-field__particles span:nth-child(1)  { left: 11%; top: 74%; width: 10px; height: 10px; background: var(--zzf-au-royal);     animation-delay: 0s;    }
.zzf-field__particles span:nth-child(2)  { left: 23%; top: 86%; width: 8px;  height: 8px;  background: var(--zzf-au-red);       animation-delay: -3.1s; }
.zzf-field__particles span:nth-child(3)  { left: 34%; top: 68%; width: 12px; height: 12px; background: var(--zzf-au-lightblue); animation-delay: -6.4s; }
.zzf-field__particles span:nth-child(4)  { left: 46%; top: 90%; width: 9px;  height: 9px;  background: var(--zzf-au-silver);    animation-delay: -9.2s; }
.zzf-field__particles span:nth-child(5)  { left: 57%; top: 78%; width: 8px;  height: 8px;  background: var(--zzf-au-royal);     animation-delay: -1.7s; }
.zzf-field__particles span:nth-child(6)  { left: 68%; top: 88%; width: 11px; height: 11px; background: var(--zzf-au-lightblue); animation-delay: -12.6s;}
.zzf-field__particles span:nth-child(7)  { left: 79%; top: 72%; width: 9px;  height: 9px;  background: var(--zzf-au-silver);    animation-delay: -4.8s; }
.zzf-field__particles span:nth-child(8)  { left: 88%; top: 84%; width: 8px;  height: 8px;  background: var(--zzf-au-red);       animation-delay: -15.3s;}
.zzf-field__particles span:nth-child(9)  { left: 17%; top: 62%; width: 9px;  height: 9px;  background: var(--zzf-au-lightblue); animation-delay: -8.1s; }
.zzf-field__particles span:nth-child(10) { left: 40%; top: 58%; width: 8px;  height: 8px;  background: var(--zzf-au-royal);     animation-delay: -11.4s;}
.zzf-field__particles span:nth-child(11) { left: 63%; top: 60%; width: 10px; height: 10px; background: var(--zzf-au-silver);    animation-delay: -2.6s; }
.zzf-field__particles span:nth-child(12) { left: 92%; top: 66%; width: 8px;  height: 8px;  background: var(--zzf-au-lightblue); animation-delay: -14.0s;}

@keyframes zzf-float {
	0%   { transform: translate3d(0,0,0);              opacity: 0; }
	12%  {                                              opacity: var(--zzf-particle-opacity); }
	82%  {                                              opacity: var(--zzf-particle-opacity); }
	100% { transform: translate3d(24px,-190px,0);      opacity: 0; }
}

/* ---------- 4. grain overlay ---------- */
/* 2.5% opacity noise so the gradients read as film rather than flat vector.
   Pure texture; no movement. */
.zzf-field__grain {
	position: absolute;
	inset: 0;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E");
	opacity: var(--zzf-grain-opacity);
	pointer-events: none;
}

/* ---------- 4 + 6. wave system ---------- */
.zzf-waves { position: absolute; left: 0; right: 0; bottom: 0; line-height: 0; pointer-events: none; }
.zzf-waves--back  { z-index: 1; }
.zzf-waves--front { z-index: 4; }
.zzf-waves svg { display: block; width: 100%; height: var(--zzf-wave-height); }

.zzf-wave { will-change: transform; }
.zzf-wave--silver    { fill: var(--zzf-au-silver);    opacity: .55; animation: zzf-wave-a calc(26s / var(--zzf-au-speed)) ease-in-out infinite alternate; }
.zzf-wave--lightblue { fill: var(--zzf-au-lightblue); opacity: .46; animation: zzf-wave-b calc(31s / var(--zzf-au-speed)) ease-in-out infinite alternate; }
.zzf-wave--royal     { fill: var(--zzf-au-royal);     opacity: .92; animation: zzf-wave-c calc(29s / var(--zzf-au-speed)) ease-in-out infinite alternate; }
.zzf-wave--white     { fill: var(--zzf-au-white);                   animation: zzf-wave-a calc(35s / var(--zzf-au-speed)) ease-in-out infinite alternate; }
.zzf-wave--red       { fill: var(--zzf-au-red);                     animation: zzf-wave-b calc(24s / var(--zzf-au-speed)) ease-in-out infinite alternate; }
.zzf-wave--navy      { fill: var(--zzf-au-navy);                    animation: zzf-wave-c calc(33s / var(--zzf-au-speed)) ease-in-out infinite alternate; }

@keyframes zzf-wave-a { to { transform: translate3d(-2.0%,  5px, 0); } }
@keyframes zzf-wave-b { to { transform: translate3d( 2.4%, -6px, 0); } }
@keyframes zzf-wave-c { to { transform: translate3d(-1.6%, -4px, 0); } }

/* ---------- responsive ---------- */
@media (max-width: 1024px) {           /* tablet: thin the field */
	.zzf-field__particles span:nth-child(n+8) { display: none; }
	.zzf-veil--4 { display: none; }
	:root { --zzf-veil-scale: .76; }
}
@media (max-width: 640px) {            /* mobile: fewer layers, more air */
	.zzf-field__particles { display: none; }
	.zzf-veil--2, .zzf-veil--3 { display: none; }
	.zzf-wave--silver, .zzf-wave--white { display: none; }
	:root { --zzf-wave-height: clamp(120px, 34vw, 190px); --zzf-veil-scale: .67; }
	/* Thin the atmosphere here, do not drop it. Two veils and all twelve
	   particles are already gone at this width, so the atmosphere is
	   carrying the field on its own on the client's commonest device.
	   Scaled rather than set, so a client override still applies. */
	:root { --zzf-atmos-scale: .78; }
	.zzf-field__grain { display: none; }
}

/* ---------- reduced motion ----------
   Movement stops; the composition stays complete and visible. */
@media (prefers-reduced-motion: reduce) {
	.zzf-field__atmosphere,
	.zzf-veil, .zzf-veil::before, .zzf-field::after,
	.zzf-field__veils, .zzf-field__particles,
	.zzf-wave, .zzf-field__particles span { animation: none !important; }
	.zzf-field__particles span { opacity: var(--zzf-particle-opacity); }
	/* The sweep is parked mid-field rather than left off-screen, so the
	   composition a reduced-motion visitor sees is the complete one and not
	   the field minus a layer. Same principle as keeping the particles
	   visible above instead of hiding them. */
	.zzf-field::after { transform: translate3d(60%, 0, 0) !important; }
}

/* ---------- hero (editorial) ----------
   Left aligned, oversized, two lines. The right half stays EMPTY on
   desktop — crowding it reads as a sales page, leaving it reads as an
   institution. That restraint is the whole effect; do not fill it.

   Conversion edits:
   - Eyebrow is a small pill so RUO + 21+ read as a credential, not a caption.
   - Trust micro-bar sits between lede and CTAs: four short, verifiable
     signals that answer "is this real?" before the user decides to click.
   - Stats are grouped into one continuous glass bar so the three claims
     read as a single verified statement rather than isolated pills.        */
.zzf-hero { padding: clamp(64px, 8vw, 116px) 0 clamp(112px, 13vw, 156px); }
.zzf-hero__inner { max-width: 1200px; }
.zzf-eyebrow { font-weight: 600; font-size: 11px; letter-spacing: .24em;
	text-transform: uppercase; color: var(--wp--preset--color--muted, #5A6C89); margin: 0; }

/* The pill keeps the RUO/age-gate line from looking like a breadcrumb. */
.zzf-hero__pill {
	display: inline-flex; align-items: center; gap: .35em;
	padding: 6px 12px;
	background: rgba(255,255,255,.72);
	border: 1px solid rgba(255,255,255,.92);
	border-radius: 999px;
	box-shadow: 0 8px 20px -14px rgba(11,27,54,.22);
	backdrop-filter: blur(8px);
}
.zzf-hero__pill span { opacity: .5; }

.zzf-hero__title { margin: .3em 0 0; line-height: .98; letter-spacing: -.028em; }
.zzf-hero__title .zzf-hero__l1,
.zzf-hero__title .zzf-hero__l2 { display: block; }

.zzf-hero__l1 {
	font-family: var(--wp--preset--font-family--display, Marcellus, serif);
	font-weight: 400;
	font-size: clamp(46px, 7.6vw, 112px);
	color: var(--wp--preset--color--contrast, #0B1B36);
}
/* Marcellus has no italic — Playfair Display Italic is the pairing, and
   it is already bundled in the kit. This line carries the brand gradient,
   the one place navy and red both run at full strength. */
.zzf-hero__l2 {
	font-family: 'Playfair Display', Georgia, serif;
	font-style: italic; font-weight: 400;
	font-size: clamp(46px, 7.6vw, 112px);
	background: linear-gradient(96deg,
		var(--wp--preset--color--accent, #14346E) 0%,
		#3A62A8 30%, #8E5A8C 58%,
		var(--wp--preset--color--accent-2, #C8102E) 96%);
	-webkit-background-clip: text; background-clip: text; color: transparent;
	padding-bottom: .06em;
}

.zzf-hero__lede { margin: 30px 0 0; max-width: 52ch; font-size: 17.5px;
	line-height: 1.74; color: var(--wp--preset--color--muted, #5A6C89); }

/* Trust micro-bar: four small, verifiable signals. The checkmark is drawn
   in CSS so it inherits the accent colour and needs no icon font. */
.zzf-hero__trustbar { margin-top: 26px; gap: 10px 18px; }
.zzf-hero__trustitem {
	position: relative;
	margin: 0;
	padding-left: 18px;
	font-size: 12.5px;
	letter-spacing: .02em;
	color: var(--wp--preset--color--contrast, #0B1B36);
}
.zzf-hero__trustitem::before {
	content: "";
	position: absolute; left: 0; top: .35em;
	width: 10px; height: 10px;
	background: var(--wp--preset--color--accent, #14346E);
	-webkit-mask: var(--zzf-check-mask) center / contain no-repeat;
	mask: var(--zzf-check-mask) center / contain no-repeat;
}
:root {
	--zzf-check-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E");
}

.zzf-hero__cta { margin-top: 34px; gap: 14px; }
/* Primary CTA: solid accent, slightly larger than the default button. */
.zzf-btn-primary .wp-block-button__link {
	background: var(--wp--preset--color--accent, #14346E);
	color: #fff;
	border: 1px solid var(--wp--preset--color--accent, #14346E);
	padding: 15px 28px;
	font-size: 12.5px;
	letter-spacing: .12em;
	box-shadow: 0 14px 34px -16px rgba(11,27,54,.42);
	transition: background .22s ease, transform .22s ease, box-shadow .22s ease;
}
.zzf-btn-primary .wp-block-button__link:hover {
	background: #0A1F47;
	border-color: #0A1F47;
	transform: translateY(-2px);
	box-shadow: 0 18px 40px -16px rgba(11,27,54,.5);
}
.zzf-btn-glass .wp-block-button__link {
	background: rgba(255,255,255,.62); color: var(--wp--preset--color--accent, #14346E);
	border: 1px solid rgba(255,255,255,.92);
	padding: 15px 28px;
	font-size: 12.5px;
	letter-spacing: .12em;
	-webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
	box-shadow: 0 12px 30px -14px rgba(11,27,54,.3);
	transition: background .22s ease, transform .22s ease;
}
.zzf-btn-glass .wp-block-button__link:hover { background: rgba(255,255,255,.92); transform: translateY(-2px); }

/* Stats as one continuous glass bar. */
.zzf-stats { margin-top: 48px; gap: 12px; }
.zzf-stats--bar {
	display: inline-flex;
	flex-wrap: nowrap;
	background: rgba(255,255,255,.62);
	border: 1px solid rgba(255,255,255,.9);
	-webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
	border-radius: 18px;
	padding: 6px 8px;
	box-shadow: 0 14px 36px -18px rgba(11,27,54,.28);
}
.zzf-stats--bar .zzf-stat {
	background: transparent;
	border: 0;
	border-radius: 0;
	box-shadow: none;
	backdrop-filter: none;
	min-width: auto;
	padding: 12px 22px;
}
.zzf-stats--bar .zzf-stat:not(:last-child) {
	border-right: 1px solid rgba(20,52,110,.10);
}
.zzf-stat__n { font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 21px; letter-spacing: -.02em; margin: 0; }
.zzf-stat__l { font-size: 10.5px; letter-spacing: .14em; text-transform: uppercase;
	color: var(--wp--preset--color--muted, #5A6C89); margin: 5px 0 0;
	display: flex; align-items: center; gap: 7px; }

@media (max-width: 700px) {
	.zzf-hero__trustbar { gap: 8px 14px; }
	.zzf-hero__trustitem { font-size: 11.5px; padding-left: 16px; }
	.zzf-hero__trustitem::before { width: 9px; height: 9px; }
	.zzf-stats--bar { flex-wrap: wrap; border-radius: 16px; }
	.zzf-stats--bar .zzf-stat {
		flex: 1 1 30%; min-width: 120px;
		border-right: 0 !important;
		border-bottom: 1px solid rgba(20,52,110,.10);
	}
	.zzf-stats--bar .zzf-stat:last-child { border-bottom: 0; }
}

/* ---------- trust ---------- */
.zzf-trust { padding: clamp(70px, 8vw, 100px) 0; }
.zzf-trust__head { margin-bottom: 58px; }
.zzf-trust__head h2 { font-size: clamp(30px, 3.6vw, 46px); margin-top: 14px; }
.zzf-trust__sub { margin-top: 18px; color: var(--wp--preset--color--muted, #5A6C89); }
.zzf-trust__cards { gap: 18px; }

/* Cards get a top accent line: solid accent for documented claims, muted
   hairline for policy claims. This teaches the proof-star rule visually
   before the user has read a word. */
.zzf-trust__card {
	position: relative;
	padding: 32px 28px;
	border-top: 3px solid rgba(20,52,110,.12);
	transition: transform .26s ease, box-shadow .26s ease, border-color .26s ease;
}
.zzf-trust__card--proof { border-top-color: var(--wp--preset--color--accent-2, #C8102E); }
.zzf-trust__card--proof:hover { border-top-color: var(--wp--preset--color--accent, #14346E); }

/* Document-kind label: mono, small, uppercase. Sets the frame for the claim. */
.zzf-trust__kind {
	margin: 0 0 10px;
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 10px;
	letter-spacing: .18em;
	text-transform: uppercase;
	color: var(--wp--preset--color--muted, #5A6C89);
}
.zzf-trust__card--proof .zzf-trust__kind {
	color: var(--wp--preset--color--accent, #14346E);
}

.zzf-trust__claim { font-family: var(--wp--preset--font-family--display, serif);
	font-size: 19px; margin: 0 0 10px; display: flex; align-items: center; gap: 9px; }
.zzf-trust__evidence { font-size: 14px; color: var(--wp--preset--color--muted, #5A6C89); line-height: 1.62; margin: 0; }

/* Verify link as a small pill button — it is the primary action of the card. */
.zzf-trust__verify { margin: 18px 0 0; padding-top: 14px;
	border-top: 1px solid rgba(20,52,110,.12);
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 11px; }
.zzf-trust__verify a {
	display: inline-flex; align-items: center; gap: 6px;
	padding: 7px 12px;
	border-radius: 6px;
	background: rgba(20,52,110,.06);
	color: var(--wp--preset--color--accent, #14346E);
	text-decoration: none;
	transition: background .18s ease, color .18s ease, transform .18s ease;
}
.zzf-trust__verify a::after {
	content: "\2192";
	transition: transform .18s ease;
}
.zzf-trust__verify a:hover {
	background: rgba(20,52,110,.12);
	color: var(--wp--preset--color--contrast, #0B1B36);
}
.zzf-trust__verify a:hover::after { transform: translateX(3px); }
.zzf-trust__verify.is-inert a { color: var(--wp--preset--color--muted, #5A6C89); background: transparent; }
.zzf-trust__verify.is-inert a:hover { color: var(--wp--preset--color--contrast, #0B1B36); background: rgba(20,52,110,.06); }

/* ---------- contact / signup ---------- */
.zzf-contact { padding: 0 0 clamp(72px, 9vw, 110px); }
.zzf-contact__panel { padding: clamp(36px, 4.5vw, 56px) clamp(28px, 4vw, 52px); gap: 56px; align-items: center; }
.zzf-contact__panel h2 { font-size: clamp(28px, 3.2vw, 40px); margin-top: 14px; }
.zzf-contact__sub { margin-top: 16px; color: var(--wp--preset--color--muted, #5A6C89); }
.zzf-finelabel { font-size: 11px; letter-spacing: .16em; text-transform: uppercase;
	color: var(--wp--preset--color--muted, #5A6C89); margin: 0 0 6px; display: block; }
.zzf-signup { margin-top: 26px; }
.zzf-signup__row { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 8px; }
.zzf-signup__row input {
	flex: 1; min-width: 220px; background: #fff;
	border: 1px solid var(--wp--preset--color--line, #E3EBF6);
	padding: 17px 18px; border-radius: 6px; font-size: 15px;
	color: var(--wp--preset--color--contrast, #0B1B36);
}
.zzf-signup__row button { padding: 17px 30px; border: 0; border-radius: 6px; cursor: pointer;
	background: var(--wp--preset--color--accent, #14346E); color: #fff;
	font-weight: 600; font-size: 13px; letter-spacing: .1em; text-transform: uppercase; }
.zzf-signup__row button:hover { background: #0A1F47; }
.zzf-contact__consent { margin-top: 16px; font-size: 12.5px;
	color: var(--wp--preset--color--muted, #5A6C89); line-height: 1.6; }
.zzf-reach__row { display: flex; justify-content: space-between; gap: 16px;
	padding: 15px 0; margin: 0; border-bottom: 1px solid rgba(20,52,110,.12); }
.zzf-reach__row .k { font-size: 11px; letter-spacing: .16em; text-transform: uppercase;
	color: var(--wp--preset--color--muted, #5A6C89); }
.zzf-reach__row .v { font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 13.5px; color: var(--wp--preset--color--accent, #14346E); }

@media (max-width: 1020px) {
	.zzf-trust__cards { grid-template-columns: repeat(2, 1fr) !important; }
	.zzf-contact__panel { grid-template-columns: 1fr !important; gap: 40px; }
}
@media (max-width: 640px) {
	.zzf-trust__cards { grid-template-columns: 1fr !important; }
	.zzf-hero__cta .wp-block-button { width: 100%; }
}
@media (prefers-reduced-motion: reduce) { .zzf-rib { animation: none !important; } }

/* =====================================================================
 * 8. FAQ  (native details/summary — no JS, keyboard accessible)
 * ================================================================== */
.zzf-faq { padding: clamp(70px, 8vw, 100px) 0; }
.zzf-faq__head { margin-bottom: 44px; }
.zzf-faq__head h2 { font-size: clamp(30px, 3.6vw, 46px); margin-top: 14px; }
.zzf-faq__list { max-width: 820px; margin: 0 auto; }

.zzf-faq__item {
	background: rgba(255,255,255,.55);
	-webkit-backdrop-filter: blur(20px) saturate(170%);
	backdrop-filter: blur(20px) saturate(170%);
	border: 1px solid rgba(255,255,255,.86);
	border-radius: 14px;
	margin-bottom: 10px;
	overflow: hidden;
	box-shadow: 0 14px 38px -20px rgba(11,27,54,.22);
	transition: box-shadow .24s ease;
}
.zzf-faq__item[open] { box-shadow: 0 26px 60px -22px rgba(11,27,54,.26); }

.zzf-faq__item summary {
	list-style: none; cursor: pointer;
	padding: 22px 58px 22px 26px; position: relative;
	font-family: var(--wp--preset--font-family--display, serif);
	font-size: 18px; line-height: 1.35;
	color: var(--wp--preset--color--contrast, #0B1B36);
}
.zzf-faq__item summary::-webkit-details-marker { display: none; }
/* plus → minus, drawn in CSS so it inherits colour and needs no icon font */
.zzf-faq__item summary::after,
.zzf-faq__item summary::before {
	content: ""; position: absolute; right: 26px; top: 50%;
	background: var(--wp--preset--color--accent, #14346E);
	transition: transform .26s ease, opacity .26s ease;
}
.zzf-faq__item summary::before { width: 13px; height: 1.5px; margin-top: -.75px; }
.zzf-faq__item summary::after  { width: 1.5px; height: 13px; margin-top: -6.5px; right: 32px; }
.zzf-faq__item[open] summary::after { transform: rotate(90deg); opacity: 0; }
.zzf-faq__item summary:focus-visible { outline: 2px solid var(--wp--preset--color--accent-2, #C8102E); outline-offset: -3px; }

.zzf-faq__a { padding: 0 58px 24px 26px; }
.zzf-faq__a p { margin: 0; font-size: 15.5px; line-height: 1.7;
	color: var(--wp--preset--color--muted, #5A6C89); }

/* =====================================================================
 * 9. PRODUCT SHOWCASE (image slots)
 * The frame is glass; the PHOTO itself stays clean. Do not tint or
 * overlay product imagery — buyers read filtered product shots as hiding
 * something, which is the opposite of the whole argument.
 * ================================================================== */
.zzf-showcase { padding: clamp(70px, 8vw, 104px) 0; }
.zzf-showcase__grid { gap: clamp(32px, 5vw, 64px) !important; align-items: center; }

.zzf-showcase__media { position: relative; }
.zzf-showcase__frame { padding: clamp(20px, 3vw, 34px); border-radius: 26px; }
.zzf-showcase__frame img {
	display: block; width: 100%; height: auto; border-radius: 14px;
	/* grounds the product on the light field instead of floating flat */
	filter: drop-shadow(0 26px 40px rgba(11,27,54,.16));
}
.zzf-showcase__img { margin: 0; }

/* floating spec chip — makes the photograph assert something checkable */
.zzf-showcase__chip {
	position: absolute; right: clamp(-6px, -1vw, 14px); bottom: 24px; margin: 0;
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 11.5px; letter-spacing: .02em;
	color: var(--wp--preset--color--accent, #14346E);
	background: rgba(255,255,255,.88);
	-webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
	border: 1px solid rgba(255,255,255,.95);
	padding: 11px 16px; border-radius: 999px;
	box-shadow: 0 16px 34px -16px rgba(11,27,54,.34);
}

.zzf-showcase__body h2 { font-size: clamp(28px, 3.3vw, 42px); margin-top: 14px; }
.zzf-showcase__lede { margin-top: 18px; font-size: 17px; line-height: 1.72;
	color: var(--wp--preset--color--muted, #5A6C89); }
.zzf-showcase__thumbs { gap: 12px !important; margin-top: 30px; }
.zzf-showcase__thumbs figure { margin: 0; padding: 8px; border-radius: 14px; }
.zzf-showcase__thumbs img { display: block; width: 100%; height: auto; border-radius: 8px; }
.zzf-showcase__cta { margin-top: 30px; }

/* Empty image slots must not collapse — the client needs something to
   click on in the editor, and a 0px figure is un-selectable. */
.zzf-showcase img[src=""],
.zzf-showcase img:not([src]) {
	min-height: 180px; width: 100%;
	background: repeating-linear-gradient(135deg,
		rgba(143,178,236,.16) 0 12px, rgba(255,255,255,.5) 12px 24px);
	border-radius: 10px;
}

@media (max-width: 1020px) {
	.zzf-showcase__grid { grid-template-columns: 1fr !important; }
	.zzf-showcase__chip { right: 14px; }
}
@media (max-width: 640px) {
	.zzf-faq__item summary { font-size: 16px; padding: 18px 50px 18px 20px; }
	.zzf-faq__a { padding: 0 50px 20px 20px; }
}

/* =====================================================================
 * 10. COA BATCH LOOKUP
 * The one section that invites the visitor to TEST a claim rather than
 * read it. Never gate it behind an account or an email capture.
 * ================================================================== */
.zzf-coa { padding: clamp(70px, 8vw, 100px) 0; }
.zzf-coa__panel { padding: clamp(36px, 5vw, 62px) clamp(28px, 5vw, 62px); }
.zzf-coa__title { margin: .3em 0 0; line-height: 1.0; letter-spacing: -.024em; }
.zzf-coa__title .zzf-hero__l1,
.zzf-coa__title .zzf-hero__l2 { font-size: clamp(32px, 4.4vw, 58px); display: block; }

/* `display: block` above is not decorative. The house hero shape is two
   stacked lines — upright, then italic — and the rule that does that lives
   at .zzf-hero__title .zzf-hero__l1/__l2, scoped to the hero. Reuse the same
   spans anywhere else and they stay INLINE, so the two lines run together
   with no space between them: "Look upany lot.", "A small Texassupply house."
   There is no space in the markup to save it, because the shape assumes the
   line break. Any new surface borrowing .zzf-hero__l1/__l2 needs this too. */

/* The people panel carries four elements where the COA lookup carries a
   heading, a form and a three-row index. It inherited padding scaled for
   twice the content and read as a mostly-empty box. Tightened on a modifier
   rather than on .zzf-coa__panel, which other clients still use for the
   lookup it was measured for. */
.zzf-coa--people { padding: clamp(44px, 5vw, 66px) 0; }
.zzf-coa--people .zzf-glass.zzf-coa__panel {
	padding: clamp(30px, 3vw, 44px) clamp(28px, 3.4vw, 52px);
}
.zzf-coa--people .zzf-coa__sub { margin-top: 14px; }
.zzf-coa--people .zzf-coa__cta { margin-top: 26px; }
.zzf-coa__sub { margin-top: 18px; max-width: 56ch; font-size: 16.5px; line-height: 1.7;
	color: var(--wp--preset--color--muted, #5A6C89); }

.zzf-coa__form { margin-top: 32px; }
.zzf-coa__row { display: flex; gap: 0; margin-top: 8px; flex-wrap: wrap;
	border-radius: 8px; overflow: hidden;
	box-shadow: 0 18px 44px -22px rgba(11,27,54,.4); }
.zzf-coa__row input {
	flex: 1; min-width: 240px; background: #fff; border: 1px solid var(--wp--preset--color--line, #E3EBF6);
	border-right: 0; padding: 20px 22px; font-size: 15.5px;
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	color: var(--wp--preset--color--contrast, #0B1B36);
}
.zzf-coa__row input::placeholder { color: #9BAAC4; }
.zzf-coa__row button {
	border: 0; cursor: pointer; padding: 20px 34px;
	background: var(--wp--preset--color--accent, #14346E); color: #fff;
	font-weight: 600; font-size: 12.5px; letter-spacing: .12em; text-transform: uppercase;
	display: inline-flex; align-items: center; gap: 10px;
}
.zzf-coa__row button:hover { background: #0A1F47; }

.zzf-coa__recent { margin-top: 38px; padding-top: 26px;
	border-top: 1px solid rgba(20,52,110,.12); }
.zzf-coa__lot { display: grid; grid-template-columns: 150px 1fr auto; gap: 16px;
	align-items: baseline; margin: 0; padding: 13px 0;
	border-bottom: 1px solid rgba(20,52,110,.08); }
.zzf-coa__lot .k { font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 13px; color: var(--wp--preset--color--accent, #14346E); }
.zzf-coa__lot .m { font-size: 14px; color: var(--wp--preset--color--contrast, #0B1B36); }
.zzf-coa__lot .v { font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 11.5px; color: var(--wp--preset--color--muted, #5A6C89); }
.zzf-coa__cta { margin-top: 26px; }

@media (max-width: 700px) {
	.zzf-coa__lot { grid-template-columns: 1fr; gap: 3px; padding: 15px 0; }
	.zzf-coa__row input { border-right: 1px solid var(--wp--preset--color--line, #E3EBF6); }
	.zzf-coa__row button { width: 100%; justify-content: center; }
	.zzf-hero__cta .wp-block-button { width: 100%; }
	.zzf-hero__cta .wp-block-button__link { width: 100%; text-align: center; }
}

/* =====================================================================
 * 11. LAYOUT REPAIR — panel padding + true wide content
 * ---------------------------------------------------------------------
 * Two WordPress behaviours fight this design system. Both are invisible
 * in the markup, which is why the sections looked authored correctly
 * while rendering badly.
 *
 * (a) PANEL PADDING IS ZEROED. Any group with layout:constrained gets
 *     .has-global-padding, and WP then emits
 *       .has-global-padding :where(... > .has-global-padding ...)
 *         { padding-left: 0; padding-right: 0 }
 *     to stop root padding compounding when panels nest. It cannot tell
 *     our padding from root padding, so EVERY glass panel lost its
 *     horizontal padding while keeping its vertical padding — text ran
 *     to the glass edge. That single rule is most of the "cheap" look.
 *     Its specificity is (0,1,0) and it loads after this file, so every
 *     rule below uses two classes to win on specificity, not order.
 *
 * (b) NESTED CONTENT IS CAPPED AT contentSize. A non-aligned child of a
 *     constrained container gets max-width:contentSize (720px) — even
 *     inside a 1200px .alignwide parent. The four-card trust grid was
 *     720px wide, so each card was 162px: two-word line wraps and a
 *     ragged bottom edge. The section background spanned the viewport
 *     while its content sat in a narrow column.
 *
 * Headings are deliberately NOT widened here. .zzf-trust__head and
 * .zzf-faq__head set their own contentSize in markup (640/620px) and the
 * hero lede is ~52ch by design — measure is a typographic decision, not
 * a bug. Only panels, grids and rows are released to the full width.
 * ================================================================== */

:root {
	/* Content ceiling and page gutter. Override per client in the child
	   theme under settings.custom — never edit this file for one client. */
	--zzf-wide:   1440px;
	--zzf-gutter: clamp(20px, 4.2vw, 64px);
}

/* --- (b) release the section container to the full wide measure -----
   The doubled .alignwide is deliberate. WP's own rule is also (0,2,0)
   and is emitted inline in <head>, i.e. after this file, so a matching
   selector loses on source order — the padding below would apply while
   the max-width silently did not. Repeating the class wins on
   specificity instead of hoping for load order. */
.zzf-has-ribbons > .alignwide.alignwide,
.zzf-has-aurora  > .alignwide.alignwide {
	max-width: var(--zzf-wide);
	padding-left: var(--zzf-gutter);
	padding-right: var(--zzf-gutter);
	box-sizing: border-box;
}

/* Panels and grids fill that container instead of collapsing to 720px. */
.zzf-trust .zzf-trust__cards,
.zzf-coa .zzf-coa__panel,
.zzf-contact .zzf-contact__panel,
.zzf-showcase .zzf-showcase__grid,
.zzf-coa .zzf-coa__recent { max-width: none; }

/* FAQ is the exception: a single column of prose at 1440px is unreadable,
   so it keeps a comfortable measure and centres inside the wide shell. */
.zzf-faq .zzf-faq__list { max-width: 900px; margin-inline: auto; }

/* --- (a) restore the horizontal padding WP zeroed ------------------- */
.zzf-trust__cards > .zzf-trust__card {
	padding: clamp(28px, 2.4vw, 40px) clamp(26px, 2.2vw, 36px);
}
.zzf-glass.zzf-coa__panel {
	padding: clamp(38px, 4vw, 62px) clamp(28px, 3.4vw, 56px);
}
.zzf-glass.zzf-showcase__frame { padding: clamp(26px, 2.6vw, 40px); }
.zzf-glass.zzf-contact__panel {
	padding: clamp(36px, 4.5vw, 56px) clamp(28px, 4vw, 52px);
}
.zzf-stat.zzf-stat { padding: 15px 20px; }

/* --- cards: four across, and square off the bottom edge -------------
   Explicit column counts, NOT auto-fit. The heading promises "Four
   claims. Four documents." — auto-fit resolves to 3+1 at some widths,
   which reads as a broken row rather than a set. Step 4 -> 2 -> 1 so
   the grid always stays rectangular. */
.zzf-trust .zzf-trust__cards {
	grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
	gap: clamp(16px, 1.5vw, 26px);
	align-items: stretch;
}
@media (max-width: 1120px) {
	.zzf-trust .zzf-trust__cards { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
}
/* 640px matches the existing single-column breakpoint earlier in this
   file; keep them equal or the two rules disagree between 621 and 640. */
@media (max-width: 640px) {
	.zzf-trust .zzf-trust__cards { grid-template-columns: minmax(0, 1fr) !important; }
}
/* Push the verify link to the bottom of every card so the rules line up
   across the row regardless of how long each claim runs. Ragged bottoms
   are what made four good cards read as four unfinished ones. */
.zzf-trust__cards > .zzf-trust__card { display: flex; flex-direction: column; }
.zzf-trust__card > .zzf-trust__verify { margin-top: auto; }

/* Making the card a flex column has a side effect worth knowing about.
   WP's constrained layout ships:
     .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull))
       { margin-left: auto !important; margin-right: auto !important }
   On a block child, auto margins resolve to 0 because the block already
   fills the line — invisible. On a FLEX item they centre it at its
   intrinsic width. So the moment the card became a flex column, the claim
   (203px) and the verify link (121px) started centring while the body copy
   (which happens to fill the line) stayed put: three different left edges
   inside one card. That reads as sloppy, not as centred.

   !important is required here, not preferred — it is the only thing that
   beats !important. align-self:stretch alone does NOT work, because auto
   margins win over stretch in flex layout. */
.zzf-trust__cards > .zzf-trust__card > * {
	margin-left: 0 !important;
	margin-right: 0 !important;
	max-width: none;
	align-self: stretch;
}

/* The proof star sits on the first line, not the optical centre of a
   wrapped two-line claim. */
.zzf-trust__claim.zzf-proof { align-items: flex-start; }
.zzf-trust__claim.zzf-proof::before { margin-top: .34em; }

/* --- responsive: collapse the multi-column panels in step ----------- */
@media (max-width: 1020px) {
	.zzf-showcase .zzf-showcase__grid,
	.zzf-contact .zzf-contact__panel { grid-template-columns: 1fr !important; }
}

/* =====================================================================
 * 12. THE PAGE FIELD — one continuous sky
 * ---------------------------------------------------------------------
 * Rendered ONCE per page from the template (parts/field.html), never per
 * section. Six sections each carrying their own copy of the veils and
 * particles is what made the page read as bricks: six independent light
 * sources, each beginning and ending inside its own box, separated by a
 * white seam and terminated by a wave band.
 *
 * FIXED, not absolute. The sky holds still and the content scrolls over
 * it, which is what makes a long page feel like one surface rather than a
 * stack of slabs. z-index 0 with content lifted above it — NOT z-index:-1,
 * which would push it behind the body background and disappear.
 * ================================================================== */

.zzf-field {
	position: fixed;
	inset: 0;
	z-index: 0;
	pointer-events: none;
	overflow: hidden;
	/* The page ground, formerly on .zzf-hero. One ramp for the whole
	   document, so there is no seam to see. It carries a real value range
	   now - a deep crown, a bright clearing, a deeper floor - where it used
	   to run white to #EEF7FF and read as no background at all.

	   The clearing in the middle is not decoration. This layer is
	   position:fixed, so every line of copy on the site eventually scrolls
	   across the middle of it; that band is the one place the field is not
	   allowed to get dark.

	   MEASURE IT ACROSS THE CYCLE, NOT AT REST. This field animates, so the
	   binding number is the worst ANIMATION PHASE, not the resting frame —
	   the veils drift in and out of overlap and the copy zone swings by
	   about 0.06 luminance as they do. Checking one frame is how the first
	   pass at this got a reassuring 4.75:1 for a field that was really
	   3.50:1 once the curtains moved. Sample the same block grid from y=76
	   (the sticky header covers everything above it) at a dozen phases and
	   take the minimum. As of 2026-08-20 the worst phase across every
	   breakpoint holds muted at 4.60:1 against the 4.5:1 AA floor, and the
	   binding spot is the top-right corner just under the header. */
	background:
		/* Dark crown. Pinned in px rather than %, so it lands on the 76px
		   sticky header at every viewport height instead of creeping down
		   into the copy band on a tall screen. */
		linear-gradient(180deg, rgba(20,52,110,.46) 0px, rgba(20,52,110,.40) 58px, rgba(20,52,110,.02) 78px, rgba(20,52,110,0) 92px),
		radial-gradient(112% 46% at 80% -14%, rgba(20,52,110,.07) 0%, rgba(20,52,110,0) 60%),
		radial-gradient(88% 42% at 4% -8%, rgba(87,217,208,.34) 0%, rgba(87,217,208,0) 56%),
		linear-gradient(180deg, #BCD4EF 0%, #DCEBFB 26%, #F2F8FE 50%, #E0EDFB 76%, #BFD6F0 100%);
}

/* THE SWEEP — the light that runs along the curtain.
   This is the layer that makes the field read as an aurora rather than as a
   gradient: a real borealis brightens in a band that travels, it does not sit
   still and glow. One element, one transform, 150s, and it is a pseudo so
   parts/field.html did not have to change.

   ::after paints last, so it sits above the grain - correct for light. It is
   clipped by the overflow:hidden already on .zzf-field. It stops short of the
   bottom 22% for the same reason the veils do: the wave band lives there. */
.zzf-field::after {
	content: "";
	position: absolute;
	top: -10%; bottom: 22%; left: 0;
	width: 46vw; min-width: 380px;
	opacity: var(--zzf-sweep-opacity);
	background: linear-gradient(100deg,
		rgba(255,255,255,0)   0%,
		rgba(255,255,255,.20) 34%,
		rgba(219,241,255,.30) 52%,
		rgba(255,255,255,.18) 68%,
		rgba(255,255,255,0) 100%);
	transform: translate3d(-120%, 0, 0);
	animation: zzf-field-sweep calc(150s / var(--zzf-au-speed)) linear infinite;
	will-change: transform;
	pointer-events: none;
}
@keyframes zzf-field-sweep {
	from { transform: translate3d(-120%, 0, 0); }
	to   { transform: translate3d(320%, 0, 0); }
}

/* SCROLL-LINKED DRIFT — the field is position:fixed and deliberately still,
   which is what made it read as flat. Tying a parallax offset to scroll adds
   life exactly while someone is reading and costs nothing at rest.

   It goes on the two CONTAINERS, never on .zzf-veil or the particles
   themselves: those already animate transform, and a second transform
   animation on the same element does not compose, it wins. Containers have
   no transform of their own, so the two layers stack cleanly.

   @supports means unsupported browsers keep the time-based field and lose
   nothing. Guarded on no-preference so it never fights the block above. */
@supports (animation-timeline: scroll()) {
	@media (prefers-reduced-motion: no-preference) {
		.zzf-field__veils {
			animation: zzf-field-parallax linear;
			animation-timeline: scroll(root block);
		}
		.zzf-field__particles {
			animation: zzf-field-parallax-far linear;
			animation-timeline: scroll(root block);
		}
	}
}
@keyframes zzf-field-parallax     { to { transform: translate3d(0, -6%, 0); } }
@keyframes zzf-field-parallax-far { to { transform: translate3d(0, -11%, 0); } }

/* Everything the user reads sits above the field. .wp-site-blocks children
   are given a stacking position so the fixed layer cannot cover them. */
.wp-site-blocks > main,
.wp-site-blocks > header,
.wp-site-blocks > footer { position: relative; z-index: 1; }

/* Veils and particles now span the viewport rather than a section box. */
.zzf-field__veils     { position: absolute; inset: -12% -8% 6%; }
.zzf-field__particles { position: absolute; inset: 0 0 4% 0; }

/* PERFORMANCE — a fixed layer is cheap, but a fixed layer with animated
   children under a page full of backdrop-filter panels is not. The four
   veils stay off below 640px for that reason: that is the mid-range
   Android budget, and it is most of this traffic.

   The GROUND used to be flattened to a white ramp in here alongside them,
   and that was the wrong thing to drop. A multi-stop gradient on a fixed
   layer is one paint with no per-frame cost — it was never what made the
   phone work. Flattening it threw the entire background away on the
   device most likely to see it, which is why mobile looked like a blank
   page while the desktop had an aurora. The ground stays now. Only the
   atmosphere still moves here, on a single transform, thinned to .78. */
@media (max-width: 640px) {
	.zzf-field__veils { display: none; }
}

@media (prefers-reduced-motion: reduce) {
	.zzf-field .zzf-veil { animation: none !important; }
}

/* ---------- the closing band + footer ----------
   .zzf-outro is a bare shell whose only job is to give the wave band a box
   to sit in directly above the footer. It has no content and no padding. */
.zzf-outro {
	position: relative;
	height: var(--zzf-wave-height);
	margin-bottom: -1px;   /* kill the sub-pixel hairline between band and footer */
}
.zzf-outro .zzf-waves svg { height: var(--zzf-wave-height); }

/* Refill the last wave to the footer's own colour so the ribbon resolves
   INTO the footer. Left at --zzf-au-navy (#08265D) it would band against
   contrast (#0B1B36) — a two-tone step, i.e. exactly the kind of seam this
   whole change exists to remove. */
.zzf-outro .zzf-wave--navy { fill: var(--wp--preset--color--contrast, #0B1B36); }

/* The wave paths all fill down to the viewBox floor and stack on each
   other, so the bottom row is decided by antialiasing between the navy
   path and the red one beneath it. At some widths the red wins by a pixel
   and draws a hairline straight across the join — a bright red 1px rule
   between band and footer, which is precisely the kind of seam this work
   exists to delete. Cap the join in the footer's own colour so the result
   does not depend on subpixel rounding. */
.zzf-outro::after {
	content: "";
	position: absolute;
	left: 0; right: 0; bottom: 0;
	height: 3px;
	z-index: 5;
	background: var(--wp--preset--color--contrast, #0B1B36);
	pointer-events: none;
}

/* ---------------------------------------------------------------------
 * THE FOOTER
 * Same vocabulary as the rest of the site, inverted onto navy: mono
 * letterspaced labels, display wordmark, generous measure, hairlines
 * instead of boxes. It is the one place the page comes to rest, so it
 * gets air rather than density.
 *
 * Colour discipline on dark: body copy is surface-2 (#E9F1FC), never
 * muted (#5A6C89) — muted on contrast is ~3:1 and fails WCAG AA. Links
 * are base white. There is no accent-2 red down here on purpose; the
 * brand ribbon directly above already carries it, and repeating it would
 * compete with the one thing the footer should feel like: a full stop.
 * ------------------------------------------------------------------ */
.zzf-footer {
	position: relative;
	z-index: 1;
	padding-top: clamp(56px, 6vw, 88px) !important;
	padding-bottom: clamp(30px, 3vw, 44px) !important;
}
.zzf-footer > .alignwide.alignwide {
	max-width: var(--zzf-wide, 1440px);
	padding-left: var(--zzf-gutter, 40px);
	padding-right: var(--zzf-gutter, 40px);
	box-sizing: border-box;
}

/* four columns: brand gets the widest measure, link columns are even */
.zzf-footer__grid {
	grid-template-columns: 1.6fr 1fr 1fr 1fr !important;
	gap: clamp(28px, 3.4vw, 64px) !important;
	align-items: start;
}

.zzf-footer__mark a,
.zzf-footer__mark {
	font-size: 19px; letter-spacing: .07em; text-transform: uppercase;
	color: var(--wp--preset--color--base, #FFFFFF);
	text-decoration: none; line-height: 1.1;
}

/* The lockup is navy ink, which is invisible on a navy footer. Knocked out
   to solid white rather than shipping a second recoloured asset — one file
   to maintain, and it stays correct if the logo is ever replaced.
   The red star goes white with it, which is deliberate and consistent with
   the rule above: the footer carries no accent-2, because the brand ribbon
   directly overhead already does and two reds compete. */
.zzf-footer__logo { margin: 0 0 20px !important; }
.zzf-footer__logo img {
	display: block;
	width: 180px; height: auto;
	filter: brightness(0) invert(1);
	opacity: .92;
}
.zzf-footer__descriptor {
	/* margin-inline:0 is load-bearing. WP's constrained layout applies
	   `margin-left/right: auto !important` to children, which CENTRES any
	   element narrow enough to have slack — so this 34ch line sat indented
	   while the wordmark and address above and below it stayed flush left.
	   Same trap as the trust cards; !important is required to beat it. */
	margin: 14px 0 22px !important;
	max-width: 34ch;
	font-size: 14px; line-height: 1.62;
	color: var(--wp--preset--color--surface-2, #E9F1FC);
	opacity: .82;
}
.zzf-footer__addr {
	margin: 0 0 14px;
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 12px; line-height: 1.85;
	color: var(--wp--preset--color--surface-2, #E9F1FC);
	opacity: .74;
}
.zzf-footer__addr a { color: var(--wp--preset--color--surface-2, #E9F1FC); text-decoration: none; }
.zzf-footer__addr a:hover { color: #fff; text-decoration: underline; }

/* Column labels use the same mono eyebrow as every section heading on the
   site, so the footer reads as part of the system rather than a new one. */
.zzf-footer__label {
	margin: 0 0 16px;
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 10.5px; letter-spacing: .18em; text-transform: uppercase;
	color: var(--wp--preset--color--base, #FFFFFF);
	opacity: .55;
}
.zzf-footer__link { margin: 0 0 11px; font-size: 14px; line-height: 1.4; }
.zzf-footer__link a {
	color: var(--wp--preset--color--surface-2, #E9F1FC);
	text-decoration: none;
	transition: color .18s ease, padding-left .18s ease;
}
.zzf-footer__link a:hover { color: #FFFFFF; padding-left: 4px; }

/* The legal column renders wp_navigation 47, so it stays in sync with the
   menu instead of being a second hand-typed list that can drift. */
.zzf-footer__legalnav { gap: 11px !important; }
.zzf-footer__legalnav .wp-block-navigation-item__content {
	color: var(--wp--preset--color--surface-2, #E9F1FC);
	font-size: 14px; text-decoration: none;
	transition: color .18s ease, padding-left .18s ease;
}
.zzf-footer__legalnav .wp-block-navigation-item__content:hover { color: #FFFFFF; padding-left: 4px; }

/* Base line. A single hairline in a navy tint — NOT the light --line
   colour, which at this scale reads as a bright rule across the dark. */
.zzf-footer__base {
	margin-top: clamp(38px, 4.4vw, 62px) !important;
	padding-top: 22px;
	border-top: 1px solid rgba(233, 241, 252, .14);
	gap: 14px !important;
}
.zzf-footer__fine {
	margin: 0;
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 11px; letter-spacing: .04em;
	color: var(--wp--preset--color--surface-2, #E9F1FC);
	opacity: .58;
}

/* THE RUO BAR.
   zzf-core prints this on wp_footer as a direct child of <body>, i.e.
   OUTSIDE the footer template part, and it shipped with a light surface-2
   background. The result was a pale band sitting directly under the navy
   footer on every page — a hard divider at the very bottom of the site,
   undoing the full stop the wave band and footer just built.

   Restyled to read as the footer's closing line. The TEXT is untouched:
   it renders from zzf_ruo_line() in the plugin and must not be edited or
   removed — this is presentation only. Compliance copy still appears on
   every page exactly once. */
.zzf-ruo-footer-bar {
	background: var(--wp--preset--color--contrast, #0B1B36) !important;
	color: var(--wp--preset--color--surface-2, #E9F1FC) !important;
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 11px !important;
	letter-spacing: .04em;
	line-height: 1.6;
	padding: 0 var(--zzf-gutter, 40px) clamp(30px, 3.4vw, 48px) !important;
	text-align: center;
	position: relative;
	z-index: 1;
}
.zzf-ruo-footer-bar > * { opacity: .5; margin: 0; }

@media (max-width: 900px) {
	.zzf-footer__grid { grid-template-columns: 1fr 1fr !important; }
}
@media (max-width: 560px) {
	.zzf-footer__grid { grid-template-columns: 1fr !important; }
	.zzf-footer__base { justify-content: flex-start !important; }
}

/* =====================================================================
 * 13. THE BRAND STRIPE — the page's one dark anchor
 * ---------------------------------------------------------------------
 * Same idea as the footer close, applied mid-page: the hero's wave band
 * descends into a dark block, the content sits inside it, and a mirrored
 * band lifts back out into the light field. Entered and exited the same
 * way, so it reads as a passage the page travels through rather than
 * another slab set on top.
 *
 * ONE stripe only. A second dark block turns an anchor back into stripes,
 * which is the problem this whole change exists to solve.
 * ================================================================== */

/* Sections must butt up against each other with no gap. WP's constrained
   layout puts `margin-block: 1.5rem 0` on every child of post-content, so
   there is a 24px gap between all six sections. While everything was
   transparent that gap just showed more field and was invisible — but the
   moment one section is dark it becomes a bright 24px stripe sitting
   between the hero's wave band and the navy, i.e. a brand new seam. */
.wp-block-post-content > .zzf-has-ribbons { margin-block: 0 !important; }

.zzf-band {
	position: relative;
	z-index: 1;
	/* NO background here — see ::before. A background on the section paints
	   the full box, including behind the rising band, so the navy ran flat
	   to the section edge no matter what the waves did and the stripe still
	   ended on a hard line. The fill has to STOP at the wave line. */
	background: transparent;
	color: var(--wp--preset--color--base, #FFFFFF);
	/* Room for the descending band above and the rising one below, so
	   content never collides with either wave. */
	padding-top: clamp(96px, 11vw, 150px) !important;
	/* Must clear --zzf-wave-height (up to 300px) or the cards sit on the
	   rising band. Keep this above that value if either is retuned. */
	padding-bottom: clamp(200px, 27vw, 360px) !important;
}

/* The stripe's fill, stopping exactly where the rising band starts. Below
   this the flipped waves paint navy above their own line and nothing below
   it, so the real page field — the fixed gradient, not a flat approximation
   of it — shows through and the stripe dissolves upward into the light. */
.zzf-band::before {
	content: "";
	position: absolute;
	left: 0; right: 0; top: 0;
	bottom: calc(var(--zzf-wave-height) - 2px);   /* -2px: overlap the band, no hairline */
	background: var(--wp--preset--color--contrast, #0B1B36);
	z-index: 0;
	pointer-events: none;
}

/* Type on dark. muted (#5A6C89) on contrast is ~3:1 and fails AA, so the
   sub-copy steps up to surface-2 exactly as it does in the footer. */
.zzf-band .zzf-eyebrow      { color: var(--wp--preset--color--surface-2, #E9F1FC); opacity: .78; }
.zzf-band .zzf-trust__head h2,
.zzf-band h2                { color: var(--wp--preset--color--base, #FFFFFF); }
.zzf-band .zzf-trust__sub   { color: var(--wp--preset--color--surface-2, #E9F1FC); }

/* Cards stay LIGHT on the dark stripe — near-opaque white rather than the
   58% fill used over the pale field, which over navy would read as murky
   blue-grey and drag the body copy under AA. Crisp white panels on navy is
   the whole point of the stripe. Text inside stays dark; do not invert it. */
.zzf-band .zzf-glass {
	background: rgba(255, 255, 255, .95);
	border-color: rgba(255, 255, 255, .5);
	box-shadow: 0 30px 70px -24px rgba(0, 0, 0, .5);
}
.zzf-band .zzf-glass::after { opacity: .45; }
.zzf-band .zzf-trust__claim { color: var(--wp--preset--color--contrast, #0B1B36); }

/* --- the two joins ---
   Both the descending band (hero) and the rising one must resolve into the
   stripe's own colour. Left at --zzf-au-navy (#08265D) they band against
   contrast (#0B1B36), and the stacked wave paths antialias a 1px red
   hairline across the seam — which is exactly what the screenshot showed
   at the hero edge. Refill, then cap, the same as the footer. */
.zzf-hero .zzf-wave--navy,
.zzf-riser .zzf-wave--navy { fill: var(--wp--preset--color--contrast, #0B1B36); }

.zzf-hero::after {
	content: "";
	position: absolute;
	left: 0; right: 0; bottom: 0;
	height: 3px;
	z-index: 5;
	background: var(--wp--preset--color--contrast, #0B1B36);
	pointer-events: none;
}

/* --- the rising band ---
   The same two wave parts, mirrored. scaleY(-1) turns the descent into a
   climb, so the navy edge meets the stripe above and the light edge opens
   into the field below. Nothing new is drawn — this is the other half of
   the band that brought us in. */
.zzf-riser {
	position: absolute;
	left: 0; right: 0; bottom: 0;
	height: var(--zzf-wave-height);
	z-index: 2;
	pointer-events: none;
	/* The riser is a group inside a constrained section, so WP caps it at
	   contentSize and centres it with margin-inline:auto !important. Left
	   alone it renders as a ~720px ribbon floating in the middle of the
	   stripe instead of a full-width band. */
	width: auto;
	max-width: none !important;
	margin: 0 !important;
}

/* Cap the riser's TOP edge — the third occurrence of the same hairline.
   Every wave path closes with `L1560,320 L-120,320 Z`, so all four fills
   coincide exactly at the SVG edge. Navy is painted last and should win,
   but preserveAspectRatio="none" scales 320 -> 300 and the edge pixels
   blend, letting the red underneath fringe through as a thin line clean
   across the page — precisely where the stripe is supposed to meet the
   rising band invisibly.

   The hero join (.zzf-hero::after) and the footer join (.zzf-outro::after)
   were already capped. This one was missed because flipping the SVG rather
   than the container moved the join from the riser's bottom edge to its
   top. Any new wave placement needs a cap on whichever edge meets a fill. */
.zzf-riser::before {
	content: "";
	position: absolute;
	left: 0; right: 0; top: 0;
	height: 4px;
	z-index: 5;
	background: var(--wp--preset--color--contrast, #0B1B36);
	pointer-events: none;
}

/* Flip the SVG, NOT the container.
   Flipping .zzf-riser itself also flips where its children sit: the waves
   are anchored bottom:0, so a container flip threw them to the visual TOP
   of the riser, leaving the ribbon stranded mid-stripe with a hard navy
   edge below it. Transforming only the SVG keeps the band pinned to the
   stripe's bottom edge while inverting the fill — navy above the wave
   line, nothing below it, so the page field shows through and the stripe
   dissolves upward into the light instead of stopping against it. */
.zzf-riser .zzf-waves svg {
	height: var(--zzf-wave-height);
	transform: scaleY(-1);
}

/* =====================================================================
 * 14. SHOP + PRODUCT  (classic WooCommerce)
 * ---------------------------------------------------------------------
 * The Woo templates render through wp:woocommerce/legacy-template, i.e.
 * classic PHP markup — ul.products > li.product with float layout — not
 * blocks. None of the block-layout rules above reach it, which is why the
 * shop arrived completely unstyled.
 *
 * THE MESH RULE APPLIES HERE FIRST: product cards are DATA surfaces.
 * Flat, hairlines, NEVER glass. A grid of 16-40 backdrop-filter cards
 * destroys scroll on mid-range Android, which is most of this traffic.
 * ================================================================== */

/* ---------- the Woo content shell ----------
   Woo wraps its legacy output in .content-area.alignwide, which picks up
   WP's alignwide rule and therefore theme.json's wideSize (1200px) — not
   the site's --zzf-wide (1440px). Left alone the shop sits visibly
   narrower than every other page. Two classes beats WP's one, so no
   !important needed here. Vertical padding matches the section rhythm. */
/* The doubled .alignwide is required, not stylistic. WP's own alignwide
   rule is also (0,2,0) and is emitted inline in <head>, i.e. after this
   file, so a matching selector loses on source order — the padding below
   applied while the max-width silently did not and the shop stayed at
   theme.json's 1200px while every other page ran 1440. Third class wins
   on specificity instead of hoping for load order. Same fix as
   .zzf-has-ribbons > .alignwide.alignwide in section 12. */
.content-area.alignwide.alignwide {
	max-width: var(--zzf-wide, 1440px);
	padding-left: var(--zzf-gutter, 40px);
	padding-right: var(--zzf-gutter, 40px);
	padding-top: clamp(44px, 5vw, 76px);
	padding-bottom: clamp(70px, 8vw, 110px);
	box-sizing: border-box;
}

/* ---------- shop header ---------- */
.woocommerce-breadcrumb {
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
	color: var(--wp--preset--color--muted, #5A6C89);
	margin: 0 0 10px;
}
.woocommerce-breadcrumb a { color: var(--wp--preset--color--accent, #14346E); text-decoration: none; }
.woocommerce-products-header__title {
	font-size: clamp(30px, 3.6vw, 46px);
	line-height: 1.05;
	margin: 0 0 8px;
}
.woocommerce-result-count {
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 11.5px; color: var(--wp--preset--color--muted, #5A6C89); margin: 0;
}
/* The native select is the one control Woo cannot style for us. Same
   hairline treatment as the spec tables, so it reads as instrumentation. */
.woocommerce-ordering select {
	appearance: none; -webkit-appearance: none;
	background: var(--wp--preset--color--base, #FFFFFF);
	border: 1px solid var(--wp--preset--color--line, #E3EBF6);
	border-radius: 8px; padding: 10px 34px 10px 14px;
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 12px; color: var(--wp--preset--color--contrast, #0B1B36); cursor: pointer;
}

/* ---------- the grid ----------
   Classic Woo floats li.product with inline percentage widths. Force a
   real grid so the row is even and the gutters are ours. */
ul.products {
	display: grid !important;
	grid-template-columns: repeat(4, minmax(0, 1fr));
	gap: clamp(18px, 1.8vw, 30px);
	max-width: none !important;
	margin: 34px 0 0 !important;
	padding: 0 !important;
	list-style: none !important;
}
ul.products::before, ul.products::after { content: none !important; display: none !important; }
ul.products li.product {
	width: auto !important;
	margin: 0 !important;
	float: none !important;
	clear: none !important;
	display: flex; flex-direction: column;
	background: var(--wp--preset--color--base, #FFFFFF);
	border: 1px solid var(--wp--preset--color--line, #E3EBF6);
	border-radius: 14px;
	/* Woo ships `.woocommerce ul.products li.product { padding: 0 }` at
	   (0,3,0), which beats this selector — without !important the title,
	   price and button all sit flush against the card edge. */
	padding: 14px 14px 18px !important;
	transition: border-color .22s ease, box-shadow .22s ease, transform .22s ease;
}
ul.products li.product:hover {
	border-color: rgba(20, 52, 110, .28);
	box-shadow: 0 18px 40px -26px rgba(11, 27, 54, .45);
	transform: translateY(-2px);
}

/* ---------- the image slot ----------
   There is no product photography yet AND Woo's own placeholder file is
   missing, so every card currently renders a BROKEN image. A broken <img>
   still paints its background, so give the slot a clinical panel and zero
   the font-size to suppress the alt text. An absent photo then reads as a
   deliberate spec panel rather than a broken site, and when real images
   land they simply draw over it with no CSS change. */
ul.products li.product img {
	width: 100% !important;
	height: auto;
	aspect-ratio: 1 / 1;
	object-fit: cover;
	margin: 0 0 14px !important;
	border-radius: 10px;
	background: linear-gradient(158deg, #F7FBFF 0%, #E9F1FC 62%, #DCE7F6 100%);
	font-size: 0;
	color: transparent;
	border: 1px solid var(--wp--preset--color--line, #E3EBF6);
}

/* ---------- card type ---------- */
ul.products li.product .woocommerce-loop-product__title {
	font-family: var(--wp--preset--font-family--display, serif) !important;
	font-size: 17px !important;
	line-height: 1.25;
	color: var(--wp--preset--color--contrast, #0B1B36);
	margin: 0 0 6px !important;
	padding: 0 !important;
}
ul.products li.product .price {
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace) !important;
	font-size: 14px !important;
	color: var(--wp--preset--color--contrast, #0B1B36) !important;
	margin: 0 0 14px !important;
}
ul.products li.product .price del { color: var(--wp--preset--color--muted, #5A6C89); opacity: .7; }
ul.products li.product .price ins { text-decoration: none; }

/* Button pinned to the card floor so every button in the row shares a
   baseline regardless of how long each product name runs. */
ul.products li.product .button {
	margin-top: auto !important;
	display: inline-flex; align-items: center; justify-content: center;
	background: var(--wp--preset--color--accent, #14346E);
	color: #fff; border: 0; border-radius: 6px; cursor: pointer;
	padding: 13px 18px;
	font-size: 11.5px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase;
	transition: background .22s ease;
}
ul.products li.product .button:hover { background: #0A1F47; }
ul.products li.product .added_to_cart { display: none; }

/* ---------- pagination ---------- */
/* PAGINATION — and a specificity lesson that had already been written down.
   These rules used to be `.woocommerce-pagination .page-numbers a` (0,2,1).
   WooCommerce ships `.woocommerce nav.woocommerce-pagination ul li a` at
   (0,2,4) from woocommerce.css, which loads AFTER glass.css, so every one of
   them lost silently. Measured on the live shop 2026-08-20: the links kept
   Woo's 0.5em padding and a 16px box instead of a 40px square, the <ul> kept
   Woo's `border-width: 1px 0 1px 1px` so the buttons read as one welded
   strip, and the current page wore Woo's grey #e9e6ed instead of the brand
   navy. It looked like a styling mistake; it was a cascade loss.

   Every selector below now outranks Woo's. `.current.current` is doubled on
   purpose — the plain version ties Woo's own `li span.current` and loses on
   load order. Same move as `.alignwide.alignwide`. Do not "tidy" either. */
.woocommerce nav.woocommerce-pagination { text-align: center; }
.woocommerce nav.woocommerce-pagination ul.page-numbers {
	display: flex; gap: 8px; list-style: none; padding: 0; margin: 40px 0 0;
	border: 0; justify-content: center; white-space: normal;
}
.woocommerce nav.woocommerce-pagination ul.page-numbers li {
	border: 0; margin: 0; padding: 0; float: none;
	display: inline-flex; overflow: visible;
}
.woocommerce nav.woocommerce-pagination ul.page-numbers li a.page-numbers,
.woocommerce nav.woocommerce-pagination ul.page-numbers li span.page-numbers {
	display: inline-flex; align-items: center; justify-content: center;
	box-sizing: border-box;
	min-width: 40px; height: 40px; padding: 0 13px; margin: 0;
	border: 1px solid var(--wp--preset--color--line, #E3EBF6);
	border-radius: 10px; background: var(--wp--preset--color--base, #FFFFFF);
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 13px; line-height: 1; text-decoration: none;
	color: var(--wp--preset--color--accent, #14346E);
	transition: background .2s ease, border-color .2s ease, color .2s ease;
}
.woocommerce nav.woocommerce-pagination ul.page-numbers li a.page-numbers:hover,
.woocommerce nav.woocommerce-pagination ul.page-numbers li a.page-numbers:focus {
	background: var(--wp--preset--color--surface-2, #E9F1FC);
	border-color: #C5D8F2;
	color: var(--wp--preset--color--accent, #14346E);
}
/* Doubled class: the single version ties Woo's `li span.current` rule. */
.woocommerce nav.woocommerce-pagination ul.page-numbers li span.current.current,
.woocommerce nav.woocommerce-pagination ul.page-numbers li a.current.current {
	background: var(--wp--preset--color--accent, #14346E);
	border-color: var(--wp--preset--color--accent, #14346E);
	color: #fff;
}
/* The prev/next arrows are the same square, without the number's side padding. */
.woocommerce nav.woocommerce-pagination ul.page-numbers li a.prev,
.woocommerce nav.woocommerce-pagination ul.page-numbers li a.next {
	padding: 0; font-size: 15px;
}

/* ---------- single product ---------- */
.single-product div.product .product_title { font-size: clamp(28px, 3.2vw, 42px); line-height: 1.08; margin: 0 0 10px; }
.single-product div.product .price {
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 21px; color: var(--wp--preset--color--contrast, #0B1B36);
}
.single-product div.product .woocommerce-product-gallery img {
	border-radius: 14px;
	border: 1px solid var(--wp--preset--color--line, #E3EBF6);
	background: linear-gradient(158deg, #F7FBFF 0%, #E9F1FC 62%, #DCE7F6 100%);
	font-size: 0; color: transparent;
}
.single-product div.product button.single_add_to_cart_button {
	background: var(--wp--preset--color--accent, #14346E); color: #fff;
	border: 0; border-radius: 6px; padding: 16px 30px; cursor: pointer;
	font-size: 12.5px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase;
}
.single-product div.product button.single_add_to_cart_button:hover { background: #0A1F47; }

/* ---------- responsive ---------- */
@media (max-width: 1120px) { ul.products { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 860px)  { ul.products { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px)  { ul.products { grid-template-columns: minmax(0, 1fr); } }

/* =====================================================================
 * 15. INTERIOR PAGES — about, contact, FAQ, COA library, legal
 * ---------------------------------------------------------------------
 * These pages were authored before the field existed, so every section
 * carries an OPAQUE background (base / surface / surface-2, alternating)
 * to fake separation. Those fills sit on top of the fixed field, hiding
 * it completely and banding the page — the same stacked-brick problem the
 * home page had, by a different mechanism.
 *
 * Sections are made transparent so the one continuous field shows through
 * and interior pages match the home page. Separation now comes from
 * rhythm and type, not from alternating fills.
 *
 * Deliberately scoped to DIRECT children of post-content, i.e. the
 * section level only. Nested panels that legitimately want a fill (spec
 * tables, cards, the brand stripe) are untouched — .zzf-band paints via
 * ::before precisely so it survives this.
 * ================================================================== */
.wp-block-post-content > .has-base-background-color,
.wp-block-post-content > .has-surface-background-color,
.wp-block-post-content > .has-surface-2-background-color {
	background-color: transparent !important;
}

/* --- the dividers ---
   Interior pages were authored with TWO kinds of section separator, both
   of which now read as seams across a continuous field:

   1. a literal `border-bottom: 1px solid var(--line)` on some sections —
      a hairline rule drawn straight across the page, and
   2. WP's default `margin-block: 1.5rem` between every section, leaving a
      24px trough at each joint.

   Both are removed at the SECTION level only. Separation comes from
   rhythm and type now, the same way it does on the home page. Borders
   inside content — spec tables, card rules, the FAQ list — are untouched,
   because this only matches direct children of post-content. */
.wp-block-post-content > * {
	border-top: 0 !important;
	border-bottom: 0 !important;
	margin-block: 0 !important;
}

/* Section rhythm on interior pages, matching the home page cadence. */
.wp-block-post-content > .alignfull {
	padding-top: clamp(56px, 6.5vw, 96px);
	padding-bottom: clamp(56px, 6.5vw, 96px);
}

/* Interior pages get the same wide measure and gutter as everything else. */
.wp-block-post-content > .alignfull > .alignwide.alignwide {
	max-width: var(--zzf-wide, 1440px);
	padding-left: var(--zzf-gutter, 40px);
	padding-right: var(--zzf-gutter, 40px);
	box-sizing: border-box;
}

/* Centred intro blocks (page heroes) were capped at 760px by an authored
   contentSize while their section ran full-bleed, so the page opened with a
   narrow column floating in a wide field. Widened to a proper display
   measure — but NOT to the full 1440: prose at that width is unreadable,
   and "full width" is a property of the section, not of the line length. */
.wp-block-post-content > .alignfull > .has-text-align-center {
	max-width: 900px !important;
}
.wp-block-post-content > .alignfull > h2.has-text-align-center,
.wp-block-post-content > .alignfull > h1.has-text-align-center {
	max-width: 18ch !important;
	font-size: clamp(32px, 4vw, 52px);
	line-height: 1.06;
}

/* The About panels that replaced the empty image blocks. Same frame as the
   home page's showcase, so the page reads as one system. */
.zzf-about__panel {
	padding: clamp(28px, 3vw, 44px) clamp(24px, 2.6vw, 38px) !important;
	max-width: none !important;
}
.zzf-about__panel .zzf-spec { margin: 14px 0 0; font-size: 12.5px; line-height: 2.1; }
.zzf-about__panel .zzf-finelabel { margin: 0; }

/* =====================================================================
 * 16. CART + CHECKOUT  (classic WooCommerce)
 * ---------------------------------------------------------------------
 * These render from [woocommerce_cart] / [woocommerce_checkout], i.e.
 * classic PHP markup. That is a settled decision, not an oversight: the
 * compliance hooks (RUO block, attestation checkbox) do not fire on the
 * block cart/checkout, and an audit previously caught the attestation
 * going missing. Everything below styles classic markup only.
 *
 * THE MESH RULE DECIDES THE TREATMENT. Checkout is a DATA surface —
 * flat, hairlines, mono for anything numeric, and NEVER glass. Money and
 * totals must read as instrument output; a blurred, translucent panel
 * under a price is the fastest way to make a store feel unserious. The
 * field still shows through behind the page, which is what ties these
 * pages to the rest of the site — the panels themselves stay clinical.
 * ================================================================== */

.zzf-wooshell { padding-bottom: clamp(70px, 8vw, 110px); }

/* Woo caps .woocommerce at 1000px of its own accord, so cart and checkout
   sat narrower than every other page regardless of the wide measure above.
   Scoped to this shell so shop/product archives are untouched. */
.zzf-wooshell .woocommerce { max-width: none !important; width: 100%; }

.zzf-wooshell .alignwide.alignwide {
	max-width: var(--zzf-wide, 1440px);
	padding-left: var(--zzf-gutter, 40px);
	padding-right: var(--zzf-gutter, 40px);
	box-sizing: border-box;
}

.zzf-wooshell__title {
	font-size: clamp(30px, 3.6vw, 46px);
	line-height: 1.05;
	margin: clamp(40px, 5vw, 72px) 0 8px;
}

/* Compliance notices keep their own weight — never dimmed into chrome. */
.zzf-wooshell .zzf-ruo,
.zzf-wooshell .zzf-ruo-block {
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 12px;
	line-height: 1.7;
	color: var(--wp--preset--color--contrast, #0B1B36);
	background: var(--wp--preset--color--surface-2, #E9F1FC);
	border-radius: 10px;
	padding: 14px 18px;
	margin: 0 0 26px;
}

/* ---------- shared surfaces: flat, hairline, clinical ---------- */
.woocommerce table.shop_table {
	border: 1px solid var(--wp--preset--color--line, #E3EBF6) !important;
	border-radius: 14px !important;
	border-collapse: separate !important;
	border-spacing: 0 !important;
	background: var(--wp--preset--color--base, #FFFFFF);
	overflow: hidden;
	margin: 0 0 28px;
}
.woocommerce table.shop_table th {
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 10.5px;
	letter-spacing: .16em;
	text-transform: uppercase;
	color: var(--wp--preset--color--muted, #5A6C89);
	font-weight: 500;
	padding: 16px 18px;
	border-bottom: 1px solid var(--wp--preset--color--line, #E3EBF6);
}
.woocommerce table.shop_table td {
	padding: 18px;
	border-top: 1px solid rgba(20, 52, 110, .08);
	vertical-align: middle;
}
.woocommerce table.shop_table tr:first-child td { border-top: 0; }

/* Every number is mono. A price in the body face reads as marketing; in
   mono it reads as a figure. Same rule as the spec tables. */
.woocommerce .amount,
.woocommerce .woocommerce-Price-amount,
.woocommerce td.product-price,
.woocommerce td.product-subtotal,
.woocommerce .order-total .amount {
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace) !important;
	color: var(--wp--preset--color--contrast, #0B1B36);
}
.woocommerce .order-total .amount { font-size: 18px; }

.woocommerce td.product-name a {
	font-family: var(--wp--preset--font-family--display, serif);
	font-size: 16px;
	color: var(--wp--preset--color--contrast, #0B1B36);
	text-decoration: none;
}
.woocommerce td.product-name a:hover { color: var(--wp--preset--color--accent, #14346E); }
.woocommerce td.product-thumbnail img {
	width: 62px; height: 62px;
	object-fit: cover;
	border-radius: 10px;
	border: 1px solid var(--wp--preset--color--line, #E3EBF6);
	background: linear-gradient(158deg, #F7FBFF 0%, #E9F1FC 62%, #DCE7F6 100%);
}

/* ---------- cart layout ---------- */
@media (min-width: 981px) {
	/* Items left, totals right — the standard scan order, and it keeps the
	   totals in view while quantities are edited. */
	/* :has(> .woocommerce-cart-form) is doing real work here, not tidiness.
	   A bare `.woocommerce-cart .woocommerce` ALSO matches the store-notices
	   block, which carries the same class — so the grid was being applied to
	   a wrapper holding one notices div while the actual cart container kept
	   Woo's float layout. Everything downstream then measured against the
	   wrong box: the totals panel sat at Woo's floated width no matter what
	   the grid column said. Match the element that actually holds the cart. */
	.woocommerce-cart .woocommerce:has(> .woocommerce-cart-form) {
		display: grid;
		/* The totals panel needs real width, not leftovers. Fee and shipping
		   labels are client-configured and can be long — "Zelle payment
		   discount" already wrapped to three lines at 320px, which is what
		   made the panel look cramped. 420px min holds a two-word label on
		   one line and a three-word label on two. */
		grid-template-columns: minmax(0, 1.45fr) minmax(420px, 1fr);
		gap: clamp(24px, 3vw, 48px);
		/* stretch, not start. The two columns are different lengths by nature,
		   and a short cart left the items card ending 67px above the checkout
		   button — which reads as a layout that broke rather than one that
		   breathes. Stretching lets the items card grow to the row height so
		   its bottom edge lands on the button's. .cart-collaterals opts out
		   below, so a long cart never drags the summary down to match it. */
		align-items: stretch;
	}
	/* The card chrome moves off the table and onto the form. The form is the
	   grid item, so it is the only box here that knows the row height — with
	   the border and background left on the table, the card stops at the last
	   row however tall the column gets, and stretching buys nothing. The table
	   keeps every internal rule and gives up only its outer edge. */
	.woocommerce-cart .woocommerce-cart-form {
		grid-column: 1;
		margin: 0;
		display: flex;
		flex-direction: column;
		background: var(--wp--preset--color--base, #FFFFFF);
		border: 1px solid var(--wp--preset--color--line, #E3EBF6);
		border-radius: 14px;
		overflow: hidden;
	}
	.woocommerce-cart .woocommerce-cart-form > table.shop_table {
		border: 0 !important;
		border-radius: 0 !important;
		background: transparent !important;
		margin: 0 !important;
		/* Fill the stretched card instead of ending partway down it. Left at
		   its natural height the table stopped at the coupon row and dumped
		   the whole surplus below it as dead white — 83px at two items, and
		   worse the shorter the cart. */
		flex: 1 1 auto;
		height: 100%;
	}
	/* Which row absorbs the surplus is otherwise up to the browser, and the
	   default is to spread it across every row so the line items go slack. A
	   declared height makes a row take its content height and no more, so all
	   of it lands on the one auto row — the coupon band. Its td already
	   vertical-aligns middle, so the field and buttons centre in the space
	   rather than sitting on top of it. */
	.woocommerce-cart .woocommerce-cart-form > table.shop_table > thead > tr,
	.woocommerce-cart .woocommerce-cart-form > table.shop_table > tbody > tr.cart_item { height: 1px; }
	/* Woo's own table.shop_table th (9px 12px) loads after this stylesheet and
	   wins the tie, so the header row sat 15px shorter than the summary's.
	   Forced here rather than globally: the totals and order-review tables use
	   th for row labels, not headers, and already set their own padding. */
	.woocommerce-cart .woocommerce-cart-form > table.shop_table thead th {
		padding: 16px 18px !important;
		line-height: 1.6;
	}
	/* width:100%, not auto. Woo's classic cart sizes .cart-collaterals as a
	   float, and `auto` left it shrink-to-fit at 420px inside a 516px grid
	   column — so widening the column changed nothing and every percentage
	   inside it resolved against the wrong box. */
	.woocommerce-cart .cart-collaterals {
		grid-column: 2;
		width: 100% !important;
		max-width: none !important;
		float: none !important;
		/* Opts out of the stretch above. The summary is a fixed-length object;
		   stretching it to a ten-line cart would open a few hundred pixels of
		   dead space above the button. Sticky instead, matching #order_review
		   on checkout — a long cart scrolls past a summary that stays put, so
		   the total and the button are never off screen while quantities are
		   being edited. */
		align-self: start;
		position: sticky;
		top: calc(var(--zzf-header-h, 76px) + 20px);
	}
	.woocommerce-cart .cart-collaterals .cart_totals {
		width: 100% !important;
		max-width: none !important;
		float: none !important;
	}
}
.woocommerce-cart .cart-collaterals .cross-sells { display: none; }

/* ---------- the totals panel ----------
   Its own spacing, looser than the line-items table. This is the panel a
   shopper re-reads before committing, so it gets air rather than density.

   The explicit 100% widths matter: Woo's classic cart floats .cart_totals,
   so even inside a grid column it stays shrink-to-fit and the table sizes
   to its content. Widening the column alone did nothing — the panel sat at
   420px inside a 516px column, and the percentage label column below was
   resolving against the narrow table rather than the space available. */
.woocommerce .cart_totals,
.woocommerce .cart_totals table.shop_table {
	width: 100% !important;
	max-width: none !important;
}
/* table-layout:fixed makes the label-column width below authoritative.
   Under the default `auto`, a width on a cell is only a suggestion — the
   browser sizes columns from content, which is why setting 48% changed
   nothing and the fee label kept wrapping. */
.woocommerce .cart_totals table.shop_table { table-layout: fixed; }
.woocommerce .cart_totals table.shop_table th,
.woocommerce .cart_totals table.shop_table td {
	padding: 18px 22px;
	vertical-align: top;
}
.woocommerce .cart_totals table.shop_table th {
	/* Fixed label column so figures line up down the right edge instead of
	   shifting with each label's length. Lighter tracking than the line-item
	   headers — at .16em a three-word fee label wraps for no good reason. */
	/* 48% keeps a three-word fee label ("Zelle payment discount") on one
	   line. The value side still clears a formatted total and the shipping
	   radio rows comfortably, so nothing is traded away for it. */
	width: 48%;
	letter-spacing: .1em;
	line-height: 1.5;
	white-space: normal;
}
.woocommerce .cart_totals table.shop_table td { text-align: right; }
.woocommerce .cart_totals .order-total th,
.woocommerce .cart_totals .order-total td {
	border-top: 1px solid rgba(20, 52, 110, .16);
	padding-top: 20px;
	padding-bottom: 20px;
}

/* Shipping choices: one per line, radio and label on a shared baseline, and
   the price never orphaned onto its own row. */
.woocommerce .cart_totals #shipping_method,
.woocommerce .woocommerce-shipping-methods {
	list-style: none;
	margin: 0;
	padding: 0;
}
.woocommerce .cart_totals #shipping_method li {
	display: flex;
	align-items: baseline;
	gap: 9px;
	margin: 0 0 10px;
	text-align: left;
}
.woocommerce .cart_totals #shipping_method li:last-child { margin-bottom: 0; }
.woocommerce .cart_totals #shipping_method label {
	display: inline;
	font-size: 14.5px;
	line-height: 1.5;
	color: var(--wp--preset--color--contrast, #0B1B36);
}
.woocommerce .cart_totals .woocommerce-shipping-destination {
	margin: 12px 0 0;
	font-size: 13px;
	line-height: 1.55;
	color: var(--wp--preset--color--muted, #5A6C89);
	text-align: left;
}
.woocommerce .cart_totals .shipping-calculator-button {
	font-size: 13px;
	color: var(--wp--preset--color--accent, #14346E);
}

/* The heading is the panel's header strip, not a label floating above it.
   Two things come of that. The summary's top edge lands level with the items
   table's top edge, so the eye reads one horizon across the two columns
   instead of a 26px step. And the summary gains the same mono/uppercase
   header row the items table already has, which is what makes the pair read
   as one system rather than two cards that happen to sit side by side.

   No element wraps the heading and the table together, so the card is drawn
   across both: the h2 carries the top corners, the table carries the bottom
   ones, and the seam is the h2's bottom border with the table's top border
   removed — one hairline, not two stacked. */
.woocommerce .cart_totals h2 {
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 10.5px;
	letter-spacing: .18em;
	text-transform: uppercase;
	color: var(--wp--preset--color--muted, #5A6C89);
	margin: 0;
	padding: 16px 22px;
	line-height: 1.6;
	background: var(--wp--preset--color--base, #FFFFFF);
	border: 1px solid var(--wp--preset--color--line, #E3EBF6);
	border-radius: 14px 14px 0 0;
}
.woocommerce .cart_totals > table.shop_table {
	border-top: 0 !important;
	border-radius: 0 0 14px 14px !important;
	margin: 0 0 28px !important;
}
/* The button is the column's last ink. Woo pads this wrapper 1em top and
   bottom and hangs another 1em under the anchor, so the column's box ended
   29px below the button — and the items card opposite, stretching to the
   column, overshot the button by exactly that. */
.woocommerce .wc-proceed-to-checkout {
	margin: 0 !important;
	padding: 0 !important;
}
.woocommerce .wc-proceed-to-checkout a.checkout-button {
	display: block;
	margin: 0 !important;
}

/* Quantity stepper and coupon field share the input treatment. */
.woocommerce .quantity input.qty,
.woocommerce input.input-text,
.woocommerce textarea,
.woocommerce select,
.woocommerce .select2-selection {
	background: var(--wp--preset--color--base, #FFFFFF) !important;
	border: 1px solid var(--wp--preset--color--line, #E3EBF6) !important;
	border-radius: 8px !important;
	padding: 13px 14px !important;
	font-size: 15px !important;
	color: var(--wp--preset--color--contrast, #0B1B36) !important;
	height: auto !important;
	box-shadow: none !important;
	transition: border-color .18s ease, box-shadow .18s ease;
}
.woocommerce .quantity input.qty { width: 74px !important; text-align: center; }
.woocommerce input.input-text:focus,
.woocommerce textarea:focus,
.woocommerce select:focus {
	border-color: var(--wp--preset--color--accent, #14346E) !important;
	box-shadow: 0 0 0 3px rgba(20, 52, 110, .12) !important;
	outline: none !important;
}

/* ---------- buttons ---------- */
.woocommerce .button,
.woocommerce button.button,
.woocommerce input.button,
.woocommerce a.checkout-button,
.woocommerce #place_order {
	background: var(--wp--preset--color--accent, #14346E) !important;
	color: #fff !important;
	border: 0 !important;
	border-radius: 6px !important;
	padding: 15px 26px !important;
	font-size: 12px !important;
	font-weight: 600 !important;
	letter-spacing: .12em !important;
	text-transform: uppercase !important;
	cursor: pointer;
	transition: background .2s ease;
}
.woocommerce .button:hover,
.woocommerce button.button:hover,
.woocommerce a.checkout-button:hover,
.woocommerce #place_order:hover { background: #0A1F47 !important; }

/* Secondary actions must not compete with the primary one. */
.woocommerce .actions .button[name="update_cart"],
.woocommerce .button.wc-backward {
	background: transparent !important;
	color: var(--wp--preset--color--accent, #14346E) !important;
	border: 1px solid var(--wp--preset--color--line, #E3EBF6) !important;
}
.woocommerce .actions .button[name="update_cart"]:hover,
.woocommerce .button.wc-backward:hover {
	background: rgba(187, 212, 247, .28) !important;
}
/* box-sizing is the whole reason the checkout button hung 52px past the right
   edge of the summary above it. Woo's buttons are content-box, so `width:100%`
   meant 100% of the column PLUS 26px of padding on each side — the overflow
   was exactly 2 × 26. Nothing about the column or the panel was wrong. */
.woocommerce a.checkout-button,
.woocommerce #place_order {
	width: 100%;
	box-sizing: border-box !important;
	text-align: center;
	padding: 18px 26px !important;
	font-size: 13px !important;
}

/* ---------- checkout ---------- */
@media (min-width: 981px) {
	.woocommerce-checkout form.checkout {
		display: grid;
		grid-template-columns: minmax(0, 1.15fr) minmax(340px, .85fr);
		gap: clamp(24px, 3vw, 52px);
		align-items: start;
	}
	/* EXPLICIT ROWS, and they are load-bearing. #order_review_heading is a
	   13px element that Woo prints as a SIBLING of #order_review, so with
	   auto placement it claimed row 1 of column 2 and pushed the review into
	   row 2 — whose top edge is the bottom of the 1400px-tall form. The
	   summary rendered correctly in the right-hand column and completely
	   below the fold, which reads as "the order summary is at the bottom of
	   the page". Measured on the live checkout 2026-08-22: review at y=1921
	   against a form ending at y=1875. */
	.woocommerce-checkout #customer_details { grid-column: 1; grid-row: 1; width: auto !important; float: none !important; }
	.woocommerce-checkout #customer_details .col-1,
	.woocommerce-checkout #customer_details .col-2 { width: auto !important; float: none !important; }
	.woocommerce-checkout #order_review_heading,
	.woocommerce-checkout #order_review { grid-column: 2; width: auto !important; float: none !important; }
	/* The heading comes OUT of grid sizing rather than being sized around.
	   Two earlier attempts failed for the same reason: an in-flow grid item
	   contributes to intrinsic track sizing, so with the form spanning both
	   rows its surplus height landed in row 1 and opened a ~116px gap above
	   the summary. min-content did not help — a spanning item contributes to
	   min-content tracks too.

	   Absolutely positioned grid children make no track contribution at all,
	   so the heading stops affecting the rows entirely and both columns start
	   at the same y. It stays in the DOM and stays in the accessibility tree
	   as a real <h3>; it is only removed visually, because the table below it
	   already carries its own PRODUCT / SUBTOTAL header row. */
	.woocommerce-checkout #order_review { grid-row: 1; }
	.woocommerce-checkout #order_review_heading { margin-top: 0; }
	/* The summary follows on long forms so the total stays visible. */
	.woocommerce-checkout #order_review { position: sticky; top: calc(var(--zzf-header-h, 76px) + 20px); }
}

.woocommerce-checkout h3,
.woocommerce-checkout #order_review_heading {
	font-family: var(--wp--preset--font-family--mono, ui-monospace, monospace);
	font-size: 10.5px;
	letter-spacing: .18em;
	text-transform: uppercase;
	color: var(--wp--preset--color--muted, #5A6C89);
	margin: 0 0 16px;
}
.woocommerce-checkout .form-row { margin: 0 0 16px; padding: 0; }
.woocommerce-checkout .form-row label {
	display: block;
	margin: 0 0 7px;
	font-size: 12px;
	letter-spacing: .06em;
	color: var(--wp--preset--color--muted, #5A6C89);
}
.woocommerce-checkout .form-row .required { color: var(--wp--preset--color--accent-2, #C8102E); text-decoration: none; }
.woocommerce-checkout .woocommerce-billing-fields,
.woocommerce-checkout .woocommerce-shipping-fields,
.woocommerce-checkout .woocommerce-additional-fields {
	background: var(--wp--preset--color--base, #FFFFFF);
	border: 1px solid var(--wp--preset--color--line, #E3EBF6);
	border-radius: 14px;
	padding: clamp(20px, 2.4vw, 30px);
	margin: 0 0 20px;
}

/* Payment methods — flat rows, one accent border on the chosen rail. */
.woocommerce-checkout #payment {
	background: transparent !important;
	border-radius: 0 !important;
}
.woocommerce-checkout #payment ul.payment_methods {
	border: 0 !important;
	padding: 0 !important;
	margin: 0 0 18px !important;
	list-style: none;
}
.woocommerce-checkout #payment ul.payment_methods li {
	background: var(--wp--preset--color--base, #FFFFFF);
	border: 1px solid var(--wp--preset--color--line, #E3EBF6);
	border-radius: 10px;
	padding: 15px 16px;
	margin: 0 0 10px;
}
.woocommerce-checkout #payment ul.payment_methods li:has(input:checked) {
	border-color: var(--wp--preset--color--accent, #14346E);
	box-shadow: 0 0 0 3px rgba(20, 52, 110, .10);
}
.woocommerce-checkout #payment ul.payment_methods label {
	font-size: 15px;
	color: var(--wp--preset--color--contrast, #0B1B36);
	display: inline;
}
.woocommerce-checkout #payment div.payment_box {
	background: var(--wp--preset--color--surface-2, #E9F1FC) !important;
	color: var(--wp--preset--color--contrast, #0B1B36);
	border-radius: 8px;
	font-size: 13.5px;
	line-height: 1.65;
	margin: 12px 0 0 !important;
	padding: 14px 16px !important;
}
.woocommerce-checkout #payment div.payment_box::before { display: none !important; }
.zzf-gateway-note { display: block; font-size: 12.5px; color: var(--wp--preset--color--muted, #5A6C89); margin-top: 4px; }

/* ---------- the attestation ----------
   This is a compliance GATE, not a form nicety: it blocks the order and is
   recorded to order meta. It gets a visible frame and an oversized target
   so it cannot read as fine print, and it is never dimmed or collapsed.
   Do not restyle this into something quieter. */
.zzf-attestation {
	background: var(--wp--preset--color--surface-2, #E9F1FC);
	border: 1px solid rgba(20, 52, 110, .18);
	border-radius: 12px;
	padding: 16px 18px;
	margin: 0 0 18px;
}
.zzf-attestation label {
	display: flex;
	align-items: flex-start;
	gap: 12px;
	font-size: 13.5px;
	line-height: 1.6;
	color: var(--wp--preset--color--contrast, #0B1B36);
	cursor: pointer;
}
.zzf-attestation input[type="checkbox"] {
	flex: none;
	width: 22px;
	height: 22px;
	margin: 1px 0 0;
	accent-color: var(--wp--preset--color--accent, #14346E);
	cursor: pointer;
}

/* ---------- notices ---------- */
.woocommerce-message,
.woocommerce-info,
.woocommerce-error {
	border-top: 0 !important;
	border-radius: 10px !important;
	background: var(--wp--preset--color--surface-2, #E9F1FC) !important;
	color: var(--wp--preset--color--contrast, #0B1B36) !important;
	padding: 15px 18px !important;
	font-size: 14px;
}
.woocommerce-error {
	background: rgba(200, 16, 46, .08) !important;
	border: 1px solid rgba(200, 16, 46, .28) !important;
}
.woocommerce-message::before,
.woocommerce-info::before,
.woocommerce-error::before { display: none !important; }

/* ---------- empty cart ---------- */
.woocommerce .cart-empty,
.wc-empty-cart-message p {
	font-family: var(--wp--preset--font-family--display, serif);
	font-size: clamp(20px, 2.4vw, 28px);
	color: var(--wp--preset--color--contrast, #0B1B36);
	text-align: center;
	margin: 40px 0 22px;
}
.woocommerce .return-to-shop { text-align: center; margin-bottom: 60px; }

/* ---------- responsive ---------- */
@media (max-width: 980px) {
	.woocommerce table.shop_table td.product-thumbnail { display: none; }
	.woocommerce a.checkout-button,
	.woocommerce #place_order { padding: 17px 20px !important; }
}

/* ==================================================================
 * 20. FIELD VARIANT — "Gem" · white ground, green sparkles
 * ==================================================================
 * A SECOND background for the house field, not a replacement. The
 * aurora in section 12 is the first client's signature and stays
 * exactly as it is; this variant exists because the second client asked
 * for a background that does not resemble it (2026-08-25).
 *
 * OPT-IN, AND INERT WITHOUT THE CLASS. Everything here is scoped to
 * `body.zzf-field-gem`, which zzf-core adds from client.json
 * `design.field`. A client that does not ask for it renders exactly as
 * before — this whole section costs them nothing but bytes.
 *
 * WHAT IT DOES
 *   - hides the atmosphere, the four aurora veils, the grain and the
 *     light sweep: those four ARE the borealis
 *   - repaints the ground white with the faintest emerald wash at the
 *     crown, so it reads as light rather than as sterile paper
 *   - turns the twelve particle spans into roughly 130 sparkles
 *
 * HOW TWELVE ELEMENTS BECOME 130 SPARKLES. Each span paints itself plus
 * a list of box-shadows at offsets. A box-shadow is painted by the same
 * element, so the DOM stays at twelve nodes and the animation still
 * moves one opacity per span. Adding 130 real elements would cost 130
 * style recalculations per frame; this costs twelve.
 *
 * THE RULES THIS STILL OBEYS
 *   - transform and opacity ONLY. No filter, no blur, ever: a filter
 *     re-rasterises every frame and destroys scroll on mid-range
 *     Android, which is most of this traffic.
 *   - no decorative stars. These are round sparkles, not stars — the
 *     star is the proof mark and means "a document backs this claim".
 *   - every knob reads a theme.json override first, so a client can
 *     retune it without this agency-wide file being edited.
 * ================================================================== */
:root {
	--zzf-gem-ground:   var(--wp--custom--zzf--gem-ground,   #FBFCFB);
	--zzf-gem-wash:     var(--wp--custom--zzf--gem-wash,     #EEF7F2);
	--zzf-gem-spark:    var(--wp--custom--zzf--gem-spark,    #0B6B4F);
	--zzf-gem-spark-2:  var(--wp--custom--zzf--gem-spark-2,  #3FBF8F);
	--zzf-gem-spark-3:  var(--wp--custom--zzf--gem-spark-3,  #C9A227);
	--zzf-gem-opacity:  var(--wp--custom--zzf--gem-opacity,  .85);
	--zzf-gem-speed:    var(--wp--custom--zzf--gem-speed,    1);
}

/* --- 20.1 strip the borealis ------------------------------------- */
body.zzf-field-gem .zzf-field__veils,
body.zzf-field-gem .zzf-field__grain,
body.zzf-field-gem .zzf-field::after { display: none !important; }

/* The ground. A wash at the crown only — a flat white page under glass
   panels reads as unfinished, and the wash is what keeps the panels
   legible as panels. */
body.zzf-field-gem .zzf-field {
	background:
		radial-gradient(120% 55% at 50% -10%, var(--zzf-gem-wash) 0%, rgba(255,255,255,0) 70%),
		linear-gradient(180deg, var(--zzf-gem-ground) 0%, #FFFFFF 55%, var(--zzf-gem-ground) 100%);
}

/* --- 20.2 sparkles ------------------------------------------------ */
body.zzf-field-gem .zzf-field__particles { inset: 0; }

body.zzf-field-gem .zzf-field__particles span {
	width: 3px; height: 3px;
	border-radius: 50%;
	background: var(--zzf-gem-spark);
	opacity: 0;
	animation: zzf-gem-twinkle calc(7s / var(--zzf-gem-speed)) ease-in-out infinite;
	will-change: opacity, transform;
}

@keyframes zzf-gem-twinkle {
	0%,  100% { opacity: 0;                                  transform: scale(.6);  }
	18%       { opacity: calc(var(--zzf-gem-opacity) * .55); transform: scale(1);   }
	50%       { opacity: var(--zzf-gem-opacity);             transform: scale(1.15);}
	82%       { opacity: calc(var(--zzf-gem-opacity) * .40); transform: scale(.9);  }
}

/* Each span carries its own constellation. Offsets are hand-scattered
   rather than evenly spaced: a regular grid reads as a pattern, and a
   pattern reads as a texture rather than as sparkle. */
body.zzf-field-gem .zzf-field__particles span:nth-child(1)  { left: 6%;  top: 12%; animation-delay: 0s;    box-shadow: 22vw 9vh 0 var(--zzf-gem-spark-2), 41vw 26vh 0 var(--zzf-gem-spark), 9vw 38vh 0 var(--zzf-gem-spark-2), 55vw 6vh 0 var(--zzf-gem-spark), 71vw 31vh 0 var(--zzf-gem-spark-2), 33vw 52vh 0 var(--zzf-gem-spark), 88vw 17vh 0 var(--zzf-gem-spark-2), 64vw 44vh 0 var(--zzf-gem-spark); }
body.zzf-field-gem .zzf-field__particles span:nth-child(2)  { left: 18%; top: 61%; animation-delay: -1.3s; box-shadow: 14vw 12vh 0 var(--zzf-gem-spark), 37vw 5vh 0 var(--zzf-gem-spark-2), 58vw 22vh 0 var(--zzf-gem-spark), 76vw 9vh 0 var(--zzf-gem-spark-2), 27vw 30vh 0 var(--zzf-gem-spark), 49vw 34vh 0 var(--zzf-gem-spark-2); }
body.zzf-field-gem .zzf-field__particles span:nth-child(3)  { left: 31%; top: 8%;  animation-delay: -2.6s; box-shadow: 18vw 21vh 0 var(--zzf-gem-spark-2), 44vw 47vh 0 var(--zzf-gem-spark), 8vw 63vh 0 var(--zzf-gem-spark-2), 61vw 15vh 0 var(--zzf-gem-spark), 29vw 71vh 0 var(--zzf-gem-spark-2); }
body.zzf-field-gem .zzf-field__particles span:nth-child(4)  { left: 44%; top: 74%; animation-delay: -3.9s; box-shadow: 11vw 8vh 0 var(--zzf-gem-spark), 33vw 17vh 0 var(--zzf-gem-spark-3), 52vw 4vh 0 var(--zzf-gem-spark-2), 24vw 23vh 0 var(--zzf-gem-spark); }
body.zzf-field-gem .zzf-field__particles span:nth-child(5)  { left: 57%; top: 29%; animation-delay: -5.2s; box-shadow: 16vw 33vh 0 var(--zzf-gem-spark-2), 31vw 9vh 0 var(--zzf-gem-spark), 7vw 51vh 0 var(--zzf-gem-spark-2), 39vw 58vh 0 var(--zzf-gem-spark), 22vw 66vh 0 var(--zzf-gem-spark-2); }
body.zzf-field-gem .zzf-field__particles span:nth-child(6)  { left: 69%; top: 55%; animation-delay: -6.5s; box-shadow: 9vw 14vh 0 var(--zzf-gem-spark), 21vw 28vh 0 var(--zzf-gem-spark-2), 6vw 37vh 0 var(--zzf-gem-spark), 26vw 6vh 0 var(--zzf-gem-spark-2); }
body.zzf-field-gem .zzf-field__particles span:nth-child(7)  { left: 82%; top: 19%; animation-delay: -0.7s; box-shadow: 8vw 26vh 0 var(--zzf-gem-spark-2), 14vw 48vh 0 var(--zzf-gem-spark), 4vw 61vh 0 var(--zzf-gem-spark-3), 17vw 12vh 0 var(--zzf-gem-spark-2); }
body.zzf-field-gem .zzf-field__particles span:nth-child(8)  { left: 93%; top: 68%; animation-delay: -2.1s; box-shadow: -12vw 11vh 0 var(--zzf-gem-spark), -28vw 24vh 0 var(--zzf-gem-spark-2), -6vw 30vh 0 var(--zzf-gem-spark), -19vw 5vh 0 var(--zzf-gem-spark-2); }
body.zzf-field-gem .zzf-field__particles span:nth-child(9)  { left: 12%; top: 41%; animation-delay: -4.4s; box-shadow: 26vw 13vh 0 var(--zzf-gem-spark-2), 47vw 29vh 0 var(--zzf-gem-spark), 66vw 8vh 0 var(--zzf-gem-spark-2), 35vw 43vh 0 var(--zzf-gem-spark); }
body.zzf-field-gem .zzf-field__particles span:nth-child(10) { left: 38%; top: 88%; animation-delay: -5.8s; box-shadow: 13vw -18vh 0 var(--zzf-gem-spark), 29vw -7vh 0 var(--zzf-gem-spark-2), 44vw -26vh 0 var(--zzf-gem-spark), 7vw -33vh 0 var(--zzf-gem-spark-3); }
body.zzf-field-gem .zzf-field__particles span:nth-child(11) { left: 63%; top: 83%; animation-delay: -3.2s; box-shadow: -14vw -12vh 0 var(--zzf-gem-spark-2), -31vw -25vh 0 var(--zzf-gem-spark), 12vw -19vh 0 var(--zzf-gem-spark-2), -8vw -38vh 0 var(--zzf-gem-spark); }
body.zzf-field-gem .zzf-field__particles span:nth-child(12) { left: 76%; top: 36%; animation-delay: -1.9s; box-shadow: -19vw 18vh 0 var(--zzf-gem-spark), -9vw 41vh 0 var(--zzf-gem-spark-2), -34vw 27vh 0 var(--zzf-gem-spark), -24vw 54vh 0 var(--zzf-gem-spark-2); }

/* --- 20.3 the crops ----------------------------------------------- */
/* Mobile drops half the constellations. Same reasoning as the aurora's
   mobile block: an intentional crop, not a shrink. The ground stays —
   flattening the background on the device that is most of this traffic
   is throwing the design away to save one paint. */
@media (max-width: 640px) {
	body.zzf-field-gem .zzf-field__particles span:nth-child(2n) { display: none; }
}

/* Motion is decoration here, so it goes first when asked. The sparkles
   stay VISIBLE at rest rather than vanishing — an empty white field is
   not the design, it is the design failing to load. */
@media (prefers-reduced-motion: reduce) {
	body.zzf-field-gem .zzf-field__particles span {
		animation: none;
		opacity: calc(var(--zzf-gem-opacity) * .6);
	}
}

/* --- 20.4 iridescence -------------------------------------------- */
/* Asked for on 2026-08-25: "a subtle, iridescent glow".
 *
 * This REUSES .zzf-field__atmosphere rather than adding an element. That
 * div already exists in parts/field.html, is already positioned and
 * already sits at the right depth in the stack — hiding it and then
 * adding a new one beside it would have meant two nodes doing one job.
 *
 * A conic gradient rotated by transform is the cheap way to make a
 * surface look iridescent: the hue at any given pixel changes as the
 * cone sweeps past, which is what an oil-film or a gem facet actually
 * does. It is one composited layer and it never repaints — the
 * alternative, animating gradient colour stops, repaints the whole
 * layer every frame.
 *
 * Kept deliberately faint. The default .09 is the point at which it
 * reads as a sheen on white rather than as colour; push it past ~.16
 * and it stops being iridescence and starts being a background. It also
 * sits UNDER the sparkles and under all content, so it cannot pull body
 * copy toward the AA floor the way the aurora's veils could.
 *
 * Overscanned to -45% so the rotating square's corners never enter the
 * viewport — a rotating full-bleed layer shows its edges otherwise.
 */
body.zzf-field-gem .zzf-field__atmosphere {
	display: block !important;
	position: absolute;
	inset: -45%;
	opacity: var(--zzf-gem-iri, .09);
	background: conic-gradient(
		from 0deg at 50% 50%,
		rgba(63,191,143,.85)  0deg,
		rgba(79,214,174,.55)  50deg,
		rgba(201,162,39,.45)  110deg,
		rgba(120,200,255,.50) 175deg,
		rgba(160,120,220,.42) 240deg,
		rgba(79,214,174,.50)  300deg,
		rgba(63,191,143,.85)  360deg
	);
	animation: zzf-gem-iri calc(120s / var(--zzf-gem-speed)) linear infinite;
	will-change: transform;
}

@keyframes zzf-gem-iri { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
	body.zzf-field-gem .zzf-field__atmosphere { animation: none; }
}

/* --- 20.5 no waves, no borealis chrome ---------------------------- */
/* The wave band is the aurora system's other half: it descends out of
 * the hero into the brand stripe, mirrors back out of it (.zzf-riser),
 * and descends once more into the footer (.zzf-outro). Three moments,
 * one shape — and that shape is the first client's, as much as the
 * curtains are. Removed here, in full.
 *
 * The band was doing real work, so what replaces it matters. It hid the
 * seam where a section met the dark stripe. With the waves gone the
 * stripe needs a clean, deliberate edge instead — see below — or the
 * join reads as an accident rather than a decision.
 */
body.zzf-field-gem .zzf-waves,
body.zzf-field-gem .zzf-riser,
body.zzf-field-gem .zzf-outro { display: none !important; }

/* .zzf-band paints its fill through ::before, which stops at
   --zzf-wave-height so the navy can sit behind a rising wave. With no
   wave to sit behind, that height is meaningless and the stripe would
   simply stop part-way down. The fill goes back onto the section, full
   height, with a straight edge. */
body.zzf-field-gem .zzf-band::before { display: none !important; }
body.zzf-field-gem .zzf-band {
	background: linear-gradient(180deg,
		var(--wp--preset--color--contrast, #0B1310) 0%,
		#0A1A14 100%);
	border-top: 1px solid rgba(63,191,143,.28);
	border-bottom: 1px solid rgba(63,191,143,.28);
}
/* The hairline is emerald rather than a grey rule because the stripe is
   now the only dark object on a white page: a grey edge reads as a box,
   a tinted one reads as the same material as the sparkles. */

/* --- 20.6 header ---------------------------------------------------
 * The header is transparent and sits on whatever the field paints
 * behind it. Under the aurora that is a deliberately DARK crown, pinned
 * in px so it lands on the 76px bar. Under this field it is white, so
 * the glass panel needs to read against white instead — otherwise the
 * bar dissolves and the header looks broken, which is exactly what was
 * reported. The text tokens were already dark; the panel was not.
 */
body.zzf-field-gem .zzf-header::before {
	background: rgba(255,255,255,.72);
}
body.zzf-field-gem .zzf-header.is-scrolled::before {
	background: rgba(255,255,255,.94);
}
body.zzf-field-gem .zzf-header::after {
	background: linear-gradient(90deg,
		rgba(11,107,79,0) 0%,
		rgba(11,107,79,.22) 22%,
		rgba(201,162,39,.20) 50%,
		rgba(11,107,79,.22) 78%,
		rgba(11,107,79,0) 100%);
}
/* Nav pills: the default hover fill is a pale BLUE from the first
   client's palette and reads as a bruise next to emerald. */
body.zzf-field-gem .zzf-header__nav .wp-block-navigation-item__content:hover,
body.zzf-field-gem .zzf-header__nav .wp-block-navigation-item__content:focus-visible,
body.zzf-field-gem .zzf-header__nav .current-menu-item > .wp-block-navigation-item__content {
	background: rgba(63,191,143,.16);
	color: var(--wp--preset--color--accent, #0B6B4F);
}
/* The ticker sits above the header on the same white ground. */
body.zzf-field-gem .zzf-ticker {
	background: rgba(238,247,242,.85);
	border-bottom: 1px solid rgba(63,191,143,.18);
}

/* ==================================================================
 * 21. THE GATE PAGE + SIGNUP CAPTURE
 * ==================================================================
 * Styles templates/coming-soon.html and [zzf_signup]. Agency-wide and
 * palette-driven — it inherits whichever field and tokens the client
 * runs, so it looks like the brand rather than like a plugin.
 *
 * This page is the ONLY public surface while a site is closed, so it
 * gets designed rather than defaulted. It is also the page a crawler
 * indexes, which is why it carries the wordmark and a real sentence
 * instead of "coming soon".
 * ================================================================== */
.zzf-gate {
	min-height: calc(100svh - var(--zzf-header-h, 76px));
	display: flex;
	align-items: center;
	justify-content: center;
	padding: clamp(32px, 7vw, 80px) 20px;
}

.zzf-gate__panel {
	width: 100%;
	padding: clamp(26px, 4.5vw, 46px) clamp(22px, 4vw, 44px);
	border-radius: 14px;
	text-align: left;
}

.zzf-gate__crumb { margin-bottom: 18px; }

.zzf-gate__wordmark {
	margin: 0 0 6px;
	font-family: var(--wp--preset--font-family--display, serif);
	font-size: 15px;
	letter-spacing: .22em;
	text-transform: uppercase;
	color: var(--wp--preset--color--accent, #0B6B4F);
}

.zzf-gate__title {
	margin: 0 0 14px;
	font-size: clamp(30px, 5.2vw, 46px);
	line-height: 1.06;
	letter-spacing: -.015em;
	color: var(--wp--preset--color--contrast, #0B1310);
}

.zzf-gate__lede {
	margin: 0 0 26px;
	max-width: 52ch;
	color: var(--wp--preset--color--muted, #46564F);
	font-size: 15.5px;
	line-height: 1.62;
}

.zzf-gate__ruo {
	margin: 22px 0 0;
	padding-top: 16px;
	border-top: 1px solid var(--wp--preset--color--line, #DEE7E1);
	font-size: 11.5px;
	line-height: 1.6;
	color: var(--wp--preset--color--muted, #46564F);
	opacity: .85;
}

/* --- the form ----------------------------------------------------- */
.zzf-signup2 { display: block; }

.zzf-signup2__row { margin-bottom: 14px; }

.zzf-signup2__row input {
	width: 100%;
	padding: 12px 14px;
	border: 1px solid var(--wp--preset--color--line, #DEE7E1);
	border-radius: 8px;
	background: var(--wp--preset--color--surface, #FFFFFF);
	color: var(--wp--preset--color--contrast, #0B1310);
	font: inherit;
	font-size: 15px;
}
.zzf-signup2__row input:focus-visible {
	outline: 2px solid var(--wp--preset--color--accent, #0B6B4F);
	outline-offset: 1px;
	border-color: transparent;
}

.zzf-signup2__opt {
	margin-left: 6px;
	text-transform: none;
	letter-spacing: 0;
	opacity: .7;
}

/* The honeypot. Off-screen rather than display:none — some bots skip
   fields that are display:none, which defeats the point of the trap. */
.zzf-signup2__hp {
	position: absolute;
	left: -9999px;
	width: 1px; height: 1px;
	overflow: hidden;
}

.zzf-signup2__consents {
	list-style: none;
	margin: 18px 0 22px;
	padding: 0;
	display: grid;
	gap: 12px;
}
.zzf-signup2__consents label {
	display: grid;
	grid-template-columns: 18px 1fr;
	gap: 10px;
	align-items: start;
	cursor: pointer;
	font-size: 12.5px;
	line-height: 1.55;
	color: var(--wp--preset--color--muted, #46564F);
}
.zzf-signup2__consents input[type="checkbox"] {
	margin: 2px 0 0;
	width: 17px; height: 17px;
	accent-color: var(--wp--preset--color--accent, #0B6B4F);
}

.zzf-signup2__submit { width: 100%; }

.zzf-signup2__fine {
	margin: 14px 0 0;
	font-size: 11.5px;
	color: var(--wp--preset--color--muted, #46564F);
	opacity: .85;
}
.zzf-signup2__fine a { color: var(--wp--preset--color--accent, #0B6B4F); }

.zzf-signup2__error {
	margin: 0 0 14px;
	padding: 10px 12px;
	border-radius: 8px;
	border: 1px solid rgba(201,34,34,.35);
	background: rgba(201,34,34,.07);
	color: #8A1F1F;
	font-size: 13px;
}

/* --- the thank-you state ------------------------------------------ */
.zzf-signup__done {
	padding: 20px 0 4px;
}
.zzf-signup__donetitle {
	margin: 0 0 8px;
	font-family: var(--wp--preset--font-family--display, serif);
	font-size: 22px;
	color: var(--wp--preset--color--contrast, #0B1310);
}
.zzf-signup__donesub {
	margin: 0;
	color: var(--wp--preset--color--muted, #46564F);
	font-size: 14.5px;
	line-height: 1.6;
}

/* ==================================================================
 * 22. GEM DESIGN LANGUAGE — replaces the house look, does not edit it
 * ==================================================================
 * Section 12's "Pastel Glass" is the first client's design and it stays
 * exactly as authored. This section is a SECOND language, scoped to
 * body.zzf-field-gem, for a client that must not resemble it.
 *
 * Four things in the house look are its signature, and each is replaced
 * here rather than tweaked. Tweaking a signature produces a worse
 * version of the same thing; the point is a different thing.
 *
 *   1. THE HERO SHAPE — oversized two-line display headline, line two
 *      set in italic carrying a brand gradient. That device is written
 *      into CLAUDE.md as "the house hero shape". Gone: one voice, one
 *      weight, no gradient, no italic.
 *   2. GLASS — translucent panels over a light field. Replaced with
 *      flat surfaces and hairlines. This also removes 8-12
 *      backdrop-filter surfaces per viewport, which is the single
 *      biggest scroll cost on the mid-range Android that is most of
 *      this traffic. The house rule budgeted them; this design does not
 *      spend them at all.
 *   3. THE EYEBROW — 2px-letterspaced uppercase micro-caps above every
 *      heading. Replaced with a short emerald rule and a plain label.
 *   4. THE PROOF STAR — .zzf-proof is the other client's mark and it
 *      means "a document backs this". Hidden here. It is not decoration
 *      to be reused, and this brand has no documents yet anyway.
 *
 * Type is Space Grotesk + Inter, set in theme.json. This section only
 * handles what type alone cannot: size, rhythm and the components.
 * ================================================================== */

/* --- 22.1 hero: one voice ----------------------------------------- */
body.zzf-field-gem .zzf-hero__title {
	display: block;
	font-family: var(--wp--preset--font-family--display, sans-serif);
	font-size: clamp(34px, 6.4vw, 68px);
	font-weight: 600;
	line-height: 1.03;
	letter-spacing: -.035em;
	max-width: 16ch;
}
/* Both halves are the same face at the same weight. The house look
   splits them; splitting them is the tell. */
body.zzf-field-gem .zzf-hero__l1,
body.zzf-field-gem .zzf-hero__l2 {
	display: block;
	font-family: inherit !important;
	font-style: normal !important;
	font-weight: inherit;
	background: none !important;
	-webkit-text-fill-color: currentColor !important;
	color: var(--wp--preset--color--contrast, #0B1310);
}
/* The second line carries the emphasis with COLOUR, not a gradient and
   not a different family — one idea, stated once. */
body.zzf-field-gem .zzf-hero__l2 {
	color: var(--wp--preset--color--accent, #0B6B4F);
}

body.zzf-field-gem .zzf-hero__lede {
	max-width: 46ch;
	font-size: clamp(15px, 1.6vw, 17.5px);
	line-height: 1.62;
	color: var(--wp--preset--color--muted, #46564F);
}

/* The 21+ pill: a plain bordered chip rather than a soft glass lozenge. */
body.zzf-field-gem .zzf-hero__pill {
	background: none;
	border: 1px solid var(--wp--preset--color--line, #DEE7E1);
	border-radius: 4px;
	padding: 5px 10px;
	font-family: var(--wp--preset--font-family--mono, monospace);
	font-size: 10.5px;
	letter-spacing: .08em;
	text-transform: uppercase;
	color: var(--wp--preset--color--muted, #46564F);
}

/* --- 22.2 the eyebrow becomes a marker ---------------------------- */
body.zzf-field-gem .zzf-eyebrow {
	display: flex;
	align-items: center;
	gap: 10px;
	font-family: var(--wp--preset--font-family--mono, monospace);
	font-size: 11px;
	font-weight: 500;
	letter-spacing: .04em;      /* the house look runs 2px+; this does not */
	text-transform: uppercase;
	color: var(--wp--preset--color--accent, #0B6B4F);
}
body.zzf-field-gem .zzf-eyebrow::before {
	content: "";
	width: 22px;
	height: 2px;
	background: var(--wp--preset--color--accent, #0B6B4F);
	flex: none;
}
body.zzf-field-gem .has-text-align-center.zzf-eyebrow { justify-content: center; }

/* --- 22.3 flat, not glass ----------------------------------------- */
body.zzf-field-gem .zzf-glass,
body.zzf-field-gem .zzf-glass--lift,
body.zzf-field-gem .zzf-trust__card,
body.zzf-field-gem .zzf-coa__panel,
body.zzf-field-gem .zzf-contact__panel,
body.zzf-field-gem .zzf-about__panel,
body.zzf-field-gem .zzf-showcase__frame,
body.zzf-field-gem .zzf-gate__panel {
	-webkit-backdrop-filter: none !important;
	backdrop-filter: none !important;
	background: var(--wp--preset--color--surface, #FFFFFF) !important;
	border: 1px solid var(--wp--preset--color--line, #DEE7E1) !important;
	border-radius: 10px !important;
	box-shadow: none !important;
}
/* Hover lift replaced by a border that answers. Cheaper and quieter. */
body.zzf-field-gem .zzf-glass--lift { transition: border-color .18s ease; }
body.zzf-field-gem .zzf-glass--lift:hover {
	transform: none !important;
	border-color: var(--wp--preset--color--accent, #0B6B4F) !important;
}

/* --- 22.4 the other client's proof mark is not ours ---------------- */
body.zzf-field-gem .zzf-proof::before,
body.zzf-field-gem .zzf-proof::after { display: none !important; }
body.zzf-field-gem .zzf-proof { padding-left: 0 !important; }

/* --- 22.5 the dark stripe becomes a light band -------------------- */
/* The house look anchors the page with ONE dark stripe. On a white
   sparkle field that block is the loudest thing on the page and it is
   the first client's anchor, not this one's. It becomes a tinted band
   with a hairline: still a change of surface, still punctuation, no
   longer a slab. */
body.zzf-field-gem .zzf-band {
	background: var(--wp--custom--zzf--gem-wash, #EEF7F2) !important;
	border-top: 1px solid var(--wp--preset--color--line, #DEE7E1) !important;
	border-bottom: 1px solid var(--wp--preset--color--line, #DEE7E1) !important;
}
/* Everything inside it was coloured for a dark ground. */
body.zzf-field-gem .zzf-band h2,
body.zzf-field-gem .zzf-band .zzf-trust__head h2 {
	color: var(--wp--preset--color--contrast, #0B1310) !important;
}
body.zzf-field-gem .zzf-band .zzf-eyebrow {
	color: var(--wp--preset--color--accent, #0B6B4F) !important;
	opacity: 1 !important;
}
body.zzf-field-gem .zzf-band .zzf-trust__sub,
body.zzf-field-gem .zzf-band p {
	color: var(--wp--preset--color--muted, #46564F) !important;
}

/* --- 22.6 trust cards: label / claim / evidence ------------------- */
body.zzf-field-gem .zzf-trust__kind {
	font-family: var(--wp--preset--font-family--mono, monospace);
	font-size: 10.5px;
	letter-spacing: .06em;
	text-transform: uppercase;
	color: var(--wp--preset--color--accent, #0B6B4F);
}
body.zzf-field-gem .zzf-trust__claim {
	font-family: var(--wp--preset--font-family--display, sans-serif);
	font-size: 18px;
	font-weight: 600;
	letter-spacing: -.02em;
	line-height: 1.25;
	color: var(--wp--preset--color--contrast, #0B1310);
}
body.zzf-field-gem .zzf-trust__evidence {
	font-size: 13.5px;
	line-height: 1.6;
	color: var(--wp--preset--color--muted, #46564F);
}
body.zzf-field-gem .zzf-trust__verify a {
	font-family: var(--wp--preset--font-family--mono, monospace);
	font-size: 11.5px;
	letter-spacing: .03em;
	text-decoration: none;
	border-bottom: 1px solid currentColor;
	padding-bottom: 1px;
}

/* --- 22.7 the stat bar reads as instrument output ----------------- */
body.zzf-field-gem .zzf-stats--bar {
	gap: 0;
	border: 1px solid var(--wp--preset--color--line, #DEE7E1);
	border-radius: 10px;
	overflow: hidden;
	background: var(--wp--preset--color--surface, #FFFFFF);
}
body.zzf-field-gem .zzf-stat {
	flex: 1;
	padding: 16px 18px;
	border-left: 1px solid var(--wp--preset--color--line, #DEE7E1);
}
body.zzf-field-gem .zzf-stat:first-child { border-left: 0; }
body.zzf-field-gem .zzf-stat__n {
	font-family: var(--wp--preset--font-family--display, sans-serif);
	font-size: clamp(17px, 2.1vw, 22px);
	font-weight: 600;
	letter-spacing: -.02em;
	color: var(--wp--preset--color--contrast, #0B1310);
}
body.zzf-field-gem .zzf-stat__l {
	font-family: var(--wp--preset--font-family--mono, monospace);
	font-size: 10.5px;
	letter-spacing: .05em;
	text-transform: uppercase;
	color: var(--wp--preset--color--muted, #46564F);
}

/* --- 22.8 headings, buttons, section rhythm ----------------------- */
body.zzf-field-gem h2 {
	font-size: clamp(24px, 3.4vw, 38px);
	letter-spacing: -.03em;
	line-height: 1.12;
}
body.zzf-field-gem .zzf-btn-glass .wp-block-button__link {
	-webkit-backdrop-filter: none;
	backdrop-filter: none;
	background: transparent;
	border: 1px solid var(--wp--preset--color--accent, #0B6B4F);
	color: var(--wp--preset--color--accent, #0B6B4F);
}
body.zzf-field-gem .zzf-btn-glass .wp-block-button__link:hover {
	background: var(--wp--custom--zzf--gem-wash, #EEF7F2);
}

/* --- 21.1 gate: centre the header block --------------------------- */
/* Asked for 2026-08-26. The panel is a narrow centred column, so a
   left-ragged header inside it reads as though it slipped. The FORM
   stays left-aligned: a field label centred over a full-width input
   loses the edge that tells you where the input starts. */
.zzf-gate__panel .zzf-gate__crumb,
.zzf-gate__panel .zzf-gate__wordmark,
.zzf-gate__panel .zzf-gate__title,
.zzf-gate__panel .zzf-gate__lede,
.zzf-gate__panel .zzf-gate__ruo { text-align: center; }

.zzf-gate__panel .zzf-gate__crumb { justify-content: center; }
body.zzf-field-gem .zzf-gate__panel .zzf-eyebrow { justify-content: center; }

.zzf-gate__panel .zzf-gate__lede { margin-left: auto; margin-right: auto; }
.zzf-gate__panel .zzf-gate__title { max-width: none; }

/* --- 21.2 consent rows: text beside the box, wrapping in its column - */
/* Belt and braces for the wpautop problem fixed in signup.php: even if a
   stray <br> survives, it is removed here rather than becoming a grid
   item that pushes the label onto its own line. */
.zzf-signup2__consents label br { display: none; }
.zzf-signup2__consents label {
	display: grid;
	grid-template-columns: 18px minmax(0, 1fr);
	column-gap: 10px;
	row-gap: 0;
	align-items: start;
	text-align: left;
}
/* minmax(0, 1fr) rather than 1fr: a grid column's default minimum is its
   CONTENT size, so a long unbroken consent sentence can force the column
   wider than the panel instead of wrapping inside it. */
.zzf-signup2__consents label > span { min-width: 0; overflow-wrap: anywhere; }
