/* ==========================================================
   Joe Le — motion layer
   Real content only. No decorative effects.
   Everything is gated behind .js-m (set in <head> only when JS
   runs and the visitor hasn't asked for reduced motion), so
   no-JS and reduced-motion visitors get the static site.
   ========================================================== */
.m-noanim *,.m-noanim *::before,.m-noanim *::after{transition:none!important}
:root{--ease:cubic-bezier(.16,1,.3,1);--ease-io:cubic-bezier(.65,0,.35,1)}

/* ---------- square buttons + square tags (always on) ---------- */
.btn,.nav-links .btn,.navcall{border-radius:3px}
a.btn:focus-visible,button.btn:focus-visible{border-radius:3px}
form.book input,form.book textarea{border-radius:3px}
.g-rate{border-radius:3px}
.dash-tag,.mock .badge{border-radius:4px}
.btn svg{transition:transform .3s var(--ease)}
.btn:hover svg{transform:translateX(2px)}

/* numbers never jitter while counting */
.metabar b,.pcell b,.num-grid b,.story-stats b,.case-stats b,.dash-tag b,.mock .badge b{font-variant-numeric:tabular-nums}

/* ---------- nav settles once you scroll ---------- */
nav{transition:background .3s,box-shadow .3s,border-color .3s}
nav.m-scrolled{background:rgba(251,250,247,.94);box-shadow:0 8px 24px -18px rgba(24,22,19,.35)}

/* ---------- pre-ready guard (prevents a flash before split) ---------- */
.js-m:not(.m-ready) .hero h1,
.js-m:not(.m-ready) .ty h1,
.js-m:not(.m-ready) .doc-head h1{opacity:0}

/* ---------- headline word reveal ---------- */
.m-w{display:inline-block;overflow:hidden;vertical-align:top;padding:0 .02em .12em;margin:0 -.02em -.12em}
.m-w>span{display:inline-block;transform:translate3d(0,108%,0);opacity:0;transition:transform 1.05s var(--ease),opacity .6s ease;transition-delay:calc(var(--wi,0) * 55ms + var(--wd,0ms))}
.m-go .m-w>span{transform:none;opacity:1}

/* ---------- hero: supporting copy follows the headline ---------- */
.js-m .hero .m-follow{opacity:0;transform:translateY(16px);filter:blur(4px);transition:opacity .9s var(--ease),transform 1s var(--ease),filter .9s var(--ease);transition-delay:var(--d,0ms)}
.js-m.m-ready .hero .m-follow{opacity:1;transform:none;filter:none}

/* ---------- scroll reveals (upgrade of the existing .reveal) ---------- */
.js-m .reveal{opacity:0;transform:translateY(30px);filter:blur(6px);transition:opacity .95s var(--ease),transform 1.1s var(--ease),filter .95s var(--ease);transition-delay:var(--d,0ms)}
.js-m .reveal.in{opacity:1;transform:none;filter:none}
/* hero columns are handled by the boot sequence instead */
.js-m .hero .reveal.in:not(.metabar){filter:none}

/* children of a revealed group arrive one after another */
.js-m .m-kids>*{opacity:0;transform:translateY(22px);transition:opacity .85s var(--ease),transform 1s var(--ease);transition-delay:calc(var(--ki,0) * 90ms + 120ms)}
.js-m .in.m-kids>*,.js-m .in .m-kids>*{opacity:1;transform:none}

/* ---------- media: screenshots settle into frame ---------- */
.js-m .dashshot img,.js-m .framed img,.js-m .browser img,.js-m .g-thumb img,.js-m .story-photo img{transition:transform 1.4s var(--ease),opacity 1s var(--ease)}
.js-m .reveal:not(.in) .dashshot img,
.js-m .reveal:not(.in) .framed img,
.js-m .reveal:not(.in) .case-media .browser img{transform:scale(1.07)}
.js-m .reveal:not(.in) .story-photo img{transform:scale(.96) translateY(18px);opacity:0}

/* case studies: screenshot comes in from its own side, stats follow */
.js-m .case.reveal{filter:none;transform:none}
.js-m .case .case-media{transition:transform 1.15s var(--ease),opacity 1s var(--ease)}
.js-m .case.reveal:not(.in) .case-media{transform:translateX(-46px);opacity:0}
.js-m .case.rev.reveal:not(.in) .case-media{transform:translateX(46px)}
.js-m .case .shot2{transition:transform 1.1s var(--ease) .45s,opacity .9s var(--ease) .45s}
.js-m .case.reveal:not(.in) .shot2{transform:translateY(36px);opacity:0}
.js-m .case .case-media .browser{animation:none}

/* ---------- hero product shot: the dashboard boots in ---------- */
.js-m .hero .dash,.js-m .hero .mock{perspective:1400px}
.js-m .hero .dash-frame,.js-m .hero .mock>.browser,.js-m .hero .mock>.framed{
  transform:rotateX(16deg) translate3d(0,70px,0) scale(.94);opacity:0;transform-origin:50% 100%;
  transition:transform 1.4s var(--ease) .25s,opacity .9s ease .25s;will-change:transform}
.js-m.m-ready .hero .dash-frame,.js-m.m-ready .hero .mock>.browser,.js-m.m-ready .hero .mock>.framed{transform:none;opacity:1}
.js-m .hero .browser{animation:none}
/* the screen paints top to bottom, like the app loading */
.js-m .hero .dash-frame img,.js-m .hero .mock .browser img,.js-m .hero .mock .framed img{clip-path:inset(0 0 100% 0);transition:clip-path 1.25s var(--ease-io) .75s}
.js-m.m-ready .hero .dash-frame img,.js-m.m-ready .hero .mock .browser img,.js-m.m-ready .hero .mock .framed img{clip-path:inset(0 0 0 0)}
.js-m .hero .mock .browser-bar .url{opacity:0;transition:opacity .6s ease .6s}
.js-m.m-ready .hero .mock .browser-bar .url{opacity:1}
/* the figure tag lands after the screen */
.js-m .hero .dash-tag,.js-m .hero .mock .badge{opacity:0;transform:translateY(14px) scale(.92);transition:transform .9s var(--ease) 1.55s,opacity .6s ease 1.55s}
.js-m.m-ready .hero .dash-tag,.js-m.m-ready .hero .mock .badge{opacity:1;transform:none}
.js-m .hero .dash-cap,.js-m .hero .mock figcaption{opacity:0;transition:opacity .8s ease 1.9s}
.js-m.m-ready .hero .dash-cap,.js-m.m-ready .hero .mock figcaption{opacity:1}
/* pointer tilt target */
.m-tilt-in{transition:transform .5s var(--ease)}

/* annotation: a square accent box drawn around the real ROAS line */
.m-screen{position:relative;display:block}
.m-mark{position:absolute;pointer-events:none;border:2.5px solid var(--accent);clip-path:inset(0 100% 0 0);transition:clip-path .9s var(--ease-io) 2.2s}
.m-ready .m-mark{clip-path:inset(0 0 0 0)}

/* ---------- cards that come alive ---------- */
.m-card{transform-style:preserve-3d;will-change:transform}
.js-m .svc,.js-m .gcard,.js-m .pcard,.js-m .step,.js-m .feat{transition:transform .8s var(--ease),box-shadow .4s var(--ease),border-color .3s,opacity .95s var(--ease),filter .95s var(--ease);transition-delay:var(--d,0ms)}
.js-m .svc.in:hover,.js-m .gcard.in:hover,.js-m .pcard.in:hover,.js-m .feat.in:hover{box-shadow:0 30px 60px -34px rgba(24,22,19,.45)}
.svc-ic{transition:background .35s var(--ease),color .35s var(--ease),transform .5s var(--ease)}
.svc:hover .svc-ic{background:var(--accent);color:#fff;transform:translateY(-2px)}
.feat .fi{transition:background .35s,color .35s}
.feat:hover .fi{background:var(--accent);color:#fff}
.pcard{transition:border-color .3s}
.pcard:hover{border-color:var(--accent)}
.g-thumb img{transition:transform 1.2s var(--ease)}
.gcard:hover .g-thumb img{transform:scale(1.045)}
.js-m .framed,.js-m .dashshot{transition:transform .8s var(--ease),box-shadow .5s var(--ease),opacity .95s var(--ease),filter .95s var(--ease);transition-delay:var(--d,0ms)}
.js-m .framed:hover,.js-m .dashshot:hover{transform:translateY(-4px)}

/* process: each step's top rule fills in sequence = the order of work */
.step{overflow:hidden}
.step::before{content:"";position:absolute;left:0;top:0;height:2px;width:100%;background:var(--accent);transform:scaleX(0);transform-origin:0 50%;transition:transform 1.1s var(--ease-io);transition-delay:calc(var(--d,0ms) + 350ms)}
.step.in::before{transform:scaleX(1)}
.step:hover{border-color:rgba(255,255,255,.16)}
.step .n{transition:letter-spacing .5s var(--ease)}
.step:hover .n{letter-spacing:.2em}

/* why-list ticks pop on arrival */
.js-m .why-item .tick{transform:scale(0);transition:transform .7s cubic-bezier(.34,1.56,.64,1);transition-delay:calc(var(--d,0ms) + 250ms)}
.js-m .why-item.in .tick{transform:scale(1)}

/* ---------- client roster: real names, slow continuous run ---------- */
.roster.m-marq .roster-in{flex-wrap:nowrap}
.roster.m-marq .m-track-wrap{flex:1;min-width:0;overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent);mask-image:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent)}
.roster.m-marq .m-track{display:flex;width:max-content;animation:m-marq 38s linear infinite}
.roster.m-marq .m-track ul{flex-wrap:nowrap;justify-content:flex-start;padding-right:30px}
.roster.m-marq li{white-space:nowrap}
.roster.m-marq:hover .m-track{animation-play-state:paused}
@keyframes m-marq{to{transform:translateX(-50%)}}

/* ---------- thank-you: the tick draws itself ---------- */
.js-m .ty .mark{transform:scale(.6);opacity:0;transition:transform .8s cubic-bezier(.34,1.56,.64,1),opacity .4s}
.js-m.m-ready .ty .mark{transform:none;opacity:1}
.js-m .ty .mark polyline{stroke-dasharray:30;stroke-dashoffset:30;transition:stroke-dashoffset .7s var(--ease-io) .45s}
.js-m.m-ready .ty .mark polyline{stroke-dashoffset:0}

@media(max-width:920px){
  .roster.m-marq .roster-in{flex-wrap:wrap}
  .roster.m-marq .m-track-wrap{flex:1 1 100%}
  .js-m .case.reveal:not(.in) .case-media,.js-m .case.rev.reveal:not(.in) .case-media{transform:translateY(30px)}
}
@media(prefers-reduced-motion:reduce){
  .roster.m-marq .m-track{animation:none}
  .m-w>span{transform:none;opacity:1;transition:none}
}
