/*
 * Home. WHM-56.
 *
 * The dark/light split at its clearest: the hero and the tested-batch ticker
 * are the brand moment, everything below them is commercial and light.
 *
 * The card rules here are deliberately NOT duplicated from cards.css, the
 * featured row reuses .ep-card, and cards.css is enqueued alongside this file
 * on the front page for exactly that reason. Duplicating them is how two
 * definitions of the same object drift apart.
 */

.ep-home { font-family: var(--ep-font); background: var(--ep-lt-bg); }

/* ZONE-LEVEL HEADING COLOURS.
   The theme styles h1-h4 with enough specificity to beat an inherited colour,
   which is how the hero headline rendered near-black on the dark ground in the
   first preview. Every heading inside each zone is therefore given its colour
   explicitly rather than relying on the parent. Cheap insurance against the
   same bug appearing in whichever section is added next. */
.ep-hero h1, .ep-hero h2, .ep-hero h3 { color: var(--ep-white); }
.ep-lt h1, .ep-lt h2, .ep-lt h3, .ep-lt h4 { color: var(--ep-lt-ink); }
/* EXCEPT the featured banner, which is a DARK band sitting inside the light
   zone. The zone rule above painted its heading in the light zone's near-black
   ink, on a near-black band, the title was almost invisible in the live
   preview. Any dark island added inside .ep-lt later needs the same exemption,
   which is the cost of zone-level colour rules and still cheaper than the bug
   they prevent. */
.ep-lt .ep-deal h1, .ep-lt .ep-deal h2, .ep-lt .ep-deal h3 { color: var(--ep-white); }
/* The Toro introduction is the second such island. Added at the same time as
   the section itself was found rendering its heading in near-black on its own
   near-black band, exactly the failure the note above predicted. */
.ep-lt .ep-toro h1, .ep-lt .ep-toro h2, .ep-lt .ep-toro h3 { color: var(--ep-white); }
.ep-tick h1, .ep-tick h2, .ep-tick h3 { color: var(--ep-white); }
.ep-home a { text-decoration: none; }
.ep-home :focus-visible { outline: var(--ep-focus); outline-offset: var(--ep-focus-offset); border-radius: var(--ep-r-xs); }

/* ---------------------------------------------------------------------------
   1. HERO

   NO GRID. The first version laid a 64px square grid over the dark ground,
   which is the same device thepedshop.to uses on its own hero, the one thing
   this storefront must not borrow, since the whole point is that the two stores
   should not look related.

   Instead: three overlapping radial blooms at different sizes, offsets and
   hues, plus a vignette. Because none of them repeat and none align to a grid,
   the result reads as depth and light rather than as structure, and it cannot
   be mistaken for anyone's tiled pattern.

   All decorative, both layers sit behind the content and neither is announced.
   --------------------------------------------------------------------------- */
.ep-hero {
	position: relative;
	overflow: hidden;
	background: var(--ep-ground);
	color: var(--ep-white);
}

/* The bloom. Three radials in one layer: a bright violet core lifted above
   centre, a wider cooler indigo wash pulled to the right, and a low warm
   magenta at the lower left to stop the whole thing reading as one symmetrical
   blob. */
.ep-hero::before {
	content: "";
	position: absolute;
	inset: -20% -10% auto -10%;
	height: 150%;
	background-image:
		radial-gradient(38% 46% at 50% 22%, rgba(168, 85, 247, 0.32) 0%, rgba(168, 85, 247, 0) 100%),
		radial-gradient(46% 40% at 78% 44%, rgba(109, 40, 217, 0.26) 0%, rgba(109, 40, 217, 0) 100%),
		radial-gradient(34% 34% at 18% 76%, rgba(192, 132, 252, 0.16) 0%, rgba(192, 132, 252, 0) 100%);
	pointer-events: none;
}

/* Vignette, so the bloom has an edge to fall off against and the stats row at
   the bottom sits on something darker than the middle. */
.ep-hero::after {
	content: "";
	position: absolute;
	inset: 0;
	background:
		radial-gradient(120% 90% at 50% 35%, rgba(13, 7, 22, 0) 40%, rgba(13, 7, 22, 0.55) 100%);
	pointer-events: none;
}


/* The drift. The two blooms in ::before are static; this layer adds a pair of
   slow moving lights over them so the hero is not a still image. Transform
   only, so it composites on the GPU and costs nothing per frame. */
.ep-hero__glow {
	position: absolute;
	inset: -30%;
	pointer-events: none;
	background-image:
		radial-gradient(26% 26% at 30% 40%, rgba(192, 132, 252, 0.20) 0%, rgba(192, 132, 252, 0) 100%),
		radial-gradient(22% 22% at 70% 60%, rgba(88, 28, 235, 0.22) 0%, rgba(88, 28, 235, 0) 100%);
	animation: epHeroDrift 26s ease-in-out infinite alternate;
	will-change: transform;
}
@keyframes epHeroDrift {
	0%   { transform: translate3d(-3%, -2%, 0) scale(1); }
	50%  { transform: translate3d(3%, 2%, 0) scale(1.08); }
	100% { transform: translate3d(-2%, 3%, 0) scale(1.02); }
}

@media (prefers-reduced-motion: reduce) {
	.ep-hero__glow { animation: none; }
	.ep-hero__brand, .ep-hero__amp { animation: none; opacity: 1; transform: none; }
	.ep-hero__kick, .ep-hero__t1, .ep-hero__t2, .ep-hero__sub,
	.ep-hero__cta, .ep-hero .ep-social, .ep-hero__lab, .ep-hero__facts {
		animation: none;
		opacity: 1;
		transform: none;
	}
}

.ep-hero__in {
	position: relative;
	z-index: 2;
	padding-top: var(--ep-s-12);
	padding-bottom: var(--ep-s-10);
	text-align: center;
}

.ep-hero__kick {
	display: inline-flex;
	align-items: center;
	gap: var(--ep-s-2);
	max-width: 34ch;
	font-size: var(--ep-t-sm);
	font-weight: var(--ep-w-semi);
	color: var(--ep-vio-2);
	margin: 0 0 var(--ep-s-5);
}

.ep-hero__title {
	/* Colour set EXPLICITLY rather than inherited from .ep-hero. The theme
	   styles h1 with higher specificity, so the inherited white lost and the
	   headline rendered near-black on the dark ground, unreadable, and the
	   first thing visible in the preview. The em inside it looked correct only
	   because that rule sets its own colour. */
	color: var(--ep-white);
	font-size: var(--ep-t-2xl);
	font-weight: var(--ep-w-black);
	line-height: var(--ep-lh-tight);
	letter-spacing: var(--ep-track-hero);
	margin: 0 auto var(--ep-s-5);
	max-width: 15ch;
	text-wrap: balance;
}
.ep-hero__title em { font-style: normal; color: var(--ep-vio); }

.ep-hero__sub {
	font-size: var(--ep-t-base);
	color: var(--ep-mut);
	margin: 0 auto var(--ep-s-6);
	max-width: 50ch;
}

.ep-hero__cta { display: flex; gap: var(--ep-s-3); justify-content: center; flex-wrap: wrap; margin-bottom: var(--ep-s-8); }

/* NO flex gap. The label is a sentence with an inline <span> around the count
   ("Shop all 71 products"), and a flex gap lands 8px either side of that span,
   so it rendered as "Shop all  71  products". Word spacing is what separates
   words; gap is for separating an icon from a label, which these buttons do not
   have. */
.ep-btn {
	display: inline-flex;
	align-items: center;
	min-height: var(--ep-control);
	padding: 0 var(--ep-s-8);
	border-radius: var(--ep-r-md);
	font-size: var(--ep-t-base);
	font-weight: var(--ep-w-bold);
	letter-spacing: var(--ep-track-tight);
}
/* DEEPER VIOLET, WHITE TEXT. It was --ep-vio with near-black ink, and the
   tokens file already warns that --ep-vio is too light to carry white text.
   Rather than keep black on lilac, the fill drops to the deep step, which
   carries white at about 6.5:1. The glow goes with it: a coloured halo under a
   solid button was most of what made these read as shouting. */
.ep-btn--p {
	background: var(--ep-vio-deep);
	color: #FFFFFF !important;
	box-shadow: 0 8px 22px rgba(109, 40, 217, 0.34);
}
.ep-btn--p:hover { background: #5B21B6; }
.ep-btn--s { background: var(--ep-surf); color: var(--ep-white) !important; border: 1px solid var(--ep-edge); }

/* FLEX, not grid, and that is the whole point.
   A grid creates cells whether or not there is content for them: three stats in
   a two-column grid left a fourth cell that, with a 1px gap over a lighter
   background, rendered as a visible empty block. auto-fit did not help, it
   fits COLUMNS, not items. Seen on a real 375px screen.
   Flex only lays out items that exist, and the last one grows to fill its row,
   so any number of stats produces a full rectangle. */
/* --- the credential: both house lockups, side by side ---
   This is the thing the hero exists to say, so it sits above the headline.
   Each lockup is inlined with its own id suffix (see inline_svg) because both
   files render again further down the page. */
.ep-hero__brands {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: var(--ep-s-4);
	margin-bottom: var(--ep-s-6);
}
.ep-hero__brand { display: block; line-height: 0; }
.ep-hero__brand svg { display: block; height: auto; }
.ep-hero__brand--tiva svg { width: 84px; }
/* The Toro lockup is a wide two-line block against Tiva's compact wordmark, so
   matching their WIDTHS would leave Toro looking twice the size. Matched on
   optical height instead. */
.ep-hero__brand--toro svg { width: 116px; }

/* BOTH MARKS KNOCKED TO WHITE. Tiva is teal on navy and Toro is gold on black;
   neither was drawn for deep violet, and side by side on it the navy wordmark
   sank while the gold shouted. Repainting one fill at a time was treating a
   symptom, so both are flattened and inverted instead. Nothing is left to
   clash, and the credential reads as one statement rather than two badges.

   brightness(0) then invert(1) works on ANY source fill, gradients included,
   which is what neither logo's own classes would have allowed. */
.ep-hero__brand svg { filter: brightness(0) invert(1); opacity: 0.96; }

/* The join between them. A hairline plus, not the word "and": at this size any
   word competes with the two marks it is supposed to be subordinate to. */
.ep-hero__amp {
	position: relative;
	width: 13px;
	height: 13px;
	flex: none;
	opacity: 0.5;
}
.ep-hero__amp::before,
.ep-hero__amp::after {
	content: "";
	position: absolute;
	inset: 50% 0 auto 0;
	height: 1.5px;
	border-radius: 2px;
	background: var(--ep-mut);
	transform: translateY(-50%);
}
.ep-hero__amp::after { transform: translateY(-50%) rotate(90deg); }

/* Each mark arrives on its own beat, left then right, so the pair reads as one
   statement being assembled rather than two logos fading up together. */
@keyframes epHeroBrandIn {
	0%   { opacity: 0; transform: translateY(12px) scale(0.9); }
	100% { opacity: 1; transform: none; }
}
.ep-hero__brand,
.ep-hero__amp { animation: epHeroBrandIn 700ms var(--ep-ease) both; }
.ep-hero__brand--tiva { animation-delay: 60ms; }
.ep-hero__amp         { animation-delay: 150ms; }
.ep-hero__brand--toro { animation-delay: 210ms; }

/* ---------------------------------------------------------------------------
   THE REST OF THE HERO ARRIVES TOO

   The lockups were choreographed and everything under them appeared at once,
   which does not read as calm, it reads as half finished: the eye follows the
   logos in and then the whole page lands behind them in one flat beat.

   So the same language extends down the hero, on one ladder. It is deliberately
   NOT more than this. No particles, no parallax on the pointer, no headline
   typing itself out, no number counting up to 73. Those are things a hero does
   INSTEAD of having something to say, and this one has something to say.

   The only piece of interpretation here is the headline, which is split
   because the sentence is: "Every batch tested" is the claim and "Every result
   published" is what makes the claim checkable. They arrive in that order,
   about a beat apart, which is the order somebody reads them in anyway. That
   is the whole idea. It plays once, on load, and never again.
   --------------------------------------------------------------------------- */
@keyframes epHeroIn {
	0%   { opacity: 0; transform: translateY(10px); }
	100% { opacity: 1; transform: none; }
}
.ep-hero__kick,
.ep-hero__t1,
.ep-hero__t2,
.ep-hero__sub,
.ep-hero__cta,
.ep-hero .ep-social,
.ep-hero__lab,
.ep-hero__facts {
	animation: epHeroIn 480ms var(--ep-ease) both;
}
/* The em is inline; it needs a block box of its own to be moved. */
.ep-hero__t1,
.ep-hero__t2 { display: inline-block; }

/* THE LADDER IS SHORT ON PURPOSE, and for two reasons.
   
   A hero that takes a second and a half to assemble is not calm, it is slow,
   and the visitor is looking at the fold while it happens. The whole thing is
   settled inside about a second.
   
   The second reason is exposure. Anything held at opacity 0 by a fill mode is
   content that is not on screen yet, and the longer that lasts the more it
   matters if the animation is ever interrupted. Short delays keep that window
   small. Reduced motion skips it entirely and everything is simply there. */
.ep-hero__kick   { animation-delay: 0ms; }
.ep-hero__t1     { animation-delay: 90ms; }
.ep-hero__t2     { animation-delay: 200ms; }
.ep-hero__sub    { animation-delay: 310ms; }
.ep-hero__cta    { animation-delay: 400ms; }
.ep-hero .ep-social { animation-delay: 470ms; }
.ep-hero__lab    { animation-delay: 520ms; }
.ep-hero__facts  { animation-delay: 560ms; }

/* --- the primary button ---
   WHITE, not violet. On this ground white is the highest contrast available,
   so the button carries more weight than a saturated fill while adding no
   fourth hue to a hero that already had three. Violet is then free to mean one
   thing only, the accent in the headline.

   The base is very slightly violet tinted rather than flat #FFF, because a
   glint needs something to be brighter THAN. On pure white it is invisible. */
.ep-btn--w {
	position: relative;
	overflow: hidden;
	gap: var(--ep-s-2);
	background: linear-gradient(180deg, #FFFFFF 0%, #EFE9FB 100%);
	color: #16091F !important;
	border: 1px solid rgba(255, 255, 255, 0.9);
	box-shadow: 0 10px 26px rgba(10, 4, 20, 0.45);
}
.ep-btn__t, .ep-btn__a { position: relative; z-index: 2; }
.ep-btn__a { opacity: 0.55; }

/* The glint. One narrow band crossing on a long cycle: the pause IS most of
   the keyframe, because a highlight sweeping continuously would compete with
   the copy it is meant to draw the eye toward. Transform only, so it
   composites on the GPU and costs nothing per frame. */
.ep-btn__glint {
	position: absolute;
	top: -60%;
	left: 0;
	width: 42%;
	height: 220%;
	z-index: 1;
	background: linear-gradient(90deg,
		rgba(255,255,255,0) 0%,
		rgba(255,255,255,0.95) 50%,
		rgba(255,255,255,0) 100%);
	transform: translateX(-160%) rotate(18deg);
	animation: epGlint 5.2s ease-in-out 1.4s infinite;
	pointer-events: none;
}
@keyframes epGlint {
	0%   { transform: translateX(-160%) rotate(18deg); }
	26%  { transform: translateX(320%)  rotate(18deg); }
	100% { transform: translateX(320%)  rotate(18deg); }
}

/* --- the communities ---
   The capsule itself is defined in chrome.css, because the burger drawer uses
   the same component and that renders on every page while this file loads only
   on the front page. Only its spacing in the hero belongs here. It is
   deliberately NOT given a glint: one moving thing in a hero is a highlight,
   two is a fairground. */
.ep-hero .ep-social { margin-bottom: var(--ep-s-4); }

.ep-hero__lab { margin: 0 0 var(--ep-s-8); font-size: var(--ep-t-xs); }
.ep-hero__lab a {
	color: var(--ep-mut) !important;
	border-bottom: 1px solid var(--ep-edge);
	padding-bottom: 2px;
	font-weight: var(--ep-w-semi);
}
.ep-hero__lab a:hover { color: var(--ep-white) !important; }

@media (prefers-reduced-motion: reduce) {
	.ep-btn__glint { animation: none; opacity: 0; }
}

/* --- the fact row ---
   A slim inline row, not the four stat cards this hero used to end on. Those
   cards were the widest thing in the hero and pushed the buttons under the fold
   on a phone, which is where nearly all of this store's traffic is. */
.ep-hero__facts {
	list-style: none;
	margin: 0;
	padding: var(--ep-s-3) var(--ep-s-2);
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: var(--ep-s-2) var(--ep-s-5);
	border-top: 1px solid var(--ep-edge);
	border-bottom: 1px solid var(--ep-edge);
	font-size: var(--ep-t-xs);
	color: var(--ep-mut);
}
.ep-hero__facts li { margin: 0; }
.ep-hero__facts b {
	color: var(--ep-white);
	font-weight: var(--ep-w-black);
	letter-spacing: var(--ep-track-head);
}

/* ---------------------------------------------------------------------------
   2. TESTED TICKER

   The signature device: what scrolls is lab data, not product names. Renders
   only when batch data exists, so it is never an empty rail.
   --------------------------------------------------------------------------- */
.ep-tick {
	background: var(--ep-surf);
	border-top: 1px solid var(--ep-edge);
	border-bottom: 1px solid var(--ep-edge);
	overflow: hidden;
	color: var(--ep-white);
}
.ep-tick__in { display: flex; align-items: center; padding-left: var(--ep-gutter-now, var(--ep-s-5)); }
/* NO COLOURED BLOCK. A violet tile with a pulsing green dot is the stock "live
   status" component, and on a phone it was a 171px slab carrying one dot. The
   label sits on the rail's own ground now, separated by a hairline, which is
   what a divider is for. */
.ep-tick__lbl {
	flex: none;
	display: flex;
	align-items: center;
	gap: var(--ep-s-3);
	padding: 0 var(--ep-s-5) 0 0;
	margin-right: var(--ep-s-5);
	height: 54px;
	border-right: 1px solid var(--ep-edge);
	font-size: var(--ep-t-xs);
	font-weight: var(--ep-w-bold);
	letter-spacing: 0.12em;
	text-transform: uppercase;
	color: var(--ep-mut);
	white-space: nowrap;
}
/* The moving track lives in here so it is clipped before it can reach the
   label. min-width:0 matters: without it a flex item refuses to shrink below
   its content, and the content is a 2000px marquee. */
.ep-tick__vp {
	flex: 1 1 auto;
	min-width: 0;
	overflow: hidden;
}
.ep-tick__trace {
	flex: none;
	width: 40px;
	height: 18px;
	color: var(--ep-ok);
	overflow: visible;
}
/* Stroked on once when the section arrives, at about the speed a plotter draws
   it. Once, not looping: a trace that redraws forever is an animation, and this
   is meant to read as a reading. */
.ep-reveal--ready .ep-tick .ep-tick__trace path {
	stroke-dasharray: 120;
	stroke-dashoffset: 120;
}
.ep-reveal--ready .ep-tick.is-in .ep-tick__trace path {
	animation: ep-trace 1400ms var(--ep-ease) 200ms forwards;
}
@keyframes ep-trace { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
	.ep-reveal--ready .ep-tick .ep-tick__trace path,
	.ep-reveal--ready .ep-tick.is-in .ep-tick__trace path {
		stroke-dashoffset: 0;
		animation: none;
	}
}
.ep-tick__track {
	display: flex;
	gap: var(--ep-s-8);
	align-items: center;
	height: 54px;
	white-space: nowrap;
	padding-left: var(--ep-s-6);
	animation: ep-tick 46s linear infinite;
	will-change: transform;
	/* SIZED TO ITS CONTENT, not to the box it sits in. Inside the new viewport
	   the track became a block level flex container and took the viewport's
	   width, so translateX(-50%) shifted it by half the SCREEN rather than half
	   the LIST: the loop jumped instead of running seamlessly. max-content
	   makes -50% mean exactly one copy of the list, which is what the two
	   printed passes in the template are for. */
	width: max-content;
}
@keyframes ep-tick {
	from { transform: translateX(0); }
	to   { transform: translateX(-50%); }
}
/* Motion sensitivity: the rail stops rather than disappearing, so the data is
   still readable. */
@media (prefers-reduced-motion: reduce) {
	.ep-tick__track { animation: none; }
}
.ep-tick__i { display: inline-flex; align-items: center; gap: var(--ep-s-3); font-size: var(--ep-t-sm); }

/* On a phone the label was taking 171px of a 375px screen, leaving the rail
   about two words wide. The dot and the rule keep the meaning; the words go. */
@media (max-width: 600px) {
	.ep-tick__lbl { padding: 0 var(--ep-s-4) 0 0; margin-right: var(--ep-s-4); gap: 0; }
	.ep-tick__trace { width: 32px; }
	.ep-tick__lbl span {
		position: absolute;
		width: 1px; height: 1px;
		overflow: hidden;
		clip: rect(0, 0, 0, 0);
		clip-path: inset(50%);
		white-space: nowrap;
	}
	.ep-tick__track { padding-left: var(--ep-s-4); gap: var(--ep-s-6); }
	.ep-tick__i { font-size: var(--ep-t-xs); }
}
.ep-tick__n { font-weight: var(--ep-w-medium); }
.ep-tick__l { color: var(--ep-mut); }
.ep-tick__p { color: var(--ep-ok); font-weight: var(--ep-w-bold); }

/* ---------------------------------------------------------------------------
   3. LIGHT ZONE
   --------------------------------------------------------------------------- */
.ep-lt { background: var(--ep-lt-bg); color: var(--ep-lt-ink); }
/* 32 on a phone, 40 from 700px up. It was 56 at desktop, which put 112px of
   air between every band plus a rule, and read as the page having run out of
   things to say between sections. */
.ep-sec { padding: var(--ep-s-8) 0; }
.ep-sec + .ep-sec { border-top: 1px solid var(--ep-lt-line); }

.ep-sec__h {
	display: flex;
	align-items: flex-end;
	justify-content: space-between;
	gap: var(--ep-s-5);
	margin-bottom: var(--ep-s-6);
	flex-wrap: wrap;
}
.ep-sec__h h2 {
	font-size: var(--ep-t-xl);
	font-weight: var(--ep-w-black);
	letter-spacing: var(--ep-track-head);
	margin: 0;
	color: var(--ep-lt-ink);
}
.ep-sec__h p { margin: var(--ep-s-1) 0 0; color: var(--ep-lt-mut); font-size: var(--ep-t-sm); }
.ep-sec__h a { color: var(--ep-vio-deep); font-weight: var(--ep-w-semi); font-size: var(--ep-t-sm); white-space: nowrap; }

/* --- trust --- */
.ep-trust { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--ep-s-5); }
.ep-trust__c { display: flex; gap: var(--ep-s-3); }
.ep-trust__c svg { flex: none; color: var(--ep-vio-deep); }
.ep-trust__c b { display: block; font-size: var(--ep-t-sm); font-weight: var(--ep-w-bold); margin-bottom: 1px; }
.ep-trust__c span { font-size: var(--ep-t-xs); color: var(--ep-lt-mut); }

/* --- house brands --- */
.ep-brands { display: grid; grid-template-columns: 1fr; gap: var(--ep-s-4); }
.ep-bcard {
	background: var(--ep-lt-card);
	border: 1px solid var(--ep-lt-line);
	border-radius: var(--ep-r-lg);
	padding: var(--ep-s-6);
	display: flex;
	flex-direction: column;
}
.ep-bcard.is-lead { background: linear-gradient(160deg, var(--ep-lt-tint), var(--ep-lt-card) 70%); border-color: var(--ep-lt-tint-e); }
/* THE LOCKUP, flattened to one colour. On a light card two brand palettes
   would fight each other exactly as they did in the hero, so both marks are
   knocked to ink and given the same optical height. Opacity rather than a
   lighter fill, so they sit back without going muddy. */
.ep-bcard__mk {
	display: block;
	line-height: 0;
	height: 26px;
	margin-bottom: var(--ep-s-4);
	color: var(--ep-lt-ink);
}
.ep-bcard__mk svg {
	display: block;
	height: 100%;
	width: auto;
	filter: brightness(0);
	opacity: 0.82;
}
/* The refer card's mark is already a single colour line drawing, so it takes
   currentColor and must not be run through the flattening filter. */
.ep-bcard__mk--refer svg { filter: none; opacity: 0.5; }

.ep-bcard__tag { font-size: var(--ep-t-xs); font-weight: var(--ep-w-bold); color: var(--ep-vio-deep); margin-bottom: var(--ep-s-2); }
.ep-bcard__h { font-size: var(--ep-t-lg); font-weight: var(--ep-w-black); letter-spacing: var(--ep-track-sub); margin: 0 0 var(--ep-s-2); }
.ep-bcard p { font-size: var(--ep-t-sm); color: var(--ep-lt-mut); margin: 0 0 var(--ep-s-5); }
.ep-bcard__go {
	margin-top: auto;
	align-self: flex-start;
	display: inline-flex; align-items: center;
	min-height: var(--ep-tap);
	padding: 0 var(--ep-s-5);
	border-radius: var(--ep-r-sm);
	background: var(--ep-lt-sunk);
	border: 1px solid var(--ep-lt-line);
	color: var(--ep-lt-ink) !important;
	font-size: var(--ep-t-sm);
	font-weight: var(--ep-w-bold);
}
.ep-bcard.is-lead .ep-bcard__go { background: var(--ep-vio-deep); border-color: transparent; color: var(--ep-white) !important; }

/* --- quick browse: the merchandising views --- */
/* One column on a phone. Three fixed columns put "Recently Tested" and its
   count into about 110px at 375px wide, which wraps to two lines and reads as
   broken. The store is overwhelmingly mobile, so the phone case is the default
   and the row only appears at width. */

/* --- categories ---

   MOBILE FIRST, AND MOBILE IS THE PROBLEM THIS SOLVES.

   The first version gave every category a full-width row with a 56px image, a
   name, a count, a chevron and a wrapped row of child chips. Seven of those on
   a 375px screen ran past two full screen heights before a single product
   appeared. On a store that is overwhelmingly phone traffic, the category
   block was pushing the thing people came to buy below three scrolls.

   On a phone it is now a TWO-COLUMN TILE: image above, name and count beneath,
   no chevron, no child chips. Seven categories become four short rows.

   The children are not lost, they are one tap away on the parent's own
   archive, which is where somebody who has chosen "Toro Labs" is going anyway.
   Showing them on the homepage was spending a lot of a phone screen on a
   decision most visitors have not made yet.

   At 700px the tile becomes the horizontal row again, with the chips, because
   there the width is free.
   --------------------------------------------------------------------------- */
/* A SCROLLER ON PHONES, not a grid.
   Two-column tiles were already far shorter than the original full-width rows,
   but seven categories still cost four rows of vertical space on a screen where
   vertical space is the whole budget. As a horizontal rail the entire block
   costs ONE row, and swiping sideways through categories is a gesture people
   already use everywhere else on a phone.

   scroll-snap so it comes to rest on a tile rather than half of one, and the
   negative margin with matching padding lets the rail bleed to both screen
   edges while its first tile still lines up with the section heading. Without
   that the rail looks inset and the last tile appears to be the end of the
   list when it is not. */
/* ---------------------------------------------------------------------------
   SHOP BY CATEGORY

   COMPACT ROWS, and the third rebuild of this block. The first two tried to
   make one card shape serve categories that do and do not have children, which
   cannot be done: a shape that fits three chips leaves a hole in the four cards
   with none, and a shape that fits none clips the ones with three. The last
   version did both at once.

   So the chips are gone and the card is a ROW: thumbnail, name, count,
   chevron. Every card is identical because every card now holds identical
   things. It is also roughly half the height it was, which is the other half
   of the brief.

   The ranges are not lost. All eighteen categories sit in the burger menu with
   children indented, and each parent archive lists its own. The card carries
   the fact that ranges exist in its count line instead.
   --------------------------------------------------------------------------- */
.ep-cats {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: var(--ep-s-2);
}

.ep-cat {
	display: flex;
	align-items: center;
	gap: var(--ep-s-3);
	min-height: 60px;
	padding: var(--ep-s-2) var(--ep-s-3);
	background: var(--ep-lt-card);
	border: 1px solid var(--ep-lt-line);
	border-radius: var(--ep-r-md);
	color: var(--ep-lt-ink) !important;
	transition: border-color var(--ep-dur-1) var(--ep-ease), transform var(--ep-dur-1) var(--ep-ease);
}
.ep-cat:hover { border-color: var(--ep-vio-deep); }
.ep-cat:active { transform: scale(0.99); }

.ep-cat__pic {
	flex: none;
	width: 38px;
	height: 38px;
	border-radius: var(--ep-r-sm);
	background: var(--ep-lt-sunk);
	overflow: hidden;
	display: block;
}
.ep-cat__pic img { width: 100%; height: 100%; object-fit: contain; display: block; mix-blend-mode: multiply; }

/* min-width 0 so a long name can ellipsis instead of pushing the chevron out
   of the card, which is what a flex item does by default. */
.ep-cat__name { min-width: 0; flex: 1; }
.ep-cat__name b {
	display: block;
	font-size: var(--ep-t-sm);
	font-weight: var(--ep-w-bold);
	letter-spacing: var(--ep-track-tight);
	line-height: 1.2;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}
.ep-cat__n {
	display: block;
	font-size: 11px;
	color: var(--ep-lt-mut);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}
.ep-cat svg { flex: none; color: var(--ep-lt-mut); }

@media (min-width: 700px) {
	.ep-cats { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--ep-s-3); }
}
@media (min-width: 1000px) {
	/* THREE ACROSS, NOT FOUR. Six categories over four columns leaves two
	   orphans on the second row and the block reads as unfinished. Three gives
	   two full rows of three.

	   FLEX RATHER THAN GRID, deliberately: a grid centres its tracks but still
	   packs a partial last row from the left, so the moment the category count
	   stops being a multiple of three this goes lopsided again. The category
	   list is built from the taxonomy and will change. Flex centres whatever
	   the last row happens to hold. */
	.ep-cats {
		display: flex;
		flex-wrap: wrap;
		justify-content: center;
		max-width: 1020px;
		margin-inline: auto;
	}
	.ep-cats > * { flex: 0 1 300px; }
	.ep-cat { min-height: 64px; padding: var(--ep-s-2) var(--ep-s-4); }

	/* The heading stack goes over the grid rather than beside it, and the
	   All products link drops underneath instead of sitting out on the right
	   where it would pull the eye off centre. */
	.ep-sec--mid .ep-sec__h {
		flex-direction: column;
		align-items: center;
		text-align: center;
		gap: var(--ep-s-3);
	}
	/* Card interiors stay left aligned on purpose. Each one is a thumbnail,
	   then two lines of text, then a chevron, and centring the text between a
	   fixed left image and a fixed right arrow reads as misalignment rather
	   than as centring. The section is centred; the cards are still rows. */
}

/* --- lab table --- */
.ep-lab {
	background: var(--ep-lt-card);
	border: 1px solid var(--ep-lt-line);
	border-radius: var(--ep-r-lg);
	overflow: hidden;
}
/* The grid is driven by how many optional columns actually got printed, rather
   than by nth-child guesses about which ones they were. */
.ep-lab__r {
	display: grid;
	grid-template-columns: 1fr auto;
	gap: var(--ep-s-3);
	padding: var(--ep-s-4);
	align-items: center;
	font-size: var(--ep-t-sm);
}
.ep-lab__r + .ep-lab__r { border-top: 1px solid var(--ep-lt-line-2); }
.ep-lab__r--h { background: #FBFAFE; color: var(--ep-lt-mut); font-weight: var(--ep-w-semi); font-size: var(--ep-t-xs); }
.ep-lab__n { font-weight: var(--ep-w-bold); }
.ep-lab__m { color: var(--ep-lt-mut); }
.ep-lab__p { color: var(--ep-lt-ok); font-weight: var(--ep-w-black); }
.ep-lab__r a { color: var(--ep-vio-deep); font-weight: var(--ep-w-semi); }

/* On a phone the lot and date columns are dropped rather than squeezed: five
   columns at 360px produces one word per line and reads as damage. */
/* On a phone the optional columns go entirely: a lot number and a date do not
   fit beside a product name at 375px, and the certificate link is what somebody
   is reaching for anyway. */
.ep-lab__c--opt { display: none; }

/* ---------------------------------------------------------------------------
   4. WIDER SCREENS
   --------------------------------------------------------------------------- */
@media (min-width: 600px) {
	.ep-trust { grid-template-columns: repeat(4, 1fr); }
	.ep-lab__c--opt { display: block; }
	.ep-lab--m0 .ep-lab__r { grid-template-columns: 1fr auto; }
	.ep-lab--m1 .ep-lab__r { grid-template-columns: 2.2fr 1.1fr auto; }
	.ep-lab--m2 .ep-lab__r { grid-template-columns: 2.2fr 1fr 1fr auto; }
	.ep-lab--m3 .ep-lab__r { grid-template-columns: 2.2fr 1fr 1fr 0.9fr auto; }
	/* m4 was added when Measured and Purity were separate columns. They were
	   merged into one Result column, so the count tops out at 3 again. Kept as
	   a backstop rather than deleted: the modifier is generated from a count,
	   and a count with no matching rule silently falls back to the two column
	   base with every optional cell piled into the first track. */
	.ep-lab--m4 .ep-lab__r { grid-template-columns: 2fr 1fr 1fr 1fr 0.9fr auto; }
}

@media (min-width: 900px) {
	.ep-hero__in { padding-top: var(--ep-s-14); padding-bottom: var(--ep-s-12); }
	.ep-hero__title { font-size: var(--ep-t-4xl); }
	.ep-hero__sub { font-size: var(--ep-t-lg); }
	.ep-hero__brand--tiva svg { width: 112px; }
	.ep-hero__brand--toro svg { width: 154px; }
	.ep-hero__brands { gap: var(--ep-s-6); margin-bottom: var(--ep-s-8); }
	.ep-hero__facts { font-size: var(--ep-t-sm); gap: var(--ep-s-3) var(--ep-s-8); }
	.ep-hero .ep-social__h { font-size: var(--ep-t-base); }

	.ep-sec { padding: var(--ep-s-10) 0; }
	.ep-sec__h h2 { font-size: var(--ep-t-2xl); }
	.ep-sec__h p { font-size: var(--ep-t-base); }

	.ep-brands { grid-template-columns: 1.25fr 1fr 1fr; gap: var(--ep-s-4); }
	.ep-trust__c b { font-size: var(--ep-t-base); }
	.ep-trust__c span { font-size: var(--ep-t-sm); }
	.ep-lab__r { padding: var(--ep-s-4) var(--ep-s-6); font-size: var(--ep-t-base); }
}


/* ---------------------------------------------------------------------------
   FEATURED DEAL BANNER

   A BRAND MOMENT, so it sits on the dark ground with the same violet bloom as
   the hero. That is deliberate: the light zone is for browsing, and the two
   dark bands on the page are the two moments that are selling rather than
   listing. It also breaks up four near-identical product rows at the point
   attention sags.

   The product image is NOT on a white card here. Everywhere else the cut-out
   shots need white behind them, but against the dark band a soft violet halo
   behind the product reads as a spotlight rather than a mistake, and the
   packaging is bright enough to hold its own.
   --------------------------------------------------------------------------- */
.ep-deal {
	position: relative;
	overflow: hidden;
	background: var(--ep-ground);
	color: var(--ep-white);
}
.ep-deal::before {
	content: "";
	position: absolute;
	inset: -40% -20% auto auto;
	width: 720px; height: 720px;
	background:
		radial-gradient(40% 40% at 62% 42%, rgba(168,85,247,0.30) 0%, rgba(168,85,247,0) 100%),
		radial-gradient(34% 34% at 30% 70%, rgba(109,40,217,0.22) 0%, rgba(109,40,217,0) 100%);
	pointer-events: none;
}

.ep-deal__in {
	position: relative;
	z-index: 2;
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--ep-s-6);
	padding-top: var(--ep-s-8);
	padding-bottom: var(--ep-s-8);
	align-items: center;
}

.ep-deal__kick { display: flex; align-items: center; gap: var(--ep-s-3); flex-wrap: wrap; margin: 0 0 var(--ep-s-4); }
.ep-deal__flag {
	display: inline-flex;
	align-items: center;
	padding: 5px var(--ep-s-3);
	border-radius: var(--ep-r-xs);
	background: var(--ep-vio);
	color: var(--ep-vio-ink);
	font-size: var(--ep-t-xs);
	font-weight: var(--ep-w-black);
	letter-spacing: var(--ep-track-tight);
}
.ep-deal__brand { font-size: var(--ep-t-sm); font-weight: var(--ep-w-bold); color: var(--ep-vio-2); }

.ep-deal__title em { font-style: normal; color: var(--ep-vio); }
.ep-deal__title {
	font-size: var(--ep-t-2xl);
	font-weight: var(--ep-w-black);
	letter-spacing: var(--ep-track-head);
	line-height: 1.04;
	margin: 0 0 var(--ep-s-3);
	color: var(--ep-white);
	text-wrap: balance;
}
.ep-deal__desc { font-size: var(--ep-t-sm); color: var(--ep-mut); margin-bottom: var(--ep-s-5); max-width: 52ch; }
.ep-deal__desc p { margin: 0; }

/* Ticks drawn with a mask so they take the accent and need no image. */
.ep-deal__facts {
	list-style: none;
	margin: 0 0 var(--ep-s-5);
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	gap: var(--ep-s-2) var(--ep-s-5);
}
.ep-deal__facts li {
	display: flex;
	align-items: center;
	gap: var(--ep-s-2);
	font-size: var(--ep-t-sm);
	color: var(--ep-white);
}
.ep-deal__facts li::before {
	content: "";
	display: block;
	width: 16px; height: 16px;
	flex: none;
	background-color: var(--ep-ok);
	-webkit-mask: var(--ep-icon-tick) center / 16px 16px no-repeat;
	mask: var(--ep-icon-tick) center / 16px 16px no-repeat;
}

.ep-deal__buy { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--ep-s-3); margin-bottom: var(--ep-s-5); }
.ep-deal__now { font-size: var(--ep-t-2xl); font-weight: var(--ep-w-black); letter-spacing: -0.04em; color: var(--ep-white); }
.ep-deal__was { font-size: var(--ep-t-md); color: var(--ep-mut); text-decoration: line-through; }
.ep-deal__save {
	display: inline-flex; align-items: center; gap: 6px;
	font-size: var(--ep-t-xs);
	font-weight: var(--ep-w-bold);
	color: var(--ep-vio-2);
	background: rgba(168,85,247,0.14);
	border: 1px solid rgba(168,85,247,0.35);
	padding: 4px var(--ep-s-2);
	border-radius: var(--ep-r-xs);
	white-space: nowrap;
}
.ep-deal__save i { font-style: normal; font-weight: var(--ep-w-semi); padding-left: 6px; border-left: 1px solid rgba(168,85,247,0.35); opacity: 0.8; }

.ep-deal__cta { display: flex; gap: var(--ep-s-3); flex-wrap: wrap; }

/* THE ARTWORK IS CUT OUT, so it needs no plate and no card. The earlier
   version used the product's own studio photograph, a vial on white, and a
   white rectangle on a dark band reads as a hole punched in the page.

   THE CROP IS THE IMPORTANT PART. Lander-Images is a 1:1 canvas holding a WIDE
   composition: box, pen and caps across the middle, with roughly a quarter of
   the height empty alpha above and the same below. Rendered at its own aspect
   ratio on a phone that produced a banner over 1,500px tall, most of it
   nothing.

   The container is therefore given a wide aspect ratio and the image covers it,
   which crops the empty alpha away instead of scaling the product down. The
   product ends up LARGER on a phone than it was before, in a third of the
   height. object-position sits slightly above centre because the composition's
   visual mass is the box, which sits high.
   --------------------------------------------------------------------------- */
.ep-deal__pic {
	position: relative;
	display: block;
	width: 100%;
	aspect-ratio: 16 / 9;
	min-height: 0;
}
.ep-deal__pic::before {
	content: "";
	position: absolute;
	left: 50%;
	top: 52%;
	transform: translate(-50%, -50%);
	width: 92%; padding-bottom: 40%;
	border-radius: 50%;
	background: radial-gradient(closest-side, rgba(168,85,247,0.40) 0%, rgba(168,85,247,0.12) 55%, rgba(168,85,247,0) 100%);
	filter: blur(10px);
}
.ep-deal__pic img {
	position: relative;
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: center 46%;
	display: block;
	filter: drop-shadow(0 18px 26px rgba(0,0,0,0.5));
}

/* The description earns less room on a phone than the price and the button do. */
.ep-deal__desc {
	display: -webkit-box;
	-webkit-line-clamp: 3;
	-webkit-box-orient: vertical;
	overflow: hidden;
}

@media (min-width: 900px) {
	.ep-deal__in {
		grid-template-columns: 1.15fr 0.85fr;
		gap: var(--ep-s-12);
		padding-top: var(--ep-s-14);
		padding-bottom: var(--ep-s-14);
	}
	.ep-deal__title { font-size: var(--ep-t-3xl); }
	.ep-deal__desc { font-size: var(--ep-t-base); }
	.ep-deal__now { font-size: var(--ep-t-3xl); }
	/* Desktop has the width for the whole composition, so it goes back to its
	   own proportions and stops cropping. */
	.ep-deal__pic { aspect-ratio: 4 / 3; }
	.ep-deal__pic img { object-fit: contain; }
	.ep-deal__pic::before { top: 50%; padding-bottom: 46%; }
	.ep-deal__desc { -webkit-line-clamp: none; display: block; overflow: visible; }
}


/* ---------------------------------------------------------------------------
   CUSTOMER REVIEWS

   A RAIL, reusing the pattern the categories already use on phones, swiping
   sideways through twenty cards costs one row of height, where a grid of
   twenty would run for pages.

   STARS ARE VIOLET, not green and not gold. Green is spoken for in this
   system (stock, and purity that passed), and green five-square stars are
   Trustpilot's mark, borrowing it on a store that has no Trustpilot account
   would be claiming something untrue. Gold would collide with the amber used
   for out-of-stock. Violet is the accent this storefront already owns.

   The card keeps a left accent rule rather than the previous design's heavy
   black border with an orange underline: quieter, and it lets twenty cards sit
   next to each other without the page looking like a fence.
   --------------------------------------------------------------------------- */
.ep-revs__h { margin-bottom: var(--ep-s-6); }
.ep-revs__eyebrow {
	display: block;
	font-size: var(--ep-t-xs);
	font-weight: var(--ep-w-bold);
	color: var(--ep-vio-deep);
	margin-bottom: var(--ep-s-2);
}
.ep-revs__h h2 {
	margin: 0;
	font-size: var(--ep-t-xl);
	font-weight: var(--ep-w-black);
	letter-spacing: var(--ep-track-head);
	line-height: 1.08;
	color: var(--ep-lt-ink);
	max-width: 20ch;
	text-wrap: balance;
}

.ep-revs__rail {
	display: flex;
	gap: var(--ep-s-3);
	overflow-x: auto;
	scroll-snap-type: x mandatory;
	-webkit-overflow-scrolling: touch;
	scrollbar-width: none;
	margin-inline: calc(var(--ep-gutter-now) * -1);
	padding-inline: var(--ep-gutter-now);
	padding-bottom: var(--ep-s-2);
	/* SNAPPING IGNORES PADDING WITHOUT THIS, which is why the first card sat
	   flush against the edge of the screen while the heading above it was
	   inset. scroll-snap-align:start snaps a card to the SCROLLPORT's start,
	   and the scrollport's start is inside the padding unless scroll-padding
	   moves it. The rail was landing at scrollLeft:20 on load, eating the exact
	   width of its own gutter. */
	scroll-padding-inline: var(--ep-gutter-now);
}
.ep-revs__rail::-webkit-scrollbar { display: none; }

.ep-rev {
	flex: 0 0 268px;
	scroll-snap-align: start;
	display: flex;
	flex-direction: column;
	gap: var(--ep-s-2);
	margin: 0;
	padding: var(--ep-s-4);
	background: var(--ep-lt-card);
	border: 1px solid var(--ep-lt-line);
	border-left: 3px solid var(--ep-vio-deep);
	border-radius: var(--ep-r-md);
}

.ep-rev__stars { display: flex; gap: 3px; }
.ep-rev__stars i {
	width: 15px; height: 15px;
	display: block;
	background-color: var(--ep-vio-deep);
	-webkit-mask: var(--ep-icon-star) center / 15px 15px no-repeat;
	mask: var(--ep-icon-star) center / 15px 15px no-repeat;
}

.ep-rev__title {
	font-size: var(--ep-t-base);
	font-weight: var(--ep-w-black);
	letter-spacing: var(--ep-track-tight);
	line-height: 1.2;
	color: var(--ep-lt-ink);
}
.ep-rev__text {
	margin: 0;
	font-size: var(--ep-t-sm);
	line-height: 1.5;
	color: var(--ep-lt-ink-2);
	flex: 1;
}
.ep-rev__meta {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--ep-s-2);
	padding-top: var(--ep-s-3);
	border-top: 1px solid var(--ep-lt-line-2);
	font-size: var(--ep-t-xs);
}
.ep-rev__who { font-weight: var(--ep-w-bold); color: var(--ep-lt-ink); }
.ep-rev__ok {
	display: inline-flex;
	align-items: center;
	gap: 5px;
	color: var(--ep-lt-ok);
	font-weight: var(--ep-w-semi);
}
/* The one place green is right: this is a state, not decoration. */
.ep-rev__ok::before {
	content: "";
	width: 13px; height: 13px;
	display: block;
	background-color: var(--ep-lt-ok);
	-webkit-mask: var(--ep-icon-tick) center / 13px 13px no-repeat;
	mask: var(--ep-icon-tick) center / 13px 13px no-repeat;
}

@media (min-width: 700px) {
	.ep-revs__h h2 { font-size: var(--ep-t-2xl); max-width: 26ch; }
	.ep-revs__rail { gap: var(--ep-s-4); }
	.ep-rev { flex: 0 0 320px; padding: var(--ep-s-5); }
}


/* ---------------------------------------------------------------------------
   REVEAL ON SCROLL

   The hidden state is gated on .ep-reveal--ready, which reveal.js puts on <html>
   before it does anything else. Without that gate, a visitor whose JavaScript
   is blocked or still loading would get a permanently invisible section,
   hiding content and hoping a script un-hides it is how pages end up blank.

   Reduced motion still reveals; it just does not travel.
   --------------------------------------------------------------------------- */
.ep-reveal--ready .ep-reveal > * {
	opacity: 0;
	transform: translateY(14px);
}
.ep-reveal--ready .ep-reveal.is-in > * {
	opacity: 1;
	transform: none;
	transition: opacity 620ms var(--ep-ease), transform 620ms var(--ep-ease);
}

/* STAGGER. Revealing a whole band at once is one event; revealing its heading
   and then its items in sequence reads as the page assembling itself, which is
   the difference between "animated" and "alive". Every section that holds a
   list carries --stagger.

   On those sections the WRAPPER is released immediately and the items animate
   instead, otherwise the band would fade in and then its contents would fade
   in again inside it, which reads as a slow page rather than a designed one. */
.ep-reveal--ready .ep-reveal--stagger > * { opacity: 1; transform: none; transition: none; }

.ep-reveal--ready .ep-reveal--stagger .ep-sec__h,
.ep-reveal--ready .ep-reveal--stagger .ep-brandsec__h,
.ep-reveal--ready .ep-reveal--stagger .ep-revs__h,
.ep-reveal--ready .ep-reveal--stagger .ep-cards > *,
.ep-reveal--ready .ep-reveal--stagger .ep-cats > *,
.ep-reveal--ready .ep-reveal--stagger .ep-revs__rail > * {
	opacity: 0;
	transform: translateY(18px);
}
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-sec__h,
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-brandsec__h,
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-revs__h,
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-cards > *,
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-cats > *,
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-revs__rail > * {
	opacity: 1;
	transform: none;
	transition: opacity 560ms var(--ep-ease), transform 560ms var(--ep-ease);
}

/* The cascade. Capped at eight because no row on this page holds more, and an
   uncapped nth-child list would keep adding delay until the last item arrived
   long after the visitor had scrolled past it. Anything beyond eight simply
   uses the last delay. */
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-cards > *:nth-child(1),
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-cats > *:nth-child(1),
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-revs__rail > *:nth-child(1) { transition-delay: 90ms; }
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-cards > *:nth-child(2),
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-cats > *:nth-child(2),
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-revs__rail > *:nth-child(2) { transition-delay: 160ms; }
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-cards > *:nth-child(3),
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-cats > *:nth-child(3),
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-revs__rail > *:nth-child(3) { transition-delay: 230ms; }
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-cards > *:nth-child(4),
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-cats > *:nth-child(4),
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-revs__rail > *:nth-child(4) { transition-delay: 300ms; }
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-cards > *:nth-child(n+5),
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-cats > *:nth-child(n+5),
.ep-reveal--ready .ep-reveal--stagger.is-in .ep-revs__rail > *:nth-child(n+5) { transition-delay: 370ms; }

/* Motion under the finger, and under the pointer. The press state is the one
   that matters here: this store is almost entirely phones, where hover never
   happens, and a card that does not acknowledge a tap feels broken. */
.ep-card {
	transition: transform 220ms var(--ep-ease), box-shadow 220ms var(--ep-ease), border-color 220ms var(--ep-ease);
}
.ep-card:active { transform: scale(0.985); }
@media (hover: hover) {
	.ep-card:hover {
		transform: translateY(-4px);
		box-shadow: 0 16px 34px rgba(10, 4, 20, 0.16);
	}
	.ep-card:hover .ep-card__shot img { transform: scale(1.045); }
}
.ep-card__shot img { transition: transform 420ms var(--ep-ease); }

@media (prefers-reduced-motion: reduce) {
	.ep-reveal--ready .ep-reveal--stagger .ep-sec__h,
	.ep-reveal--ready .ep-reveal--stagger .ep-brandsec__h,
	.ep-reveal--ready .ep-reveal--stagger .ep-revs__h,
	.ep-reveal--ready .ep-reveal--stagger .ep-cards > *,
	.ep-reveal--ready .ep-reveal--stagger .ep-cats > *,
	.ep-reveal--ready .ep-reveal--stagger .ep-revs__rail > * { opacity: 1; transform: none; }
	.ep-card, .ep-card__shot img { transition: none; }
}
@media (prefers-reduced-motion: reduce) {
	.ep-reveal--ready .ep-reveal > * { opacity: 1; transform: none; }
}

/* ---------------------------------------------------------------------------
   THE TIVA INTRODUCTION

   A LIGHT band, and that is forced rather than chosen: the three product
   photographs are shot on WHITE and are not cut out. On the dark ground used by
   the hero and the featured banner they would sit as three white rectangles,
   exactly the fault the featured banner was rebuilt to remove. On white they
   have no visible edge at all, which is why there is no card, no border and no
   radius on them.

   Compact by construction. The brief was that it must not eat scroll: the mark,
   the claim, the three shots, the coming-soon line and both buttons fit inside
   roughly one phone screen.
   --------------------------------------------------------------------------- */
.ep-tiva {
	position: relative;
	isolation: isolate;
	background:
		radial-gradient(120% 80% at 50% 0%, var(--ep-lt-tint) 0%, var(--ep-lt-card) 60%);
	border-top: 1px solid var(--ep-lt-line);
	border-bottom: 1px solid var(--ep-lt-line);
	color: var(--ep-lt-ink);
	overflow: hidden;
}
/* The canvas sits behind the content and is blurred in CSS rather than in the
   render loop: a blur filter on a composited layer costs the GPU almost
   nothing, where blurring in canvas would mean redrawing at a larger size and
   sampling every frame. isolation on the section keeps the blend from reaching
   anything outside it. */
.ep-tiva__bg {
	position: absolute;
	inset: -6%;
	z-index: 0;
	width: 112%;
	height: 112%;
	display: block;
	pointer-events: none;
	filter: blur(26px) saturate(118%);
	opacity: 1;
	/* MASKED AWAY FROM THE MIDDLE, and this is the fix for the product shots.
	
	   Those photographs have solid white backgrounds. Over a plain near-white
	   section they blend into it; over a coloured wash they stop being
	   photographs and start being white rectangles sitting on top of one. The
	   answer is not to weaken the field but to keep it out of the column where
	   the content lives: colour stays in the corners and along the edges, and
	   fades to nothing through the middle. The text gets a cleaner ground out
	   of the same change. */
	-webkit-mask-image:
		radial-gradient(115% 78% at 50% 46%, transparent 34%, rgba(0,0,0,0.55) 62%, #000 88%);
	mask-image:
		radial-gradient(115% 78% at 50% 46%, transparent 34%, rgba(0,0,0,0.55) 62%, #000 88%);
}
.ep-tiva__in { position: relative; z-index: 1; }
@media (max-width: 700px) {
	/* Less blur on a phone: the field is smaller, so the same radius turns it
	   into a flat wash with no shape left in it. */
	/* A phone is one narrow column, so the clear middle has to be narrower and
	   taller or there is no colour left anywhere. */
	.ep-tiva__bg {
		filter: blur(18px) saturate(118%);
		opacity: 0.95;
		-webkit-mask-image:
			radial-gradient(80% 62% at 50% 46%, transparent 30%, rgba(0,0,0,0.5) 60%, #000 86%);
		mask-image:
			radial-gradient(80% 62% at 50% 46%, transparent 30%, rgba(0,0,0,0.5) 60%, #000 86%);
	}
}
.ep-tiva__in {
	padding-top: var(--ep-s-10);
	padding-bottom: var(--ep-s-10);
	text-align: center;
}

/* --- the mark --- */
.ep-tiva__mark {
	display: flex;
	justify-content: center;
	margin-bottom: var(--ep-s-4);
}
.ep-tiva__mark svg {
	width: 104px;
	height: auto;
	display: block;
}

/* The mark arrives on its own timing, slightly after its section, so it reads
   as the thing being introduced rather than as part of the furniture. */
.ep-reveal--ready .ep-tiva .ep-tiva__mark svg {
	opacity: 0;
	transform: translateY(10px) scale(0.94);
}
.ep-reveal--ready .ep-tiva.is-in .ep-tiva__mark svg {
	opacity: 1;
	transform: none;
	transition: opacity 700ms var(--ep-ease) 120ms, transform 700ms var(--ep-ease) 120ms;
}
@media (prefers-reduced-motion: reduce) {
	.ep-reveal--ready .ep-tiva .ep-tiva__mark svg { opacity: 1; transform: none; }
}

/* The mark's own motion, inside the SVG. Tiva's character is verification
   rather than force, so where the Toro lockup charges in, this one settles and
   is then read by a light sweep, which is the gesture its own scan product
   makes. Same clip-and-sweep mechanism as Toro, in white rather than gold.
   Scoped to .ep-tiva so the hero copy of this same file stays still: the hero
   animates its lockups as whole blocks instead. */
@keyframes epTivaFade {
	0%   { opacity: 0; }
	100% { opacity: 1; }
}
@keyframes epTivaShine {
	0%   { transform: translateX(-90px); }
	22%  { transform: translateX(320px); }
	100% { transform: translateX(320px); }
}
/* THE MARK'S TRANSFORM IS LOAD BEARING AND MUST NOT BE ANIMATED.
   <g id="epTiva-art"> carries transform="translate(0,218.2)" as a presentation
   attribute: the artwork is drawn at negative y and that translate is what
   puts it inside the viewBox. A CSS transform OVERRIDES the attribute, so an
   animation ending on "transform: none" cancelled the positioning and threw
   the whole mark off canvas. The <svg> box kept its size, so the section
   rendered as a tall empty band with the logo nowhere in it.

   The travel is therefore done one level up, on the <svg> itself, which has no
   attribute to clobber. Only opacity is touched here.

   Toro is not affected: those groups were authored without transforms. */
.ep-tiva .epTiva-shine { transform: translateX(-90px); }
.ep-reveal--ready .ep-tiva .epTiva-mark { opacity: 0; }
.ep-reveal--ready .ep-tiva.is-in .epTiva-mark {
	animation: epTivaFade 700ms var(--ep-ease) 160ms both;
}
.ep-reveal--ready .ep-tiva.is-in .epTiva-shine {
	animation: epTivaShine 5.4s linear 1100ms infinite;
}
@media (prefers-reduced-motion: reduce) {
	.ep-reveal--ready .ep-tiva .epTiva-mark { opacity: 1; animation: none; }
	.ep-reveal--ready .ep-tiva.is-in .epTiva-shine { animation: none; }
	.ep-tiva .epTiva-shine { opacity: 0; }
}

.ep-tiva__eyebrow {
	margin: 0 0 var(--ep-s-2);
	font-size: var(--ep-t-xs);
	font-weight: var(--ep-w-bold);
	color: var(--ep-vio-deep);
}
.ep-tiva__h {
	margin: 0 auto var(--ep-s-3);
	max-width: 18ch;
	font-size: var(--ep-t-xl);
	font-weight: var(--ep-w-black);
	letter-spacing: var(--ep-track-head);
	line-height: 1.06;
	color: var(--ep-lt-ink);
	text-wrap: balance;
}
.ep-tiva__h em { font-style: normal; color: var(--ep-vio-deep); }
.ep-tiva__lead {
	margin: 0 auto var(--ep-s-6);
	max-width: 54ch;
	font-size: var(--ep-t-sm);
	color: var(--ep-lt-mut);
}

/* --- the three shots ---
   No card, no border, no radius: the photographs are already on white and the
   band is white, so any frame would draw a box around nothing. */
.ep-tiva__shots {
	list-style: none;
	margin: 0 0 var(--ep-s-5);
	padding: 0;
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: var(--ep-s-2);
	align-items: center;
}
.ep-tiva__shots li { margin: 0; min-width: 0; }
.ep-tiva__shots img {
	width: 100%;
	height: auto;
	display: block;
	mix-blend-mode: multiply;
}

.ep-tiva__soon {
	display: inline-flex;
	align-items: center;
	gap: var(--ep-s-2);
	margin: 0 0 var(--ep-s-5);
	padding: 6px var(--ep-s-3);
	border: 1px dashed var(--ep-lt-tint-e);
	border-radius: var(--ep-r-full);
	font-size: var(--ep-t-xs);
	color: var(--ep-lt-ink-2);
}
.ep-tiva__soon span {
	font-weight: var(--ep-w-bold);
	color: var(--ep-vio-deep);
}

.ep-tiva__cta { display: flex; gap: var(--ep-s-3); justify-content: center; flex-wrap: wrap; }
.ep-btn--ghost {
	background: var(--ep-lt-card);
	color: var(--ep-lt-ink) !important;
	border: 1px solid var(--ep-lt-line);
	gap: var(--ep-s-2);
}
.ep-btn--ghost:hover { border-color: var(--ep-vio-deep); color: var(--ep-vio-deep) !important; }

@media (min-width: 700px) {
	.ep-tiva__in { padding-top: var(--ep-s-12); padding-bottom: var(--ep-s-12); }
	.ep-tiva__mark svg { width: 128px; }
	.ep-tiva__h { font-size: var(--ep-t-2xl); max-width: 22ch; }
	.ep-tiva__lead { font-size: var(--ep-t-base); }
	.ep-tiva__shots { gap: var(--ep-s-5); max-width: 780px; margin-inline: auto; }
}


/* ---------------------------------------------------------------------------
   THE REVIEW RIBBON

   A LIGHT band between the chrome and the hero. The header, the category row
   and the hero were three dark bands stacked with nothing between them, so the
   top of the page read as one wall and the hero had no edge to start at.

   THE LOOP: the track is printed twice and travels exactly -50% of its own
   width. At the end of the cycle the second copy sits exactly where the first
   started, so the reset is invisible. Any other distance snaps.

   Width is max-content, not a percentage, so the track measures its real
   content and the 50% is meaningful.
   --------------------------------------------------------------------------- */
.ep-rmq {
	background: var(--ep-lt-bg);
	border-bottom: 1px solid var(--ep-lt-line);
	color: var(--ep-lt-ink);
}
.ep-rmq__viewport {
	overflow: hidden;
	/* Fades the ribbon out at both ends instead of cutting a quote in half at
	   the screen edge. */
	-webkit-mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
	mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
}
.ep-rmq__track {
	display: flex;
	align-items: center;
	width: max-content;
	gap: var(--ep-s-6);
	padding: var(--ep-s-3) 0;
	animation: epRmq 64s linear infinite;
	will-change: transform;
}
@keyframes epRmq {
	from { transform: translateX(0); }
	to   { transform: translateX(-50%); }
}
.ep-rmq:hover .ep-rmq__track { animation-play-state: paused; }

.ep-rmq__i {
	display: inline-flex;
	align-items: center;
	gap: var(--ep-s-2);
	flex: none;
	font-size: var(--ep-t-xs);
	white-space: nowrap;
}
.ep-rmq__s { display: inline-flex; gap: 1px; color: #E0AC42; }
.ep-rmq__q { font-weight: var(--ep-w-bold); color: var(--ep-lt-ink); }
.ep-rmq__a { color: var(--ep-lt-mut); }
.ep-rmq__v {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 15px;
	height: 15px;
	border-radius: 50%;
	background: var(--ep-ok, #22A06B);
	color: #fff;
	flex: none;
}

@media (prefers-reduced-motion: reduce) {
	/* Still readable, just not moving: the viewport becomes a normal scroller
	   and the duplicate half is harmless. */
	.ep-rmq__track { animation: none; }
	.ep-rmq__viewport { overflow-x: auto; }
}

/* ---------------------------------------------------------------------------
   THE TORO LABS INTRODUCTION

   A DARK band, and unlike the Tiva one that is a brand decision rather than a
   forced one: Toro Labs packaging is black and gold, so a light band would
   fight the product it is introducing. The ground is the site's deep violet
   rather than pure black, so the section still belongs to this storefront
   instead of reading as a pasted in advert.

   THE SHOTS SIT ON PLINTHS. They are photographed on white and are not cut
   out, so on this ground they would be four white rectangles. The Tiva section
   dissolves that white with mix-blend-mode: multiply against a white band,
   which cannot work here: multiply against a dark ground crushes the whole
   image to black. So each shot gets a deliberate lit tile instead, and the
   multiply happens against THAT. The white then reads as a surface the product
   is standing on. Cutting the shots out was tried and rejected: a corner flood
   fill leaves the drop shadow behind as a grey blob, worst on the oil box, and
   the near white Winstrol bottle is too close in value to its own background
   to key safely.
   --------------------------------------------------------------------------- */
/* ---------------------------------------------------------------------------
   THE TORO GROUND

   It was a violet radial, which put it in the same key as every other dark
   band on the page. Toro's packaging is black and gold and the name is a bull,
   so the ground is now near black with heat coming in low from one side rather
   than a symmetrical glow behind the middle. Asymmetry is most of the work: a
   centred wash reads as calm no matter what colour it is.

   ON WHAT THIS DELIBERATELY IS NOT. The brief mentioned lightning, and there is
   no repeating flash here. A background that strobes on a shop's homepage is a
   photosensitivity risk, it is the first thing to grate on the fifth visit, and
   it makes the section harder to read for as long as it lasts. The charge is
   delivered ONCE, as a sweep when the section arrives, and the rest is carried
   by contrast, temperature and texture, which do not wear out.
   --------------------------------------------------------------------------- */
.ep-toro {
	position: relative;
	isolation: isolate;
	background:
		radial-gradient(90% 70% at 18% 0%, rgba(236, 186, 76, 0.34) 0%, rgba(236, 186, 76, 0) 64%),
		radial-gradient(70% 60% at 88% 18%, rgba(146, 82, 216, 0.30) 0%, rgba(146, 82, 216, 0) 66%),
		radial-gradient(130% 105% at 50% 118%, #281653 0%, #100823 74%);
	border-top: 1px solid var(--ep-edge);
	border-bottom: 1px solid var(--ep-edge);
	color: var(--ep-white);
	overflow: hidden;
}
/* Forged texture: a fine diagonal hatch, almost subliminal. It stops the ground
   reading as a flat fill without putting anything on it you would notice. */
.ep-toro::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 0;
	pointer-events: none;
	background-image: repeating-linear-gradient(
		122deg,
		rgba(255, 255, 255, 0.028) 0px,
		rgba(255, 255, 255, 0.028) 1px,
		transparent 1px,
		transparent 7px
	);
	opacity: 0.6;
}
.ep-toro__in { position: relative; z-index: 1; }

/* THE CHARGE. One sweep of hot light across the band as it arrives, then gone.
   Skewed, so it crosses like a rake of light rather than a wipe. */
.ep-reveal--ready .ep-toro .ep-toro__arc {
	position: absolute;
	top: -20%;
	bottom: -20%;
	left: -35%;
	width: 26%;
	z-index: 0;
	pointer-events: none;
	opacity: 0;
	transform: skewX(-14deg);
	background: linear-gradient(
		90deg,
		rgba(255, 214, 130, 0) 0%,
		rgba(255, 214, 130, 0.10) 42%,
		rgba(255, 240, 205, 0.30) 50%,
		rgba(255, 214, 130, 0.10) 58%,
		rgba(255, 214, 130, 0) 100%
	);
}
.ep-reveal--ready .ep-toro.is-in .ep-toro__arc {
	animation: epToroArc 1500ms var(--ep-ease) 260ms 1;
}
@keyframes epToroArc {
	0%   { opacity: 0; left: -35%; }
	18%  { opacity: 1; }
	82%  { opacity: 1; }
	100% { opacity: 0; left: 108%; }
}
.ep-toro__in {
	padding-top: var(--ep-s-10);
	padding-bottom: var(--ep-s-10);
	text-align: center;
}

/* --- the mark --- */
.ep-toro__mark {
	display: flex;
	justify-content: center;
	margin-bottom: var(--ep-s-4);
}
.ep-toro__mark svg {
	width: 172px;
	height: auto;
	display: block;
	overflow: visible;
}

/* The charge. The brief was that it should pop in the way a taurus would, so
   the emblem does not fade: it comes in low and left, overshoots, and settles
   with one small counter kick. The word follows a beat later so the two read
   as one movement rather than a pair of independent fades. */
@keyframes epToroCharge {
	0%   { opacity: 0; transform: translateX(-24%) scale(0.68) rotate(-8deg); }
	52%  { opacity: 1; transform: translateX(3%)   scale(1.07) rotate(2deg); }
	70%  { transform: translateX(-1.5%) scale(0.97) rotate(-1deg); }
	86%  { transform: translateX(0.5%)  scale(1.015) rotate(0.4deg); }
	100% { opacity: 1; transform: none; }
}
@keyframes epToroWord {
	0%   { opacity: 0; transform: translateX(-14px); }
	100% { opacity: 1; transform: none; }
}
/* The shine rests off the left edge, crosses once, then waits. The long tail
   of the keyframe IS the pause: a shine that swept continuously would pull the
   eye off the copy it is meant to introduce. */
@keyframes epToroShine {
	0%   { transform: translateX(-46px); }
	24%  { transform: translateX(156px); }
	100% { transform: translateX(156px); }
}

.epToro-mark, .epToro-word { transform-box: fill-box; transform-origin: center; }
.epToro-shine { transform: translateX(-46px); }

/* ---------------------------------------------------------------------------
   THE LOCKUP DRAWS ITSELF

   The mark and the word are eleven filled paths, so there is no outline to
   animate: the trick is to stroke them, run the stroke on, and bring the gold
   fill up behind it once the shape exists. It reads as the logo being cut
   rather than faded in, which is the right verb for this brand.

   Every path carries pathLength="1" in the file, so one dasharray works for all
   eleven whatever their real geometry. Guessing a dash length per path is how
   this normally goes wrong: too short and it never completes, too long and the
   first half of the animation is nothing happening.
   --------------------------------------------------------------------------- */
/* THE RESTING STATE IS THE FINISHED LOGO, and that is load bearing rather than
   tidy. The undrawn state used to live in a plain rule here, which meant an
   unfilled path with an undrawn stroke was the DEFAULT: anything that stopped
   the animation arriving, a dropped is-in, an observer that never fires, a
   browser that declines the animation, left the Toro lockup invisible on a
   live shop. The start state belongs in the keyframes with fill-mode both, so
   the animation reaches down and hides the logo only while it is actually
   running to reveal it. No animation now means a normal gold logo. */
.ep-reveal--ready .ep-toro.is-in #epToro-art path {
	animation:
		epToroDraw 900ms cubic-bezier(0.65, 0, 0.35, 1) both,
		epToroFill 620ms var(--ep-ease) 620ms both;
}
/* The mark leads, the word follows it on. */
.ep-reveal--ready .ep-toro.is-in .epToro-mark path { animation-delay: 120ms, 700ms; }
.ep-reveal--ready .ep-toro.is-in .epToro-word path { animation-delay: 360ms, 940ms; }
@keyframes epToroDraw {
	from {
		stroke: rgba(246, 223, 164, 0.9);
		stroke-width: 0.6;
		stroke-dasharray: 1;
		stroke-dashoffset: 1;
	}
	to {
		stroke: rgba(246, 223, 164, 0.9);
		stroke-width: 0.6;
		stroke-dasharray: 1;
		stroke-dashoffset: 0;
	}
}
@keyframes epToroFill {
	from { fill-opacity: 0; stroke-opacity: 1; }
	to   { fill-opacity: 1; stroke-opacity: 0; }
}

/* The groups stay visible. The old fade-and-scale entrance ran across the same
   900ms as the stroke, so the cut happened behind a fade and read as mush;
   two entrances on one object is one too many. The draw carries it. */
.ep-reveal--ready .ep-toro .epToro-mark,
.ep-reveal--ready .ep-toro .epToro-word { opacity: 1; }

.ep-reveal--ready .ep-toro.is-in .epToro-mark {
	animation: none;
}
.ep-reveal--ready .ep-toro.is-in .epToro-word {
	animation: none;
}
.ep-reveal--ready .ep-toro.is-in .epToro-shine {
	animation: epToroShine 5s linear 1800ms infinite;
}

/* Motion off: the mark must still be visible, and the shine must not sweep. */
@media (prefers-reduced-motion: reduce) {
	.ep-reveal--ready .ep-toro .epToro-mark,
	.ep-reveal--ready .ep-toro .epToro-word { opacity: 1; }
	/* No draw, no sweep: the lockup is simply there, filled, and the band is
	   still. */
	/* Nothing to undo any more: the resting state IS the finished logo, so
	   cancelling the animation is the whole fix. */
	.ep-reveal--ready .ep-toro.is-in #epToro-art path { animation: none; }
	.ep-reveal--ready .ep-toro.is-in .ep-toro__arc { animation: none; opacity: 0; }
	/* One still wisp per tile, so the tile keeps some depth without anything
	   moving. */
	.ep-reveal--ready .ep-toro.is-in .ep-toro__smoke i { animation-name: none; opacity: 0; }
	.ep-reveal--ready .ep-toro.is-in .ep-toro__smoke i:nth-child(1) { opacity: 0.3; }
	/* Dust holds still at a low opacity: a static constellation still reads
	   as motes in the air, where a frozen sweep read as a fault. */
	.ep-reveal--ready .ep-toro.is-in .ep-toro__dust::before { animation-name: none; opacity: 0.5; }
	.ep-reveal--ready .ep-toro.is-in .ep-toro__dust::after { animation-name: none; opacity: 0; }
	.ep-reveal--ready .ep-toro.is-in .epToro-mark,
	.ep-reveal--ready .ep-toro.is-in .epToro-word,
	.ep-reveal--ready .ep-toro.is-in .epToro-shine { animation: none; }
	.epToro-shine { opacity: 0; }
}

.ep-toro__eyebrow {
	margin: 0 0 var(--ep-s-2);
	font-size: var(--ep-t-xs);
	font-weight: var(--ep-w-bold);
	color: #E0AC42;
}
.ep-toro__h {
	margin: 0 auto var(--ep-s-3);
	max-width: 18ch;
	font-size: var(--ep-t-xl);
	font-weight: var(--ep-w-black);
	letter-spacing: var(--ep-track-head);
	line-height: 1.06;
	color: var(--ep-white);
	text-wrap: balance;
}
.ep-toro__h em {
	font-style: normal;
	background: linear-gradient(92deg, #F6DFA4, #E0AC42 45%, #F3D68F);
	-webkit-background-clip: text;
	background-clip: text;
	color: transparent;
}
.ep-toro__lead {
	margin: 0 auto var(--ep-s-6);
	max-width: 56ch;
	font-size: var(--ep-t-sm);
	color: var(--ep-mut);
}

/* --- the four shots, each on its own lit plinth --- */
.ep-toro__shots {
	list-style: none;
	margin: 0 0 var(--ep-s-5);
	padding: 0;
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	gap: var(--ep-s-3);
}
.ep-toro__shots li { margin: 0; min-width: 0; }

/* HEXAGONAL PLINTHS, echoing the emblem in the lockup above.
   Two nested clipped elements, not one: clip-path cuts borders off, so the
   gold rim has to be a second shape sitting behind the first and showing
   through a 2px inset. */
.ep-toro__hex {
	display: block;
	position: relative;
	/* THE EMBLEM'S OWN PROPORTIONS, not a square. Its viewBox is 56.13 x 62.17,
	   so it is taller than it is wide. Clipped into a square box the hexagon
	   comes out squat and its top and bottom points read as blunt, which stops
	   it looking like the mark it is supposed to echo. */
	aspect-ratio: 56.13 / 62.17;
	background: linear-gradient(160deg, #FFF0C8 0%, #E0AC3C 42%, #8E5F12 100%);
	clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
	/* Two shadows doing two jobs: the black one sits the plinth on the band,
	   the gold one is the halo. Stacked drop-shadows rather than box-shadow
	   because clip-path throws a box-shadow away with the corners, and the
	   whole point is a glow that follows the hexagon. */
	filter:
		drop-shadow(0 10px 22px rgba(0, 0, 0, 0.55))
		drop-shadow(0 0 16px rgba(224, 172, 60, 0.34));
	transition: transform 320ms var(--ep-ease), filter 320ms var(--ep-ease);
}
.ep-toro__shots li:hover .ep-toro__hex,
.ep-toro__shots li:focus-within .ep-toro__hex {
	transform: translateY(-4px);
	filter:
		drop-shadow(0 16px 30px rgba(0, 0, 0, 0.6))
		drop-shadow(0 0 26px rgba(255, 197, 84, 0.58));
}
/* THE TILE IS A LIT STAGE NOW.

   The shots are cutouts on transparency, so the white rectangle that used to
   need a gold wash over it is simply gone, and the wash went with it. What
   replaces it has to solve a specific problem: three of these four products
   are BLACK and gold packaging and the fourth is a white bottle. Drop black
   packaging on a dark hexagon and it disappears; drop a white bottle on a
   bright one and you are back to clip art. So the light is a pool BEHIND the
   product rather than a tint on top of it. The black cartons get a lit field
   to read their edges against, the white bottle has somewhere dark to sit at
   the rim, and the same tile works for both.

   THE FLOOR. An elliptical pool under the product, so it stands on something
   instead of hovering in a glow. Sits below the image and above the ground. */
.ep-toro__hexin::before {
	content: "";
	position: absolute;
	left: 16%;
	right: 16%;
	bottom: 15%;
	height: 9%;
	z-index: 1;
	pointer-events: none;
	background: radial-gradient(50% 50% at 50% 50%, rgba(255, 214, 140, 0.78) 0%, rgba(255, 190, 96, 0.26) 45%, rgba(255, 190, 96, 0) 72%);
	filter: blur(3px);
}
/* THE VIGNETTE, above everything, pulling the corners down so the eye lands on
   the product rather than wandering to the six points of the hexagon. This is
   the "focused" half of focused light: without a falloff the pool below just
   reads as a brighter background. */
.ep-toro__hexin::after {
	content: "";
	position: absolute;
	inset: 0;
	/* mesh 0, smoke 1, dust 3, this 4, product 5 on top of everything. */
	z-index: 4;
	pointer-events: none;
	background: radial-gradient(88% 80% at 50% 44%, rgba(62, 34, 6, 0) 84%, rgba(62, 34, 6, 0.10) 94%, rgba(52, 28, 4, 0.28) 100%);
}

/* ---------------------------------------------------------------------------
   SMOKE AND SHEEN

   THE SMOKE. The first attempt at this was an ambient haze and it was invisible
   in practice, which is the same as not existing. This is vapour that actually
   reads as vapour: three wisps per tile rising from behind the product,
   growing and thinning as they climb, exactly the way real smoke dissipates.
   Rising and expanding together is what sells it; a blob that merely drifts
   reads as a moving light, not smoke.

   EVERY PLUME IS DIFFERENT. Twelve wisps, each with its own drift direction,
   climb height, growth, speed and starting point in its own cycle, written out
   below rather than generated so each is a deliberate value. The negative
   delays matter most: without them all twelve would begin from nothing at the
   same instant, and the eye reads simultaneous starts as a mechanism. Starting
   each one already mid-plume means the smoke is simply there, always has been.

   Behind the product, so it rises from it rather than over it.

   THE SHEEN is a highlight masked by the PRODUCT'S OWN SILHOUETTE, so the light
   travels through the goods rather than across the tile. Gold foil is what
   these cartons are actually printed with, so light crossing them is a real
   thing being shown, not an effect being applied.

   ON THE ANIMATION SHORTHAND, which already cost one round here: the per-tile
   rules below set duration and delay as LONGHANDS, and the rules that arm the
   animations name them as longhands too. Nothing uses the shorthand, so
   nothing can silently reset a longhand it did not mention.
   --------------------------------------------------------------------------- */
.ep-toro__smoke {
	position: absolute;
	inset: 0;
	z-index: 1;
	pointer-events: none;
	overflow: hidden;
}
.ep-toro__smoke i {
	position: absolute;
	bottom: 18%;
	width: 62%;
	aspect-ratio: 1;
	margin-left: -31%;
	border-radius: 50%;
	/* DARK SMOKE, and that is the whole fix rather than a preference.

	   The first two attempts made warm cream vapour and laid it over a tile
	   whose centre is a bright warm gold glow. Light smoke on a lit backdrop
	   has nothing to be seen against, so it rendered perfectly and was
	   invisible, twice. Smoke over a light source reads DARK in life for the
	   same reason: it is blocking the light, not emitting any. So this is a
	   warm near black, and it shows exactly where the tile is brightest, which
	   is where the product is. */
	background: radial-gradient(50% 50% at 50% 50%,
		rgba(48, 26, 4, 0.72) 0%,
		rgba(66, 38, 8, 0.46) 38%,
		rgba(84, 50, 14, 0) 72%);
	filter: blur(6px);
	opacity: 0;
	will-change: transform, opacity;
}
.ep-reveal--ready .ep-toro.is-in .ep-toro__smoke i {
	animation-name: epToroRise;
	animation-timing-function: cubic-bezier(0.32, 0.12, 0.62, 1);
	animation-iteration-count: infinite;
}
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(1) .ep-toro__smoke i:nth-child(1) { --ep-dx: -8%; --ep-dy: -78%; --ep-sc: 1.9; --ep-op: 0.80; left: 32%; animation-duration: 7.4s; animation-delay: -3.1s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(1) .ep-toro__smoke i:nth-child(2) { --ep-dx: 14%; --ep-dy: -88%; --ep-sc: 2.2; --ep-op: 0.70; left: 54%; animation-duration: 9.1s; animation-delay: -6.4s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(1) .ep-toro__smoke i:nth-child(3) { --ep-dx: 4%; --ep-dy: -70%; --ep-sc: 1.7; --ep-op: 0.72; left: 44%; animation-duration: 11.3s; animation-delay: -1.2s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(2) .ep-toro__smoke i:nth-child(1) { --ep-dx: 12%; --ep-dy: -84%; --ep-sc: 2.1; --ep-op: 0.78; left: 46%; animation-duration: 8.6s; animation-delay: -5.5s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(2) .ep-toro__smoke i:nth-child(2) { --ep-dx: -13%; --ep-dy: -74%; --ep-sc: 1.8; --ep-op: 0.68; left: 58%; animation-duration: 10.2s; animation-delay: -2.3s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(2) .ep-toro__smoke i:nth-child(3) { --ep-dx: 6%; --ep-dy: -92%; --ep-sc: 2.4; --ep-op: 0.60; left: 36%; animation-duration: 12.1s; animation-delay: -8.7s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(3) .ep-toro__smoke i:nth-child(1) { --ep-dx: -11%; --ep-dy: -80%; --ep-sc: 2.0; --ep-op: 0.78; left: 52%; animation-duration: 9.7s; animation-delay: -1.8s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(3) .ep-toro__smoke i:nth-child(2) { --ep-dx: 9%; --ep-dy: -70%; --ep-sc: 1.7; --ep-op: 0.72; left: 38%; animation-duration: 7.9s; animation-delay: -4.9s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(3) .ep-toro__smoke i:nth-child(3) { --ep-dx: -4%; --ep-dy: -90%; --ep-sc: 2.3; --ep-op: 0.58; left: 48%; animation-duration: 11.8s; animation-delay: -7.2s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(4) .ep-toro__smoke i:nth-child(1) { --ep-dx: 10%; --ep-dy: -76%; --ep-sc: 1.9; --ep-op: 0.80; left: 42%; animation-duration: 8.2s; animation-delay: -6.1s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(4) .ep-toro__smoke i:nth-child(2) { --ep-dx: -14%; --ep-dy: -86%; --ep-sc: 2.2; --ep-op: 0.66; left: 56%; animation-duration: 10.8s; animation-delay: -2.8s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(4) .ep-toro__smoke i:nth-child(3) { --ep-dx: 2%; --ep-dy: -72%; --ep-sc: 1.8; --ep-op: 0.72; left: 34%; animation-duration: 12.6s; animation-delay: -9.4s; }

/* Climb, spread and thin. The opacity peak is early and the long tail is the
   dissipation, which is where smoke spends most of its life. */
@keyframes epToroRise {
	0%   { opacity: 0; transform: translate3d(0, 12%, 0) scale(0.45); }
	16%  { opacity: var(--ep-op, 0.5); }
	48%  { opacity: calc(var(--ep-op, 0.5) * 0.66); }
	100% { opacity: 0; transform: translate3d(var(--ep-dx, 6%), var(--ep-dy, -80%), 0) scale(var(--ep-sc, 2)); }
}

/* ---------------------------------------------------------------------------
   THE LATTICE AND THE DUST

   THE SHEEN IS GONE. A highlight sweeping through the product was too much: it
   drew the eye to the effect rather than to the goods, which is the wrong way
   round on a tile whose whole job is showing what you are buying.

   THE LATTICE is not a texture picked because a texture was wanted. Toro
   cartons are printed with a gold triangular pattern and every one of these
   four shots has it on the box. This is that motif continuing off the
   packaging and onto the plinth, so the tile belongs to the product rather
   than being a frame the product was dropped into. Three repeating gradients
   at sixty degrees to each other make a true triangular lattice.

   It is masked to fade out behind the middle. Left flat it competes with the
   product for attention exactly where the product is; fading it there means it
   reads as the plinth's surface catching light toward the rim, which is what a
   real printed surface under a spotlight does.

   THE DUST sits behind the product, so the motes read around the goods
   without ever dimming them.
   Each layer is ONE element carrying eight dots as separate radial gradients,
   so a whole constellation animates as a single compositor layer instead of
   one element per mote. Two layers per tile at different speeds.
   --------------------------------------------------------------------------- */
.ep-toro__mesh {
	position: absolute;
	inset: 0;
	z-index: 0;
	pointer-events: none;
	background-image:
		repeating-linear-gradient(60deg, rgba(88, 50, 10, 0) 0 13px, rgba(88, 50, 10, 0.20) 13px 14px),
		repeating-linear-gradient(-60deg, rgba(88, 50, 10, 0) 0 13px, rgba(88, 50, 10, 0.20) 13px 14px),
		repeating-linear-gradient(0deg, rgba(88, 50, 10, 0) 0 13px, rgba(88, 50, 10, 0.14) 13px 14px);
	-webkit-mask-image: radial-gradient(58% 52% at 50% 44%, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.45) 58%, rgba(0, 0, 0, 1) 88%);
	mask-image: radial-gradient(58% 52% at 50% 44%, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.45) 58%, rgba(0, 0, 0, 1) 88%);
}

.ep-toro__dust {
	position: absolute;
	inset: 0;
	/* Below the product now, so the motes read around it rather than over
	   it. */
	z-index: 3;
	pointer-events: none;
	overflow: hidden;
}
.ep-toro__dust::before,
.ep-toro__dust::after {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	top: 0;
	height: 150%;
	opacity: 0;
	will-change: transform, opacity;
}
.ep-toro__dust::before {
	background-image:
		radial-gradient(1.4px 1.4px at 14% 78%, rgba(255, 250, 226, 0.95) 0%, rgba(255, 250, 226, 0) 100%),
		radial-gradient(1.0px 1.0px at 31% 62%, rgba(255, 253, 248, 0.75) 0%, rgba(255, 253, 248, 0) 100%),
		radial-gradient(1.6px 1.6px at 46% 84%, rgba(255, 242, 208, 0.9) 0%, rgba(255, 242, 208, 0) 100%),
		radial-gradient(1.1px 1.1px at 58% 54%, rgba(255, 253, 250, 0.7) 0%, rgba(255, 253, 250, 0) 100%),
		radial-gradient(1.3px 1.3px at 72% 74%, rgba(255, 248, 222, 0.88) 0%, rgba(255, 248, 222, 0) 100%),
		radial-gradient(0.9px 0.9px at 85% 60%, rgba(255, 253, 244, 0.62) 0%, rgba(255, 253, 244, 0) 100%),
		radial-gradient(1.2px 1.2px at 24% 44%, rgba(255, 253, 234, 0.72) 0%, rgba(255, 253, 234, 0) 100%),
		radial-gradient(1.0px 1.0px at 66% 36%, rgba(255, 253, 250, 0.6) 0%, rgba(255, 253, 250, 0) 100%);
}
.ep-toro__dust::after {
	background-image:
		radial-gradient(1.0px 1.0px at 9% 66%, rgba(255, 253, 242, 0.7) 0%, rgba(255, 253, 242, 0) 100%),
		radial-gradient(1.5px 1.5px at 22% 86%, rgba(255, 244, 214, 0.88) 0%, rgba(255, 244, 214, 0) 100%),
		radial-gradient(0.9px 0.9px at 38% 50%, rgba(255, 253, 250, 0.58) 0%, rgba(255, 253, 250, 0) 100%),
		radial-gradient(1.3px 1.3px at 52% 72%, rgba(255, 250, 228, 0.82) 0%, rgba(255, 250, 228, 0) 100%),
		radial-gradient(1.1px 1.1px at 64% 88%, rgba(255, 253, 236, 0.76) 0%, rgba(255, 253, 236, 0) 100%),
		radial-gradient(1.4px 1.4px at 78% 46%, rgba(255, 246, 218, 0.84) 0%, rgba(255, 246, 218, 0) 100%),
		radial-gradient(1.0px 1.0px at 90% 80%, rgba(255, 253, 246, 0.66) 0%, rgba(255, 253, 246, 0) 100%),
		radial-gradient(0.9px 0.9px at 44% 32%, rgba(255, 253, 250, 0.55) 0%, rgba(255, 253, 250, 0) 100%);
}

.ep-reveal--ready .ep-toro.is-in .ep-toro__dust::before {
	animation-name: epToroDust;
	animation-duration: 14s;
	animation-timing-function: linear;
	animation-iteration-count: infinite;
}
.ep-reveal--ready .ep-toro.is-in .ep-toro__dust::after {
	animation-name: epToroDust;
	animation-duration: 19s;
	animation-timing-function: linear;
	animation-iteration-count: infinite;
}
/* Every tile starts somewhere else in the drift, so no two constellations
   travel together. Negative, so all four are already mid drift on arrival. */
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(1) .ep-toro__dust::before { animation-delay: -2.4s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(2) .ep-toro__dust::before { animation-delay: -7.1s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(3) .ep-toro__dust::before { animation-delay: -10.6s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(4) .ep-toro__dust::before { animation-delay: -5.3s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(1) .ep-toro__dust::after { animation-delay: -13.2s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(2) .ep-toro__dust::after { animation-delay: -3.7s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(3) .ep-toro__dust::after { animation-delay: -16.9s; }
.ep-reveal--ready .ep-toro.is-in .ep-toro__shots li:nth-child(4) .ep-toro__dust::after { animation-delay: -8.8s; }

/* Rise and fade. Motes are brightest through the middle of the climb, where
   the spotlight is, and gone before they reach the rim. */
@keyframes epToroDust {
	0%   { opacity: 0; transform: translate3d(0, 8%, 0); }
	18%  { opacity: 0.85; }
	62%  { opacity: 0.7; }
	100% { opacity: 0; transform: translate3d(2%, -34%, 0); }
}

/* Inset by the rim thickness, so the gold shows as an edge all the way round.
   A border would not work: clip-path cuts borders off with the corners. */
.ep-toro__hexin {
	position: absolute;
	inset: 2px;
	display: flex;
	align-items: center;
	justify-content: center;
	/* Tighter than before, because the shots are trimmed to their content
	   now. They used to carry a wide transparent margin of their own, so the
	   product rendered small inside the plinth whatever this said. */
	padding: 12% 14%;
	background:
		radial-gradient(70% 62% at 50% 42%, rgba(255, 248, 230, 1) 0%, rgba(253, 218, 152, 0.86) 30%, rgba(224, 166, 68, 0.50) 58%, rgba(158, 106, 32, 0.20) 78%, rgba(0, 0, 0, 0) 92%),
		linear-gradient(180deg, #DCAB5C 0%, #AB7830 44%, #6B4315 100%);
	clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
	/* Keeps the tile's own layers to itself. Nothing blends any more, but the
	   z-index stack of floor, product and vignette does want containing. */
	isolation: isolate;
}
.ep-toro__hexin img {
	position: relative;
	/* TOP OF THE STACK, above the vignette and the dust. Everything else in
	   the tile is atmosphere and the product is the thing being sold, so
	   nothing is allowed to sit over it: the vignette was dimming its edges
	   and the smoke behind was showing through the glass on the vials, which
	   between them were washing out the goods on a tile built to show them. */
	z-index: 5;
	/* Grounds the cutout on the floor below it, and a whisper of warm rim so
	   the black cartons separate from the dark ground behind them. */
	filter:
		drop-shadow(0 6px 10px rgba(0, 0, 0, 0.55))
		drop-shadow(0 0 14px rgba(255, 198, 104, 0.30))
		brightness(1.27) contrast(1.03) saturate(1.07);
	max-width: 100%;
	max-height: 100%;
	width: auto;
	height: auto;
	display: block;
	object-fit: contain;
	/* Multiplies against the PLINTH, not against the band. */
	mix-blend-mode: multiply;
}
@media (hover: hover) {
	.ep-toro__hex:hover { transform: translateY(-5px); }
}

/* --- the three ranges --- */
.ep-toro__ranges {
	list-style: none;
	margin: 0 0 var(--ep-s-5);
	padding: 0;
	display: flex;
	justify-content: center;
	flex-wrap: wrap;
	gap: var(--ep-s-2);
}
.ep-toro__ranges li { margin: 0; }
.ep-toro__ranges a {
	display: inline-flex;
	align-items: center;
	gap: var(--ep-s-2);
	padding: 8px var(--ep-s-3);
	border-radius: var(--ep-r-full);
	background: rgba(224, 172, 66, 0.09);
	border: 1px solid rgba(224, 172, 66, 0.34);
	color: var(--ep-white) !important;
	font-size: var(--ep-t-xs);
	font-weight: var(--ep-w-bold);
	text-decoration: none;
	transition: border-color var(--ep-dur-1) var(--ep-ease), background var(--ep-dur-1) var(--ep-ease);
}
.ep-toro__ranges a:hover {
	background: rgba(224, 172, 66, 0.18);
	border-color: rgba(224, 172, 66, 0.7);
}
.ep-toro__rn { color: #E0AC42; }

.ep-toro__cta { display: flex; justify-content: center; }
.ep-btn--gold {
	gap: var(--ep-s-2);
	background: linear-gradient(180deg, #F0C868 0%, #D69F32 100%);
	color: #2A1B04 !important;
	border: 1px solid rgba(255, 245, 214, 0.5);
	box-shadow: 0 8px 24px rgba(214, 159, 50, 0.3);
}
.ep-btn--gold:hover { filter: brightness(1.06); }

@media (min-width: 700px) {
	.ep-toro__in { padding-top: var(--ep-s-12); padding-bottom: var(--ep-s-12); }
	.ep-toro__mark svg { width: 216px; }
	.ep-toro__h { font-size: var(--ep-t-2xl); max-width: 22ch; }
	.ep-toro__lead { font-size: var(--ep-t-base); }
	.ep-toro__shots {
		grid-template-columns: repeat(4, 1fr);
		gap: var(--ep-s-3);
		max-width: 820px;
		margin-inline: auto;
		align-items: center;
	}
	/* Honeycomb, using MARGIN rather than transform.
	   A transform paints the shifted hexagons lower without reserving the room,
	   so the row kept its unshifted height and the two offset shots hung 28px
	   past the bottom of the list and sat 8px ON TOP of the range pills below.
	   Margin is part of layout, so the row grows to hold them. */
	.ep-toro__shots { align-items: start; }
	.ep-toro__shots li:nth-child(even) { margin-top: 13%; }
}
