Text Morph
Cycle through words with a letter-level morph: shared letters glide into place, the rest blur away, the line stays centred.
Split any string into chars, words or lines and reveal it with rise, wave, scatter, blur or a human-paced typewriter.
npx shadcn@latest add https://freedesignmd.com/r/split-text.json1/**2 * SplitText — splits a string into characters, words or lines and plays an3 * entrance preset across the pieces.4 *5 * Presets6 * - "rise" pieces slide up out of a clipping mask (per word).7 * - "wave" a crest travels through the text: each piece lifts, overshoots, settles.8 * - "scatter" pieces fly in from deterministic random offsets and rotations.9 * - "blur" pieces sharpen from a soft blur while drifting up.10 * - "typewriter" characters appear one by one with human timing (bursts within11 * words, pauses after punctuation, the odd hesitation) and a caret.12 *13 * Props14 * - text: string The content. Always the accessible text.15 * - as?: "h1"…"h6" | "p" | "span" | "div" Element. Default "h2".16 * - split?: "chars" | "words" | "lines" Default "chars" ("typewriter" always uses chars).17 * - preset?: see above. Default "rise".18 * - trigger?: "view" | "mount" | "manual". Default "view" (35% visible, once).19 * - replay?: number | string Change it to replay (on-demand trigger).20 * - stagger?: number ms between pieces. Defaults per split: 22 / 70 / 140.21 * - duration?: number ms per piece. Default 700 (--fd-dur-4).22 * - delay?: number ms before the first piece. Default 0.23 * - cps?: number Typewriter speed in characters per second. Default 20.24 * - caret?: boolean Typewriter caret. Default true.25 * - onDone?: () => void Fires once the last piece has landed.26 * - ref?: { play(), reset() } Imperative control (React 19 ref-as-prop).27 * CSS vars on root: --fd-st-caret (caret colour), --fd-st-blur (blur radius).28 * Typography: inherits nothing; defaults live in :where() so any class you29 * pass (font, size, colour) wins.30 *31 * Accessibility: headings carry aria-label with the full text; other tags get32 * a visually hidden copy. All pieces are aria-hidden. Reduced motion shows the33 * text immediately (the typewriter caret stays, without blinking).34 * Browser support: all evergreen browsers.35 */36import { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";37import type { CSSProperties, Ref } from "react";38 39export type SplitPreset = "rise" | "wave" | "scatter" | "blur" | "typewriter";40export type SplitMode = "chars" | "words" | "lines";41export type SplitTextHandle = { play: () => void; reset: () => void };42 43export interface SplitTextProps {44 text: string;45 as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "span" | "div";46 split?: SplitMode;47 preset?: SplitPreset;48 trigger?: "view" | "mount" | "manual";49 replay?: number | string;50 stagger?: number;51 duration?: number;52 delay?: number;53 cps?: number;54 caret?: boolean;55 onDone?: () => void;56 className?: string;57 style?: CSSProperties;58 ref?: Ref<SplitTextHandle>;59}60 61// deterministic 0..1 noise, SSR-safe62const hash = (i: number, s: number) => {63 const x = Math.sin(i * 127.1 + s * 311.7) * 43758.5453;64 return x - Math.floor(x);65};66const useIso = typeof window === "undefined" ? useEffect : useLayoutEffect;67 68/** Human-ish keystroke schedule: returns cumulative ms per character. */69function typeSchedule(text: string, cps: number) {70 const base = 1000 / cps;71 const out: number[] = [];72 let t = 0;73 let burst = 0;74 for (let i = 0; i < text.length; i++) {75 const c = text[i];76 const prev = text[i - 1] ?? "";77 let d = base * (0.45 + hash(i, 3) * 0.9);78 if (burst > 0) {79 d *= 0.55; // fingers already moving: quick run inside a word80 burst--;81 } else if (hash(i, 9) > 0.72) burst = 2 + Math.floor(hash(i, 4) * 4);82 if (prev === " ") d += base * 0.35 * hash(i, 5);83 if (/[,;:]/.test(prev)) d += base * (2.2 + hash(i, 6) * 1.5);84 if (/[.!?—–]/.test(prev)) d += base * (4 + hash(i, 7) * 3);85 if (hash(i, 8) > 0.965) d += base * (3 + hash(i, 2) * 4); // the hesitation86 if (c === " ") d *= 0.7;87 t += i === 0 ? 0 : d;88 out.push(Math.round(t));89 }90 return out;91}92 93const HEADINGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);94 95export function SplitText({96 text,97 as = "h2",98 split = "chars",99 preset = "rise",100 trigger = "view",101 replay,102 stagger,103 duration = 700,104 delay = 0,105 cps = 20,106 caret = true,107 onDone,108 className,109 style,110 ref,111}: SplitTextProps) {112 const root = useRef<HTMLElement>(null);113 const timers = useRef<number[]>([]);114 const doneRef = useRef(onDone);115 doneRef.current = onDone;116 const mode: SplitMode = preset === "typewriter" ? "chars" : split;117 const step = stagger ?? (mode === "chars" ? 22 : mode === "words" ? 70 : 140);118 119 // words → chars, keeping a global char index for staggering120 const words = useMemo(() => {121 let ci = 0;122 return text.split(/(\s+)/).filter(Boolean).map((w) => {123 const space = /^\s+$/.test(w);124 const chars = Array.from(w).map((ch) => ({ ch, i: ci++ }));125 return { w, space, chars };126 });127 }, [text]);128 const total = useMemo(() => words.reduce((n, w) => n + w.chars.length, 0), [words]);129 const wordCount = useMemo(() => words.filter((w) => !w.space).length, [words]);130 131 const clear = () => {132 timers.current.forEach((t) => window.clearTimeout(t));133 timers.current = [];134 };135 136 // line indices for split="lines": measured, written straight to the DOM137 useIso(() => {138 const el = root.current;139 if (!el || mode !== "lines") return;140 const measure = () => {141 const ws = el.querySelectorAll<HTMLElement>(".fd-st-w");142 let line = -1;143 let lastTop = -Infinity;144 ws.forEach((w) => {145 const top = w.offsetTop;146 if (top > lastTop + 2) {147 line++;148 lastTop = top;149 }150 w.style.setProperty("--fd-st-i", String(line));151 });152 el.style.setProperty("--fd-st-n", String(line + 1));153 };154 measure();155 const ro = new ResizeObserver(measure);156 ro.observe(el);157 return () => ro.disconnect();158 }, [mode, text]);159 160 const runTypewriter = useCallback(() => {161 const el = root.current;162 if (!el) return;163 clear();164 const cs = Array.from(el.querySelectorAll<HTMLElement>(".fd-st-c"));165 cs.forEach((c) => {166 c.removeAttribute("data-on");167 c.removeAttribute("data-caret");168 });169 const lead = el.querySelector<HTMLElement>(".fd-st-lead");170 lead?.setAttribute("data-caret", "");171 el.setAttribute("data-typing", "");172 const sched = typeSchedule(text, cps);173 cs.forEach((c, i) => {174 timers.current.push(175 window.setTimeout(() => {176 c.setAttribute("data-on", "");177 (i === 0 ? lead : cs[i - 1])?.removeAttribute("data-caret");178 c.setAttribute("data-caret", "");179 if (i === cs.length - 1) {180 el.removeAttribute("data-typing");181 doneRef.current?.();182 }183 }, delay + sched[i]),184 );185 });186 // pause the blink while keys are moving; resume shortly after each key187 }, [text, cps, delay]);188 189 const play = useCallback(() => {190 const el = root.current;191 if (!el) return;192 const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;193 clear();194 if (reduced) {195 el.setAttribute("data-reduced", "");196 if (preset === "typewriter") {197 const cs = el.querySelectorAll<HTMLElement>(".fd-st-c");198 cs.forEach((c) => c.setAttribute("data-on", ""));199 el.querySelector(".fd-st-lead")?.removeAttribute("data-caret");200 cs[cs.length - 1]?.setAttribute("data-caret", "");201 }202 el.dataset.phase = "in";203 doneRef.current?.();204 return;205 }206 if (preset === "typewriter") {207 el.dataset.phase = "in";208 runTypewriter();209 return;210 }211 // restart: snap to idle without transitions, flush styles, then play212 el.setAttribute("data-snap", "");213 el.dataset.phase = "idle";214 void el.offsetWidth;215 el.removeAttribute("data-snap");216 void el.offsetWidth;217 el.dataset.phase = "in";218 const n = mode === "chars" ? total : mode === "words" ? wordCount : Number(getComputedStyle(el).getPropertyValue("--fd-st-n")) || 3;219 const spread = preset === "scatter" ? 260 : step * Math.max(0, n - 1);220 timers.current.push(window.setTimeout(() => doneRef.current?.(), delay + spread + duration + 60));221 }, [preset, mode, total, wordCount, step, duration, delay, runTypewriter]);222 223 const reset = useCallback(() => {224 const el = root.current;225 clear();226 if (!el) return;227 el.setAttribute("data-snap", "");228 el.dataset.phase = "idle";229 el.querySelectorAll(".fd-st-c").forEach((c) => {230 c.removeAttribute("data-on");231 c.removeAttribute("data-caret");232 });233 el.querySelector(".fd-st-lead")?.setAttribute("data-caret", "");234 requestAnimationFrame(() => el.removeAttribute("data-snap"));235 }, []);236 237 useImperativeHandle(ref, () => ({ play, reset }), [play, reset]);238 239 // triggers240 useEffect(() => {241 const el = root.current;242 if (!el) return;243 if (trigger === "mount") {244 play();245 return clear;246 }247 if (trigger !== "view") return clear;248 if (!("IntersectionObserver" in window)) {249 play();250 return clear;251 }252 const io = new IntersectionObserver(253 (es) => {254 if (es.some((e) => e.isIntersecting)) {255 io.disconnect();256 play();257 }258 },259 { threshold: 0.35 },260 );261 io.observe(el);262 return () => {263 io.disconnect();264 clear();265 };266 // eslint-disable-next-line react-hooks/exhaustive-deps267 }, [trigger, text, preset, split]);268 269 const first = useRef(true);270 useEffect(() => {271 if (first.current) {272 first.current = false;273 return;274 }275 play();276 // eslint-disable-next-line react-hooks/exhaustive-deps277 }, [replay]);278 279 const Tag = as as "h2";280 const isHeading = HEADINGS.has(as);281 let wi = -1;282 283 return (284 <Tag285 ref={root as Ref<HTMLHeadingElement>}286 className={`fd-st${className ? " " + className : ""}`}287 data-preset={preset}288 data-split={mode}289 data-phase="idle"290 data-caret-on={preset === "typewriter" && caret ? "" : undefined}291 aria-label={isHeading ? text : undefined}292 style={{ ...style, ["--fd-st-dur" as string]: `${duration}ms`, ["--fd-st-step" as string]: `${step}ms`, ["--fd-st-delay" as string]: `${delay}ms` }}293 >294 <style>{css}</style>295 {!isHeading && <span className="fd-st-sr">{text}</span>}296 <span className="fd-st-inner" aria-hidden="true">297 {preset === "typewriter" && <span className="fd-st-lead" data-caret="" />}298 {words.map((w, k) => {299 if (w.space) {300 // spaces stay real text so lines wrap naturally; typewriter types them too301 return preset === "typewriter" ? (302 <span key={k} className="fd-st-c fd-st-sp">{w.w.replace(/\s+/g, " ")}</span>303 ) : (304 " "305 );306 }307 wi++;308 const pieces =309 mode === "chars"310 ? w.chars.map(({ ch, i }) => (311 <span312 key={i}313 className="fd-st-c"314 style={{315 ["--fd-st-i" as string]: i,316 ["--fd-st-x" as string]: `${((hash(i, 1) - 0.5) * 2.4).toFixed(3)}em`,317 ["--fd-st-y" as string]: `${((hash(i, 2) - 0.5) * 1.8).toFixed(3)}em`,318 ["--fd-st-r" as string]: `${((hash(i, 3) - 0.5) * 90).toFixed(1)}deg`,319 ["--fd-st-j" as string]: hash(i, 4).toFixed(3),320 }}321 >322 {ch}323 </span>324 ))325 : w.w;326 return (327 <span328 key={k}329 className="fd-st-w"330 style={331 mode === "words"332 ? {333 ["--fd-st-i" as string]: wi,334 ["--fd-st-x" as string]: `${((hash(wi, 1) - 0.5) * 1.6).toFixed(3)}em`,335 ["--fd-st-y" as string]: `${((hash(wi, 2) - 0.5) * 1.4).toFixed(3)}em`,336 ["--fd-st-r" as string]: `${((hash(wi, 3) - 0.5) * 40).toFixed(1)}deg`,337 ["--fd-st-j" as string]: hash(wi, 4).toFixed(3),338 }339 : undefined340 }341 >342 <span className="fd-st-wi">{pieces}</span>343 </span>344 );345 })}346 </span>347 </Tag>348 );349}350 351const css = `352:where(.fd-st){margin:0;padding:0;border:0;background:none;color:inherit;353 font:600 clamp(2rem,5vw,4rem)/1.04 "SF Pro Display",ui-sans-serif,-apple-system,BlinkMacSystemFont,"Inter","Segoe UI",Roboto,sans-serif;354 letter-spacing:-.03em;text-wrap:balance}355.fd-st,.fd-st *,.fd-st *::before,.fd-st *::after{box-sizing:border-box}356.fd-st{--fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-spring:cubic-bezier(.34,1.56,.64,1);--fd-st-blur:14px;--fd-st-caret:currentColor;357 display:block;font-kerning:normal;font-feature-settings:"kern"}358.fd-st-inner{display:inline;margin:0;padding:0}359.fd-st-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}360.fd-st-w{display:inline-block;margin:0;padding:0;white-space:nowrap;vertical-align:top}361.fd-st-wi,.fd-st-c{display:inline-block;margin:0;padding:0;will-change:transform,opacity}362.fd-st-c{white-space:pre}363 364/* shared timing: chars/words use their index, lines use the measured line index */365.fd-st[data-split="chars"] .fd-st-c,366.fd-st[data-split="words"] .fd-st-w > .fd-st-wi,367.fd-st[data-split="lines"] .fd-st-w > .fd-st-wi{368 --fd-st-t:calc(var(--fd-st-delay) + var(--fd-st-i,0) * var(--fd-st-step));369 transition:transform var(--fd-st-dur) var(--fd-ease-out) var(--fd-st-t),opacity var(--fd-st-dur) var(--fd-ease-out) var(--fd-st-t),filter var(--fd-st-dur) var(--fd-ease-out) var(--fd-st-t)}370.fd-st[data-split="lines"] .fd-st-w{--fd-st-i:0}371.fd-st[data-snap] *{transition:none!important;animation:none!important}372 373/* rise: words are masks, the inner content slides up through them */374.fd-st[data-preset="rise"] .fd-st-w{overflow:hidden;padding:.06em .02em .14em;margin:-.06em -.02em -.14em}375.fd-st[data-preset="rise"][data-phase="idle"] .fd-st-wi,376.fd-st[data-preset="rise"][data-split="chars"][data-phase="idle"] .fd-st-c{transform:translate3d(0,135%,0) rotate(4deg)}377.fd-st[data-preset="rise"][data-split="chars"] .fd-st-wi{transform:none;transition:none}378.fd-st[data-preset="rise"][data-split="chars"] .fd-st-c{transform-origin:0 100%}379.fd-st[data-preset="rise"] .fd-st-wi{transform-origin:0 100%}380 381/* blur */382.fd-st[data-preset="blur"][data-phase="idle"] .fd-st-wi,383.fd-st[data-preset="blur"][data-split="chars"][data-phase="idle"] .fd-st-c{opacity:0;filter:blur(var(--fd-st-blur));transform:translate3d(0,.18em,0) scale(1.04)}384.fd-st[data-preset="blur"][data-split="chars"] .fd-st-wi{opacity:1;filter:none;transform:none}385 386/* scatter: random offsets, a little extra random delay */387.fd-st[data-preset="scatter"] .fd-st-c,.fd-st[data-preset="scatter"][data-split="words"] .fd-st-wi{388 --fd-st-t:calc(var(--fd-st-delay) + var(--fd-st-j) * 260ms);transition-duration:calc(var(--fd-st-dur) * 1.25)}389.fd-st[data-preset="scatter"][data-phase="idle"] .fd-st-c,390.fd-st[data-preset="scatter"][data-split="words"][data-phase="idle"] .fd-st-wi{opacity:0;filter:blur(6px);391 transform:translate3d(var(--fd-st-x),var(--fd-st-y),0) rotate(var(--fd-st-r)) scale(.6)}392 393/* wave: a crest passes through, pieces overshoot and settle */394.fd-st[data-preset="wave"][data-phase="idle"] .fd-st-c,395.fd-st[data-preset="wave"][data-split="words"][data-phase="idle"] .fd-st-wi,396.fd-st[data-preset="wave"][data-split="lines"][data-phase="idle"] .fd-st-wi{opacity:0;transform:translate3d(0,.55em,0)}397.fd-st[data-preset="wave"][data-phase="in"] .fd-st-c,398.fd-st[data-preset="wave"][data-split="words"][data-phase="in"] .fd-st-wi,399.fd-st[data-preset="wave"][data-split="lines"][data-phase="in"] .fd-st-wi{animation:fd-st-wave var(--fd-st-dur) cubic-bezier(.3,.7,.4,1) var(--fd-st-t) both}400@keyframes fd-st-wave{0%{opacity:0;transform:translate3d(0,.55em,0) scaleY(.9)}38%{opacity:1}55%{transform:translate3d(0,-.16em,0) scaleY(1.04)}78%{transform:translate3d(0,.03em,0)}100%{opacity:1;transform:none}}401 402/* typewriter */403.fd-st[data-preset="typewriter"] .fd-st-c{opacity:0;transition:none;will-change:auto}404.fd-st[data-preset="typewriter"] .fd-st-c[data-on]{opacity:1}405.fd-st[data-preset="typewriter"] .fd-st-w{white-space:nowrap}406.fd-st-lead{display:inline-block;width:0;margin:0;padding:0}407.fd-st-lead::before{content:"\\200b"}408.fd-st[data-caret-on] [data-caret]{position:relative}409.fd-st[data-caret-on] [data-caret]::after{content:"";position:absolute;top:.1em;bottom:.06em;right:-.1em;width:max(2px,.06em);border-radius:1px;410 background:var(--fd-st-caret);opacity:1;animation:fd-st-blink 1.06s steps(1,end) infinite}411.fd-st[data-caret-on] .fd-st-lead[data-caret]::after{right:auto;left:0}412.fd-st[data-typing] [data-caret]::after{animation:none}413@keyframes fd-st-blink{0%,55%{opacity:1}56%,100%{opacity:0}}414 415@media (prefers-reduced-motion:reduce){416 .fd-st *{transition:none!important;animation:none!important}417 .fd-st[data-phase] .fd-st-wi,.fd-st[data-phase] .fd-st-c{opacity:1;transform:none;filter:none}418 .fd-st[data-preset="typewriter"] .fd-st-c{opacity:1}419}420`;421 422/* ───────────────────────────── demo ───────────────────────────── */423 424const PRESETS: { id: SplitPreset; label: string }[] = [425 { id: "rise", label: "Rise" },426 { id: "wave", label: "Wave" },427 { id: "scatter", label: "Scatter" },428 { id: "blur", label: "Blur" },429 { id: "typewriter", label: "Type" },430];431 432export function Demo() {433 const [keys, setKeys] = useState<Record<string, number>>({});434 const bump = (id: string) => setKeys((k) => ({ ...k, [id]: (k[id] ?? 0) + 1 }));435 const replayAll = () => setKeys((k) => Object.fromEntries(PRESETS.map((p) => [p.id, (k[p.id] ?? 0) + 1])));436 437 const Replay = ({ id, label }: { id: string; label: string }) => (438 <button type="button" className="fd-st-demo-replay" onClick={() => bump(id)} aria-label={`Replay ${label} preset`}>439 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">440 <path d="M3 12a9 9 0 1 0 3-6.7L3 8" />441 <path d="M3 3v5h5" />442 </svg>443 {label}444 </button>445 );446 447 return (448 <div className="fd-st-demo">449 <style>{demoCss}</style>450 <header className="fd-st-demo-bar">451 <span className="fd-st-demo-logo" aria-label="Halyard Research">452 <svg width="22" height="22" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10.5" fill="none" stroke="currentColor" strokeWidth="1.6" /><path d="M5 15.5c3-5.5 11-5.5 14 0" fill="none" stroke="currentColor" strokeWidth="1.6" /><circle cx="12" cy="8" r="1.8" fill="currentColor" /></svg>453 Halyard454 </span>455 <nav className="fd-st-demo-nav" aria-label="Primary">456 <a href="#research">Research</a>457 <a href="#models">Models</a>458 <a href="#safety">Safety</a>459 <a href="#careers">Careers</a>460 </nav>461 <button type="button" className="fd-st-demo-all" onClick={replayAll}>Replay all</button>462 </header>463 464 <main className="fd-st-demo-main">465 <section className="fd-st-demo-hero">466 <div className="fd-st-demo-kick">467 <span className="fd-st-demo-dot" aria-hidden="true" />468 Release note · 14 November 2026469 <Replay id="rise" label="Rise · lines" />470 </div>471 <SplitText472 as="h1"473 preset="rise"474 split="lines"475 text="Halyard 3 reasons in long arcs, not short hops."476 className="fd-st-demo-h1"477 trigger="mount"478 replay={keys.rise}479 />480 <SplitText481 as="p"482 preset="blur"483 split="words"484 stagger={28}485 delay={380}486 text="A 1.2M-token context, verified tool use and a planning head trained on 41 billion steps of agentic traces. Available today in the API and on-prem."487 className="fd-st-demo-lede"488 trigger="mount"489 replay={keys.rise}490 />491 </section>492 493 <section className="fd-st-demo-grid" aria-label="Highlights">494 <article className="fd-st-demo-card fd-st-demo-card--stat">495 <div className="fd-st-demo-cardhead"><span>Long-horizon eval</span><Replay id="scatter" label="Scatter" /></div>496 <SplitText as="p" preset="scatter" text="+38.4%" className="fd-st-demo-stat" replay={keys.scatter} />497 <p className="fd-st-demo-note">Task completion on HorizonBench-500 over Halyard 2, with no change to latency at p50.</p>498 </article>499 500 <article className="fd-st-demo-card fd-st-demo-card--quote">501 <div className="fd-st-demo-cardhead"><span>From the paper</span><Replay id="wave" label="Wave" /></div>502 <SplitText503 as="h2"504 preset="wave"505 text="Plans should bend, not break."506 className="fd-st-demo-quote"507 duration={900}508 stagger={30}509 replay={keys.wave}510 />511 <p className="fd-st-demo-note">Dr. Ines Achterberg, Head of Reasoning</p>512 </article>513 514 <article className="fd-st-demo-card fd-st-demo-card--term">515 <div className="fd-st-demo-cardhead"><span>halyard-3 · playground</span><Replay id="typewriter" label="Type" /></div>516 <div className="fd-st-demo-term">517 <span className="fd-st-demo-prompt" aria-hidden="true">›</span>518 <SplitText519 as="p"520 preset="typewriter"521 cps={24}522 delay={300}523 text="Plan a 3-week migration of our billing service to event sourcing, with a rollback at every step."524 className="fd-st-demo-typed"525 replay={keys.typewriter}526 />527 </div>528 </article>529 </section>530 531 <section className="fd-st-demo-foot">532 <div className="fd-st-demo-cardhead"><span>Research agenda</span><Replay id="blur" label="Blur · words" /></div>533 <SplitText534 as="h2"535 preset="blur"536 split="words"537 text="Interpretability you can audit. Evaluations you can rerun. Weights you can host."538 className="fd-st-demo-h2"539 replay={keys.blur}540 />541 </section>542 </main>543 </div>544 );545}546 547const demoCss = `548.fd-st-demo,.fd-st-demo *,.fd-st-demo *::before,.fd-st-demo *::after{box-sizing:border-box}549.fd-st-demo{--d-font:ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;550 --d-disp:"SF Pro Display",ui-sans-serif,-apple-system,"Inter Display","Inter",sans-serif;551 --d-serif:ui-serif,"New York","Iowan Old Style","Charter","Georgia",serif;552 --d-mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace;553 --d-bg:#F3F1EC;--d-ink:#101014;--d-mut:#64625C;--d-line:rgba(16,16,20,.12);--d-acc:#E0461B;554 margin:0;padding:0;min-height:100vh;background:var(--d-bg);color:var(--d-ink);font:400 15px/1.5 var(--d-font);-webkit-font-smoothing:antialiased}555.fd-st-demo-bar{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:0;padding:20px clamp(16px,4vw,48px)}556.fd-st-demo-logo{display:inline-flex;align-items:center;gap:10px;margin:0;font:600 17px/1 var(--d-disp);letter-spacing:-.01em;color:var(--d-ink)}557.fd-st-demo-logo svg{display:block}558.fd-st-demo-nav{display:flex;gap:28px;margin:0;padding:0}559.fd-st-demo-nav a{margin:0;padding:6px 0;font:400 15px/1 var(--d-font);color:var(--d-mut);text-decoration:none}560.fd-st-demo-nav a:hover{color:var(--d-ink)}561.fd-st-demo-all{appearance:none;-webkit-appearance:none;margin:0;height:34px;padding:0 16px;border:0;border-radius:9999px;background:var(--d-ink);color:var(--d-bg);562 font:500 13px/1 var(--d-font);cursor:pointer;transition:transform 120ms cubic-bezier(.22,1,.36,1)}563.fd-st-demo-all:active{transform:scale(.96)}564.fd-st-demo a:focus-visible,.fd-st-demo button:focus-visible{outline:2px solid rgba(10,132,255,.7);outline-offset:2px;border-radius:9999px}565.fd-st-demo-main{max-width:1280px;margin:0 auto;padding:clamp(24px,6vh,64px) clamp(16px,4vw,48px) 64px}566.fd-st-demo-kick{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin:0 0 22px;font:500 13px/1 var(--d-mono);color:var(--d-mut);letter-spacing:.01em}567.fd-st-demo-dot{display:block;width:8px;height:8px;border-radius:50%;background:var(--d-acc);box-shadow:0 0 0 4px rgba(224,70,27,.16)}568.fd-st-demo-replay{appearance:none;-webkit-appearance:none;display:inline-flex;align-items:center;gap:6px;margin:0 0 0 auto;height:26px;padding:0 10px;border:0;border-radius:9999px;569 background:rgba(16,16,20,.06);color:var(--d-mut);font:500 12px/1 var(--d-mono);letter-spacing:0;cursor:pointer;transition:background-color 120ms,color 120ms}570.fd-st-demo-replay:hover{background:rgba(16,16,20,.1);color:var(--d-ink)}571.fd-st-demo-replay svg{display:block}572.fd-st-demo-h1{max-width:13ch;color:var(--d-ink);font:600 clamp(44px,8.4vw,112px)/.94 var(--d-disp);letter-spacing:-.045em}573.fd-st-demo-lede{max-width:34em;margin:28px 0 0;color:var(--d-mut);font:400 clamp(17px,1.6vw,20px)/1.45 var(--d-font);letter-spacing:-.01em;text-wrap:pretty}574.fd-st-demo-grid{display:grid;grid-template-columns:1fr 1.15fr 1.35fr;gap:16px;margin:clamp(40px,8vh,80px) 0 0}575.fd-st-demo-card{display:flex;flex-direction:column;gap:14px;margin:0;padding:22px;border-radius:24px;background:#FBFAF7;box-shadow:0 0 0 1px var(--d-line);min-height:220px}576.fd-st-demo-cardhead{display:flex;align-items:center;gap:10px;margin:0;font:500 12px/1 var(--d-mono);color:var(--d-mut);text-transform:uppercase;letter-spacing:.06em}577.fd-st-demo-cardhead .fd-st-demo-replay{text-transform:none}578.fd-st-demo-stat{margin:auto 0 0;color:var(--d-acc);font:600 clamp(56px,6vw,88px)/1 var(--d-disp);letter-spacing:-.05em;font-variant-numeric:tabular-nums}579.fd-st-demo-note{margin:0;color:var(--d-mut);font:400 13px/1.45 var(--d-font);max-width:32em}580.fd-st-demo-card--quote{background:var(--d-ink);color:#F3F1EC;box-shadow:none}581.fd-st-demo-card--quote .fd-st-demo-cardhead,.fd-st-demo-card--quote .fd-st-demo-note{color:rgba(243,241,236,.6)}582.fd-st-demo-card--quote .fd-st-demo-replay{background:rgba(255,255,255,.1);color:rgba(243,241,236,.7)}583.fd-st-demo-quote{margin:auto 0 0;color:#F3F1EC;font:400 italic clamp(32px,3.2vw,44px)/1.05 var(--d-serif);letter-spacing:-.025em}584.fd-st-demo-card--term{background:#16161B;box-shadow:0 0 0 1px rgba(255,255,255,.06),0 24px 48px -28px rgba(16,16,20,.6)}585.fd-st-demo-card--term .fd-st-demo-cardhead{color:rgba(243,241,236,.5);text-transform:none;letter-spacing:0}586.fd-st-demo-card--term .fd-st-demo-replay{background:rgba(255,255,255,.08);color:rgba(243,241,236,.7)}587.fd-st-demo-term{display:flex;gap:12px;margin:auto 0 0;padding:16px;border-radius:12px;background:rgba(255,255,255,.04)}588.fd-st-demo-prompt{margin:0;color:#7BE0A6;font:500 17px/1.45 var(--d-mono)}589.fd-st-demo-typed{--fd-st-caret:#7BE0A6;color:#EDEBE6;font:400 15px/1.55 var(--d-mono);letter-spacing:0;text-wrap:pretty}590.fd-st-demo-foot{margin:clamp(48px,10vh,96px) 0 0;padding:24px 0 0;border-top:1px solid var(--d-line)}591.fd-st-demo-h2{margin:22px 0 0;max-width:22ch;color:var(--d-ink);font:500 clamp(32px,4.4vw,64px)/1.02 var(--d-disp);letter-spacing:-.035em}592@media (max-width:900px){.fd-st-demo-grid{grid-template-columns:1fr}.fd-st-demo-nav{display:none}}593`;Cycle through words with a letter-level morph: shared letters glide into place, the rest blur away, the line stays centred.
A headline whose letters drift away from — or toward — your cursor and spring home, each on its own spring.
A decoding effect: characters cycle through glyphs and lock in left to right. Hover re-scrambles; layout never shifts.