/* The Crossing — index.html hero region, acts 1 to 3.
   Grammar: continuous world. One sticky stage, one spacer, one canvas the scroll travels through.
   Nothing scrolls over the canvas; the copy arrives inside the world.
   Brand exceptions taken deliberately and only here: gradient text and neon glow.
   Loads after style.css. Every selector is under .crossing so nothing else on the page moves. */

.crossing {
  --cx-span: 520vh;          /* 3.1 for the threshold, the swarm and the echo; 2.1 for the world */
  --cx-nav: 77px;
  --cx-ink: #f2ecff;
  --cx-ink-soft: #ded6f5;
  --cx-void: #04010b;
  position: relative;
  height: var(--cx-span);
  margin: 0;
  padding: 0;
  background: var(--cx-void);
  isolation: isolate;
}

/* The stage is the world. It holds for the whole crossing and then releases into the document. */
.crossing-stage {
  position: sticky;
  top: 0;
  /* LARGE viewport height, not small. With 100svh the sticky stage is only as tall as the frame
     with the browser chrome showing, so the moment the chrome retracts it stops short and leaves a
     strip of the next section under it: on a 390x844 phone the stage was 679 and the frame 844.
     The spacer above already measures in vh, which is the large height, so svh here also made the
     two disagree. The stage is a full-bleed backdrop and its content is centred, so the band that
     sits under the chrome when the chrome is showing costs nothing. */
  height: 100vh;
  height: 100lvh;
  overflow: clip;
  display: grid;
  place-items: center;
  /* The void grade the canvas used to paint. It moved here when the canvas went transparent so the
     orbs could cross in front of the seeded world. */
  background: radial-gradient(125% 125% at 50% 102%, #260846 0%, #0c021a 55%, #04010b 100%);
}

.crossing-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 1;
  pointer-events: none;
}

/* Act 4, the world the descent arrives at. Five depth planes from one painting, parallaxed apart.
   It carries no copy of its own. Reduced motion holds the planes at rest, which composes the
   original picture exactly, so the still state is the painting rather than a disabled animation. */
.crossing-world {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

.crossing-world .cx-plane {
  position: absolute;
  left: 0;
  width: 100%;
  /* No will-change here: five permanently promoted layers at 1700px wide cost more in GPU memory
     and compositing than they save, and the transform animation is composited regardless. */
  background-repeat: no-repeat;
  background-position: 0 100%;
}

.crossing-world .cx-plane img {
  display: block;
  width: 100%;
  height: auto;
}


/* The backdrop: a blurred copy of the whole scene under every plane, so a parallax gap between two
   feathered bands reads as distance haze rather than a hole punched through to the void. */
.crossing-world .cx-haze {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: auto;
  z-index: 0;
  display: block;
  will-change: transform, opacity;
}

.crossing-world .cx-rise {
  position: absolute;
  border-radius: 50%;
  will-change: transform, opacity;
}

.crossing-scrim {
  position: absolute;
  z-index: 2;
  left: 50%;
  /* Taller than the stage on purpose, so the column's own fade-out happens off-frame. */
  top: -20%;
  width: min(138%, 104rem);
  height: 140%;
  transform: translateX(-50%);
  pointer-events: none;
  /* Density and falloff are separated. The fill is flat, and a radial mask does the fading, which
     is why there is no edge to cut off. The mask ellipse is exactly the size of its box, and the
     box overhangs the stage on all four sides, so the mask reaches zero off-frame. An ellipse wider
     than its own box gets clipped at a visible value and draws a faint line down each side, which
     is what a 60% horizontal radius did here; a 50% radius inside a column the width of the copy
     fixed the line but was then too narrow to cover the strip at tall shapes. Overhanging the box
     satisfies both. The earlier construction put the falloff in the gradient itself, where the last stop
     had to stay inside the box, and that ceiling left the lead and the trusted strip sitting on
     bare artwork at tall viewport shapes. */
  background: rgba(4, 1, 11, 0.78);
  -webkit-mask-image: radial-gradient(50% 72% at 50% 50%, #000 0%, #000 40%, rgba(0, 0, 0, 0.60) 66%, rgba(0, 0, 0, 0) 100%);
  mask-image: radial-gradient(50% 72% at 50% 50%, #000 0%, #000 40%, rgba(0, 0, 0, 0.60) 66%, rgba(0, 0, 0, 0) 100%);
}

/* A tall frame has to crop harder into a 16:9 painting, which lands the copy on the bright horizon
   band and the lit valley. 768x1024 failed on the desktop column as badly as a phone did, so the
   driver is the viewport's shape rather than its width. */
@media (max-aspect-ratio: 5 / 4) {
  .crossing-scrim {
    background: rgba(4, 1, 11, 0.62);
    -webkit-mask-image: radial-gradient(50% 72% at 50% 50%, #000 0%, #000 40%, rgba(0, 0, 0, 0.58) 66%, rgba(0, 0, 0, 0) 100%);
    mask-image: radial-gradient(50% 72% at 50% 50%, #000 0%, #000 40%, rgba(0, 0, 0, 0.58) 66%, rgba(0, 0, 0, 0) 100%);
  }
}

/* Copy lives inside the world, centred on the stage, above the canvas. */
.crossing-copy {
  position: relative;
  z-index: 3;
  width: min(100% - 2.5rem, 62rem);
  margin: 0 auto;
  padding-top: var(--cx-nav);
  text-align: center;
  color: var(--cx-ink);
}

/* Each group is driven from scroll by crossing.js, which writes opacity and a transform.
   Before the script runs they are hidden, so nothing flashes at the top of the page.
   Without JavaScript the :not([data-crossing-js]) rule below hands the whole hero back visible. */
.crossing-copy > * {
  will-change: transform, opacity;
}

.crossing .cx-hide {
  opacity: 0;
}

/* Stats: the smallest voice in the world, and the first thing that arrives after the crossing. */
.crossing .hero-stats {
  display: flex;
  justify-content: center;
  gap: clamp(1.5rem, 5vw, 3.5rem);
  margin-bottom: clamp(1.25rem, 3vw, 2rem);
}

.crossing .hero-stat-value {
  font-family: 'Orbitron', 'Orbitron Fallback', sans-serif;
  font-size: clamp(1.5rem, 4.2vw, 2.2rem);
  font-weight: 800;
  line-height: 1;
  background: linear-gradient(135deg, var(--ai-cyan), var(--ai-primary));
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

.crossing .hero-stat-label {
  font-family: 'JetBrains Mono', 'JetBrains Mono Fallback', monospace;
  font-size: 0.62rem;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--cx-ink-soft);
  margin-top: 0.4rem;
}

/* The headline. Size-stable: the reserved line box means a font swap cannot re-wrap the world. */
.crossing .orb-hero-title,
.crossing h1 {
  font-family: 'Orbitron', 'Orbitron Fallback', sans-serif;
  font-size: clamp(2rem, 8.6vw, 3.6rem);
  line-height: 1.08;
  letter-spacing: 0.5px;
  margin: 0 0 1rem;
  min-height: calc(3 * 1.08 * clamp(2rem, 8.6vw, 3.6rem));
  color: #fff;
  text-shadow:
    0 0 26px rgba(255, 0, 255, 0.45),
    0 0 62px rgba(0, 212, 255, 0.28);
  text-wrap: balance;
  overflow-wrap: anywhere;
}

.crossing h1::before,
.crossing h1::after {
  content: none;
}

.crossing .orb-hero-lead,
.crossing .lead {
  font-family: 'Inter', 'Inter Fallback', sans-serif;
  font-size: clamp(0.98rem, 2.6vw, 1.12rem);
  line-height: 1.65;
  color: var(--cx-ink-soft);
  max-width: 34rem;
  margin: 0 auto 1.6rem;
  min-height: 108px;
  animation: none;
  text-wrap: pretty;
}

.crossing .orb-hero-cta {
  display: flex;
  justify-content: center;
  gap: 0.9rem;
  flex-wrap: wrap;
}

.crossing .trusted-strip {
  margin-top: clamp(1.6rem, 4vw, 2.6rem);
  margin-left: -8%;
  margin-right: -8%;
  padding: clamp(1.1rem, 3vw, 1.8rem) 14% 1.1rem;
  border-top: 1px solid rgba(255, 0, 255, 0.16);
  /* Its own ground haze. The strip is the last thing in the column, where the scrim has decayed and
     the lit valley is brightest, and lifting the whole column to reach it dulled the painting
     everywhere. The ellipse is 50% of this box wide so it fades to nothing at its own edge. */
  /* The band's box is wider than the text it covers, by the negative margins plus the padding, so
     full density reaches past the outermost logo and the fade to nothing happens on bare artwork.
     Measured directly from pixels: the outermost logos were the last thing failing, at 3.36.
     The fade runs down the gradient and across the mask, one axis each, so all four edges dissolve
     instead of ending. A radial fitted to this box left hard top and bottom edges and read as a
     slab laid over the painting. */
  background: linear-gradient(to bottom, rgba(4, 1, 11, 0) 0%, rgba(4, 1, 11, 0.93) 16%, rgba(4, 1, 11, 0.93) 88%, rgba(4, 1, 11, 0) 100%);
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 11%, #000 89%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0%, #000 11%, #000 89%, transparent 100%);
}

.crossing .trusted-label {
  font-family: 'JetBrains Mono', 'JetBrains Mono Fallback', monospace;
  font-size: 0.6rem;
  letter-spacing: 1.8px;
  text-transform: uppercase;
  /* Lifted from #b9afdb, which measured 4.32 over the resolved world against a required 4.5.
     It now takes the same soft ink as the rest of the secondary copy. */
  color: var(--cx-ink-soft);
  margin-bottom: 0.7rem;
}

.crossing .trusted-logos {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.7rem 1.4rem;
  flex-wrap: wrap;
  min-height: 91px;
  color: #ddd6f4;
  font-size: 0.78rem;
}

@media (min-width: 390px) { .crossing .trusted-logos { min-height: 57px; } }
@media (min-width: 375px) { .crossing .orb-hero-lead, .crossing .lead { min-height: 81px; } }

@media (min-width: 768px) {
  .crossing .orb-hero-title, .crossing h1 { min-height: calc(2 * 1.08 * 3.6rem); }
  .crossing .orb-hero-lead, .crossing .lead { min-height: 65px; }
  .crossing .trusted-logos { min-height: 22px; }
}

/* Focus stays visible inside the world. */
.crossing a:focus-visible {
  outline: 2px solid var(--ai-cyan);
  outline-offset: 3px;
}

/* ---------- No JavaScript: the world cannot be travelled, so hand back a legible hero ---------- */
html:not([data-crossing-js]) .crossing {
  height: auto;
  min-height: 100svh;
}

html:not([data-crossing-js]) .crossing-stage {
  position: relative;
  height: auto;
  min-height: 100svh;
  padding: clamp(3rem, 8vh, 6rem) 0 clamp(2rem, 6vh, 4rem);
}

html:not([data-crossing-js]) .crossing .cx-hide {
  opacity: 1;
}

/* ---------- Reduced motion: the composition, not the off switch ----------
   The world stops travelling and holds its end state: every orb awake, arranged, with the echo
   drawn as a static trace between them. The copy is all present at once, because there is no
   journey to reveal it along. crossing.js paints that single frame and never starts a loop. */
html[data-motion="still"] .crossing,
html:not([data-motion]) .crossing {
  height: auto;
  min-height: 100svh;
}

html[data-motion="still"] .crossing-stage,
html:not([data-motion]) .crossing-stage {
  position: relative;
  height: auto;
  min-height: 100svh;
  padding: clamp(3rem, 8vh, 6rem) 0 clamp(2rem, 6vh, 4rem);
}

html[data-motion="still"] .crossing .cx-hide,
html:not([data-motion]) .crossing .cx-hide {
  opacity: 1;
}

html[data-motion="still"] .crossing-copy > *,
html:not([data-motion]) .crossing-copy > * {
  will-change: auto;
}

/* The OS query is the FALLBACK, not the authority. data-motion is the authority, and it defaults to
   full because most Windows machines ship with animations off, so the OS flag stands in for a choice
   the visitor never made. Giovanni: "modion is disabled by default please enable it".

   This block was briefly gated on the OS query alone. That collapsed the hero for anyone whose system
   asks for less motion even though data-motion said full: measured on 2026-09-10, header 994px where
   the design is 520vh (5169px), stage relative where it should be sticky. 4175px of the journey gone,
   while crossing.js still ran the travelling composition against a stage with no travel to give it.

   Scoped back onto html:not([data-motion="full"]), which is the same gate every other module uses
   (body.css section 8, secondary.css section 8). It still catches the case that matters and cannot be
   scripted around: NO JavaScript, so no data-motion attribute at all, reduced-motion visitor gets the
   still composition rather than 520vh of nothing. The accommodation for everyone else is the Reduce
   motion button in the footer, which sets data-motion="still" and is on every page. */
@media (prefers-reduced-motion: reduce) {
  html:not([data-motion="full"]) .crossing {
    height: auto;
    min-height: 100svh;
  }
  html:not([data-motion="full"]) .crossing-stage {
    position: relative;
    height: auto;
    min-height: 100svh;
    padding: clamp(3rem, 8vh, 6rem) 0 clamp(2rem, 6vh, 4rem);
  }
  html:not([data-motion="full"]) .crossing .cx-hide { opacity: 1; }
}
