/* =============================================================================
   Site intro overlay
   A full-screen cinematic cover that plays once per session: eight shards
   converge into the brand cube, a flash covers the handover, the cube settles
   with seams/core/halo, then the whole stage zooms in and fades to reveal the
   site. Ported to vanilla from the source DC component. Follows the site's
   active theme (--in-* tokens below) so the handover to the page itself
   isn't a jarring dark-to-light cut; the cube render itself stays achromatic
   (palette + theme only, never accent hue — see the hero cube's own comment),
   while the chrome text/logo/button around it use --accent-text so the intro
   reads as the active palette, same as the rest of the site.
   All keyframes are prefixed `in-` so they never collide with the hero cube.
   ========================================================================== */
.intro {
  /* --in-bg ties straight to the site's own --bg token, not an approximated
     hex, so the intro's ground is exactly the page's ground — no seam at
     handover — and it inherits the active palette's tint for free. The
     other tokens still default dark; light theme overrides them below. */
  --in-bg: var(--bg);
  --in-glow: #fff;
  --in-vignette: #000;
  /* the raw accent (Innodisk Blue's h1≈263.5 reads as a saturated blue-violet)
     is fine as a small UI accent elsewhere, but covering the logo/eyebrow in
     it solid was too much colour for this screen. Mixed toward --text so the
     intro's chrome reads as tinted, not painted. */
  --intro-accent: color-mix(in oklch, var(--accent-text) 60%, var(--text) 40%);
  position: fixed;
  inset: 0;
  z-index: 2000;
  background: var(--in-bg);
  overflow: hidden;
  cursor: pointer;
  font-family: var(--font-ui);
}
[data-theme="light"] .intro {
  /* reuses the hero cube's own light-theme glow tone (--hc-lit), so the
     intro's halo matches the muted-grey cube treatment used everywhere
     else on a light background */
  --in-glow: #7b8794;
  /* a dark vignette read fine as a cinematic focus device on the dark
     theme, but on the light theme it just looked like dirty smudged
     corners against a near-white ground — this reuses the same light
     neutral tone already in the halo/pool gradients above */
  --in-vignette: #c6d0d6;
}
/* the zoom + fade that hands over to the site */
.intro-stage {
  position: absolute;
  inset: 0;
  transform: scale(1);
  transform-origin: 50% 46%;
  transition: transform 1.35s cubic-bezier(0.5, 0, 0.7, 0.25);
}
.intro.entering {
  opacity: 0;
  pointer-events: none;
  transition: opacity 1.05s cubic-bezier(0.5, 0, 0.75, 0.2);
}
.intro.entering .intro-stage {
  transform: scale(2.6);
}
.intro.done {
  display: none;
}
.intro-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
/* chrome: back to centred — mark stacked top, headline/bar/button stacked
   bottom, all on one axis. The off-centre editorial layout was tried and
   rejected; centred is the one that's stayed liked. */
.intro-chrome {
  position: absolute;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  pointer-events: none;
  transition: opacity 0.5s ease;
}
.intro.entering .intro-chrome {
  opacity: 0;
}
.intro-top {
  top: 0;
  padding-top: clamp(24px, 4vh, 44px);
}
.intro-mark {
  opacity: 0;
  animation: in-fade-up 1s cubic-bezier(0.16, 1, 0.3, 1) 2.9s both;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.intro-word {
  display: flex;
  align-items: center;
  gap: 9px;
  color: var(--text);
}
.intro-word .logo {
  width: 26px;
  height: 26px;
  color: var(--text);
}
.intro-word span {
  font-family: var(--font-display, var(--font-ui));
  font-weight: 700;
  font-size: 22px;
  letter-spacing: 0.5px;
}
.intro-eyebrow {
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: clamp(11px, 1.5vh, 13px);
  letter-spacing: 3px;
  text-transform: uppercase;
  color: var(--text-2);
}
.intro-bottom {
  bottom: 0;
  flex-direction: column;
  align-items: center;
  gap: 22px;
  /* this block got shorter once the loader started disappearing instead of
     sitting there permanently (no more bar/pct row) — anchored flush to
     bottom:0, that left a big gap above it and almost none below, hugging
     the bottom edge. A bigger bottom padding lifts it back toward the
     middle of the space under the cube instead. */
  padding-bottom: clamp(64px, 12vh, 140px);
}
/* display:none until intro.js flips .bar-done on .intro (a hard cut driven
   by one JS timer, not a second animation-delay that has to be hand-kept
   in sync with the loader's own — see intro.js). While it's none, it isn't
   in the layout at all, so .intro-slot sizes itself to the small loader
   alone; the moment it switches to block it fades in via the same
   in-fade-up beat the rest of the intro's chrome uses. */
.intro-headline {
  display: none;
  opacity: 0;
  font-family: var(--font-display, var(--font-ui));
  font-weight: 800;
  /* combines vw and vh so it stays large on short/wide windows too — 7vh
     alone collapsed toward the 32px floor whenever the viewport was wider
     than it was tall. */
  font-size: clamp(42px, 5vw + 4vh, 72px);
  line-height: 1;
  letter-spacing: 0.1px;
  color: var(--text);
  text-align: center;
  padding: 0 24px;
  text-wrap: balance;
}
.intro-headline em { font-style: normal; color: var(--intro-accent); }
/* the actual swap: one class flip, both sides of it. The loader vanishes
   and the headline enters the layout in the same style recalculation, so
   there's never a frame with both present (or the old "half loader, half
   headline" leftover from an animation-delay that drifted). */
.intro.bar-done .intro-loader { display: none; }
.intro.bar-done .intro-headline { display: block; animation: in-fade-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; }
/* the 5vw+4vh formula above hits its own 72px ceiling at nearly every
   laptop/desktop width (e.g. 1366x768 already computes to 102px), so above
   ~900px wide it wasn't scaling at all — just pegged flat at the max. This
   mirrors the hero's own laptop-range pass (same 1025-2000px window) with
   a lower ceiling suited to this full-screen moment instead of the hero's
   inline title. */
@media (min-width: 1025px) and (max-width: 2000px) {
  .intro-headline { font-size: clamp(40px, 2.4vw + 1.6vh, 50px); }
}
@media (max-width: 480px) {
  .intro-headline { font-size: clamp(36px, 11vw, 48px); }
}
/* the reference brief: a rounded pill track with a small character riding
   its leading edge, and a percentage readout underneath. The character is
   the brand's own mark (squares + dot), not a cartoon animal — same beat,
   on-brand cast. The walker moves via `transform`, not `left`, so it stays
   compositor-only and doesn't reintroduce the jank the Pac-Man version had. */
/* .intro-slot holds this and the headline in one shared spot — see the
   .intro.bar-done rules above/below for the actual swap, which is a hard
   display:none/block flip driven by a single JS timer (intro.js), not a
   timed CSS animation this has to stay in sync with by coincidence. */
.intro-slot { display: flex; flex-direction: column; align-items: center; }
.intro-loader {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
/* tick marks on the track itself — a ruler/rail texture (same idea as the
   node-ticked .rl-rail in the race section) instead of a flat blank pill */
.intro-bar {
  position: relative;
  width: min(280px, 46vw);
  height: 8px;
  border-radius: 999px;
  background:
    repeating-linear-gradient(to right,
      color-mix(in oklch, var(--border-strong) 80%, transparent) 0 1px,
      transparent 1px 14px),
    color-mix(in oklch, var(--border) 70%, transparent);
  overflow: visible;
}
.intro-bar i {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  background: var(--gradient-btn);
  box-shadow: 0 0 12px 1px var(--accent-glow, var(--accent));
  transform: scaleX(0);
  transform-origin: left center;
  animation: in-bar-grow 3.4s cubic-bezier(0.4, 0, 0.2, 1) 0.2s both;
}
@keyframes in-bar-grow { to { transform: scaleX(1); } }
/* rides the fill's leading edge: translateX over the track's own width,
   same duration/easing as the fill so it never drifts off the edge, plus
   a little hop for personality. Split across two elements on purpose: the
   outer one alone animates `left` (the only layout-triggering property in
   this whole loader, and a percentage move has to use `left` — transform
   percentages resolve against the element's own box, not the track's). The
   inner one only ever animates `transform`, so the hop stays compositor-
   only instead of doubling up layout work on one element every frame —
   that combination was the real source of the Pac-Man version's jank. */
.ib-walker {
  position: absolute;
  top: 50%;
  left: 0;
  width: 30px;
  height: 30px;
  /* -50% on Y would centre the walker exactly on the line, cutting through
     its middle — this pulls it up less, so the line sits higher within
     the icon (closer to its top) instead of splitting it in half */
  transform: translate(-50%, -30%);
  animation: in-walker-move 3.4s cubic-bezier(0.4, 0, 0.2, 1) 0.2s both;
}
/* the same rendered cube used on the Get Started button and the hero's
   field motes (--mote-img) — not a redrawn icon, the actual asset, so the
   walker is visibly the same object as everywhere else it appears. */
.ib-walker-in {
  display: block;
  width: 100%;
  height: 100%;
  background-image: var(--mote-img);
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  filter: drop-shadow(0 0 6px var(--accent-glow, var(--accent)));
  animation: in-walker-hop 0.5s ease-in-out 0.2s infinite;
}
@keyframes in-walker-move { to { left: 100%; } }
@keyframes in-walker-hop { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
/* an animated percentage counter — @property lets the browser interpolate
   a custom <integer> and print it via counter(), no JS timer needed.
   Degrades gracefully (static "0%") in browsers without @property support. */
@property --intro-pct-n {
  syntax: '<integer>';
  inherits: false;
  initial-value: 0;
}
.intro-pct {
  --intro-pct-n: 0;
  counter-reset: intro-pct var(--intro-pct-n);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  color: var(--text-2);
  animation: in-pct-count 3.4s cubic-bezier(0.4, 0, 0.2, 1) 0.2s both;
}
.intro-pct::after { content: counter(intro-pct) "%"; }
@keyframes in-pct-count { to { --intro-pct-n: 100; } }
@media (prefers-reduced-motion: reduce) {
  /* moot in practice — intro.js skips the whole overlay under reduced
     motion before any of this ever renders — but kept for the same
     defensive-fallback reason the other rules here are: if the loader
     somehow is on screen, it should show its finished state, not animate */
  .intro-bar i, .ib-walker, .ib-walker-in, .intro-pct { animation: none; }
  .intro-bar i { transform: scaleX(1); }
  .ib-walker { left: 100%; }
  .intro-pct { --intro-pct-n: 100; }
  .intro-loader { display: none; }
  .intro-headline { display: block; opacity: 1; }
}
/* the whole overlay is already click-to-skip (and Enter/Esc work too, see
   intro.js) — a separate visible button was just repeating that same
   affordance while costing a chunk of vertical rhythm on short/laptop
   viewports. A quiet text hint keeps the "you can skip this" signal
   without the extra row of button chrome. */
.intro-hint {
  opacity: 0;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  color: var(--text-2);
  animation: in-fade-up 1s cubic-bezier(0.16, 1, 0.3, 1) 3.45s both,
    intro-hint-pulse 2.6s ease-in-out 4.6s infinite;
}
@keyframes intro-hint-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
/* the halo/pool/sweep gradients are a near-white fade in the markup (each
   stop its own shade); collapsing them to one theme token loses that subtle
   falloff but keeps the glow visible instead of a white halo washing out
   against a near-white background. */
#inHalo stop, #inPool stop, #inSweep stop {
  stop-color: var(--in-glow);
}
/* the corner vignette darkens edges for focus on the dark theme, but on
   light theme it should lighten (soften) instead — see --in-vignette above */
#inVignette stop {
  stop-color: var(--in-vignette);
}
/* the white bloom that peaks as the stage hands over */
.intro-flash {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(72% 62% at 50% 46%, rgba(255, 255, 255, 0.95) 0%, rgba(246, 249, 251, 0.6) 28%, rgba(232, 238, 242, 0.22) 56%, rgba(226, 233, 238, 0) 100%);
  opacity: 0;
  transition: opacity 0.3s cubic-bezier(0.4, 0, 0.4, 1);
}
.intro.flash .intro-flash {
  opacity: 0.5;
  transition: opacity 0.75s cubic-bezier(0.4, 0, 0.4, 1);
}

/* --- shards converging on the eight octant seats --- */
@keyframes in-shard1 { 0% { opacity: 0; transform: translate(-330px, -186px) scale(0.3); } 18% { opacity: 1; } 72% { opacity: 1; transform: translate(-46px, -26px) scale(0.3); } 100% { opacity: 0; transform: translate(0, 0) scale(0.3); } }
@keyframes in-shard2 { 0% { opacity: 0; transform: translate(330px, -186px) scale(0.3); } 18% { opacity: 1; } 72% { opacity: 1; transform: translate(46px, -26px) scale(0.3); } 100% { opacity: 0; transform: translate(0, 0) scale(0.3); } }
@keyframes in-shard3 { 0% { opacity: 0; transform: translate(-330px, 186px) scale(0.3); } 18% { opacity: 1; } 72% { opacity: 1; transform: translate(-46px, 26px) scale(0.3); } 100% { opacity: 0; transform: translate(0, 0) scale(0.3); } }
@keyframes in-shard4 { 0% { opacity: 0; transform: translate(330px, 186px) scale(0.3); } 18% { opacity: 1; } 72% { opacity: 1; transform: translate(46px, 26px) scale(0.3); } 100% { opacity: 0; transform: translate(0, 0) scale(0.3); } }
@keyframes in-shard5 { 0% { opacity: 0; transform: translate(0, -330px) scale(0.3); } 18% { opacity: 1; } 72% { opacity: 1; transform: translate(0, -52px) scale(0.3); } 100% { opacity: 0; transform: translate(0, 0) scale(0.3); } }
@keyframes in-shard6 { 0% { opacity: 0; transform: translate(0, 330px) scale(0.3); } 18% { opacity: 1; } 72% { opacity: 1; transform: translate(0, 52px) scale(0.3); } 100% { opacity: 0; transform: translate(0, 0) scale(0.3); } }
@keyframes in-shard7 { 0% { opacity: 0; transform: translate(-390px, 40px) scale(0.3); } 18% { opacity: 1; } 72% { opacity: 1; transform: translate(-56px, 6px) scale(0.3); } 100% { opacity: 0; transform: translate(0, 0) scale(0.3); } }
@keyframes in-shard8 { 0% { opacity: 0; transform: translate(390px, 40px) scale(0.3); } 18% { opacity: 1; } 72% { opacity: 1; transform: translate(56px, 6px) scale(0.3); } 100% { opacity: 0; transform: translate(0, 0) scale(0.3); } }
@keyframes in-merge-flash { 0% { opacity: 0; transform: scale(0.3); } 40% { opacity: 0.55; transform: scale(1.15); } 100% { opacity: 0; transform: scale(1.9); } }
@keyframes in-cube-settle { 0% { opacity: 0; transform: scale(0.42); } 30% { opacity: 0.5; } 60% { opacity: 1; transform: scale(1.035); } 80% { transform: scale(0.994); } 100% { opacity: 1; transform: scale(1); } }
@keyframes in-seam-in { 0% { opacity: 0; transform: scale(0.4); } 45% { opacity: 1; transform: scale(1.16); } 100% { opacity: 0.9; transform: scale(1); } }
@keyframes in-seam-breathe { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }
@keyframes in-core-in { 0% { opacity: 0; transform: scale(0.25); } 50% { opacity: 1; transform: scale(1.18); } 100% { opacity: 0.94; transform: scale(1); } }
@keyframes in-core-pulse { 0% { opacity: 0.76; } 50% { opacity: 1; } 100% { opacity: 0.76; } }
@keyframes in-halo-in { 0% { opacity: 0; transform: scale(0.5); } 100% { opacity: 0.2; transform: scale(1); } }
@keyframes in-halo-breathe { 0% { opacity: 0.15; transform: scale(0.986); } 50% { opacity: 0.25; transform: scale(1.036); } 100% { opacity: 0.15; transform: scale(0.986); } }
@keyframes in-shock { 0% { opacity: 0; transform: scale(0.35); } 14% { opacity: 0.6; } 100% { opacity: 0; transform: scale(2.3); } }
@keyframes in-cluster-bob { 0% { transform: translateY(-7px); } 50% { transform: translateY(7px); } 100% { transform: translateY(-7px); } }
@keyframes in-sweep { 0% { transform: translateX(-330px) skewX(-20deg); opacity: 0; } 6% { opacity: 1; } 50% { opacity: 1; } 58% { transform: translateX(500px) skewX(-20deg); opacity: 0; } 100% { transform: translateX(500px) skewX(-20deg); opacity: 0; } }
@keyframes in-rise { 0% { opacity: 0; transform: translateY(46px); } 22% { opacity: 0.7; } 100% { opacity: 0; transform: translateY(-250px); } }
@keyframes in-grid-drift { 0% { transform: translate(0, 0); } 100% { transform: translate(-110.85px, -192px); } }
@keyframes in-fade-up { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: translateY(0); } }
@keyframes in-fade { from { opacity: 0; } to { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  /* no cinematic intro under reduced motion: it is pure decoration and blocks
     content — the site is shown immediately by intro.js instead */
  .intro {
    display: none;
  }
}

/* The glow layers are large feGaussianBlur regions animated every frame. Hint
   the compositor up front so their first cycle does not stall while the blur
   is rasterised for the first time. */
.intro-svg g[filter],
.intro-svg circle[fill^="url"],
.intro-svg ellipse[fill^="url"],
.intro-svg rect[fill^="url"] {
  will-change: opacity, transform;
}
.intro-stage {
  will-change: transform;
  backface-visibility: hidden;
}

/* The bloom is baked into the cube render, so eight shards each carried a full
   glow and the convergence read as a cluster of lanterns. Dim and desaturate
   them while they travel — the light belongs to the assembled cube. */
.intro-svg g[style*="in-shard"] {
  opacity: 0.5;
  filter: brightness(0.62) saturate(0.7);
}

/* Any delayed animation must hold its 0% frame during the delay, or the
   element renders at its authored style until it starts — which is how the
   sweep band appeared before the sequence had begun. */
.intro-svg [style*="in-rise"],
.intro-svg [style*="in-sweep"] {
  animation-fill-mode: backwards;
}

/* the bobbing cluster carries every blurred glow layer; promote it so the
   first cycle does not stall re-rasterising those filter regions */
.intro-svg g[style*="in-cluster-bob"] {
  will-change: transform;
}
