/* Adamas Digital — hero heading entrance.
   The page's first line resolves word by word: each rises a fraction of its
   own size and pulls into focus, with the serif italic travelling further and
   landing last, so the eye finishes on the emphasis. site.js splits the words
   at runtime, which leaves the markup in every page a plain heading.

   A typewriter was the other candidate and was rejected on this type: at
   clamp(58px, 7.6vw, 124px) with line-height .88, revealing one character at a
   time reflows the whole hero column on every keystroke, and the cadence reads
   as a terminal rather than as a brand statement. */

/* The hidden state is gated on .js, added by an inline snippet in <head> so it
   lands before first paint. With scripting off the heading is never hidden at
   all, and the failsafe below covers site.js failing to arrive. */
.js .hero h1,
.js .inner-hero h1,
.js .contact-hero h1,
.js .service-detail-hero h1 {
  visibility: hidden;
  animation: type-failsafe 0s linear 1.6s forwards;
}

.js .hero h1.is-split,
.js .inner-hero h1.is-split,
.js .contact-hero h1.is-split,
.js .service-detail-hero h1.is-split { visibility: visible; }

@keyframes type-failsafe {
  to { visibility: visible; }
}

.type-word {
  display: inline-block;
  animation: type-word-in .95s cubic-bezier(.22, 1, .36, 1) both;
  animation-delay: calc(var(--type-i, 0) * 72ms);
}

/* Travel is set in em so it scales with the clamped heading rather than
   drifting from a fixed pixel distance across breakpoints. */
@keyframes type-word-in {
  from {
    opacity: 0;
    transform: translateY(var(--type-rise, .34em));
    filter: blur(var(--type-blur, 14px));
  }

  to {
    opacity: 1;
    transform: none;
    filter: blur(0);
  }
}

/* A heading that animates beside a static eyebrow and button row reads as a
   glitch rather than an entrance, so the column arrives around it: label
   first, then the heading, then the copy the heading introduces. */
.js .hero .eyebrow,
.js .inner-hero .eyebrow,
.js .contact-hero .eyebrow,
.js .hero-copy > p:not(.eyebrow),
.js .inner-hero-grid > p,
.js .contact-hero > p:not(.eyebrow),
.js .hero .button-row {
  animation: type-rise-in .8s cubic-bezier(.22, 1, .36, 1) both;
}

.js .hero .eyebrow,
.js .inner-hero .eyebrow,
.js .contact-hero .eyebrow { animation-duration: .7s; }

/* --type-total is the heading's word count, set on the section by site.js, so
   the copy waits on the real last word instead of a guessed delay. */
.js .hero-copy > p:not(.eyebrow),
.js .inner-hero-grid > p,
.js .contact-hero > p:not(.eyebrow) {
  animation-delay: calc(var(--type-total, 5) * 72ms + .3s);
}

.js .hero .button-row {
  animation-delay: calc(var(--type-total, 5) * 72ms + .42s);
}

@keyframes type-rise-in {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: none; }
}

/* ---- Looping typewriter ------------------------------------------------
   The serif italic types itself out, holds, erases and goes again, for as long
   as the hero is on screen. It arrives on the beat after the last roman word,
   so the line resolves before the loop takes over. */
.js .hero h1.is-split em.tw,
.js .inner-hero h1.is-split em.tw,
.js .contact-hero h1.is-split em.tw,
.js .service-detail-hero h1.is-split em.tw {
  display: inline-block;
  position: relative;
  animation: type-rise-in .8s cubic-bezier(.22, 1, .36, 1) both;
  animation-delay: calc(var(--type-total, 5) * 72ms + .1s);
}

/* The finished phrase, invisible but still occupying its full box, so the
   heading never resizes mid-cycle. Kept out of selections — copying the
   heading should not yield the phrase twice. */
.tw-ghost { opacity: 0; }

.tw-live {
  position: absolute;
  inset: 0;
  -webkit-user-select: none;
  user-select: none;
}

/* Zero by zero, with the bar painted by the pseudo-element outside that box.
   .tw-live is exactly as wide as the finished phrase, so a caret with real
   width has nowhere to sit once the last character is typed: it wraps to a
   second line and hangs below the heading, over the paragraph. At no width it
   cannot wrap, and at no height it cannot change the line box either, so the
   typed text stays registered with the ghost underneath it. */
.tw-caret {
  width: 0;
  height: 0;
  vertical-align: baseline;
  display: inline-block;
  position: relative;
  animation: tw-blink 1.05s step-end infinite;
}

.tw-caret:before {
  content: "";
  background: currentColor;
  width: .055em;
  min-width: 2px;
  height: .7em;
  position: absolute;
  bottom: -.05em;
  left: .08em;
}

/* Solid while characters are moving. A caret that blinks mid-word reads as a
   dropped frame rather than as typing. */
.tw.is-typing .tw-caret {
  opacity: 1;
  animation: none;
}

@keyframes tw-blink {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0; }
}

/* Blur is the expensive half of the effect and this text is enormous, so cut
   the radius where the GPU budget is smallest. */
@media (max-width: 720px) {
  .type-word { --type-blur: 9px; }
}

/* Reduced motion gets the finished heading and no loop at all: the ghost is
   simply made visible and the animated layer removed, so the phrase is present
   and static rather than typing forever. site.js skips the timer to match. */
@media (prefers-reduced-motion: reduce) {
  .type-word,
  .js .hero .eyebrow,
  .js .inner-hero .eyebrow,
  .js .contact-hero .eyebrow,
  .js .hero-copy > p:not(.eyebrow),
  .js .inner-hero-grid > p,
  .js .contact-hero > p:not(.eyebrow),
  .js .hero .button-row,
  .js .hero h1.is-split em.tw,
  .js .inner-hero h1.is-split em.tw,
  .js .contact-hero h1.is-split em.tw,
  .js .service-detail-hero h1.is-split em.tw {
    opacity: 1;
    filter: none;
    transform: none;
    animation: none;
  }

  .tw-ghost { opacity: 1; }
  .tw-live { display: none; }
}
