/* ==========================================================================
   AVEN — air.css
   Type that condenses out of the air. Headings, eyebrows and the hero line
   arrive word by word — a little high, a little out of focus — and settle.
   Pairs with air.js. Vanilla CSS, no libraries, no build. Delete these two
   files and the site is behaviourally identical.

   ── ONE-WRITER CONTRACT ─────────────────────────────────────────────────
   Every property in this file is written on `.air-w` spans that air.js
   CREATES at runtime. Those spans do not exist in the source HTML, so no
   other stylesheet in this codebase can be styling them. This layer
   therefore cannot collide with:
       · the reveal system  — opacity + transform on `.reveal`   (motion.css)
       · the flow layer     — translate/scale/clip-path on `.flow-*` (flow.css)
       · the seam dissolve  — filter on `.dz` carriers             (flow.css)
   All three write on ANCESTORS of these spans; compositing an ancestor's
   transform with a descendant's is exactly what the compositor is for.

   ── WHY AN ANIMATION AND NOT A TRANSITION ───────────────────────────────
   A transition needs a "from" state to already be painted, which means one
   frame where the split text is visible before it is hidden — a blink. So
   the spans are born carrying `.air-hold` (the from-state) in the same JS
   tick that creates them: there is never a frame of unstyled split text.
   The observer then swaps `.air-hold` → `.air-go`, and `animation-fill-mode:
   both` holds the from-state through the per-word delay.

   ── SAFE-DEGRADE CONTRACT (mirrors motion.css / flow.css) ───────────────
   · No JS            ⇒ nothing is split ⇒ nothing is hidden. Plain text.
   · Setup throws     ⇒ air.js removes `air-on` and strips both classes.
   · JS dies mid-run  ⇒ the 3.2s `html.air-fallback` backstop forces every
                        span to opacity 1 / no transform / no blur.
   · reduced motion   ⇒ air.js never splits at all, and the rules below are
                        neutralised anyway. Belt and braces.
   There is no state in which a word is left invisible.
   ========================================================================== */

/* ---- Tunable tokens (specificity 0; override freely) -------------------- */
:root{
  --air-ease:  cubic-bezier(.16, .84, .28, 1);  /* matches --flow-ease      */
  --air-dur:   .78s;    /* per-word settle                                  */
  --air-step:  42ms;    /* stagger between words                            */
  --air-y:     .38em;   /* how far below its home each word starts          */
  --air-blur:  7px;     /* how far out of focus it starts                   */
}

/* Words are inline-block so they can carry a transform; the whitespace
   BETWEEN them is left as real text nodes, so line-breaking, `text-wrap:
   balance` and the accessible name ("Aven isn't lost. I was.") are all
   exactly what they were before the split. */
.air-w{ display:inline-block; }

/* From-state. Born with this class — see the note above. */
.air-w.air-hold{
  opacity:0;
  transform:translate3d(0, var(--air-y), 0);
  filter:blur(var(--air-blur));
}

/* Arrival. Blur clears before the drift finishes, so the word reads as
   focusing into place rather than sliding into place. */
.air-w.air-go{
  /* The delay is capped inside the shorthand: a long line must not stagger
     into a crawl, and a cap declared in a separate lower-specificity rule
     would simply be overwritten by this shorthand. Only the words actually
     in flight carry the compositor hint — air.js releases it on land — so a
     page of headings never promotes 150 layers at once. */
  animation:air-in var(--air-dur) var(--air-ease)
            min(calc(var(--air-i, 0) * var(--air-step)), 900ms) both;
  will-change:opacity, transform, filter;
}
@keyframes air-in{
  from{ opacity:0;   transform:translate3d(0, var(--air-y), 0); filter:blur(var(--air-blur)); }
  55% { opacity:1;                                              filter:blur(0); }
  to  { opacity:1;   transform:none;                            filter:blur(0); }
}

/* ---- Per-target character -------------------------------------------------
   The hero line is the front door: slowest, furthest, softest — the page
   breathes in. Section headings are a beat quicker. Eyebrows are a fast
   little run of light, so the label lands before the headline it announces.
   -------------------------------------------------------------------------- */
.hero h1.air{  --air-dur:1.15s; --air-step:78ms; --air-y:.46em; --air-blur:14px; }
h2.sec.air{    --air-dur:.86s;  --air-step:46ms; --air-y:.40em; --air-blur:9px;  }
.eyebrow.air{  --air-dur:.55s;  --air-step:26ms; --air-y:.30em; --air-blur:5px;  }

/* ==========================================================================
   FAILSAFE — air.js adds html.air-fallback at 3.2s ONLY IF no element was
   ever released, i.e. the observer never worked at all. (A blanket timer
   would strip the effect from every heading still below the fold, which on
   a page this long is most of them.) A second, softer backstop lives in
   air.js: a scroll-driven sweep that releases by hand anything that has
   come into view and is still holding.
   ========================================================================== */
html.air-fallback .air-w,
html.air-fallback .air-w.air-hold,
html.air-fallback .air-w.air-go{
  opacity:1 !important;
  transform:none !important;
  filter:none !important;
  animation:none !important;
  will-change:auto !important;
}

/* ==========================================================================
   REDUCED MOTION — required path. air.js does not split under `reduce`, so
   these spans should not exist there at all; this is the second lock.
   ========================================================================== */
@media (prefers-reduced-motion: reduce){
  .air-w,
  .air-w.air-hold,
  .air-w.air-go{
    opacity:1 !important;
    transform:none !important;
    filter:none !important;
    animation:none !important;
    will-change:auto !important;
  }
}

/* ==========================================================================
   PRINT — a printed page has no arrival.
   ========================================================================== */
@media print{
  .air-w,
  .air-w.air-hold,
  .air-w.air-go{
    opacity:1 !important; transform:none !important;
    filter:none !important; animation:none !important;
  }
}
