/* ═══════════════════════════════════════════════════════════════════════════
   rzv.studio — constellation.css   (CONSTELLATION agent · owns #constellation)

   THREE RINGS, SIX DISCIPLINES — Haseeb's Figma frame RZV-STUDIO node 7-133,
   and the Lottie he built of the same idea. THREE large thin-stroke circles
   overlap venn/trefoil-style around the middle, and the six discipline names
   are LABELS distributed across them, two to a ring — a ring is the visual
   unit, a name is the data unit. Three circles make seven regions; the middle
   one holds the pearl and the remaining six take a name each, which is almost
   certainly why he drew three for six. Each name sits small in tracked mono at
   the AREA CENTROID of its own region, solved per resize in constellation.js —
   never on a rim, because a rim belongs to two regions and the name would stop
   saying which one it names. There is no other text in this section, and
   nothing to click open — verbatim from him: "no other texts or modals on this
   section".

   NOT INVERTED FROM THE FIGMA — his frame is white-on-black and so is this.
   The hero no longer floods the screen; it drops the pearl into a page that
   stays night for the whole of the venn, so the strokes and the labels are
   white ink throughout and the tokens below never flip. The white arrives
   LATER and as an object rather than as a ground: the centre pearl grows until
   it is the screen, and the venn rides it in mix-blend-mode:difference, which
   is what turns white strokes dark for that stretch without a single token
   swap. That is also what the exit choreography collapses back into — the
   phase cut under the pearl, then the implosion.

   THE CENTRE IS THE EXACT VIEWPORT CENTRE. Not a preference: it is the pixel
   intro.js hands its fused dot to, and the pixel the collapse lands back on.
   Hence .cn-space is inset:0 on a stage that is sticky at top:0 and 100svh
   tall; the header clearance comes out of the CIRCLE RADII in layout(), never
   out of the centre.

   Everything below is either
     (a) gated on html.constellation-live — the class constellation.js adds via
         RZV.claim() only once the stage is actually built, or
     (b) scoped to .cn-* elements that only exist because constellation.js made
         them.
   So a stubbed, bailed or crashed agent leaves the semantic .cn-fallback
   pillar list readable, which is the whole point of it.

   base.css TRAP 1 IS LIVE HERE, not moot — two elements carry
   mix-blend-mode:difference, the stage and the ether layer, and iOS Safari
   paints a solid black box if any ANCESTOR of a blended element carries
   backdrop-filter. Discharged by inspection rather than by hope: the only
   backdrop-filter rules in the project are on the header and the glass
   surfaces, and neither is an ancestor of html / body / #constellation /
   .wrap / .constellation-stage. Re-check it before adding a blur anywhere in
   that chain. TRAP 2 stands — transform is ABSENT from every transition list
   on the rings, the labels, the pearls, the veil and the flyer, because the
   rAF writes those every frame and a CSS transition on top smears each frame
   into the next.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Section-local knobs ────────────────────────────────────────────────
   WHITE ON DARK is now the resting state — his Figma exactly. The hero no
   longer floods the screen; it drops the pearl into this section and the page
   stays night the whole way through the venn. The light-phase values below are
   the inversion, and they only ever apply during the statement hold, by which
   point the rings have already folded away. ── */
#constellation{
  --cn-ink:#FAFAFA;              /* labels */
  --cn-ring:rgba(250,250,250,0.58);
  --cn-sub:rgba(250,250,250,0.62);
}
/* THERE IS NO LIGHT-PHASE INVERSION ANY MORE, and its absence is the point.
   The page does cut to light in this section — but it cuts WHILE THE VENN IS
   STILL ON SCREEN, sitting on the white pearl in mix-blend-mode:difference.
   Difference is what inverts it: white strokes over white render dark, and the
   pearl's iridescent drift tints them as it moves. Flipping the tokens as well
   would invert it twice and hand back a white venn on white. So the ink stays
   white at every phase and the blend does the work. */

/* ═══ THE RUNWAY ════════════════════════════════════════════════════════
   1000vh of section, a 100svh sticky stage inside it: 900vh of scroll travel,
   and the whole assembly is SCRUBBED across it — three rings popping out of the
   pearl staggered, then the six names letter by letter, then rotation advancing
   with scroll on top of a slow idle for most of the runway, then the exit. The
   runway is not decoration, it is the timeline. Native position:sticky, not a
   ScrollTrigger pin: no pin-spacer to reconcile with the view transitions. ═══ */
html.constellation-live #constellation{
  min-height:1000vh;padding-block:0;
}
html.constellation-live #constellation > .wrap{
  min-height:1000vh;padding-top:0;padding-bottom:0;
  /* ── THE STACKING FIX, and the reason the statement was invisible ──
     base.css gives EVERY .wrap `position:relative; z-index:1`, which makes it
     a stacking context. Everything this section paints lives inside it, so a
     z-index of 521 on the statement was resolved INSIDE a context sitting at 1
     in the root — while the veil, parented to <body>, sat at 520 in the root
     itself. A root-level 520 covers an entire subtree whose context is 1, so
     the statement rendered UNDER the white pearl the whole time. Its opacity
     and its line wrappers were never the problem; it was buried.

     Raising this one wrap above the header's 500 fixes it at the source: the
     veil moves inside here too, so veil, venn and statement are now ordered
     against each other in ONE context and the numbers below mean what they
     say. Scoped to this section and gated on constellation-live — base.css's
     global .wrap is untouched.

     It is also what makes the blend possible. mix-blend-mode blends with the
     backdrop of the nearest ISOLATED group, and any stacking context isolates.
     With the veil outside this wrap there was no arrangement of z-indexes that
     could put the two in the same group. */
  z-index:530;
}
/* THERE IS NO .section-head IN THIS SECTION ANY MORE. "what i do brow - kill
   this", verbatim: the overline is out of index.html, so the two rules that
   used to live here — the sticky zero-height placement, and the cn-on-pearl
   fade that got it out of the way before the difference blend turned on — have
   nothing left to select and are deleted rather than left dangling. If an
   overline ever comes back it needs both halves again, not just the first: an
   element painted UNDER the venn is an element a swelling ring differences
   against. */
/* base.css gives .constellation-stage a plain relative box; the live rules
   take it over here.

   ARMED: hidden until the section pins. Before that the sticky stage is still
   travelling up with the page, so its centre is NOT the viewport centre — and
   a stray black dot drifting up the screen while intro.js fuses its own at
   dead centre would give the whole trick away. visibility, not display: the
   labels have to stay measurable for layout(). */
html.constellation-live .constellation-stage{
  position:sticky;top:0;height:100vh;height:100svh;min-height:0;
  overflow:visible;min-width:0;
  z-index:12;
}

/* ═══ THE ARMING GATE ═══════════════════════════════════════════════════
   NOTHING THIS SECTION OWNS MAY PAINT UNTIL THE SECTION HAS PINNED.

   This used to be one rule on the stage, and that was fine while everything
   lived inside the stage. It stopped being fine the moment the core pearl, the
   veil, the black pearl and the ether were moved out into the wrap to keep
   them clear of the difference blend: they left the gate behind. .cn-core is
   position:fixed at the viewport centre inside a wrap raised to z 530 — above
   the header, above the hero — so from the first frame of the page it painted
   a second white pearl next to the hero's own. Exactly what he saw.

   So the gate is now a property of the SECTION, and every surface is listed
   here rather than carrying its own copy. Two rules about how it is written:

   · HIDDEN IS THE CSS DEFAULT, not something the first frame has to correct.
     A throttled, late or never-delivered first frame cannot flash any of this
     over the hero, because none of it is visible until a class arrives.
   · It gates on VISIBILITY, not opacity. Opacity is the rAF's channel and it
     is mid-animation for most of these; visibility is orthogonal to it, still
     lays out (which layout() depends on for the statement's height), and
     cannot be undone by a stale inline style.

   The per-element .is-live classes stay and compose with this: armed says the
   section owns the screen, is-live says this particular object is currently
   part of the film. Both must be true. ═══ */
html.constellation-live .constellation-stage,
html.constellation-live .cn-veil,
html.constellation-live .cn-ether-layer,
html.constellation-live .cn-core,
html.constellation-live .cn-ink-pearl,
html.constellation-live .cn-statement{
  visibility:hidden;
}
html.constellation-live #constellation.is-armed .constellation-stage,
html.constellation-live #constellation.is-armed .cn-ether-layer,
html.constellation-live #constellation.is-armed .cn-core{
  visibility:visible;
}
html.constellation-live #constellation.is-armed .cn-veil.is-live,
html.constellation-live #constellation.is-armed .cn-ink-pearl.is-live,
html.constellation-live #constellation.is-armed .cn-statement.is-live{
  visibility:visible;
}

/* ── THE VENN ON THE PEARL ────────────────────────────────────────────────
   The blend goes HERE, on the stage, and it has to. position:sticky creates a
   stacking context, so anything inside the stage is isolated from the veil and
   would blend against nothing; the stage itself is the outermost thing that is
   still a sibling of the veil, so it is the only element in the chain whose
   backdrop actually contains the white.

   Toggled by the rAF rather than left on: mix-blend-mode forces this whole
   subtree onto its own compositing layer, and the venn spends most of the
   section over plain dark where it buys nothing. It is switched on the frame
   the pearl starts growing and off again well after the last ring has swelled
   away and faded out — constellation.js's EX.blendFrom/blendTo, both edges
   chosen so nothing visible changes on the frame the class flips.

   No pop when it turns on. Before the veil exists the backdrop inside this
   group is transparent, and difference against a transparent backdrop returns
   the source colour unchanged — a white ring stays a white ring. The core
   pearl is deliberately NOT in here (it is a sibling above), so the handoff to
   the veil is never differenced against the veil it is handing off to.

   base.css TRAP 1: nothing in the ancestor chain — html, body, #constellation,
   .wrap, .constellation-stage — carries backdrop-filter. Verified, and the
   only backdrop-filter rules in the project are on the header and the glass
   surfaces, neither of which is an ancestor of this. */
html.constellation-live #constellation.cn-on-pearl .constellation-stage{
  mix-blend-mode:difference;
}
/* The stage's blend group is .wrap, so the veil (10) AND the word cloud (11)
   are both in its backdrop. That is correct and deliberate: the venn is meant
   to difference against the white, and a name that happens to lie under a ring
   simply contributes its own few percent to what that ring inverts. */

/* ═══ THE FALLBACK ══════════════════════════════════════════════════════
   Moved here from base.css: it gates on html.constellation-live, and that
   class is this file's to add and this file's to answer for. It is the other
   half of the stage's own live rule above — the same class that turns the
   section into a 1000vh runway is what folds this list away, and the section always has exactly one visible version
   of itself: the circles, or the words, never both and never neither.

   The live map renders LABELS ONLY. The descriptions in the markup exist for
   readers, crawlers and the no-JS case and are never drawn. ═══ */
.cn-fallback{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
  gap:clamp(26px,3vw,46px);margin-top:clamp(30px,4vw,56px)}
html.constellation-live .cn-fallback{position:absolute;width:1px;height:1px;padding:0;margin:0;
  overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
.cn-pillar{display:flex;flex-direction:column;gap:12px}
.cn-pillar h3{color:var(--text)}
.cn-subs{display:flex;flex-wrap:wrap;gap:7px;margin-top:4px}
.cn-subs li{font-family:var(--mono);font-size:9.5px;font-weight:700;letter-spacing:0.11em;
  text-transform:uppercase;color:var(--muted);border:1px solid var(--border);
  border-radius:var(--r-pill);padding:6px 12px;background:var(--surface)}

/* ═══ THE PLANE ═════════════════════════════════════════════════════════
   inset:0 — the centre of this box is the centre of the viewport and every
   circle centre is expressed as an offset from it. Flat 2D throughout: the
   rAF computes positions and writes transforms, nothing is perspective. ═══ */
/* NOTHING in here takes a pointer. There is no hover left in the section —
   it is a scroll film — so the whole stage is inert and stays that way. */
.cn-space{position:absolute;inset:0;z-index:1;pointer-events:none}

/* ═══ THE CIRCLES ═══════════════════════════════════════════════════════
   One <g> per discipline carrying translate+scale, one <circle> inside it at a
   fixed radius. Scaling the group rather than animating `r` means ONE attribute
   write per circle per frame and no SVG geometry invalidation.

   non-scaling-stroke IS BACK, and it is safe again. It is the obvious way to
   hold the hairline at 1px through the breathing, and this rule used to carry
   it — until the exit wipe went in. non-scaling-stroke measures a
   stroke-dasharray in the UNSCALED space while pathLength=1 normalises the dash
   against the path's own USER-space length; the two disagree by exactly the
   group's scale, so the dash ran out before the circle closed and every ring
   was drawn with a gap. The workaround was to drop it and have the frame write
   stroke-width as 1/scale instead.

   THE WIPE IS GONE — the exit is now a scale-up and a fade — and with it the
   dasharray and the pathLength it was normalised against. There is no dash on
   this element for a space to be measured in, so the mismatch has nothing to
   arise between and cannot come back without someone re-introducing both. So
   the hairline goes back in the stylesheet where it belongs, and the frame
   writes one property fewer per ring per frame.

   It is also REQUIRED now rather than merely tidier: the assembly scales from
   ZERO, and the old 1/scale write evaluates to Infinity at the bottom of it. ═══ */
.cn-orbits{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none}
.cn-ring{
  fill:none;stroke:var(--cn-ring);stroke-width:1;
  vector-effect:non-scaling-stroke;
}
/* NO TRANSITION ON THE STROKE. There was one — .4s, left over from a hover
   that lit the rings of the name under the pointer and swapped this colour for
   a brighter --cn-ring-hi. Both are gone: nothing in this section changes the
   stroke any more, the exit is carried by the group's scale and an inline
   opacity, and a transition on a property nobody writes is a transition that
   can only ever cost a style recalc. */
/* ── the travellers ── small solid dots running each ring's circumference at
   its own speed. Inside the ring's <g>, so they inherit its breathing and its
   fold; one wrapper per ring so the whole set takes one opacity write.

   Each carries its own ripple: a hairline circle sharing the dot's centre,
   idle at zero and scaled outward by the rAF only while it is emitting. The
   stroke does not scale with it — the ring thins as it grows otherwise, and
   what should read as one hairline leaving the dot reads as a smudge. */
.cn-orbs{opacity:0}
.cn-orb{fill:var(--cn-ink)}
.cn-orb-ripple{
  fill:none;stroke:var(--cn-ink);stroke-width:1;
  vector-effect:non-scaling-stroke;opacity:0;
}

/* ═══ LABELS ════════════════════════════════════════════════════════════
   The house overline voice, tracked wide and small. A name does not sit on a
   circle and it is not placed at an angle: it sits at the AREA CENTROID of its
   venn region, which is as far from every rim as that region allows, and its
   whole position is written by the rAF from numbers solveRegions() solved at
   the last resize. Nothing here places anything. DOM, not SVG <text>: per-glyph
   assembly needs one element per character, and the browser lays out and hints
   DOM text far better at 10px.

   The wrapper is positioned by the rAF; the inner span carries the tracking so
   the two never fight over one transform. ═══ */
.cn-lab{
  position:absolute;left:0;top:0;
  font-family:var(--mono);font-weight:700;
  font-size:clamp(8.5px,0.72vw,10.5px);
  letter-spacing:0.35em;text-transform:uppercase;
  color:var(--cn-ink);white-space:nowrap;
  /* letter-spacing puts a trailing space after the LAST glyph too, so the box
     is 0.35em wider than the ink. A negative margin pulls the box back onto
     the glyphs, which is what makes translate(-50%) actually centre them. */
  margin-right:-0.35em;
}
.cn-char{display:inline-block;will-change:transform,opacity}

/* The sub-service dots are GONE — eighteen beads around the middle read as
   specks, and the names they used to carry float free in the ether now. The
   pulse they owned moved to the nodes that are actually in the drawing: the
   three travellers above, and the pearl below. */

/* ═══ THE ETHER ═════════════════════════════════════════════════════════
   The eighteen names, scattered through the field around and between the
   orbits — not tied to a rim, not tied to their dot. Invisible by default and
   lit only where the cursor is.

   One opacity per name, written by the rAF from its distance to the cursor.
   Not mask-image: moving a mask repaints the layer it sits on every frame, and
   this layer carries eighteen text nodes, where an opacity composites and costs
   nothing. The falloff is smoothstepped across the whole radius so the light
   has no rim, and every name keeps a low ambient floor so the field reads as
   ether you are carrying a lamp through rather than a void.

   Under the discipline labels in paint order, so a lit name never covers a
   ring's own name. ═══ */
/* ── THE LAYER ──
   It lives in the WRAP now, not in the plane, and that move is what lets the
   word cloud outlive the venn. Inside .constellation-stage it would be caught
   by the stage's difference blend and flip to invisible the moment the pearl
   took the screen; and it would die with the stage's own life. Out here it is
   a sibling: z 11, directly over the veil and under everything else, which is
   what "in the background" has to mean structurally.

   ITS OWN DIFFERENCE BLEND, and this is the whole colour answer. The cloud
   spends the first half of its life on the dark page and the second half on
   the white pearl, and difference resolves that per-pixel with no logic at
   all: over nothing (transparent backdrop) a name renders as its own light
   grey, exactly as it does today; over the white veil the same grey inverts to
   a quiet mid-grey. One declaration instead of a token swap, a class toggle
   and a transition — and it cannot fall out of step with the veil, because it
   IS the veil it is reading.

   Left and top and the box size come from layout(), so this sits exactly over
   the stage's own box and every coordinate the frame computes stays valid. */
.cn-ether-layer{position:fixed;pointer-events:none;z-index:11}
html.constellation-live #constellation.cn-on-white .cn-ether-layer{
  mix-blend-mode:difference;
}
.cn-ether{
  position:absolute;left:0;top:0;white-space:nowrap;pointer-events:none;
  font-family:var(--mono);font-size:9.5px;font-weight:700;letter-spacing:0.13em;
  text-transform:uppercase;color:var(--cn-sub);
  opacity:0;will-change:opacity;
}

/* ═══ CENTRE ════════════════════════════════════════════════════════════
   The mark in the triple region all three rings share — and the pixel intro.js
   hands over to, which is why it is dead centre and why it is the PEARL rather
   than an ink dot. 22px, and that size is stated once in constellation.js
   (SEED) and arrives here as --cn-core-size, so the two cannot disagree about
   the handoff. It keeps the pearl material through every phase because it is
   the object that grows into the veil and, at the far end, flies into the
   header. ═══ */
/* THE CORE LIVES IN THE WRAP, NOT IN THE PLANE. Two reasons, both structural:
   it must paint ABOVE the veil so the handoff still reads as one object
   becoming another, and it must stay OUT of the venn's difference blend or the
   pearl would go dark against the very veil it is handing off to. Positioned
   by layout() at the same pixel the veil uses, which is the same pixel the
   plane's centre resolves to. */
html.constellation-live .cn-core{
  position:fixed;left:0;top:0;width:0;height:0;z-index:13;pointer-events:none;
}
/* base.css .pearl, parameterised — the object the hero hands over, continued.
   Not an ink dot any more: the pearl is the one thing on this page that keeps
   its own material through every phase, and at the end of the section it is
   what grows to fill the screen. */
.cn-core-pearl{
  position:absolute;left:0;top:0;
  --pearl-size:var(--cn-core-size,22px);--pearl-glow:0.85;
  /* transform is the rAF's — translate(-50%,-50%) scale(k) (TRAP 2) */
}
/* The pearl's pulse — the same hairline emission the travellers make, sized
   off the pearl rather than off a dot so it leaves the pearl's own edge. Its
   own element, not a ::after on the pearl: base.css owns .pearl's box and its
   pseudo-elements, and this must not reach into them. */
.cn-core-ripple{
  position:absolute;left:0;top:0;
  width:var(--cn-core-size,22px);height:var(--cn-core-size,22px);
  border-radius:50%;border:1px solid var(--cn-ink);
  transform:translate(-50%,-50%);opacity:0;pointer-events:none;
  /* transform is the rAF's (TRAP 2) — nothing may transition it */
}

/* ═══ THE VEIL — the pearl, grown ═══════════════════════════════════════
   Not a lid sliding over the page: the CENTRE PEARL ITSELF expanding until it
   is the screen. It takes over from the core pearl at exactly the same size on
   exactly the same pixel — the identical-objects trick this section opens with
   — grows to cover, holds while the statement reads on it, then contracts back
   down through the same path and keeps going, to 15px, where the flyer
   detaches for the header.

   So it wears the pearl's own material rather than a flat white: the three
   background layers from base.css .pearl, minus its border and box-shadow,
   which are the two things that cannot survive being scaled fifty times. The
   iridescent sweep drifts across it at full screen, which is the whole reason
   this reads as a pearl rather than as a white rectangle.

   IT LIVES INSIDE #constellation > .wrap NOW, not on <body>. That wrap is
   raised to 530 — above the header's 500 — so the takeover still swallows the
   header, and in exchange the veil, the venn and the statement finally order
   against each other inside ONE stacking context. That is what fixes the
   statement, and it is what lets the venn blend with this. ═══ */
.cn-veil{
  position:fixed;left:0;top:0;z-index:10;
  width:var(--cn-base,800px);height:var(--cn-base,800px);
  margin-left:calc(var(--cn-base,800px) * -0.5);margin-top:calc(var(--cn-base,800px) * -0.5);
  border-radius:50%;pointer-events:none;opacity:0;visibility:hidden;
  background:
    radial-gradient(circle at 32% 26%, rgba(255,255,255,0.95), rgba(255,255,255,0) 58%),
    linear-gradient(120deg, rgba(129,140,248,0.34), rgba(244,114,182,0.26) 32%, rgba(34,211,238,0.30) 62%, rgba(167,139,250,0.32) 95%),
    linear-gradient(160deg,#ffffff,#e9ecf6);
  background-size:100% 100%, 240% 240%, 100% 100%;
  animation:iriDrift 7s ease-in-out infinite;
  will-change:transform,opacity;
  /* transform intentionally absent from any transition (TRAP 2) */
}
html.rzv-no-motion .cn-veil{animation:none}

/* ═══ THE BLACK PEARL ═══════════════════════════════════════════════════
   ⚠️ THIS IS A MATERIAL, AND MATERIALS BELONG IN base.css. It lives here only
   because #constellation is its one consumer today. The moment a second
   surface wants a dark pearl, move `.pearl.is-ink` to base.css beside `.pearl`
   verbatim — it is deliberately written as a paint-only modifier with no
   positional or sectional assumptions in it, so the move is a cut and paste.

   A MODIFIER, NOT A COPY. `.pearl` owns the anatomy — the box, the radius, the
   --pearl-size parameterisation, the iriDrift animation — and this overrides
   nothing but the paint. Every offset below is still a ratio of --pearl-size,
   so it is correct at 15px and at 200px and can be animated by SIZE rather
   than by scale when the morph work starts.

   Specificity note: it must beat `body.phase-light .pearl` (0,2,1), because
   the page IS in the light phase for the whole of this pearl's life — that
   rule would otherwise hand it the white pearl's graphite shadow. Prefixing
   with html.constellation-live takes it to (0,3,0), which wins.

   THE RECIPE, layer by layer, against .pearl's own:

   BODY — .pearl closes with linear-gradient(160deg,#ffffff,#e9ecf6): a white
   with a cool shade at the bottom. This is the same move inverted, but NOT
   flat #0A080A: #17141F at the lit shoulder down to #050409 at the base, a
   cool near-black with a violet cast. That two-stop fall is most of what makes
   it read as a sphere rather than a hole.

   IRIDESCENCE — the thing that makes it a black PEARL and not a black ball.
   Same 120deg sweep, same four stops at the same positions as .pearl, same
   240% background-size so iriDrift still walks it left to right. Three of the
   four hues are the site's own; cyan is swapped for emerald, because green is
   the signature overtone of a black pearl and cyan simply disappears on a
   dark body. Alphas are LOWER than .pearl's, not higher: over near-black these
   are additive-looking and a little goes a long way.

   SPECULAR — same position as the white pearl, 32%/26%, because that is what
   makes the two obviously the same object under different light. But tight and
   dim where .pearl's is broad and bright: falls off by 30% instead of 58%, and
   0.58 alpha instead of 0.95. A catchlight, not a sheen.

   RIM / TERMINATOR — a dark sphere is read almost entirely through its edge.
   Two insets do that job: a faint lip at the top under the specular, and a
   REFLECTED-LIGHT rim weighted to the opposite side — offset down-and-right,
   away from the 32%/26% light — which is the single most sphere-making mark
   on any dark ball.

   GROUND — it sits on WHITE, so it needs to sit ON something. Not the white
   pearl's outward bloom inverted, which on white just reads as dirt: a real
   contact shadow, tight and directly under it, plus a wider softer one for the
   ambient occlusion. Both are neutral ink, no hue. ═══ */
html.constellation-live .pearl.is-ink{
  border:1px solid rgba(255,255,255,0.13);
  background:
    radial-gradient(circle at 32% 26%, rgba(255,255,255,0.58), rgba(255,255,255,0) 30%),
    linear-gradient(120deg, rgba(129,140,248,0.26), rgba(244,114,182,0.20) 32%, rgba(52,211,153,0.24) 62%, rgba(167,139,250,0.26) 95%),
    linear-gradient(160deg,#17141F,#050409);
  background-size:100% 100%, 240% 240%, 100% 100%;
  box-shadow:
    /* the lip, under the catchlight */
    inset 0 max(1px, calc(var(--pearl-size) * 0.028)) max(1px, calc(var(--pearl-size) * 0.055)) rgba(255,255,255,0.20),
    /* reflected light on the shadow side — the terminator */
    inset calc(var(--pearl-size) * -0.07) calc(var(--pearl-size) * -0.06) calc(var(--pearl-size) * 0.20) rgba(196,181,253,0.30),
    /* contact shadow: it is standing on the white, not glowing over it */
    0 calc(var(--pearl-size) * 0.10) calc(var(--pearl-size) * 0.24) calc(var(--pearl-size) * -0.09) rgba(10,8,10,0.44),
    0 calc(var(--pearl-size) * 0.30) calc(var(--pearl-size) * 0.62) calc(var(--pearl-size) * -0.24) rgba(10,8,10,0.26);
}

/* ── THE OBJECT ──
   The white pearl is the SCREEN and this is the OBJECT. They arrive together
   at full cover and they leave together at the collapse; in between, this is
   the one thing that persists — the venn's centre node through the blend, then
   the mark above the statement while it reads.

   OUT of .constellation-stage on purpose: a black pearl differenced against
   white inverts to white and vanishes. It is a sibling of the blended stage,
   at z 13 — over the veil and the venn, under the statement.

   ⚠️ MORPH HOOK. Its position and size come from exactly ONE transform, written
   in one place in tick() and read from L.perchDY. Anything that wants to morph,
   travel or deform this object attaches there and nowhere else — do not start a
   second transform path on this element. */
html.constellation-live .cn-ink-pearl{
  position:fixed;left:0;top:0;z-index:14;
  --pearl-size:var(--cn-ink-size,22px);
  pointer-events:none;opacity:0;visibility:hidden;
  /* transform is the rAF's (TRAP 2) — .pearl's transition list already omits
     it, and nothing here may add it back. */
}

/* ═══ THE STATEMENT ═════════════════════════════════════════════════════
   Reads on the white while the pearl holds at full screen. Masked slide-up,
   one line at a time, scrubbed — the reveal belongs to the scroll like
   everything else in this section, so you can stop halfway up a line and it
   waits for you.

   z 15, inside #constellation > .wrap, and the top of this section's stack:
   above the veil (10), the word cloud (11), the venn (12), the core (13) and
   the black pearl (14). Below only the flyer. Dark ink — #0A080A — because by the time it
   shows, the page has cut to phase-light and it is reading on white pearl.
   These numbers only became meaningful when the veil moved into this same
   wrap; see the stacking note on the wrap itself. ═══ */
/* STATIC (no JS, motion off): ordinary flow under the stage, reading as the
   statement it is. Nothing here is pre-hidden — every overlay rule below is
   gated on html.constellation-live, which only exists once this file's JS has
   claimed the section and can therefore be trusted to reveal it. */
.cn-statement{margin-top:clamp(40px,6vw,80px);max-width:24ch}
.cn-statement-copy{
  font-size:clamp(1.5rem,3.2vw,2.6rem);font-weight:300;
  letter-spacing:-0.035em;line-height:1.14;color:var(--text);
}
.cn-statement .line{display:block}
.cn-statement-em{color:var(--muted)}

html.constellation-live .cn-statement{
  position:fixed;left:50%;top:50%;z-index:15;
  transform:translate(-50%,-50%);
  /* ── THE MEASURE ──
     Two corrections live in this one line, and they pulled in opposite
     directions, which is why it has been wrong twice.

     IT WAS min(20ch,86vw), AND THAT WAS A SQUASH. Two faults: 20ch is a
     paragraph measure, not a display measure — against the real font, Inter
     Light, the first sentence is 28.34em wide with the -0.035em tracking on,
     so it broke into three or four short stacked lines — and worse, `ch` HERE
     IS THE WRONG CH, because this element sets no font-size and the display
     size lives on .cn-statement-copy inside it. 20ch resolved against the
     inherited ~16px body size: a ~196px box holding 43px type.

     THEN IT WAS min(1460px,92vw), AND THAT WAS FULL BLEED. "make this text
     centre and not full widh" — 92vw is the whole screen less a hair, so a
     sentence set on it reads as a banner rather than as a held statement, and
     nothing about it looks centred because there is no margin for it to be
     centred within.

     78vw with a 1240px ceiling is the answer to both, and it is still solved
     rather than picked — see the arithmetic on the copy below. Plus
     text-align:center, which is the actual ask: the block was already centred
     as a box, it was the RAGGED LINES INSIDE it that were not. */
  width:min(1240px,78vw);max-width:none;margin:0;
  text-align:center;
  /* ── AIR FOR THE PEARL ──
     Mirrors intro.css's --hero-pearl-gap rather than inventing a second
     mechanism: the black pearl sits above these two lines the way the hero
     pearl sits above the headline, and this padding is what holds the space
     open for it. It is real padding, not a margin on the pearl, for two
     reasons — the whole block stays optically centred as one unit, and
     layout() reads this computed value back to place the pearl, so the gap is
     stated in ONE place and the JS cannot disagree with the CSS about it. */
  --cn-pearl-gap:clamp(38px,5.6vh,78px);
  padding-top:var(--cn-pearl-gap);
  pointer-events:none;opacity:0;visibility:hidden;
}
/* ── SIZED SO EACH SENTENCE IS STILL ONE LINE IN THE NARROWER BOX ──
   The fit test is width-independent while the vw term is live, which is what
   makes it safe at every desktop size rather than at the three anyone checked:
   the box is 0.78·VW and the type is N·VW, so the longest line has to satisfy
   (line in em) × N < 0.78 at ANY viewport width.

   The 2.5vw this carried briefly was solved against the sentence the section
   USED TO ship — 28.343em, measured out of Inter-Light's own hmtx. That
   sentence is gone. The statement is now "Clients choose to work with me
   because I can / take care of things from start to finish.", and the longest
   line of it is 44 characters against the old 65 — about 19.2em on the same
   per-character measure. Sizing the new copy against the old copy's width
   would have bought the centred box by spending a third of the type for
   nothing, so the type goes back up.

   At 2.9vw: 19.2 × 0.029 = 0.557·VW against 0.780 — a 29% margin, wider than
   the old pairing ever had, and there is deliberately room in it because the
   19.2em is arithmetic on advance widths rather than a browser measurement.
   Above ~1655px the type caps at 3.0rem and the box at 1240px, where the line
   needs ~922px: clear by 26%. Below ~700px the floor takes over and each
   sentence wraps to two visual lines, which is the phone answer — and centred,
   they wrap symmetrically rather than ragging right.

   ⚠️ THIS NUMBER IS COUPLED TO THE COPY. If the statement is rewritten longer,
   re-run the arithmetic above rather than assuming it still fits. */
html.constellation-live .cn-statement-copy{
  font-size:clamp(1.3rem,2.9vw,3.0rem);color:#0A080A;
}
html.constellation-live .cn-statement-em{color:rgba(10,8,10,0.52)}
/* Each line becomes its own mask; the inner span the JS wraps is what slides. */
html.constellation-live .cn-statement .line{overflow:hidden}
html.constellation-live .cn-line-in{display:block;will-change:transform}

/* ═══ THE FLYER ═════════════════════════════════════════════════════════
   The implosion stops at 15px; in that same frame the held dot is swapped for
   this one — the real base.css .pearl material at the same size on the same
   pixel — which then sweeps up into the header's brand slot. Two identical
   objects trading places, the same trick the section opened with.

   It stays on <body>, so its z is a ROOT-context number and has to clear the
   raised wrap (530) and the header (500), not the veil's 10.

   NOT ONE-SHOT ANY MORE. The sweep is scrubbed on the exit's own progress and
   reverses when the reader scrolls back up, so this element is built once and
   kept for the mount's life rather than made and destroyed around a tween.
   That is why it now carries the same visibility discipline as the veil and
   the black pearl: hidden and transparent by default, revealed only on the
   frames constellation.js has actually written a position for. There is NO
   arming gate on it and it needs none — it is on <body> and therefore outside
   the section's is-armed subtree, and its opacity is a pure function of scroll
   which is zero everywhere except the last 4% of a pinned #constellation.

   --pearl-size stays 15px, the size the veil hands over at. The JS scales the
   element down to the header pearl's 9px across the flight rather than
   animating this token, so the gradient stack is rasterised once and the sweep
   is a composited transform the whole way. ═══ */
.cn-flyer{
  position:fixed;left:0;top:0;z-index:560;
  --pearl-size:15px;--pearl-glow:0.95;
  pointer-events:none;opacity:0;visibility:hidden;
  will-change:transform,opacity;
  /* transform intentionally absent from any transition (TRAP 2) — the scrub
     owns it, and a transition here would fight the scroll frame by frame. */
}
.cn-flyer.is-live{visibility:visible}

/* The cut. base.css cross-fades colour/background over .7–.9s so a phase swap
   is never a hard edge — but the swap in the middle of the collapse happens
   UNDER an opaque white lid, and a .9s cross-fade there means a fast scroller
   sees the disc shrink to reveal half-lit grey. For the ~140ms around the
   flip, colour changes are instantaneous instead. Nothing visible is
   transitioning at that moment: the lid covers the viewport. */
html.cn-cut body,html.cn-cut body *{transition:none!important}

/* ═══ MOBILE (≤760px) ═══════════════════════════════════════════════════
   Same drawing, smaller: the circles scale off the stage's short side, so a
   phone gets the identical composition at phone size. Two things do need a
   hand. The RUNWAY comes down from 1000vh to 900vh, because a vh on a phone is
   a much shorter scroll in the hand than a vh on a desktop and the full ten
   screens read as a section that will not end. And the TYPE — 10px at 0.35em
   tracking is wider than a 375px screen can hold once a label sits out near a
   rim. Both min-heights move together: the section and its wrap are the two
   halves of one sticky runway and a mismatch strands the stage. ═══ */
@media(max-width:760px){
  html.constellation-live #constellation{min-height:900vh}
  html.constellation-live #constellation > .wrap{min-height:900vh}
  .cn-lab{font-size:8px;letter-spacing:0.26em;margin-right:-0.26em}
  .cn-ether{font-size:8.5px;letter-spacing:0.1em}
}

/* ═══ REDUCED MOTION ════════════════════════════════════════════════════
   constellation.js does not build at all when RZV.motionOK() is false — the
   semantic .cn-fallback list stays the section and none of the live rules ever
   match, because html.constellation-live is never added. There is no CSS
   animation in this file to freeze; the guard lives entirely in the JS. ═══ */
