A study in restrained typographic motion — Volume I.
// Mechanisme: hero-char-reveal-editorial
import gsap from 'https://esm.sh/gsap@3.12.5';
const el = document.querySelector('.char-reveal-target');
if (el) {
const wrap = (txt) => [...txt].map(c => c === ' '
? '<span style="display:inline-block;width:.28em"> </span>'
: '<span class="cw" style="display:inline-block;overflow:hidden;vertical-align:bottom"><span class="ci" style="display:inline-block;will-change:transform">' + c + '</span></span>'
).join('');
const walk = (node) => {
[...node.childNodes].forEach(n => {
if (n.nodeType === 3) {
const span = document.createElement('span');
span.innerHTML = wrap(n.textContent);
node.replaceChild(span, n);
} else if (n.nodeType === 1) walk(n);
});
};
walk(el);
const chars = el.querySelectorAll('.ci');
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
gsap.set(chars, { yPercent: 0, autoAlpha: 1 });
} else {
gsap.fromTo(chars,
{ yPercent: 110, autoAlpha: 0 },
{ yPercent: 0, autoAlpha: 1, duration: 1.4, stagger: 0.018, ease: 'expo.out' }
);
}
} <!-- Skeleton: hero-char-reveal-editorial -->
<section class="he-stage">
<aside class="he-eyebrow">
<span class="he-num">N° 003</span>
<span class="he-rule"></span>
<span class="he-cat">Heroes — Char Reveal — Editorial</span>
</aside>
<h1 class="char-reveal-target">
Movement begins with <em>intention</em>
</h1>
<p class="he-credit">A study in restrained typographic motion.</p>
</section> /* Styling: hero-char-reveal-editorial */
.he-stage{background:#F4F1EB;color:#0A0A0A;font-family:'Fraunces',Georgia,serif}
.he-eyebrow{display:flex;flex-direction:column;gap:.6rem;font-size:.7rem;letter-spacing:.18em;text-transform:uppercase}
.he-rule{display:block;width:48px;height:1px;background:#0A0A0A}
.char-reveal-target{font-family:'Fraunces',Georgia,serif;font-weight:300;font-size:clamp(3rem,9vw,8.5rem);line-height:.95;letter-spacing:-.02em}
.char-reveal-target em{font-style:italic;font-weight:400}
.he-credit{font-style:italic;color:rgba(10,10,10,.55)}