/*
 * Motion system.
 *
 * Three rules make this feel native rather than "a WordPress site with fades":
 *
 * 1. Only `transform` and `opacity` are ever animated. Both are composited on
 *    the GPU, so nothing here can trigger layout or paint mid-animation — that
 *    is the entire difference between 60fps and jank.
 *
 * 2. Nothing is hidden by CSS alone. The hidden state is scoped to
 *    [data-hlm-reveal="pending"], an attribute only JavaScript sets. If the
 *    script fails, is blocked, or never runs, every element renders normally
 *    instead of leaving a blank page.
 *
 * 3. Easing is expo-out, not ease-in-out. Fast departure and a long settle is
 *    what reads as "designed" rather than "animated".
 */

:root {
  --hlm-reveal-duration: 900ms;
  --hlm-reveal-stagger: 90ms;
  --hlm-reveal-shift: 28px;
}

/* Hidden state — applied only once JS has claimed the element. */
[data-hlm-reveal='pending'] {
  opacity: 0;
  will-change: opacity, transform;
}

/*
 * Two-phase reveal: opacity is DONE at 55% while the transform settles through
 * 100%. Fading and moving on one curve reads translucent; finishing the fade
 * early reads solid and "expensive". Keyframes are the only way to express it.
 */
[data-hlm-reveal='in'] {
  animation: hlm-rise 1000ms cubic-bezier(0.215, 0.61, 0.355, 1) both;
  animation-delay: calc(var(--hlm-i, 0) * var(--hlm-reveal-stagger));
}

@keyframes hlm-rise {
  0% { opacity: 0; transform: translate3d(0, 40px, 0); }
  55% { opacity: 1; }
  to { opacity: 1; transform: translate3d(0, 0, 0); }
}

[data-hlm-reveal-from='fade'][data-hlm-reveal='in'] {
  animation-name: hlm-fade;
  animation-duration: 800ms;
}

@keyframes hlm-fade {
  0% { opacity: 0; }
  to { opacity: 1; }
}

/* Pace set per section in the editor. */
[data-hlm-pace='calm'][data-hlm-reveal='in'] {
  animation-duration: 1400ms;
}

[data-hlm-pace='brisk'][data-hlm-reveal='in'] {
  animation-duration: 650ms;
}

/* Cards travel further and settle from a slight compression — a different
   voice from headings, so the page doesn't repeat one movement everywhere. */
:is(.hlm-services__grid, .hlm-projects__grid, .hlm-team__grid, .hlm-certs__grid) > [data-hlm-reveal='in'] {
  animation-name: hlm-rise-card;
}

@keyframes hlm-rise-card {
  0% { opacity: 0; transform: translate3d(0, 52px, 0) scale3d(0.975, 0.975, 1); }
  55% { opacity: 1; }
  to { opacity: 1; transform: translate3d(0, 0, 0) scale3d(1, 1, 1); }
}

/* Section heads travel less — they should feel settled, not launched. */
:is(.hlm-services__head, .hlm-projects__headtext, .hlm-team__head, .hlm-certs__head, .hlm-stats__head, .hlm-faq__head, .hlm-richtext__head)[data-hlm-reveal='in'] {
  animation-name: hlm-rise-soft;
}

@keyframes hlm-rise-soft {
  0% { opacity: 0; transform: translate3d(0, 26px, 0); }
  55% { opacity: 1; }
  to { opacity: 1; transform: translate3d(0, 0, 0); }
}

/* Once settled, stop promoting the layer so we don't hold GPU memory. */
[data-hlm-reveal='done'] {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/*
 * Headline words: a true masked rise. Each word clips itself while its inner
 * span rises its own full height from below the mask — the text appears from
 * an invisible edge rather than fading in place. The clip is lifted once
 * settled so descenders are never permanently cut.
 */
/* The word mask is the reveal — never double-hide with the generic fade.
   Only the vertical axis clips: the rise comes from below, and the synthetic
   italic plus the outline stroke overhang the inline box on the right, so a
   four-sided clip would shave the last letter until the word settles. */
.hlm-hero__word[data-hlm-reveal='pending'],
.hlm-hero__word[data-hlm-reveal='in'] {
  overflow-x: visible;
  overflow-y: clip;
  opacity: 1;
  animation: none;
}

.hlm-hero__word[data-hlm-reveal='pending']:where(:not([data-hlm-reveal-from='fade'])) .hlm-hero__wordinner {
  display: inline-block;
  opacity: 0;
  transform: translate3d(0, 112%, 0);
}

.hlm-hero__word[data-hlm-reveal='in']:where(:not([data-hlm-reveal-from='fade'])) .hlm-hero__wordinner {
  display: inline-block;
  animation: hlm-word-rise 1100ms cubic-bezier(0.215, 0.61, 0.355, 1) both;
  animation-delay: calc(var(--hlm-i, 0) * 110ms);
}

@keyframes hlm-word-rise {
  0% { opacity: 0; transform: translate3d(0, 112%, 0) rotate(0.5deg); }
  45% { opacity: 1; }
  to { opacity: 1; transform: translate3d(0, 0, 0) rotate(0); }
}

/*
 * Hero parallax, native scroll-driven where supported.
 *
 * animation-timeline runs off the main thread, so the image drifts at a steady
 * rate no matter what JavaScript is doing. Progressive enhancement: browsers
 * without it simply get a static image, no fallback script needed.
 */
@supports (animation-timeline: view()) {
  .hlm-hero__img {
    animation: hlm-parallax linear both;
    animation-timeline: view();
    animation-range: entry 0% exit 100%;
  }

  @keyframes hlm-parallax {
    from { transform: translate3d(0, -4%, 0) scale(1.12); }
    to { transform: translate3d(0, 4%, 0) scale(1.12); }
  }
}

/*
 * Curtain image reveals (the "soil" treatment): media unmasks upward via
 * clip-path while the image inside settles from a slight zoom. Keyed off the
 * SAME reveal attributes the runtime already sets, so this costs no new JS.
 * clip-path on a simple inset and transform both stay off the layout path.
 */
/*
 * Masked media is never ALSO opacity-hidden — a curtain fading in shows only
 * the tail of its own motion and reads sluggish. The mask alone is the reveal.
 */
.hlm-about__media[data-hlm-reveal='pending']:where(:not([data-hlm-reveal-from='fade'])),
.hlm-about__media[data-hlm-reveal='in']:where(:not([data-hlm-reveal-from='fade'])) {
  opacity: 1;
  animation: none;
}

.hlm-about__media[data-hlm-reveal='pending']:where(:not([data-hlm-reveal-from='fade'])) .hlm-about__media-inner {
  clip-path: inset(100% 0 0 0);
}

.hlm-about__media[data-hlm-reveal='in']:where(:not([data-hlm-reveal-from='fade'])) .hlm-about__media-inner {
  clip-path: inset(0 0 0 0);
  transition: clip-path 950ms var(--hlm-ease-out-expo);
}

.hlm-about__media[data-hlm-reveal='done'] .hlm-about__media-inner {
  clip-path: none;
}

.hlm-about__media[data-hlm-reveal='pending']:where(:not([data-hlm-reveal-from='fade'])) .hlm-about__img {
  transform: scale3d(1.09, 1.09, 1);
}

.hlm-about__media[data-hlm-reveal='in']:where(:not([data-hlm-reveal-from='fade'])) .hlm-about__img {
  transform: scale3d(1, 1, 1);
  transition: transform 1300ms var(--hlm-ease-out-expo);
}

/* Hero eyebrow rule draws itself after the text lands. */
.hlm-hero__eyebrow[data-hlm-reveal='pending']::after,
.hlm-hero__eyebrow[data-hlm-reveal='pending']::before {
  transform: scaleX(0);
  transform-origin: left center;
}

.hlm-hero__eyebrow[data-hlm-reveal='in']::after,
.hlm-hero__eyebrow[data-hlm-reveal='in']::before {
  transform: scaleX(1);
  transition: transform calc(var(--hlm-duration) * 3.75) var(--hlm-ease-out-expo) calc(var(--hlm-duration) * 1.45);
}

/* ---------- interaction motion ---------- */

.hlm-services__item {
  position: relative;
  isolation: isolate;
  transition: transform calc(var(--hlm-duration) * 2.15) var(--hlm-ease-out-expo);
}

.hlm-services__item:hover {
  transform: translate3d(0, -6px, 0);
}

/* The hover shadow is a pre-rendered pseudo-element whose OPACITY crossfades —
   animating box-shadow itself repaints every frame. */
.hlm-services__item::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: var(--hlm-radius-lg);
  box-shadow: var(--hlm-shadow-lg);
  opacity: 0;
  transition: opacity calc(var(--hlm-duration) * 1.75) var(--hlm-ease-out-expo);
}

.hlm-services__item:hover::after {
  opacity: 1;
}

.hlm-project__img,
.hlm-service__img {
  transition: transform calc(var(--hlm-duration) * 3.75) var(--hlm-ease-out-expo);
}

.hlm-project:hover .hlm-project__img,
.hlm-service:hover .hlm-service__img {
  transform: scale3d(1.05, 1.05, 1);
}

/* Buttons: a quick press response reads as physical. */
.hlm-btn {
  transition:
    background var(--hlm-duration) var(--hlm-ease-out-quint),
    color var(--hlm-duration) var(--hlm-ease-out-quint),
    border-color var(--hlm-duration) var(--hlm-ease-out-quint),
    transform var(--hlm-duration) var(--hlm-ease-spring);
}

.hlm-btn:hover { transform: translate3d(0, -2px, 0); }
.hlm-btn:active { transform: translate3d(0, 0, 0) scale3d(0.98, 0.98, 1); }

.hlm-hero__cta { transition: color var(--hlm-duration) var(--hlm-ease-out-quint); }

.hlm-hero__chev { transition: transform calc(var(--hlm-duration) * 1.75) var(--hlm-ease-spring); }
.hlm-hero__cta:hover .hlm-hero__chev { transform: translate3d(6px, 0, 0); }

.hlm-faq__marker::before,
.hlm-faq__marker::after {
  transition: transform calc(var(--hlm-duration) * 1.75) var(--hlm-ease-spring);
}

/* Icon badges settle with a spring on reveal. */
.hlm-certs__item:where([data-hlm-reveal='in']) .hlm-certs__iconwrap,
.hlm-about__highlight:where([data-hlm-reveal='in']) .hlm-about__iconwrap {
  animation: hlm-pop calc(var(--hlm-duration) * 2.9) var(--hlm-ease-spring) both;
  animation-delay: calc(var(--hlm-i, 0) * var(--hlm-reveal-stagger) + var(--hlm-duration) * 0.5);
}

@keyframes hlm-pop {
  from { transform: scale3d(0.6, 0.6, 1); opacity: 0; }
  to { transform: scale3d(1, 1, 1); opacity: 1; }
}

/*
 * Reduced motion: reveal instantly and cancel every drift. Content is never
 * left hidden — the pending state is overridden to fully visible.
 */
@media print, (prefers-reduced-motion: reduce) {
  [data-hlm-reveal='pending'],
  [data-hlm-reveal='in'],
  [data-hlm-reveal='done'],
  .hlm-hero__word[data-hlm-reveal] .hlm-hero__wordinner {
    opacity: 1;
    transform: none;
    transition: none;
    animation: none;
    will-change: auto;
  }

  .hlm-about__media[data-hlm-reveal] .hlm-about__media-inner,
  .hlm-about__media[data-hlm-reveal] .hlm-about__img,
  .hlm-hero__eyebrow[data-hlm-reveal]::after,
  .hlm-hero__eyebrow[data-hlm-reveal]::before {
    clip-path: none;
    transform: none;
    transition: none;
  }

  .hlm-hero__img,
  .hlm-hero__scroll {
    animation: none;
  }

  .hlm-certs__item .hlm-certs__iconwrap,
  .hlm-about__highlight .hlm-about__iconwrap {
    animation: none;
  }
}
