/* ============================================================
   Etha — Vata ritual page, mobile draft (design frame 402 × 874)
   Values from tokens.md. Vertical positions are y/874 (academy: y/734).
   ============================================================ */

:root {
  --cream: #FFEFDE;
  --plum:  #3D233B;
  --white: #FFFFFF;
  --vata:  #000F9F;
  --serif: "plantin", Georgia, 'Times New Roman', serif;
  --sans:  "brandon-grotesque", 'Helvetica Neue', Helvetica, Arial, sans-serif;
  --ease:  cubic-bezier(0.22, 1, 0.36, 1); /* motion grammar, for later */

  /* The navigation drawer keeps the timing it ships with in
     etha-mobile-drawer, which is not the page's motion grammar — it is a menu
     opening, not a reveal. */
  --drawer-speed: 0.6s;
  --drawer-ease:  cubic-bezier(0.7, 0, 0.2, 1);

  /* One screen. dvh is the honest default, but on iOS it changes as Safari's
     toolbar collapses, which resized every panel mid-scroll and made the page
     shake. lvh is the same measurement with the toolbars retracted: it fills
     the screen AND never changes while scrolling, so it fixes both the shake
     and the stripes of the next panel that a locked short height left behind.
     It scales with whatever screen it lands on — nothing here is a fixed
     device size. */
  --panel-h: 100dvh;
}

@supports (height: 100lvh) {
  :root { --panel-h: 100lvh; }
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  background: #161311;     /* dark neutral — pre-paint flash must not be purple */
  font-family: var(--serif);
  color: var(--cream);
}

img { display: block; }

.page {
  max-width: 402px;
  margin: 0 auto;
}

/* ---------- full-screen photo panel ---------- */

.panel {
  position: relative;
  height: var(--panel-h, 100dvh);
  overflow: hidden;
  background-color: var(--plum);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/* every direct child of a panel is placed on the design grid */
.panel > * {
  position: absolute;
  z-index: 1;
}

.bg-hero    { background-image: url(/ritual/images/hero.jpg?v=1bd1f518); }
.bg-fork    { background-image: url(/ritual/images/mode-fork.jpg?v=b255310c); }
.bg-step1   { background-image: url(/ritual/images/step-01.jpg?v=9a93db74); }
.bg-step2   { background-image: url(/ritual/images/step-02.jpg?v=6e51f463); }
.bg-step3   { background-image: url(/ritual/images/step-03.jpg?v=1e3512df); }
.bg-summary { background-image: url(/ritual/images/summary.jpg?v=0d7c9d03); }
.bg-bundle  { background-image: url(/ritual/images/bundle.jpg?v=882afe87); }
.bg-academy { background-image: url(/ritual/images/academy.jpg?v=33e82fad); }

/* soft gradient scrims behind text — never a flat overlay */

.scrim-top::before,
.scrim-bottom::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  pointer-events: none;
}

.scrim-top::before {
  top: 0;
  height: 34%;
  background: linear-gradient(to bottom, rgba(28, 18, 14, 0.38), rgba(28, 18, 14, 0));
}

.scrim-bottom::after {
  bottom: 0;
  height: 36%;
  background: linear-gradient(to top, rgba(28, 18, 14, 0.45), rgba(28, 18, 14, 0));
}

/* ---------- shared type ---------- */

h1, h2, h3 {
  font-weight: 600; /* Plantin Semibold — never bold, never uppercase */
}

.kicker {
  font-family: var(--sans);
  font-weight: 500;
  font-size: 14px;
  line-height: 1.15;
  letter-spacing: 0.08em;
}

/* blob buttons — exported outline as background, label centred */

.blob-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  min-height: 44px;
  color: var(--cream);
  text-decoration: none;
  font-family: var(--sans);
  font-weight: 500;
  font-size: 16px;
  letter-spacing: 0.08em;
  line-height: 1.25;
}

/* Kali's drawn blob, from --blob. It is no longer what a reader normally sees —
   the live aura is an inline SVG that script.js redraws every frame — but it is
   still what the page falls back to, and that is why it stays: with JavaScript
   off, and under reduced motion, this is the outline. Absolutely positioned, so
   it is not a flex item and the label stays centred; every .blob-btn is itself
   absolutely placed, so inset:0 is the button's own box. */
.blob-btn::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--blob);
  background-position: center;
  background-size: contain;
  background-repeat: no-repeat;
  pointer-events: none;
}

.blob-btn span {
  position: relative;      /* over the aura, which is painted behind it */
  z-index: 1;
  padding: 0 34px;
  pointer-events: none;    /* fix iOS event delegation (clicks bubble to document only if target is clickable) */
}

/* THE AURA. The supplied etha-aura-button component, ported into these three
   files rather than added as a fourth and fifth: the outline is an inline SVG
   per button (index.html) whose path script.js rebuilds every frame from four
   sine harmonics, with the stroke breathing on a 9.5s cycle, an feTurbulence
   shiver over the top, and a push away from wherever a finger lands. The words
   never move — the SVG is a sibling of the label, not a wrapper.
   preserveAspectRatio="none" stretches the component's 280x80 viewBox to
   whatever box each button's own Figma geometry gives it; all four are close to
   that 3.5:1, so the wave is barely distorted. overflow is visible because the
   wave is meant to swell past the viewBox — at full amplitude it reaches about
   22% beyond, which is the aura rather than a mistake.
   This, the doorway icons and nothing else loop on this page. CLAUDE.md's motion
   grammar rules loops out; all of them were asked for directly. */
.aura-btn__svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

/* Only once JS is running does the drawn blob step aside for the live one, and
   only when motion is welcome. Under prefers-reduced-motion the blob stays and
   script.js never starts the loop, so the button is exactly the shape Figma
   drew, holding still. */
@media (prefers-reduced-motion: no-preference) {
  html.js .blob-btn::before { display: none; }
}

/* ============ 1 · HERO ============ */

.hero-nav {
  top: 7.3%;               /* y 64 */
  left: 10px;
  right: 10px;
  height: 45px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.nav-btn {
  width: 46px;
  height: 45px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

.nav-btn-menu { justify-content: flex-start; }

.nav-btn:focus-visible {
  outline: 2px solid var(--cream);
  outline-offset: 2px;
}

/* Centred on its own rather than by flex balance: the header used to have a
   third item (the search icon) holding the wordmark in the middle, and
   removing that pushed it to the right. */
.nav-logo {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

/* The burger exists only once script.js has run — with JS off there is
   nothing for it to open, and the menu below stays display:none, so the
   navigation is simply absent on a phone rather than broken. From the
   desktop breakpoint up the three links are in the header instead and the
   burger is hidden again (see the desktop block at the end of this file). */
.nav-btn-menu { display: none; }
html.js .nav-btn-menu { display: flex; }

/* ---------- the navigation drawer ---------- */
/* Ported from etha-mobile-drawer, the side drawer built for
   shop.ethalife.com: a fixed cream panel that slides in from the left over a
   dimmed page, 55px rows, uppercase Brandon. Its own values are kept —
   0.6s on cubic-bezier(0.7, 0, 0.2, 1), 80vw capped at 380px, 30px gutter,
   0.04em tracking — so the two builds stay recognisably the same menu. The
   only thing dropped is the second level: with three links there is nothing
   to nest, so the panel machinery and the chevron rows are gone.
   ONE list serves both presentations; the desktop block at the end of this
   file hides the drawer chrome and lays the same links out inline. */

.nav-scrim {
  position: fixed;
  inset: 0;
  z-index: 1199;
  display: none;                       /* absent without JS */
  background: rgba(0, 0, 0, 0.35);     /* a backdrop, not a scrim over type */
  opacity: 0;
  visibility: hidden;
}

.site-nav {
  position: fixed;
  inset: 0 auto 0 0;
  z-index: 1200;
  display: none;                       /* absent without JS */
  flex-direction: column;
  width: 80vw;
  max-width: 380px;
  overflow: hidden;
  background: var(--cream);
  color: var(--plum);
  font-family: var(--sans);
  font-weight: 500;
  font-size: 15px;
  letter-spacing: 0.04em;
  transform: translateX(-101%);
  /* visibility, not just the transform: it takes the closed drawer out of the
     tab order and out of the accessibility tree, which a translate alone does
     not. That is why nothing here has to manage aria-hidden. */
  visibility: hidden;
}

html.js .nav-scrim { display: block; }
html.js .site-nav { display: flex; }

html.js[data-nav-open] .site-nav { transform: translateX(0); visibility: visible; }
html.js[data-nav-open] .nav-scrim { opacity: 1; visibility: visible; }

@media (prefers-reduced-motion: no-preference) {
  /* visibility flips after the slide-out, and at once on the way in */
  html.js .site-nav {
    transition: transform var(--drawer-speed) var(--drawer-ease),
                visibility 0s var(--drawer-speed);
  }
  html.js[data-nav-open] .site-nav {
    transition: transform var(--drawer-speed) var(--drawer-ease),
                visibility 0s;
  }
  html.js .nav-scrim {
    transition: opacity var(--drawer-speed) var(--drawer-ease),
                visibility 0s var(--drawer-speed);
  }
  html.js[data-nav-open] .nav-scrim {
    transition: opacity var(--drawer-speed) var(--drawer-ease),
                visibility 0s;
  }
}

.site-nav-head {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  min-height: 55px;
  padding: 14px 30px;
}

.site-nav-logo { display: block; width: 96px; height: auto; color: var(--plum); }

/* The burger is underneath the open drawer, so closing needs its own control
   here. .drawer__close from the component, moved into the head. */
.site-nav-close {
  position: absolute;
  right: 15px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  background: none;
  border: 0;
  color: inherit;
  cursor: pointer;
}

.site-nav-list { padding: 0 30px; }

.site-nav a {
  display: flex;
  align-items: center;
  min-height: 55px;
  padding: 10px 0;
  border-bottom: 1px solid #E2D0C6;   /* the drawer's rule colour */
  color: inherit;
  text-decoration: none;
  text-transform: uppercase;
  line-height: normal;
}

.site-nav a:hover { opacity: 0.7; }

.site-nav a:focus-visible,
.site-nav-close:focus-visible {
  outline: 2px solid var(--plum);
  outline-offset: 3px;
}


.hero-lines {
  top: 20.6%;              /* y 180 */
  left: 25px;
  font-size: 26px;
  line-height: 1.12;
}

.hero-why {
  top: 32.8%;              /* y 287 */
  right: 24px;
  font-size: 26px;
  line-height: 1.12;
  text-align: right;
}

.hero-you-are {
  top: 50.2%;              /* y 439 */
  left: 27px;
  font-size: 26px;
  line-height: 1.12;
}

.hero-dosha {
  top: 55%;                /* y ~481 */
  left: 0;
  width: 100%;
  text-align: center;
  font-size: 86px;
  line-height: 1;
  font-weight: 600;
  font-style: italic;
}

.hero-cue {
  top: 92.6%;              /* y 809 */
  left: 0;
  width: 100%;
  text-align: center;
  font-family: var(--sans);
  font-weight: 500;
  font-size: 16px;
  letter-spacing: 0.08em;
  line-height: 1;
}

/* ============ 2 · WHAT CHANGES — pinned, 3 states ============
   Default layout below = three plain stacked panels. That is what people
   with JavaScript off or prefers-reduced-motion get. The pinned behaviour
   further down switches on only for html.js + motion allowed, and is the
   reusable pattern for the other pinned sections. */

#what-changes .pin-stage {
  --p: 1;                  /* without JS the drawn line rests fully drawn */
  position: relative;
}

.wc-state {
  position: relative;
  height: var(--panel-h, 100dvh);
  overflow: hidden;
  background-color: var(--plum);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

.wc-state > * {
  position: absolute;
  z-index: 1;
}

.wc-state-1,
.wc-state-2 { background-image: url(/ritual/images/what-changes-a.jpg?v=93ed9d1b); }
.wc-state-3 { background-image: url(/ritual/images/what-changes-b.jpg?v=179c0ae6); }

/* stacked only: states 2 and 3 repeat the drawn line as a background image
   so each panel reads alone (the real SVG sits over state 1). The file holds
   the exact recovered geometry in the 402×874 frame, and cover crops it
   exactly the way the photo is cropped. */
.wc-state-2::before,
.wc-state-3::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url(/ritual/images/line-what-changes.svg?v=6fa892b8) center / cover no-repeat;
}

/* stacked scrims: state 1 top, state 3 bottom */
.wc-state-1::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 34%;
  background: linear-gradient(to bottom, rgba(28, 18, 14, 0.38), rgba(28, 18, 14, 0));
  pointer-events: none;
}

.wc-state-3::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 36%;
  background: linear-gradient(to top, rgba(28, 18, 14, 0.45), rgba(28, 18, 14, 0));
  pointer-events: none;
}

.wc-bg { display: none; }  /* the two photo layers only exist while pinned */

.wc-kicker {
  position: absolute;
  z-index: 2;
  top: 8%;               /* y 70, one kicker for the whole section */
  left: 28px;
}

.wc-line {
  position: absolute;
  z-index: 2;
  display: block;
  top: 0;                  /* path coords live in the 402×874 design frame; */
  left: 0;                 /* xMidYMid slice crops it exactly like the photo */
  width: 100%;
  height: var(--panel-h, 100dvh);          /* one viewport panel — the stage's first screen */
  pointer-events: none;
}

.wc-line path {
  stroke-dasharray: 1;     /* the inline path carries pathLength="1" */
  stroke-dashoffset: calc(1 - var(--p, 1));
  /* The three drawn lines are meant to read as one hand. This one was inked
     at 1px and 0.5, against body-mind-senses' 2px at 0.6, so it looked like a
     different, fainter pen. Set here rather than on the inline path, so the
     one value reaches all three doshas. */
  stroke-width: 2px;
  stroke-opacity: 0.6;
}

.wc1-heading {
  top: 12.4%;
  left: 28px;
  font-size: 36px;
  line-height: 1.3;
}

.wc1-body {
  top: 25.8%;
  left: 210px;
  right: 24px;
  font-size: 16px;
  line-height: 1.35;
}

.wc1-body p + p { margin-top: 22px; }

.wc2-heading {
  top: 46.1%;
  left: 28px;
  font-size: 36px;
  line-height: 1.2;
}

.wc2-body {
  top: 52.6%;
  left: 28px;
  width: 250px;
  font-size: 18px;
  line-height: 1.2;
}

.wc3-heading {
  top: 71.9%;
  left: 117px;
  width: 270px;
  font-size: 36px;
  line-height: 1.3;
}

.wc3-body {
  top: 84.5%;
  left: 117px;
  right: 24px;
  font-size: 18px;
  line-height: 1.25;
}

/* ---- pinned behaviour: JS present and motion allowed ----
   script.js writes scroll progress (0→1) as --p on .pin-stage;
   every rule below only READS --p. */

@media (prefers-reduced-motion: no-preference) {

  html.js #what-changes { height: calc(var(--panel-h, 100dvh) * 3); }

  html.js #what-changes .pin-stage {
    position: sticky;
    top: 0;
    height: var(--panel-h, 100dvh);
    overflow: hidden;
  }

  /* TWO-LAYER PARALLAX. Both layers share one canvas, so they start
     perfectly registered: cover on a portrait panel fits the canvas by
     height (1059 → 874), and 79.2% across puts the figure at the right with
     her hand where the mockup has it. From there they diverge — the sky
     drifts UP and grows a little, she grows more and settles DOWN — so the
     depth reads. Scrubbers: they follow --p directly, no transition, and
     nothing cross-fades any more. */
  html.js #what-changes .wc-bg {
    display: block;
    position: absolute;
    inset: 0;
    background-size: cover;
    background-position: 79.2% center;
    background-repeat: no-repeat;
    will-change: transform;
  }

  /* the far layer: rises, grows least. Scale outruns the rise (0.12 against
     2×34/874 = 0.078), so the frame can never slide off its own edge. */
  html.js #what-changes .wc-sky {
    background-image: url(/ritual/images/wc-clouds.png?v=5b6a8986);
    transform-origin: 50% 50%;
    transform: scale(calc(1 + var(--p, 0) * 0.12))
               translateY(calc(var(--p, 0) * -34px));
  }

  /* the near layer: grows more and drifts the other way, coming forward.
     Origin sits on her mass so she grows in place rather than walking out
     of frame, and the growth keeps her clipped edges outside the panel. */
  html.js #what-changes .wc-girl {
    background-image: url(/ritual/images/wc-girl.png?v=758c01ed);
    transform-origin: 72% 78%;
    transform: scale(calc(1 + var(--p, 0) * 0.16))
               translateY(calc(var(--p, 0) * 14px));
  }

  /* The bottom scrim used to ride on the -b photo so it arrived with the
     swap. There is no swap now, so it fades in with the third state instead
     — it is only needed once the Dinacharya copy sits over the photo. It
     lives on the stage so the layer transforms cannot drag it. */
  html.js #what-changes .pin-stage::after {
    content: "";
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 36%;
    background: linear-gradient(to top, rgba(28, 18, 14, 0.45), rgba(28, 18, 14, 0));
    opacity: clamp(0, calc((var(--p) - 0.58) / 0.14), 1);
    z-index: 1;
    pointer-events: none;
  }

  /* persistent top scrim behind the kicker */
  html.js #what-changes .pin-stage::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 34%;
    background: linear-gradient(to bottom, rgba(28, 18, 14, 0.38), rgba(28, 18, 14, 0));
    z-index: 1;
    pointer-events: none;
  }

  html.js #what-changes .wc-kicker { top: 8%; z-index: 3; }
  html.js #what-changes .wc-line   { height: 100%; }  /* fills the pinned stage */

  html.js #what-changes .wc-state {
    position: absolute;
    inset: 0;
    height: auto;
    background: none;
    overflow: visible;
    pointer-events: none;
    z-index: 2;
    transition: opacity 600ms var(--ease), transform 600ms var(--ease);
  }

  /* no repeated lines and no per-state scrims while pinned */
  html.js #what-changes .wc-state::before,
  html.js #what-changes .wc-state::after { content: none; }

  /* state 1: readable from p = 0, leaves around p = 0.33 */
  html.js #what-changes .wc-state-1 {
    --out: clamp(0, calc((0.36 - var(--p)) / 0.08), 1);
    opacity: var(--out);
    transform: translateY(calc((1 - var(--out)) * -12px));
  }

  /* state 2: rises in around 0.33, leaves around 0.66 */
  html.js #what-changes .wc-state-2 {
    --in:  clamp(0, calc((var(--p) - 0.34) / 0.08), 1);
    --out: clamp(0, calc((0.69 - var(--p)) / 0.08), 1);
    opacity: min(var(--in), var(--out));
    transform: translateY(calc((1 - var(--in)) * 24px + (1 - var(--out)) * -12px));
  }

  /* state 3: rises in around 0.66 and stays */
  html.js #what-changes .wc-state-3 {
    --in: clamp(0, calc((var(--p) - 0.67) / 0.08), 1);
    opacity: var(--in);
    transform: translateY(calc((1 - var(--in)) * 24px));
  }
}

/* ============ 3 · BODY MIND SENSES — pinned so the line draws ============
   One state: the 200dvh wrapper gives the stage one extra screen of scroll
   during which the drawn line traces itself (--p 0→1). The default below is
   one plain panel with the line fully drawn — what no-JS and
   prefers-reduced-motion get. */

#body-mind-senses .pin-stage {
  --p: 1;                  /* without JS the drawn line rests fully drawn */
  position: relative;
  height: var(--panel-h, 100dvh);
  overflow: hidden;
  background-color: var(--plum);
  /* CHANGES 4 photograph, from Nothing_stand_v2.png (3× = 1206×2622),
     saved as JPEG: the PNG was 2.6MB of fully opaque pixels, which is a lot
     to decode mid-scroll on a pinned section */
  background-image: url(/ritual/images/body-mind-senses-v2.jpg?v=580ff88d);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/* A mask, at about three quarters of the strength of the first attempt. The
   history matters here, because it was tuned by eye against measurement twice:
   0.66 at the leading edge took the copy to 3.13:1 and read as too dark over
   the film; no mask at all left it at 1.39:1 and read as too bright; 0.50 was
   still a shade heavy. Every stop is now 20% more transparent than that —
   every alpha multiplied by 0.8, so the curve keeps its shape — leaving 0.40 at
   the leading edge. Well under the 3:1 the rest of the page holds, and that is
   the deliberate trade.
   On ::after, not ::before: pseudo-elements paint in tree order alongside the
   z-index 0 film, so ::before would be painted underneath it (NOTES.md 18). */
#body-mind-senses .pin-stage::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(100deg,
                    rgba(28, 18, 14, 0.40) 0%,
                    rgba(28, 18, 14, 0.38) 30%,
                    rgba(28, 18, 14, 0.24) 55%,
                    rgba(28, 18, 14, 0.10) 76%,
                    rgba(28, 18, 14, 0) 92%),
    linear-gradient(to top,
                    rgba(28, 18, 14, 0.11) 0%,
                    rgba(28, 18, 14, 0.04) 24%,
                    rgba(28, 18, 14, 0) 48%);
  pointer-events: none;
}

#body-mind-senses .pin-stage > * {
  position: absolute;
  z-index: 1;
}

/* Pinned to 0 so it sits under the scrim above and the words and line at 1.
   object-position keeps her in frame: she is the right-hand band of the film
   (x 0.83-1.0, measured), and a 16:9 film in a tall phone panel shows only its
   middle quarter, so the phone pans almost fully right — far enough to hold
   her, not so far that the copy loses its sky. Desktop shows 90% of the width,
   so centre already includes her. */
/* the selector needs the id too: `#body-mind-senses .pin-stage > *` sets
   z-index 1 on every child and would otherwise win, putting the film above the
   scrim instead of under it */
#body-mind-senses .pin-stage .bms-video {
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 92% center;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .bms-video { display: none; }
}

@media (prefers-reduced-motion: no-preference) {
  html.js #body-mind-senses { height: calc(var(--panel-h, 100dvh) * 2); }

  html.js #body-mind-senses .pin-stage {
    position: sticky;
    top: 0;
  }
}

/* This top group is fixed-size type, so it is anchored in PIXELS from the
   top, not in percentages. On iPhone the dynamic viewport is shorter than
   the 874 design frame, and percentage tops pulled these three together
   until "One simple ritual." collided with "Senses." */
.bms-kicker {
  top: 55px;               /* y 55 */
  left: 23px;
}

.bms-heading {
  top: 82px;               /* CHANGES 4 box y 82, three lines of ~59.8 */
  left: 23px;
  font-size: 46px;
  line-height: 1.3;        /* tokens.md scale: 46px / 1.3 — measured 59.8px
                              line pitch in screen-05; 55px was the Figma
                              box height, not the rendered leading */
}

.bms-small {
  top: 262px;              /* CHANGES 4 box y 262 */
  left: 23px;
  font-size: 18px;
  line-height: 1.2;
}

.bms-line {
  top: 0;                  /* path coords live in the 402×874 design frame; */
  left: 0;                 /* xMidYMid slice crops it exactly like the photo */
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.bms-line path {
  stroke-dasharray: 1;     /* the inline path carries pathLength="1" */
  stroke-dashoffset: calc(1 - var(--p, 1));
}

/* and the closing group is anchored in pixels from the BOTTOM, for the same
   reason — a shorter viewport then takes its slack out of the photo in the
   middle instead of out of the spacing between lines.

   CHANGES 4 swapped the emphasis of these two blocks (Kalina's comment:
   "Updated the typography for these two — heading and body"). What was the
   small body line is now the large statement; the two closing sentences are
   now the quiet pair beneath it, set as one 23px-leaded block. */
.bms-body {
  bottom: 138px;           /* box y 649, three lines of 29 → ends y 736 */
  left: 117px;
  width: 260px;
  font-size: 24px;
  line-height: 29px;
  font-weight: 600;
}

.bms-close {
  left: 117px;
  width: 264px;
  font-size: 18px;
  line-height: 23px;
  font-weight: 400;        /* they are h3s, so undo the heading weight */
}

.bms-close-1 { bottom: 89px; }   /* box y 762 */
.bms-close-2 { bottom: 66px; }   /* one 23px line below it */

/* ============ 4 · HERO HERB — three stages, two herbs ============
   Scrolling in shows the Ayurveda intro with neither herb chosen. The next
   scroll selects Sunrise, the next Moonlight, and the next leaves for the
   doorway — three stops in one pinned section, driven by data-step exactly as
   #three-steps is. Clicking a control is a SHORTCUT, not a second mechanism:
   script.js scrolls to that stage, so what is on screen and where the page is
   scrolled to can never disagree.

   Everything below this comment is the flow fallback: no pinning, the intro,
   the control and BOTH herbs stacked down the screen in document order, which
   is what a phone with JavaScript off and a reduced-motion visitor get. The
   pinned version lives in the prefers-reduced-motion: no-preference block
   further down, so nothing here has to be undone for it. */

#herb-story .pin-stage {
  position: relative;
  min-height: var(--panel-h, 100dvh);
  overflow: hidden;
  background-color: var(--plum);
  padding: 7% 6.5% 9%;
}

/* Both photographs are painted and stacked, the inactive one at opacity 0 —
   never display:none, because a display:none background is never fetched and
   the first switch would then flash. Fading opacity also means the swap cannot
   move anything: the layers are absolutely positioned and identical in size. */
#herb-story .herb-bg {
  /* display is declared HERE on purpose. The base `.herb-bg` rule further down
     is display:none — it was switched on by the old pinned block, and when that
     block went so did the switch, which left both photographs 0x0 on the phone
     and the section showing nothing but the stage's plum. */
  display: block;
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: 50% 45%;   /* keeps the root/leaf mass in frame */
  background-repeat: no-repeat;
  opacity: 0;
  transition: opacity 700ms var(--ease);
}

/* Sunrise is the default in CSS, not only in JS, so the no-JS page and the
   first paint both show it without waiting for script. */
#herb-story .herb-bg-sunrise { opacity: 1; }

/* readability: a soft scrim top and bottom, never a flat wash over the photo */
#herb-story .pin-stage::before,
#herb-story .pin-stage::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  pointer-events: none;
  z-index: 1;
}
#herb-story .pin-stage::before {
  top: 0;
  height: 46%;
  background: linear-gradient(to bottom, rgba(28, 18, 14, 0.52), rgba(28, 18, 14, 0));
}
#herb-story .pin-stage::after {
  bottom: 0;
  height: 46%;
  background: linear-gradient(to top, rgba(28, 18, 14, 0.58), rgba(28, 18, 14, 0));
}

#herb-story .herb-intro,
#herb-story .hero-herb-switch,
#herb-story .herb-hero {
  position: relative;   /* becomes absolute once pinned */
  z-index: 2;
}

/* ---- phone: intro upper-left, the two choices under it, herb lower-left.
   The left/top/bottom values are the PINNED geometry; in the flow fallback the
   browser ignores them and the margins below do the spacing instead, so one set
   of numbers serves both. ---- */
#herb-story .herb-intro { left: 6.5%; top: 6%; width: 82%; }
#herb-story .herb-intro h2 { margin-top: 14px; }
#herb-story .herb-lede-1 { margin-top: 10px; font-size: 17px; line-height: 1.35; }
#herb-story .herb-lede-2 { margin-top: 12px; font-size: 15px; line-height: 1.42; opacity: 0.92; }

#herb-story .hero-herb-switch { left: 6.5%; top: 45%; margin-top: 30px; }

#herb-story .herb-hero { left: 6.5%; bottom: 7%; width: 82%; margin-top: 34px; }
#herb-story .herb-name { font-family: var(--serif); font-weight: 400; font-size: 30px; line-height: 1.1; }
#herb-story .herb-name-en { display: block; font-size: 19px; margin-top: 4px; }
#herb-story .herb-tale { margin-top: 12px; font-size: 16px; line-height: 1.38; }
#herb-story .herb-close { margin-top: 14px; font-size: 16px; line-height: 1.5; }

/* ---- the selector, drawn as the reference has it: no frame, no fill, just the
   sun or moon above a small-caps label, and the two staggered rather than sitting
   in a row. The selected one is fully present, the other dimmed but still plainly
   a control. Still real buttons, still aria-pressed, still 44px of target — the
   look changed, not the mechanics.

   It reads differently from the doorway on the next screen even without a frame:
   this pair is small, staggered and captioned "Hero", where the doorway is two
   large centred icons captioned with the ritual names. ---- */
#herb-story .hero-herb-switch {
  display: flex;
  align-items: flex-start;
  gap: 30px;
}

.hero-herb-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 9px;
  min-height: 44px;              /* the project's tap-target floor, unchanged */
  padding: 0;
  background: none;
  border: 0;
  color: var(--cream);
  font-family: var(--sans);
  font-weight: 500;
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  opacity: 0.45;
  transition: opacity 400ms var(--ease);
}

/* the sun is an <img>, the moon an inline <svg> — see the note in the markup */
.hero-herb-btn img,
.hero-herb-btn svg { display: block; width: 34px; height: 34px; }
.hero-herb-btn[aria-pressed="true"] { opacity: 1; }
.hero-herb-btn:hover { opacity: 0.75; }
.hero-herb-btn[aria-pressed="true"]:hover { opacity: 1; }
.hero-herb-btn:focus-visible { outline: 2px solid var(--cream); outline-offset: 6px; }

/* the moon hangs below the sun, which is the diagonal the reference shows */
.hero-herb-btn[data-hero-pick="moonlight"] { margin-top: 38px; }

/* ---- pinned behaviour: JS present and motion allowed ----
   Three screens of scroll, one sticky stage, and data-step 1/2/3 chooses what
   is on it. The control never moves or fades between stages; only the intro,
   the herb text and which photograph is on top change. */
@media (prefers-reduced-motion: no-preference) {

  html.js #herb-story { height: calc(var(--panel-h, 100dvh) * 3); }

  html.js #herb-story .pin-stage {
    position: sticky;
    top: 0;
    height: var(--panel-h, 100dvh);
    min-height: 0;
    padding: 0;
  }

  /* back to the corner geometry the numbers above describe */
  html.js #herb-story .herb-intro,
  html.js #herb-story .hero-herb-switch,
  html.js #herb-story .herb-hero {
    position: absolute;
    margin-top: 0;
  }

  /* the intro and the two herb texts are stacked layers; one shows per stage */
  html.js #herb-story .herb-intro,
  html.js #herb-story .herb-hero {
    opacity: 0;
    visibility: hidden;
    transform: translateY(24px);
    transition: opacity 700ms var(--ease), transform 700ms var(--ease), visibility 0s 700ms;
  }

  html.js #herb-story .pin-stage[data-step="1"] .herb-intro,
  html.js #herb-story .pin-stage[data-step="2"] .herb-hero[data-hero-panel="sunrise"],
  html.js #herb-story .pin-stage[data-step="3"] .herb-hero[data-hero-panel="moonlight"] {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: opacity 700ms var(--ease), transform 700ms var(--ease), visibility 0s;
  }

  /* the photograph follows the stage: Sunrise through 1 and 2, Moonlight at 3 */
  html.js #herb-story .pin-stage[data-step="3"] .herb-bg-sunrise { opacity: 0; }
  html.js #herb-story .pin-stage[data-step="3"] .herb-bg-moonlight { opacity: 1; }

  /* and it creeps very slightly closer across the three screens — a scale, not
     a shift, so no edge of the photograph can walk into frame. Well inside the
     8% parallax ceiling. */
  html.js #herb-story .herb-bg {
    transform: scale(calc(1 + 0.045 * var(--p, 0)));
    transform-origin: 50% 45%;
  }

  /* the two icons breathe like the doorway's, same keyframes, same timings */
  .hero-herb-btn .hero-herb-sun  { animation: fork-sun-turn 45s linear infinite; }
  .hero-herb-btn .hero-herb-moon { animation: fork-moon-lean 9s ease-in-out infinite; }
}

@media (prefers-reduced-motion: reduce) {
  #herb-story .herb-bg,
  .hero-herb-btn { transition: none; }
}


/* ============ HERO HERB PHOTOGRAPHS ============
   Two slots per page, all six now REAL exports — Kristian supplied the twelve
   files (mobile and desktop for each of the six herbs) on 2026-08-13, which
   retired the marked stand-ins this comment used to describe. Only four of the
   six could be found by searching layer names on the open Figma page, because
   the MCP sees only the file currently open in the desktop app; the rest came
   across by hand. See NOTES.md 25. */
.herb-bg-sunrise  { background-image: url(/ritual/images/herb-sunrise.jpg?v=3127c96b); }    /* Shunthi */
.herb-bg-moonlight { background-image: url(/ritual/images/herb-moonlight.jpg?v=ac2a9afa); } /* Amalaki */

.herb-state {
  position: relative;
  height: var(--panel-h, 100dvh);
  overflow: hidden;
  background-color: var(--plum);
  background-image: url(/ritual/images/herb-story.jpg?v=b05f7ede);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

.herb-state > * {
  position: absolute;
  z-index: 1;
}

/* stacked scrim: state 1 top, as before */
.herb-state-1::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 34%;
  background: linear-gradient(to bottom, rgba(28, 18, 14, 0.38), rgba(28, 18, 14, 0));
  pointer-events: none;
}

.herb-bg { display: none; }  /* shared photo layer exists only while pinned */

.herb-block {
  left: 22px;
  width: 319px;
}

.herb-block-1 { top: 7.3%; }   /* y 64 */
.herb-block-2 { top: 58.8%; left: 24px; }  /* y 514 */

.herb-block .kicker { margin-bottom: 8px; }

.herb-block h2 {
  font-size: 36px;
  line-height: 1.2;
  margin-bottom: 6px;
}

.herb-body {
  width: 256px;
  font-size: 18px;
  line-height: 1.15;
}

.herb-body-2 { width: 278px; }

.herb-right {
  width: 224px;
  margin-left: 95px;       /* block sits at x 117 */
  margin-top: 14px;
  text-align: right;
  font-size: 18px;
  line-height: 1.15;
}

/* HERO HERB is no longer pinned or scroll-staged — it is one screen, and its two
   herbs are a choice rather than two positions. The height doubling, the sticky
   stage, the --p zoom and the state-1/state-2 crossfade that used to live here
   are all gone; that is what makes a single forward scroll from this screen land
   on the doorway. Its `@media (prefers-reduced-motion: no-preference)` wrapper
   went with them — it had nothing left inside it, and leaving it open silently
   nested every rule below in it. */

/* ============ 5 · MODE FORK ============ */

.fork-heading {
  top: 6.6%;               /* y 58 */
  left: 26px;
  right: 27px;             /* Georgia runs wider than Plantin — keep it to 2 lines */
  font-size: 36px;
  line-height: 1.05;
}

.fork-body {
  top: 16.7%;              /* y 146 */
  left: 26px;
  width: 320px;
  font-size: 18px;
  line-height: 1.15;
}

.fork-blob {
  top: 30.4%;
  left: 2%;
  width: 93%;              /* 379 × 378 blob outline */
  height: 42.6%;
}

/* the two doorway choices are real, equal buttons. All boxes come from the
   mockup vectors and are held as frame proportions so they scale cleanly
   from 360 to 440px wide:
     sun icon (66, 378) 49×48, label x 66.3–190.4, y 446.9–458.5
     moon icon (283.5, 464.7) 35.4×38.7, label x 166.0–316.0, y 530.9–542.5
   The two boxes never overlap (sun ends y 458.5, moon starts y 464.7). */
.fork-option {
  display: block;
  min-height: 44px;
  background: none;
  border: 0;
  padding: 0;
  text-align: left;
  cursor: pointer;
  color: var(--cream);
  font-family: var(--sans);
  font-weight: 500;
}

.fork-option:focus-visible {
  outline: 2px solid var(--cream);
  outline-offset: 6px;
}

.fork-sun {
  left: 16.42%;            /* x 66 */
  top: 43.25%;             /* y 378 */
  width: 30.85%;           /* the 124px label box */
  height: 9.21%;           /* icon top to label bottom (y 458.5) */
}

.fork-sun img {
  position: absolute;
  top: 0;
  left: 0;
  width: 39.5%;            /* 49px of the 124px box */
  height: auto;
}

.fork-moon {
  left: 41.29%;            /* x 166 */
  top: 53.17%;             /* y 464.7 */
  width: 37.29%;           /* the 149.9px label box */
  height: 8.9%;            /* icon top to label bottom (y 542.5) */
}

.fork-moon img {
  position: absolute;
  top: 0;
  left: 78.3%;             /* icon x 283.5 inside the box starting at 166 */
  width: 23.6%;            /* 35.4px of the 149.9px box */
  height: auto;
}

/* The two doorways are alive: the sun turns, the moon rocks and bobs. Both are
   single SVG files, so the whole glyph moves rather than parts of it — which is
   exactly right for the sun, whose eight rays give it 45deg of symmetry, so the
   figure repeats every 5.6s of a 45s turn. That is 8 degrees a second: enough
   to see at a glance, which the first pass at 150s was not. The moon is a
   crescent and cannot turn without looking wrong, so it leans 12 degrees either
   side and rises and falls 3px over 9s. Labels do not move: only the icons
   carry the animation. Sizes and positions come from the layout rules above and
   below this, untouched. */
@media (prefers-reduced-motion: no-preference) {
  .fork-sun img  { animation: fork-sun-turn 45s linear infinite; }
  .fork-moon img { animation: fork-moon-lean 9s ease-in-out infinite; }
}

@keyframes fork-sun-turn {
  to { transform: rotate(360deg); }
}

@keyframes fork-moon-lean {
  0%, 100% { transform: translateY(-3px) rotate(-12deg); }
  50%      { transform: translateY(3px)  rotate(12deg); }
}

.fork-label {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  white-space: nowrap;
  line-height: 1;
  letter-spacing: 0.08em;
  font-size: min(15.7px, 3.906vw); /* the web Brandon runs ~2% wider than the
                                      mockup's cut: 15.7px fills both vector
                                      boxes exactly at the 402 frame, and the
                                      vw term scales it down on narrower
                                      phones with the frame */
}

/* the doorway gate: with JS, everything below the fork stays hidden until a
   path is chosen — nothing below means nothing to scroll to, no scroll
   trapping. The class is only ever added by script.js, so with JavaScript
   off nothing is hidden and the page reads end to end. */
html.js.gated #three-steps,
html.js.gated #summary,
html.js.gated #bundle,
html.js.gated #closing,
html.js.gated #academy,
html.js.gated #footer {
  display: none;
}

/* ============ 6 · THE THREE STEPS ============ */

/* mode-aware photos: the doorway writes data-mode on <html>. Only the
   applied background is fetched, so a sunrise visitor never downloads the
   evening photos and vice versa. */
html[data-mode="moonlight"] .bg-step1   { background-image: url(/ritual/images/evening-step-01.jpg?v=6aa0b74e); }
html[data-mode="moonlight"] .bg-step2   { background-image: url(/ritual/images/evening-step-02.jpg?v=bb9cbddc); }
html[data-mode="moonlight"] .bg-step3   { background-image: url(/ritual/images/evening-step-03.jpg?v=8eeb55f7); }
html[data-mode="moonlight"] .bg-summary { background-image: url(/ritual/images/evening-summary.jpg?v=537d4928); }

/* the kicker's icon follows the mode too */
.steps-kicker .kicker-icon-moon { display: none; }
html[data-mode="moonlight"] .steps-kicker .kicker-icon-sun  { display: none; }
html[data-mode="moonlight"] .steps-kicker .kicker-icon-moon { display: block; }

.steps-kicker {
  top: 5.1%;               /* y 45 */
  left: 22px;
  display: flex;
  align-items: center;
  gap: 26px;
  font-family: var(--sans);
  font-weight: 500;
  font-size: 20px;
  letter-spacing: 0.08em;
  line-height: 1;
}

.steps-subline {
  top: 10%;                /* y 87 */
  left: 22px;
  font-size: 20px;
  line-height: 1.1;
}

/* one state per step: its own photo, its own words */
.steps-state {
  position: relative;
  height: var(--panel-h, 100dvh);
  overflow: hidden;
  background-color: var(--plum);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

.steps-state > * {
  position: absolute;
  z-index: 1;
}

.steps-state::before,
.steps-state::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  pointer-events: none;
}

.steps-state::before {
  top: 0;
  height: 34%;
  background: linear-gradient(to bottom, rgba(28, 18, 14, 0.38), rgba(28, 18, 14, 0));
}

.steps-state::after {
  bottom: 0;
  height: 36%;
  background: linear-gradient(to top, rgba(28, 18, 14, 0.45), rgba(28, 18, 14, 0));
}

/* the shared furniture — kicker, subline, path, pins, labels. Stacked
   fallback: it sits over the first panel. Pinned: it fills the stage. */
.steps-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: var(--panel-h, 100dvh);
  z-index: 2;
  pointer-events: none;
}

.steps-overlay > * { position: absolute; }

/* the AURA path, in the 402×874 design frame — cropped like the photos */
.steps-line {
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

/* the progress copy needs scroll to mean anything; without JS it stays off
   and the base line alone carries the drawing, as the mockup shows it */
.steps-line-progress { display: none; }

/* pins and labels are SVG children now (see index.html): their x/y live in
   the path's own coordinate space, so they cannot drift off the line.
   Without JS the idle art is hidden and every pin reads as reached. */
.step-pin-idle { opacity: 0; }
.step-pin-live { opacity: 1; }

/* labels are <text>: x is the glyph left edge, y the baseline, both measured
   off screen-09/10/11 */
.step-label {
  font-family: var(--serif);
  font-size: 36px;
  fill: var(--cream);
}

.steps-block {
  top: 75.9%;              /* y 663 */
  left: 25px;
  width: 356px;
}

.steps-block h2 {
  font-size: 36px;
  line-height: 1.2;
}

.product-label {
  font-family: var(--sans);
  font-weight: 500;
  font-size: 14px;
  letter-spacing: 0.08em;
  line-height: 1.2;
  margin-top: 8px;
}

.steps-body {
  margin-top: 14px;
  font-size: 16px;
  line-height: 1.3;
}

/* ---- pinned behaviour: JS present and motion allowed ----
   TWO different timings on purpose:
   · the progress line is a SCRUBBER — it reads --p directly, no transition,
     so it tracks the thumb exactly;
   · everything else (photo, text, pin, label) is a STATE CHANGE keyed off
     data-step, transitioned at 700ms with the house easing. */

@media (prefers-reduced-motion: no-preference) {

  html.js #three-steps { height: calc(var(--panel-h, 100dvh) * 3); }

  html.js #three-steps .pin-stage {
    position: sticky;
    top: 0;
    height: var(--panel-h, 100dvh);
    overflow: hidden;
  }

  /* the three states become stacked layers that cross-fade */
  html.js #three-steps .steps-state {
    position: absolute;
    inset: 0;
    height: auto;
    opacity: 0;
    visibility: hidden;
    transition: opacity 700ms var(--ease), visibility 0s 700ms;
  }

  html.js #three-steps .pin-stage[data-step="1"] .steps-state-1,
  html.js #three-steps .pin-stage[data-step="2"] .steps-state-2,
  html.js #three-steps .pin-stage[data-step="3"] .steps-state-3 {
    opacity: 1;
    visibility: visible;
    transition: opacity 700ms var(--ease), visibility 0s;
  }

  html.js #three-steps .steps-overlay {
    inset: 0;
    height: auto;
  }

  /* SCRUBBER — no transition, ever */
  html.js #three-steps .steps-line-progress {
    display: block;
    stroke-dashoffset: calc(1 - var(--p, 0));
  }

  /* STATE CHANGES — the two pin drawings cross-fade, the label grows */
  html.js #three-steps .step-pin {
    transition: opacity 700ms var(--ease);
  }

  html.js #three-steps .step-pin-idle { opacity: 1; }
  html.js #three-steps .step-pin-live { opacity: 0; }

  html.js #three-steps .step-label {
    font-size: 20px;
    opacity: 0.8;            /* the inactive floor is 0.55; this sits above it */
    transition: font-size 700ms var(--ease), opacity 700ms var(--ease);
  }

  html.js #three-steps .pin-stage[data-step="1"] .step-pin-live.step-pin-1,
  html.js #three-steps .pin-stage[data-step="2"] .step-pin-live.step-pin-2,
  html.js #three-steps .pin-stage[data-step="3"] .step-pin-live.step-pin-3 {
    opacity: 1;
  }

  html.js #three-steps .pin-stage[data-step="1"] .step-pin-idle.step-pin-1,
  html.js #three-steps .pin-stage[data-step="2"] .step-pin-idle.step-pin-2,
  html.js #three-steps .pin-stage[data-step="3"] .step-pin-idle.step-pin-3 {
    opacity: 0;
  }

  html.js #three-steps .pin-stage[data-step="1"] .step-label-1,
  html.js #three-steps .pin-stage[data-step="2"] .step-label-2,
  html.js #three-steps .pin-stage[data-step="3"] .step-label-3 {
    font-size: 36px;
    opacity: 1;
  }
}

/* ============ 7 · SUMMARY ============ */

/* the photo layer, outset so its own edges fall outside the panel */
.summary-photo {
  inset: -8px;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  z-index: 0;
}

.summary-block {
  top: 37%;                /* y 323; body flows 16px below the heading (y 425) */
  left: 29px;
  width: 344px;
}

.summary-heading {
  font-size: 36px;
  line-height: 43px;
}

.summary-body {
  margin-top: 16px;
  margin-left: auto;
  width: 224px;
  text-align: right;
  font-size: 16px;
  line-height: 21px;
}

.blob-btn-summary {
  top: 84%;                /* y 734 */
  left: 44px;
  width: 314px;
  height: 88px;
  --blob: url(/ritual/images/blob-button.svg?v=d2dd3bb6);
}

/* ============ 8 · BUNDLE ============ */

/* sun / moon are real toggle buttons; the selected one is full strength,
   the unselected one dims — never below 0.55 */
.bundle-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 3px;
  background: none;
  border: 0;
  cursor: pointer;
  z-index: 2;
  opacity: 1;
}

.bundle-icon:focus-visible {
  outline: 2px solid var(--cream);
  outline-offset: 4px;
}

.bundle-icon[aria-pressed="false"] { opacity: 0.55; }

.bundle-icon svg { display: block; }

.bundle-icon-sun  { top: calc(3.7% - 3px); left: 17px; }   /* icon y 32 */
.bundle-icon-moon { top: calc(3.7% - 3px); right: 17px; }

/* the two bundles live in layers that cross-fade on toggle;
   sun (morning) is the default, .mode-moon on the section swaps them */
/* the whole bundle follows the page mode: html[data-mode] is set at the
   doorway and by the sun/moon icons here. Sunrise is the default layer. */
.bundle-mode { inset: 0; }

.bundle-mode > * {
  position: absolute;
  z-index: 1;
}

.bundle-mode-moon,
html[data-mode="moonlight"] #bundle .bundle-mode-sun {
  opacity: 0;
  visibility: hidden;
}

html[data-mode="moonlight"] #bundle .bundle-mode-moon {
  opacity: 1;
  visibility: visible;
}

/* the evening bundle is its own night photograph (exported from the Figma
   evening frame 1332:3544 — the source shot behind it, not the composed
   frame, so no copy is burned in) */
html[data-mode="moonlight"] .bg-bundle { background-image: url(/ritual/images/evening-bundle.jpg?v=aabf9d4c); }



/* And the products move down by every pixel the crop allows: both evening
   files are slightly narrower than their panels, so cover leaves ~94px (phone)
   and ~119px (desktop) of vertical slack that centring was splitting evenly.
   Anchoring to the top spends all of it pushing the arrangement clear of the
   copy. The morning file is exactly the phone's aspect, so it has no slack —
   which is the other half of why its scrim has to do the work. */
/* ANCHORED TO THE BOTTOM, not the top. Only Vata's evening photograph has any
   vertical slack against the phone panel — 804x1936 covers to 968px against 874,
   so 94px is cropped, where Pitta's and Kapha's evening files are already the
   panel's aspect and have 0 and 1px respectively. This rule therefore moves Vata
   alone. Anchoring top spent that 94px pushing the arrangement DOWN, which left
   the CTA sitting on the products. Anchoring bottom spends it the other way and
   opens 94px of clear ground beneath them for the button, which is what was
   asked for.

   Then measured on an actual iPhone rather than at the design size: bottom was one
   pin too far, the arrangement sitting above its own pins. 67% of the 94px slack
   is a 63px offset, which is the bottom anchor minus 31px — one pin drop — so the
   products come down by exactly that and the pins are not touched. A percentage
   here is a no-op for Pitta and Kapha: their evening files are already the panel's
   aspect, with 0 and 1px of slack, so there is nothing for it to move. */
html[data-mode="moonlight"] .bg-bundle { background-position: center 67%; }

/* THE BUNDLE CARRIES NO CSS VEIL AT ALL. Every one of these photographs now has
   its readability mask baked in, so the page-wide .scrim-top and .scrim-bottom
   are switched off over this section — leaving them on would darken it twice.
   The per-dosha measured veils that used to live here, and the desktop morning
   vignette, are gone for the same reason. NOTES.md 26.
   .bundle-dim below is untouched: that is the tap-to-dim interaction, not a veil. */
#bundle .panel.scrim-top::before,
#bundle .panel.scrim-bottom::after { display: none; }

/* tapping a pin dims the room so the lit product reads as the subject */
.bundle-dim {
  inset: 0;
  background: rgba(12, 9, 8, 0.55);
  opacity: 0;
  pointer-events: none;
}

#bundle.pin-open .bundle-dim { opacity: 1; }

/* motion grammar for the swaps — skipped under reduced motion (instant) */
@media (prefers-reduced-motion: no-preference) {
  .bundle-icon { transition: opacity 600ms var(--ease); }

  .bundle-dim { transition: opacity 800ms var(--ease); } /* a room dimming */

  .bundle-mode { transition: opacity 600ms var(--ease), visibility 0s; }

  .bundle-mode-moon,
  html[data-mode="moonlight"] #bundle .bundle-mode-sun {
    transition: opacity 600ms var(--ease), visibility 0s 600ms;
  }

  html[data-mode="moonlight"] #bundle .bundle-mode-moon {
    transition: opacity 600ms var(--ease), visibility 0s;
  }
}

/* ---- product pins over the photo's cream marks ----
   Centres come from the Figma bundle frames (morning 1303:1858, evening
   1332:3544), where each mark is a 30.8px "Group 7" ellipse — they beat the
   BUNDLE-SPEC.md hand measurements by ≤0.25%. The button is a 44px
   invisible hit area centred on the mark; opening a product fills the mark
   (~4.4% of frame width, like the photographed marks). */

.bundle-pin {
  z-index: 2;
  width: 44px;
  height: 44px;
  margin: -22px 0 0 -22px;   /* left/top mark the mark's CENTRE */
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  border-radius: 50%;
  opacity: 1;
}

.bundle-pin:focus-visible {
  outline: 2px solid var(--cream);
  outline-offset: 2px;
}

/* the mark itself: the cream drop from Figma (images/pin-drop.svg, the
   30.81px "Group 7" ellipse), always visible — it is the only sign the
   photograph is tappable */
.bundle-pin::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 31px;
  height: 31px;
  margin: -15.5px 0 0 -15.5px;
  background: url(/ritual/images/pin-drop.svg?v=2b3ef5d4) center / contain no-repeat;
}

/* the open pin lights up; a soft halo lifts it off the darkened photo */
.bundle-pin[aria-expanded="true"]::before {
  filter: drop-shadow(0 0 10px rgba(255, 239, 222, 0.85))
          drop-shadow(0 0 22px rgba(255, 239, 222, 0.45));
}

/* with a card open, the other pins recede — never below 0.55 */
#bundle.pin-open .bundle-pin { opacity: 0.55; }
#bundle.pin-open .bundle-pin[aria-expanded="true"] { opacity: 1; }

/* morning set — Figma frame 1303:1858 */
.pin-m1 { left: 21.6%; top: 68.8%; }  /* candle     (101.4, 592.4) */
.pin-m2 { left: 46.8%; top: 51.3%; }  /* tea elixir (199.4, 444.4) */
.pin-m3 { left: 69.2%; top: 59.8%; }  /* daybreak   (286.4, 516.4) */
.pin-m4 { left: 43.7%; top: 75.8%; }  /* serum      (187.4, 651.4) */

/* evening set — Figma frame 1332:3544; a different arrangement */
.pin-e1 { left: 14.7%; top: 72.7%; }  /* candle     (68.4, 634.4)  */
.pin-e2 { left: 40.6%; top: 57.1%; }  /* tea        (168.4, 504.4) */
.pin-e3 { left: 87.6%; top: 59.0%; }  /* night veil (349.4, 520.4) */
/* face cream: Figma's (180.4, 627.4) lands just above the jar's lid in our
   crop of the evening photo, so this one is placed on the jar itself */
.pin-e4 { left: 45.0%; top: 76.0%; }

/* ---- overview ↔ product card views (same pattern in both modes) ---- */

.bundle-view { inset: 0; }

.bundle-view > * {
  position: absolute;
  z-index: 1;
}

.bundle-detail {
  opacity: 0;
  visibility: hidden;
}

.bundle-mode[data-open] .bundle-overview {
  opacity: 0;
  visibility: hidden;
}

.bundle-mode[data-open="candle"]     .bundle-detail[data-product="candle"],
.bundle-mode[data-open="tea"]        .bundle-detail[data-product="tea"],
.bundle-mode[data-open="supplement"] .bundle-detail[data-product="supplement"],
.bundle-mode[data-open="skin"]       .bundle-detail[data-product="skin"] {
  opacity: 1;
  visibility: visible;
}

@media (prefers-reduced-motion: no-preference) {
  .bundle-pin { transition: opacity 600ms var(--ease); }
  .bundle-pin::before { transition: opacity 600ms var(--ease); }

  .bundle-view { transition: opacity 600ms var(--ease), visibility 0s; }

  .bundle-detail,
  .bundle-mode[data-open] .bundle-overview {
    transition: opacity 600ms var(--ease), visibility 0s 600ms;
  }

  .bundle-mode[data-open="candle"]     .bundle-detail[data-product="candle"],
  .bundle-mode[data-open="tea"]        .bundle-detail[data-product="tea"],
  .bundle-mode[data-open="supplement"] .bundle-detail[data-product="supplement"],
  .bundle-mode[data-open="skin"]       .bundle-detail[data-product="skin"] {
    transition: opacity 600ms var(--ease), visibility 0s;
  }
}

.bundle-time {
  top: 10.3%;              /* y 90 */
  left: 25px;
  font-family: var(--sans);
  font-weight: 500;
  font-size: 14px;
  letter-spacing: 0.08em;
}

.bundle-heading {
  top: 15%;                /* y 131 */
  left: 28px;
  font-size: 36px;
  line-height: 1;
}

.bundle-subline {
  top: 20.4%;              /* y 178 */
  left: 28px;
  font-size: 20px;
  line-height: 1.1;
}

.bundle-price {
  top: 24.5%;              /* y 214 */
  left: 28px;
  font-size: 20px;
  line-height: 1.1;
  font-weight: 600;
}

.bundle-body {
  top: 28.3%;              /* y 247 */
  left: 118px;
  width: 262px;
  font-size: 16px;
  line-height: 1.3;
}

.bundle-indent {
  top: 35.8%;              /* y 313 */
  left: 207px;
  width: 180px;
  font-size: 16px;
  line-height: 1.3;
}

.blob-btn-bundle {
  top: 84.1%;              /* y 735 */
  left: 53px;
  width: 291px;
  height: 88px;
  --blob: url(/ritual/images/blob-button-bundle.svg?v=d66233da);
}

/* the hint is the one affordance saying the pins are tappable */
.bundle-hint {
  top: 44%;                /* below the indented line, above the tea pin */
  left: 118px;
  width: 262px;
  font-size: 16px;
  line-height: 1.3;
  opacity: 0.85;
}

/* product cards: name, role, description (positions follow the old
   state-2 text block, Figma (22, 67) 347 wide) */
/* The card FLOWS: a name that wraps to two lines pushes the role and the
   description down instead of crowding them. Padding puts the first line
   where the fixed positions used to (y 87, clear of the sun/moon row). */
.bundle-mode .bundle-detail > * { position: static; }

.bundle-detail {
  padding: 87px 33px 0 22px;
}

.card-name {
  font-size: 36px;
  line-height: 1.1;
}

.card-role {
  margin-top: 24px;
  font-family: var(--sans);
  font-weight: 500;
  font-size: 14px;
  letter-spacing: 0.08em;
  line-height: 1.2;
}

.card-text {
  margin-top: 14px;
  font-size: 16px;
  line-height: 20px;
}

/* ============ 9 · CLOSING — pinned, staged reveal ============
   Default below = one plain panel with everything visible and the line fully
   drawn: what no-JS and prefers-reduced-motion get. */

#closing .pin-stage {
  position: relative;
  height: var(--panel-h, 100dvh);
  overflow: hidden;
  background-color: var(--plum);
  /* Outcome_v2.png (3×), saved as JPEG for the same reason */
  background-image: url(/ritual/images/closing-v2.jpg?v=38fb847e);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

#closing .pin-stage > * {
  position: absolute;
  z-index: 1;
}

.closing-line {
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.closing-line-path {
  stroke-dasharray: 1;     /* pathLength="1", so 1 = the whole line */
  stroke-dashoffset: 0;    /* at rest: fully drawn */
  opacity: 0.6;            /* was 0.5 on the path: same width as body-mind-senses,
                              a sixth less ink. Now the same pen as the other two. */
}

/* fixed-size type, so anchored in pixels — percentages of a viewport-height
   panel squeeze these together on a shorter phone (see NOTES.md item 8) */
.closing-heading {
  top: 104px;
  left: 24px;
  width: 354px;
  font-size: 36px;
  line-height: 39px;
}

.closing-sub {
  width: 170px;
  font-size: 26px;
  line-height: 30px;
}

.closing-day  { top: 198px; left: 209px; }
.closing-life { top: 285px; left: 208px; }

.closing-body {
  width: 170px;
  font-size: 20px;
  line-height: 23px;
}

.closing-body-1 { top: 600px; left: 209px; }
.closing-body-2 { top: 683px; left: 209px; }

/* ---- pinned behaviour ----
   the line is a SCRUBBER (reads --p directly, no transition, draws from
   p 0.2 to 1); the three text reveals are transitioned STATE CHANGES keyed
   off data-step, each rising 24px as it fades in */

@media (prefers-reduced-motion: no-preference) {

  html.js #closing { height: calc(var(--panel-h, 100dvh) * 3); }

  html.js #closing .pin-stage {
    position: sticky;
    top: 0;
  }

  /* The first 30.8% of this path runs off-screen to the left (it starts at
     x -314), so the draw starts already 0.308 in — otherwise a third of the
     scroll would spend itself on line nobody can see. From there it draws to
     completion between p 0.22 (as the first block arrives) and p 1. */
  html.js #closing .closing-line-path {
    stroke-dashoffset: calc(0.692px * (1 - clamp(0, calc((var(--p, 0) - 0.22) / 0.78), 1)));
  }

  html.js #closing .closing-sub,
  html.js #closing .closing-body {
    opacity: 0;
    transform: translateY(24px);
    transition: opacity 800ms var(--ease), transform 800ms var(--ease);
  }

  html.js #closing .pin-stage[data-step="2"] .closing-day,
  html.js #closing .pin-stage[data-step="3"] .closing-day,
  html.js #closing .pin-stage[data-step="4"] .closing-day,
  html.js #closing .pin-stage[data-step="3"] .closing-life,
  html.js #closing .pin-stage[data-step="4"] .closing-life,
  html.js #closing .pin-stage[data-step="4"] .closing-body {
    opacity: 1;
    transform: none;
  }
}

/* ============ 10 · ACADEMY (frame is 734 tall) ============ */

/* The film sits under the scrim and the type: .panel > * would give it
   z-index 1 alongside them, so it is pinned to 0. The scrim pseudo-element
   carries no z-index, so painting order runs film -> scrim -> words. */
.academy-video {
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .academy-video { display: none; }
}

/* Sampled three frames of the clip behind each block of type. On the phone the
   crop lands on the dark middle of the frame and cream already read at
   3.1-7:1, so this stays light; the desktop block below carries a much
   stronger one, where the same type measured 1.24:1. Diagonal, so it is
   heaviest under the words at the top left and gone by the button. */
.academy-dim {
  inset: 0;
  pointer-events: none;
  background: linear-gradient(115deg,
              rgba(28, 18, 14, 0.34) 0%,
              rgba(28, 18, 14, 0.28) 34%,
              rgba(28, 18, 14, 0.16) 58%,
              rgba(28, 18, 14, 0.06) 78%,
              rgba(28, 18, 14, 0) 94%);
}

.academy-kicker {
  top: 9.9%;               /* y 73 */
  left: 32px;
}

.academy-heading {
  top: 13.5%;              /* y ~99 */
  left: 32px;
  width: 331px;
  font-size: 36px;
  line-height: 46px;
}

.academy-body {
  top: 33.1%;              /* y ~243 */
  left: 32px;
  width: 211px;
  font-size: 16px;
  line-height: 20px;
}

.academy-indent {
  top: 39.4%;              /* y ~289 */
  left: 149px;
  right: 24px;             /* the 261px Figma box would poke past the frame */
  font-size: 16px;
  line-height: 20px;
}

.blob-btn-academy {
  top: 80.8%;              /* y 593 */
  left: 41px;
  width: 320px;
  height: 92px;
  --blob: url(/ritual/images/blob-button-academy.svg?v=d357aed1);
}

/* ============ 11 · FOOTER — cream, plum text ============ */

/* One footer, both layouts: mark, the two display lines, the social row over
   its rule, the copyright, the legal links. Every size and gap here is fluid
   (clamp on vw) so the phone version holds together from a 320px screen up to
   the breakpoint, where the desktop block pins the same structure to Kali's
   fixed measurements. */
.footer {
  background: var(--cream);
  color: var(--plum);
  padding: clamp(56px, 15vw, 104px) 24px clamp(40px, 10vw, 64px);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.footer-logo {
  display: block;
  width: clamp(108px, 34vw, 150px);
  height: auto;
}

.footer-line {
  align-self: stretch;
  text-align: left;
  /* The ceiling is 46px, not the desktop 68px: below the breakpoint .page is
     still capped at 402px, so between 403 and 899 the column stays 354px wide
     while vw keeps growing — at 68px "Your rhythm." wrapped. 46px is the
     largest that still fits that column, and the desktop block sets 68px. */
  font-size: clamp(32px, 11.2vw, 46px);
  line-height: 1.15;
  letter-spacing: -0.01em;
}

.footer-line-1 { margin-top: clamp(26px, 8vw, 58px); }
.footer-line-2 { margin-top: clamp(12px, 4vw, 34px); padding-left: 13%; }

/* the rule runs the full width, so the row reaches back through the padding */
.footer-social {
  align-self: stretch;
  display: flex;
  justify-content: center;
  gap: clamp(6px, 2.5vw, 12px);
  margin: clamp(34px, 10vw, 58px) -24px 0;
  padding: clamp(22px, 7vw, 34px) 24px 0;
  border-top: 1px solid rgba(61, 35, 59, 0.14);
}

.footer-social a {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
}

.footer-social img { width: clamp(19px, 5.4vw, 25px); height: auto; }

.footer-copy {
  margin-top: clamp(22px, 6.5vw, 36px);
  font-size: clamp(13px, 3.4vw, 15px);
  opacity: 0.65;
}

.footer-links { margin-top: clamp(8px, 2.5vw, 12px); }

.footer-row {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;         /* fallback sans is wider than Brandon; keep links whole */
  column-gap: clamp(16px, 5vw, 24px);
}

.footer-row-2 { margin-top: 2px; }

.footer-links a {
  display: inline-block;
  white-space: nowrap;
  min-height: 44px;        /* 35px line + padding = comfortable tap target */
  padding: 5px 0;
  font-family: var(--sans);
  font-weight: 400;
  font-size: 14px;
  line-height: 35px;
  color: rgba(61, 35, 59, 0.6);
  text-decoration: underline;
}

/* ============================================================
   DESKTOP — design frame 1440 wide
   Read from the Figma desktop frames on page VATA_V5: the
   " Vata Ritual Page" frame (1520:42, 1440x7293) plus the state
   frames beside it (CHANGES 2/3/4, CHANGES 7, Section_03,
   ritauls_section_03_step_00..02, Morning Ritual, Night Ritual,
   Secction_06_Holistic ritual, Academy section, Footer).

   How this block is built, so it stays predictable:
   * Nothing above this line changed. Every desktop rule lives inside
     the single @media (min-width: 900px) below, so the mobile page
     renders and downloads exactly what it did before — no desktop
     photograph is ever fetched on a phone.
   * Desktop-only markup is display:none by default (right here) and
     switched on inside the query.
   * x positions are percentages of the frame width (50px -> 3.472%)
     so margins grow with the window; text measures are capped in px
     so no line gets too long on a wide screen.
   * y positions are percentages of that section's frame height,
     except groups the frames anchor to an edge (header, kickers),
     which stay px from that edge.
   * Each drawn line is its own inline SVG placed in the same
     percentage grid with preserveAspectRatio="none" — which is how
     Figma exported them, because the vectors are non-uniformly
     scaled into their boxes. The step pins and labels use that same
     grid, so line and pins are one linear map and stay registered to
     each other at every window shape.
   * Pinned-section positioning sits inside prefers-reduced-motion:
     no-preference + html.js, exactly like the mobile rules, so the
     stacked reduced-motion fallback is untouched.
   ============================================================ */

/* .site-nav is NOT in this list: it has its own phone presentation above, and
   the footer rows are no longer desktop-only either. */
.d-line, .fork-blob-d, .d-step-pin, .d-step-label { display: none; }

@media (min-width: 900px) {

/* ---------- shell ---------- */

.page { max-width: none; }

.d-line { display: block; position: absolute; z-index: 1; }
.d-step-pin, .d-step-label { display: block; }
.fork-blob-d { display: block; }

/* mobile furniture the desktop frames do not carry. The burger needs the
   html.js prefix to outrank `html.js .nav-btn-menu` in the phone block. */
.nav-btn, .fork-blob, .wc-line, .bms-line, .steps-line, .closing-line,
html.js .nav-btn-menu { display: none; }

/* a line is fully drawn unless the scroll handler is driving it */
.d-line-path { stroke-dasharray: none; stroke-dashoffset: 0; }
.steps-line-d-progress { display: none; }

.kicker, .steps-kicker, .academy-kicker { font-size: 14px; }

/* ---------- photographs ---------- */

/* Every one of these is exported already cropped to its design frame, so
   `cover` + centre is the whole story — the hand-computed offsets that used
   to be here were derived from raw layer positions and were simply wrong.
   The doorway photograph is Figma's "image 76" (1570:501): the layer above
   it, "Rectangle 21", is a background blur, which is why exporting THAT one
   produced a sheet of flat grey. */
.bg-hero    { background-image: url(/ritual/images/d-hero.jpg?v=eb471cce); }
.bg-fork    { background-image: url(/ritual/images/d-mode-fork.jpg?v=6b1ab9bf); }
.bg-step1   { background-image: url(/ritual/images/d-step-01.jpg?v=e1ef1449); }
.bg-step2   { background-image: url(/ritual/images/d-step-02.jpg?v=1a89fdc1); }
.bg-step3   { background-image: url(/ritual/images/d-step-03.jpg?v=2db6427d); }
.bg-summary { background-image: url(/ritual/images/d-step-03.jpg?v=2db6427d); }
.bg-bundle  { background-image: url(/ritual/images/d-bundle.jpg?v=5ed83e35); }
.bg-academy { background-image: url(/ritual/images/d-academy.jpg?v=58eaa041); }

html[data-mode="moonlight"] .bg-step1 { background-image: url(/ritual/images/d-evening-step-01.jpg?v=c55453de); }
html[data-mode="moonlight"] .bg-step2 { background-image: url(/ritual/images/d-evening-step-02.jpg?v=06a399a0); }
html[data-mode="moonlight"] .bg-step3 { background-image: url(/ritual/images/d-evening-step-03.jpg?v=44cc64dd); }
html[data-mode="moonlight"] .bg-summary { background-image: url(/ritual/images/d-evening-step-03.jpg?v=44cc64dd); }
html[data-mode="moonlight"] .bg-bundle { background-image: url(/ritual/images/d-bundle-evening.jpg?v=758b6e20); }

/* Both bundle photographs are about 1.42 wide. `cover` only shows their whole
   width while the panel is wider than that; on a window taller in proportion
   — a tall or a zoomed one — cover scales by height instead and takes the
   sides off, which cuts the outermost jar, since the arrangement runs from
   0.11 to 0.97 of the frame's width.
   So the width is pinned instead: `100% auto` always shows the whole width
   and spends the difference on the sky and the sand, which are the parts that
   can afford it. At a normal laptop shape this computes to exactly what cover
   already did — 1440x900 gives 1440x1013 either way — so nothing moves there.
   Where it would leave the panel short, a gradient behind it carries the two
   edge tones of the photograph itself, sampled from its top and bottom twelve
   rows: #AEB0B6 dune haze and #6E6462 sand for the morning, #474F68 and
   #585762 for the evening. A second copy of the photograph at cover was the
   first idea and is worse — it repeats the top rows at a second scale, which
   duplicates the grass stalks visibly. A tone cannot do that. */
#bundle .panel.bg-bundle {
  background-image: url(/ritual/images/d-bundle.jpg?v=5ed83e35),
                    linear-gradient(to bottom, #AEB0B6 0%, #6E6462 100%);
  background-size: 100% auto, 100% 100%;
  background-position: center center, center center;
  background-repeat: no-repeat, no-repeat;
}

/* The evening keeps its top anchor: what vertical room there is goes on
   pushing the arrangement down, clear of the copy — which also puts the
   whole of the shortfall at the bottom edge rather than splitting it. */
html[data-mode="moonlight"] #bundle .panel.bg-bundle {
  background-image: url(/ritual/images/d-bundle-evening.jpg?v=758b6e20),
                    linear-gradient(to bottom, #474F68 0%, #585762 100%);
  background-size: 100% auto, 100% 100%;
  /* 55%, not a pixel value, because the amount of photograph hanging below the
     panel scales with the window: at 1920x900 the overflow is 459px and 55% of
     it is the 250px the cream jar needed, while at the 1440 design width the
     photograph very nearly fits and 55% is under 2px. A -250px would have
     opened a 247px gap there. Pitta and Kapha set their own value. */
  background-position: center 55%, center top;
  background-repeat: no-repeat, no-repeat;
}

#body-mind-senses .pin-stage { background-image: url(/ritual/images/d-body-mind-senses.jpg?v=72149c87); }
/* No object-position override up here on purpose. Desktop sees 90% of the
   film's width, and the phone's 92% pan holds her whole (visible slice
   0.092-0.992 against her 0.828-0.995) where centre would clip her right
   edge — so one value serves both. */
#closing .pin-stage { background-image: url(/ritual/images/d-closing.jpg?v=7887f11b); }
/* the retired .herb-state keeps its desktop photograph for the no-JS stacked
   fallback; #herb-story .pin-stage must NOT have one — the two .herb-bg layers
   are the picture now, and a background on the stage showed through beside them
   as a second, wrong image on Pitta and Kapha. */
.herb-state { background-image: url(/ritual/images/d-herb-story.jpg?v=88d6a7cc); }
.wc-state-2, .wc-state-3 { background-image: url(/ritual/images/wc-clouds.png?v=5b6a8986); }
/* the phone paints its own line as a ::before on state 3; the desktop
   line is a real SVG, so that copy has to go */
.wc-state-3::before { content: none; }

/* The desktop "CHANGES 5" frame is a single flattened photograph with the
   kicker AND the line burned into it, so it is not usable as a background.
   images/wc-clouds.png and wc-girl.png are the same photograph already split
   into two clean landscape layers (1486x1059, the girl with an alpha
   channel) - so desktop uses those, and gets the same real depth parallax
   the phone has. Nothing is cropped away: the canvas is 1.40 wide to the
   panel's 1.60, so cover trims only ~6% off the top and bottom. */
#what-changes .wc-bg { background-size: cover; background-position: center center; }
html.js #what-changes .wc-sky,
html.js #what-changes .wc-girl { background-position: center center; }

/* the summary photograph is the step 3 photograph, blurred — on the
   phone that blur is baked into summary.jpg; here it is a filter, and
   .summary-photo already bleeds past every panel edge so the blur has
   no edge to sample */
.summary-photo { filter: blur(14px); }

/* ---------- 1 · hero + header ---------- */

.hero-nav {
  top: 0; left: 0; right: 0;
  height: 110px;
  padding: 0 35px;
  display: flex;
  align-items: center;
}


.site-nav { order: 1; }

/* Centring comes from the base rule; the desktop frame only changes the size.
   Kali's header has the wordmark at x=680 (centre 746), 26px right of the page
   centre, which does not survive an arbitrary window width — this sits on 720
   and stays there. */
.nav-logo { width: 132px; height: auto; }

/* The same three links, now inline in the header and left-aligned. The nav
   element lives outside main.page (it is the phone's fixed drawer), so up here
   it is absolutely positioned back into the header: the initial containing
   block starts at the document origin, which is exactly where the header sits,
   and it scrolls away with the page as it should.
   Measurements are Kali's header frame (1520:84): the list sits at x=35 and
   each item carries 15px of its own padding, which puts the first label at
   x=50 and leaves 30px between labels — her HOME label ends at 98.9 and the
   next begins at 128.6. 14px Brandon 500. */
.nav-scrim, .site-nav-head { display: none; }
html.js .nav-scrim { display: none; }   /* outranks `html.js .nav-scrim` above */

.site-nav,
html.js .site-nav,
html.js[data-nav-open] .site-nav {
  visibility: visible;
  position: absolute;
  top: 0;
  left: 35px;
  right: auto;
  bottom: auto;
  z-index: 3;
  display: flex;
  flex-direction: row;
  align-items: center;
  height: 110px;
  width: auto;
  max-width: none;
  overflow: visible;
  padding: 0;
  background: none;
  color: var(--cream);
  transform: none;
  transition: none;
  font-size: 14px;
  letter-spacing: 0.08em;
}

.site-nav-list { display: flex; flex-direction: row; align-items: center; padding: 0; }

.site-nav a {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0 15px;
  border-bottom: 0;
  color: var(--cream);
  text-decoration: none;
}

.site-nav a:focus-visible {
  outline-color: var(--cream);
}

/* the phone's three hard line breaks are dropped so the desktop frame's
   two lines can form from the measure instead */
.hero-lines   { left: 3.96%;  top: 28.4%;  width: min(515px, 36%); font-size: 32px; line-height: 1.25; }
.hero-lines br { display: none; }

/* Two gaps here have to read as one interval: the one under the heading and
   the one under "You are". Measured ink to ink at 1440x900 they were 20px and
   30px, and they could never be equal while both lines were positioned as a
   percentage of the panel — the gap is (percentage difference x height) minus
   a font block that does not scale, so equalising at one window height breaks
   at the next. So each of the two lower lines is now anchored to the line it
   answers, in pixels: 79px is the heading's own two-line ink block, 37px is
   "You are" plus the cap-height offset of the dosha line, and the 30px is the
   interval itself. Now equal at every height. Kali's frame runs 40/42 at 945
   tall for the same two gaps, so 30 is the tighter of her two, not a new
   invention. */
.hero-why     { left: 11.67%; top: calc(28.4% + 109px);  right: auto; text-align: left; font-size: 32px; }

/* "You are" sits on the em-dash of "-Vata", as it does in the frame (her two
   left edges are 4px apart, which is just the dash's own side bearing). Both
   lines were centred in the same box, which put "You are" 89px right of the
   dash; left-aligning both to one edge makes them align by construction
   rather than by a measured offset, so it survives a font fallback. 32.5% is
   where the centred dash already sat, so the dosha line itself does not move. */
.hero-you-are { left: 32.5%; top: 50.05%; width: 28.75%; text-align: left; font-size: 26px; }
.hero-dosha   { left: 32.5%; top: calc(50.05% + 67px); width: 28.75%; text-align: left; font-size: 86px; }
.hero-cue     { left: 41.88%; top: 87.62%; width: auto; text-align: left; }

/* ---------- 5 · mode fork (doorway) ---------- */

.fork-heading { left: 3.68%; top: 11.33%; right: auto; width: min(411px, 28.6%); font-size: 36px; line-height: 1.25; }
.fork-body    { left: 3.68%; top: 23.33%; width: min(441px, 30.6%); font-size: 20px; line-height: 1.4; }

.fork-blob-d { position: absolute; left: 11.46%; top: 38.33%; width: 77.08%; height: 49.44%; z-index: 1; }

/* boxes read off the doorway frame: the sun group sits at x 395-602
   y 495-612, the moon group at x 850-1092 y 540-662 */
.fork-option { z-index: 2; }
.fork-sun  { left: 27.43%; top: 55%; width: 14.38%; height: 13%; }
.fork-moon { left: 59.03%; right: auto; top: 60%; width: 16.81%; height: 13.56%; }
.fork-sun img  { position: absolute; top: 0; left: 5px;  width: 66px; height: auto; }
.fork-moon img { position: absolute; top: 0; left: auto; right: 2px; width: 50px; height: auto; }
.fork-label { font-size: 16px; }
.fork-sun  .fork-label { text-align: left; }
.fork-moon .fork-label { text-align: right; }

/* ---------- 7 · summary ---------- */

.summary-block   { left: 27.43%; top: 35.89%; width: min(515px, 36%); }
.summary-heading { font-size: 40px; line-height: 1.35; }
.summary-body    { font-size: 22px; line-height: 1.3; margin-top: 38px; width: auto; text-align: left; }

.blob-btn-summary {
  left: 43.13%; top: 67.78%;
  width: 315px; height: 89px;
  --blob: url(/ritual/images/d-blob-summary.svg?v=d2dd3bb6);
  font-size: 15px;
}

/* ---------- 8 · bundle ---------- */

/* The whole left column comes off the Morning Ritual frame (1570:1079),
   whose own y values are: sun icon 70, its time label 178.85, heading 229.85,
   subline 303.85, price 364.85, body 460, indent 552 — as fractions of that
   frame's 1018px height. The time and the ritual name used to sit 6px apart
   because I had guessed those two numbers; the frame gives them 51px. The
   indent and hint sit a little lower than the frame has them, because this
   build's body copy is a line taller than hers at these sizes. */
.bundle-icon-sun  { left: 3.47%; right: auto; top: 6.88%; width: 56px; height: 56px; }
.bundle-icon-moon { left: auto; right: 5.63%; top: 6.88%; width: 56px; height: 56px; }
.bundle-icon img, .bundle-icon svg { width: 56px; height: auto; }

.bundle-mode-sun  .bundle-time { left: 3.47%; right: auto; top: 17.57%; text-align: left; }
.bundle-mode-moon .bundle-time { left: auto; right: 3.40%; top: 16.50%; text-align: right; }

.bundle-heading { left: 3.47%; top: 22.58%; font-size: 44px; }
.bundle-subline { left: 3.47%; top: 29.85%; font-size: 26px; }
.bundle-price   { left: 3.47%; top: 35.84%; font-size: 20px; }
.bundle-body    { left: 3.40%; top: 45.19%; width: min(430px, 30%); font-size: 18px; line-height: 1.45; }

/* The two lines after the body step in, rather than all three sharing one
   edge. 9.86% is Kali's own value — her "Chosen for Vata" block starts at
   x=142 against the body's x=50, a 92px step, and the phone steps by 89px
   between the same two lines, so it is the design's interval both places.
   The hint takes a second step of the same 92px: her desktop frame has no
   hint line to read (it is ours, from COPY.md), and a progressive staircase
   is the grammar the outcome section already uses. Both bundles get this —
   the morning and evening copy blocks are separate elements sharing these
   class rules. */
.bundle-indent  { left: 9.86%;  top: 56.50%; width: min(430px, 30%); font-size: 18px; line-height: 1.45; }
.bundle-hint    { left: 16.25%; top: 64.50%; font-size: 13px; }



.bundle-detail  { left: 3.47%; top: 13.78%; width: min(480px, 34%); padding: 0; }
.bundle-detail .card-name { font-size: 40px; line-height: 1.3; }
.bundle-detail .card-role { font-size: 18px; margin-top: 20px; }
.bundle-detail .card-text { font-size: 20px; line-height: 1.4; margin-top: 18px; }

/* held off the RIGHT edge, not the left: the frame puts its right edge 35px
   in from 1440, and a fixed 313px width measured from a left percentage
   walks off the screen once the window is narrower than about 1150px */
.blob-btn-bundle {
  left: auto; right: 2.43%; top: 85.33%;
  width: min(313px, 30%); height: 83px;
  --blob: url(/ritual/images/d-blob-bundle.svg?v=ec8fc1c5);
  font-size: 15px;
}

/* pin centres read off the Morning Ritual frame (1570:1129): the four
   48x48 groups at (532,584) (939,382) (1261,585) (308,825), +24 for
   their centres, as fractions of 1440x1016 */
.bundle-pin { width: 48px; height: 48px; margin: -24px 0 0 -24px; }
.bundle-pin::before {
  width: 48px; height: 48px;
  margin: -24px 0 0 -24px;
  background-image: url(/ritual/images/d-bundle-pin-idle.svg?v=d4eb30d1);
}
.bundle-pin[aria-expanded="true"]::before {
  width: 116px; height: 116px;
  margin: -58px 0 0 -58px;
  background-image: url(/ritual/images/d-bundle-pin-active.svg?v=d1c17fda);
  filter: none;
}

/* The candle pin is the one exception to those frame coordinates. At 38.61% /
   59.84% it lands on the first line of "Chosen for Vata, so you don't have to
   piece it together yourself." — 30px into it at 1440, 7px at 1280, because the
   paragraph is min(430px, 30%) wide while the pin is a fixed percentage, so the
   two close on each other as the window narrows. Moved 2% right and 4% down,
   which clears the ink by 19px at 1440 and 11px at 1280 and keeps clear of the
   hint line below as well. Percentages rather than pixels on purpose: the
   paragraph is positioned in percentages too, so the gap holds at every width
   instead of only the one it was measured at. It also reads better — 38.61% put
   the mark on the corner of the candle's box, and this puts it on its face. */
.pin-m1 { left: 44.0%; top: 65.0%; }   /* candle — see above, not the frame value */
.pin-m2 { left: 92.6%; top: 61.1%; }   /* tea */
.pin-m3 { left: 70.2%; top: 41.1%; }   /* supplement */
.pin-m4 { left: 26.4%; top: 84.7%; }   /* skin */

/* The evening frame (Night Ritual, 1570:1165) is flattened in Figma — its
   photograph is the frame's own fill, "Products_on_stone_slab_4K", and it
   carries no pin layers to read. These four are the pin marks measured off
   the frame's render at (290,707) (533,878) (749,613) (1207,617) in its
   1440x1018 space, then shifted for the ~60px that cover trims off the top
   when that 1.41-wide photograph fills a 1.60-wide panel. Which pin belongs
   to which product is read from the labels on the bottles, so still worth a
   check: NOTES.md item 12. */
.pin-e1 { left: 27.9%; top: 71.2%; }   /* candle, front left */
.pin-e2 { left: 84.2%; top: 62.3%; }   /* tea, right jar */
.pin-e3 { left: 60.7%; top: 61.1%; }   /* supplement, tall centre jar — moved onto the jar */
.pin-e4 { left: 46.8%; top: 85.3%; }   /* skin, small front jar — moved onto the jar */

/* ---------- 10 · academy ---------- */

/* The desktop crop shows the wide frame, including its bright passages: the
   kicker measured 0.701 average luminance at t=7 — 1.24:1 against cream — and
   the body 0.606 at t=3. This is sized to the worst moment sampled, not the
   average, because the picture keeps moving under the words. */
.academy-dim {
  background: linear-gradient(115deg,
              rgba(28, 18, 14, 0.58) 0%,
              rgba(28, 18, 14, 0.50) 34%,
              rgba(28, 18, 14, 0.30) 58%,
              rgba(28, 18, 14, 0.10) 78%,
              rgba(28, 18, 14, 0) 94%);
}

.academy-kicker  { left: 3.47%; top: 31.57%; }
.academy-heading { left: 3.47%; top: 36.51%; width: min(620px, 43%); font-size: 48px; line-height: 1.2; }
.academy-body    { left: 9.58%; top: 54.77%; width: min(340px, 24%); font-size: 20px; line-height: 1.3; }
.academy-indent  { left: 9.58%; top: 62.54%; right: auto; width: min(340px, 24%); font-size: 20px; line-height: 1.3; }

.blob-btn-academy {
  left: auto; right: 9.24%; top: 75.38%;
  width: min(369px, 34%); height: 92px;
  --blob: url(/ritual/images/d-blob-academy.svg?v=3b35a45c);
  font-size: 15px;
}

/* ---------- 11 · footer ---------- */

/* No horizontal padding up here, so the social row's rule reaches the full
   width without the negative margins the phone needs. */
.footer { padding: clamp(96px, 12vw, 170px) 0 58px; }

.footer-logo { width: 140px; }

/* the frame holds both display lines in a left-aligned 803px box starting at
   x=395 — deliberately right of the page centre — with the second line stepped
   in by 112px. They now sit under the mark rather than above it. */
.footer-line {
  padding-left: min(395px, 27.43%);
  font-size: 68px;
}
.footer-line-1 { margin-top: 74px; }
.footer-line-2 { margin-top: 34px; padding-left: calc(min(395px, 27.43%) + 112px); }

.footer-social { gap: 12px; margin: 58px 0 0; padding: 34px 0 0; }
.footer-social img { width: auto; }

.footer-copy { margin-top: 36px; font-size: 15px; }

/* five links on one row up here, divided, instead of the phone's two rows */
.footer-links { display: flex; justify-content: center; margin-top: 12px; }
.footer-row { display: flex; flex-wrap: nowrap; column-gap: 0; }
.footer-row-2 { margin-top: 0; }

.footer-links a { position: relative; padding: 0 41px; font-size: 15px; }
.footer-links a::before {
  content: "";
  position: absolute;
  left: 0; top: 4px; bottom: 4px;
  width: 1px;
  background: rgba(61, 35, 59, 0.25);
}
.footer-row:first-child a:first-child::before { display: none; }

/* ---------- pinned sections ---------- */

@media (prefers-reduced-motion: no-preference) {

/* --- 2 · what changes --- */

html.js #what-changes .wc-kicker { left: 11.46%; top: 178px; }
html.js #what-changes .wc-state  { inset: 0; }

/* state 2's heading is one line, state 1's is two, so state 1 starts
   higher and its body drops below the second line instead of into it */
html.js #what-changes .wc1-heading,
html.js #what-changes .wc2-heading { left: 27.15%; top: 38.38%; width: min(589px, 40.9%); font-size: 48px; line-height: 1.13; }
html.js #what-changes .wc1-heading { top: 33.5%; }
html.js #what-changes .wc1-body,
html.js #what-changes .wc2-body    { left: 27.15%; top: 47.13%; right: auto; width: min(376px, 26.1%); font-size: 24px; line-height: 1.46; }
html.js #what-changes .wc1-body    { top: 48.5%; }

/* State 1 alone comes back to the kicker's own left edge, heading and body
   together. Kali indents all three states equally at 27.15%, so this is a
   departure from her file, asked for directly — and it leaves the three
   states walking right as the section scrubs, 11.46% then 27.15% then
   41.25%, which is the same progression the outcome section uses. */
html.js #what-changes .wc1-heading,
html.js #what-changes .wc1-body    { left: 11.46%; }
html.js #what-changes .wc1-body p + p { margin-top: 26px; }
html.js #what-changes .wc3-heading { left: 41.25%; top: 53%;    width: min(375px, 26%);   font-size: 48px; line-height: 1.15; }
html.js #what-changes .wc3-body    { left: 41.25%; top: 69.63%; right: auto; width: min(422px, 29.3%); font-size: 22px; line-height: 1.41; }

/* The box bleeds top and bottom only, and is flush left. Sampling the path
   at 6000 points confirms the only places it leaves the panel are its two
   ends: it enters through the top edge and exits through the bottom edge, as
   one unbroken run. (At left:-4% it was also one run, but the bottom tail
   left through the left edge as well as the bottom; flush left keeps the
   whole of the drawing on screen and cannot clip it mid-path.) */
html.js #what-changes .wc-line-d { left: 0; top: -6%; width: 17.5%; height: 118%; }
html.js #what-changes .wc-line-d .d-line-path {
  /* 2.11px, not 2px, ON PURPOSE. Each of these SVG boxes is a different size
     relative to its own viewBox, so an equal stroke-width attribute is NOT an
     equal line on screen: measured at 1440, this box renders a nominal width
     at 0.9494 and body-mind-senses' at 0.9982. 2 / 0.9494 = 2.11 puts this
     line on screen at the same 2px as that one. */
  stroke-width: 2.11px;
  stroke-opacity: 0.6;
  /* NEVER put vector-effect="non-scaling-stroke" on a path that is drawn by
     stroke-dashoffset. It measures the dash in device pixels while
     pathLength="1" normalises against user units, so on a 2x screen the dash
     comes out roughly half the path: the stroke breaks into two pieces with a
     gap in the middle, and never finishes. It looks correct at 1x, which is
     what makes it easy to miss. See NOTES.md item 15. */
  stroke-dasharray: 1;
  stroke-dashoffset: calc(1 - var(--p, 0));
}

/* --- 3 · body, mind, senses --- */

/* The two intervals in this stack — kicker to "Body." and "Senses." to "One
   simple ritual." — measured 16px and 25px, both too tight and visibly
   unequal. They are 32px each now. Fixed pixels, not percentages, for the
   same reason as the hero: the kicker is already pinned at 178px and the type
   block does not scale with the panel, so a percentage top makes the interval
   drift with the window height. 226 = 196 (the kicker's ink bottom) + 32 - 2
   (the heading's own ink offset); 470 = 438 (the heading's new ink bottom) +
   32. The right-hand column is untouched and cannot collide — it sits at
   27.08%, a different column entirely. */
html.js #body-mind-senses .bms-kicker  { left: 3.47%; top: 178px; }
html.js #body-mind-senses .bms-heading { left: 3.47%; top: 225px; font-size: 56px; line-height: 1.28; }
html.js #body-mind-senses .bms-small   { left: 3.47%; top: 469px; font-size: 22px; }

html.js #body-mind-senses .bms-body {
  left: 27.08%; top: 57.78%;
  bottom: auto;
  width: 300px;
  font-size: 36px; line-height: 1.1;
}
html.js #body-mind-senses .bms-body-2 { display: block; width: 340px; margin: 26px 0 0 115px; }

html.js #body-mind-senses .bms-close   { left: 35.07%; bottom: auto; width: min(340px, 24%); font-size: 19px; line-height: 1.3; }
html.js #body-mind-senses .bms-close-1 { top: 83%; }
html.js #body-mind-senses .bms-close-2 { top: 85.78%; }

/* -8%/116% so both ends clear the panel by ~70px rather than 18px, which
   was too little to read as running off the edge */
html.js #body-mind-senses .bms-line-d { left: 13.89%; top: -8%; width: 39.58%; height: 116%; }
html.js #body-mind-senses .bms-line-d .d-line-path {
  stroke-dasharray: 1;
  /* this path is drawn bottom-to-top in the design file, so it is revealed
     from its END - that way it grows downward from the top edge, the same
     direction the phone's line grows */
  stroke-dashoffset: calc(var(--p, 0) - 1);
}

/* --- 4 · herb story --- */

/* HERO HERB, desktop: the reference composition — intro upper-left, the two
   choices upper-right, the chosen herb lower-right. Three corners, so the
   photograph keeps its middle and nothing sits on the plant itself. */
#herb-story .herb-intro { left: 3.47%; top: 10.5%; width: min(470px, 33%); }
#herb-story .herb-intro h2 { font-size: 48px; margin-top: 20px; }
#herb-story .herb-lede-1 { font-size: 20px; line-height: 1.4; }
#herb-story .herb-lede-2 { font-size: 16px; line-height: 1.5; margin-top: 16px; }

/* The control spans exactly the herb text block beneath it and the two choices
   are pushed to its ends: the sun's icon and label share the block's left edge,
   the moon's share its right edge. That is the alignment the mockup draws — and
   it is the doorway's, on the next screen, down to the 5px and 2px insets. The
   icon and label sizes are the doorway's desktop values as well (66px sun, 50px
   moon, 16px label), so at this width the two screens read as one family. */
#herb-story .hero-herb-switch {
  left: auto;
  right: 3.47%;
  top: 10.5%;
  width: min(430px, 31%);      /* the same box as .herb-hero below it */
  justify-content: space-between;
  gap: 0;
}
#herb-story .hero-herb-btn { gap: 34px; font-size: 16px; }
#herb-story .hero-herb-btn[data-hero-pick="sunrise"]   { align-items: flex-start; }
#herb-story .hero-herb-btn[data-hero-pick="moonlight"] { align-items: flex-end; margin-top: 80px; }
#herb-story .hero-herb-sun  { width: 66px; height: auto; margin-left: 5px; }
#herb-story .hero-herb-moon { width: 50px; height: auto; margin-right: 2px; }

#herb-story .herb-hero { left: auto; right: 3.47%; bottom: 9%; width: min(430px, 31%); text-align: left; }
#herb-story .herb-name { font-size: 44px; }
#herb-story .herb-name-en { font-size: 22px; margin-top: 6px; }
#herb-story .herb-tale { font-size: 17px; line-height: 1.45; margin-top: 16px; }
#herb-story .herb-close { font-size: 17px; line-height: 1.6; margin-top: 18px; }

/* the photo is wider than tall here, so bias the crop up a little */
#herb-story .herb-bg { background-position: 50% 40%; }
.herb-bg-sunrise  { background-image: url(/ritual/images/d-herb-sunrise.jpg?v=1ec917da); }
.herb-bg-moonlight { background-image: url(/ritual/images/d-herb-moonlight.jpg?v=d0e23ec3); }
html.js #herb-story .herb-body,
html.js #herb-story .herb-body-2 { font-size: 20px; line-height: 1.3; margin-top: 46px; width: auto; }
html.js #herb-story .herb-right { font-size: 20px; line-height: 1.3; margin-top: 30px; margin-left: 118px; width: 300px; text-align: left; }

/* --- 6 · the three steps --- */

html.js #three-steps .steps-kicker  { left: 3.47%; top: 82px; }
html.js #three-steps .steps-subline { left: 3.47%; top: 118px; }

html.js #three-steps .steps-state { inset: 0; }
html.js #three-steps .steps-block { left: 33.89%; width: min(562px, 39%); }
html.js #three-steps .steps-block > * { position: static; }
html.js #three-steps .steps-state-1 .steps-block { top: 43%; }
html.js #three-steps .steps-state-2 .steps-block { top: 44.67%; }
html.js #three-steps .steps-state-3 .steps-block { top: 46.67%; }
html.js #three-steps .steps-block h2 { font-size: 30px; }
html.js #three-steps .steps-block .product-label { font-size: 14px; margin-top: 11px; }
html.js #three-steps .steps-block .steps-body { font-size: 22px; line-height: 1.3; margin-top: 22px; width: auto; }

/* the vector node is x=140 y=255 w=1161 h=450 in the 1440x900 frame;
   the exported viewBox carries the 1px stroke, so the box is that
   rectangle grown by 1px all round */
html.js #three-steps .steps-line-d { left: 9.65%; top: 28.22%; width: 80.76%; height: 50.22%; }
/* The inlined desktop path is ROTATED so that it begins on step 1's pin —
   images/d-line-three-steps.svg starts at the loop's leftmost point instead,
   which had the stroke appearing in mid-air on the left. Rotating a closed
   loop leaves the shape identical (the segment carrying the pin is split with
   de Casteljau and the pieces reordered), and it means the draw is the plain
   mechanism the phone uses: out of step 1, once round, closing back onto step
   1 at p=1. An earlier attempt offset the dash instead, which started in the
   right place but could never draw the final arc — a dash cannot wrap past
   the end of its own path. */
html.js #three-steps .steps-line-d-progress {
  display: block;
  stroke-dasharray: 1;
  stroke-dashoffset: calc(1 - var(--p, 0));
}

html.js #three-steps .d-step-pin {
  position: absolute;
  z-index: 2;
  width: 17px; height: 23px;
  transform: translate(-50%, -50%);
  background: center / contain no-repeat url(/ritual/images/d-pin-idle.svg?v=4a9b3b97);
  transition: width 700ms var(--ease), height 700ms var(--ease);
}
html.js #three-steps .d-step-pin-1 { left: 20%;    top: 38.22%; }
html.js #three-steps .d-step-pin-2 { left: 81.67%; top: 39.56%; }
html.js #three-steps .d-step-pin-3 { left: 32.5%;  top: 76.33%; }

html.js #three-steps .d-step-label {
  position: absolute;
  z-index: 2;
  font-family: var(--serif);
  font-size: 20px;
  opacity: 0.8;
  transition: font-size 700ms var(--ease), opacity 700ms var(--ease);
}
html.js #three-steps .d-step-label-1 { left: 17.85%; top: 43.19%; }
html.js #three-steps .d-step-label-2 { left: 77.43%; top: 43.67%; }
html.js #three-steps .d-step-label-3 { left: 28.47%; top: 80.33%; }

html.js #three-steps .pin-stage[data-step="1"] .d-step-pin-1,
html.js #three-steps .pin-stage[data-step="2"] .d-step-pin-2,
html.js #three-steps .pin-stage[data-step="3"] .d-step-pin-3 {
  width: 62px; height: 49px;
  background-image: url(/ritual/images/d-pin-active.svg?v=07d7ee65);
}

html.js #three-steps .pin-stage[data-step="1"] .d-step-label-1,
html.js #three-steps .pin-stage[data-step="2"] .d-step-label-2,
html.js #three-steps .pin-stage[data-step="3"] .d-step-label-3 {
  font-size: 30px;
  opacity: 1;
}

/* --- 9 · closing --- */

html.js #closing .closing-heading { left: 27.36%; top: 27.13%; width: min(487px, 34%); font-size: 40px; line-height: 1.24; }
html.js #closing .closing-day     { left: 34.86%; top: 42.75%; font-size: 30px; }
html.js #closing .closing-life    { left: 43.06%; top: 51.25%; font-size: 30px; }
html.js #closing .closing-body-1  { left: 51.04%; top: 58.38%; width: min(318px, 22%); font-size: 22px; line-height: 1.36; }
html.js #closing .closing-body-2  { left: 58.68%; top: 69.13%; width: min(322px, 22.4%); font-size: 22px; line-height: 1.36; }

html.js #closing .closing-line-d { left: -14.93%; top: 11.25%; width: 73.06%; height: 88.75%; }

/* The first 0.1718 of this path is off the left edge of the panel — the box
   starts at -14.93%, and where the curve crosses x=0 is the same fraction at
   any window size. Drawing plainly from 0.22 therefore spent the first third
   of the reveal on an invisible run, and the line only appeared at p=0.354,
   well after "Then parts of your day." had arrived. Starting the drawn
   fraction AT 0.1718 puts the visible tip on the edge exactly as that line
   appears, and still completes at p=1. The phone's rule does the same thing
   with 0.692, its own path having a longer hidden run. */
html.js #closing .closing-line-d-path {
  /* same arithmetic as the what-changes line: this box renders at 0.9108, so
     2 / 0.9108 = 2.20 to land on 2px, and the 0.5 on the path comes up to the
     0.6 the other two carry */
  stroke-width: 2.2px;
  opacity: 0.6;
  stroke-dasharray: 1;
  stroke-dashoffset: calc(0.8282 * (1 - clamp(0, calc((var(--p, 0) - 0.22) / 0.78), 1)));
}

}  /* prefers-reduced-motion: no-preference */

}  /* min-width: 900px */
