/* ==========================================================================
   NetrAI — Motion layer
   Shared motion language (easing, duration, rhythm), page transitions, a
   parallax wrapper utility, and the last mile of prefers-reduced-motion
   coverage. Loaded after style.css on every page.
   ========================================================================== */

/* ---------------------------------------------------------------------
   Motion tokens — the shared vocabulary
   Every section on the site has its own distinctive interaction, but they
   all draw their easing and timing from this one set. That's what makes
   eight different animations read as one coherent system rather than
   eight unrelated effects.

     --ease-out    entrances. Decelerating, lands softly, never overshoots.
     --ease-inout  state changes (active/inactive, open/closed). Symmetric.
     (--ease-spring was a third name here. It never sprang: its value was
                   character-for-character --ease-apple, which documents itself
                   as having no overshoot, and refine.css then redefined it to
                   --ease-out's exact value on the belief that it did. Three
                   names, two curves, and one of them named for behaviour
                   nothing in the file produced. Folded into --ease-out, which
                   is what it resolved to anyway. If a real spring is ever
                   wanted, js/core/fluid.js has one — a curve cannot carry velocity
                   or be interrupted, which is the reason §4 asks for springs
                   in the first place.)
     --dur-1/2/3   fast / standard / cinematic.
     --stagger     one beat. Sequences multiply this rather than inventing
                   their own numbers.
   --------------------------------------------------------------------- */
:root {
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-inout: cubic-bezier(0.65, 0, 0.35, 1);

  --dur-1: 240ms;
  --dur-2: 520ms;
  --dur-3: 900ms;

  --stagger: 70ms;
}

/* ---------------------------------------------------------------------
   Page transitions
   A soft cross-fade + rise on load, and a quick fade-down on the way out
   when navigating to another page on the site. Pure CSS on the way in
   (so it works the instant the document paints, no flash of unstyled
   content); JS-toggled class on the way out (see js/core/motion.js).
   --------------------------------------------------------------------- */
/* THE FADE-IN ON ARRIVAL IS GONE. It was:

     body { animation: page-fade-in 0.2s <ease> both; }
     @keyframes page-fade-in { from { opacity: 0 } to { opacity: 1 } }

   `both` resolves to fill-mode: both, which means the body takes the
   keyframe's `from` value — opacity 0 — for the whole period before the
   animation's first frame actually runs. The document was laid out and
   ready and painted nothing, because everything in it was transparent.

   Two measured consequences, both on the pristine build at 4x CPU throttle:

     - body computed opacity was 0 on the first sampled frame of all nine
       pages, and the page did not become opaque until 789-2718 ms.
     - Chrome recorded no First Contentful Paint at all on services.html,
       privacy.html and terms.html (getEntriesByName returned []), because a
       fully transparent paint is not a contentful one. Where FCP did fire it
       fired *after* the body was already opaque — pricing.html reported FCP
       1592 ms against an opaque body at 1180 ms. Field Core Web Vitals for
       this site were therefore either missing or wrong.

   Nothing else referenced page-fade-in, so both the rule and the keyframes
   are deleted rather than reworked. A fade that has to finish before the
   page is visible is not an entrance; it is a delay. The cross-fade on the
   way *out* is a separate rule and is dealt with in js/core/motion.js.

   (Kept for the record: opacity, not transform, was the right property for
   this effect — a transform on <body> would make it the containing block for
   every position:fixed descendant (back-to-top button, cursor dot, toast,
   confetti particles) and pull them into the scrolling page. That reasoning
   still applies to anything added here later.) */

/* 0.16s, matched to the 80ms wait in js/core/motion.js so the page is visibly
   mid-fade when it leaves. At the old 0.28s the navigation fired before the
   fade was half done, which meant paying for a delay to show an effect that
   never arrived. */
body.page-transition-out {
  opacity: 0;
  transition: opacity 0.16s cubic-bezier(0.28, 0.11, 0.32, 1);
}

/* ---------------------------------------------------------------------
   Parallax wrapper utility
   Wraps a section's decorative background shape(s) so a scroll-linked
   translateY (applied via inline style in js/core/motion.js) never collides
   with that shape's own CSS animation (e.g. .blob's floatBlob drift).
   The wrapper takes the original absolute-position box; its child fills
   it and keeps whatever animation it already had.
   --------------------------------------------------------------------- */
.page-hero-blobs {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

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

/* ==========================================================================
   SECTION MOTION
   One distinctive interaction per section, all built from the tokens above.
   Paired with js/pages/animate.js — the JS adds state classes, the CSS owns every
   transition, so timing lives in one place and can be tuned without
   touching behaviour.
   ========================================================================== */

/* ---------------------------------------------------------------------
   HERO — typewriter headline
   Characters are split into .tw-c spans by animate.js and revealed with
   opacity. Opacity specifically: the text stays in the accessibility tree
   and keeps its layout box, so the headline is fully laid out (final line
   breaks, final height) from the first frame and nothing on the page moves
   while it types.
   --------------------------------------------------------------------- */
.hero .hero-inner h1 .tw-c {
  opacity: 0;
  /* No transition on the way in — a fade would blur the crispness of a
     keystroke. The characters snap on; the *cadence* is what sells it. */
}
.hero .hero-inner h1 .tw-c.is-on { opacity: 1; }

/* The caret rides on whichever character was typed last, so it's placed by
   the text layout itself rather than measured and positioned in JS. */
.hero .hero-inner h1 .tw-c.is-caret {
  position: relative;
}
.hero .hero-inner h1 .tw-c.is-caret::after {
  content: "";
  position: absolute;
  right: -0.1em;
  top: 0.08em;
  width: 2px;
  height: 0.92em;
  background: var(--accent-fill);
  box-shadow: none;
  border-radius: 1px;
}
.hero .hero-inner h1 .tw-c.is-blinking::after {
  animation: tw-blink 530ms steps(1, end) infinite;
}
@keyframes tw-blink {
  0%, 50%  { opacity: 1; }
  51%, 100% { opacity: 0; }
}

/* Fallback if JS never runs: the headline must not stay invisible. */
.hero .hero-inner h1:not(.tw-ready) .tw-c { opacity: 1; }
.hero .hero-inner h1.tw-instant .tw-c { opacity: 1; }


.hero-aurora span { will-change: transform; }

/* Hero furniture waits for the sentence to land. */
.hero-visual {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity var(--dur-3) var(--ease-out),
              transform var(--dur-3) var(--ease-out);
}
.hero-visual.is-in {
  opacity: 1;
  transform: translateY(0);
}


/* ---------------------------------------------------------------------
   SERVICES — individual rise, then pointer-tracked 3D tilt
   The grid gets perspective so the tilt is a real rotation in depth
   rather than a skew. Cards keep their own transform, so the entrance
   (translate/scale) and the hover (rotate) are composed in one property
   via custom properties instead of fighting over it.
   --------------------------------------------------------------------- */
.services-grid,
.services-featured-row {
  perspective: 1200px;
}

/* The ENTRANCE only. These cards used to answer the pointer as well — a 3D
   tilt tracking the cursor, a -6px hover lift, a border swap and a 420px
   specular sheen sliding across the face. That whole layer is gone; a hover
   is the single background step defined on .card:hover in style.css.

   What survives is the scroll entrance: the card rises 40px and scales up
   from 0.97 as it enters view. Nothing here responds to a pointer, so
   --tilt-x/--tilt-y and --sheen-x/--sheen-y are no longer read (js/pages/animate.js
   no longer writes them either). */
.card.service-card {
  --lift: 0px;
  --scale: 0.97;
  --fade: 0;

  opacity: var(--fade);
  transform:
    translateY(var(--lift))
    scale(var(--scale));
  transition:
    opacity var(--dur-3) var(--ease-out),
    transform var(--dur-3) var(--ease-out),
    background var(--transition);
}

.card.service-card:not(.is-in) { --lift: 40px; }
.card.service-card.is-in { --fade: 1; --lift: 0px; --scale: 1; }

/* Neutralised rather than deleted — .is-tilting is no longer applied, and
   the sheen it drove is gone, but leaving inert rules keeps any stray
   reference from resolving to something unexpected. */
.card.service-card.is-tilting { box-shadow: none; border-color: var(--glass-border); }
.service-card::after { content: none; }

/* ---------------------------------------------------------------------
   INDUSTRIES — ambient bubble field
   Sits behind the Three.js canvas and is entirely non-interactive, so the
   sphere and the chip row keep working exactly as they did.
   --------------------------------------------------------------------- */
.industries-chapter { position: relative; }

.industry-bubbles {
  position: absolute;
  inset: -10% 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}
.industry-bubbles span {
  position: absolute;
  border-radius: 50%;
  background: radial-gradient(
    circle at 32% 28%,
    rgba(0, 113, 227, 0.30),
    var(--accent-tint-strong) 45%,
    transparent 70%
  );
  filter: blur(14px);
  will-change: transform;
  animation-name: bubble-drift;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  animation-direction: alternate;
}
@keyframes bubble-drift {
  from { margin-top: 0; }
  to   { margin-top: -34px; }
}
/* Keep section content above the field. */
.industries-chapter > .container { position: relative; z-index: 1; }

/* A slow idle bob on the chips so the row feels buoyant too. */
.industry-chip {
  animation: chip-bob 6s ease-in-out infinite;
}
.industry-chip:nth-child(3n+2) { animation-delay: -2s; }
.industry-chip:nth-child(3n+3) { animation-delay: -4s; }
@keyframes chip-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-4px); }
}

/* ---------------------------------------------------------------------
   PROCESS — sticky heading, scroll-driven active step, drawn connector
   Shared by index / services / about so the section behaves identically
   wherever it appears.
   --------------------------------------------------------------------- */
/* Two columns: the heading pins while the steps scroll past it. This is
   what turns a static four-across strip into something that reads as a
   sequence you're moving through. */
[data-anim="process"] .process-layout {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: 64px;
  align-items: start;
}

[data-anim="process"] .process-sticky {
  position: sticky;
  top: 120px;
  align-self: start;
}
[data-anim="process"] .process-sticky .section-head { margin-bottom: 0; }

[data-anim="process"] .process-track {
  position: relative;
  padding-left: 52px;
  display: flex;
  flex-direction: column;
  gap: 56px;
}

/* The steps came from a horizontal .process-strip, where they were equal
   flex columns divided by vertical rules. In a stacked rail those borders
   and the flex sizing are wrong, so they're unset here. */
[data-anim="process"] .process-step {
  flex: none;
  padding: 0;
  border-right: none;
  overflow: visible;
}
[data-anim="process"] .process-step .step-num {
  top: -26px;
  right: auto;
  left: -4px;
  font-size: 3.4rem;
  opacity: 0.16;
}
[data-anim="process"] .process-step h4,
[data-anim="process"] .process-step h3 { font-size: 1.2rem; }
[data-anim="process"] .process-step p { font-size: 0.95rem; max-width: 46ch; }

@media (max-width: 900px) {
  [data-anim="process"] .process-layout {
    grid-template-columns: 1fr;
    gap: 40px;
  }
  [data-anim="process"] .process-sticky { position: static; }
  [data-anim="process"] .process-track { gap: 42px; padding-left: 40px; }
  [data-anim="process"] .process-line { left: 11px; }
  [data-anim="process"] .process-step::before { left: -35px; }
  /* Undo the mobile card treatment inherited from .process-strip. */
  [data-anim="process"] .process-step {
    flex: none;
    border: none;
    border-radius: 0;
    background: none;
  }
}

/* The rail the steps hang off, and the gradient that fills it on scroll. */
[data-anim="process"] .process-line {
  position: absolute;
  left: 15px;
  top: 8px;
  bottom: 8px;
  width: 2px;
  background: var(--line);
  border-radius: 2px;
  overflow: hidden;
}
[data-anim="process"] .process-line-fill {
  display: block;
  width: 100%;
  height: 100%;
  transform: scaleY(0);
  transform-origin: top;
  /* Was violet -> blue -> cyan. Three chromatic colours in one element is
     the same one-blue-rule break the problem-section thread line had. */
  background: var(--accent);
  transition: transform var(--dur-3) var(--ease-out);
}

[data-anim="process"] .process-step {
  position: relative;
  opacity: 0;
  transform: translateY(26px);
  transition:
    opacity var(--dur-2) var(--ease-out),
    transform var(--dur-2) var(--ease-out),
    color var(--dur-2) var(--ease-inout);
}
[data-anim="process"] .process-step.is-in { opacity: 0.4; transform: translateY(0); }
[data-anim="process"] .process-step.is-past { opacity: 0.55; }
[data-anim="process"] .process-step.is-active { opacity: 1; }

/* The node on the rail. Scales and lights up when its step is active. */
[data-anim="process"] .process-step::before {
  content: "";
  position: absolute;
  left: -45px;
  top: 10px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--surface-2);
  border: 2px solid var(--line-strong);
  transform: scale(0.8);
  transition:
    transform var(--dur-2) var(--ease-out),
    background var(--dur-2) var(--ease-inout),
    border-color var(--dur-2) var(--ease-inout),
    box-shadow var(--dur-2) var(--ease-inout);
}
/* Latched on arrival, not tied to a moving "active" marker. A step that
   lights up and then goes dim again as you scroll past reads as the page
   losing its place; a step that completes and stays completed reads as
   progress. .is-past is kept identical to .is-in for the same reason —
   nothing regresses once you have passed it. */
[data-anim="process"] .process-step.is-in::before,
[data-anim="process"] .process-step.is-past::before,
[data-anim="process"] .process-step.is-active::before {
  transform: scale(1.15);
  background: var(--accent-fill);
  border-color: var(--accent);
  box-shadow: none;
}

/* Inside an active step, the heading and copy shift up a touch — a small
   secondary move that makes activation feel like focus, not just opacity. */
[data-anim="process"] .process-step h3,
[data-anim="process"] .process-step p {
  transition: transform var(--dur-2) var(--ease-out);
}
[data-anim="process"] .process-step.is-active h3 { transform: translateX(4px); }

/* ---------------------------------------------------------------------
   WORK — clip-path wipe entrance, hover expand, animated UI preview
   A wipe rather than a rise, so the gesture reads as distinct from the
   Services grid even though both are card grids.
   --------------------------------------------------------------------- */
.work-card {
  clip-path: inset(0 0 100% 0);
  opacity: 0;
  transition:
    clip-path var(--dur-3) var(--ease-out),
    opacity var(--dur-2) var(--ease-out),
    transform var(--dur-2) var(--ease-out),
    box-shadow var(--dur-2) var(--ease-out),
    border-color var(--dur-2) var(--ease-out);
}
.work-card.is-in {
  clip-path: inset(0 0 0 0);
  opacity: 1;
}

/* Was an 8px lift plus a 2% scale plus a border swap — by far the loudest
   card hover on the site. Now the same single background step as the rest.
   z-index is kept: the thumbnail inside still animates and needs to sit
   above its neighbours while it does. */
.work-card:hover {
  transform: none;
  box-shadow: none;
  border-color: var(--glass-border);
  background: var(--glass-bg-hover);
  z-index: 2;
}

/* The thumbnail holds a small mock interface. At rest only the gradient
   and the label show; on hover the label steps aside and the mock UI
   assembles itself. */
.work-thumb {
  position: relative;
  overflow: hidden;
}
.work-thumb > span {
  transition: opacity var(--dur-2) var(--ease-out),
              transform var(--dur-2) var(--ease-out);
}
.work-card:hover .work-thumb > span {
  opacity: 0;
  transform: translateY(-8px);
}

.work-mock {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 14px;
  opacity: 0;
  transition: opacity var(--dur-2) var(--ease-out);
  pointer-events: none;
}
.work-card:hover .work-mock { opacity: 1; }

/* Every piece of the mock rises in sequence, so the preview looks like an
   interface loading rather than an image appearing. */
.work-mock > * {
  opacity: 0;
  transform: translateY(10px);
  transition:
    opacity var(--dur-2) var(--ease-out),
    transform var(--dur-2) var(--ease-out);
}
.work-card:hover .work-mock > * { opacity: 1; transform: translateY(0); }
.work-card:hover .work-mock > *:nth-child(1) { transition-delay: calc(var(--stagger) * 0); }
.work-card:hover .work-mock > *:nth-child(2) { transition-delay: calc(var(--stagger) * 1); }
.work-card:hover .work-mock > *:nth-child(3) { transition-delay: calc(var(--stagger) * 2); }
.work-card:hover .work-mock > *:nth-child(4) { transition-delay: calc(var(--stagger) * 3); }

/* -- Mock UI primitives ------------------------------------------------ */
.mock-bar {
  display: flex;
  align-items: center;
  gap: 5px;
  height: 16px;
  padding: 0 7px;
  border-radius: 5px;
  background: var(--surface-canvas);
  backdrop-filter: none;
}
.mock-dot {
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--line-strong);
}
.mock-line {
  height: 5px;
  border-radius: 3px;
  background: var(--line);
}
.mock-row { display: flex; gap: 6px; }
.mock-tile {
  flex: 1;
  border-radius: 6px;
  background: var(--surface-canvas);
  backdrop-filter: blur(4px);
  padding: 7px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-height: 34px;
}
.mock-tile .mock-line:first-child { width: 55%; background: var(--surface-pebble); }
.mock-tile .mock-line:last-child  { width: 80%; height: 7px; background: var(--surface-pebble); }

.mock-chart {
  flex: 1;
  display: flex;
  align-items: flex-end;
  gap: 4px;
  padding: 8px;
  border-radius: 6px;
  background: var(--surface-canvas);
  backdrop-filter: blur(4px);
  min-height: 46px;
}
.mock-chart i {
  flex: 1;
  border-radius: 2px 2px 0 0;
  background: linear-gradient(180deg, var(--fill-hover), var(--fill-hover));
  transform: scaleY(0.2);
  transform-origin: bottom;
  transition: transform var(--dur-2) var(--ease-out);
}
.work-card:hover .mock-chart i { transform: scaleY(1); }
.work-card:hover .mock-chart i:nth-child(1) { transition-delay: 120ms; }
.work-card:hover .mock-chart i:nth-child(2) { transition-delay: 170ms; }
.work-card:hover .mock-chart i:nth-child(3) { transition-delay: 220ms; }
.work-card:hover .mock-chart i:nth-child(4) { transition-delay: 270ms; }
.work-card:hover .mock-chart i:nth-child(5) { transition-delay: 320ms; }
.work-card:hover .mock-chart i:nth-child(6) { transition-delay: 370ms; }

/* Phone frame for the mobile-app card. */
.mock-phone {
  align-self: center;
  width: 54px;
  flex: 1;
  border-radius: var(--radius);
  border: 2px solid var(--line);
  background: var(--surface-canvas);
  padding: 8px 6px;
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.mock-phone .mock-line { background: var(--surface-pebble); }
.mock-phone .mock-line:nth-child(1) { width: 60%; }
.mock-phone .mock-line:nth-child(2) { width: 100%; }
.mock-phone .mock-line:nth-child(3) { width: 82%; }

/* ---------------------------------------------------------------------
   VALUES — connector line drawn between the cards
   --------------------------------------------------------------------- */
.values-connector {
  position: relative;
  height: 2px;
  margin: 0 auto 26px;
  max-width: 74%;
  opacity: 0.75;
}
.values-connector svg { width: 100%; height: 2px; overflow: visible; }
.values-connector path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}
.values-connector.is-drawing path {
  animation: values-draw var(--dur-3) var(--ease-out) forwards;
}
@keyframes values-draw { to { stroke-dashoffset: 0; } }

[data-anim="values"] .mvv-card {
  --lift: 26px;
  --scale: 0.97;
  opacity: 0;
  transform: translateY(var(--lift)) scale(var(--scale));
  transition:
    opacity var(--dur-2) var(--ease-out),
    transform var(--dur-2) var(--ease-out);
  /* The hand-tuned nth-child delays in style.css were built for the old
     uniform fade; the sequence is driven from JS now so they must not
     also apply. */
  transition-delay: 0s !important;
}
[data-anim="values"] .mvv-card.is-in {
  --lift: 0px;
  --scale: 1;
  opacity: 1;
}
/* The hover lift that used to live here is gone with the rest of them — a
   hover is the background step on .card:hover and nothing else. The entrance
   above still uses --lift, which is why the variable stays. */

/* A node on the connector above each card, to tie line and card together. */
[data-anim="values"] .mvv-card::before {
  content: "";
  position: absolute;
  top: -27px;
  left: 50%;
  width: 9px;
  height: 9px;
  margin-left: -4.5px;
  border-radius: 50%;
  background: var(--accent-fill);
  box-shadow: none;
  opacity: 0;
  transform: scale(0.4);
  transition:
    opacity var(--dur-1) var(--ease-out),
    transform var(--dur-2) var(--ease-out);
}
[data-anim="values"] .mvv-card.is-in::before { opacity: 1; transform: scale(1); }

/* ---------------------------------------------------------------------
   CTA — proximity glow + magnetic button
   --glow is written by animate.js as the banner nears the middle of the
   viewport; everything visual is derived from it here.
   --------------------------------------------------------------------- */
.cta-banner,
.site-finale { --glow: 0; }

/* inset:0 rather than a negative inset — .cta-banner sets overflow:hidden,
   so anything bleeding outside the box would simply be clipped. The wash is
   sized and blurred to fill the panel from within instead. */
.cta-banner::after,
.site-finale::after { content: none; }
.site-finale { position: relative; overflow: hidden; }
.site-finale > * { position: relative; z-index: 1; }

/* transform is driven frame-by-frame from JS. style.css puts a transition on
   .cta-banner .btn's transform as part of the banner's entrance sequence,
   and leaving that active would make every lerped step queue its own tween —
   the button smears along behind the cursor instead of tracking it.

   transition-property is overridden on its own rather than the transition
   shorthand: the shorthand would also reset transition-delay, and that delay
   is what staggers the button in after the heading and copy. This drops
   transform from the list and leaves the entrance timing untouched. */
.cta-banner .btn.is-magnetic,
.site-finale .btn.is-magnetic {
  will-change: transform;
  transition-property: opacity, box-shadow, background, border-color;
}
.magnet-label { transition: none !important; }
.magnet-label {
  display: inline-flex;
  align-items: center;
  gap: inherit;
  pointer-events: none;
}
.btn.is-magnetic.is-pulled {
  box-shadow: none;
}

/* ---------------------------------------------------------------------
   Reduced motion — belt-and-suspenders on top of style.css's global rule.
   JS also checks prefers-reduced-motion directly and skips page-transition
   timing, parallax, auto-stagger and every section module above, so this
   is the static fallback for anything that starts life hidden.
   --------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  /* The body animation this used to cancel no longer exists. opacity stays
     as a guard for the fade-out class below. */
  body {
    opacity: 1;
  }
  body.page-transition-out {
    opacity: 1;
    transition: none;
  }
  [data-parallax] {
    transform: none !important;
  }

  /* Anything that defaults to hidden must default to visible instead —
     if JS fails to run for any reason, no content can be left invisible. */
  .hero .hero-inner h1 .tw-c,
  .hero-visual,
    .service-card,
  .work-card,
  [data-anim="process"] .process-step,
  [data-anim="values"] .mvv-card {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
    transition: none !important;
  }
  .hero .hero-inner h1 .tw-c.is-caret::after { display: none; }
  .industry-bubbles { display: none; }
  .industry-chip { animation: none; }
  .values-connector path { stroke-dashoffset: 0; }
  .process-line-fill { transform: scaleY(1) !important; }

}
