/**
 * Product Instant staging shell — scoped to body.ghostdma-product-instant
 * Layout mirrors Elementor single-product template (956).
 * Adds AJAX swap-transition classes used by assets/js/instant-nav.js.
 */

/* ---- AJAX swap transitions (instant-nav.js) ----
 * IMPORTANT: never put a permanent `will-change`, `transform`, `opacity<1`,
 * `filter`, or `isolation` on [data-gdm-swap]. This element is an ancestor of
 * the hero .gdm-product__media, and any of those creates a stacking context
 * that isolates mix-blend-mode:difference from the body wallpaper (making the
 * hero blend invisible — difference over black = the original image). So
 * `will-change` is applied ONLY while a swap animation class is active, and the
 * settled state carries no compositing hints at all. */
/* Modern browsers transition stable snapshots, so route-class changes and Woo
 * reinitialization never appear as partially styled live DOM. These names only
 * exist while a swap is active to avoid a permanent main stacking context. */
/* Keep the persistent header in an independent, motionless transition layer.
 * The entrance replay that made this unsafe before is now latched permanently
 * after the first route change; isolating the header prevents the changing dark
 * root/main snapshots from dimming identical header pixels during their overlap. */
html.gdm-instant-transition [data-gdm-swap] {
	view-transition-name: gdm-instant-main;
}
html.gdm-instant-transition .gdm-site-header {
	view-transition-name: gdm-instant-header;
}
/* On touch, instant-nav.js clones the header into a frozen stand-in
 * (.gdm-transition-header-guard) for the length of the swap. The clone keeps the
 * .gdm-site-header class, so the rule above named BOTH elements
 * `gdm-instant-header` — and a duplicate view-transition-name makes the browser
 * abort the whole transition ("Unexpected duplicate view-transition-name"). Every
 * View Transition on a phone was therefore a hard cut: the Back-from-deep and
 * logo-from-deep swaps showed no motion at all.
 *
 * While guarded, only the stand-in carries the name. It is the header actually on
 * screen (the real one's contents are hidden, see below), so it becomes the
 * motionless header layer — and stays out of the root snapshot, which the logo
 * zoom-out scales. */
html.gdm-instant-transition.gdm-instant-header-guarded .gdm-site-header:not(.gdm-transition-header-guard) {
	view-transition-name: none;
}
html.gdm-instant-transition,
html.gdm-instant-flight {
	scroll-behavior: auto !important;
	overflow-anchor: none;
}

html.gdm-instant-transition::view-transition-old(root),
html.gdm-instant-transition::view-transition-new(root) {
	animation: none;
	mix-blend-mode: normal;
}

/* ---- Zoom out: logo tap from a product with its photo off screen ----
 * instant-nav.js sets .gdm-instant-zoomout. The main is left UN-named so the
 * page is captured as part of the viewport-sized root snapshot: the main itself
 * can be thousands of pixels tall, and scaling that about its own centre would
 * swing the visible part far off screen. The header keeps its own motionless
 * layer, so only the page under it moves.
 *
 * ~200ms by request — someone this deep in a description going home is moving on.
 * Outgoing: ease-in, so it stays readable for the first beats and then falls away.
 * Incoming: starts at once with ease-out, so coverage never dips to a dark gap. */
html.gdm-instant-transition.gdm-instant-zoomout [data-gdm-swap] {
	view-transition-name: none;
}

html.gdm-instant-transition.gdm-instant-zoomout::view-transition-old(root) {
	animation: gdm-instant-zoomout-old 190ms cubic-bezier(0.4, 0, 1, 1) both;
	transform-origin: 50% 50%;
}

html.gdm-instant-transition.gdm-instant-zoomout::view-transition-new(root) {
	animation: gdm-instant-zoomout-new 210ms cubic-bezier(0.22, 1, 0.36, 1) both;
	transform-origin: 50% 50%;
}

@keyframes gdm-instant-zoomout-old {
	to {
		opacity: 0;
		transform: scale(0.94);
	}
}

@keyframes gdm-instant-zoomout-new {
	from {
		opacity: 0;
		transform: scale(1.03);
	}
}

/* Whatever the snapshots don't cover during the zoom stays black. The html
 * element's own background is transparent (body's black propagates to the canvas),
 * and a dimmed or scaled snapshot must never reveal anything lighter. */
html.gdm-instant-transition.gdm-instant-zoomout::view-transition {
	background: #000;
}

/* ---- Phone versions (1.9.15) ----
 * 1.9.14 read as "not good or fluid" on a real iPhone. What was wrong with it:
 *  - opacity was held to 50% and then dropped on a second keyframe segment, and the
 *    easing restarts per segment, so the page snapped away mid-motion;
 *  - its iOS-style curve front-loaded the scale (most of it in ~60ms, then a crawl);
 *  - a 70px shadow, an outline and a border-radius on a full-screen layer, which is
 *    heavy on phone GPUs and all of it decoration.
 * Now each property gets its OWN single, uninterrupted curve (two animations on one
 * pseudo-element: movement eases out, fade eases in, so the motion is seen before
 * the page disappears), and nothing but transform and opacity is animated — the
 * two properties a phone composites for free.
 *
 * The outgoing page sits ABOVE home; home waits behind it, dimmed, and brightens.
 * Dim-behind is what gives the moving page a visible edge on this all-black site.
 *
 * Three variants, switchable on the device: ?gdm-zoom=a|b|c (remembered for the tab
 * by instant-nav.js, which sets html[data-gdm-zoom]). Default: a. */
@media (max-width: 767px), (pointer: coarse) {
	/* A — recede (the owner's pick): the page shrinks back and fades; home
	 * brightens behind it.
	 *
	 * Tuned in 1.9.16 against a per-frame measurement (zfc-edit/_diag-zoom-curve.py):
	 *  - cubic-bezier(0.2, 0, 0, 1) was so steep at the start that ONE frame carried
	 *    26% of the whole shrink at full speed — it cannot read as fluid. The
	 *    standard ease-in-out peaks at roughly twice the average step instead.
	 *  - At 4x CPU the first frames were dropped (the browser is painting the whole
	 *    home grid as the transition starts) and a single frame then jumped 45%.
	 *    The 40ms head start lets that work pass while the page is still at 100%,
	 *    so no motion is scheduled into the frames that get dropped.
	 *  - The fade starts at 150ms, so the shrink is seen before the page leaves.
	 *  - Home only brightens. Scaling it up from 1.04 upsampled the live page
	 *    (soft text for the whole transition, then a snap to sharp) and made the
	 *    busiest layer do extra work. */
	/* 1.9.17, after "the black lingers too long": the description page is dark
	 * (~30/255) and home is bright (~116/255). With home starting at 45% behind a
	 * dark page that only began fading at 150ms, the screen reached half of home's
	 * brightness at 281ms of 381ms — dark for three quarters of the transition —
	 * and the two nearly full-size layers overlapped heavily for 83ms (muddy text
	 * over cards). Now: home waits at 70% so it shows through at once, the page
	 * recedes further (0.88) and faster, and its fade is short and earlier. */
	html.gdm-instant-transition.gdm-instant-zoomout::view-transition-old(root) {
		z-index: 2;
		/* Early rather than fast: a short fade brightened the screen sooner but put
		 * 25% of itself into one frame (40% of the movement at 4x CPU). Starting a
		 * longer fade earlier reaches the same brightness without big steps. Both
		 * curves are symmetric and gentle (low peak speed) for the same reason. */
		animation:
			gdm-zo-a-old-scale 320ms cubic-bezier(0.45, 0, 0.55, 1) 20ms both,
			gdm-zo-old-fade 240ms cubic-bezier(0.4, 0, 0.6, 1) 50ms both;
	}

	html.gdm-instant-transition.gdm-instant-zoomout::view-transition-new(root) {
		z-index: 1;
		animation: gdm-zo-a-new 300ms cubic-bezier(0.25, 0.1, 0.25, 1) both;
	}

	/* B — drop away: the page sinks a quarter screen and fades, like dismissing an
	 * iOS sheet. */
	html[data-gdm-zoom="b"].gdm-instant-transition.gdm-instant-zoomout::view-transition-old(root) {
		animation:
			gdm-zo-b-old-move 340ms cubic-bezier(0.3, 0, 0.2, 1) both,
			gdm-zo-old-fade 340ms cubic-bezier(0.55, 0, 1, 1) both;
	}

	/* C — slide back: the iOS back gesture's motion. The page slides off right,
	 * home comes in from a short parallax offset on the left. */
	html[data-gdm-zoom="c"].gdm-instant-transition.gdm-instant-zoomout::view-transition-old(root) {
		animation:
			gdm-zo-c-old-move 340ms cubic-bezier(0.2, 0, 0, 1) both,
			gdm-zo-old-fade 340ms cubic-bezier(0.7, 0, 1, 1) both;
	}

	html[data-gdm-zoom="c"].gdm-instant-transition.gdm-instant-zoomout::view-transition-new(root) {
		animation: gdm-zo-c-new 340ms cubic-bezier(0.2, 0, 0, 1) both;
	}
}

/* ---- Phones: "live" zoom-out (1.9.23) ----
 * instant-nav.js adds .gdm-instant-zoomlive on touch. The root is not captured, so
 * home and the header stay live underneath (no black strip under iOS Safari's
 * toolbar, no header blur handoff). Only the outgoing page is captured, as the
 * exit-only group `gdm-zoom-old`; JS sets the clip (below the header) and the
 * transform origin (the centre of the screen, in the element's own coordinates). */
html.gdm-instant-transition.gdm-instant-zoomlive {
	view-transition-name: none;
}

/* The incoming main must not carry a name either (the outgoing one is named inline
 * by instant-nav.js, which beats this). */
html.gdm-instant-transition.gdm-instant-zoomlive [data-gdm-swap] {
	view-transition-name: none;
}

html.gdm-instant-transition.gdm-instant-zoomlive .gdm-site-header,
html.gdm-instant-transition.gdm-instant-zoomlive .gdm-site-header.gdm-transition-header-guard {
	view-transition-name: none !important;
}

html.gdm-instant-transition.gdm-instant-zoomlive::view-transition {
	background: transparent;
}

/* Pinned elements (popups) captured as their own still layers, above the page. */
html.gdm-instant-transition.gdm-instant-zoomlive::view-transition-group(*) {
	z-index: 3;
	animation: none;
}

html.gdm-instant-transition.gdm-instant-zoomlive::view-transition-old(*),
html.gdm-instant-transition.gdm-instant-zoomlive::view-transition-new(*) {
	animation: none;
}

html.gdm-instant-transition.gdm-instant-zoomlive::view-transition-group(gdm-zoom-old) {
	z-index: 1;
	animation: none;
	clip-path: inset(var(--gdm-zo-clip-top, 0px) 0 0 0);
}

html.gdm-instant-transition.gdm-instant-zoomlive::view-transition-old(gdm-zoom-old) {
	transform-origin: 50% var(--gdm-zo-origin-y, 50%);
	animation:
		gdm-zo-a-old-scale 320ms cubic-bezier(0.45, 0, 0.55, 1) 20ms both,
		gdm-zo-old-fade 240ms cubic-bezier(0.4, 0, 0.6, 1) 50ms both;
}

/* Back from deep in a description, on phones: the same live capture as the zoom-out
 * but just a fade — an iOS edge-swipe back has already drawn Safari's own slide. */
html.gdm-instant-transition.gdm-instant-zoomlive.gdm-instant-backfade::view-transition-old(gdm-zoom-old) {
	animation: gdm-zo-old-fade 240ms cubic-bezier(0.4, 0, 0.6, 1) both;
}

@keyframes gdm-zo-old-fade {
	to {
		opacity: 0;
	}
}

@keyframes gdm-zo-a-old-scale {
	to {
		transform: scale(0.86);
	}
}

@keyframes gdm-zo-a-new {
	from {
		opacity: 0.7;
	}
}

@keyframes gdm-zo-b-old-move {
	to {
		transform: translateY(26vh) scale(0.96);
	}
}

@keyframes gdm-zo-c-old-move {
	to {
		transform: translateX(55%);
	}
}

@keyframes gdm-zo-c-new {
	from {
		opacity: 0.5;
		transform: translateX(-20%);
	}
}

html.gdm-instant-transition::view-transition-group(gdm-instant-header) {
	z-index: 1000;
	animation: none;
	/* The header's frosted blur, re-applied to its transition layer so it blurs the
	 * page animating underneath (instant-nav.js copies the live value into the
	 * variable just before each transition). Without it the snapshot shows only the
	 * flat tint and the blur pops back in when the transition ends. */
	backdrop-filter: var(--gdm-vt-header-backdrop, none);
	-webkit-backdrop-filter: var(--gdm-vt-header-backdrop, none);
}

html.gdm-instant-transition::view-transition-old(gdm-instant-header),
html.gdm-instant-transition::view-transition-new(gdm-instant-header) {
	animation: none;
	opacity: 1;
	mix-blend-mode: normal;
}

/* Touch navigation uses the WAAPI flight rather than View Transitions. Keep the
 * header in the normal paint tree during the handoff: promoting its translucent
 * backdrop to a new GPU layer can produce a one-frame blank on mobile Chromium. */
html.gdm-instant-loading .gdm-site-header,
html.gdm-instant-flight .gdm-site-header {
	opacity: 1 !important;
	visibility: visible !important;
}

html.gdm-instant-header-guarded .gdm-site-header:not(.gdm-transition-header-guard) > .gdm-site-header__inner {
	visibility: hidden !important;
}

/* During a View Transition on touch, the stand-in carries the header's transition
 * name, so it must also carry the header's glass; otherwise the glass is captured
 * with the page and the phone zoom-out shrinks it away (a dark band under the logo,
 * then an untinted header). instant-nav.js copies the real header's computed tint,
 * border and blur into these variables just before the transition starts. The blur
 * itself is re-applied on the header's transition layer (see
 * ::view-transition-group(gdm-instant-header)). */
html.gdm-instant-transition.gdm-instant-header-guarded body.ghostdma-chrome-v2 .gdm-site-header.gdm-transition-header-guard {
	background: var(--gdm-vt-header-bg, rgba(0, 0, 0, 0.54)) !important;
	border-bottom: var(--gdm-vt-header-border, 0 none transparent) !important;
}

html.gdm-instant-transition.gdm-instant-header-guarded body.ghostdma-chrome-v2 .gdm-site-header:not(.gdm-transition-header-guard) {
	background: transparent !important;
	border-bottom-color: transparent !important;
	backdrop-filter: none !important;
	-webkit-backdrop-filter: none !important;
}

/* Absolute rather than fixed/sticky is deliberate: the guard follows the saved
 * scroll coordinate in JS and stays out of the compositor path that blanks the
 * real header for one frame on mobile. */
html body.ghostdma-chrome-v2 .gdm-site-header.gdm-transition-header-guard {
	position: absolute !important;
	left: 0 !important;
	right: 0 !important;
	width: 100% !important;
	pointer-events: none !important;
	z-index: 1001 !important;
	background: transparent !important;
	backdrop-filter: none !important;
	-webkit-backdrop-filter: none !important;
}

/* Belt-and-braces latch on the header's load entrance.
 *
 * `gdm-enter-header` is a page-LOAD flourish. On a shell that navigates without
 * reloading there is no second page load, so it must never run a second time.
 * instant-nav.js sets `gdm-instant-entrance-done` on the first route change, so
 * the initial entrance still plays exactly once and nothing afterwards — whatever
 * future change might otherwise retrigger it — can replay it. */
html body.ghostdma-chrome-v2.gdm-entrance-on.gdm-entered.gdm-instant-entrance-done .gdm-site-header,
html body.ghostdma-chrome-v2.gdm-entrance-on.gdm-instant-entrance-done .gdm-site-header,
html body.ghostdma-chrome-v2.gdm-instant-entrance-done .gdm-site-header {
	animation: none !important;
	opacity: 1 !important;
	transform: none !important;
	filter: none !important;
}

html.gdm-instant-transition::view-transition-group(gdm-instant-main) {
	/* Must outlast the longest child below (55ms delay + 260ms), or the pair is
	 * clipped while the incoming is still fading up. */
	animation-duration: 320ms;
	animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}

html.gdm-instant-transition::view-transition-image-pair(gdm-instant-main) {
	isolation: isolate;
	overflow: clip;
}

/* Overlapped, NOT sequential — the same correction the morph pair below carries.
 *
 * This was `110ms ease-in` out against a new layer delayed by exactly 90ms. Ease-in
 * dumps most of the fade into the last third, so the outgoing already read as dark
 * around 80ms while the incoming had barely started: a visible dip to nothing in the
 * middle of every non-morph swap.
 *
 * It mattered more once returning to the grid from a scrolled-down product started
 * using this path (there is no on-screen artwork to fly, so instant-nav hands those
 * swaps to View Transitions). That is the "all the products go blank and then pop
 * back in" case, and a dip here would have survived the JS fix. */
html.gdm-instant-transition::view-transition-old(gdm-instant-main) {
	animation: gdm-instant-main-out 175ms cubic-bezier(0.4, 0, 0.7, 0.4) both;
}

html.gdm-instant-transition::view-transition-new(gdm-instant-main) {
	animation: gdm-instant-main-in 260ms cubic-bezier(0.22, 1, 0.36, 1) 55ms both;
}

/* ---- Shared-element hero morph (instant-nav.js tags `gdm-hero-art`) ----
 *
 * When a product is opened from the grid, the card's artwork and the PDP hero
 * share one view-transition-name, so the browser interpolates the square from
 * its place in the grid to its place in the hero instead of cross-fading two
 * pages. The art is the constant the eye tracks; everything else is scenery.
 *
 * Timing: the art leads and the page follows. The morph runs longer than the
 * main fade (420ms vs 300ms) on a decelerating curve so the square arrives and
 * settles rather than stopping dead — the part that reads as "native" rather
 * than "web page". */
html.gdm-instant-transition.gdm-instant-morph::view-transition-group(gdm-hero-art) {
	/* Above the fading page so the art is never dimmed mid-flight. */
	z-index: 20;
	animation-duration: 420ms;
	animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
}

/* Both sides are the same square of the same source image, so a short overlap
 * dissolve reads as one solid object moving, not two pictures trading places. */
html.gdm-instant-transition.gdm-instant-morph::view-transition-image-pair(gdm-hero-art) {
	isolation: auto;
}

html.gdm-instant-transition.gdm-instant-morph::view-transition-old(gdm-hero-art),
html.gdm-instant-transition.gdm-instant-morph::view-transition-new(gdm-hero-art) {
	/* The snapshot already contains the blended pixels; re-applying difference
	 * here would blend them a second time against the page snapshot underneath. */
	mix-blend-mode: normal;
	/* Grid art is object-fit:cover, hero art is background-size:contain. Both are
	 * square boxes showing a square image, so `contain` keeps the artwork the same
	 * size in both snapshots and the morph has no internal scale jump. */
	object-fit: contain;
	/* Top-aligned, matching the hero's own `background-position: center top`. With
	 * the default centre alignment the square would settle ~50px low inside a
	 * taller purchase-stack pane and then jump on the final frame. */
	object-position: 50% 0%;
	height: 100%;
	width: 100%;
}

/* No dissolve between the two artwork snapshots.
 *
 * They are the SAME square of the SAME source file at two sizes, so cross-fading
 * them gains nothing and actively costs: mid-dissolve you have two ~50% copies
 * stacked, which over a dark page reads dimmer than one opaque copy. The artwork
 * visibly dipped in brightness partway through every flight. Holding both fully
 * opaque lets the group's position/size interpolation do all the work, and the
 * photo keeps a constant density from start to landing. */
html.gdm-instant-transition.gdm-instant-morph::view-transition-old(gdm-hero-art),
html.gdm-instant-transition.gdm-instant-morph::view-transition-new(gdm-hero-art) {
	animation: none;
	opacity: 1;
}

/* Direct child of body: preserve difference blending against the wallpaper,
 * stay below the persistent header, and never capture touches. */
.gdm-flight-art {
	position: fixed;
	z-index: 999;
	pointer-events: none;
	/* Match the real hero (`contain`, top-centred) at landing. Source cards are
	 * square, so contain still fills their source box without changing the start. */
	background-size: contain;
	background-position: center top;
	background-repeat: no-repeat;
	/* NOT difference — deliberately, and this is the fix for the black flash at
	 * the end of the mobile flight.
	 *
	 * The card art and the hero both use `mix-blend-mode: difference`, so copying
	 * it here looked like the faithful choice. It is not, because this layer MOVES
	 * over a changing backdrop. On the return the grid fades 0->1 underneath it, so
	 * `difference(photo, backdrop)` gets darker every frame as the backdrop
	 * brightens, bottoming out exactly when the flight ends — then the handoff
	 * swaps in the real card sitting on its own dark slot and it snaps bright.
	 * Measured: the flying layer rendered at luma 133 while the settled card is
	 * 153.4. It also meant the layer and the card were two difference copies of one
	 * photo, and `difference(photo, photo)` is solid black if they ever overlap.
	 *
	 * The blend was never doing visible work here anyway: the wallpaper behind the
	 * artwork is near-black, and `difference(photo, ~black) ~= photo`. So painting
	 * the photo normally looks the same as the blended source and destination,
	 * while being completely independent of whatever is passing underneath it. */
	mix-blend-mode: normal;
	transform-origin: top left;
	will-change: transform;
}

@keyframes gdm-hero-art-out {
	to {
		opacity: 0;
	}
}

@keyframes gdm-hero-art-in {
	from {
		opacity: 0;
	}
	to {
		opacity: 1;
	}
}

/* While the art is in flight the page itself should stay quiet: a softer, later
 * fade so two things aren't competing for attention. */
html.gdm-instant-transition.gdm-instant-morph::view-transition-group(gdm-instant-main) {
	animation-duration: 420ms;
}

html.gdm-instant-transition.gdm-instant-morph::view-transition-old(gdm-instant-main) {
	/* Overlapped, NOT sequential. This used to be `140ms` out against a new layer
	 * that started at exactly 140ms delay — so at t=140 the outgoing main was at
	 * opacity 0 and the incoming had not begun. One frame of nothing, on every
	 * product click: the desktop "flashing". Ease-in also dumps most of the fade
	 * into the last third, so it read as dark from ~110ms.
	 * The incoming now starts at 60ms (see below) and total coverage never drops
	 * near zero. Both keyframes carry blur, so the brief overlap reads as depth
	 * rather than double-exposed text — which is what the sequential timing was
	 * originally protecting against. */
	animation: gdm-instant-main-out 175ms cubic-bezier(0.4, 0, 0.7, 0.4) both;
}

html.gdm-instant-transition.gdm-instant-morph::view-transition-new(gdm-instant-main) {
	animation: gdm-instant-main-in 280ms cubic-bezier(0.22, 1, 0.36, 1) 60ms both;
}

@keyframes gdm-instant-main-out {
	to {
		opacity: 0;
		transform: translateY(-3px) scale(0.997);
	}
}

@keyframes gdm-instant-main-in {
	from {
		opacity: 0;
		transform: translateY(4px);
	}
	to {
		opacity: 1;
		transform: none;
		filter: none;
	}
}

/* Fallback for browsers without View Transitions: use a simple opacity fade.
 * Avoid translating live text while body route classes are changing. */
html body.ghostdma-chrome-v2 [data-gdm-swap].gdm-swap-leave-active {
	opacity: 0 !important;
	transition: opacity 0.16s ease !important;
	will-change: opacity !important;
}

html body.ghostdma-chrome-v2 [data-gdm-swap].gdm-swap-enter {
	opacity: 0 !important;
	will-change: opacity !important;
}

html body.ghostdma-chrome-v2 [data-gdm-swap].gdm-swap-enter-active {
	opacity: 1 !important;
	transition: opacity 0.24s cubic-bezier(0.22, 1, 0.36, 1) !important;
	will-change: opacity !important;
}

@media (prefers-reduced-motion: reduce) {
	html body.ghostdma-chrome-v2 [data-gdm-swap].gdm-swap-leave-active,
	html body.ghostdma-chrome-v2 [data-gdm-swap].gdm-swap-enter,
	html body.ghostdma-chrome-v2 [data-gdm-swap].gdm-swap-enter-active {
		opacity: 1 !important;
		transform: none !important;
		transition: none !important;
	}
}

/* NOTE: a `mix-blend-mode: screen` experiment lived here (1.8.47-1.8.48) — a
 * black-background video with no alpha, on the theory that `screen(0, backdrop)`
 * renders black as transparent. It does NOT work on this header: the header's
 * `backdrop-filter` creates an isolated stacking context, so the logo blends
 * against that group rather than the page behind it, and the video's black box
 * stayed visible. Measured as a delta of 7 -> 21 down the header gradient against
 * the surrounding surface. Removed; the logo uses a real alpha channel instead. */

/* NOTE: a custom header surface lived here (1.8.27-1.8.53) — a gradient background,
 * a 0.13 hairline and a drop shadow — added because the header appeared to vanish
 * at the top of a dark PDP. It was removed in 1.8.55: the ACTUAL cause of that was
 * Home V2's load entrance replaying on every swap (see CLIENT_OWNED_CLASSES and the
 * gdm-instant-entrance-done latch in instant-nav.js), and once that was fixed the
 * surface change was only making the instant header visibly different from live —
 * lighter, more opaque, and with a border and shadow live does not have.
 *
 * The instant header must match live exactly: flat rgba(0,0,0,.54) from Home
 * Controls, backdrop blur, NO border, NO shadow. Do not re-add a surface here
 * without checking the live header first. */

/* ---- Slow-navigation progress hint ----
 *
 * A warm click swaps in ~250ms and should show no chrome at all. A cold one can
 * cost 1.1-1.4s of server render, and in that window nothing on screen changes,
 * so the click feels dropped. This bar is delayed past the warm case and only
 * ever appears when the wait is real.
 *
 * instant-nav.js carries `gdm-instant-loading` for exactly the fetch window (it
 * is cleared before the transition starts), so the bar tracks time-to-content and
 * nothing else. `both` fill-mode plus the delay means it holds the invisible
 * `from` state for the whole warm case. */
html.gdm-instant-loading::after {
	content: "";
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 2px;
	transform: scaleX(0);
	transform-origin: 0 50%;
	background: linear-gradient(
		90deg,
		rgba(255, 255, 255, 0.12) 0%,
		rgba(255, 255, 255, 0.55) 55%,
		rgba(255, 255, 255, 0.92) 100%
	);
	box-shadow: 0 0 12px rgba(255, 255, 255, 0.35);
	z-index: 2147483000;
	pointer-events: none;
	animation: gdm-instant-progress 2.6s cubic-bezier(0.16, 0.9, 0.24, 1) 240ms both;
}

@keyframes gdm-instant-progress {
	from {
		transform: scaleX(0);
		opacity: 0;
	}
	8% {
		opacity: 1;
	}
	to {
		/* Never reaches 100%: it is indeterminate, and the content arriving is what
		 * ends it. A bar that completes and then waits looks broken. */
		transform: scaleX(0.92);
		opacity: 1;
	}
}

@media (prefers-reduced-motion: reduce) {
	html.gdm-instant-loading::after {
		animation: none;
		transform: scaleX(1);
		opacity: 0.5;
		transition: opacity 0.2s linear 240ms;
	}
}

/* ---- Staging ribbon ---- */
html body.ghostdma-product-instant .gdm-product-instant-staging,
html body.ghostdma-product-instant-index .gdm-product-instant-staging {
	display: block;
	margin: 0 auto;
	/* Full width to match the now-uncapped shell (was capped 1180px). */
	max-width: none;
	/* Quiet marker, not a chrome band. The filled bar + bottom border drew a hard
	 * horizontal rule directly under the header that live has nothing like, and it
	 * was the first thing the eye hit on every product. Now it reads as a faint
	 * caption: still unmistakably "this is staging", but it no longer breaks the
	 * page into two visual halves before the product has even been seen. */
	padding: 0.5rem 0 0;
	font-size: 0.62rem;
	font-weight: 500;
	line-height: 1.4;
	letter-spacing: 0.22em;
	text-transform: uppercase;
	color: rgba(200, 204, 210, 0.42);
	background: transparent;
	border-bottom: 0;
	text-align: center;
}

html body.ghostdma-product-instant .gdm-product-instant-staging strong,
html body.ghostdma-product-instant-index .gdm-product-instant-staging strong {
	font-weight: 500;
}

html body.ghostdma-product-instant .gdm-product-instant-staging a,
html body.ghostdma-product-instant-index .gdm-product-instant-staging a {
	color: #fff;
	text-decoration: underline;
	text-underline-offset: 2px;
}

/* ---- Shell / transparent wrappers (wallpaper shows through) ---- */
html body.ghostdma-product-instant .gdm-product-instant-shell,
html body.ghostdma-product-instant .gdm-product__shell,
html body.ghostdma-product-instant .gdm-product__hero,
html body.ghostdma-product-instant .gdm-product__buy,
html body.ghostdma-product-instant .gdm-product__body,
html body.ghostdma-product-instant .gdm-product__related-wrap {
	background: transparent !important;
	background-image: none !important;
}

html body.ghostdma-product-instant .gdm-product-instant-shell {
	/* Match live Elementor's product container EXACTLY: the outer Elementor con
	 * (elementor-element-7dfc98bc) has no max-width and a flat 8% side padding, so
	 * the content column scales linearly with the viewport (measured: 1075px @1280,
	 * 1210px @1440, 1613px @1920, 2150px @2560). The old 1180px cap made instant
	 * stop widening past ~1400px, so it read narrower than live on big screens.
	 * Uncapped + 8% padding restores 1:1 width.
	 *
	 * The gutter stays a flat 8% at EVERY width, phones included — live does the
	 * same (measured 390px viewport: live content 327.6px = 390 - 2x31.2 = 8%).
	 * An earlier revision dropped phones to a 1rem gutter, which pushed instant's
	 * content 30px wider than live, ran text to the screen edge, and changed how
	 * long titles wrapped (pc-kit: 6 lines on instant vs 7 on live). */
	max-width: none;
	width: 100%;
	margin: 0 auto;
	padding: 0 8% 4rem;
	box-sizing: border-box;
}

/* ---- Vertical rhythm scale ------------------------------------------------
 * Every section gap on this route resolves from these four values instead of
 * being whatever margin collapsing happened to produce. Measured off the live
 * Elementor PDP so the page keeps live's cadence, but unlike live the numbers
 * are declared in one place and cannot drift per product.
 *
 *  --gdm-rhythm-title-price : baseline of the title to the price
 *  --gdm-rhythm-price-cart  : price to the first purchase control
 *  --gdm-rhythm-section     : description block to the "What's Trending" heading
 *  --gdm-rhythm-heading-grid: that heading to the card grid
 *
 * Live reference at 1440: 30 / 56.4 / 134.4 / 50  (content->grid total 226.4).
 * Live reference at 390:  15 / 29.4 / 94.8  / 22. */
html body.ghostdma-product-instant {
	/* Header bottom to the top of the hero. Live scales this with width: ~10-15px
	 * on phones, ~28px on tablets, 31px at 1280 and 36px at 1440 (measured). It
	 * used to be a flat 8px plus the 22px "Instant staging" caption. */
	--gdm-rhythm-header-hero: max(28px, 2.5vw);
	--gdm-rhythm-title-price: 30px;
	--gdm-rhythm-price-cart: 44px;
	--gdm-rhythm-hero-body: 12px;
	--gdm-rhythm-divider: 11px;
	--gdm-rhythm-section: 134px;
	--gdm-rhythm-heading-grid: 50px;
}

/* ---- Hero: buy column + product art ---- */
html body.ghostdma-product-instant .gdm-product__hero {
	display: grid;
	/* Match live proportions: on a 1140px row live renders the buy column ~592px
	 * and the hero image pane ~506px (text ~1.17 : image 1). The old 1 : 1.05 gave
	 * the image MORE room than the text, rendering the photo ~11% larger than live
	 * (the "too zoomed in" regression). */
	grid-template-columns: minmax(0, 1.17fr) minmax(0, 1fr);
	/* Elementor's two columns keep an 8vw centre channel: at 1440px the live
	 * buy/image gap is ~116px and at 1920px it is ~155px. The old capped 3vw
	 * gap made both columns wider and the artwork visibly oversized. */
	column-gap: 8vw;
	row-gap: 1.5rem;
	align-items: start;
	padding-top: var(--gdm-rhythm-header-hero);
	padding-bottom: 0;
}

html body.ghostdma-product-instant .gdm-product__title {
	margin: 0;
	font-family: "Questrial", sans-serif;
	font-size: 28px;
	font-weight: 400;
	font-style: normal;
	line-height: 42px;
	letter-spacing: 19px;
	text-transform: uppercase;
	color: rgb(152, 152, 152) !important;
	-webkit-text-stroke: 1px rgb(248, 248, 248);
	text-shadow: rgba(0, 0, 0, 0.3) 0 0 10px;
}

/* Match the live Elementor product-price widget exactly:
 * Questrial 22px / 400, colour rgb(148,153,151). (Was defaulting to the system
 * font at 18.4px / 500 / #d0d3d8 — visibly off vs live.) */
html body.ghostdma-product-instant .gdm-product__price-wrap .price,
html body.ghostdma-product-instant .gdm-product__price-wrap .price .amount,
html body.ghostdma-product-instant .gdm-product__price-wrap .price .amount bdi,
html body.ghostdma-product-instant .gdm-product__price-wrap .price bdi,
html body.ghostdma-product-instant .gdm-product__price-wrap .price ins,
html body.ghostdma-product-instant .gdm-product__price-wrap .price del,
html body.ghostdma-product-instant .gdm-product__price-wrap .price .woocommerce-Price-currencySymbol {
	font-family: "Questrial", sans-serif !important;
	font-size: 22px !important;
	font-weight: 400 !important;
	line-height: 1.4 !important;
	letter-spacing: normal !important;
	color: rgb(148, 153, 151) !important;
}

html body.ghostdma-product-instant .gdm-product__price-wrap .price {
	/* Live Elementor has a 30px widget gap above Price and another 30px widget
	 * gap after its own 14.4px margin before Add to cart. */
	margin: 0 0 var(--gdm-rhythm-price-cart) !important;
}

html body.ghostdma-product-instant .gdm-product__price-wrap {
	margin-top: var(--gdm-rhythm-title-price);
}

/* WooCommerce variable-price range shows a redundant screen-reader "Price range"
 * line; live hides it. Keep the visible range, drop the duplicate. */
html body.ghostdma-product-instant .gdm-product__price-wrap .price .screen-reader-text {
	position: absolute !important;
	width: 1px;
	height: 1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
}

html body.ghostdma-product-instant .gdm-product__cart {
	width: 100%;
	max-width: 100%;
	margin-bottom: 0.35rem;
}

/* Live PDP purchase-control typography. */
html body.ghostdma-product-instant .ghostdma-cart-actions .single_add_to_cart_button,
html body.ghostdma-product-instant .ghostdma-cart-actions .ghostdma_buy_now_button {
	font-family: "Questrial", sans-serif !important;
	font-size: 16px !important;
	font-weight: 600 !important;
	line-height: 1.2 !important;
	letter-spacing: 0.02em !important;
	text-transform: uppercase !important;
	white-space: nowrap;
}

/* CRITICAL anti-FOUC: the glass button skin normally arrives from the
 * mu-ghostdma-product-button.php <style> printed in wp_footer (PHP_INT_MAX). On
 * this native route there is no Elementor head CSS to pre-style the buttons, so
 * between first paint and the footer style the Add-to-cart / Buy-now render as
 * raw WooCommerce purple (rgb(127,84,179), 3px radius) and then snap to glass —
 * the flash the user reported. Because product-instant.css is enqueued in <head>,
 * duplicating the essential glass appearance here means the buttons paint correct
 * from the very first frame. The footer MU style still loads and simply re-asserts
 * the same values (harmless). Scoped to .ghostdma-product-instant so live is
 * untouched. Match the MU values exactly. */
html body.ghostdma-product-instant .single_add_to_cart_button.button,
html body.ghostdma-product-instant button.single_add_to_cart_button,
html body.ghostdma-product-instant .ghostdma_buy_now_button.button,
html body.ghostdma-product-instant button.ghostdma_buy_now_button {
	display: inline-flex !important;
	align-items: center !important;
	justify-content: center !important;
	background: rgba(255, 255, 255, 0.16) !important;
	border: 1px solid rgba(255, 255, 255, 0.28) !important;
	border-radius: 10px !important;
	color: #ffffff !important;
	font-weight: 600 !important;
	letter-spacing: 0.02em !important;
	line-height: 1.2 !important;
	padding: 14px 28px !important;
	min-height: 48px !important;
	box-sizing: border-box !important;
	text-decoration: none !important;
	text-shadow: none !important;
	box-shadow: 0 6px 16px rgba(0, 0, 0, 0.22) !important;
	transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease !important;
}

html body.ghostdma-product-instant .single_add_to_cart_button.button:hover,
html body.ghostdma-product-instant button.single_add_to_cart_button:hover,
html body.ghostdma-product-instant .single_add_to_cart_button.button:focus,
html body.ghostdma-product-instant button.single_add_to_cart_button:focus,
html body.ghostdma-product-instant .ghostdma_buy_now_button.button:hover,
html body.ghostdma-product-instant button.ghostdma_buy_now_button:hover,
html body.ghostdma-product-instant .ghostdma_buy_now_button.button:focus,
html body.ghostdma-product-instant button.ghostdma_buy_now_button:focus {
	background: rgba(255, 255, 255, 0.24) !important;
	border-color: rgba(255, 255, 255, 0.4) !important;
	color: #ffffff !important;
	transform: translateY(-1px) !important;
}

/* Anti-FOUC for the variation <select> dropdowns (same footer-style origin). */
html body.ghostdma-product-instant .variations select,
html body.ghostdma-product-instant .woocommerce-variation-add-to-cart select {
	-webkit-appearance: none !important;
	-moz-appearance: none !important;
	appearance: none !important;
	background-color: #1b1b1b !important;
	background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%239aa0a6' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E") !important;
	background-repeat: no-repeat !important;
	background-position: right 14px center !important;
	border: 1px solid rgba(255, 255, 255, 0.14) !important;
	border-radius: 10px !important;
	color: #e6e8ec !important;
	font-size: 14px !important;
	padding: 12px 40px 12px 16px !important;
	line-height: 1.3 !important;
}

/* Express-wallet "Select product options above to continue." hint.
 * On the curated /instant/ route we never want this sentence wedged between the
 * Apple/Google Pay combo and the Amazon express stack. The wallet MU-plugin still
 * greys/disables the combo button itself when options aren't chosen (so checkout
 * stays protected), but the TEXT is suppressed here. Head-loaded + !important so
 * the plugin's own footer script (hint.hidden = ...) can never re-reveal it.
 * Scoped to .ghostdma-product-instant, so live product pages are untouched. */
html body.ghostdma-product-instant #gdm-wallet-combo-hint,
html body.ghostdma-product-instant .gdm-wallet-combo__hint {
	display: none !important;
}

/* Hero product image — contain the full square like the live Elementor image,
 * blended against the body wallpaper with mix-blend-mode: difference (the same
 * "difference transformation" the live PDP hero uses via .gdm-hero-parallax-pane).
 *
 * IMPORTANT (blend footguns, per Home V2 playbook):
 *  - No overflow:hidden, isolation, filter, or settled transform on this node —
 *    any of those isolate the blend from the body wallpaper and flatten the photo.
 *  - border-radius clips the background on its own (background-clip:border-box),
 *    so rounded corners survive without overflow:hidden.
 */
html body.ghostdma-product-instant .gdm-product__media {
	position: relative;
	width: 100%;
	/* Height rule: never shorter than a SQUARE of its own column, and free to
	 * stretch taller when the purchase stack is taller.
	 *
	 * Every product image in the catalogue is a 600x600 square (verified across
	 * the range), and `background-size: contain` therefore paints a square of
	 * side min(paneW, paneH). The previous `min-height: clamp(360px,20vw,380px)`
	 * meant that on any product with a short buy column the pane collapsed to
	 * ~380px and the artwork shrank with it — tier-4 rendered 388px of art where
	 * live renders 497px, a visibly undersized hero. The square floor below fixes
	 * that and, as a bonus, makes the artwork the SAME relative size on every
	 * product. (Live's own pane heights are per-product Elementor leftovers that
	 * range 441-837px at 1440, so live is the inconsistent one here; the square
	 * matches live exactly on 5 of the 6 products sampled.)
	 *
	 * The floor is set by a zero-width padding spacer rather than `aspect-ratio`
	 * because the pane is a stretched grid item: `aspect-ratio` is ignored under
	 * stretch, whereas the spacer's `padding-top: 100%` resolves against the
	 * pane's own width and so raises the grid row's max-content height. */
	height: auto;
	min-height: 0;
	align-self: stretch;
	border-radius: 14px;
	overflow: visible;
	isolation: auto;
	background-color: transparent;
	background-image: var(--gdm-product-hero);
	background-repeat: no-repeat;
	/* Hint the decoder that this never paints above 600px, so the 1024 source is
	 * downscaled with a quality filter rather than shown 1:1. */
	image-rendering: auto;
	/* Live hero pane is top-aligned (background-position: 50% 0%). Match it so the
	 * product photo sits at the top of the hero column instead of floating in the
	 * vertical centre. */
	background-position: center top;
	/* Art size from the pane WIDTH only, capped at the 600px source (owner 2026-09-26:
	 * the photo "resizes" after arriving from home). With `contain` the art was
	 * min(width, height) — and the pane's height follows the buy column, so on wide
	 * screens (pane wider than 600) every change in that column (wallet buttons
	 * appearing, a different variation) re-sized the photo. The ::before floor below
	 * keeps the pane at least this tall, so the art never needs the height to fit.
	 * instant-nav.js artFrame() applies the same cap when it measures the landing. */
	/* Steps with the screen (owner): 1440 -> 504 (pane width), 1920 -> 600,
	 * 2560+ -> 640. Linear between, never above 640. */
	--gdm-hero-cap: min(640px, calc(480px + 6.25vw));
	background-size: min(100%, var(--gdm-hero-cap)) auto;
	mix-blend-mode: difference;
}

/* Retina source. A custom property holding an invalid value is still "valid" at
 * declaration time (it is only a token stream), so the usual two-declaration
 * fallback does not work for it — this @supports guard is what actually keeps
 * older engines on the plain url(). */
/* Upgrade inside the existing blended artwork layer. Never fade its ancestors. */
html body.ghostdma-product-instant .gdm-product__media[data-gdm-hero-full]::after {
	content: "";
	position: absolute;
	inset: 0;
	border-radius: inherit;
	background-image: var(--gdm-product-hero-full, none);
	background-size: inherit;
	background-position: inherit;
	background-repeat: no-repeat;
	pointer-events: none;
	opacity: 0;
	transition: opacity 220ms ease;
}
html body.ghostdma-product-instant .gdm-product__media[data-gdm-hero-quality="full"]::after {
	opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
	html body.ghostdma-product-instant .gdm-product__media[data-gdm-hero-full]::after {
		transition: none;
	}
}

/* Floor for the hero pane: at least min(its own width, 600px) tall, while still
 * free to stretch past that when the purchase stack is taller.
 *
 * NOTE FOR THE NEXT AGENT — this rule has been flipped twice, so here is the
 * whole trade-off in one place:
 *   - A pure square (an earlier revision) made the pane 723px tall at 1920px on
 *     every product, where live measures 580 (COD) / 709 (PC Kit) / 442 (Makcu).
 *     Too big, and that is why it was removed in 1.8.16.
 *   - Removing the floor entirely (1.8.16-1.8.17) sized the pane from the BUY
 *     COLUMN instead, which broke worse: tier-4 rendered 388px of art against
 *     live's 497px, and between 861px and 1024px — stacked layout, short column,
 *     nothing to stretch to — the pane collapsed to ZERO HEIGHT and the product
 *     image disappeared completely.
 *   - So: floor, but capped. 600px is not arbitrary; every catalogue image is a
 *     600x600 JPEG, so it is the largest size that never upscales the source.
 * Result: 1440px gives 504px (live 505.8), 1920px gives 600px (between live's
 * 580 and 674, never the old 723), 861-1024px gives the column width so the
 * image can never vanish again.
 *
 * `aspect-ratio` on a spacer child rather than on the pane itself, because the
 * pane is a stretched grid item and aspect-ratio is ignored under stretch —
 * whereas the child's height still raises the row's max-content height. The
 * spacer is transparent and paints nothing, so the difference blend is safe. */
html body.ghostdma-product-instant .gdm-product__media::before {
	content: "";
	display: block;
	width: 100%;
	aspect-ratio: 1 / 1;
	max-height: var(--gdm-hero-cap, 600px);
	pointer-events: none;
}

@media (max-width: 860px) {
	html body.ghostdma-product-instant .gdm-product__media::before {
		/* Stacked layout: the pane's own aspect-ratio already squares it. */
		display: none;
	}

	html body.ghostdma-product-instant .gdm-product__media {
		width: 100% !important;
		min-height: 0 !important;
		aspect-ratio: 1 / 1;
		/* Same 600px source-resolution cap as the desktop floor. Without it the
		 * stacked pane is a full-width square, so a 860px tablet showed a 722px
		 * tall image before any of the buy column — and the art jumped 722 -> 600
		 * across the 860/861 breakpoint. Capped, the two sides meet at 600. */
		max-height: 600px;
		background-size: contain !important;
		background-position: center center !important;
		mix-blend-mode: difference;
	}
}

html body.ghostdma-product-instant .gdm-product__media-img {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

/* Mouse parallax (product-instant.js initParallax): the pane drifts a few px with
 * sub-pixel `translate` instead of stepping background-position in whole pixels. */
html body.ghostdma-product-instant .gdm-product__media[data-gdm-parallax-ready] {
	translate: var(--gdm-parallax-tx, 0px) var(--gdm-parallax-ty, 0px);
}

/* Elementor-style scroll entrances, re-initialized after instant swaps. */
html body.ghostdma-product-instant.gdm-instant-motion-on .gdm-instant-reveal {
	opacity: 0;
	transform: translateY(22px);
	transition:
		opacity 0.7s cubic-bezier(0.22, 1, 0.36, 1) var(--gdm-instant-delay, 0ms),
		transform 0.8s cubic-bezier(0.22, 1, 0.36, 1) var(--gdm-instant-delay, 0ms);
	will-change: opacity, transform;
}

html body.ghostdma-product-instant.gdm-instant-motion-on .gdm-instant-reveal.is-in {
	opacity: 1;
	transform: none;
	will-change: auto;
}

/* Description paragraphs (initScrollReveals textItems): rise out of a soft blur, one by one.
 * Their own entrance, separate from the gallery / features below them. */
html body.ghostdma-product-instant.gdm-instant-motion-on .gdm-instant-reveal--text {
	transform: translateY(16px);
	filter: blur(6px);
	transition:
		opacity 0.75s cubic-bezier(0.22, 1, 0.36, 1) var(--gdm-instant-delay, 0ms),
		transform 0.85s cubic-bezier(0.22, 1, 0.36, 1) var(--gdm-instant-delay, 0ms),
		filter 0.75s cubic-bezier(0.22, 1, 0.36, 1) var(--gdm-instant-delay, 0ms);
}

html body.ghostdma-product-instant.gdm-instant-motion-on .gdm-instant-reveal--text.is-in {
	filter: none;
}

@media (prefers-reduced-motion: reduce) {
	html body.ghostdma-product-instant .gdm-instant-reveal {
		opacity: 1 !important;
		transform: none !important;
		transition: none !important;
	}
}

/* Phone / tablet buy-box entrance (product-instant.js initBuyFx; only that adds
 * .gdm-buyfx-on). Each row waits hidden for its turn; the motion itself is WAAPI. */
html body.ghostdma-product-instant .gdm-buyfx-on [data-gdm-fx]:not(.gdm-fx-in) {
	opacity: 0;
}

/* Old price: the strike draws across once the price has risen in. */
html body.ghostdma-product-instant .gdm-buyfx-on .single_variation del {
	position: relative;
	text-decoration: none !important;
}

html body.ghostdma-product-instant .gdm-buyfx-on .single_variation del * {
	text-decoration: none !important;
}

html body.ghostdma-product-instant .gdm-buyfx-on .single_variation del::after {
	content: "";
	position: absolute;
	left: -2%;
	right: -2%;
	top: 52%;
	height: 2px;
	margin-top: -1px;
	background: currentColor;
	pointer-events: none;
	transform-origin: left center;
	transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}

html body.ghostdma-product-instant .gdm-buyfx-on .single_variation:not(.gdm-fx-strike) del::after {
	transform: scaleX(0);
}

/* Variation price change (product-instant.js initPriceSwap): rolling digits. Inline
 * throughout so the sale price's underline keeps running under the digits. */
html body.ghostdma-product-instant .gdm-roll__slot,
html body.ghostdma-product-instant .gdm-roll__ch {
	position: relative;
}

html body.ghostdma-product-instant .gdm-roll__out {
	position: absolute;
	left: 0;
	top: 0;
	line-height: normal;
	white-space: pre;
	pointer-events: none;
}

html body.ghostdma-product-instant .gdm-roll__out--lead {
	left: auto;
	right: 100%;
}

/* One faint, slow light pass over Buy now / Add to cart (owner: "extremely subtle"). */
html body.ghostdma-product-instant .gdm-fx-sheen-host {
	position: relative !important;
	overflow: hidden !important;
}

html body.ghostdma-product-instant .gdm-fx-sheen {
	position: absolute;
	inset: 0;
	pointer-events: none;
	border-radius: inherit;
	background: linear-gradient(105deg, transparent 35%, rgba(255, 255, 255, 0.09) 50%, transparent 65%);
	translate: -130% 0;
}

html body.ghostdma-product-instant .gdm-buyfx-on .gdm-wallet-combo__split {
	transform-origin: center;
}

/* Desktop first paint carries the default variation's price (inc/product-instant.php).
 * Below 1025px it stays hidden until WooCommerce shows it, as before. */
@media (max-width: 1024px) {
	html body.ghostdma-product-instant .single_variation.gdm-variation-prefilled > * {
		display: none !important;
	}
}

/* ---- Description / gallery / cheat features ---- */
html body.ghostdma-product-instant .gdm-product__body {
	/* Breathing room above the divider rule that opens every product description.
	 * The first child's margin is zeroed just below, so this token is the SOLE
	 * owner of the hero-to-description gap — it cannot drift depending on whether
	 * a given description starts with an <hr>, a heading or a paragraph. Live
	 * measures 0 here, but live gets away with it because its hero row carries a
	 * lot of empty space underneath; the tighter instant hero puts the rule right
	 * up against the purchase stack, which reads as a mistake rather than a seam. */
	padding-top: var(--gdm-rhythm-hero-body);
	/* The description-to-Trending gap is owned by .gdm-product__related-wrap's
	 * padding-top (--gdm-rhythm-section) alone. Previously this 2rem pad stacked
	 * with the related wrap's 0.5rem AND whatever bottom margin the description's
	 * last node happened to carry, so the gap drifted between 57px and 80px from
	 * product to product — the single clearest "not thought out" spacing on the
	 * route. One owner, one number, identical on every product. */
	padding-bottom: 0;
}

html body.ghostdma-product-instant .gdm-product__content {
	max-width: 100%;
	color: #d0d3d8;
	/* Contain the description's inner margins. Without a block formatting context
	 * the bottom margin of the last node INSIDE a wrapper (the cheat-feature and
	 * hardware-feature blocks both end in one) collapses straight through the
	 * wrapper and out of this element, adding an uncontrolled 22px to the section
	 * gap on some products and not others. flow-root stops that escape without
	 * creating a stacking context, so the hero's difference blend is untouched. */
	display: flow-root;
}

/* The leading element's own top margin must be zero, or it stacks on top of
 * --gdm-rhythm-hero-body and the token stops being the single source of truth.
 * The extra `.gdm-product__body` in the chain is deliberate: a mobile rule gives
 * a leading <hr> `margin-top: 8px !important`, which outranked the shorter
 * selector and silently added 8px on phones only. */
html body.ghostdma-product-instant .gdm-product__body .gdm-product__content > :first-child {
	margin-top: 0 !important;
}

/* Dividers sit symmetrically in the description.
 *
 * A <hr> between two paragraphs looked lopsided because the two sides are set by
 * DIFFERENT things. Above the text it is the divider's own margin-bottom; below the
 * text it is a margin COLLAPSE between the paragraph's margin-bottom and the next
 * divider's margin-top, and the larger of the two wins. Measured on this product:
 *   live    1440: 14px above the text, 10px below   |  390: 10 above, 6 below
 *   instant 1440: 14 / 10                            |  390:  8 above, 6 below
 * So neither route was even, and instant was also 2px tighter than live on phones.
 *
 * The real cause is that a shared MU-plugin ships TWO divider rules:
 *     ... hr             { margin: 14px 0 !important }   (8px under 767px)
 *     ... hr:has(+ .gdg) { margin: 10px 0 !important }   (6px under 767px)
 * i.e. the divider immediately preceding a gallery is deliberately tighter. A
 * paragraph sitting between a normal divider and a pre-gallery one is therefore
 * bracketed by 14 above and 10 below, and reads lopsided. Live has exactly the same
 * markup and exactly the same result.
 *
 * This normalises BOTH divider types to one value on the instant route, so every
 * divider sits symmetrically and all of them match each other. Only spacing is
 * touched — colour and thickness stay with the theme.
 *
 * SPECIFICITY: `hr:has(+ .gdg)` scores (0,3,2) because :has() contributes its
 * argument's specificity, which beat the first attempt at this rule (0,2,3) and
 * made it silently do nothing. Chaining `.gdm-product__content` with the widget
 * class it already carries takes this to (0,3,3)/(0,4,3) and wins cleanly.
 *
 * The leading divider is exempt: its top spacing is owned by --gdm-rhythm-hero-body
 * via the :first-child reset above, otherwise the two would stack. */
html body.ghostdma-product-instant .gdm-product__content.elementor-widget-woocommerce-product-content hr,
html body.ghostdma-product-instant .gdm-product__content.elementor-widget-woocommerce-product-content hr:has(+ .gdg) {
	/* Each side falls back to the symmetric token, so only the axis that needs to
	 * differ has to be declared (phones run 6 above / 7 below for optical balance). */
	margin-top: var(--gdm-rhythm-divider-top, var(--gdm-rhythm-divider)) !important;
	margin-bottom: var(--gdm-rhythm-divider-bottom, var(--gdm-rhythm-divider)) !important;
}

/* Kill the trailing margin so the section gap below is exactly --gdm-rhythm-section
 * whether the description ends in a <p>, a feature matrix, or a gallery. */
html body.ghostdma-product-instant .gdm-product__content > :last-child {
	margin-bottom: 0 !important;
}

/* ---- Related / What's Trending ---- */
html body.ghostdma-product-instant .gdm-product__related-wrap {
	/* Live's related grid is a flat 1000px at every viewport width (measured
	 * identical at 1280/1440/1920) with a 40px/15px gap. The old 983.5px came
	 * from a single 1366px sample and left the grid 16.5px narrower than live. */
	width: min(100%, 1000px);
	margin: 0 auto;
	padding-top: var(--gdm-rhythm-section);
}

html body.ghostdma-product-instant .gdm-product__related-title {
	margin: 0 0 var(--gdm-rhythm-heading-grid);
	text-align: center;
	font-family: "Questrial", sans-serif;
	font-size: 28px;
	font-weight: 400;
	line-height: 42px;
	letter-spacing: 19px;
	text-transform: uppercase;
	color: rgb(192, 192, 192) !important;
}

html body.ghostdma-product-instant .gdm-product__related .related.products > h2 {
	display: none;
}

html body.ghostdma-product-instant .gdm-product__related ul.products {
	display: grid !important;
	grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
	gap: 40px 15px !important;
	width: 100% !important;
	margin: 0 !important;
	padding: 0 !important;
}

html body.ghostdma-product-instant .gdm-product__related ul.products::before,
html body.ghostdma-product-instant .gdm-product__related ul.products::after {
	display: none !important;
	content: none !important;
}

html body.ghostdma-product-instant .gdm-product__related ul.products li.product {
	float: none !important;
	clear: none !important;
	width: auto !important;
	height: 100% !important;
	margin: 0 !important;
}

/* Safety net: the args filter already returns three, but never show a stray fourth. */
html body.ghostdma-product-instant .gdm-product__related ul.products li.product:nth-child(n + 4) {
	display: none !important;
}

/* Live hides the WooCommerce round "Sale!" flash on Trending cards (the tacky
 * olive-yellow "sail"). Match that exactly. */
html body.ghostdma-product-instant .gdm-product__related .onsale,
html body.ghostdma-product-instant .gdm-product__related span.onsale,
html body.ghostdma-product-instant .gdm-product__related li.product .onsale {
	display: none !important;
	visibility: hidden !important;
}

html body.ghostdma-product-instant .gdm-product__related li.product .woocommerce-loop-product__title {
	padding: 11px 0 !important;
	font-family: "Questrial", sans-serif !important;
	font-size: 22px !important;
	font-weight: 600 !important;
	line-height: 1.35 !important;
	letter-spacing: 9px !important;
	text-transform: uppercase !important;
}

html body.ghostdma-product-instant .gdm-product__related li.product .price,
html body.ghostdma-product-instant .gdm-product__related li.product .price .amount {
	font-family: "Questrial", sans-serif !important;
	font-size: 22px !important;
	font-weight: 600 !important;
	line-height: 1.5 !important;
	letter-spacing: 5px !important;
	text-transform: uppercase !important;
}

html body.ghostdma-product-instant .gdm-product__related li.product a.button {
	font-family: "Questrial", sans-serif !important;
}

/* ---- V4 index page ---- */
html body.ghostdma-product-instant-index .gdm-product-instant-index {
	max-width: 720px;
	margin: 0 auto;
	padding: 2rem 1.25rem 4rem;
	color: #d0d3d8;
}

html body.ghostdma-product-instant-index .gdm-product-instant-index__title {
	margin: 0 0 0.5rem;
	font-family: "Questrial", "DM Sans", system-ui, sans-serif;
	font-size: 1.75rem;
	color: #fff;
}

html body.ghostdma-product-instant-index .gdm-product-instant-index__lead {
	margin: 0 0 1.5rem;
	color: #9aa0a6;
}

html body.ghostdma-product-instant-index .gdm-product-instant-index__list {
	list-style: none;
	margin: 0;
	padding: 0;
}

html body.ghostdma-product-instant-index .gdm-product-instant-index__list li {
	padding: 0.55rem 0;
	border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

html body.ghostdma-product-instant-index .gdm-product-instant-index__list a {
	color: #fff;
	text-decoration: none;
	font-weight: 500;
}

html body.ghostdma-product-instant-index .gdm-product-instant-index__list a:hover {
	text-decoration: underline;
}

html body.ghostdma-product-instant-index .gdm-product-instant-index__live {
	margin-left: 0.35rem;
	font-size: 0.88em;
	color: #8f959c;
}

html body.ghostdma-product-instant-index .gdm-product-instant-index__live a {
	color: #aab0b8;
	font-weight: 400;
}

/* ---- Mobile: stack buy above image (matches Elementor tablet behavior) ---- */
@media (max-width: 1024px) {
	html body.ghostdma-product-instant .gdm-product__title {
		font-size: 24px;
		line-height: 36px;
		letter-spacing: 15px;
	}

	html body.ghostdma-product-instant .gdm-product__hero {
		grid-template-columns: 1fr;
	}

	html body.ghostdma-product-instant .gdm-product__media {
		order: -1;
		min-height: 0;
	}

	html body.ghostdma-product-instant .gdm-product__buy {
		order: 0;
	}

	/* Two columns through the tablet range. Dropping straight to one column here
	 * made a single Trending card 822px wide at 1024px — a product thumbnail
	 * bigger than the hero. (Live goes to one column too, but narrows its whole
	 * related container to 704px to compensate; two columns at full width reads
	 * better and keeps the card size continuous with the desktop grid.) */
	html body.ghostdma-product-instant .gdm-product__related ul.products {
		grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
		gap: 30px 15px !important;
	}

	/* Stacked layout steps the whole scale down one notch. */
	html body.ghostdma-product-instant {
		--gdm-rhythm-header-hero: 28px;
		--gdm-rhythm-hero-body: 11px;
		--gdm-rhythm-divider: 9px;
		--gdm-rhythm-section: 110px;
		--gdm-rhythm-heading-grid: 36px;
	}
}

@media (max-width: 767px) {
	/* Phone values measured off live at 390px: title->price 15, price->cart 29.4,
	 * description->Trending 94.8, heading->grid 22. Instant was running the
	 * desktop scale here (30 / 44 / 134 / 22), which read loose at the top of the
	 * page and cramped at the bottom — the opposite of live's cadence. */
	html body.ghostdma-product-instant {
		--gdm-rhythm-header-hero: calc(2.5vw + 1px);
		--gdm-rhythm-title-price: 15px;
		--gdm-rhythm-price-cart: 30px;
		--gdm-rhythm-hero-body: 10px;
		--gdm-rhythm-divider: 7px;
		--gdm-rhythm-divider-top: 6px;
		--gdm-rhythm-divider-bottom: 7px;
		--gdm-rhythm-section: 95px;
		--gdm-rhythm-heading-grid: 22px;
	}

	/* Match live's phone gutters.
	 *
	 * Live runs THREE different gutters below 768px: 5% for the purchase column,
	 * 7% for Trending, 8% for the description. The instant shell uses a flat 8% for
	 * everything, which left the title / price / cart / wallets measurably narrower
	 * than live — 21.6px at 360, 23.4 at 390, 32.4 at 540, 46.0 at 767. The
	 * description, gallery and feature matrix already matched exactly and are left
	 * alone.
	 *
	 * The shell's content box is 84vw (100 - 2x8), so a -3vw inline margin gives 90vw
	 * = 5% gutters, and -1vw gives 86vw = 7%. Viewport units rather than percentages
	 * because a percentage margin resolves against the 84vw content box, not the
	 * screen. Verified to land on live's exact pixel values.
	 *
	 * Phone only: at 768px and up live switches to a narrow 297.9px column that the
	 * instant full-width stack deliberately improves on, so parity stops here. */
	html body.ghostdma-product-instant .gdm-product__hero {
		margin-left: -3vw;
		margin-right: -3vw;
	}

	html body.ghostdma-product-instant .gdm-product__related-wrap {
		/* `width: auto` is not enough here — the base rule pins
		 * `width: min(100%, 1000px)`, so the negative margins only SHIFTED the block
		 * instead of widening it (correct left edge, still 84vw wide). State the width
		 * explicitly so both edges move. */
		width: calc(100% + 2vw) !important;
		/* ...and `max-width: 100%` was then clamping that straight back to 84vw, so the
		 * block still only shifted. Both have to be released. */
		max-width: none !important;
		margin-left: -1vw;
		margin-right: -1vw;
	}

	html body.ghostdma-product-instant .gdm-product__related ul.products {
		grid-template-columns: 1fr !important;
		gap: 22px !important;
	}

	/* Instant-only card entrances override Home V2's product-page Safari guard.
	 * Use relative top instead of a transform so the related image and footer
	 * never enter the GPU layer path that produced ghost tiles on iOS. */
	html body.ghostdma-chrome-v2.single-product.ghostdma-product-instant.gdm-instant-motion-on .gdm-product__related .related.products ul.products li.product {
		position: relative;
		/* 2.75rem is Home V2's card travel (see home-v2.css .gdm-reveal). Matching
		 * it — along with the 0.75s/0.85s pair and the same easing below — is what
		 * makes these read as the same entrance rather than a smaller cousin. */
		top: 2.75rem;
		opacity: 0 !important;
		transform: none !important;
		transition: none !important;
		pointer-events: none;
	}

	html body.ghostdma-chrome-v2.single-product.ghostdma-product-instant.gdm-instant-motion-on .gdm-product__related .related.products ul.products li.product.is-in {
		top: 0;
		opacity: 1 !important;
		transform: none !important;
		transition:
			opacity 0.75s cubic-bezier(0.22, 1, 0.36, 1),
			top 0.85s cubic-bezier(0.22, 1, 0.36, 1) !important;
		pointer-events: auto;
	}
}

@media (max-width: 767px) {
	html body.ghostdma-product-instant .gdm-product__title {
		font-size: 20px;
		line-height: 30px;
		letter-spacing: 8.5px;
	}

	html body.ghostdma-product-instant .gdm-product__related-title {
		font-size: 20px;
		line-height: 30px;
		letter-spacing: 8.5px;
	}

	html body.ghostdma-product-instant .gdm-product__related li.product .woocommerce-loop-product__title {
		padding: 0 !important;
		font-size: 15px !important;
		line-height: 1.25 !important;
		letter-spacing: 0.04em !important;
	}

	html body.ghostdma-product-instant .gdm-product__related li.product .price,
	html body.ghostdma-product-instant .gdm-product__related li.product .price .amount {
		font-size: 14px !important;
		line-height: 1.3 !important;
		letter-spacing: 0.05em !important;
	}

	html body.ghostdma-product-instant .ghostdma-cart-actions .single_add_to_cart_button,
	html body.ghostdma-product-instant .ghostdma-cart-actions .ghostdma_buy_now_button {
		font-size: 14px !important;
		line-height: 1.1 !important;
		letter-spacing: 0.03em !important;
	}

	/* Live's phone purchase buttons are 40px tall; the desktop anti-FOUC block
	 * pins min-height 48px, which made the instant pair 8px taller than live and
	 * pushed the whole express-wallet stack down. */
	html body.ghostdma-product-instant .single_add_to_cart_button.button,
	html body.ghostdma-product-instant button.single_add_to_cart_button,
	html body.ghostdma-product-instant .ghostdma_buy_now_button.button,
	html body.ghostdma-product-instant button.ghostdma_buy_now_button {
		min-height: 40px !important;
		padding: 10px 20px !important;
	}

	/* Live drops the price to 16px on mobile; our desktop rule pins 22px, so
	 * override it here to keep parity. */
	html body.ghostdma-product-instant .gdm-product__price-wrap .price,
	html body.ghostdma-product-instant .gdm-product__price-wrap .price .amount,
	html body.ghostdma-product-instant .gdm-product__price-wrap .price .amount bdi,
	html body.ghostdma-product-instant .gdm-product__price-wrap .price bdi,
	html body.ghostdma-product-instant .gdm-product__price-wrap .price ins,
	html body.ghostdma-product-instant .gdm-product__price-wrap .price del,
	html body.ghostdma-product-instant .gdm-product__price-wrap .price .woocommerce-Price-currencySymbol {
		font-size: 16px !important;
	}
}

/* The flight layer's two parts (instant-nav.js performSwap): a canvas copy of the
 * pixels already on screen underneath, and the same photo by URL on top. Both fitted
 * like the hero (contain, top-centred) and clipped to the layer's radius. */
.gdm-flight-art {
	overflow: hidden;
}
.gdm-flight-art > .gdm-flight-art__px,
.gdm-flight-art > .gdm-flight-art__bg {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	display: block;
}
.gdm-flight-art > .gdm-flight-art__px {
	object-fit: contain;
	object-position: center top;
}
.gdm-flight-art > .gdm-flight-art__bg {
	background-size: contain;
	background-position: center top;
	background-repeat: no-repeat;
}
