/* ══ KOVA — navigation and motion ══════════════════════════════════════════

   THE LAG SAYED REPORTED, DIAGNOSED. He said the menu opens fast at the top of
   the landing page but is "stuck" after scrolling. Measured at 390px:
       at top       backdrop-filter: none
       after scroll backdrop-filter: blur(20px) saturate(1.8)
   .nav.scrolled switches the filter ON. A backdrop-filter makes its element the
   CONTAINING BLOCK for position:fixed descendants, so the drawer lives inside
   the nav while it is blurred — and opening the menu forces the browser to tear
   down a full-width blurred compositing layer first. At the top there is no
   layer to tear down, which is exactly why it felt instant there and sluggish
   after scrolling. The filter is removed rather than retimed.

   .nav.scrolled also changed padding 14px -> 12px, reflowing the bar on every
   scroll event. Gone: the bar is one height always.                          */

.nav{
  background:var(--black) !important;          /* OPAQUE. No translucency, no blur. */
  backdrop-filter:none !important; -webkit-backdrop-filter:none !important;
  border-bottom:1px solid var(--black-4);
  padding-block:14px !important;               /* one height, never reflows */
  transition:none;                             /* nothing to animate on scroll */
}
.nav.scrolled{
  background:var(--black) !important;
  backdrop-filter:none !important; -webkit-backdrop-filter:none !important;
  padding-block:14px !important;
  border-bottom-color:var(--black-5);
}

/* ── THE DRAWER ────────────────────────────────────────────────────────────
   Opaque, aligned to the same left spine as the page, set in the site's own
   type rather than a separate decorative treatment. The old panel was 98.5%
   translucent with blur(22px) over it, carried a counter that numbered the
   links 01-10, and staggered them 8ms apart against a 160ms transition, which
   is a simultaneous flash rather than a stagger. */
@media (max-width:1180px){
  .nav-links{
    background:var(--black) !important;        /* fully opaque */
    backdrop-filter:none !important; -webkit-backdrop-filter:none !important;
    padding:calc(64px + env(safe-area-inset-top,0px)) var(--spine) 32px !important;
    align-items:stretch !important;
    justify-content:flex-start !important;
    gap:0 !important;
    /* transform is compositor-only, so the panel cannot be blocked by layout */
    transform:translate3d(100%,0,0);
    transition:transform .28s cubic-bezier(.32,.72,0,1) !important;
    opacity:1 !important; visibility:visible !important;
    will-change:transform;
  }
  .nav-links.open{ transform:translate3d(0,0,0); pointer-events:auto; }
  .nav-links:not(.open){ pointer-events:none; }

  /* the 01-10 counter was decoration that drew attention to the link count */
  .nav-links > a::before{ content:none !important; }

  .nav-links > a{
    font-family:var(--sans) !important;
    font-size:clamp(26px,7vw,34px) !important;
    font-weight:700 !important;
    letter-spacing:-.022em !important;
    line-height:1.1 !important;
    color:var(--off-white) !important;
    padding:15px 0 !important;
    border-bottom:1px solid var(--black-4) !important;
    max-width:none !important; margin:0 !important;
    display:block !important;
    /* real stagger: 34ms apart against a 260ms move, so they arrive in sequence */
    opacity:0; transform:translateY(10px);
    transition:opacity .26s ease, transform .26s cubic-bezier(.32,.72,0,1);
  }
  .nav-links.open > a{ opacity:1; transform:none; }
  .nav-links.open > a:nth-child(1){transition-delay:.03s}
  .nav-links.open > a:nth-child(2){transition-delay:.064s}
  .nav-links.open > a:nth-child(3){transition-delay:.098s}
  .nav-links.open > a:nth-child(4){transition-delay:.132s}
  .nav-links.open > a:nth-child(5){transition-delay:.166s}
  .nav-links.open > a:nth-child(6){transition-delay:.20s}
  .nav-links.open > a:nth-child(7){transition-delay:.234s}
  .nav-links.open > a:nth-child(8){transition-delay:.268s}
  .nav-links.open > a:nth-child(9){transition-delay:.30s}
  .nav-links.open > a:nth-child(10){transition-delay:.334s}

  .nav-links .m-menu-cta{ margin-top:28px !important; }
  .nav-links::after{ content:none !important; }   /* the orphaned "MENU" label */

  /* FAIL-OPEN: with JavaScript off nothing can add .open, so the drawer must
     not be the only route to the links. Confirmed broken before this: 0 of 10
     links were reachable with JS disabled. */
  html:not(.js) .nav-links{ transform:none; position:static; padding:0 !important; }
  html:not(.js) .nav-links > a{ opacity:1; transform:none; }
}

@media (prefers-reduced-motion:reduce){
  .nav-links, .nav-links > a{ transition-duration:.01ms !important; transition-delay:0s !important; }
}

/* ══ SCROLL-IN TEXT ════════════════════════════════════════════════════════
   Sayed asked for text that transitions in smoothly while scrolling, on mobile
   and desktop. Two hard rules govern how it is built here:

   1. FAIL OPEN. Text is the product. The class below only ever applies when
      html.js is present, so with JavaScript disabled every word is visible at
      full opacity — this site has already shipped words stuck at opacity 0 once
      when a layout change outran the reveal script.
   2. NEVER MOVE TEXT A READER IS ALREADY READING. Only opacity and a 14px rise,
      both compositor-only, and only on blocks entering from BELOW the fold.
      No per-word animation on paragraphs, no scroll-linked scrubbing. */

html.js .kv-in{
  opacity:0;
  transform:translate3d(0,12px,0);
  transition:opacity .62s cubic-bezier(.22,.61,.36,1),
             transform .62s cubic-bezier(.22,.61,.36,1);
  will-change:opacity, transform;
}
html.js .kv-in.kv-on{ opacity:1; transform:none; will-change:auto; }

@media (prefers-reduced-motion:reduce){
  html.js .kv-in{ opacity:1 !important; transform:none !important; transition:none !important; }
}

/* ══ THE MOTION LAYER ══════════════════════════════════════════════════════
   Sayed asked for a lot more text animation, and for it to look expensive.
   Two things make the difference between "things fade in" and something that
   reads as designed:

   1. A FOCUS PULL, NOT A FADE. Opacity alone is flat. Adding a short blur that
      resolves to sharp reads the way a lens pulls focus, and it is the single
      biggest quality gain available here. It is kept to headings and leads —
      display type, a handful of elements on screen at once — because blur is
      the one property in this file the compositor cannot do for free.

   2. ONE DECELERATION CURVE. cubic-bezier(.22,1,.36,1) starts fast and lands
      slowly, so motion arrives rather than stops. The site had four different
      curves; everything below shares this one, which is what makes separate
      animations read as one system instead of as separate effects.

   The rules from the rest of the site still bind: nothing here can hide text
   if a script fails, and nothing moves body copy someone is already reading. */

:root{
  --kv-ease:cubic-bezier(.22,1,.36,1);
  --kv-dur:.78s;
}

/* ── Block reveal: every heading, lead, card and row entering from below. */
html.js .kv-in{
  opacity:0;
  transform:translate3d(0,14px,0);
  transition:opacity .58s linear,
             transform var(--kv-dur) var(--kv-ease),
             filter var(--kv-dur) var(--kv-ease);
  will-change:opacity, transform;
}
/* The focus pull, on display TYPE only — never on a container. Blurring a
   wrapper blurs everything inside it, so .section-head took its own 7px and
   then blurred the h2 that already had its own: two filters stacked on one
   heading. Only elements that are themselves text carry the blur. */
html.js h1.kv-in, html.js h2.kv-in, html.js h3.kv-in, html.js p.kv-in.lead{
  filter:blur(7px);
  transform:translate3d(0,20px,0);
}
/* Body copy gets opacity and a rise and NO blur. A reader whose eye is already
   on a paragraph would watch the words resolve, which is irritating rather
   than elegant, and body copy is the product here. */

/* THE ON STATE GOES LAST, AND OUTRANKS EVERY START STATE ABOVE IT.
   This cost a real bug: `html.js .section-head.kv-in` and
   `html.js .kv-in.kv-on` are both (0,3,1), the blur was written second, so it
   won even once .kv-on had landed and the section head stayed permanently out
   of focus. Ties on specificity are decided by source order, so the reset is
   both more specific AND last — either alone would be relying on luck. */
html.js .kv-in.kv-on,
html.js h1.kv-in.kv-on, html.js h2.kv-in.kv-on, html.js h3.kv-in.kv-on,
html.js p.kv-in.lead.kv-on{
  opacity:1 !important;
  transform:none !important;
  filter:none !important;
  will-change:auto;
}

/* ── Line-masked word reveal (agc-motion.js builds .fxr-m / .fxr-i).
      The mask clips a single line box so a descender is never sliced; the
      inner span is the only thing that moves. Re-tuned onto the shared curve
      and given the same focus pull, at a smaller radius because these are
      per-word and there can be a dozen on screen. */
html.js .fxr .fxr-i{
  transition:transform var(--kv-dur) var(--kv-ease),
             opacity .42s linear,
             filter .5s var(--kv-ease);
  filter:blur(4px);
}
html.js .fxr.fxr-on .fxr-i{ filter:none; }

/* ── The hero headline. It already staggered ten words in on load; it now
      arrives through the same focus pull as everything else, so the first
      thing the visitor sees and everything they scroll past behave alike. */
.hero h1 .word{
  filter:blur(9px);
  animation-duration:.9s !important;
  animation-timing-function:var(--kv-ease) !important;
}
@keyframes wordIn{ to{ opacity:1; transform:translateY(0); filter:blur(0); } }

/* ── Links and buttons: a 2px lift on the same curve, so hover belongs to the
      same system as the scroll motion rather than being a separate idiom. */
.btn, .social-icon, .nav-cta{
  transition:transform .32s var(--kv-ease), background-color .32s var(--kv-ease),
             border-color .32s var(--kv-ease), color .32s var(--kv-ease) !important;
}
.btn:hover, .nav-cta:hover{ transform:translateY(-2px); }
.btn:active, .nav-cta:active{ transform:translateY(0); transition-duration:.08s !important; }

/* ══ THE ONE RULE THAT OUTRANKS ALL OF THE ABOVE ═══════════════════════════
   Text is the product. Every hiding declaration in this file is scoped to
   html.js, which the reveal script adds to itself — so a script that 404s or
   fails to parse leaves every word on screen. And anyone who has asked their
   operating system to stop animations gets a page with no motion at all,
   including the two animations that do not need JavaScript to run. */
@media (prefers-reduced-motion:reduce){
  html.js .kv-in, html.js .kv-in.kv-on,
  html.js h1.kv-in, html.js h2.kv-in, html.js h3.kv-in, html.js .kv-in.lead{
    opacity:1 !important; transform:none !important;
    filter:none !important; transition:none !important;
  }
  .fxr .fxr-i, html.js .fxr .fxr-i{
    opacity:1 !important; transform:none !important;
    filter:none !important; transition:none !important; animation:none !important;
  }
  .hero h1 .word{
    opacity:1 !important; transform:none !important;
    filter:none !important; animation:none !important;
  }
  .btn:hover, .nav-cta:hover, .btn:active, .nav-cta:active{ transform:none !important; }
}

/* ══ MORE MOTION ═══════════════════════════════════════════════════════════
   Sayed asked for more, and said desktop felt static. Measured cause of the
   second half: the first screen animated only the hero headline, because the
   reveal deliberately skipped anything already on screen at load. On a phone
   that is two or three elements. On a 1680x1050 display it is most of what a
   visitor sees before they touch the scroll wheel — so the page opened, the
   headline moved, and then nothing did. Above-the-fold content now animates on
   LOAD, staggered, which is what the hero headline already did alone. */

html.js .kv-load{
  opacity:0;
  animation:kvLoad .8s var(--kv-ease) forwards;
  will-change:opacity, transform, filter;
}
html.js h1.kv-load, html.js h2.kv-load, html.js h3.kv-load, html.js p.kv-load.lead{
  animation-name:kvLoadType;      /* the same focus pull the scroll reveal uses */
}
@keyframes kvLoad{
  from{ opacity:0; transform:translate3d(0,16px,0) }
  to  { opacity:1; transform:none }
}
@keyframes kvLoadType{
  from{ opacity:0; transform:translate3d(0,22px,0); filter:blur(7px) }
  to  { opacity:1; transform:none; filter:blur(0) }
}

/* ── The rule between sections draws itself in rather than just being there.
      It is a 1px hairline, so this is the cheapest possible piece of motion
      and it gives the eye something to follow at every section boundary. */
main > section + section{ position:relative; border-top-color:transparent !important; }
main > section + section::before{
  content:''; position:absolute; inset-inline:0; top:0; height:1px;
  background:var(--black-4);
  transform:scaleX(0); transform-origin:left center;
  transition:transform 1.1s var(--kv-ease);
}
html.js main > section + section.kv-rule::before,
html:not(.js) main > section + section::before{ transform:scaleX(1); }

/* ── Cards and rows lift very slightly as they arrive, then settle. */
html.js .service.kv-in, html.js .tier.kv-in, html.js .mech.kv-in,
html.js .pain-card.kv-in, html.js .proc-step.kv-in{
  transform:translate3d(0,18px,0) scale(.994);
}
html.js .service.kv-in.kv-on, html.js .tier.kv-in.kv-on, html.js .mech.kv-in.kv-on,
html.js .pain-card.kv-in.kv-on, html.js .proc-step.kv-in.kv-on{
  transform:none;
}

/* ── Nav links: the underline grows from the left rather than appearing. */
.nav-links > a{ position:relative; }
.nav-links > a::after{
  content:''; position:absolute; left:0; right:0; bottom:-3px; height:1px;
  background:var(--off-white);
  transform:scaleX(0); transform-origin:right center;
  transition:transform .34s var(--kv-ease);
}
.nav-links > a:hover::after,
.nav-links > a:focus-visible::after{ transform:scaleX(1); transform-origin:left center; }

/* ── Scroll progress. One 2px bar, one compositor transform, no layout. */
.kv-progress{
  position:fixed; inset-block-start:0; inset-inline:0; height:2px; z-index:9999;
  background:var(--off-white); transform:scaleX(0); transform-origin:left center;
  pointer-events:none;
}

/* ── Everything above obeys the same one rule. */
@media (prefers-reduced-motion:reduce){
  html.js .kv-load{ opacity:1 !important; animation:none !important;
                    transform:none !important; filter:none !important; }
  main > section + section::before{ transform:scaleX(1) !important; transition:none !important; }
  html.js .service.kv-in, html.js .tier.kv-in, html.js .mech.kv-in,
  html.js .pain-card.kv-in, html.js .proc-step.kv-in{ transform:none !important; }
  .nav-links > a::after{ transition:none !important; }
  .kv-progress{ display:none !important; }
}

/* ── THE EYEBROW SETTLES. Uppercase tracked labels arrive slightly wider than
      they end, so the word tightens into place. It is a small thing and it is
      the sort of small thing that separates a page that animates from a page
      that has animations bolted to it. */
html.js .h-eyebrow.kv-in, html.js .eyebrow.kv-in,
html.js .svc-tag.kv-in, html.js .h-eyebrow.kv-load, html.js .eyebrow.kv-load{
  letter-spacing:.22em !important;
  transition:opacity .58s linear,
             transform var(--kv-dur) var(--kv-ease),
             letter-spacing var(--kv-dur) var(--kv-ease),
             filter var(--kv-dur) var(--kv-ease);
}
html.js .h-eyebrow.kv-in.kv-on, html.js .eyebrow.kv-in.kv-on, html.js .svc-tag.kv-in.kv-on{
  letter-spacing:.08em !important;
}

/* ── THE ORDINALS. 01 / 02 / 03 rise a beat behind the heading they label, so
      a card reads as one gesture with an accent rather than as a block.
   TRANSFORM ONLY — deliberately. The .num numerals are set to 0.18 opacity as
   a design decision (RESUME records a QA threshold flagging them as a false
   positive once already). Animating opacity here would drive them to 1 and
   quietly undo that. The rise alone gives the accent without touching tone. */
html.js .kv-in .mech-n, html.js .kv-in .proc-circle,
html.js .kv-in .service-num, html.js .kv-in .num{
  transform:translate3d(0,10px,0);
  transition:transform .7s var(--kv-ease) .12s;
}
html.js .kv-in.kv-on .mech-n, html.js .kv-in.kv-on .proc-circle,
html.js .kv-in.kv-on .service-num, html.js .kv-in.kv-on .num{
  transform:none;
}

/* ── IMAGES WIPE. A clip-path reveal reads as a frame being uncovered rather
      than a picture fading up, and it never shows a half-transparent photo. */
html.js figure.kv-in img, html.js .kv-in > img, html.js img.kv-in{
  clip-path:inset(0 0 100% 0);
  transition:clip-path .9s var(--kv-ease);
}
html.js figure.kv-in.kv-on img, html.js .kv-in.kv-on > img, html.js img.kv-in.kv-on{
  clip-path:inset(0 0 0 0);
}

@media (prefers-reduced-motion:reduce){
  html.js .h-eyebrow.kv-in, html.js .eyebrow.kv-in, html.js .svc-tag.kv-in,
  html.js .h-eyebrow.kv-load, html.js .eyebrow.kv-load{
    letter-spacing:.08em !important; transition:none !important;
  }
  html.js .kv-in .mech-n, html.js .kv-in .proc-circle,
  html.js .kv-in .service-num, html.js .kv-in .num{
    transform:none !important; transition:none !important;
  }
  html.js figure.kv-in img, html.js .kv-in > img, html.js img.kv-in{
    clip-path:none !important; transition:none !important;
  }
}

/* ══ MOTION SCALES WITH THE SCREEN ═════════════════════════════════════════
   The second half of "the animations don't work on desktop". A 14px rise and a
   7px blur read clearly on a 390px phone, where 14px is 3.6% of the width. On a
   1680px display the same 14px is 0.8% — technically running, visually nothing.
   Motion has to be sized in proportion to the surface it happens on, so the
   travel roughly doubles on a large screen and the focus pull deepens with it.

   The phone values are deliberately NOT increased. They are already correct
   there, and more travel on a small screen reads as jitter. */
@media (min-width:1100px){
  html.js .kv-in{ transform:translate3d(0,26px,0); }
  html.js h1.kv-in, html.js h2.kv-in, html.js h3.kv-in, html.js p.kv-in.lead{
    transform:translate3d(0,38px,0);
    filter:blur(10px);
  }
  html.js .service.kv-in, html.js .tier.kv-in, html.js .mech.kv-in,
  html.js .pain-card.kv-in, html.js .proc-step.kv-in{
    transform:translate3d(0,34px,0) scale(.988);
  }
  html.js .fxr .fxr-i{ filter:blur(6px); }
  html.js .kv-in .mech-n, html.js .kv-in .proc-circle,
  html.js .kv-in .service-num, html.js .kv-in .num{
    transform:translate3d(0,18px,0);
  }
  /* A longer distance needs a longer ride, or it reads as a snap rather than
     as an arrival. */
  :root{ --kv-dur:.92s; }
}

/* The .kv-on reset already carries !important and sits last, so it clears all
   of the above without needing to be restated per breakpoint. */

@media (min-width:1100px) and (prefers-reduced-motion:reduce){
  html.js .kv-in, html.js h1.kv-in, html.js h2.kv-in, html.js h3.kv-in,
  html.js p.kv-in.lead, html.js .service.kv-in, html.js .tier.kv-in,
  html.js .mech.kv-in, html.js .pain-card.kv-in, html.js .proc-step.kv-in,
  html.js .kv-in .mech-n, html.js .kv-in .proc-circle,
  html.js .kv-in .service-num, html.js .kv-in .num, html.js .fxr .fxr-i{
    transform:none !important; filter:none !important;
  }
}
