/* textrise.css — the site-wide TEXT ENTRANCE tier (with textrise.js / window.TextRise).
   Headings (>=30px) enter as a MASKED LINE RISE: each line rises translateY(115%) -> 0
   behind its own line-box mask, token-timed — --dur-text-heading (900ms) per line,
   --stagger-line (80ms) with a 6-line stagger cap (block <= 1.3s), --ease-out-expo (fast
   attack, long deceleration). Masks reserve final geometry, so headings occupy IDENTICAL
   space before/during/after (zero CLS); rest-state text carries no mask at all (wrapping
   headings restore their original nodes on settle, explicit-line headings open their mask
   after it), so descenders are never clipped at rest even at .85 display leading. Body text
   enters as the SAME masked line rise at a quieter tuning: --dur-text-body (700ms) per line,
   60ms line stagger (same 6-line cap; line count always computed from the current wrap),
   translateY(105%) with opacity .4 -> 1 over the first 40% of the motion (TextRise.bodyLines /
   viewport-gated TextRise.body); a paragraph entering with its heading starts 150ms after the
   heading's first line. Entrances start after the page transition
   completes (PageGlide.settled); below-the-fold headings enter ONCE on viewport entry via
   the tier's shared IntersectionObserver (TextRise.heading). Splits are visual only:
   textrise.js keeps a visually-hidden copy for screen readers and restores the original
   nodes on settle. prefers-reduced-motion renders text instantly. Pages link this after
   buttons.css and hide entrance text with .trh until TextRise adds .tr-on (tripled so it
   outranks page-level opacity rules regardless of load order). */
/* JS-gated hide (pagetrans.js stamps data-vtdir on <html> at head eval on every page): with
   JS off the rule never applies, so entrance text can never be invisible without the tier. */
html[data-vtdir] .trh{opacity:0}
.tr-on.tr-on.tr-on{opacity:1}
.trsr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
.trw{display:inline-block}
/* Word-level mask for wrapping headings. clip-path, NOT overflow:hidden: an inline-block
   with overflow hidden baselines at its bottom margin edge, which would shift every word
   against the unsplit original (= CLS). The bottom allowance is .08em — the descender
   allowance the padding-block:.08em/margin-block:-.08em wrapper trick reserves — and the
   mask is transient anyway (removed with the split on settle). */
.trm{display:inline-block;clip-path:inset(-.35em -.25em -.08em -.25em)}
/* Shared keyframes for headings with EXPLICIT line elements (Intro staircase, Work scene
   labels, Expertise): the page wraps each line's content in a riser and runs trLine on the
   riser — duration var(--dur-text-heading), easing var(--ease-out-expo), delays stepped by
   var(--stagger-line) in the page's animation shorthand — and trClipOpen on the line box
   (hold the mask through the rise, then open it so descenders/rotated hand glyphs are never
   clipped at rest; size it calc(<rise end>/.9) so the open lands after settle). */
@keyframes trLine{0%{opacity:1;transform:translateY(115%)}100%{opacity:1;transform:none}}
@keyframes trClipOpen{0%,90%{clip-path:inset(-.35em -9999px 0 -9999px)}100%{clip-path:inset(-3em -9999px -3em -9999px)}}
@media (prefers-reduced-motion:reduce){
html[data-vtdir] .trh{opacity:1}
.trw,.trm{animation:none!important;transform:none!important;clip-path:none!important}
}
