/* ==========================================================================
   Motion
   Everything animated lives here, so the whole system can be reasoned about
   — or disabled — in one place.

   Two rules govern all of it:
   1. Only transform and opacity are animated. Both are compositor-only, so
      nothing here triggers layout or paint.
   2. Nothing loops. Continuous motion reads as decoration and this is a
      professional services site, not a showreel. The one deliberate
      exception is the trust ticker, which pauses on hover and falls back
      to a static row under reduced motion.

   The reduced-motion block at the bottom neutralises every rule above it.
   ========================================================================== */

/* --- Scroll reveal --------------------------------------------------------
   Hidden only when JS is present, so a no-JS visitor still sees everything.
   main.js adds .is-visible once and then stops observing — this is a one-shot
   entrance, not a scroll-linked effect that replays. */

.js [data-reveal] {
  opacity: 0;
  transform: translateY(18px);
  transition:
    opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
}
.js [data-reveal].is-visible {
  opacity: 1;
  transform: none;
}

/* Variants for elements where a vertical rise reads wrong */
.js [data-reveal="fade"]        { transform: none; }
.js [data-reveal="left"]        { transform: translateX(-22px); }
.js [data-reveal="right"]       { transform: translateX(22px); }
.js [data-reveal].is-visible    { transform: none; }

/* --- Hero entrance --------------------------------------------------------
   The one piece of load-time motion. Staggered so the eye lands on the
   eyebrow, then the headline, rather than the whole block arriving at once.

   Deliberately CSS-only, with no .js or JS-applied class gating it. The hero
   headline is the most important text on the site, and anything that makes
   its visibility depend on a script is a single point of failure — a blocked
   script, or a requestAnimationFrame suspended because the page opened in a
   background tab, would leave it invisible. animation-fill-mode: both holds
   the start state through the delay, so the stagger still works. */

.hero__content > * {
  animation: hero-in 760ms var(--ease-out) both;
}
.hero__content > *:nth-child(1) { animation-delay:  60ms; }
.hero__content > *:nth-child(2) { animation-delay: 150ms; }
.hero__content > *:nth-child(3) { animation-delay: 260ms; }
.hero__content > *:nth-child(4) { animation-delay: 370ms; }

@keyframes hero-in {
  from { opacity: 0; transform: translateY(22px); }
  to   { opacity: 1; transform: none; }
}

/* The photograph settles as the copy arrives. 1.06 -> 1 over a long duration
   reads as depth rather than as a zoom. */
.hero__media img {
  animation: hero-img 1600ms var(--ease-out) both;
}
@keyframes hero-img {
  from { transform: scale(1.06); }
  to   { transform: none; }
}

/* The consultation card rises onto the frame just after the buttons. */
.hero__card { animation: hero-in 760ms var(--ease-out) 520ms both; }

/* --- Stat rules -----------------------------------------------------------
   The accent rule above each figure draws itself in from the left. Replaces
   a static border-top, so the figure has something to arrive with. */

.stat {
  position: relative;
  border-top: 0;
}
.stat::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background: var(--blue-500);
  transform: scaleX(1);
  transform-origin: left;
  transition: transform 620ms var(--ease-out);
}

/* Held back only when the stat is itself a reveal target, so the rule is
   always drawn for no-JS visitors. */
.js .stat[data-reveal]::before { transform: scaleX(0); }
.js .stat[data-reveal].is-visible::before {
  transform: scaleX(1);
  transition-delay: 120ms;
}

/* --- Navigation -----------------------------------------------------------
   Hover draws an underline from the left. The current page keeps its solid
   inset underline, so the two states stay distinguishable. */

.nav__link { position: relative; }
.nav__link::after {
  content: "";
  position: absolute;
  left: var(--sp-4);
  right: var(--sp-4);
  bottom: 0.55rem;
  height: 1.5px;
  background: var(--blue-400);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur) var(--ease-out);
}
.nav__link:hover::after { transform: scaleX(1); }
.nav__link[aria-current="page"]::after { display: none; }

/* --- Service rows ---------------------------------------------------------
   An accent bar grows down the left edge on hover. Sits inside the row's own
   padding so nothing around it moves. */

.service-row { position: relative; }
.service-row::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--blue-500);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform var(--dur) var(--ease-out);
}
.service-row:hover::before,
.service-row:focus-visible::before { transform: scaleY(1); }

/* --- Cards ----------------------------------------------------------------
   A 3px lift. transform does not affect layout, so neighbours never move. */

.card {
  transition:
    border-color var(--dur) var(--ease-out),
    box-shadow var(--dur) var(--ease-out),
    transform var(--dur) var(--ease-out);
}
.card:hover { transform: translateY(-3px); }
.section--dark .card:hover { transform: translateY(-3px); }

/* --- Trust ticker ---------------------------------------------------------
   The one deliberate loop: the credential strip drifts continuously and
   pauses on hover. Slow enough to read. */

.trust__track { animation: trust-marquee 42s linear infinite; }
.trust__ticker:hover .trust__track { animation-play-state: paused; }

@keyframes trust-marquee {
  to { transform: translateX(-100%); }
}

/* --- Footer and inline links ---------------------------------------------- */

.site-footer a,
.mobile-nav__sub a {
  transition: color var(--dur-fast) var(--ease-out);
}

/* Underline grows from the left on text links carrying an arrow */
.link-arrow { position: relative; }
.link-arrow::after {
  content: "";
  position: absolute;
  left: 0;
  right: 1.25rem;
  bottom: -3px;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur) var(--ease-out);
}
.link-arrow:hover::after { transform: scaleX(1); }

/* --- Accordion ------------------------------------------------------------
   The panel height is animated by grid-template-rows in components.css.
   Fade the contents alongside it so text does not appear to slide out of a
   hard edge. */

.accordion__panel > div > * {
  opacity: 0;
  transition: opacity var(--dur) var(--ease-out);
}
.accordion__panel[data-open="true"] > div > * {
  opacity: 1;
  transition-delay: 90ms;
}

/* --- Reduced motion -------------------------------------------------------
   Neutralises every rule above. Elements land in their final state rather
   than being left mid-animation and invisible. */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }

  /* The hero selectors carry no .js prefix, matching motion.css above — the
     entrance is CSS-only, so it must be neutralised for a reduced-motion
     visitor whether or not scripts ran. */
  [data-reveal],
  .hero__content > *,
  .hero__card,
  .accordion__panel > div > * {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
  }

  /* Ticker becomes a static wrapped row; the duplicate track only exists
     for the loop, so it is hidden rather than shown twice. */
  .trust__track {
    animation: none !important;
    transform: none !important;
    flex-wrap: wrap;
    min-width: 0;
  }
  .trust__track[aria-hidden="true"] { display: none !important; }

  .hero__media img { transform: none !important; animation: none !important; }

  .stat::before,
  .js .stat[data-reveal]::before { transform: scaleX(1) !important; }

  .card:hover { transform: none !important; }

  .intro { display: none !important; }
}


/* --- Mobile intro -----------------------------------------------------------
   Letters rise in one after another (d, w, A), the tagline follows, a short
   hold, then the navy panel lifts away with a blue panel trailing just behind
   it, and the page is there. About two seconds, once per visit, phones only.
   Every step is transform or opacity. */

html.intro-on .intro__piece { animation: intro-rise 700ms var(--ease-out) both; }
html.intro-on .intro__piece--d { animation-delay: 150ms; }
html.intro-on .intro__piece--w { animation-delay: 260ms; }
html.intro-on .intro__piece--a { animation-delay: 370ms; }
html.intro-on .intro__piece--tag { animation: intro-fade 600ms var(--ease-out) 720ms both; }
html.intro-on .intro__logo { animation: intro-logo-out 420ms cubic-bezier(0.55, 0, 1, 0.45) 1450ms both; }
html.intro-on .intro::after { animation: intro-lift 640ms cubic-bezier(0.7, 0, 0.2, 1) 1500ms both; }
html.intro-on .intro::before { animation: intro-lift 640ms cubic-bezier(0.7, 0, 0.2, 1) 1600ms both; }
html.intro-on .intro { animation: intro-done 1ms linear 2300ms both; }

@keyframes intro-rise { from { opacity: 0; transform: translateY(26%); } }
@keyframes intro-fade { from { opacity: 0; transform: translateY(10%); } }
@keyframes intro-logo-out { to { opacity: 0; transform: translateY(-18%); } }
@keyframes intro-lift { to { transform: translateY(-100%); } }
@keyframes intro-done { to { visibility: hidden; } }

/* The home hero rises in as the panels lift, instead of finishing unseen. */
html.intro-on .hero__content > *:nth-child(1) { animation-delay: 1760ms; }
html.intro-on .hero__content > *:nth-child(2) { animation-delay: 1850ms; }
html.intro-on .hero__content > *:nth-child(3) { animation-delay: 1960ms; }
html.intro-on .hero__content > *:nth-child(4) { animation-delay: 2070ms; }
html.intro-on .hero__card { animation-delay: 2220ms; }

/* --- Page transitions --------------------------------------------------------
   Cross-document view transitions: the outgoing page fades and settles, the
   incoming page rises into place, and the floating header is named so it is
   carried across without crossfading. Browsers without support navigate as
   they always have. Reduced motion turns the whole thing off. */

@view-transition { navigation: auto; }

@keyframes vt-out { to { opacity: 0; transform: translateY(-6px); } }
@keyframes vt-in { from { opacity: 0; transform: translateY(14px); } }

::view-transition-old(root) { animation: 180ms cubic-bezier(0.4, 0, 1, 1) both vt-out; }
::view-transition-new(root) { animation: 360ms var(--ease-out) both vt-in; }

.site-header { view-transition-name: site-header; }
::view-transition-old(site-header),
::view-transition-new(site-header) { animation: none; mix-blend-mode: normal; }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}
