/* ═══════════════════════════════════════════════════════════════════════════
   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 never flip. ⚠️ AND NO WHITE EVER
   ARRIVES. The centre pearl growing until it was the screen, the venn riding
   it in mix-blend-mode:difference, the lid, the implosion and the lift that
   replaced the implosion are ALL DELETED — see the tombstones at THE VEIL IS
   DELETED and THE BLACK PEARL IS DELETED. The section is night from its first
   frame to its last: the rings swell away, the statement's words arrive on
   that same night, and #work is the next thing the reader meets.

   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 100dvh
   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 MOOT HERE NOW, and that is worth stating rather than
   leaving to be rediscovered: THERE IS NO mix-blend-mode LEFT IN THIS FILE.
   The stage's and the ether layer's difference blends both existed to survive
   a white ground and both went with it. iOS Safari's black box needs a blended
   element under a backdrop-filter ancestor and there is no blended element, so
   the trap cannot fire — but the moment anyone re-introduces a blend here it
   is live again, and the chain to check is html / body / #constellation /
   .wrap / .constellation-stage. TRAP 2 stands — transform is ABSENT from every
   transition list on the rings, the labels and the core pearl, 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, and there is nothing left for one to
   answer: the page never cuts to light in this section at all. White ink on
   night, from the first frame to the last, one set of tokens. */

/* ═══ THE RUNWAY ════════════════════════════════════════════════════════
   1000vh of section, a 100dvh 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 —
   GSAP is only reading progress here, at line ~2424 (trigger:SECTION,
   start:'top top', end:'bottom bottom'), it never touches this element's
   layout directly.

   svh, NOT lvh — REVERTED, having briefly been lvh, after it broke the exit:
   "when scrolling down the pearl expands - stays doesn't contract again. if i
   refresh from bottom of page then i can see the rest of the page by
   scrolling up", his words, immediately after this ran as lvh for one deploy.
   Root cause: `end:'bottom bottom'` is computed by GSAP as
   `SECTION.offsetTop + SECTION.height - window.innerHeight` — a mix of this
   CSS's fixed height and a LIVE, un-fixed `window.innerHeight` reading taken
   whenever ScrollTrigger refreshes (onResize below re-refreshes 140ms after
   every resize event, and Mobile Safari fires resize events as its toolbar
   animates). `window.innerHeight` tracks the toolbar's CURRENT, typically
   near-expanded resting state, i.e. close to `svh` — so anchoring this
   runway's OWN fixed height to svh keeps GSAP's live term and this CSS's
   fixed term close together, and the residual gap stays a few px, forever
   negligible against a 1000-viewport runway. Anchoring it to `lvh` instead
   pushed this CSS term to the FARTHEST possible value from what GSAP's own
   refresh converges on — on a page that can only ever scroll to this fixed
   CSS height, a `refresh()` landing mid-scroll with a smaller
   `window.innerHeight` than the toolbar-hidden lvh basis stretches `end`
   further than the page can physically scroll, so `progress` caps below 1
   AT THE ACTUAL BOTTOM OF THE PAGE and never reaches it — the pearl finishes
   growing and then simply sits there, because the exit math that takes it away
   never sees q reach 1. (That exit is the LIFT now rather than the implosion it
   was when this was written; the failure mode is identical either way, which is
   the point — anything keyed to the far end of q is hostage to this.) A fresh load already scrolled near the
   bottom re-creates the trigger from scratch against the CURRENT
   `window.innerHeight`, which is exactly why reloading there and scrolling up
   showed a correct page: nothing about scrolling up is special, a clean boot
   is. base.css's #hero, .case-hero, .page-hero, #contact and --section-y —
   plain visual full-bleed padding, nothing reading `p` off their own
   height — are NOT part of this exception and stay on lvh; a bare few px of
   possible gap there is cosmetic, not a stuck page. See onResize()'s own note
   for the second half of this fix — a width-only guard on the refresh
   itself, so a toolbar-only resize can't retrigger this class of bug even if
   some other height on the page ever drifts from svh again. */
html.constellation-live #constellation{
  min-height:650vh;min-height:650svh;padding-block:0;   /* 4 Sep: was 1000 — "takes too long to scroll" */
}
html.constellation-live #constellation > .wrap{
  min-height:650vh;min-height:650svh;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().

   height IS dvh here, NOT svh — a DIFFERENT unit from THE RUNWAY above, and
   deliberately so, after "the ven diagram section is out of view now -
   shifted to the top not in the middle as it was before", his words, the
   moment THE RUNWAY reverted to svh. svh is fixed at the toolbar-EXPANDED
   size — smaller than the real, current viewport whenever the toolbar has
   actually retracted, which is most of the time a reader is mid-scroll. A
   sticky stage shorter than the real viewport doesn't fill it: it pins at
   top:0 and leaves bare page showing below its own bottom edge, and every
   coordinate .cn-space (inset:0 on this stage) computes as "centre" is the
   stage's OWN centre — which is now above the true visual centre by half
   that shortfall. dvh is the one viewport unit that TRACKS the actual current
   viewport rather than freezing at either extreme, so the stage always
   genuinely fills the screen and the centre stays the centre, at the cost of
   being exactly what svh/lvh exist to avoid: a value that moves as the
   toolbar animates. That trade is fine here specifically because this height
   only feeds VISUAL fill and the native sticky's own un-stick point — a ±60px
   wobble in exactly where a 1000-viewport pin releases is invisible — and NOT
   the scroll-progress math: THE RUNWAY's min-height (what GSAP's progress is
   actually computed against) stays on svh untouched, so the "pearl expands,
   stays" bug this same session already fixed cannot come back through this
   door. The ResizeObserver below is guarded the same way onWindowResize is,
   for the same reason: dvh means this element's own box now genuinely
   resizes as the toolbar animates, and unguarded that observer would refresh
   ScrollTrigger on every one of those, reopening exactly that bug through a
   second door. */
html.constellation-live .constellation-stage{
  /* lvh, not dvh: dvh made the pinned stage breathe with the toolbar on
     every scroll and the figure re-centred mid-scrub. lvh is one number for
     the whole visit; while the toolbar is still expanded the stage runs
     under it by its height, which is invisible on black. */
  position:sticky;top:0;height:100vh;height:calc(var(--vh,1vh) * 100);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. ═══ */
/* FOUR SURFACES, NOT SIX. .cn-veil and .cn-ink-pearl were listed here; the
   white moment is deleted and neither element is ever built, so a selector for
   them is a selector that can only ever match something a stale cached page
   left behind. */
html.constellation-live .constellation-stage,
html.constellation-live .cn-ether-layer,
html.constellation-live .cn-core,
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-statement.is-live{
  visibility:visible;
}

/* ═══ THE VENN'S DIFFERENCE BLEND IS DELETED ════════════════════════════
   `#constellation.cn-on-pearl .constellation-stage{mix-blend-mode:difference}`
   stood here with forty lines explaining why the blend had to live on the
   stage and nowhere else: it was how white strokes survived riding a white
   pearl. There is no white pearl and the page is night from the first frame of
   this section to the last, so the class is never added, the blend group it
   needed no longer has to exist, and the whole compositing layer it forced is
   recovered. Nothing to re-enable — the ground it was fighting is gone. ═══ */

/* ═══ 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:var(--fs-label-s);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, and that is STILL TRUE now the venn has a
   hover. The hover is not a hit target: constellation.js reads one window
   pointermove and tests the cursor against the three discs it has just
   positioned, so the response exists without a single element becoming
   interactive. Do not put pointer-events on any of this to "make hover work" —
   it already does, and turning it on would lay an inert full-screen surface
   over the page. */
.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;transform-origin:50% 50%;will-change:transform}
.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;
  /* "dont use uppercase bold for the core offerings, use regular normal case
     normal weight." Sentence case, 400, and the tracking that only ever made
     sense on tracked uppercase comes down with it (the trailing-space margin
     trick shrinks to match, so translate(-50%) still centres the ink). */
  /* 7 Sep, Haseeb: the three overlap names (websites, automation, branding)
     take the portfolio caption's recipe — mono, uppercase, 0.14em — a size down. */
  font-family:var(--mono);font-weight:400;
  font-size:clamp(10px,0.85vw,12.5px);
  letter-spacing:0.14em;text-transform:uppercase;
  color:var(--cn-ink);white-space:nowrap;
  margin-right:-0.14em;
  /* THE MASK. The one inner span (.cn-char — the name is historical) slides
     up from 105% inside this box; the padding/negative-margin pair gives
     descenders room without adding height to the box. */
  overflow:hidden;padding-bottom:0.16em;margin-bottom:-0.16em;
}
.cn-char{display:inline-block;will-change:transform}

/* ── THE THREE PRIMARY LABELS ────────────────────────────────────────────
   "make the labels for my services larger sans font not mono for the three
   main ones", his words — colour stays the site's black-and-white scheme
   throughout, only size, weight and family change. Scoped to the three PURE
   disciplines (design, marketing, strategy — one ring each, marked
   cn-lab-primary in build()); the three overlap names (websites, ai,
   branding) keep the tracked-mono overline voice and the per-glyph assembly
   unchanged.

   letter-spacing and margin-right are reset rather than inherited: .cn-lab's
   0.35em tracking and its -0.35em compensating margin (so translate(-50%)
   centres the INK, not the trailing tracked space) were sized for a 10px
   overline. At headline size that tracking reads as gaps, not a voice, so
   both numbers are zeroed together — losing one without the other would
   throw the centring off by the space the old tracking used to reserve. The
   per-glyph assembly still runs (constellation.js), just with every glyph on
   the same clock — see the step calculation in tick(). */
/* 7 Sep, Haseeb: MARKETING / DESIGN / STRATEGY exactly as the portfolio
   thumbnail title (.subhead in base.css): mono 400, uppercase, 0.14em, its size. */
.cn-lab.cn-lab-primary{
  font-family:var(--mono);
  font-weight:400;
  font-size:var(--fs-mono);
  letter-spacing:0.14em;text-transform:uppercase;margin-right:-0.14em;
}

/* 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, not in the plane, and that is what lets the word cloud
   outlive the venn: inside .constellation-stage it would die with the stage's
   own life. Out here it is a sibling at z 11, under everything else, which is
   what "in the background" has to mean structurally.

   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.

   ⚠️ THE BOX IS NOT THE CLOUD'S BOUND. layout() solves the eighteen names
   against L.ethHX / L.ethHY, which is this box widened by as much of the
   page's gutter as ETHER_GUT allows — the stage sits inside .wrap's padding
   and is 335px of a 375px phone, while the rings bleed straight past it. This
   element stays overflow:visible so a name may paint over that gutter; the
   guarantee that it never leaves the SCREEN is arithmetic, in etherLim(). */
/* The layer carried a difference blend of its own (under .cn-on-white) so one
   light grey could read on both the dark page and the white veil. Same story
   as the stage's: no veil, no white, no blend. */
.cn-ether-layer{position:fixed;pointer-events:none;z-index:11}
.cn-ether{
  position:absolute;left:0;top:0;white-space:nowrap;pointer-events:none;
  font-family:var(--mono);font-size:var(--fs-label-s);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 for the whole section because it is
   the object that outlives the figure: at the exit it lets go of the venn and
   takes the third seat in the row of four above the statement. ═══ */
/* THE CORE LIVES IN THE WRAP, NOT IN THE PLANE, and now for one reason rather
   than three: it must not die with the stage, because it is still on screen
   long after the rings have swelled away. Positioned by layout() on 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;
  /* THE NODE IS THE LOGO'S ROUNDED SQUARE. Same material as .pearl, but the
     shape is the mark's own full stop: corner radius ~26% of the side
     (7.16 of 27.5 in the file) and rotated 45° by the rAF's transform. */
  border-radius:26%;
  /* 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 IS DELETED ══════════════════════════════════════════════
   .cn-veil was the centre pearl grown until it was the screen: a fixed disc
   sized off the viewport's corner distance, wearing .pearl's own three
   background layers and iriDrift, that covered, held while the statement read
   on it, then translated up until its own bottom arc cleared the top of the
   frame. Its rule, its 30-line note and the rzv-no-motion rule that stilled
   its animation are all gone.

   "in same place on the black background" — the page never cuts to light, so
   there is nothing for a white lid to be. The statement reads on the night in
   the dark-phase tokens and the venn simply swells away around it. ═══ */

/* ═══ THE BLACK PEARL IS DELETED ═══════════════════════════════════════
   Two rules stood here across ~90 lines. `html.constellation-live
   .pearl.is-ink` was a paint-only modifier on base.css's .pearl — a
   violet-cast near-black body, a tightened specular, a reflected-light
   terminator and a real contact shadow, all in ratios of --pearl-size — and
   `html.constellation-live .cn-ink-pearl` was the fixed element that wore it.

   The object existed to stand ON the white and survive it. There is no white,
   nothing builds the element, and nothing anywhere in the project asks for
   .is-ink. Both are gone rather than kept warm: if a dark pearl is ever wanted
   again it is a MATERIAL and it belongs in base.css beside .pearl, written
   fresh against whatever surface asks for it — which is what the note that
   stood here said in the first place. The recipe is in the git history. ═══ */

/* ═══ THE STATEMENT ═════════════════════════════════════════════════════
   Reads on the night, per WORD, in a shuffled but hash-fixed order — scrubbed
   like everything else here, so you can stop halfway through the sentence and
   it waits for you.

   z 15, inside #constellation > .wrap, and the top of this section's stack:
   above the word cloud (11), the venn (12) and the core (13). Nothing sits
   above it. Light ink on the dark-phase tokens; the white ground it used to
   read on is deleted. These numbers only became meaningful once everything
   this section paints moved into this one 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. */
/* THREE BLOCKS NOW, NOT ONE. index.html's white moment gained an overline brow
   and a lead paragraph above the display copy — #salam's content, dissolved in
   (SPEC §5) — so this is a small typographic stack rather than a single
   sentence, and it needs its own vertical rhythm in BOTH states. Nothing here
   hides anything: the static version is the readable fallback and it has to
   read on its own. */
.cn-statement{margin-top:clamp(40px,6vw,80px);max-width:56ch}
.cn-statement-copy{
  font-size:var(--fs-statement);font-weight:300;   /* 7 Sep: the range line's size — "I prefer the larger size" */
  letter-spacing:-0.035em;line-height:1.14;color:var(--text);
}
.cn-statement .line{display:block}
.cn-statement-em{color:inherit}

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;
}
/* lvh override for --cn-pearl-gap, in its own @supports rather than a second
   declaration on the line above — "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". A CUSTOM property doesn't
   validate its value at assignment (unlike a real property, it accepts any
   token stream), so `--x:1vh; --x:1lvh;` does NOT safely fall back on a
   browser that lacks lvh the way `padding:1vh; padding:1lvh;` does — it would
   just overwrite --cn-pearl-gap with a value that becomes invalid only later,
   at the var()-consuming padding-top above, discarding the whole declaration
   and collapsing the gap to nothing instead of gracefully keeping the vh
   number. @supports gates the redefinition on the browser actually
   understanding lvh, so unsupporting browsers never see this rule at all. */
@supports (height: 1lvh) {
  html.constellation-live .cn-statement{
    --cn-pearl-gap:clamp(38px,5.6lvh,78px);
  }
}
/* ── 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. */
/* ⚠️ THE ARITHMETIC ABOVE WAS RUN AGAINST COPY THAT HAS SINCE CHANGED, AND
   THIS IS THE RE-RUN. 2.9vw was solved for a 44-character longest line —
   ~19.2em on Inter-Light's own advance widths with the -0.035em tracking on —
   which fitted 0.78·VW with a 29% margin, one visual line per sentence at
   every desktop size. The copy is now "I leverage my 21 years of experience
   working with clients, media, and my own start-ups", 86 characters, ~37.5em
   on the same measure. At 2.9vw that is 1.09·VW against a 0.78·VW box: it
   overflows by 40% and breaks wherever it lands.

   TWO WAYS OUT, and the size is the wrong one to take all the way. Solving for
   one visual line needs 0.78/37.5 = 2.08vw — 30px at 1440 — which is barely
   1.4× the lead paragraph under it at its own ceiling, and a display line that
   close to body copy stops being a display line. So the sentence is allowed to
   WRAP, and the size comes down only as far as the block's height wants:

   At 2.6vw / 1440px: 37.5 × 37.4px = 1403px against a 1123px box, so line one
   takes two visual lines and line two (47 chars, ~20.5em, 767px) takes one.
   Three visual lines of display, which with the brow and a two-line lead is a
   ~295px block on a 900px viewport — comfortably inside the pearl's gap above
   it. Above ~1600px the type caps at 2.6rem and the box at 1240px and the
   proportion holds. The 1.35rem floor is a phone answer and is deliberately
   above the lead's 1.05rem floor so the hierarchy survives at 390px, where
   everything wraps anyway.

   ⚠️ STILL COUPLED TO THE COPY. Rewrite the statement and re-run this. */
html.constellation-live .cn-statement-copy{
  font-size:var(--fs-statement);color:var(--text);margin:0;   /* 7 Sep: same size live as the range line */
}
/* The brow and lead that sat above the copy are gone from the markup ("just
   stick to the leverage line"), so their colour and margin rules went with
   them. The copy is --text, its emphasised line --muted; two steps now. */
/* ONE COLOUR. The emphasised second line used to be --muted; on the night it
   read as two texts in two colours ("text seems broken and in two colors").
   It inherits now. The two .line spans also flow as one paragraph while the
   section is live — display:block per line forced a break after a first line
   that already wraps, which is what looked broken. */
html.constellation-live .cn-statement-em{color:inherit}
html.constellation-live .cn-statement .line{display:inline}
/* ── THE WORD MASKS ──
   constellation.js wraps each word in .cn-wm (the mask) holding .cn-wi (the
   inner that slides up from 105%). inline-block so the mask can clip; the
   padding/negative-margin pair keeps descenders from being clipped by the
   overflow while adding no height to the line box. vertical-align:top keeps
   the masks on one baseline grid. The inner is inline-block so it can take a
   transform. Spaces between masks are real text nodes, so wrapping is the
   browser's own. */
html.constellation-live .cn-wm{display:inline-block;overflow:hidden;vertical-align:top;
  padding-bottom:0.14em;margin-bottom:-0.14em}
html.constellation-live .cn-wi{display:inline-block;will-change:transform}

/* ═══ THE FLYER IS DELETED ══════════════════════════════════════════════
   .cn-flyer was a second .pearl on <body> at z 560 — above the raised wrap and
   above the header — that took over from the imploding veil at 15px and swept
   into the header's brand slot. There is no implosion and no sweep, so there
   is no flyer, and the rules that positioned it are gone rather than left to
   rot.

   Two files used to name it in prose — glass.css's stacking note and glass.js's
   z-index inventory, both listing ".cn-flyer at 560". Neither ever had a rule
   or a line of code depending on it; they were comments describing a
   neighbour, and both have been corrected to say the ceiling on this page is
   530. Nothing at 560 exists here any more. ═══ */

/* The cut. base.css cross-fades colour/background over .7–.9s so a phase swap
   is never a hard edge. THE SCRUB NO LONGER SWAPS A PHASE AT ALL — the section
   is night end to end — but cut('dark') survives on the deep-load, jump-scroll
   and unmount paths, where this section asserts the ground it expects rather
   than inheriting whatever the hero left behind. Those all happen with the
   section off-screen or mid-jump, and suppressing the cross-fade for ~140ms
   around one keeps it from being watched as a slow grey wash. */
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:600vh;min-height:600svh}   /* 4 Sep: was 900 */
  html.constellation-live #constellation > .wrap{min-height:600vh;min-height:600svh}
  .cn-lab{font-size:9px;letter-spacing:0.12em;margin-right:-0.12em}
  .cn-ether{font-size:8.5px;letter-spacing:0.1em}
}

/* ═══ REDUCED MOTION — AND EVERY OTHER WAY THIS SECTION CAN NOT RUN ═════
   constellation.js does not build at all when RZV.motionOK() is false, and it
   unmounts itself on any throw. In every one of those cases — motion off, JS
   disabled, a script that 404s, a crash — html.constellation-live is never
   added or is taken away again, so none of the live rules above match and the
   section is exactly what index.html ships: a statement, and the semantic
   .cn-fallback pillar list. There is no CSS animation left in this file to
   freeze (the one that existed was the veil's iriDrift, deleted with it), so
   the guard lives entirely in the JS.

   ONE THING THE MARKUP LEAVES BEHIND, AND THIS IS WHERE IT IS SWEPT UP.
   .constellation-stage is an empty, aria-hidden div that only ever has
   children because constellation.js put them there — and base.css gives it
   min-height:clamp(420px,62vh,720px), which is deliberate while the live
   section owns it and is 420–720px of blank page when nothing does. A reader
   with motion off met the statement, then most of a screen of nothing, then
   the list. Collapsing it is not hiding anything shipped: there is nothing in
   it, and the moment the class arrives the base rule is back in force. ═══ */
html:not(.constellation-live) .constellation-stage{display:none}
