/* ==========================================================================
   VYSoft — scroll motion system
   --------------------------------------------------------------------------
   Jitter-style web motion, adapted for a B2B services site: mask reveals,
   image curtain wipes, scroll parallax and staggered grid entrances.

   Four hard rules, all learned the expensive way on this codebase:

   1. ONLY opacity and transform are animated. Both run on the compositor and
      never trigger layout or re-rasterise text. No clip-path, no filter:blur,
      no animated left/top/width.
   2. ONE animating element per glyph. Where a heading gets a mask, its
      .reveal container is pinned static, and vice versa. Stacking the two
      makes the same glyphs composite through several layers, which is what
      made the hero headline strobe on phones.
   3. Nothing here is `!important` on a property that something needs to
      animate. Important author declarations outrank CSS animations, so
      pinning an animated property silently freezes it.
   4. Everything is gated on `html.vy-motion`, set by scroll-motion.js. If the
      script never runs, every element sits at its natural resting state -
      visible, unmasked, uncovered. No content can be stranded behind a
      curtain that never lifts. (The `prefers-reduced-motion` gate that used
      to sit alongside it is disabled site-wide - see MOTION POLICY below.)
   ========================================================================== */

/* --------------------------------------------------------------------------
   Resting state - applies in every mode, including reduced motion.
   The mask wrappers are inserted by JS, so they must be harmless on their own.
   -------------------------------------------------------------------------- */
.vy-mask{display:block}
.vy-mask-in{display:block}
.vy-stagger-item{opacity:1}
.vy-fade{opacity:1}
.vy-soft-in{opacity:1}

/* --------------------------------------------------------------------------
   The mask wrappers are <span>s placed INSIDE the heading, and this theme
   colours heading accents with descendant selectors on exactly that element:

     .sub-hero h1 span, .about-hero h1 span, .industries-hero h1 span,
     .sub-heading h2 span, .intro-copy h2 span, .about-heading h2 span,
     .industry-intro h2 span, .sector-copy h2 span, .section-heading h2 span,
     .about-copy h2 span, .ai-copy h2 span   -> all { color: var(--orange) }

   Those match the wrappers as readily as the real accent word, so wrapping a
   heading turned the WHOLE heading orange instead of just its accent. Reset
   the two wrapper classes to inherit; the author's own <span> nested inside
   still matches those rules and still goes orange, which is the intended
   two-tone look the homepage hero has.

   `!important` is correct here rather than a specificity duel: those selectors
   are (0,2,1) and there are eleven of them across five stylesheets, so any
   tie would be decided by load order. `color` is not animated anywhere in this
   system, so marking it important cannot freeze an animation - the trap that
   this codebase has hit before applies to transform and opacity, not colour.
   -------------------------------------------------------------------------- */
:is(h1,h2) .vy-mask,
:is(h1,h2) .vy-mask-in{color:inherit!important}

/* MOTION POLICY: was gated on (prefers-reduced-motion:no-preference); now
   runs for everyone. To restore the gate, put that condition back. */
@media all{

  /* ======================================================================
     1. HEADING MASK REVEAL
     ----------------------------------------------------------------------
     The signature Jitter move: text slides up from behind a hard edge
     rather than fading in place. Built as overflow:hidden + translateY on
     an inner span - pure transform, compositor-only. A clip-path version
     of the same effect animates a paint property and stutters on mobile.

     padding-bottom / negative margin-bottom give descenders (g, y, p) room
     inside the clipping box without changing the heading's layout height.
     ====================================================================== */
  html.vy-motion .vy-mask{
    overflow:hidden;
    padding-bottom:.14em;
    margin-bottom:-.14em;
  }
  html.vy-motion .vy-mask-in{
    transform:translate3d(0,110%,0);
    transition:transform .8s cubic-bezier(.22,1,.36,1) var(--vy-mask-delay,0ms);
    will-change:transform;
  }
  /* Settled state is `none`, not translate3d(0,0,0). Leaving a 3D transform in
     place keeps the element on its own composited layer, and on iOS and some
     Android browsers text rendered that way stays subtly soft — it is
     rasterised once for the layer rather than with normal subpixel
     antialiasing. Dropping to `none` hands the glyphs back to the normal text
     renderer the moment the motion finishes. scroll-motion.js additionally
     clears will-change on transitionend, so nothing holds a GPU layer for a
     transition that already ended. */
  html.vy-motion .vy-in .vy-mask-in,
  html.vy-motion .vy-in.vy-mask-host .vy-mask-in{
    transform:none;
  }

  /* Rule 2. A masked heading's own .reveal container must not also move it.
     Opacity still fades; only the translate is dropped. */
  html.vy-motion .reveal.vy-has-mask{transform:none}

  /* ======================================================================
     2. IMAGE CURTAIN WIPE
     ----------------------------------------------------------------------
     A solid brand-navy panel sits over the image and slides off to the
     right. Visually identical to a clip-path wipe, but it is a transform on
     a separate element, so the photograph is never re-rasterised.

     These four wrappers already carry overflow:hidden, so the curtain is
     clipped to the frame (including its border-radius) for free. ::before is
     free on all four - .ai-panel::before is the only one where ::after was
     already spoken for.
     ====================================================================== */
  html.vy-motion .about-visual,
  html.vy-motion .feature-image,
  html.vy-motion .split-image,
  html.vy-motion .ai-panel{
    position:relative;
  }
  html.vy-motion .about-visual:before,
  html.vy-motion .feature-image:before,
  html.vy-motion .split-image:before,
  html.vy-motion .ai-panel:before{
    content:"";
    position:absolute;
    inset:-1px;
    z-index:4;
    background:var(--navy-deep,#0a1a30);
    transform:translate3d(0,0,0);
    transition:transform .92s cubic-bezier(.76,0,.24,1) var(--vy-curtain-delay,80ms);
    pointer-events:none;
    will-change:transform;
  }
  html.vy-motion .about-visual.vy-in:before,
  html.vy-motion .feature-image.vy-in:before,
  html.vy-motion .split-image.vy-in:before,
  html.vy-motion .ai-panel.vy-in:before{
    transform:translate3d(102%,0,0);
  }

  /* The wrapper's own .reveal translate would drag the curtain with it and
     soften the wipe, so hold the frame still and let the curtain do the work. */
  html.vy-motion .about-visual.reveal,
  html.vy-motion .feature-image.reveal,
  html.vy-motion .split-image.reveal,
  html.vy-motion .ai-panel.reveal{transform:none}

  /* ======================================================================
     3. SCROLL PARALLAX
     ----------------------------------------------------------------------
     scroll-motion.js writes the transform inline, so CSS only prepares the
     layer and must NOT declare a competing transform here. The image is
     scaled past its frame to give the drift somewhere to go without
     exposing an edge; overflow:hidden on the wrapper crops the surplus.
     ====================================================================== */
  html.vy-motion .vy-parallax{
    will-change:transform;
    backface-visibility:hidden;
  }

  /* ======================================================================
     4. STAGGERED GRID ENTRANCES
     ----------------------------------------------------------------------
     The cards already use the theme's .reveal transition. This retimes it:
     a firmer ease-out and slightly more travel, so a row of cards reads as
     one settling sequence rather than four things fading at once. No
     overshoot - bouncing cards read as consumer, not enterprise.

     `.vy-stagger-group` is put on any container holding two or more .reveal
     siblings by scroll-motion.js, rather than being a list of grid class
     names here. Every page names its grids differently, and a hard-coded
     list silently misses the next one someone adds.
     ====================================================================== */
  html.vy-motion .vy-stagger-group>.reveal{
    transform:translate3d(0,26px,0);
    transition:opacity .7s cubic-bezier(.22,1,.36,1) var(--reveal-delay,0ms),
               transform .7s cubic-bezier(.22,1,.36,1) var(--reveal-delay,0ms);
  }
  html.vy-motion .vy-stagger-group>.reveal.visible{
    transform:translate3d(0,0,0);
  }
  /* A masked heading inside a staggered group still must not move twice. */
  html.vy-motion .vy-stagger-group>.reveal.vy-has-mask{transform:none}

  /* Client logos: the grid is a single .reveal, so the individual marks had
     no entrance of their own. Stagger the children and hold the grid still
     (rule 2 again - one animating element per logo). */
  html.vy-motion .client-logo-grid.vy-staggered{transform:none}
  html.vy-motion .vy-stagger-item{
    opacity:0;
    transform:translate3d(0,16px,0);
    transition:opacity .55s cubic-bezier(.22,1,.36,1) var(--vy-stagger-delay,0ms),
               transform .55s cubic-bezier(.22,1,.36,1) var(--vy-stagger-delay,0ms);
  }
  html.vy-motion .vy-in .vy-stagger-item{
    opacity:1;
    transform:translate3d(0,0,0);
  }

  /* ======================================================================
     5. PHONES
     ----------------------------------------------------------------------
     Same choreography, shorter. Long travel on a small screen reads as lag,
     and parallax range is cut in JS rather than here.
     ====================================================================== */
  /* ======================================================================
     5. COMPACT SCREENS — a different vocabulary, not a smaller one
     ----------------------------------------------------------------------
     scroll-motion.js builds no masks and no curtains below 760px (or on any
     coarse-pointer device). Nothing here has an overflow:hidden wrapper, a
     percentage translate, or a permanently promoted layer. Headings fade up a
     few pixels as themselves and images fade in.

     Why not just retune the mask: a wrapped heading at phone width is a tall
     block, and sliding it 110% of its own height is a long slow move that
     reads as sluggish; overflow:hidden clips descenders on the last line; and
     a page of large composited text layers is what makes phone text render
     soft or flicker. None of those problems have a tuning fix.
     ====================================================================== */
  html.vy-motion .vy-fade{
    opacity:0;
    transform:translate3d(0,10px,0);
    transition:opacity .45s ease-out,transform .45s ease-out;
  }
  html.vy-motion .vy-fade.vy-in,
  html.vy-motion .vy-in .vy-fade{
    opacity:1;
    /* `none`, not translate3d(0,0,0): a settled 3D transform keeps the element
       on its own layer, where glyphs stay rasterised rather than getting normal
       subpixel antialiasing. This is the whole point of the compact path. */
    transform:none;
  }

  html.vy-motion .vy-soft-in{
    opacity:0;
    transition:opacity .5s ease-out;
  }
  html.vy-motion .vy-soft-in.vy-in{opacity:1}

  @media (max-width:760px){
    html.vy-motion .vy-mask-in{transition-duration:.62s}
    html.vy-motion .vy-in .vy-mask-in{will-change:auto}
    html.vy-motion .about-visual:before,
    html.vy-motion .feature-image:before,
    html.vy-motion .split-image:before,
    html.vy-motion .ai-panel:before{transition-duration:.7s}
    html.vy-motion .vy-stagger-group>.reveal{
      transform:translate3d(0,18px,0);
      transition-duration:.56s;
    }
    html.vy-motion .vy-stagger-item{transition-duration:.46s}
  }
}

/* ==========================================================================
   REDUCED MOTION
   --------------------------------------------------------------------------
   scroll-motion.js does not add .vy-motion in this mode, so none of the above
   is live. These rules are the second line of defence: if the class is
   somehow present, everything still resolves to its visible resting state.
   Reduce means reduce MOVEMENT, not remove the content.
   ========================================================================== */
/* MOTION POLICY: this site animates for every visitor, matching how peer
   corporate sites behave. The reduced-motion gate below is kept intact but
   made unreachable by `and (min-width:99999px)`. To honour the OS setting
   again, delete that clause. See claude/vysoft-local-site-notes.md. */
@media (prefers-reduced-motion:reduce) and (min-width:99999px){
  html.vy-motion .vy-mask{overflow:visible;padding-bottom:0;margin-bottom:0}
  html.vy-motion .vy-mask-in{transform:none;transition:none}
  html.vy-motion .about-visual:before,
  html.vy-motion .feature-image:before,
  html.vy-motion .split-image:before,
  html.vy-motion .ai-panel:before{display:none}
  html.vy-motion .vy-parallax{transform:none;will-change:auto}
  html.vy-motion .vy-stagger-item{opacity:1;transform:none;transition:none}
}
