/* Vedika singularity — engine support layer.
 *
 * IMPORTANT: this file does NOT lay out the hero. css/vedika-cinematic-home.css
 * already carries the original hero design — copy position, the 92px DM Sans
 * headline with its serif italic em, the kicker, subtitle, HUD, status pill and
 * sound control. Duplicating those rules here only fought it: this file loads
 * later, but it never set `transform`, so cinematic-home's translateY(-46%) kept
 * winning and dragged the headline 329px off the top of the viewport.
 *
 * What belongs here is only what the engine needs and cinematic-home does not
 * provide: canvas layering, moon labels, and station gating.
 *
 * CONTENT RETENTION IS A HARD RULE: no display:none on content. Gate with
 * opacity / visibility / content-visibility so every word stays in the DOM.
 */

:root {
  --vd-lav: #a78bfa;
  --vd-ink: #05030c;
  --vd-hair: rgba(167, 139, 250, 0.20);
  --vd-mono: "Fragment Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
  --vd-nav-h: 4rem;
}

/* ---------------------------------------------------------------- canvas */

#vedika-blackhole-canvas {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  /* BEHIND all content. At z-index 0 an opaque canvas paints over every
     unpositioned in-flow section on the page. */
  z-index: -1;
  pointer-events: none;
  background: #030208;
}

/* The three.js engine owns the hero canvas.
 *
 * vedika-cinematic-home.css carries `.singularity-static-fallback
 * #vedika-blackhole-canvas { opacity: 0 }` for the OLD 2D engine, which used to
 * add that class to the hero section. The class outlived the engine: the new
 * renderer ran at full quality into a canvas the page had faded to zero, which
 * is why the disc "did not exist" while every uniform read correct.
 *
 * !important is warranted here — this is a hard override of a rule that must
 * never apply while the live engine is running. */
html.sing-engine-owns-hero #vedika-blackhole-canvas,
html.singularity-live #vedika-blackhole-canvas { opacity: 1 !important; }

/* Same story for the second background canvas: vedika-space-scroll.js painted
 * it at z-index 0, above this one at -1, so its CSS ring fallback was the
 * "black hole" on screen. */
html.sing-engine-owns-hero #vedika-space-canvas,
html.singularity-live #vedika-space-canvas { opacity: 0 !important; z-index: -2 !important; }



/* the DOM medallion is superseded by the 3D logo planet */
.singularity-core { display: none; }

/* ------------------------------------------------------- edge telemetry */


/* The HUD and the headline both live top-left, so they MUST occupy separate
 * horizontal bands. Both are fixed to the viewport — mixing fixed and absolute
 * here put them in different coordinate systems and no padding value could
 * reliably separate them. Keep these two in sync: copy padding-top must exceed
 * hud top + hud line-height. */



.singularity-button::after {
  content: "";
  position: absolute; z-index: -1;
  right: -18%; top: 50%;
  width: 62%; aspect-ratio: 1;
  transform: translateY(-50%);
  border-radius: 50%;
  background:
    radial-gradient(circle at 30% 40%, rgba(196, 181, 253, 0.95), transparent 62%),
    radial-gradient(circle at 70% 60%, rgba(129, 140, 248, 0.90), transparent 58%),
    radial-gradient(circle at 50% 80%, rgba(232, 121, 249, 0.75), transparent 60%);
  filter: blur(14px);
  opacity: 0.85;
  transition: transform 520ms cubic-bezier(0.22, 1, 0.36, 1), opacity 320ms ease;
  pointer-events: none;
}
.singularity-button:hover { transform: translateY(-2px); border-color: rgba(167, 139, 250, 0.6); color: #fff; }
.singularity-button:hover::after { transform: translateY(-50%) scale(1.5); opacity: 1; }
.singularity-button--primary { background: var(--vd-lav); border-color: var(--vd-lav); color: #0a0711; font-weight: 600; }
.singularity-button--primary::after {
  background:
    radial-gradient(circle at 30% 40%, rgba(109, 40, 217, 0.85), transparent 62%),
    radial-gradient(circle at 70% 60%, rgba(37, 99, 235, 0.75), transparent 58%),
    radial-gradient(circle at 50% 80%, rgba(219, 39, 119, 0.70), transparent 60%);
  opacity: 0.75;
}
.singularity-button > * { position: relative; z-index: 1; }
.singularity-button:focus-visible,
.singularity-sound:focus-visible { outline: 2px solid var(--vd-lav); outline-offset: 3px; }

/* --------------------------------------------------------- moon labels --- */

/* z-index 2, not 4. The moon layer is FIXED, so it escapes the hero's
 * `isolation: isolate` and lands in the ROOT stacking context — at 4 it
 * painted a full-viewport sheet over every station, and only stopped doing
 * so late in the journey because the layer fades ITSELF to 0. It is hero
 * furniture and belongs above the hero (z-index 1) but below station copy. */
.sing-moons { position: fixed; inset: 0; z-index: 2; pointer-events: none;
  transition: opacity 200ms linear; overflow: visible; }

.sing-moon {
  position: absolute; top: 0; left: 0;
  margin: -0.9rem 0 0 0.6rem;
  display: inline-flex; align-items: center; gap: 0.5rem;
  /* generous padding: at 0.28rem only the 5px dot was hoverable */
  padding: 0.55rem 0.8rem 0.55rem 0.5rem;
  border: 1px solid transparent;
  border-radius: 999px;
  text-decoration: none; white-space: nowrap;
  font-family: var(--vd-mono);
  font-size: 9.5px; letter-spacing: 0.16em; text-transform: uppercase;
  color: rgba(226, 218, 255, 0.72);
  transition: background 180ms, border-color 180ms, color 180ms;
  will-change: transform;
}
.sing-moon__dot { width: 5px; height: 5px; border-radius: 50%; flex: none;
  background: var(--vd-lav); box-shadow: 0 0 8px 1px rgba(167, 139, 250, 0.8); }
.sing-moon__body { display: inline-flex; flex-direction: column; gap: 1px; }
.sing-moon__body b { font-weight: 500; color: #efeaff; }
.sing-moon__body i,
.sing-moon__body code,
.sing-moon__body em { display: none; }

/* Hover card — the three reference pill themes, 1:1.
 *
 * POLAR (dark charcoal, warm blob), DUBDOT (white, cold blue blob) and VERCEL
 * (white, pastel blob). Each moon already carries a `kind` of 0/1/2, so the
 * three themes distribute across the sixteen surfaces instead of one style
 * repeating. Sleek and compact: a pill, not a panel.
 *
 * white-space: normal matters — with nowrap the sub-label clipped mid-word. */
.sing-moon:hover,
.sing-moon:focus-visible,
.sing-moon.is-hover {
  min-width: 196px; max-width: 232px;
  white-space: normal; overflow: hidden;
  border-radius: 999px;
  padding: 9px 15px 9px 14px;
  gap: 0;
  isolation: isolate;
  box-shadow: 0 14px 34px -14px rgba(0, 0, 0, 0.62), 0 2px 6px -2px rgba(0, 0, 0, 0.28);
}

/* the dot is the moon's tag at rest; inside the open card it is noise */
.sing-moon:hover .sing-moon__dot,
.sing-moon.is-hover .sing-moon__dot { display: none; }

/* The flowing gradient blob, bleeding in from the right edge. Animated —
 * a static gradient reads as a printed sticker. */
.sing-moon:hover::after,
.sing-moon.is-hover::after {
  content: ""; position: absolute; z-index: -1;
  right: -16%; top: 50%; width: 62%; aspect-ratio: 1.15;
  transform: translateY(-50%); border-radius: 50%;
  background-size: 260% 260%;
  filter: blur(13px); opacity: 0.95;
  animation: sing-moon-flow 8s ease-in-out infinite alternate;
  pointer-events: none;
}
@keyframes sing-moon-flow {
  0%   { background-position:   0% 50%; transform: translateY(-50%) scale(1.00); }
  50%  { background-position: 100% 20%; transform: translateY(-50%) scale(1.16); }
  100% { background-position:  40% 90%; transform: translateY(-50%) scale(1.05); }
}

/* ---- theme 0: POLAR — dark pill, warm blob */
.sing-moon--k0:hover, .sing-moon--k0.is-hover {
  background: #17161a; border-color: rgba(255, 255, 255, 0.10); color: #fff;
}
.sing-moon--k0:hover::after, .sing-moon--k0.is-hover::after {
  background:
    radial-gradient(circle at 30% 40%, rgba(255, 214, 92, 0.98), transparent 58%),
    radial-gradient(circle at 66% 56%, rgba(255, 110, 64, 0.95), transparent 56%),
    radial-gradient(circle at 50% 84%, rgba(255, 62, 138, 0.88), transparent 58%);
}

/* ---- theme 1: DUBDOT — white pill, cold blue blob */
.sing-moon--k1:hover, .sing-moon--k1.is-hover {
  background: #f7f7fa; border-color: rgba(15, 18, 32, 0.08); color: #0d0f16;
}
.sing-moon--k1:hover::after, .sing-moon--k1.is-hover::after {
  background:
    radial-gradient(circle at 28% 42%, rgba(120, 205, 255, 0.98), transparent 58%),
    radial-gradient(circle at 68% 54%, rgba(38, 132, 255, 0.95), transparent 56%),
    radial-gradient(circle at 52% 84%, rgba(150, 225, 255, 0.85), transparent 58%);
}

/* ---- theme 2: VERCEL — white pill, pastel blob */
.sing-moon--k2:hover, .sing-moon--k2.is-hover {
  background: #fbfbfd; border-color: rgba(15, 18, 32, 0.07); color: #0d0f16;
}
.sing-moon--k2:hover::after, .sing-moon--k2.is-hover::after {
  background:
    radial-gradient(circle at 30% 38%, rgba(255, 176, 205, 0.95), transparent 58%),
    radial-gradient(circle at 66% 58%, rgba(150, 225, 255, 0.92), transparent 56%),
    radial-gradient(circle at 48% 84%, rgba(255, 214, 168, 0.82), transparent 58%);
}

/* ---- type, shared */
.sing-moon:hover .sing-moon__body,
.sing-moon.is-hover .sing-moon__body { gap: 2px; }

.sing-moon:hover .sing-moon__body b,
.sing-moon.is-hover .sing-moon__body b {
  font-family: "OverusedGrotesk", "Plus Jakarta Sans", system-ui, sans-serif;
  font-size: 14px; font-weight: 800; letter-spacing: -0.01em;
  text-transform: none; line-height: 1.15; color: inherit;
}
.sing-moon:hover .sing-moon__body i,
.sing-moon.is-hover .sing-moon__body i {
  display: block; font-style: normal;
  font-family: var(--vd-mono);
  font-size: 7.5px; letter-spacing: 0.15em; text-transform: uppercase;
  line-height: 1.3; white-space: nowrap;
  opacity: 0.62; color: inherit;
}
/* the endpoint and the call-to-action are panel furniture; the reference pill
   carries exactly two lines, so they stay out of the open card */
.sing-moon:hover .sing-moon__body code,
.sing-moon.is-hover .sing-moon__body code,
.sing-moon:hover .sing-moon__body em,
.sing-moon.is-hover .sing-moon__body em { display: none; }

/* -------------------------------------------------- scroll-hijack stations */

html.sing-hijack, html.sing-hijack body { height: 100%; overflow: hidden; overscroll-behavior: none; }
html.sing-hijack #main-content,
html.sing-native  #main-content { position: fixed; inset: 0; z-index: 1; overflow: hidden; }

html.sing-hijack .sing-station,
html.sing-native  .sing-station {
  /* ABOVE THE HERO, OR NOTHING POST-HORIZON IS EVER SEEN.
   *
   * .vedika-singularity is `position: relative; z-index: 1; isolation: isolate`
   * and it is a SIBLING of these stations inside #main-content. The isolate
   * traps its canvas's own z-index:-1 inside the hero's stacking context, so
   * the entire hero layer — opaque canvas included — painted OVER all 22
   * stations for the whole journey. The content was never missing; it was
   * covered.
   *
   * This was invisible to every check we ran because the canvas is
   * pointer-events:none, so document.elementFromPoint happily returns the
   * station underneath it. The page HIT-TESTS clean while painting nothing.
   * Verify this rule with a PAINT test, never a hit test.
   *
   * The site <footer> was the one thing that always rendered correctly, and
   * that is the confirming evidence: it is a sibling of <main>, so it was
   * never inside the covered subtree. */
  z-index: 3;
  position: absolute; inset: 0;
  display: grid;
  /* safe center: plain center traps the top of any station taller than the
     viewport, because the overflow spills in BOTH directions */
  align-content: safe center;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: calc(var(--vd-nav-h) + 1rem) clamp(1rem, 5vw, 4rem) clamp(3.5rem, 7vh, 5rem);
  opacity: 0;
  visibility: hidden;
  content-visibility: hidden;
  /* NO TRANSITION ON OPACITY.
   *
   * The engine writes el.style.opacity every frame from the scroll position, so
   * a CSS transition on top of it can only ever lag it — and because the value
   * changes again on the next frame the transition restarts before it lands, so
   * it never catches up while the journey is moving.
   *
   * Measured, page held still for 900 ms at p = 0.20: the OUTGOING station was
   * written to opacity 0 but still PAINTED at 0.909, and the INCOMING station
   * was written to 1 but painted at 0.091. The screen was showing the previous
   * station at nine-tenths strength and the current one at one-tenth. That is
   * "i see this data but too weak to see" and "its disappearing the shown one"
   * in a single bug. The frame loop is the animation; nothing else may smooth
   * it. */
  transition: none;
}

/* THE STATIONS' CONTAINING BLOCK MUST BE THE VIEWPORT.
 *
 * `position: absolute; inset: 0` is only the viewport if nothing between the
 * station and #main-content is positioned, and if nothing beats this rule on
 * specificity. Both assumptions were false, and both cost whole stretches of
 * the journey:
 *
 *  1. buildStations() flattens <div class="relative"> wrappers and marks them
 *     .sing-passthrough. Tailwind's `relative` makes that wrapper a positioning
 *     context, and under the hijack it is a 0px-tall box sitting at the very
 *     BOTTOM of the fixed viewport. The six sections inside it therefore
 *     resolved inset: 0 against that — measured 141px tall at y = 825...1411 in
 *     an 869px viewport, i.e. entirely below the fold, at full opacity, for
 *     p = 0.16...0.35. That is the founder's "no data post 40%".
 *
 *  2. #pricing carries an INLINE style="position: relative" in index.html, and
 *     no selector of any specificity can beat an inline declaration. That
 *     dropped station 17 out of the overlay and into normal flow at y = 1971 —
 *     off-screen for the whole of p = 0.66...0.70, which is the pricing
 *     section, i.e. the one station on the page that sells anything.
 *
 * So: neutralise the wrapper, and re-assert the geometry with !important. The
 * !important is scoped to html.sing-hijack / html.sing-native, so it exists
 * only while the engine owns the page — the reduced-motion and no-webgl
 * documents are untouched and keep the author's inline positioning. Nothing is
 * hidden here; this is pure positioning. */
html.sing-hijack .sing-passthrough,
html.sing-native  .sing-passthrough { position: static; }

html.sing-hijack .sing-station,
html.sing-native  .sing-station {
  position: absolute !important;
  inset: 0 !important;
  width: auto !important;
  height: auto !important;
  margin: 0 !important;
  float: none !important;
}

/* ------------------------------------------------------------- OFFSTAGE ---
 *
 * Sections that buildStations() judged to be page furniture rather than beats.
 * They keep every word, every link and every heading, in DOM order, and they
 * render completely normally in the reduced-motion / no-WebGL document — this
 * rule only exists while the engine owns the page.
 *
 * NOT display:none, NOT visibility:hidden. They are simply parked at the bottom
 * edge of #main-content's fixed viewport box, where its own `overflow: hidden`
 * clips them. Crawlers see ordinary in-flow content; the journey does not have
 * to give a 77px logo bar a full cinematic frame.
 *
 * !important for the same reason .sing-station needs it: #pricing and friends
 * carry inline `position: relative` in index.html, and no selector beats an
 * inline declaration. */
html.sing-hijack .sing-offstage,
html.sing-native  .sing-offstage {
  position: absolute !important;
  top: 100vh !important;
  left: 0 !important;
  width: 100% !important;
  margin: 0 !important;
  float: none !important;
  pointer-events: none;
}

/* --------------------------------------------------- fit a station on screen
 *
 * The wheel is hijacked (window wheel handler calls preventDefault), so the
 * `overflow-y: auto` on a station is unreachable by the user — anything taller
 * than the viewport simply had its bottom cut off forever. Five of the kept
 * stations are 1.4-2.3 viewports tall.
 *
 * The engine measures each station the first time it goes live and sets
 * --sing-fit, a zoom factor floored at a readable value; whatever still
 * overflows after that is scrolled by the journey itself (el.scrollTop, driven
 * from the slot position) so the whole station is seen without the user
 * needing a scrollbar the hijack has taken away.
 *
 * zoom, not transform: scale — the station's transform is already written every
 * frame for the parallax drift, and zoom reflows rather than squashing. */
html.sing-hijack .sing-station,
html.sing-native  .sing-station { zoom: var(--sing-fit, 1); }

/* The adopted footer is a SIBLING of #main-content, which carries z-index: 1,
 * so the ending of the journey painted and hit-tested underneath it. Nothing
 * inside #main-content is opaque by then, so it did not read as missing — but
 * every footer link in the last 17% of the scroll was unclickable, because the
 * hit test landed on <main> instead. It is the incoming station for that whole
 * span, so it belongs on top. */
html.sing-hijack body > footer.sing-station,
html.sing-native  body > footer.sing-station { z-index: 2; }
html.sing-hijack .sing-station.is-live,
html.sing-native  .sing-station.is-live {
  visibility: visible;
  content-visibility: visible;
  will-change: opacity, transform;
}
html.sing-hijack .vedika-singularity,
html.sing-native  .vedika-singularity.sing-hero-retired { pointer-events: none; }

/* Once the hole has swallowed the hero, take it out of the way.
 *
 * This rule previously had NO DECLARATION BLOCK — the selector ran straight
 * into the next line and CSS silently merged them, so `sing-hero-retired` was
 * a class the engine toggled that did nothing at all. The hero chrome sits at
 * z-index 6-8, above station content, so it kept painting over every station
 * for the whole rest of the journey.
 *
 * visibility + pointer-events, never display:none: all 48k characters of copy
 * stay in the DOM and stay crawlable. */
.vedika-singularity.sing-hero-retired {
  pointer-events: none;
  visibility: hidden;
}
.vedika-singularity.sing-hero-retired #vedika-blackhole-canvas { visibility: visible; }

html.sing-native body { min-height: 460vh; }

/* --------------------------------------------------------------- fallback */

html.no-webgl #vedika-blackhole-canvas {
  background:
    radial-gradient(ellipse at 50% 46%, rgba(167,139,250,0.22), transparent 60%),
    #05030c;
}
/* This used to be a bare `html.no-webgl` selector with NO declaration block,
 * immediately followed by an unclosed `@media (max-width: 767px) {`. CSS then
 * swallows everything that follows as part of one enormous selector, so every
 * rule appended after this point was silently dead — which is why the CTA
 * pills and the support-widget placement had no effect at all. */
@media (max-width: 767px) {
  html.no-webgl #vedika-blackhole-canvas { background-size: cover; }
}

/* ------------------------------------------------- corner furniture spacing */

/* The support widget is fixed at right:20px / bottom:96px with z-index 999999,
 * which lands it on top of the "Read the API docs" CTA and the status pill now
 * that the copy is corner-placed. It is injected by vedika-support-widget.js
 * and cannot be moved from there without touching that script, so it is nudged
 * into the true corner here and the hero furniture is given room. */
#vedika-support-widget.vedika-widget-container {
  right: 18px !important;
  bottom: 18px !important;
}

/* status pill sits above the widget's column, not under it */
.singularity-status {
  right: clamp(96px, 8vw, 132px) !important;
}

/* ------------------------------------------------------------- hero CTAs ---
 *
 * The two hero buttons are the same family as the moon hover cards: fully
 * round pills with a flowing gradient blob bleeding in from the right, in the
 * reference's POLAR / DUBDOT / VERCEL treatment. They were square-cornered
 * panels, which read as a different design system from everything else on the
 * canvas.
 *
 * These override vedika-cinematic-home.css, which loads earlier. */
.singularity-actions .singularity-button {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  border-radius: 999px;
  min-width: 0;
  padding: 13px 22px 13px 24px;
  gap: 18px;
  font: 700 13px/1 "OverusedGrotesk", "Plus Jakarta Sans", "DM Sans", system-ui, sans-serif;
  letter-spacing: -0.01em;
  backdrop-filter: none;
  box-shadow: 0 14px 34px -14px rgba(0, 0, 0, 0.62), 0 2px 6px -2px rgba(0, 0, 0, 0.28);
  transition: transform .25s cubic-bezier(.22,1,.36,1), box-shadow .25s;
}

.singularity-actions .singularity-button > * { position: relative; z-index: 1; }

.singularity-actions .singularity-button::before { content: none; }

.singularity-actions .singularity-button::after {
  content: "";
  position: absolute; z-index: -1;
  right: -14%; top: 50%;
  width: 56%; aspect-ratio: 1.15;
  transform: translateY(-50%);
  border-radius: 50%;
  background-size: 260% 260%;
  filter: blur(14px);
  opacity: 0.95;
  animation: sing-moon-flow 8s ease-in-out infinite alternate;
  pointer-events: none;
}

/* primary — the VERCEL pill: near-white with a pastel blob */
.singularity-actions .singularity-button--primary {
  background: #fbfbfd;
  border-color: rgba(15, 18, 32, 0.07);
  color: #0d0f16;
}
.singularity-actions .singularity-button--primary::after {
  background:
    radial-gradient(circle at 30% 38%, rgba(196, 181, 253, 0.98), transparent 58%),
    radial-gradient(circle at 66% 58%, rgba(129, 140, 248, 0.95), transparent 56%),
    radial-gradient(circle at 48% 84%, rgba(232, 121, 249, 0.82), transparent 58%);
}

/* secondary — the POLAR pill: charcoal with a cool blob */
.singularity-actions .singularity-button:not(.singularity-button--primary) {
  background: #17161a;
  border-color: rgba(255, 255, 255, 0.10);
  color: #fff;
}
.singularity-actions .singularity-button:not(.singularity-button--primary)::after {
  background:
    radial-gradient(circle at 28% 42%, rgba(150, 225, 255, 0.95), transparent 58%),
    radial-gradient(circle at 68% 54%, rgba(129, 140, 248, 0.92), transparent 56%),
    radial-gradient(circle at 52% 84%, rgba(196, 181, 253, 0.85), transparent 58%);
}

.singularity-actions .singularity-button:hover {
  transform: translateY(-2px);
  box-shadow: 0 20px 44px -14px rgba(0, 0, 0, 0.72), 0 3px 8px -2px rgba(0, 0, 0, 0.32);
}

/* keep the support widget clear of the CTA row entirely */
#vedika-support-widget.vedika-widget-container {
  right: 16px !important;
  bottom: 16px !important;
  transform: scale(0.82);
  transform-origin: bottom right;
}

/* Labels left of frame centre must open LEFTWARD.
 *
 * updateMoons has always toggled `is-left` on them, but no rule ever matched
 * it, so every card grew rightward regardless of which side of the hole it
 * was on — and the left-hand ones fanned inward across the singularity. */
.sing-moon.is-left {
  flex-direction: row-reverse;
  margin-left: 0;
  margin-right: 0.6rem;
  transform-origin: right center;
}
.sing-moon.is-left .sing-moon__body { text-align: right; }
.sing-moon.is-left:hover,
.sing-moon.is-left.is-hover { transform-origin: right center; }

/* ============================================================================
 * BEATS — one distilled line, centred in the 3D scene
 * ==========================================================================
 *
 * WHAT USED TO BE HERE, AND WHY IT IS GONE.
 *
 * 567 lines: a flowing aurora "sheet" drawn as .sing-station.is-live::before, a
 * masked instrument grid as ::after, two keyframe animations, and a large
 * contrast pass (an --ink-* palette, a light-card fence, nine missing Tailwind
 * text utilities, four CTA gradient darkenings) whose entire purpose was to make
 * fifteen sections that were AUTHORED FOR A WHITE PAGE legible when the engine
 * showed them full-viewport over a dark moving scene.
 *
 * The sections are no longer shown. A station now presents as a beat — the
 * section's own heading, its own display statistic and one line of its own
 * standfirst, centred over the landscape — and the section itself is painted at
 * zero for the whole journey. It is still in the DOM, still in reading order,
 * still crawlable, still in the accessibility tree for its own slot. It is just
 * not the picture any more.
 *
 * So every rule in that block had become unreachable: it selected
 * `html.sing-hijack .sing-station.is-live`, and everything that matches that is
 * inside an element at `opacity: 0`. Keeping 567 lines of contrast tuning for a
 * layer that cannot paint is how a stylesheet starts lying about what the page
 * does. The measurements that produced it are preserved in git history
 * (`git log -S "STATION INK" css/vedika-singularity.v2.css`) and would be the
 * starting point if the sheets ever come back.
 *
 * Nothing here hides content. The two rules that block DID own and that still
 * matter — the reduced-motion document's grounds — are below, untouched.
 *
 * ---------------------------------------------------------------------------
 *
 * THE BEAT, from REF-MESH3D-ENDING.md §3 and §8. One beat there, entire:
 *
 *     Unique makers (235)
 *     STUDIOS AND SOLO BUILDERS SHIPPING THE WORK.
 *
 *   headline   8vw, line-height 1, letter-spacing -0.01em, weight 400, #ffffff,
 *              mix-blend-mode: difference
 *   (n)        0.32em of the headline, vertical-align: super, #bababa
 *   pre-label  18px, uppercase, letter-spacing 0.02em, 56px above
 *   caption    14px, uppercase, letter-spacing 0.02em, #cfdee5
 *   reveal     per character, from translateY(110%), 700ms
 *              cubic-bezier(0.22,1,0.36,1), 35ms stagger; exit to
 *              translateY(-110%) — in from below, out through the top
 *
 * THE ONE DECISION THAT CARRIES THE LOOK is `mix-blend-mode: difference` on
 * white type. Over the near-black throat it renders pure white; where it crosses
 * the violet disc and the acid particles it inverts. The type is not "on top of"
 * the scene, it is reacting to it, for free, with no per-frame work.
 */

/* The ground the blend inverts against. One wide radial, no edge, no border, no
 * animation — at the corners it is fully transparent, so it reads as the scene
 * getting deeper toward the middle rather than as a panel behind the type. Its
 * opacity is written every frame from the strongest beat on screen. */
.sing-beats-veil {
  position: fixed;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(64% 48% at 50% 46%,
    rgba(5, 3, 16, 0.74) 0%,
    rgba(5, 3, 16, 0.52) 42%,
    rgba(5, 3, 16, 0.20) 68%,
    rgba(5, 3, 16, 0) 88%);
}

.sing-beats {
  /* FIXED, ON <body>, ABOVE THE CANVAS.
   *
   * The same trap that made every station invisible for the whole journey
   * applies here: .vedika-singularity is `z-index: 1; isolation: isolate` and
   * its canvas is trapped inside that stacking context, so anything that has to
   * paint over the scene must be a SIBLING of #main-content, not a descendant
   * of the hero. .sing-moons is fixed on <body> at z-index 4 for exactly this
   * reason; the beats sit one above it.
   *
   * Verify this rule with a PAINT test, never a hit test — the canvas is
   * pointer-events: none, so elementFromPoint returns a fully-covered element
   * quite happily. That is how the invisible-content bug survived. */
  position: fixed;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  /* THE BLEND HAS TO BE ON THE LAYER. MEASURED, NOT ASSUMED.
   *
   * With `mix-blend-mode: difference` one level down on .sing-beat__stack and a
   * solid rgb(255,0,0) slab injected under this layer, the sampled frame came
   * back 11,879 pure-white glyph pixels and ZERO cyan: the blend never reached
   * the slab, because a fixed, z-indexed layer is a stacking context and
   * confines it. On the layer it works — and that is why the labels had to move
   * out to .sing-beats-chrome instead. */
  mix-blend-mode: difference;
  /* All fifteen beats occupy ONE grid cell, centred. Grid rather than
     `left: 50%; transform: translate(-50%, -50%)` because `transform` would make
     .sing-beat its own stacking context for no gain. Padding, not `top: 46%`,
     carries the reference's slight lift above centre. */
  display: grid;
  place-items: center;
  padding-bottom: 7vh;
  font-family: "OverusedGrotesk", "DM Sans", ui-sans-serif, system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* The plain layer: pre-label, caption, pill. Same viewport box, same --beat-dy,
 * no blend. Its children are positioned from the headline's measured box by
 * fitBeats(), which is what holds the two layers together. */
.sing-beats-chrome {
  position: fixed;
  inset: 0;
  z-index: 6;
  pointer-events: none;
  font-family: "OverusedGrotesk", "DM Sans", ui-sans-serif, system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

.sing-beat {
  grid-area: 1 / 1;
  width: min(92vw, 1560px);
  text-align: center;
  margin-top: var(--beat-dy, 0px);
}

.sing-beat-chrome {
  position: absolute;
  inset: 0;
  transform: translateY(var(--beat-dy, 0px));
}

/* WHAT IS BLENDED AND WHAT IS NOT.
 *
 * The reference blends the HEADLINE and sets its pre/post labels flat in #bababa
 * (REF-MESH3D-ENDING.md §8). That split is not a stylistic detail, it is a
 * legibility requirement, and our scene makes it sharper than theirs: difference
 * INVERTS, so white type is brilliant over the black throat and goes dark olive
 * over the bright particle ring. At 100px that reads as the type reacting to the
 * scene, which is the whole effect. At 13px it reads as a label that has
 * disappeared — measured on the first pass, "PRODUCT CATEGORIES" and the caption
 * under it were all but invisible against the disc. */
.sing-beat__pre {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--pre-bottom, 60%);
  font-size: clamp(12px, 1vw, 15px);
  line-height: 1.4;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  text-align: center;
  color: #d9d2f2;
  text-shadow: 0 1px 18px rgba(6, 3, 18, 0.95), 0 0 4px rgba(6, 3, 18, 0.8);
  opacity: var(--beat-o, 0);
}

.sing-beat__foot {
  position: absolute;
  left: 0;
  right: 0;
  top: var(--foot-top, 60%);
  padding: 0 clamp(1rem, 4vw, 4rem);
  text-align: center;
}

.sing-beat__stack {
  position: relative;
  font-size: var(--beat-fs, 8vw);
  letter-spacing: -0.015em;
  font-weight: 400;
  color: #ffffff;
  opacity: var(--beat-o, 0);
  will-change: opacity;
}

/* LEADING HAS TO BE DECLARED ON EVERY LEVEL, NOT INHERITED.
 *
 * Measured with it set once on the stack: computed line-height on
 * .sing-beat__head came back 157.5px at font-size 92.66px — 1.7, not 1.02. The
 * page carries element-level line-height rules (redesign.css and the Tailwind
 * build both set it on bare elements), and an element rule beats inheritance
 * from an ancestor no matter how specific that ancestor's selector is. The
 * result was a 1.7 headline with a hole between its two lines, and — worse —
 * fitBeats() measuring those inflated heights and shrinking every beat to fit
 * leading that was never wanted. */
.sing-beat__head,
.sing-beat__ghost,
.sing-beat__line,
.sing-beat .bw,
.sing-beat .bc { line-height: 1.02; }

.sing-beat__line { display: block; }

/* The atom source. Same lines, plain text, laid out over the visible type and
 * never transformed by the roll — buildTextAtoms rasterises whatever it is given
 * AT THE SCREEN POSITION IT OCCUPIES, and sampling the rolling characters would
 * freeze a mid-animation layout into the cached cloud.
 *
 * visibility: hidden, deliberately: it keeps full layout (which is the entire
 * point) while painting nothing. It is aria-hidden in the markup and it is a
 * duplicate of copy that is already in the page twice over. */
.sing-beat__ghost {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  visibility: hidden;
  pointer-events: none;
}

/* the superscript count, exactly as the reference sets it */
.sing-beat__num {
  font-size: 0.32em;
  vertical-align: super;
  margin-left: 0.08em;
  letter-spacing: 0;
  color: #bcb5da;
}

.sing-beat__sub {
  font-size: clamp(12px, 1vw, 15px);
  font-weight: 500;
  letter-spacing: 0.18em;
  line-height: 1.7;
  text-transform: uppercase;
  color: #e8e3ff;
  /* unblended, so it needs its own ground against a moving field. A tight dark
     glow rather than a plate: the line still floats. */
  text-shadow: 0 1px 18px rgba(6, 3, 18, 0.95), 0 0 4px rgba(6, 3, 18, 0.8);
  opacity: var(--beat-o, 0);
}

/* ---- the roster: portraits as accretion systems ---------------------------
 *
 * The faces the section already carries, in the chrome layer, under the sub-line.
 * Nothing textual is invented: the names and roles are the section's own words.
 *
 * WHY NOT A RECTANGLE, OR EVEN A PLAIN CIRCLE. A flat box around a headshot would
 * be the one un-lensed thing in a frame that is otherwise all infall - it reads as
 * a CMS avatar pasted onto a render. So each portrait is built out of the scene's
 * own grammar at portrait scale: a nebula bed, two counter-rotating accretion
 * rings, a hard hexagonal bevel, and a specular sweep, arranged on real Z so the
 * photo sits IN FRONT of its own ring rather than beside it. All CSS, on the
 * page's own violet and azure. No new asset, nothing to download, nothing to rot.
 *
 * THE OPACITY IS A PRODUCT, WHICH IS WHY IT IS SPLIT ACROSS ELEMENTS.
 * A member obeys two independent things at once - the beat's scroll fade
 * (--beat-o, rewritten every frame) and its own arrival (0 -> 1, once, off
 * .is-in). Writing both to one element means one clobbers the other, and scrubbing
 * a transition from the frame loop is the bug documented at the --beat-o write in
 * the engine: it restarts every frame and never lands. So the WRAP carries the
 * scroll fade and the PARTS carry the arrival, and the compositor multiplies them.
 *
 * The isolated group that opacity < 1 creates here is harmless, and load-bearing:
 * this is the UNBLENDED layer (that is why the roster is in it at all) and the
 * isolation is also what keeps the glint's screen blend off the WebGL canvas. */
.sing-beat__roster {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-start;
  gap: clamp(26px, 4vw, 60px);
  margin-top: clamp(18px, 2.4vh, 36px);
  opacity: var(--beat-o, 0);
}

/* Layout and the 3D stage, and nothing else - no opacity of its own. Stacking a
 * third fade here would multiply into the two below and the members would arrive
 * grey. `perspective` is what makes translateZ read as depth instead of scale. */
.sing-beat__member {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: clamp(120px, 13vw, 172px);
  perspective: 900px;
}

/* ---- the portal ----
 *
 * ONE DELAY VARIABLE, THREE DERIVED TIMES. The engine writes --m-delay once per
 * member (its slot in the row); the portal fires on it, the name 140ms later, the
 * role 240ms later. That is "one by one" across the row and "synced" within a
 * member from a single number - the two staggers cannot drift apart because there
 * is only one clock to change. */
.sing-beat__portal {
  position: relative;
  /* THE BOX CONTAINS THE HALO. THAT IS THE WHOLE POINT OF THIS SIZE.
   *
   * First pass sized this box to the HEXAGON and hung the glow off it with
   * negative insets, which looks equivalent and is not: negative insets overflow
   * the layout box, so flow layout below has no idea they exist. Measured, at
   * 1440x900 - portal box ended at 689, name started at 707, and the nebula ran
   * to 729 with the outer ring at 732. The halo was painting straight down
   * THROUGH the name and into the role, which read exactly like the frame being
   * too tall and the caption sitting on the photo.
   *
   * So the box is now the size of the whole visual and every plate is inset
   * POSITIVELY inside it. Nothing overflows, the margin below can go back to a
   * normal 12px, and the total vertical footprint is unchanged (150 against the
   * old 110 + 26 of overhang + 18 of margin) - it is the same picture with an
   * honest box around it. */
  width: clamp(104px, 10.4vw, 150px);
  height: clamp(104px, 10.4vw, 150px);
  margin-bottom: clamp(10px, 1.2vh, 16px);
  transform-style: preserve-3d;
  /* the resting state is the OUT state: turned away, pushed back into depth */
  transform: rotateX(22deg) rotateY(-40deg) translateZ(-90px) scale(0.82);
  opacity: 0;
  transition:
    transform 820ms cubic-bezier(0.22, 1, 0.36, 1),
    opacity 620ms ease;
  transition-delay: var(--m-delay, 0ms);
}
/* it does not settle flat - a residual tilt is what says "object", not "sticker" */
.sing-beat-chrome.is-in .sing-beat__portal {
  transform: rotateX(9deg) rotateY(-7deg) translateZ(0) scale(1);
  opacity: 1;
}

/* the bed: a violet/azure cloud wider than the frame, sunk behind everything */
.sing-beat__neb {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background:
    radial-gradient(circle at 32% 28%, rgba(139, 92, 246, 0.55), transparent 58%),
    radial-gradient(circle at 72% 70%, rgba(56, 189, 248, 0.42), transparent 62%),
    radial-gradient(circle at 50% 50%, rgba(12, 6, 30, 0.92), transparent 74%);
  filter: blur(9px);
  transform: translateZ(-26px);
}

/* THE RINGS SPIN ONLY WHILE THE BEAT IS ON SCREEN.
 * Six portraits x two rings is twelve infinite animations, and left running they
 * would burn compositor work behind a section nobody is looking at - the sort of
 * idle cost that surfaces later as a dropped frame in the hero gate. Paused is
 * the default; .is-in is the only thing that starts them. */
.sing-beat__ring {
  position: absolute;
  border-radius: 50%;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  animation-play-state: paused;
}
.sing-beat-chrome.is-in .sing-beat__ring { animation-play-state: running; }

/* Counter-rotating, at 22s against 14s. Two rings turning the same way at
 * different speeds read as a mistake; opposed, they read as a system with angular
 * momentum, which is what the rest of the page is about.
 *
 * The mask is what makes each one a RING rather than a disc - a conic gradient is
 * a filled pie, and the radial mask cuts the annulus out of it. */
.sing-beat__ring--out {
  /* 4% in from the box edge, which is still 19px OUTSIDE the hexagon: the ring has
     to clear the plate's widest points or it only shows in the notches beside the
     flat edges, and four stray arcs do not read as one ring in orbit. */
  inset: 4%;
  background: conic-gradient(from 0deg,
    rgba(167, 139, 250, 0) 0deg, rgba(167, 139, 250, 0.95) 34deg,
    rgba(56, 189, 248, 0.30) 96deg, rgba(167, 139, 250, 0) 168deg,
    rgba(244, 114, 182, 0.55) 232deg, rgba(167, 139, 250, 0) 300deg);
  -webkit-mask: radial-gradient(circle, transparent 63%, #000 67%, #000 82%, transparent 86%);
  mask: radial-gradient(circle, transparent 63%, #000 67%, #000 82%, transparent 86%);
  transform: translateZ(-8px);
  animation-name: sing-ring-cw;
  animation-duration: 22s;
}
.sing-beat__ring--in {
  inset: 13%;
  background: conic-gradient(from 180deg,
    rgba(255, 255, 255, 0) 0deg, rgba(219, 234, 254, 0.92) 26deg,
    rgba(167, 139, 250, 0) 92deg, rgba(56, 189, 248, 0.70) 210deg,
    rgba(255, 255, 255, 0) 286deg);
  -webkit-mask: radial-gradient(circle, transparent 76%, #000 80%, #000 92%, transparent 96%);
  mask: radial-gradient(circle, transparent 76%, #000 80%, #000 92%, transparent 96%);
  transform: translateZ(-4px);
  animation-name: sing-ring-ccw;
  animation-duration: 14s;
}

/* BOTH KEYFRAMES DECLARE `from` EXPLICITLY, WITH THE SAME FUNCTION LIST.
 *
 * This is not style. Omitting `from` takes the base transform, translateZ(-8px),
 * which is a ONE-function list against the two-function list in `to` - mismatched
 * lists make CSS fall back to interpolating the composed MATRICES, and the matrix
 * of rotate(360deg) is the identity matrix. The ring would animate from itself to
 * itself and sit perfectly still. Matching lists interpolate per function, so
 * 0deg -> 360deg is a real turn. */
@keyframes sing-ring-cw {
  from { transform: translateZ(-8px) rotate(0deg); }
  to   { transform: translateZ(-8px) rotate(360deg); }
}
@keyframes sing-ring-ccw {
  from { transform: translateZ(-4px) rotate(0deg); }
  to   { transform: translateZ(-4px) rotate(-360deg); }
}

/* The hexagon. Shared by all three stacked plates so their edges register
 * exactly; a 1px disagreement here reads as a printing misalignment. */
.sing-beat__bevel,
.sing-beat__face,
.sing-beat__glint {
  position: absolute;
  -webkit-clip-path: polygon(50% 0%, 96% 25%, 96% 75%, 50% 100%, 4% 75%, 4% 25%);
  clip-path: polygon(50% 0%, 96% 25%, 96% 75%, 50% 100%, 4% 75%, 4% 25%);
}

/* the lit frame edge: the photo is inset 3px further, so this shows as a hard
 * 3px bevel. 17% in from the box leaves the ring lane around it. */
.sing-beat__bevel {
  inset: 17%;
  background: linear-gradient(150deg, #c4b5fd 0%, #7c3aed 38%, #38bdf8 72%, #e9d5ff 100%);
  transform: translateZ(0);
}

/* THE SIZE IS COMPUTED, NOT INHERITED FROM THE INSETS.
 *
 * `inset: 3px; width: auto` is correct for a div and WRONG for an image, and the
 * difference is visible: a REPLACED element with auto width resolves to its
 * INTRINSIC size, not to the box its insets describe. Measured on the first pass -
 * abhishek-singh.jpeg is 754x800 and vansh-joshi.webp is 1200x2106, so one
 * hexagon came out near-square and the other came out half again as tall, and the
 * taller one grew straight down through its own name. Two portraits in a row at
 * two different heights, from a rule that looks symmetric.
 *
 * calc(100% - 6px) states the size the insets only implied. object-fit then does
 * the job it was always meant to do: crop a 1:1.75 portrait into a square hexagon
 * instead of stretching the box to fit the photo. */
.sing-beat__face {
  /* 17% (the bevel) + 3px (the bevel's visible thickness). The width has to be
     stated because this is a REPLACED element - see the note above. */
  inset: calc(17% + 3px);
  width: calc(66% - 6px);
  height: calc(66% - 6px);
  object-fit: cover;
  object-position: 50% 22%;   /* heads sit high in a headshot; centre crops foreheads */
  background: #0a061a;
  transform: translateZ(2px);
}

/* A specular sweep across the glass, in front of the photo. It SWEEPS - it does
 * not sit there.
 *
 * First pass parked a 0.58-white band at a fixed 118deg and left it, which is not
 * a glint, it is a scratch. Measured on the founder's own portrait at p=0.69:
 * mean luma across the face +17.3, 31.7% of the face brightened by more than
 * 20/255, peak brightening 147/255 - over half the dynamic range - and contrast
 * down to 94% of the clean photo. Reported as "weird shiny line" and "photo not
 * visible", and both halves of that were the same layer: a permanent haze reads
 * as a mark on the glass AND washes out the face underneath it.
 *
 * A real highlight is an EVENT. So the band now travels across on the same gate
 * and the same --m-delay clock as the rest of the member, once, and ends off the
 * far edge at zero - the resting state of this layer is now nothing at all, which
 * is what leaves the photograph clean. Peak alpha 0.58 -> 0.20 because a sweep is
 * read as motion and needs nowhere near the amplitude a static band was using to
 * be noticed.
 *
 * EVERY KEYFRAME DECLARES THE SAME TWO-FUNCTION TRANSFORM. Same trap the rings
 * document above: mismatched function lists make CSS interpolate the composed
 * matrices instead of per-function, and the sweep would never move. */
.sing-beat__glint {
  inset: calc(17% + 3px);
  background: linear-gradient(118deg,
    rgba(255, 255, 255, 0) 38%, rgba(255, 255, 255, 0.20) 50%, rgba(255, 255, 255, 0) 62%);
  mix-blend-mode: screen;
  transform: translateZ(3px) translateX(-150%);
  opacity: 0;
  pointer-events: none;
}
.sing-beat-chrome.is-in .sing-beat__glint {
  /* 300ms after the portal itself lands, so the frame arrives first and the light
     catches it second. `both` holds the final frame: off the edge, at zero. */
  animation: sing-glint-sweep 1150ms cubic-bezier(0.33, 0, 0.2, 1) both;
  animation-delay: calc(var(--m-delay, 0ms) + 300ms);
}
@keyframes sing-glint-sweep {
  0%   { transform: translateZ(3px) translateX(-150%); opacity: 0; }
  30%  { transform: translateZ(3px) translateX(-40%);  opacity: 1; }
  70%  { transform: translateZ(3px) translateX(60%);   opacity: 1; }
  100% { transform: translateZ(3px) translateX(150%);  opacity: 0; }
}

/* ---- the details, arriving behind their own portrait ---- */
.sing-beat__name,
.sing-beat__role {
  transform: translateY(12px);
  opacity: 0;
  transition:
    transform 620ms cubic-bezier(0.22, 1, 0.36, 1),
    opacity 620ms ease;
}
.sing-beat__name { transition-delay: calc(var(--m-delay, 0ms) + 140ms); }
.sing-beat__role { transition-delay: calc(var(--m-delay, 0ms) + 240ms); }
.sing-beat-chrome.is-in .sing-beat__name,
.sing-beat-chrome.is-in .sing-beat__role { transform: translateY(0); opacity: 1; }

.sing-beat__name {
  font-size: clamp(14px, 1.15vw, 18px);
  font-weight: 600;
  line-height: 1.25;
  color: #ffffff;
  text-shadow: 0 1px 18px rgba(6, 3, 18, 0.95), 0 0 4px rgba(6, 3, 18, 0.8);
}

.sing-beat__role {
  margin-top: 4px;
  font-size: clamp(10px, 0.78vw, 12px);
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  /* MEASURED ON RENDERED PIXELS, NOT COMPUTED FROM A PARENT.
   *
   * This sits on the moving WebGL field, so no ancestor carries the colour it is
   * actually read against - it has to be sampled off a frame. At #c6b9f4 with the
   * name's wide shadow it measured 8.73:1 against typical field and 3.23:1 where
   * the dust runs bright, and at ~11px/500 this is SMALL text, so 4.5:1 is the
   * bar, not 3:1. It failed exactly where the founder said it was unreadable.
   *
   * Two changes, because either alone is marginal: the ink goes to a near-white
   * violet (keeps the tint, raises luminance), and the halo gains a TIGHT dense
   * ring. The 18px blur it had is a glow - it is nearly transparent at the glyph
   * edge, which is the one place small type needs the background pushed down. The
   * 3px/6px stops do that work; the 18px stop stays for the soft lift. */
  color: #E9E3FF;
  text-shadow:
    0 0 3px rgba(6, 3, 18, 0.98),
    0 1px 6px rgba(6, 3, 18, 0.95),
    0 0 18px rgba(6, 3, 18, 0.9);
}

/* The ending's one action. The reference's outro is a single line and one pill;
 * this is the section's own primary anchor, with its own label and its own href.
 * pointer-events are re-enabled on it alone — the rest of the layer stays
 * transparent to the pointer so the scene keeps its cursor interactions. */
.sing-beat__cta {
  display: inline-block;
  margin-top: clamp(1.6rem, 2.6vw, 2.8rem);
  padding: 16px 38px;
  border-radius: 19px;
  background: #ede9fe;
  color: #17102e;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.01em;
  text-decoration: none;
  pointer-events: auto;
  opacity: var(--beat-o, 0);
  transition: transform 240ms cubic-bezier(0.22, 1, 0.36, 1), background-color 200ms ease;
}
.sing-beat__cta:hover { background: #ffffff; transform: translateY(-2px); }

/* ---- the per-character roll ------------------------------------------------
 *
 * The reference clips each character in an overflow:hidden box and rolls it in
 * from translateY(110%). Ours have to WRAP — its headlines are two words, ours
 * are the page's own sentences — so the clip box is per WORD and the characters
 * live inside it. Word boxes wrap like text; the characters inside one never do,
 * so a word still rolls as a unit.
 *
 * The box is grown downward by the same amount it is pulled back up, so
 * descenders are not clipped by the very overflow that makes the roll work. */
.sing-beat .bw {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  padding-bottom: 0.18em;
  margin-bottom: -0.18em;
}
.sing-beat .bc {
  display: inline-block;
  transform: translateY(115%);
  opacity: 0;
  transition:
    transform 700ms cubic-bezier(0.22, 1, 0.36, 1),
    opacity 700ms ease;
}
.sing-beat.is-in .bc { transform: translateY(0); opacity: 1; }
/* out through the TOP, never back the way it came */
.sing-beat.is-out .bc { transform: translateY(-115%); opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .sing-beat .bc,
  .sing-beat__cta,
  .sing-beat__portal,
  .sing-beat__name,
  .sing-beat__role { transition: none; }
  .sing-beat .bc { transform: none; opacity: 1; }
  /* The portrait's resting state is turned away at opacity 0 waiting on a gate.
     With transitions off that gate is a hard cut from nothing to everything, so
     the parts are simply present and flat - same decision as .bc above. The rings
     stop turning: a permanent linear rotation is exactly what this query is for. */
  .sing-beat__portal { transform: none; opacity: 1; }
  .sing-beat__name,
  .sing-beat__role { transform: none; opacity: 1; }
  .sing-beat__ring { animation: none; }
  .sing-beat__glint { display: none; }
  /* The member's resting state is transform:none/opacity:0 waiting on a gate. With
     transitions off that gate would be a hard cut from nothing to everything, so
     the members are simply present — same decision as .bc above. */
  .sing-beat__member { transform: none; opacity: 1; }
}

/* Portrait phones: the reference drops to 14vw and goes full-bleed with a 24px
 * gutter. fitBeats() already steps the size down by measurement, so all this has
 * to do is give it the width to work with and tighten the vertical rhythm. */
@media (max-width: 640px) {
  .sing-beat { width: calc(100vw - 32px); }
  .sing-beat__foot { padding: 0 16px; }
  .sing-beat__sub { letter-spacing: 0.12em; }
  /* Two faces side by side is the point of a roster - wrapping them into a column
     at 390px would push the second one under the fold, which is the defect this
     whole feature exists to fix. So the row is kept and the cell is shrunk:
     2 x 132px + 20px gap = 284px inside a 358px box. The portal loses its
     perspective travel too - a 90px translateZ on a 68px portrait at that scale
     reads as a glitch rather than as depth. */
  .sing-beat__roster { gap: 20px; margin-top: 16px; }
  .sing-beat__member { width: min(46vw, 132px); perspective: 620px; }
  .sing-beat__portal { width: 94px; height: 94px; margin-bottom: 8px; transform: rotateY(-30deg) translateZ(-46px) scale(0.86); }
  .sing-beat__role { letter-spacing: 0.1em; }
}

/* ============================================================================
 * THE REDUCED-MOTION / NO-ENGINE DOCUMENT
 * ==========================================================================
 *
 * Measured with prefers-reduced-motion: reduce, all 15 beats carried an inline
 * `opacity: 0` and the page rendered as the hero and nothing else.
 *
 * The cause is in the engine, not here. update() computes
 *     const p = this.reduced ? 0 : clamp(this.Ssmooth / this.SMAX, 0, 1);
 * and then walks every station writing `el.style.opacity = o` UNCONDITIONALLY —
 * that unconditional write is deliberate and correct for the journey (it is what
 * stops a station being left is-live at opacity 0). But with p pinned to 0 the
 * distance to every slot centre is enormous, so `o` is 0 for all fifteen and
 * every beat is painted at zero in a document that never hijacks the scroll.
 *
 * Nothing is display:none and the copy is all still in the DOM and crawlable,
 * so this was invisible to a text audit — it only shows up when you PAINT the
 * reduced-motion page and look at it.
 *
 * Overridden here rather than in js/vedika-singularity.js because that file is
 * shared with other work in flight. `!important` is required: the engine writes
 * an inline style and nothing else can beat one. If the engine is later fixed to
 * skip the write when `reduced`, this whole block becomes an inert no-op and can
 * be deleted.
 *
 * Scoped to the document where the engine does NOT own the page, so the journey
 * itself is completely unaffected. */
html:not(.sing-hijack):not(.sing-native) .sing-station,
html:not(.sing-hijack):not(.sing-native) .sing-offstage {
  opacity: 1 !important;
  transform: none !important;
  visibility: visible !important;
  content-visibility: visible !important;
  /* AND THEY HAVE TO BE ABOVE THE HERO LAYER.
   *
   * Raising the opacity alone changed nothing on screen, because the second
   * half of the same bug was still in force here: `.vedika-singularity` is
   * `position: relative; z-index: 1; isolation: isolate`, so the isolate traps
   * its `position: fixed` canvas INSIDE the hero's stacking context and the
   * whole hero layer paints at z-index 1 — over every one of its in-flow
   * siblings, at every scroll position, because the canvas is fixed.
   *
   * That is the exact failure the `z-index: 3` rule at the top of the station
   * block exists to fix, but that rule is scoped to html.sing-hijack, so the
   * reduced-motion document never got it. Measured: scrollY 11508 with station
   * 9 at rect top -39px, opacity 1, background white — and the viewport still
   * painting nothing but the hero.
   *
   * z-index needs a position to apply; `relative` with no offsets does not
   * move anything. */
  position: relative;
  z-index: 3;
}

/* With the beats visible again they need a ground. In normal flow they sit over
 * the FIXED hero canvas, and nine of them declare no background of their own —
 * `#pricing` included, because .pricing-section-modern's `background:` shorthand
 * resets the background-color its own `bg-white` class had set. Their authored
 * `text-gray-900` headings measured 1.16:1 against it.
 *
 * These sections were authored for a white page and every card inside them is
 * built for one, so the fix is to give them that page back rather than to
 * recolour them: the authored type then measures 17.0-17.7:1 with no other
 * change. The two beats that were authored DARK keep their own ground. */
/* ZERO specificity on purpose. Any section that declares a ground of its own
 * must keep it — three of the offstage bands are authored DARK with white type
 * ("Computed context and generated prose", "Research informs architecture",
 * "Build your astrology product on Vedika"), and an !important white here
 * turned all three into white-on-white. This rule only reaches the sections
 * that declare nothing at all. */
:where(html:not(.sing-hijack):not(.sing-native)) :where(.sing-station, .sing-offstage) {
  background-color: #ffffff;
}
/* #pricing is the exception that needs specificity: it carries `bg-white`, but
 * .pricing-section-modern's `background:` SHORTHAND resets background-color back
 * to transparent, so the section has a class promising white and no white. Its
 * "Token-Based Pricing" heading measured 1.16:1 in this document. */
html:not(.sing-hijack):not(.sing-native) #pricing.sing-station {
  background-color: #ffffff;
}

/* One more utility the Tailwind build dropped, and this one matters in BOTH
 * documents: the "Vedika Ephemeris / Calculation before interpretation" panel
 * is `bg-slate-950 ... text-white`, and `bg-slate-950` has no rule in
 * css/tailwind.min.css. In the journey the panel got away with it because the
 * dark sheet was behind it; in normal flow its white type landed on the
 * section's own gray-50 and measured 1.05:1. Restored unscoped so both
 * documents get the dark ground the markup asks for. */
.sing-station .bg-slate-950,
.sing-offstage .bg-slate-950 { background-color: #020617; }

/* ===========================================================================
   THE ARRIVAL
   ===========================================================================

   The end of the journey: the AI-query demo, the multilingual beat, the CTA,
   and the page's own footer resolving out of the tunnel's particles.

   Z-INDEX 7. Above the moons (2), the stations (3), the beat veil (4), the
   beats (5) and the beat chrome (6). While the arrival is up it is the only
   thing meant to be read, so it sits above all of them — and, like every other
   layer that has to paint over the scene, it is a SIBLING of #main-content on
   <body>, never a descendant of .vedika-singularity, whose `isolation: isolate`
   traps its canvas's stacking context. That is the bug that has made content
   invisible on this page three separate times.

   NO display:none ANYWHERE IN HERE. Panels that are not up are opacity 0 with
   `content-visibility: hidden`, exactly as the stations are — the markup stays
   in the DOM, in reading order, and in the accessibility tree whenever it is
   readable. --------------------------------------------------------------- */

.sing-arrival {
  position: fixed;
  inset: 0;
  z-index: 7;
  pointer-events: none;
  display: grid;
  place-items: center;
  /* how far the ending has folded into the footer: 0 while the CTA owns the
     frame, 1 once the footer has claimed the lower two thirds */
  --arr-fold: 0;
  --arr-veil: 0;
  font-family: "OverusedGrotesk", "Plus Jakarta Sans", "DM Sans", ui-sans-serif, system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* THE SCRIM.
 *
 * The same problem .sing-beats-veil documents, and the same answer. The tunnel
 * carries a lit particle ring exactly where this copy lands, and the provenance
 * lines — the one part of these panels whose entire job is to be checkable —
 * measured unreadable against it. So the scene is dimmed under the arrival
 * rather than the type being given a plate: no edge, fully transparent at the
 * corners, and it follows whichever panel is up so the wormhole is never dimmed
 * when there is nothing on it to read.
 *
 * position: absolute takes it out of the grid flow (the layer is `position:
 * fixed`, so it is the containing block), and z-index: -1 stays trapped inside
 * this layer's own stacking context — it cannot fall behind the canvas. */
.sing-arrival::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: var(--arr-veil, 0);
  background:
    radial-gradient(78% 62% at 50% 48%,
      rgba(5, 3, 16, 0.90) 0%,
      rgba(5, 3, 16, 0.78) 40%,
      rgba(5, 3, 16, 0.42) 72%,
      rgba(5, 3, 16, 0) 94%);
}

/* Every panel occupies the same grid cell. Grid rather than absolute + translate
   so a panel is never its own stacking context for nothing, which is the same
   reasoning .sing-beats records. */
.sing-arr {
  grid-area: 1 / 1;
  width: min(94vw, 1180px);
  max-height: 100vh;
  opacity: 0;
  visibility: hidden;
  content-visibility: hidden;
  transform: translateY(var(--arr-dy, 0px));
  zoom: var(--arr-fit, 1);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(14px, 1.8vh, 26px);
  text-align: center;
}

.sing-arr.is-live {
  visibility: visible;
  content-visibility: visible;
  pointer-events: auto;
  will-change: opacity, transform;
}

/* --------------------------------------------------------------- the label */

.sing-arr__pre {
  margin: 0;
  font-size: clamp(11px, 0.92vw, 14px);
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: #cfc6ee;
  text-shadow: 0 1px 18px rgba(6, 3, 18, 0.95);
}

.sing-arr__h {
  margin: 0;
  font-size: clamp(26px, 3.4vw, 52px);
  line-height: 1.06;
  letter-spacing: -0.025em;
  font-weight: 600;
  color: #f6f3ff;
  text-shadow: 0 2px 40px rgba(4, 2, 14, 0.9);
}
.sing-arr__h em { font-style: italic; color: #c9b6ff; }

/* ------------------------------------------------------------ liquid glass */

/* One surface definition, used by every CARD in the arrival.
   Backdrop-filter on a fixed layer over a WebGL canvas is the cheap version of
   this effect — it samples what is already composited rather than re-rendering
   anything — and it is what makes the panels read as glass held up against the
   tunnel rather than as opaque plates dropped on top of it.

   THE FOOTER IS NO LONGER IN THIS LIST, AND THAT MATTERS.
   `.sing-arr-footer.is-live` used to be, which handed the footer a 1px white
   border, an 18px radius and a backdrop-filter. The `background: transparent
   !important` further down outranks the fill but reaches none of those three,
   so the footer was still drawing a hard-edged, blurred, bordered rectangle
   across the bottom of the frame — the seam the founder is pointing at, drawn
   from here as well as from the ::before. A card is an object with edges; the
   footer is a place the scene dissolves into, and it cannot be both. */
.sing-arr__card,
.sing-arr__ep {
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.085) 0%, rgba(255, 255, 255, 0.028) 46%, rgba(255, 255, 255, 0.05) 100%),
    rgba(11, 7, 26, 0.80);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 18px;
  -webkit-backdrop-filter: blur(16px) saturate(150%);
  backdrop-filter: blur(16px) saturate(150%);
  box-shadow:
    0 26px 70px -30px rgba(0, 0, 0, 0.9),
    inset 0 1px 0 rgba(255, 255, 255, 0.16),
    inset 0 -1px 0 rgba(255, 255, 255, 0.05);
}

.sing-arr__card {
  padding: clamp(12px, 1.5vh, 18px) clamp(14px, 1.6vw, 22px);
  text-align: left;
  min-width: 0;
  overflow: hidden;
}

/* ------------------------------------------------------ 1. request/response */

.sing-arr__wire {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: stretch;
  gap: clamp(8px, 1vw, 16px);
  width: 100%;
  min-width: 0;
}

.sing-arr__cardhead {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
  padding-bottom: 9px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.10);
  min-width: 0;
}

/* THE COPY CONTROL. `margin-left: auto` pushes it to the far end of the head, so it
   sits opposite the verb and the path rather than crowding them - the head is a flex
   row and the path is the flexible item between them.
   It is quiet until you approach the card: a developer scanning the panel should read
   the request first, and the control should be there the moment they want it. It stays
   fully visible on keyboard focus regardless of hover, because a control that only
   appears on hover is unreachable by keyboard. */
.sing-arr__copy {
  flex: none;
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 9px;
  border-radius: 999px;
  border: 1px solid rgba(206, 199, 255, 0.28);
  background: rgba(128, 122, 200, 0.16);
  color: #d9d2f6;
  font: 600 10px/1.5 var(--vds-font-mono, ui-monospace, SFMono-Regular, monospace);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  opacity: 0.62;
  transition: opacity .18s ease, background-color .18s ease, border-color .18s ease, color .18s ease;
}
.sing-arr__card--req:hover .sing-arr__copy,
.sing-arr__copy:focus-visible { opacity: 1; }
.sing-arr__copy:hover {
  background: rgba(140, 130, 220, 0.30);
  border-color: rgba(206, 199, 255, 0.62);
  color: #fff;
}
.sing-arr__copy:focus-visible { outline: 2px solid #c9b6ff; outline-offset: 3px; }
.sing-arr__copy svg { width: 12px; height: 12px; flex: none; }
.sing-arr__copy.is-done {
  opacity: 1;
  background: rgba(110, 231, 183, 0.22);
  border-color: rgba(110, 231, 183, 0.66);
  color: #b8f5dc;
}

.sing-arr__verb,
.sing-arr__ok {
  flex: none;
  padding: 3px 9px;
  border-radius: 999px;
  font: 600 10px/1.5 var(--vds-font-mono, ui-monospace, SFMono-Regular, monospace);
  letter-spacing: 0.09em;
}
.sing-arr__verb { background: rgba(155, 124, 255, 0.22); color: #d8caff; border: 1px solid rgba(155, 124, 255, 0.42); }
.sing-arr__ok   { background: rgba(78, 222, 159, 0.16); color: #8ff0c4; border: 1px solid rgba(78, 222, 159, 0.36); }

.sing-arr__path,
.sing-arr__reslabel,
.sing-arr__code-inline {
  font: 500 clamp(10px, 0.85vw, 13px)/1.4 var(--vds-font-mono, ui-monospace, SFMono-Regular, monospace);
  color: #e6dffb;
  /* the path is the load-bearing fact on this panel: it must never ellipsise */
  overflow-wrap: anywhere;
}
.sing-arr__reslabel { color: #a79ec6; }

.sing-arr__code {
  margin: 0;
  font: 400 clamp(9.5px, 0.76vw, 12px)/1.62 var(--vds-font-mono, ui-monospace, SFMono-Regular, monospace);
  color: #c6bee0;
  white-space: pre;
  /* wide content scrolls inside its own box; the page never scrolls sideways */
  overflow-x: auto;
  overflow-y: hidden;
}
.sing-arr__code .k { color: #9db8ff; }
.sing-arr__code .s { color: #ffcf9a; }
.sing-arr__code .n { color: #8ff0c4; }
.sing-arr__code .v { color: #d8caff; font-weight: 600; }
.sing-arr__code .p { color: #f6f3ff; font-weight: 600; }

/* the three travelling dots between request and response */
.sing-arr__flow {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 7px;
  padding: 0 2px;
}
.sing-arr__flow i {
  width: 5px; height: 5px;
  border-radius: 50%;
  background: #9b7cff;
  opacity: 0.25;
  animation: sing-arr-flow 1.5s ease-in-out infinite;
}
.sing-arr__flow i:nth-child(2) { animation-delay: 0.18s; }
.sing-arr__flow i:nth-child(3) { animation-delay: 0.36s; }
@keyframes sing-arr-flow {
  0%, 100% { opacity: 0.18; transform: scale(0.8); }
  45%      { opacity: 1;    transform: scale(1.25); }
}

.sing-arr__answer,
.sing-arr__a {
  font-size: clamp(11px, 0.92vw, 14px);
  line-height: 1.58;
  color: #e7e1f7;
  /* the response is long and the panel is one viewport: it scrolls in its own
     box rather than pushing the meta strip off the bottom of the frame */
  max-height: 34vh;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.sing-arr__para { margin: 0 0 0.65em; }
.sing-arr__para:last-child { margin-bottom: 0; }
.sing-arr__answer strong,
.sing-arr__a strong { color: #f4f0ff; font-weight: 600; }

/* the reveal is driven by scroll position, so the caret is the only thing on
   the panel running on its own clock */
.sing-arr__caret {
  display: inline-block;
  width: 2px; height: 1em;
  margin-left: 2px;
  vertical-align: text-bottom;
  background: #c9b6ff;
  animation: sing-arr-caret 1s steps(2, end) infinite;
}
@keyframes sing-arr-caret { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

.sing-arr__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  margin-top: 11px;
  padding-top: 9px;
  border-top: 1px solid rgba(255, 255, 255, 0.10);
  font-size: clamp(9.5px, 0.74vw, 11.5px);
  letter-spacing: 0.04em;
  color: #ada4c6;
}
.sing-arr__meta b { color: #ddd4f6; font-weight: 600; }
.sing-arr__case {
  font-family: var(--vds-font-mono, ui-monospace, SFMono-Regular, monospace);
  opacity: 0.72;
}

/* The provenance line is not decoration. Every number and every sentence on
   these panels is traceable, and this is where the trace is printed. */
.sing-arr__prov {
  margin: 0;
  max-width: 76ch;
  font-size: clamp(10px, 0.75vw, 12px);
  line-height: 1.55;
  color: #b5adcc;
  text-shadow: 0 1px 14px rgba(5, 3, 16, 0.95);
  text-wrap: balance;
}
.sing-arr__prov code {
  font-family: var(--vds-font-mono, ui-monospace, SFMono-Regular, monospace);
  color: #d5cbf2;
}

/* ------------------------------------------------------- 2. the 29 languages */

.sing-arr__langs {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(10px, 1.2vw, 18px);
  width: 100%;
}

/* the two cards land one after the other, not together */
.sing-arr__lang {
  opacity: 0;
  transform: translateY(18px) scale(0.985);
  transition: opacity 620ms cubic-bezier(.22, 1, .36, 1), transform 620ms cubic-bezier(.22, 1, .36, 1);
}
.sing-arr__lang.is-on { opacity: 1; transform: none; }

.sing-arr__native {
  font-size: clamp(14px, 1.25vw, 19px);
  font-weight: 600;
  color: #f2ecff;
}
.sing-arr__q {
  margin: 0 0 9px;
  font-size: clamp(10.5px, 0.86vw, 13px);
  line-height: 1.5;
  color: #c3bade;
  padding-left: 10px;
  border-left: 2px solid rgba(155, 124, 255, 0.5);
}
.sing-arr__a { max-height: 22vh; }

.sing-arr__codes {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 5px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.sing-arr__codes li {
  padding: 3px 8px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.04);
  font: 500 10px/1.4 var(--vds-font-mono, ui-monospace, SFMono-Regular, monospace);
  color: #bdb4d6;
  text-shadow: 0 1px 10px rgba(5, 3, 16, 0.9);
}
.sing-arr__codes li.is-on {
  border-color: rgba(155, 124, 255, 0.62);
  background: rgba(155, 124, 255, 0.2);
  color: #e2d8ff;
}

/* ------------------------------------------------------------ 3. the ending */

/* The CTA lifts and compacts as the footer claims the lower frame, so the two
   share the ending instead of one covering the other. --arr-fold is written by
   updateArrival once per changed frame. */
.sing-arr--cta {
  gap: clamp(14px, 2.0vh, 30px);
  /* MEASURED, NOT GUESSED. At -30vh and scale 0.80 the badge was cut off by the
     top of the viewport at p = 1.0 on a 1440x900 frame: the block stands about
     490px tall, 0.80 of that is 392px, and the space above the footer is 378px.
     0.70 puts it at 343px inside 378px with the margin on both edges. */
  /* COMPACT HARDER, DO NOT LIFT FURTHER. 18vh and 0.46.
   *
   * I tried 30vh first and it did clear the footer - and cut the badge off the top
   * of the viewport at every size (measured badge.top of -39, -9, -44, -57). That is
   * precisely the failure the note above already records at -30vh, so I reproduced a
   * documented bug by reaching for the same lever.
   *
   * Lift moves the block toward an edge it is already near; scale shrinks it away
   * from BOTH edges at once. So the lift comes DOWN to 18vh, which keeps the badge
   * on screen, and the compaction goes UP to 0.54 of full size, which is what
   * actually buys the clearance over the footer. */
  /* SCALE IS NOT A HEIGHT LEVER, AND IT WAS BUYING THE CLEARANCE WITH THE TYPE.
   *
   * This was `scale(calc(1 - var(--arr-fold, 0) * 0.56))`. At the end of the
   * journey --arr-fold settles at 0.988, so the whole section rendered at 0.447
   * and every label inside it shrank with it. Measured across five frames:
   *
   *   1920x1080  scale 0.447 -> button label 6.3px
   *   1440x950   scale 0.447 -> button label 6.3px
   *   1440x900   scale 0.368 -> button label 5.2px
   *   1366x768   scale 0.368 -> button label 5.2px
   *   1280x800   scale 0.368 -> button label 5.2px
   *
   * That is CREATE AN ACCOUNT / GENERATE AN API KEY / LOG IN and the endpoint
   * chip, the last thing on the page, at a third of a legible size. Raising the
   * scale alone cannot fix it: the largest scale that still clears the footer
   * measures 0.764 / 0.713 / 0.705 / 0.490 / 0.552 on those same frames, which
   * still lands the label between 6.9px and 10.7px.
   *
   * The reason scale had to go so low is that it reclaims NO LAYOUT HEIGHT - a
   * transform moves paint, not flow, so the section kept asking for its full
   * 480-547px no matter how small it drew. Compacting therefore has to change
   * real box metrics, which is what the three rules below now do: the badge's
   * width, the heading's font-size and the gap all fold, so the height genuinely
   * comes back. The interactive row and the endpoint chip are deliberately left
   * out of that fold - they are the part the visitor is meant to be able to
   * read and click.
   *
   * A floored scale stays on the section for the last few pixels of clearance.
   * 0.86 here and 0.70 on short viewports, against 0.447 and 0.368 before. */
   * The lift goes 18vh -> 26vh at the same time, and that is not a separate
   * change: a bigger scale paints a taller block, so the block that used to
   * clear the footer by drawing itself small now has to clear it by sitting
   * higher. Measured at 0.86 with the old 18vh, the overlap was 47px at
   * 1920x1080 and 45px at 1440x950. */
  gap: calc(clamp(14px, 2.0vh, 30px) * (1 - var(--arr-fold, 0) * 0.62));
  transform: translateY(calc(var(--arr-dy, 0px) - var(--arr-fold, 0) * 26vh)) scale(calc(1 - var(--arr-fold, 0) * 0.14));
  transform-origin: 50% 22%;
}

/* The decoration folds; the buttons and the chip do not. */
.sing-arr--cta .sing-arr__badge {
  width: calc(clamp(66px, 6.6vw, 96px) * (1 - var(--arr-fold, 0) * 0.58));
}
.sing-arr--cta .sing-arr__h {
  font-size: calc(clamp(26px, 3.4vw, 52px) * (1 - var(--arr-fold, 0) * 0.46));
}

/* SHORT VIEWPORTS GET A HARDER FOLD AND A SHORTER FOOTER.
   Both objects are sized in vh, so a short screen shrinks the gap between them from
   both directions at once: at 1280x800 and 1366x768 the CTA still landed 14-15px
   inside the footer where 1440x900 and 1990x1100 cleared by 23 and 68. The footer
   also still ran 9-24px over its own box there. Same tier as the design system's
   short-viewport footer rules, so the two stay in step. */
@media (max-height: 920px) {
  .sing-arr--cta {
    /* Was scale 0.36 here, which is where the 5.2px labels came from. Same
     * treatment as the base rule: the decoration gives up the height, the
     * scale keeps a floor. 0.70 against 0.368 before. */
    gap: calc(clamp(14px, 2.0vh, 30px) * (1 - var(--arr-fold, 0) * 0.72));
    transform: translateY(calc(var(--arr-dy, 0px) - var(--arr-fold, 0) * 27vh)) scale(calc(1 - var(--arr-fold, 0) * 0.30));
  }
  .sing-arr--cta .sing-arr__badge {
    width: calc(clamp(66px, 6.6vw, 96px) * (1 - var(--arr-fold, 0) * 0.70));
  }
  .sing-arr--cta .sing-arr__h {
    font-size: calc(clamp(26px, 3.4vw, 52px) * (1 - var(--arr-fold, 0) * 0.56));
  }
  html.sing-hijack .sing-arr-footer,
  html.sing-native  .sing-arr-footer { max-height: 60vh; }
}

/* ---- THE COPY CONDENSES OUT OF THE PARTICLE FALL ----
 *
 * "i had asked on particals assembling vedika logo and all data tho but its
 * static show up on scroll." The WebGL half is in updateArrivalAtoms; this is
 * the DOM half, and the two run on the same span so they read as one event: the
 * cloud converges onto each element's box while the element itself precipitates
 * out of blur underneath it, and the cloud is gone by the time the type is
 * legible.
 *
 * Driven by a custom property, not a transition — this has to run BACKWARDS
 * when the founder scrolls back up, and a transition cannot. Same mechanism,
 * same reason, as the footer's columns a few hundred lines down.
 *
 * The parts land in the order a reader needs them. */
.sing-arr--cta > * {
  --d: 0;
  --t: clamp(0, calc((var(--arr-asm, 1) - var(--d)) / 0.34), 1);
  opacity: var(--t);
  transform: translateY(calc((1 - var(--t)) * 20px)) scale(calc(0.955 + var(--t) * 0.045));
  filter: blur(calc((1 - var(--t)) * 7px));
}
/* The badge rises exactly as its own particles land on it: --arr-asm runs
   0.951 -> 0.986 and this part is complete at 0.34 of that, i.e. p = 0.963,
   which is the frame the mark cloud has finished fading out on. The element
   resolves underneath the last of the convergence rather than sitting there
   solid while the cloud is still flying toward it. */
.sing-arr--cta .sing-arr__badge { --d: 0.00; }
.sing-arr--cta .sing-arr__h     { --d: 0.16; }
.sing-arr--cta .sing-arr__acts  { --d: 0.34; }
.sing-arr--cta .sing-arr__ep    { --d: 0.52; }

/* Formed. Drop the properties rather than zeroing them: `filter: blur(0px)`
   still promotes each part to its own composited layer and still costs a pass
   on a frame where nothing is moving — measured at 30ms of p95 on the footer
   for a blur of exactly zero pixels. */
.sing-arr--cta.is-formed > * {
  filter: none;
  transform: none;
  opacity: 1;
}

/* MEASURED WHILE STILL INVISIBLE.
 *
 * buildArrivalAtoms has to read the real screen boxes of the badge and the copy
 * to know where the particles are converging TO, and the panel is
 * `content-visibility: hidden` until its slot opens — which means its
 * descendants are not laid out at all and every rect comes back zero. This
 * restores layout for the single frame of the build without restoring paint:
 * the panel's own opacity is still 0 and is still owned by updateArrival. */
.sing-arr.is-measure {
  visibility: visible;
  content-visibility: visible;
}

/* A document that has asked not to be animated gets the ending, complete, with
   no convergence and no blur — never a held state waiting on an effect. */
@media (prefers-reduced-motion: reduce) {
  .sing-arr--cta > * {
    filter: none;
    transform: none;
    opacity: 1;
  }
}

.sing-arr__badge {
  display: grid;
  place-items: center;
  width: clamp(66px, 6.6vw, 96px);
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 42%, rgba(155, 124, 255, 0.30), rgba(155, 124, 255, 0) 68%);
}
.sing-arr__badge img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: drop-shadow(0 6px 26px rgba(139, 106, 255, 0.55));
}

/* THE ROW IS A HIERARCHY WITH ROOM IN IT.
 *
 * Measured on the rejected frame: three 46px capsules, ~525px of total width on
 * a 1440px viewport, separated by an 8px gap — the middle capsule's ink layer
 * was touching both of its neighbours. That is the "poor placement": not that
 * they were in the wrong part of the frame, but that nothing separated them and
 * nothing ranked them.
 *
 * Two columns, a gap you can see, and the third link on its own line. The gap
 * is fluid and floors at 16px, which is above the widest bloom bleed a capsule
 * of this size produces, so the two ink layers can never touch again. */
.sing-arr__acts {
  display: grid;
  grid-template-columns: auto auto;
  justify-content: center;
  align-items: stretch;
  column-gap: clamp(16px, 2.0vw, 34px);
  row-gap: clamp(10px, 1.4vh, 18px);
  width: 100%;
}

/* The two capsules stand at a size that matches what they are asking for. The
   old pair were both at the component's default 46px with two lines of text
   inside — the label and the sub had 18px of leading between them and read as
   one crammed block rather than as a label with a caption. */
.sing-arr__act--primary,
.sing-arr__act--secondary {
  min-width: clamp(210px, 20vw, 292px);
  padding-block: clamp(11px, 1.5vh, 17px);
}

/* Logging in is not a peer of signing up. Its own line, centred, no bloom, no
   fill — the quietest thing the component offers. It is still a real capsule
   with a real target size, because the people who need it need to hit it. */
.sing-arr__act--tertiary {
  grid-column: 1 / -1;
  justify-self: center;
  min-width: 0;
}

/* ANALOGOUS BLOOMS, AND THE TWO TRAPS THAT COME WITH THEM.
 *
 * `.vds-bloom-host` is on the element alongside `.vds-bloom` — the ink layer's
 * selector is `.vds-bloom-host.vds-bloom::before` and the geometry custom
 * properties are declared on `.vds-bloom-host`, so dropping it yields a
 * correctly sized, correctly coloured, completely bloom-less capsule with no
 * error to explain it.
 *
 * TRAP 1. `.vds-bloom--violet` is the one colourway in the token file that ships
 * with NO `--vds-bloom-strength`, so it falls back to the root value of 1 while
 * --sky, --dawn and --paper are at 1.5 and --indigo at 1.15. Left alone it
 * renders visibly weaker than its neighbour and the natural conclusion is that
 * the hue is wrong. It is not; the dial is. Set here, explicitly.
 *
 * TRAP 2. A DARK capsule needs `screen` and a BLACK null together. The bloom's
 * clearing gradient paints the blend identity, and under multiply that identity
 * is white — on a dark capsule that is a white haze, i.e. the mud. The design
 * system already pairs these two correctly for `.vds-pill--primary` (which is
 * light-on-dark and therefore needs the opposite pair); the secondary capsule
 * here is the mirror case and has to state it.
 *
 * STATIC blooms only. Measured on this codebase: a static bloom is effectively
 * free while a DRIFTING one costs ~12.9ms each and ~50ms for four. */
.sing-arr__act--secondary {
  --vds-bloom-strength: 1.5;
  --vds-bloom-mode: screen;
  --vds-bloom-null: var(--vds-rgb-black);
}
.sing-arr__act { text-align: left; }

/* THE LABEL HAS TO SIT ON TOP OF ITS OWN BLOOM.
 *
 * The bloom is a ::before on the capsule and it paints after the text in
 * document order, so without this the ink layer covers the label — measured on
 * the first pass, "Log in" vanished completely and "Generate an API key" came
 * back as "n API key" with its left half painted over. The sub-label survived
 * only because it sits lower than the bloom's brightest band.
 *
 * `.vds-pill` already declares `isolation: isolate`, so lifting the children to
 * z-index 1 is contained to the capsule. This is the same one-line fix
 * `.singularity-actions .singularity-button > *` carries a few hundred lines up,
 * for exactly the same reason. */
.sing-arr__act > * { position: relative; z-index: 1; }

/* THE DESIGN SYSTEM LOSES THE CASCADE ON THIS PAGE. PIN THE TWO COLOURS.
 *
 * Measured, not assumed. On the arrival's capsules `--vds-pill-fg` resolves
 * correctly to #f4f2fb under `.vds-dark` — and the computed `color` came back
 * rgb(17, 24, 39), Tailwind's gray-900. `.vds-pill { color: var(--vds-pill-fg) }`
 * lives in `@layer vds.components`, and index.html does not load
 * css/vedika-theme.css, which is the file that declares the layer order and
 * pulls the legacy sheets into `@layer legacy` underneath the system. So every
 * one of this page's ~300 KB of Tailwind and inline rules is UNLAYERED — and
 * unlayered beats every layer, whatever order they are in. No amount of
 * `@layer` juggling fixes that; only an unlayered rule can, which is what this
 * stylesheet is.
 *
 * The visible symptom was two invisible labels: dark-on-dark "Log in" and
 * "Generate an API key", each readable only across the stripe where its own
 * bloom happened to be bright enough to show the text through.
 *
 * Geometry, radius, bloom and typography still come from the design system —
 * this pins only the two properties the page steals. */
.sing-arr__act.vds-pill--secondary { color: #f2eeff; background-color: rgba(11, 10, 34, 0.78); border-color: rgba(150, 160, 255, 0.30); }
.sing-arr__act.vds-pill--secondary .vds-pill__label { color: #f6f3ff; }
.sing-arr__act.vds-pill--secondary .vds-pill__sub { color: #b9b1d6; }
.sing-arr__act.vds-pill--primary { color: #0d0a20; background-color: #fbfafd; }
.sing-arr__act.vds-pill--primary .vds-pill__label { color: #0d0a20; }
.sing-arr__act.vds-pill--primary .vds-pill__sub { color: #464060; }
/* QUIET IS NOT THE SAME AS INVISIBLE.
 *
 * The first pass gave the tertiary a fully transparent ground and a 0.34-alpha
 * border, and on the near-black arrival field it measured as a barely-there
 * outline with a 3.1:1 label — a link the founder could not see is not a
 * demoted link, it is a lost one. It keeps the lowest weight in the row (no
 * bloom, no fill of its own beyond a whisper) and still clears AA on its label
 * against the darkest part of the field. */
/* "LOG IN IS THE WORST, CANNOT EVEN SEE IT" - and it was right.
 *
 * A ghost capsule is meant to be quieter than its two neighbours, not absent. At
 * a 14% fill on a near-black sky the shape carried almost no edge, and a 52%
 * border on a 1px line is the faintest possible way to draw one, so on the
 * arrival panel it read as empty space with a word floating in it.
 *
 * Tertiary is a RANK, not an invisibility setting. The fill is now 0.30 and the
 * border 0.86, which still sits clearly below the solid white primary and the
 * violet secondary - the hierarchy the comment above this block describes is
 * intact - while actually reading as a button you can find. */
.sing-arr__act.vds-pill--ghost {
  color: #efeaff;
  background-color: rgba(128, 122, 200, 0.30);
  border-color: rgba(206, 199, 255, 0.86);
}
.sing-arr__act.vds-pill--ghost .vds-pill__label { color: #ece8ff; }
.sing-arr__act.vds-pill--ghost:hover { background-color: rgba(140, 130, 220, 0.26); border-color: rgba(206, 198, 255, 0.78); }
.sing-arr__act.vds-pill--ghost:hover .vds-pill__label { color: #ffffff; }
.sing-arr__act .vds-pill__label { font-weight: 600; }

/* THE LABEL GROWS INTO SLACK THE CAPSULE ALREADY HAD, SO IT COSTS NO HEIGHT.
 *
 * `.vds-pill` inherits `--vds-text-xs`, which is 11px. Under the ending's fold
 * that reaches the eye at 11 x 0.862 = 9.5px on a tall screen and 11 x 0.704 =
 * 7.7px on a short one, and these three capsules are the page's last and most
 * important controls. Raising the size here is free: the capsule's height comes
 * from `min-height: var(--vds-tap-comfortable)` and `padding-block`, not from
 * its text - measured, a 16px label sits in a 60px box, so there are ~28px of
 * vertical slack and a 14px label still does not touch the walls. The capsules
 * get a little wider, which the row has room for; verified no wrap and no
 * overflow at 1366x768 and 1280x800, the two tightest frames.
 *
 * Scoped to `.sing-arr--cta` on purpose. `.vds-pill` is the design system's
 * button and is used on every page; this is a correction for one oversized
 * transform on one section, not a change to the button. */
.sing-arr--cta .sing-arr__act { font-size: 14px; }
.sing-arr--cta .sing-arr__ep { font-size: 13px; }
/* the primary carries the frame's only inverted surface, so it gets the one
   lift that says so — a soft royal halo, static, costing a single shadow */
.sing-arr__act--primary {
  box-shadow:
    0 18px 46px -18px rgba(88, 96, 255, 0.62),
    0 0 0 1px rgba(255, 255, 255, 0.10);
}

.sing-arr__ep {
  /* clear of the capsules: their blooms bleed a few pixels past the border box
     and the code block was reading as if it were tucked underneath them */
  margin-top: clamp(6px, 1.1vh, 14px);
  padding: 11px 18px;
  border-radius: 12px;
  text-align: left;
  /* This block is the one verifiable fact in the frame — POST
     /api/v1/astrology/query and X-API-Key, both checked against the router — and
     at the ending's 0.70 scale it was reading as decoration. The path and the
     header name are lifted; the block is still the quietest thing on the panel. */
  border-color: rgba(150, 160, 255, 0.26);
}
.sing-arr__ep .k { color: #b8ccff; }
.sing-arr__ep .v { color: #cfe4ff; }
.sing-arr__ep .p { color: #efeaff; }
.sing-arr__ep .s { color: #a9e8dd; }

/* ===========================================================================
   4. THE FOOTER, RESOLVING OUT OF THE PARTICLES
   ===========================================================================

   THE MARKUP IS THE PAGE'S OWN AND IS NOT TOUCHED. All 33 links, the support
   phone number, the four columns, the copyright, the address and the byline are
   exactly as index.html declares them. This is a re-skin and a timing, not a
   rewrite — losing a link here would be losing a link from the site.

   The `background:#0F172A` the footer carries is an INLINE style attribute, so
   nothing but !important can reach it. Every override below is therefore scoped
   to html.sing-hijack / html.sing-native, i.e. to the engine actually being in
   charge: with JS off, with reduced motion, or on a crawl, the footer keeps its
   original slate ground and its original look, untouched. ------------------ */

html.sing-hijack .sing-arr-footer,
html.sing-native  .sing-arr-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 8;                 /* above the arrival layer's CTA */
  margin: 0;
  padding: clamp(14px, 2.2vh, 30px) 0 clamp(12px, 1.8vh, 22px) !important;
  background: transparent !important;
  /* Only for the OLD dark footer. .vk-footer is a light plate and sets its
     own ink; this pale lavender left its copy at roughly 1.3:1 on the
     gradient, which is the unreadable footer that got reported. */
  color: #cdc5e6;
  opacity: 0;
  pointer-events: none;
  /* NO zoom HERE. The arrival panels are fitted with `zoom` and it is right for
   * them, but this element is `position: fixed` with left:0/right:0, and zooming
   * an inset-anchored fixed box rescales the coordinate system its own anchors
   * are resolved in. Measured at 390x844: the footer laid itself out in a narrow
   * column against the left edge, about half the viewport wide, with the rest of
   * the frame empty. It fits by scrolling instead — driven, see below. */
  /* the dust field is the ::before, so the columns can rise through it */
  isolation: isolate;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* the journey drives this; no user-reachable scrollbar exists while the wheel
     is hijacked, so it must never look like one */
  scrollbar-width: none;
  /* 74vh. This was 58vh, chosen so the CTA block above kept air around it, on the
     reasoning that "whatever the footer loses in box height it plays through by
     scrolling". That reasoning does not survive contact with the rebuilt footer:
     measured at 1990x1100 the content is 1019px tall in a 638px box, so 381px -
     37% of the footer, including the whole top of every nav column - was simply
     off-screen, and the scroll that was supposed to recover it is driven by the
     journey rather than by the reader, so nobody could reach it. Reported as
     "30% top of footer is missing" and "footer list not fitting".
     74vh plus the compaction in the design system brings the content inside the
     box. The CTA still clears it - the arrival panel is measured against this.

     UPDATE - 66vh. This walked 58 -> 88 -> 66 and the middle number was my mistake,
     worth naming: at 88vh the footer covered the arrival CTA outright, the headline
     cut in half, measured ctaCovered at three of four sizes. The fold that lifts the
     CTA was itself MEASURED against a 58vh footer ("the space above the footer is
     378px"); at 88vh that space is 108px against a compacted CTA of ~343px, so it
     could not fit by construction. Raising the cap to fix the footer's own overflow
     silently broke a relationship someone had already measured - which is exactly
     what that earlier comment existed to prevent.
     What makes 66 work is the two-column list flow now in the design system: the
     content needs 67vh with it and 86.6vh without. The height came down because the
     CONTENT got shorter, not because anything was cut - all 49 links remain. */
  max-height: 66vh;
}
html.sing-hijack .sing-arr-footer::-webkit-scrollbar,
html.sing-native  .sing-arr-footer::-webkit-scrollbar { width: 0; height: 0; }

html.sing-hijack .sing-arr-footer.is-live,
html.sing-native  .sing-arr-footer.is-live { pointer-events: auto; }

/* ---------------------------------------------------------------------------
   THE SEAM WAS THE PROBLEM, NOT THE FILL.

   "the footer needs fix tho man make it in this theme ... like the old sound bar
   kinda design of flowing dust gradient man."

   What shipped was a glass SLAB: `inset: 0` over the footer's own box, a solid
   rgba(11,7,26,0.62) ground, a 1px white border along its top edge, and a
   backdrop-filter. Every one of those four things draws an edge. A
   backdrop-filter is clipped to its element's box by definition, so the blur
   itself terminates on a hard horizontal line; the border then draws that same
   line again in white. Screenshotted at p = 1.0 the result is exactly what the
   founder describes — the particle field stops, and a flat dark-violet rectangle
   from a different website starts, with a visible cut between them.

   So this is no longer a panel behind the footer. It is a column of luminous
   dust standing in the lower frame that the footer's type happens to sit inside:

     * position: FIXED, not absolute. The footer scrolls its own content (see
       the note above on why it cannot use zoom), and an absolute pseudo scrolls
       with it — the ground slid under the type. Fixed also lets the field stand
       78vh tall while the footer's box is only 62vh, so the gradient starts
       well ABOVE the box and there is no box edge for it to terminate on. It
       stays inside this element's stacking context (`isolation: isolate` on the
       parent, z-index: -1 here), so it cannot fall behind the canvas — position:
       fixed escapes an ancestor's containing block, never its painting order.

     * NO backdrop-filter and NO border. Both draw the box. The depth now comes
       from the gradient itself, which is the only thing here without an edge.

     * A MASK doing the dissolve. linear-gradient to transparent over the top
       half, so the field literally has no top: it thins into the scene the exit
       particles are still falling through. That is the "resolve into" the
       ending always claimed and never did.

   Colour is the arrival's register, not a slate: near-black neutral ground with
   royal blue and violet emission concentrated low and dissolving upward — the
   same construction as the sky shader, for the same reason.

   ONE LIVE SHEET, AND IT IS STATIC. Four fixed gradient stops, no animation, no
   drift. A drifting CSS bloom measures ~12.9ms each on this codebase and ~50ms
   for four; a static one is free. The motion in this band comes from the WebGL
   particles falling through it, which are already being drawn.
   ------------------------------------------------------------------------- */
html.sing-hijack .sing-arr-footer::before,
html.sing-native  .sing-arr-footer::before {
  content: "";
  position: fixed;
  /* `inset: auto 0 0 0`, NOT `left/right/bottom` — AND THE DIFFERENCE IS A
   * VISIBLE HARD LINE ACROSS THE PAGE.
   *
   * `.vd-mesh-footer::before` in css/vedika-cinematic-home.css declares
   * `inset: -20%`, which is the shorthand and therefore sets `top` as well.
   * Naming only left/right/bottom here leaves that `top: -20%` standing, and a
   * fixed box with top, bottom AND height all specified drops `bottom`: the
   * field ran from y = -180 to y = 558 on a 900px viewport instead of from 162
   * to 900. Measured — a 47-value step across the full width at exactly y=557,
   * and the dissolve mask fading in above the top of the screen where nobody
   * could see it. The shorthand resets top to auto and the box is right. */
  inset: auto 0 0 0;
  height: min(82vh, 780px);
  z-index: -1;
  pointer-events: none;
  background:
    /* the dust: two soft off-centre plumes rising out of the lower edge, in the
       royal / violet register, concentrated rather than spread — a wide even
       haze is what made the old field muddy and it is not repeated here */
    radial-gradient(120% 74% at 22% 118%, rgba(56, 92, 255, 0.36) 0%, rgba(56, 92, 255, 0.14) 34%, rgba(56, 92, 255, 0) 68%),
    radial-gradient(108% 66% at 79% 124%, rgba(140, 66, 255, 0.32) 0%, rgba(140, 66, 255, 0.11) 36%, rgba(140, 66, 255, 0) 70%),
    /* one neon thread along the very bottom, saturated and narrow — the "little
       neon" against the deep ground, never an overall lift */
    radial-gradient(140% 26% at 50% 132%, rgba(64, 226, 255, 0.30) 0%, rgba(64, 226, 255, 0) 72%),
    /* the ground: near-black and NEUTRAL, so everything above reads as emission
       rather than as a tint on grey */
    linear-gradient(180deg, rgba(3, 3, 10, 0) 0%, rgba(4, 4, 13, 0.62) 42%, rgba(4, 4, 14, 0.90) 78%, rgba(3, 3, 11, 0.96) 100%);
  /* THE DISSOLVE. No top edge exists to see. */
  -webkit-mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.28) 26%, rgba(0, 0, 0, 0.86) 56%, #000 78%);
  mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.28) 26%, rgba(0, 0, 0, 0.86) 56%, #000 78%);
  /* CANCEL THE OTHER SHEET'S DRIFT AND BLUR, EXPLICITLY.
   *
   * css/vedika-cinematic-home.css declares `.vd-mesh-footer::before` with
   * `filter: blur(14px) saturate(126%)` and `animation: vd-mesh-drift 42s
   * infinite alternate`. This rule outranks it on every property it names —
   * background, position, opacity — but a property you do not name is a
   * property you do not win, so the blur and the 42-second transform animation
   * were both still live underneath the new field: a DRIFTING CSS bloom, which
   * measures ~12.9ms a frame on this codebase, running for the entire ending.
   * Named, and cancelled. */
  filter: none;
  animation: none;
  /* rises with --arr-f, so the field forms out of the frame rather than
     switching on; translate only, no size change, so nothing relayouts */
  transform: translateY(calc((1 - var(--arr-f, 0)) * 46px));
  opacity: var(--arr-f, 0);
}

/* THE OTHER HALF OF THE SEAM, AND IT WAS NOT THIS FILE'S ::before AT ALL.
 *
 * Measured on the p = 1.0 capture at 1440x900: a hard horizontal step at
 * y = 378 — the exact top of the footer's box — where the field jumps from
 * rgb(7, 6, 24) to rgb(18, 18, 34) in a single row. That is
 * `.vd-mesh-footer::after` from css/vedika-cinematic-home.css: an
 * `inset: 0` grain rectangle at opacity 0.30, which this file had never touched
 * because every override here was written against ::before.
 *
 * The grain is worth keeping — a large smooth gradient bands on 8-bit displays
 * and this is what stops it. It just cannot be a rectangle. Given the same
 * fixed box and the same dissolve mask as the field it grains, it becomes part
 * of the dust instead of the lid on it. */
html.sing-hijack .sing-arr-footer::after,
html.sing-native  .sing-arr-footer::after {
  position: fixed;
  inset: auto 0 0 0;
  height: min(82vh, 780px);
  z-index: -1;
  opacity: calc(var(--arr-f, 0) * 0.22);
  -webkit-mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.28) 26%, rgba(0, 0, 0, 0.86) 56%, #000 78%);
  mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.28) 26%, rgba(0, 0, 0, 0.86) 56%, #000 78%);
}

/* THE COLUMNS CONDENSE, ONE AFTER ANOTHER.
 *
 * "make it appear like all the tunnel end partcals coverting to footer all area
 * text". The 3D half of that is the exit rim coming apart over exactly this
 * span (uSpread, in updateExitPortal); this is the other half — each column
 * arrives out of a blur, from below, on its own delay, so the type looks like it
 * is precipitating out of the particle fall rather than being switched on.
 *
 * Driven by --arr-f, which is scroll position, NOT by a transition: this has to
 * run backwards when the founder scrolls back up, and a transition cannot.
 * The clamps are what keep the maths honest at the ends of the range. */
html.sing-hijack .sing-arr-footer .grid > div,
html.sing-native  .sing-arr-footer .grid > div,
html.sing-hijack .sing-arr-footer > div:first-child,
html.sing-native  .sing-arr-footer > div:first-child,
html.sing-hijack .sing-arr-footer:not(.vk-footer) .border-t,
html.sing-native  .sing-arr-footer:not(.vk-footer) .border-t {
  --d: 0;
  --t: clamp(0, calc((var(--arr-f, 0) - var(--d)) / 0.55), 1);
  opacity: var(--t);
  transform: translateY(calc((1 - var(--t)) * 26px));
  filter: blur(calc((1 - var(--t)) * 5px));
}
html.sing-hijack .sing-arr-footer .grid > div:nth-child(2),
html.sing-native  .sing-arr-footer .grid > div:nth-child(2) { --d: 0.10; }
html.sing-hijack .sing-arr-footer .grid > div:nth-child(3),
html.sing-native  .sing-arr-footer .grid > div:nth-child(3) { --d: 0.18; }
html.sing-hijack .sing-arr-footer .grid > div:nth-child(4),
html.sing-native  .sing-arr-footer .grid > div:nth-child(4) { --d: 0.26; }
html.sing-hijack .sing-arr-footer .border-t,
html.sing-native  .sing-arr-footer .border-t { --d: 0.34; }

/* Formed. Drop the filters rather than setting them to zero — see the note in
 * updateArrival: a blur of 0px still promotes every column to its own composited
 * layer and still costs a pass on a frame where nothing is moving. */
html.sing-hijack .sing-arr-footer.is-set .grid > div,
html.sing-native  .sing-arr-footer.is-set .grid > div,
html.sing-hijack .sing-arr-footer.is-set > div:first-child,
html.sing-native  .sing-arr-footer.is-set > div:first-child,
html.sing-hijack .sing-arr-footer.is-set .border-t,
html.sing-native  .sing-arr-footer.is-set .border-t {
  filter: none;
  transform: none;
  opacity: 1;
}

/* type, in the new theme. Colours only — no layout, no reordering, nothing
   removed. The Tailwind classes on the markup are unlayered, so these need the
   html.sing-* scope to outrank them, which they have. */
/* :not(.vk-footer) - same reason as the link rules above. This paints column
   headings #f4f0ff, near-white, correct on the old dark slate footer and close to
   invisible on the new light plate: PRODUCT measured 1.04:1 against the bright band
   of the gradient it sits on, with RESOURCES at 1.19 and COMPANY at 1.09. Reported
   as "lot of titles not readable". The light footer sets its own heading ink. */
html.sing-hijack .sing-arr-footer:not(.vk-footer) h5,
html.sing-native  .sing-arr-footer:not(.vk-footer) h5 {
  color: #f4f0ff;
  letter-spacing: -0.008em;
}
html.sing-hijack .sing-arr-footer:not(.vk-footer) a,
html.sing-native  .sing-arr-footer:not(.vk-footer) a {
  color: #c2b9de;
  text-decoration: none;
  transition: color 160ms linear;
}
html.sing-hijack .sing-arr-footer:not(.vk-footer) a:hover,
html.sing-native  .sing-arr-footer:not(.vk-footer) a:hover { color: #ffffff; }
html.sing-hijack .sing-arr-footer a:focus-visible,
html.sing-native  .sing-arr-footer a:focus-visible {
  outline: 2px solid #c9b6ff;
  outline-offset: 3px;
  border-radius: 4px;
}
html.sing-hijack .sing-arr-footer .border-t,
html.sing-native  .sing-arr-footer .border-t { border-color: rgba(255, 255, 255, 0.12) !important; }
html.sing-hijack .sing-arr-footer:not(.vk-footer) .text-gray-500,
html.sing-native  .sing-arr-footer:not(.vk-footer) .text-gray-500 { color: #8d85a9 !important; }

/* the support strip at the top of the footer keeps its accent, warmed to the
   arrival's palette rather than the old violet-on-slate */
html.sing-hijack .sing-arr-footer:not(.vk-footer) a[style*="a855f7"],
html.sing-native  .sing-arr-footer:not(.vk-footer) a[style*="a855f7"] { color: #c9b6ff !important; }

/* ------------------------------------------------------------------ mobile */

@media (max-width: 767px) {
  .sing-arr { width: 94vw; gap: 10px; }
  .sing-arr__wire {
    grid-template-columns: minmax(0, 1fr);
    gap: 8px;
  }
  /* the arrow turns to point down the column instead of across it */
  .sing-arr__flow { flex-direction: row; justify-content: center; padding: 2px 0; }
  /* the request is the smaller half of the story on a phone: the answer is the
     thing worth the height, so the body of the request collapses to its head */
  .sing-arr__card--req .sing-arr__code { max-height: 15vh; overflow: auto; }
  .sing-arr__answer { max-height: 30vh; }
  .sing-arr__langs { grid-template-columns: minmax(0, 1fr); }
  .sing-arr__a { max-height: 17vh; }
  .sing-arr__codes li { padding: 2px 6px; font-size: 9px; }
  /* ONE COLUMN. The desktop rule is a two-column grid, and `flex-direction`
     does not reach a grid — left as it was, the two capsules kept their
     `min-width: 210px` side by side and measured 201px wide starting at x = -13
     on a 390px screen, i.e. both of them hanging off the left edge. A grid has
     to be re-declared as a grid. */
  .sing-arr__acts {
    grid-template-columns: minmax(0, 1fr);
    column-gap: 0;
    row-gap: 10px;
  }
  .sing-arr__act { width: 100%; }
  .sing-arr__act--primary,
  .sing-arr__act--secondary {
    min-width: 0;
    padding-block: 10px;
  }
  /* THE TERTIARY STOPS BEING A THIRD FULL-WIDTH SLAB.
   *
   * "100th time i asking u fix that login in button ui ux".
   *
   * The previous pass on this button (see the ghost-fill block above) fixed the
   * wrong half. That one was about VISIBILITY - "cannot even see it" - and it is
   * genuinely fixed: measured at the ending, the label composites to 9.8:1 on the
   * capsule over the sky. Contrast was never what was left.
   *
   * What was left is the hierarchy, and only on mobile. Measured at 390x844 at the
   * ending, before this rule:
   *
   *     primary   Create an account   280 x 64
   *     secondary Generate an API key 280 x 51
   *     tertiary  Log in              280 x 46   <- 100% of the primary's WIDTH
   *
   * with a 53x18 label sitting in that 46px box, i.e. 28px of empty band. The markup
   * comment in vedika-singularity.js states the three doors are a RANK and that Log
   * in is the quiet one; `justify-self: stretch` contradicted that at the one
   * viewport where the three stack vertically and the comparison is unavoidable.
   * Three identical bars in a column is what "all 3 buttoins are poorly designed"
   * meant originally - stretching the ghost had quietly reintroduced it on phones
   * after it had been fixed on desktop, where `justify-self: center` was left alone.
   *
   * It now hugs its label. It keeps a full 44px tap target: demoting a control in
   * rank is not licence to make it hard to hit, and the label is the only thing that
   * shrinks - the touch area does not. */
  .sing-arr__act--tertiary {
    justify-self: center;
    /* beats `.sing-arr__act { width: 100% }` on order, not specificity - both are
       one class, and this block is the later one. Keep them in this order. */
    width: auto;
    min-width: 0;
    padding-block: 0;
    padding-inline: 30px;
    min-height: 44px;
    text-align: center;
  }
  .sing-arr__act--tertiary .vds-pill__label { margin-inline: auto; }
  /* THE ENDING HAS TO SHARE 844 PIXELS.
   *
   * At the desktop numbers the CTA block and the footer both wanted the middle
   * of a 390x844 frame and simply drew on top of each other — screenshotted at
   * p = 0.985, the three capsules were sitting across the footer's brand column
   * and both were unreadable. Neither can be hidden, so both are made smaller
   * and given a boundary: the footer takes the bottom 58vh, the CTA compacts to
   * about 27vh and rides above it. */
  .sing-arr--cta {
    gap: 8px;
    transform: translateY(calc(var(--arr-dy, 0px) - var(--arr-fold, 0) * 32vh)) scale(calc(1 - var(--arr-fold, 0) * 0.32));
  }
  .sing-arr--cta .sing-arr__badge { width: clamp(48px, 14vw, 62px); }
  .sing-arr--cta .sing-arr__h { font-size: clamp(21px, 6.2vw, 28px); }
  /* THE CHIP HAS TO CLEAR THE FOOTER'S TOP EDGE, NOT JUST LOOK SMALL.
   *
   * Measured at 390x844 at the ending: the chip occupied y336-384 against a footer
   * top edge of y354, so 29px of it lay inside the footer's box. The footer wins the
   * hit test (z-index 8 over the arrival layer's 7) but it is at opacity 0.73 while
   * it forms, so the two composite and both are legible at once - that is the frame
   * where "POST /api/v1/astrology/query" reads straight through the Changelog link.
   *
   * The overlap is fixed by giving the chip back the ~30px, not by raising a
   * z-index: whichever of the two wins outright, one of them is then wrong. */
  .sing-arr__ep { padding: 4px 12px; font-size: 9px; line-height: 1.34; }
  html.sing-hijack .sing-arr-footer,
  html.sing-native  .sing-arr-footer {
    /* a four-column footer on a 390px screen is a very tall column, so it gets
       more of the frame and the journey plays through whatever still overflows */
    max-height: 58vh;
    padding-top: 12px !important;
    padding-bottom: 10px !important;
  }
  /* TWO COLUMNS, NOT FOUR AND NOT ONE. Four is unreadable at 390px; one is
     roughly 1400px of column and would spend the whole ending scrolling. Every
     link and every heading is still present and still in source order — this
     only changes how many of them sit side by side. */
  html.sing-hijack .sing-arr-footer .grid,
  html.sing-native  .sing-arr-footer .grid {
    display: grid !important;
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
    gap: 14px 12px !important;
  }
  html.sing-hijack .sing-arr-footer .grid > div:first-child,
  html.sing-native  .sing-arr-footer .grid > div:first-child { grid-column: 1 / -1; }
  html.sing-hijack .sing-arr-footer ul,
  html.sing-native  .sing-arr-footer ul { line-height: 1.35; }
  html.sing-hijack .sing-arr-footer li,
  html.sing-native  .sing-arr-footer li { margin: 0 0 3px !important; }
  html.sing-hijack .sing-arr-footer h5,
  html.sing-native  .sing-arr-footer h5 { margin-bottom: 6px !important; font-size: 13px; }
  html.sing-hijack .sing-arr-footer a,
  html.sing-native  .sing-arr-footer a { font-size: 12px; }
}

/* --------------------------------------------------------- reduced motion */

/* Under reduced motion the engine never builds the arrival at all and the
   footer never gets .sing-arr-footer, so the document is a normal page. These
   only cover the case where the class is present and the user changes the
   preference mid-session. */
@media (prefers-reduced-motion: reduce) {
  .sing-arr__lang { transition: none; }
  .sing-arr__flow i,
  .sing-arr__caret { animation: none; }
  html.sing-hijack .sing-arr-footer .grid > div,
  html.sing-native  .sing-arr-footer .grid > div,
  html.sing-hijack .sing-arr-footer > div:first-child,
  html.sing-native  .sing-arr-footer > div:first-child,
  html.sing-hijack .sing-arr-footer .border-t,
  html.sing-native  .sing-arr-footer .border-t { filter: none; transform: none; }
}

/* =========================================================================
   MOBILE DESIGN PASS — 2026-07-29
   =========================================================================
   These pages were designed desktop-first and only spot-checked at 390x844.
   Everything below was found by sweeping 320/360/390/414/480/640/641/768/
   834/1024 and measuring, not by eyeballing one width. Each block records
   the measurement that justifies it so the next pass can re-derive it.

   Nothing here hides content. Where a grid could not hold its own text the
   grid was made fluid; the text was never removed, shortened or clipped.
   ------------------------------------------------------------------------ */

/* --- 1. THE HERO STANDFIRST WAS HALF OFF THE LEFT EDGE ON EVERY PHONE ----
 *
 * vedika-cinematic-home.css centres the standfirst on desktop with
 * `left: 50%; transform: translateX(-50%)`. Its `@media (max-width: 767px)`
 * block then returns the copy children to the normal flow with
 * `position: static; left/right/top/bottom: auto; width: auto` — but it never
 * resets the transform. `left: 50%` stops applying the moment the element is
 * static; the translate does not. So the paragraph kept a permanent
 * translateX(-50%) of its own width and hung off the left edge of the screen.
 *
 * Measured on index.html before this fix:
 *   390px viewport — p.singularity-subtitle box L-151 R195 (346px wide,
 *                    transform matrix(1,0,0,1,-173,0) = exactly -50%)
 *   320px viewport — L-116 R160, first 20 characters of every line off-screen
 * The rendered text read "computed from a real ephemeris / plain language, in
 * your own." — the sentence started off the side of the phone.
 *
 * documentElement.scrollWidth never caught this because body is
 * `overflow-x: hidden`, so the copy was clipped rather than scrollable. The
 * overflow check passed while the sentence was unreadable.
 *
 * This must stay a plain (non-!important) declaration. vedika-singularity.js
 * consumeHero() captures getComputedStyle(el).transform as the resting matrix
 * and re-applies it as the first entry of the fall transform, so the value
 * resolving to `none` here is what makes the fall start from the right place.
 * An inline transform written by that script still wins, as it must. */
@media (max-width: 767px) {
  .singularity-copy .singularity-kicker,
  .singularity-copy h1,
  .singularity-copy .singularity-subtitle,
  .singularity-copy .singularity-actions { transform: none; }
}

/* --- 2. HERO CTA PAIR BROKE MID-PHRASE ----------------------------------
 *
 * At <=479 the two pills sit at `flex: 1 1 46%`, which gave each about 160px
 * and wrapped the labels to "Enter the / sandbox" and "Read the API / docs" —
 * a two-word last line under a two-word first line, with the arrow floated
 * away from the phrase it belongs to. One pill per row reads as a decision;
 * two broken pills read as a bug.
 *
 * (The pills' own scrollWidth exceeds clientWidth at every width. That is the
 * `::before` conic sweep at `inset: -80%`, which is decorative and is what the
 * `overflow: hidden` is for. It is not a text overflow — do not "fix" it.) */
/* UPDATE: two-up again, but at a size where the phrase actually fits.
 *
 * The one-per-row stack above was the right call against a 46% pill at the ORIGINAL
 * font size - two pills broken mid-phrase read as a bug, and that reasoning stands.
 * What it cost was measured later: each pill spanned 346px of a 390px screen, 89% of
 * the width, and the pair ate 107px of height - 12.7% of the viewport - on the one
 * layout with the least room to spare.
 *
 * The wrap was never about the column count, it was about the label not fitting the
 * box. At 10.5px with 12px side padding, "Enter the sandbox" needs about 146px and a
 * 2-up row at 390 gives each pill 168px, so it fits with room. Verified at 390 and
 * 400 before shipping - if a future label is longer, this goes back to a stack
 * rather than being allowed to wrap. */
@media (max-width: 479px) {
  .singularity-copy .singularity-actions {
    flex-direction: row;
    align-items: stretch;
    gap: 8px;
    flex-wrap: nowrap;
  }
  .singularity-copy .singularity-button {
    flex: 1 1 0;
    min-width: 0;
    width: auto;
    min-height: 42px;
    padding: 10px 12px;
    font-size: 10.5px;
    white-space: nowrap;
  }
}

/* --- 3. THE HOME-PAGE PLAN CARDS AT TABLET WIDTHS ------------------------
 *
 * `md:grid-cols-4` is Tailwind's 768px breakpoint, so four plan cards snap to
 * four columns at exactly the width where four columns do not fit. Each card
 * carries `p-8` (64px of horizontal padding), so a 137px column leaves 71px of
 * content — narrower than every price on it.
 *
 * Measured clipping inside those cards, before this fix:
 *    768px  — column 136.8px, content 71px
 *             "Vedika Standard: $0.01-$0.03/query"  needs 187px  (+116 clipped)
 *             "Vedika Swift: $0.02-$0.05/query"     needs 165px  (+94)
 *             "Professional"                        needs 139px  (+68)
 *             "$120/mo", "Subscribe - $12/mo"       both cut
 *    834px  — +101 worst
 *   1024px  — +63 worst
 * Every one of those is a price or a plan name, cut in half, on the home page.
 *
 * Fixed by letting the track count follow the available width instead of the
 * viewport, and by relaxing the card padding below the desktop layout. The
 * 4-up desktop composition is untouched at >=1280. */
@media (max-width: 1279px) {
  div[class~="grid"][class~="md:grid-cols-4"][class~="gap-8"] {
    grid-template-columns: repeat(auto-fit, minmax(238px, 1fr));
  }
  div[class~="grid"][class~="md:grid-cols-4"][class~="gap-8"] > div[class~="p-8"] {
    padding-left: 22px;
    padding-right: 22px;
  }
}

/* --- 4. THE METRIC / CAPABILITY GRIDS ------------------------------------
 *
 * Same family of failure in the smaller stat grids: a fixed column count meets
 * a label that has never been allowed to wrap.
 *
 *   .grid-cols-2.md:grid-cols-4  320px -> 120px cols
 *        "Source-documented interpretation languages*"  +41 clipped
 *        "VERIFY"                                       +32
 *        "2026-07-26 source-documented operations*"     +15
 *                                768px -> 148.8px cols, "VERIFY" +26
 *   .grid-cols-3.gap-4.text-white 320px -> 69.3px cols
 *        "Monthly subscription" +33, "Pay-Per-Token" +23, "$0.02" +23
 *   .grid-cols-2.gap-3.text-sm   320px -> 104px cols
 *        "Door auspiciousness classifier" +30
 *
 * "$0.02" being clipped is a price. These are made fluid rather than given a
 * smaller font, so the numbers stay at the size the design intends. */
div[class~="grid"][class~="grid-cols-2"][class~="md:grid-cols-4"] {
  grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
}
@media (max-width: 400px) {
  div[class~="grid"][class~="grid-cols-3"][class~="text-white"] {
    grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
  }
}
@media (max-width: 380px) {
  div[class~="grid"][class~="grid-cols-2"][class~="gap-3"][class~="text-sm"] {
    grid-template-columns: 1fr;
  }
}

/* Language chips: at 320 the chip's flex row is 110px wide and the language
 * name sits in the second track, so "Malayalam" needed 122px and lost its
 * last glyphs (+12; "Gujarati" and "Assamese" +2). Wrapping the row alone does
 * not help — the label's own box is the one that is too narrow — so the row is
 * allowed to wrap AND the name is allowed to break rather than be cut. */
@media (max-width: 400px) {
  div[class~="flex"][class~="gap-3"][class~="hover-scale"] { flex-wrap: wrap; }
  div[class~="flex"][class~="gap-3"][class~="hover-scale"] .font-semibold,
  div[class~="flex"][class~="gap-3"][class~="hover-scale"] .text-xs { overflow-wrap: anywhere; }
}

/* --- 5. THE SUPPORT WIDGET LANDED ON THE LAST LINE OF THE FOOTER ---------
 *
 * #vedika-support-widget is fixed, 46-56px square, injected by
 * vedika-support-widget.js and shared by 135 pages, so it cannot be restyled
 * from here without affecting all of them. Instead the page ends with enough
 * clear floor for it to sit on.
 *
 * Measured at the document bottom before this fix:
 *   320px — widget [258,782,304,828] covering
 *           "Pune, Maharashtra, India | support@vedika.io" and
 *           "Made by Abhishek Raj"
 *   390px — same collision
 * The widget is bottom-right, so the reserve only has to clear its height plus
 * its own bottom offset. */
@media (max-width: 767px) {
  body > footer,
  footer.sing-arr-footer { padding-bottom: 104px; }
}

/* --- 6. HERO TYPE DETAIL ON PHONES ---------------------------------------
 *
 * Three things the desktop rules leave behind once the copy is back in flow:
 *
 * a) The standfirst keeps `text-align: center` from the desktop composition
 *    (where it is centred under the hole) while the kicker and headline are
 *    left-aligned. Mixed alignment in a three-element stack reads as an
 *    accident. The mobile block already sets `.singularity-copy` to
 *    `text-align: left`; the standfirst just never inherited it.
 *
 * b) The headline's italic second clause sits on its own line-height, so
 *    "Get a computed / answer." opened a gap roughly twice the leading of the
 *    line above it at 320-360.
 *
 * c) `text-wrap: balance` on the standfirst is what keeps the last line from
 *    coming down as one orphaned word once the measure is this narrow. */
@media (max-width: 767px) {
  .singularity-copy .singularity-subtitle {
    text-align: left;
    text-wrap: pretty;
  }
  .singularity-copy h1,
  .singularity-copy h1 em { line-height: 1.06; }
  .singularity-copy h1 em { display: inline-block; }
}

/* --- 7. STAT GRIDS: 4-UP ONLY WHERE 4-UP FITS ---------------------------
 *
 * minmax(148px) is right for a phone but still lets four tracks form in the
 * 641-1279 band, where the stat values are at their `sm:text-4xl` size.
 * "VERIFY" is a single unbreakable token 133px wide at 36px extrabold, so a
 * 148px track with the card's own 41px of padding still cut it (+26 at 768).
 * Raising the floor in that band drops it to three tracks, which fit.
 *
 * The band starts at 640, not 641: `sm:text-4xl` is Tailwind's 640px
 * breakpoint, so 640 is the first width where the value is 36px. Starting the
 * band at 641 left exactly one width — 640 — with 165px tracks and "VERIFY"
 * clipped by 10. */
@media (min-width: 640px) and (max-width: 1279px) {
  div[class~="grid"][class~="grid-cols-2"][class~="md:grid-cols-4"] {
    grid-template-columns: repeat(auto-fit, minmax(178px, 1fr));
  }
}

/* Stat captions are long hyphenated compounds — "Source-documented
 * interpretation languages*" measured a 119px unbreakable run against a 110px
 * track. Let the compound break rather than let the caption be cut. */
div[class~="grid"][class~="grid-cols-2"][class~="md:grid-cols-4"] .text-sm,
div[class~="grid"][class~="grid-cols-2"][class~="md:grid-cols-4"] .text-xs {
  overflow-wrap: anywhere;
  hyphens: auto;
}

/* --- 8. TAP TARGETS -----------------------------------------------------
 * The hero sound toggle measured 104x34 at 390. The orbiting `.sing-moon`
 * labels measure 36px and are deliberately NOT grown: they are placed on an
 * orbit by the engine each frame, and padding them to 44 makes adjacent moons
 * overlap — which is a worse tap outcome than 36px, not a better one. */
@media (max-width: 767px) {
  .singularity-sound { min-height: 44px; }
}

/* --- 9. THE FAB ON THE FOOTER'S LAST LINE, IN THE ARRIVAL STATE ---------
 *
 * Section 5 gives the NORMAL document footer clear floor. The arrival footer
 * is a different object: `html.sing-hijack .sing-arr-footer` is a fixed,
 * scroll-driven panel pinned to `bottom: 0` with `max-height: 62vh` and its
 * own `overflow-y: auto`, so page-level padding never reaches it.
 *
 * This state is only reachable by actually crossing the event horizon, which
 * means driving the hijack with a real WheelEvent carrying a NON-ZERO deltaX —
 * `mouse.wheel(0, 220)` sends deltaX: 0, a value no trackpad ever produces, and
 * the journey ignores it. Measured after 220 wheel notches at 390x844:
 *
 *    footer  fixed [0,354,390,844], padding-bottom 10px
 *    FAB     [328,782,374,828]
 *    address line "Pune, Maharashtra, India | support@vedika.io"
 *            spans L35-R355 at T790-B804
 *    -> the widget covers the last 27px of that line, over the support
 *       address, which is the one string on the page a stuck user needs.
 *
 * Because the panel is its own scroll container, bottom padding here is clear
 * floor at the END of its scroll rather than a squeeze on the columns. */
@media (max-width: 900px) {
  html.sing-hijack .sing-arr-footer,
  html.sing-native  .sing-arr-footer {
    padding-bottom: 74px !important;
  }
}
