/* ==========================================================================
   SIGMA SCANNER — animations.css
   Motion tokens · keyframes · reveal states · accessibility contracts.

   ---------------------------------------------------------------------------
   THE NO-JS CONTRACT — read before editing anything in this file
   ---------------------------------------------------------------------------
   Content ships VISIBLE. The motion layer opts elements OUT.

   Every from-state in this file is scoped behind [data-motion="full"], which
   js/animation.js sets on <html> only after GSAP is confirmed loaded. If the
   CDN fails, JS is disabled, or a script errors, the attribute stays "none"
   and the page renders complete.

   The opposite pattern — opacity:0 in CSS, revealed by JS — is the most
   common and most damaging mistake in GSAP landing pages, because a single
   script failure produces a blank page. Do not introduce it here.

   Rule: no selector in this file may hide content unless it is nested
   inside [data-motion="full"].
   ---------------------------------------------------------------------------

   Tiers (06-DESIGN-SYSTEM.md §16.1)
     T0 Functional     accordion, menu, focus — never removed
     T1 Informational  hero sort, curve draw, heatmap — END STATE on reduce
     T2 Orientational  section entrances, staggers — opacity only on reduce
     T3 Delight        cursor, magnetic, tilt, parallax — REMOVED on reduce
   ========================================================================== */

@layer tokens {

  :root {
    /* ---------- DURATIONS ---------------------------------------------
       UI is fast; narrative is slow. Getting this backwards is the most
       common motion mistake: a 400ms hover feels broken, and a 180ms
       story beat does not register. */
    --dur-instant: 100ms;   /* toggles, checkboxes, state flips     */
    --dur-fast:    180ms;   /* hover, focus, colour change          */
    --dur-base:    280ms;   /* accordion, tab switch, card lift     */
    --dur-slow:    480ms;   /* section entrance, panel crossfade    */
    --dur-cine:    900ms;   /* the hero sort, the wipe              */
    --dur-draw:   1400ms;   /* an SVG path drawing its full length  */

    /* ---------- EASING ------------------------------------------------
       CSS values with their GSAP equivalents, so both layers stay
       identical. Changing one without the other is a bug. */
    --ease-out-quad:  cubic-bezier(.25, .46, .45, .94);  /* ≈ power1.out   — subtle, secondary reveals */
    --ease-out-cubic: cubic-bezier(.33, 1, .68, 1);      /* ≈ power2.out   — the standard reveal ease */
    --ease-out-expo:  cubic-bezier(.16, 1, .3, 1);       /* ≈ expo.out     — the ONE hero-emphasis beat per timeline */
    --ease-in-out:    cubic-bezier(.65, 0, .35, 1);      /* ≈ power3.inOut */
    --ease-spring:    cubic-bezier(.34, 1.56, .64, 1);   /* ≈ back.out(1.4)— standard "settles into place" pop */
    --ease-linear:    linear;                            /* ALL scrubs     */

    /* Two more GSAP-only values used consistently enough across this
       project's section modules to write down, even though no CSS
       transition needs their bezier form:
         power3.out    — quickTo()-driven pointer-follow / magnetic effects
                          (hero.js's tilt + floats, cursor.js, testimonials.js's
                          card tilt) and this file's own timeline defaults.
                          Never used for a plain content reveal — that's
                          power2.out's job.
         back.out(1.7) — the energetic tier, for multi-element stagger pops
                          (scattering dots, candlesticks) and single-icon
                          snap-bounces that want more life than the standard
                          back.out(1.4). Not a third, fourth or fifth
                          overshoot value picked ad hoc — exactly these two
                          tiers, chosen deliberately each time. */

    /* ---------- STAGGER ------------------------------------------------ */
    --stg-tight:  26ms;   /* list rows, heatmap cells      */
    --stg-base:   60ms;   /* cards in a grid               */
    --stg-loose: 110ms;   /* headline lines, large blocks  */

    /* ---------- REVEAL GEOMETRY ---------------------------------------- */
    --reveal-y:      20px;
    --reveal-y-sm:   12px;
    --reveal-scale:  .985;
  }
}


/* ==========================================================================
   @layer base — transition defaults
   ========================================================================== */
@layer base {

  /* Never `transition: all`. It animates properties you did not intend,
     including layout-triggering ones, and is the most common cause of
     mysterious jank. Components declare their own property lists. */

  a[class], button, [role="button"], summary {
    transition:
      color var(--dur-fast) var(--ease-out-quad),
      background-color var(--dur-fast) var(--ease-out-quad),
      border-color var(--dur-fast) var(--ease-out-quad),
      box-shadow var(--dur-fast) var(--ease-out-quad),
      transform var(--dur-fast) var(--ease-out-quad);
  }

  input, select, textarea {
    transition:
      border-color var(--dur-fast) var(--ease-out-quad),
      box-shadow var(--dur-fast) var(--ease-out-quad),
      background-color var(--dur-fast) var(--ease-out-quad);
  }

  /* Focus must be instant. An animated focus ring lags behind keyboard
     navigation and makes fast tabbing feel broken. */
  :focus-visible { transition: none; }
}


/* ==========================================================================
   @layer components — reveal system
   ========================================================================== */
@layer components {

  /* ------------------------------------------------------------------
     REVEAL — the page's single entrance grammar.

     One grammar for the whole page. No fly-ins from the left, no
     rotations, no scale-from-zero, no bounce. Everything rises slightly
     and resolves. The restraint IS the premium feel — variety in
     entrance animation reads as indecision.

     Markup contract:
       <div data-reveal>              default: opacity + y
       <div data-reveal="fade">       opacity only
       <div data-reveal="rise">       opacity + larger y + scale
       <div data-reveal-group>        staggers its direct children
       <div data-reveal-delay="2">    +2 stagger steps

     JS drives these with GSAP. These CSS rules exist so that the
     from-state is correct on the very first paint, before GSAP has
     initialised — preventing a one-frame flash of final position.
     ------------------------------------------------------------------ */

  [data-motion="full"] [data-reveal] {
    opacity: 0;
    transform: translate3d(0, var(--reveal-y-sm), 0);
    will-change: opacity, transform;
  }

  [data-motion="full"] [data-reveal="fade"] {
    transform: none;
  }

  [data-motion="full"] [data-reveal="rise"] {
    transform: translate3d(0, var(--reveal-y), 0) scale(var(--reveal-scale));
  }

  [data-motion="full"] [data-reveal-group] > * {
    opacity: 0;
    transform: translate3d(0, var(--reveal-y-sm), 0);
  }

  /* Set by GSAP onComplete. `will-change` is REMOVED here — a permanent
     will-change on a dozen elements exhausts GPU memory on mid-range
     Android, which is the majority of this audience's hardware. */
  [data-motion="full"] [data-reveal].is-revealed,
  [data-motion="full"] [data-reveal-group] > .is-revealed {
    opacity: 1;
    transform: none;
    will-change: auto;
  }

  /* ------------------------------------------------------------------
     SVG PATH DRAW
     Implemented with stroke-dashoffset rather than GSAP DrawSVG — six
     lines of our own code, no licence question, ~6 KB smaller.
     JS measures the real length with getTotalLength() and overrides
     --path-length; 1000 is only a pre-measurement placeholder.
     ------------------------------------------------------------------ */
  [data-motion="full"] [data-draw] {
    --path-length: 1000;
    stroke-dasharray: var(--path-length);
    stroke-dashoffset: var(--path-length);
  }
  [data-motion="full"] [data-draw].is-drawn {
    stroke-dashoffset: 0;
    transition: stroke-dashoffset var(--dur-draw) var(--ease-out-cubic);
  }

  /* ------------------------------------------------------------------
     COUNTERS
     Reserve the final width so counting never reflows its neighbours.
     Tabular figures are mandatory — without them, counting animations
     cause visible horizontal jitter.
     ------------------------------------------------------------------ */
  [data-count] {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
  }

  /* ------------------------------------------------------------------
     T3 · DELIGHT
     The custom cursor was removed — see js/cursor.js for the reasoning.
     Magnetic buttons are all that remain of this tier.
     ------------------------------------------------------------------ */
  .ss-magnetic { will-change: transform; }

  /* ------------------------------------------------------------------
     THE LINE
     Spec: 03-MOTION-SYSTEM.md §9. One SVG path spanning the full
     document height, drawn in exact sync with total scroll progress —
     "scroll = time" made literal, the one motion device on the page
     tying every section together rather than living inside one. Built
     and driven entirely by js/animation.js's initLine(); this file only
     supplies its resting (empty) appearance so there is nothing to flash
     before that script runs.

     mix-blend-mode: difference, not a fixed colour, is what lets a single
     line stay visible over both the page's light sections and its two
     dark ones (§7 Proof, §14 Footer) without per-section overrides — the
     rendered colour is always the inverse of whatever sits behind it. */
  .ss-line__rail {
    position: absolute;
    /* Starts below the fixed nav (z-index 100 vs. this rail's z-index 1),
       not at the true viewport top — the rail used to run the full 100vh,
       which put the first ~64–88px of it directly underneath the nav bar,
       permanently hidden and effectively wasted rail length. initLine()
       (animation.js) reads the rail's own rendered height at runtime, so
       shortening it here is the whole fix — nothing on the JS side needs
       to know the nav even exists. */
    inset-block-start: var(--nav-h-scrolled);
    inset-block-end: 0;
    inset-inline-start: 0;
    inline-size: 32px;
    mix-blend-mode: difference;
  }
  .ss-line__svg { display: block; inline-size: 100%; block-size: 100%; overflow: visible; }
  .ss-line__track {
    fill: none;
    stroke: rgb(255 255 255 / .45);
    stroke-width: 1.25;
    vector-effect: non-scaling-stroke;
  }
  .ss-line__fill {
    fill: none;
    stroke: rgb(255 255 255 / .92);
    stroke-width: 1.5;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
    filter: drop-shadow(0 0 4px rgb(74 222 128 / .8));
  }
  .ss-line__dot {
    position: absolute;
    inset-inline-start: 16px;
    inline-size: 7px;
    block-size: 7px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 0 10px 3px rgb(74 222 128 / .85);
    transform: translate(-50%, -50%);
  }
}


/* ==========================================================================
   @layer components — keyframes
   Kept few on purpose. Anything sequenced or scroll-linked belongs in
   GSAP, not here; CSS keyframes are for autonomous, looping, or
   fire-and-forget motion only.
   ========================================================================== */
@layer components {

  @keyframes ss-fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  @keyframes ss-rise-in {
    from { opacity: 0; transform: translate3d(0, var(--reveal-y), 0); }
    to   { opacity: 1; transform: none; }
  }

  /* Loading spinner — button loading state. Width is held by the button
     so this never causes layout shift. */
  @keyframes ss-spin {
    to { transform: rotate(360deg); }
  }

  /* Skeleton shimmer for async content (Trustpilot rating, blog list).
     Deliberately slow and low-contrast — an aggressive shimmer reads as
     a broken page rather than a loading one. */
  @keyframes ss-shimmer {
    from { background-position: -200% 0; }
    to   { background-position: 200% 0; }
  }

  .ss-skeleton {
    background: linear-gradient(90deg, var(--ink-100) 25%, var(--ink-150) 50%, var(--ink-100) 75%);
    background-size: 200% 100%;
    animation: ss-shimmer 1.8s var(--ease-linear) infinite;
    border-radius: var(--r-sm);
    color: transparent;
    user-select: none;
  }

  .ss-spinner {
    inline-size: 1em;
    block-size: 1em;
    border: 2px solid currentColor;
    border-block-start-color: transparent;
    border-radius: var(--r-full);
    animation: ss-spin 700ms var(--ease-linear) infinite;
  }

  /* Utility for one-off entrances that are not part of the scroll system
     — e.g. a form success message replacing a form. */
  .ss-animate-in { animation: ss-rise-in var(--dur-slow) var(--ease-out-cubic) both; }
}


/* ==========================================================================
   ACCESSIBILITY CONTRACTS
   These blocks are not optional polish. They are the acceptance criteria
   for Phase 3.
   ========================================================================== */

/* --------------------------------------------------------------------------
   REDUCED MOTION

   A T1 animation is NEVER simply disabled — its END STATE is what the user
   sees. That distinction is the whole contract, and it is the difference
   between an accessible page and a broken one.

   Acceptance test: with reduce enabled, the page must still be complete and
   good. Not degraded — different.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {

  /* Nothing that moves, moves. Kept at 1ms rather than 0 so transitionend
     and animationend listeners still fire and JS state machines complete. */
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }

  /* T1 — final state, rendered immediately. */
  [data-reveal],
  [data-reveal-group] > *,
  [data-motion] [data-reveal],
  [data-motion] [data-reveal-group] > * {
    opacity: 1 !important;
    transform: none !important;
  }

  [data-draw] {
    stroke-dasharray: none !important;
    stroke-dashoffset: 0 !important;
  }

  /* T3 — removed entirely, not merely stilled. js/cursor.js also declines
     to construct these; this is belt and braces. */
  .ss-magnetic { transform: none !important; }

  /* T0 — kept, shortened. Instant accordion open with no transition reads
     as a page glitch; 100ms reads as intentional. */
  [data-accordion-panel] { transition-duration: var(--dur-instant) !important; }

  .ss-skeleton { animation: none !important; background: var(--ink-100); }
}


/* --------------------------------------------------------------------------
   FORCED COLOURS  (Windows High Contrast)

   Rarely implemented, cheap to implement, and the difference between usable
   and unusable for the people who rely on it. System colours win; our job is
   to make sure the structure survives when every background is stripped.
   -------------------------------------------------------------------------- */
@media (forced-colors: active) {

  /* Borders are often all that is left — make sure they exist. */
  .ss-glass,
  .ss-glass--dark {
    background: Canvas;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border: 1px solid CanvasText;
  }

  /* box-shadow is discarded in forced-colors, so the focus ring must be a
     real outline or it disappears completely. */
  :focus-visible {
    outline: 3px solid Highlight;
    outline-offset: 2px;
    box-shadow: none;
  }

  /* Icons inherit system text colour rather than a token. */
  .ss-icon, svg[class*="icon"] { stroke: CanvasText; }

  /* Anything encoding meaning purely in background colour needs a border
     to survive. The heatmap is the critical case. */
  [data-heat-cell] { border: 1px solid CanvasText; }

  .ss-skeleton { background: Canvas; border: 1px solid GrayText; animation: none; }
}


/* --------------------------------------------------------------------------
   NO JAVASCRIPT / MOTION UNAVAILABLE

   data-motion stays "none" when GSAP has not loaded. Nothing above hides
   content in that state, so this block is a safety assertion rather than a
   fix — if it ever becomes load-bearing, something upstream is wrong.
   -------------------------------------------------------------------------- */
[data-motion="none"] [data-reveal],
[data-motion="none"] [data-reveal-group] > * {
  opacity: 1;
  transform: none;
}

[data-motion="none"] [data-draw] {
  stroke-dasharray: none;
  stroke-dashoffset: 0;
}
