/* ─────────────────────────────────────────────────────────────────────
 * Flowella — Motion overlay
 *
 * Native CSS Scroll-Driven Animations + a small set of perpetual
 * micro-motions. This file is the single home for every animation in
 * the theme. It is loaded after design-system.css and main.css from
 * every site template.
 *
 * Authoring rules (must hold for every rule below):
 *   1. Final-state-first: nothing in this file moves an element off its
 *      final visual position by default. Animations only override.
 *   2. prefers-reduced-motion: reduce → all motion stops cleanly.
 *      Static "ambient" shadows / glows from tasks 5b + 6 remain.
 *   3. Scroll-driven rules sit inside
 *        @media (prefers-reduced-motion: no-preference) {
 *          @supports (animation-timeline: view()) { ... }
 *        }
 *      so unsupported browsers (Safari ≤ 17, Firefox without flag) get
 *      the static final state, no jank.
 *   4. Tasks 5b (italic glow breathe) and 6 (CTA glow pulse + hover
 *      sheen) are NOT scroll-driven and only need the prefers-reduced-
 *      motion guard.
 *   8. Task 8 (WhatsApp template .tpl-grid) uses .tpl-grid--revealed +
 *      IntersectionObserver — see block at end of file.
 *   5. Easing for entrances: cubic-bezier(0.22, 1, 0.36, 1). No bounce.
 *   6. animation-duration on scroll-driven rules is 1ms — Firefox needs
 *      a non-zero duration even when timeline drives progress.
 *   7. Tokens come from design-system.css. Where the spec called for
 *      explicit rgba() values, those numbers are the same ones already
 *      compiled into --primary / --secondary / --green.
 * ─────────────────────────────────────────────────────────────────── */

/* ── Tokens ─────────────────────────────────────────────────────────── */
:root {
  --fr-ease-entry: cubic-bezier(0.22, 1, 0.36, 1);
  --fr-glow-secondary-soft: 0 0 12px rgba(79, 182, 236, .25);
  --fr-glow-secondary-strong: 0 0 20px rgba(79, 182, 236, .45);
  --fr-glow-green-soft: 0 0 12px rgba(37, 211, 102, .25);
  --fr-glow-green-strong: 0 0 20px rgba(37, 211, 102, .45);
}


/* Task 1 — Section entry reveal: REMOVED.
   The full-section opacity-0 → 1 fade was reading as a grey placeholder
   on the surrounding sections (especially light ones), which conflicted
   with the "final-state-first" intent. Sections now render in their
   final state on every browser, no cross-fade. The fr-reveal-up
   keyframe below survives because the comparison-card stagger (task 4)
   still depends on it. */
@keyframes fr-reveal-up {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}


/* ─────────────────────────────────────────────────────────────────────
 *  Task 3 — Hero subtle motion (flowella-hero)
 *
 *  hero-art-glow + hero-eyebrow .dot are perpetual micro-motions. They
 *  only need the prefers-reduced-motion guard (no scroll involvement,
 *  no @supports needed for animation-timeline). hero-bg-word IS scroll
 *  driven and sits inside the @supports block.
 *
 *  Tuning: the original 0.85→1.0 / 0.97→1.03 envelope read as too
 *  subtle behind the mascot. We bump the base intensity of the radial
 *  gradient AND widen the breathing envelope to 0.7→1.0 / 0.9→1.1 so
 *  the glow is felt without becoming a strobe.
 * ─────────────────────────────────────────────────────────────────── */

/* More punchy ambient base. Stops match the original sky-blue → mint
   gradient but with higher alpha at the centre. The `.hero` ancestor
   selector lifts specificity to (0,2,0) so we win against the module
   CSS definition (specificity 0,1,0) that loads after motion.css. */
.hero .hero-art-glow {
  background: radial-gradient(circle at 50% 50%,
    rgba(79, 182, 236, .80),
    rgba(48, 213, 200, .55) 35%,
    transparent 70%);
}

@media (prefers-reduced-motion: no-preference) {
  .hero-art-glow {
    animation: fr-hero-glow 6s ease-in-out infinite alternate;
    will-change: opacity, transform;
  }

  .hero-eyebrow .dot {
    animation: fr-hero-dot 1.6s ease-in-out infinite alternate;
    will-change: transform, box-shadow;
  }

  @supports (animation-timeline: scroll()) {
    .hero-bg-word {
      animation: fr-hero-parallax 1ms linear both;
      animation-timeline: scroll();
      animation-range: 0% 100%;
      will-change: transform;
    }
  }
}

@keyframes fr-hero-glow {
  from { opacity: .7; transform: scale(.9);  }
  to   { opacity: 1;  transform: scale(1.1); }
}

@keyframes fr-hero-dot {
  from {
    transform: scale(1);
    box-shadow: 0 0 8px var(--green);
  }
  to {
    transform: scale(1.25);
    box-shadow: 0 0 16px var(--green);
  }
}

@keyframes fr-hero-parallax {
  from { transform: translateY(0); }
  to   { transform: translateY(-60px); }
}


/* ─────────────────────────────────────────────────────────────────────
 *  Task 4 — Comparison cards (home) — narrative motion sequence
 *
 *  The home page's .flowella-comparison-cards module tells a side-by-
 *  side story: the LEFT card (.vs-card.bad) shows a free-text WhatsApp
 *  chat dragging on with the customer drifting; the RIGHT card
 *  (.vs-card.good) shows a Flowella WhatsApp Flow being completed
 *  decisively in fewer beats.
 *
 *  We mirror the IntersectionObserver pattern used by Task 8 below
 *  (`.tpl-grid--revealed` on the use-case detail). When the module
 *  scrolls into view its inline JS adds `.vs-grid--revealed` to
 *  `.vs-grid`, which gates every animation here.
 *
 *  Final-state-first: every selector below paints its FINAL state in
 *  module.hubl.css. The `from` keyframes here describe the pre-reveal
 *  state, and `animation-fill-mode: both` (via the shorthand) holds
 *  that pre-state during the delay. No-JS users never see the
 *  pre-state because `.vs-grid--revealed` is never added — they
 *  see the final design directly. prefers-reduced-motion: reduce
 *  users skip the JS class altogether (see the inline observer in
 *  the module template) so they also see the final state.
 *
 *  Timing — total ~5s, aligned with the use-case page rhythm:
 *      LEFT (chat one-by-one, ~700ms beats):
 *        msg1 0.50s  →  msg6 4.00s  →  typing dots 4.60s
 *      RIGHT (flow body cascades early, then "answer"):
 *        chrome 0.30s, intro 0.55s, details 0.85s, question 1.15s,
 *        options 1.45/1.65/1.85s,
 *        selected option highlight 3.20s,
 *        check pop 3.40s,
 *        submit "click" 4.10s
 *  Both sides finish around 4.6–5.0s — the Flowella side resolves
 *  cleanly while the Broadcast side is still typing.
 * ─────────────────────────────────────────────────────────────────── */

@keyframes fr-vs-bubble-pop {
  from { opacity: 0; transform: translateY(6px) scale(.96); }
  to   { opacity: 1; transform: translateY(0)   scale(1);   }
}
@keyframes fr-vs-fade-up {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0);   }
}
@keyframes fr-vs-typing-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0);   }
}
@keyframes fr-vs-flow-bg-on {
  from { background: #fff;     }
  to   { background: #F0FAF4;  }
}
@keyframes fr-vs-flow-check-on {
  from { opacity: 0; transform: scale(.4); }
  60%  { opacity: 1; transform: scale(1.15); }
  to   { opacity: 1; transform: scale(1);   }
}
@keyframes fr-vs-flow-radio-off {
  from { opacity: 1; }
  to   { opacity: 0; }
}
/* Submit "click": small scale dip + flash of brighter green shadow,
   then settle. Mimics a real tap on the Flow's confirm pill. */
@keyframes fr-vs-flow-submit-click {
  0%   { transform: scale(1);   box-shadow: 0 6px 18px -8px rgba(37, 211, 102, .35); }
  35%  { transform: scale(.96); box-shadow: 0 2px 6px  -4px rgba(37, 211, 102, .25); }
  55%  { transform: scale(.96); box-shadow: 0 2px 6px  -4px rgba(37, 211, 102, .25); }
  80%  { transform: scale(1.015); box-shadow: 0 10px 28px -8px rgba(37, 211, 102, .55); }
  100% { transform: scale(1);   box-shadow: 0 6px 18px -8px rgba(37, 211, 102, .35); }
}

@media (prefers-reduced-motion: no-preference) {
  /* Tuning tokens — keep them in one place so future tweaks are local. */
  .vs-grid {
    --vs-ease: var(--fr-ease-entry);
    --vs-msg-dur: 0.45s;
    --vs-msg-step: 0.7s;
    --vs-msg-base: 0.5s;
    --vs-flow-dur: 0.5s;
    --vs-flow-base: 0.3s;
    --vs-flow-step: 0.3s;
    --vs-select-bg-delay: 3.2s;
    --vs-select-mark-delay: 3.4s;
    --vs-submit-delay: 4.1s;
    --vs-submit-dur: 0.65s;
    --vs-typing-delay: 4.6s;
  }

  /* ── LEFT — chat messages stagger in one by one ─────────────────── */
  .vs-grid.vs-grid--revealed .vs-card.bad .vs-chat-msg {
    animation: fr-vs-bubble-pop var(--vs-msg-dur) var(--vs-ease) both;
  }
  .vs-grid.vs-grid--revealed .vs-card.bad .vs-chat-msg--in  { transform-origin: left bottom; }
  .vs-grid.vs-grid--revealed .vs-card.bad .vs-chat-msg--out { transform-origin: right bottom; }
  .vs-grid.vs-grid--revealed .vs-card.bad .vs-chat-msg:nth-child(1) { animation-delay: var(--vs-msg-base); }
  .vs-grid.vs-grid--revealed .vs-card.bad .vs-chat-msg:nth-child(2) { animation-delay: calc(var(--vs-msg-base) + var(--vs-msg-step) * 1); }
  .vs-grid.vs-grid--revealed .vs-card.bad .vs-chat-msg:nth-child(3) { animation-delay: calc(var(--vs-msg-base) + var(--vs-msg-step) * 2); }
  .vs-grid.vs-grid--revealed .vs-card.bad .vs-chat-msg:nth-child(4) { animation-delay: calc(var(--vs-msg-base) + var(--vs-msg-step) * 3); }
  .vs-grid.vs-grid--revealed .vs-card.bad .vs-chat-msg:nth-child(5) { animation-delay: calc(var(--vs-msg-base) + var(--vs-msg-step) * 4); }
  .vs-grid.vs-grid--revealed .vs-card.bad .vs-chat-msg:nth-child(n + 6) { animation-delay: calc(var(--vs-msg-base) + var(--vs-msg-step) * 5); }

  /* Typing dots fade in last so the "still chasing" beat lands after
     the conversation has ended. The bubble itself keeps its perpetual
     three-dot bounce from module.hubl.css. */
  .vs-grid.vs-grid--revealed .vs-card.bad .vs-chat-typing {
    animation: fr-vs-typing-in 0.45s var(--vs-ease) var(--vs-typing-delay) both;
    transform-origin: left bottom;
  }

  /* ── RIGHT — Flow body cascades, then "answers" the question ─────── */
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-chrome,
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-progress,
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-intro,
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-details,
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-q,
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-options li,
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-foot {
    animation: fr-vs-fade-up var(--vs-flow-dur) var(--vs-ease) both;
    transform-origin: center top;
  }
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-chrome    { animation-delay: var(--vs-flow-base); }
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-progress  { animation-delay: calc(var(--vs-flow-base) + 0.10s); }
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-intro     { animation-delay: calc(var(--vs-flow-base) + var(--vs-flow-step) * 1); }
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-details   { animation-delay: calc(var(--vs-flow-base) + var(--vs-flow-step) * 2); }
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-q         { animation-delay: calc(var(--vs-flow-base) + var(--vs-flow-step) * 3); }
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-options li:nth-child(1) { animation-delay: calc(var(--vs-flow-base) + var(--vs-flow-step) * 3 + 0.2s); }
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-options li:nth-child(2) { animation-delay: calc(var(--vs-flow-base) + var(--vs-flow-step) * 3 + 0.4s); }
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-options li:nth-child(n + 3) { animation-delay: calc(var(--vs-flow-base) + var(--vs-flow-step) * 3 + 0.6s); }
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-foot      { animation-delay: calc(var(--vs-flow-base) + var(--vs-flow-step) * 4 + 0.2s); }

  /* Selected option "lights up" mid-sequence: the row's mint background
     fades in, the radio fades out, and the green check pops. Whichever
     option carries .is-selected gets the treatment, regardless of its
     position in the list. */
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-options li.is-selected {
    animation: fr-vs-fade-up var(--vs-flow-dur) var(--vs-ease) both,
               fr-vs-flow-bg-on 0.5s var(--vs-ease) var(--vs-select-bg-delay) both;
  }
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-options li.is-selected .vs-flow-opt-radio {
    animation: fr-vs-flow-radio-off 0.25s var(--vs-ease) var(--vs-select-mark-delay) both;
  }
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-options li.is-selected .vs-flow-opt-check {
    animation: fr-vs-flow-check-on 0.45s var(--vs-ease) var(--vs-select-mark-delay) both;
    transform-origin: center center;
  }

  /* Submit pill "tap": brief scale-dip + brighter shadow, then settle. */
  .vs-grid.vs-grid--revealed .vs-card.good .vs-flow-submit {
    animation: fr-vs-flow-submit-click var(--vs-submit-dur) var(--vs-ease) var(--vs-submit-delay) both;
    transform-origin: center center;
    will-change: transform, box-shadow;
  }
}


/* ─────────────────────────────────────────────────────────────────────
 *  Task 5a — Italic accent shimmer (continuous gradient pulse)
 *
 *  Continuous time-driven loop. Every italic accent pulses on its own
 *  rhythm whether the user is scrolling or sitting still.
 *
 *  Per-context --shimmer-base sets the gradient base colour so the
 *  keyframe is shared. Three families:
 *     - blue (var(--secondary))  — italics on dark headings + hero
 *     - blue (var(--primary))    — italics on light .ph-headline
 *     - green (var(--green))     — .acc accents
 *
 *  Hero h1 stagger: .acc lags 500ms behind so "conversations →
 *  conversions" pulses in reading order each cycle.
 *
 *  WHY THE @supports GATE IS background-clip: text, NOT view():
 *  -----------------------------------------------------------
 *  An earlier version gated this on @supports (animation-timeline:
 *  view()), a leftover from when the sweep was scroll-driven. Now that
 *  the animation is time-driven, that gate is wrong — it leaves out
 *  Safari and any other browser that supports background-clip: text
 *  but not animation-timeline: view(). Replaced with the actually-
 *  needed feature gate: background-clip: text. (Without the gate, an
 *  old browser would render `color: transparent` against no painted
 *  background and the text would just disappear.)
 *
 *  Apparent text colour at the rest endpoints: with background-size
 *  320% the visible window is 31% of the gradient, narrower than the
 *  white plateau region (45-55%) plus shoulders (35-65%), so both rest
 *  positions land in the solid --shimmer-base region only. Text colour
 *  at rest is therefore identical to the base colour — no gradient is
 *  visible while the highlight is parked off-screen.
 * ─────────────────────────────────────────────────────────────────── */

/* ── Shimmer colours per context ──────────────────────────────────── */
/* Two custom properties per context:
 *   --shimmer-base     → the gradient base colour (token reference)
 *   --shimmer-glow-rgb → the same colour decomposed as an "r, g, b"
 *                        triple so it can be plugged into rgba() inside
 *                        the drop-shadow keyframes. We need the triple
 *                        because we animate the alpha channel and CSS
 *                        can't animate alpha alone of a custom prop
 *                        without @property declarations. */
.hero h1 i,
.hero h1 em,
.hero h1 .it,
.ph--dark .ph-headline i,
.ph--dark .ph-headline em,
.ph--dark .ph-headline .it,
.cta-strip h2 i,
.cta-strip h2 em,
.big-quote p i,
section.s.s-dark-1 .h-display i,
section.s.s-dark-2 .h-display i {
  --shimmer-base: var(--secondary);
  --shimmer-glow-rgb: 79, 182, 236;
}

.ph--light .ph-headline i,
.ph--light .ph-headline em,
.ph--light .ph-headline .it,
.about-feature h3 i,
.about-feature h3 em,
.legal-block h2 i,
.legal-block h2 em,
.flowella-rich .rich-prose h2 i,
.flowella-rich .rich-prose h2 em,
.flowella-rich .rich-prose h3 i,
.flowella-rich .rich-prose h3 em,
.feature-heading i,
.feature-heading em,
.h-display i {
  --shimmer-base: var(--primary);
  --shimmer-glow-rgb: 14, 110, 166;
}

.hero h1 .acc,
.ph-headline .acc {
  --shimmer-base: var(--green);
  --shimmer-glow-rgb: 37, 211, 102;
}

/* ── Shimmer animation ────────────────────────────────────────────── */
@media (prefers-reduced-motion: no-preference) {
  @supports ((background-clip: text) or (-webkit-background-clip: text)) {
    .hero h1 i,
    .hero h1 em,
    .hero h1 .it,
    .hero h1 .acc,
    .ph-headline i,
    .ph-headline em,
    .ph-headline .it,
    .ph-headline .acc,
    .about-feature h3 i,
    .about-feature h3 em,
    .legal-block h2 i,
    .legal-block h2 em,
    .big-quote p i,
    .cta-strip h2 i,
    .cta-strip h2 em,
    .flowella-rich .rich-prose h2 i,
    .flowella-rich .rich-prose h2 em,
    .flowella-rich .rich-prose h3 i,
    .flowella-rich .rich-prose h3 em,
    .feature-heading i,
    .feature-heading em,
    .h-display i {
      /* Peaked highlight: soft shoulders (35→45 / 55→65) ramp base
         to full white, with a 10% flat-white plateau (45-55) so the
         text holds at peak brightness for a beat rather than just
         brushing past. */
      background-image: linear-gradient(110deg,
        var(--shimmer-base, currentColor) 0%,
        var(--shimmer-base, currentColor) 35%,
        rgba(255, 255, 255, 1) 45%,
        rgba(255, 255, 255, 1) 55%,
        var(--shimmer-base, currentColor) 65%,
        var(--shimmer-base, currentColor) 100%);
      /* 320% (was 250%) means the visible window is 31% of the
         gradient — narrower than the highlight region — so both rest
         positions paint the text in pure --shimmer-base. No partial-
         gradient bleed at rest. */
      background-size: 320% 100%;
      background-position: 100% 50%;
      background-repeat: no-repeat;
      -webkit-background-clip: text;
              background-clip: text;
      -webkit-text-fill-color: transparent;
              color: transparent;
      /* 4s linear infinite loop. Single animation, no filter pair —
         the previous filter: drop-shadow() pair was failing to
         interpolate cleanly from 0-blur transparent to 22px coloured
         on at least one engine, leaving the whole shorthand inert. */
      animation: fr-shimmer-sweep 4s linear infinite both;
    }

    /* Hero / placeholder .acc lags 500ms behind the surrounding
       italics so "conversations → conversions" pulses in reading
       order each cycle, not as a chord. */
    .hero h1 .acc,
    .ph-headline .acc {
      animation-delay: 500ms;
    }
  }
}

/* Hold-then-sweep rhythm at 4s/cycle:
     0%   →  45% : rest off-screen-right (≈1.8s of solid base colour)
     45%  →  85% : sweep across (≈1.6s — visible "shine" passes)
     85%  → 100% : hold off-screen-left (≈0.6s of solid base colour)
   The loop snap from 0%-pos to 100%-pos at the cycle boundary is
   invisible because both endpoint positions paint the visible window
   in pure --shimmer-base. */
@keyframes fr-shimmer-sweep {
  0%, 45%   { background-position: 100% 50%; }
  85%       { background-position: 0% 50%; }
  100%      { background-position: 0% 50%; }
}


/* ─────────────────────────────────────────────────────────────────────
 *  Task 5b — Italic accent persistent glow (low-amplitude breathing)
 *
 *  text-shadow is the persistent ambient — it stays as a soft static
 *  glow even with prefers-reduced-motion: reduce. The breathing
 *  animation only runs when the user hasn't asked us to be still.
 *
 *  Applied ONLY to italic accents that act as heading accents. Body
 *  copy contexts (.tone-line, .ed-drop, etc.) are deliberately
 *  excluded — see the selector list in 5a.
 * ─────────────────────────────────────────────────────────────────── */

.hero h1 i,
.hero h1 em,
.hero h1 .it,
.ph-headline i,
.ph-headline em,
.ph-headline .it,
.about-feature h3 i,
.about-feature h3 em,
.legal-block h2 i,
.legal-block h2 em,
.big-quote p i,
.cta-strip h2 i,
.cta-strip h2 em,
.flowella-rich .rich-prose h2 i,
.flowella-rich .rich-prose h2 em,
.flowella-rich .rich-prose h3 i,
.flowella-rich .rich-prose h3 em,
.feature-heading i,
.feature-heading em,
.h-display i {
  text-shadow: var(--fr-glow-secondary-soft);
}

.hero h1 .acc,
.ph-headline .acc {
  text-shadow: var(--fr-glow-green-soft);
}

@media (prefers-reduced-motion: no-preference) {
  .hero h1 i,
  .hero h1 em,
  .hero h1 .it,
  .ph-headline i,
  .ph-headline em,
  .ph-headline .it,
  .about-feature h3 i,
  .about-feature h3 em,
  .legal-block h2 i,
  .legal-block h2 em,
  .big-quote p i,
  .cta-strip h2 i,
  .cta-strip h2 em,
  .flowella-rich .rich-prose h2 i,
  .flowella-rich .rich-prose h2 em,
  .flowella-rich .rich-prose h3 i,
  .flowella-rich .rich-prose h3 em,
  .feature-heading i,
  .feature-heading em,
  .h-display i {
    animation: fr-glow-breathe-blue 5.5s ease-in-out infinite alternate;
  }

  .hero h1 .acc,
  .ph-headline .acc {
    animation: fr-glow-breathe-green 5.5s ease-in-out infinite alternate;
  }
}

@keyframes fr-glow-breathe-blue {
  from { text-shadow: var(--fr-glow-secondary-soft); }
  to   { text-shadow: var(--fr-glow-secondary-strong); }
}

@keyframes fr-glow-breathe-green {
  from { text-shadow: var(--fr-glow-green-soft); }
  to   { text-shadow: var(--fr-glow-green-strong); }
}


/* ─────────────────────────────────────────────────────────────────────
 *  Task 6 — CTA glow (per-button colour, subtle)
 *
 *  Reworked: the glow now picks up the button's own brand colour so it
 *  reads as a soft halo rather than a contrasting accent.
 *    .btn-primary  → sky-blue halo  (var(--primary))
 *    .btn-wa       → WhatsApp green (var(--green))
 *  Other variants (.btn-ghost, .btn-secondary, text links) stay
 *  untouched.
 *
 *  Amplitudes are deliberately low — felt-not-seen. Hover lifts the
 *  shadow to a stronger fixed state (no breathing), and a 200ms sheen
 *  sweeps once across the button via a ::before. The sheen tints
 *  match the button colour family.
 *
 *  The button needs `position: relative; overflow: hidden` to host the
 *  sheen pseudo-element. Specificity is bumped to (0,2,1) via the
 *  `body` ancestor so the `.cta-strip .btn-primary` override in the
 *  cta-banner module (which loads AFTER motion.css) doesn't win.
 *
 *  Per-CTA colour comes from --fr-cta-glow-rgb, set per selector. The
 *  keyframes consume rgb(var(...) / opacity) so a single keyframe pair
 *  serves every variant.
 * ─────────────────────────────────────────────────────────────────── */
body .btn.btn-primary,
body .btn.btn-wa {
  position: relative;
  overflow: hidden;
}

body .btn.btn-primary {
  --fr-cta-glow-rgb: 14, 110, 166;
  box-shadow: 0 6px 18px -8px rgba(14, 110, 166, .35);
}
body .btn.btn-primary:hover {
  box-shadow: 0 10px 26px -8px rgba(14, 110, 166, .55);
}

body .btn.btn-wa {
  --fr-cta-glow-rgb: 37, 211, 102;
  box-shadow: 0 6px 18px -8px rgba(37, 211, 102, .35);
}
body .btn.btn-wa:hover {
  box-shadow: 0 10px 26px -8px rgba(37, 211, 102, .55);
}

body .btn.btn-primary::before,
body .btn.btn-wa::before {
  content: "";
  position: absolute;
  inset: 0;
  /* Sheen tint stays on-brand. White at .25 across all variants reads
     as a soft brushed-metal pass; tinting it would over-saturate. */
  background: linear-gradient(110deg,
    transparent 0%,
    transparent 38%,
    rgba(255, 255, 255, .28) 50%,
    transparent 62%,
    transparent 100%);
  background-size: 250% 100%;
  background-position: 100% 50%;
  background-repeat: no-repeat;
  pointer-events: none;
  opacity: 0;
}

@media (prefers-reduced-motion: no-preference) {
  body .btn.btn-primary,
  body .btn.btn-wa {
    animation: fr-cta-glow 6s ease-in-out infinite alternate;
  }

  /* Suspend the breathing while hovered so the stronger static hover
     shadow reads as a single, deliberate state. */
  body .btn.btn-primary:hover,
  body .btn.btn-wa:hover {
    animation: none;
  }

  body .btn.btn-primary:hover::before,
  body .btn.btn-wa:hover::before {
    animation: fr-cta-sheen 220ms linear forwards;
  }
}

@keyframes fr-cta-glow {
  from { box-shadow: 0 6px 18px -8px rgba(var(--fr-cta-glow-rgb, 14, 110, 166), .25); }
  to   { box-shadow: 0 8px 22px -8px rgba(var(--fr-cta-glow-rgb, 14, 110, 166), .42); }
}

@keyframes fr-cta-sheen {
  0%   { opacity: 1; background-position: 100% 50%; }
  100% { opacity: 1; background-position: 0% 50%; }
}


/* ─────────────────────────────────────────────────────────────────────
 *  Task 7 — Stats count-up on entry (flowella-stats)
 *
 *  CSS-only count-up animation. Used by the stats trust strip whenever
 *  a stat value is shaped like "N/M" (e.g. "5/5 on the HubSpot
 *  Marketplace") — the leading number animates from 0 → N as the
 *  stat enters the viewport, the "/M" suffix stays static.
 *
 *  How it works:
 *    1. @property registers --fr-count as an interpolable <integer>,
 *       so CSS animations on it tick through whole numbers (0,1,2…)
 *       rather than jumping endpoint-to-endpoint.
 *    2. The keyframe runs --fr-count: 0 → var(--count-to). The target
 *       per stat is set inline on the .stat-count element by the HubL
 *       template (e.g. `style="--count-to: 5"`).
 *    3. The pseudo-element ::before renders the current value via
 *       counter-reset + content: counter(c). Each frame that mutates
 *       --fr-count re-resolves counter-reset, re-paints the counter.
 *    4. animation-timeline: view() with `animation-range: entry 0%
 *       entry 80%` drives the count via scroll position rather than
 *       wall-clock time — the digits roll up as the stat scrolls in,
 *       not on a fixed-duration delay after page load. Matches the
 *       scroll-driven entry pattern used by tasks 1 and 4 above.
 *
 *  Fallback chain (in line with the file's "final-state-first" rule):
 *    - prefers-reduced-motion: reduce → static `attr(data-final)`
 *      value rendered immediately, no count.
 *    - browser without animation-timeline: view() → ditto.
 *    - browser without @property → counter-reset receives the
 *      unregistered custom property as a string, fails the cast to
 *      <integer>, and the counter falls back to 0. To avoid showing
 *      a stuck "0", the @supports gate ensures we only swap the ::before
 *      content from attr(data-final) to counter(c) when view() is
 *      available — which on every shipping engine implies @property
 *      support as well.
 *
 *  Layout-shift mitigation: `font-variant-numeric: tabular-nums` keeps
 *  every digit the same width (so 0 → 5 doesn't tweak the layout).
 * ─────────────────────────────────────────────────────────────────── */
@property --fr-count {
  syntax: "<integer>";
  initial-value: 0;
  inherits: false;
}

.stat-count {
  display: inline-block;
  font-variant-numeric: tabular-nums;
  min-width: 1ch;
}
.stat-count::before {
  content: attr(data-final);
}

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .stat-count::before {
      content: counter(fr-c);
      counter-reset: fr-c var(--fr-count);
    }
    .stat-count {
      animation: fr-stat-count linear both;
      animation-duration: 1ms;
      animation-timeline: view();
      animation-range: entry 0% entry 80%;
      will-change: contents;
    }
  }
}

@keyframes fr-stat-count {
  from { --fr-count: 0; }
  to   { --fr-count: var(--count-to, 0); }
}


/* ─────────────────────────────────────────────────────────────────────
 *  Task 8 — WhatsApp template card stagger (use-case detail .tpl-grid)
 *
 *  Triggered by hubdb-use-case-detail.hubl.html adding .tpl-grid--revealed
 *  via IntersectionObserver (scroll-driven view() was unreliable: per-
 *  fragment timelines finished instantly; @supports (animation-timeline:
 *  --name) failed in Chrome while IO bailed out on view-timeline-axis).
 *
 *  Layers when .tpl-grid--revealed:
 *    1. Cards stagger left → middle → right (animation-delay).
 *    2. Bubble parts pop in (header → body → meta → buttons).
 *    3. Card 2 Flow rises; card 3 stack cadence; card 1 image zoom.
 * ─────────────────────────────────────────────────────────────────── */

@keyframes fr-wa-bubble-pop {
  from { opacity: 0; transform: scale(0.96); }
  to   { opacity: 1; transform: scale(1); }
}

@keyframes fr-wa-tick-read {
  from { color: #8696a0; }
  to   { color: #53bdeb; }
}

@keyframes fr-wa-flow-in {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes fr-wa-img-zoom {
  from { transform: scale(1); }
  to   { transform: scale(1.02); }
}

@media (prefers-reduced-motion: no-preference) {
  /* Tuning tokens — full sequence ~5s so cards still building as you scroll in */
  .tpl-grid.tpl-grid--revealed {
    --tpl-dur-card: 1.15s;
    --tpl-dur-part: 1s;
    --tpl-dur-flow: 1.15s;
    --tpl-dur-tick: 0.7s;
    --tpl-gap-card: 0.55s;
    --tpl-gap-part: 0.32s;
    --tpl-gap-stack-bubble: 0.72s;
    --tpl-lead-in: 0.65s;
    --tpl-lead-flow: 0.58s;
  }

  .tpl-grid.tpl-grid--revealed .tpl-card {
    animation: fr-reveal-up var(--tpl-dur-card) var(--fr-ease-entry) both;
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(2) {
    animation-delay: var(--tpl-gap-card);
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(n + 3) {
    animation-delay: calc(var(--tpl-gap-card) * 2);
  }

  .tpl-grid.tpl-grid--revealed .tpl-card .wa-msg > .wa-header,
  .tpl-grid.tpl-grid--revealed .tpl-card .wa-msg > .sender,
  .tpl-grid.tpl-grid--revealed .tpl-card .wa-msg .tpl-body-slot,
  .tpl-grid.tpl-grid--revealed .tpl-card .wa-msg .wa-msg-footer,
  .tpl-grid.tpl-grid--revealed .tpl-card .wa-msg .meta,
  .tpl-grid.tpl-grid--revealed .tpl-card .wa-msg .url-buttons,
  .tpl-grid.tpl-grid--revealed .tpl-card .wa-msg .reply-buttons,
  .tpl-grid.tpl-grid--revealed .tpl-card .wa-flow-preview,
  .tpl-grid.tpl-grid--revealed .tpl-card .wa-stack-item {
    animation: fr-wa-bubble-pop var(--tpl-dur-part) var(--fr-ease-entry) both;
    transform-origin: left bottom;
  }

  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(1) .wa-msg > .wa-header {
    animation-delay: var(--tpl-lead-in);
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(1) .wa-msg > .sender {
    animation-delay: calc(var(--tpl-lead-in) + var(--tpl-gap-part));
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(1) .wa-msg .tpl-body-slot {
    animation-delay: calc(var(--tpl-lead-in) + var(--tpl-gap-part) * 2);
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(1) .wa-msg .meta {
    animation-delay: calc(var(--tpl-lead-in) + var(--tpl-gap-part) * 3);
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(1) .wa-msg .url-buttons {
    animation-delay: calc(var(--tpl-lead-in) + var(--tpl-gap-part) * 4);
  }

  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(2) .wa-flow-preview {
    animation: fr-wa-flow-in var(--tpl-dur-flow) var(--fr-ease-entry)
      calc(var(--tpl-gap-card) + var(--tpl-lead-flow)) both;
    transform-origin: center bottom;
  }

  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(n + 3) .wa-stack-item {
    animation: fr-reveal-up var(--tpl-dur-part) var(--fr-ease-entry) both;
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(n + 3) .wa-stack-item ~ .wa-stack-item {
    animation-delay: var(--tpl-gap-stack-bubble);
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(3) .wa-stack-item:first-of-type .wa-msg > .wa-header {
    animation-delay: calc(var(--tpl-gap-card) * 2 + var(--tpl-lead-in));
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(3) .wa-stack-item:first-of-type .wa-msg .tpl-body-slot {
    animation-delay: calc(var(--tpl-gap-card) * 2 + var(--tpl-lead-in) + var(--tpl-gap-part) * 2);
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(3) .wa-stack-item:first-of-type .wa-msg .meta {
    animation-delay: calc(var(--tpl-gap-card) * 2 + var(--tpl-lead-in) + var(--tpl-gap-part) * 3);
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(3) .wa-stack-item:first-of-type .wa-msg .url-buttons {
    animation-delay: calc(var(--tpl-gap-card) * 2 + var(--tpl-lead-in) + var(--tpl-gap-part) * 4);
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(3) .wa-stack-item ~ .wa-stack-item .wa-msg > .wa-header {
    animation-delay: calc(var(--tpl-gap-card) * 2 + var(--tpl-gap-stack-bubble) + var(--tpl-lead-in));
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(3) .wa-stack-item ~ .wa-stack-item .wa-msg .tpl-body-slot {
    animation-delay: calc(var(--tpl-gap-card) * 2 + var(--tpl-gap-stack-bubble) + var(--tpl-lead-in) + var(--tpl-gap-part) * 2);
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(3) .wa-stack-item ~ .wa-stack-item .wa-msg .meta {
    animation-delay: calc(var(--tpl-gap-card) * 2 + var(--tpl-gap-stack-bubble) + var(--tpl-lead-in) + var(--tpl-gap-part) * 3);
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(3) .wa-stack-item ~ .wa-stack-item .wa-msg .url-buttons {
    animation-delay: calc(var(--tpl-gap-card) * 2 + var(--tpl-gap-stack-bubble) + var(--tpl-lead-in) + var(--tpl-gap-part) * 4);
  }

  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(1) .wa-header--image img {
    animation: fr-wa-img-zoom 1.4s var(--fr-ease-entry) 0.72s both;
    transform-origin: center center;
  }

  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(1) .wa-msg .meta .tick {
    color: #8696a0;
    animation: fr-wa-tick-read var(--tpl-dur-tick) linear both;
    animation-delay: calc(var(--tpl-lead-in) + var(--tpl-gap-part) * 3 + 0.18s);
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(1) .wa-msg .meta .tick--second {
    animation: fr-wa-bubble-pop 0.55s var(--fr-ease-entry) both,
               fr-wa-tick-read var(--tpl-dur-tick) linear both;
    animation-delay: calc(var(--tpl-lead-in) + var(--tpl-gap-part) * 3 + 0.38s),
                     calc(var(--tpl-lead-in) + var(--tpl-gap-part) * 3 + 0.55s);
    transform-origin: left bottom;
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(3) .wa-msg .meta .tick {
    animation-delay: calc(var(--tpl-gap-card) * 2 + var(--tpl-lead-in) + var(--tpl-gap-part) * 3 + 0.18s);
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(3) .wa-stack-item ~ .wa-stack-item .wa-msg .meta .tick {
    animation-delay: calc(var(--tpl-gap-card) * 2 + var(--tpl-gap-stack-bubble) + var(--tpl-lead-in) + var(--tpl-gap-part) * 3 + 0.18s);
  }
  .tpl-grid.tpl-grid--revealed .tpl-card:nth-child(3) .wa-stack-item ~ .wa-stack-item .wa-msg .meta .tick--second {
    animation-delay: calc(var(--tpl-gap-card) * 2 + var(--tpl-gap-stack-bubble) + var(--tpl-lead-in) + var(--tpl-gap-part) * 3 + 0.38s),
                     calc(var(--tpl-gap-card) * 2 + var(--tpl-gap-stack-bubble) + var(--tpl-lead-in) + var(--tpl-gap-part) * 3 + 0.55s);
  }
}
