/* =========================================================================
   RequestMe — Motion layer
   Loaded last, on the five public pages only. Purely additive: nothing here
   is required for the site to work, it makes the working site feel premium.
   Every animation is transform/opacity only (GPU-friendly, no layout
   thrash). The blanket prefers-reduced-motion override in base.css already
   forces all durations to ~0 — the JS side additionally short-circuits
   itself under that preference (see motion.js) since rAF-driven counters
   aren't covered by a CSS duration.
   ========================================================================= */

/* ---- Smooth cross-document page transitions (progressive enhancement) --- */
@view-transition { navigation: auto; }
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) { animation: rmViewOut 180ms var(--ease-standard) both; }
  ::view-transition-new(root) { animation: rmViewIn 220ms var(--ease-standard) both; }
}
@keyframes rmViewOut { to { opacity: 0; } }
@keyframes rmViewIn { from { opacity: 0; transform: translateY(6px); } }

/* Entry fade for the very first paint / browsers without view-transitions */
body { animation: rmPageIn 520ms var(--ease-standard) both; }
@keyframes rmPageIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* ---- Reveal-on-scroll variants (base opacity/transform lives in marketing.css) */
[data-reveal] { transition-duration: 640ms; }
[data-reveal="left"] { transform: translateX(-28px); }
[data-reveal="left"].is-visible { transform: none; }
[data-reveal="right"] { transform: translateX(28px); }
[data-reveal="right"].is-visible { transform: none; }
[data-reveal="scale"] { transform: scale(0.94); }
[data-reveal="scale"].is-visible { transform: scale(1); }
[data-reveal="fade"] { transform: none; }

/* ---- Card lift & depth --------------------------------------------------- */
.card, .plan-card, .testimonial-card, .doc-mock, .browser-frame, .tg-card, .list-manager-row {
  transition: transform 260ms var(--ease-standard), box-shadow 260ms var(--ease-standard), border-color 260ms var(--ease-standard);
}
.card:hover, .testimonial-card:hover, .plan-card:hover, .doc-mock:hover, .browser-frame:hover {
  transform: translateY(-4px);
  box-shadow: 0 20px 36px -18px rgba(11, 14, 20, 0.22);
}
.plan-card.is-featured:hover { transform: translateY(-6px); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .card:hover,
  :root:not([data-theme="light"]) .testimonial-card:hover,
  :root:not([data-theme="light"]) .plan-card:hover,
  :root:not([data-theme="light"]) .doc-mock:hover,
  :root:not([data-theme="light"]) .browser-frame:hover {
    box-shadow: 0 20px 40px -16px rgba(0, 0, 0, 0.55);
  }
}
:root[data-theme="dark"] .card:hover,
:root[data-theme="dark"] .testimonial-card:hover,
:root[data-theme="dark"] .plan-card:hover,
:root[data-theme="dark"] .doc-mock:hover,
:root[data-theme="dark"] .browser-frame:hover {
  box-shadow: 0 20px 40px -16px rgba(0, 0, 0, 0.55);
}
.doc-mock { transition: transform 300ms var(--ease-standard), box-shadow 300ms var(--ease-standard); }
.doc-mock:hover { box-shadow: 0 24px 48px -20px color-mix(in srgb, var(--action-primary) 32%, rgba(11,14,20,.2)); }

/* ---- Buttons: hover lift, glow, ripple ------------------------------------ */
.btn { position: relative; overflow: hidden; transform: translateZ(0); }
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn-primary:hover { box-shadow: 0 10px 24px -8px color-mix(in srgb, var(--action-primary) 55%, transparent); }
.btn-telegram:hover { box-shadow: 0 10px 24px -8px rgba(34, 158, 217, 0.5); }
.btn .rm-ripple {
  position: absolute; border-radius: 50%; pointer-events: none;
  background: rgba(255, 255, 255, 0.45);
  transform: scale(0); opacity: 0.9;
  animation: rmRipple 620ms ease-out forwards;
}
.btn-ghost .rm-ripple, .btn-quiet .rm-ripple, .btn-ghost-on-dark .rm-ripple {
  background: color-mix(in srgb, var(--action-primary) 30%, transparent);
}
@keyframes rmRipple { to { transform: scale(2.6); opacity: 0; } }

/* ---- Chips & nav links: elegant hover ------------------------------------- */
.chip { transition: transform 180ms var(--ease-standard), border-color 180ms, color 180ms, background-color 180ms; }
.chip:hover:not([aria-selected="true"]) { border-color: var(--action-primary); color: var(--text-primary); transform: translateY(-1px); }

.nav-links a:not([aria-current="page"]) {
  background-image: linear-gradient(currentColor, currentColor);
  background-position: 0 100%; background-repeat: no-repeat; background-size: 0% 2px;
  transition: background-size 260ms var(--ease-standard), color 200ms;
}
.nav-links a:not([aria-current="page"]):hover { background-size: 100% 2px; }

.footer-col a { transition: color 180ms, padding-left 180ms var(--ease-standard); }
.footer-col a:hover { padding-left: 4px; }

/* ---- Sticky nav: hide on scroll down, reveal on scroll up ----------------- */
.site-header { transition: transform 320ms var(--ease-standard), border-color 200ms, background-color 200ms; }
.site-header.is-hidden { transform: translateY(-100%); }

/* ---- Dashboard tab-panel crossfade ----------------------------------------- */
.tab-panel { transition: opacity 220ms var(--ease-standard), transform 220ms var(--ease-standard); }
.tab-panel.is-leaving { opacity: 0; transform: translateY(6px); }
.tab-panel.is-entering { opacity: 0; transform: translateY(6px); }

/* ---- Accordion content fade (pairs with max-height in components.css) ----- */
.accordion-panel-inner { opacity: 0; transition: opacity 260ms var(--ease-standard) 40ms; }
.accordion-item.is-open .accordion-panel-inner { opacity: 1; }

/* Rendered accordion rows fade/stagger in on insertion — CSS-only, so it
   works the instant render.js writes them, no scroll-observer needed. */
@keyframes rmFadeUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.accordion-item { animation: rmFadeUp 460ms var(--ease-standard) both; }
.accordion-item:nth-child(1) { animation-delay: 0ms; }
.accordion-item:nth-child(2) { animation-delay: 50ms; }
.accordion-item:nth-child(3) { animation-delay: 100ms; }
.accordion-item:nth-child(4) { animation-delay: 150ms; }
.accordion-item:nth-child(5) { animation-delay: 200ms; }
.accordion-item:nth-child(6) { animation-delay: 250ms; }
.accordion-item:nth-child(n+7) { animation-delay: 300ms; }

/* ---- How it works: steps + the product stage beside them -------------------
   The old vertical connecting line is gone: the active step now carries a
   solid accent rail on its left edge, and two competing vertical lines in the
   same 40px of gutter read as clutter rather than structure. */
.step-num { transform: scale(0.5); opacity: 0; transition: transform 480ms var(--ease-standard) 80ms, opacity 480ms var(--ease-standard) 80ms; }
.step-item.is-visible .step-num { transform: scale(1); opacity: 1; }

/* Rows enter one after another whenever their panel becomes the active one,
   so each state assembles itself rather than appearing all at once. Driven by
   nth-child delays instead of JS, because the row count is fixed per panel. */
.hiw-panel.is-active .hiw-field,
.hiw-panel.is-active .hiw-sig,
.hiw-panel.is-active .hiw-attach,
.hiw-panel.is-active .hiw-note {
  animation: hiwRowIn 420ms var(--ease-standard) both;
}
.hiw-panel.is-active > *:nth-child(2) { animation-delay: 90ms; }
.hiw-panel.is-active > *:nth-child(3) { animation-delay: 170ms; }
.hiw-panel.is-active > *:nth-child(4) { animation-delay: 250ms; }
.hiw-panel.is-active > *:nth-child(5) { animation-delay: 330ms; }
@keyframes hiwRowIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* The caret blinks only while the "Raise" panel is the one on screen. */
.hiw-panel.is-active .hiw-caret { animation: hiwBlink 1.05s steps(2, jump-none) infinite; }
@keyframes hiwBlink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

/* The payoff: after the approve panel has been up for a moment the buttons
   give way to the confirmation. Pure CSS on animation-delay — no timer to
   keep in sync with the panel rotation, and it restarts every time the panel
   is shown because the animation is re-applied with the class. */
.hiw-panel[data-panel="2"].is-active .hiw-actions { animation: hiwActionsOut 360ms var(--ease-standard) 1500ms both; }
.hiw-panel[data-panel="2"].is-active .hiw-approved { animation: hiwApprovedIn 420ms var(--ease-standard) 1700ms both; }
@keyframes hiwActionsOut { to { opacity: 0; transform: translateY(-6px); } }
@keyframes hiwApprovedIn { from { opacity: 0; transform: translateY(8px) scale(0.97); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  .hiw-panel.is-active .hiw-field, .hiw-panel.is-active .hiw-sig,
  .hiw-panel.is-active .hiw-attach, .hiw-panel.is-active .hiw-note,
  .hiw-panel.is-active .hiw-caret { animation: none !important; }
  /* Without the timed hand-off, show the outcome rather than a button that
     never resolves. */
  .hiw-panel[data-panel="2"].is-active .hiw-actions { animation: none !important; display: none; }
  .hiw-panel[data-panel="2"].is-active .hiw-approved { animation: none !important; opacity: 1; }
}

/* ---- Pricing amount pulse on billing toggle -------------------------------- */
@keyframes rmPulse { 0% { transform: scale(1); } 35% { transform: scale(1.1); color: var(--action-primary); } 100% { transform: scale(1); } }
.plan-price .amount.is-pulsing { animation: rmPulse 480ms var(--ease-standard); display: inline-block; }
.switch { transition: background-color 220ms var(--ease-standard); }
.switch::after { transition: transform 220ms var(--ease-standard); }

/* ---- Floating ambient background orbs -------------------------------------- */
.bg-orb { position: absolute; border-radius: 50%; filter: blur(60px); pointer-events: none; z-index: 0; opacity: 0.32; }
.bg-orb--a { width: 420px; height: 420px; background: radial-gradient(circle, var(--action-primary), transparent 70%); animation: rmFloatA 24s ease-in-out infinite; }
.bg-orb--b { width: 320px; height: 320px; background: radial-gradient(circle, var(--brass-600), transparent 72%); opacity: 0.18; animation: rmFloatB 30s ease-in-out infinite; animation-delay: -8s; }
@keyframes rmFloatA {
  0%, 100% { transform: translate(0, 0) scale(1); }
  33% { transform: translate(26px, -22px) scale(1.06); }
  66% { transform: translate(-18px, 20px) scale(0.96); }
}
@keyframes rmFloatB {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50% { transform: translate(-24px, 18px) scale(1.08); }
}
.hero, .page-hero, section.section-inverse { isolation: isolate; overflow: clip; }
.hero-glow { animation: rmGlowPulse 9s ease-in-out infinite; }
@keyframes rmGlowPulse { 0%, 100% { opacity: 0.7; transform: scale(1); } 50% { opacity: 1; transform: scale(1.07); } }
section.section-inverse .bg-orb, section.final-cta .bg-orb { opacity: 0.22; }
/* Orbs sit at z-index:0 behind every section's actual content container */
.wrap-wide, .wrap-narrow, .wrap { position: relative; z-index: 1; }

/* ---- Contact form success state ------------------------------------------- */
#contact-success:not([hidden]) { animation: rmFadeUp 480ms var(--ease-standard) both; }

/* ---- Loading skeleton (shown until JS renders real content) ---------------- */
.skeleton-card {
  border-radius: var(--radius-m); border: 1px solid var(--border-hairline);
  background: var(--bg-fill); min-height: 148px; position: relative; overflow: hidden;
}
.skeleton-card::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--bg-hover) 70%, white 15%), transparent);
  transform: translateX(-100%);
  animation: rmShimmer 1.5s ease-in-out infinite;
}
:root[data-theme="dark"] .skeleton-card::after { background: linear-gradient(90deg, transparent, rgba(255,255,255,.06), transparent); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .skeleton-card::after { background: linear-gradient(90deg, transparent, rgba(255,255,255,.06), transparent); }
}
@keyframes rmShimmer { to { transform: translateX(100%); } }

/* ---- Homepage: live approval demo (Mobile + Telegram section) -----------
   Shows the one thing the whole product does: press Approve, the request
   moves. Plays once automatically when it scrolls into view; the button
   stays real, so clicking it again replays the same sequence. */
#demo-status-pill { transition: background-color 220ms var(--ease-standard), color 220ms var(--ease-standard); }

#demo-btn-approve { position: relative; transition: transform 140ms var(--ease-standard); }
#demo-phone.is-approving #demo-btn-approve { transform: scale(0.96); pointer-events: none; }
#demo-phone.is-approving #demo-btn-return { opacity: 0.5; pointer-events: none; transition: opacity 140ms var(--ease-standard); }
#demo-btn-approve .demo-spin { display: none; width: 16px; height: 16px; animation: demoSpin 800ms linear infinite; }
#demo-phone.is-approving #demo-btn-approve .demo-label { display: none; }
#demo-phone.is-approving #demo-btn-approve .demo-spin { display: inline-flex; }

.demo-confirm {
  display: none; align-items: center; justify-content: center; gap: var(--space-2);
  height: 44px; border-radius: var(--radius-s);
  background: var(--status-approved-bg); color: var(--status-approved-fg);
  font-weight: 600; font-size: var(--text-body-s);
  opacity: 0; transform: translateY(4px);
  transition: opacity 280ms var(--ease-standard), transform 280ms var(--ease-standard);
}
.demo-confirm .icon { width: 16px; height: 16px; }
/* Both action rows give way to the confirmation — leaving "Reject with a
   reason" sitting under an approved request would read as still actionable. */
#demo-phone.is-approved #demo-actions,
#demo-phone.is-approved #demo-reject-row { display: none; }
#demo-phone.is-approved .demo-confirm { display: flex; opacity: 1; transform: translateY(0); }
#demo-phone.is-approved .demo-confirm .icon { animation: demoCheckIn 380ms var(--ease-standard) both 60ms; }
/* One settle pulse on approval, not a loop — a loop here would read as
   "broken record," not "alive." */
#demo-phone.is-approved { animation: demoSettle 900ms var(--ease-standard) both; }

@keyframes demoSpin { to { transform: rotate(360deg); } }
@keyframes demoCheckIn { from { transform: scale(0.5); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@keyframes demoSettle {
  0% { box-shadow: var(--shadow-elevated), 0 0 0 0 color-mix(in srgb, var(--status-approved-fg) 45%, transparent); }
  40% { box-shadow: var(--shadow-elevated), 0 0 0 8px color-mix(in srgb, var(--status-approved-fg) 0%, transparent); }
  100% { box-shadow: var(--shadow-elevated), 0 0 0 0 transparent; }
}

@media (prefers-reduced-motion: reduce) {
  #demo-phone, #demo-btn-approve, .demo-confirm { transition: none !important; animation: none !important; }
  .demo-spin { animation-duration: 2000ms; }
}

/* ---- Respect the user: JS also checks this, but belt-and-braces here too -- */
@media (prefers-reduced-motion: reduce) {
  .bg-orb, .hero-glow { animation: none !important; }
  body { animation: none !important; }
  /* The stage does not rotate under reduced motion (the JS timer never
     starts), so make the panel swap instant rather than a slow fade for
     anyone stepping through it by click. */
  .hiw-panel { transition: none !important; }
}

/* =========================================================================
   Next-gen hero: animated mesh gradient + film grain
   Four soft, heavily-blurred brand-blue blobs (plus a touch of brass).
   Each blob runs TWO independent infinite loops at once — a "flow"
   (translate + non-uniform scale + a few degrees of rotate) and a
   "breathe" (a gentle opacity dip) — every one of them on its own prime
   -ish duration between 21s and 29s. Only `transform` and `opacity` are
   ever animated: both are compositor-only properties, so the browser
   rasterizes each blurred blob exactly once and every frame after that is
   pure GPU compositing — no repaint, no layout, 60fps regardless of how
   many blobs are on screen.

   Why eight loops with mismatched, mutually-coprime-ish periods (21, 22,
   23, 24, 27, 28, 29s) instead of one shared cycle: any single loop is
   trivially seeable as "the same thing again" once you've watched it
   twice. Eight independent loops layered on four overlapping blobs only
   realign to their exact combined starting state after the LCM of all
   eight periods — on the order of days, not seconds — so nothing about
   the composite scene ever visibly resets, even though every individual
   layer underneath is a small seamless loop (0% and 100% match exactly
   on all of them, so no layer itself ever jumps).

   The Request Preview mockup sits above this with its own always-on
   ambient glow, so it reads as the lit, in-focus object against a calm,
   slow-moving atmosphere — never the other way around.
   ========================================================================= */
.mesh-gradient { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.mesh-blob { position: absolute; border-radius: 50%; filter: blur(90px); will-change: transform, opacity; }
.mesh-blob--1 {
  width: 620px; height: 620px; top: -18%; left: -6%;
  background: radial-gradient(circle, var(--brand-500), transparent 68%); opacity: .22;
  animation: meshFlowA 23s ease-in-out infinite, meshBreatheA 27s ease-in-out infinite;
}
.mesh-blob--2 {
  width: 520px; height: 520px; top: 2%; right: -10%;
  background: radial-gradient(circle, var(--brand-800), transparent 70%); opacity: .20;
  animation: meshFlowB 29s ease-in-out infinite -9s, meshBreatheB 22s ease-in-out infinite -4s;
}
.mesh-blob--3 {
  width: 460px; height: 460px; bottom: -22%; left: 26%;
  background: radial-gradient(circle, var(--brass-600), transparent 72%); opacity: .10;
  animation: meshFlowC 21s ease-in-out infinite -15s, meshBreatheC 28s ease-in-out infinite -11s;
}
.mesh-blob--4 {
  width: 380px; height: 380px; bottom: -2%; right: 14%;
  background: radial-gradient(circle, var(--brand-300), transparent 70%); opacity: .16;
  animation: meshFlowD 27s ease-in-out infinite -20s, meshBreatheD 24s ease-in-out infinite -7s;
}

@keyframes meshFlowA {
  0%, 100% { transform: translate(0%, 0%) scale(1, 1) rotate(0deg); }
  21%      { transform: translate(4%, 2%) scale(1.12, 0.9) rotate(7deg); }
  48%      { transform: translate(1%, 6%) scale(0.94, 1.1) rotate(-5deg); }
  76%      { transform: translate(-3%, 3%) scale(1.05, 0.97) rotate(4deg); }
}
@keyframes meshFlowB {
  0%, 100% { transform: translate(0%, 0%) scale(1, 1) rotate(0deg); }
  33%      { transform: translate(-4%, 2%) scale(0.9, 1.15) rotate(-7deg); }
  61%      { transform: translate(-1%, -4%) scale(1.08, 0.92) rotate(5deg); }
  87%      { transform: translate(3%, 1%) scale(0.98, 1.04) rotate(-3deg); }
}
@keyframes meshFlowC {
  0%, 100% { transform: translate(0%, 0%) scale(1, 1) rotate(0deg); }
  37%      { transform: translate(3%, -3%) scale(1.1, 0.95) rotate(6deg); }
  68%      { transform: translate(-2%, 4%) scale(0.92, 1.12) rotate(-6deg); }
}
@keyframes meshFlowD {
  0%, 100% { transform: translate(0%, 0%) scale(1, 1) rotate(0deg); }
  44%      { transform: translate(-3%, -4%) scale(1.06, 0.94) rotate(-8deg); }
  79%      { transform: translate(2%, 3%) scale(0.95, 1.08) rotate(5deg); }
}

@keyframes meshBreatheA { 0%, 100% { opacity: .22; } 42% { opacity: .18; } }
@keyframes meshBreatheB { 0%, 100% { opacity: .20; } 58% { opacity: .16; } }
@keyframes meshBreatheC { 0%, 100% { opacity: .10; } 38% { opacity: .08; } }
@keyframes meshBreatheD { 0%, 100% { opacity: .16; } 63% { opacity: .13; } }

/* A whisper of film grain keeps the gradient from reading as flat/digital. */
.mesh-grain {
  position: absolute; inset: 0; z-index: 0; pointer-events: none; opacity: .035; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 160px 160px;
}
:root[data-theme="dark"] .mesh-grain { opacity: .05; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .mesh-grain { opacity: .05; } }

@media (prefers-reduced-motion: reduce) { .mesh-blob { animation: none !important; } }

/* Eyebrow reads as a small glass badge when it sits over the mesh. */
.hero-copy .eyebrow {
  display: inline-flex; padding: 6px 14px; border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--bg-page) 55%, transparent);
  border: 1px solid var(--border-hairline);
  backdrop-filter: blur(8px);
}

/* The Request Preview mockup is the focal point of the whole hero, so it is
   the only element carrying brand-blue light: a faint blue rim, a close
   contact shadow to seat it on the page, and a wide blue cast beneath. The
   "Without RequestMe" pile beside it deliberately gets none of this — the
   difference in elevation is what makes one side read as the answer. */
.hero-visual .doc-mock {
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--action-primary) 12%, transparent),
    0 2px 6px rgba(11, 14, 20, 0.05),
    0 12px 24px -12px rgba(11, 14, 20, 0.10),
    0 36px 72px -28px color-mix(in srgb, var(--action-primary) 42%, transparent);
}
/* Bare :root:not([data-theme="light"]) would match a light-mode page too — the
   default state stamps no attribute at all — so the system-preference variant
   has to stay inside the media query. Same pattern as the rest of this file. */
:root[data-theme="dark"] .hero-visual .doc-mock {
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--action-primary) 22%, transparent),
    0 2px 6px rgba(0, 0, 0, 0.35),
    0 36px 72px -28px color-mix(in srgb, var(--action-primary) 50%, transparent);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .hero-visual .doc-mock {
    box-shadow:
      0 0 0 1px color-mix(in srgb, var(--action-primary) 22%, transparent),
      0 2px 6px rgba(0, 0, 0, 0.35),
      0 36px 72px -28px color-mix(in srgb, var(--action-primary) 50%, transparent);
  }
}

/* =========================================================================
   Hero stage motion — "from scattered requests to clear approvals"
   Three separate ideas, each kept deliberately quiet so the product preview
   is still the loudest thing on the screen:

     1. The "Today" pile drifts. Each card gets its own duration (13/17/11/19s
        — no two share a factor) and its own resting rotation, so the pile
        never visibly re-synchronises into a pattern. Transform only, so every
        frame is GPU compositing with no repaint.
     2. The funnel flows. Dashes travel along the four curves toward the
        product, which is the one place a stroke-dashoffset animation earns
        its keep: it is the only way to express movement *along a path*, and
        it is confined to five hairlines inside a ~128px column, so the
        repaint area is negligible.
     3. One pulse walks the rail. The wrapper translates a full 100% of its
        own width — and because that wrapper is pinned to the exact centres of
        the first and last steps, the dot lands precisely on Create and Track
        without any measurement in JS.
   ========================================================================= */
/* No will-change here on purpose: an infinite transform animation already
   keeps these on their own compositor layer for their whole lifetime, so the
   hint would only add memory pressure on low-end phones without changing
   anything about how they render. */
.chaos-item--chat  { animation: chaosDriftA 13s ease-in-out infinite; }
.chaos-item--form  { animation: chaosDriftB 17s ease-in-out infinite -3s; }
.chaos-item--docs  { animation: chaosDriftC 11s ease-in-out infinite -7s; }
.chaos-item--who   { animation: chaosDriftD 19s ease-in-out infinite -5s; }

@keyframes chaosDriftA {
  0%, 100% { transform: rotate(-2.4deg) translate(0, 0); }
  50%      { transform: rotate(-1.2deg) translate(3px, -5px); }
}
@keyframes chaosDriftB {
  0%, 100% { transform: rotate(1.8deg) translate(0, 0); }
  50%      { transform: rotate(0.7deg) translate(-4px, 4px); }
}
@keyframes chaosDriftC {
  0%, 100% { transform: rotate(-1.2deg) translate(0, 0); }
  50%      { transform: rotate(-2.6deg) translate(4px, 3px); }
}
@keyframes chaosDriftD {
  0%, 100% { transform: rotate(2.6deg) translate(0, 0); }
  50%      { transform: rotate(1.1deg) translate(-3px, -4px); }
}

/* dash + gap = 18, so the loop closes on itself with no visible seam. */
.funnel-line { stroke-dasharray: 7 11; animation: funnelFlow 1.9s linear infinite; }
@keyframes funnelFlow { to { stroke-dashoffset: -18; } }

/* A CSS circle now, so it can pulse on transform instead of opacity — the ring
   around it scales with the dot rather than fading independently of it. */
.funnel-node { animation: funnelNodePulse 1.9s ease-in-out infinite; }
@keyframes funnelNodePulse {
  0%, 100% { transform: scale(1); opacity: .8; }
  50%      { transform: scale(1.22); opacity: 1; }
}

/* Same 5.6s on both, so the fade always lands on the same point of the
   travel and the reset happens while the dot is fully transparent. */
.rail-flow { animation: railTravel 5.6s cubic-bezier(.5, 0, .5, 1) infinite; }
.rail-pulse { animation: railFade 5.6s linear infinite; }
@keyframes railTravel { from { transform: translateX(0); } to { transform: translateX(100%); } }
@keyframes railFade { 0% { opacity: 0; } 7% { opacity: 1; } 90% { opacity: 1; } 100% { opacity: 0; } }

.funnel-down { animation: funnelDownNudge 2.6s ease-in-out infinite; }
@keyframes funnelDownNudge {
  0%, 100% { transform: translateY(-3px); opacity: .55; }
  50%      { transform: translateY(3px); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .chaos-item, .funnel-line, .funnel-node, .rail-flow, .rail-pulse, .funnel-down {
    animation: none !important;
  }
  /* The pile still needs to look like a pile, so keep the resting tilts. */
  .chaos-item--chat { transform: rotate(-2.4deg); }
  .chaos-item--form { transform: rotate(1.8deg); }
  .chaos-item--docs { transform: rotate(-1.2deg); }
  .chaos-item--who  { transform: rotate(2.6deg); }
  /* Without the travelling pulse, park it on the final step so the rail still
     reads as a completed sequence rather than a dot stranded at the start. */
  .rail-flow { transform: translateX(100%); }
}
