/* ═══════════════════════════════════════════════════════════════════════════
   rzv.studio — intro.css   (INTRO/CINEMATIC agent: the opening film + #hero)

   THE FILM RUNS ON THE HERO STAGE, NOT IN #loader. #loader is now nothing but
   an anti-flash plate: intro.js dismisses it on its first frame, once the
   runtime hidden states are set, and it fades out over black onto black. That
   is why there is almost nothing about it below.

   The consequence worth knowing: there is exactly ONE aurora and at most ONE
   pearl on the stage at a time. The aurora is revealed by a circular clip
   opening from the stage's centre — no second copy to cross-fade — and the
   pearl is a single element whose --pearl-size is written rather than scaled,
   so every inset shadow, the border and the iridescent band stay correct at
   each size it takes (115px as it arrives, 25px through the mark, then 9px as
   it lands in the brand slot). base.css parameterised .pearl by --pearl-size
   precisely so this could be one object — and inverting the birth into a scale
   DOWN is what turned that from a nicety into a requirement: the arrival is now
   the LARGEST the pearl ever is, and a 25px disc magnified 4.6× is a smear.
   Which is also why .hx-pearl below switches base.css's box-shadow transition
   off; see there.

   AND THE PEARL LEAVES. Haseeb: "no need for the pearl in hero." The film's
   pearl flies into the header at beat 7 and never comes back; the composed
   hero is the menu, the brow, the headline and the CTA on the settled aurora.
   Everything that held space or scroll for a resting hero pearl — the
   --hero-pearl-gap padding above the headline, and the drop that carried it to
   the viewport centre — has gone with it.

   THE ONE RULE THAT SHAPES THIS FILE: nothing that ships in index.html is
   pre-hidden here. base.css delivers the page composed and readable; intro.js
   sets every hidden state at RUNTIME and only when RZV.motionOK() is true, so
   a crashed or absent script can never leave a blank screen behind. The
   `opacity:0` rules below are only ever on elements intro.js itself created.

   TRAP 2: GSAP owns `transform` on everything it animates, so those elements
   are centred with margin offsets (or xPercent/yPercent, which GSAP owns too)
   — never with a CSS translate that GSAP's next write would delete. The
   exceptions are .rz-word, .hx-skip and .hero-inner, which GSAP never
   transforms.
   ═══════════════════════════════════════════════════════════════════════ */


/* ═══════════════════════════════════════════════════════════════════════════
   1 · THE ANTI-FLASH PLATE
   Black, over everything, for the ~0.9s it takes to fade — which is spent over
   the film's own opening black, so it is never seen going. It exists for the
   frames between first paint and intro.js setting .hero-copy's hidden state.
   ═══════════════════════════════════════════════════════════════════════ */
#loader{
  -webkit-user-select:none;user-select:none;pointer-events:none;
  /* base.css gives it a .9s fade, which was right when it was the curtain the
     whole film played behind. It is not the curtain any more — the film plays
     on the stage BENEATH it — so a .9s fade would dim the pearl's arrival by
     up to 65% for the first half-second. .22s fits inside the film's opening
     black and is gone before anything moves. */
  transition:opacity .22s linear, visibility 0s linear .22s;
}


/* ═══════════════════════════════════════════════════════════════════════════
   1b · THE HEADER LEAVES WITH THE HERO

   Haseeb: "After scroll down from hero header hides up." nav.js's
   RZV.headerAway() sets html.hdr-away, and intro.js calls it off the hero
   ScrollTrigger's own boundary — the same instant it parks the intro stage.
   The rule: the header is away for exactly as long as #hero is behind the
   reader, and it returns the moment they scroll back up into it.

   ⚠️ NOT html.hdr-veil, AND THAT IS THE WHOLE POINT. glass.js gates every
   autonomous glass solid on the home page on the veil being absent, so
   reusing it here would switch the glass off for the whole of the venn and the
   work grid. This class is unknown to glass.js.

   ⚠️ OPACITY AND TRANSFORM, NEVER display:none OR visibility. intro.js's
   flight and constellation.js's both measure #brandPearl with
   getBoundingClientRect through this state, and both suspend .site-header's
   transform INLINE for the read — inline beats this rule, and neither cares
   about opacity. Take the header out of layout and both measurements return a
   zero-width box.

   translateY(-100%) rather than the veil's -12px: this is the header LEAVING,
   not a chrome-free frame being held, and it has the whole header height to
   travel through. It rides base.css's existing .9s opacity+transform
   transition on .site-header, so it leaves and returns at the site's pace.

   This lives here rather than in base.css because base.css is another agent's
   file — and index.html is the only page that loads this stylesheet and the
   only page with a hero to be past, so the scoping is right either way.
   ═══════════════════════════════════════════════════════════════════════ */
html.hdr-away .site-header{
  opacity:0;transform:translateY(-100%);pointer-events:none;
}


/* ═══════════════════════════════════════════════════════════════════════════
   2 · THE WORDMARK — `RZV` + pearl, and the pearl is the FULL STOP

   The mark the film builds is `RZV.` — three caps with the pearl seated after
   the V as the period. It is NOT a split word with the pearl in the middle any
   more; there is no `studio` half, and there is therefore nothing to keep
   balanced either side of a gap.

   That kills the 1fr/auto/1fr grid this used to be, and the reasoning is worth
   keeping because it is what the grid was FOR. With `rzv` · pearl · `studio`
   the pearl had to sit on the screen's centre while the two halves weighed
   three characters against six, so a plain centred row carried the gap left of
   the pearl by half the difference. Forcing the outer columns equal put the
   middle column dead centre by construction. None of that applies to a mark
   whose dot is at the END: the composition is simply `RZV` followed by the
   dot's seat, and CENTRING THE WHOLE ROW is now the correct answer — the
   optical centre of `RZV.` is the centre of `RZV.`, not the centre of `RZV`.

   .rz-gap is the dot's SEAT — a real, laid-out box the pearl is flown into,
   not empty air. intro.js measures its centre and lands the pearl there, so
   the period's sidebearing is a CSS number in this file rather than a constant
   in a timeline. Its width is about one mono advance (0.6em) at every size,
   which is what a full stop occupies.

   `translate(-50%, calc(-50% + 0.05em))` — the nudge that agrees the letters'
   ink with the pearl rather than with their em box. It was 0.02em for
   `rzv.studio`, whose `t`, `d` and the tittle on the `i` pushed the ink most of
   the way up the box on its own. ALL CAPS puts it back: R Z V span baseline to
   cap height and nothing at all uses the descender space, so the ink now sits
   HIGH in the box by roughly the same margin it used to sit low, and the word
   has to come down to meet a pearl centred on the box.
   ═══════════════════════════════════════════════════════════════════════ */
/* Font swapped to Bizmo (base.css's @font-face) — "the logo isnt using the
   Bizmo font. logo in intro too", his words. Nothing structural depended on
   it being Space Mono specifically: measureWord (intro.js) reads .rz-gap's
   and each glyph's ACTUAL rendered box via getBoundingClientRect() after
   document.fonts.ready, not a hardcoded pixel assumption, so the pearl still
   lands correctly wherever the gap ends up being for whatever font is
   loaded. What the comment below calling 0.6em "one Space Mono advance,
   exactly what a period occupies" can no longer promise is that the gap is
   Bizmo's own period-width to the pixel — that was tuned for mono's fixed
   advance, and Bizmo is not a mono face. Left as the starting point rather
   than re-tuned blind; the seat is his to narrow or widen once he can see
   where a real Bizmo period would sit. letter-spacing dropped from -0.02em
   to normal for the same reason as base.css's .brand-text: that value was
   fitted to Space Mono's glyphs, and there is no basis yet for what Bizmo's
   own capitals want at this size. */
.rz-word{
  position:absolute;left:50%;top:50%;
  transform:translate(-50%,-50%);
  display:block;pointer-events:none;opacity:0;
  /* THE MARK's box. Width is the old wordmark's span; height follows the
     logo's 1578:403 ratio. The svg overflows visibly so a round stroke cap
     at the path's edge is never clipped. */
  width:clamp(110px,17vw,230px);
   /* 50% smaller, 3 Sep */}
.rz-mark svg{display:block;width:100%;height:auto;overflow:visible}
/* The fill is always solid; what reveals it is the SVG mask intro.js builds —
   Haseeb's centre line stroked brush-wide and dash-drawn in step with the
   pearl. The guide path the pearl rides has no paint at all. */
.rz-fill{fill:url(#rzPearlBase)}          /* the pearl material (intro.js defs), 3 Sep */
.rz-fill-hi{fill:url(#rzPearlHi);pointer-events:none}
.rz-dot{fill:url(#rzPearlBase);opacity:0}
/* The wipe clips the wrapper; will-change so the clip edge animates on the compositor. */
.rz-word{will-change:clip-path}

.hx-skip{
  position:absolute;left:50%;bottom:clamp(22px,5vh,46px);bottom:clamp(22px,5lvh,46px);
  transform:translateX(-50%);
  color:var(--dim);opacity:0;pointer-events:none;
}


/* ═══════════════════════════════════════════════════════════════════════════
   3 · THE HERO STATEMENT

   base.css used to own .hero-inner / .hero-copy / .hero-title; those rules were
   purged in the cleanup sweep, so the layout lives here now — with the code
   that animates it.

   THE RESERVED AIR ABOVE THE HEADLINE IS GONE. --hero-pearl-gap held open
   clamp(58px,9vh,110px) of padding at the top of .hero-copy for the pearl to
   rest in, and intro.js's restOffset() measured the pearl's landing off exactly
   that computed value. With no pearl in the hero it was padding held for
   nothing, and it pushed the statement half its own height below the optical
   centre of a block that is centred by transform. Removed, so the sentence now
   sits dead centre. Nothing else read it: constellation.css names it in a
   comment but declares its own --cn-pearl-gap, and glass.js only quotes the
   number in prose.

   Live, .hero-inner is FIXED and viewport-centred. #hero carries ~130vh of
   empty runway for the takeover, and `justify-content:center` cannot centre
   against padding — so in flow the statement would sit at the top of a very
   tall box instead of in the middle of the screen. Fixed also means it holds
   still while the pearl grows through it, which is the point. Without
   html.intro-live it stays in normal flow and base.css's
   `#hero{justify-content:center}` centres it perfectly well.
   ═══════════════════════════════════════════════════════════════════════ */

.hero-inner{
  display:flex;flex-direction:column;align-items:center;text-align:center;
}
html.intro-live .hero-inner{
  position:fixed;left:50%;top:calc(var(--vh,1vh) * 50);   /* --vh: fixed frame, immune to the toolbar */
  transform:translate(-50%,-50%);
  z-index:1;
  /* ── AND A WIDTH, WHICH IT HAS NEVER HAD ──
     A fixed element with no width is shrink-to-fit, and its containing block is
     the VIEWPORT — so with left:50% and no right, the space it fits itself into
     is measured from the middle to the right edge and the -50% translate then
     hangs the overflow off BOTH sides. Nothing here was bounded by the screen
     at all: .hero-title's max-width is in `ch`, which is a function of the font
     size, not of the window, so at a large clamp value it exceeds the viewport
     and the headline runs off both edges — which is exactly what it was doing.
     Bounding the block is the fix, not shrinking the type: the h1 wraps inside
     a box that is now guaranteed narrower than the screen. */
  width:100vw;max-width:var(--wrap-hero);   /* 7 Sep: the title spans the hero's full measure now (split layout), same cap as the header row and the corner meta */
}
.hero-inner.wrap{max-width:var(--wrap-hero)}   /* static path too */
/* ── THE CORNER META, WHILE THE FILM OWNS THE SCREEN ──
   meta.css owns .hero-meta's resting layout: `position:absolute; bottom:0`
   inside #hero, which is exactly right on the static path — #hero is one
   viewport tall there and the corners sit on the fold.

   Live, it is not. html.intro-live buys #hero ~85vh of runway (section 4
   below) and `bottom:0` goes with it, off the bottom of the screen — the
   corners would never be seen at all. Same diagnosis and same prescription as
   .hero-inner directly above: pin it to the viewport for as long as the stage
   is fixed.

   HOW FAR OFF THE SCREEN, EXACTLY — because a neighbouring file's arithmetic
   turns on this number and an earlier draft of this note got it wrong. The
   runway does NOT stack on top of the 100lvh: box-sizing is border-box
   site-wide (base.css line 131), so min-height INCLUDES padding and #hero is
   max(100lvh, padding-top + padding-bottom), not their sum. That comes out
   ~110vh, not ~185vh — the corners sit ~10vh under the fold rather than a
   screen and a half. Invisible either way, so the rule below is unchanged;
   the figure is corrected because constellation.js reads the hero height as
   "~1.07vh" when it bounds the window it consults glass.js's handoff() in,
   and that bound is right. Anyone shortening this runway should check that
   note (THE GLASS-TO-PEARL HANDOFF) still holds.

   ONE DECLARATION, on purpose. meta.css's left/right/max-width/margin/padding
   all mean the same thing against the viewport as they did against #hero, so
   the corners keep the .wrap gutter and stay aligned to the headline's measure
   rather than to the raw screen edge. Its z-index:1 also still means what it
   meant: above the fixed stage (z-index:0), below the glass solid, so the cube
   still refracts the type — which is the whole reason the corners are worth
   having.

   NO OPACITY AND NO TRANSFORM HERE, and none in meta.css either. This is
   shipped markup: intro.js sets its hidden state at runtime and only under
   RZV.motionOK() (prepCopy), and takes it away again in destroy(). With no JS,
   or with motion off, html.intro-live is never set — the rule above does not
   apply, and meta.css alone composes the corners on the fold.

   AND IT IS TAKEN AWAY ON THE SCRUB, not left riding the viewport: intro.js's
   hero timeline fades it out over the first 26% of the hero's scroll, which
   is a pure function of progress and therefore exact in reverse. A fixed
   element with no exit would sit over the venn and the work grid for the rest
   of the page. */
/* Fixed at bottom:0 ON PURPOSE: the corners ride the toolbar like a native
   bottom bar, so they are always at the visible bottom edge — with the
   toolbar open and after it retracts. Everything else in the hero is sized
   to the large viewport and does not move. */
html.intro-live .hero-meta{position:fixed;bottom:0}

.hero-copy{
  width:100%;   /* 7 Sep: the split title spans the whole measure; a shrink-to-fit flex item would cap it at the words */
  display:flex;flex-direction:column;align-items:center;
  /* Was one 30–46px gap when .hero-copy held two children. It holds THREE now
     — the `Salam / Hello` overline, the headline, the CTA — and a single gap
     applies to both joints, which put 46px between a 10.5px mono label and the
     headline it belongs to. That reads as two unrelated blocks. The row gap is
     now the BROW's gap (14–20px, so the label sits on the headline), and the
     CTA buys its old separation back explicitly below. */
  gap:clamp(14px,1.5vw,20px);
}
/* 20 + 26 = the 46px the single gap used to give the button at full size, so
   the change above moves the brow and nothing else. */
.hero-copy .hero-cta{margin-top:clamp(16px,2.1vw,26px)}

/* ── THE HEADLINE'S SIZE ──
   base.css sizes every h1 at clamp(3rem,6vw,5.6rem) — 48 → 89.6px. That was
   drawn for the old five-word statement. The headline is now a full sentence
   in three authored .line spans, the longest of which is 37 characters, and at
   89.6px that sentence is a wall: ~1420px of ink against 1248px of usable wrap,
   so it wrapped again inside its own lines and arrived as five or six.

   Overridden here rather than in base.css, which this agent does not own and
   which is right for every other h1 on the site. clamp(1.7rem,4.3vw,3.5rem)
   caps at 56px — 37.5% down — which puts the longest line at ~890px, comfortably
   inside the wrap at every width above ~1000px and still display-scale type.
   The 1.7rem floor is the phone: 6vw of a 375px screen is 22px of headline,
   which is body copy, so the floor holds it at 27px and lets the sentence wrap.

   max-width is in ch so it tracks the size instead of fighting it. The longest
   line measures ~26.5ch in Inter 300 at −0.04em (37 characters at ~0.51em of
   lowercase advance, less 1.48em of tracking, over a 0.6em `0`); 30ch leaves
   ~13% of slack for fallback metrics before a fourth line falls out, and is
   still narrow enough that the sentence would break to three-ish lines on its
   own if the authored spans ever went away. 19ch — the old value, tuned to a
   short statement — forces this one to six lines at any size. ── */
.hero-title{
  font-size:clamp(1.4rem,3vw,2.75rem);   /* 7 Sep: up from clamp(1.15rem,2.7vw,2.25rem) — each side is 13 glyphs at most now, so there is room */
  max-width:none;width:100%;
  /* ── SPLIT ACROSS THE WIDTH (7 Sep, Haseeb) ──
     Three columns: left group, an empty centre reserved for the glass cube
     (glass.js mountHero sizes it 150px desktop / 120px phones; --hero-gap is
     that plus breathing room), right group. minmax(0,1fr) on the sides so a
     slot's width tween can never grow its column and nudge the other side. */
  /* 7 Sep, HIS ORDER: two rows of three cells. Row 1: A · slot · of. Row 2:
     slot · & · slot. The cube sits BETWEEN the rows, at the block's centre, so
     the row gap is the cube's diameter plus air (150px desktop / 120px phones
     in glass.js mountHero). Side columns are minmax(0,1fr): a slot's width
     tween can widen a word but never move the flush edges. */
  --hero-cube:150px;--hero-gap-y:calc(var(--hero-cube) + 56px);
  display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);column-gap:24px;row-gap:var(--hero-gap-y);align-items:center;
  /* 4 Sep: uppercase mono, letter-spaced — the subhead voice at hero scale */
  font-family:var(--mono);font-weight:400;text-transform:uppercase;letter-spacing:0.08em;line-height:1.12;
}
.hero-title .hero-row{display:contents}
.hero-title .hc-l{grid-column:1;justify-self:start;text-align:left}
.hero-title .hc-c{grid-column:2;justify-self:center;text-align:center}
.hero-title .hc-r{grid-column:3;justify-self:end;text-align:right}
/* nowrap at every width: the type is sized to fit the longest word in its
   column, so the block's height is a constant — two lines a side on desktop,
   one word a row on phones — and the rolling slots never reflow it. */
.hero-title .line{display:block;white-space:nowrap}
/* word masks: each word is an inline-block window; the inner slides within it */
/* clip-path as well as overflow: Safari does not clip a composited
   (will-change:transform) child with overflow:hidden alone — on the phone the
   outgoing word rolled up over the line above and the incoming one rose from
   below, both fully visible (6 Sep). inset(0) clips at the padding box, so the
   .06em descender room is kept. */
.hero-title .hw{display:inline-block;overflow:hidden;vertical-align:bottom;padding-bottom:.06em;margin-bottom:-.06em;
  -webkit-clip-path:inset(0);clip-path:inset(0);isolation:isolate}
.hero-title .hw-i{display:inline-block;will-change:transform}
.hero-title .hw-slot{position:relative;white-space:nowrap}
.hero-title .hw-slot .hw-i.is-out{position:absolute;left:0;top:0}


/* ═══════════════════════════════════════════════════════════════════════════
   4 · THE STAGE — the pearl, the aurora, and the runway
   ═══════════════════════════════════════════════════════════════════════ */

html.intro-live #hero{
  /* ~85vh of runway. #hero has no content in flow once .hero-inner goes fixed,
     so its height IS this padding: every scroll pixel the hero's scrub gets is
     bought here deliberately.

     WHAT IT BUYS NOW: the statement letting go (0.00–0.34 of the scrub) and the
     aurora fading to night across the whole of it, plus the boundary at the end
     where the stage parks and the header goes away. It used to buy THE DROP as
     well — the pearl falling ~190px from the air above the headline to the
     viewport centre — and 85vh was chosen against that distance, about five
     parts scroll to one part travel.

     LEFT AT 85vh WITH THE DROP GONE, on purpose. The number now only decides
     how long the statement's exit and the aurora's fade are given, and both
     read well over roughly one screen; shortening it would also move the scroll
     position of `bottom top`, which is exactly where #constellation's sticky
     pins — retiming the venn to tidy up a runway nobody is watching.

     The scrub runs `top top → bottom top`, so it ends at the exact scroll
     position where #constellation's sticky pins.

     Declared twice, vh then lvh — "fix the whole iOS toolbar issue with
     vh / svh / dvh", and later, "prevent the screen resizing on mobile —
     open for whichever [viewport height] is largest". This height IS #hero's
     scrub AND the exact handoff point to #constellation (see above); a
     runway that can shift mid-scroll as Mobile Safari's toolbar animates
     would retime the glass-to-pearl handoff along with it. See
     constellation.css's THE RUNWAY for the full reasoning — same fix, same
     cascade trick (vh first as the pre-15.4 fallback, lvh silently winning
     wherever it's understood). */
  /* 9 Sep 2026 — 85lvh → 145lvh. "slow down the scroll/scrub for when lava
     becomes the beautiful shape", "should hold as beautiful shape … a lot
     longer", "make 'the fall' … a little longer". Every one of those is a
     LENGTH, and the hero's runway is where lengths are bought: lava-hero.js's
     bands are fractions of THIS box, so widening it slows all of them at once
     without redistributing anything. #constellation's sticky simply pins
     ~60vh lower down the document; nothing in the venn is timed off the
     absolute position, only off its own runway. */
  padding-bottom:calc(clamp(48px,8vh,96px) + 145vh);
  padding-bottom:calc(clamp(48px,8lvh,96px) + 145lvh);
}
html.intro-live .hero-stage{position:fixed;top:0;left:0;right:0;height:calc(var(--vh,1vh) * 100)}

/* Everything on the stage is centred on ONE point, and it has to be the SAME
   point .rz-word uses (`top:50%` of its fixed parent) — the pearl is the full
   stop in `RZV.` and is flown onto a seat measured out of that row, so a few px
   of vertical disagreement is a visibly crooked
   logo. Live, the stage is fixed and inset:0, so `top:50%` is the centre of the
   layout viewport and the two agree by construction. The 50lvh default is for
   the static fallback only, where the stage is absolute inside a hero taller
   than the screen. (Not interchangeable on a phone: with the URL bar still
   expanded — first paint, before any scroll — 50lvh sits a little BELOW the
   visual centre; it lands exact the moment the bar retracts. `50svh` had the
   opposite trade, sitting ~30px above centre once retracted — kept as `lvh`
   now for the same reason as every other viewport-height rule in the
   codebase: "prevent the screen resizing on mobile — open for whichever
   [viewport height] is largest".) */
.hx-c{position:absolute;left:50%;top:calc(var(--vh,1vh) * 50)}
html.intro-live .hx-c{top:50%}

/* ── the aurora ──
   A HOST, not the aurora itself: RZV.aurora() mounts base.css's six-blob
   .aurora inside it and drives the idle drift, and this wrapper carries the
   opacity intro.js animates: up on the burst, down to AURORA_REST as the film
   settles, and then all the way to zero across the hero's scrub, so the venn
   pins onto night. Two elements on purpose — base.css puts a .55s
   opacity transition on .aurora, and GSAP writing opacity every frame onto an
   element with a CSS transition on that same property smears each frame into
   the next (trap 2).

   The burst is a circular clip opening from the pearl's centre, written inline
   by intro.js and removed when it completes. Everything behind it is the page's
   own black, so the reveal needs no second layer to be revealed FROM.

   DELIBERATELY NOT base.css's `.au` host, which is otherwise the same box. `.au`
   carries `opacity:0; transition:opacity 1.2s` and reveals via `.on` — a CSS
   transition on the exact property this film animates frame by frame, which
   would smear every frame into the next (trap 2). `.au` is right for a host
   that fades in once and stays; this one is choreographed.

   ── THE BAND: THE AURORA ONLY LIVES AT THE TOP ──
   The glow is a band across the upper screen that falls off to nothing well
   before the fold, not a wash over the whole viewport. It is done with a MASK
   and the box is left full-size, which is the whole point of the choice:
   base.css's six blobs are sized and placed in PERCENTAGES of their own box
   (`.aurora{inset:-22%}`, `.ab.a1{width:124%;left:-41%}` and so on) and
   RZV.aurora() drives the idle drift in percentages too. Shrink the host to
   `height:60%` and every blob and every stride of drift shrinks with it — the
   field stops moving at the amplitude Haseeb signed off on last night ("needs
   to feel 100% like when i open the portal chat"). A mask changes only where
   the layer PAINTS; the motion inside it is untouched.

   The cost is a mask group per frame on a layer whose children move.
   constellation.js weighed exactly this and chose per-element opacity over a
   mask — but it had EIGHTEEN ether elements to mask. There is ONE layer here,
   already promoted, already grouped (`.aurora` carries a filter, so the
   mix-blend-mode:screen children are isolated into their own stacking context
   before the mask ever sees them — which is also why the mask cannot break the
   blending), and the mask itself never changes. One static mask on one
   composited layer is a rasterise-once cost, not a per-frame one.

   THE NUMBERS, against what sits underneath. Full strength to 22%, half at
   41%, gone by 60% — of the host, which live is exactly the viewport. The
   ramp is a SMOOTHSTEP written out as stops (the same 1−k²(3−2k) curve the
   scroll fade in syncHero uses), not a two-stop linear gradient: a linear ramp
   has a derivative kink at the top stop that reads as a faint horizontal seam,
   and the statement block sits centred — the seam would land across the type.
   Smoothstep is flat at both ends, so the band has no edge anywhere. 22% of the
   viewport is above the brow at every size, so the statement sits in the
   falloff rather than at the band's lip; 60% is below the CTA and a long way
   above the fold, which is what stops this being a full-screen wash again.
   (Both still hold with the reserved pearl gap removed: the statement block
   moved UP by half that padding, i.e. further into the falloff, not into the
   full-strength stop — at a 900px window 22% is 198px and the brow sits around
   360px.)

   mask-mode is `match-source`, and for a gradient that means ALPHA — hence
   black-with-alpha stops, not the white/black of a luminance mask. Same for
   the -webkit- pair, which Safari still needs. no-repeat, because `.aurora`
   overflows the host by 22% a side and repeat would tile the band into that
   overflow; off-screen today, but a lie waiting for a layout change. */
.hx-aurora{
  position:absolute;inset:0;z-index:0;pointer-events:none;opacity:0;
  -webkit-mask-image:linear-gradient(to bottom,
    rgba(0,0,0,1) 0%, rgba(0,0,0,1) 22%,
    rgba(0,0,0,0.95) 27%, rgba(0,0,0,0.83) 32%, rgba(0,0,0,0.50) 41%,
    rgba(0,0,0,0.17) 50%, rgba(0,0,0,0.05) 55%, rgba(0,0,0,0) 60%);
  mask-image:linear-gradient(to bottom,
    rgba(0,0,0,1) 0%, rgba(0,0,0,1) 22%,
    rgba(0,0,0,0.95) 27%, rgba(0,0,0,0.83) 32%, rgba(0,0,0,0.50) 41%,
    rgba(0,0,0,0.17) 50%, rgba(0,0,0,0.05) 55%, rgba(0,0,0,0) 60%);
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-size:100% 100%;mask-size:100% 100%;
}

.hx-ring{overflow:visible;opacity:0;pointer-events:none;will-change:transform,opacity}
/* stroke-dasharray "0.5 13" + round caps = actual round dots, not dashes.
   var(--text) tracks the phase, so the rings are correct on black and white. */
.hx-ring circle{
  fill:none;stroke:var(--text);stroke-width:2;stroke-linecap:round;
  stroke-dasharray:0.5 13;opacity:0.30;
}

/* ── the three dots ── (behind the PLANETS flag in intro.js — see there)
   MONOCHROME. White on the night, black on the white, and nothing in between:
   the pearl is the ONE object on this site allowed to be iridescent, and the
   dots reading as flat 2D discs beside it is what makes that read as material
   rather than as decoration. */
.hx-planet{
  width:12px;height:12px;margin:-6px 0 0 -6px;border-radius:50%;
  background:#fff;opacity:0;pointer-events:none;
  will-change:transform,opacity;
}
/* <b> is the dot's glow — a separate, larger, blurred layer rather than a
   box-shadow, because the glow has to LEAVE on the same scrub that the ink
   arrives on, and a box-shadow cannot be faded independently of its element. */
.hx-planet > b{
  position:absolute;inset:-150%;border-radius:50%;filter:blur(3px);pointer-events:none;
  background:radial-gradient(circle,rgba(255,255,255,0.45),rgba(255,255,255,0.12) 42%,transparent 72%);
}
/* <i> is the ink for that dormant path. --ink IS body.phase-light's --text,
   stated once, here, and deliberately not var(--text) or var(--bg): both flip
   with the phase, and on the ⋯ path this crossfade ran well before the flip —
   a phase-tracking token would have put white ink on a white ground. Same
   reasoning as .hx-planet's white base: the values that must survive a phase
   change unchanged are the ones that stay literal. */
.hero-stage{--ink:#0A080A}
.hx-planet > i{position:absolute;inset:0;border-radius:50%;background:var(--ink);opacity:0}

/* ── THE PEARL ──
   ONE element, for the whole film and the whole hero. Its --pearl-size is
   written rather than its scale, so base.css's inset highlights, the iridescent
   band and the 1px border stay correct at every size it takes: a 25px pearl
   scaled is a smear, and the pearl is the one thing on this site that has to
   read as a material.

   ONE element, for the whole film — AND ONLY FOR THE FILM. It ARRIVES at 115px
   at the stage's centre and CONTRACTS to 25, seats itself as the full stop in
   `RZV.`, flies into the brand slot at 9px, and is then held at opacity 0 for
   the rest of the page: the composed hero has no pearl in it. (The element is
   kept rather than removed because it is still a live tween target on the
   frame it fades out.)

   IT SCALES DOWN INTO FRAME, SPEC §1's first line, and it still is not a moon.
   115px is 4.6× the pea and a third of what the old moon was; the gesture is
   the CHANGE, not the absolute size. There is no FACE either: two eyes existed
   to give a 340px moon presence while it contracted, and at 115px — let alone
   25 — they were never legible. What gives the arrival presence instead is the
   halo, which tracks the pearl's own size and therefore arrives as a ~550px
   bloom and contracts with it. Light condensing to a point is a birth; a disc
   getting smaller on its own is a shrink.

   Centred with xPercent/yPercent from GSAP rather than margins, because its
   width changes as it flies into the header, and a fixed margin would walk it
   off centre as it shrank. */
/* THE HALO IS OFF, THE TRACER STAYS. 2 Sep: "just kill the glow." The
   blurred halo layer never paints, and the square's own bloom alpha is 0 —
   a flat rounded square drawing the line, nothing glowing around it. */
.hx-pearlglow{display:none !important}
.hx-pearl{
  --pearl-glow:0;opacity:0;will-change:transform,opacity;
  /* THE ROUNDED SQUARE. The tracer is the logo's own full stop, not a disc:
     corner radius ~26% of the side (7.16 of 27.5 in the file); the 45° is in
     intro.js's one transform write. Same material as .pearl otherwise. */
  border-radius:26%;
  /* ⚠️ TRANSITION OFF, AND IT IS LOAD-BEARING. base.css's .pearl carries
     `transition:filter .3s var(--ease), box-shadow .4s var(--ease)` — correct
     for the header's pearl, which changes state at UI pace. This one changes
     --pearl-size EVERY FRAME on two beats (115 → 25 across the birth, 25 → 9
     across the flight), and every one of those frames recomputes the three
     box-shadows that are derived from it. With the transition live, each frame
     starts a fresh 0.4s interpolation, so the bloom lags the disc by up to
     0.4s: a large soft shadow trailing a small pearl. This is trap 2's own
     logic applied to a property GSAP does not own — a CSS transition on a
     property something else is driving per frame smears every frame into the
     next. Same specificity as base.css's rule (0,1,0); this file loads after
     it, so it wins. */
  transition:none;
}

/* The pearl's halo. DELIBERATELY NOT MASKED by the aurora's band, and it is a
   sibling of .hx-aurora rather than a child so the question does not even
   arise: it belongs to the OBJECT and travels with it, and the object crosses
   the whole width of the screen through a band that has already fallen to zero
   at the edges. Masking it would put the pearl out mid-sweep. It stays at
   GLOW_REST 0.85 rather than coming down with the aurora for the same reason —
   with the ambient field confined to the top, this is most of the light around
   a pearl that is moving.

   (It no longer has to survive a DROP to the viewport centre: there is no drop
   and no resting hero pearl. THE HALO'S LAST ACT IS THE SETTLE — it rides the
   whole flight, shrinking with the pearl to ~43px, gathers briefly on the
   brand slot's pixel and gives its light up over 0.62s. That settle is SPEC
   §1's trigger for the whole hero cascade, so it has to be there to be seen;
   it used to be stripped off across the flight's first 45%, which left the
   landing with nothing to settle. The end state is unchanged either way: what
   sits in the brand slot is a 9px logo pearl, and a logo does not glow.)

   A separate layer rather than a tweened --pearl-glow,
   because that custom property feeds three box-shadows: animating it repaints
   the pearl every frame, while scaling and fading a pre-rasterised halo is two
   compositor properties. The loop still gives it a slow swell of its own while
   it travels — the one term in that loop that was never gated on the pearl
   being seated, so it outlived the lean and the breath that were. */
.hx-pearlglow{
  width:120px;height:120px;margin:-60px 0 0 -60px;border-radius:50%;
  opacity:0;pointer-events:none;filter:blur(6px);
  background:radial-gradient(circle,
    rgba(255,255,255,0.28) 0%,
    rgba(129,140,248,0.30) 22%,
    rgba(167,139,250,0.14) 44%,
    rgba(34,211,238,0.05) 66%,
    rgba(34,211,238,0) 80%);
  will-change:transform,opacity;
}

/* ── AND WHEN THE FILM IS OVER, ITS LAYERS GO ─────────────────────────────
   intro.js's filmEnd() adds .hx-settled to .hero-stage — one class, on the
   frame the hero has finished composing, taken off again by buildLive() and
   destroy() so a motion switch mid-page gets a clean stage back.

   WHAT IT RETIRES, and neither is a look:

   · will-change on .hx-pearl, .hx-pearlglow and the glyphs. All three animate
     transform/opacity for the film's ~5s and are then at opacity 0 for the
     entire rest of the page — but a live `will-change` is a promise the
     compositor keeps, so three layers stay resident, on a phone, for as long
     as the tab is open, for objects that will never move again. Promoted for
     the film, released after it, which is the whole contract of the property.
   · .hx-pearl's `animation:iriDrift 7s infinite` — base.css parameterises the
     pearl's iridescence as a background-position walk, which is a REPAINT, not
     a composited property, and `infinite` means exactly that. Correct on the
     header's 9px dot, which is on screen; on a stage pearl that has already
     flown away it is a repaint loop on an invisible element with no end.

   Nothing here changes a pixel of the composed hero: every one of these
   elements is at opacity 0 by the time the class lands. It is deliberately NOT
   applied on the PLANETS path (intro.js gates it), where the loop keeps
   driving this furniture after the film. ── */
.hero-stage.hx-settled .hx-pearl,
.hero-stage.hx-settled .hx-pearlglow{will-change:auto}
.hero-stage.hx-settled .hx-pearl{animation:none}

/* THERE IS NO FLOOD AND NO WHITE VEIL HERE ANY MORE. The hero used to end by
   growing the pearl to 130× and washing the screen white at phase('light');
   that moment moved — it now happens after the venn collapses, and the
   constellation owns it. This section is dark from its first frame to its last,
   and the pearl's last act here is to FLY INTO THE HEADER. Nothing of ours
   reaches the viewport centre, so the venn grows its own node rather than
   inheriting one. If you are looking for the takeover, it is not in this
   file. */


/* ═══════════════════════════════════════════════════════════════════════════
   5 · REDUCED MOTION / MOTION OFF — freeze, never remove (trap 3)

   With motion off intro.js builds the aurora — still, but built — and stops
   there: no film, no wordmark, no pearl, no header veil, no header hide. The
   statement is index.html's own markup, centred by base.css, and is never
   touched on this path, so the hero is composed and readable from CSS alone.

   The .hx-pearl / .hx-pearlglow rules below are the trap-3 freeze for the
   ORDERING WINDOW ONLY: html.rzv-no-motion is set by nav.js on the same event
   intro.js tears the stage down on, and whichever runs first, nothing may be
   left travelling with a compositor layer in the meantime. A moment later
   there is no pearl to match.

   The top-only band needs nothing here and deliberately gets nothing: it is a
   mask on .hx-aurora, which is the same element on both paths, so motion-off
   gets the same band rather than the full-screen wash everyone else lost. Its
   stops are percentages of the host, and on this path the host is
   `.hero-stage` inside a `min-height:100lvh` #hero rather than the fixed
   viewport — near enough the same box that the band lands in the same place,
   and it tracks the box either way instead of assuming one.
   ═══════════════════════════════════════════════════════════════════════ */
@media(prefers-reduced-motion:reduce){
  .hx-pearl{animation:none}
  .hx-ring,.hx-planet,.hx-pearl,.hx-pearlglow{will-change:auto}
}
html.rzv-no-motion .hx-pearl{animation:none}
html.rzv-no-motion .hx-ring,
html.rzv-no-motion .hx-planet,
html.rzv-no-motion .hx-pearl,
html.rzv-no-motion .hx-pearlglow{will-change:auto}


/* ═══════════════════════════════════════════════════════════════════════════
   6 · MOBILE (≤760px)
   A shorter runway, and a smaller halo. (No `--hero-pearl-gap` override here
   any more: the reserved air went with the hero's pearl, at every width. No
   `.hx-face` rule either — the face is gone from every width; see THE PEARL.)
   ═══════════════════════════════════════════════════════════════════════ */
@media(max-width:760px){
  html.intro-live #hero{
    padding-bottom:calc(clamp(48px,8vh,96px) + 120vh);
    padding-bottom:calc(clamp(48px,8lvh,96px) + 120lvh);
  }
  .hx-planet{width:10px;height:10px;margin:-5px 0 0 -5px}
  .hx-ring circle{stroke-dasharray:0.5 10;stroke-width:1.6}
  .hx-pearlglow{width:96px;height:96px;margin:-48px 0 0 -48px}
}

/* The frame that carries the mark, halo and tracer together (intro.js). Full
   size of the stage so every child keeps its stage-relative position; the
   draw-on scale is written to it by GSAP (TRAP 2). */
.hx-frame{position:absolute;inset:0;pointer-events:none;will-change:transform}

/* PHONES (7 Sep): the same split, a bigger cube. The centre column is the
   120px cube plus 8px a side; each word takes its own row (the .line becomes a
   column flexbox, so "A / CONVERGENCE / OF" left and "INTELLIGENCE / & /
   IMAGINATION" right) and the type is sized off the side column: the widest
   word is INTELLIGENCE, 12 mono glyphs ≈ 7.6em at this tracking, and the side
   is (100vw − 16px gutters − 136px gap) / 2, so font = side / 15.8·2 with a
   little slack. 375px → ~14px; 430px → ~17.5px. */
@media(max-width:760px){
  /* Phones: three columns must share (100vw − 16px) with two 12px gaps. Worst
     row is INTELLIGENCE · & · IMAGINATION = 12 + 1 + 11 glyphs ≈ 24 × 0.64em
     + 2 gaps, so font ≈ (100vw − 40px) / 16. 375px → ~14px; 430px → ~15.6px. */
  .hero-title{--hero-cube:120px;--hero-gap-y:calc(var(--hero-cube) + 40px);column-gap:12px;font-size:clamp(0.75rem,calc((100vw - 40px) / 16),1.15rem);letter-spacing:0.02em;line-height:1.15}
  html.intro-live .hero-inner{width:100vw;padding:0 8px}
  .hero-inner.wrap{padding-left:8px;padding-right:8px}
  /* the −30px phone lift is retired with the two-row layout: the cube sits
     between the rows, so the block must be centred on it exactly. */
}

/* Phones: the hero text block sits 50px higher (4 Sep). Live it is the fixed
   frame's top; on the static path it is a margin, so no transform (TRAP 2). */
@media(max-width:760px){
}

/* ═══ 8 Sep 2026, Haseeb: "Go back to regular text style in hero but with words
   still rotating." The three-column split around the cube is retired with the
   cube (the lava roams the words now): the title is a centred two-line display
   headline in the site's sentence-case light weight. The cells and slots stay
   (hero-words.js rotates them, intro.js converges them on scroll). ═══ */
.hero-title{
  display:block;text-align:center;
  font-family:var(--sans,'Inter',system-ui,sans-serif);font-weight:300;text-transform:none;
  letter-spacing:-0.03em;line-height:1.08;
  font-size:var(--fs-hero);
  max-width:22ch;margin-inline:auto;
}
.hero-title .hero-row{display:block}
.hero-title .hc{display:inline;text-align:center}
.hero-title .hc + .hc{margin-left:.28em}
@media (max-width:760px){
  .hero-title{font-size:var(--fs-hero-m);letter-spacing:-0.02em;line-height:1.1;max-width:16ch}
  .hero-title .hc + .hc{margin-left:.24em}
}

/* ═══ EXPERIMENT, 9 Sep 2026, Haseeb: "see how homepage looks on desktop with hero
   text situation on one line max only." Desktop only: the two rows run as ONE line —
   the rows become inline, nothing wraps, and the size is bound to the viewport so
   the whole sentence always fits. Phones keep the two-line headline above.
   To retire the experiment, delete this block. ═══ */
@media (min-width:761px){
  .hero-title{max-width:none;white-space:nowrap;
    /* 9 Sep: larger — the line is bound to the viewport, so it still never wraps. */
    font-size:clamp(1.45rem,3.55vw,4.6rem);letter-spacing:-0.045em;line-height:1.02}
  .hero-title .hero-row{display:inline}
  .hero-title .hero-row + .hero-row{margin-left:.34em}
  .hero-title .hw,.hero-title .hw-slot{white-space:nowrap}
}
