/* Page entrance.
 *
 * One authored moment per load: the display headline setting itself. Each word
 * rises out of its own line carrying a brief signal bloom that settles to
 * paper, and the chrome and supporting copy arrive around it on a short
 * stagger. Everything below the first viewport keeps the scroll reveals it
 * already had; a load animation that re-animates every section is a page that
 * never stops moving.
 *
 * Three states on <html>:
 *   (none)  no JS, or reduced motion. Everything is simply visible.
 *   hold    set by an inline primer in <head>, before first paint, so the
 *           entrance never flashes its own end state first.
 *   go      the entrance runs.
 *   done    animations finished; will-change is released.
 *
 * The default is visible and the primer carries a failsafe, so a script that
 * never loads leaves a readable page rather than a blank one.
 */

/* Held before the entrance runs. Containers rather than a list of every
   animated element: one rule to keep in step with the JS, not eight. */
[data-enter="hold"] .opening-body,
[data-enter="hold"] .page-open,
[data-enter="hold"] .gone-body,
[data-enter="hold"] .chrome,
[data-enter="hold"] .site-chrome-header {
  opacity: 0;
}

/* ---- the headline ------------------------------------------------------- */
/* The mask is a clip, not an overflow.
 *
 * `overflow: hidden` needs padding to clear the descenders, and the site's own
 * name ends in a q. But vertical margins do not cancel padding inside a line
 * box, so that padding grew `.name` by 8px the moment the words were split:
 * a layout shift caused by the animation that was supposed to be free.
 * `clip-path` is a paint operation and cannot move anything.
 *
 * The insets bleed outwards on three sides so nothing real is ever cut: the
 * bottom clears descenders, the sides clear the didone's overhangs, and only
 * the generous top is doing work, holding the word until it arrives. */
/* Armour, and it is load-bearing.
 *
 * These wrappers are injected into headings that already have their own
 * descendant rules. `.name span { display: block; padding-left: .5em }` is
 * what drops and indents the surname, and it matched both injected spans too,
 * stacking two indents onto a line that should have had none and turning the
 * inline-block masks into blocks. The name lost its left edge entirely.
 *
 * The doubled class is specificity 0,2,0, which beats a `.container element`
 * rule at 0,1,1 without reaching for !important. Any structural property a
 * host heading might set has to be reset here, not just the two that bit.
 * The heading's own span, which is not `.enter-w`, keeps its rule untouched. */
.enter-w.enter-w,
.enter-wi.enter-wi {
  display: inline-block;
  padding: 0; margin: 0; border: 0;
  text-indent: 0; float: none; width: auto; height: auto;
}
[data-enter="hold"] .enter-w,
[data-enter="go"] .enter-w { clip-path: inset(-0.4em -0.12em -0.18em -0.12em); }

[data-enter="go"] .enter-wi,
[data-enter="done"] .enter-wi { will-change: auto; }
[data-enter="go"] .enter-wi { will-change: transform; }

[data-enter="go"] .enter-wi {
  animation: enter-word .85s var(--ease) var(--d, 0ms) both;
}
/* The bloom is skipped on words already carrying the signal, which would
   otherwise animate from the accent back to the accent and, on the way,
   through paper. */
[data-enter="go"] .enter-wb {
  animation:
    enter-word .85s var(--ease) var(--d, 0ms) both,
    enter-bloom .72s linear var(--d, 0ms) backwards;
}

@keyframes enter-word {
  /* 135%, not 112%: the clip's bottom bleed is 0.18em of slack, and a
     shorter travel let the top of the word peek under it on the first frame */
  0%   { transform: translateY(135%); opacity: 0; }
  /* opacity lands well before the travel does, so the entrance costs the
     largest-contentful paint a third of its duration rather than all of it */
  45%  { opacity: 1; }
  100% { transform: translateY(0); opacity: 1; }
}
@keyframes enter-bloom {
  0%, 30% { color: var(--signal); }
  100%    { color: inherit; }
}

/* ---- everything else in the first viewport ------------------------------ */
[data-enter="go"] .enter-up {
  animation: enter-up .7s var(--ease) var(--d, 0ms) both;
}
@keyframes enter-up {
  0%   { opacity: 0; transform: translateY(10px); }
  100% { opacity: 1; transform: translateY(0); }
}

/* The chrome is positioned, and on the home page it is positioned over the
   hero. Moving it would fight its own layout, so it only resolves. */
[data-enter="go"] .enter-fade {
  animation: enter-fade .6s var(--ease) var(--d, 0ms) both;
}
@keyframes enter-fade {
  0%   { opacity: 0; }
  100% { opacity: 1; }
}

/* ---- phones -------------------------------------------------------------
   Shorter travel and a quicker curve. The same 112% rise that reads as
   composed on a wide display reads as slow on a phone, where the headline is
   larger relative to the viewport and the reader is closer to it. */
@media (max-width: 620px) {
  [data-enter="go"] .enter-wi,
  [data-enter="go"] .enter-wb { animation-duration: .68s, .58s; }
  [data-enter="go"] .enter-up { animation-duration: .56s; }
}

/* Belt and braces. The primer already refuses to set `hold` under reduced
   motion, so this only matters if the setting changes mid-session. */
@media (prefers-reduced-motion: reduce) {
  [data-enter] .opening-body,
  [data-enter] .page-open,
  [data-enter] .gone-body,
  [data-enter] .chrome,
  [data-enter] .site-chrome-header { opacity: 1; }
  [data-enter="go"] .enter-wi,
  [data-enter="go"] .enter-wb,
  [data-enter="go"] .enter-up,
  [data-enter="go"] .enter-fade { animation: enter-fade .4s linear both; }
  .enter-w { overflow: visible; }
}
