/* ============================================================
   Plain CSS — deliberately NOT processed by Tailwind.

   The Tailwind browser build only compiles inline <style type="text/tailwindcss">
   blocks; it ignores <link> entirely. None of these classes need Tailwind
   variants, so they are written as ordinary CSS and loaded with a normal
   <link>. That keeps the HTML free of inline styles AND makes them
   render-blocking, so `.reveal` hides the hero before the first paint.
   ============================================================ */

body {
  font-family: "Switzer", ui-sans-serif, system-ui, sans-serif;
}

/* ---------- Marquees ----------
   NOTE: longhands, never the `animation` shorthand. Tailwind v4 emits its
   utilities inside `@layer utilities`, and unlayered CSS (this file) always
   beats layered CSS — so a shorthand here would silently reset the
   per-element [animation-delay:…] classes to 0s. Leaving animation-delay
   undeclared makes Tailwind the only source of it. */

.animate-marquee {
  animation-name: marquee;
  animation-duration: 38s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
@keyframes marquee {
  from {
    transform: translate3d(0, 0, 0);
  }
  to {
    transform: translate3d(-50%, 0, 0);
  }
}

.animate-marquee-reverse {
  animation-name: marquee-reverse;
  animation-duration: 30s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
@keyframes marquee-reverse {
  from {
    transform: translate3d(-50%, 0, 0);
  }
  to {
    transform: translate3d(0, 0, 0);
  }
}

/* Gradient fade-out on both sides of the marquee */
.fade-x {
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 10%,
    #000 90%,
    transparent 100%
  );
  mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 10%,
    #000 90%,
    transparent 100%
  );
}

/* Same idea, but a wider fade — for the narrow avatar rows inside a card */
.fade-x-strong {
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 20%,
    #000 80%,
    transparent 100%
  );
  mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 20%,
    #000 80%,
    transparent 100%
  );
}

/* ============ INTRO ============
   Timeline lives in assets/js/intro.js — keep the two in sync. */

/* Scroll lock — added by intro.js, removed once the logo is gone.
   Applied via JS so the page can never stay locked if JS fails. */
.intro-lock,
.intro-lock body {
  overflow: hidden;
}

/* Held while the hero video buffers. Every intro animation sits frozen on
   its first keyframe (they all use fill-mode: both, so that is the hidden
   state) until intro.js drops this class. Also JS-applied, so a JS failure
   simply plays the intro immediately rather than freezing the page. */
.intro-wait #intro,
.intro-wait .intro-letter,
.intro-wait .reveal {
  animation-play-state: paused;
}

/* Each letter drifts in from its own direction, set per path via --tx/--ty */
.intro-letter {
  opacity: 0;
  animation-name: letter-in;
  animation-duration: 0.95s;
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  animation-fill-mode: both;
}
@keyframes letter-in {
  from {
    opacity: 0;
    transform: translate(var(--tx, 0), var(--ty, 0));
    filter: blur(3px);
  }
  to {
    opacity: 1;
    transform: translate(0, 0);
    filter: blur(0);
  }
}

@keyframes intro-out {
  from {
    opacity: 1;
    filter: blur(0);
    transform: scale(1);
  }
  to {
    opacity: 0;
    filter: blur(6px);
    transform: scale(1.04);
  }
}

/* Hero components blur into focus as the logo leaves.
   Longhands again — see the marquee note above. */
.reveal {
  opacity: 0;
  animation-name: blur-in;
  animation-duration: 1.1s;
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  animation-fill-mode: both;
}
@keyframes blur-in {
  from {
    opacity: 0;
    filter: blur(14px);
    transform: translateY(26px);
  }
  to {
    opacity: 1;
    filter: blur(0);
    transform: translateY(0);
  }
}

/* Drop the finished filters entirely — a lingering blur(0) would still act as a
   backdrop root and kill the nav's backdrop-blur. Deliberately does not reset
   `transform`, so the .gpu layer promotion below survives. */
.intro-done #intro {
  display: none;
}
.intro-done .reveal {
  animation: none;
  opacity: 1;
  filter: none;
}

/* Keeps an element on its own GPU layer so it is rasterised once instead of
   being repainted with every video frame / fractional scroll offset.
   Never put this on an ancestor of a backdrop-filter (it becomes a backdrop
   root and kills the effect) — so the header/nav is deliberately excluded. */
.gpu {
  will-change: transform;
  transform: translateZ(0);
  backface-visibility: hidden;
}

/* ============ SERVICES ACCORDION ============
   Toggled by assets/js/services.js, which flips [data-open] on the row.
   The 0fr → 1fr grid trick animates to the content's natural height,
   which max-height cannot do without hardcoding a guess. */

.svc-panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}
.svc-panel > * {
  overflow: hidden;
}
[data-open] .svc-panel {
  grid-template-rows: 1fr;
}

/* the vertical stroke of the + collapses, leaving a − */
.svc-plus-v {
  transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
[data-open] .svc-plus-v {
  transform: scaleY(0);
}

/* dimmed → full white on the open row (sage background) */
.svc-num {
  color: rgb(255 255 255 / 0.5);
  transition: color 0.3s ease;
}
[data-open] .svc-num {
  color: rgb(255 255 255);
}

@media (prefers-reduced-motion: reduce) {
  .animate-marquee,
  .animate-marquee-reverse {
    animation: none;
  }
  .svc-panel,
  .svc-plus-v,
  .svc-num {
    transition: none;
  }
  #intro {
    display: none;
  }
  .reveal {
    animation: none;
    opacity: 1;
    filter: none;
  }
}
