/* ==========================================================================
   Lumenæ — landing page
   Palette, wordmark typography and stack are locked brand constraints.
   Cream leads; warm-charcoal is used as punctuation, not as the base.
   ========================================================================== */

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; }
/* `height: auto` is load-bearing, not boilerplate. HTML width/height attributes
   are presentational hints that map to CSS width/height. A rule setting only
   `width` overrides half the pair and the element keeps the attribute height:
   .sa-cover__mark (`width: 54px`, attributes 1024x1024) rendered 54x1024 and
   stretched the journal cover placeholder to 320x1196 instead of 320x427.
   Measured 2026-08-31. Every other image on the site sets both dimensions, which
   is why this stayed latent. */
img, svg { display: block; max-width: 100%; height: auto; }
picture { display: contents; }   /* keep <img> in the parent's flex/grid flow */
/* ...but ONLY the <img>. `display: contents` promotes EVERY child of <picture>,
   and <source> is a child. It then becomes a real flex or grid item: measured
   2026-08-31 on /guides/saor/, a <source> took a 490x392 box inside .sa-who__moon
   and pushed the journal cover into the wrong grid column entirely. It stayed
   latent for a long time because in .brand and .footer__brand the stray item
   measures 0x0 and nothing moves. <source> is never rendered; say so. */
source { display: none; }
button, input { font: inherit; color: inherit; }
ul[role="list"], ol[role="list"] { list-style: none; padding: 0; }

/* ---------- Tokens ---------- */
:root {
  /* Locked brand palette */
  --cream:          #F7F1E6;
  --cream-lift:     #FBF8F1;
  --cream-deep:     #EFE5D4;
  --cream-edge:     #E2D6C2;

  --terracotta:     #9B4019;   /* text-safe over hazed cream — 4.95:1 worst case */
  --terracotta-mid: #C4602F;
  --terracotta-lit: #E08A50;   /* dark surfaces only */

  --char:           #221D1A;   /* warm charcoal */
  --char-lift:      #2C2521;
  --char-edge:      #3E352E;

  --ink:            #221D1A;
  --ink-soft:       #5C5148;   /* 6.9:1 on cream */
  --ink-faint:      #695B50;   /* 4.85:1 over hazed cream — smallest text clears AA */

  --on-dark:        #F4EDE2;
  --on-dark-soft:   #BCB0A3;   /* 7:1 on charcoal */
  --gold:           #D9A441;

  /* Rhythm — 4/8 based */
  --s-1: .25rem;  --s-2: .5rem;   --s-3: .75rem;  --s-4: 1rem;
  --s-5: 1.5rem;  --s-6: 2rem;    --s-7: 3rem;    --s-8: 4rem;
  --s-9: 6rem;    --s-10: 8rem;

  --shell: 1140px;
  --radius: 14px;
  --radius-sm: 9px;

  /* Motion — one rhythm across the whole page */
  --ease-out: cubic-bezier(.22, 1, .36, 1);
  --ease-soft: cubic-bezier(.4, 0, .2, 1);
  --ease-reveal: cubic-bezier(.16, 1, .3, 1);   /* long weighted deceleration */
  --t-fast: 160ms;
  --t-mid: 260ms;
  --t-slow: 620ms;
  --t-reveal: 900ms;

  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='.42'/%3E%3C/svg%3E");
}

/* ---------- Atmosphere ----------
   Depth in the light sections comes from haze, not from darkness: a few very
   low-contrast warm blooms plus a whisper of grain. Nothing here should be
   consciously visible — it should only stop the cream reading as flat paint. */
.atmos { position: relative; isolation: isolate; overflow: hidden; }
.atmos > * { position: relative; z-index: 1; }

.atmos::before {
  content: "";
  position: absolute; inset: -12% 0;   /* vertical bleed only — horizontal
                                          overflow would widen the document */
  z-index: 0; pointer-events: none;
  background:
    radial-gradient(58% 42% at 18% 12%, rgba(168, 71, 31, .050), transparent 68%),
    radial-gradient(46% 38% at 88% 76%, rgba(196, 96, 47, .038), transparent 70%),
    radial-gradient(70% 50% at 50% 108%, rgba(34, 29, 26, .045), transparent 72%);
}
.atmos::after {                       /* grain, matched to the dark sections */
  content: "";
  position: absolute; inset: 0;
  z-index: 0; pointer-events: none;
  background-image: var(--grain);
  opacity: .028;
  mix-blend-mode: multiply;
}

/* Per-section bloom placement, so no two sections haze identically. */
.premise::before   { background:
  radial-gradient(52% 40% at 82% 8%,  rgba(168, 71, 31, .055), transparent 68%),
  radial-gradient(64% 46% at 12% 82%, rgba(34, 29, 26, .040), transparent 72%); }

.path::before      { background:
  radial-gradient(44% 52% at 6% 30%,  rgba(196, 96, 47, .048), transparent 66%),
  radial-gradient(58% 40% at 92% 88%, rgba(34, 29, 26, .036), transparent 70%); }

.ecosystem::before { background:
  radial-gradient(66% 44% at 50% -6%, rgba(34, 29, 26, .050), transparent 70%),
  radial-gradient(40% 36% at 14% 92%, rgba(168, 71, 31, .042), transparent 68%); }

/* Soft horizons instead of hard section seams. */
.section-seam { position: relative; }
.section-seam::before {
  content: "";
  position: absolute; inset-inline: 0; top: 0; height: 1px;
  background: linear-gradient(to right,
    transparent, var(--cream-edge) 18%, var(--cream-edge) 82%, transparent);
  opacity: .85;
}

/* Anchor targets clear the sticky bar.

   Every cross-page link in the nav is <page>#section, so arriving at one lands
   the section top flush with the viewport top -- and .nav is position: sticky,
   so the first line of the section arrives underneath it. scroll-margin-top is
   the property that fixes it without moving anything on the page: it applies
   only while scrolling TO the element.

   The value is the stuck bar (56px) plus a little air, not the resting bar
   (68px). By the time a jump of any length settles, .nav.is-stuck has already
   engaged. */
:target, .section, .hero, .sa-hero { scroll-margin-top: 72px; }

/* ---------- Base ---------- */
html { scroll-behavior: smooth; }
body {
  background: var(--cream);
  color: var(--ink);
  font-family: "Instrument Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: 1rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: max(clamp(1.25rem, 5vw, 3rem), env(safe-area-inset-left, 0px));
  padding-inline-end: max(clamp(1.25rem, 5vw, 3rem), env(safe-area-inset-right, 0px));
}

.section { position: relative; padding-block: clamp(4.5rem, 11vw, 9rem); }

/* Below 700px the shared clamp() renders every section at the same ~72px,
   flattening the desktop's proportional rhythm. Weight each section to its
   own role instead: threshold (the dramatic pivot) and access (the closing
   CTA) stay roomy; the denser informational sections tighten up. */
@media (max-width: 699px) {
  .premise   { padding-block: clamp(3.5rem, 13vw, 4.5rem); }
  .threshold { padding-block: clamp(4.5rem, 16vw, 6rem); }
  .path      { padding-block: clamp(3.25rem, 12vw, 4.25rem); }
  .ecosystem { padding-block: clamp(3rem, 11vw, 4rem); }
  .access    { padding-block: clamp(4rem, 14vw, 5rem); }
}

/* ---------- Typography ---------- */
/* Bodoni Moda is locked to the "Lumenæ" wordmark itself, wherever it appears —
   nav/footer lockup and inline in running copy alike. Italic is deliberate: at
   this weight Bodoni Moda's italic æ is a drawn ligature where the a and e
   strokes join into one flowing curve, rather than sitting as two separate
   letterforms — the treatment matches lumenae.lovable.app. Tracking is
   slightly tightened (vs. the previous positive spacing) so the joined
   glyph isn't pulled apart from its neighbours. Colors are untouched.
   .wordmark inherits the surrounding font-size; .brand__word additionally
   fixes the lockup's size. */
.wordmark {
  font-family: "Bodoni Moda", Georgia, serif;
  font-style: italic;
  font-weight: 500;
  letter-spacing: -.01em;
}
.brand__word {
  font-family: "Bodoni Moda", Georgia, serif;
  font-style: italic;
  font-weight: 500;
  letter-spacing: -.01em;
  font-size: 1.32rem;
  line-height: 1;
}

h1, .h2, .h3 {
  font-family: Fraunces, Georgia, "Times New Roman", serif;
  font-weight: 400;
  line-height: 1.08;
  letter-spacing: -.015em;
  text-wrap: balance;
}

.hero__title {
  font-size: clamp(2.4rem, 6.6vw, 4.6rem);
  font-variation-settings: "opsz" 120;
  letter-spacing: -.025em;
}
.hero__title em {
  font-style: italic;
  color: var(--terracotta);
  font-variation-settings: "opsz" 144;
}

.h2 {
  font-size: clamp(1.85rem, 4vw, 2.9rem);
  font-variation-settings: "opsz" 72;
  margin-bottom: var(--s-5);
}
.h2--light { color: var(--on-dark); }

.h3 {
  font-size: clamp(1.12rem, 1.8vw, 1.3rem);
  line-height: 1.25;
  font-weight: 500;
  font-variation-settings: "opsz" 24;
  margin-bottom: var(--s-3);
}

.eyebrow {
  font-size: .74rem;
  font-weight: 600;
  letter-spacing: .17em;
  text-transform: uppercase;
  color: var(--terracotta);
  margin-bottom: var(--s-4);
}
.eyebrow--light { color: var(--terracotta-lit); }

.lede, .section-lede {
  font-size: clamp(1.03rem, 1.5vw, 1.16rem);
  color: var(--ink-soft);
  max-width: 56ch;
  text-wrap: pretty;
}
.section-lede { margin-bottom: var(--s-7); }
.section-lede--light { color: var(--on-dark-soft); }
.section-lede + .section-lede { margin-top: calc(var(--s-5) * -1 + var(--s-3)); }

/* ---------- Accessibility ---------- */
.skip-link {
  position: absolute; left: var(--s-4); top: var(--s-4);
  transform: translateY(-160%);
  background: var(--char); color: var(--on-dark);
  padding: var(--s-3) var(--s-5); border-radius: var(--radius-sm);
  text-decoration: none; z-index: 1000;
  transition: transform var(--t-mid) var(--ease-out);
}
.skip-link:focus-visible { transform: translateY(0); }

:where(a, button, input):focus-visible {
  outline: 2.5px solid var(--terracotta);
  outline-offset: 3px;
  border-radius: 4px;
}
.threshold :where(a, button, input):focus-visible,
.access :where(a, button, input):focus-visible { outline-color: var(--gold); }

/* ---------- Reveal system ----------
   Motion varies by what an element IS, not by where it sits. A heading wipes
   up as if rising off the baseline; body copy barely moves and mostly fades;
   a label settles its tracking; a card eases open. Uniform fade-up on every
   block is what makes a page feel machine-assembled.
   The hidden state is scoped to .js so the page degrades to fully visible
   content when JavaScript is unavailable or fails. */
.reveal {
  transition: opacity var(--t-slow) var(--ease-reveal),
              transform var(--t-slow) var(--ease-reveal);
  transition-delay: calc(var(--d, 0) * 90ms);
}
.js .reveal { opacity: 0; transform: translate3d(0, 10px, 0); }
.js .reveal.is-in { opacity: 1; transform: translate3d(0, 0, 0); }

/* Headings — wipe up off the baseline, weighted and slow. */
.reveal--rise {
  transition: opacity var(--t-reveal) var(--ease-reveal),
              transform var(--t-reveal) var(--ease-reveal),
              clip-path var(--t-reveal) var(--ease-reveal);
  transition-delay: calc(var(--d, 0) * 90ms);
}
.js .reveal--rise {
  opacity: 0;
  transform: translate3d(0, 26px, 0);
  clip-path: inset(0 0 42% 0);
}
.js .reveal--rise.is-in {
  opacity: 1;
  transform: translate3d(0, 0, 0);
  clip-path: inset(0 0 -18% 0);   /* negative so descenders never clip */
}

/* Labels and eyebrows — tracking settles inward as they arrive. */
.reveal--settle {
  transition: opacity var(--t-reveal) var(--ease-reveal),
              letter-spacing var(--t-reveal) var(--ease-reveal),
              transform var(--t-reveal) var(--ease-reveal);
  transition-delay: calc(var(--d, 0) * 90ms);
}
.js .reveal--settle { opacity: 0; letter-spacing: .34em; transform: none; }
.js .reveal--settle.is-in { opacity: 1; letter-spacing: .17em; }

/* Cards and panels — ease open rather than slide. */
.reveal--bloom {
  transition: opacity var(--t-reveal) var(--ease-reveal),
              transform var(--t-reveal) var(--ease-reveal);
  transition-delay: calc(var(--d, 0) * 90ms);
}
.js .reveal--bloom { opacity: 0; transform: scale(.982) translate3d(0, 12px, 0); }
.js .reveal--bloom.is-in { opacity: 1; transform: scale(1) translate3d(0, 0, 0); }

/* ---------- Buttons ---------- */
.btn {
  --btn-bg: transparent;
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s-2);
  min-height: 48px;
  padding: var(--s-3) var(--s-6);
  border: 1px solid transparent;
  border-radius: 100px;
  background: var(--btn-bg);
  font-size: .95rem; font-weight: 500;
  text-decoration: none;
  cursor: pointer;
  transition: transform var(--t-fast) var(--ease-soft),
              background-color var(--t-fast) var(--ease-soft),
              border-color var(--t-fast) var(--ease-soft),
              box-shadow var(--t-mid) var(--ease-soft);
}
.btn:active { transform: scale(.975); }

.btn--primary {
  --btn-bg: var(--terracotta);
  color: #FFF8F0;
  position: relative;
  overflow: hidden;
  box-shadow: 0 1px 2px rgba(34,29,26,.16), 0 8px 22px -12px rgba(155,64,25,.75);
}
.btn--primary:hover { --btn-bg: #8A3814; box-shadow: 0 2px 4px rgba(34,29,26,.2), 0 14px 30px -12px rgba(155,64,25,.85); }

/* A slow gleam crosses the button on hover — the same light that comes
   through the threshold, not a generic shine. */
.btn--primary::after {
  content: "";
  position: absolute; inset: 0;
  pointer-events: none;
  background: linear-gradient(105deg,
    transparent 32%,
    rgba(255, 240, 214, .20) 47%,
    rgba(255, 246, 230, .30) 50%,
    rgba(255, 240, 214, .20) 53%,
    transparent 68%);
  transform: translateX(-120%);
  transition: transform 900ms var(--ease-reveal);
}
.btn--primary:hover::after,
.btn--primary:focus-visible::after { transform: translateX(120%); }
.btn--primary > * { position: relative; z-index: 1; }

.btn--quiet { color: var(--ink); border-color: var(--cream-edge); }
.btn--quiet:hover { border-color: var(--ink-faint); background: var(--cream-lift); }
.btn--quiet svg { transition: transform var(--t-mid) var(--ease-out); }
.btn--quiet:hover svg { transform: translateY(3px); }

.btn--ghost {
  color: var(--ink); border-color: var(--cream-edge);
  padding-inline: var(--s-5); min-height: 44px; font-size: .88rem;
}
.btn--ghost:hover { border-color: var(--terracotta); color: var(--terracotta); }

/* ---------- Nav ---------- */
.nav {
  position: sticky; top: 0; z-index: 100;
  background: color-mix(in srgb, var(--cream) 82%, transparent);
  border-bottom: 1px solid transparent;
  padding-top: env(safe-area-inset-top, 0px);
  transition: border-color var(--t-mid) var(--ease-soft),
              background-color var(--t-mid) var(--ease-soft),
              margin-bottom var(--t-mid) var(--ease-soft),
              translate var(--t-mid) var(--ease-soft);
}
/* Retracts on scroll down and returns on scroll up, so reading is uninterrupted
   but the menu and CTA are always one short upward scroll away rather than
   stranded at the top of the document. `translate` rather than `transform` for
   the same reason the cards use it — nothing else competes for the property. */
.nav.is-hidden { translate: 0 -100%; }
@supports (backdrop-filter: blur(1px)) {
  .nav { backdrop-filter: blur(14px) saturate(1.4); }
}
.nav.is-stuck { border-bottom-color: var(--cream-edge); background: color-mix(in srgb, var(--cream) 92%, transparent); }

/* The bar tightens by 12px once it is stuck, so it takes less of the screen while
   reading. `.nav` is position:sticky, which means it is still IN FLOW — shrinking
   its height alone would pull every following section up by the delta and the page
   would visibly jump at ~1px of scroll, which is exactly when .is-stuck engages.
   The margin-bottom gives back precisely what the height gives up, on the same
   duration and easing, so the nav's total flow contribution is constant at every
   frame and nothing below it moves. Change one of these two numbers and you must
   change the other. */
.nav.is-stuck { margin-bottom: 12px; }
.nav.is-stuck .nav__inner { min-height: 56px; }

.nav__inner {
  display: flex; align-items: center; gap: var(--s-6);
  min-height: 68px;
  transition: min-height var(--t-mid) var(--ease-soft);
}
.brand {
  display: inline-flex; align-items: center; gap: var(--s-3);
  min-height: 44px;                     /* touch target */
  text-decoration: none; color: var(--ink);
}
.brand__mark { width: 34px; height: 34px; }
.nav__links { display: none; gap: var(--s-5); margin-inline-start: auto; }
.nav__links a {
  position: relative; color: var(--ink-soft); text-decoration: none;
  font-size: .9rem; padding-block: var(--s-2);
  transition: color var(--t-fast) var(--ease-soft);
}
.nav__links a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 2px; height: 1px;
  background: var(--terracotta);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--t-mid) var(--ease-out);
}
.nav__links a:hover { color: var(--ink); }
.nav__links a:hover::after { transform: scaleX(1); }
.nav__cta { margin-inline-start: auto; }

/* ---------- Phone navigation ----------
   Below 900px the desktop link row is replaced, not hidden: a full-height
   numbered index set in the display face.

   The switch went 900 -> 1000 on 2026-08-30 on arithmetic that turned out to be
   wrong on both sides, and came back to 900 on 2026-08-31 once it was measured
   in Brave. With five links the row needs 795px at a 900px viewport against
   885px available — 90px of headroom, and it does not wrap at any width down to
   860px. `need` falls with the viewport because the type scale is fluid, so the
   row shrinks alongside the space it has; the earlier "fixed need vs shrinking
   space" reasoning is what produced the bad estimate. Do not re-derive this on
   paper — run `node tools/audit.js`, which asserts the fit and the collapse.

   FOUR places carry this number and must move together: the block pair below,
   the matching block in css/saor.css (self-contained by design), and the
   matchMedia in js/main.js. Splitting them can strand the page unscrollable
   with the menu hidden, which is exactly what the CSS-driven scroll lock
   exists to prevent. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.nav__toggle {
  display: grid; place-items: center;
  flex: none;                     /* the nav row would otherwise shrink it */
  width: 44px; height: 44px;
  margin-inline-start: var(--s-2);
  background: none; border: 0; cursor: pointer;
  color: var(--ink);
}
.nav__toggle-rules { display: grid; gap: 5px; width: 21px; }
.nav__toggle-rules span {
  display: block; height: 1.5px; width: 100%;
  background: currentColor; border-radius: 2px;
  transition: transform var(--t-mid) var(--ease-out),
              opacity var(--t-fast) var(--ease-soft);
}
.nav__toggle[aria-expanded="true"] .nav__toggle-rules span:first-child {
  transform: translateY(3.25px) rotate(13deg);
}
.nav__toggle[aria-expanded="true"] .nav__toggle-rules span:last-child {
  transform: translateY(-3.25px) rotate(-13deg);
}

/* Scroll lock lives in CSS, driven by a class on <html>, so that crossing the
   desktop breakpoint releases it even if no JS event fires. Relying on a
   resize/matchMedia handler alone can strand the page unscrollable with the
   menu hidden. */
.nav-locked body {
  position: fixed;
  top: var(--lock-top, 0px);
  left: 0; right: 0; width: 100%;
}
@media (min-width: 900px) {
  .nav-locked body { position: static; top: auto; width: auto; }
}

.nav__panel {
  position: fixed; inset: 0; z-index: 200;
  background: var(--cream);
  display: grid; align-content: center;
  overflow-x: hidden; overflow-y: auto;
  padding: var(--s-9) clamp(1.5rem, 8vw, 3rem)
           calc(var(--s-8) + env(safe-area-inset-bottom, 0px));
  opacity: 0; visibility: hidden;
  transition: opacity var(--t-mid) var(--ease-soft),
              visibility 0s linear var(--t-mid);
}
/* Short viewports (phone landscape) can't fit a vertically-centered panel —
   the CTA ends up below the fold with no way to scroll to it. */
@media (max-height: 500px) {
  .nav__panel { align-content: start; padding-block: var(--s-7) var(--s-6); }
  .nav__index { margin-bottom: var(--s-6); }
  .nav__index a { min-height: 44px; }
}
.nav__panel.is-open {
  opacity: 1; visibility: visible;
  transition: opacity var(--t-mid) var(--ease-soft), visibility 0s;
}
.nav__panel::before {           /* same haze as the light sections */
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(58% 40% at 84% 10%, rgba(155, 64, 25, .055), transparent 68%),
    radial-gradient(64% 44% at 10% 88%, rgba(34, 29, 26, .045), transparent 72%);
}
.nav__panel-inner { position: relative; z-index: 1; }

.nav__index { display: grid; gap: var(--s-2); margin-bottom: var(--s-8); }
.nav__index li {
  border-top: 1px solid var(--cream-edge);
  opacity: 0; transform: translate3d(0, 14px, 0);
  transition: opacity var(--t-slow) var(--ease-reveal),
              transform var(--t-slow) var(--ease-reveal);
}
.nav__panel.is-open .nav__index li { opacity: 1; transform: none; }
.nav__panel.is-open .nav__index li:nth-child(1) { transition-delay: 60ms; }
.nav__panel.is-open .nav__index li:nth-child(2) { transition-delay: 120ms; }
.nav__panel.is-open .nav__index li:nth-child(3) { transition-delay: 180ms; }
.nav__panel.is-open .nav__index li:nth-child(4) { transition-delay: 240ms; }

.nav__index a {
  display: flex; align-items: baseline; gap: var(--s-4);
  min-height: 60px; padding-block: var(--s-3);
  font-family: Fraunces, Georgia, serif;
  font-variation-settings: "opsz" 48;
  font-size: clamp(1.28rem, 7vw, 2.1rem);
  line-height: 1.1; letter-spacing: -.015em;
  color: var(--ink); text-decoration: none;
  overflow-wrap: anywhere;
}
.nav__index a span {
  font-family: "Instrument Sans", sans-serif;
  font-size: .72rem; font-weight: 600; letter-spacing: .14em;
  color: var(--terracotta); flex: none;
}
.nav__panel-cta { width: 100%; }

@media (max-width: 379px) {
  .nav__cta { display: none; }
}

@media (min-width: 900px) {
  .nav__links { display: flex; }
  .nav__cta { display: inline-flex; margin-inline-start: 0; }
  .nav__toggle { display: none; }
  .nav__panel { display: none; }
}

/* ---------- Hero ---------- */
.hero {
  position: relative;
  display: flex; align-items: center;
  min-height: min(92svh, 900px);
  padding-block: var(--s-9) var(--s-10);
  overflow: hidden;
}
.hero::after {   /* soft settle into the next section */
  content: ""; position: absolute; inset-inline: 0; bottom: 0; height: 180px;
  background: linear-gradient(to bottom, transparent, var(--cream));
  pointer-events: none;
}
.hero__inner { position: relative; z-index: 2; }

.hero__spiral {
  position: absolute; z-index: 1;
  top: 50%; left: 72%;
  width: clamp(300px, 68vw, 900px); aspect-ratio: 1;
  translate: -50% calc(-50% + var(--drift, 0px));   /* JS-driven parallax */
  opacity: .17;
  animation: spin 99s linear infinite;   /* 10% faster than the original 110s */
  pointer-events: none;
  mask-image: radial-gradient(circle at 50% 50%, #000 42%, transparent 72%);
  will-change: translate;
}
.hero__spiral img { width: 100%; height: auto; }
@keyframes spin { to { rotate: 360deg; } }

.hero__title { margin-bottom: var(--s-5); max-width: 16ch; }
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-block: var(--s-7) var(--s-4); }
.hero__note { font-size: .85rem; color: var(--ink-faint); }

@media (max-width: 399px) {
  .hero__actions { flex-direction: column; align-items: stretch; }
  .hero__actions .btn { width: 100%; }
}

.hero__scroll {
  position: absolute; bottom: var(--s-6); left: 50%; translate: -50% 0;
  width: 1px; height: 54px; overflow: hidden;
  background: var(--cream-edge); z-index: 2;
}
.hero__scroll span {
  display: block; width: 100%; height: 40%;
  background: var(--terracotta);
  animation: trickle 2.6s var(--ease-soft) infinite;
}
@keyframes trickle {
  0%   { transform: translateY(-100%); }
  60%, 100% { transform: translateY(250%); }
}

@media (min-width: 720px) {
  .hero__spiral { left: 78%; opacity: .2; }
}

/* On desktop the spiral is a large ambient form sitting beside and behind the
   type, bleeding off the right edge — it reads as light in the room. Simply
   letting that placement shrink onto a phone crops it to a sliver hiding
   behind the copy, which is not the same design at a smaller size.
   On phones it instead gets its own band at the top of the hero: centred,
   sized past both edges so it keeps the "larger than the frame" quality, and
   fading out exactly where the copy begins. */
@media (max-width: 699px) {
  .hero {
    align-items: start;
    min-height: min(94svh, 820px);
    padding-block: var(--s-7) var(--s-9);
  }
  /* No negative Y offset here, and that is the whole point. The mask fades to
     nothing at 72% of the radius, so with the box top flush to the hero the
     visible edge already begins 28% of the radius down (~36px at this size) —
     clear air between the sticky header and the top of the mark. Every earlier
     negative offset pulled that edge up under the nav, which is what read as
     the header hiding part of the spiral. Keep the Y at drift only. */
  .hero__spiral {
    top: 0; left: 50%;
    /* Stepped down three times: 126vw overhung the frame, 98vw crowded the
       section, 80vw still ran under the header. 68vw clears it outright. */
    width: min(68vw, 280px);
    translate: -50% var(--drift, 0px);
    /* Ceiling set by contrast, not taste: the eyebrow is 11.8px terracotta
       sitting over the spiral's densest ring, and because the mark rotates
       the worst case is the darkest pixel anywhere on that ring. Measured
       against the real artwork — .26 gives 4.33:1 (fails AA), .22 is 4.57:1,
       .20 is 4.70:1. The presence comes from size and centring anyway. */
    opacity: .20;
  }
  /* Tracks the spiral: the copy should start about where the mark fades out,
     so the mark reads as complete rather than as something the text sits on. */
  .hero__inner { padding-top: min(46vw, 190px); }
}

/* ---------- Hero: short landscape ----------
   The one case the hero's vertical budget does not survive. At 812x375 the desktop
   padding alone spends 224px of a 375px-tall viewport, and below 700px the portrait
   spiral band adds another ~190px before the headline even starts. Short-and-wide is
   closer to the desktop proportion than to a phone in portrait, so the mark returns
   to its beside-the-copy placement rather than the band: the same design at the
   aspect ratio it was drawn for, not a stripped-down one. Keyed on height, not
   width, because the real landscape phone widths (812, 844) sit above the 699px
   phone breakpoint and would otherwise never be caught. */
@media (max-height: 560px) and (orientation: landscape) {
  .hero {
    align-items: center;
    min-height: 0;
    padding-block: var(--s-6) var(--s-7);
  }
  .hero__inner { padding-top: 0; }
  /* Would sit well below the fold at this height and cue nothing. */
  .hero__scroll { display: none; }
}
@media (max-width: 699px) and (max-height: 560px) and (orientation: landscape) {
  .hero__spiral {
    top: 50%; left: 74%;
    width: min(56vw, 380px);
    translate: -50% calc(-50% + var(--drift, 0px));
  }
}

/* ---------- Premise / mirror ---------- */
.premise { background: var(--cream); }
.mirror { display: grid; gap: var(--s-5); margin-top: var(--s-8); }
.mirror__item {
  display: grid; grid-template-columns: auto 1fr; gap: var(--s-5);
  align-items: start;
  padding-block: var(--s-4);
  border-top: 1px solid var(--cream-edge);
}
.mirror__item p {
  font-family: Fraunces, Georgia, serif;
  font-variation-settings: "opsz" 40;
  font-size: clamp(1.15rem, 2.2vw, 1.55rem);
  line-height: 1.4; color: var(--ink);
  text-wrap: pretty; max-width: 42ch;
}
.mirror__rule {
  width: 26px; height: 1px; margin-top: 1.1em;
  background: var(--terracotta);
  transform-origin: left;
  transition: transform var(--t-slow) var(--ease-out) 180ms;
}
.js .mirror__rule { transform: scaleX(0); }
.js .mirror__item.is-in .mirror__rule { transform: scaleX(1); }
@media (min-width: 760px) { .mirror__rule { width: 48px; } }
@media (max-width: 480px) { .mirror__item p { font-size: 1rem; } }

/* ---------- Threshold (dark) ---------- */
.threshold, .access {
  background: var(--char);
  color: var(--on-dark);
  overflow: hidden;
}
.threshold__grain {
  position: absolute; inset: 0;
  background-image: var(--grain);
  opacity: .05; mix-blend-mode: overlay; pointer-events: none;
}
.threshold::before, .access::before {   /* warm floor glow */
  content: ""; position: absolute; inset-inline: 0; bottom: -30%;
  height: 70%; pointer-events: none;
  background: radial-gradient(ellipse 60% 100% at 50% 100%, rgba(217,164,65,.14), transparent 70%);
}

.threshold__inner { position: relative; z-index: 2; }

/* Below the desktop breakpoint the door floats into the right of the single
   text column and the copy wraps around it, reclaiming the full measure once
   it clears. A narrow second column was the alternative and is worse: at
   375px it would hold body copy to ~26 characters per line. Because the art
   sits after the <h2> in source order, the headline keeps the full width and
   only the paragraphs share the line with it. */
@media (max-width: 939px) {
  .threshold__art {
    float: right;
    width: clamp(102px, 32%, 240px);   /* ~8% up on the previous 30%/96px */
    margin: var(--s-1) 0 var(--s-5) var(--s-5);
  }
  .threshold__inner::after { content: ""; display: block; clear: both; }
}
/* Every px the door gives back becomes measure in the wrapped lines beside it,
   which is the scarcest thing on a 320-375px screen. */
@media (max-width: 479px) {
  .threshold__art { margin-left: var(--s-4); }
}

/* At desktop width grid placement overrides the source order back into two
   real columns: everything except the art in column one, the art alone in
   column two spanning the full block. */
@media (min-width: 940px) {
  .threshold__inner {
    display: grid;
    grid-template-columns: 1.05fr .95fr;
    column-gap: var(--s-8);
    align-items: start;
  }
  .threshold__inner > :not(.threshold__art) { grid-column: 1; }
  /* `grid-row: 1 / -1` is WRONG here and was the cause of the copy sitting far
     down the section: line -1 refers to the end of the *explicit* grid, and
     these rows are all implicit (no grid-template-rows), so -1 collapsed back
     to line 1. The art occupied row 1 alone, stretched it to its own 673px
     height, and left a 653px hole between the eyebrow and the headline.
     An explicit span count is the fix — it must exceed the number of copy
     elements in column 1. */
  .threshold__art { grid-column: 2; grid-row: 1 / span 20; align-self: center; }
}

.disclosure {
  display: flex; gap: var(--s-3); align-items: flex-start;
  margin-top: var(--s-7); padding: var(--s-4) var(--s-5);
  border: 1px solid var(--char-edge); border-radius: var(--radius);
  background: rgba(255,255,255,.025);
  font-size: .9rem; color: var(--on-dark-soft); max-width: 52ch;
}
.disclosure svg { flex: none; margin-top: 2px; color: var(--gold); }

/* The door — pure CSS, no image, scales to any size */
.door { position: relative; width: min(300px, 74%); margin-inline: auto; }
/* Materials are deliberately the page's own: a hairline frame and warm light,
   not a rendered object. The previous heavy outer drop-shadow and mirror-
   gloss reflection were the only occurrence of either treatment anywhere on
   the site, which is precisely what made the door read as imported. */
.door__frame {
  position: relative; aspect-ratio: 1 / 1.92;
  border-radius: 150px 150px 6px 6px;
  background: linear-gradient(175deg, var(--char-edge), var(--char-lift) 62%);
  box-shadow: inset 0 0 0 1px rgba(244,237,226,.14);
  overflow: hidden;
}
.door__light {
  position: absolute; inset: 9px 9px 0;
  border-radius: 142px 142px 4px 4px;
  background:
    radial-gradient(ellipse 78% 46% at 50% 92%, #FFF3DC 0%, var(--gold) 34%, rgba(217,164,65,.34) 62%, transparent 82%),
    linear-gradient(to bottom, transparent 6%, rgba(217,164,65,.30) 52%, rgba(255,240,214,.62) 100%);
  animation: breathe 7.5s var(--ease-soft) infinite;
}
.door__beam {
  position: absolute; left: 50%; bottom: 0;
  width: 34%; height: 74%; translate: -50% 0;
  background: linear-gradient(to top, rgba(255,244,224,.85), rgba(255,238,208,.16) 55%, transparent 88%);
  filter: blur(9px);
  animation: breathe 7.5s var(--ease-soft) infinite 400ms;
}
@keyframes breathe {
  0%, 100% { opacity: .84; transform: scale(1); }
  50%      { opacity: 1;   transform: scale(1.035); }
}
/* Light spilling onto the floor, drawn the same way as the section's own
   warm floor glow (.threshold::before) rather than as a mirrored surface. */
/* Sized by ratio rather than a fixed height so it tracks the door at every
   width — at the desktop 300px door this resolves to ~97px, matching the
   value it replaces, and it scales down correctly in the floated layout
   without needing a per-breakpoint override. */
.door__reflection {
  aspect-ratio: 3.1 / 1;
  margin-top: 0;
  background: radial-gradient(ellipse 64% 100% at 50% 0%,
              rgba(217,164,65,.30), rgba(217,164,65,.10) 46%, transparent 74%);
  opacity: .9;
}

@media (max-width: 939px) {
  /* The floated wrapper is already the intended width, so the door fills it
     rather than re-applying its own 74% inset. */
  .door { width: 100%; }
}

@media (max-width: 699px) {
  /* At phone width a bordered, filled, rounded panel inside a narrow column
     reads as a UI component dropped into an editorial page. A rule and a
     set-in indent carry the same aside meaning. */
  .disclosure {
    margin-top: var(--s-6);
    padding: var(--s-4) 0 0;
    border: 0;
    border-top: 1px solid var(--char-edge);
    border-radius: 0;
    background: none;
  }
}

/* ---------- Path ---------- */
.path__wrap { position: relative; margin-top: var(--s-8); padding-left: 34px; }
.path__line {
  position: absolute; left: 6px; top: 10px;
  width: 4px; height: calc(100% - 40px);
  color: var(--cream-edge);
  overflow: visible;
}
.path__line path {
  stroke-dasharray: 1;
  stroke-dashoffset: calc(1 - var(--draw, 1));   /* defaults to fully drawn without JS */
  stroke: var(--terracotta);
  opacity: .55;
}
.path__wrap::before {   /* untraced remainder of the line */
  content: ""; position: absolute; left: 7px; top: 10px;
  width: 2px; height: calc(100% - 40px);
  background: var(--cream-edge);
}

.steps { display: grid; gap: var(--s-8); position: relative; }
.step { position: relative; }
.step__dot {
  position: absolute; left: -34px; top: 8px;
  width: 15px; height: 15px; border-radius: 50%;
  background: var(--cream);
  box-shadow: inset 0 0 0 2px var(--cream-edge);
  transition: box-shadow var(--t-reveal) var(--ease-reveal) 240ms,
              transform var(--t-reveal) var(--ease-reveal) 240ms;
}
.step.is-in .step__dot {
  box-shadow: inset 0 0 0 5px var(--terracotta);
  transform: scale(1.12);
}
/* A single ring expands out once, as the path reaches this waypoint. */
.step__dot::after {
  content: "";
  position: absolute; inset: 0;
  border-radius: 50%;
  border: 1.5px solid var(--terracotta);
  opacity: 0;
  transform: scale(1);
}
.step.is-in .step__dot::after { animation: waypoint 1400ms var(--ease-reveal) 300ms 1; }
@keyframes waypoint {
  0%   { opacity: .55; transform: scale(1); }
  100% { opacity: 0;   transform: scale(2.9); }
}
.step__num {
  display: block; font-size: .75rem; font-weight: 600;
  letter-spacing: .14em; color: var(--terracotta); margin-bottom: var(--s-2);
}
.step__body p { color: var(--ink-soft); max-width: 52ch; text-wrap: pretty; }

@media (min-width: 760px) { .path__wrap { padding-left: 56px; } .step__dot { left: -56px; } }

/* The traced line is the section's whole argument — a path with waypoints —
   so it stays on phones rather than being dropped. The gutter narrows and the
   dots come down in scale instead, which recovers the line measure without
   turning the mobile layout into a different idea from the desktop one. */
@media (max-width: 699px) {
  .path__wrap { padding-left: 25px; }
  .path__line { left: 4px; }
  .path__wrap::before { left: 5px; }
  .step__dot { left: -25px; top: 6px; width: 11px; height: 11px; }
  .step.is-in .step__dot { box-shadow: inset 0 0 0 4px var(--terracotta); }
  .steps { gap: var(--s-7); }
}

/* ---------- Ecosystem ---------- */
/* Seam is drawn by .section-seam as a fading hairline — a hard border on both
   edges was part of what made these sections read as stacked slabs. */
.ecosystem { background: var(--cream-lift); }
/* Two columns from tablet up rather than auto-fit. auto-fit resolved to three
   across at ~1024px, which orphans the fourth card onto a row of its own; a
   fixed pair keeps the set as an even 2x2 block at every width above phones
   and gives each card enough room to hold a real sentence. */
.cards {
  display: grid;
  gap: var(--s-4);
  grid-template-columns: 1fr;
  margin-top: var(--s-6);
}
/* 1 / 2 / 4 columns. 560px is where a pair still clears ~240px each — the width
   the cards were authored against. The 4-across row returns at 1100px, which is
   the first width where four columns also clear ~240px; going 4-wide any
   earlier (or using auto-fit, which resolved to three at 1024px and orphaned
   the fourth card) is what made the tablet range look broken. */
@media (min-width: 560px) {
  .cards { grid-template-columns: repeat(2, 1fr); gap: var(--s-5); }
}
@media (min-width: 1100px) {
  .cards { grid-template-columns: repeat(4, 1fr); gap: var(--s-4); }
}
.card {
  padding: var(--s-6);
  background: var(--cream);
  border: 1px solid var(--cream-edge);
  border-radius: var(--radius);
  /* This list re-states the reveal's own opacity/transform transition on
     purpose. A plain `transition` shorthand here silently replaced it: opacity
     dropped out of the property list entirely, so the entrance snapped instead
     of fading, and transition-delay reset to 0s, which killed the stagger. */
  transition:
    opacity var(--t-reveal) var(--ease-reveal),
    transform var(--t-reveal) var(--ease-reveal),
    translate var(--t-mid) var(--ease-out),
    border-color var(--t-mid) var(--ease-soft),
    box-shadow var(--t-mid) var(--ease-out);
  transition-delay: calc(var(--d, 0) * 90ms), calc(var(--d, 0) * 90ms), 0s, 0s, 0s;
}
/* The lift uses the independent `translate` property, not `transform`:
   `.js .reveal--bloom.is-in` sets `transform` at higher specificity than
   `.card:hover`, so a transform-based lift here never actually rendered. */
.card:hover {
  translate: 0 -4px;
  border-color: color-mix(in srgb, var(--terracotta) 42%, var(--cream-edge));
  box-shadow: 0 18px 34px -24px rgba(34,29,26,.5);
}
/* Touch has no hover, so the same highlight is bound to the press instead —
   otherwise the cards are completely inert on a phone. */
@media (hover: none) {
  .card:active {
    translate: 0 -2px;
    border-color: color-mix(in srgb, var(--terracotta) 42%, var(--cream-edge));
    box-shadow: 0 14px 26px -20px rgba(34,29,26,.5);
  }
}
.card__icon {
  display: grid; place-items: center;
  width: 44px; height: 44px; margin-bottom: var(--s-4);
  border-radius: 11px;
  background: var(--cream-deep); color: var(--terracotta);
}
.card p { font-size: .95rem; color: var(--ink-soft); text-wrap: pretty; }
.card em { color: var(--ink); font-style: italic; }

/* The cards keep their full desktop treatment on phones — fill, border, radius
   and lift are what make the reveal legible as motion. An earlier hairline-list
   variant flattened them and read as a downgrade, so it is deliberately gone.
   The card is not a link, so touch gets its feedback from the staggered
   entrance rather than from a press state. */
@media (max-width: 559px) {
  .card { padding: var(--s-5); }
}
/* Below 400px the card's own padding is competing with the shell gutter for the
   same scarce measure, and the gutter has to win — it is what keeps the page from
   reading as edge-to-edge. Tightening to s-4 returns 16px per card to the copy
   while fill, border, radius and the lift are all untouched, so the card still
   reads as a card. Reuses the existing 400px boundary (hero buttons stack there)
   rather than introducing another breakpoint. */
@media (max-width: 399px) {
  .card { padding: var(--s-4); }
  .card__icon { width: 40px; height: 40px; margin-bottom: var(--s-3); }
}
/* Named aside rather than a fifth card: the guides card says what a guide is,
   this says who the first one actually is. Set off by a terracotta rule so it
   reads as a specific claim, not more body copy. */
.ecosystem__note {
  margin-top: var(--s-6);
  padding: var(--s-4) 0 var(--s-4) var(--s-5);
  border-left: 2px solid var(--terracotta);
  font-size: .95rem; color: var(--ink-soft);
  max-width: 62ch; text-wrap: pretty;
}
.ecosystem__note strong { color: var(--ink); font-weight: 500; }
.ecosystem__note em { font-style: italic; color: var(--ink); }

.ecosystem__foot {
  margin-top: var(--s-7); padding-top: var(--s-5);
  border-top: 1px solid var(--cream-edge);
  font-size: .93rem; color: var(--ink-faint); max-width: 60ch;
}

/* ---------- Access / form ---------- */
.access__inner { position: relative; z-index: 2; max-width: 640px; }
.form { margin-top: var(--s-7); display: grid; gap: var(--s-5); }
.field { display: grid; gap: var(--s-2); }
.field__label { font-size: .88rem; font-weight: 500; color: var(--on-dark); }
/* An editorial rule rather than a boxed field. On focus a warm line draws
   across it from the left — the same light that comes through the threshold,
   arriving under what you are about to write. */
.field__rule { position: relative; display: block; }
.field__input {
  width: 100%; min-height: 56px;
  padding: var(--s-3) 0;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--char-edge);
  border-radius: 0;
  color: var(--on-dark);
  font-size: 1.06rem;
  transition: border-color var(--t-mid) var(--ease-soft);
}
.field__input::placeholder { color: #9A8D80; }   /* 4.74:1 on charcoal */
.field__input:hover { border-bottom-color: #554A40; }

/* Pointer focus is indicated by the drawn line alone. Keyboard focus keeps a
   real outline as well — the line is a flourish and must never be the only
   focus indicator. */
.field__input:focus { outline: none; }
.field__input:focus-visible {
  outline: 2.5px solid var(--gold);
  outline-offset: 4px;
  border-radius: 2px;
}

.field__draw {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 1.5px; background: var(--gold);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--t-reveal) var(--ease-reveal),
              background-color var(--t-fast) var(--ease-soft);
}
.field__input:focus ~ .field__draw { transform: scaleX(1); }
.field__input[aria-invalid="true"] { border-bottom-color: #E4785C; }
.field__input[aria-invalid="true"] ~ .field__draw {
  transform: scaleX(1); background: #E4785C;
}
.field__help { font-size: .82rem; color: var(--on-dark-soft); }
.field__error { font-size: .85rem; color: #F09A80; display: flex; gap: var(--s-2); }

.btn--submit { justify-self: start; position: relative; min-width: 210px; }
.btn__spinner {
  position: absolute; width: 17px; height: 17px;
  border: 2px solid rgba(255,255,255,.35);
  border-top-color: #FFF8F0; border-radius: 50%;
  opacity: 0; animation: spin 700ms linear infinite;
}
.btn.is-busy { pointer-events: none; }
.btn.is-busy .btn__label { opacity: 0; }
.btn.is-busy .btn__spinner { opacity: 1; }

/* Confirmation and the not-yet-connected notice share one material — same box, same
   hairline, same phone treatment — and differ only in accent, so the notice reads as
   part of the page rather than as a second invented component. */
.form__success, .form__notice {
  display: flex; gap: var(--s-3);
  margin-top: var(--s-6); padding: var(--s-4) var(--s-5);
  border: 1px solid rgba(217,164,65,.42);
  border-radius: var(--radius);
  background: rgba(217,164,65,.09);
  color: var(--on-dark); font-size: .95rem;
}
.form__success { align-items: center; }
.form__success svg { flex: none; color: var(--gold); }

/* Terracotta rather than gold, and it must stay that way: this is the opposite of a
   confirmation and cannot be allowed to read as one. */
.form__notice {
  align-items: flex-start;
  border-color: rgba(224,138,80,.42);
  background: rgba(224,138,80,.08);
}
.form__notice svg { flex: none; margin-top: 3px; color: var(--terracotta-lit); }

@media (max-width: 699px) {
  /* Confirmation keeps its gold, but as a drawn rule rather than a filled
     panel — consistent with the disclosure and the ecosystem index. */
  .form__success, .form__notice {
    padding: var(--s-4) 0 0;
    border: 0;
    border-top: 1px solid rgba(217,164,65,.42);
    border-radius: 0;
    background: none;
  }
  .form__notice { border-top-color: rgba(224,138,80,.42); }
}

/* ---------- Footer ---------- */
.footer {
  background: var(--cream);
  padding-block: var(--s-8) calc(var(--s-7) + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--cream-edge);
}
.footer__inner { display: grid; gap: var(--s-3); justify-items: center; text-align: center; }
/* An <a> since 2026-08-31, not a <div>: the footer wordmark is the thing every
   visitor tries to click at the end of a page, and on a subpage it is the most
   obvious way back. It goes to #top on the homepage and to the homepage
   everywhere else. inline-flex plus the 44px floor because it is now a control
   and audit.js measures it as one. */
.footer__brand {
  display: inline-flex; align-items: center; gap: var(--s-3);
  min-height: 44px;
  text-decoration: none; color: var(--ink);
  transition: color var(--t-fast) var(--ease-soft);
}
.footer__brand:hover { color: var(--terracotta); }
.footer__line { color: var(--ink-soft); font-size: .95rem; }
.footer__meta { color: var(--ink-faint); font-size: .82rem; }

/* ---------- Not found (404.html) ----------
   The only page on the site with no content of its own, so it has to earn its
   height rather than sit in a thin band under the nav with a screenful of cream
   below it. `min-height` uses svh, not vh: on iOS vh counts the retracted URL
   bar, which would push the footer below the fold on first paint and make the
   page look scrollable when there is nothing to scroll to. The nav (68px) and a
   breathing allowance come off the top so the block centres against what the
   visitor can actually see. */
.notfound { display: grid; align-items: center; min-height: calc(100svh - 68px - var(--s-8)); }
/* Cap the TEXT, never `.shell` itself. `.shell` centres with margin-inline:auto,
   so a max-width on the same element shrinks the thing being centred and the
   whole block drifts to the middle of the page — left-aligned everywhere else
   on the site, centred only here. Same approach as .hero__title's 16ch. */
.notfound__title { max-width: 18ch; }
.notfound__actions { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-6); }

@media (max-width: 399px) {
  /* Matches the hero's treatment at the same boundary — below 400px two buttons
     side by side leave neither with a readable label. */
  .notfound__actions { flex-direction: column; align-items: stretch; }
  .notfound__actions .btn { width: 100%; }
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    /* The card stagger sets real per-property delays; without this they would
       survive the reset and hold elements back for up to 270ms. */
    transition-delay: 0s !important;
    animation-delay: 0s !important;
    scroll-behavior: auto !important;
  }
  .reveal { opacity: 1; transform: none; }
  .mirror__rule { transform: scaleX(1); }
  .path__line path { stroke-dashoffset: 0; }
  .hero__scroll { display: none; }

  /* Motion variants and micro-interactions all resolve to their end state. */
  .js .reveal--rise,
  .js .reveal--rise.is-in { opacity: 1; transform: none; clip-path: none; }
  .js .reveal--settle,
  .js .reveal--settle.is-in { opacity: 1; letter-spacing: .17em; }
  .js .reveal--bloom,
  .js .reveal--bloom.is-in { opacity: 1; transform: none; }
  .btn--primary::after { display: none; }
  .step__dot::after { animation: none !important; opacity: 0; }
  /* Deliberately does NOT force `translate`. `--drift` is only ever set by the
     parallax handler, and that handler is skipped entirely under reduced motion
     (main.js guards on `!reduced`), so --drift stays 0 and each breakpoint's own
     translate already resolves to its correct static placement. The previous
     `translate: -50% -50% !important` re-introduced precisely the negative Y offset
     the phone band must never have: against the band's `top: 0` it lifted the mark
     by half its height (~140px), clipping it above the hero and tucking it under
     the sticky header — the exact failure the band placement was built to avoid,
     and only for reduced-motion users, which is why it went unnoticed. */
  .hero__spiral { will-change: auto; }
}
