Ledger Card Hero

A photoreal CSS-3D metal card that tilts to the cursor, then flips and docks on scroll while spend stats count up.

  • Heroes
  • Fintech
  • React only · 0 deps
  • 1 file
Full screen

Browser support · All evergreen browsers. Glass panel uses backdrop-filter (opaque fallback with prefers-reduced-transparency).

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/hero-fintech-card.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/heroes/hero-fintech-card.tsx
1/**
2 * HeroFintechCard — an Apple-clean hero for a corporate card. A CSS-3D metal
3 * card (front, back and a stacked edge for real thickness) floats, tilts
4 * toward the cursor on a spring and catches a specular highlight. As the
5 * section scrolls, a sticky stage scrubs the card through a full flip and docks
6 * it into a spend panel while the stats count up with the scroll.
7 *
8 * Props: eyebrow, title (ReactNode), lede, primaryCta, secondaryCta,
9 * holder, company, last4, stats ({ label, value, prefix?, suffix?, decimals? }[]).
10 * CSS vars on the root: --fd-ldg-bg, --fd-ldg-ink, --fd-ldg-accent, --fd-ldg-card-w.
11 *
12 * Browser support: all evergreen browsers (CSS 3D transforms, clip-path).
13 * Motion: pointer tilt (spring k=380, c=30), ambient float, scroll scrub via
14 * rAF + getBoundingClientRect (transforms/opacity only). The loop pauses when
15 * off-screen or when the tab is hidden. prefers-reduced-motion renders the
16 * composed final state without a scroll scene.
17 * Accessibility: the card is decorative (aria-hidden) and its details are
18 * repeated in text; stats are a <dl>; CTAs are real links with focus rings.
19 */
20import { useEffect, useRef } from "react";
21import type { ReactNode } from "react";
22
23export type LedgerStat = { label: string; value: number; prefix?: string; suffix?: string; decimals?: number };
24
25export interface HeroFintechCardProps {
26 eyebrow?: ReactNode;
27 title?: ReactNode;
28 lede?: ReactNode;
29 primaryCta?: { label: string; href: string };
30 secondaryCta?: { label: string; href: string };
31 holder?: string;
32 company?: string;
33 last4?: string;
34 stats?: LedgerStat[];
35 dockTitle?: ReactNode;
36}
37
38const clamp = (v: number, a = 0, b = 1) => Math.min(b, Math.max(a, v));
39const ease = (t: number) => 1 - Math.pow(1 - t, 3);
40const easeIO = (t: number) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2);
41const seg = (p: number, a: number, b: number) => clamp((p - a) / (b - a));
42
43function offsetIn(el: HTMLElement, stop: HTMLElement) {
44 let x = 0;
45 let y = 0;
46 let n: HTMLElement | null = el;
47 while (n && n !== stop) {
48 x += n.offsetLeft;
49 y += n.offsetTop;
50 n = n.offsetParent as HTMLElement | null;
51 }
52 return { x, y, w: el.offsetWidth, h: el.offsetHeight };
53}
54
55const fmt = (v: number, d = 0) => v.toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d });
56
57const DEFAULT_STATS: LedgerStat[] = [
58 { label: "Spend under policy this month", value: 1284610, prefix: "$" },
59 { label: "Receipts matched automatically", value: 14920 },
60 { label: "Days to close the books", value: 1.6, decimals: 1 },
61 { label: "Cashback earned", value: 30830, prefix: "$" },
62];
63
64export function HeroFintechCard({
65 eyebrow = (
66 <>
67 <b>New</b> 2.4% back on every SaaS invoice
68 </>
69 ),
70 title = (
71 <>
72 <span className="fd-ldg-line"><span>The corporate</span></span>{" "}
73 <span className="fd-ldg-line"><span>card that closes</span></span>{" "}
74 <span className="fd-ldg-line"><span><em>your books.</em></span></span>
75 </>
76 ),
77 lede = "Metal cards for every employee, policy enforced at the swipe, and every receipt reconciled to your general ledger before the month is out.",
78 primaryCta = { label: "Open an account", href: "#open" },
79 secondaryCta = { label: "Watch the 3-min tour", href: "#tour" },
80 holder = "Amara Okonkwo",
81 company = "Halden Robotics",
82 last4 = "4021",
83 stats = DEFAULT_STATS,
84 dockTitle = "Every swipe, already reconciled.",
85}: HeroFintechCardProps) {
86 const root = useRef<HTMLElement>(null);
87 const stage = useRef<HTMLDivElement>(null);
88 const home = useRef<HTMLDivElement>(null);
89 const slot = useRef<HTMLDivElement>(null);
90 const anchor = useRef<HTMLDivElement>(null);
91 const card = useRef<HTMLDivElement>(null);
92 const shadow = useRef<HTMLDivElement>(null);
93 const copy = useRef<HTMLDivElement>(null);
94 const panel = useRef<HTMLDivElement>(null);
95 const nums = useRef<(HTMLElement | null)[]>([]);
96
97 useEffect(() => {
98 const el = root.current;
99 const st = stage.current;
100 if (!el || !st) return;
101 const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
102 let geo = { hx: 0, hy: 0, hw: 0, hh: 0, dx: 0, dy: 0, ds: 1 };
103 const measure = () => {
104 if (!home.current || !slot.current || !anchor.current) return;
105 const h = offsetIn(home.current, st);
106 const s = offsetIn(slot.current, st);
107 anchor.current.style.left = `${h.x}px`;
108 anchor.current.style.top = `${h.y}px`;
109 anchor.current.style.width = `${h.w}px`;
110 anchor.current.style.height = `${h.w / 1.586}px`;
111 const hh = h.w / 1.586;
112 if (shadow.current) {
113 shadow.current.style.left = `${h.x + h.w * 0.05}px`;
114 shadow.current.style.top = `${h.y + hh * 0.98}px`;
115 }
116 geo = {
117 hx: h.x, hy: h.y, hw: h.w, hh,
118 dx: s.x + s.w / 2 - (h.x + h.w / 2),
119 dy: s.y + s.h / 2 - (h.y + hh / 2),
120 ds: s.w / h.w,
121 };
122 };
123 if (reduced) el.setAttribute("data-static", "");
124 measure();
125 const ro = new ResizeObserver(measure);
126 ro.observe(st);
127 if (reduced) {
128 nums.current.forEach((n, i) => n && (n.textContent = fmt(stats[i].value, stats[i].decimals)));
129 return () => ro.disconnect();
130 }
131
132 // springs for tilt
133 const sp = { x: 0, y: 0, vx: 0, vy: 0, tx: 0, ty: 0 };
134 const K = 380;
135 const C = 30;
136 const onMove = (e: PointerEvent) => {
137 if (e.pointerType === "touch") return;
138 const r = st.getBoundingClientRect();
139 sp.tx = clamp(((e.clientX - r.left) / r.width) * 2 - 1, -1, 1);
140 sp.ty = clamp(((e.clientY - r.top) / r.height) * 2 - 1, -1, 1);
141 };
142 const onLeave = () => {
143 sp.tx = 0;
144 sp.ty = 0;
145 };
146 el.addEventListener("pointermove", onMove);
147 el.addEventListener("pointerleave", onLeave);
148
149 let raf = 0;
150 let last = 0;
151 let visible = true;
152 const t0 = performance.now();
153 const lastShown: number[] = [];
154
155 const frame = (now: number) => {
156 raf = 0;
157 const dt = Math.min((now - last) / 1000 || 0.016, 0.032);
158 last = now;
159 for (let i = 0; i < 2; i++) {
160 const h = dt / 2;
161 sp.vx += (-K * (sp.x - sp.tx) - C * sp.vx) * h;
162 sp.vy += (-K * (sp.y - sp.ty) - C * sp.vy) * h;
163 sp.x += sp.vx * h;
164 sp.y += sp.vy * h;
165 }
166 const r = el.getBoundingClientRect();
167 const vh = window.innerHeight;
168 const travel = Math.max(1, r.height - st.offsetHeight);
169 const p = clamp(-r.top / travel);
170 const t = (now - t0) / 1000;
171
172 // phases
173 const flip = easeIO(seg(p, 0.06, 0.78));
174 const dock = easeIO(seg(p, 0.12, 0.8));
175 const settle = 1 - dock;
176 const floatY = Math.sin(t * 1.1) * 7 * settle;
177 const floatR = Math.sin(t * 0.7) * 1.2 * settle;
178 const tiltX = (-sp.y * 14 + 9) * settle + 0 * dock;
179 const tiltY = sp.x * 20 * settle - 14 * settle;
180 const rotY = tiltY + flip * 360;
181 const rotX = tiltX + Math.sin(flip * Math.PI) * -12;
182 const sc = 1 + (geo.ds - 1) * dock;
183 if (card.current) {
184 card.current.style.transform = `translate3d(${(geo.dx * dock).toFixed(2)}px, ${(geo.dy * dock + floatY).toFixed(2)}px, 0) scale(${sc.toFixed(4)}) rotateX(${rotX.toFixed(2)}deg) rotateY(${rotY.toFixed(2)}deg) rotateZ(${(floatR - 4 * settle).toFixed(2)}deg)`;
185 // specular follows the light, which sits up-left of the pointer
186 const facing = Math.cos((rotY * Math.PI) / 180);
187 card.current.style.setProperty("--sx", `${(50 + sp.x * 38 - (rotY % 360) * 0.12).toFixed(1)}%`);
188 card.current.style.setProperty("--sy", `${(35 + sp.y * 30).toFixed(1)}%`);
189 card.current.style.setProperty("--gl", `${(0.55 + Math.abs(sp.x) * 0.35 + (1 - Math.abs(facing)) * 0.4).toFixed(3)}`);
190 card.current.style.setProperty("--band", `${(sp.x * 30 + rotY * 0.18).toFixed(1)}%`);
191 }
192 if (shadow.current) {
193 const lift = 1 - floatY / 30;
194 shadow.current.style.transform = `translate3d(${(geo.dx * dock + sp.x * -18 * settle).toFixed(1)}px, ${(geo.dy * dock).toFixed(1)}px,0) scale(${(sc * lift).toFixed(3)}, ${(sc * lift * 0.9).toFixed(3)})`;
195 shadow.current.style.opacity = `${(0.85 - dock * 0.55).toFixed(3)}`;
196 }
197 if (copy.current) {
198 const c = seg(p, 0.02, 0.4);
199 copy.current.style.transform = `translate3d(0, ${(-c * vh * 0.16).toFixed(1)}px, 0) scale(${(1 - c * 0.04).toFixed(4)})`;
200 copy.current.style.opacity = `${(1 - ease(c)).toFixed(3)}`;
201 }
202 if (panel.current) {
203 const q = ease(seg(p, 0.42, 0.78));
204 panel.current.style.setProperty("--q", q.toFixed(4));
205 }
206 const cnt = ease(seg(p, 0.5, 0.92));
207 nums.current.forEach((n, i) => {
208 if (!n) return;
209 const v = stats[i].value * cnt;
210 const d = stats[i].decimals ?? 0;
211 const key = Math.round(v * Math.pow(10, d));
212 if (lastShown[i] === key) return;
213 lastShown[i] = key;
214 n.textContent = fmt(v, d);
215 });
216 if (visible && !document.hidden) raf = requestAnimationFrame(frame);
217 };
218 const start = () => {
219 if (!raf && visible && !document.hidden) {
220 last = performance.now();
221 raf = requestAnimationFrame(frame);
222 }
223 };
224 const io = new IntersectionObserver(([e]) => {
225 visible = e.isIntersecting;
226 if (visible) start();
227 });
228 io.observe(el);
229 const onVis = () => start();
230 document.addEventListener("visibilitychange", onVis);
231 start();
232 return () => {
233 cancelAnimationFrame(raf);
234 io.disconnect();
235 ro.disconnect();
236 document.removeEventListener("visibilitychange", onVis);
237 el.removeEventListener("pointermove", onMove);
238 el.removeEventListener("pointerleave", onLeave);
239 };
240 }, [stats]);
241
242 const name = holder.toUpperCase();
243
244 return (
245 <section ref={root} className="fd-ldg" aria-label="Ledger corporate card">
246 <style>{css}</style>
247 <div ref={stage} className="fd-ldg-stage">
248 <div className="fd-ldg-bgglow" aria-hidden="true" />
249 <div className="fd-ldg-grid">
250 <div ref={copy} className="fd-ldg-copy">
251 <p className="fd-ldg-eyebrow">
252 <span className="fd-ldg-pill">{eyebrow}</span>
253 </p>
254 <h1 className="fd-ldg-h1">{title}</h1>
255 <p className="fd-ldg-lede">{lede}</p>
256 <div className="fd-ldg-ctas">
257 <a className="fd-ldg-btn" href={primaryCta.href}>
258 {primaryCta.label}
259 </a>
260 <a className="fd-ldg-link" href={secondaryCta.href}>
261 <span className="fd-ldg-play" aria-hidden="true">
262 <svg viewBox="0 0 12 12" width="10" height="10"><path d="M3 1.8v8.4L10 6z" fill="currentColor" /></svg>
263 </span>
264 {secondaryCta.label}
265 </a>
266 </div>
267 <p className="fd-ldg-fine">SOC 2 Type II · Issued by Calder Trust Bank, N.A., Member FDIC</p>
268 </div>
269 <div className="fd-ldg-visual">
270 <div ref={home} className="fd-ldg-home" aria-hidden="true" />
271 </div>
272 </div>
273
274 <div ref={panel} className="fd-ldg-panel">
275 <div className="fd-ldg-panel-in">
276 <div className="fd-ldg-slotwrap">
277 <div ref={slot} className="fd-ldg-slot" aria-hidden="true" />
278 <p className="fd-ldg-slotcap">
279 {holder} · Corporate •••• {last4}
280 </p>
281 </div>
282 <div className="fd-ldg-panel-copy">
283 <p className="fd-ldg-panel-k">October · {company}</p>
284 <h2 className="fd-ldg-h2">{dockTitle}</h2>
285 <dl className="fd-ldg-stats">
286 {stats.map((s, i) => (
287 <div key={s.label} className="fd-ldg-stat" style={{ ["--i" as string]: i }}>
288 <dt>{s.label}</dt>
289 <dd>
290 {s.prefix}
291 <span ref={(n) => { nums.current[i] = n; }}>0</span>
292 {s.suffix}
293 </dd>
294 </div>
295 ))}
296 </dl>
297 </div>
298 </div>
299 </div>
300
301 <div ref={shadow} className="fd-ldg-shadow-anchor" aria-hidden="true">
302 <div className="fd-ldg-shadow" />
303 </div>
304 <div ref={anchor} className="fd-ldg-anchor" aria-hidden="true">
305 <div className="fd-ldg-enter">
306 <div ref={card} className="fd-ldg-card">
307 {Array.from({ length: 7 }, (_, i) => (
308 <div key={i} className="fd-ldg-edge" style={{ transform: `translateZ(${-(i + 1) * 0.55}px)` }} />
309 ))}
310 <div className="fd-ldg-face fd-ldg-front">
311 <div className="fd-ldg-metal" />
312 <div className="fd-ldg-band" />
313 <div className="fd-ldg-sheen" />
314 <div className="fd-ldg-fc">
315 <div className="fd-ldg-row">
316 <span className="fd-ldg-wm">LEDGER</span>
317 <svg className="fd-ldg-nfc" viewBox="0 0 24 24" width="22" height="22">
318 <path d="M7.5 8.5a5 5 0 0 1 0 7M11 6a9 9 0 0 1 0 12M14.5 3.5a13 13 0 0 1 0 17" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
319 </svg>
320 </div>
321 <div className="fd-ldg-chip">
322 <i /><i /><i /><i />
323 </div>
324 <div className="fd-ldg-row fd-ldg-bottom">
325 <div>
326 <span className="fd-ldg-name">{name}</span>
327 <span className="fd-ldg-co">{company}</span>
328 </div>
329 <div className="fd-ldg-right">
330 <span className="fd-ldg-num">•••• {last4}</span>
331 <span className="fd-ldg-net">
332 <b /> CORPORATE
333 </span>
334 </div>
335 </div>
336 </div>
337 </div>
338 <div className="fd-ldg-face fd-ldg-back">
339 <div className="fd-ldg-metal" />
340 <div className="fd-ldg-stripe" />
341 <div className="fd-ldg-band" />
342 <div className="fd-ldg-sheen" />
343 <div className="fd-ldg-bc">
344 <div className="fd-ldg-sig">
345 <span>{holder}</span>
346 <b>CVV •••</b>
347 </div>
348 <p>
349 This card is property of Calder Trust Bank, N.A. and is issued under licence to {company}. Lost or stolen: +1 (415) 555-0142 · ledger.example/help
350 </p>
351 <span className="fd-ldg-wm fd-ldg-wm-s">LEDGER</span>
352 </div>
353 </div>
354 </div>
355 </div>
356 </div>
357 </div>
358 </section>
359 );
360}
361
362const noise = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .22 0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E")`;
363
364const css = `
365.fd-ldg, .fd-ldg *, .fd-ldg *::before, .fd-ldg *::after { box-sizing: border-box; }
366.fd-ldg {
367 --fd-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
368 --fd-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
369 --fd-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
370 --fd-dur-1: 120ms; --fd-dur-2: 240ms; --fd-dur-3: 420ms; --fd-dur-4: 700ms;
371 --fd-ldg-bg: #F5F5F7;
372 --fd-ldg-ink: #0B0B0F;
373 --fd-ldg-slate: #6B6B76;
374 --fd-ldg-accent: #007AFF;
375 --fd-ldg-card-w: min(440px, 34vw);
376 --fd-ldg-sans: "SF Pro Display", ui-sans-serif, -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif;
377 position: relative; display: block; margin: 0; padding: 0;
378 height: 185vh; min-height: 1200px;
379 background: var(--fd-ldg-bg); color: var(--fd-ldg-ink);
380 font: 400 1rem/1.5 var(--fd-ldg-sans);
381 isolation: isolate;
382}
383.fd-ldg-stage {
384 position: sticky; top: 0; height: 100vh; min-height: 640px; overflow: hidden;
385 perspective: 1600px;
386}
387.fd-ldg-bgglow {
388 position: absolute; inset: -10% -10% auto auto; width: 70%; height: 110%; pointer-events: none;
389 background:
390 radial-gradient(40% 38% at 62% 46%, rgba(160, 182, 255, 0.38), transparent 70%),
391 radial-gradient(30% 30% at 78% 66%, rgba(255, 196, 168, 0.32), transparent 70%),
392 radial-gradient(28% 26% at 52% 22%, rgba(196, 236, 255, 0.5), transparent 70%);
393 filter: blur(10px);
394 animation: fd-ldg-fade 1400ms var(--fd-ease-out) both;
395}
396.fd-ldg-grid {
397 position: relative; z-index: 2; height: 100%;
398 max-width: 1240px; margin: 0 auto; padding: 120px 40px 64px;
399 display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); align-items: center; gap: 40px;
400}
401.fd-ldg-copy { will-change: transform, opacity; }
402.fd-ldg-eyebrow { margin: 0 0 28px; padding: 0; }
403.fd-ldg-pill {
404 display: inline-flex; align-items: center; gap: 8px; margin: 0; padding: 6px 14px 6px 6px;
405 border-radius: 9999px; background: #fff; border: 1px solid rgba(11, 11, 15, 0.06);
406 box-shadow: 0 1px 2px rgba(11, 11, 15, 0.04), 0 8px 24px -12px rgba(11, 11, 15, 0.18);
407 font: 500 0.8125rem/1.2 var(--fd-ldg-sans); letter-spacing: -0.005em; color: #2a2a31;
408 animation: fd-ldg-rise var(--fd-dur-4) var(--fd-ease-out) 80ms both;
409}
410.fd-ldg-pill b {
411 font: 600 0.75rem/1 var(--fd-ldg-sans); letter-spacing: 0.01em; color: #fff;
412 background: linear-gradient(180deg, #1a8cff, #0063d6); padding: 5px 9px; border-radius: 9999px;
413}
414.fd-ldg-h1 {
415 margin: 0; padding: 0; color: var(--fd-ldg-ink);
416 font: 600 clamp(2.75rem, 5.1vw, 4.75rem)/1 var(--fd-ldg-sans);
417 letter-spacing: -0.045em; text-wrap: balance;
418}
419.fd-ldg-line { display: block; overflow: hidden; padding: 0 0 0.08em; margin: 0 0 -0.08em; }
420.fd-ldg-line > span { display: inline-block; animation: fd-ldg-mask var(--fd-dur-4) var(--fd-ease-out) both; }
421.fd-ldg-line:nth-child(2) > span { animation-delay: 90ms; }
422.fd-ldg-line:nth-child(3) > span { animation-delay: 180ms; }
423.fd-ldg-h1 em {
424 font-style: normal;
425 background: linear-gradient(100deg, #0B0B0F 0%, #4a4a57 40%, #8d8d9a 100%);
426 -webkit-background-clip: text; background-clip: text; color: transparent;
427}
428.fd-ldg-lede {
429 margin: 28px 0 0; padding: 0; max-width: 30rem; color: var(--fd-ldg-slate);
430 font: 400 clamp(1.0625rem, 1.25vw, 1.25rem)/1.5 var(--fd-ldg-sans); letter-spacing: -0.012em; text-wrap: pretty;
431 animation: fd-ldg-rise var(--fd-dur-4) var(--fd-ease-out) 260ms both;
432}
433.fd-ldg-ctas { display: flex; flex-wrap: wrap; align-items: center; gap: 22px; margin: 36px 0 0; animation: fd-ldg-rise var(--fd-dur-4) var(--fd-ease-out) 360ms both; }
434.fd-ldg-btn {
435 display: inline-flex; align-items: center; margin: 0; padding: 0 24px; height: 50px; border-radius: 9999px;
436 background: var(--fd-ldg-ink); color: #fff; text-decoration: none; border: 0;
437 font: 500 1rem/1 var(--fd-ldg-sans); letter-spacing: -0.01em;
438 box-shadow: 0 1px 0 rgba(255,255,255,.12) inset, 0 10px 30px -10px rgba(11, 11, 15, 0.55);
439 transition: transform var(--fd-dur-2) var(--fd-spring), background var(--fd-dur-1);
440}
441.fd-ldg-btn:hover { background: #24242b; }
442.fd-ldg-btn:active { transform: scale(0.96); transition-duration: var(--fd-dur-1); transition-timing-function: var(--fd-ease-out); }
443.fd-ldg-link {
444 display: inline-flex; align-items: center; gap: 10px; margin: 0; padding: 0; color: var(--fd-ldg-ink); text-decoration: none;
445 font: 500 1rem/1 var(--fd-ldg-sans); letter-spacing: -0.01em;
446}
447.fd-ldg-play {
448 display: inline-grid; place-items: center; width: 30px; height: 30px; border-radius: 9999px; padding-left: 2px;
449 background: #fff; color: var(--fd-ldg-ink); box-shadow: 0 0 0 1px rgba(11,11,15,.08), 0 4px 12px -4px rgba(11,11,15,.2);
450 transition: transform var(--fd-dur-2) var(--fd-spring);
451}
452.fd-ldg-play svg { display: block; }
453.fd-ldg-link:hover .fd-ldg-play { transform: scale(1.1); }
454.fd-ldg-btn:focus-visible, .fd-ldg-link:focus-visible { outline: 2px solid rgba(0, 122, 255, 0.7); outline-offset: 2px; }
455.fd-ldg-fine { margin: 40px 0 0; padding: 0; color: #8a8a95; font: 400 0.8125rem/1.4 var(--fd-ldg-sans); letter-spacing: 0; animation: fd-ldg-fade var(--fd-dur-4) var(--fd-ease-out) 520ms both; }
456
457.fd-ldg-visual { position: relative; height: 100%; display: grid; place-items: center; }
458.fd-ldg-home { width: var(--fd-ldg-card-w); aspect-ratio: 1.586; margin: 0 0 0 4%; }
459
460.fd-ldg-anchor { position: absolute; left: 0; top: 0; z-index: 5; perspective: 1400px; pointer-events: none; }
461.fd-ldg-enter { width: 100%; height: 100%; transform-style: preserve-3d; animation: fd-ldg-cardin 1100ms var(--fd-ease-out) 120ms both; }
462.fd-ldg-card {
463 position: relative; width: 100%; height: 100%; transform-style: preserve-3d; will-change: transform;
464 transform: rotateX(9deg) rotateY(-14deg) rotateZ(-4deg);
465 --sx: 40%; --sy: 30%; --gl: .6; --band: 0%;
466}
467.fd-ldg-face, .fd-ldg-edge {
468 position: absolute; inset: 0; border-radius: 4.6% / 7.3%; overflow: hidden;
469 backface-visibility: hidden; -webkit-backface-visibility: hidden;
470}
471.fd-ldg-edge { background: linear-gradient(90deg, #4c4d55, #9a9ba4 30%, #3b3c43 60%, #8e8f98); backface-visibility: visible; -webkit-backface-visibility: visible; }
472.fd-ldg-back { transform: translateZ(-4.4px) rotateY(180deg); }
473.fd-ldg-metal {
474 position: absolute; inset: 0;
475 background:
476 ${noise},
477 repeating-linear-gradient(90deg, rgba(255,255,255,0.035) 0 1px, rgba(0,0,0,0.04) 1px 2px, transparent 2px 4px),
478 linear-gradient(160deg, rgba(255,255,255,0.10), transparent 35%, transparent 70%, rgba(255,255,255,0.05)),
479 radial-gradient(120% 140% at 0% 0%, #2c2d34 0%, #16171b 48%, #0b0b0e 100%);
480 background-size: 160px 160px, auto, auto, auto;
481 box-shadow: inset 0 0 0 1px rgba(255,255,255,0.09), inset 0 1px 0 rgba(255,255,255,0.18);
482}
483.fd-ldg-band {
484 position: absolute; inset: -40%; pointer-events: none; mix-blend-mode: screen; opacity: .9;
485 background: linear-gradient(115deg, transparent 34%, rgba(120,160,255,0.12) 42%, rgba(255,255,255,0.30) 49%, rgba(255,255,255,0.08) 52%, rgba(255,180,150,0.12) 58%, transparent 66%);
486 transform: translateX(var(--band));
487}
488.fd-ldg-sheen {
489 position: absolute; inset: 0; pointer-events: none; mix-blend-mode: screen; opacity: var(--gl);
490 background: radial-gradient(50% 70% at var(--sx) var(--sy), rgba(255,255,255,0.26), rgba(255,255,255,0.05) 45%, transparent 70%);
491}
492.fd-ldg-fc, .fd-ldg-bc { position: absolute; inset: 0; padding: 7% 7.5%; display: flex; flex-direction: column; justify-content: space-between; color: #e9e9ef; }
493.fd-ldg-row { display: flex; justify-content: space-between; align-items: center; }
494.fd-ldg-bottom { align-items: flex-end; }
495.fd-ldg-wm {
496 font: 600 clamp(0.75rem, 1.05vw, 0.9375rem)/1 var(--fd-ldg-sans); letter-spacing: 0.34em;
497 background: linear-gradient(180deg, #ffffff, #b9bac4 60%, #8c8d97); -webkit-background-clip: text; background-clip: text; color: transparent;
498 filter: drop-shadow(0 1px 0 rgba(0,0,0,0.5));
499}
500.fd-ldg-nfc { display: block; color: rgba(235,235,242,0.75); width: 9%; height: auto; }
501.fd-ldg-chip {
502 position: relative; width: 15%; aspect-ratio: 1.3; border-radius: 18%; margin: -2% 0 0;
503 background: linear-gradient(135deg, #f3e2b4 0%, #c9a65e 35%, #f7e7bd 55%, #a8853f 100%);
504 box-shadow: inset 0 0 0 1px rgba(80,55,10,0.35), 0 1px 1px rgba(0,0,0,0.4);
505}
506.fd-ldg-chip i { position: absolute; display: block; background: rgba(90,62,15,0.38); }
507.fd-ldg-chip i:nth-child(1) { left: 0; right: 0; top: 33%; height: 1px; }
508.fd-ldg-chip i:nth-child(2) { left: 0; right: 0; top: 66%; height: 1px; }
509.fd-ldg-chip i:nth-child(3) { top: 0; bottom: 0; left: 36%; width: 1px; }
510.fd-ldg-chip i:nth-child(4) { top: 22%; bottom: 22%; left: 36%; right: 36%; width: auto; background: transparent; border: 1px solid rgba(90,62,15,0.38); border-radius: 3px; }
511.fd-ldg-name, .fd-ldg-co, .fd-ldg-num { display: block; margin: 0; }
512.fd-ldg-name { font: 500 clamp(0.6875rem, 0.95vw, 0.875rem)/1.2 var(--fd-ldg-sans); letter-spacing: 0.14em; color: #ececf2; text-shadow: 0 1px 0 rgba(0,0,0,0.6); }
513.fd-ldg-co { margin-top: 4px; font: 400 clamp(0.5625rem, 0.75vw, 0.6875rem)/1.2 var(--fd-ldg-sans); letter-spacing: 0.04em; color: #9d9ea9; }
514.fd-ldg-right { text-align: right; }
515.fd-ldg-num { font: 500 clamp(0.6875rem, 0.95vw, 0.875rem)/1.2 ui-monospace, "SF Mono", Menlo, monospace; letter-spacing: 0.1em; color: #d6d6de; }
516.fd-ldg-net { display: inline-flex; align-items: center; gap: 6px; margin-top: 6px; font: 600 clamp(0.5rem, 0.62vw, 0.5625rem)/1 var(--fd-ldg-sans); letter-spacing: 0.22em; color: #a4a5b0; }
517.fd-ldg-net b { display: block; width: 14px; height: 14px; border-radius: 4px; background: conic-gradient(from 210deg, #8fb4ff, #f4c2ff, #ffd5a8, #8fb4ff); box-shadow: inset 0 0 0 1px rgba(255,255,255,0.4); }
518.fd-ldg-stripe { position: absolute; left: 0; right: 0; top: 11%; height: 19%; background: linear-gradient(180deg, #070708, #17171b); }
519.fd-ldg-bc { justify-content: flex-end; gap: 7%; }
520.fd-ldg-sig { display: flex; align-items: center; justify-content: space-between; gap: 12px; height: 18%; padding: 0 4%; border-radius: 4px; background: repeating-linear-gradient(-45deg, #e9e6dd 0 4px, #dcd8cc 4px 8px); }
521.fd-ldg-sig span { font: italic 400 clamp(0.75rem, 1.1vw, 1rem)/1 ui-serif, "New York", "Iowan Old Style", Georgia, serif; color: #2b2b33; }
522.fd-ldg-sig b { font: 600 clamp(0.5rem, 0.7vw, 0.625rem)/1 ui-monospace, "SF Mono", Menlo, monospace; color: #2b2b33; letter-spacing: 0.08em; }
523.fd-ldg-bc p { margin: 0; padding: 0; max-width: 88%; font: 400 clamp(0.4375rem, 0.58vw, 0.5625rem)/1.45 var(--fd-ldg-sans); color: #8e8f9a; letter-spacing: 0.01em; }
524.fd-ldg-wm-s { font-size: clamp(0.5625rem, 0.75vw, 0.6875rem); align-self: flex-end; }
525
526.fd-ldg-shadow-anchor { position: absolute; z-index: 1; left: 0; top: 0; width: 0; height: 0; pointer-events: none; will-change: transform; }
527.fd-ldg-shadow {
528 position: absolute; width: calc(var(--fd-ldg-card-w) * 0.9); height: calc(var(--fd-ldg-card-w) * 0.2); left: 0; top: 0;
529 border-radius: 50%; background: radial-gradient(closest-side, rgba(20, 22, 40, 0.34), rgba(20,22,40,0.12) 60%, transparent);
530 filter: blur(14px); animation: fd-ldg-fade 1200ms var(--fd-ease-out) 300ms both;
531}
532
533.fd-ldg-panel {
534 --q: 0;
535 position: absolute; z-index: 3; left: 0; right: 0; bottom: 7vh; width: min(1040px, calc(100% - 48px)); margin: 0 auto;
536 transform: translate3d(0, calc((1 - var(--q)) * 48px), 0) scale(calc(0.96 + var(--q) * 0.04));
537 opacity: var(--q); pointer-events: none;
538}
539.fd-ldg-panel-in {
540 display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.4fr); gap: 40px; align-items: center;
541 padding: 28px; border-radius: 28px;
542 background: rgba(255,255,255,0.72); border: 1px solid rgba(255,255,255,0.9);
543 box-shadow: 0 1px 0 rgba(255,255,255,0.9) inset, 0 30px 80px -30px rgba(30, 34, 60, 0.28), 0 0 0 1px rgba(11,11,15,0.04);
544 -webkit-backdrop-filter: blur(24px) saturate(1.6); backdrop-filter: blur(24px) saturate(1.6);
545}
546.fd-ldg-slotwrap { margin: 0; }
547.fd-ldg-slot { width: 100%; aspect-ratio: 1.586; border-radius: 4.6% / 7.3%; background: linear-gradient(180deg, #ececf1, #f6f6f9); box-shadow: inset 0 2px 6px rgba(11,11,15,0.08), inset 0 0 0 1px rgba(11,11,15,0.05); }
548.fd-ldg-slotcap { margin: 14px 0 0; padding: 0; font: 500 0.8125rem/1.3 var(--fd-ldg-sans); color: var(--fd-ldg-slate); letter-spacing: 0; }
549.fd-ldg-panel-k { margin: 0; padding: 0; font: 600 0.75rem/1 var(--fd-ldg-sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--fd-ldg-accent); }
550.fd-ldg-h2 { margin: 10px 0 0; padding: 0; font: 600 clamp(1.5rem, 2.4vw, 2rem)/1.1 var(--fd-ldg-sans); letter-spacing: -0.03em; color: var(--fd-ldg-ink); text-wrap: balance; }
551.fd-ldg-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px 28px; margin: 22px 0 0; padding: 0; }
552.fd-ldg-stat { margin: 0; padding: 14px 0 0; border-top: 1px solid rgba(11,11,15,0.08); }
553.fd-ldg-stat dt { margin: 0; padding: 0; font: 400 0.8125rem/1.3 var(--fd-ldg-sans); color: var(--fd-ldg-slate); letter-spacing: 0; }
554.fd-ldg-stat dd { margin: 6px 0 0; padding: 0; font: 600 clamp(1.5rem, 2.3vw, 2rem)/1 var(--fd-ldg-sans); letter-spacing: -0.03em; color: var(--fd-ldg-ink); font-variant-numeric: tabular-nums; }
555
556@keyframes fd-ldg-mask { from { transform: translateY(105%); } to { transform: none; } }
557@keyframes fd-ldg-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
558@keyframes fd-ldg-fade { from { opacity: 0; } to { opacity: 1; } }
559@keyframes fd-ldg-cardin {
560 from { opacity: 0; transform: translate3d(40px, 80px, -260px) rotateY(-38deg) rotateX(18deg); filter: blur(10px); }
561 60% { filter: blur(0); }
562 to { opacity: 1; transform: none; filter: none; }
563}
564
565@media (max-width: 820px) {
566 .fd-ldg { --fd-ldg-card-w: min(340px, 78vw); height: 175vh; min-height: 1300px; }
567 .fd-ldg-grid { grid-template-columns: 1fr; grid-template-rows: auto 1fr; padding: 96px 20px 32px; gap: 8px; align-content: start; }
568 .fd-ldg-visual { min-height: 260px; }
569 .fd-ldg-home { margin: 0; }
570 .fd-ldg-fine { display: none; }
571 .fd-ldg-ctas { margin-top: 28px; gap: 18px; }
572 .fd-ldg-lede { margin-top: 20px; }
573 .fd-ldg-eyebrow { margin-bottom: 20px; }
574 .fd-ldg-panel { bottom: 20px; width: calc(100% - 32px); }
575 .fd-ldg-panel-in { grid-template-columns: 1fr; gap: 18px; padding: 18px; border-radius: 24px; }
576 .fd-ldg-slotwrap { width: 52%; margin: 0 auto; }
577 .fd-ldg-slotcap { display: none; }
578 .fd-ldg-stats { gap: 12px 18px; margin-top: 14px; }
579 .fd-ldg-stat { padding-top: 10px; }
580 .fd-ldg-stat dd { font-size: 1.375rem; }
581 .fd-ldg-stat dt { font-size: 0.75rem; }
582 .fd-ldg-h2 { font-size: 1.375rem; }
583}
584
585.fd-ldg[data-static] { height: auto; min-height: 0; }
586.fd-ldg[data-static] .fd-ldg-stage { position: relative; height: auto; min-height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
587.fd-ldg[data-static] .fd-ldg-grid { height: auto; min-height: 88vh; }
588.fd-ldg[data-static] .fd-ldg-enter { animation: none; }
589.fd-ldg[data-static] .fd-ldg-panel { position: relative; left: auto; bottom: auto; transform: none; opacity: 1; margin: 0 auto 64px; }
590.fd-ldg[data-static] .fd-ldg-slotwrap { display: none; }
591.fd-ldg[data-static] .fd-ldg-panel-in { grid-template-columns: 1fr; }
592@media (prefers-reduced-motion: reduce) {
593 .fd-ldg *, .fd-ldg *::before { animation: none !important; transition-duration: 1ms !important; }
594}
595@media (prefers-reduced-transparency: reduce) {
596 .fd-ldg-panel-in { background: #fff; -webkit-backdrop-filter: none; backdrop-filter: none; }
597}
598`;
599
600/* ───────────────────────── Demo ───────────────────────── */
601
602export function Demo() {
603 return (
604 <div className="fd-ldg-demo">
605 <style>{demoCss}</style>
606 <header className="fd-ldg-demo-nav">
607 <a className="fd-ldg-demo-logo" href="#top" aria-label="Ledger home">
608 <svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true">
609 <rect x="1" y="4" width="18" height="12" rx="3" fill="#0B0B0F" />
610 <rect x="1" y="7" width="18" height="2.4" fill="#fff" opacity=".9" />
611 </svg>
612 Ledger
613 </a>
614 <nav aria-label="Primary">
615 <a href="#cards">Cards</a>
616 <a href="#spend">Spend</a>
617 <a href="#bills">Bill pay</a>
618 <a href="#treasury">Treasury</a>
619 <a href="#pricing">Pricing</a>
620 </nav>
621 <div className="fd-ldg-demo-actions">
622 <a href="#signin">Sign in</a>
623 <a className="fd-ldg-demo-cta" href="#open">Get started</a>
624 </div>
625 </header>
626 <HeroFintechCard />
627 <section className="fd-ldg-demo-after" aria-label="Customers">
628 <p>Running finance at 38,000 companies — from seed to IPO</p>
629 <ul>
630 <li>Halden Robotics</li>
631 <li>Northwind Bio</li>
632 <li>Kestrel Air</li>
633 <li>Oslo Freight</li>
634 <li>Marlow &amp; Finch</li>
635 <li>Tidewater</li>
636 </ul>
637 </section>
638 </div>
639 );
640}
641
642const demoCss = `
643.fd-ldg-demo { position: relative; margin: 0; padding: 0; min-height: 100vh; background: #F5F5F7; color: #0B0B0F; font: 400 1rem/1.5 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif; }
644.fd-ldg-demo *, .fd-ldg-demo *::before, .fd-ldg-demo *::after { box-sizing: border-box; }
645.fd-ldg-demo-nav {
646 position: absolute; z-index: 20; top: 0; left: 0; right: 0; height: 72px; margin: 0 auto; max-width: 1240px; padding: 0 40px;
647 display: flex; align-items: center; justify-content: space-between; gap: 24px;
648 animation: fd-ldg-demo-in 700ms cubic-bezier(0.22, 1, 0.36, 1) both;
649}
650.fd-ldg-demo-nav .fd-ldg-demo-logo { display: inline-flex; align-items: center; gap: 9px; margin: 0; color: #0B0B0F; text-decoration: none; font: 600 1.0625rem/1 "SF Pro Display", ui-sans-serif, -apple-system, "Inter", sans-serif; letter-spacing: -0.02em; }
651.fd-ldg-demo-logo svg { display: block; }
652.fd-ldg-demo-nav nav { display: flex; gap: 30px; margin: 0; padding: 0; }
653.fd-ldg-demo-nav a { color: #3a3a42; text-decoration: none; font: 500 0.875rem/1 ui-sans-serif, -apple-system, "Inter", sans-serif; letter-spacing: -0.005em; }
654.fd-ldg-demo-nav nav a:hover, .fd-ldg-demo-actions a:hover { color: #0B0B0F; }
655.fd-ldg-demo-actions { display: flex; align-items: center; gap: 20px; }
656.fd-ldg-demo-actions .fd-ldg-demo-cta { display: inline-flex; align-items: center; height: 34px; padding: 0 16px; border-radius: 9999px; background: #fff; color: #0B0B0F; box-shadow: 0 0 0 1px rgba(11,11,15,.1), 0 2px 6px -2px rgba(11,11,15,.15); }
657.fd-ldg-demo-nav a:focus-visible { outline: 2px solid rgba(0,122,255,.7); outline-offset: 2px; border-radius: 6px; }
658.fd-ldg-demo-after { margin: 0; padding: 72px 24px 120px; text-align: center; background: #F5F5F7; }
659.fd-ldg-demo-after p { margin: 0; padding: 0; color: #6B6B76; font: 500 0.875rem/1.4 ui-sans-serif, -apple-system, "Inter", sans-serif; }
660.fd-ldg-demo-after ul { display: flex; flex-wrap: wrap; justify-content: center; gap: 18px 48px; margin: 28px auto 0; padding: 0; list-style: none; max-width: 980px; }
661.fd-ldg-demo-after li { margin: 0; padding: 0; color: #9a9aa4; font: 600 1.25rem/1 "SF Pro Display", ui-sans-serif, -apple-system, "Inter", sans-serif; letter-spacing: -0.03em; }
662@keyframes fd-ldg-demo-in { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
663@media (max-width: 820px) {
664 .fd-ldg-demo-nav { padding: 0 20px; height: 64px; }
665 .fd-ldg-demo-nav nav, .fd-ldg-demo-actions a:not(.fd-ldg-demo-cta) { display: none; }
666}
667@media (prefers-reduced-motion: reduce) { .fd-ldg-demo-nav { animation: none; } }
668`;

More Heroes. Same file contract.

Agent Terminal Hero

A terminal types a prompt and streams an agent's plan while the generated dashboard assembles itself beside it.

Maison Editorial Hero

A giant serif masthead and procedural duotone fabric frames revealed by clip-path wipes, drifting apart on scroll.

Fjord Parallax Hero

A layered SVG landscape with deep pointer and scroll parallax, a mirrored lake and golden hour fading to dusk.