/* ==========================================================================
   Lambda Phi Epsilon — Alpha Xi Chapter
   BlurText — styles for the vanilla port of the React Bits BlurText.
   Scoped entirely to .blur-text* and one pre-paint hook; restyles nothing
   that already exists.

   Four things this file deliberately does.

   §0 hides a candidate that has NOT yet been painted, and un-hides it again
   on its own if the script never arms it. The rule needs two marks at once —
   a class on <html> that only the script can add, and an attribute on the
   element that only the script can stamp, and which it stamps only after
   checking the Paint Timing API — so with JS disabled, blocked or thrown,
   nothing in this file can hide anything: every heading is inked from the
   first paint and stays that way, and even with JS running, an element the
   browser has already inked is never marked at all. When the script DOES
   hide something, the hiding carries its own expiry: a one-millisecond
   animation on a --bt-prehide-life delay (1.2s) puts the opacity back with
   no script involved, so a throw between "hide" and "arm" cannot leave a
   blank block on the page — and past that deadline the script refuses to
   arm an on-screen block, so the un-hide is never undone by a second blank.

   It never hides anything on the bare .blur-text class — only
   .blur-text--armed hides, and the script adds that in the same task in
   which it removes the pre-paint attribute, so the two states hand over
   without a frame in between.

   It has no per-character rules, because there is no per-character split. A
   piece is always a whole word or a whole box: one box per word keeps EB
   Garamond's kern pairs and its fi/fl/ffi ligatures intact inside the word,
   which is the one place they do any work, and keeps the accessible name and
   a copy-paste of the heading identical to the authored text.

   And it ends the motion on the SETTLED rendering rather than on an
   identical-looking one: the last keyframe is `filter: none; transform:
   none`, not blur(0)/translate(0), and there is no will-change anywhere.
   Both of those exist to stop the letters re-rasterising — visibly changing
   weight — when the animation's compositing layer goes away a moment after
   the motion has stopped.

   Colour is inherited outright — nothing here names a token, so the two
   themes and the remapped palette inside .section--navy all just work.
   ========================================================================== */

/* ---- 0. The pre-paint hook ---------------------------------------------- */
/* Both marks are written by blur-text.js: the class at script evaluation,
   the attribute on each candidate — and only on a candidate the script has
   established was not already painted, so this can never hide text a reader
   is looking at. Neither mark exists in the HTML, and neither survives the
   mount pass.

   opacity, NOT visibility. The pre-hidden block is the page's h1, its crest
   and both call-to-action links, and visibility:hidden takes a subtree out
   of the accessibility tree and out of hit-testing for as long as it is on:
   a screen reader landing in that window would find no heading, and a tap
   would fall through. opacity:0 leaves the box, the a11y tree and hit
   testing exactly as they are — and it is the SAME rendering .blur-text
   --armed uses one line later, so the hand-over from pre-hidden to armed is
   not merely frame-perfect but pixel-identical. The stacking context it
   costs lasts under a second, contains nothing positioned against the page,
   and — unlike a filter — establishes no containing block for the fixed
   [data-gradual-blur] overlay. Geometry is untouched, so every component
   that measures its own box during this window still reads the truth. */
html.bt-prehide [data-bt-prehide] {
  opacity: 0;
  /* THE EXPIRY, and the reason it is a CSS animation rather than a timer:
     it needs to fire on a page whose script threw between "hide" and "arm",
     and an opacity animation can be run by the compositor, so it fires even
     while the main thread is busy with the very component work that delayed
     the sweep. The delay is written by the script as --bt-prehide-life from
     one constant (PREHIDE_LIFE_MS), so the stylesheet, the script's own
     backstop timer and mount()'s "is this mark still live" test cannot
     disagree; the value below is the fallback, not the authority. Past it
     mount() REFUSES an on-screen block, so the un-hide is never followed by
     the same text being blanked a second time. */
  animation: bt-prehide-expire 1ms linear var(--bt-prehide-life, 1200ms) forwards;
}

@keyframes bt-prehide-expire {
  to { opacity: 1; }
}

/* ---- 1. Host ------------------------------------------------------------ */
/* The six numbers the keyframes read. They are declared here so the file is
   legible on its own; the script overwrites all six inline from its resolved
   config, so these are the fallback, not the authority. The values are the
   house-restrained ones: 10px of travel and 6px of blur, against the React
   demo's 50px and 10px. */
.blur-text {
  --bt-blur: 6px;
  --bt-blur-mid: 3px;
  --bt-y0: -10px;      /* where a piece arrives from; +ve for direction:bottom */
  --bt-y1: 2px;        /* the small overshoot at step 1                        */
  --bt-step: 0.26s;    /* a piece runs for two of these                        */
  --bt-ease: ease-out;
}

/* ---- 2. Pieces ---------------------------------------------------------- */
/* A piece is one of two things: a generated span around a single word, or an
   authored element that could not be split and therefore fades as one box.
   The second kind can be the host itself, which is why every rule below is
   written twice — once as a descendant, once as a compound. */
.blur-text--armed .blur-text__piece,
.blur-text--armed.blur-text__piece {
  opacity: 0;
  filter: blur(var(--bt-blur));
  transform: translate3d(0, var(--bt-y0), 0);
}

/* Words: inline-block, never flex. The pieces stay in the normal text flow
   so the heading still wraps, still inherits its typography and still sits on
   the same baseline grid — the reference's display:flex row breaks all three.

   Why the line does not visibly re-wrap when the split is undone: the boxes
   are the same boxes. line-height and vertical-align are pinned to the
   inherited values; the whitespace BETWEEN words is left as real text nodes
   by the script, so it still collapses and still offers the same break
   opportunities; and style.css sets no hyphens, overflow-wrap or word-break,
   so there is no break opportunity INSIDE a word for inline-block to have
   taken away. The one shape that could still shift is a single word wider
   than its line, which no heading on this site has. The unwrap itself
   happens in the same task as the class removal, so even that would be one
   layout, not a visible reflow. */
.blur-text--armed .blur-text__word {
  display: inline-block;
  line-height: inherit;
  vertical-align: baseline;
}

/* A unit piece is an existing element and keeps its own display — block,
   flex, inline-flex, whatever it already was. Nothing here may change its
   layout, because at settle it has to be exactly what it was. */

/* A member that hosts another component fades but never moves: a transform
   changes what getBoundingClientRect reports, and two of the siblings in
   this folder measure their own boxes. Declared after the rule above so it
   wins on source order. */
.blur-text--armed .blur-text__still,
.blur-text--armed.blur-text__still {
  transform: none;
}

/* ---- 3. The reveal ------------------------------------------------------ */
/* Two steps, exactly the reference's shape: blurred, transparent and offset →
   half-blurred, half-opaque, just past the mark → settled. Only opacity,
   filter and transform are touched, so the whole thing stays on the
   compositor. animation-delay is written inline, per piece, by the script,
   which also caps the whole stagger — ~0.72s from trigger to fully inked for
   a heading, ~0.9s for a seven-part masthead, so a reader scrolling at speed
   actually sees the ink settle rather than arriving after it has finished.

   There is deliberately NO will-change. Sixty simultaneous hints are worth
   nothing here (the browser promotes an animating filter/opacity/transform
   on its own), and the hint outlives the motion by however long the classes
   stay on — which is exactly the interval in which text visibly re-weights.

   filter creates a stacking context and a containing block for fixed
   descendants — which is why a member that owns another component's host
   gets the `still` treatment above, and why nothing above the page-level
   [data-gradual-blur] overlay is ever given one. */
.blur-text.is-revealing .blur-text__piece,
.blur-text.is-revealing.blur-text__piece {
  animation-name: blur-text-in;
  animation-duration: calc(var(--bt-step) * 2);
  animation-timing-function: var(--bt-ease, ease-out);
  animation-fill-mode: both;
}

.blur-text.is-revealing .blur-text__still,
.blur-text.is-revealing.blur-text__still {
  animation-name: blur-text-in-still;
}

/* The last stop is `none`, not a zero-valued function. blur(0) and
   translate3d(0,0,0) look identical but still declare a filter and a
   transform, so the box stays on its own composited layer and its text keeps
   the greyscale antialiasing that goes with one; dropping to `none` at 100%
   means the final animated frame is rendered the same way the settled
   element is, and the hand-over is invisible. CSS interpolates a filter or
   transform list toward `none` as the identity, so the motion is unchanged. */
@keyframes blur-text-in {
  0% {
    opacity: 0;
    filter: blur(var(--bt-blur));
    transform: translate3d(0, var(--bt-y0), 0);
  }
  50% {
    opacity: .5;
    filter: blur(var(--bt-blur-mid));
    transform: translate3d(0, var(--bt-y1), 0);
  }
  100% {
    opacity: 1;
    filter: none;
    transform: none;
  }
}

@keyframes blur-text-in-still {
  0%   { opacity: 0;  filter: blur(var(--bt-blur)); }
  50%  { opacity: .5; filter: blur(var(--bt-blur-mid)); }
  100% { opacity: 1;  filter: none; }
}

/* ---- 4. Reduced motion -------------------------------------------------- */
/* The script never pre-hides, splits or arms anything under reduced motion,
   so in practice none of these selectors can match. This block exists for the
   two seams where they could: a preference switched on mid-reveal, and a
   stylesheet that outlives the script. In both cases the text must be plain
   and settled — never a half-played frame, never a blurred word, and above
   all never a hidden one. */
@media (prefers-reduced-motion: reduce) {
  html.bt-prehide [data-bt-prehide] {
    opacity: 1 !important;
    animation: none !important;
  }
  .blur-text--armed .blur-text__piece,
  .blur-text--armed.blur-text__piece,
  .blur-text.is-revealing .blur-text__piece,
  .blur-text.is-revealing.blur-text__piece {
    animation: none !important;
    opacity: 1 !important;
    filter: none !important;
    transform: none !important;
  }
}

/* ---- 5. Print ----------------------------------------------------------- */
/* This site's whole argument is that it is set as on a printed page, so on
   actual paper the ink is simply there — including a block that had not been
   scrolled to when the print was taken. */
@media print {
  html.bt-prehide [data-bt-prehide] {
    opacity: 1 !important;
    animation: none !important;
  }
  .blur-text--armed .blur-text__piece,
  .blur-text--armed.blur-text__piece,
  .blur-text.is-revealing .blur-text__piece,
  .blur-text.is-revealing.blur-text__piece {
    animation: none !important;
    opacity: 1 !important;
    filter: none !important;
    transform: none !important;
  }
}
