/** Shopify CDN: Minification failed

Line 132:56 Expected identifier but found whitespace
Line 134:76 Unterminated string token

**/
/* SP Hero Fan — "By The Sea" collection-drop hero. Figma node 2017:29729,
   file 8LgeiDvHrFFbc7rxWYxmmT, 1920px reference frame. See
   sections/sp-hero-fan.liquid header comment for the
   block-order-is-paint-order z-stacking rule; the card-entrance animation
   system is at the bottom of this file.

   Scaling model: everything in .sp-hero-fan__cards is positioned as a % of
   the reference 1920px design width, via CSS custom properties set inline
   per block (--x/--y/--w/--h/--rot, all px @1920 reference) converted to %
   here. Card Y-offsets are REBASED to the topmost card (sheet-m) = 0, not
   Figma's absolute frame coordinates — the text cluster above sits in
   normal document flow instead of sharing Figma's absolute coordinate
   space with the cards, so the two don't need one shared origin. Only the
   cards' geometry relative to EACH OTHER (the fan shape) needed to survive
   the trip from Figma; the exact gap between text and cards is ordinary
   CSS spacing, tuned by eye. */

.sp-hero-fan {
  --sp-hero-fan-bg: #e9f0f3;
  /* The theme's signature motion curve — easeOutQuint. Same one the mobile
     drawer's accordion cascade uses (see sp-mobile-drawer.css "Accordion
     motion signature"): decisive start, long quiet landing, zero overshoot.
     One voice across every SP surface that moves. */
  --sp-hero-fan-ease: cubic-bezier(0.22, 1, 0.36, 1);
  /* Leaving is not a performance. Same principle the drawer's accordion
     uses — unfolding gets the 460ms settle, collapsing gets 300ms and a
     standard curve — so a slide exits on a plain ease-out while the
     incoming one gets the full staggered deal. */
  --sp-hero-fan-ease-exit: cubic-bezier(0.33, 1, 0.68, 1);
  position: relative;
  overflow: hidden;
  background: var(--sp-hero-fan-bg);
  /* Hairline edge. See the ::after rule at the end of this block for why this
     is a pseudo-element rather than a real `border`. */
  --sp-hero-fan-edge: #ded3cb;
  /* the active slide's colour is written to this element's inline
     background-color by sp-hero-fan.js, so kit colourways cross-fade */
  transition: background-color 620ms var(--sp-hero-fan-ease);
  font-family: var(--sp-font-family, 'Manrope', sans-serif);
  padding-bottom: 0;
}

/* The section's 1px edge, drawn as an overlay rather than a real `border`.

   Three reasons it cannot be a border. (1) A border would add 2px to the box
   and unstick the card band from the section's bottom edge — the crop's bottom
   edge IS the section's bottom edge, which is what keeps the dots seated on
   the band (see the dead-strip note further down). (2) `box-shadow: inset`
   avoids that but paints UNDER descendants, so the wall's tiles and the fade
   would cover the hairline exactly along the bottom edge where it matters
   most. (3) An overlay above both (z-index 3, over the fade's 2) is the only
   one of the three that is visible the whole way round and costs zero layout.

   Colour is derived per slide and cross-faded by sp-hero-fan.js on the same
   handover as the background and the dots, so the edge stays the same distance
   from whatever colourway is on screen. */
.sp-hero-fan::after {
  content: '';
  position: absolute;
  inset: 0;
  border: 1px solid var(--sp-hero-fan-edge);
  transition: border-color 620ms var(--sp-hero-fan-ease);
  pointer-events: none;
  z-index: 3;
}

/* Slides occupy ONE grid cell, so the section is always as tall as its
   tallest slide and never resizes mid-rotation. Absolute positioning would
   collapse the section's height instead. */
.sp-hero-fan__stack {
  display: grid;
}

.sp-hero-fan__slide {
  grid-area: 1 / 1;
  position: relative;
  min-width: 0;
  /* Flex column so slack can be parked ABOVE the card band — see the auto
     margin on .sp-hero-fan__inner below. */
  display: flex;
  flex-direction: column;
}

.sp-hero-fan__slide:not(.is-active) {
  pointer-events: none;
}

/* Art and copy cross-fade; the cards do their own thing (see "Card
   entrance" at the bottom) and the art also swings (see "Background art
   swing" below). Every slide is now a different kit, so this cross-fade
   always does visible work — it used to be a deliberate no-op between
   slides 1 and 2 back when both were By The Sea. */
.sp-hero-fan__inner {
  transition: opacity 420ms var(--sp-hero-fan-ease);
}

/* The art fades slower than the copy on the way IN, so the travel below is
   still running while the illustration is fully visible — fading and
   arriving at the same instant hides the movement. The fade OUT is slower
   than the copy's for the same reason in reverse: the art now has to be
   seen LEAVING the frame, and a quick fade would delete it in place with
   the exit travel never visible. Both are still shorter than the travel
   they accompany, so neither ever holds a stale illustration on screen. */
.sp-hero-fan__art {
  transition: opacity 900ms var(--sp-hero-fan-ease);
}

.sp-hero-fan__slide:not(.is-active) .sp-hero-fan__inner {
  opacity: 0;
}

.sp-hero-fan__slide:not(.is-active) .sp-hero-fan__art {
  opacity: 0;
  transition: opacity 480ms var(--sp-hero-fan-ease-exit);
}

/* WHICH SLIDE IS VISIBLE MUST NOT DEPEND ON THE ENTRANCE ANIMATION. The
   per-card rules at the bottom of this file happen to hide inactive slides'
   cards, but they are gated three ways — `min-width: 768px`, the theme's
   `animations_enabled_desktop` setting, and `[data-cc-animate]`. Leaning on
   them for visibility means every slide renders stacked on top of every
   other one below 768px, or whenever a merchant turns animations off.
   (Both were real: mobile showed slide 1 and slide 2 superimposed.) These
   two rules are ungated on purpose — the animation owns MOTION, never
   visibility. */
   Scoped to the whole CROP, not to .sp-hero-fan__cards. It used to target the
   cards element, which silently stopped covering everything the moment the
   bottom fade was added as a sibling rather than a child: all three slides'
   fades then painted at once, stacking three multiply layers and darkening
   the band roughly 2-3x past Figma. Hiding the crop covers the cards, the
   fade, and anything added inside it later. */
.sp-hero-fan__cards-crop {
  transition: opacity 320ms var(--sp-hero-fan-ease);
}

.sp-hero-fan__slide:not(.is-active) .sp-hero-fan__cards-crop {
  opacity: 0;
  transition: opacity 260ms var(--sp-hero-fan-ease-exit);
}

/* Decorative bleed illustrations — soft/low-contrast, not focal content.
   Figma-exact left/width/margin-top (see .sp-hero-fan__art-img-- variants
   below) — all computed from Object 2017:30094/2017:30096's real
   absoluteBoundingBox, not eyeballed. Capped to the same 1920px reference
   max-width as .sp-hero-fan__cards-crop (not just inset:0 of the full,
   uncapped section) — without this cap the art would keep growing past
   its Figma-reference size on viewports wider than 1920px while the
   cards stopped scaling at that cap, drifting out of alignment with
   each other. */
.sp-hero-fan__art {
  position: absolute;
  top: 0;
  left: 50%;
  width: min(1920px, 100%);
  height: 0;
  overflow: visible;
  transform: translateX(-50%);
  z-index: 0;
  pointer-events: none;
}

.sp-hero-fan__art-img {
  position: absolute;
  top: 0;
  height: auto;
  /* Figma's own layer opacity is IDENTICAL on both illustrations (0.2,
     checked directly on Object 2017:30094/2017:30096 -- not eyeballed).
     The left one reads more visible at the same opacity purely because
     its artwork (dock/boat linework) has more inherent contrast than the
     right's softer lighthouse scene -- don't reintroduce an asymmetric
     opacity split, that was a misread of the composite screenshot.
     left/width/margin-top below are all % of container WIDTH (percentage
     `top` resolves against height, `margin-top` resolves against width
     per spec) -- pins both to Figma's exact 1920px-reference geometry
     (node 2017:29729) at any viewport width.

     Opacity is now PER-SLIDE, defaulting to the 0.2 above. It became a
     variable on 2026-08-07 when the August/Checking In art landed: its
     Figma layers (2203:23498 / 2203:23496) are drawn at 0.4, not 0.2, so
     a single shared value could no longer be right for every slide. The
     default keeps By The Sea and Parkside byte-identical to before -- a
     slide only diverges when its `art_opacity` setting says so. The
     "don't reintroduce an asymmetric opacity split" rule above still
     holds WITHIN a slide: this varies per slide, never left-vs-right. */
  opacity: var(--sp-hero-fan-art-opacity, 0.2);
  /* Phone width. The art is one shared size on the narrow tiers and only
     splits into its two Figma-exact per-side widths at the desktop tier,
     which is why `width` is the one property tiered away from the variant
     rules below — they own everything that is constant across breakpoints.

     2026-09-19: 32% -> 44%, operator call ("try making larger sizes and more
     faded out on mobile views"). Paired with the opacity scale and the right
     scene's new margin-top in the <=767 block below — a bigger shape at the
     old 0.4 would have read louder, not softer, so the three move together.
     Blind gemma-4-26B judgement over four size/opacity pairs rendered at 390
     (32/1.0, 44/0.7, 52/0.6, 60/0.5) picked 44/0.7 best and the shipped
     32/1.0 WORST — the small-and-dark version read as "images just placed in
     corners", the two largest as washed out. */
  width: 44%;
}

.sp-hero-fan__art-img--left {
  left: -11.979%;
  /* DELIBERATE DEVIATION FROM FIGMA — operator call, 2026-08-05. Figma's
     value is -13.594%; this is that lowered by 10 percentage points in the
     same unit the property already uses (% of container WIDTH), so ~192px
     down at the 1920 reference and proportionally less on narrower
     viewports. One rule covers every slide and every breakpoint: the
     variant class is shared, and only `width` is tiered (see the min-width
     blocks below). Don't "restore" this to the Figma figure as a fidelity
     fix. */
  margin-top: -3.594%;
  max-width: 537px;
}

.sp-hero-fan__art-img--right {
  left: 79.479%;
  /* Figma-exact (node 2017:30096, y=244 on the 1920 frame). Only the >=992
     tier reads this value — <=767 overrides it, and so does 768-991.98.

     2026-09-19: briefly shipped at 17% on an operator call, then reverted the
     same day on a second look at the largest view ("looks better on figma ref
     on largest view"). Keeping the round trip recorded because the evidence
     pointed the other way and a future session will find that study: four
     values (4/8/12.708/17%) rendered at 1920 were judged blind by the local
     gemma-4-26B, which picked 17% best and 4% worst, reasoning that a lower
     scene ballasts the bottom-heavy sheet fan. The operator's eye disagreed,
     and on a comp-derived value the operator's eye wins — the model has no
     brand context and got exactly one pass. Do not "re-apply" 17% from the
     study alone.

     At 17% the scene's bottom cropped 198px at the section edge at 1920 and
     24px at 1440; at this value those are 116px and nothing. `.sp-hero-fan`
     is overflow:hidden, so either way it crops rather than bleeding. */
  margin-top: 12.708%;
  max-width: 516px;
}

/* ---- Background art swing ----
   Only some slides carry background art (By The Sea and Parkside do;
   Checking In and From Paris With Love have blank bg_*_asset), so these
   rules are scoped to the images and are a no-op on a slide with none —
   there is nothing to hinge.

   HINGE AT THE OUTER TOP CORNER, not the centre. Both illustrations bleed
   off their own outer edge (left sits at -11.979%, right at 79.479%) and
   the left one bleeds off the top as well, so the outer corner is the only
   part of each that is always off-canvas. Pivoting there reads as the art
   swinging in from outside the frame; pivoting centrally would read as a
   flat plane being rotated on screen, which is the thing to avoid.

   Directions are mirrored so the pair converges on the composition rather
   than both leaning the same way. 3.5deg is deliberately small: on a
   ~537px-wide illustration that still travels ~33px at the far edge, which
   is plenty of movement for art sitting at opacity 0.2.

   Individual `rotate`/`translate` properties, not `transform` — same
   reason the cards use them: .sp-hero-fan__art carries its own
   `transform: translateX(-50%)` for centring, and composing via separate
   properties keeps the two from clobbering each other. */
.sp-hero-fan__art-img {
  /* Long, quiet landing on the theme's signature easeOutQuint. The travel
     outlasts the 900ms fade on purpose — the illustration finishes
     arriving after it is already fully visible, which is what makes the
     movement read as a settle rather than a slide. Stretched from 1150ms
     to 1400ms when the travel went from a nudge to a full frame exit, so
     the SPEED stays gentle over the longer distance. */
  transition:
    rotate 1400ms var(--sp-hero-fan-ease),
    translate 1400ms var(--sp-hero-fan-ease);
}

.sp-hero-fan__art-img--left {
  transform-origin: 0% 0%;
}

.sp-hero-fan__art-img--right {
  transform-origin: 100% 0%;
}

.sp-hero-fan__slide.is-active .sp-hero-fan__art-img {
  rotate: 0deg;
  translate: 0 0;
}

/* Leaving is not a performance — the exit is still well under half the
   entry and on the plain ease-out, so an outgoing illustration clears
   rather than lingering through the incoming one's arrival. It went 420ms
   -> 560ms only because it now has a whole frame-width to cross; any
   quicker and the departure reads as a snap. */
.sp-hero-fan__slide:not(.is-active) .sp-hero-fan__art-img {
  transition:
    rotate 560ms var(--sp-hero-fan-ease-exit),
    translate 560ms var(--sp-hero-fan-ease-exit);
}

/* OFF-FRAME REST POSITION. The X figures are what actually clears the
   composition, computed rather than eyeballed: both images are sized and
   positioned as a % of the art container's width, so the translate needed
   to clear is a constant % of each image's OWN width at every viewport —
   no breakpoints. The left image's right edge sits at 15.99% of the
   container, which is 57.2% of its own width; the right image's left edge
   is 20.52% short of the container's right, which is 76.4% of its own
   width. -70% / +85% clear both with margin (376px and 439px of travel at
   1920) and keep clearing as the viewport changes.

   The Y drift is what makes it float rather than slide: percentages here
   resolve against each image's HEIGHT, so 6% is ~39px. Mirrored — the left
   rises into place, the right settles down into it — which with the
   mirrored rotation gives the pair a slow pinwheel instead of two panels
   sliding along one axis. */
.sp-hero-fan__slide:not(.is-active) .sp-hero-fan__art-img--left {
  rotate: -3.5deg;
  translate: -70% 6%;
}

.sp-hero-fan__slide:not(.is-active) .sp-hero-fan__art-img--right {
  rotate: 3.5deg;
  translate: 85% -6%;
}

.sp-hero-fan__inner {
  position: relative;
  z-index: 2;
  /* Figma's text frame (node 2095586049) is 850px of TEXT COLUMN with no
     padding of its own. Under border-box a plain max-width:850px would
     include the 24px gutters and leave only 802px, which is narrower than
     Figma everywhere and is too narrow to hold Parkside's 850px headline on
     two lines. Widened so the column itself is Figma's 850. */
  max-width: calc(850px + 48px);
  margin: 0 auto;
  padding: 48px 20px 24px;
  text-align: center;
}

.sp-hero-fan__eyebrow {
  margin: 0 0 12px;
  font-family: var(--sp-font-family, 'Manrope', sans-serif);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0.04em;
  color: var(--sp-color-graphite, #101010);
}

.sp-hero-fan__headline {
  /* Figma sets each headline's text node to a DELIBERATE width so the line
     break falls where the copy reads best -- an intentional break, not an
     incidental wrap. "Salt air, soft light, and quiet harbors," / "By the
     Sea" is pinned at 618px inside its 850px frame (node 2017:29969);
     Parkside's longer headline gets the full 850px so it still lands in two
     lines. So this is PER SLIDE, not one constant: hardcoding 618 for
     everything wrapped Parkside onto a third line, which made slide 3 55px
     taller than the others and — because the slides share one grid cell —
     padded every other slide with dead space. Pinned at 1:1 at the desktop
     tier (those headlines are 46px, matching Figma's reference font-size);
     the narrower tiers scale the same per-slide width by the same ratio as
     the font-size, so each slide keeps its own intended break. */
  max-width: calc(var(--sp-hero-fan-headline-w, 618px) * 22 / 46);
  margin: 0 auto 28px;
  font-family: var(--sp-font-family, 'Manrope', sans-serif);
  /* 22px is the mobile heading size this comp file uses in EVERY other band —
     sticker club, categories, newsletter, testimonials and club CTA are all
     Manrope 22/28.6, and so was the previous mobile hero (`1039:4012`). The
     hero had been the one outlier at 26, which cost ~5px a line on a headline
     that wraps to three lines on the sale wall, and that height is multiplied
     by 6.4 in the wall's corridor arithmetic below. */
  font-size: 22px;
  font-weight: 300;
  line-height: 1.2;
  text-transform: none; /* override the theme's default h2 uppercase */
  color: var(--sp-color-graphite, #101010);
}

/* Phone: the two CTAs stack full-width. Content-width buttons that wrap onto
   two rows land at different widths (measured 242.5 and 208.6 at 390) and read
   as ragged; a stacked pair on one edge reads as deliberate and doubles the tap
   target. This is the file's own mobile CTA language — `1039:4009` is a Link at
   the full content width, 43 tall — not an invention. Row layout returns at 768,
   the vendor's drawer breakpoint, which is where this theme stops being "phone".

   `gap` is 15, not 16: the comp puts the button row at gap=15 on BOTH the
   desktop hero (`2017:29972`) and mobile Traveler's (`1039:4093`). */
.sp-hero-fan__buttons {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  gap: 15px;
}

.sp-hero-fan__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 15px, not 14: 15 + 13 line-height + 15 + 2 border = 45, clearing the 44px
     minimum touch target. The comp's mobile Link (`1039:4009`) is 43 with 15px
     padding because Figma does not count the stroke in the frame height. */
  padding: 15px 22px;
  font-family: var(--sp-font-family, 'Manrope', sans-serif);
  font-size: 13px;
  font-weight: 700;
  /* A CONSTANT 2px, not 0.125em. Every button in the comp is ls=2 at every
     size — desktop `2017:29974` at 16px and mobile `1039:4010` at 13px both
     read 2.0 — and `--sp-ls-label` is the token the rest of the theme uses
     (sp-travelers, sp-club-cta, sp-favorites). An em value happened to be
     right at 16px and quietly shrank to 1.625px at the 13px mobile step. */
  letter-spacing: var(--sp-ls-label, 2px);
  line-height: 13px;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
}

.sp-hero-fan__btn--solid {
  background: var(--sp-color-ink, #000);
  border: 1px solid var(--sp-color-ink, #000);
  color: var(--sp-color-paper, #fff);
}

.sp-hero-fan__btn--solid:hover,
.sp-hero-fan__btn--solid:focus-visible {
  background: #262626;
  border-color: #262626;
}

.sp-hero-fan__btn--outline {
  background: transparent;
  border: 1px solid var(--sp-color-graphite, #101010);
  color: var(--sp-color-graphite, #101010);
}

.sp-hero-fan__btn--outline:hover,
.sp-hero-fan__btn--outline:focus-visible {
  background: var(--sp-color-graphite, #101010);
  color: var(--sp-color-paper, #fff);
}

/* Figma (node 2017:29729) crops the fan at the frame's own bottom edge —
   cards visibly cut off mid-height rather than shown in full. The outer
   crop viewport uses the Figma-exact clip height (380px @1920 reference:
   frame bottom minus the topmost card's start); the inner stage keeps its
   own full 640px reference height so every card's --x/--y/--w/--h math
   (a % of THIS element's box) stays correct — only the visible window
   shrinks. */
/* THE CARD BAND MUST STAY FLUSH WITH THE SECTION'S BOTTOM EDGE. Slides are
   grid items sharing one cell, so every slide is as tall as the TALLEST one
   — and they do not have equal natural heights: at 1440 the sale wall is
   687px while a kit slide's own content is 660px. Grid stretches the short
   one, and in normal flow that 27px of slack lands underneath its cards,
   which is precisely where the dots sit (30px up from the section bottom).
   The result was a strip of bare background between the sheets and the
   pagination.

   The two cannot simply be equalised — the wall's height is pinned by the
   1690px floor its copy corridor needs. So the slack is moved instead: an
   auto bottom margin on the copy block pushes all free space between the
   copy and the cards, leaving the band flush at the bottom on every slide
   at every width. On the sale wall the crop is out of flow entirely, so
   this is a no-op there. */
.sp-hero-fan__slide > .sp-hero-fan__inner {
  margin-bottom: auto;
}

/* Mobile only: shrink the copy-to-fan gap by ~24px without reopening the
   fan-to-dots gap the auto margin above exists to prevent (see "THE CARD
   BAND MUST STAY FLUSH" above it). Can't just reduce the auto margin's
   value directly — it isn't a number, it's "claim 100% of this slide's
   leftover slack," so shrinking any fixed sibling (crop's margin-top,
   inner's own padding-bottom) is silently reabsorbed 1:1 with zero
   visible change — verified live before landing on this. Instead: cap
   .inner's side at a fixed 18px and let .cards-crop's OWN new auto
   margin-bottom claim whatever's left, same as before — it still lands
   flush at the slide's bottom edge, just with less parked above it
   instead of below. `:not(--wall)` because the sale slide's .inner
   already has its own padding-based override below and was never part
   of this gap in the first place.

   All four kit slides share ~42px of natural slack, so 18px lands them
   within a rounding error of a clean -24px (verified live: 90px -> 66px).

   THERE WAS A PARKSIDE-SPECIFIC SUPPLEMENT HERE; IT IS GONE ON PURPOSE
   (2026-08-19). It existed because Parkside's cards carried a +180
   stage-unit offset baked into its `card` blocks in templates/index.json,
   which pushed its fan ~81px down the crop on mobile and needed the gap
   above clawed back to compensate. That +180 was breakpoint-agnostic, so
   it also leaked onto DESKTOP, where it showed only 42% of the centre card
   against the other two fans' 79.8% — the "extra desktop offset". The
   offset has been reverted at source (all nine card y_offsets -180, so
   Parkside's topmost card is back at y=0 like Checking In's and
   Romanticize Life's), which makes the supplement not just unnecessary but
   actively wrong: with the shift gone it over-tightened Parkside to a 16px
   copy-to-fan gap against the other three slides' 42px, measured live on
   avd-1. Every kit slide now uses the shared 24px/18px pair above.
   Do not reintroduce a per-slide index-coupled supplement here — it was
   FRAGILE by construction (`:nth-of-type(4)` is slide POSITION, not
   identity, so any reorder in templates/index.json silently retargets it). */
@media (max-width: 767px) {
  .sp-hero-fan__slide:not(.sp-hero-fan__slide--wall) > .sp-hero-fan__inner {
    margin-bottom: 18px;
  }

  /* Slack goes ABOVE the band, never below it, so the crop's bottom edge —
     which IS the cut, now that cards overflow it by ~180px — lands on the
     slide's own bottom edge on every slide. The kit slides do NOT have equal
     copy heights (Parkside's headline wraps a 3rd line: its .inner measures
     312px against the other three at 286), so 26px has to be spent somewhere.
     Spending it BELOW the crop (the previous `margin-bottom: auto`) spent it
     moving the band itself, which changed how much of each sheet got clipped
     from slide to slide — measured live on avd-1: Parkside flush at 0 while
     Checking In, By the Sea and Romanticize Life all sat 26px up, i.e. four
     fans with three different cut lines. Spending it ABOVE puts it on the
     copy-to-fan gap instead (18px on Parkside, ~44px on the other three),
     which reads as nothing next to a clip line that moves. .inner's 18px
     above stays as the floor for that gap. */
  .sp-hero-fan__slide:not(.sp-hero-fan__slide--wall) .sp-hero-fan__cards-crop {
    margin-top: auto;
    margin-bottom: 0;
  }

  /* Right-side bleed illustration: drop it to sit just above the fan, so the
     phone reads like the desktop composition instead of stranding the art in
     the empty upper third. Operator call, 2026-08-19; a deliberate mobile
     deviation from Figma in the same spirit as --left's -10pp above, and for
     the same reason (Figma's number is a 1920-reference value that does not
     reflow).

     WHY IT DRIFTS UP ON ITS OWN: `margin-top` is a % of the container's WIDTH
     (per spec, for both margin axes), not its height, so the Figma-exact
     12.708% below resolves against a 434px art layer on a 444px phone = 55px,
     against 1430px on desktop = 182px. The art layer's height barely changes
     between tiers but that offset scales with WIDTH, so the narrower the
     screen the higher the art rides. Measured on avd-5 (444px): art bottom
     landed at 218-231 with the fan starting at 352 — 121-134px of dead space
     between them, while at 1440 the same rule runs the art 224-259px DOWN
     BEHIND the fan and at 992 leaves only a 39-62px gap.

     36.5% of the 434px layer = ~158px, which puts the three arted slides'
     bottoms at 321/331/334 against a fan top of 352 — an 18-31px gap on all
     three, i.e. "just above", never overlapping. The spread is only the three
     illustrations' own differing heights (163/173/176px); a single number
     cannot close it, and 13px of variance is invisible at 0.2-0.4 opacity.
     Only the RIGHT side moves: the left illustration is already correctly
     placed top-left at every tier and was not part of this. Slides without
     `bg_right_asset` (the sale wall, Romanticize Life) have no such element,
     so this rule simply does not apply to them. */
  /* More faded on the phone, in proportion rather than flat: each slide keeps
     its own art_opacity relationship (0.2 for By The Sea and Parkside, 0.4 for
     the newer kits) and the whole tier is simply softened — a flat value here
     would have thrown away that per-slide art direction. 0.4 -> 0.28. */
  /* SP 2026-10-28 (sp-theme-dev-134, designer note 10/27): "make the watercolor
     in the background less opacity, it's messing with the text a bit". Phone
     multiplier 0.7 -> 0.45, so the 0.4 slides go 0.28 -> 0.18 behind the
     headline; art stays visible, just quieter. */
  .sp-hero-fan__art-img {
    opacity: calc(var(--sp-hero-fan-art-opacity, 0.2) * 0.45);
  }

  .sp-hero-fan__art-img--right {
    /* 2026-09-19: 36.5% -> 52%. The 36.5% above was measured on a 444px device
       and, because this is a % of container WIDTH, it does not hold its own
       stated intent on a narrower phone: at 390 it put the scene's bottom at
       296 against a fan top of 381, an 85px void, where the comment below is
       explicit that the target is "just above", 18-31px. 52% of 390 lands the
       bottom at 356 — a 24px gap, back inside that band — and 52% at 444 gives
       a 21px gap, so the one value now holds across the phone range instead of
       only at the width it was sampled at.
       Independently preferred by a blind 4-way vision judgement on the local
       gemma-4-26B (30/36.5/45/52% rendered at 390): BEST=52%, WORST=30%, on
       the grounds that a higher scene competes with the two CTA rows while the
       lowest reads as one connected band with the sheet fan. Full study and the
       rendered variants are on the breakpoint board for this pass.
       Re-derived the same day when the tier's width went 32% -> 44%: the scene
       grew 153.6px tall -> 211.1px, so holding the same 24px gap above the fan
       needs 37.3% rather than 52%. The gap is the invariant here, not the
       percentage — recompute it if the width moves again. */
    margin-top: 37.3%;
  }
}

.sp-hero-fan__cards-crop {
  /* Never let flex shrink the band: its height comes from aspect-ratio, and
     a shrink would silently squash the sheets rather than overflow. */
  flex-shrink: 0;
  /* Centred by margin ARITHMETIC, not `margin-inline: auto` and not a
     transform.
     - `auto` was wrong: below 992px the crop is deliberately WIDER than its
       container (140%/200%, so the fan bleeds off both edges), and over-wide
       auto margins don't split — per CSS2.1 10.3.3 the right margin takes the
       whole overflow in LTR. Measured at 390px: marginLeft 0, marginRight
       -390px, i.e. the fan sat flush LEFT and dumped 100% of the bleed on the
       right, leaving 4 of 9 cards at literally 0 visible pixels, unreachable
       at any scroll position. `calc(50% - w/2)` splits it correctly at any
       width, over- or under-sized.
     - a transform would centre it too, but `transform` creates a STACKING
       CONTEXT, which would isolate .sp-hero-fan__cards-fade below and stop it
       multiplying against the section background. Figma's gradient tints the
       bare background either side of the fan, so that blend must reach it. */
  /* PHONE: a big fan, not a sliver. Two numbers set the band, both in the
     1920-wide / 640-tall stage units every card's --x/--y already uses:

       --sp-hero-fan-crop-w  how wide the stage is drawn (so, how big a sheet is)
       --sp-hero-fan-crop-h  how much of that 640-tall stage the window shows

     At 390 the old 200%/380 pair drew ~110px-wide sheets into a 154px-tall
     window — the sheets read as confetti, and because the kit slides are far
     shorter than the sale wall (which sets the shared height), 276px of the
     slide was empty background between the CTAs and that sliver. 360%/590
     draws sheets big enough to read into a window deep enough to hold them:
     two-and-a-bit sheets bleeding off both edges, the same "wider than the
     page on purpose" device the mobile comps use for the restock and
     favorites rails. Sheets stay bottom-cropped (500 of 640) so the band
     still reads as a fan running off the section edge, not a finished row.

     THE TRADE-OFF, because the next person will want to shrink the sheets
     further. Visible fan height is capped at crop-w / 3: the stage keeps its
     1920x640 aspect so the cards cannot distort, so a narrower stage is also
     a shorter band. Sheets small enough to match desktop's nine-across arc
     (crop-w 100%, ~12.5% of the viewport each) would leave a 147px band under
     a ~627px floor set by the sale wall's corridor — a 250px hole that no
     amount of spacing hides. 240% is the smallest sheet that still fills the
     slide: ~4 sheets on screen at ~30% of the viewport each, against the
     first pass at 360% (2.5 sheets, ~44% each, section 846px = 99% of a
     393pt iPhone). Desktop and tablet override both below, untouched.

     195%/620 WAS the third and smallest step (~106px sheets at about 24% of a
     440pt viewport), reached by raising crop-h as crop-w fell — the two
     multiply, so trading them against each other was the only way to shrink
     the sheets without the band losing contact with the sale wall's floor.
     That is no longer what this tier wants, because 620 of 640 shows a kit
     sheet in FULL: the centre card is only 476 of the 640 stage units tall, so
     any window taller than ~477 cannot clip it at all. Measured live on avd-1
     (444px): cards overflowed the crop by 1-9px, i.e. the fan read as a
     finished row of whole sheets floating above the section's bottom edge,
     while desktop (crop-h 380, below) cuts every sheet at ~79.8% and runs the
     band off that edge. That mismatch is the "mobile should match desktop on
     the fan version" report, 2026-08-19.

     281%/430 CLIPS THE SHEETS WITHOUT REOPENING THE HOLE, and the two numbers
     MUST move together or it does. Band height is crop-w x crop-h / 1920, so
     dropping crop-h on its own would shorten the band — and because the sale
     wall pins the slide's height, every px of that comes straight back as a
     gap, the same void the 360%/590 -> 195%/620 history above was fighting.
     (Shifting the cards down inside the window instead is no better: it buys
     the clip at the bottom by opening the same distance above, which is what
     the +180 stage-unit offset baked into Parkside's card blocks was doing
     until it was reverted this same session.) So crop-w x crop-h is held at
     its measured constant (195 x 620 = 120900): any crop-h picked here sets
     crop-w = 120900 / crop-h, and the band stays ~279.6px at 444px. The
     1400px cap scales by the same factor (1400 x 620/448 = 1938) so the
     viewport at which it starts clamping is unchanged at ~718px.

     THE ONE TRADE THIS TIER CANNOT ESCAPE: sheet size vs clip depth. With the
     band held constant the two are locked — crop-h alone decides how much of a
     sheet is cut (centre card is 476.55 of the 640 stage units tall, so visible
     fraction = crop-h / 476.55), and crop-w alone decides how wide a sheet
     draws, but fixing the band means fixing their product. The measured
     range, and which rung is in use, is the table on the declaration below —
     kept in one place on purpose, because this comment carried a second,
     coarser copy of it for a day and the two immediately disagreed.

     476.55 is a hard ceiling, not a preference: a window taller than the centre
     card cannot clip it at all, which is what 620 was doing and why mobile read
     as a finished row of whole sheets.

     448 is the operator's second pass (2026-08-20, "slightly smaller to see
     more of the cards"), after 430 on 2026-08-19 for the same ask. Both rungs
     were measured live on By the Sea rather than picked by eye, and the count
     is a STEP function — this is the rung that actually buys a card:

       crop-h 430 / crop-w 281%   151px sheets, 3 whole cards, 90.2% visible
       crop-h 448 / crop-w 270%   145px sheets, 4 whole cards, 94.0% visible  <- here
       crop-h 458 / crop-w 264%   142px sheets, 4 whole cards, 96.1% visible
       crop-h 476 / crop-w 253%   136px sheets, 4 whole cards, ~100% (NO clip)

     Everything past 448 shrinks the sheets further and returns NOTHING —
     still 4 whole cards, 6 overlapping — while spending the clip that makes
     the band read as a fan running off the edge instead of a finished row.
     If a future ask is "smaller again", the honest answer is that it costs
     the clip and buys no cards; the count ceiling is ~4 at this viewport.
     The px cap tracks crop-h so the two stay in proportion: it is
     1400 x 620 / crop-h (the desktop cap scaled), 868000 / 448 = 1938. */
  --sp-hero-fan-crop-w: min(1938px, 270%);
  --sp-hero-fan-crop-h: 448;
  position: relative; /* positioning context for the fade; no z-index, so it
                         does NOT become a stacking context */
  width: var(--sp-hero-fan-crop-w);
  /* 24 on phones, back to the comp's 56 from 768 up. The visible gap between
     the CTA and the first sheet is this margin PLUS the copy's own 24px
     padding-bottom plus whatever slack `margin-bottom: auto` parks there —
     56 made that 111px on a 440pt screen, which read as a hole. */
  margin: 24px 0 0 calc(50% - var(--sp-hero-fan-crop-w) / 2);
  /* `1920 / var(...)`, NOT `calc(1920 / var(...))`. var() substitutes as text,
     so this stays the exact ratio `1920 / 380`; wrapping it in calc() collapses
     it to a rounded single number (5.05263) and quietly costs precision. */
  aspect-ratio: 1920 / var(--sp-hero-fan-crop-h, 380);
  overflow: hidden;
}

/* Figma's "Rectangle 149481795": a vertical multiply gradient that settles the
   card band into the background at the frame's bottom edge, present on every
   hero frame and previously unimplemented.

   THE DIVISOR IS A CONSTANT 380, AND IT MUST STAY ONE. It is tempting to
   divide by `--sp-hero-fan-crop-h` so the gradient tracks the cards' own stage
   coordinates — that was tried on 2026-08-07 and it is wrong. This gradient's
   job is to settle the band into the background AT THE CUT, and the cut is the
   crop's bottom edge, not a row of the stage. Dividing by a 560-tall crop put
   it at 52%-90% of the window: a glow band across the middle of the sheets
   with untinted sheet visible below it, ending 26px short of the edge.

   Dividing by the constant 380 makes 294/380 and 211/380 a RATIO OF THE CROP
   (77.4% and 55.5%) at every tier, so the ramp always starts 22.6% from the
   bottom and always runs past it. The element keeps its full 211-unit height
   and lets the crop's existing overflow:hidden clip it, exactly as the Figma
   frame edge clips the source rectangle — only the gentle first 41% of the
   ramp is ever seen, which is why the glow reads as a settle and not a wash.
   Pre-shortening it to the visible slice would silently drift if tuned.

   --top differs per slide because each slide rebases to its own topmost card:
   294 (fan), 301 (grid — its cards ride slide 1's origin, so it is NOT 294),
   295 (Parkside). All three verified against the raw absoluteBoundingBox.

   Colours are per KIT, not global: By the Sea shifts hue (#D3D7E5 -> #A2B0E2)
   while Parkside is a pure alpha ramp of its own #E8F1E8. Both stops matter —
   a gradient interpolates colour as well as alpha, so the transparent stop's
   hue is not cosmetic. */
.sp-hero-fan__cards-fade {
  position: absolute;
  left: 0;
  right: 0;
  /* FIGMA-EXACT, AND THE OVERFLOW IS THE DESIGN — do not "fix" it. Checked
     against node 2017:29729 on 2026-09-18 after exactly that mistake: the comp's
     hero frame is 1920x758, its gradient rectangle (149481795) sits at y=672 and
     is 211 tall, and the topmost sheet starts at y=378. So the crop is 380 units
     (758-378), the gradient begins 294 into it, and it runs 211 — the three
     numbers below, verbatim. It ends at 883 against a frame that stops at 758,
     which means the comp itself only ever shows 40.8% of the ramp and cuts the
     fan on a hard edge. That edge is drawn, not a bug.
     Measuring the render alone says otherwise and is convincing: the ramp reads
     "30% done at the cut" on a phone and 41% at 1440, which looks exactly like a
     gradient that was sized wrong. It was re-based to finish at the crop's bottom
     (02e8ec55) and the operator called the result immediately — too much glow.
     Reverted. Sample the comp before trusting a measurement that says a value is
     wrong; a number can be off-looking and still be the design. */
  top: calc((var(--sp-hero-fan-fade-top, 294) / 380) * 100%);
  height: calc((211 / 380) * 100%);
  background: linear-gradient(
    to bottom,
    var(--sp-hero-fan-fade-from, rgba(211, 215, 229, 0)) 0%,
    var(--sp-hero-fan-fade-to, rgba(162, 176, 226, 1)) 100%
  );
  mix-blend-mode: multiply;
  pointer-events: none;
  z-index: 2; /* above .sp-hero-fan__cards (z-index:1) */
}

.sp-hero-fan__cards {
  /* Stage height is a variable so a slide can be measured against a taller
     reference. The fan/grid slides use Figma's 640; the sale wall sets 781,
     its own frame height. Everything below divides by it, so one override
     re-bases every card on that slide. */
  --sp-hero-fan-stage-h: 640;
  position: relative;
  z-index: 1;
  width: 100%;
  aspect-ratio: 1920 / var(--sp-hero-fan-stage-h);
}

.sp-hero-fan__card {
  position: absolute;
  left: calc(50% + (var(--x) / 1920) * 100%);
  top: calc((var(--y) / var(--sp-hero-fan-stage-h, 640)) * 100%);
  width: calc((var(--w) / 1920) * 100%);
  height: calc((var(--h) / var(--sp-hero-fan-stage-h, 640)) * 100%);
  transform: translateX(-50%) rotate(var(--rot));
  transform-origin: center top;
  box-shadow: 0 1px 2px rgba(16, 16, 16, 0.06), 0 12px 28px rgba(16, 16, 16, 0.12);
  border-radius: 2px;
  overflow: hidden;
}

.sp-hero-fan__card-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
}

/* ---- Responsive tiers ---------------------------------------------------
   MOBILE-FIRST. The base rules above ARE the phone layout: the fan bleeds
   off both edges at 200% rather than cramming 9 rotated cards into a 390px
   viewport, and the type sits at its smallest step. Everything below is a
   min-width enhancement, at the vendor's own breakpoints (sp-tokens.css
   --sp-bp-*, D19) — no max-width patches, so no rule here undoes one above.

   Inverted from two stacked max-width blocks on 2026-08-07. The old form
   expressed the same three steps downward (base 46px, ≤991.98 → 32px,
   ≤767.98 → 26px) and relied on those two blocks NESTING below 768 with
   source order picking the winner. Output is unchanged at every width; the
   stacking is now explicit instead of implicit. Verified by a 13-width
   before/after geometry diff, design/mobile-first-refactor-plan.md §4. */

/* ≥768px — tablet. Type steps up and the fan tightens toward its reference
   box, but the copy column keeps the narrow padding until 992. */
@media (min-width: 768px) {
  .sp-hero-fan__eyebrow {
    font-size: 18px;
  }

  .sp-hero-fan__headline {
    font-size: 32px;
    /* scaled to the 32px/46px font-size ratio so each slide keeps its own
       Figma line break */
    max-width: calc(var(--sp-hero-fan-headline-w, 618px) * 32 / 46);
  }

  /* Buttons return to a row and pick up the comp's own geometry from here up.
     `2017:29973` is 50 tall with 16/46 padding — and BOTH of those already
     exist as tokens minted for this exact element: `--sp-space-hero-btn-pad-x`
     is commented "hero's two buttons only" and `--sp-btn-pad-x` is commented
     "hero uses --sp-space-hero-btn-pad-x instead". The hero had been using
     neither, hardcoding 18px 32px. min-height carries the 50 because the
     comp's frame is a fixed 50 that its own 16+13+16 padding does not sum to. */
  .sp-hero-fan__buttons {
    flex-direction: row;
    align-items: center;
    flex-wrap: wrap;
  }

  .sp-hero-fan__btn {
    min-height: var(--sp-btn-height, 50px);
    padding: var(--sp-btn-pad-y, 16px) var(--sp-space-hero-btn-pad-x, 46px);
    font-size: 16px;
  }

  /* Back to the Figma window: 380 of the 640-tall stage, i.e. sheets cut off
     just past halfway by the section's bottom edge. The phone tier opens this
     to 590 to fill a much shorter slide; from 768 up the sale wall no longer
     leaves a void worth filling. */
  .sp-hero-fan__cards-crop {
    --sp-hero-fan-crop-w: min(1400px, 140%);
    --sp-hero-fan-crop-h: 380;
    margin-top: 56px;
  }

  .sp-hero-fan__art-img {
    width: 22%;
  }
}

/* 768–991 only: the two background scenes (picks board threads bc702dac and
   804c1191). The left scene sat mostly off the left edge at this tier, so it
   comes in 6 points to reveal more of it; the right scene drops from 12.7% to
   30% of the width so it sits under the buttons rather than beside the
   headline. Desktop keeps the Figma values above. */
@media (min-width: 768px) and (max-width: 991.98px) {
  .sp-hero-fan__art-img--left {
    left: -5.979%;
  }

  .sp-hero-fan__art-img--right {
    margin-top: 30%;
  }

  /* 768–991 only: the fan fills the band instead of floating below a void
     (picks board thread 74fdfee2: "move bottom up to close the distance on
     this breakpoint"). The slide's height is pinned at 652 by the sale wall,
     the copy ends at 260, and the 380-unit window drew only ~224px at 820, so
     `margin-bottom: auto` on the copy parked 111px of nothing between the
     buttons and the sheets (126 at 768, 64 at 991). Letting the crop take the
     remaining height (aspect-ratio off, flex-grow on) puts the fan's top edge
     a fixed 32px under the copy at every width in the tier and shows more of
     each sheet, which is what the void was made of. The cards are sized by
     the stage WIDTH, not the window, so nothing rescales; overflow:hidden
     still cuts the sheets at the section edge, and the fade gradient tracks
     the window as before. Desktop (≥992) keeps the Figma 380 window. */
  .sp-hero-fan__slide:not(.sp-hero-fan__slide--wall) .sp-hero-fan__cards-crop {
    /* basis 0, not auto: with `auto` the crop's content (the full 640-unit
       stage) became its size and the kit slides grew past the wall's 652
       (measured 670 at 820, 750 at 991). Basis 0 makes it share only the
       leftover height, so the slide stays at the wall's 652. */
    flex: 1 1 0px;
    min-height: 0;
    aspect-ratio: auto;
    /* 2026-09-17 (picks board thread a7f47651: "move down so center item has
       some cutoff"): the fan drops until the centre sheet loses ~15% at the
       slide's bottom edge at every width in the tier. The centre sheet is
       264 tall at 768 and 341 at 991 while the slide stays 652, so a fixed
       gap cannot cut it evenly; this line passed through (768, 168) and
       (991, 102). `%` margins resolve against the containing block's width,
       which is what the sheet's size follows (and, unlike vw, excludes the
       scrollbar). Was a flat 32px.

       2026-09-17, same thread's follow-up ("reduce this height distance by
       1/3"): both terms are two thirds of what they were, so the buttons-to-
       fan gap goes 195 -> ~130 at 768 and the line keeps its slope. The
       sheets do not lose their cut in the process because the slide got
       60px shorter in the same pass (see the wall's width below) — the fan
       moves up and the floor moves up with it. */
    margin-top: max(32px, calc(263px - 19.7%));
  }
}

/* ≥992px — desktop. Full Figma geometry: headline at its 1:1 46px reference
   size, the copy column at its designed padding, the fan at its true 1920
   reference width, and the background art back on its two per-side widths. */
@media (min-width: 992px) {
  .sp-hero-fan__headline {
    font-size: 46px;
    max-width: var(--sp-hero-fan-headline-w, 618px);
  }

  .sp-hero-fan__inner {
    padding: 64px 24px 32px;
  }

  /* COPY SITS LOWER (9/14 feedback, node 2908:10832: "We should move this
     text down the text is too top heavy."). Measured at 1920 before this: 64px
     above the eyebrow, 124px between the buttons and the topmost sheet (32
     inner padding + 56 crop margin + 36 of empty crop), so the copy hugged
     the header while a band of bare colour sat under it. Two moves, both on
     the kit slides only (the sale wall lays its copy out on its own):
       - 30px moves from the crop's top margin to the inner's top padding,
         which centres the copy in that space at 1920 (94 above, 94 below);
       - the grid slack a kit slide has under the taller sale wall (85px at
         1440, 72 at 1200) is split above and below the copy instead of all
         parked below it: `margin-top: auto` beside the existing
         `margin-bottom: auto`. The band stays flush at the bottom, since
         the crop still gets no slack of its own. */
  .sp-hero-fan__slide:not(.sp-hero-fan__slide--wall) > .sp-hero-fan__inner {
    padding-top: 94px;
    margin-top: auto;
  }

  .sp-hero-fan__slide:not(.sp-hero-fan__slide--wall) .sp-hero-fan__cards-crop {
    margin-top: 26px;
  }

  /* The comp's copy column is two nested auto-layouts, and their gaps are the
     two numbers below: `2017:29969` stacks eyebrow over headline at gap=14,
     and `2017:29968` stacks that whole block over the buttons at gap=32. The
     build had 12 and 28, which compounded with the 2px inner-padding delta to
     seat the button row 8px above where the comp puts it (236 vs 244). */
  .sp-hero-fan__eyebrow {
    margin-bottom: 14px;
  }

  .sp-hero-fan__headline {
    margin-bottom: 32px;
  }

  .sp-hero-fan__cards-crop {
    --sp-hero-fan-crop-w: min(1920px, 100%);
  }

  .sp-hero-fan__art-img--left {
    width: 27.969%;
  }

  .sp-hero-fan__art-img--right {
    width: 26.875%;
  }
}

/* ≥1921px — the bottom fade runs to the section's edges (9/14 feedback,
   node 2908:10828: "Need to have this gradient expand to edges").

   The crop stops growing at 1920, and the fade lives inside it, so on a wider
   screen the tint ended in a hard vertical edge with bare background beside
   it: 190px each side at 2300. The fade has to leave the crop sideways while
   the crop still cuts the sheets at its bottom edge, so the crop clips on the
   Y axis only (`clip` is the one overflow value that does not force the other
   axis to `auto`) and the sheets get their own horizontal clip, keeping them
   inside the 1920 stage exactly as before. The fade then spans 100vw centred
   on the crop; the section's own overflow:hidden trims the scrollbar's worth
   of overshoot that `vw` carries.

   Kit slides only. The sale wall has its own crop and fades with different
   geometry and a normal blend, and nobody asked for those to change. Below
   1921 the crop already fills the section, so nothing here would move. */
@media (min-width: 1921px) {
  .sp-hero-fan__cards-crop:not(.sp-hero-fan__cards-crop--wall) {
    overflow-x: visible;
    overflow-y: clip;
    /* aspect-ratio's automatic minimum is content-sized whenever overflow
       is visible, so without this the 640-tall stage stretches the 380 crop
       to 640 and the band doubles in height. Measured, not assumed. */
    min-height: 0;
  }

  .sp-hero-fan__cards-crop:not(.sp-hero-fan__cards-crop--wall) .sp-hero-fan__cards {
    clip-path: inset(-100vh 0 -100vh 0);
  }

  .sp-hero-fan__cards-crop:not(.sp-hero-fan__cards-crop--wall) .sp-hero-fan__cards-fade {
    left: calc(50% - 50vw);
    right: calc(50% - 50vw);
  }
}

/* ---- Card entrance: "dealing the fan" ----------------------------------
   The fan is a stack of sticker SHEETS, so the entrance is the gesture of
   laying them down — the same metaphor (and the same easeOutQuint curve)
   the mobile drawer's accordion cascade already speaks in. Each card fades
   up 36px and settles from 0.975 scale: rising = emerging from the crop
   edge below, scaling up = coming toward the viewer, so both halves of the
   move read as one hand setting a sheet into place. No rotation component
   on purpose — every card lands at a DIFFERENT final --rot, so a shared
   rotational settle would read as the whole fan wobbling rather than as
   nine sheets being dealt.

   ORDER IS FREE, AND IT IS THE POINT. The stagger is keyed off --i
   (forloop.index0), which is block order, which is Figma's layer/paint
   order — and that order happens to sweep the right wing outside-in
   (x +604 → +432 → +271), then the left wing outside-in (-627 → -446 →
   -274), then the inner pair, and land on the center hero card (x -3,
   y 0) LAST. So the fan builds inward from both edges and closes on its
   own focal card. Don't reorder the blocks to "fix" the stagger; the
   z-stacking rule in the .liquid header already forbids it, and this is
   the second reason.

   70ms/step over 9 cards = 560ms of spread + a 700ms settle ≈ 1.26s total.
   Capped at index 9 so the 12-block ceiling can't trail off.

   Deliberately NOT animated: the eyebrow/headline/buttons. The headline is
   the section's likely LCP element and hiding it behind opacity:0 would
   push LCP back by the entrance duration for a purely decorative gain. The
   cards are aria-hidden, lazy-loaded decoration — animating them is free.

   Plumbing: rides the vendor's animate-on-scroll.js (body.cc-animate-enabled
   + [data-cc-animate] observed by IntersectionObserver → .cc-animate-in),
   so this adds zero new JS and inherits the vendor's three safety nets,
   mirrored below for the per-card state: `.no-js`, prefers-reduced-motion,
   and the <768px gate (the theme setting is `animations_enabled_desktop`).

   The moving properties are `translate`/`scale`, NOT `transform` — the
   card's `transform: translateX(-50%) rotate(var(--rot))` carries its
   Figma-exact centering and fan angle and must not be touched. The
   individual transform properties compose ahead of it, so the entrance
   layers on top of the layout instead of overwriting it. */

/* animate-on-scroll.css hides EVERY [data-cc-animate] with a blanket
   opacity:0 and only restores it for its own fade-in/fade-in-up presets.
   This section is neither — it's an observer target only, and the reveal
   happens per-card below. Out-specifies `.cc-animate-enabled
   [data-cc-animate]` so the vendor's hide can't swallow the stagger. */
.cc-animate-enabled .sp-hero-fan[data-cc-animate] {
  opacity: 1;
}

@media (min-width: 768px) {
  /* Resting/hidden state: every card of every slide. This is also the EXIT
     state, which is why the outgoing slide's cards retreat downward the
     same way they arrived rather than simply blinking out. */
  .cc-animate-enabled .sp-hero-fan[data-cc-animate] .sp-hero-fan__card {
    opacity: 0;
    translate: 0 36px;
    scale: 0.975;
  }

  /* Exit timing only applies once the section has been revealed — before
     that the hidden state must be instant, or the first paint animates
     from nothing. No stagger on the way out: a departing slide shouldn't
     take 1.26s to leave. */
  .cc-animate-enabled .sp-hero-fan[data-cc-animate].cc-animate-in .sp-hero-fan__card {
    transition:
      opacity 260ms var(--sp-hero-fan-ease-exit),
      translate 260ms var(--sp-hero-fan-ease-exit),
      scale 260ms var(--sp-hero-fan-ease-exit);
  }

  .cc-animate-enabled .sp-hero-fan[data-cc-animate].cc-animate-in .sp-hero-fan__slide.is-active .sp-hero-fan__card {
    opacity: 1;
    translate: 0 0;
    scale: 1;
    transition:
      opacity 520ms var(--sp-hero-fan-ease),
      translate 700ms var(--sp-hero-fan-ease),
      scale 700ms var(--sp-hero-fan-ease);
    /* after the shorthand on purpose — the shorthand resets delay to 0s.
       --sp-hero-fan-lag is 0 on first reveal and is set by the JS for
       subsequent slide changes, so an incoming slide waits for the
       outgoing one to clear instead of crossing through it. Capped at 11
       to match max_blocks' ceiling on cards per slide. */
    transition-delay: calc(min(var(--i, 0), 11) * 70ms + var(--sp-hero-fan-lag, 0ms));
  }
}

/* ---- Side arrows ----
   Node 2587:23170 "Arrow Button": a 44x44 frame (r=366 — any radius past
   half the box is a circle) filled #000000 at 40%, holding an 8.8x17.6
   chevron stroked #ffffff at 2px inside a 35.2 inner frame.

   Distinct from the product-card arrows landed the same day (2587:23162 /
   2587:23166), and deliberately so: those are 29.16px, taupe at 20%, with a
   1.3255px dark chevron, because they sit ON a white photo tile inside a
   small card. These sit on full-bleed kit artwork whose colour changes with
   every slide — pastel blues, greens, creams — so they need a fill that
   holds contrast against all of them. Black at 40% does; the card recipe
   would disappear into a pale slide. Two surfaces, two recipes, both from
   their own node.

   NOT SHOWN BELOW 768px. At phone width the fan already bleeds off both
   edges, so an overlaid control has nowhere quiet to land — the same reason
   the dots drop out of overlay and into normal flow there (see the dots
   block below). The dots remain the way to change slides on a phone.

   The 24px inset is a house value, flagged per figma-ref rule 3: the node is
   a standalone component and carries no placement. 24px is the section's own
   narrow-tier gutter (--sp-space-6), so the arrows sit on a line the layout
   already uses rather than on a new one.

   z-index 5 matches the dots — above the slide artwork and its crop, below
   nothing that matters. Hidden from the a11y tree is NOT wanted here: unlike
   the vendor's product-card arrows these are real, reachable controls with
   their own labels, so they stay in tab order. */
/* MOBILE FIRST: the arrows exist at every width (2026-08-29, operator). They
   were >=768 only when first built, on the reasoning that the fan bleeds off
   both edges on a phone so an overlaid control has nowhere quiet to land —
   the same argument the dots use to drop out of overlay there. That argument
   is about SIZE and PLACEMENT, not about whether the control belongs: a phone
   still needs a way to reach slide 4 that is not "wait 21 seconds". So the
   base tier keeps them and shrinks them instead — 36px against the desktop
   44, tucked 12px in rather than 24 — and the tier below scales them up.

   Swipe (sp-hero-fan.js) is the primary gesture on touch and the arrows are
   the visible affordance beside it; on desktop, where nobody thinks to drag a
   hero, they are the only discoverable one. */
.sp-hero-fan__arrow {
  position: absolute;
  z-index: 5;
  /* ON THE DOTS' LINE, NOT THE SLIDE'S MIDDLE — phones only. Vertical centring
     works on desktop, where the CTAs are a few hundred px wide in a 1920 frame
     and the edges are empty. At 390 the CTAs are nearly full-width and the
     hero's vertical centre is exactly where they sit, so a centred arrow lands
     ON "SHOP SUMMER STICKERS" — caught in a capture, and worse than untidy:
     the arrow paints above, so it would eat taps meant for the primary CTA.

     Dropping them to the bottom line the dots already occupy puts them in the
     one horizontal band of a phone hero guaranteed to be free of copy and
     buttons, and reads as one navigation cluster with the dots instead of two
     systems. The >=768 tier centres them again, where that is both safe and
     what the comp draws. */
  bottom: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  transition: background-color 0.2s ease;
  border: 0;
  border-radius: 50%;
  /* FROSTED 2026-09-26 (operator: "update all next/back arrow that flow over
     images ... that frosted soft faded out look"), was a 40%-black disc with a
     white chevron. Same glass as the quick buy bar; tokens in sp-tokens.css. */
  background-color: var(--sp-frost-fill);
  -webkit-backdrop-filter: var(--sp-frost-filter);
  backdrop-filter: var(--sp-frost-filter);
  color: var(--sp-color-graphite);
  cursor: pointer;
}

.sp-hero-fan__arrow svg {
  display: block;
  width: 16px;
  height: 16px;
}

/* TOUCH TARGET, not a size change. The disc is 36px on this tier by design —
   the base rule above explains why a phone gets a smaller circle than the 44
   desktop draws. 36 is also under the 44px touch floor, which a screenshot
   cannot show and a thumb can. This extends the hit area to 44x44 without
   moving a painted pixel: a transparent pseudo-element is part of the
   button's own box for hit testing, so it takes the tap and the button fires.
   The button is position:absolute, so it is the containing block and the
   overlay centres on the disc rather than on the section.

   Only 4px overhangs each side against a 12px gutter inset, so nothing else
   is reachable underneath it. At >=768 the button is already 44x44 and this
   sits exactly coincident with it — harmless, not a second target. */
.sp-hero-fan__arrow::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

.sp-hero-fan__arrow--prev {
  left: var(--sp-space-3); /* 12px */
}

.sp-hero-fan__arrow--next {
  right: var(--sp-space-3);
}

/* The slide stack owns the horizontal gesture; the browser keeps the vertical
   one, so a swipe changes slides and a scroll still scrolls the page. Without
   this the two compete and neither feels right. */
.sp-hero-fan__stack {
  touch-action: pan-y;
}

@media (min-width: 768px) {
  .sp-hero-fan__arrow {
    position: absolute;
    z-index: 5;
    top: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Geometry differs from the base tier; the FILL no longer does — see the
       faded treatment below, which is desktop-only. Vertical centring is
       restored here — see the base rule for why phones do not get it. */
    top: 50%;
    bottom: auto;
    transform: translateY(-50%);
    width: 44px;
    height: 44px;

    /* FADED UNTIL HOVER — desktop only. Operator pick, 2026-09-18, at the 45%
       rest state captured on board 3Qt2cc41zsfgnruzaVemxj rather than the 65%
       I recommended; 45% is the strength they looked at and chose.

       Why this tier and not the phone: there is no hover on touch, so a
       control that only appears on hover has no equivalent there. The base
       tier keeps its solid black-at-40% disc, which is the same operator's
       separate call on the disc colour — the two answers are about two
       different surfaces, not a contradiction.

       Cream and graphite are both existing tokens, so this adds none. rgba()
       is spelled out because the fill needs an alpha the token cannot carry.

       THE COMP DOES NOT DRAW THIS, OR ANY ARROW. 2017:29729 is a static fan:
       no controls, no dots, nothing at the edges. Every arrow value in this
       file is a build decision with no comp behind it, so there is nothing
       here that a Figma read can contradict — and equally nothing it can
       confirm. Do not go looking for a node to check these against. */
    opacity: 0.45;
    background-color: var(--sp-frost-fill); /* frosted, 2026-09-26; was cream @ 55% */
    color: var(--sp-color-graphite);
    transition:
      opacity 380ms cubic-bezier(0.4, 0, 0.2, 1),
      background-color 380ms cubic-bezier(0.4, 0, 0.2, 1);
  }

  /* The chevron. The snippet ships a 24px viewBox with the glyph drawn
     inside it; 20px puts the drawn stroke at the comp's 8.8x17.6 (the base
     tier runs 16px to match its smaller circle). Colour comes from
     `currentColor` on the button, so hover moves the fill without touching
     the icon. */
  .sp-hero-fan__arrow svg {
    width: 20px;
    height: 20px;
  }

  /* Focus rides with hover deliberately. A keyboard user who cannot trigger
     hover would otherwise be tabbing to a control at 45% — the fade is a
     mouse affordance, not a statement that the button is unavailable. */
  .sp-hero-fan__arrow:hover,
  .sp-hero-fan__arrow:focus-visible {
    opacity: 0.96;
    background-color: var(--sp-frost-fill-hover);
    /* No hover shadow (operator 2026-09-27): every frosted arrow answers hover with the lighter frost only,
       matching the product gallery, card and full-screen viewer arrows. Was 0 2px 10px rgba(0,0,0,.18). */
  }

  /* Focus ring on the light artwork, not on the button — the disc is now a
     pale cream, so an ink ring reads against the slide either way. (This
     comment used to say "a white ring on a 40%-black circle"; that circle is
     the phone's now, not this tier's.) */
  .sp-hero-fan__arrow:focus-visible {
    outline: 2px solid var(--sp-color-ink);
    outline-offset: 2px;
  }

  .sp-hero-fan__arrow--prev {
    left: var(--sp-space-6); /* 24px — house value, see above */
  }

  .sp-hero-fan__arrow--next {
    right: var(--sp-space-6);
  }
}

@media (prefers-reduced-motion: reduce) {
  .sp-hero-fan__arrow {
    transition: none;
  }
}

/* ---- Slide dots ----
   Parked at the section's bottom-right and taken OUT of flow. In flow they
   added a strip of dead background under the cropped card band — measured
   at 25px (crop bottom 934.2 == dots top 934.2; section 780 tall vs stack
   755) — which read as the fan being chopped short rather than bleeding off
   the section's bottom edge. Absolute positioning hands those 25px back, so
   the crop's bottom edge IS the section's bottom edge.

   The right edge aligns to the site's content grid, not the viewport. The
   vendor container is max-width:1400 with 60px padding, so its content edge
   sits (100vw - 1400)/2 + 60 in from the right — i.e. calc(50% - 640px),
   floored at 60px once the viewport is narrower than the container. That
   puts the dots on the same line every other section's content ends on
   (live-measured: 320px from the right at 1920, matching "View all").

   Colour: flat ink, not per-kit-derived. A per-slide-derived active colour
   (a darkened/saturated version of that slide's own pale background — sea
   blue for By The Sea, leaf green for Parkside) shipped first and read as
   muddy on every slide regardless of hue — the desaturated-pastel
   derivation itself was the problem, not one bad colour. --sp-color-ink
   matches every other "selected/primary" control on this site (the solid
   CTA buttons right above these dots included), so the dots read as part
   of the same UI language instead of carrying their own colour system.
   Inactive dots are the same ink at 50% alpha (~3.46:1 against any of
   these pale kit backgrounds, comfortably past WCAG 1.4.11's 3:1 floor for
   non-text UI — the low-alpha same-hue tint this replaced measured only
   ~2.1:1).

   Shape: plain circles, active one simply bigger — not the elongating
   pill this went through first. The pill (rectangular, wider than tall)
   read as a chunky black blob against these softer, pastel/floral kit
   slides specifically — flagged on three different slides/screenshots
   before landing on "the pill itself is the problem," not any one colour
   or slide. A circle scales up gracefully at any size and never reads as
   a rectangle no matter how small it renders, which a pill's flat parallel
   edges can. */
/* Phone: the fan is already bleeding off both edges and there is no content
   grid to align to, so the dots sit in normal flow underneath and centre.
   Overlaying the bottom-right of a phone-width fan would put them on top of
   the cards with nowhere quiet to land. They only lift out of flow and park
   against the content edge at ≥768 — see the tier block below. */
.sp-hero-fan__dots {
  position: relative;
  z-index: 5;
  right: auto;
  bottom: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  /* -20px pulls the row up over the crop's own last 20px (crop has its own
     overflow:hidden below it, so nothing of the dots themselves gets
     clipped) — they were already flush against the crop's bottom edge at
     0px, so there's no remaining gap to close any other way. */
  margin: -20px 0 0;
  padding: 0 0 20px;
}

/* Kit-fan slides need MORE pull than the -20px above to land inside the
   art the same way the sale wall already does. The wall's crop is out of
   flow (absolute, inset:0 — see its own rules), always genuinely flush
   with the slide's bottom edge with zero residual, so -20px lands it
   cleanly over the tile grid's own bottom row. A kit-fan slide's crop is
   in normal flex flow and — even after the redirect fix above — keeps a
   small ~4px residual (Parkside included, which gets the biggest direct
   supplement and still can't fully close it), so the same -20px only
   ever reaches the plain background just past the fan's edge, never the
   art itself. `.sp-hero-fan__dots` isn't a descendant of any one slide
   (it's a single row shared by the whole carousel, sibling to the
   stack), so slide-scoped selectors can't reach it directly — :has()
   reads which slide is currently .is-active instead. */
.sp-hero-fan:not(:has(.sp-hero-fan__slide--wall.is-active)) .sp-hero-fan__dots {
  margin-top: -44px;
}

@media (min-width: 768px) {
  .sp-hero-fan__dots {
    position: absolute;
    /* CENTRED (operator call 2026-08-29, marked in red on the annotated
       hero: the dots were circled where they sit today, bottom-right, and a
       red bar drawn at the bottom CENTRE where they belong).

       Was `right: max(60px, calc(50% - 640px))`, which parked the row
       against the content column's right edge — the same edge the copy and
       CTAs align to, which is why it was chosen. It reads as an orphan
       against the fan though: the fan is symmetric about the slide centre
       and the dots were the only element on that line, sitting off to one
       side of it.

       Spanning left:0/right:0 and centring the flex row keeps them centred
       on the SLIDE rather than on the content column, which are the same
       axis here — the copy column is itself centred — so this holds at every
       width without a second breakpoint. `justify-content: center` also
       matches the mobile tier's own value, so the two tiers now agree
       instead of diverging; that removes the reason the `normal` note below
       existed, and the byte-identical-computed-style contract it refers to
       is superseded by this deliberate change. */
    left: 0;
    right: 0;
    bottom: 30px;
    justify-content: center;
    padding: 0;
  }
}

.sp-hero-fan__dot {
  width: 8px;
  height: 8px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.5);
  cursor: pointer;
  transition:
    background-color 320ms var(--sp-hero-fan-ease),
    transform 320ms var(--sp-hero-fan-ease);
}

.sp-hero-fan__dot:hover {
  background: var(--sp-color-ink, #000);
}

.sp-hero-fan__dot.is-active {
  background: var(--sp-color-ink, #000);
  transform: scale(1.375); /* 8px -> 11px */
}

.sp-hero-fan__dot:focus-visible {
  outline: 2px solid var(--sp-color-ink, #000);
  outline-offset: 3px;
}

/* Vendor's own escape hatches, extended to the per-card state (its rules
   only ever target the [data-cc-animate] element itself, not descendants).
   Without JS the first slide is the only one server-rendered `.is-active`,
   so this correctly reveals exactly one slide's worth of cards. */
.no-js .sp-hero-fan__card {
  opacity: 1 !important;
  translate: none !important;
  scale: none !important;
}

.no-js .sp-hero-fan__slide:not(.is-active) {
  display: none;
}

.no-js .sp-hero-fan__dots {
  display: none;
}

@media (prefers-reduced-motion: reduce) {
  .sp-hero-fan__btn {
    transition: none;
  }

  .sp-hero-fan,
  .sp-hero-fan__art,
  .sp-hero-fan__art-img,
  .sp-hero-fan__inner,
  .sp-hero-fan__dot {
    transition: none !important;
  }

  /* Killing the transition alone would leave an inactive slide's art
     permanently hinged open — it would just jump there instead of easing.
     The swing has to be flattened outright, on every slide. */
  .sp-hero-fan__art-img,
  .sp-hero-fan__slide:not(.is-active) .sp-hero-fan__art-img--left,
  .sp-hero-fan__slide:not(.is-active) .sp-hero-fan__art-img--right {
    rotate: none !important;
    translate: none !important;
  }

  /* Cards of the ACTIVE slide only — inactive slides must stay hidden or
     all three would stack on top of each other. */
  .cc-animate-enabled .sp-hero-fan[data-cc-animate] .sp-hero-fan__slide.is-active .sp-hero-fan__card {
    opacity: 1 !important;
    translate: none !important;
    scale: none !important;
    transition: none !important;
  }

  .cc-animate-enabled .sp-hero-fan[data-cc-animate] .sp-hero-fan__slide:not(.is-active) .sp-hero-fan__card {
    transition: none !important;
  }
}

/* ==========================================================================
   SALE WALL  (Figma 2017:30864 "buy 3 get one free")
   A third arrangement, and the only one that needed more than new numbers.
   Two things genuinely differ from the fan/grid:

     1. The copy sits ON the sheets rather than above them. In the fan the
        stack is copy-then-cards in normal flow; here the wall fills the
        whole slide and the copy floats over a corridor deliberately left
        empty through the middle of the composition. That corridor is why
        there is no scrim — Figma solves the contrast by not putting tiles
        where the words go, and a scrim would flatten the whole wall to
        solve a problem the layout has already solved.
     2. The stage is 781 reference units tall (the Figma frame) instead of
        640, so --y and --h re-base against it via --sp-hero-fan-stage-h.

   The wall does NOT set its own height: it is absolutely positioned to fill
   whatever height the kit slides establish. That keeps the one-shared-height
   rule intact — adding this slide must not resize the section, or the dots
   stop sitting flush with the crop's bottom edge.
   ========================================================================== */

.sp-hero-fan__slide--wall {
  /* Drives the wall's size AND the copy's offset, so the two can never
     drift apart.

     WHY A PIXEL FLOOR AND NOT A LADDER OF PERCENTAGES. The corridor the copy
     sits in is a fixed FRACTION of the wall (stage rows 250-549, so 299 of
     781), while the copy inside it is a fixed PIXEL height — the headline
     does not shrink with the viewport. Requiring the corridor to fit the
     copy gives

         wall_width >= copy_height x 781 / (299 x 781/1920)
                    =  copy_height x 6.423

     in which the viewport width cancels out entirely: the wall needs a
     constant number of PIXELS, whatever the screen. Measured copy height is
     223px down to 768 (46px headline, two lines), so 223 x 6.423 = 1432px,
     and 1690 carries ~18% margin for a longer headline. `max(100%, …)` then
     keeps it full-bleed on anything wider. This replaced a five-step
     breakpoint ladder that was hand-tuned and still collided at 1280, 1100,
     990 and 390.

     Phone still gets a BIGGER wall than desktop, because below 768 the
     headline wraps onto more lines and the copy gets TALLER. But the wall is
     also what makes the phone hero tall: its padding is 0.2745 x this value,
     so 1920 spent 527px of vertical space on padding alone and pinned the
     section at 846px — 99% of a 393pt viewport. 1550 is chosen against two
     constraints, not by eye. Floor: the corridor inequality above gives
     0.15573 x 1500 = 234px against a measured 22px-headline copy of 215px,
     i.e. 9% headroom. Ceiling: this value alone decides the section's height
     on every phone (its padding is 0.2745 x it, a CONSTANT number of pixels
     since the viewport cancels out), and anything taller than the kit slides'
     own ~600px leaves slack that `margin-bottom: auto` parks between the CTA
     and the sheets as a visible hole. 1550 lands the wall on the kit slides
     on. Do not raise it without re-measuring that gap, or lower it
     without re-measuring the copy — the failure mode below is words landing
     on tiles.
     The 1690 desktop value is restored at ≥768 in the tier block below. */
  --sp-hero-fan-wall-w: max(100%, 1500px);
}

@media (min-width: 768px) {
  .sp-hero-fan__slide--wall {
    --sp-hero-fan-wall-w: max(100%, 1690px);
  }
}

/* 768–991: a slightly shorter hero (picks board thread 4504c990, "make hero
   slightly less tall on tablet"). The wall sets the shared slide height
   through its own padding — 250/1920 above the copy and 277/1920 below, both
   of this width — so the height is (250+277)/1920 x wall_w + the copy: 464 +
   188 = 652 at 1690. 1470 buys back 60px and lands the slide on 592. The
   corridor floor in the block above still holds with room to spare: the copy
   is ~188px here (32px headline), and 188 x 6.423 = 1207, well under 1470.
   Nothing else in the tier reads this variable, and ≥992 keeps 1690. */
@media (min-width: 768px) and (max-width: 991.98px) {
  .sp-hero-fan__slide--wall {
    --sp-hero-fan-wall-w: max(100%, 1470px);
  }
}

.sp-hero-fan__cards-crop--wall {
  /* Fills the slide instead of being a 380-tall band under the copy. Inset
     to 0 rather than given an aspect-ratio so it inherits the section's
     height rather than driving it. */
  position: absolute;
  inset: 0;
  width: auto;
  margin: 0;
  aspect-ratio: auto;
  overflow: hidden;
}

.sp-hero-fan__cards--wall {
  --sp-hero-fan-stage-h: 781;
  /* The wall is taller than the frame that shows it (tiles run from -309 to
     1301 in stage units, a 1610 span inside 781), so it is anchored to the
     stage top and the crop clips the rest — exactly as the Figma frame
     clips its own group. */
  position: absolute;
  top: 0;
  left: calc((100% - var(--sp-hero-fan-wall-w, 100%)) / 2);
  width: var(--sp-hero-fan-wall-w, 100%);
  aspect-ratio: 1920 / 781;
}

/* Figma's frame fill: a vertical wash of #f5f0ed from 37% to 100% alpha.
   Painted BEHIND the tiles (z-index 0 against .sp-hero-fan__cards' 1), which
   is where Figma has it — it is the frame's background, not an overlay, and
   putting it on top would dim the sheets it is meant to sit behind. */
.sp-hero-fan__cards-crop--wall::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  background: linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.63) 0%,
    rgba(255, 255, 255, 0) 100%
  );
}

/* A sheet shown on its side. Figma stores these as LANDSCAPE rectangles
   carrying the same portrait source art, so the image is rotated inside the
   tile rather than cropped — `object-fit: cover` on a 2:1 box would throw
   away most of a 1:2 sheet.

   The pre-rotation box has to be the tile's dimensions swapped. Card width
   renders as (--w / 1920) of the crop and card height as (--h / stage) of a
   crop that is itself (stage / 1920) as tall as it is wide, so the stage
   cancels and the rendered tile is exactly --w : --h. That makes the swap
   expressible as a plain ratio of the two variables. */
.sp-hero-fan__card--turned .sp-hero-fan__card-img {
  position: absolute;
  top: 50%;
  left: 50%;
  width: calc(var(--h) / var(--w) * 100%);
  height: calc(var(--w) / var(--h) * 100%);
  transform: translate(-50%, -50%) rotate(90deg);
  object-position: center;
}

/* THE COPY'S OFFSET MUST BE DERIVED FROM THE WALL'S SCALE, NEVER FIXED PX.
   Figma leaves a clear corridor through the CENTRE of the composition —
   stage rows 195 to 549 have no tiles near the middle — and the copy lives
   in it. The wall scales with viewport width, so a fixed px offset only
   lands in that corridor at exactly 1920: at 1440 the wall shrank to 586px
   tall while the copy stayed at 263px down, which put the buttons straight
   through the lower tile band.

   Expressing the offset as a fraction of --sp-hero-fan-wall-w fixes it for
   good, because percentage padding resolves against WIDTH and the wall's
   height is a pure function of its width. The copy therefore sits at stage
   row 250 at every viewport and every wall scale — the algebra cancels.

   Padding rather than absolute centring so the slide still establishes a
   height of its own if the sale slide is ever the only one in the section;
   with the wall absolute, nothing else would. 250 + 277 = 527, matching the
   kit slides' own copy block, so this slide never becomes the tallest and
   never drives the section taller (which would unstick the dots from the
   crop's bottom edge). */
.sp-hero-fan__slide--wall .sp-hero-fan__inner {
  position: relative;
  z-index: 2;
  padding-top: calc(250 / 1920 * var(--sp-hero-fan-wall-w, 100%));
  padding-bottom: calc(277 / 1920 * var(--sp-hero-fan-wall-w, 100%));
}

/* The sale frame has no fade rectangle in Figma, and without one its tiles ran
   to the crop's bottom edge and stopped dead against the section below. It
   needs the same resolution the kit heroes get, but the geometry cannot be
   shared: the wall crop is inset:0 (it fills the slide) rather than a 380-unit
   band, so the kit fade's `top: 294/380` would land far above the tiles. This
   one anchors to the crop's bottom instead.

   Height matches the kit fade's physical 211 stage units, expressed against
   the wall's own 1920-referenced width so it tracks the composition at every
   viewport exactly as the tiles do.

   That width basis is why this is `padding-top` on a zero-height box and not
   `height`. --sp-hero-fan-wall-w is `max(100%, 1690px)`, and a percentage in
   `height` resolves against the parent's HEIGHT — so the term collapsed to the
   1690px floor at every viewport and the fade came out 185.7px flat, 12% short
   at 1920 where the wall is actually 1920 wide. Percentage PADDING always
   resolves against the containing block's width, which is the basis this needs.
   Measured: 211px at 1920, 185.7px where the wall sits on its 1690 floor.

   NORMAL blend, not the kit fade's multiply. Multiplying a light ramp over a
   near-white sticker tile only TINTS it — the artwork stays legible right to
   the cut, which is the very thing that read as "chopped off". A normal ramp
   into the slide's own background actually dissolves the tiles into it. */
.sp-hero-fan__cards-fade--wall {
  top: auto;
  bottom: 0;
  height: 0;
  /* Walked down twice at the operator's call (2026-08-06): the kit fade's 211 →
     105.5 → 84 units, keeping progressively more of each sheet in focus. 84 is
     about the floor: much shallower and the ramp stops reading as a fade and
     starts reading as the hard cut it exists to fix. Combined with the 0.67
     opacity below, it is both shorter and lighter than the kit heroes'. */
  padding-top: calc(84 / 1920 * var(--sp-hero-fan-wall-w, 100%));
  background: linear-gradient(
    to bottom,
    var(--sp-hero-fan-fade-from, rgba(245, 240, 237, 0)) 0%,
    var(--sp-hero-fan-fade-to, rgba(245, 240, 237, 1)) 100%
  );
  mix-blend-mode: normal;
  /* Softened a third at the operator's call (2026-08-06): the full-strength ramp
     erased the bottom row outright, and the sheets read better still faintly
     present at the edge. Scaled on OPACITY, not on the ramp's height — a shorter
     ramp still reaches full opacity at the bottom, so it hides the same amount of
     art over a shorter distance and lands as a HARDER edge, which is the opposite
     of softening. Opacity scales the whole ramp uniformly and leaves the
     transparent stop at zero. */
  opacity: 0.67;
}

/* The wall's TOP edge, cut just as flat as its bottom edge was.

   Deliberately lighter and shallower than the bottom — 56 units at 0.45 against
   the bottom's 84 at 0.67. The site header sits directly above this edge, so the
   eye already takes that white bar as the frame; matching the bottom's weight
   reads as a smudge under the header rather than as the wall running past the
   frame. The bottom edge has no such helper — it meets the next section cold —
   which is why the two are asymmetric on purpose. Don't "tidy" them to match.

   Gradient runs the other way (opaque at the top, transparent below), and the
   same padding-top-not-height rule applies: --sp-hero-fan-wall-w is
   max(100%, …px), and a percentage in `height` resolves against the parent's
   HEIGHT, which would silently pin this to the px floor. */
.sp-hero-fan__cards-fade--wall-top {
  top: 0;
  bottom: auto;
  height: 0;
  padding-top: calc(56 / 1920 * var(--sp-hero-fan-wall-w, 100%));
  background: linear-gradient(
    to bottom,
    var(--sp-hero-fan-fade-to, rgba(245, 240, 237, 1)) 0%,
    var(--sp-hero-fan-fade-from, rgba(245, 240, 237, 0)) 100%
  );
  mix-blend-mode: normal;
  opacity: 0.45;
}


/* ==========================================================================
   FLOATING BACKGROUND ART — ON AT EVERY WIDTH
   ==========================================================================
   History, kept because each step was a separate call and the reasoning still
   governs the numbers below:

   2026-09-17: the whole layer went off at every width. The two decorative
   bleed illustrations never lined up responsively — their geometry is % of
   container WIDTH throughout (`left`, `width` and especially `margin-top`,
   see percent-margin-resolves-against-width), so a value that is Figma-exact
   at the 1920 reference rides ~127px higher at 444 while the art it is
   supposed to sit behind barely changes height.

   2026-09-18: brought back for desktop only, scoped by `max-width: 991.98px`,
   on the same reading of "desktop and up" the brand hero got.

   2026-09-19, operator call: bring it back for mobile and tablet too. The
   `display: none` block that lived here is deleted, so the layer renders at
   every width and each tier's own overrides — unreachable under that block,
   i.e. dead rules for a day — are live again:

     <=767      .sp-hero-fan__art-img         width 32%
                .sp-hero-fan__art-img--right  margin-top 36.5%
     768-991.98 .sp-hero-fan__art-img         width 22%
                .sp-hero-fan__art-img--left   left -5.979%
                .sp-hero-fan__art-img--right  margin-top 30%
     >=992      per-side widths 27.969% / 26.875%

   Those narrow-tier values were tuned on a 444px device (avd-5) before the
   layer was switched off, and they are Figma-deviating on purpose. They do
   not hold their intent at 390: `margin-top: 36.5%` resolves against the
   container's WIDTH, so the right scene lands ~84px above the fan there
   against the ~18-31px the rule was written for, and it crosses the CTA rows
   on the way. Measured this pass, and left as-is rather than re-tuned with
   no comp to tune against — the fan hero exists in Figma only at 1920.

   .sp-hero-fan__art is position:absolute with height:0, so it reserves no
   room and no layout follows it — every slide's height is identical whether
   the layer paints or not, at any width.

   Every slide keeps its own bg_left_asset/bg_right_asset and art_opacity, and
   the swing keyframes are untouched; slides with no bg assets (the sale wall,
   Pretty Autumn) still render nothing here. To take the layer off for a tier
   again, add back a `display: none` rule scoped to that tier's query.
   ========================================================================== */

/* ==========================================================================
   SHEET COLUMNS (Figma 3163:1764, operator 2026-10-11). Three columns of
   sheets drift past each other, A and C up, B down; from 1200px the copy sits
   to the right of them as in the comp, below that it stacks over a band of
   the same columns like the fan does.

   One unit, --u, is a Figma 1920-reference pixel. Every slot is placed in it,
   so the composition scales as one piece. It is measured against the drift
   box itself (container query units), because that box is sized differently
   per tier: the 802-wide column group on phones and tablets (capped at 640px
   so a tablet does not get sheets taller than the band), the comp's own
   0..1014 strip of the 1920 frame from 1200 up.

   The loop: each track holds its three slots three times over and moves by
   exactly one loop (three heights + three 26px gaps), so a copy lands where
   the one before it started. Down columns start one loop higher, so the copy
   on screen at t=0 is the second one, at Figma's own position.
   ========================================================================== */
.sp-hero-fan__drift {
  position: relative;
  flex: 1 1 0px;
  min-height: 0;
  overflow: hidden;
  container-type: inline-size;
  z-index: 1;
  --u: calc(min(100cqw, 640px) / 802);
  --drift-ox: calc(50% - 613 * var(--u));
  transition: opacity 320ms var(--sp-hero-fan-ease);
}

/* 480ms, the art's exit fade, so the exit drift is seen before it fades. */
.sp-hero-fan__slide:not(.is-active) .sp-hero-fan__drift {
  opacity: 0;
  transition: opacity 480ms var(--sp-hero-fan-ease-exit);
}

.sp-hero-fan__drift-col {
  position: absolute;
  top: calc(var(--start) * var(--u));
  left: calc(var(--drift-ox) + var(--cx) * var(--u));
  width: calc(250 * var(--u));
}

.sp-hero-fan__drift-col--down {
  top: calc((var(--start) - var(--loop)) * var(--u));
}

/* Still while the slide is on screen (operator 10/16: "do not animate this
   section when standing still, only slightly on enter and exit"; it used to
   loop forever). Each column travels 40 units in its own direction, A and C
   up, B down: it arrives from 40 below (above for B) on the hero's long
   entry ease, and leaves by drifting the same way on the exit ease while
   the slide fades. sp-hero-fan.js holds `is-leaving` on the outgoing slide
   for the exit; once it drops, the track snaps back to its pre-entry offset
   unseen (transition none). The three copies per track are kept: down
   columns still start one loop higher, so copy two sits at Figma's spot. */
.sp-hero-fan__drift-track {
  display: flex;
  flex-direction: column;
  gap: calc(26 * var(--u));
  --drift-dir: -1;
  translate: 0 0;
  transition: translate 1400ms var(--sp-hero-fan-ease);
}

.sp-hero-fan__drift-col--down .sp-hero-fan__drift-track {
  --drift-dir: 1;
}

.sp-hero-fan__slide:not(.is-active):not(.is-leaving) .sp-hero-fan__drift-track {
  translate: 0 calc(var(--drift-dir) * -40 * var(--u));
  transition: none;
}

.sp-hero-fan__slide.is-leaving:not(.is-active) .sp-hero-fan__drift-track {
  translate: 0 calc(var(--drift-dir) * 40 * var(--u));
  transition: translate 560ms var(--sp-hero-fan-ease-exit);
}

/* Figma's four-layer drop shadow, tinted from the slide's own fade colour
   (the comp's #bbccd3 is By the Sea's background darkened the same way). */
.sp-hero-fan__drift-tile {
  height: calc(var(--h) * var(--u));
  flex-shrink: 0;
  background: #fff;
  box-shadow:
    0 calc(1 * var(--u)) calc(3 * var(--u)) color-mix(in srgb, var(--sp-drift-tint) 12%, transparent),
    0 calc(8 * var(--u)) calc(18 * var(--u)) color-mix(in srgb, var(--sp-drift-tint) 40%, transparent),
    0 calc(28 * var(--u)) calc(34 * var(--u)) color-mix(in srgb, var(--sp-drift-tint) 26%, transparent),
    0 calc(60 * var(--u)) calc(50 * var(--u)) color-mix(in srgb, var(--sp-drift-tint) 10%, transparent);
}

.sp-hero-fan__drift-tile img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
}

/* Layer 3 of the comp: the columns dissolve into the background at the top
   (130 units) and bottom (150 units) instead of being cut by the frame. */
.sp-hero-fan__drift-fade {
  position: absolute;
  left: 0;
  right: 0;
  pointer-events: none;
}

.sp-hero-fan__drift-fade--top {
  top: 0;
  height: calc(130 * var(--u));
  background: linear-gradient(to bottom, var(--sp-drift-bg), color-mix(in srgb, var(--sp-drift-bg) 0%, transparent));
}

.sp-hero-fan__drift-fade--bottom {
  bottom: 0;
  height: calc(150 * var(--u));
  background: linear-gradient(to top, var(--sp-drift-bg), color-mix(in srgb, var(--sp-drift-bg) 0%, transparent));
}

.sp-hero-fan__body {
  display: none;
}

/* ≥1200: the comp. Columns fill the 0..1014 strip of the centred 1920 frame,
   copy sits at x=1128 in a 580 column, vertically centred (Figma: 218 of a
   758 frame for a 306 block, i.e. centred to within 8px). The two scene
   illustrations both move to the right edge, Scene L above Scene R. */
@media (min-width: 1200px) {
  .sp-hero-fan__slide--columns {
    --sp-frame-w: min(1920px, 100%);
  }

  .sp-hero-fan__slide--columns .sp-hero-fan__drift {
    position: absolute;
    top: 0;
    bottom: 0;
    left: calc(50% - var(--sp-frame-w) / 2);
    width: calc(var(--sp-frame-w) * 1014 / 1920);
    --u: calc(100cqw / 1014);
    --drift-ox: 0px;
  }

  /* Doubled class: the ≥992 kit-slide rule above is (0,3,0) and would keep
     its 94px top padding. The column is 600 at 1920 (Figma's 580 holds its
     two 287/276 buttons on one row only to the pixel; ours measure 292/279)
     with a 440px floor, so the headline keeps three lines at 1200, not five. */
  .sp-hero-fan__slide.sp-hero-fan__slide--columns > .sp-hero-fan__inner {
    max-width: none;
    width: max(440px, var(--sp-frame-w) * 600 / 1920);
    margin: auto 0 auto calc(50% - var(--sp-frame-w) / 2 + var(--sp-frame-w) * 1128 / 1920);
    padding: 48px 0;
    text-align: left;
  }

  .sp-hero-fan__slide--columns .sp-hero-fan__eyebrow {
    margin-bottom: 18px;
  }

  .sp-hero-fan__slide--columns .sp-hero-fan__headline {
    max-width: none;
    margin-bottom: 18px;
    font-size: clamp(34px, 2.396vw, 46px);
    line-height: 1.2;
    text-wrap: balance;
  }

  .sp-hero-fan__slide--columns .sp-hero-fan__body {
    display: block;
    margin: 0 0 32px;
    max-width: 580px;
    font-size: clamp(17px, 1.042vw, 20px);
    line-height: 1.4;
    font-weight: 400;
    color: #484c50;
  }

  .sp-hero-fan__slide--columns .sp-hero-fan__buttons {
    justify-content: flex-start;
  }

  .sp-hero-fan__slide--columns .sp-hero-fan__art-img--left {
    left: 88.542%;
    width: 27.083%;
    max-width: none;
    margin-top: -17.188%;
    opacity: calc(var(--sp-hero-fan-art-opacity, 0.2) * 0.45);
  }

  /* Scene R is anchored to the hero's BOTTOM edge (operator 10/16, "fix the
     position of bottom right placement"). Figma puts it at y=290 of a 758
     frame with 225 units below the frame (290 + 693 = 983), so it always
     bleeds off the bottom. A top-anchored margin-top: 15.104% only holds
     at the comp's 758/1920 ratio: on a taller hero (e.g. 1035 high at
     1853 wide) the scene's own flat bottom edge showed beside the copy.
     The art box spans the slide's height here so `bottom` can resolve; the
     left scene still hangs from the top. The 225 is in cqw of the art box
     (= the frame width): --sp-frame-w would not do, its 100% resolves
     against HEIGHT inside `bottom` (measured -87px instead of -217). */
  .sp-hero-fan__slide--columns .sp-hero-fan__art {
    height: 100%;
    container-type: inline-size;
  }

  .sp-hero-fan__slide--columns .sp-hero-fan__art-img--right {
    left: 77.083%;
    width: 29.167%;
    max-width: none;
    top: auto;
    bottom: calc(100cqw * -225 / 1920);
    margin-top: 0;
  }

  .sp-hero-fan__slide--columns:not(.is-active) .sp-hero-fan__art-img--left {
    translate: 85% -6%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sp-hero-fan__slide .sp-hero-fan__drift-track {
    translate: 0 0 !important;
    transition: none !important;
  }
}

/* SALE WALL ON PHONES: a fan of five upright sheets under the copy (operator 10/24, "fix the sale wall on the
   homepage for mobile"). The wall is a 1920-wide landscape composition with an empty corridor through its middle;
   the corridor rule above (wall >= copy x 6.423) made it 1500px wide on a phone, so a 390 screen saw only fragments
   of the top and bottom rows around a hole. Phones now put the copy at the top and fan five of the wall's own
   upright tiles (7, 12, 1, 13, 11: tiles 2, 4, 6, 8, 10, 15, 17, 19 are turned) like the kit slides' fans. The
   `.sp-hero-fan` prefix outranks the base wall rules; `rotate` (not --rot, which each card sets inline) adds the
   tilt without touching the entrance transform. Measured 390: all three slides 490px tall, was 566. */
@media (max-width: 767px) {
  /* padding-bottom reserves the fan's 300px band below the copy, so the slide holds its own height when it is the only
     slide (operator 10/30, one-slide hero): with no kit slide setting the shared height it shrank to the copy (191px)
     and the fan sat on the words. With kit slides present the total stays the measured 490. */
  .sp-hero-fan .sp-hero-fan__slide--wall .sp-hero-fan__inner { padding-top: 36px; padding-bottom: 300px; }
  .sp-hero-fan .sp-hero-fan__cards--wall { top: auto; bottom: 0; left: 0; width: 100%; height: 300px; aspect-ratio: auto; }
  .sp-hero-fan .sp-hero-fan__cards--wall > .sp-hero-fan__card { display: none; }
  .sp-hero-fan .sp-hero-fan__cards--wall > .sp-hero-fan__card:is(:nth-child(7), :nth-child(12), :nth-child(1), :nth-child(13), :nth-child(11)) {
    display: block; width: 30%; height: auto; aspect-ratio: 1 / 2;
    left: calc(50% + var(--sp-px)); top: var(--sp-py); rotate: var(--sp-pr); transform-origin: center bottom; z-index: var(--sp-pz);
  }
  .sp-hero-fan .sp-hero-fan__cards--wall > .sp-hero-fan__card:nth-child(7)  { --sp-px: -34%; --sp-py: 46px; --sp-pr: -8deg; --sp-pz: 1; }
  .sp-hero-fan .sp-hero-fan__cards--wall > .sp-hero-fan__card:nth-child(12) { --sp-px: -17%; --sp-py: 26px; --sp-pr: -4deg; --sp-pz: 2; }
  .sp-hero-fan .sp-hero-fan__cards--wall > .sp-hero-fan__card:nth-child(1)  { --sp-px: 0%;   --sp-py: 16px; --sp-pr: 0deg;  --sp-pz: 3; }
  .sp-hero-fan .sp-hero-fan__cards--wall > .sp-hero-fan__card:nth-child(13) { --sp-px: 17%;  --sp-py: 26px; --sp-pr: 4deg;  --sp-pz: 2; }
  .sp-hero-fan .sp-hero-fan__cards--wall > .sp-hero-fan__card:nth-child(11) { --sp-px: 34%;  --sp-py: 46px; --sp-pr: 8deg;  --sp-pz: 1; }
}

/* SHEET COLUMNS AS THE ONLY SLIDE (operator 10/30: One More Chapter leads, one slide). Below 1200 the drift is a flex
   child that takes the height left over, and above 1200 it is absolute; either way its height came from a sibling slide
   (the sale wall pinned 652-692). Alone, the drift fell to 0 on phones (271px hero, no sheets) and the desktop hero to
   431px. These floors give the slide its own height at each tier; with taller siblings present they change nothing. */
.sp-hero-fan .sp-hero-fan__slide--columns .sp-hero-fan__drift { min-height: 300px; }
@media (min-width: 768px) {
  .sp-hero-fan .sp-hero-fan__slide--columns .sp-hero-fan__drift { min-height: 380px; }
}
@media (min-width: 1200px) {
  .sp-hero-fan .sp-hero-fan__slide--columns { min-height: clamp(560px, 39.5vw, 758px); }
}

/* the kit name after a headline's last comma sits on its own line (sections/sp-hero-fan.liquid, operator 10/30 "odd rag") */
.sp-hero-fan__headline-kit { display: block; }
