/* ═══════════════════════════════════════════════════════════════════════════
   DRIIF — MOTION v1.0
   Lenis for smooth scroll, GSAP + ScrollTrigger for choreography.

   Three rules this file obeys:
     1. transform and opacity ONLY. Never width/height/top/left — those trigger
        layout and paint; transform and opacity stay on the GPU.
     2. Transitions, not keyframes, for anything the user can retrigger. A
        transition retargets from its current value; keyframes restart at zero.
     3. Nothing is hidden unless .js is present, so the page is never blank
        without scripting — and reduced motion gets a gentler version, not none.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── LOADER ────────────────────────────────────────────────────────────────
   PURE CSS. No JavaScript is involved in showing, filling or removing it.

   This is the third revision and the reason is worth recording: the loader was
   originally driven by JS that waited on fonts and the video, then removed the
   element. Twice that left the page permanently sealed behind a black screen —
   once when a library threw before the removal code ran, once for reasons that
   were never reproducible off the author's machine. Any design where JS is the
   only way out of a full-screen overlay is wrong.

   It is also no longer waiting for anything: the video carries a poster, so
   there is nothing to pop in. This is a deliberate 1.4s beat, not a wait, and
   CSS can express that with total reliability.
   ───────────────────────────────────────────────────────────────────────── */
.loader{ display:none; }
.js .loader{
  display:grid; place-items:center;
  position:fixed; inset:0; z-index:1000;
  background:var(--black);
  animation:loader-out 520ms var(--ease-drawer) 620ms forwards;
}
@keyframes loader-out{
  to{ clip-path:inset(0 0 100% 0); opacity:0; visibility:hidden; pointer-events:none; }
}
.loader-inner{ display:flex; flex-direction:column; align-items:center; gap:var(--space-5); }
.loader-mark{
  font-family:var(--font-display); font-weight:700; font-size:var(--ui-md);
  letter-spacing:.06em; line-height:var(--lh-flat); text-transform:uppercase;
  color:var(--white);
}
.loader-track{ width:140px; height:var(--hair); background:rgba(var(--rgb-white),.16); overflow:hidden; }
/* scaleX, not width — width would trigger layout on every frame */
.loader-bar{
  height:100%; background:var(--color-accent);
  transform:scaleX(0); transform-origin:left center;
  animation:loader-fill 620ms var(--ease-out) forwards;
}
@keyframes loader-fill{ to{ transform:scaleX(1); } }

/* ── HERO INTRO ────────────────────────────────────────────────────────────
   Also pure CSS, and for the same reason plus a better one: this is
   predetermined motion firing at page load — the single busiest moment for the
   main thread. CSS animations run off it. A GSAP timeline here was both more
   fragile and more likely to drop frames.

   Each line sits in its own overflow-hidden band and rides up from behind the
   edge. Lines are authored as spans, not runtime-split, so the h1 keeps one
   intact accessible name and no SplitText dependency exists.
   Delays are absolute from page load and sequenced to start as the loader
   lifts. Nothing here can fail to complete.
   ───────────────────────────────────────────────────────────────────────── */
.line{ display:block; overflow:hidden; padding-bottom:.12em; margin-bottom:-.12em; }
.line-i{ display:block; }
.js .line-i{ transform:translateY(110%); animation:line-rise 1s var(--ease-out) forwards; }
/* HERO TIMELINE — one place to tune. The footage plays alone from ~1.15s
   (loader gone) until 2.2s, then the copy arrives. Lines are 340ms apart, not
   100ms, so they read as one-after-the-other rather than as a stagger. */
.js .line:nth-of-type(1) .line-i{ animation-delay:2.46s; }
.js .line:nth-of-type(2) .line-i{ animation-delay:2.80s; }
@keyframes line-rise{ to{ transform:translateY(0); } }

.js .hero-seq{ opacity:0; transform:translateY(16px);
  animation:seq-in 700ms var(--ease-out) forwards; }
@keyframes seq-in{ to{ opacity:1; transform:none; } }
.js .hero .eyebrow{ animation-delay:2.2s; }
.js .hero-side .t-lead{ animation-delay:3.5s; }
.js .hero-side .btn{ animation-delay:3.7s; }

/* ── PAGE HERO INTRO (non-cinematic) ───────────────────────────────────────
   VIPER, NEXUS, Applications, Company, and Contact/Glossary's own opening
   headline all get the same visual language as Home's intro above — the
   headline rises out of a mask, the eyebrow/lead/CTA fade up after it — so
   every page's landing text arrives the same way instead of Home alone
   moving and the rest sitting static.

   Deliberately its own set of classes rather than reusing .line/.line-i/
   .hero-seq directly (though the keyframes ARE shared — line-rise, seq-in):
   those carry Home's loader+hero-video-synced delays (.hero .eyebrow at
   2.2s, .line:nth-of-type at 2.46s/2.80s, .hero-side .t-lead at 3.5s,
   .hero-side .btn at 3.7s — see above), timed to start only once a ~2.5s
   cinematic preamble finishes. None of these other pages run that
   preamble — no loader, most with no hero video either — so reusing those
   selectors verbatim would leave every one of them sitting blank for over
   two seconds before anything moved. This runs the same motion starting
   at page load instead. */
.intro-line{ display:block; overflow:hidden; padding-bottom:.12em; margin-bottom:-.12em; }
.js .intro-line-i{ display:block; transform:translateY(110%);
  animation:line-rise 900ms var(--ease-out) 180ms forwards; }
/* Per direct feedback (NEXUS, VIPER): a heading authored as two lines —
   same as Home's own "The Future of" / "Autonomous Missions." — should
   rise the same way Home's does, one after the other, not as one mask
   covering both. 340ms after the first, matching the gap Home's own two
   lines use (2.46s/2.80s). Self-scoping: a page with only one .intro-line
   (Company, Glossary, Contact, Applications) never matches nth-of-type(2),
   so this changes nothing for them. */
.js .intro-line:nth-of-type(2) .intro-line-i{ animation-delay:520ms; }
.js .intro-seq{ opacity:0; transform:translateY(16px);
  animation:seq-in 650ms var(--ease-out) forwards; }
.js .intro-seq.intro-d1{ animation-delay:40ms; }
.js .intro-seq.intro-d3{ animation-delay:480ms; }
.js .intro-seq.intro-d4{ animation-delay:600ms; }


/* The poster doubles as a CSS background, so a hero is never an empty black
   rectangle even if the video fails to decode or is blocked. Only the black
   floor is global — WHICH poster is per page, so each page stylesheet sets its
   own image. This rule used to hardcode the homepage's poster here, which meant
   every page loading motion.css got the homepage's footage behind its hero. */
.hero .media{ background:var(--black) center/cover no-repeat; }

/* ── SCROLL REVEAL ─────────────────────────────────────────────────────────
   ScrollTrigger drives these when GSAP is present; the IntersectionObserver
   fallback in the JS drives them when it isn't. Either way the same class
   flips, so the two paths can't disagree.
   24px/600ms used to be the travel/pace here — both bumped (64px, and
   --dur-enter to 900ms in tokens.css) after comparing against a reference
   site's own scroll-in effect: bigger and slower reads as deliberate,
   where the old pair read as a quick fade-up twitch. A slight scale-up
   (.96 to 1) rides along on the same transform/transition pair — still one
   property, so it costs nothing extra to animate — reinforcing the same
   "settling into place" read the slide-up already has, kept subtle enough
   (4%) that it doesn't compete with it.
   ───────────────────────────────────────────────────────────────────────── */
.js .reveal{
  opacity:0; transform:translateY(64px) scale(.96);
  transition:opacity var(--dur-enter) var(--ease-out),
             transform var(--dur-enter) var(--ease-out);
  /* FAILSAFE: content must never depend on a script succeeding. If the observer
     never runs, this forces the final state at 6s. Late is survivable; a blank
     page is not. */
  animation:reveal-force 1ms linear 6s forwards;
}
@keyframes reveal-force{ to{ opacity:1; transform:none; } }
.js .reveal.in{ opacity:1; transform:none; animation:none; }

/* ── SOFT REVEAL ───────────────────────────────────────────────────────────
   The "super light" entrance, applied by soft-reveal.js to section-level
   blocks that carry no .reveal of their own. Deliberately much smaller than
   .reveal above: 12px and no scale, at --dur-enter's pace.

   The difference is the point. .reveal's 64px + scale was tuned to read as
   deliberate on the pieces someone chose to choreograph; giving that same
   weight to every remaining block on the page would turn a scroll into a
   sequence of large moves. This is meant to be noticed only as the page
   feeling alive.

   Same 6s animation failsafe as .reveal, for the same reason: content must
   never depend on a script succeeding. */
.js .reveal-soft{
  opacity:0; transform:translateY(12px);
  transition:opacity var(--dur-enter) var(--ease-out),
             transform var(--dur-enter) var(--ease-out);
  animation:reveal-force 1ms linear 6s forwards;
}
.js .reveal-soft.in{ opacity:1; transform:none; animation:none; }

/* ── PROBLEM DIAGRAM: four boxes, one beat each ──────────────────────────
   Was a 3-sub-beat trace per column (label lands, connector draws, operator
   appears — 12 beats across the four columns, ~2.9s total to fully arrive).
   The four stacks are bordered boxes now (home.css), so the border carries
   the "these don't talk to each other" argument the animation used to carry
   alone — which frees the motion to do one simple thing instead: each box
   fades/slides up as a whole, staggered by its own --n (the same custom
   property the four columns already carried). Still arrives in sequence,
   but each arrival now reads as one clean motion, not three, which was the
   actual complaint about the old version being hard to follow.
   Keyed off .prob-diagram.in with the same 6s CSS failsafe as everything
   else, so a dead observer still leaves the whole diagram drawn.
   ───────────────────────────────────────────────────────────────────────── */
.js .prob-diagram .pd-stack{ opacity:0; transform:translateY(24px);
  animation:reveal-force 1ms linear 6s forwards; }
.js .prob-diagram.in .pd-stack{ opacity:1; transform:none; animation:none;
  transition:opacity var(--dur-enter) var(--ease-out),
             transform var(--dur-enter) var(--ease-out);
  transition-delay:calc(.15s + var(--n) * 110ms); }

/* The close statement is this section's punchline — held back a beat after
   the paragraph above it settles rather than landing in the same reveal
   batch as its siblings (reveals.js batches nearby .reveal elements within
   ~60ms of each other, too close to read as its own arrival), so it visibly
   comes up on its own. Same .reveal/.in mechanism, just a later delay. */
.js #problem .prob-close.in{ transition-delay:.4s; }

/* ── PROBLEM DIAGRAM: connector scan ──────────────────────────────────────
   The short line from each feed label down to "Operator" plays a signal-
   travelling loop for as long as the diagram is on screen: grows from the
   feed down to the operator, resets, repeats — problem-scan.js toggles
   .is-scanning on whichever section holds the diagram via a plain
   IntersectionObserver (not scroll-scrubbed like problem-brighten.js
   above; this is a binary "are we looking at it" loop, not something tied
   to how far you've scrolled).
   Not #problem-scoped any more — generalised while Nexus briefly carried
   a second copy of this diagram in its own #problem-gaps section; that
   copy didn't survive (dropped for a plain photo, nexus.html), but this
   keys off the .is-scanning class alone regardless, since the
   generalisation costs nothing and still works for any current or future
   page that reuses the diagram — problem-scan.js finds every section
   with a .pd-link in it and toggles the class on that section
   specifically, whatever its id.
   Entirely additive: with no JS, or under reduced motion (problem-scan.js
   checks REDUCED and never adds the class at all — the @media block below
   is the same belt-and-suspenders backup every other motion rule in this
   file gets), .pd-link just stays the plain solid connector it always was
   (home.css). Nothing here can make it disappear; .is-scanning only ever
   adds motion on top of an already-correct static line. */
.js .is-scanning .pd-link{
  animation:pd-scan 1.6s linear infinite;
}
@keyframes pd-scan{
  0%   { clip-path:inset(0 0 100% 0); }
  100% { clip-path:inset(0 0 0% 0); }
}

@media (prefers-reduced-motion:reduce){
  .js .prob-diagram .pd-stack{ opacity:1; transform:none;
    animation:none !important; transition:none !important; }
  .js .is-scanning .pd-link{ animation:none !important; }
}

/* The tab diagrams that lived here are gone. Tab 01 is now a rendered video
   scene and tabs 02-05 hold a still of the same stage, so ~200 lines of SVG
   keyframes had no markup left to animate. Removed rather than left in place:
   dead CSS is how the next person reintroduces a system nobody uses.
   The scene needs no motion CSS at all — that is the point of rendering it.
   ───────────────────────────────────────────────────────────────────────── */

/* ── PARALLAX ──────────────────────────────────────────────────────────────
   A hook, not a component: put data-parallax on any media wrapper and the JS
   drifts its inner element against the scroll. Strength is per-element.
   The inner element is scaled up so the drift never exposes an edge.
   ───────────────────────────────────────────────────────────────────────── */
[data-parallax] > video,
[data-parallax] > img{ transform:scale(1.12); will-change:transform; }

/* ── PRESS FEEDBACK ────────────────────────────────────────────────────────
   Every pressable thing scales down on press. This is the cheapest possible
   way to make an interface feel like it heard you, and its absence is felt
   even though its presence never is.
   ───────────────────────────────────────────────────────────────────────── */
.btn{ transition:color var(--dur-fast) var(--ease-out),
                 transform var(--dur-press) var(--ease-out); }
.btn:active{ transform:scale(.97); }

/* ── HOVER — gated, because touch fires a false hover on tap ─────────────── */
@media (hover:hover) and (pointer:fine){
  .card-product{ transition:border-color var(--dur-fast) var(--ease-out); }
  .card-product:hover{ border-color:var(--color-border-strong); }
  .card-notch .edge{ transition:background var(--dur-fast) var(--ease-out); }
  .card-notch:hover .edge{ background:rgba(var(--pole),.5); }
  /* tile hover: flip the ground to black and swap to the render lit for it */
  .tile:hover{
    --pole: var(--rgb-white);
    --color-bg: var(--black);
    --color-text: var(--white);
    --color-text-muted: rgba(var(--rgb-white),.60);
    background:var(--color-bg);
  }
  .tile:hover .tile-hover{ opacity:1; }
  .tile:hover .tile-static{ opacity:0; }
  .tile:hover .media img{ transform:scale(1); }   /* the render opens out to fill */
}

/* ── TAB INDICATOR ─────────────────────────────────────────────────────────
   A single filled block that slides between tabs instead of four blocks
   toggling on and off. translate + scaleX only — animating left/width would
   relayout the row on every frame. ease-in-out because it is moving across
   the screen rather than entering or leaving it.
   ───────────────────────────────────────────────────────────────────────── */
.tabs{ position:relative; }
/* A 2px accent rule that slides along the bottom edge — not a filled block.
   The filled version sat at height:100% and painted straight over the active
   tab's own label. translateX + scaleX so nothing relayouts per frame. */
.tab-ind{
  position:absolute; left:0; bottom:0; height:2px; width:1px;
  background:var(--color-accent); transform-origin:left center;
  transform:translateX(0) scaleX(0); z-index:1;
  transition:transform var(--dur-ui) var(--ease-in-out);
  pointer-events:none;
}
.tab{ position:relative; z-index:2; }

/* The sliding rule is positioned at the BAR's bottom edge, so it only tells the
   truth while the tabs sit on one line. Five tabs wrap below lg, and the
   indicator then parked under whichever tab happened to share its x — it read
   as "04 EXECUTE" selected while panel 01 was showing. Wrapped layouts get a
   per-tab underline instead, which cannot lie about which row it is on. */
@media (max-width:1023px){
  .tab-ind{ display:none; }
  .tab{ padding-bottom:var(--space-3); }
  .tab.active{ box-shadow:inset 0 -2px 0 0 var(--color-accent); }
}

/* ── REDUCED MOTION ────────────────────────────────────────────────────────
   Fewer and gentler, not zero: comprehension-carrying opacity stays, every
   positional change goes. The loader still functions, it just doesn't travel.
   ───────────────────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion:reduce){
  html{ scroll-behavior:auto; }
  .js .reveal, .js .reveal-soft{ opacity:1; transform:none; transition:none; animation:none; }
  .js .line-i{ transform:none; animation:none; }
  .js .hero-seq{ opacity:1; transform:none; animation:none; }
  .js .intro-line-i{ transform:none; animation:none; }
  .js .intro-seq{ opacity:1; transform:none; animation:none; }
  .js .loader{ animation:loader-out 200ms linear 400ms forwards; }
  .loader-bar{ animation:none; transform:scaleX(1); }
  [data-parallax] > video, [data-parallax] > img{ transform:none; }
  .tile .media img{ transition:none; }
  .tab-ind{ transition:none; }
  .btn:active{ transform:none; }
}
