/* Reveal primitives — IntersectionObserver toggles [data-reveal="in"] */
[data-reveal] {
  opacity: 0;
  transform: translateY(14px);
  transition:
    opacity var(--dur-reveal) var(--ease-out-expo),
    transform var(--dur-reveal) var(--ease-out-expo);
  transition-delay: calc(var(--i, 0) * 60ms);
  will-change: opacity, transform;
}

[data-reveal="in"] {
  opacity: 1;
  transform: translateY(0);
}

[data-reveal-stagger]>* {
  opacity: 0;
  transform: translateY(48px);
  transition:
    opacity 720ms var(--ease-out-expo),
    transform 720ms var(--ease-out-expo);
  transition-delay: calc(var(--i, 0) * 80ms);
  will-change: opacity, transform;
}

[data-reveal-stagger="in"]>* {
  opacity: 1;
  transform: translateY(0);
}

/* Headline char/word splitter classes */
.split-word {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  padding-bottom: 0.08em;
}

.split-word>span {
  display: inline-block;
  transform: translateY(110%);
  transition: transform 620ms var(--ease-out-expo);
  transition-delay: calc(var(--i, 0) * 70ms);
}

[data-reveal="in"] .split-word>span {
  transform: translateY(0);
}

/* Char-level mask reveal — split.js wraps each character in
   <span class="char"><span>X</span></span>, grouped under <span class="word">
   so wrapping happens at word boundaries. Inner span starts below the mask
   and lifts when an ancestor toggles [data-reveal="in"]. Per-char stagger
   delay is set via --i with a 36-char cap. */
.word {
  display: inline-block;
  white-space: nowrap;
}

.char {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  padding-bottom: 0.08em;
  line-height: inherit;
}

.char>span {
  display: inline-block;
  transform: translateY(110%);
  transition: transform 720ms var(--ease-out-expo);
  transition-delay: calc(var(--i, 0) * 22ms);
  will-change: transform;
}

[data-reveal="in"] .char>span {
  transform: translateY(0);
}

.char.accent>span {
  color: var(--accent);
}

/* Hero h1 scrubbed lift — translateY is written inline by scroll.js;
   --clip drives the top inset so the headline gets eaten from above as
   --scroll-progress climbs through 0..0.18. */
.hero h1 {
  clip-path: inset(var(--clip, 0%) 0 0 0);
  will-change: transform, clip-path;
}

/* Process step active indicator — scroll.js sets data-active="true" on the
   step nearest viewport center. Opacity itself is set inline per frame. */
.process-step {
  transition: opacity 220ms var(--ease-out-expo);
}

.process-step .num {
  transition:
    transform 280ms var(--ease-out-expo),
    color 280ms var(--ease-out-expo),
    text-shadow 280ms var(--ease-out-expo);
  display: inline-block;
  transform-origin: left center;
}

.process-step[data-active="true"] .num {
  transform: scale(1.6);
  color: var(--accent);
  text-shadow: 0 0 28px rgba(34, 173, 254, 0.6);
}

/* Work card scrub baseline */
.work-card {
  will-change: transform;
}

/* H2 velocity skew — every section h2 reads --scroll-velocity each frame
   and skews proportionally. will-change is gated by a body flag set by
   scroll.js while velocity is active, so idle h2s don't keep a layer. */
section h2 {
  transform: skewY(calc(var(--scroll-velocity, 0) * 1.2deg));
  transform-origin: left center;
  transition: transform 240ms var(--ease-out-expo);
}

body[data-vel-active] section h2 {
  will-change: transform;
}

/* Top scroll-progress bar — Lenis writes --progress on :root via
   lenis.on('scroll'). */
.scroll-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  width: 100%;
  transform: scaleX(var(--progress, 0));
  transform-origin: 0 50%;
  background: var(--accent);
  z-index: 9999;
  pointer-events: none;
  box-shadow: 0 0 12px rgba(34, 173, 254, 0.55);
  will-change: transform;
}

/* Section-level reveal — IntersectionObserver in main.js toggles .is-visible. */
.reveal {
  opacity: 0;
  transform: translateY(40px);
  transition:
    opacity 0.9s cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.9s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: opacity, transform;
}

.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.reveal>* {
  opacity: 0;
  transform: translateY(20px);
  transition:
    opacity 0.7s ease-out,
    transform 0.7s ease-out;
}

.reveal.is-visible>* {
  opacity: 1;
  transform: translateY(0);
}

.reveal.is-visible>*:nth-child(1) {
  transition-delay: 0.05s;
}

.reveal.is-visible>*:nth-child(2) {
  transition-delay: 0.15s;
}

.reveal.is-visible>*:nth-child(3) {
  transition-delay: 0.25s;
}

.reveal.is-visible>*:nth-child(4) {
  transition-delay: 0.35s;
}

.reveal.is-visible>*:nth-child(5) {
  transition-delay: 0.45s;
}

.reveal.is-visible>*:nth-child(6) {
  transition-delay: 0.55s;
}

/* Hero reveals immediately on load. */
.reveal--hero {
  opacity: 1;
  transform: none;
}

.reveal--hero>* {
  animation: heroIn 1s cubic-bezier(0.22, 1, 0.36, 1) both;
}

.reveal--hero>*:nth-child(1) {
  animation-delay: 0.1s;
}

.reveal--hero>*:nth-child(2) {
  animation-delay: 0.25s;
}

.reveal--hero>*:nth-child(3) {
  animation-delay: 0.4s;
}

.reveal--hero>*:nth-child(4) {
  animation-delay: 0.55s;
}

@keyframes heroIn {
  from {
    opacity: 0;
    transform: translateY(30px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

[data-parallax] {
  will-change: transform;
}

/* Tech ornaments — three position:fixed objects that persist across the
   whole page and react to scroll via CSS custom properties only. The
   nested two-layer pattern composes idle keyframe rotation (outer) with
   scroll-driven static transform (inner) so they don't conflict.
   No transition on the scroll layer — --scroll-velocity is already
   low-pass filtered in scroll.js, so reading it raw is glitch-free. */
:root {
  --cube-size: clamp(160px, 20vw, 280px);
}

.tech-orb {
  position: fixed;
  pointer-events: none;
  z-index: 0;
  perspective: 1000px;
  will-change: transform, opacity, filter;
  /* Hidden = scaled down + out of focus + transparent. Active = neutral.
     Three transitions stagger slightly (filter resolves first) so the swap
     reads as a focus-pull rather than a flat cross-fade. No vertical
     translation. */
  opacity: 0;
  transform: scale(0.86);
  filter: blur(3px);
  transition:
    opacity   600ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 600ms cubic-bezier(0.22, 1, 0.36, 1),
    filter    420ms cubic-bezier(0.22, 1, 0.36, 1);
}

.tech-orb.is-active {
  opacity: var(--orb-active-opacity, 0.4);
  transform: scale(1);
  filter: blur(0);
}

.tech-orb__idle,
.tech-orb__scroll {
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
}

/* Velocity-driven lean. Scroll fast → orb tilts in the direction of
   travel; idle → upright. --scroll-velocity is already low-pass filtered
   in scroll.js so this needs no transition (var changes smoothly). */
.tech-orb__scroll {
  transform: rotate(calc(var(--scroll-velocity, 0) * 5deg));
}

/* ── 1. Monitor — top-right, vertically centered.
   Wireframe SVG of a desktop monitor showing terminal-style code.
   Idle: slow tilt back-and-forth (a monitor wouldn't spin all the way).
   Scroll: velocity-driven drift + slight Z-tilt. */
.tech-orb--monitor {
  right: clamp(20px, 5vw, 80px);
  top: clamp(80px, 14vh, 160px);
  width: clamp(180px, 22vw, 280px);
  aspect-ratio: 5 / 4;
  color: var(--accent);
  --orb-active-opacity: 0.5;
}

/* No rotation, no scroll-driven motion. Each orb gets a slow continuous
   drift via .tech-orb__idle keyframes — three different paths (a/b/c)
   distributed across the six orbs so they wander asynchronously. */
@keyframes orb-float-a {
  0%, 100% { transform: translate3d(  0px,   0px, 0); }
  25%      { transform: translate3d(  9px,  -6px, 0); }
  50%      { transform: translate3d(  0px, -11px, 0); }
  75%      { transform: translate3d( -9px,  -6px, 0); }
}

@keyframes orb-float-b {
  0%, 100% { transform: translate3d(  0px,   0px, 0); }
  33%      { transform: translate3d( -7px,  -9px, 0); }
  66%      { transform: translate3d(  9px,   7px, 0); }
}

@keyframes orb-float-c {
  0%, 100% { transform: translate3d(  0px,   0px, 0); }
  40%      { transform: translate3d( 11px,   7px, 0); }
  70%      { transform: translate3d( -7px,  11px, 0); }
}

.tech-orb--monitor .tech-orb__idle { animation: orb-float-a 26s ease-in-out infinite; }
.tech-orb--code    .tech-orb__idle { animation: orb-float-b 30s ease-in-out infinite; }
.tech-orb--prompt  .tech-orb__idle { animation: orb-float-c 24s ease-in-out infinite; }
.tech-orb--db      .tech-orb__idle { animation: orb-float-a 28s ease-in-out infinite reverse; }
.tech-orb--braces  .tech-orb__idle { animation: orb-float-c 32s ease-in-out infinite reverse; }
.tech-orb--server  .tech-orb__idle { animation: orb-float-b 22s ease-in-out infinite reverse; }

.tech-orb__monitor {
  width: 100%;
  height: 100%;
  filter: drop-shadow(
    0 0
    calc(32px + var(--scroll-velocity-abs, 0) * 50px)
    rgba(34, 173, 254, calc(0.45 + var(--scroll-velocity-abs, 0) * 0.35))
  );
}

/* ── 2. Floating "</>"  glyph — bottom-left ──────────────────────── */
.tech-orb--code {
  left: clamp(20px, 5vw, 80px);
  bottom: clamp(80px, 14vh, 160px);
  width: clamp(120px, 16vw, 240px);
  height: clamp(120px, 16vw, 240px);
  display: grid;
  place-items: center;
  --orb-active-opacity: 0.28;
  color: var(--accent);
}

.tech-orb--code .tech-orb__scroll {
  display: grid;
  place-items: center;
}

.tech-orb__glyph {
  font-family: var(--font-mono);
  font-size: clamp(80px, 14vw, 200px);
  font-weight: 300;
  line-height: 1;
  letter-spacing: -0.02em;
  text-shadow: 0 0
    calc(40px + var(--scroll-velocity-abs, 0) * 60px)
    rgba(34, 173, 254, calc(0.4 + var(--scroll-velocity-abs, 0) * 0.4));
  user-select: none;
}

.tech-orb__caret {
  display: inline-block;
  margin-left: 0.05em;
  animation: tech-orb-caret 1.05s steps(2) infinite;
}

@keyframes tech-orb-caret {

  0%,
  49% {
    opacity: 1;
  }

  50%,
  100% {
    opacity: 0;
  }
}

/* ── 3. Terminal prompt — bottom-right, mirrors the </> glyph pattern ── */
.tech-orb--prompt {
  right: clamp(20px, 5vw, 50px);
  bottom: clamp(28px, 2vh, 160px);
  height: clamp(120px, 16vw, 240px);
  display: grid;
  place-items: center;
  --orb-active-opacity: 0.28;
  color: var(--accent);
}

.tech-orb--prompt .tech-orb__scroll {
  display: grid;
  place-items: center;
}

/* ── 4. Database cylinder — top-left ───────────────────────────── */
.tech-orb--db {
  left: clamp(20px, 5vw, 80px);
  top: clamp(80px, 14vh, 160px);
  width: clamp(110px, 13vw, 180px);
  height: clamp(132px, 16vw, 220px);
  --orb-active-opacity: 0.4;
  color: var(--accent);
}

/* ── 5. Braces "{ }" — top-right ──────────────────────────────── */
.tech-orb--braces {
  right: clamp(20px, 5vw, 80px);
  top: clamp(80px, 14vh, 160px);
  height: clamp(120px, 16vw, 240px);
  display: grid;
  place-items: center;
  --orb-active-opacity: 0.28;
  color: var(--accent);
}

.tech-orb--braces .tech-orb__scroll {
  display: grid;
  place-items: center;
}

/* ── 6. Server rack — center-left ────────────────────────────── */
.tech-orb--server {
  left: clamp(20px, 5vw, 80px);
  top: clamp(80px, 14vh, 160px);
  width: clamp(160px, 20vw, 260px);
  aspect-ratio: 6 / 5;
  color: var(--accent);
  --orb-active-opacity: 0.42;
}


.tech-orb__server {
  width: 100%;
  height: 100%;
  filter: drop-shadow(
    0 0
    calc(28px + var(--scroll-velocity-abs, 0) * 45px)
    rgba(34, 173, 254, calc(0.42 + var(--scroll-velocity-abs, 0) * 0.32))
  );
}


.tech-orb__db {
  width: 100%;
  height: 100%;
  filter: drop-shadow(
    0 0
    calc(22px + var(--scroll-velocity-abs, 0) * 40px)
    rgba(34, 173, 254, calc(0.4 + var(--scroll-velocity-abs, 0) * 0.35))
  );
}

@media (max-width: 1100px) {
  .tech-orb--code   { bottom: clamp(60px, 10vh, 120px); --orb-active-opacity: 0.22; }
  .tech-orb--prompt { --orb-active-opacity: 0.22; }
  .tech-orb--db     { --orb-active-opacity: 0.30; }
  .tech-orb--braces { --orb-active-opacity: 0.22; }
  .tech-orb--server { --orb-active-opacity: 0.30; }
}

@media (max-width: 768px) {
  .tech-orb--monitor {
    width: clamp(140px, 36vw, 200px);
    right: clamp(12px, 4vw, 32px);
    top: clamp(70px, 12vh, 110px);
    --orb-active-opacity: 0.4;
  }
  .tech-orb--braces {
    width: clamp(90px, 22vw, 140px);
    height: clamp(90px, 22vw, 140px);
    right: clamp(12px, 4vw, 32px);
    top: clamp(70px, 12vh, 110px);
    --orb-active-opacity: 0.24;
  }
  .tech-orb--db {
    width: clamp(70px, 18vw, 120px);
    height: clamp(84px, 22vw, 144px);
    top: clamp(70px, 12vh, 110px);
    left: clamp(8px, 3vw, 24px);
    --orb-active-opacity: 0.30;
  }
  .tech-orb--server {
    width: clamp(120px, 30vw, 180px);
    top: clamp(70px, 12vh, 110px);
    left: clamp(8px, 3vw, 24px);
    --orb-active-opacity: 0.32;
  }
  .tech-orb--code,
  .tech-orb--prompt {
    width: clamp(80px, 20vw, 140px);
    height: clamp(80px, 20vw, 140px);
    bottom: clamp(70px, 12vh, 110px);
    --orb-active-opacity: 0.22;
  }
}

@media (max-width: 480px) {
  .tech-orb--db,
  .tech-orb--server {
    display: none;
  }
}

/* Scroll cue (Moment 9) — bottom-center of the hero. Opacity fades from
   1 → 0 as --scroll-progress climbs from 0 → 0.06 (calc consumes the var
   directly, no JS write needed). The dot loops via translateY only. */
.scroll-cue {
  position: absolute;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.24em;
  color: var(--text-dim);
  pointer-events: none;
  opacity: calc(1 - clamp(0, var(--scroll-progress, 0) / 0.06, 1));
  z-index: 2;
}

.scroll-cue .line {
  position: relative;
  width: 1px;
  height: 56px;
  background: var(--hairline);
  overflow: hidden;
}

.scroll-cue .dot {
  position: absolute;
  left: -1.5px;
  top: 0;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--accent);
  animation: scroll-cue-fall 2.4s var(--ease-out-expo) infinite;
  will-change: transform;
}

@keyframes scroll-cue-fall {
  0% {
    transform: translateY(-100%);
    opacity: 0;
  }

  20% {
    opacity: 1;
  }

  80% {
    opacity: 1;
  }

  100% {
    transform: translateY(56px);
    opacity: 0;
  }
}

/* Boot — runs once via JS */
body[data-booting] [data-reveal] {
  transition: none;
}

/* Reduced motion: opacity-only fades, no transforms, no marquee.
   Every rule is scoped to :root:not([data-force-motion]) so the JS-set
   force-motion bypass also short-circuits the CSS overrides. */
@media (prefers-reduced-motion: reduce) {

  :root:not([data-force-motion]) *,
  :root:not([data-force-motion]) *::before,
  :root:not([data-force-motion]) *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  :root:not([data-force-motion]) [data-reveal] {
    opacity: 0;
    transform: none;
    transition: opacity 240ms ease;
  }

  :root:not([data-force-motion]) [data-reveal="in"] {
    opacity: 1;
  }

  :root:not([data-force-motion]) .split-word>span {
    transform: none;
    opacity: 0;
    transition: opacity 240ms ease;
  }

  :root:not([data-force-motion]) [data-reveal="in"] .split-word>span {
    opacity: 1;
  }

  :root:not([data-force-motion]) .char>span {
    transform: none;
    opacity: 0;
    transition: opacity 240ms ease;
  }

  :root:not([data-force-motion]) [data-reveal="in"] .char>span {
    opacity: 1;
  }

  :root:not([data-force-motion]) .marquee-track {
    animation: none;
    transform: none !important;
  }

  :root:not([data-force-motion]) .hero h1 {
    clip-path: none !important;
    transform: none !important;
  }

  :root:not([data-force-motion]) .work-card {
    transform: none !important;
  }

  :root:not([data-force-motion]) .process-step {
    opacity: 1 !important;
  }

  :root:not([data-force-motion]) .process-step .num {
    transform: none !important;
    text-shadow: none !important;
  }

  :root:not([data-force-motion]) section h2 {
    transform: none !important;
  }

  :root:not([data-force-motion]) .scroll-cue {
    display: none !important;
  }

  :root:not([data-force-motion]) .reveal,
  :root:not([data-force-motion]) .reveal>*,
  :root:not([data-force-motion]) .reveal--hero>* {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
    animation: none !important;
  }

  :root:not([data-force-motion]) [data-parallax] {
    transform: none !important;
  }

  :root:not([data-force-motion]) .tech-orb__idle {
    animation: none !important;
  }

  :root:not([data-force-motion]) .tech-orb__scroll {
    transform: none !important;
  }

  /* Reduced motion: show every orb at low opacity (no section-driven swap),
     bypassed by data-force-motion. */
  :root:not([data-force-motion]) .tech-orb {
    opacity: 0.2 !important;
  }
  :root:not([data-force-motion]) .tech-orb.is-active {
    opacity: 0.2 !important;
  }

  :root:not([data-force-motion]) .hero-eyebrow .pulse,
  :root:not([data-force-motion]) .global-map .pulse::after {
    animation: none;
  }
}

/* Touch device hover suppression — already gated by component CSS, but ensure no false hover */
@media (hover: none) {
  .work-card .detail {
    grid-template-rows: 1fr;
  }
}