/* ══════════════════════════════════════════════════════════════
   Keln — the landing.

   ══ WHAT THIS REBUILD CHANGES AND WHY ═══════════════════════

   ONE RING, NOT FOUR. The previous build drew .portal-orbit::before
   at inset 17.5% and ::after at inset 16% — two near-identical
   circles about eleven pixels apart on an 840px field, plus a
   spinning conic sheen over both. That is what read as overlapping
   orbits. An instrument gets its density from GRADUATION, not from
   repeated rings, so there is now a single ellipse with tick marks
   written by landing.js.

   THE DISC IS INCLINED, AND THE NODES OBEY IT. Nodes are placed on
   an ellipse whose y-radius is 0.62 of its x-radius — the exact
   squash constant pipeline-orbit.js uses — and their scale and
   opacity are driven by how far "forward" on that ellipse they
   sit. Dragging moves them ALONG the disc rather than rotating the
   whole container, because rotating an inclined plane in screen
   space instantly destroys the illusion that it is inclined.

   THERE IS A HEADLINE NOW. The old landing had no statement on it
   at all — three labelled cards and a hint line. A visitor could
   not tell what Keln was without clicking. That absence was most
   of what read as unfinished.

   TYPE IS THE OTHER HALF. Serif for anything a person reads, mono
   for anything the machine produced. See theme.css for why the
   serif is now pinned rather than inherited from a hole in the
   token layer.

   NOTHING HERE RESTYLES .logo. topbar.css owns it, which is what
   gives the landing logo the same hover, press and open-pulse the
   dashboard has. Adding a .logo rule to this file would silently
   take it back, because landing.css loads after topbar.css.

   THE PLANE PASSES THROUGH THE SPHERE. The disc is drawn twice —
   the far half of the ellipse behind Vega, the near half in front
   of it — so the ring reads as a plane the sphere sits IN rather
   than a ring the sphere sits ON. See the .portal-disc block.

   ══ Z-INDEX BUDGET ══════════════════════════════════════════
     sky canvas        0    fixed, behind everything
     disc, far half    1    the arc that runs behind the sphere
     drag surface      2    under the nodes so it never eats a click
     nodes, far side   4    written by landing.js, below Vega
     Vega              6
     disc, near half   7    the arc that runs in front of the sphere
     nodes, near side 12    written by landing.js, above everything
     hud              30
     nav              80
     sidebar scrim    39  ·  sidebar 40   (sidebar.css)
     Vega transcript  900    (landing-vega-guide.css)
     title screen    3200    (landing-cinema.css)
   ══════════════════════════════════════════════════════════════ */

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* ── Where the page background lives ────────────────────────
   `scrollbar-gutter: stable` reserves the scrollbar's width
   whether or not one is showing, so the layout does not jump by
   15px the moment content grows past the viewport. That is worth
   keeping, and it is also what produced a pale band down the right
   edge of the landing.

   The band was never the gutter itself. It was WHERE THE
   BACKGROUND WAS. A background on <body> propagates to the canvas
   and paints the whole viewport only while <html> has none of its
   own; but body's box stops at the reserved gutter, so the fixed
   layers — the star field, the grain, the vignette — spanned the
   full 1600px while the page's own gradient stopped at 1585, and
   the last 15px showed the difference.

   So the gradient moves to <html>, which is never inset by the
   gutter, and body is left transparent. Measured: body.offsetWidth
   was 1585 against documentElement.clientWidth of 1600, with
   margin already 0 and overflow hidden on both — which is what
   ruled out the margin and the scrollbar in turn and left the
   painting box as the only candidate.

   landing.css is loaded by landing.html alone, so styling the bare
   html element here cannot reach any other page. */
html {
  scrollbar-gutter: stable;
  background:
    radial-gradient(ellipse 96% 68% at 50% 44%, rgba(26,38,68,.50) 0%, rgba(8,12,22,.86) 58%, rgba(2,5,10,.98) 100%),
    #02050a;
}

body.landing {
  /* The user agent's default 8px body margin, never reset anywhere
     in the product. On a page with a full-bleed background it is
     visible as a lighter band down the right edge and along the
     bottom, because the fixed layers (sky, grain, vignette) span
     the whole viewport while the body's own background stops 8px
     short of it.

     Measured rather than guessed: documentElement.clientWidth was
     1600 and body.clientWidth 1585 at a 1600px viewport. Scoped to
     .landing rather than fixed globally — a bare `body { margin: 0 }`
     would move every other page in the product by 8px, which is
     precisely the kind of quiet, everywhere change the design-system
     batch measured and declined to make. */
  margin: 0;

  min-height: 100svh;

  /* The gradient is on <html> — see the note there. Transparent
     rather than simply omitted, so that if anything later gives
     body a background it is an explicit decision rather than a
     silent return of the band. */
  background: transparent;
  color: var(--ink, #e9f0f7);
  font-family: var(--font);
  font-size: var(--t-xl);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

/* One screen, no scroll — but only where a screen can actually
   hold it. Below these thresholds the layout stacks and scrolling
   is the correct behaviour, not a failure. */
@media (min-width: 901px) and (min-height: 640px) {
  html, body.landing { height: 100%; overflow: hidden; }
}

a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; }

/* The accent phrase. Italic does the work; the gradient is a
   shimmer on the serif, not a rainbow across it.

   It used to run into teal at 62% and hold it to the end, which
   put a second saturated object on a page that is meant to carry
   exactly one. The emphasis was never coming from the colour —
   it comes from the italic and from Source Serif 4's optical-size
   axis redrawing the letterforms at display size. So the gradient
   is now cold throughout: bright white into blue-white, which
   separates the phrase from the ink around it without introducing
   a hue the palette has to account for. */
em {
  font-style: italic;
  background: linear-gradient(104deg, #ffffff 0%, #cfe0fb 54%, #93b6ea 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* ── Sky ────────────────────────────────────────────────────
   Fixed and full-bleed. The title screen draws its own canvas on
   top; when that dissolves, this is what remains, which is why
   arrival reads as one continuous place. */
.portal-stars {
  position: fixed;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* ── Title-screen gating ────────────────────────────────────
   The hidden state is applied by an ATTRIBUTE that JavaScript
   sets, never by a bare CSS rule. If any script fails, the
   attribute is never written and the page falls through visible.
   Same discipline as reveal.js. */
body[data-cinema="pending"] .portal-nav,
body[data-cinema="pending"] .portal-hud,
body[data-cinema="pending"] .portal-field,
body[data-cinema="pending"] .portal-foot {
  opacity: 0;
  pointer-events: none;
}
body[data-cinema="pending"] .portal-hud  { transform: translateY(14px); }
body[data-cinema="pending"] .portal-field { transform: scale(.97); }

body[data-cinema="done"] .portal-nav,
body[data-cinema="done"] .portal-hud,
body[data-cinema="done"] .portal-field,
body[data-cinema="done"] .portal-foot {
  opacity: 1;
  transform: none;
  pointer-events: auto;
  transition:
    opacity   1.10s cubic-bezier(.16,.86,.22,1) var(--in, 0s),
    transform 1.40s cubic-bezier(.16,.86,.22,1) var(--in, 0s);
}
/* The field arrives first and the copy follows it — the machine is
   the thing being revealed, the sentence explains what just
   appeared. Reversing this order makes the portal feel like an
   illustration of the headline instead of the subject. */
body[data-cinema="done"] .portal-field { --in: .05s; }
body[data-cinema="done"] .portal-hud   { --in: .34s; }
body[data-cinema="done"] .portal-nav   { --in: .62s; }
body[data-cinema="done"] .portal-foot  { --in: .78s; }

/* The proof line arrives last and alone, about a second after the
   sentence above it has settled.

   The delay is the whole point of the element. Landing on a page
   that states its headline, its subtitle and its credentials all
   at once reads as a pitch; the same three facts arriving after
   you have already looked at the thing read as an answer to a
   question you had just started to form. It is also the order the
   page wants: the machine, then what it is, then why you can
   trust it.

   It rides the SAME reveal machinery as everything else — one
   more --in value, no separate script, nothing to fail on its own.
   The delay is deliberately longer than the gap between any other
   two elements so it reads as a distinct beat rather than as the
   tail of the copy's animation. */
body[data-cinema="pending"] .portal-proof { opacity: 0; transform: translateY(8px); }
body[data-cinema="done"]    .portal-proof {
  opacity: 1;
  transform: none;
  transition:
    opacity   .9s cubic-bezier(.16,.86,.22,1) 1.65s,
    transform 1.1s cubic-bezier(.16,.86,.22,1) 1.65s;
}

/* ── Nav ────────────────────────────────────────────────────
   No bar, no border, no backdrop. On a full-bleed portal a solid
   topbar cuts the sky off at the top of the screen and the
   immersion goes with it. */
.portal-nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 80;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-8);
  padding: clamp(16px, 2.2vw, 26px) clamp(16px, 3vw, 34px);
  pointer-events: none;
}
.portal-nav > * { pointer-events: auto; }

.portal-cta {
  display: inline-flex;
  align-items: center;
  padding: 9px 18px;
  border-radius: 999px;
  border: 1px solid rgba(180,210,255,.20);
  background: rgba(8,12,20,.55);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  font-size: 14.5px;
  font-weight: 600;
  color: var(--ink, #e9f0f7);
  transition: border-color .22s ease, background .22s ease, transform .18s ease;
}
.portal-cta:hover {
  border-color: rgba(34,211,166,.55);
  background: rgba(14,20,32,.72);
  transform: translateY(-1px);
}
.portal-cta:focus-visible { outline: 2px solid var(--accent2, #22d3a6); outline-offset: 3px; }

/* The pill ships hidden and is revealed by paintAnon() once the
   session call confirms there is no session. Fading in is the
   difference between "an element arrived" and "the page changed
   its mind", which is what the old paint-then-hide looked like. */
@keyframes portal-cta-arrive { from { opacity: 0; transform: translateY(-4px); } }
.portal-cta:not([hidden]) { animation: portal-cta-arrive .32s cubic-bezier(.16,.86,.22,1) both; }

/* `hidden` has to be able to win.

   paintMember() has always set navCta.hidden = true for a signed-in
   visitor, and it has never had any effect: the browser's own
   `[hidden] { display: none }` comes from the user-agent stylesheet,
   which loses to ANY author declaration — and `.portal-cta` above
   declares `display: inline-flex`. So the "Log in" pill stayed on
   screen for members, indefinitely.

   This is a general trap rather than a quirk of this button: every
   element in this codebase given an explicit `display` is immune to
   the `hidden` attribute unless something like this rule exists.
   `!important` is the correct tool here and not a shortcut — the
   attribute is a statement that the element is not relevant to the
   current state of the page, and nothing about a display value
   should be able to overrule that. */
[hidden] { display: none !important; }

/* ── Layout ─────────────────────────────────────────────────── */
.portal {
  position: relative;
  z-index: 10;
  min-height: 100svh;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.02fr);
  align-items: center;
  gap: clamp(20px, 4vw, 76px);
  padding: clamp(90px, 11vh, 132px) clamp(20px, 5vw, 76px) clamp(78px, 10vh, 104px);
}

/* ── HUD ────────────────────────────────────────────────────── */
.portal-hud { position: relative; z-index: 30; max-width: 34rem; }

.portal-hud__eyebrow {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .28em;
  text-indent: .28em;
  text-transform: uppercase;
  /* Dim and cold, not accented. Mono micro-type at 10.5px with
     .28em tracking is already doing the signalling; colouring it
     as well makes it compete with the headline directly beneath
     it, and a label should never outrank the thing it labels. */
  color: rgba(178,201,236,.60);
  margin-bottom: var(--s-9);
}

/* Weight 400, not 700. Source Serif 4's optical-size axis already
   thins the strokes for display sizes; adding weight on top of
   that is how a serif headline turns into a newspaper masthead. */
.portal-hud__title {
  font-size: clamp(40px, 5.4vw, 74px);
  font-weight: 400;
  line-height: 1.02;
  letter-spacing: -.022em;
  margin-bottom: 22px;
  text-wrap: balance;
}

.portal-hud__sub {
  font-size: clamp(16px, 1.15vw, 18.5px);
  line-height: 1.62;
  color: var(--muted, #8b9bb0);
  max-width: 38ch;
  margin-bottom: 30px;
}

.portal-hud__cta { display: flex; flex-wrap: wrap; gap: var(--s-6); }

/* ── Buttons ────────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-6) var(--s-10);
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: 15.5px;
  font-weight: 600;
  cursor: pointer;
  transition: transform .18s ease, box-shadow .24s ease,
              border-color .22s ease, background .22s ease, filter .2s ease;
}
.btn:focus-visible { outline: 2px solid var(--accent2, #22d3a6); outline-offset: 3px; }
.btn:active { transform: translateY(0) scale(.985); }

.btn--primary {
  background: linear-gradient(135deg, var(--accent, #5b8cff), var(--accent2, #22d3a6));
  color: #04121a;
  box-shadow: 0 10px 30px rgba(34,211,166,.20);
}
.btn--primary:hover {
  transform: translateY(-1px);
  filter: brightness(1.06);
  box-shadow: 0 14px 38px rgba(34,211,166,.30);
}

.btn--ghost {
  border-color: rgba(180,210,255,.20);
  background: rgba(8,12,20,.42);
  color: var(--ink, #e9f0f7);
}
.btn--ghost:hover {
  transform: translateY(-1px);
  border-color: rgba(34,211,166,.5);
  background: rgba(14,20,32,.62);
}

/* ── Vega's readout ─────────────────────────────────────────
   Vega's presence in the copy column. The name is mono so it reads
   as a system entity; what it says is serif so it reads as speech.
   That contrast is the entire characterisation. */
.portal-readout {
  display: flex;
  align-items: baseline;
  gap: var(--s-5);
  margin-top: 30px;
  padding-top: 18px;
  border-top: 1px solid rgba(180,210,255,.10);
  min-height: 3.1em;          /* reserved, so a changing line never reflows the CTAs */
  font-size: var(--t-lg);
  line-height: 1.55;
  color: var(--muted, #8b9bb0);
}
/* ── Proof ──────────────────────────────────────────────────
   Three facts, mono, small, dim. Everything about the treatment is
   chosen to keep it subordinate: it has to be legible to someone
   who has decided to evaluate the product and close to invisible
   to someone who has not. Loud reassurance reads as anxiety. */
.portal-proof {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 var(--s-5);
  margin: 18px 0 0;
  font-size: 10.5px;
  letter-spacing: .10em;
  text-transform: uppercase;
  color: rgba(170,194,232,.52);
}

/* The separator is drawn rather than typed. A "·" between items
   inherits the letter-spacing above and lands visibly closer to
   one neighbour than the other; a 2px square centred in its own
   flex gap is symmetrical by construction. It is also aria-hidden,
   so a screen reader gets three phrases and no punctuation. */
.portal-proof__d {
  width: 2px;
  height: 2px;
  border-radius: 50%;
  background: currentColor;
  opacity: .55;
  flex: none;
}

.portal-readout__mark {
  flex-shrink: 0;
  font-size: 10.5px;
  letter-spacing: .14em;
  text-transform: uppercase;
  /* Same reasoning as the eyebrow: this names the speaker, and a
     speaker label that is brighter than the sentence it attributes
     is reading the room wrong. Cold and dim, one step up from the
     eyebrow so the two do not sit at identical weight. */
  color: rgba(186,208,240,.72);
}
.portal-readout__text { font-style: italic; }
.portal-readout__text::after {
  /* Cursor. Present only while Vega is mid-sentence — landing.js
     drops .portal-readout--typing when the line completes. */
  content: "";
  display: none;
}
.portal-readout--typing .portal-readout__text::after {
  display: inline-block;
  width: 2px; height: 1em;
  margin-left: 3px;
  vertical-align: -.12em;
  background: var(--accent2, #22d3a6);
  animation: portal-caret 1s steps(2) infinite;
}
@keyframes portal-caret { 50% { opacity: 0; } }

/* ── The field ──────────────────────────────────────────────── */
/*
   The field is a single-cell grid. The disc SVG and the Vega button
   both live in that one cell (grid-area: 1/1). The grid engine
   handles their centering in one place — no position:absolute +
   translate on Vega, which can be off by a subpixel when the field
   resolves to an odd pixel width. Absolutely-positioned nodes still
   treat the field as their containing block (position:relative). */
.portal-field {
  position: relative;
  display: grid;
  width: min(100%, 560px);
  aspect-ratio: 1;
  margin: 0 auto;
  justify-self: center;
  touch-action: none;

  /* ── THE INSTRUMENT'S GEOMETRY, WRITTEN ONCE ──────────────
     Four things on this screen have to agree about where the
     inclined plane is: the ellipse, its graduation, the three
     nodes riding it, and the sphere at the focus. They agree
     because they all read these numbers — landing.js included,
     which pulls --disc-rx and --disc-squash off this element
     rather than restating them. Change a number here and the
     whole instrument moves together.

     --disc-squash is 0.62, the ry/rx that pipeline-orbit.js
     calls SQUASH. The landing and the real module must describe
     the same inclined plane, so it is shared, never re-chosen.

     THE CLEARANCE RULE. A node rides the ellipse, so at the near
     and far points of the plane it sits exactly ry from the
     centre — that is the closest a node ever comes to the
     sphere. For no node to cut through the sphere:

       --disc-rx x --disc-squash  >  --vega-d / 2 x 1.045  +  3%
              (the ellipse's ry)     (sphere radius at    (dot radius
                                      peak breathe)        plus a gap)

       45 x .62 = 27.9%   >   46 / 2 x 1.045 + 3 = 27.03%   ✓

     The margin is thin on purpose: the sphere is as large as it
     can be and still let three bodies orbit it cleanly. Shrink
     --disc-rx or grow --vega-d and the near node starts
     transiting the sphere's face. */
  --disc-rx: 45%;
  --disc-squash: .62;

  /* 40%, down from 46%. At 46 the sphere very nearly filled the
     ellipse's minor axis: the clearance rule above was satisfied
     by about a percentage point, so nothing overlapped, but the
     instrument had no air in it and the disc read as a tight
     collar around the sphere rather than as a plane the sphere
     sits in.

     Checking it against the rule stated above:
       45 x 0.62 = 27.9   >   (40 / 2) x 1.045 + 3 = 23.9
     — which is nearly 4 points of margin instead of under 1, so
     the near node clears the sphere's bloom at every width rather
     than only at the ones that happen to get tested. */
  --vega-d: 40%;
}

/* ── The disc ───────────────────────────────────────────────
   Drawn TWICE, into two SVGs stacked in the same grid cell, and
   that duplication is the entire depth illusion.

   A single ring behind the sphere reads as a ring the sphere is
   sitting ON — a sticker on a backdrop. A real body in a real
   plane occludes the half of the plane that runs behind it and
   is crossed by the half that runs in front. So --far carries
   the plane fill and the top arc at z-index 1 (behind Vega, and
   partly washed out by its bloom) and --near carries the bottom
   arc at z-index 7 (over Vega, crossing that same bloom). The
   nodes already obey the same rule: landing.js writes z-index 4
   on the far side of the disc and 12 on the near side.

   Both layers use one viewBox and one gradient, so the arcs are
   two halves of one drawn ellipse, not two ellipses that have to
   be kept in step. */
.portal-disc {
  grid-area: 1 / 1;
  align-self: stretch;
  justify-self: stretch;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}
.portal-disc--far  { z-index: 1; }
.portal-disc--near { z-index: 7; }

.portal-disc__ticks line {
  stroke: rgba(180,210,255,.30);
  stroke-width: 1;
  stroke-linecap: round;
}
.portal-disc__ticks line.is-major { stroke: rgba(180,210,255,.55); }

/* Distance, not decoration: the far half of the plane is further
   away and is being looked at through the sphere's bloom, so it is
   quieter. Reading the same graduation at two weights is what tells
   the eye which half is behind. */
.portal-disc--far  .portal-disc__ticks,
.portal-disc--far  .portal-disc__ring { opacity: .52; }
.portal-disc--near .portal-disc__ring { opacity: 1; }

.portal-drag {
  grid-area: 1 / 1;
  align-self: stretch;
  justify-self: stretch;
  width: 100%;
  height: 100%;
  z-index: 2;
  cursor: grab;
  border-radius: 50%;
}
.portal-drag:active { cursor: grabbing; }
.portal--dragging .portal-node { transition: none; }

/* Vega and the nodes are drag handles as well as buttons (see the
   wireDrag block in landing.js), so they have to LOOK grabbable.
   A pointer cursor on the biggest object in the instrument, while
   the empty space around it says grab, is the affordance telling
   the visitor the middle of the disc is the one part that does not
   turn — which was true until now and is the bug being fixed. */
.portal-vega,
.portal-node { cursor: grab; }
.portal--dragging .portal-vega,
.portal--dragging .portal-node,
.portal-vega:active,
.portal-node:active { cursor: grabbing; }

/* ── Vega ───────────────────────────────────────────────────
   The only thing on this screen that breathes. Everything else is
   still until the visitor moves it, which is what makes the one
   moving object read as alive rather than as decoration. */
.portal-vega {
  grid-area: 1 / 1;
  align-self: center;
  justify-self: center;
  z-index: 6;

  /* THE POSITIONING CONTEXT. Load-bearing, and it was missing.

     .portal-vega__core and .portal-vega__corona are
     position:absolute with an inset, so they resolve against the
     nearest POSITIONED ancestor. Without this line that ancestor
     is .portal-field, and two things go wrong at once:

       · the core's `inset: 19%` was taken from the 560px field
         rather than from this button, painting the sphere at
         ~347px instead of ~93px — six times its size, wider than
         the ring's minor axis, which is why the sphere read as
         pasted over the instrument instead of sitting in it;
       · the corona's `inset: 0` covered the ENTIRE field at
         z-index 6, so it silently swallowed every pointer event
         aimed at the drag surface (z-index 2) and at any node on
         the far side of the disc (z-index 4). "Drag the disc"
         in the footer was not a hint, it was a promise the page
         could not keep.

     Moving Vega to grid placement is what dropped it — the note
     above about "no position:absolute on Vega" is about not
     TRANSLATING it, and position:relative does not translate
     anything. */
  position: relative;

  /* Explicit width AND height — aspect-ratio alone can resolve
     differently on some engines when the containing axis is
     constrained. Both read the same token, which guarantees a
     perfectly square hit-target whose centre sits exactly on the
     grid cell's centre, which is also the disc's centre.

     No clamp. The sphere is a fixed fraction of a square field and
     so is the ellipse, so the clearance rule on .portal-field holds
     at every viewport width. A clamp would pin the sphere while the
     ellipse kept shrinking, and the nodes would begin cutting
     through it on narrow screens only — the worst kind of bug to
     find, because the layout it breaks is the one nobody tests. */
  width:  var(--vega-d);
  height: var(--vega-d);
  border: 0;
  padding: 0;
  background: none;
  border-radius: 50%;
  cursor: pointer;
}
/* The button IS the sphere now. The core used to be inset 19% to
   leave bloom room inside a hit-target larger than the body it
   drew; with the size written on the button itself that indirection
   only makes --vega-d lie about how big the sphere is. */
.portal-vega__core,
.portal-vega__corona { position: absolute; inset: 0; border-radius: 50%; }

/* Vega is a SPHERE now — a deliberate reversal of the Batch 2 call.

   Batch 2 ended the gradient on transparent and kept the highlight
   nearly centred on purpose, because a hard edge + an offset
   highlight is exactly what reads as a lit 3D object, and the brief
   at the time was "a star, not a sphere." The brief has since
   changed: real volumetric depth, a full-3D-model read. So this
   does now what that comment warned against — on purpose.

   Three layers build the illusion:
     1. The base gradient ends OPAQUE and DARK at 100% instead of
        fading to transparent — an object with a lit face and a
        shadowed far side, not a diffuse glow with no back.
     2. ::before drops a small, tight, screen-blended hotspot near
        the same point the base gradient highlights — a sharper
        specular glint sitting on top of the softer ambient one,
        which is what a rendered (rather than painted) sphere has.
     3. Inset box-shadows reinforce both ends of that gradient —
        a dark inset on the shadow side, a soft light inset near the
        highlight — the classic cheap trick for selling curvature on
        a flat circle.
   The outer box-shadow glows remain outside all of that, because
   Vega still has to read as light-emitting in a starfield — a
   sphere with zero ambient bloom would look like a marble sitting
   on black paper, not a small sun. */
/* ── The colour of Vega ─────────────────────────────────────
   The SPHERE is kept. What changed is its hue, and only its hue.

   Vega was the most saturated object on a page whose whole
   aesthetic is cold and near-monochrome: a bright green-teal body
   with a teal bloom, on a blue-white star field. It did not read
   as the brightest thing in the scene, it read as the only
   COLOURED thing in the scene, which is a different and much
   cheaper effect.

   The landing audit already had the rule, and it is worth quoting
   because the failure mode is so specific:

     "Cold blue-white glow with only a whisper of brand teal at
      the outside; leading with teal turns the mark green."

   That is exactly what had happened. Teal sat at 24% — the
   brightest, largest stop after the white core — so the eye
   integrated the whole sphere as green.

   So the body is now the temperature of a real hot star: white
   core, blue-white, then blue into deep navy on the shadowed
   side. The teal survives in exactly one place, the outermost and
   faintest bloom ring, where it tints the very edge of the light
   without ever being a colour you could name. */
.portal-vega__core {
/* Cold, but HOT. The first cold version fixed the hue and
   introduced a different problem: it read as a planet — a matte
   blue sphere lit from off-screen, somewhere between Neptune and a
   marble. Correct temperature, wrong kind of object.

   The difference between a lit sphere and a luminous one is not
   the body, it is how much light escapes it. A planet's gradient
   ends at its edge. A star's does not: the brightness carries well
   past the disc, far enough that you cannot point at where the
   object stops and the glow starts.

   So the body keeps its shading — the sphere read is what makes it
   an orb rather than a flat disc — and the bloom is where the work
   went: four rings reaching 240px, each fainter and wider than the
   last. The three inner ones are cold; the outermost is the teal,
   at a tenth opacity, which is the whisper the audit asked for and
   the only place on this screen that hue appears at rest. */
  background: radial-gradient(circle at 34% 28%,
    #ffffff 0%,
    #f6fbff 11%,
    #d3e6ff 27%,
    #8cb2f0 47%,
    #40609c 70%,
    #1c3160 87%,
    #0a1330 100%);
  box-shadow:
    /* shadowed far side of the sphere, opposite the highlight */
    inset -9px -9px 20px rgba(3,6,16,.58),
    /* faint lift near the highlight, reinforcing the curve */
    inset 5px 5px 12px rgba(255,255,255,.20),
    /* The corona. Each ring roughly doubles the last one's radius
       and drops its opacity — the falloff a real light has, rather
       than one soft halo with a findable edge. */
    0 0 30px  rgba(228,241,255,.55),
    0 0 78px  rgba(152,192,255,.38),
    0 0 160px rgba(92,150,240,.22),
    0 0 240px rgba(34,211,166,.10);
  animation: portal-breathe 5.2s ease-in-out infinite;
}
.portal-vega__core::before {
  /* The second, sharper glint. Screen-blended so it lightens
     whatever's under it rather than replacing it — a soft ambient
     highlight plus one hard glint is what separates a rendered
     sphere from a flat painted circle. */
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 30% 24%,
    rgba(255,255,255,.95) 0%,
    rgba(255,255,255,.32) 10%,
    rgba(255,255,255,0) 24%);
  mix-blend-mode: screen;
  pointer-events: none;
  animation: portal-glint-drift 9s ease-in-out infinite;
}
@keyframes portal-breathe {
  0%, 100% { transform: scale(1);    filter: brightness(1); }
  50%      { transform: scale(1.045); filter: brightness(1.11); }
}
/* Slow enough to read as "alive," not as a visible loop — the glint
   drifts a couple of percent and back, suggesting the sphere is
   very slowly turning rather than sitting static under a fixed
   light. */
@keyframes portal-glint-drift {
  0%, 100% { background-position: 0% 0%; opacity: 1; }
  50%      { background-position: 3% 2%; opacity: .88; }
}

/* One hairline, static. The previous build animated a plain solid
   circle with `rotate(360deg)` — a uniform circle rotating is
   visually identical to a uniform circle standing still, so it
   was pure cost for zero effect. */
/* The corona is Vega's hit target and its focus ring, kept faint so
   it never draws a hard outline around a deliberately soft object. */
/* Transparent by default. Vega is a soft object; drawing a crisp
   circle around it both undoes the softness and — on a page whose
   whole subject is a ring — reads as one more ring. It exists to
   carry the hit area and to appear on hover and focus. */
.portal-vega__corona {
  border: 1px solid transparent;
  transition: border-color .3s ease, box-shadow .3s ease;
}
.portal-vega:hover .portal-vega__corona,
.portal-vega:focus-visible .portal-vega__corona {
  border-color: rgba(34,211,166,.6);
  box-shadow: 0 0 0 10px rgba(34,211,166,.07);
}
.portal-vega:focus-visible { outline: 2px solid var(--accent2, #22d3a6); outline-offset: 6px; }

/* ── Nodes ──────────────────────────────────────────────────
   Anchored by their DOT, which is the point that actually sits on
   the ellipse. The label stacks above it with a hairline leader,
   so a node never needs horizontal room it might not have — the
   old side-set labels are what forced the nodes into the corners.

   --depth runs 0 (far side of the disc) to 1 (near side) and is
   written every frame by landing.js. Scale and opacity read from
   it, which is what sells the plane as inclined. */
.portal-node {
  position: absolute;
  left: var(--x, 50%);
  top: var(--y, 50%);
  z-index: 10;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  border: 0;
  background: none;
  padding: 0 var(--s-5);
  cursor: pointer;
  white-space: nowrap;
  text-align: center;

  --s: calc(.87 + var(--depth, 1) * .17);
  opacity: calc(.52 + var(--depth, 1) * .48);
  /* Anchor the DOT to (--x, --y): shift the whole column up by its
     own height, then back down by half the dot, and scale about
     that same point so depth never moves the anchor. */
  transform: translate(-50%, calc(-100% + 5px)) scale(var(--s));
  transform-origin: bottom center;
  transition: opacity .3s ease, filter .25s ease;
}

/* Near-side nodes annotate DOWNWARD.

   With every label above its dot, the node at the front of the disc
   puts its label straight through Vega — the front of an inclined
   ellipse passes directly below the centre. Flipping the near side
   to annotate downward moves those labels away from the focus
   instead of across it, and it is what a real chart would do
   anyway: callouts point out of the figure, not into it.

   landing.js adds this class when depth crosses 0.62. */
.portal-node--below {
  flex-direction: column-reverse;
  transform: translate(-50%, -5px) scale(var(--s));
  transform-origin: top center;
}

.portal-node__text { display: block; margin-bottom: var(--s-4); }
.portal-node--below .portal-node__text { margin-bottom: 0; margin-top: var(--s-4); }

.portal-node__label {
  display: block;
  font-size: var(--t-xl);
  font-weight: 500;
  letter-spacing: -.005em;
  color: var(--ink, #e9f0f7);
  transition: color .22s ease;
}
.portal-node__sub {
  display: block;
  margin-top: 3px;
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: .17em;
  text-indent: .17em;
  text-transform: uppercase;
  color: var(--dim, #5a6b80);
  transition: color .22s ease;
}

.portal-node__leader {
  display: block;
  width: 1px;
  /* Long enough for the caption above it to clear the ring the dot
     sits on. At 24px the sub-label crossed the ellipse at the two
     nodes nearest the major axis, where the curve is steepest. */
  height: 34px;
  /* Brightest at the dot end, fading toward the label — a leader
     line should read as coming FROM the point it marks. */
  background: linear-gradient(180deg, rgba(180,210,255,0), rgba(180,210,255,.55));
  transition: background .25s ease, height .25s cubic-bezier(.16,.86,.22,1);
}
.portal-node--below .portal-node__leader {
  background: linear-gradient(180deg, rgba(180,210,255,.55), rgba(180,210,255,0));
}

.portal-node__dot {
  display: block;
  width: 10px; height: 10px;
  border-radius: 50%;
  background: radial-gradient(circle at 38% 32%, #fff 0%, #cfe2ff 34%, var(--accent, #5b8cff) 72%, rgba(91,140,255,.4) 100%);
  box-shadow:
    0 0 0 1px rgba(255,255,255,.22),
    0 0 10px rgba(160,200,255,.95),
    0 0 26px rgba(91,140,255,.55);
  transition: box-shadow .25s ease, background .25s ease, transform .25s ease;
}

.portal-node:hover,
.portal-node:focus-visible { opacity: 1; outline: none; }
.portal-node:hover .portal-node__label,
.portal-node:focus-visible .portal-node__label { color: #ffffff; }
.portal-node:hover .portal-node__sub,
.portal-node:focus-visible .portal-node__sub { color: var(--accent2, #22d3a6); }
.portal-node:hover .portal-node__leader,
.portal-node:focus-visible .portal-node__leader {
  height: 36px;
  background: linear-gradient(180deg, rgba(34,211,166,0), rgba(34,211,166,.85));
}
.portal-node--below:hover .portal-node__leader,
.portal-node--below:focus-visible .portal-node__leader {
  background: linear-gradient(180deg, rgba(34,211,166,.85), rgba(34,211,166,0));
}
.portal-node:hover .portal-node__dot,
.portal-node:focus-visible .portal-node__dot {
  background: radial-gradient(circle at 38% 32%, #fff 0%, var(--accent2, #22d3a6) 58%, rgba(34,211,166,.4) 100%);
  box-shadow: 0 0 20px rgba(34,211,166,.9), 0 0 0 1px rgba(255,255,255,.22);
  transform: scale(1.15);
}
.portal-node:focus-visible .portal-node__text {
  outline: 1px solid rgba(34,211,166,.55);
  outline-offset: 6px;
  border-radius: 6px;
}
.portal-node[aria-pressed="true"] .portal-node__dot {
  background: radial-gradient(circle at 38% 32%, #fff 0%, var(--accent2, #22d3a6) 52%, rgba(34,211,166,.45) 100%);
  box-shadow: 0 0 24px rgba(34,211,166,1), 0 0 0 1px rgba(255,255,255,.3);
}

/* ── Foot ───────────────────────────────────────────────────── */
.portal-foot {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: var(--s-7);
  padding: 18px clamp(16px, 3vw, 34px);
  font-size: var(--t-md);
  color: var(--muted, #8b9bb0);
  pointer-events: none;
}
.portal-foot > * { pointer-events: auto; }
.portal-foot__link { transition: color .2s ease; }
.portal-foot__link:hover { color: var(--ink, #e9f0f7); }
.portal-foot__link:focus-visible { outline: 2px solid var(--accent2, #22d3a6); outline-offset: 3px; border-radius: var(--r-xs); }
.portal-foot__sep { width: 3px; height: 3px; border-radius: 50%; background: rgba(180,210,255,.28); }
.portal-foot__hint {
  margin-left: auto;
  font-size: var(--t-2xs);
  letter-spacing: .18em;
  text-indent: .18em;
  text-transform: uppercase;
  color: var(--dim, #5a6b80);
  pointer-events: none;
}

/* ── Fallback ───────────────────────────────────────────────── */
.portal-fallback {
  position: fixed;
  left: 50%; bottom: 64px;
  transform: translateX(-50%);
  z-index: 90;
  max-width: min(560px, calc(100vw - 32px));
  padding: 11px var(--s-8);
  border-radius: var(--r-md);
  border: 1px solid rgba(255,122,122,.34);
  background: rgba(35,10,10,.78);
  color: #ffd6d6;
  font-size: 13.5px;
  text-align: center;
}
.portal-fallback[hidden] { display: none !important; }
.portal-fallback a { text-decoration: underline; }

/* ── Responsive ─────────────────────────────────────────────── */
@media (max-width: 1080px) {
  .portal { gap: clamp(16px, 3vw, 40px); }
  .portal-hud__sub { max-width: 34ch; }
  .portal-node__label { font-size: var(--t-lg); }
}

/* Stacked. The field leads, the copy follows — same order as the
   reveal, so the page reads the same way it arrives. */
@media (max-width: 900px), (max-height: 639px) {
  .portal {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    text-align: center;
    gap: 30px;
    padding: 96px var(--s-9) 92px;
  }
  /* `order` must be set on EVERY child once it is set on any of
     them. The foot has no explicit order, so it defaults to 0 and
     renders above both — which put Pricing / FAQ / Support at the
     top of the phone layout. */
  .portal-field { order: 1; width: min(100%, 420px); }
  .portal-hud   { order: 2; max-width: 40rem; }
  .portal-foot  { order: 3; position: static; justify-content: center; flex-wrap: wrap; }

  .portal-hud__sub { margin-inline: auto; }
  .portal-hud__cta { justify-content: center; }
  .portal-readout { justify-content: center; text-align: left; }
  .portal-foot__hint { display: none; }
}

@media (max-width: 560px) {
  .portal-hud__title { font-size: clamp(32px, 9vw, 44px); }
  .portal-node__label { font-size: var(--t-md); }
  /* Gone below this width. "EVERY DEAL IN MOTION" is ~130px of
     tracked-out mono centred on a node that sits at 86% of a 350px
     field — it runs off the screen, and a caption that has to be
     clipped is a caption that was never going to be read. The name
     alone still identifies the node; the sub-line is a desktop
     luxury. */
  .portal-node__sub { display: none; }
  .portal-node__leader { height: 20px; }
  .portal-cta { font-size: 13.5px; padding: var(--s-4) var(--s-7); }
}

/* ── Reduced motion ─────────────────────────────────────────
   The disc stops drifting and Vega stops breathing, but nothing
   is hidden and every interaction still works. Reduced motion is
   a request for less movement, not for less product. */
@media (prefers-reduced-motion: reduce) {
  .portal-vega__core,
  .portal-vega__core::before { animation: none; }
  .portal-readout--typing .portal-readout__text::after { animation: none; }
  .portal-node,
  .portal-node__leader,
  .portal-node__dot,
  .portal-cta,
  .btn { transition: none; }
  body[data-cinema="done"] .portal-nav,
  body[data-cinema="done"] .portal-hud,
  body[data-cinema="done"] .portal-field,
  body[data-cinema="done"] .portal-foot {
    transition: none;
    opacity: 1;
    transform: none;
  }
}
