/* ============================================================================
   Homepage — only what the sixteen bands need beyond the shared system.

   Everything here is composed from tokens.css custom properties. No new
   colour, radius, spacing step or breakpoint is introduced.
   ========================================================================= */

/* ── the film ──────────────────────────────────────────────────────────────
   Band 3. The engine in js/scrub-engine.js owns everything inside #world,
   including its own height — it measures a scroll track on load and sizes the
   element to it. Do not give #world a height, a max-width, or a transform:
   each one changes what the engine measured against and desynchronises the
   scrub from the scroll. */

/* --paper, not --graphite. The film's actual ground is --sw-bg, which .sw-root
   below points at --paper, so graphite was only ever visible in a paint gap.
   There is normally no such gap — #world has no height until the engine sizes
   it, so .film collapses before mount — but an engine throw or an iOS resize
   between layout() calls would have flashed near-black under --ink copy. Same
   value as the thing it sits behind makes that failure invisible instead of
   alarming; it is not a "the film is missing" tell and was never used as one. */
.film { position: relative; background: var(--paper); }

/* ── the hero poster ───────────────────────────────────────────────────────
   Band 3's LCP element. build/pages_home.py explains why it exists at all;
   this is why it is laid out the way it is.

   position:fixed, not absolute. .film has no height until the engine measures
   its track, so an absolutely-positioned poster would be a 0px box at parse
   time — present in the DOM, never an LCP candidate, and the whole point lost.
   Fixed makes it a full-viewport element from the first paint.

   z-index 5 puts it in the one gap in the engine's stack: above .sw-sky (0),
   which is an opaque --sw-bg wash that would otherwise cover it, and below
   .sw-stage (10), whose first scene is the same asset and paints over it from
   cache. It is under the site's own nav (50) and ticker (60) as well, so it
   cannot swallow the only controls on the fold.

   It is not hidden the moment the engine mounts. #world:not(:empty) would fire
   while the engine's own still is still decoding and flash bare --paper across
   the hero; instead it retires with the rest of the fixed furniture below. */
.film-poster { position: fixed; inset: 0; z-index: 5; pointer-events: none; }
/* base.css sets `img { height: auto }` unlayered — the same rule the scene
   layers have to undo at line 52. Framing matches .sw-scene__still exactly, so
   the engine's still lands on top of an identical crop and nothing shifts. */
.film-poster img { width: 100%; height: 100%; object-fit: cover; object-position: center 42%; }
@media (max-width: 860px) { .film-poster img { object-position: center 44%; } }

/* ── the film's CTA ────────────────────────────────────────────────────────
   Fixed for the same reason every other piece of the film's furniture is: the
   section is ~6,700px tall, so an in-flow button would scroll away after one
   viewport of it and the page would be back to nothing buyable for eight
   screens. z-index sits above .sw-copylayer (20) and below .sw-route (40).

   Left edge is the engine's own copy inset, so on mobile the button reads as
   the last line of the bottom-anchored stack rather than as a separate layer. */
.film-cta {
  position: fixed;
  z-index: 30;
  left: clamp(18px, 5vw, 64px);
  bottom: calc(20px + env(safe-area-inset-bottom));
}
@media (min-width: 861px) { .film-cta { bottom: 26px; } }

/* The last chapter carries its own two buttons, so this one stands down rather
   than making it three. .sw-final is set on #world by the engine when the final
   chapter becomes active; .film-cta is its next sibling in build/pages_home.py,
   which is what makes a plain combinator enough here. Transition, not display,
   so it does not pop out mid-scroll. */
#world.sw-final ~ .film-cta { opacity: 0; pointer-events: none; transition: opacity .3s var(--ease); }

/* The engine ships its own defaults inside `@layer sw` and expects the host
   site to override them unlayered — that is the entire reason it wraps its CSS
   in a layer. Nothing did, so the film was running on the skill's stock
   demo palette: --sw-bg #F5EDE0, a sand that is not --paper, and --sw-ink
   #241D2B, a purple-black next to the site's cool #292724. The mobile legibility
   scrim covers the bottom 60% of the film with a near-solid wash of --sw-bg, so
   on a phone the largest flat colour on the homepage was a tone that appears
   nowhere else on the site.

   Only the values the engine actually reads are set here. The display and body
   faces are deliberately left alone: base.css styles h1-h4 unlayered, so the
   film's title already resolves to Archivo and restating it here would just be
   a second place to keep in sync. */
.sw-root {
  --sw-bg:       var(--paper);
  --sw-ink:      var(--ink);
  --sw-ink-soft: var(--ink-muted);
}

/* The film's two full-bleed layers need their height back.

   base.css sets `img, svg, video { height: auto }`, and that rule earns its
   place — without it an <img> carrying width/height attributes renders
   stretched whenever CSS constrains only its width. But it is an unlayered
   element selector, and the engine's own `height: 100%` lives inside
   `@layer sw`, so base.css wins and the scene's video and still fall back to
   their intrinsic aspect inside a stage that is a full viewport tall.

   Measured before this rule: at 1280x900 the stage was 900px and the video
   1280x720; at 375x900 the video was 375x667. So the film ran as a letterboxed
   strip with 180-233px of bare page ground under it for its whole length, and
   because the box already matched the source's aspect, `object-fit: cover` and
   the engine's deliberate `object-position: center 42%` framing did nothing
   either. Two class selectors beat one element selector, so this restores the
   engine's intent without weakening the base rule anywhere else. */
.sw-scene__video,
.sw-scene__still { height: 100%; }

/* ── the fold ──────────────────────────────────────────────────────────────
   Rhode's fold is one photograph, one headline and one button. Ours stacked a
   01/04 counter, an eyebrow, a title, a three-line body and two ingredient
   pills into 28% of a 390x844 viewport, and then bottom-anchored the scroll
   cue against the same edge with no shared budget: .sw-copy's floor is 56px
   and the cue occupies 20-84px, so the pills landed ON the words "SCROLL TO
   BEGIN" on every phone at or under 390px. Measured gap between them, before:
   -27px at 320x568, -17px at 375x667, 0px at 390x844 — and worse than that in
   practice, because .sw-copy also carries a live translateY of about +2dvh.

   The counter and the pills go. The ingredient names are repeated in full in
   band 8 and on all three PDPs, so nothing is lost, and cutting them hands
   roughly 100px of the fold back to the photograph.

   The scroll cue goes too, on mobile only — the CTA now occupies that slot and
   two things cannot share one edge, which is the bug above restated. What is
   left is a photograph, a headline, one sentence and a priced button.

   The remaining budget is explicit rather than hopeful, which is the actual
   lesson of the collision: .film-cta ends at 20px + a ~48px button = 68px, so
   the copy's floor is set clear of it with roughly 25px of air once the
   translateY is applied. Anything that moves .film-cta's bottom has to move
   this with it. */
@media (max-width: 860px) {
  .film .sw-copy__num,
  .film .sw-copy__tags { display: none; }
  .film .sw-hint { display: none; }
  .film .sw-copy { bottom: calc(clamp(112px, 13dvh, 128px) + env(safe-area-inset-bottom)); }
}

/* The scroll cue reads at 1.25:1 on desktop.
   --sw-ink-soft is --ink-muted (#62615D), which is 5.50:1 on --paper and fine
   wherever the film's scrim actually covers it. On mobile it sat inside the
   bottom scrim and measured 4.98:1. On desktop that scrim is a LEFT gradient,
   width min(58vw,780px), while the cue is centred at left:50% — so it sits in
   the transparent tail, on raw footage, measured at 1.25-2.00:1 against the
   sampled pixels of the water still. A hard 1.4.3 failure that Lighthouse
   cannot see because it only emulates mobile.

   The ink is not the problem and is not touched: --ink-muted is measured
   against the four grounds and darkening it here would fork that decision.
   The background is the problem, and it is a moving video — no text colour is
   reliable against it — so the cue gets a ground of its own. Opaque --paper
   rather than a translucent wash: build/check.py forbids backdrop-filter
   outright, and a partial alpha over footage is the same gamble at lower odds.
   --ink-muted on --paper is the 5.50:1 pair tokens.css already documents. */
@media (min-width: 861px) {
  .film .sw-hint {
    padding: 8px 14px 10px;
    gap: 6px;
    border-radius: var(--r-md);
    background: var(--paper);
  }
  /* The engine's 22x34 wheel glyph is what makes this a box rather than a
     chip once it has a ground of its own. Scaled down so the cue stays the
     quietest thing on the fold and does not compete with the CTA. */
  .film .sw-hint i { width: 17px; height: 26px; }
}

/* The film's titles size themselves from base.css's unlayered `h2`, a flat
   2.25rem — the engine's own responsive clamp lives in @layer sw and loses to
   it, so the hero line was the same 36px on a 320px phone and a 1440px desktop
   and the engine's mobile clamp has never once applied. Restated here as one
   deliberate value: the same 36px on a phone, opening up on desktop, where a
   flat 36px was the quietest thing on a full-bleed hero and Rhode's is the
   loudest. Class beats element, so this holds regardless of the tag — which
   matters, because chapter one's is now an h1 (js/scrub-engine.js) and h1 and
   h2 carry different sizes in base.css. */
.film .sw-copy__title { font-size: clamp(2.25rem, 4.6vw, 3.25rem); }

/* The film's primary CTA, restored to legibility.

   The engine's own rule is `.sw-btn--primary{color:#fff;background:var(--sw-ink)}`
   and it lives inside `@layer sw`. base.css sets `a { color: inherit }`
   unlayered — and unlayered always beats layered regardless of specificity, so
   one element selector overrode the engine's class and the label inherited
   --ink from its ancestor. The background is `var(--sw-ink)`, which .sw-root
   above maps to --ink on purpose, so the button was painting #292724 text on a
   #292724 fill: contrast 1.00, an invisible "Meet the three" at the end of the
   film. The ghost button beside it was unaffected because it wants --ink.

   Only `color` is set. The ink fill is correct and already comes from the
   engine, and the same cascade quirk would silently undo any value restated
   here the moment base.css changed. --on-dark is what .btn--primary uses, so
   the film's CTA and the site's CTAs stay one decision. */
.sw-btn--primary { color: var(--on-dark); }

/* The film's own furniture — route dots, progress bar, copy layer — retires
   once #products comes into view. js/home.js sets .film-done on <body>; this
   is the half that actually removes it from the tab order, because six
   invisible-but-focusable controls sitting over the rest of the page is a
   keyboard trap rather than a decoration.

   These are the engine's real class names, read out of js/scrub-engine.js.
   An earlier version of this rule guessed at `.sw-chrome` and
   `[data-world-chrome]`, neither of which the engine has ever emitted — so it
   matched nothing and the trap it describes was live the whole time: six
   focusable controls (two CTAs and four route dots) sitting invisible over
   every band below the film. A selector that matches nothing fails silently,
   which is why this list is worth checking against the engine after any
   upgrade.

   .sw-stage (fixed, z-10) and .sw-sky (fixed, z-0) are the last two entries and
   they retire for a different reason than the rest. Nothing above them is
   broken — the bands below carry their own opaque grounds, so the film never
   showed through — but a fixed layer that stays `visible` is still composited
   under every section that scrolls past it, for the entire remaining length of
   a 16,600px page. Measured after film-done: both were visibility:visible,
   opacity:1. Hiding them is free because home.js toggles .film-done live off
   `products.getBoundingClientRect().top <= 0` rather than latching it, so both
   layers come back the moment you scroll up into the film again. */
body.film-done .film .sw-topbar,
body.film-done .film .sw-route,
body.film-done .film .sw-topcta,
body.film-done .film .sw-hint,
body.film-done .film .sw-scrollbar,
body.film-done .film .sw-copylayer,
body.film-done .film .sw-stage,
body.film-done .film .sw-sky,
/* Not the engine's, but fixed and inside .film, so they belong to the same
   inventory and fail the same way: .film-poster is a full-viewport image and
   .film-cta is a focusable link, and both would otherwise stay composited —
   and the CTA tabbable — over every band below the film. */
body.film-done .film .film-poster,
body.film-done .film .film-cta { visibility: hidden; pointer-events: none; }

/* ── band grounds ──────────────────────────────────────────────────────────
   Two extra grounds beyond base.css, used once each. */

.band--sage { background: var(--sage); color: var(--shell); }
/* --sage-wash on --sage is 3.98:1 and these are 18px and 12px — neither is
   large text, so that pair fails AA. It shipped that way on the rose palette
   (--accent-wash on --accent was 3.67:1). --shell is 4.58:1 and clears. */
.band--sage .t-lede,
.band--sage .t-eyebrow { color: var(--shell); }
.band--sage .btn--primary { background: var(--shell); color: var(--sage-deep); }
.band--sage .btn--primary:hover { background: var(--sage-wash); box-shadow: none; }

/* The assurance strip is a rule between two bands, not a band of its own. */
.band--tight { padding-block: var(--s-10); }

.band-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-4);
  margin-top: var(--s-10);
}

/* ── band 5: the three ─────────────────────────────────────────────────── */

.three-foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4);
  margin-top: var(--s-10);
}

/* ── band 6: bundle ────────────────────────────────────────────────────────
   Kylie's promo band, and the only --ember on the page. */

.bundle {
  display: flex;
  flex-direction: column;
  gap: var(--s-6);
  padding: var(--s-8);
  background: var(--shell);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
}
.bundle-copy { display: flex; flex-direction: column; gap: var(--s-3); max-width: 60ch; }
.bundle-copy h2 { font-size: var(--t-h2); }

@media (min-width: 1024px) {
  .bundle { flex-direction: row; align-items: center; justify-content: space-between; gap: var(--s-16); padding: var(--s-10); }
  .bundle-act { flex: none; }
}

/* ── band 7: editorial split ───────────────────────────────────────────────
   Rhode. The copy column leads on desktop; on mobile the still follows the
   words rather than pushing them below the fold. */

.split { display: grid; gap: var(--s-10); }
.split-copy { display: flex; flex-direction: column; gap: var(--s-4); }
.split-act { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-4); }

@media (min-width: 1024px) {
  .split { grid-template-columns: 1fr 1fr; gap: var(--s-16); align-items: center; }
}

/* ── band 8: actives ───────────────────────────────────────────────────── */

.active-card {
  display: flex;
  flex-direction: column;
  background: var(--shell);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  overflow: hidden;
}
.active-media { aspect-ratio: 3 / 2; background: var(--mist); }
.active-media:empty { display: none; }
.active-media img { width: 100%; height: 100%; object-fit: cover; }
.active-body { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-3); padding: var(--s-5); }
.active-body p { color: var(--ink-soft); font-size: var(--t-sm); }

/* ── band 9: routine ───────────────────────────────────────────────────── */

.step-when {
  display: block;
  margin-top: var(--s-2);
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: var(--ls-micro);
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* ── band 10: campaign ─────────────────────────────────────────────────────
   Aspect-ratio rather than a fixed height so the band reserves its space
   before the video decodes. CLS is a release gate. */

.media-band { aspect-ratio: 16 / 9; }
@media (max-width: 767px) { .media-band { aspect-ratio: 4 / 5; } }

/* ── band 11: notes ────────────────────────────────────────────────────────
   Kylie's testimonial grid, carrying the brand's own commitments. The <cite>
   names a promise, never a person — see build/pages_home.py. */

.notes { display: grid; gap: var(--s-5); }
@media (min-width: 768px)  { .notes { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1280px) { .notes { grid-template-columns: repeat(4, 1fr); gap: var(--s-6); } }
.notes .note p { font-size: var(--t-body); }

/* ── bands 12 and 15: CTA bands ────────────────────────────────────────── */

.quiz-cta,
.loyalty {
  display: flex;
  flex-direction: column;
  gap: var(--s-6);
  align-items: flex-start;
}
.quiz-cta h2,
.loyalty h2 { font-size: var(--t-h2); max-width: 20ch; }
.quiz-cta p,
.loyalty p { max-width: 56ch; }

@media (min-width: 1024px) {
  .quiz-cta,
  .loyalty { flex-direction: row; align-items: center; justify-content: space-between; gap: var(--s-16); }
  .quiz-cta > .btn,
  .loyalty > .btn { flex: none; }
}

.band--metal .t-lede { color: var(--ink-soft); }

/* ── footer wordmark overflow guard ────────────────────────────────────────
   --t-mega is Archivo at 18vw with -0.035em tracking, which can overrun a
   320px viewport on a long word. The footer wordmark (build/shell.py) is the
   only element on this page that uses it: --t-mega is defined in tokens.css
   and consumed only by .t-mega in base.css, and .t-mega appears nowhere else.

   .film used to be in this selector on the stated grounds that "the film's own
   copy layer is the only place --t-mega appears above the footer". That was
   simply untrue — the film's title is hard-coded in the engine at a clamp that
   cannot overrun, and never touches --t-mega. Measured with the rule and with
   .film{overflow-x:visible !important}: scrollWidth === clientWidth === 1440
   either way. It prevented nothing, and it was a live trap: overflow does not
   clip fixed descendants today, but adding any transform, filter, will-change
   or contain to .film would make it a containing block and the clip would then
   swallow the entire fixed film — including the poster and CTA added above.
   That is the exact accident the warning at the top of this file exists for. */
.footer { overflow-x: clip; }
