.tn-product-carousel {
	position: relative;
	/* pan-y gives the vertical axis to the browser and keeps a horizontal drag
	   from panning the page underneath it. What it does NOT do is neutralise
	   Splide's preventDefault() on a vertical swipe : pan-y forbids a direction,
	   it doesn't make a cancelled touchmove uncancellable. That half is handled
	   in JS by tn-splide-behaviour.js; the two are a pair, neither is
	   sufficient alone. !important overrides any touch-action Splide may set via
	   inline style on the root. */
	touch-action: pan-y !important;
	/* The slides live in a nowrap flex track far wider than the column (~4980px vs a
	   ~773px viewport). overflow:hidden + min-width:0 alone do NOT fix the layout:
	   they clip the row visually, but inside a Stackable flex column the section still
	   reports its 4980px content size to ancestors' intrinsic-width calc, so the column
	   sizes to the carousel's content and drags its sibling blocks past the viewport
	   (body can't scroll, but the content does). contain:inline-size makes the section's
	   width depend only on its column, never on its contents : sealing the leak.
	   width/max-width:100% size it to the column; overflow:hidden clips the off-screen
	   cards. Scoped entirely to the carousel; no other blocks referenced. */
	width: 100%;
	/* Constrain the whole section to the theme content width and center it. The
	   is-layout-constrained class only sizes the inner track, leaving the <section>
	   full-width, so the arrows (anchored to the section at left/right:8px) escaped
	   into the page margins. Sizing the section itself keeps them on the card edges.
	   --theme-block-width is min(100%, ...), so this is a no-op inside a narrower
	   column and nested carousels are unaffected. */
	max-width: var(--theme-block-width, 100%);
	margin-inline: auto;
	min-width: 0;
	overflow: hidden;
	contain: inline-size;
}

/* Override WC/theme grid or wrapping flex on ul.products so Splide can scroll */
.tn-product-carousel .splide__list {
	display: flex !important;
	flex-wrap: nowrap !important;
	/* The theme puts a grid `gap` on ul.products; it survives our display:flex override
	   and stacks on top of Splide's own margin-based gap, doubling it (60px) so the last
	   per-view card gets sliced off. Zero it out and let Splide own the spacing. */
	gap: 0 !important;
	margin: 0 !important;
}

.tn-product-carousel .splide__slide {
	flex-shrink: 0;
	float: none !important;
	width: auto;
}

/* Arrow styling mirrors Blocksy's flexy-arrow design: circular 40px button,
   CSS-variable colours, subtle shadow, and a hover-fade-in with translate. */
.tn-product-carousel .splide__arrow {
	position: absolute;
	top: calc(50% - 20px);
	z-index: 2;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 40px;
	height: 40px;
	padding: 0;
	cursor: pointer;
	border: none;
	border-radius: var(--tn-radius-pill);
	color: var(--flexy-nav-arrow-color, var(--theme-text-color));
	/* Frosted: the arrows sit directly on the slide imagery, which is the one
	   place on a product carousel where a blur actually has something to work
	   on. Mixing the theme's own arrow colour keeps it configurable. */
	background: color-mix(in srgb,
		var(--flexy-nav-background-color, #fff)
		var(--tn-frost-tint-opacity, 85%), transparent);
	-webkit-backdrop-filter: blur(var(--tn-frost-blur-subtle, 6px));
	backdrop-filter: blur(var(--tn-frost-blur-subtle, 6px));
	box-shadow: 0 2px 8px rgba(0, 0, 0, .12);
	transition: all 0.12s cubic-bezier(0.455, 0.03, 0.515, 0.955);
	opacity: 1;
}

.tn-product-carousel .splide__arrow svg {
	pointer-events: none;
	width: 18px;
	height: 18px;
	fill: currentColor;
}

/* Splide ships one right-pointing chevron for both arrows; mirror the prev one. */
.tn-product-carousel .splide__arrow--prev svg { transform: scaleX(-1); }

/* Touch devices: meet the 44px minimum tap target (desktop keeps the 40px design). */
@media (pointer: coarse) {
	.tn-product-carousel .splide__arrow {
		width: 44px;
		height: 44px;
		top: calc(50% - 22px);
	}
	.tn-product-carousel .splide__arrow svg {
		width: 20px;
		height: 20px;
	}
}

/* On pointer devices: fade arrows in when the carousel is hovered, with a
   slight translate that slides them inward : matching flexy's hover behaviour. */
@media (any-hover: hover) {
	.tn-product-carousel .splide__arrow {
		opacity: 0;
		transform: translateX(var(--tn-arrow-translate-x, 0));
	}

	.tn-product-carousel:hover .splide__arrow {
		opacity: 1;
		--tn-arrow-translate-x: 0;
	}

	/* At the ends, keep disabled arrows subtly visible so they absorb misclicks
	   instead of letting them fall through to the product card underneath. */
	.tn-product-carousel:hover .splide__arrow:disabled {
		opacity: .35;
		cursor: default;
	}

	.tn-product-carousel .splide__arrow:hover:not(:disabled) {
		color: var(--flexy-nav-arrow-hover-color, #fff);
		background: color-mix(in srgb,
			var(--flexy-nav-background-hover-color, var(--theme-palette-color-1))
			var(--tn-frost-tint-opacity, 85%), transparent);
	}
}

.tn-product-carousel .splide__arrow:focus-visible {
	outline: 2px solid var(--tn-accent);
	outline-offset: 2px;
}

/* Visible focus if the track is ever given a tab stop. Splide's
   keyboard:'focused' binds its keydown handler to the root but adds no tabindex
   to anything, so today the track is not focusable and this never matches:
   arrow-key nav works only once focus is on a link *inside* the carousel, and
   the keydown bubbles up to the root from there. Kept because it is the right
   style the moment anything makes the track focusable. */
.tn-product-carousel .splide__track:focus-visible {
	outline: 2px solid var(--tn-accent);
	outline-offset: 2px;
}

.tn-product-carousel .splide__arrow--prev {
	left: 20px;
	--tn-arrow-translate-x: -10px;
}

.tn-product-carousel .splide__arrow--next {
	right: 20px;
	--tn-arrow-translate-x: 10px;
}

/* ================================================================
   Mobile / touch: drop the arrows, lead with swipe + pagination dots
   ================================================================ */

/* Arrows XOR dots : exactly one nav affordance is always visible. Arrows are
   hover-revealed, so on a touch device (no hover) OR at a mobile width they'd be
   invisible at rest; in both cases hide them and show the dots instead. The
   max-width clause is what makes the dots appear in a desktop browser narrowed
   to a phone width, not just on real touch hardware. Above 768px with a hovering
   pointer, the hover-arrows return and the dots hide. Covers the product,
   compatible, range and bundle carousels (all .tn-product-carousel). */
@media (any-hover: none), (max-width: 768px) {
	.tn-product-carousel .splide__arrows,
	.tn-product-carousel .splide__arrow {
		display: none;
	}
	/* Splide core ships .splide__pagination as display:none; reveal it here. */
	.tn-product-carousel .splide__pagination {
		display: flex;
	}
}

/* Pagination dots : rendered on every viewport (pagination:true) but only shown
   at mobile widths / on touch (above). Splide core leaves them in normal flow
   with no absolute positioning, so they sit below the track; we just space and
   skin them. */
.tn-product-carousel .splide__pagination {
	display: none; /* hidden by default; shown on touch via the query above */
	position: static;
	/* 16px, not the 8px this used to be, because the dot pitch is what caps the
	   tap target: hit areas may not overlap, so each one can be at most
	   dot + gap wide. 8 + 16 = 24px, which is the WCAG 2.5.8 minimum. */
	gap: 16px;
	margin: 16px 0 0;
	padding: 0;
}

.tn-product-carousel .splide__pagination__page {
	display: block;
	width: 8px;
	height: 8px;
	margin: 0;
	padding: 0;
	border: none;
	border-radius: var(--tn-radius-pill);
	background: var(--tn-separator, #d1d5db);
	box-shadow: var(--tn-shadow-dot-ring);
	opacity: 1;
	cursor: pointer;
	transition: transform .2s ease, background .2s ease;
}

.tn-product-carousel .splide__pagination__page.is-active {
	background: var(--tn-accent);
	transform: scale(1.4);
}

.tn-product-carousel .splide__pagination__page:focus-visible {
	outline: 2px solid var(--tn-accent);
	outline-offset: 2px;
}

/* The dots are the *only* visible navigation on touch : the arrows are hidden
   there : and an 8px dot is an 8x8px tap target, which is a miss more often
   than a hit. Expand the hit area with a transparent pseudo-element rather than
   by growing the dot, so the visual design is untouched: -8px on every side of
   an 8px dot is 24x24, the WCAG 2.5.8 minimum. Vertically that stays inside the
   reserved dot band; horizontally it is exactly half the 16px gap, so
   neighbouring targets meet without overlapping. They must not overlap :
   overlapping is worse than small, because the tap lands on whichever dot
   happens to paint last rather than the one under the finger. */
.tn-product-carousel .splide__pagination__page {
	position: relative;
}

.tn-product-carousel .splide__pagination__page::after {
	content: '';
	position: absolute;
	inset: -8px;
}

/* Past ~15 dots the 24px pitch above no longer fits one row on a 360px phone,
   and the row wraps : which both looks like a rash and breaks the reserved
   band's net-zero-height maths (the second row grows the section on mount, the
   CLS the band exists to prevent). Past that count the dots have also stopped
   being controls : nobody aims for dot 19 of 24 : so let them become what they
   actually are, a position indicator, and shrink them to keep one row. Costs
   the tap target on exactly the carousels where it was never usable anyway.
   :has() is the whole mechanism: it matches a list that *has* a 16th item.
   Where it isn't supported the rule is skipped and the row wraps as before. */
.tn-product-carousel .splide__pagination:has(li:nth-child(16)) {
	gap: 6px;
	/* Re-balance the band: margin-bottom is -(margin-top + dot height), and the
	   dot is 6px here rather than 8px. Left alone the section collapses 2px on
	   mount : a small shift, but the band exists to have none. */
	margin-bottom: -8px;
}

.tn-product-carousel .splide__pagination:has(li:nth-child(16)) .splide__pagination__page {
	width: 6px;
	height: 6px;
}

.tn-product-carousel .splide__pagination:has(li:nth-child(16)) .splide__pagination__page::after {
	inset: -9px -3px;
}

/* Fail-open: Splide hides .splide until it mounts (visibility:hidden). If mounting
   ever fails, reveal the products as a plain horizontally scrollable row instead of
   leaving blank space. Applied by the init's catch block. */
.tn-product-carousel.tn-no-splide { visibility: visible; }
.tn-product-carousel.tn-no-splide .splide__track { overflow-x: auto; }

/* ================================================================
   CLS guard : reserve the pagination row before Splide injects it
   ================================================================ */

/* Splide appends <ul class="splide__pagination"> to the DOM during mount(),
   which runs after first paint (the engine loads deferred). On touch/mobile :
   where the dots are the visible nav : that injection grows every carousel by
   the dot row's height and shoves all following content down, scoring a large
   layout shift (worst on the range archive, where 3–5 carousels stack and each
   one's growth compounds).

   The dots MUST stay in normal flow: the section carries overflow:hidden (to
   clip the off-screen cards), which would clip an absolutely-positioned
   pagination away entirely. So instead we reserve the row's height as bottom
   padding from first paint, and cancel the flow height the in-flow dots add
   with a negative bottom margin (margin-bottom = -(margin-top + dot height)).
   Net result: the dots sit in the reserved padding band, fully visible, and the
   section's box is byte-identical before and after mount → zero shift. Scoped
   to the same query that reveals the dots so desktop (arrows, no dots) gets no
   dead space. Placed last so it wins the cascade over the base rule above.

   The band is 20px of section padding-bottom, and the dots are dropped into it
   as net-zero-height in-flow content: margin-bottom = -(margin-top + 8px dot).
   So the two gaps either side of the dots are:
       above = track padding-bottom + pagination margin-top
       below = 20px band - (margin-top + 8px)
   which is worth writing down, because the previous values (20px track padding
   + 8px margin-top) put 28px above the dots and 4px below : the lopsidedness
   that reads as "too much space above the dots" on a phone. 2px margin-top with
   the touch track padding below gives 10px / 10px.
   The bundle carousel is in this block too. It used to be excluded on the
   grounds that reserving a band would add dead space to a compact widget, but
   it was paying for that space anyway : its dots are in flow with no negative
   margin, so they added 24px on mount as real layout shift, sat 36px below the
   card, and the scaled active dot was clipped by the section's overflow:hidden.
   Reserving is strictly better: same finished layout, no shift.
   Known limit: the net-zero maths assumes the dots fit on one row. Past ~23
   slides at 360px they wrap, and the second row grows the section again (16px
   measured). A carousel that needs 24 dots has a bigger problem than CLS. */
@media (any-hover: none), (max-width: 768px) {
	.tn-product-carousel {
		padding-bottom: 20px;
	}

	/* Re-assert display:flex here so this later rule wins. */
	.tn-product-carousel .splide__pagination {
		display: flex;
		margin: 2px 0 -10px;
	}
}

/* ================================================================
   Product cards : give the hover-lift room (regression fix)
   ================================================================
   The product-card look + hover-lift is owned by the child theme so it stays
   consistent across the shop, range and carousels. Here we only reserve
   vertical room in the track so a lifted card (translateY(-4px) + its shadow)
   isn't clipped by the section's overflow:hidden : mirrors the bundle carousel.
   Horizontal padding stays 0 so Splide's slide-width math is untouched. */
.tn-product-carousel .splide__track {
	padding: 8px 0 20px;
	/* Repeated from the root because iOS Safari computes touch-action from the
	   touched element rather than trusting the ancestor value alone. It buys the
	   native vertical axis, not immunity from preventDefault() : Splide binds
	   {passive:false} touchmove listeners to this track and can still cancel a
	   vertical move, which is what tn-splide-behaviour.js exists to stop. */
	touch-action: pan-y;
}

/* …but only where a hover-lift can actually happen. On a touch device that 20px
   is dead space sitting directly above the pagination dots, which is most of
   why the dots looked so far from the cards; drop it to what the *resting*
   shadow needs. Scoped to (any-hover: none) rather than folded into the
   dot-band query above on purpose: that query also fires for a desktop window
   narrowed past 768px, where the pointer still hovers and the lift still needs
   its room. Last in the file so it wins over the base rule it amends. */
@media (any-hover: none) {
	.tn-product-carousel .splide__track {
		padding-bottom: 8px;
	}
}
