/* ════════════════════════════════════════════════════════════════════════
   OZRIC — App homepage (v3)
   Black + Royal Purple palette · OZRIC gold accent · Fraunces + Inter
   Static-but-dynamic: orb is fixed in place; cards crossfade on scroll.
   ════════════════════════════════════════════════════════════════════════ */

:root {
  --void:        #050505;
  --void-deep:   #020202;
  --purple-deep: #1A0838;          /* deep royal — radial bloom backdrop */
  --purple:      #3B1379;          /* mid royal */
  --purple-rich: #6B2BB8;          /* atmospheric layer */
  --violet:      #C687FF;          /* matches orb engine */
  --gold:        #C9A84C;          /* OZRIC accent */
  --gold-soft:   #A6884B;          /* lifted from #8E7634 (4.06:1 fails AA) → 5.45:1 (passes AA Normal). Used in footer separator + button-gradient end-stop. */
  --bone:        #EFE9DD;
  --bone-soft:   #C8C2B6;
  --ash:         #8A8278;
  --ash-deep:    #5C564E;
  --line:        #1F1F1F;

  /* Multi-step black ramp — surfaces between --void and --line. Powers
     hover-state backgrounds, modal interior depth, and any "lifted"
     surface (Framer pattern). Ordering: void < surface-1..4 < line.
     Each step is ~40-50% darker than the next, calibrated so a hover
     state reads as deeper, not louder. */
  --surface-1:   #0A0A0A;          /* card scrim mid */
  --surface-2:   #0F0F0F;          /* hover surface base */
  --surface-3:   #161616;          /* modal interior */
  --surface-4:   #1F1F1F;          /* highest card lift (= --line) */

  /* OZRIC display = Newsreader (Production Type). Body = Inter Tight.
     Both expose stylistic alternates that we enable globally for a more
     bespoke feel — not the typography you'd see on an AI demo page. */
  --serif: 'Newsreader', 'Cormorant Garamond', Georgia, serif;
  --sans:  'Inter Tight', 'Inter', system-ui, -apple-system, sans-serif;

  --gutter:      6vw;
  --gutter-sm:   7vw;

  --ease-luxe:   cubic-bezier(0.22, 1, 0.36, 1);   /* slow-out, satisfying tail */
  --ease-tense:  cubic-bezier(0.65, 0, 0.35, 1);   /* tension — Apple cadence */

  --card-d: 950ms;
}

* { margin: 0; padding: 0; box-sizing: border-box; -webkit-font-smoothing: antialiased; }

/* ────────────────────────────────────────────────────────────────────
   Self-hosted fonts — drops the Google Fonts CDN dependency. Three
   variable-font woff2 files cover the full weight + italic axes used
   across the page. unicode-range pinned to Latin + curly punctuation
   so browsers fetch only what they need.
   ──────────────────────────────────────────────────────────────────── */
@font-face {
  font-family: 'Newsreader';
  font-style: normal;
  font-weight: 200 500;            /* variable axis */
  font-display: swap;
  src: url('/fonts/newsreader-200.woff2') format('woff2-variations'),
       url('/fonts/newsreader-200.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Newsreader';
  font-style: italic;
  font-weight: 200 500;
  font-display: swap;
  src: url('/fonts/newsreader-200-italic.woff2') format('woff2-variations'),
       url('/fonts/newsreader-200-italic.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter Tight';
  font-style: normal;
  font-weight: 200 600;
  font-display: swap;
  src: url('/fonts/inter-tight-200.woff2') format('woff2-variations'),
       url('/fonts/inter-tight-200.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215, U+FEFF, U+FFFD;
}

html, body {
  background: var(--void);
  color: var(--bone);
  font-family: var(--sans);
  font-weight: 300;
  line-height: 1.65;
  letter-spacing: 0.005em;
  /* OpenType features — page-wide:
       liga = standard ligatures (fi, fl, etc.)
       kern = pair kerning
       lnum = lining figures (uniform cap-height numerals)
       tnum = TABULAR figures — every digit takes the same advance
              width, so stat rows ("3,735" vs "7,590") align
              column-true without monospaced typography. Linear
              ships this universally; it's the single highest-leverage
              numeric-typography signal we don't yet have. */
  font-feature-settings: 'liga' 1, 'kern' 1, 'lnum' 1, 'tnum' 1;
}
/* Horizontal-scroll guard lives on <html> only, NOT body — putting it on
   body creates a non-default overflow context that breaks `position:
   sticky` for body-direct children (the .scene--closing terminal lock).
   Keeping it on html alone clips any stray horizontal overflow without
   converting body into a scroll container. */
html { overflow-x: hidden; }
::selection { background: rgba(201,168,76,0.30); color: var(--bone); }

/* Skip-to-content link — visually hidden until focused. WCAG 2.4.1
   (Bypass Blocks, Level A). Visible only when a keyboard user lands
   on it via Tab from the URL bar; appears as a gold pill at the
   top-left and shifts focus to #main on activation. Custom cursor
   is overridden to a regular pointer so the user knows it's
   interactive. */
/* ════════════════════════════════════════════════════════════════════
   FOCUS-VISIBLE — keyboard accessibility
   ────────────────────────────────────────────────────────────────────
   The page-wide `cursor: none !important` rule (suppressing the native
   cursor in favour of the bespoke gold ring) has the side effect of
   leaving keyboard users with no native focus indicator either. We
   provide an explicit gold outline ring on every interactive element
   when it receives focus from a keyboard (`:focus-visible`, never
   from a click). WCAG 2.4.7 (Focus Visible, AA).

   The ring uses outline + outline-offset (rather than box-shadow) so
   it never affects layout. 3px gold + 2px offset reads as "OZRIC
   selection" rather than a generic browser default. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
[tabindex]:focus-visible,
.dot:focus-visible,
.modal-close:focus-visible {
  outline: 3px solid var(--gold);
  outline-offset: 2px;
  border-radius: 2px;
}
/* Higher-contrast outline for inputs that already have a border —
   the gold against bone background reads cleaner with a tight inset. */
input:focus-visible,
textarea:focus-visible {
  outline-offset: 1px;
}
/* Footer + meta-nav links sit on a near-black background; a gold
   outline reads cleanly against the void without needing offset. */
.footer-nav a:focus-visible,
.meta-nav-links a:focus-visible,
.breadcrumb a:focus-visible,
.journal-footer-nav .continue:focus-visible,
.meta-nav-cta:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 3px;
}
/* Suppress :focus-visible on click for elements that have their own
   active-state styling (the .progress dots, the hero CTA glass-btn)
   — keyboard users still get the ring, mouse users don't double-up. */
.progress .dot:focus:not(:focus-visible),
.btn:focus:not(:focus-visible),
.btn--primary:focus:not(:focus-visible) {
  outline: none;
}

/* Screen-reader-only utility — visually hides content while keeping
   it in the accessibility tree. Used for form labels that must exist
   for WCAG 3.3.2 but where the placeholder carries the visual cue. */
.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;
}

.skip-link {
  position: fixed;
  top: 12px;
  left: 12px;
  z-index: 10000;
  background: var(--gold);
  color: var(--void);
  font-family: var(--sans);
  font-weight: 500;
  font-size: 12px;
  letter-spacing: 3px;
  text-transform: uppercase;
  padding: 12px 22px;
  border: 1px solid var(--gold);
  border-radius: 2px;
  text-decoration: none;
  cursor: pointer !important;          /* override the page-wide cursor:none */
  transform: translateY(-200%);
  transition: transform 0.25s var(--ease-luxe);
}
.skip-link:focus,
.skip-link:focus-visible {
  transform: translateY(0);
  outline: 3px solid var(--bone);
  outline-offset: 2px;
}
a { color: inherit; text-decoration: none; }
button { font: inherit; }

/* ════════════════════════════════════════════════════════════════════
   VIEW TRANSITIONS API — cross-document (MPA) opt-in.
   ────────────────────────────────────────────────────────────────────
   Browsers that support cross-document view transitions (Chrome 126+,
   Edge 126+, Safari 18.2+) animate same-origin navigation with a
   GPU-composited crossfade-and-scale rather than a hard cut. Older
   browsers (Safari 17, Firefox stable, Chrome ≤125) navigate
   instantly with zero behavioural regression — this rule is a
   no-op on them.

   Applies on both the source and destination page; because every
   OZRIC page links the same /src/style.css, a single rule here
   covers every navigation in the site (homepage ↔ journal index ↔
   each essay).

   navigation: auto = enable for every same-origin nav. Per-link
   filtering (e.g. opt-out on external CTAs) handled via the
   browser's default behaviour for cross-origin destinations. */
@view-transition { navigation: auto; }

/* Default page transition — soft scale + crossfade, 600ms with the
   --ease-luxe slow-out tail. Reads as "the page exhales and the
   next one inhales" rather than a switch. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 600ms;
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
::view-transition-old(root) {
  animation-name: ozrik-page-fade-out;
}
::view-transition-new(root) {
  animation-name: ozrik-page-fade-in;
}
@keyframes ozrik-page-fade-out {
  from { opacity: 1; transform: scale(1.000); }
  to   { opacity: 0; transform: scale(0.985); }
}
@keyframes ozrik-page-fade-in {
  from { opacity: 0; transform: scale(1.015); }
  to   { opacity: 1; transform: scale(1.000); }
}
@media (prefers-reduced-motion: reduce) {
  /* No animation for users opted-out — same instant nav as
     unsupported browsers. The transition still fires (so the
     browser doesn't fall back to a flash) but at zero duration. */
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 0ms;
  }
}

/* ════════════════════════════════════════════════════════════════════
   BESPOKE OZRIC CURSOR
   ════════════════════════════════════════════════════════════════════ */
@media (hover: hover) and (pointer: fine) {
  /* Suppress native cursor in favour of the bespoke gold ring. ONLY
     on pages that actually render the bespoke cursor markup +
     JS — i.e. the homepage. Static pages (journal, faq, about,
     proof, people, privacy, terms, 404) keep the native cursor so
     reading + clicking links still has a visible pointer.

     Earlier this rule was unscoped (`*, *::before, *::after`), which
     hid the cursor on every page including the static ones — leaving
     keyboard-only focus and zero mouse feedback. Scoped here via
     `body.has-bespoke-cursor` (set on the homepage <body>). */
  body.has-bespoke-cursor *,
  body.has-bespoke-cursor *::before,
  body.has-bespoke-cursor *::after { cursor: none !important; }
}

#cursor-ring, #cursor-dot {
  position: fixed;
  top: 0; left: 0;
  pointer-events: none;
  z-index: 9999;
  /* mix-blend-mode: screen removed for perf — was forcing full-page composite
     every frame. Gold-on-void reads luminous from the box-shadow halo alone. */
  will-change: transform;
  transform: translate3d(0,0,0);   /* promote to its own GPU layer */
}

#cursor-ring {

  /* Elliptical proportions matching the OZRIC "O" letterform —
     vertical oval, slightly taller than wide (Fraunces 200 uppercase O). */
  width: 30px; height: 40px;
  border: 1px solid rgba(201, 168, 76, 0.65);
  border-radius: 50%;        /* on a non-square box → ellipse */
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;

  /* Soft gold halo so the O reads as luminous against the void */
  box-shadow: 0 0 18px rgba(201, 168, 76, 0.10);

  transition:
    width 0.42s var(--ease-luxe),
    height 0.42s var(--ease-luxe),
    border-color 0.42s var(--ease-luxe),
    box-shadow 0.42s var(--ease-luxe),
    opacity 0.30s var(--ease-luxe);
  /* Transform stays out of CSS transition — JS damping owns position. */
}

/* Inner gold dot — the "eye" of the O. Centred via flexbox above. */
#cursor-ring::before {
  content: '';
  display: block;
  width: 4px; height: 4px;
  background: var(--gold);
  border-radius: 50%;
  box-shadow:
    0 0 6px rgba(201, 168, 76, 0.85),
    0 0 14px rgba(201, 168, 76, 0.35);
  transition:
    width 0.30s var(--ease-luxe),
    height 0.30s var(--ease-luxe),
    opacity 0.30s var(--ease-luxe);
}

/* Hover state — over interactives, the O grows to frame the target,
   ring brightens to full gold, the eye-dot scales up subtly. */
#cursor-ring.is-hover {
  width: 56px; height: 72px;
  border-color: var(--gold);
  box-shadow: 0 0 28px rgba(201, 168, 76, 0.20);
}
#cursor-ring.is-hover::before {
  width: 6px; height: 6px;
}

/* Click — quick contraction, brightens to bone for a flash. */
#cursor-ring.is-down {
  width: 22px; height: 30px;
  border-color: var(--bone);
}
#cursor-ring.is-down::before {
  width: 3px; height: 3px;
}

/* Trailing precise dot — sharp gold pinpoint at the exact pointer
   position, no lag. The ring follows behind for the graceful tail.
   Together they communicate both PRECISION (dot at point) and
   IDENTITY (the elliptical O brand mark trailing). */
#cursor-dot {
  width: 5px; height: 5px;
  background: var(--gold);
  border-radius: 50%;
  box-shadow:
    0 0 8px rgba(201, 168, 76, 0.95),
    0 0 18px rgba(201, 168, 76, 0.40);
  transition: opacity 0.25s var(--ease-luxe), transform 0.25s var(--ease-luxe);
}
#cursor-dot.is-hover {
  opacity: 0.35;                /* fade but stay legible — guidance over the ring */
  transform-origin: center;
}

/* Hide when cursor leaves window */
body.cursor-out #cursor-ring,
body.cursor-out #cursor-dot { opacity: 0; }

/* Touch devices — no custom cursor */
@media (hover: none), (pointer: coarse) {
  #cursor-ring, #cursor-dot { display: none; }
}

/* ════════════════════════════════════════════════════════════════════
   ORB STAGE  —  fixed full-bleed iframe, deep-purple bloom underlay,
   left-edge mask so cards always read on top.
   ════════════════════════════════════════════════════════════════════ */
#orb-stage {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: var(--void);
}

/* Deep-purple radial glow centered where the orb sits (~70% from left).
   Adds richness without competing — still reads as "black background." */
#orb-bloom {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(ellipse 60% 60% at 72% 50%,
      rgba(107, 43, 184, 0.40) 0%,
      rgba(59,  19, 121, 0.26) 28%,
      rgba(26,  8,  56,  0.15) 55%,
      transparent 78%);
  /* mix-blend-mode removed for perf — radial gradient on void already
     additive-looking; the blend-mode was adding a 4ms paint on every frame. */
}

#orb {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  pointer-events: none;     /* parent forwards cursor + scroll passes through */
  background: transparent;
  z-index: 1;
}

/* ─── Time-of-day mood lighting ──────────────────────────────────────
   The page reads warmer at dawn (06–09) and dusk (18–22), cooler/deeper
   at night (22–06), and baseline-neutral midday (09–18). The brand
   reference range — Aman, Loro Piana, Patek, Rolls-Royce — all share
   "Mayfair 06:00" connotation: quiet prestige, candlelight before dawn.
   We render this literally as a subtle HSL overlay on the orb stage,
   capped at ~10% opacity so it never competes with the orb itself.

   JS sets body[data-mood] on boot + every 15 min. Default (no attr)
   reads as "day" (no tint). Hour-band defaults: 06–09 dawn / 09–18
   day / 18–22 dusk / 22–06 night.

   Implementation: a single full-bleed pseudo-element on #orb-stage
   reads `--mood-tint` (HSL components) + `--mood-opacity` from the
   body[data-mood] rules. mix-blend-mode: overlay so the tint multiplies
   into the void without crushing blacks. 6-second crossfade between
   moods so the transition reads as sunrise/sunset, not a switch. */
:root {
  --mood-tint: 0deg, 0%, 0%;
  --mood-opacity: 0;
}
body[data-mood="dawn"]  { --mood-tint:  30deg, 60%, 12%; --mood-opacity: 0.08; }
body[data-mood="day"]   { --mood-tint:   0deg,  0%,  0%; --mood-opacity: 0.00; }
body[data-mood="dusk"]  { --mood-tint:  20deg, 55%, 16%; --mood-opacity: 0.06; }
body[data-mood="night"] { --mood-tint: 240deg, 35%,  6%; --mood-opacity: 0.10; }

#orb-stage::after {
  content: '';
  position: absolute;
  inset: 0;
  background: hsl(var(--mood-tint));
  opacity: var(--mood-opacity);
  mix-blend-mode: overlay;
  pointer-events: none;
  z-index: 3;             /* above #orb but below #orb-mask vignette */
  transition: background 6s linear, opacity 6s linear;
  will-change: opacity;
}

/* Wordmark gold gets a tiny extra warmth at dusk and a slight cool
   pull at night — a near-invisible filter shift that keeps the brand
   mark in conversation with the void's mood, not floating above it. */
body[data-mood="dusk"]  .brand-mark { filter: brightness(1.05) saturate(1.05); }
body[data-mood="night"] .brand-mark { filter: brightness(0.94) saturate(0.96); }

@media (prefers-reduced-motion: reduce) {
  #orb-stage::after { transition: none; }
}

/* ─── Orb fallbacks ──────────────────────────────────────────────────
   The live three.js orb is the right half of the homepage composition;
   when it cannot run (JS disabled, prefers-reduced-motion, or the
   prefers-reduced-data hint), we paint a still snapshot of the orb on
   the orb-stage instead. The still is a Playwright capture of the live
   engine at clean composition (1200x800, x=2.6 desktop pose) so the
   composition reads identical without the rAF loop or the GPU cost.

   - JS-off:    handled by the <noscript> block in index.html.
   - reduced-motion: hide iframe, paint still as a background image
                     positioned to match the live orb's screen position.
   - reduced-data:   hide iframe, paint LOW-res still (11KB), kill the
                     bloom heartbeat animation. Slow connections get a
                     11KB image instead of a 260KB engine + data file.

   noscript fallback styling — keeps the picture absolute-positioned
   inside #orb-stage so it occupies the same screen real estate as
   the iframe would. */
.orb-fallback {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}
.orb-fallback img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  opacity: 0.92;
}

@media (prefers-reduced-motion: reduce) {
  /* Hide the live iframe; show a still image background on the stage
     instead. The still is a snapshot of the engine at clean composition
     so the layout reads identical. */
  #orb { display: none; }
  #orb-stage::before {
    content: '';
    position: absolute;
    inset: 0;
    background-image: url('/assets/orb-still.webp');
    background-position: center;
    background-size: cover;
    background-repeat: no-repeat;
    opacity: 0.88;
    z-index: 1;
    pointer-events: none;
  }
  /* Bloom heartbeat already disabled in the existing reduced-motion
     rule — confirming it stays off here so the still is fully static. */
}

@media (prefers-reduced-data: reduce) {
  /* Same approach as reduced-motion but with a lower-res still
     (~11KB vs the 260KB orb engine + positions data). Bloom keeps its
     gradient but loses the keyframe animation to save GPU on
     constrained devices. */
  #orb { display: none; }
  #orb-stage::before {
    content: '';
    position: absolute;
    inset: 0;
    background-image: url('/assets/orb-still-low.webp');
    background-position: center;
    background-size: cover;
    background-repeat: no-repeat;
    opacity: 0.85;
    z-index: 1;
    pointer-events: none;
  }
  #orb-bloom { animation: none; opacity: 0.55; }
}

/* Soft gradient on the LEFT half — pushes blacks darker so the card
   typography always reads with high contrast, even while the orb
   breathes underneath. Vignette on right is gentler. */
#orb-mask {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 2;
  background:
    linear-gradient(to right,
      rgba(5,5,5,0.86) 0%,
      rgba(5,5,5,0.62) 22%,
      rgba(5,5,5,0.30) 38%,
      rgba(5,5,5,0.00) 55%,
      rgba(5,5,5,0.00) 100%),
    radial-gradient(ellipse 100% 70% at 50% 50%,
      transparent 0%,
      transparent 55%,
      rgba(5,5,5,0.55) 90%,
      rgba(5,5,5,0.92) 100%);
}

/* ════════════════════════════════════════════════════════════════════
   HEADER  —  fixed, glass-sheet on scroll
   ════════════════════════════════════════════════════════════════════ */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 50;
  display: flex; justify-content: space-between; align-items: center;
  /* viewport-fit=cover means the page extends UNDER iOS status bar /
     Dynamic Island. Without env() padding, the gold "O" + REQUEST
     INVITATION button render under the status bar on real iPhones.
     22px floor keeps desktop spacing; safe-area-inset-top adds the
     extra room iOS needs (≈47px on Pro models with the island). */
  padding-top:    max(22px, calc(env(safe-area-inset-top, 0px) + 12px));
  padding-bottom: 22px;
  padding-left:   var(--gutter);
  padding-right:  var(--gutter);
  pointer-events: none;
  transition: background 0.6s ease, border-color 0.6s ease, backdrop-filter 0.6s;
}
.site-header > * { pointer-events: auto; }
.site-header.is-scrolled {
  background: linear-gradient(to bottom, rgba(5,5,5,0.85), rgba(5,5,5,0.55));
  border-bottom: 1px solid var(--line);
  backdrop-filter: blur(18px) saturate(140%);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
}
/* iOS Safari: a full-width backdrop-filter over an async-scrolling document
   forces an expensive live recomposite every frame and is a known scroll-
   hitch source. Swap it for an opaque gradient sheet on touch devices — the
   header still reads as a glass bar, without the per-frame blur cost. */
@supports (-webkit-touch-callout: none) {
  .site-header.is-scrolled {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: linear-gradient(to bottom, rgba(5,5,5,0.94), rgba(5,5,5,0.66));
  }
}
.brand { display: flex; align-items: center; gap: 11px; }
.brand-mark {
  /* `position: relative` + `isolation` so the ::after halo can be
     absolutely positioned behind the glyph WITHOUT escaping into
     the surrounding header chrome. The isolation creates a fresh
     stacking context which prevents the halo's z-index:-1 from
     pushing it behind the page's void background. */
  position: relative;
  isolation: isolate;
  font-family: var(--serif);
  font-weight: 200;
  font-size: 28px;
  color: var(--gold);
  line-height: 1;
  /* Was margin-top: -3px which lifted the O above the OZRIC wordmark.
     Newsreader 200's "O" has a tall em-box and rides high in its line —
     align-items: center on .brand was thus mis-aligning the visual
     centres. Nudging +1px puts the O's geometric centre on the OZRIC
     cap-line midpoint. Net shift from previous: 4px down. */
  margin-top: 1px;
  text-shadow: 0 0 22px rgba(201,168,76,0.32);
}
/* Breathing halo behind the gold "O" — Superhuman pattern (the sidebar
   logo there pulses softly so the chrome reads alive even when the
   user is reading body content). 6-second cycle, opacity 0.18 → 0.55,
   scale 1.00 → 1.06. The halo is gold, soft, and capped at 5% display
   intensity (radial gradient hits transparent at 65%) so on a quiet
   tab the eye catches it as life, not as motion. */
.brand-mark::after {
  content: '';
  position: absolute;
  inset: -120%;            /* halo extends well beyond the glyph bounds */
  border-radius: 50%;
  background: radial-gradient(circle,
              rgba(201, 168, 76, 0.28) 0%,
              rgba(201, 168, 76, 0.10) 30%,
              rgba(201, 168, 76, 0.04) 50%,
              transparent 65%);
  z-index: -1;
  pointer-events: none;
  animation: brand-breathe 6s var(--ease-luxe) infinite;
  /* will-change drops the layer onto the GPU compositor — this halo
     animates opacity + transform only, both compositor-level. */
  will-change: opacity, transform;
}
@keyframes brand-breathe {
  0%, 100% { opacity: 0.18; transform: scale(1.00); }
  50%      { opacity: 0.55; transform: scale(1.06); }
}
@media (prefers-reduced-motion: reduce) {
  .brand-mark::after {
    animation: none;
    opacity: 0.18;          /* visible but static — never invisible */
    transform: scale(1.00);
  }
}
.brand-name {
  font-size: 11px; letter-spacing: 6px;
  text-transform: uppercase;
  color: var(--bone);
  font-weight: 400;
}
/* Header centre link — sits between the brand mark on the left and the
   nav cluster on the right. Styled identically to .nav .meta ("YOURS ·
   INVITE ONLY") so the trio reads as one continuous header line. */
.header-link {
  font-size: 10px;
  letter-spacing: 4px;
  text-transform: uppercase;
  color: var(--ash);
  font-weight: 500;
}
.nav { display: flex; align-items: center; gap: 28px; }
.nav .meta {
  font-size: 10px; letter-spacing: 4px;
  text-transform: uppercase;
  color: var(--ash);
  font-weight: 500;
}
.nav .cta {
  position: relative;
  overflow: hidden;
  font-size: 11px; letter-spacing: 3px;
  text-transform: uppercase; font-weight: 500;
  /* Gold-tinted glass — primary OZRIC accent in the chrome.
     Bone text reads on the muted gold panel; on hover the panel
     deepens to solid gold and the type flips to void for max contrast. */
  color: var(--bone);
  border: 1px solid rgba(201, 168, 76, 0.55);
  background: linear-gradient(180deg,
              rgba(201, 168, 76, 0.22) 0%,
              rgba(142, 118, 52, 0.32) 100%);
  backdrop-filter: blur(10px) saturate(140%);
  -webkit-backdrop-filter: blur(10px) saturate(140%);
  padding: 10px 18px;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.18),
    0 6px 18px  rgba(0, 0, 0, 0.30),
    0 0 0 1px   rgba(201, 168, 76, 0.10);
  transition: all 0.4s var(--ease-luxe);
}
.nav .cta::before {
  /* Top inner highlight — the "wet glass" sheen */
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
              rgba(255, 255, 255, 0.14) 0%,
              transparent 55%);
  pointer-events: none;
}
.nav .cta:hover {
  background: linear-gradient(180deg,
              rgba(231, 196, 96, 0.95) 0%,
              rgba(201, 168, 76, 1.00) 100%);
  border-color: var(--gold);
  color: var(--void);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.40),
    0 12px 36px  rgba(201, 168, 76, 0.35),
    0 0 0 1px    rgba(201, 168, 76, 0.45);
  transform: translateY(-1px);
}
@media (max-width: 700px) {
  .nav .meta { display: none; }
  /* Tighten the header CTA on phones so it stays a compact pill (not a
     stretched-banner) while keeping ≥40px effective tap height. */
  .nav .cta {
    padding: 12px 16px;
    font-size: 10px;
    letter-spacing: 2px;
    white-space: nowrap;
  }
  /* The OZRIC wordmark stays visible on mobile (sits to the right of
     the gold "O"). Tightened tracking + slightly smaller cap-size keeps
     it inside the header's left half so the REQUEST INVITATION CTA on
     the right has clean breathing room. */
  .brand-name {
    font-size: 10px;
    letter-spacing: 4px;
  }
  /* CASE STUDIES — was wrapping into two lines and colliding with the
     brand wordmark (three header items > 390px). nowrap + a quieter
     size keeps it on one line between the brand and the CTA. */
  .header-link {
    font-size: 9px;
    letter-spacing: 2.5px;
    white-space: nowrap;
  }
}
@media (max-width: 520px) {
  /* Monogram restraint — the gold "O" alone carries the brand at phone
     width (the wordmark re-appears in the loader, hero and closing).
     Frees the room that was forcing CASE STUDIES into collision. */
  .brand-name { display: none; }
}

/* ════════════════════════════════════════════════════════════════════
   PAGE / STAGE
   #page is tall enough for ~7 cards × 100vh (card-cycle), plus footer.
   .stage is sticky-pinned to top so the orb + cards stay in view while
   the body scrolls. JS reads window.scrollY → progress in [0..1].
   ════════════════════════════════════════════════════════════════════ */
#page {
  position: relative;
  z-index: 1;
}
/* The .stage is `position: fixed` (not sticky) — sticky was breaking on
   browsers that treat body's `overflow-x: hidden` as a non-default overflow
   context. Fixed gives deterministic pinning; .track below provides the
   scroll range that JS reads via window.scrollY. */
.stage {
  position: fixed;
  top: 0; left: 0; right: 0;
  width: 100%;
  /* STABLE height — JS sets --oz-stage-h once from a captured viewport and
     refreshes it ONLY on real width/orientation changes, never on the iOS
     URL-bar deltas. svh is the fallback (the bar-visible height, which also
     doesn't move when the bar hides). This is half the smooth-scroll fix:
     the cards stop repositioning every time the toolbar animates. */
  height: var(--oz-stage-h, 100svh);
  overflow: hidden;
  z-index: 1;
  pointer-events: none;       /* re-enabled on children that need clicks */
  /* Contain layout/paint so the fixed stage doesn't invalidate the whole
     page during scroll; isolate gives it its own stacking + compositor. */
  contain: layout paint style;
  isolation: isolate;
}
.stage > * { pointer-events: auto; }
.stage.is-released {
  /* Past progress 0.93 (set in main.js), fade the entire card stack out
     before the curtain enters the viewport. 0.5s is fast enough to
     complete during the last 7% of the track at typical scroll speeds. */
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s var(--ease-luxe);
}

/* Scroll track — a tall empty element that gives the page enough height
   for scroll progress to span all cards. JS reads scrollY against this. */
.track {
  width: 100%;
  height: 720vh;
  pointer-events: none;
}

/* ════════════════════════════════════════════════════════════════════
   CARD STACK  —  all cards stacked, JS toggles per-card opacity / lift.
   ════════════════════════════════════════════════════════════════════ */
.card-stack {
  position: absolute;
  inset: 0;
  pointer-events: none;     /* cards re-enable for buttons + form */
  display: block;
}
.card {
  position: absolute;
  top: 46%;                  /* aligned with orb visual centre (y=+0.6 world) */
  left: var(--gutter);
  max-width: min(46vw, 600px);
  transform: translateY(-50%);
  pointer-events: auto;
  z-index: 3;                /* above the left-edge orb-mask */
  /* Initial state: invisible, slightly lifted; JS animates in & out. */
  opacity: 0;
}
/* Dynamic GPU promotion — JS adds .is-near only to cards within ~1 card of
   the viewport, so iOS never holds 10 permanent composited layers (the
   permanent will-change was a flagged source of layer-memory hitching). */
.card.is-near {
  will-change: opacity, transform;
}

/* Subtle scrim so any orb edge that creeps left doesn't reduce text contrast.
   filter: blur(4px) removed — was running on every paint, including when
   the card was at opacity:0. The radial gradient itself is already soft. */
.card::before {
  content: '';
  position: absolute;
  inset: -8vh -3vw -8vh -4vw;
  background: radial-gradient(ellipse 90% 75% at 30% 50%,
              rgba(5,5,5,0.55) 0%,
              rgba(5,5,5,0.32) 38%,
              rgba(5,5,5,0.10) 64%,
              transparent 84%);
  z-index: -1;
  pointer-events: none;
}

/* ─── Typography on cards ─────────────────────────────────────────── */
/* Eyebrow — used as both <span> (modal) AND <h3> (cards) so the h3
   semantics give AI engines section headers without changing the visual
   register. Neutralise browser-default h3 margin/line-height so the
   element reads as a small uppercase tracking-spaced label either way. */
.eyebrow {
  display: inline-block;
  font-family: var(--sans);
  font-size: 10px; letter-spacing: 5px;
  text-transform: uppercase;
  color: var(--violet);                /* loveyours purple — secondary accent */
  font-weight: 500;
  line-height: 1;
  margin: 0 0 22px 0;
}
h3.eyebrow {
  /* Belt-and-braces: ensure no UA stylesheet leaks a margin/font-size. */
  font-size: 10px;
  line-height: 1;
}

/* Display headings — match the page-loader treatment.
   The loader's "INTELLIGENCE" word reads as: Newsreader 200, uppercase,
   opened tracking, bone-soft cream colour, with the italic emphasis
   ("Private") rendered as gold + 1.18× scale. We mirror that exact
   treatment here so the homepage headline and every card headline live
   in the same typographic universe as the loader. */
.display {
  font-family: var(--serif);            /* Newsreader — same as loader */
  font-weight: 200;                     /* same as loader */
  /* 10% smaller pass (Apr 2026 PM) — quieter heading scale reads more
     luxe. Was clamp(42, 5.4vw, 78) → now (38, 4.86vw, 70). Eyebrows on
     content-heavy cards (card 7 Compared) reclaim viewport room as a
     bonus; the previous size pushed them above the fold. */
  font-size: clamp(38px, 4.86vw, 70px);
  line-height: 1.06;                    /* matches the Bodoni-era headroom; Newsreader italic 500 fits within this without collision */
  letter-spacing: 0.06em;               /* OPENED tracking — was -0.024em (tight). loader uses 0.42em at small size */
  text-transform: uppercase;            /* same as loader */
  color: var(--bone-soft);              /* cream, not pure white — matches loader */
  margin-bottom: 28px;                  /* trimmed from 30px to match the smaller heading scale */
  font-feature-settings: 'liga' 1, 'kern' 1, 'ss01' 1, 'lnum' 1, 'salt' 1;
}
.display em {
  /* Gold italic emphasis — Newsreader Italic 500. Returned to Newsreader
     (the same family as the display roman above) so the italic harmonises
     with the heading instead of "stepping out". Bodoni Moda was opulent
     but its high-contrast Didone strokes read thin / unclear at body
     scale. Newsreader italic at weight 500 (not 300, which earlier
     attempts found "swooshy") is upright, opaque, screen-optimised, and
     a perfect family match. Optical-size pinned to the 14-24 reading
     range via opsz so terminals stay restrained. */
  font-family: 'Newsreader', 'Bodoni Moda', 'Cormorant Garamond', serif;
  font-style: italic;
  font-weight: 500;
  font-variation-settings: 'opsz' 18;
  color: var(--gold);
  text-transform: none;                 /* italic word stays sentence-case for elegance */
  letter-spacing: 0.005em;
  font-size: 1.10em;
  background: none;
  -webkit-text-fill-color: var(--gold);
  /* Cursor-magnetised glow — halos tightened (was 18+28 / 60 → 12+18 / 36)
     so glow stays inside the cap-line and doesn't bleed across rows. */
  text-shadow:
    0 0 calc(12px + 18px * var(--glow, 0)) rgba(201, 168, 76, calc(0.22 + 0.32 * var(--glow, 0))),
    0 0 calc(36px * var(--glow, 0))        rgba(201, 168, 76, calc(0.16 * var(--glow, 0)));
  transition: text-shadow 0.18s linear;
}

/* The card display headings themselves get a soft cream bloom that
   responds to the same --glow var. Only on the .card elements (the
   scroll-driven left column), not on the unveil scenes. Amplified pass
   (Apr 2026 PM): bloom radius 20→32 / opacity 0.18→0.30 so the cursor
   actually feels felt across the heading, not just sensed.
   The whole .card lifts subtly toward the cursor too — `--glow` set on
   each text element doubles as a card-presence signal, so we can lean
   the card root by 1px on hover without per-card listeners. */
.card .display {
  text-shadow:
    0 0 calc(32px * var(--glow, 0)) rgba(239, 233, 221, calc(0.32 * var(--glow, 0))),
    0 0 calc(8px  * var(--glow, 0)) rgba(239, 233, 221, calc(0.18 * var(--glow, 0)));
  transition: text-shadow 0.22s linear;
}
/* Lede — soft cream bloom, restrained (it's body copy, not the focal). */
.card .lede {
  text-shadow:
    0 0 calc(18px * var(--glow, 0)) rgba(239, 233, 221, calc(0.20 * var(--glow, 0)));
  transition: text-shadow 0.22s linear, color 0.4s var(--ease-luxe);
}
/* Optional inline CTA below a card lede / micro-row — small, restrained,
   serif italic to read as a hand-drawn sign-off rather than a button. */
.card .card-cta {
  margin-top: 18px;
  font-family: var(--serif, "Newsreader", serif);
  font-style: italic;
  font-weight: 200;
  font-size: 0.92em;
  letter-spacing: 0.01em;
  opacity: 0.78;
}
.card .card-cta a { color: inherit; }
/* Eyebrow — violet halo (matches its colour token). The eyebrow is the
   smallest text element so the bloom stays close. */
.card .eyebrow {
  text-shadow:
    0 0 calc(14px * var(--glow, 0)) rgba(198, 135, 255, calc(0.55 * var(--glow, 0))),
    0 0 calc(4px  * var(--glow, 0)) rgba(198, 135, 255, calc(0.30 * var(--glow, 0)));
  transition: text-shadow 0.22s linear;
}
/* Stat row — the violet number + bone label both lift on cursor approach.
   These are the eye-catching figures (3,735 / 7,590 / 140 etc.), giving
   them a glow makes the whole stat row feel cursor-aware. */
.card .micro-row b {
  text-shadow:
    0 0 calc(20px * var(--glow, 0)) rgba(198, 135, 255, calc(0.45 * var(--glow, 0)));
  transition: text-shadow 0.22s linear;
}
.card .micro-row i {
  text-shadow:
    0 0 calc(12px * var(--glow, 0)) rgba(239, 233, 221, calc(0.20 * var(--glow, 0)));
  transition: text-shadow 0.22s linear;
}

/* Per-word cursor colour shift — wraps every text-node word in a `.cw`
   span at runtime (main.js). Each span carries `--word-glow` (0..1) set
   from cursor distance; color-mix() blends the base tone toward the
   highlight tone proportionally. The page now feels word-aware: cream
   copy leans toward violet under the cursor, already-violet copy
   inverts toward gold so it stays expressive. */
.cw {
  transition: color 0.18s linear;
  /* Inherit a sensible default before glow kicks in, in case the loop
     hasn't ticked yet (e.g., first render frame). */
  color: inherit;
}
.cw.cw--bone {
  color: color-mix(in srgb,
    var(--bone-soft) 100%,
    var(--violet) calc(var(--word-glow, 0) * 100%)
  );
}
.cw.cw--violet {
  color: color-mix(in srgb,
    var(--violet) 100%,
    var(--gold) calc(var(--word-glow, 0) * 100%)
  );
}

/* ─── Hero word-stagger reveal (Anthropic SplitText pattern, native CSS)
   ──────────────────────────────────────────────────────────────────────
   When the loader dismisses, main.js adds .is-revealing to card 0 and
   sets `--i` (word index) inline on every .cw inside the hero. CSS
   animates each word from translateY(8px) opacity 0 → translateY(0)
   opacity 1 with a 60ms-per-word stagger. Total reveal completes in
   ~1.1s for a typical 10-word hero — the orb's "alive" entrance reads
   as one breath followed by a quiet word-by-word arrival.
   Single-pass, runs once, then transition stays available for the
   cursor glow + word colour shift. */
.card[data-i="0"] .display .cw,
.card[data-i="0"] .eyebrow .cw,
.card[data-i="0"] .lede .cw {
  opacity: 0;
  transform: translate3d(0, 8px, 0);
  transition: none;
}
.card[data-i="0"].is-revealing .display .cw,
.card[data-i="0"].is-revealing .eyebrow .cw,
.card[data-i="0"].is-revealing .lede .cw {
  animation: ozrik-word-rise 0.95s cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
  animation-delay: calc(var(--i, 0) * 60ms + 80ms);
}
@keyframes ozrik-word-rise {
  0%   { opacity: 0; transform: translate3d(0, 8px, 0); }
  100% { opacity: 1; transform: translate3d(0, 0, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .card[data-i="0"].is-revealing .display .cw,
  .card[data-i="0"].is-revealing .eyebrow .cw,
  .card[data-i="0"].is-revealing .lede .cw {
    animation: none;
    opacity: 1;
    transform: none;
  }
}
.display br { display: block; }

.lede {
  font-size: clamp(15px, 1.3vw, 18.5px);
  line-height: 1.7;
  color: var(--bone-soft);
  max-width: 520px;
  margin-bottom: 32px;
  font-weight: 300;
}

/* CTA row */
.cta-row { display: flex; gap: 18px; align-items: center; flex-wrap: wrap; }
.btn {
  font-family: var(--sans);
  font-size: 11px; letter-spacing: 3px;
  text-transform: uppercase; font-weight: 500;
  padding: 17px 30px;
  border: 1px solid;
  cursor: pointer;
  transition: all 0.3s var(--ease-luxe);
}
.btn--primary {
  position: relative;
  overflow: hidden;
  color: var(--bone);
  /* Layered gradient: violet highlight at top, fading into purple-rich
     at the base — gives the button a 3D "polished glass orb" depth. */
  background: linear-gradient(180deg,
              rgba(155, 78, 220, 0.85) 0%,
              rgba(107, 43, 184, 0.95) 50%,
              rgba(59,  19, 121, 1.00) 100%);
  border: 1px solid rgba(198,135,255,0.55);
  backdrop-filter: blur(10px) saturate(140%);
  -webkit-backdrop-filter: blur(10px) saturate(140%);
  /* Multi-layer shadow — inner top sheen, inner base depth,
     outer halo, outer ambient. */
  box-shadow:
    inset 0 1px 0   rgba(255,255,255,0.22),
    inset 0 -1px 0  rgba(0,0,0,0.30),
    inset 0 -10px 24px rgba(0,0,0,0.18),
    0 10px 28px    rgba(107,43,184,0.42),
    0 0 0 1px      rgba(198,135,255,0.15);
}
.btn--primary::before {
  /* Wet-glass top sheen — sharp at top, fades fast */
  content: '';
  position: absolute;
  left: 0; right: 0; top: 0; height: 55%;
  background: linear-gradient(180deg,
              rgba(255,255,255,0.16) 0%,
              transparent 100%);
  pointer-events: none;
  border-radius: inherit;
}
.btn--primary::after {
  /* Diagonal shine sweep — animates across on hover */
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg,
              transparent 30%,
              rgba(255,255,255,0.22) 50%,
              transparent 70%);
  transform: translateX(-110%);
  transition: transform 0.9s var(--ease-luxe);
  pointer-events: none;
}
.btn--primary:hover {
  background: linear-gradient(180deg,
              rgba(198,135,255,0.95) 0%,
              rgba(155, 78,220, 0.98) 50%,
              rgba(107, 43,184, 1.00) 100%);
  border-color: var(--violet);
  box-shadow:
    inset 0 1px 0   rgba(255,255,255,0.32),
    inset 0 -1px 0  rgba(0,0,0,0.30),
    inset 0 -10px 24px rgba(0,0,0,0.14),
    0 16px 44px    rgba(198,135,255,0.45),
    0 0 0 1px      rgba(198,135,255,0.40);
  transform: translateY(-1px);
}
.btn--primary:hover::after {
  transform: translateX(110%);
}
.btn--primary:active {
  transform: translateY(0);
  box-shadow:
    inset 0 1px 4px rgba(0,0,0,0.30),
    inset 0 1px 0  rgba(255,255,255,0.10),
    0 4px 16px    rgba(107,43,184,0.30);
}

/* Scroll hint on hero card */
.hint {
  display: inline-flex; align-items: center; gap: 10px;
  font-size: 10px; letter-spacing: 4px;
  text-transform: uppercase;
  color: var(--ash);
  font-weight: 500;
}
.hint .tick {
  display: inline-block;
  width: 1px; height: 26px;
  background: linear-gradient(to bottom, var(--violet) 0%, transparent 100%);
  animation: tick 2.6s var(--ease-luxe) infinite;
}
@keyframes tick {
  0%, 100% { opacity: 0.35; transform: translateY(-3px); }
  50%      { opacity: 0.85; transform: translateY(3px);  }
}

/* Micro-row for the System card stats */
.micro-row {
  display: flex; gap: 38px; flex-wrap: wrap;
  margin-top: 6px;
}
.micro-row > span {
  display: flex; flex-direction: column; gap: 4px;
}
.micro-row b {
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(22px, 2.2vw, 30px);
  color: var(--violet);              /* secondary accent — purple */
  letter-spacing: -0.02em;
}
.micro-row i {
  font-size: 9px; letter-spacing: 3px;
  text-transform: uppercase;
  color: var(--ash);
  font-style: normal;
  font-weight: 500;
}

/* Invite form */
.invite {
  display: flex;
  max-width: 460px;
  border: 1px solid rgba(198,135,255,0.18);
  background: linear-gradient(180deg,
              rgba(26, 8, 56, 0.55) 0%,
              rgba(5, 5, 5, 0.75) 100%);
  backdrop-filter: blur(18px) saturate(140%);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  position: relative;
  overflow: hidden;
  transition: border-color 0.4s var(--ease-luxe), box-shadow 0.4s var(--ease-luxe);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.06),
    0 12px 30px rgba(0,0,0,0.35);
}
.invite:focus-within {
  border-color: rgba(198,135,255,0.55);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.10),
    0 0 0 3px rgba(107,43,184,0.20),
    0 16px 40px rgba(0,0,0,0.40);
}
.invite input {
  flex: 1;
  background: transparent;
  border: 0;
  color: var(--bone);
  font-family: var(--sans);
  font-size: 14px;
  padding: 17px 20px;
  outline: none;
  letter-spacing: 0.4px;
}
.invite input::placeholder { color: var(--ash); }
.invite button {
  position: relative;
  overflow: hidden;
  border: 0;
  color: var(--bone);
  padding: 17px 26px;
  font-size: 11px;
  letter-spacing: 3px;
  text-transform: uppercase;
  font-weight: 500;
  cursor: pointer;
  /* Match .btn--primary glassy treatment so they read as one family. */
  background: linear-gradient(180deg,
              rgba(155, 78, 220, 0.85) 0%,
              rgba(107, 43, 184, 0.95) 50%,
              rgba(59,  19, 121, 1.00) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.22),
    inset 0 -1px 0 rgba(0,0,0,0.30);
  transition: all 0.4s var(--ease-luxe);
}
.invite button::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 0; height: 55%;
  background: linear-gradient(180deg, rgba(255,255,255,0.16) 0%, transparent 100%);
  pointer-events: none;
}
.invite button::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg, transparent 30%, rgba(255,255,255,0.22) 50%, transparent 70%);
  transform: translateX(-110%);
  transition: transform 0.9s var(--ease-luxe);
  pointer-events: none;
}
.invite button:hover {
  background: linear-gradient(180deg,
              rgba(198,135,255,0.95) 0%,
              rgba(155, 78,220, 0.98) 50%,
              rgba(107, 43,184, 1.00) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.32),
    inset 0 -1px 0 rgba(0,0,0,0.30),
    0 12px 28px rgba(198,135,255,0.30);
}
.invite button:hover::after { transform: translateX(110%); }
.invite .thanks { color: var(--violet); }
.invite .thanks {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--serif); font-style: italic;
  font-size: 16px; color: var(--gold);
  background: rgba(5,5,5,0.95);
  opacity: 0; pointer-events: none;
  transition: opacity 0.5s var(--ease-luxe);
}
.invite[data-sent="1"] .thanks { opacity: 1; }

/* Honeypot — visually hidden but not display:none (some bots skip
   display:none fields). Off-canvas + 0×0 + tab-blocked. */
.invite .hp {
  position: absolute !important;
  left: -10000px !important;
  top: auto !important;
  width: 1px !important;
  height: 1px !important;
  overflow: hidden !important;
  opacity: 0 !important;
}

/* Error pill — appears below the form on submission failure. Coral
   tone (between gold and ash) so it reads urgent but stays in palette. */
.invite-error {
  display: block;
  min-height: 14px;
  margin-top: 12px;
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: 1.5px;
  color: #E89B7C;        /* coral — palette-adjacent, not a generic red */
  text-transform: uppercase;
  opacity: 0;
  transition: opacity 0.3s var(--ease-luxe);
}
.invite-error:not(:empty) { opacity: 0.92; }

/* Loading state — pulse the submit button while in flight. */
.invite[data-sending="1"] button[type="submit"] {
  opacity: 0.65;
  cursor: progress;
}
.invite button[type="submit"]:disabled {
  cursor: not-allowed;
}

.closing-meta {
  font-family: var(--serif); font-style: italic;
  font-size: 12px; color: var(--ash);
  margin-top: 12px;
  letter-spacing: 0.2px;
}

/* ════════════════════════════════════════════════════════════════════
   PROGRESS DOTS  —  right edge, vertically centered with orb
   ════════════════════════════════════════════════════════════════════ */
.progress {
  position: absolute;
  right: calc(var(--gutter) * 0.7);
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: 14px;
  z-index: 4;
  pointer-events: auto;
}
.progress .dot {
  width: 8px; height: 8px;
  border: 0;
  border-radius: 50%;
  background: rgba(239,233,221,0.18);
  cursor: pointer;
  padding: 0;
  position: relative;
  transition: background 0.45s var(--ease-luxe), transform 0.45s var(--ease-luxe);
}
.progress .dot::after {
  content: '';
  position: absolute;
  inset: -8px;
}
.progress .dot.is-active {
  background: var(--violet);
  transform: scale(1.35);
  box-shadow: 0 0 14px rgba(198,135,255,0.55);
}
.progress .dot:hover { background: var(--bone-soft); }
@media (max-width: 700px) {
  .progress { right: 4vw; gap: 11px; }
  .progress .dot { width: 6px; height: 6px; }
}

/* ════════════════════════════════════════════════════════════════════
   FOOTER  —  fixed-bottom curtain that rises during the terminal lock.
   ════════════════════════════════════════════════════════════════════
   The footer is pinned to the viewport bottom (not in normal flow), so
   the closing scene above can stay sticky-locked while only this rises.
   main.js maps scroll progress through .terminal-lock (0..1) onto:
     • opacity     0   → 1
     • translateY +44 → 0px
   `.is-rising` is toggled when progress crosses ~5% (re-enables pointer
   events for the links). The gradient fades the void into the footer
   so there is no hard edge between the closing scene and the curtain. */
.site-footer {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 6;
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  padding: 44px var(--gutter) 38px;
  background: linear-gradient(to top, rgba(5,5,5,0.97) 70%, rgba(5,5,5,0));
  opacity: 0;
  transform: translate3d(0, 44px, 0);
  pointer-events: none;
  will-change: opacity, transform;
  /* No CSS transition — the render loop now writes opacity/transform every
     frame from scroll position, so a transition would make the footer chase
     its own animation (lag) instead of tracking the scroll exactly. */
  transition: none;
}
.site-footer.is-rising {
  opacity: 1;
  transform: translate3d(0, 0, 0);
  pointer-events: auto;
}
/* LOVE YOURS wordmark — quiet maker's mark. The asset is a wide
   aspect-ratio purple wordmark; we size by height (with auto width)
   and let opacity drop it to a "subtle attribution" register so it
   never competes with the OZRIC voice above it. */
.footer-mark {
  display: inline-flex;
  align-items: center;
  line-height: 0;
  margin-bottom: 4px;
  opacity: 0.62;                /* subtle by default — strengthens on hover */
  transition: opacity 0.4s var(--ease-luxe);
}
.footer-mark img {
  display: block;
  height: 16px;                 /* small — proportional to footer-tag (14px) */
  width: auto;
  user-select: none;
  -webkit-user-drag: none;
}
.footer-mark:hover { opacity: 0.95; }
.footer-tag {
  font-family: var(--serif); font-style: italic;
  font-size: 14px; color: var(--bone);
  font-weight: 300;
}
.footer-meta {
  font-size: 9px; letter-spacing: 4px;
  text-transform: uppercase;
  color: var(--ash);
}
.footer-links {
  display: flex; gap: 14px; align-items: center;
  font-size: 11px; letter-spacing: 2px;
  color: var(--ash);
  margin-top: 6px;
}
.footer-links a { color: var(--ash); transition: color 0.25s var(--ease-luxe); }
.footer-links a:hover { color: var(--gold); }
.footer-links span { color: var(--gold-soft); }

/* ════════════════════════════════════════════════════════════════════
   META-NAV — journal hairline strip
   ────────────────────────────────────────────────────────────────────
   Sits between the iPhone unveil chapter and the closing flourish.
   Provides three named internal links to the journal essays + a cap
   link to the journal index. Style register: gold hairline above, soft
   bone copy, eyebrow-style caps. Reads as a quiet "you can keep going
   here" rather than a primary nav surface. */
.meta-nav {
  position: relative;
  z-index: 4;
  padding: 64px var(--gutter) 80px;
  text-align: center;
  /* No border-top — the section flows seamlessly out of the iPhone
     unveil chapter above. The eyebrow + display heading carry the
     visual transition; a hairline rule was redundant. */
}
.meta-nav-eyebrow {
  display: block;
  font-size: 9px;
  letter-spacing: 6px;
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: 14px;
  font-weight: 500;
}
.meta-nav-title {
  font-family: var(--serif);
  font-weight: 200;
  font-size: clamp(24px, 3.4vw, 38px);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--bone-soft);
  line-height: 1.1;
  margin-bottom: 28px;
}
.meta-nav-title em {
  font-family: 'Newsreader', serif;
  font-style: italic;
  font-weight: 500;
  font-variation-settings: 'opsz' 18;
  color: var(--gold);
  text-transform: none;
  letter-spacing: 0.005em;
  font-size: 1.10em;
}
.meta-nav-links {
  list-style: none;
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 28px 36px;
  margin: 0 auto 28px;
  max-width: 720px;
  padding: 0;
}
.meta-nav-links li {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.meta-nav-links li::before { content: none; }    /* override bullet rule */
.meta-nav-links a {
  font-family: var(--sans);
  font-size: 13px;
  letter-spacing: 1px;
  color: var(--bone);
  font-weight: 400;
  border-bottom: 1px solid rgba(201, 168, 76, 0.32);
  padding-bottom: 4px;
  transition: color 0.3s var(--ease-luxe), border-color 0.3s var(--ease-luxe);
}
.meta-nav-links a:hover {
  color: var(--gold);
  border-color: var(--gold);
}
.meta-nav-cap {
  font-size: 8px;
  letter-spacing: 4px;
  text-transform: uppercase;
  color: var(--ash);
  font-weight: 400;
}
.meta-nav-cta {
  display: inline-block;
  font-size: 10px;
  letter-spacing: 5px;
  text-transform: uppercase;
  color: var(--gold);
  font-weight: 500;
  border: 1px solid rgba(201, 168, 76, 0.45);
  padding: 12px 24px;
  transition: all 0.3s var(--ease-luxe);
}
.meta-nav-cta:hover {
  background: rgba(201, 168, 76, 0.10);
  color: var(--bone);
  border-color: var(--gold);
}
@media (max-width: 880px) {
  .meta-nav { padding: 48px var(--gutter-sm) 60px; }
  .meta-nav-links { gap: 22px 24px; }
  .meta-nav-links a { font-size: 12px; }
}
/* Desktop — align the journal-meta-nav block with the left card column
   above it so the page reads as a single left-justified spine. Mirrors
   .card { left: var(--gutter); max-width: min(46vw, 600px) }. */
@media (min-width: 881px) {
  .meta-nav {
    text-align: left;
    max-width: calc(min(46vw, 600px) + var(--gutter) * 2);
    margin-left: 0;
    margin-right: auto;
  }
  .meta-nav-links {
    justify-content: flex-start;
    margin-left: 0;
    margin-right: 0;
    max-width: 100%;
  }
  .meta-nav-links li {
    align-items: flex-start;
  }
}

/* Footer site-map nav — six SEO surfaces (Journal · FAQ · About · Proof
   · Privacy · Terms). Sits between the OZRIC tagline and the maker
   credit row. Links are rendered as small uppercase tracking-spaced
   labels matching the eyebrow voice across the rest of the site, so
   they read as supporting attribution rather than a primary nav. */
.footer-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 14px 28px;
  align-items: center;
  justify-content: center;
  margin: 6px 0 4px;
  max-width: 540px;
}
.footer-nav a {
  font-family: var(--sans);
  font-size: 10px;
  letter-spacing: 4px;
  text-transform: uppercase;
  color: var(--ash);
  font-weight: 400;
  transition: color 0.25s var(--ease-luxe);
}
.footer-nav a:hover { color: var(--gold); }
@media (max-width: 700px) {
  /* Tighter spacing on phones — 6 links on a 393px viewport without
     this would either wrap into a tall stack or push the maker
     credit below the fold. 14px column gap + 12px row gap reads as
     a clean 2x3 grid at this size. */
  .footer-nav { gap: 12px 14px; max-width: 320px; }
  .footer-nav a { font-size: 9px; letter-spacing: 3px; }
}

/* ════════════════════════════════════════════════════════════════════
   RESPONSIVE
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 880px) {
  /* GLOBAL MOBILE CENTRING — every body/typography element on the page
     is centred horizontally on mobile, except the fixed-position header
     chrome and the site footer. The user's spec: cards, chapters,
     unveil scenes, and the closing flourish all read as a single
     centred composition on a phone, with the orb breathing dead-centre
     behind them. Header (brand mark + REQUEST INVITATION) and footer
     (LOVE YOURS wordmark + links) keep their flex space-between layout. */
  .card {
    left: var(--gutter-sm);
    right: var(--gutter-sm);
    max-width: none;
    text-align: center;
  }
  /* Centre any block-level lede / display / eyebrow whose max-width
     would otherwise leave it left-anchored. margin auto + the card-level
     text-align: center handle inline-flow content; these handle the
     few elements that have explicit max-widths. */
  .card .lede,
  .card .display,
  .card .eyebrow,
  .scene .lede,
  .scene .display,
  .scene .eyebrow,
  .chapter-heading,
  .chapter-eyebrow,
  .chapter-tagline,
  .chapter-rule {
    margin-left: auto;
    margin-right: auto;
  }
  /* Eyebrow is inline-block by default — switch it to block on mobile
     so margin auto actually centres it under the headline. */
  .card .eyebrow,
  .scene .eyebrow,
  .chapter-eyebrow {
    display: block;
    text-align: center;
  }
  /* Chapter II "OZRIC on iPhone" — same centred treatment as cards. */
  .chapter-mark {
    align-items: center !important;
    text-align: center !important;
    padding-left: 0 !important;
    padding-right: 0 !important;
  }
  /* Unveil scene wrappers (phones, scenes that hold .display) — let
     their content centre rather than left-align inside the grid col. */
  .scene-content {
    margin-left: auto;
    margin-right: auto;
    text-align: center;
  }
  .display { font-size: clamp(40px, 9vw, 60px); }
  .lede { font-size: clamp(15px, 4vw, 17px); max-width: 92vw; }
  /* Mobile: orb sits dead-centre behind the cards (JS sends pose x:0,
     y:0). Vignette is mostly uniform with a touch of darker top/bottom
     so the header chrome and the URL-bar safe-area always read. */
  #orb-mask {
    background:
      linear-gradient(to bottom,
        rgba(5,5,5,0.55) 0%,
        rgba(5,5,5,0.18) 22%,
        rgba(5,5,5,0.10) 50%,
        rgba(5,5,5,0.30) 78%,
        rgba(5,5,5,0.85) 100%),
      radial-gradient(ellipse 110% 80% at 50% 50%,
        transparent 0%,
        transparent 60%,
        rgba(5,5,5,0.40) 92%,
        rgba(5,5,5,0.85) 100%);
  }
  /* Mobile bloom: recentre the deep-purple radial from the desktop
     "right-side" position (72%) to dead-centre. Pulse-animated below. */
  #orb-bloom {
    background:
      radial-gradient(ellipse 70% 55% at 50% 50%,
        rgba(107, 43, 184, 0.46) 0%,
        rgba(59,  19, 121, 0.32) 26%,
        rgba(26,  8,  56,  0.18) 52%,
        transparent 76%);
    animation: orb-heartbeat 5.4s var(--ease-luxe) infinite;
    transform-origin: 50% 50%;
    will-change: transform, opacity;
  }
  /* On mobile every card centres in the viewport. The JS already applies
     translate3d(0, calc(-50% + Xpx), 0) on each frame — combined with
     top: 50% (inherited from the desktop block) the card sits perfectly
     centred. We clamp it inside a top/bottom safe-area cushion so the
     headline never collides with the header CTA above or the iOS URL
     bar below, regardless of card content height. */
  .card {
    /* Vertical anchor: keep the desktop top:50% (already set above) so
       the JS-applied -50% transform centres the card. We add inner
       padding instead of an absolute offset — that way short cards sit
       centred and tall cards expand inside the safe band. */
    padding-top:    max(8vh,  calc(env(safe-area-inset-top, 0px)    + 72px));
    padding-bottom: max(10vh, calc(env(safe-area-inset-bottom, 0px) + 24px));
    box-sizing: border-box;
  }

  /* Mobile spacing pass — let cards breathe.
     The on-device pixel real estate is ~390×844; on desktop the .card
     has near-infinite vertical room because the orb sits beside it.
     On mobile the orb is BEHIND the card, so the card *is* the page
     and deserves generous distancing between elements. Numbers below
     are tuned to the iPhone 14 Pro / 15 Pro / 16e form factor where
     the card vertically centres inside ~720px of usable height. */
  .card .eyebrow { margin-bottom: 28px; letter-spacing: 6px; }
  .card .display { margin-bottom: 36px; line-height: 1.05; }
  .card .lede    { margin-bottom: 44px; line-height: 1.72; max-width: 100%; }
  .cta-row       { gap: 22px; flex-wrap: wrap; row-gap: 18px; }

  /* Hero (card 0) gets an extra notch of breathing room — this is the
     first impression and the "scroll to begin" hint should feel earned,
     not crammed under the CTA. */
  .card[data-i="0"] .eyebrow { margin-bottom: 34px; }
  .card[data-i="0"] .display { margin-bottom: 42px; }
  .card[data-i="0"] .lede    { margin-bottom: 54px; }

  /* CTA button on mobile — bigger tap target (Apple HIG ≥44px) with
     proportionally heavier weight so the action reads as a destination,
     not a footnote. Min-width keeps it from collapsing on tiny labels. */
  .card .btn--primary {
    padding: 19px 32px;
    font-size: 12px;
    letter-spacing: 4px;
    min-width: 220px;
    text-align: center;
  }

  /* "Scroll to begin" hint — drops to its own line below the CTA on
     narrow viewports (the flex row already wraps). Centred horizontally
     on the screen via display:flex + justify-content:center, which keeps
     the trailing tick mark visually attached to the label as one
     centred group. */
  .cta-row .hint {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    margin-top: 8px;
    padding-left: 0;
    opacity: 0.78;
  }

  /* Stat micro-rows — give them more vertical air when they appear
     beneath a lede on cards 3, 6, 7, 8. The desktop 28px feels tight
     on mobile because the row often wraps to two lines. */
  .card .micro-row {
    margin-top: 32px;
    gap: 18px 28px;
    row-gap: 22px;
  }

  /* CTAs centered horizontally on mobile — buttons sit dead-centre on
     the screen rather than left-anchored. This applies to:
       • .cta-row on hero (card 0) — REQUEST INVITATION + scroll hint
       • inline form on card 9 (For the few) — email + REQUEST INVITATION
       • any standalone .btn or invite form across the cards
     The text content above (eyebrow, display, lede) stays left-aligned
     because reading-flow lives left-to-right; only the action elements
     pull centre to feel like the iOS app primary-action they preview. */
  .cta-row {
    justify-content: center;
  }
  .card .invite {
    justify-content: center;
    margin-left: auto;
    margin-right: auto;
  }
  .card .closing-meta {
    text-align: center;
  }
}

/* ════════════════════════════════════════════════════════════════════
   CLOSING SCENE — mobile centring
   On desktop the gold "O" + tagline pin to the LEFT column so the orb
   breathes on the right. On mobile the orb is BEHIND everything (pose
   x:0, dead-centre), so the "O" mark must sit dead-centre too — exactly
   on the horizontal centre line of the screen. We span the full grid,
   align children to centre, and centre-align inline content. The
   hairline rule keeps its 56px length but gets auto-margins so it
   centres under the tagline.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 880px) {
  .scene--closing {
    height: 100dvh;        /* iOS Safari URL-bar safe (matches .stage) */
    min-height: 100dvh;
  }
  /* Higher specificity (parent selector) so this beats the desktop
     `.closing-mark` rule that sits LATER in the source — without this,
     CSS cascade picks the desktop rule because same specificity +
     later source wins. */
  .scene--closing .closing-mark {
    grid-column: 1 / -1;             /* span every column the grid offers */
    align-items: center;             /* flex children centre horizontally */
    text-align: center;
    padding-left: 0;
    padding-right: 0;
    gap: 26px;
  }
  .scene--closing .closing-O {
    /* Slightly larger than desktop on mobile — it's now THE focal
       composition (no left/right split competing with it). */
    font-size: clamp(112px, 36vw, 180px);
  }
  .scene--closing .closing-rule {
    margin-left: auto;
    margin-right: auto;
  }
}

/* Heartbeat pulse — gentle 5.4s cycle. Two beats per cycle (lub-dub) so
   it feels alive without being literal. Mobile-only: the desktop bloom
   is wide enough that a pulse would be distracting; mobile reads as a
   small medallion of light behind text where a slow pulse adds presence. */
@keyframes orb-heartbeat {
  0%   { transform: scale(1.000); opacity: 0.92; }
  14%  { transform: scale(1.038); opacity: 1.00; }   /* lub */
  26%  { transform: scale(1.014); opacity: 0.95; }
  40%  { transform: scale(1.054); opacity: 1.00; }   /* dub */
  60%  { transform: scale(1.012); opacity: 0.93; }
  100% { transform: scale(1.000); opacity: 0.92; }
}
@media (prefers-reduced-motion: reduce) {
  /* No pulse for users who opt out of motion. */
  @media (max-width: 880px) {
    #orb-bloom { animation: none; }
  }
}
@media (max-width: 520px) {
  .display { font-size: clamp(34px, 11vw, 50px); }
  .micro-row { gap: 22px; }
  .invite input { font-size: 13px; padding: 14px 16px; }
  .invite button { padding: 14px 18px; font-size: 10px; }
}

/* Reduced motion: cards just snap visible — no fade tension. */
@media (prefers-reduced-motion: reduce) {
  .card { transition: none !important; }
}

/* ════════════════════════════════════════════════════════════════════
   INVITE MODAL  — glass-panel dialog, centred, backdrop blur
   ════════════════════════════════════════════════════════════════════ */
.modal {
  position: fixed; inset: 0;
  z-index: 200;
  display: flex; align-items: center; justify-content: center;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.5s var(--ease-luxe), visibility 0.5s var(--ease-luxe);
}
.modal.is-open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}
.modal-backdrop {
  position: absolute; inset: 0;
  background:
    radial-gradient(ellipse 80% 60% at 50% 50%,
      rgba(26, 8, 56, 0.55) 0%,
      rgba(5, 5, 5, 0.85) 80%);
  backdrop-filter: blur(28px) saturate(140%);
  -webkit-backdrop-filter: blur(28px) saturate(140%);
}
.modal-card {
  position: relative;
  width: min(92vw, 480px);
  padding: 56px 52px 44px;
  text-align: center;
  /* Frosted glass panel — slight purple tint, generous blur */
  background: linear-gradient(160deg,
              rgba(26,  8, 56, 0.78) 0%,
              rgba(20,  6, 44, 0.82) 50%,
              rgba(5,   5,  5, 0.88) 100%);
  border: 1px solid rgba(198,135,255,0.30);
  backdrop-filter: blur(36px) saturate(150%);
  -webkit-backdrop-filter: blur(36px) saturate(150%);
  box-shadow:
    inset 0 1px 0  rgba(255,255,255,0.10),
    inset 0 -1px 0 rgba(0,0,0,0.40),
    0 30px 80px   rgba(0,0,0,0.65),
    0 0 80px      rgba(107,43,184,0.22),
    0 0 0 1px     rgba(198,135,255,0.10);
  overflow: hidden;
  transform: translateY(18px) scale(0.97);
  transition: transform 0.6s var(--ease-luxe);
}
/* Top sheen on the glass card */
.modal-card::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 0; height: 35%;
  background: linear-gradient(180deg,
              rgba(255,255,255,0.06) 0%,
              transparent 100%);
  pointer-events: none;
}
/* Faint violet edge glow that breathes (matches orb cadence) */
.modal-card::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  pointer-events: none;
  background:
    linear-gradient(135deg,
      rgba(198,135,255,0.40) 0%,
      transparent 35%,
      transparent 65%,
      rgba(198,135,255,0.30) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  padding: 1px;
  opacity: 0.55;
}
.modal.is-open .modal-card { transform: translateY(0) scale(1); }

.modal-close {
  position: absolute;
  top: 16px; right: 16px;
  width: 32px; height: 32px;
  display: flex; align-items: center; justify-content: center;
  background: transparent;
  border: 1px solid rgba(198,135,255,0.20);
  color: var(--bone-soft);
  border-radius: 50%;
  cursor: pointer;
  transition: all 0.3s var(--ease-luxe);
  z-index: 1;
}
.modal-close:hover {
  background: rgba(198,135,255,0.18);
  border-color: rgba(198,135,255,0.55);
  color: var(--violet);
  transform: rotate(90deg);
}
.modal-title {
  font-family: var(--serif);
  font-weight: 200;
  font-size: clamp(38px, 4.6vw, 56px);
  line-height: 1.0;
  letter-spacing: -0.03em;
  color: var(--bone);
  margin: 16px 0 18px;
}
.modal-title em {
  font-style: italic; font-weight: 300;
  color: var(--violet);
  background: linear-gradient(180deg, var(--violet) 0%, var(--purple-rich) 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.modal-lede {
  font-size: 14.5px;
  color: var(--bone-soft);
  line-height: 1.65;
  margin: 0 auto 28px;
  max-width: 340px;
}
.modal-meta {
  font-family: var(--serif);
  font-style: italic;
  font-size: 11px;
  letter-spacing: 1px;
  color: var(--ash);
  margin-top: 18px;
}

/* Modal-mode invite form: stack vertically for centered layout */
.invite--modal {
  flex-direction: column;
  max-width: 100%;
  border: 1px solid rgba(198,135,255,0.16);
  border-radius: 0;
}
.invite--modal input {
  text-align: center;
  border-bottom: 1px solid rgba(198,135,255,0.10);
}
.invite--modal button {
  padding: 18px 26px;
  font-size: 11.5px;
}

@media (max-width: 520px) {
  .modal-card { padding: 48px 28px 36px; }
  .modal-title { font-size: clamp(32px, 9vw, 44px); }
}

/* ════════════════════════════════════════════════════════════════════
   PAGE LOADER  — brief luxury signifier
   The OZRIC O draws itself, eye-dot appears, tagline fades in below.
   The whole overlay dissolves once the orb engine has finished booting.
   Total visible duration ≤ 1.6s.
   ════════════════════════════════════════════════════════════════════ */
#page-loader {
  position: fixed;
  inset: 0;
  z-index: 10000;
  display: flex;
  align-items: center;
  justify-content: center;
  background:
    radial-gradient(ellipse 60% 50% at 50% 50%,
      rgba(26, 8, 56, 0.30) 0%,
      transparent 70%),
    var(--void);
  pointer-events: auto;
  transition:
    opacity 0.95s cubic-bezier(0.7, 0, 0.3, 1) 0.05s,
    visibility 0.95s linear 0.05s;
  /* While the loader is up, prevent the body's native cursor flashing
     before the bespoke cursor takes over — body is cursor:none anyway. */
}
#page-loader.is-done {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.loader-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 28px;
  color: var(--gold);
}
/* Brand-mark "O" typed in Newsreader 200 — same letterform as the
   header. Scaled up for centre-stage moment, with a layered text-shadow
   halo so the gold blooms against the void. The settle-in keyframe
   fades + relaxes a hair of extra tracking, like a stamp pressing flat. */
.loader-o {
  font-family: var(--serif);
  font-weight: 200;
  font-size: clamp(72px, 8vw, 104px);
  line-height: 1;
  color: var(--gold);
  letter-spacing: 0;
  text-shadow:
    0 0 28px rgba(201, 168, 76, 0.50),
    0 0 70px rgba(201, 168, 76, 0.25);
  opacity: 0;
  transform: scale(0.94);
  animation: loader-o-in 1.10s cubic-bezier(0.22, 1, 0.36, 1) 0.10s forwards;
}
@keyframes loader-o-in {
  0%   { opacity: 0; transform: scale(0.94); letter-spacing: 0.04em; }
  60%  { opacity: 1; }
  100% { opacity: 1; transform: scale(1);    letter-spacing: 0;       }
}

.loader-tag {
  font-family: var(--serif);
  font-weight: 200;
  font-size: clamp(12px, 1.05vw, 14px);
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: var(--bone-soft);
  white-space: nowrap;
  opacity: 0;
  transform: translateY(8px);
  animation: loader-tag-in 0.85s cubic-bezier(0.22, 1, 0.36, 1) 0.55s forwards;
}
.loader-tag em {
  /* Same Garamond-italic treatment as .display em — keeps the loader and
     the cards in one typographic universe. */
  font-family: 'Cormorant Garamond', 'Newsreader', serif;
  font-style: italic;
  font-weight: 500;
  color: var(--gold);
  text-transform: none;
  letter-spacing: 0.01em;
  font-size: 1.22em;
}
@keyframes loader-tag-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ════════════════════════════════════════════════════════════════════
   UNVEIL CHAPTER  —  appears after the 10 cards finish.
   Phone mockups + gold-toned scene copy, IntersectionObserver-driven
   reveals (cheap — only animates while in viewport, doesn't burn the
   scroll handler). Each scene posts a different orb pose so the orb
   keeps living behind the chapter.
   ════════════════════════════════════════════════════════════════════ */

/* ─── Chapter mark — replaces the old curtain divider ────────────────
   A proper section heading pinned to the LEFT column. Eyebrow ("II"),
   serif heading with gold italic emphasis, gold rule, tagline. Fills
   what used to be dead space between the last card and the first phone
   with a luxurious, intentional moment. Vertical padding kept tight so
   the chapter doesn't feel like an interruption. */
.chapter {
  position: relative;
  z-index: 4;
  /* Tightened from 60px / 80px → 28px / 56px so the iPhone chapter lifts
     off the screen sooner after card 9 ("For the few") finishes. Removes
     the dead air the user flagged between the closing card and the phone
     reveals — keeps the continuous-scroll feeling intact. */
  padding: 28px 0 56px clamp(40px, 5vw, 100px);
  pointer-events: none;
}
.chapter-mark {
  width: 42vw;
  max-width: 520px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 18px;
}
.chapter-eyebrow {
  font-family: var(--serif);
  font-weight: 500;
  font-style: italic;
  font-variation-settings: 'opsz' 24;
  font-size: 28px;
  color: var(--gold);
  letter-spacing: 0.06em;
  text-shadow: 0 0 14px rgba(201,168,76,0.20);
}
.chapter-heading {
  font-family: var(--serif);
  font-weight: 200;
  font-size: clamp(46px, 5.6vw, 76px);
  line-height: 1.06;       /* matches .display so the chapter italic em can sit cleanly within line bounds */
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--bone-soft);
  margin: 0;
}
.chapter-heading em {
  font-family: 'Newsreader', serif;
  font-style: italic;
  font-weight: 500;
  font-variation-settings: 'opsz' 36;
  color: var(--gold);
  text-transform: none;
  letter-spacing: 0.005em;
  font-size: 1.06em;
  -webkit-text-fill-color: var(--gold);
  text-shadow: 0 0 14px rgba(201, 168, 76, 0.20);
}
.chapter-rule {
  display: block;
  width: 56px;
  height: 1px;
  background: linear-gradient(to right, rgba(201,168,76,0.55), rgba(201,168,76,0.05));
  margin-top: 4px;
}
.chapter-tagline {
  font-family: 'Newsreader', serif;
  font-style: italic;
  font-weight: 500;
  font-variation-settings: 'opsz' 22;
  font-size: 22px;
  color: var(--bone-soft);
  letter-spacing: 0.01em;
}

/* Reveal cadence — slow, reverent, like the closing flourish */
.chapter .reveal {
  transition:
    opacity 1.1s cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 1.1s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.chapter .reveal[data-r="0"].is-in { transition-delay: 0ms;   }
.chapter .reveal[data-r="1"].is-in { transition-delay: 240ms; }
.chapter .reveal[data-r="2"].is-in { transition-delay: 520ms; }
.chapter .reveal[data-r="3"].is-in { transition-delay: 700ms; }

/* ─── Scene shell ───────────────────────────────────────────────────
   Every unveil scene is a 12-column grid section, ~100vh tall, anchored
   to the page.

   NOTE — content-visibility: auto was tried for paint-skip wins, but it
   defers layout enough that IntersectionObserver can't compute accurate
   bounding rects for the .reveal children inside. Result: the `is-in`
   class never fired, phones stayed at opacity:0, the chapter looked
   empty. Three sections aren't enough to justify it. Removed. */
.scene {
  position: relative;
  z-index: 4;
  min-height: 92vh;
  padding: 80px var(--gutter);
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  grid-auto-flow: dense;
  gap: 32px;
  align-items: center;
}

/* ─── Content placement ─────────────────────────────────────────── */
.scene-content { position: relative; }
.content--left   { grid-column: 1 / span 6; }
.content--right  { grid-column: 7 / span 6; }
.content--center {
  grid-column: 2 / span 10;
  max-width: 740px;
  margin: 0 auto;
  text-align: left;
}
.content--center .eyebrow.centred,
.content--center .display.centred,
.content--center .lede.centred { text-align: center; }
.content--center .display.centred,
.content--center .lede.centred { margin-left: auto; margin-right: auto; }

/* Soft scrim behind centred copy when the orb sits behind it. */
.scrim::before {
  content: '';
  position: absolute;
  inset: -10vh -8vw;
  background: radial-gradient(ellipse 80% 70% at 50% 50%,
              rgba(5,5,5,0.78) 0%,
              rgba(5,5,5,0.42) 40%,
              transparent 80%);
  z-index: -1;
  pointer-events: none;
}

/* ─── Eyebrow / display / lede on unveil scenes (gold-tinted) ──── */
.scene .eyebrow {
  color: var(--gold);            /* shifted from violet — chapter is gold-toned */
  text-shadow: 0 0 16px rgba(201,168,76,0.18);
}
.scene .display {
  /* Reuses .display from cards above, but per-line wrappers stagger.
     Each .line is a discrete inline-block so its own translateY can
     animate independently. */
  margin-bottom: 30px;
}
.scene .display .line {
  display: block;
  overflow: hidden;
}
.scene .display em {
  /* In the unveil, italic emphasis goes gold instead of violet —
     signals chapter shift. Override the upstream gradient. */
  background: linear-gradient(180deg, var(--gold) 0%, var(--gold-soft) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: var(--gold);
}

/* ─── Check list ────────────────────────────────────────────────── */
.check-list {
  list-style: none;
  padding: 0;
  margin: 18px 0 0;
}
.check-list li {
  position: relative;
  padding: 9px 0 9px 26px;
  font-size: 14px;
  line-height: 1.55;
  color: var(--bone-soft);
  border-bottom: 1px solid rgba(201,168,76,0.10);
  font-weight: 300;
}
.check-list li:last-child { border-bottom: none; }
.check-list li::before {
  content: '';
  position: absolute;
  left: 0; top: 18px;
  width: 12px; height: 1px;
  background: var(--gold);
}

/* ─── Phone mockups ─────────────────────────────────────────────────
   ORB-CENTRIC LAYOUT: every phone sits in the LEFT half of the grid
   (columns 1–5). The right half (cols 7–12) is intentionally empty so
   the orb breathes uninterrupted in its hero position.
   ───────────────────────────────────────────────────────────────── */
.phone {
  position: relative;
  margin: 0;
  grid-column: 1 / span 5;       /* LEFT half — orb owns the right half */
  display: flex;
  justify-content: center;
  align-items: center;
  padding-left: clamp(20px, 4vw, 80px);   /* breathing room from page edge */
  /* `translate` is a separate CSS property from `transform`, so the
     hover lift composes cleanly on top of the per-image transforms
     (rotation on triage 1+3, baseline -18px on .phone--lifted) without
     overriding them. Chrome 104+, Safari 14.1+ — universal in target. */
  transition: translate 0.4s var(--ease-luxe);
  will-change: translate;
}
@media (hover: hover) and (pointer: fine) {
  /* Card-hover micro-lift — Framer pattern. 2px is barely-perceptible
     standalone, but combined with the existing gold halo box-shadow on
     the img it reads as "this is an interactive surface." Gated to
     fine-pointer so touch users aren't penalised by sticky-hover state. */
  .phone:hover { translate: 0 -2px; }
  .phone:hover img {
    /* Slight halo lift on hover — adds a soft gold ring, doesn't
       dim/recompute the existing 5-layer box-shadow. */
    filter: brightness(1.55) contrast(1.05);
  }
  .phone img { transition: filter 0.4s var(--ease-luxe); }
}
.phone img {
  display: block;
  width: 100%;
  max-width: 320px;
  height: auto;
  border-radius: 32px;
  /* +50% brightness/clarity per spec. brightness/contrast are GPU-cheap
     and don't need a filter pass on resize. The phone PNGs have dark
     internal UI; brightness lifts the whole frame to read luxe rather
     than dim. */
  filter: brightness(1.5) contrast(1.05);
  /* Multi-layer ambient: stronger gold edge + deep shadow + brighter
     gold halo so the phone reads "lit from within" against the void. */
  box-shadow:
    0 0 0 1px rgba(201,168,76,0.18),
    0 18px 38px rgba(0,0,0,0.58),
    0 50px 90px rgba(0,0,0,0.48),
    0 0 90px rgba(201,168,76,0.14),
    0 0 160px rgba(201,168,76,0.06);
  transform: translate3d(0,0,0);    /* own GPU layer */
}

/* ─── Triage — small 3-phone fan, constrained to the left half ───── */
.phone-row {
  grid-column: 1 / span 6;
  display: flex;
  justify-content: center;
  align-items: flex-end;
  gap: clamp(10px, 1.6vw, 22px);
  perspective: 1200px;
  padding-left: clamp(20px, 4vw, 60px);
}
.phone-row .phone--small img {
  max-width: 160px;
  border-radius: 22px;
  /* Same +50% brightness boost as the solo phones above. */
  filter: brightness(1.5) contrast(1.05);
}
.phone-row .phone--small {
  flex: 0 1 auto;
  display: flex;
}
.phone-row .phone--small:nth-child(1) img {
  transform: rotateY(8deg) rotateZ(-2deg);
}
.phone-row .phone--small.phone--lifted img {
  transform: translateY(-18px) scale(1.05);
  z-index: 2;
  box-shadow:
    0 0 0 1px rgba(201,168,76,0.22),
    0 22px 50px rgba(0,0,0,0.62),
    0 60px 110px rgba(0,0,0,0.5),
    0 0 110px rgba(201,168,76,0.16);
}
.phone-row .phone--small:nth-child(3) img {
  transform: rotateY(-8deg) rotateZ(2deg);
}

/* Unveil scene shell — keeps orb fully visible on the right by leaving
   columns 6+ empty. Tightened from 78vh / 100px padding to 64vh / 50px
   so the chapter feels brisk and intentional rather than dragging. */
.scene--unveil {
  min-height: 64vh;
  padding-top: 50px;
  padding-bottom: 60px;
}
/* The first unveil scene immediately after the chapter mark — extra
   tightening so the chapter heading and the first phone read as a
   single composition. */
#chapter-ios + .scene--unveil {
  padding-top: 20px;
  min-height: 60vh;
}

/* ════════════════════════════════════════════════════════════════════
   CLOSING FLOURISH  —  the chapter's last breath.
   The Newsreader "O" from the page-loader returns, larger and slower.
   Pinned to the left column so the orb still breathes on the right.
   ════════════════════════════════════════════════════════════════════ */
/* TERMINAL LOCK — the closing scene becomes sticky-pinned during the
   .terminal-lock runway that follows it. While the user scrolls through
   that runway, the closing composition (gold "O" + tagline on the left,
   orb on the right) stays locked in viewport, and main.js animates the
   footer rising up from below. The user feels the page "lock" at this
   final composition with only the footer entering — a curtain-call rather
   than open-ended scrolling. */
.scene--closing {
  position: sticky;
  top: 0;
  height: 100vh;
  min-height: 100vh;
  padding-top: 0;
  padding-bottom: 0;
  align-items: center;
}

/* Scroll runway that drives the footer rise. Empty by design — its only
   purpose is to give the user a deliberate ~80vh of scroll during which
   the sticky .scene--closing stays locked at viewport top while the
   footer animates upward. After this runway ends, the document is at
   its bottom — there is no further scroll, by design. */
.terminal-lock {
  height: 80vh;
  pointer-events: none;
}
.closing-mark {
  grid-column: 1 / span 5;   /* same column as the phones */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 24px;
  padding-left: clamp(40px, 5vw, 100px);
}
.closing-O {
  /* Same letterform as the loader-O, scaled larger. The reveal animation
     adds its own opacity/translate so we don't run a CSS keyframe here. */
  font-family: var(--serif);
  font-weight: 200;
  font-size: clamp(96px, 11vw, 160px);
  line-height: 1;
  color: var(--gold);
  letter-spacing: 0;
  /* Layered halo — same anatomy as loader-O but warmer and bigger. */
  text-shadow:
    0 0 36px rgba(201, 168, 76, 0.55),
    0 0 90px rgba(201, 168, 76, 0.28);
}
.closing-tag {
  /* Echoes the loader-tag verbatim. Same font, weight, tracking,
     uppercase, bone-soft. The italic "Private" stays gold + 1.18em. */
  font-family: var(--serif);
  font-weight: 200;
  font-size: clamp(13px, 1.15vw, 16px);
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: var(--bone-soft);
  white-space: nowrap;
}
.closing-tag em {
  /* Newsreader italic 500 — same gold-emphasis token used across cards + chapter. */
  font-family: 'Newsreader', serif;
  font-style: italic;
  font-weight: 500;
  font-variation-settings: 'opsz' 16;
  color: var(--gold);
  text-transform: none;
  letter-spacing: 0.01em;
  font-size: 1.16em;
  text-shadow: 0 0 12px rgba(201, 168, 76, 0.20);
}
.closing-rule {
  display: block;
  width: 56px;
  height: 1px;
  background: linear-gradient(to right, rgba(201,168,76,0.55), transparent);
  margin-top: 8px;
}
.closing-coda {
  font-family: var(--sans);
  font-size: 9px;
  letter-spacing: 6px;
  text-transform: uppercase;
  color: var(--ash);
  font-weight: 400;
}

/* Closing reveals — slower, more reverent than the unveil reveals above.
   The "O" takes 1.6s to fully arrive; the tagline 1.1s. This is the
   page's "fin." moment, not a snap-cut. */
.scene--closing .reveal {
  transition:
    opacity 1.6s cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 1.6s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.scene--closing .reveal[data-r="0"].is-in { transition-delay: 0ms;   }
.scene--closing .reveal[data-r="1"].is-in { transition-delay: 600ms; }
.scene--closing .reveal[data-r="2"].is-in { transition-delay: 1100ms; }
.scene--closing .reveal[data-r="3"].is-in { transition-delay: 1400ms; }
.scene--closing .closing-O.reveal {
  /* The O scales up gently as it appears — same gesture as the loader,
     larger and slower. */
  transform: translate3d(0, 24px, 0) scale(0.92);
}
.scene--closing .closing-O.reveal.is-in {
  transform: translate3d(0, 0, 0) scale(1);
}

/* When the closing scene is active, gently fade the orb so the mark
   becomes the focal point. Driven by JS setting body[data-scene="closing"]. */
body[data-scene="closing"] #orb-stage {
  opacity: 0.55;
  transition: opacity 1.4s cubic-bezier(0.22, 0.61, 0.36, 1);
}
#orb-stage {
  transition: opacity 1.0s cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* ─── Stat ring (System scene) ──────────────────────────────────── */
.stat-ring {
  grid-column: 1 / -1;
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  margin-top: 56px;
  background: rgba(201,168,76,0.10);  /* hairline divider colour */
  border-top:    1px solid rgba(201,168,76,0.18);
  border-bottom: 1px solid rgba(201,168,76,0.18);
}
.stat {
  background: var(--void);
  padding: 28px 18px;
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.stat .n {
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(28px, 3.4vw, 44px);
  color: var(--gold);
  letter-spacing: -0.02em;
  line-height: 1;
}
.stat .l {
  font-family: var(--sans);
  font-size: 9px;
  letter-spacing: 4px;
  text-transform: uppercase;
  color: var(--bone-soft);
  font-weight: 400;
}

/* ─── Reveal (IntersectionObserver-driven) ───────────────────────
   Each .reveal element has data-r="N" → staggered delay. Cards stack
   gracefully into view: 28px lift, 600ms eased. Phones go bigger —
   scale + lift + 1.1s — so they feel like they slide forward into
   real space. */
.reveal {
  opacity: 0;
  transform: translate3d(0, 28px, 0);
  transition:
    opacity 0.75s cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 0.75s cubic-bezier(0.22, 0.61, 0.36, 1);
  will-change: opacity, transform;
}
.reveal.is-in {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}
.reveal[data-r="0"].is-in { transition-delay: 0ms;   }
.reveal[data-r="1"].is-in { transition-delay: 110ms; }
.reveal[data-r="2"].is-in { transition-delay: 230ms; }
.reveal[data-r="3"].is-in { transition-delay: 360ms; }
.reveal[data-r="4"].is-in { transition-delay: 500ms; }

.phone.reveal {
  /* Bigger, slower entrance — feels like a physical object sliding in. */
  opacity: 0;
  transform: translate3d(0, 60px, 0) scale(0.94);
  transition:
    opacity 1.1s cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 1.2s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.phone.reveal.is-in {
  opacity: 1;
  transform: translate3d(0, 0, 0) scale(1);
}

.curtain.reveal {
  /* The chapter divider lifts in slowly — the literal curtain. */
  opacity: 0;
  transform: translate3d(0, 16px, 0);
  transition:
    opacity 1.0s cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 1.0s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.curtain.reveal.is-in {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

/* ─── Mobile — stack everything single-column ──────────────────── */
@media (max-width: 880px) {
  .scene {
    grid-template-columns: 1fr;
    padding: 60px var(--gutter);
    min-height: auto;
  }
  .content--left, .content--right, .content--center {
    grid-column: 1;
    max-width: none;
  }
  .phone, .phone[data-side="left"], .phone[data-side="right"] {
    grid-column: 1;
    margin-top: 28px;
  }
  .phone img { max-width: 260px; }
  .phone-row { flex-wrap: wrap; gap: 14px; }
  .phone-row .phone--small img { max-width: 140px; }
  .stat-ring { grid-template-columns: repeat(2, 1fr); }
  .curtain { padding: 70px var(--gutter) 36px; }
  .curtain-rule { max-width: 80px; }
}

/* ─── Reduced motion + show=all bypass ─────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  .phone.reveal { opacity: 1; transform: none; transition: none; }
  .curtain.reveal { opacity: 1; transform: none; transition: none; }
}
body[data-show="all"] .reveal { opacity: 1; transform: none; transition: none; }

@media (prefers-reduced-motion: reduce) {
  .loader-o   { opacity: 1; transform: none; animation: none; letter-spacing: 0; }
  .loader-tag { opacity: 1; transform: none; animation: none; }
}

/* ════════════════════════════════════════════════════════════════════
   MOBILE REFINEMENT PASS — 2026-06-11
   ────────────────────────────────────────────────────────────────────
   Appended LAST so it wins the cascade at equal specificity. Fixes the
   audited mobile defects (hero overload, orb-wash legibility, broken
   stat rows, dead viewport, phone-fan pile-up) and raises the page to
   mobile-native: HIG tap targets, iOS no-zoom inputs, safe-area floor.
   Desktop is untouched — every rule is gated to ≤880px / ≤520px.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 880px) {

  /* ── A. Card scrim — recentred + deepened ─────────────────────────
     The orb poses dead-centre behind centred cards on mobile, but the
     desktop scrim was a weak ellipse parked at 30% 50% (tuned for the
     left-column layout). Re-centre it and deepen the inner stops so
     body copy reads calmly over the particle field while the orb's
     glow still breathes around the card's edges. */
  .card::before {
    inset: -10vh -12vw -14vh;
    background: radial-gradient(ellipse 100% 90% at 50% 46%,
                rgba(5,5,5,0.84) 0%,
                rgba(5,5,5,0.66) 40%,
                rgba(5,5,5,0.34) 66%,
                transparent 88%);
  }

  /* ── B. Stat micro-rows — hairline-divided triptych ───────────────
     Was: left-anchored flex that wrapped 2+1 and misaligned under the
     centred card. Now: a quiet three-column register with a gold top
     hairline and faint vertical separators — the Aman/Patek stat-row
     idiom. Numbers centre over their labels; labels wrap inside their
     own third. */
  .card .micro-row {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0;
    margin-top: 34px;
    border-top: 1px solid rgba(201,168,76,0.16);
  }
  .card .micro-row > span {
    align-items: center;
    text-align: center;
    padding: 16px 8px 0;
    border-left: 1px solid rgba(201,168,76,0.10);
  }
  .card .micro-row > span:first-child { border-left: 0; }
  .card .micro-row b { font-size: clamp(20px, 5.6vw, 26px); }
  .card .micro-row i {
    font-size: 8px;
    letter-spacing: 2.5px;
    line-height: 1.6;
    color: var(--bone-soft);   /* --ash was sinking into the orb bleed */
  }
  /* "with" on the memory card occupies a numeral slot — set it italic
     serif so it reads as deliberate wordplay, not a missing number. */
  .card[data-i="4"] .micro-row > span:nth-child(2) b {
    font-style: italic;
    font-weight: 500;
    font-variation-settings: 'opsz' 18;
  }

  /* ── C. Progress dots — desktop affordance, mobile hazard ─────────
     6px dots sit exactly where the right thumb rests; an accidental
     tap jumps the whole page. Sub-HIG targets, no hover preview.
     The orb + card crossfade already communicate progress. */
  .progress { display: none; }

  /* ── D. Chapter II — full-width centred composition + scrim ──────
     The chapter inherited a desktop left padding and a 42vw-wide mark
     (≈164px on a phone) that broke centring and let the heading spill.
     Full width, centred, with the same soft scrim the cards use. */
  .chapter {
    padding-left: var(--gutter-sm);
    padding-right: var(--gutter-sm);
  }
  .chapter-mark {
    width: 100%;
    max-width: none;
    position: relative;
  }
  .chapter-mark::before {
    content: '';
    position: absolute;
    inset: -8vh -10vw;
    background: radial-gradient(ellipse 95% 85% at 50% 50%,
                rgba(5,5,5,0.74) 0%,
                rgba(5,5,5,0.42) 48%,
                transparent 84%);
    z-index: -1;
    pointer-events: none;
  }
  /* Pull Chapter II up into the runway the released card stack leaves
     behind — closes the orb-only dead viewport (paired with the
     tighter mobile perCard in main.js sizeTrack). */
  #chapter-ios { margin-top: -26vh; }

  /* ── E. Phones — true centring, one-row fan ───────────────────────
     Solo phones carried a desktop left padding that pushed them off
     the centre line. The triage fan (3 × 140px + gaps) exceeded the
     viewport and wrapped into an overlapping pile — now a single
     centred fan, scaled to fit, lift softened. */
  .phone { padding-left: 0; }
  .phone img { max-width: min(64vw, 280px); }
  .phone-row {
    /* grid-column: 1 — the desktop `span 6` was forcing implicit
       columns into the single-column mobile grid. */
    grid-column: 1;
    padding-left: 0;
    flex-wrap: nowrap;
    justify-content: center;
    gap: clamp(8px, 2.6vw, 14px);
  }
  .phone-row .phone--small img {
    max-width: min(26vw, 116px);
    border-radius: 16px;
  }
  .phone-row .phone--small.phone--lifted img {
    transform: translateY(-10px) scale(1.05);
  }

  /* ── F. Journal meta-nav — stacked column ─────────────────────────
     Three essays side-by-side cramped into two ragged columns at phone
     width. A single centred column reads as a quiet index. */
  .meta-nav-links {
    flex-direction: column;
    align-items: center;
    gap: 22px;
  }
  .meta-nav::before {
    content: '';
    position: absolute;
    inset: 0 -8vw;
    background: radial-gradient(ellipse 95% 90% at 50% 50%,
                rgba(5,5,5,0.66) 0%,
                rgba(5,5,5,0.36) 52%,
                transparent 86%);
    z-index: -1;
    pointer-events: none;
  }

  /* ── G. Closing tagline — full presence ──────────────────────────
     bone-soft at 13px was washing out over the dimmed orb. Lift the
     tone AND set a gentle scrim behind the whole closing mark — the
     orb already dims to 0.55 here, so this stays a whisper. */
  .closing-tag { color: var(--bone); }
  .scene--closing { overflow-x: clip; }   /* contain the scrim bleed below */
  .scene--closing .closing-mark { position: relative; }
  .scene--closing .closing-mark::before {
    content: '';
    position: absolute;
    inset: -6vh -10vw;
    background: radial-gradient(ellipse 90% 80% at 50% 55%,
                rgba(5,5,5,0.50) 0%,
                rgba(5,5,5,0.26) 52%,
                transparent 84%);
    z-index: -1;
    pointer-events: none;
  }

  /* ── H. iOS input discipline ──────────────────────────────────────
     Safari force-zooms any focused input under 16px — the page jumps
     and the user has to pinch back out. 16px kills the zoom. */
  .invite input { font-size: 16px; }

  /* ── I. Footer — home-indicator safe-area floor + honest tap zones.
     Balanced 3+3 wrap (was 5+1 with TERMS orphaned at 430px); link
     padding widens each hit area toward the 44px HIG zone without
     changing the visual register. ─────────────────────────────────── */
  .site-footer {
    padding-bottom: max(38px, calc(env(safe-area-inset-bottom, 0px) + 22px));
  }
  .footer-nav { max-width: 264px; gap: 6px 18px; }
  .footer-nav a { padding: 8px 4px; }

  /* ── J. Touch politeness — no grey tap flash, no double-tap delay ── */
  a, button, input, .btn {
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
  }

  /* ── K. Contain the scrim bleeds ──────────────────────────────────
     The chapter-mark / meta-nav scrims bleed -10vw past the viewport
     (soft edges). html's overflow-x:hidden already clips them visually,
     but they inflate document scrollWidth by ~31px. overflow-x: clip
     (NOT hidden — clip never creates a scroll container, so no sticky
     breakage) cuts the bleed exactly at the screen edge where it was
     never visible anyway. */
  .chapter, .meta-nav { overflow-x: clip; }

  /* ── L. Error slot — collapse when empty ──────────────────────────
     .invite-error reserves min-height + margin inside the bordered
     glass form, which read as a dead black band under the button on
     the stacked mobile forms. Collapse until it has a message. */
  .invite .invite-error:empty { display: none; }

  /* ── N. Hero — full-height composition ────────────────────────────
     The hero previously centred as one tight block, leaving dead space
     above and below. Now it owns the whole first screen: eyebrow +
     headline + lede distributed down the page, REQUEST INVITATION and
     SCROLL TO BEGIN anchored at the bottom (space-between shares the
     remaining air evenly between the four groups).
     transform:none!important overrides the per-frame JS translate that
     centres the other cards — the hero pins to the stage edges instead
     (it still crossfades via opacity on scroll-out). */
  .card[data-i="0"] {
    top: 0;
    bottom: 0;
    transform: none !important;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding-top: calc(env(safe-area-inset-top, 0px) + 14vh);
    padding-bottom: max(6vh, calc(env(safe-area-inset-bottom, 0px) + 30px));
  }
  .card[data-i="0"] .eyebrow { margin-bottom: 0; }
  .card[data-i="0"] .display { margin-bottom: 0; }
  .card[data-i="0"] .lede    { margin-bottom: 0; }
  .card[data-i="0"] .cta-row { margin-top: 0; }

  /* ── M. Hero word-stagger — mobile pacing cap ─────────────────────
     The hero reveal staggers EVERY word at 60ms; with ~55 words the
     lede's last line doesn't finish arriving until ~4.2s after the
     loader — on a phone that reads as missing copy, not choreography.
     Tighter per-word step + a hard 950ms cap completes the whole hero
     inside ~2s while keeping the word-by-word arrival. Desktop keeps
     its original cadence. */
  .card[data-i="0"].is-revealing .display .cw,
  .card[data-i="0"].is-revealing .eyebrow .cw,
  .card[data-i="0"].is-revealing .lede .cw {
    animation-delay: calc(min(var(--i, 0) * 45ms, 950ms) + 80ms);
  }
}

@media (max-width: 520px) {
  /* Display scale — clamp(34px, 11vw, 50px) let "INTELLIGENCE." run
     edge-to-edge at 390px and clip on 360px Androids. One notch down
     + slightly tighter tracking keeps the longest word inside the
     gutters on every phone in circulation. */
  .display {
    font-size: clamp(31px, 10vw, 46px);
    letter-spacing: 0.045em;
  }

  /* iOS no-zoom floor must survive the earlier 520px block that set
     inputs back to 13px. */
  .invite input { font-size: 16px; padding: 15px 16px; }

  /* Card-9 inline invite — stack vertically like the modal form. The
     row layout left the email field ~130px wide next to the button. */
  .card .invite {
    flex-direction: column;
    max-width: min(86vw, 380px);
  }
  .card .invite input {
    text-align: center;
    border-bottom: 1px solid rgba(198,135,255,0.10);
  }
  .card .invite button { padding: 16px 18px; }

  /* Closing tagline — keep "A PRIVATE INTELLIGENCE." on one line on
     320–375px phones by easing the 0.42em tracking. */
  .closing-tag {
    font-size: 12px;
    letter-spacing: 0.30em;
  }
}

/* ════════════════════════════════════════════════════════════════════
   SHORT VIEWPORT (landscape phones, ≤480px tall)
   ────────────────────────────────────────────────────────────────────
   At 844×390 the portrait-tuned card was taller than the stage: the
   headline ran up under the header chrome and the lede clipped
   mid-sentence against the stage's overflow:hidden. Compress the
   whole typographic scale so every card fits a 390px-tall stage.
   No desktop impact — no desktop viewport is ≤480px tall. ─────────── */
@media (max-height: 480px) {
  .card {
    padding-top:    max(4vh, calc(env(safe-area-inset-top, 0px) + 66px));
    padding-bottom: 5vh;
  }
  .card .eyebrow { margin-bottom: 10px; }
  .display,
  .card .display {
    font-size: clamp(22px, 8.5vh, 34px);
    margin-bottom: 14px;
  }
  .lede,
  .card .lede {
    font-size: 13.5px;
    line-height: 1.5;
    margin-bottom: 16px;
    max-width: min(76vw, 560px);
    margin-left: auto;
    margin-right: auto;
  }
  .card[data-i="0"] .eyebrow { margin-bottom: 12px; }
  .card[data-i="0"] .display { margin-bottom: 16px; }
  .card[data-i="0"] .lede    { margin-bottom: 20px; }
  .card .btn--primary { padding: 12px 22px; min-width: 0; font-size: 11px; }
  .cta-row .hint { margin-top: 2px; }
  .card .micro-row { margin-top: 14px; }
  .card .micro-row > span { padding-top: 8px; }
  .card .micro-row b { font-size: 18px; }
  .chapter-heading { font-size: clamp(30px, 9vh, 44px); }
  .closing-O { font-size: clamp(72px, 26vh, 112px); }
  /* Full-height hero needs a tighter top clearance at 390px tall —
     14vh of padding would crush the space-between rhythm. */
  .card[data-i="0"] {
    padding-top: calc(env(safe-area-inset-top, 0px) + 64px);
    padding-bottom: 4vh;
  }
}

/* ════════════════════════════════════════════════════════════════════
   MOBILE ORB SWAP — Concept B rendered loop  (2026-06-13)
   ────────────────────────────────────────────────────────────────────
   On phones the live three.js iframe is replaced by a lightweight,
   deterministic constellation loop. main.js sets the iframe src on
   DESKTOP ONLY, so the WebGL engine never downloads/runs on mobile —
   removing the fixed-iframe iOS compositing + battery cost the review
   flagged. Desktop is completely untouched.
   ════════════════════════════════════════════════════════════════════ */
#orb-mobile { display: none; }
@media (max-width: 880px) {
  #orb { display: none; }
  #orb-mobile {
    display: block;
    position: absolute;
    left: 50%; top: 46%;
    transform: translate3d(-50%, -50%, 0);
    width: min(138vw, 660px);
    height: auto;
    z-index: 1;
    pointer-events: none;
  }
  /* the rendered loop carries its own bloom + glowing core, so the
     separate #orb-bloom would double up — drop it on mobile. */
  #orb-bloom { display: none; }
}
/* Reduced motion / data — drop the video, fall back to the static still
   (#orb-stage::before, already defined for these queries above). */
@media (prefers-reduced-motion: reduce), (prefers-reduced-data: reduce) {
  #orb-mobile { display: none !important; }
}

/* ════════════════════════════════════════════════════════════════════
   AGENTIC LOOP SECTION — "how it works" motion beat  (2026-06-13)
   One spoken instruction → OZRIC acts across the stack. Desktop ring
   loop + mobile vertical task-feed loop, swapped by breakpoint.
   ════════════════════════════════════════════════════════════════════ */
.agentic-scene {
  position: relative;
  z-index: 4;
  padding: 72px var(--gutter) 84px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.agentic-head { max-width: 760px; }
.agentic-scene .eyebrow {
  display: block;
  color: var(--gold);
  text-shadow: 0 0 16px rgba(201,168,76,0.18);
  margin-bottom: 18px;
}
.agentic-scene .display { margin-bottom: 0; }
.agentic-lede {
  font-size: clamp(15px, 1.3vw, 18px);
  line-height: 1.7;
  color: var(--bone-soft);
  max-width: 560px;
  margin: 22px auto 0;
  font-weight: 300;
}
.agentic-figure {
  margin: 44px 0 0;
  width: 100%;
  max-width: 1000px;
}
.agentic-video {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid rgba(201, 168, 76, 0.16);
  border-radius: 14px;
  box-shadow:
    0 1px 0 rgba(255,255,255,0.04) inset,
    0 30px 70px rgba(0,0,0,0.55),
    0 0 90px rgba(150,90,210,0.08);
  background: var(--void);
  transform: translate3d(0,0,0);
}
.agentic-video--tall { display: none; max-width: 360px; margin: 0 auto; }
@media (max-width: 880px) {
  .agentic-scene { padding: 52px var(--gutter-sm) 56px; }
  .agentic-figure { margin-top: 32px; }
  .agentic-video--wide { display: none; }
  .agentic-video--tall { display: block; }
}

/* ════════════════════════════════════════════════════════════════════
   HOMEPAGE HERO — the agentic loop as card 0  (2026-06-13)
   "One instruction. Everything moves." + the HyperFrames agentic video.
   Full-width centred (overrides the left-column card layout). The
   background orb dims while the hero is dominant so the video's own orb
   owns the composition; it fades back in for cards 1-9.
   ════════════════════════════════════════════════════════════════════ */
.card--hero {
  left: 0;
  right: 0;
  max-width: none;
  /* nudge the vertical anchor down (vs the 46% card default) so the
     taller hero composition clears the fixed header when centred. */
  top: 51%;
  text-align: center;
  align-items: center;
  display: flex;
  flex-direction: column;
  padding: 0 var(--gutter);
}
.card--hero .hero-head { width: 100%; }
.card--hero .eyebrow { display: block; margin: 0 auto 16px; }
/* Hero headline runs SMALLER than a card headline — it shares the screen
   with the video below it, so the whole composition stays balanced and
   clears the fixed header when the card is vertically centred. */
.card--hero .display {
  margin: 0 auto;
  font-size: clamp(30px, 3.6vw, 54px);
  letter-spacing: 0.045em;
}
.card--hero .hero-figure {
  margin: 24px auto 22px;
  width: 100%;
  display: flex;
  justify-content: center;
}
.card--hero .agentic-video {
  display: block;
  width: auto;
  height: auto;
  max-width: min(84vw, 860px);
  max-height: 38vh;
  border: 1px solid rgba(201, 168, 76, 0.16);
  border-radius: 14px;
  box-shadow:
    0 1px 0 rgba(255,255,255,0.04) inset,
    0 30px 70px rgba(0,0,0,0.55),
    0 0 90px rgba(150,90,210,0.08);
  background: var(--void);
}
.card--hero .agentic-video--tall { display: none; }
.card--hero .hero-foot { width: 100%; }
.card--hero .lede { max-width: 580px; margin: 0 auto 24px; }
.card--hero .cta-row { justify-content: center; }

/* Dim the live orb stage while the hero is the active card. */
body[data-hero="1"] #orb-stage { opacity: 0.12; }

@media (max-width: 880px) {
  .card--hero {
    padding-left: var(--gutter-sm);
    padding-right: var(--gutter-sm);
  }
  /* keep "ONE INSTRUCTION." on one line inside the phone gutters */
  .card--hero .display { font-size: clamp(25px, 7.4vw, 40px); letter-spacing: 0.03em; }
  .card--hero .agentic-video--wide { display: none; }
  .card--hero .agentic-video--tall {
    display: block;
    max-width: min(72vw, 320px);
    max-height: 48vh;
  }
  .card--hero .hero-figure { margin: 14px auto; }
  /* the mobile feed video carries its own spoken-instruction line, so
     the hero stays tight — drop the redundant lede on phones. */
  .card--hero .lede { display: none; }
}
@media (max-width: 520px) {
  .card--hero .display { font-size: clamp(23px, 7vw, 34px); }
}
