/* =========================================================
   Strategic Code Partners - motion
   Loaded after styles.css; driven by js/motion.js.
   Hidden pre-states exist only under html.motion (added by the
   head script), so without JS everything is visible, and
   html.m-fallback shows everything again if motion.js fails.

   HOOK REFERENCE
   Attributes
     data-m="settle"     rises and fades in once, when it enters (.reveal is an alias)
     data-m="surface"    fades in once, no movement
     data-m="veil"       large statement; lines rise in turn (split by JS; whole dissolve in calm)
     data-m="numeral"    digits rise inside a clip (or SCP.wrapNumeral(sel)); follows its host
     data-m="inscribe"   SVG strokes draw on (lengths from SCP.measureStrokes); follows its host
     data-m="gate"       photo moment: its frame exposes from black once the poster has decoded
     data-m-stage        its direct children arrive in order: eyebrow, h2, p, .pricing__promise
     data-dolly          photo push-in under the finger (inline --s0, --s1, --drift)
     data-reel-base, data-reel-l, data-reel-p, data-range   frames for SCP.Reel (the story's shots, Scene 02's frame)
   Classes on html
     motion (hidden pre-states allowed), calm (fades only), lite (Save-Data),
     m-ready (engine running), m-fallback (show everything), is-cutting,
     data-hdr="story|page" (header state), data-motion-lock (?motion= set, or the film chip pressed)
   Classes on elements
     is-in (entered), m-instant (no transitions for two frames), m-wc (will-change while moving),
     is-split, ln, ln__in (veil lines), num, num__in (numerals), is-glint,
     reel + is-on (SCP.Reel's canvas, and once it shows a frame),
     cut + to-film (the jump-cut's dip, added by motion.js), menu-open (header) + nav__scrim (main.js),
     demo__old/new/label/saved + is-edit, is-new, is-saved, is-glow (the #why panel's demo, built by motion.js),
     is-link (a #free-demo step whose thread to the next step has drawn; --ld its delay),
     m-unfold + faq__body + is-open (the #faq unfold: .faq once motion.js has wrapped each answer; an item's visible state),
     aria-current="location" (the nav link for the section being read),
     is-sending + form__wait (the contact form's pending line), note__tick (its success tick; both from main.js)
   Events
     scp:in (element, when it enters), scp:gate (gate, on exposure),
     scp:unsplit (veil, text restored), scp:calm (window, calm switched),
     scp:reel (a reel's frame element: detail.state on | idle | failed)
   Section behaviour lives in motion.js modules keyed by id: #why (demo), #free-demo (chain),
   #done-for-you (beat, chip delays), .cine-band--finale (lights, glint), #faq (unfold),
   #contact (row delays, focus guard); the chrome module runs the header state, scroll-spy and
   jump-cut. There are no other single-use data hooks.
   ========================================================= */

/* ---------- Tokens (the same in every theme) ---------- */
:root {
  /* Easing */
  --ease-silk: cubic-bezier(.16, 1, .3, 1);
  --ease-veil: cubic-bezier(.33, 0, .2, 1);
  --ease-curtain: cubic-bezier(.76, 0, .24, 1);
  --ease-touch: cubic-bezier(.25, .8, .35, 1);
  --ease-exit: cubic-bezier(.4, 0, 1, 1);

  /* Durations */
  --t-press: 140ms;
  --t-touch: 420ms;
  --t-settle: 1000ms;
  --t-veil: 1200ms;
  --t-numeral: 1100ms;
  --t-exposure: 1400ms;
  --t-inscribe: 1200ms;
  --t-rule: 700ms;
  --t-calm: 700ms;

  /* Distances */
  --rise: 18px;
  --rise-chip: 8px;
  --rise-line: 108%;
  --rise-num: 100%;
  --lift-hover: 2px;
  --press: .98;

  /* Staggers */
  --stagger-line: 90ms;
  --stagger-item: 110ms;
  --stagger-chip: 55ms;
  --stagger-row: 90ms;
  --stagger-menu: 40ms;
  --stagger-pay: 400ms;
  --stagger-calm: 70ms;

  /* Stage offsets scale by this; calm plays them closer together */
  --stage-k: 1;
}
:root.calm { --stage-k: .6; }
@media (max-width: 560px) { :root { --rise: 12px; } }

/* More air between sections */
.section { padding-block: clamp(4.5rem, 11vw, 8rem); }
/* Jumps land below the header (70px plus its 1px border, plus 1) */
html { scroll-padding-top: 72px; }

/* ---------- Shared ---------- */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* Eyebrow hairline: left-aligned section heads only (never the story, centred heads or the film bands).
   Drawn by default; it only draws on inside a stage */
:is(.section__head, .split__copy, .cta__copy, .pay__copy) > .eyebrow::before {
  content: ""; display: inline-block; vertical-align: middle;
  width: 28px; height: 1px; margin: -.15em .7em 0 0;
  background: currentColor; opacity: .55; transform-origin: 0 50%;
}
@media (max-width: 359px) {
  :is(.section__head, .split__copy, .cta__copy, .pay__copy) > .eyebrow::before { display: none; }
}
.motion:not(.calm) [data-m-stage]:not(.is-in) > .eyebrow::before { transform: scaleX(0); }
.motion [data-m-stage] > .eyebrow::before {
  transition: transform var(--t-rule) var(--ease-curtain) calc(150ms * var(--stage-k) + var(--delay, 0ms));
}

/* ---------- Stage: direct children arrive in order ---------- */
.motion [data-m-stage] > .eyebrow { --sd: 150ms; --rise-at: var(--rise-chip); }
.motion [data-m-stage] > h2:not([data-m]) { --sd: 240ms; }
.motion [data-m-stage] > p:not([data-m]):not(.pricing__promise) { --sd: 380ms; }
.motion [data-m-stage] > .pricing__promise { --sd: 520ms; --rise-at: var(--rise-chip); }
.motion [data-m-stage] > :is(.eyebrow, h2:not([data-m]), p:not([data-m])) {
  --sd-at: calc(var(--sd) * var(--stage-k) + var(--delay, 0ms));
  opacity: 0;
  transform: translate3d(0, var(--rise-at, var(--rise)), 0);
  transition: opacity .8s var(--ease-veil) var(--sd-at), transform var(--t-settle) var(--ease-silk) var(--sd-at);
}
.motion [data-m-stage].is-in > :is(.eyebrow, h2:not([data-m]), p:not([data-m])) { opacity: 1; transform: none; }

/* ---------- Settle: rises into place (unmigrated .reveal elements use it too) ---------- */
/* Per-property delays: hover and press are never held back by the entrance */
.motion :is([data-m=settle], .reveal) {
  --d: calc(var(--i, 0) * var(--stagger-item) + var(--delay, 0ms));
  opacity: 0;
  transform: translate3d(0, var(--rise), 0);
  transition:
    opacity .8s var(--ease-veil) var(--d),
    transform var(--t-settle) var(--ease-silk) var(--d),
    border-color var(--t-touch) var(--ease-touch) 0s,
    translate var(--t-touch) var(--ease-touch) 0s,
    background-color var(--t-press) linear 0s;
}
.motion :is([data-m=settle], .reveal).is-in { opacity: 1; transform: none; }

/* ---------- Surface: fades, no movement ---------- */
.motion [data-m=surface] { opacity: 0; transition: opacity .8s var(--ease-veil) var(--delay, 0ms); }
.motion [data-m=surface].is-in { opacity: 1; }

/* ---------- Veil: the three large statements, line by line ---------- */
/* Passive: it follows the nearest entering ancestor, never a gate (a gate's .is-in comes first) */
.motion [data-m=veil] { opacity: 0; }
.motion [data-m=veil].is-split { opacity: 1; transition: none; }
.motion .is-in:not([data-m=gate]) [data-m=veil]:not(.is-split) {
  opacity: 1;
  transition: opacity var(--t-calm) var(--ease-veil) calc(120ms * var(--stage-k) + var(--delay, 0ms));
}
/* The top padding keeps Arial caps and apostrophes from being clipped. The margins cancel the padding;
   between lines it all sits on the bottom margin, because adjacent margins collapse instead of adding,
   so the split text lines up exactly with the plain text it is swapped back to */
.ln { display: block; overflow: hidden; padding: .1em 0 .12em; margin: 0 0 -.22em; }
.ln:first-child { margin-top: -.1em; }
.ln:last-child { margin-bottom: -.12em; }
.ln__in {
  display: block;
  transform: translate3d(0, var(--rise-line), 0);
  transition: transform var(--t-veil) var(--ease-silk)
    calc(120ms * var(--stage-k) + var(--li) * var(--stagger-line) + var(--delay, 0ms) + var(--beat-ln, 0ms));
}
.is-in:not([data-m=gate]) .ln__in { transform: none; }
/* WebKit: a gradient fragment inside a moving line needs its own paint box (it is one line by construction) */
.ln .grad { display: inline-block; }

/* ---------- Numeral: digits rise inside a clip (#why stats) ---------- */
/* Zero specificity, so the host's own display, margin or placement always wins */
:where(.num) { display: inline-block; overflow: hidden; padding-bottom: .1em; margin-bottom: -.1em; vertical-align: bottom; }
:where(.num__in) { display: inline-block; }
.motion:not(.calm) .num__in {
  transform: translate3d(0, var(--rise-num), 0);
  transition: transform var(--t-numeral) var(--ease-silk) calc(var(--num-at, 200ms) + var(--delay, 0ms));
}
.motion:not(.calm) .is-in .num__in { transform: none; }

/* ---------- Inscribe: service icons draw on (lengths measured by JS) ---------- */
/* If --dash or --off is unset the declarations are invalid at computed-value time, so the strokes show drawn */
.motion:not(.calm) [data-m=inscribe] :is(path, rect, circle) {
  stroke-dasharray: var(--dash);
  stroke-dashoffset: var(--off);
  transition: stroke-dashoffset var(--t-inscribe) var(--ease-curtain) calc(var(--d, 0ms) + 200ms + var(--pi, 0) * 120ms);
}
.motion:not(.calm) .is-in [data-m=inscribe] :is(path, rect, circle) { stroke-dashoffset: 0; }

/* ---------- Frame player (SCP.Reel): its canvas covers the poster once a frame is drawn ---------- */
.reel {
  position: absolute; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none;
  opacity: 0; transition: opacity .4s var(--ease-veil);
}
.reel.is-on { opacity: 1; }

/* ---------- While moving ---------- */
.m-wc { will-change: transform, opacity; }
[data-m-stage].m-wc { will-change: auto; }
[data-m-stage].m-wc > * { will-change: transform, opacity; }

/* Already passed (restored scroll, jumps): land in place with no transition; removed after two frames */
.m-instant, .m-instant::before, .m-instant::after,
.m-instant *, .m-instant *::before, .m-instant *::after { transition: none !important; animation: none !important; }

/* ---------- Calm (Reduce Motion): the same entrances as dissolves ---------- */
/* Keyed on html.calm only, so ?motion=full and ?motion=calm can override the phone's setting */
.calm :is([data-m=settle], .reveal),
.calm [data-m-stage] > * { transform: none; }
.calm :is([data-m=settle], .reveal) {
  --d: calc(var(--i, 0) * var(--stagger-calm) + var(--delay, 0ms));
  transition:
    opacity var(--t-calm) var(--ease-veil) var(--d),
    border-color var(--t-touch) var(--ease-touch) 0s,
    translate var(--t-touch) var(--ease-touch) 0s,
    background-color var(--t-press) linear 0s;
}
.calm [data-m-stage] > :is(.eyebrow, h2:not([data-m]), p:not([data-m])) {
  transform: none;
  transition: opacity var(--t-calm) var(--ease-veil) var(--sd-at);
}
.calm [data-m=surface] { transition: opacity var(--t-calm) var(--ease-veil) var(--delay, 0ms); }

/* ---------- Hover (fine pointers only; touch gets no sticky hover states) ---------- */
@media (hover: hover) and (pointer: fine) {
  :is(.card, .step, .member, .promise, .paystep):hover { border-color: var(--border-strong); }
}

/* =========================================================
   Chrome: header, nav, buttons, theme toggle, mobile menu,
   jump-cut, footer. States are set by motion.js (data-hdr,
   aria-current, is-cutting) and main.js (menu-open, theme)
   ========================================================= */

/* ---------- Header: two states, never a blur ---------- */
/* Page: one near-solid material (::before), the same over every band. Story: clear over the film, with a soft
   scrim (::after) and light tokens. No data-hdr (no JS, or a hash arrival before motion.js) reads as page.
   Never put transform or will-change on .site-header or its ancestors: the header is sticky and the menu's scrim is fixed */
.site-header {
  background: none; -webkit-backdrop-filter: none; backdrop-filter: none;
  transition: border-color 300ms var(--ease-veil), color 300ms var(--ease-veil);
}
.site-header::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  transition: opacity 300ms var(--ease-veil), visibility 0s;
}
.site-header::after {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 118px; z-index: -1; pointer-events: none;
  background: linear-gradient(180deg, rgba(7, 9, 14, .55), rgba(7, 9, 14, 0));
  opacity: 0; visibility: hidden;
  transition: opacity 300ms var(--ease-veil), visibility 0s 300ms;
}
html:not([data-hdr=story]) .site-header { border-bottom-color: var(--border); }
/* An open menu always uses the page material */
html[data-hdr=story] .site-header:not(.menu-open) {
  --text: #ecedf2; --muted: #d3d9e4; --surface: rgba(19, 23, 34, .45);
  --border: rgba(255, 255, 255, .16); --border-strong: rgba(255, 255, 255, .26); --accent-2: #7cb4ff;
  color: var(--text);
}
html[data-hdr=story] .site-header:not(.menu-open)::before {
  opacity: 0; visibility: hidden;
  transition: opacity 300ms var(--ease-veil), visibility 0s 300ms;
}
html[data-hdr=story] .site-header:not(.menu-open)::after {
  opacity: 1; visibility: visible;
  transition: opacity 300ms var(--ease-veil), visibility 0s;
}
html[data-hdr=story] .site-header:not(.menu-open) :is(.brand, .nav__links a) { text-shadow: 0 1px 12px rgba(0, 0, 0, .35); }
.site-header.menu-open::before { background: color-mix(in srgb, var(--bg) 97%, transparent); }

/* ---------- Nav: the link for the section being read is underlined (scroll-spy sets aria-current) ---------- */
.nav__links a { transition: color 300ms, background-color 300ms, border-color 300ms; }
.nav__links a::after {
  left: 0; right: 0; width: auto;
  transform: scaleX(0); transform-origin: 100% 50%;
  transition: transform 420ms var(--ease-touch);
}
.nav__links a[aria-current] { color: var(--text); }
.nav__links a[aria-current]::after { transform: scaleX(1); transform-origin: 0 50%; }
.mobile-menu a[aria-current] { color: var(--text); }

/* ---------- Buttons: every one sinks slightly under the finger ---------- */
.btn {
  -webkit-tap-highlight-color: transparent;
  transition:
    transform var(--t-press) var(--ease-touch),
    translate var(--t-touch) var(--ease-touch),
    opacity var(--t-press) linear,
    box-shadow var(--t-touch) var(--ease-touch),
    background-position var(--t-touch) var(--ease-touch),
    border-color 300ms, background-color 300ms, color 200ms;
}
.btn:active { transform: scale(var(--press)); }
.btn:disabled { opacity: .6; cursor: progress; }
/* The sheen plays only on a mouse hover; there is no automatic sheen anywhere */
.btn--primary { isolation: isolate; }
.btn--primary::after {
  content: ""; position: absolute; top: 0; left: 0; width: 45%; height: 100%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .45), transparent);
  transform: translate3d(-160%, 0, 0) skewX(-20deg); pointer-events: none;
}
/* Icon buttons */
.theme-toggle, .nav__burger {
  -webkit-tap-highlight-color: transparent;
  transition: transform var(--t-press) var(--ease-touch), opacity var(--t-press) linear,
    border-color 300ms, background-color 300ms, color 300ms;
}
.theme-toggle:active, .nav__burger:active { transform: scale(var(--press)); }

/* ---------- Theme toggle: the icons crossfade ---------- */
.theme-toggle svg { grid-area: 1 / 1; transition: opacity 240ms var(--ease-veil); }
.icon-moon { display: block; opacity: 0; }
[data-theme=light] .icon-sun { opacity: 0; }
[data-theme=light] .icon-moon { opacity: 1; }
/* The theme swap is a View Transition where the browser has one (main.js; skipped in calm) */
::view-transition-old(root), ::view-transition-new(root) { animation-duration: 360ms; animation-timing-function: var(--ease-veil); }
/* Both need main.js; without it the footer carries every link */
html:not(.js) .theme-toggle { display: none; }

/* ---------- Mobile menu: an overlay that drops in over the page, so nothing below moves ---------- */
.mobile-menu {
  position: absolute; top: 100%; left: 0; right: 0;
  display: flex; flex-direction: column; gap: .25rem;
  padding: 1rem var(--pad) 1.4rem;
  background: var(--bg);
  border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow);
  max-height: calc(100dvh - 70px); overflow-y: auto; overscroll-behavior: contain;
}
.mobile-menu[hidden] { display: none; }
@media (min-width: 961px) { .mobile-menu { display: none; } }
.mobile-menu a:nth-child(1) { --mi: 0; }
.mobile-menu a:nth-child(2) { --mi: 1; }
.mobile-menu a:nth-child(3) { --mi: 2; }
.mobile-menu a:nth-child(4) { --mi: 3; }
.mobile-menu a:nth-child(5) { --mi: 4; }
.mobile-menu a:nth-child(6) { --mi: 5; }
.motion .mobile-menu {
  opacity: 0; transform: translate3d(0, -8px, 0);
  transition: opacity 200ms var(--ease-exit), transform 200ms var(--ease-exit);
}
.motion .mobile-menu.open {
  opacity: 1; transform: none;
  transition: opacity 320ms var(--ease-veil), transform 320ms var(--ease-silk);
}
/* Links arrive in turn; they leave together */
.motion .mobile-menu a { opacity: 0; transition: opacity 150ms var(--ease-exit); }
.motion .mobile-menu.open a { opacity: 1; transition: opacity 320ms var(--ease-veil) calc(80ms + var(--mi) * var(--stagger-menu)); }
/* The menu's button keeps its press */
.motion .mobile-menu .btn { transition: opacity 150ms var(--ease-exit), transform var(--t-press) var(--ease-touch); }
.motion .mobile-menu.open .btn {
  transition: opacity 320ms var(--ease-veil) calc(80ms + var(--mi) * var(--stagger-menu)), transform var(--t-press) var(--ease-touch);
}
/* Scrim over the page (added by main.js); it sits under the header's material */
.nav__scrim {
  position: fixed; inset: 0; z-index: -2;
  background: rgba(7, 9, 14, .5);
  opacity: 0; visibility: hidden;
  transition: opacity 200ms, visibility 0s 200ms;
}
.menu-open .nav__scrim { opacity: 1; visibility: visible; transition: opacity 320ms, visibility 0s; }
.nav__burger span { transition: transform 320ms var(--ease-touch), opacity 200ms, background-color 300ms; }

/* ---------- Jump-cut: a long jump is a quick dip instead of a 20-screen blur (motion.js adds .cut) ---------- */
/* The dip is the page background, or film black when it lands on the film (#top, a band) */
.cut {
  position: fixed; inset: 0; z-index: 150;
  background: var(--bg);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 360ms var(--ease-veil), visibility 0s 360ms;
}
.cut.to-film { background: #07090e; }
.is-cutting .cut { opacity: 1; visibility: visible; transition: opacity 160ms var(--ease-exit), visibility 0s; }
/* Targets focused after a jump (sections get tabindex=-1) */
[tabindex='-1']:focus { outline: none; }

/* ---------- Footer: still ---------- */
.footer__col a { -webkit-tap-highlight-color: transparent; }
.footer__col a:active { color: var(--accent-2); }
@media (pointer: coarse) { .footer__col a { padding-block: .55rem; } }

/* ---------- Chrome hover (fine pointers only) ---------- */
@media (hover: hover) and (pointer: fine) {
  .nav__links a:hover { color: var(--text); }
  .nav__links a:hover::after { transform: scaleX(1); transform-origin: 0 50%; }
  .btn--primary:hover {
    translate: 0 calc(-1 * var(--lift-hover));
    box-shadow: 0 16px 40px -12px rgba(47, 111, 237, .65);
    background-position: 100% 0;
  }
  .btn--primary:hover::after { transform: translate3d(300%, 0, 0) skewX(-20deg); transition: transform 900ms var(--ease-silk); }
  .btn--ghost:hover {
    translate: 0 calc(-1 * var(--lift-hover));
    border-color: var(--accent);
    background-color: color-mix(in srgb, var(--accent) 10%, transparent);
  }
  .theme-toggle:hover { border-color: var(--accent); }
  .mobile-menu a:not(.btn):hover { color: var(--text); background: var(--surface); }
  .footer__col a:hover { color: var(--accent-2); }
}

/* ---------- Chrome in calm: no scale, no lift, no sheen; the menu, underline and burger fade ---------- */
.calm :is(.btn, .theme-toggle, .nav__burger):active { transform: none; opacity: .8; }
.calm .btn:hover { translate: none; }
.calm .btn--primary::after { display: none; }
.calm .nav__links a::after { transform: scaleX(1); opacity: 0; transition: opacity 300ms var(--ease-veil); }
.calm .nav__links a[aria-current]::after { opacity: 1; }
@media (hover: hover) and (pointer: fine) { .calm .nav__links a:hover::after { opacity: 1; } }
.calm .mobile-menu { transform: none !important; }
/* Burger: the lines crossfade to an X drawn by the pseudo-elements, instead of rotating into one */
.nav__burger { position: relative; }
.nav__burger::before, .nav__burger::after {
  content: ""; position: absolute; top: 50%; left: 50%; width: 20px; height: 2px; margin: -1px 0 0 -10px;
  border-radius: 2px; background: var(--text);
  opacity: 0; transition: opacity 240ms, background-color 300ms;
}
.nav__burger::before { transform: rotate(45deg); }
.nav__burger::after { transform: rotate(-45deg); }
.calm .nav__burger span { transform: none !important; }
.calm .nav__burger[aria-expanded=true]::before, .calm .nav__burger[aria-expanded=true]::after { opacity: 1; }
.calm .nav__burger[aria-expanded=true] span { opacity: 0; }

/* =========================================================
   Sections, in page order. Each keys its hidden pre-states on
   html.motion and its calm version on html.calm
   ========================================================= */

/* ---------- Trust strip ("Built for"): a credits line under the film ---------- */
/* A short rule draws, the label settles, the six names fade up in turn, then it is all still. The names are not links */
.trust { gap: .9rem 1.5rem; }
.trust--solo { padding-block: clamp(2rem, 5vw, 3rem); }
.trust__label {
  display: inline-flex; align-items: center; gap: .75rem;
  font-size: .78rem; letter-spacing: .14em; text-transform: uppercase; font-weight: 600; color: var(--muted);
}
.trust__label::before { content: ""; width: 28px; height: 1px; background: currentColor; opacity: .55; transform-origin: 0 50%; }
/* The row reaches one gap left of its first name and clips that strip, so a dot that would start a line never shows;
   the -.6em on the other sides leaves room for the rise */
.trust__row {
  --gap-x: 2rem;
  --trust-rest: color-mix(in srgb, var(--muted) 80%, transparent);
  gap: .7rem var(--gap-x);
  margin-inline-start: calc(-1 * var(--gap-x));
  padding-inline-start: var(--gap-x);
  -webkit-clip-path: inset(-.6em -.6em -.6em var(--gap-x));
  clip-path: inset(-.6em -.6em -.6em var(--gap-x));
}
.trust__row em { position: relative; color: var(--trust-rest); font-size: clamp(1.05rem, 1.6vw, 1.2rem); }
/* Separator dots, centred in the gap before each name */
.trust__row em::before {
  content: ""; position: absolute; top: 50%; left: calc(var(--gap-x) / -2);
  width: 3px; height: 3px; margin: -1.5px 0 0 -1.5px; border-radius: 50%;
  background: currentColor; opacity: .5;
}
@media (max-width: 560px) { .trust__row { --gap-x: 1.6rem; row-gap: .5rem; } }

.trust__row em:nth-child(2) { --ri: 1; }
.trust__row em:nth-child(3) { --ri: 2; }
.trust__row em:nth-child(4) { --ri: 3; }
.trust__row em:nth-child(5) { --ri: 4; }
.trust__row em:nth-child(6) { --ri: 5; }
.motion .trust__label {
  opacity: 0; transform: translate3d(0, var(--rise-chip), 0);
  transition: opacity .8s var(--ease-veil) 150ms, transform var(--t-settle) var(--ease-silk) 150ms;
}
/* The rule draws while the label fades up */
.motion:not(.calm) .trust:not(.is-in) .trust__label::before { transform: scaleX(0); }
.motion .trust__label::before { transition: transform 500ms var(--ease-curtain) 150ms; }
.motion .trust__row em {
  opacity: 0; transform: translate3d(0, var(--rise-chip), 0);
  transition: opacity .8s var(--ease-veil), transform var(--t-settle) var(--ease-silk);
  transition-delay: calc(300ms + var(--ri, 0) * var(--stagger-chip));
}
.trust.is-in :is(.trust__label, .trust__row em) { opacity: 1; transform: none; }
/* Calm: no travel; the label fades with its rule already drawn, then the names */
.calm .trust :is(.trust__label, em) { transform: none; }
.calm .trust__label { transition: opacity var(--t-calm) var(--ease-veil) 90ms; }
.calm .trust__row em { transition: opacity var(--t-calm) var(--ease-veil) calc(180ms + var(--ri, 0) * var(--stagger-calm)); }

/* ---------- Services: the head settles, the cards rise in reading order and each icon draws itself ---------- */
/* All shared: the stage (head), settle (cards; the flag is part of its card) and inscribe (icons, from the
   card's --d + 200ms, 120ms per stroke). The ticks, body copy and featured gradient stay still.
   Cards are not links, so they never lift: a hover only changes border colours, never delayed by the entrance */
.card__icon { transition: border-color var(--t-touch) var(--ease-touch); }
@media (hover: hover) and (pointer: fine) {
  .card:hover .card__icon { border-color: color-mix(in srgb, var(--accent) 55%, transparent); }
}
/* Calm: the icons are simply drawn and fade with their card */
.calm [data-m=inscribe] * { stroke-dasharray: none; stroke-dashoffset: 0; transition: none; }

/* ---------- Photo moments: exposure and dolly (Scene 01 first; done-for-you and Scene 02 reuse them) ---------- */
/* The frame fills the band; whatever it holds (picture, img, the reel's canvas) fills the frame */
.cine__frame { position: absolute; inset: 0; }
.cine__frame > picture, .cine__frame img, .cine__frame > canvas {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block;
}
/* Exposure: the photo fades up out of the band's own black (the .cine background); no letterbox bars.
   motion.js opens the gate once the poster has decoded (or 600ms), and a film band's text follows 400ms later */
.motion [data-m=gate] .cine__frame { opacity: 0; transition: opacity var(--t-exposure) var(--ease-veil); }
.motion [data-m=gate].is-in .cine__frame { opacity: 1; }
/* Dolly: the camera pushes in (and drifts, if --drift is set) only while the page moves. motion.js writes --v,
   0 as the band's top meets the bottom of the screen to 1 as its bottom leaves the top. A positive drift lags
   the page, which reads as depth. .cine__media is inset:0, so each frame covers its own edges: with a drift,
   min(--s0, --s1) >= 1 + 2 * drift + .01 (Scene 01 has none, and scale 1 already covers) */
.motion:not(.calm) [data-dolly] {
  transform:
    translate3d(0, calc((var(--v, .5) - .5) * 2 * var(--drift, 0%)), 0)
    scale(calc(var(--s0, 1) + (var(--s1, 1) - var(--s0, 1)) * var(--v, .5)));
}
/* Calm: a slower, even exposure, and the photo never moves (the text dissolves whole, from gate + 240ms) */
.calm [data-m=gate] .cine__frame { transition: opacity 1200ms linear; }

/* ---------- Why: the panel shows the promise instead of stating it ---------- */
/* The copy is a stage and the panel settles. The three figures rise into place after the paragraph, digits inside
   their clip (they never count). Then, once, motion.js has the panel update itself: the second row highlights,
   its price changes, the button reads "Saved" with a tick and the Live pill glows. Without JS it is today's panel */
@media (max-width: 960px) { .split__media { max-width: 440px; } }
#why .stat:nth-child(2) { --k: 1; }
#why .stat:nth-child(3) { --k: 2; }
#why .stat { --num-at: calc(720ms + var(--k, 0) * 90ms); }
.motion #why .stat {
  --stat-at: calc(520ms * var(--stage-k) + var(--k, 0) * var(--stagger-row) + var(--delay, 0ms));
  opacity: 0;
  transform: translate3d(0, var(--rise), 0);
  transition: opacity .8s var(--ease-veil) var(--stat-at), transform var(--t-settle) var(--ease-silk) var(--stat-at);
}
/* (Under #why: .split__copy.is-in .stat alone would lose to the pre-state above) */
.motion #why .is-in .stat { opacity: 1; transform: none; }
/* WebKit paints clipped gradient text blank inside a moving box: the gradient moves onto the moving part.
   The .num wrapper exists only with motion.js, so without it the figures keep today's gradient.
   (The wrapper is a span, so it also inherits back the size that styles.css's .stat span gives the labels) */
#why .stat strong.num { background: none; color: inherit; }
#why .stat strong.num .num__in {
  font-size: inherit;
  background: var(--accent-grad); -webkit-background-clip: text; background-clip: text; color: transparent;
}

/* Demo layers (always on, so nothing changes at rest). The price and the button each hold two layers in one grid
   cell that crossfade; the cell is as big as the larger layer, so the panel never changes size. motion.js builds
   the layers only when the demo starts */
#why .panel__item em { display: inline-grid; justify-items: end; min-width: 6ch; font-variant-numeric: tabular-nums; }
.demo__old, .demo__new { grid-area: 1 / 1; transition: opacity 400ms var(--ease-veil); }
.demo__new { opacity: 0; }
.is-new .demo__old { opacity: 0; }
.is-new .demo__new { opacity: 1; }
#why .panel__item { transition: border-color 300ms var(--ease-veil), background-color 600ms var(--ease-veil); }
#why .panel__item.is-edit {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
  background-color: color-mix(in srgb, var(--accent) 8%, transparent);
}
#why .panel__cta { display: grid; }
.panel__cta > span { grid-area: 1 / 1; transition: opacity 300ms var(--ease-veil); }
.demo__saved { opacity: 0; display: inline-flex; gap: .4em; align-items: center; justify-content: center; }
.is-saved .demo__label { opacity: 0; }
.is-saved .demo__saved { opacity: 1; }
/* The tick draws as "Saved" appears. Its path is 19.7 long; the offset of 22 also hides the round caps */
.demo__saved path {
  fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 20 26; stroke-dashoffset: 22;
  transition: stroke-dashoffset 400ms var(--ease-curtain) 100ms;
}
.is-saved .demo__saved path { stroke-dashoffset: 0; }
/* The Live pill glows once (opacity only, so calm keeps it) */
.panel__pill { position: relative; }
.panel__pill::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit; opacity: 0; pointer-events: none;
  box-shadow: 0 0 0 3px rgba(52, 211, 153, .28), 0 0 16px 2px rgba(52, 211, 153, .45);
}
.panel__pill.is-glow::after { animation: pill-glow 900ms var(--ease-veil) 1 both; }
@keyframes pill-glow { 0% { opacity: 0; } 35% { opacity: 1; } to { opacity: 0; } }

/* Calm: the figures dissolve with no travel (312ms + 70ms each), the tick is already drawn, and the demo's
   crossfades run at x.6 (motion.js) */
.calm #why .stat {
  transform: none;
  transition: opacity var(--t-calm) var(--ease-veil) calc(520ms * var(--stage-k) + var(--k, 0) * var(--stagger-calm) + var(--delay, 0ms));
}
.calm .num__in { transform: none; transition: none; }
.calm .demo__saved path { stroke-dashoffset: 0; transition: none; }

/* ---------- Process: the steps arrive in reading order and each number lights as its card lands ---------- */
/* The head is a stage and the cards settle. Just after a card lands (its --d + 700ms) the number rises from .6 to full
   and a fine blue rule lights beside it, running to the card's edge. The rules are per card (a shared rail on the list
   would be hidden by the opaque cards). Nothing follows the scroll, and the text is never dimmed.
   At rest (no JS, the failsafe, cards already passed) the number is full and both rules show */
/* fit-content keeps the gradient spread over the digits, as it was while the number was inline */
.step__num { display: block; width: fit-content; opacity: 1; }
/* The rule sits on the digits' middle and starts .9rem after "04" (5.2rem - 1.6rem padding - 2.7rem of digits) */
.step::before, .step::after {
  content: ""; position: absolute; left: 5.2rem; right: 1.6rem; top: calc(1.6rem + 2.4rem * .49);
  height: 1px; border-radius: 1px; pointer-events: none;
}
.step::before { background: var(--border); }
.step::after { background: linear-gradient(90deg, var(--accent), var(--accent-2)); }
.motion .step[data-m] .step__num { opacity: .6; transition: opacity 600ms var(--ease-veil) calc(var(--d) + 700ms); }
.motion .step[data-m]::after { opacity: 0; transition: opacity 600ms var(--ease-veil) calc(var(--d) + 700ms); }
.motion .step[data-m].is-in .step__num, .motion .step[data-m].is-in::after { opacity: 1; }
/* Calm: the same sequence, opacity only; the cards dissolve 70ms apart, each number and rule at its card's time + 420ms */
.calm .step[data-m] .step__num,
.calm .step[data-m]::after { transition: opacity var(--t-calm) var(--ease-veil) calc(var(--d) + 420ms); }

/* ---------- Done-for-you: the promise, over the sketch desk ---------- */
/* Loud through light, not movement; four layers and nothing more. The photo exposes (the gate) and pulls back under the
   finger (the dolly, 1.10 to 1.04 with a 1.5% drift: at v=1 it still overhangs .5%). The title veils with a beat, so
   "That's our job." lands on its own; the paragraph settles whole with the stage and is never dimmed. The chips arrive one
   by one, each ticked off with a pen stroke. The text is not cued by the gate: the section is tall and the words are the point.
   motion.js holds the chips and the CTA back when they arrive with the stage (#done-for-you module) */
#done-for-you { padding-block: clamp(5rem, 13vw, 9.5rem); }
/* The last sentence is full white, always (static) */
.statement__key { color: var(--text); }
/* The shade holds the words at .88 (the paragraph is 4.5:1 or better over the brightest pixel behind it) and lets the desk
   show between the chips; on desktop the edges open instead, where the wide still keeps its detail */
#done-for-you .cine__shade {
  background: linear-gradient(180deg, rgba(7, 9, 14, .80) 0%, rgba(7, 9, 14, .88) 14%, rgba(7, 9, 14, .88) 58%,
    rgba(7, 9, 14, .64) 70%, rgba(7, 9, 14, .64) 90%, rgba(7, 9, 14, .84) 100%);
}
@media (min-width: 961px) {
  #done-for-you .cine__shade {
    background:
      radial-gradient(42% 60% at 50% 45%, rgba(7, 9, 14, .5), transparent 72%),
      linear-gradient(90deg, rgba(7, 9, 14, .62), rgba(7, 9, 14, .80) 24%, rgba(7, 9, 14, .80) 76%, rgba(7, 9, 14, .62));
  }
}
/* The pull-back centres on the frame; on a tall screen, on the desk low in the frame */
#done-for-you .cine__frame { transform-origin: 50% 50%; }
@media (max-aspect-ratio: 4/5) { #done-for-you .cine__frame { transform-origin: 50% 72%; } }
/* Chips: a short rise, 55ms apart. Each tick draws 350ms after its chip starts; the path (M4 12l5 5L20 6, the same glyph in
   pen order) is 22.6 long, and the offset of 25 also hides the round caps */
#done-for-you .handled li { --rise: var(--rise-chip); --stagger-item: var(--stagger-chip); }
.motion:not(.calm) #done-for-you .tick path {
  stroke-dasharray: 23 27; stroke-dashoffset: 25;
  transition: stroke-dashoffset 480ms var(--ease-curtain) calc(var(--d, 0ms) + 350ms);
}
/* (#done-for-you keeps it above the pre-state) */
#done-for-you .handled li.is-in .tick path { stroke-dashoffset: 0; }
/* A button that settles (this CTA): the entrance owns its opacity and transform, delays included, only while it moves
   (.m-wc); once landed it is a plain button again (the .btn list), so a press is never held back. No sheen but a mouse hover's */
.motion .btn[data-m=settle].is-in:not(.m-wc) {
  transition:
    transform var(--t-press) var(--ease-touch),
    translate var(--t-touch) var(--ease-touch),
    opacity var(--t-press) linear,
    box-shadow var(--t-touch) var(--ease-touch),
    background-position var(--t-touch) var(--ease-touch),
    border-color 300ms, background-color 300ms, color 200ms;
}
.motion .btn[data-m=settle].is-in:not(.m-wc):active { transform: scale(var(--press)); }
/* Calm: the photo fades up over 1200ms and never moves (shared); the title and paragraph dissolve whole at x.6, the chips
   70ms apart with their ticks drawn, then the CTA; a press dims it to .8 */
.calm .tick path { stroke-dashoffset: 0; }
.calm .btn[data-m=settle].is-in:not(.m-wc):active { transform: none; opacity: .8; }

/* ---------- Free demo: the zero-risk offer lands in the order it happens ---------- */
/* The panel fades whole (surface; its background, border and top light stay static) and the head is a stage (the h2
   settles whole; the centred eyebrow has no hairline). Then $0, a fine thread drawing on to the 50% deposit, another to
   the 50% balance, and last the CTA and its note. motion.js spaces the steps 400ms apart and draws each thread as the
   step after it arrives (#free-demo module). The figures arrive with their cards and never move by themselves.
   At rest (no JS, the failsafe) the threads are simply there: a line through the discs on desktop, the gaps on phones */
.paysteps { --ppad: 1.6rem; --pbadge: 74px; --pgap: 1.25rem; gap: var(--pgap); }
/* Each card's thread runs over the next card, so the earlier card stacks higher */
.paystep:nth-child(1) { z-index: 3; }
.paystep:nth-child(2) { z-index: 2; }
.paystep:nth-child(3) { z-index: 1; }
/* Desktop: on the discs' middle, from 10px after this disc to 10px before the next one
   (the next card starts 100% + 2px of borders + the gap along, and its disc is centred in it) */
.paystep:not(:last-child)::after {
  content: ""; position: absolute; right: auto; pointer-events: none;
  top: calc(var(--ppad) + var(--pbadge) / 2 - .5px);
  left: calc(50% + var(--pbadge) / 2 + 10px);
  width: calc(100% + 2px + var(--pgap) - var(--pbadge) - 20px);
  height: 1px;
  background: linear-gradient(90deg, color-mix(in srgb, var(--accent-2) 60%, transparent), var(--border-strong));
  transform-origin: 0 50%;
}
/* Phones: the cards stack, and the thread fills the gap from one card's bottom edge to the next one's top */
@media (max-width: 780px) {
  .paysteps { --pgap: 2rem; }
  .paystep:not(:last-child)::after {
    top: calc(100% + 1px); left: calc(50% - .5px); width: 1px; height: var(--pgap);
    background: linear-gradient(180deg, color-mix(in srgb, var(--accent-2) 60%, transparent), var(--border-strong));
    transform-origin: 50% 0;
  }
}
/* A thread draws on once the step after it arrives (motion.js adds .is-link and its --ld delay) */
.motion:not(.calm) .paystep[data-m]:not(:last-child)::after {
  transform: scaleX(0);
  transition: transform 700ms var(--ease-curtain) var(--ld, 0ms);
}
@media (max-width: 780px) { .motion:not(.calm) .paystep[data-m]:not(:last-child)::after { transform: scaleY(0); } }
.motion:not(.calm) .paystep[data-m].is-link:not(:last-child)::after { transform: none; }
/* The note follows its button 220ms later. No sheen but a mouse hover's, and no warming glow */
.motion .freebie__cta[data-m] .freebie__note { opacity: 0; transition: opacity .8s var(--ease-veil) calc(var(--d) + 220ms); }
.motion .freebie__cta[data-m].is-in .freebie__note { opacity: 1; }
#free-demo { scroll-margin-top: 1rem; }
/* Calm: the panel fades, the head dissolves whole, the steps 250ms apart, and the threads fade in instead of drawing
   (x.6 for the note); a press dims to .8 (chrome) */
.motion.calm .paystep[data-m]:not(:last-child)::after {
  transform: none; opacity: 0;
  transition: opacity 500ms var(--ease-veil) var(--ld, 0ms);
}
.motion.calm .paystep[data-m].is-link:not(:last-child)::after { opacity: 1; }
.calm .freebie__cta[data-m] .freebie__note { transition: opacity var(--t-calm) var(--ease-veil) calc(var(--d) + 130ms); }

/* ---------- Pricing: the decision zone, the quietest section that moves ---------- */
/* The head is a stage (the h2 settles whole, the promise pill rises last at 520ms; its shield never draws) and the three
   plans settle in reading order, by where they sit: on phones Business (order: -1) goes first. Each flag is part of its
   card, with no beat of its own. The prices never move by themselves: no counting, no sweeps of light */
/* Centred by margins, not a transform, so no rule (the settle, the failsafe) can ever shift it */
.price__flag { left: 0; right: 0; margin-inline: auto; width: max-content; transform: none; }
/* The only card that lifts, because it holds a button. Without JS the card keeps these transitions; with motion.js the
   settle list carries them with no delay, so a hover never waits for the entrance */
.price { transition: border-color var(--t-touch) var(--ease-touch), translate var(--t-touch) var(--ease-touch); }
@media (hover: hover) and (pointer: fine) {
  .price:hover { translate: 0 calc(-1 * var(--lift-hover)); border-color: var(--border-strong); }
  .price--featured:hover { border-color: color-mix(in srgb, var(--accent) 60%, var(--border)); }
}
/* Calm: the stage dissolves at x.6 (the pill at 312ms), the plans 70ms apart (shared); a hover changes the border only */
.calm .price:hover { translate: none; }

/* ---------- Guarantees: still on purpose (L0), like a signed page ---------- */
/* The rest between pricing and the team. No data-m or stage anywhere in #guarantees, so the observer never touches it:
   the four promises are simply there on arrival, in every mode (calm and no-JS are identical). The eyebrow's hairline is
   the static one (its pre-state needs a stage). Cards are not links: a mouse hover changes the border only (shared) */
.promise { transition: border-color var(--t-touch) var(--ease-touch); }
/* Phones: the icon sits beside its title and the text takes the card's full width, so it reads in fewer lines */
@media (max-width: 560px) {
  .promise { grid-template-columns: 40px 1fr; column-gap: .9rem; row-gap: .65rem; padding: 1.4rem; }
  .promise__icon { width: 40px; height: 40px; border-radius: 11px; grid-row: auto; align-self: center; }
  .promise__icon svg { width: 21px; height: 21px; }
  .promise h3 { align-self: center; margin: 0; }
  .promise p { grid-column: 1 / -1; }
}

/* ---------- Team: quiet monograms, never photos ---------- */
/* The head is a stage (the h2 settles whole) and the three members settle in turn. Each initial sits on a deep blue disc
   inside a hairline ring; the ring's arc draws itself clockwise from 12 o'clock as its card lands, and the letter simply
   arrives with the card (no wrapper of its own). The arc is a path that starts at the top (a circle would start at 3
   o'clock); its gradient, #monoRing in the defs before .team (never display: none), runs from deep blue at the lower left
   to light blue at the upper right. At rest (no JS, the failsafe, cards already passed) the ring is drawn, because the
   offset exists only under .motion. A mouse hover changes the border only (shared) */
.svg-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.ring__s1 { stop-color: #1d4ed8; }
.ring__s2 { stop-color: #2f6fed; }
.ring__s3 { stop-color: #7cc0ff; }
:root[data-theme="light"] .ring__s1 { stop-color: #1e40af; }
:root[data-theme="light"] .ring__s2 { stop-color: #1d4ed8; }
:root[data-theme="light"] .ring__s3 { stop-color: #3b82f6; }
.member { transition: border-color var(--t-touch) var(--ease-touch); }
.member__photo--placeholder {
  position: relative; border: 0;
  background: radial-gradient(100% 100% at 50% 22%, color-mix(in srgb, var(--accent) 22%, var(--surface)) 0%, color-mix(in srgb, var(--accent) 9%, var(--surface)) 72%);
  box-shadow: 0 14px 34px -20px color-mix(in srgb, var(--accent) 60%, transparent);
}
.member__photo--placeholder .ring { position: absolute; inset: 0; width: 100%; height: 100%; }
.ring__track { fill: none; stroke: color-mix(in srgb, var(--accent) 20%, transparent); stroke-width: 1.2; }
/* r = 49, so the arc is 307.9 long: one 308 dash and a 316 gap. Butt caps, so nothing shows before it starts */
.ring__arc { fill: none; stroke: url(#monoRing); stroke-width: 1.2; stroke-linecap: butt; stroke-dasharray: 308 316; }
/* Bold, so the letter reads as a monogram rather than a default avatar (700 against 400 is the owner's call on the phone) */
.member__photo--placeholder > span {
  position: relative; font-weight: 700; font-size: 2.5rem; letter-spacing: -.01em; line-height: 1; color: var(--text);
}
/* The arc draws 200ms after its card starts to settle (--d is the card's own delay) */
.motion:not(.calm) .member[data-m] .ring__arc {
  stroke-dashoffset: 308;
  transition: stroke-dashoffset 1600ms var(--ease-curtain) calc(var(--d) + 200ms);
}
/* (as specific as the pre-state, so it wins) */
.motion:not(.calm) .member[data-m].is-in .ring__arc { stroke-dashoffset: 0; }
/* Tablet: the third member sits centred under the pair instead of alone on the left */
@media (min-width: 561px) and (max-width: 960px) {
  .team > .member:last-child:nth-child(odd) { grid-column: 1 / -1; justify-self: center; width: calc(50% - .625rem); }
}
/* Phones: each member is a compact credits row (disc on the left, words on the right), so the team fits one screen */
@media (max-width: 560px) {
  .member { display: grid; grid-template-columns: 76px 1fr; column-gap: 1.1rem; align-items: start; text-align: left; padding: 1.25rem; }
  .member__photo { grid-row: 1 / span 3; width: 76px; height: 76px; margin: 0; }
  .member__photo--placeholder > span { font-size: 1.9rem; }
  .member h3 { margin-top: .2rem; }
  .member p { margin-inline: 0; max-width: none; }
}
/* The narrowest phones: a 64px disc and a tighter row keep the words at least 170px wide (172px at 320) */
@media (max-width: 359px) {
  .member { grid-template-columns: 64px 1fr; column-gap: .9rem; padding: 1.1rem 1rem; }
  .member__photo { width: 64px; height: 64px; }
  .member__photo--placeholder > span { font-size: 1.6rem; }
}
/* Calm: the head and the cards dissolve 70ms apart (shared) and each ring is already drawn, fading with its card */
.calm .ring__arc { stroke-dashoffset: 0; }

/* ---------- FAQ: the questions rise in quickly, then the only motion is yours ---------- */
/* The head is a stage (the h2 settles whole) and the five questions settle as chips: a short rise, 55ms apart. A tap
   unfolds the answer: motion.js moves an answer body's height (#faq unfold module; about 320ms open, 220ms close, and a
   second tap reverses from where it is), the text settles just behind it and the plus turns to a minus. .is-open is the
   visible state, so on a close the border and chevron turn back at once while [open] waits for the height to close.
   Without motion.js (no .m-unfold) it is a native <details>, with the chevron driven by [open]. The press is a tint,
   not a scale; the settle list carries it (140ms) and the border (420ms) with no delay */
#faq .faq__item[data-m] { --rise: var(--rise-chip); --stagger-item: var(--stagger-chip); }
.faq__item summary { -webkit-tap-highlight-color: transparent; }
.faq__item:has(> summary:active) { background-color: var(--surface-2); }
/* The keyboard ring wraps the whole card, not just the question */
@supports selector(:has(*)) {
  .faq__item summary:focus-visible { outline: none; }
  .faq__item:has(> summary:focus-visible) { outline: 2px solid var(--accent-2); outline-offset: 3px; }
}
.faq__chev::before, .faq__chev::after { transition: transform 400ms var(--ease-touch), opacity 300ms var(--ease-veil); }
/* JS mode: the body clips while its height moves (so the answer loses its pull-up margin, which the clip would cut) */
.faq.m-unfold .faq__body { overflow: hidden; }
.faq.m-unfold .faq__body > p { margin-top: 0; }
.faq.m-unfold .faq__item[open]:not(.is-open) { border-color: var(--border); }
.faq.m-unfold .faq__item.is-open { border-color: var(--border-strong); }
.faq.m-unfold .faq__item .faq__chev::after { transform: translate(-50%, -50%) rotate(90deg); }
.faq.m-unfold .faq__item.is-open .faq__chev::after { transform: translate(-50%, -50%) rotate(0); }
/* Calm: the questions dissolve 70ms apart (shared); a tap opens at once and the answer fades in, and the chevron's
   vertical bar fades out instead of turning. The press tint and the border stay */
.calm .faq.m-unfold .faq__item.is-open .faq__body > p { animation: faq-fade 300ms var(--ease-veil) both; }
@keyframes faq-fade { from { opacity: 0; } }
.calm .faq.m-unfold .faq__item.is-open .faq__chev::after { transform: translate(-50%, -50%) rotate(90deg); opacity: 0; }

/* ---------- Scene 02 (the finale): the lights come on, the camera never moves ---------- */
/* A taller band that exposes from black (the shared gate) on the cafe across the street. Over the poster (frame 00,
   lights off) sit the lit frame (.cine__lit, the reel's own last frame, 23) and the reel's canvas (one sprite sheet per
   orientation): the lamps switch on under the finger. motion.js writes --lit (a crossfade, and all there is in calm,
   lite or after a reel failure) until the reel's canvas takes over (.cine-band--finale module) */
.cine-band--finale { min-height: clamp(420px, 80vh, 760px); min-height: clamp(420px, 80svh, 760px); }
/* Grain over the canvas: plain and faint, as over the story's film */
.cine-band--finale::after { mix-blend-mode: normal; opacity: .05; }
/* Dark under the line (bottom left) and at the foot, open across the window */
.cine__shade--finale {
  background:
    radial-gradient(60% 55% at 12% 100%, rgba(7, 9, 14, .5), transparent 70%),
    linear-gradient(180deg, rgba(7, 9, 14, .38) 0%, rgba(7, 9, 14, .08) 32%, rgba(7, 9, 14, .12) 56%, rgba(7, 9, 14, .7) 82%, rgba(7, 9, 14, .94) 100%);
}
@media (max-width: 600px) {
  .cine__shade--finale {
    background: linear-gradient(180deg, rgba(7, 9, 14, .3) 0%, rgba(7, 9, 14, .06) 28%, rgba(7, 9, 14, .1) 52%, rgba(7, 9, 14, .78) 76%, rgba(7, 9, 14, .95) 100%);
  }
}
/* The lit frame: the end state without JS (and after the failsafe); with motion it follows --lit */
.cine__lit { opacity: 1; }
.motion .cine__lit { opacity: var(--lit, 0); }
/* The page's only light sweep: once, across the blue phrase, as the last lamp comes on. The band of light starts and ends
   off the text, so the resting state is unchanged. motion.js adds .is-glint and removes it at 2400ms */
.grad.is-glint {
  background-image: linear-gradient(100deg, transparent 40%, rgba(255, 255, 255, .75) 50%, transparent 60%), var(--accent-grad);
  background-size: 250% 100%, 100% 100%;
  animation: glint 2400ms var(--ease-veil) 1 backwards;
}
@keyframes glint { from { background-position: 100% 0, 0 0; } to { background-position: 0% 0, 0 0; } }
.calm .grad.is-glint { animation: none; }

/* ---------- Contact: a calm landing, then everything answers the visitor ---------- */
/* The panel fades (surface) and its corner light warms up last, 500ms after it (x.6 in calm): the page's only warm-up.
   The copy is a stage (the h2 settles whole), the two rows settle as chips from 520ms (#contact module) and the form
   arrives as one block, no sheen. After that: a line under the field in use, missing fields turn rose (no shake; main.js
   focuses the first), a fine line runs only while a request is pending and a tick draws on success. At rest (no JS, the
   failsafe) the light is full */
.cta-section { padding-bottom: clamp(4.5rem, 10vw, 7.5rem); }
.cta { position: relative; isolation: isolate; background: var(--surface); }
.cta::before {
  content: ""; position: absolute; left: 0; top: 0; width: 70%; height: 80%; z-index: -1; pointer-events: none;
  border-top-left-radius: inherit;
  background: radial-gradient(171% 175% at 0 0, color-mix(in srgb, var(--accent) 16%, transparent), transparent 55%);
}
.motion .cta[data-m]::before { opacity: 0; transition: opacity 1600ms var(--ease-veil) calc(500ms * var(--stage-k)); }
.motion .cta[data-m].is-in::before { opacity: 1; }
/* Rows: one-tap links (the text unchanged) with a 44px target and an underline that draws on hover or focus */
.cta__contacts { gap: .15rem; }
.cta__contacts > [data-m] { --rise: var(--rise-chip); --stagger-item: var(--stagger-chip); }
.cta__link {
  display: flex; align-items: center; gap: .6rem; min-height: 44px; padding-block: .5rem;
  color: var(--muted); min-width: 0; overflow-wrap: anywhere; -webkit-tap-highlight-color: transparent;
  transition: color var(--t-touch) var(--ease-touch), opacity var(--t-press);
}
.cta__link span { position: relative; }
.cta__link span::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -2px; height: 1px; background: var(--accent-grad);
  transform: scaleX(0); transform-origin: 0 50%; transition: transform var(--t-touch) var(--ease-touch);
}
@media (hover: hover) and (pointer: fine) {
  .cta__link:hover { color: var(--text); }
  .cta__link:hover span::after { transform: scaleX(1); }
}
.cta__link:focus-visible span::after { transform: scaleX(1); }
.cta__link:active { opacity: .7; }
@media (max-width: 560px) {
  .cta__link { align-items: flex-start; }
  .cta__link svg { margin-top: .15em; }
}
/* Fields: the label turns blue and a line draws under the field in use; a missing field turns rose */
.field { position: relative; }
.field input, .field textarea { display: block; }
.field label { transition: color 180ms var(--ease-touch); }
.field:focus-within label { color: var(--accent-2); }
.field::after {
  content: ""; position: absolute; left: 12px; right: 12px; bottom: 0; height: 1px; background: var(--accent-grad);
  transform: scaleX(0); transform-origin: 0 50%; transition: transform var(--t-touch) var(--ease-touch); pointer-events: none;
}
.field:focus-within::after { transform: scaleX(1); }
.field [aria-invalid=true], .field [aria-invalid=true]:focus { border-color: #fb7185; }
:root[data-theme="light"] .field [aria-invalid=true],
:root[data-theme="light"] .field [aria-invalid=true]:focus { border-color: #e11d48; }
/* Pending: the one allowed loop, and only while a request is out (main.js sets .is-sending on the form) */
.form__wait { display: block; position: relative; height: 1px; margin: .55rem 14% 0; overflow: hidden; opacity: 0; transition: opacity 200ms; }
.form__wait::before {
  content: ""; position: absolute; inset: 0 auto 0 0; width: 34%; background: var(--accent-grad); transform: translateX(-100%);
}
.is-sending .form__wait { opacity: 1; }
.is-sending .form__wait::before { animation: wait 1.1s linear infinite; }
@keyframes wait { to { transform: translateX(300%); } }
/* Success: a tick draws before "Thanks, ..." */
.note__tick {
  display: inline-block; width: 1.1em; height: 1.1em; vertical-align: -.2em; margin-right: .4em;
  fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 20 26; stroke-dashoffset: 22; animation: tick-in 600ms var(--ease-curtain) 150ms forwards;
}
@keyframes tick-in { to { stroke-dashoffset: 0; } }
/* Calm: the panel fades and its light still warms (1100ms from 300ms); rows and form dissolve (shared). The lines
   fade instead of drawing, a press dims to .8, the tick shows drawn and only the "Sending..." text marks a request */
.calm .cta[data-m]::before { transition-duration: 1100ms; }
.calm .cta__link span::after { transform: none; opacity: 0; transition: opacity 180ms var(--ease-veil); }
@media (hover: hover) and (pointer: fine) { .calm .cta__link:hover span::after { opacity: 1; } }
.calm .cta__link:focus-visible span::after { opacity: 1; }
.calm .cta__link:active { opacity: .8; }
.calm .field::after { transform: none; opacity: 0; transition: opacity 180ms; }
.calm .field:focus-within::after { opacity: 1; }
.calm .note__tick { animation: none; stroke-dashoffset: 0; }
.calm .form__wait { display: none; }

/* ---------- Pay: still on purpose (L0), a counter where money changes hands ---------- */
/* Returning clients often land here from an invoice link, so it is readable the instant the page paints: no .reveal,
   no data-m or stage, no sheen, no lift. Its landing clears the header through the global scroll-padding alone. The
   eyebrow's hairline is the static one; the only motion is the shared press (calm: a dim to .8, shared) */
#pay { padding-block: clamp(3.5rem, 9vw, 6.5rem); }
#pay .btn--primary::after { content: none; }
@media (hover: hover) and (pointer: fine) { #pay .btn--primary:hover { translate: none; } }
/* The lock note reads left-aligned, its icon on the first line */
#pay .pay__note { justify-content: flex-start; align-items: flex-start; text-align: left; }
#pay .pay__note svg { margin-top: .28em; }

/* ---------- Failsafe: every hidden pre-state (builds 2-16 included), shown at once ---------- */
/* Set by the head script when motion.js has not started within 3s, or by motion.js on any error.
   Never use [data-m] * here */
.m-fallback :is([data-m=settle], [data-m=surface], [data-m=veil], .reveal, .ln__in, .num__in,
  [data-m=gate] .cine__frame, .trust__label, .trust__row em, #why .stat, .freebie__note, .cine__lit, .step__num, .cta),
.m-fallback [data-m-stage] > * { opacity: 1 !important; transform: none !important; transition: none !important; }
.m-fallback :is(.eyebrow, .trust__label)::before,
.m-fallback .paystep::after,
.m-fallback .step::after,
.m-fallback .cta::before { opacity: 1 !important; transform: none !important; transition: none !important; }
.m-fallback :is([data-m=inscribe], .tick, .ring) * { stroke-dashoffset: 0 !important; transition: none !important; }
