/* LumGem Product page — only loaded on is_product() (see inc/enqueue.php). */

/**
 * "Soft Pop Luxury" design tokens — product-page-scoped custom properties
 * (declared on `.single-product`, WooCommerce's own body class for product
 * pages, rather than `:root`) so this palette never leaks onto the
 * homepage/category/Cart/Checkout even if this stylesheet were ever
 * accidentally enqueued elsewhere. This round is a visual sample confined to
 * the product page only — sitewide adoption is a separate, later decision.
 */
.single-product {
	--lumgem-bg: #FFFDFC;
	--lumgem-surface: #FFFFFF;
	--lumgem-blush: #FBEDEA;
	--lumgem-champagne: #F7F1E8;
	--lumgem-gold: #C9953E;
	--lumgem-gold-hover: #AD7A2D;
	--lumgem-coral: #D96F5D;
	--lumgem-emerald: #12684F;
	--lumgem-text: #1D1916;
	--lumgem-muted: #6F655F;
	--lumgem-border: #E8DED7;
	background: var(--lumgem-bg);
}

/*
 * Outer product-page layout: gallery LEFT, buy-box RIGHT on desktop.
 *
 * WooCommerce's own content-single-product.php puts .lumgem-gallery (via
 * woocommerce_before_single_product_summary) and .summary.entry-summary as
 * DIRECT SIBLING children of div#product-{id}.product — plus the sale-flash
 * badge before them, and (after summary closes) the anchor row / trust
 * accordion / native tabs / upsells / related-products as further direct
 * siblings via woocommerce_after_single_product_summary. This is the outer
 * two-column grid that was missing: .lumgem-gallery's own internal
 * display:flex (further below) only lays out its own rail+stage — it is a
 * separate, nested layout from this one.
 */
.single-product div.product {
	position: relative; /* containing block for the .onsale badge below */
}

.single-product div.product > .onsale {
	position: absolute;
	top: var(--lumgem-space-3);
	left: var(--lumgem-space-3);
	z-index: 5;
	margin: 0;
	/* WooCommerce core's own default is an olive/khaki fill (no theme rule
	   overrode it before) — replaced with the coral promo color instead. */
	background: var(--lumgem-coral);
	color: #fff;
}

@media (min-width: 1024px) {
	.single-product div.product {
		display: grid;
		/* ~55% / ~40% split (fr ratio 55:40 = 1.375:1) with the gap eating
		   the remaining ~5% — matches the Jeulia reference proportions
		   rather than an arbitrary fixed right-column width. */
		grid-template-columns: minmax(0, 1.375fr) minmax(0, 1fr);
		gap: 40px;
		align-items: start;
		max-width: var(--lumgem-container-max);
		margin: 0 auto;
		padding: var(--lumgem-space-5);
	}

	.single-product div.product > .lumgem-gallery {
		grid-column: 1;
	}

	.single-product div.product > .summary.entry-summary {
		grid-column: 2;
		/* Core woocommerce.css still assumes the old float-based two-column
		   layout and sets .summary to width:48%/float:left — since our grid
		   column is already the sizing mechanism, that 48% would resolve
		   against the 420px track (≈202px) and squeeze the buy-box. Reset
		   both explicitly, same pattern as checkout's #customer_details.col2-set. */
		width: 100%;
		float: none;
	}

	.single-product div.product > .lumgem-gallery {
		float: none;
	}

	.single-product div.product > .lumgem-single-anchor-row,
	.single-product div.product > .lumgem-lower-section,
	.single-product div.product > .lumgem-carousel--product-page,
	.single-product div.product > #lumgem-recently-viewed-root {
		grid-column: 1 / -1;
	}
}

/* Mobile: the product root gets NO side padding at all (was
   `0 var(--lumgem-space-5)`, which pushed the media stage/sticky header
   stack/anchor nav/bottom bar in by 24px on every side — flagged by an
   independent audit as media not actually reaching full width). Full-bleed
   elements (.lumgem-gallery, the sticky announcement/header/anchor-nav
   stack, the fixed bottom bar) get zero padding of their own and now
   genuinely span the viewport; everything else that needs an inset
   (buy-box content, the trust list, native tabs) gets its OWN ~12px padding
   below instead of inheriting it from this shared parent. Uses width:100%
   (the default block behavior once padding is 0) rather than 100vw
   anywhere, which avoids the classic 100vw-vs-scrollbar horizontal-overflow
   trap. */
@media (max-width: 1023px) {
	.single-product div.product {
		padding: 0;
	}

	.single-product div.product > .summary.entry-summary {
		padding: 0 var(--lumgem-space-3);
	}

	/* .lumgem-lower-section and .lumgem-carousel--product-page get their own
	   mobile padding in the "LOWER SECTIONS" block further down this file. */
}

.product .lumgem-gallery {
	display: flex;
	gap: var(--lumgem-space-4);
	margin-bottom: var(--lumgem-space-5);
}

.lumgem-gallery__rail {
	display: flex;
	flex-direction: column;
	gap: var(--lumgem-space-2);
	flex: 0 0 80px;
	max-height: 560px;
	overflow-y: auto;
}

/* .lumgem-scope-prefixed (2 classes) so these unambiguously beat
   components.css's generic `.lumgem-scope button` reset (1 class + 1
   element — still higher specificity than a single bare class, the same
   recurring cascade trap fixed elsewhere on this site: without the prefix,
   every one of these buttons silently rendered with that reset's solid ink
   background + white text + boxy border-radius instead of its own declared
   look, which is exactly what an independent audit flagged as "black
   thumbnail cards / black block arrows"). */
.lumgem-scope .lumgem-gallery__thumb {
	display: block;
	width: 72px;
	height: 72px;
	padding: 0;
	border: 1px solid var(--lumgem-color-line);
	border-radius: var(--lumgem-radius);
	background: #fff;
	overflow: hidden;
	cursor: pointer;
	position: relative;
	flex-shrink: 0;
}

.lumgem-scope .lumgem-gallery__thumb.is-active {
	border-color: var(--lumgem-color-ink);
	border-width: 2px;
}

.lumgem-gallery__thumb img {
	width: 100%;
	height: 100%;
	/* Real photo shown in full on a white backdrop, never cropped — matches
	   the main stage's own contain behavior instead of the previous
	   edge-to-edge cover crop. */
	object-fit: contain;
}

.lumgem-scope .lumgem-gallery__thumb--video,
.lumgem-scope .lumgem-gallery__thumb--360 {
	display: flex;
	align-items: center;
	justify-content: center;
	background: var(--lumgem-color-surface-alt);
}

.lumgem-gallery__play-icon {
	display: block;
	width: 0;
	height: 0;
	border-top: 8px solid transparent;
	border-bottom: 8px solid transparent;
	border-left: 14px solid var(--lumgem-color-ink);
}

/* Real rotate-arrows icon + label, dark text on the thumb's light
   background — never a solid dark block with white text. */
.lumgem-gallery__360-icon {
	display: block;
	width: 20px;
	height: 20px;
	color: var(--lumgem-color-ink);
}

.lumgem-gallery__360-icon svg {
	width: 100%;
	height: 100%;
}

.lumgem-gallery__360-label {
	font-size: 11px;
	font-weight: 700;
	color: var(--lumgem-color-ink);
	margin-top: 2px;
}

/* --- Desktop-only thumbnail rail rules ------------------------------------
   Mobile physically moves these same buttons into .lumgem-gallery__mobile-
   controls as a small overlay (see the max-width:1023px block further down)
   with its own distinct circular translucent styling — none of this may
   leak there, hence everything here lives inside min-width:1024px. */
@media (min-width: 1024px) {
	/* Fixed visual order Video -> photos -> 360°, achieved with the `order`
	   property alone (CSS `order` never changes DOM order, so
	   single-product.js's thumbs/imageThumbs arrays, the 1/N counter, and
	   activeThumb bookkeeping — all of which walk the real DOM — are
	   completely unaffected; only paint order changes). */
	.lumgem-gallery__thumb--video {
		order: -1;
	}

	.lumgem-gallery__thumb[data-type="image"] {
		order: 0;
	}

	.lumgem-gallery__thumb--360 {
		order: 1;
	}

	/* At most 5 image thumbs shown in the rail — extras stay fully valid,
	   clickable-via-arrows slides (nothing removed from the DOM or from
	   imageThumbs/the counter), just not individually listed here. Because
	   images are always the first N children in the actual markup (Video/360
	   are appended after them — see product-image.php), the 6th real DOM
	   child onward among data-type="image" buttons is reliably the 6th+
	   photo, regardless of the `order`-based reshuffle above. */
	.lumgem-gallery__thumb[data-type="image"]:nth-child(n+6) {
		display: none;
	}

	/* Real cover image (YouTube's own official thumbnail, or the product's
	   main image as fallback — see $lumgem_video_poster in
	   product-image.php) instead of a flat color block. */
	.lumgem-gallery__thumb--video img {
		position: absolute;
		inset: 0;
		width: 100%;
		height: 100%;
		object-fit: cover;
	}

	/* An unmistakable red play button — filled circle + white triangle, the
	   universal "play this video" affordance — layered over the cover image. */
	.lumgem-gallery__thumb--video .lumgem-gallery__play-icon {
		position: relative;
		z-index: 1;
		display: flex;
		align-items: center;
		justify-content: center;
		width: 26px;
		height: 26px;
		border: none;
		border-radius: 999px;
		background: #e02020;
		box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
	}

	.lumgem-gallery__thumb--video .lumgem-gallery__play-icon::after {
		content: "";
		display: block;
		width: 0;
		height: 0;
		margin-left: 2px;
		border-top: 6px solid transparent;
		border-bottom: 6px solid transparent;
		border-left: 10px solid #fff;
	}
}

.lumgem-gallery__stage {
	position: relative;
	flex: 1 1 auto;
	min-width: 0;
	aspect-ratio: 1;
	background: var(--lumgem-color-surface-alt);
	border-radius: var(--lumgem-radius);
	overflow: hidden;
}

/* The stage itself is the keyboard-navigable unit (tabindex="0" in
   product-image.php) — left/right arrow keys step through the same unified
   media sequence the on-screen arrows and thumbnails use (see
   initGallery()'s keydown handler). Visible focus ring only for keyboard
   users, never for a mouse click. */
.lumgem-gallery__stage:focus-visible {
	outline: 2px solid var(--lumgem-color-ink);
	outline-offset: 2px;
}

.lumgem-gallery__stage-image {
	width: 100%;
	height: 100%;
	/* Main image must show in full, never cropped/zoomed — "contain" logic
	   per the Jeulia acceptance bar (thumbnails may still crop to a square,
	   see .lumgem-gallery__thumb img above, but the main stage may not). */
	object-fit: contain;
}

.lumgem-gallery__stage-video,
.lumgem-gallery__stage-360 {
	position: absolute;
	inset: 0;
}

.lumgem-gallery__stage-video iframe,
.lumgem-gallery__stage-video video,
.lumgem-gallery__stage-360 iframe,
.lumgem-gallery__stage-360 img {
	width: 100%;
	height: 100%;
	object-fit: contain;
	border: none;
}

.lumgem-scope .lumgem-gallery__close {
	position: absolute;
	top: var(--lumgem-space-2);
	right: var(--lumgem-space-2);
	width: 32px;
	height: 32px;
	padding: 0;
	border-radius: 999px;
	border: none;
	background: rgba(0, 0, 0, 0.6);
	color: #fff;
	font-size: 18px;
	line-height: 1;
	cursor: pointer;
	z-index: 5;
}

/* No filled circle/square by default — a bare, mid-gray thin-line chevron
   (see the inline SVG in product-image.php) sitting directly on the media.
   The 44px box is the click/tap target only; visually just the glyph
   shows, so it never reads as "a black block" the way a solid background
   previously did (see the .lumgem-scope prefix note above .lumgem-gallery__thumb).
   A genuine mid-gray color (--lumgem-muted) rather than "ink at low
   opacity" — the latter still visually reads closer to black than gray at
   these opacity levels. */
.lumgem-scope .lumgem-gallery__arrow {
	position: absolute;
	top: 50%;
	transform: translateY(-50%);
	width: 44px;
	height: 44px;
	padding: 0;
	border: none;
	border-radius: 999px;
	background: transparent;
	color: var(--lumgem-muted);
	/* Explicit opacity (not just a lighter text color) — a bare, low-contrast
	   line chevron at rest; identical on both sides by construction (only
	   `left`/`right` differ between --prev/--next below), so any asymmetry
	   seen in a screenshot is a hover/focus state caught mid-interaction,
	   not a real left/right difference in this rule. */
	opacity: 0.6;
	cursor: pointer;
	display: flex;
	align-items: center;
	justify-content: center;
	/* Very light shadow so the glyph stays legible over pale/light-toned
	   product photos — not a filled background block. */
	filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.15));
	transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
}

.lumgem-gallery__arrow svg {
	width: 28px;
	height: 28px;
	pointer-events: none;
}

.lumgem-scope .lumgem-gallery__arrow:hover {
	opacity: 0.9;
	color: var(--lumgem-text);
	background: rgba(29, 25, 22, 0.06);
}

.lumgem-scope .lumgem-gallery__arrow:focus-visible {
	opacity: 0.9;
	color: var(--lumgem-text);
	background: rgba(29, 25, 22, 0.06);
	outline: 2px solid var(--lumgem-gold);
	outline-offset: 2px;
}

.lumgem-gallery__arrow--prev {
	left: var(--lumgem-space-4);
}

.lumgem-gallery__arrow--next {
	right: var(--lumgem-space-4);
}

@media (max-width: 1023px) {
	.lumgem-gallery__arrow svg {
		width: 24px;
		height: 24px;
	}
}

.lumgem-gallery__counter {
	position: absolute;
	bottom: var(--lumgem-space-2);
	right: var(--lumgem-space-2);
	background: rgba(0, 0, 0, 0.6);
	color: #fff;
	font-size: 11px;
	padding: 2px 8px;
	border-radius: 999px;
	display: none;
}

/* Buy-box — native WooCommerce markup, restyled only. Width is constrained
   by the 420px grid track above on desktop; on mobile it's simply full-width
   in normal document flow (no max-width needed there either). */

/* Current/sale price — WooCommerce core's own default price color resolves
   to an olive/khaki tone (no theme rule overrode it before), exactly what
   this page's palette explicitly avoids; the coral promo color makes the
   current price the clear primary visual instead. Not gated to a
   max-width block — this applies at every viewport, since the olive
   default was never viewport-specific either. */
.summary.entry-summary .price ins {
	color: var(--lumgem-coral);
	text-decoration: none;
}

.summary.entry-summary .price del {
	color: var(--lumgem-muted);
	opacity: 1;
}

/* Star rating — WooCommerce core (woocommerce.css) sets a color on the
   EMPTY-star background (`.star-rating::before { color:#cfc8d8 }`) but
   never sets one on the FILLED-star overlay (`.star-rating span::before`),
   so it was silently inheriting whatever text color its container
   happened to have (the muted gray used for SKU/meta text) instead of
   reading as an actual rating. Applies to every `.star-rating` this page
   renders (main summary AND the Reviews tab's own average-rating display),
   so both read consistently rather than gold in one place and gray in
   another. */
.star-rating span::before {
	color: var(--lumgem-gold);
}

/* "(N customer reviews)" — was inheriting the same muted gray as
   surrounding meta text; a clear, readable color (with the underline it
   already had) makes it read as a clickable link to the Reviews section
   rather than blending into the SKU/meta row. */
.woocommerce-product-rating .woocommerce-review-link,
.lumgem-reviews-faq__panel .woocommerce-review-link {
	color: var(--lumgem-text);
	text-decoration: underline;
}

/* Real "no reviews yet" empty state (see lumgem_single_product_no_reviews_fallback()
   in single-product-hooks.php) — only ever rendered when the product
   genuinely has zero reviews, so this is never shown alongside a real star
   rating. Mobile-only: this row was added specifically to fill the
   SKU+rating line in .lumgem-mobile-meta-row for Jeulia-style parity; on
   desktop the buy-box's vertical rhythm (and specifically fitting the Add to
   Cart button within the 1366x768 first screen, verified in an earlier
   acceptance round) was already tuned around WooCommerce's native
   behavior of rendering NOTHING for a 0-review product — adding 40+px of
   real height there on desktop pushed Add to Cart below the fold, so this
   is hidden at >=1024px and only shown on mobile. */
.woocommerce-product-rating.lumgem-no-reviews {
	display: flex;
	align-items: center;
	gap: var(--lumgem-space-2);
	font-size: 12px;
	color: var(--lumgem-color-muted);
	margin: 0 0 var(--lumgem-space-2);
}

.lumgem-no-reviews .woocommerce-review-link {
	text-decoration: underline;
}

@media (min-width: 1024px) {
	.woocommerce-product-rating.lumgem-no-reviews {
		display: none;
	}
}

/* Small fulfillment trust badge near the title (see single-product-hooks.php).
   Emerald rather than gold — one of this page's "gem-tone" accent colors
   (see the goal's "emerald/Paraiba teal accents"), used sparingly so gold
   stays reserved for the primary CTA. */
.lumgem-single-trust-badge {
	display: inline-block;
	font-size: 12px;
	color: var(--lumgem-emerald);
	font-weight: 600;
	margin: var(--lumgem-space-1) 0 var(--lumgem-space-2);
}

/* Wishlist toggle — right after Add to Cart. Lightweight client-side
   (localStorage) affordance, see assets/js/wishlist.js. `.lumgem-scope`
   prefix (2 classes) beats components.css's generic `.lumgem-scope button`
   reset (1 class + 1 element) — without it this rendered as a solid
   ink-background/white-text block instead of the intended thin-bordered
   secondary button, the exact same recurring cascade bug fixed elsewhere in
   this project. */
.lumgem-scope .lumgem-wishlist-toggle {
	display: inline-flex;
	align-items: center;
	gap: var(--lumgem-space-2);
	background: none;
	border: 1px solid var(--lumgem-color-line);
	border-radius: var(--lumgem-radius);
	padding: 10px 16px;
	margin-top: var(--lumgem-space-3);
	font-size: 13px;
	color: var(--lumgem-color-ink);
	cursor: pointer;
	min-height: 44px;
}

.lumgem-wishlist-toggle .lumgem-icon-wishlist {
	display: block;
	width: 17px;
	height: 16px;
}

.lumgem-wishlist-toggle .lumgem-icon-wishlist svg {
	width: 100%;
	height: 100%;
}

/* Filled heart once saved — the SAME outline path, just with its fill
   switched on, rather than swapping in a second icon. */
.lumgem-wishlist-toggle.is-active .lumgem-icon-wishlist svg {
	fill: currentColor;
}

.lumgem-scope .lumgem-wishlist-toggle.is-active {
	border-color: var(--lumgem-color-accent);
	color: var(--lumgem-color-accent-dark);
}

/* Add Gift Accessories */
.lumgem-product-addons {
	margin: var(--lumgem-space-4) 0;
	padding: var(--lumgem-space-3) 0;
	border-top: 1px solid var(--lumgem-color-line);
	border-bottom: 1px solid var(--lumgem-color-line);
}

.lumgem-product-addons__title {
	font-size: 14px;
	margin-bottom: var(--lumgem-space-2);
}

.lumgem-product-addons__list {
	display: flex;
	flex-direction: column;
	gap: var(--lumgem-space-2);
}

.lumgem-product-addons__item {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: var(--lumgem-space-2);
	font-size: 13px;
	min-height: 44px; /* the whole row is the tap target, not just the checkbox */
}

.lumgem-product-addons__price {
	margin-left: auto;
	color: var(--lumgem-color-accent-dark);
}

.lumgem-product-addons__checkbox:disabled {
	cursor: wait;
}

.lumgem-product-addons__item:has(.lumgem-product-addons__checkbox:disabled) {
	opacity: 0.6;
}

.lumgem-product-addons__error {
	flex-basis: 100%;
	color: var(--lumgem-color-danger);
	font-size: 12px;
}

/* Anchor row (Product / Reviews / Recommended) + mobile trust accordion —
   both sit right before the native Description/Additional info/Reviews tabs. */
.lumgem-single-anchor-row {
	display: flex;
	gap: var(--lumgem-space-5);
	padding: var(--lumgem-space-3) 0;
	border-bottom: 1px solid var(--lumgem-color-line);
	font-size: 13px;
	font-weight: 600;
}

.lumgem-single-anchor-row a {
	text-decoration: none;
	scroll-margin-top: 90px;
	display: inline-flex;
	align-items: center;
	min-height: 44px;
}

/* Desktop no longer uses this row at all — Product Descriptions/Reviews/
   Recommended just flow continuously below the buy-box, no jump-nav needed.
   True display:none (not just visually hidden) so it reserves zero layout
   space; the max-width:1023px block further down independently makes this
   same element position:fixed for mobile's mid-page Goods/Reviews/Recommend
   bar, entirely unaffected by this rule since the two media queries never
   overlap. */
@media (min-width: 1024px) {
	.lumgem-single-anchor-row {
		display: none;
	}
}

.lumgem-single-trust-accordion {
	margin: var(--lumgem-space-3) 0 var(--lumgem-space-5);
}

.lumgem-single-trust-accordion .lumgem-accordion-panel {
	display: none;
}

/* Desktop: the sitewide top Trust Bar (template-parts/trust-bar.php)
   already covers Free Shipping/30-Day Return/1 Year Warranty/Secure
   Shopping — a second, duplicate trust module inside the buy-box is
   removed here entirely (display:none, not an overlay, so it reserves
   zero layout space) rather than just visually masked. Mobile (<1024px)
   is untouched: initMobileTrustList() relocates this same element to
   become the page's real vertical trust list there. */
@media (min-width: 1024px) {
	.lumgem-single-trust-accordion {
		display: none;
	}
}

/* Native Add to Cart button — restyled only; also enforces the >=44px tap
   target the mobile acceptance bar requires (core's own button can render
   shorter depending on font metrics). Desktop gets a taller 48-52px primary
   button (below) — kept separate from this base rule so mobile's 44px tap
   target is untouched. */
.single_add_to_cart_button {
	min-height: 44px;
	padding-top: 12px;
	padding-bottom: 12px;
}

/* One LumGem brand color for the primary purchase action everywhere it
   appears (this button here, and the mobile sticky-bar copy further down) —
   never WooCommerce core's default purple or a plain black. WooCommerce's
   own woocommerce.css ships `button.button.alt { background: #7f54b9 }` at
   specificity (0,0,3,1) (`.woocommerce :where(...) button.button.alt` — the
   `:where()` wrapper contributes zero specificity by spec, so it doesn't
   help beat this); the ancestor chain used here
   (`.single-product div.product .single_add_to_cart_button.button.alt`) adds
   up to (0,0,5,1), which wins outright on class-count alone, no !important
   needed. */
.single-product div.product .single_add_to_cart_button,
.single-product div.product .single_add_to_cart_button.button.alt {
	background: var(--lumgem-gold);
	border-color: var(--lumgem-gold);
	color: #fff;
}

.single-product div.product .single_add_to_cart_button:hover,
.single-product div.product .single_add_to_cart_button.button.alt:hover {
	background: var(--lumgem-gold-hover);
	border-color: var(--lumgem-gold-hover);
}

.single_add_to_cart_button.is-busy {
	opacity: 0.6;
	cursor: wait;
}

@media (min-width: 1024px) {
	.single_add_to_cart_button {
		min-height: 50px;
		font-size: 15px;
	}
}

/*
 * Add to Cart success modal — a native <dialog> (same mechanism as Size
 * Guide): showModal() gives us backdrop, Escape-to-close, and focus
 * trapping/restoration for free, satisfying the desktop overlay/Escape/
 * focus-management requirement without hand-rolled focus-trap JS.
 * Desktop: centered card. Mobile (<600px): anchored to the bottom as a
 * sheet instead, safe-area aware, full width, no horizontal scroll.
 */
.lumgem-atc-modal {
	border: none;
	border-radius: var(--lumgem-radius);
	padding: var(--lumgem-space-5);
	max-width: 420px;
	width: 90vw;
	box-sizing: border-box;
}

.lumgem-atc-modal::backdrop {
	background: rgba(0, 0, 0, 0.5);
}

body.lumgem-modal-open {
	overflow: hidden;
}

.lumgem-atc-modal__close {
	position: absolute;
	top: var(--lumgem-space-2);
	right: var(--lumgem-space-2);
	background: none;
	border: none;
	font-size: 22px;
	line-height: 1;
	cursor: pointer;
	min-width: 44px;
	min-height: 44px;
}

.lumgem-atc-modal__status {
	font-weight: 600;
	color: var(--lumgem-color-success);
	margin: 0 0 var(--lumgem-space-4);
}

.lumgem-atc-modal__item {
	display: flex;
	gap: var(--lumgem-space-3);
	align-items: center;
	margin-bottom: var(--lumgem-space-4);
}

.lumgem-atc-modal__image {
	width: 72px;
	height: 72px;
	object-fit: cover;
	border-radius: var(--lumgem-radius);
	flex-shrink: 0;
}

.lumgem-atc-modal__title {
	font-size: 14px;
	margin: 0 0 var(--lumgem-space-1);
}

.lumgem-atc-modal__price {
	font-size: 13px;
	color: var(--lumgem-color-accent-dark);
}

.lumgem-atc-modal__cart-summary {
	font-size: 12px;
	color: var(--lumgem-color-muted);
	margin-bottom: var(--lumgem-space-4);
}

.lumgem-atc-modal__actions {
	display: flex;
	flex-direction: column;
	gap: var(--lumgem-space-2);
}

.lumgem-atc-modal__actions .lumgem-button {
	text-align: center;
	min-height: 44px;
}

.lumgem-atc-modal__view-cart {
	text-align: center;
	min-height: 44px;
	display: flex;
	align-items: center;
	justify-content: center;
	text-decoration: underline;
	color: var(--lumgem-color-ink);
}

@media (max-width: 599px) {
	.lumgem-atc-modal {
		position: fixed;
		left: 0;
		right: 0;
		bottom: 0;
		top: auto;
		margin: 0;
		max-width: 100%;
		width: 100%;
		border-radius: var(--lumgem-radius) var(--lumgem-radius) 0 0;
		padding-bottom: calc(var(--lumgem-space-5) + env(safe-area-inset-bottom, 0px));
	}
}

/* Size Guide */
.lumgem-size-guide-link {
	background: none;
	border: none;
	text-decoration: underline;
	font-size: 12px;
	color: var(--lumgem-color-muted);
	padding: var(--lumgem-space-2) 0;
	cursor: pointer;
}

.lumgem-size-guide-dialog {
	border: none;
	border-radius: var(--lumgem-radius);
	padding: var(--lumgem-space-5);
	max-width: 480px;
}

.lumgem-size-guide-dialog::backdrop {
	background: rgba(0, 0, 0, 0.5);
}

.lumgem-size-guide-dialog__close {
	position: absolute;
	top: var(--lumgem-space-2);
	right: var(--lumgem-space-2);
	background: none;
	border: none;
	font-size: 20px;
	cursor: pointer;
}

/*
 * ============================================================================
 * LOWER SECTIONS (Product Descriptions / Reviews & FAQ / You May Also Like /
 * Recently Viewed) — replaces the old native-tabs restyling above (core's
 * cramped ul.tabs.wc-tabs box, woocommerce_upsell_display(), and
 * woocommerce_output_related_products() are removed in single-product-hooks.php
 * in favor of these full-width sections). See CLAUDE_PRODUCT_LOWER_SECTIONS_
 * JEULIA_LAYOUT.md.
 * ============================================================================
 */

.lumgem-lower-section {
	margin-top: var(--lumgem-space-7);
	padding-top: var(--lumgem-space-6);
	border-top: 1px solid var(--lumgem-color-line);
}

.lumgem-lower-section__header {
	display: flex;
	align-items: baseline;
	gap: var(--lumgem-space-3);
	margin-bottom: var(--lumgem-space-5);
}

.lumgem-lower-section__icon {
	display: inline-flex;
	align-self: center;
	width: 22px;
	height: 22px;
	color: var(--lumgem-color-accent-dark);
	flex-shrink: 0;
}

.lumgem-lower-section__icon svg {
	width: 100%;
	height: 100%;
}

.lumgem-lower-section__title {
	font-size: 20px;
	margin: 0;
}

.lumgem-lower-section__sku {
	font-size: 12px;
	color: var(--lumgem-color-muted);
}

/* --- Product Descriptions --------------------------------------------- */
.lumgem-section-descriptions__body {
	font-size: 14px;
	line-height: 1.7;
	max-width: 760px;
	margin-bottom: var(--lumgem-space-5);
}

.lumgem-section-descriptions__specs {
	columns: 3;
	column-gap: var(--lumgem-space-6);
	border-top: 1px solid var(--lumgem-color-line);
	padding-top: var(--lumgem-space-4);
}

.lumgem-spec-row {
	display: flex;
	justify-content: space-between;
	gap: var(--lumgem-space-3);
	padding: var(--lumgem-space-2) 0;
	border-bottom: 1px solid var(--lumgem-color-line);
	font-size: 13px;
	break-inside: avoid;
}

.lumgem-spec-row__label {
	color: var(--lumgem-color-muted);
}

.lumgem-spec-row__value {
	color: var(--lumgem-color-ink);
	text-align: right;
}

@media (max-width: 1023px) {
	.lumgem-lower-section {
		padding-left: var(--lumgem-space-3);
		padding-right: var(--lumgem-space-3);
	}

	.lumgem-lower-section__header {
		flex-wrap: wrap;
	}

	.lumgem-section-descriptions__specs {
		columns: 1;
	}
}

/* --- Reviews / FAQ ------------------------------------------------------ */
.lumgem-reviews-faq__tabs {
	display: flex;
	border-bottom: 1px solid var(--lumgem-color-line);
	margin-bottom: var(--lumgem-space-5);
}

/* .lumgem-scope prefix (2 classes) beats components.css's generic
   `.lumgem-scope button` reset (1 class + 1 element) — without it these
   rendered as two solid ink-background/white-text blocks instead of a
   plain white-tab-strip-with-underline, exactly the "两块黑砖" a design
   audit flagged. */
.lumgem-scope .lumgem-reviews-faq__tab {
	flex: 1 1 50%;
	background: none;
	border: none;
	padding: var(--lumgem-space-3) 0;
	font-size: 15px;
	color: var(--lumgem-color-muted);
	border-bottom: 2px solid transparent;
	margin-bottom: -1px;
	cursor: pointer;
	min-height: 44px;
}

.lumgem-scope .lumgem-reviews-faq__tab.is-active {
	color: var(--lumgem-color-ink);
	font-weight: 600;
	border-bottom-color: var(--lumgem-color-accent);
}

/* Progressive enhancement: by default (no JS, or JS hasn't run yet) BOTH
   panels are simply shown stacked — the content stays reachable either way.
   Only once initReviewsFaqTabs() adds .js-enhanced to the wrapper does the
   non-active panel actually get hidden; the click handlers on the tab
   buttons above are themselves inert without JS, which is fine since they
   have no href/default action to fall back to. */
.lumgem-section-reviews-faq.js-enhanced .lumgem-reviews-faq__panel:not(.is-active) {
	display: none;
}

.lumgem-reviews-summary {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: var(--lumgem-space-3);
	margin-bottom: var(--lumgem-space-5);
}

.lumgem-reviews-summary__average {
	font-size: 28px;
	font-family: var(--lumgem-font-display);
}

.lumgem-reviews-summary__empty {
	color: var(--lumgem-color-muted);
	font-size: 14px;
}

/* Secondary/outline treatment — overrides components.css's shared
   `.lumgem-button` (a solid ink CTA meant for primary actions like
   Checkout); "Leave A Review" is a lower-emphasis secondary action next to
   the review summary, not a full-bleed black button. 2 classes vs
   `.lumgem-button`'s 1 beats it outright, no !important needed. */
.lumgem-reviews-summary__cta.lumgem-button {
	margin-left: auto;
	min-height: 44px;
	display: inline-flex;
	align-items: center;
	background: none;
	color: var(--lumgem-color-ink);
	border: 1px solid var(--lumgem-color-line);
	padding: 10px 18px;
}

.lumgem-reviews-summary__cta.lumgem-button:hover {
	background: var(--lumgem-color-surface-alt);
	border-color: var(--lumgem-color-ink);
}

/* .lumgem-scope prefix + `button` element bumps this to 3 classes + 1
   element, unambiguously beating both components.css's generic
   `.lumgem-scope button` reset AND its own `.lumgem-scope
   button.lumgem-accordion-toggle` base rule (2 classes + 1 element) —
   without this, the FAQ toggle's label rendered white-on-transparent
   (invisible), same root cause as the product-page trust list below. */
.lumgem-scope .lumgem-reviews-faq__accordion button.lumgem-accordion-toggle {
	background: none;
	border: none;
	border-bottom: 1px solid var(--lumgem-color-line);
	border-radius: 0;
	padding: var(--lumgem-space-3) 0;
	min-height: 44px;
}

.lumgem-reviews-faq__accordion .lumgem-accordion-panel {
	border: none;
	padding: 0 0 var(--lumgem-space-3);
	font-size: 13px;
	color: var(--lumgem-color-muted);
}

@media (max-width: 1023px) {
	.lumgem-reviews-summary__cta {
		margin-left: 0;
		width: 100%;
		justify-content: center;
	}
}

/* --- You May Also Like / Recently Viewed carousels ---------------------- *
 * .lumgem-carousel--product-page is an OPT-IN modifier (see
 * product-carousel.php's $args['class']) so this sizing never touches the
 * existing Cart/Home carousels, which keep their original ~200px card width. */
/* Very pale blush section background (product cards stay white, on top of
   it). Kept within the section's own normal content width (no negative
   margins to bleed it to the viewport edge) — the surrounding padding
   context differs between desktop's grid layout and mobile's full-width
   flow, and guessing wrong there risks a horizontal-overflow regression
   for a purely decorative background tint. */
.lumgem-carousel--product-page {
	margin: 0 0 var(--lumgem-space-6);
	padding: var(--lumgem-space-5);
	background: var(--lumgem-blush);
	border-radius: 8px;
}

@media (max-width: 1023px) {
	.lumgem-carousel--product-page {
		border-radius: 0;
	}
}

.lumgem-carousel--product-page .lumgem-carousel__title {
	text-align: center;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	font-size: 16px;
	position: relative;
	padding-bottom: var(--lumgem-space-3);
	color: var(--lumgem-text);
}

.lumgem-carousel--product-page .lumgem-carousel__title::after {
	content: '';
	position: absolute;
	left: 50%;
	bottom: 0;
	transform: translateX(-50%);
	width: 40px;
	height: 2px;
	background: var(--lumgem-gold);
}

/* ~4 visible desktop (matches the Jeulia reference's ~276px card at a
   similar container width): each card is roughly a quarter of the track
   width minus its share of the gaps. */
.lumgem-carousel--product-page .lumgem-carousel__item,
.lumgem-carousel--product-page .lumgem-carousel__item-link {
	flex-basis: calc((100% - 3 * var(--lumgem-space-4)) / 4);
}

.lumgem-carousel--product-page .lumgem-carousel__item {
	display: flex;
	flex-direction: column;
	gap: var(--lumgem-space-2);
	/* White card sitting on top of the section's blush background — small
	   radius + hairline border + very light shadow, not an app-style large
	   rounded card. */
	background: var(--lumgem-surface);
	border: 1px solid var(--lumgem-border);
	border-radius: 6px;
	padding: var(--lumgem-space-2);
	box-shadow: 0 1px 3px rgba(29, 25, 22, 0.06);
}

/* The quick-add card variant's real link is `.lumgem-carousel__item-link`
   (the outer `.lumgem-carousel__item` is a <div> wrapper here, not the
   <a> — see product-carousel.php's `$quick_add` branch), which had no
   text-decoration rule of its own anywhere and so showed the browser's
   default underline under the product name. */
.lumgem-carousel--product-page .lumgem-carousel__item-link {
	text-decoration: none;
	color: inherit;
}

.lumgem-carousel--product-page .lumgem-carousel__image img {
	aspect-ratio: 1;
	object-fit: cover;
	width: 100%;
}

.lumgem-carousel--product-page .lumgem-carousel__name {
	display: -webkit-box;
	-webkit-line-clamp: 2;
	-webkit-box-orient: vertical;
	overflow: hidden;
	min-height: 2.6em;
	font-size: 14px;
	font-weight: 600;
	color: var(--lumgem-color-ink);
}

.lumgem-carousel--product-page .lumgem-carousel__price {
	font-weight: 600;
	color: var(--lumgem-coral);
}

/* Was inheriting the generic `.lumgem-scope button` reset's dark-ink fill
   (no color/background of its own was ever set here) — the same "solid
   black button" defect as the arrows, just via a missing declaration
   instead of a specificity loss (this selector's own 2 classes already
   beat that reset's 1-class+1-element rule on class-count alone). Now the
   LumGem warm-gold primary treatment, matching the native Add to Cart
   button below instead of a plain dark fill. */
.lumgem-carousel--product-page .lumgem-carousel__quick-add {
	width: 100%;
	min-height: 44px;
	font-size: 12px;
	padding: 8px 12px;
	background: var(--lumgem-gold);
	border-color: var(--lumgem-gold);
	color: #fff;
}

.lumgem-carousel--product-page .lumgem-carousel__quick-add:hover {
	background: var(--lumgem-gold-hover);
	border-color: var(--lumgem-gold-hover);
}

.lumgem-carousel--product-page .lumgem-carousel__quick-add.is-added {
	background: var(--lumgem-color-success);
	border-color: var(--lumgem-color-success);
}

/* Light thin-line chevron (matches the main gallery's .lumgem-gallery__arrow
   treatment — transparent, low-contrast at rest) instead of the base
   carousel's filled white circle, per the site-owner's explicit ask for
   this section specifically; Cart/Home keep their own established white
   circle look untouched. `top` is set by JS (positionCarouselArrows() in
   single-product.js) to the vertical center of the card's own image, via
   this custom property — the arrows are DOM siblings of the track (not
   descendants), so a plain CSS `top:50%` on the outer wrapper would center
   on the whole section (title + track) instead of the image alone. */
.lumgem-carousel--product-page .lumgem-carousel__arrow {
	top: var(--lumgem-pdp-carousel-arrow-top, 50%);
	/* The custom property is the desired CENTER point (image's vertical
	   midpoint), but `top` positions this box's own top edge — without this
	   translate, the arrow renders with its top edge (not its center) at
	   that point, visibly offset downward by half its own height. */
	transform: translateY(-50%);
	width: 40px;
	height: 40px;
	border: none;
	border-radius: 999px;
	background: transparent;
	color: var(--lumgem-muted);
	opacity: 0.6;
	transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
}

.lumgem-carousel--product-page .lumgem-carousel__arrow:hover {
	opacity: 0.9;
	color: var(--lumgem-text);
	background: rgba(29, 25, 22, 0.06);
}

.lumgem-carousel--product-page .lumgem-carousel__arrow svg {
	width: 18px;
	height: 18px;
}

/* ~2 visible mobile, scroll-snap (one or two cards fully in view per swipe),
   full content width (no ~250px "phone-in-the-middle" canvas). */
@media (max-width: 1023px) {
	.lumgem-carousel--product-page {
		padding: 0 var(--lumgem-space-3);
	}

	.lumgem-carousel--product-page .lumgem-carousel__item,
	.lumgem-carousel--product-page .lumgem-carousel__item-link {
		flex-basis: calc((100% - var(--lumgem-space-4)) / 2);
	}

	.lumgem-carousel--product-page .lumgem-carousel__arrow {
		display: none;
	}
}

#lumgem-recently-viewed-root[hidden] {
	display: none;
}

/* Explicit safe-area buffer so the mobile fixed Add to Cart bar (~70px,
   visible whenever the native button is out of view — which it always is
   once the user has scrolled this far down) can never sit on top of
   Recently Viewed's last row, regardless of how much/little content the
   footer below it happens to have. */
@media (max-width: 1023px) {
	#lumgem-recently-viewed-root:not([hidden]) {
		margin-bottom: calc(70px + var(--lumgem-space-4) + env(safe-area-inset-bottom, 0px));
	}
}

/* Mobile sticky Add to Cart bar — mobile/tablet only, never shown on
   desktop regardless of scroll position (JS also gates this, but the CSS
   media query is the authoritative "never on desktop" guard). Respects the
   iOS home-indicator safe area and sits above document content, not fixed
   nav (this project has no fixed bottom nav yet). */
.lumgem-sticky-atc {
	display: none;
}

@media (max-width: 1023px) {
	.lumgem-sticky-atc.is-visible {
		display: flex;
		position: fixed;
		left: 0;
		right: 0;
		bottom: 0;
		z-index: 60;
		align-items: center;
		gap: var(--lumgem-space-3);
		background: #fff;
		border-top: 1px solid var(--lumgem-color-line);
		padding: var(--lumgem-space-3) var(--lumgem-space-4);
		padding-bottom: calc(var(--lumgem-space-3) + env(safe-area-inset-bottom, 0px));
		box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1);
	}
}

/* Hidden per the remediation review: the reference pattern is a circular
   wishlist button + a wide Add to Cart button, not a desktop-style "sticky
   summary" with title/price text competing for space — title/price are
   already shown natively in the buy-box the user just scrolled past. Kept
   in the DOM (harmless, unused) rather than stripped from the JS, so a
   future design pass can re-enable it with a single CSS change if needed. */
.lumgem-sticky-atc__info {
	display: none;
}

/* .lumgem-scope prefix (2 classes) beats components.css's generic
   `.lumgem-scope button` reset (1 class + 1 element) — without it this
   rendered with that reset's dark ink background instead of the brand
   accent color declared here, same recurring cascade bug fixed throughout
   this file. Same brand color as the desktop native Add to Cart button
   above, so the two never disagree. */
.lumgem-scope .lumgem-sticky-atc__button {
	flex: 1 1 auto;
	background: var(--lumgem-color-accent);
	color: #fff;
	border: none;
	border-radius: var(--lumgem-radius);
	padding: 12px 24px;
	font-size: 14px;
	min-height: 44px;
	cursor: pointer;
}

.lumgem-scope .lumgem-sticky-atc__button:hover {
	background: var(--lumgem-color-accent-dark);
}

/* Circular wishlist button in the fixed bar — shares the exact same
   .lumgem-wishlist-toggle class/data-product-id/state-classes as the inline
   in-page toggle (see single-product-hooks.php + wishlist.js's existing
   body-level click delegation), so no new wishlist logic is needed: this is
   just a second, circular-styled instance of the same toggle. */
.lumgem-sticky-atc .lumgem-wishlist-toggle {
	flex-shrink: 0;
	width: 44px;
	height: 44px;
	padding: 0;
	margin: 0;
	border-radius: 999px;
	justify-content: center;
}

.lumgem-sticky-atc .lumgem-wishlist-toggle__label {
	display: none;
}

/*
 * ============================================================================
 * MOBILE REMEDIATION (<1024px, product page only) — Jeulia-style rebuild.
 * Everything below is additive/overriding on top of the rules above; the
 * desktop 55/40 grid, thumbnail rail, and object-fit:contain main image are
 * untouched above this point. See CLAUDE_MOBILE_PRODUCT_REMEDIATION.md.
 * ============================================================================
 */

/* --- Page shell: sticky announcement bar + header ------------------------
   --lumgem-announce-h is measured and set by single-product.js
   (initMobileStickyHeader) since the announcement bar's height is 0 (no
   active announcement) or variable (text wrap) — a hardcoded value would
   either clip content or leave a gap under the header. */
/* Product-page-only Trust Bar recolor (champagne bg + emerald icons,
   "Soft Pop Luxury" palette) — components.css's shared `.lumgem-trust-bar`
   rule (used by Cart/Checkout/every other page too) is left completely
   untouched; this only overrides on `.single-product`, so no other page's
   Trust Bar changes color. */
.single-product .lumgem-trust-bar {
	background: var(--lumgem-champagne);
	border-bottom-color: var(--lumgem-border);
}

.single-product .lumgem-trust-bar__icon {
	color: var(--lumgem-emerald);
}

.single-product .lumgem-trust-bar__label {
	color: var(--lumgem-gold-hover);
}

.single-product .lumgem-trust-bar__text {
	color: var(--lumgem-text);
}

@media (max-width: 1023px) {
	.lumgem-announcement-bar {
		position: sticky;
		top: 0;
		z-index: 101;
	}

	.lumgem-header {
		position: sticky;
		top: var(--lumgem-announce-h, 0px);
		z-index: 100;
	}

	/* The sitewide horizontal Trust Bar is replaced by the vertical list
	   further down the mobile page (see .lumgem-single-trust-accordion
	   overrides below) — showing both would be redundant and eat first-screen
	   space. Desktop and other pages' mobile views are unaffected: this file
	   only ever loads on the product page (see inc/enqueue.php's is_product()
	   gate). */
	.lumgem-trust-bar {
		display: none;
	}
}

/* --- Media: single full-width stage, no separate thumbnail-card rail -----
   Video/360 thumbs are relocated (by JS) from the rail into a small overlay
   control row under the stage image instead of a second visible row of
   large thumb cards — they are still only ever present in the DOM when the
   product actually has that meta (see product-image.php's server-side
   if ($lumgem_video_url)/if ($lumgem_360_url) guards), so "no data => no
   button" is already satisfied without extra mobile-specific logic. */
@media (max-width: 1023px) {
	.product .lumgem-gallery {
		flex-direction: column;
		gap: 0;
		margin-bottom: var(--lumgem-space-4);
	}

	.lumgem-gallery__rail {
		display: none;
	}

	.lumgem-gallery__stage {
		aspect-ratio: 1;
		max-height: 60vh;
		touch-action: pan-y;
	}

	.lumgem-gallery__counter {
		display: block;
	}

	/* Built by JS (appended INSIDE .lumgem-gallery__stage, which is already
	   position:relative): holds the video/360 thumb buttons moved out of the
	   (now hidden) rail, floating as a small horizontally-centered overlay
	   group under the main image — NOT pinned to the bottom-left corner
	   (where it risked sitting on top of a ring/pendant photographed
	   off-center) and kept clear of the "current / total" counter, which
	   stays independently anchored to the bottom-right (see
	   .lumgem-gallery__counter above). */
	.lumgem-gallery__mobile-controls {
		position: absolute;
		left: 50%;
		bottom: var(--lumgem-space-2);
		transform: translateX(-50%);
		z-index: 4;
		display: flex;
		align-items: center;
		gap: var(--lumgem-space-3);
	}

	/* 44px minimum tap target (this project's accessibility floor everywhere
	   else) — a previous pass shrank these to 36px to fit the overlay
	   visually, which was itself flagged as a regression by an independent
	   audit. Contrast/legibility of the 360° label bumped at the same time
	   (a previous 9px/default-color label was reported as "near-blank"). */
	.lumgem-gallery__mobile-controls .lumgem-gallery__thumb {
		width: 44px;
		height: 44px;
		flex-shrink: 0;
		border: none;
		border-radius: 999px;
		background: rgba(255, 255, 255, 0.92);
		box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
	}

	.lumgem-gallery__mobile-controls .lumgem-gallery__thumb.is-active {
		background: var(--lumgem-color-accent);
		color: #fff;
	}

	.lumgem-gallery__mobile-controls .lumgem-gallery__play-icon {
		border-top-width: 6px;
		border-bottom-width: 6px;
		border-left-width: 11px;
	}

	/* The video thumb's cover <img> (see product-image.php) is a desktop-only
	   feature (see the min-width:1024px rail rules above) — this small
	   translucent-circle overlay control keeps its original plain
	   triangle-on-white look unchanged, so the poster image never renders
	   here at all. */
	.lumgem-gallery__mobile-controls .lumgem-gallery__thumb--video img {
		display: none;
	}

	/* The 44px circle only has room for the "360°" text at this size —
	   same rotate-arrows icon as desktop would be too cramped alongside it
	   in a compact overlay button, so it's hidden here rather than shrunk
	   to illegibility. */
	.lumgem-gallery__mobile-controls .lumgem-gallery__360-icon {
		display: none;
	}

	.lumgem-gallery__mobile-controls .lumgem-gallery__360-label {
		font-size: 11px;
		font-weight: 700;
		color: var(--lumgem-color-ink);
		margin-top: 0;
	}

	.lumgem-gallery__mobile-controls .lumgem-gallery__thumb.is-active .lumgem-gallery__360-label {
		color: #fff;
	}

	/* Sale flash badge — small and pinned to the media corner rather than
	   the larger circular badge core renders by default. */
	.single-product div.product > .onsale {
		min-height: 0;
		min-width: 0;
		width: auto;
		height: auto;
		line-height: 1;
		font-size: 11px;
		font-weight: 600;
		padding: 4px 8px;
		border-radius: var(--lumgem-radius);
		background: var(--lumgem-color-danger);
		color: #fff;
	}
}

/* --- Media active-state: hide ordinary browsing controls while Video/360
   is actually playing, restore them on close ------------------------------
   is-video-active/is-360-active are toggled on .lumgem-gallery by
   single-product.js's setStageState(). Applies on both breakpoints (arrows
   exist on desktop too, and shouldn't invite the user to page through
   photos while a video is mid-playback) — .lumgem-gallery__mobile-controls
   itself only ever exists as an element on mobile anyway, so this rule is a
   no-op there on desktop. */
.lumgem-gallery.is-video-active .lumgem-gallery__mobile-controls,
.lumgem-gallery.is-video-active .lumgem-gallery__arrow,
.lumgem-gallery.is-video-active .lumgem-gallery__counter,
.lumgem-gallery.is-360-active .lumgem-gallery__mobile-controls,
.lumgem-gallery.is-360-active .lumgem-gallery__arrow,
.lumgem-gallery.is-360-active .lumgem-gallery__counter {
	display: none;
}

/* --- Video/360 loading + error state — a small LumGem-own animated mark
   (plain CSS shape, not copied from any brand's actual loading animation),
   never blocking the always-visible top-right close button. ---------------- */
.lumgem-media-loader {
	position: absolute;
	inset: 0;
	z-index: 3;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: var(--lumgem-space-3);
	background: var(--lumgem-color-surface-alt);
	color: var(--lumgem-color-muted);
	font-size: 13px;
	text-align: center;
	padding: var(--lumgem-space-4);
	transition: opacity 0.2s ease;
}

.lumgem-media-loader.is-hidden {
	opacity: 0;
	pointer-events: none;
}

.lumgem-media-loader__mark {
	display: block;
	width: 36px;
	height: 36px;
	border-radius: 999px;
	border: 3px solid var(--lumgem-color-line);
	border-top-color: var(--lumgem-color-accent);
	animation: lumgem-media-loader-spin 0.9s linear infinite;
}

@keyframes lumgem-media-loader-spin {
	to {
		transform: rotate(360deg);
	}
}

/* prefers-reduced-motion: a static brand mark (solid dot) instead of the
   spinner, per the remediation spec's explicit accessibility requirement. */
@media (prefers-reduced-motion: reduce) {
	.lumgem-media-loader__mark {
		animation: none;
		border-top-color: var(--lumgem-color-line);
		background: var(--lumgem-color-accent);
	}
}

.lumgem-media-loader.is-error .lumgem-media-loader__mark {
	animation: none;
	border-color: var(--lumgem-color-danger);
	border-top-color: var(--lumgem-color-danger);
	background: none;
}

.lumgem-media-loader__text {
	max-width: 220px;
}

.lumgem-scope .lumgem-media-loader__back {
	background: var(--lumgem-color-ink);
	color: #fff;
	border: none;
	border-radius: var(--lumgem-radius);
	padding: 8px 16px;
	font-size: 13px;
	min-height: 44px;
	cursor: pointer;
}

/* Degrade-only escape hatch for a genuinely broken embed (e.g. the owner
   disabled embedding, or an origin-restriction error) — a plain external
   link, never styled to look like or replace the in-site player. */
.lumgem-media-loader__watch-link {
	font-size: 13px;
	text-decoration: underline;
	color: inherit;
}

/* --- Video loading overlay — the real LumGem animated SVG (admin-
   configurable, see lumgem_get_product_video_loading_logo_url()), rendered
   as a plain <img> so its own native SMIL animation plays. Sized
   responsively (min(78%,600px), never a fixed 600x360 box) — the width/
   height attributes on the <img> itself only reserve the aspect ratio for
   CLS purposes. This is a loading OVERLAY, not a video intro/outro. ------- */
.lumgem-video-loader {
	position: absolute;
	inset: 0;
	z-index: 3;
	display: grid;
	place-items: center;
	gap: var(--lumgem-space-3);
	background: var(--lumgem-color-surface-alt);
	transition: opacity 0.22s ease, visibility 0.22s ease;
}

.lumgem-video-loader.is-hidden {
	opacity: 0;
	visibility: hidden;
	pointer-events: none;
}

.lumgem-video-loader__mark {
	width: min(78%, 600px);
	height: auto;
	aspect-ratio: 600 / 360;
}

/* Visually hidden (but screen-reader-accessible) at all times — the
   animated mark (see buildVideoLoader() in single-product.js, always
   rendered regardless of prefers-reduced-motion per an explicit site-owner
   decision) alone carries the loading state visually. Still becomes visible
   on an actual error (below), where the mark is hidden and this is the only
   on-screen "Video unavailable" indication. */
.lumgem-video-loader__text {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
	color: var(--lumgem-color-muted);
	font-size: 13px;
	text-align: center;
}

.lumgem-video-loader.is-error .lumgem-video-loader__mark {
	display: none;
}

.lumgem-video-loader.is-error .lumgem-video-loader__text {
	position: static;
	width: auto;
	height: auto;
	margin: 0;
	overflow: visible;
	clip: auto;
	white-space: normal;
	max-width: 220px;
}

/* --- 360 frame-sequence drag-to-rotate viewer ------------------------------
   Real user-data-driven rotation (see product-image.php's
   _lumgem_360_frames): dragging left/right steps through the frame array,
   Pointer Events unify mouse/touch/pen into one code path (see
   build360FrameViewer() in single-product.js). */
.lumgem-360-viewer {
	position: relative;
	width: 100%;
	height: 100%;
	touch-action: none;
	cursor: grab;
}

.lumgem-360-viewer.is-dragging {
	cursor: grabbing;
}

.lumgem-360-viewer__frame {
	width: 100%;
	height: 100%;
	object-fit: contain;
	pointer-events: none;
	user-select: none;
}

.lumgem-360-viewer__hint {
	position: absolute;
	left: 50%;
	bottom: var(--lumgem-space-3);
	transform: translateX(-50%);
	background: rgba(0, 0, 0, 0.6);
	color: #fff;
	font-size: 11px;
	padding: 4px 10px;
	border-radius: 999px;
	white-space: nowrap;
	transition: opacity 0.2s ease;
}

.lumgem-360-viewer__hint.is-hidden {
	opacity: 0;
}

/* --- Desktop title/price/rating/SKU — one consistent typographic hierarchy,
   not WooCommerce/browser defaults. No desktop-scoped rule existed for any
   of these before, so the title rendered at the UA stylesheet's native
   <h1> size (visibly oversized, wrapping early) and price/rating/SKU each
   kept whatever font family/weight core or the browser happened to apply —
   flagged by a design audit as "标题字号过大" and inconsistent font
   families/weights. */
@media (min-width: 1024px) {
	.summary.entry-summary .product_title {
		font-size: 28px;
		line-height: 1.25;
		margin: 0 0 var(--lumgem-space-3);
	}

	.summary.entry-summary .woocommerce-product-rating,
	.summary.entry-summary .sku_wrapper,
	.summary.entry-summary .posted_in,
	.summary.entry-summary .tagged_as {
		font-family: var(--lumgem-font-body);
		font-size: 13px;
		color: var(--lumgem-color-muted);
	}

	.summary.entry-summary .price {
		font-family: var(--lumgem-font-body);
		font-size: 24px;
		font-weight: 600;
		color: var(--lumgem-color-ink);
	}

	.summary.entry-summary .price del {
		font-size: 16px;
		font-weight: 400;
		color: var(--lumgem-color-muted);
		opacity: 1;
	}

	.summary.entry-summary .price ins {
		text-decoration: none;
	}
}

/* --- Title, SKU+rating row, price — compact mobile sizing -----------------
   .lumgem-mobile-meta-row is built by JS (initMobileMetaRow): it physically
   moves the REAL .sku_wrapper (from product_meta) and the REAL
   .woocommerce-product-rating (both native WooCommerce markup/data) to sit
   together directly under the title, rather than rendering a second,
   hand-written copy of the SKU/rating from a duplicate data source. */
@media (max-width: 1023px) {
	.summary.entry-summary .product_title {
		font-size: 17px;
		line-height: 1.25;
		margin: 0 0 var(--lumgem-space-2);
	}

	.lumgem-mobile-meta-row {
		display: flex;
		align-items: center;
		flex-wrap: wrap;
		gap: var(--lumgem-space-2);
		font-size: 12px;
		color: var(--lumgem-color-muted);
		margin-bottom: var(--lumgem-space-3);
	}

	.lumgem-mobile-meta-row .sku_wrapper {
		padding-right: var(--lumgem-space-2);
		border-right: 1px solid var(--lumgem-color-line);
	}

	.lumgem-mobile-meta-row .woocommerce-product-rating {
		display: flex;
		align-items: center;
		gap: var(--lumgem-space-1);
		margin: 0;
	}

	.summary.entry-summary .price {
		font-size: 20px;
	}

	/* The native SKU/category footer (product_meta, priority 40) is left in
	   place at the bottom of the buy-box for anything OTHER than SKU (the
	   SKU node itself has already been moved into .lumgem-mobile-meta-row
	   above, so it no longer renders twice). */
	.product_meta {
		font-size: 12px;
		color: var(--lumgem-color-muted);
	}
}

/* --- Buy-box info order (title -> meta-row -> price -> addons -> shipping
   hint -> native cart -> wishlist -> product_meta), per the remediation
   doc's required mobile stacking order. Reordering via CSS `order` on a
   flex container keeps this to a pure-CSS change with zero markup
   duplication and zero effect on desktop (which keeps its own CSS Grid
   layout, untouched, at >=1024px). */
@media (max-width: 1023px) {
	.single-product div.product > .summary.entry-summary {
		display: flex;
		flex-direction: column;
	}

	.summary.entry-summary > .product_title { order: 1; }
	.summary.entry-summary > .lumgem-mobile-meta-row { order: 2; }
	/* Pre-JS safety net: .woocommerce-product-rating is still a direct child
	   of .summary until initMobileMetaRow() moves it into
	   .lumgem-mobile-meta-row on DOMContentLoaded — giving it the same
	   order:2 here means there's no flash of it rendering out of place (e.g.
	   above the title) in the brief window before that JS runs. */
	.summary.entry-summary > .woocommerce-product-rating { order: 2; }
	.summary.entry-summary > .price { order: 3; }
	.summary.entry-summary > .woocommerce-product-details__short-description,
	.summary.entry-summary > .woocommerce-variation,
	.summary.entry-summary > form.variations_form,
	.summary.entry-summary > table.variations { order: 4; }
	.summary.entry-summary > .lumgem-product-addons { order: 5; }
	/* Brief "shipping/returns" purchase hint, right before Add to Cart —
	   reuses the same small fulfillment-badge element that already sits near
	   the title on desktop (see single-product-hooks.php); moving it here is
	   a pure `order` change, not a DOM move, since it's already a direct
	   child of .summary on both breakpoints. */
	.summary.entry-summary > .lumgem-single-trust-badge { order: 6; margin: var(--lumgem-space-2) 0; }
	.summary.entry-summary > form.cart,
	.summary.entry-summary > .cart,
	.summary.entry-summary > .ppcp-messages { order: 7; }
	.summary.entry-summary > .lumgem-wishlist-toggle { order: 8; }
	.summary.entry-summary > .product_meta { order: 9; }
}

/* --- Goods / Reviews / Recommend scroll-triggered floating nav ------------
   Same .lumgem-single-anchor-row element used on desktop (see
   single-product-hooks.php) — at mobile widths, initMobileAnchorNav()
   relabels the first/third links ("Product"->"Goods", "Recommended"-
   >"Recommend"; hrefs are identical on both breakpoints).
   `position:fixed` (not `sticky`) and permanently OUT of the buy-box's
   visual flow — this row must never appear as ordinary page content while
   the user is still at/above the native Add to Cart button, only
   afterward, so it can't be allowed to occupy its natural in-flow position
   at all (that was the previous bug: it showed as a plain content block
   right after the buy-box on every page load). Shown/hidden via
   `visibility`, not `display`: initMobileAnchorNav()'s IntersectionObserver
   toggles `.is-visible` based on the SAME native-Add-to-Cart sentinel the
   bottom sticky bar already uses, and `visibility` (unlike `display:none`)
   keeps this element's real box measurable via getBoundingClientRect() at
   ALL times — required so initMobileStickyHeader() can size --lumgem-h3
   correctly even before the row has ever been shown. Being `position:fixed`
   throughout (rather than toggling in/out of the layout) also means
   showing/hiding it can never shift any other element on the page — no CLS
   by construction, not just "we tried to avoid it". `visibility:hidden`
   elements are already excluded from the tab order in every browser, so
   this doesn't need a separate focus-management step either. */
@media (max-width: 1023px) {
	.lumgem-single-anchor-row {
		position: fixed;
		left: 0;
		right: 0;
		top: var(--lumgem-h2, 0px);
		z-index: 90;
		background: var(--lumgem-color-surface);
		justify-content: space-around;
		gap: 0;
		margin: 0;
		padding: 0 var(--lumgem-space-3);
		visibility: hidden;
		opacity: 0;
		transition: opacity 0.15s ease;
	}

	.lumgem-single-anchor-row.is-visible {
		visibility: visible;
		opacity: 1;
	}

	.lumgem-single-anchor-row a {
		flex: 1 1 0;
		justify-content: center;
		border-bottom: 2px solid transparent;
	}

	.lumgem-single-anchor-row a.is-active {
		color: var(--lumgem-color-ink);
		border-bottom-color: var(--lumgem-color-ink);
	}

	/* Scroll targets need to clear the full sticky stack (announcement +
	   header + this nav, once it too is stuck) — see initMobileStickyHeader()
	   for how --lumgem-h3 is measured. */
	#lumgem-section-descriptions,
	#lumgem-section-reviews-faq,
	#lumgem-section-you-may-also-like {
		scroll-margin-top: var(--lumgem-h3, 100px);
	}

	/* "Goods" jumps back UP to the main media area, above the native Add to
	   Cart button — landing there re-enters the button into view, which
	   hides this nav row again (initMobileAnchorNav()'s own
	   IntersectionObserver on that same button). Reserving room for the
	   soon-to-vanish row (h3) would leave a dangling gap above the gallery;
	   only the announcement bar + header (h2) stay on screen once landed,
	   so that's the only offset this target needs. */
	#lumgem-section-goods {
		scroll-margin-top: var(--lumgem-h2, 70px);
	}
}

/* --- Vertical trust list (Shipping / Return / Warranty / Secure Shopping) -
   Same .lumgem-single-trust-accordion element rendered by
   single-product-hooks.php (desktop keeps its existing compact accordion
   look via the shared .lumgem-accordion-toggle styling in components.css) —
   initMobileTrustList() moves this exact node to sit after the anchor nav on
   mobile, and this rule restyles each row to the icon / label / chevron list
   pattern instead of the desktop's boxed-accordion look. */
@media (max-width: 1023px) {
	.lumgem-mobile-trust-list {
		margin: var(--lumgem-space-4) 0;
		padding: 0 var(--lumgem-space-3);
	}

	/* .lumgem-scope prefix bumps this to 3 classes, beating both
	   components.css's `.lumgem-scope button` reset (dark bg/white text —
	   without this fix the label rendered invisible white-on-white here)
	   and its own 2-class `.lumgem-scope .lumgem-accordion-toggle` base
	   rule. No `button` element requirement, since rows with no confirmed
	   expandable detail render as a plain non-interactive <div> instead of
	   a <button> (see lumgem_single_product_trust_accordion()) and must be
	   styled identically to the interactive ones. */
	.lumgem-scope .lumgem-mobile-trust-list .lumgem-accordion-toggle {
		min-height: 52px;
		background: none;
		border: none;
		border-bottom: 1px solid var(--lumgem-color-line);
		border-radius: 0;
		padding: var(--lumgem-space-3) 0;
	}

	.lumgem-mobile-trust-list .lumgem-accordion-icon {
		width: 22px;
		height: 22px;
	}

	.lumgem-mobile-trust-list .lumgem-accordion-panel {
		border: none;
		padding: 0 0 var(--lumgem-space-3) var(--lumgem-space-6);
		font-size: 13px;
		color: var(--lumgem-color-muted);
	}
}
