/* ===========================================================
   Ghost text — word-by-word blur reveal + accent echo halo.

   Two independent effects, both opt-in by class:

   .gt-split  on a heading  -> ghost-text.js wraps each word in .gt-w
                               and staggers them in out of a blur.
   .gt-ghost  on an inline  -> paints a blurred, drifting accent-colored
              accent run       duplicate of the text behind it.

   Accent colour comes from --gt-accent so each page can set its own;
   the default matches the portfolio's terracotta / lilac pair.
   =========================================================== */

:root { --gt-accent: #c2572a; }
body.dark-mode { --gt-accent: #C9A0E8; }

/* ---- word reveal ---- */
.gt-w {
    display: inline-block;
    opacity: 0;
    filter: blur(18px);
    transform: translateY(0.24em);
    transition:
        opacity 1.1s cubic-bezier(0.22, 1, 0.3, 1),
        filter 1.1s cubic-bezier(0.22, 1, 0.3, 1),
        transform 1.1s cubic-bezier(0.22, 1, 0.3, 1);
}

.gt-in .gt-w {
    opacity: 1;
    filter: blur(0);
    transform: none;
}

/* Body copy: masked line reveal.

   Each line is clipped by its own box and slides up from below, one line
   after the next. Nothing blurs and nothing fades — the glyphs are either
   behind the mask or fully rendered, never in a half-legible state — which
   is why this survives at paragraph length where the blur did not.

   The mask is applied per word rather than per line. Words on the same
   visual line share a delay, so they travel as one and read as a line
   reveal, but no line wrappers have to be inserted into the markup — which
   means inline <em> and links spanning a line break survive untouched, and
   a reflow only has to recompute delays, not rebuild the DOM.

   The padding/margin pair gives descenders (g, y, p) room to exist inside
   an overflow:hidden box; without it the mask crops them. */
.gt-body .gt-w {
    display: inline-block;
    overflow: hidden;
    vertical-align: bottom;
    padding-bottom: 0.18em;
    margin-bottom: -0.18em;
    opacity: 1;
    filter: none;
    transform: none;
    transition: none;
}

.gt-body .gt-wi {
    display: inline-block;
    transform: translateY(110%);
    transition: transform 0.7s cubic-bezier(0.45, 0, 0.55, 1);
}

.gt-in.gt-body .gt-wi { transform: translateY(0); }

/* Sweep finished — stop paying for the filter on every word. */
.gt-done .gt-w {
    filter: none;
    transition: none;
    transform: none;
    opacity: 1;
}

/* ...but body words keep their identity transform, for the reason above. */
.gt-done.gt-body .gt-wi { transform: translateY(0); transition: none; }

/* ---- accent echo halo ---- */
.gt-ghost {
    position: relative;
    display: inline-block;
}

.gt-ghost::before {
    content: attr(data-gt-text);
    position: absolute;
    inset: 0;
    color: var(--gt-accent);
    filter: blur(12px);
    opacity: 0.34;
    animation: gtEcho 7s ease-in-out infinite alternate;
    pointer-events: none;
}

/* The halo reads much weaker against the light background than it does
   against the dark one, so it gets a little more to work with there. */
body.dark-mode .gt-ghost::before {
    opacity: 0.42;
    filter: blur(14px);
}

@keyframes gtEcho {
    0%   { transform: translate(-0.02em, 0.015em) scale(1.002); }
    100% { transform: translate(0.025em, -0.02em) scale(1.01); }
}

/* Someone who asked not to be animated at gets the text, plainly. */
@media (prefers-reduced-motion: reduce) {
    .gt-w {
        opacity: 1;
        filter: none;
        transform: none;
        transition: none;
    }
    .gt-body .gt-wi { transform: none; transition: none; }
    .gt-ghost::before { animation: none; }
}

/* ---- About heading: one reveal, not two ----
   The About heading already sliced in via a clip-path wipe on each line. A
   wipe travelling across words that are themselves still fading up out of a
   blur reads as neither effect — the wipe uncovers empty space, then the
   words arrive on their own schedule. So the heading now reveals by word
   alone. Deleting this block restores the original slice. */
.about-heading.gt-split .about-heading-line {
    clip-path: none;
    transition: color 0.6s ease;
}

/* Same reasoning for the About body: its clip-path wipe would uncover
   paragraphs whose words have not arrived yet. */
.about-body:has(.gt-body) {
    clip-path: none;
}

/* ---- Hero headline: one reveal, not two ----
   The hero h1 already faded up as a block on load (heroFadeUp, 0.5s into the
   page's opening sequence). Running that underneath a word reveal would move
   the whole line while its words were still arriving, so the block animation
   is dropped here and the words carry it. The h1's slot in the sequence is
   preserved by data-gt-delay instead. Deleting this block restores the fade. */
.hero h1.gt-split {
    opacity: 1;
    animation: none;
}
