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.
A dotted canvas globe with energy arcs pulsing between sites, and live MW and tCO₂ counters ticking beneath.
npx shadcn@latest add https://freedesignmd.com/r/hero-climate-data.json1/**2 * HeroClimateData — a dark climate/energy hero. A canvas globe made of dotted3 * land (procedural continents, no map data) spins slowly; glowing arcs carry4 * energy between grid nodes, and live counters (MW dispatched, tCO₂ avoided,5 * sites online) tick in real time. Drag or move the pointer to tilt the6 * globe; on scroll the globe rises and scales while the copy parallaxes away.7 *8 * Props: title, lede, primaryCta, secondaryCta, metrics9 * ({ label, base, perSecond, unit, decimals }[]), nodes ([lat, lon][]).10 * CSS vars: --fd-grd-bg, --fd-grd-accent, --fd-grd-accent-2.11 *12 * Browser support: all evergreen browsers (2D canvas). DPR capped at 2.13 * Motion: single rAF loop, paused off-screen (IntersectionObserver) and in14 * hidden tabs; pointer tilt on a spring (k=380, c=30, mass 4); scroll via15 * getBoundingClientRect (transform/opacity). prefers-reduced-motion renders16 * one static frame and frozen counters.17 * Accessibility: canvas is aria-hidden; metrics are a <dl> whose values are18 * updated at most every 500 ms and are not announced (aria-live off).19 */20import { useEffect, useRef } from "react";21import type { ReactNode } from "react";22 23export type GridMetric = { label: string; base: number; perSecond: number; unit: string; decimals?: number };24 25export interface HeroClimateDataProps {26 eyebrow?: ReactNode;27 title?: ReactNode;28 lede?: ReactNode;29 primaryCta?: { label: string; href: string };30 secondaryCta?: { label: string; href: string };31 metrics?: GridMetric[];32 nodes?: [number, number][];33}34 35const DEFAULT_METRICS: GridMetric[] = [36 { label: "Clean power dispatched now", base: 18432.6, perSecond: 3.7, unit: "MW", decimals: 1 },37 { label: "CO₂ avoided this year", base: 2841907, perSecond: 41, unit: "tCO₂" },38 { label: "Batteries & farms online", base: 3214, perSecond: 0.02, unit: "sites" },39];40 41// [lat, lon] — wind farms, solar parks and storage sites (fictional)42const DEFAULT_NODES: [number, number][] = [43 [55.7, 8.1], [52.4, 4.9], [40.4, -3.7], [37.4, -122], [34, -117.5], [31.5, -100], [41.9, -87.6], [-23.5, -46.6],44 [-33.9, 151.2], [-31.9, 115.9], [35.7, 139.7], [1.35, 103.8], [25.2, 55.3], [19, 72.8], [-26.2, 28], [64.1, -21.9], [59.3, 18], [45.5, -73.6],45];46 47const ARCS: [number, number][] = [48 [0, 1], [1, 2], [2, 15], [3, 4], [4, 5], [5, 6], [6, 17], [17, 15], [7, 14], [8, 9], [10, 11], [11, 13], [12, 13], [12, 2], [16, 0], [9, 11], [14, 12], [3, 10],49];50 51// tiny deterministic value-noise for continents52function hash(x: number, y: number) {53 const s = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;54 return s - Math.floor(s);55}56function vnoise(x: number, y: number) {57 const xi = Math.floor(x);58 const yi = Math.floor(y);59 const xf = x - xi;60 const yf = y - yi;61 const u = xf * xf * (3 - 2 * xf);62 const v = yf * yf * (3 - 2 * yf);63 const a = hash(xi, yi);64 const b = hash(xi + 1, yi);65 const c = hash(xi, yi + 1);66 const d = hash(xi + 1, yi + 1);67 return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;68}69 70// rough continental blobs (lat, lon, radius°) so the land reads as Earth-like71const BLOBS: [number, number, number][] = [72 [50, 10, 22], [60, 60, 30], [45, 95, 28], [30, 110, 18], [22, 78, 12], [25, 45, 12], [8, 20, 22], [-12, 25, 20], [-28, 25, 10],73 [50, -100, 24], [60, -110, 18], [35, -95, 16], [68, -45, 10], [15, -90, 6], [-8, -60, 18], [-25, -60, 13], [-42, -68, 6],74 [-25, 134, 16], [0, 115, 6], [-4, 140, 5], [36, 138, 4], [64, 100, 16], [8, 0, 10],75];76function isLand(lat: number, lon: number) {77 let v = 0;78 for (const [la, lo, r] of BLOBS) {79 let dl = lon - lo;80 if (dl > 180) dl -= 360;81 if (dl < -180) dl += 360;82 const d = Math.hypot((lat - la) * 1.1, dl * Math.cos((lat * Math.PI) / 180));83 v = Math.max(v, 1 - d / r);84 }85 v += (vnoise(lon / 9 + 20, lat / 9 + 20) - 0.5) * 0.7;86 return v > 0.12;87}88 89export function HeroClimateData({90 eyebrow = "Gridline · virtual power plant",91 title = (92 <>93 Every battery, turbine and panel —94 <span className="fd-grd-glow"> one grid that thinks.</span>95 </>96 ),97 lede = "Gridline orchestrates 3,214 distributed energy sites across 19 markets, dispatching clean power in under 200 ms when the grid needs it most.",98 primaryCta = { label: "Connect your assets", href: "#connect" },99 secondaryCta = { label: "Live grid map", href: "#map" },100 metrics = DEFAULT_METRICS,101 nodes = DEFAULT_NODES,102}: HeroClimateDataProps) {103 const root = useRef<HTMLElement>(null);104 const cvs = useRef<HTMLCanvasElement>(null);105 const globeWrap = useRef<HTMLDivElement>(null);106 const copy = useRef<HTMLDivElement>(null);107 const vals = useRef<(HTMLElement | null)[]>([]);108 109 useEffect(() => {110 const el = root.current;111 const canvas = cvs.current;112 if (!el || !canvas) return;113 const ctx = canvas.getContext("2d");114 if (!ctx) return;115 const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;116 117 // sample land points on a fibonacci sphere118 const N = 5200;119 const pts: { x: number; y: number; z: number; land: boolean }[] = [];120 const golden = Math.PI * (3 - Math.sqrt(5));121 for (let i = 0; i < N; i++) {122 const y = 1 - (i / (N - 1)) * 2;123 const r = Math.sqrt(1 - y * y);124 const th = golden * i;125 const x = Math.cos(th) * r;126 const z = Math.sin(th) * r;127 const lat = (Math.asin(y) * 180) / Math.PI;128 const lon = (Math.atan2(z, x) * 180) / Math.PI;129 const land = isLand(lat, lon);130 if (land || i % 5 === 0) pts.push({ x, y, z, land });131 }132 const toVec = (lat: number, lon: number) => {133 const la = (lat * Math.PI) / 180;134 const lo = (lon * Math.PI) / 180;135 return { x: Math.cos(la) * Math.cos(lo), y: Math.sin(la), z: Math.cos(la) * Math.sin(lo) };136 };137 const nv = nodes.map(([la, lo]) => toVec(la, lo));138 // great-circle arcs, lifted139 const arcs = ARCS.filter(([a, b]) => a < nv.length && b < nv.length).map(([a, b], k) => {140 const A = nv[a];141 const B = nv[b];142 const dot = Math.max(-1, Math.min(1, A.x * B.x + A.y * B.y + A.z * B.z));143 const om = Math.acos(dot);144 const seg: { x: number; y: number; z: number }[] = [];145 const S = 48;146 for (let i = 0; i <= S; i++) {147 const t = i / S;148 const s1 = Math.sin((1 - t) * om) / Math.sin(om || 1);149 const s2 = Math.sin(t * om) / Math.sin(om || 1);150 const lift = 1 + Math.sin(t * Math.PI) * (0.08 + om * 0.12);151 seg.push({ x: (A.x * s1 + B.x * s2) * lift, y: (A.y * s1 + B.y * s2) * lift, z: (A.z * s1 + B.z * s2) * lift });152 }153 return { seg, phase: (k * 0.37) % 1, speed: 0.18 + ((k * 13) % 7) * 0.025 };154 });155 156 let W = 0;157 let H = 0;158 let dpr = 1;159 const resize = () => {160 const r = canvas.getBoundingClientRect();161 dpr = Math.min(2, window.devicePixelRatio || 1);162 W = r.width;163 H = r.height;164 canvas.width = Math.round(W * dpr);165 canvas.height = Math.round(H * dpr);166 };167 resize();168 const ro = new ResizeObserver(resize);169 ro.observe(canvas);170 171 const sp = { x: 0, y: 0, vx: 0, vy: 0, tx: 0, ty: 0 };172 const onMove = (e: PointerEvent) => {173 if (e.pointerType === "touch") return;174 const r = el.getBoundingClientRect();175 sp.tx = ((e.clientX - r.left) / r.width) * 2 - 1;176 sp.ty = ((e.clientY - r.top) / r.height) * 2 - 1;177 };178 el.addEventListener("pointermove", onMove);179 180 const t0 = performance.now();181 let last = t0;182 let lastText = 0;183 const draw = (now: number) => {184 const dt = Math.min((now - last) / 1000, 0.05);185 last = now;186 for (let i = 0; i < 2; i++) {187 const h = dt / 2;188 sp.vx += ((-380 * (sp.x - sp.tx) - 30 * sp.vx) / 4) * h;189 sp.vy += ((-380 * (sp.y - sp.ty) - 30 * sp.vy) / 4) * h;190 sp.x += sp.vx * h;191 sp.y += sp.vy * h;192 }193 const t = Math.max(0, (now - t0) / 1000);194 const rotY = 2.2 + t * 0.07 + sp.x * 0.35;195 const rotX = -0.32 + sp.y * 0.18;196 const cy = Math.cos(rotY);197 const sy = Math.sin(rotY);198 const cx = Math.cos(rotX);199 const sx = Math.sin(rotX);200 const R = Math.min(W, H) * 0.44;201 const ox = W / 2;202 const oy = H / 2;203 const proj = (p: { x: number; y: number; z: number }) => {204 const x1 = p.x * cy - p.z * sy;205 const z1 = p.x * sy + p.z * cy;206 const y2 = p.y * cx - z1 * sx;207 const z2 = p.y * sx + z1 * cx;208 return { X: ox + x1 * R, Y: oy - y2 * R, Z: z2 };209 };210 ctx.setTransform(dpr, 0, 0, dpr, 0, 0);211 ctx.clearRect(0, 0, W, H);212 // atmosphere213 const g = ctx.createRadialGradient(ox, oy, R * 0.9, ox, oy, R * 1.28);214 g.addColorStop(0, "rgba(64, 224, 168, 0.20)");215 g.addColorStop(0.4, "rgba(64, 224, 168, 0.06)");216 g.addColorStop(1, "rgba(64, 224, 168, 0)");217 ctx.fillStyle = g;218 ctx.beginPath();219 ctx.arc(ox, oy, R * 1.3, 0, Math.PI * 2);220 ctx.fill();221 // sphere body222 const body = ctx.createRadialGradient(ox - R * 0.35, oy - R * 0.4, R * 0.1, ox, oy, R);223 body.addColorStop(0, "#0f2a26");224 body.addColorStop(1, "#050b0c");225 ctx.fillStyle = body;226 ctx.beginPath();227 ctx.arc(ox, oy, R, 0, Math.PI * 2);228 ctx.fill();229 // dots230 for (let i = 0; i < pts.length; i++) {231 const p = pts[i];232 const q = proj(p);233 if (q.Z < -0.05) continue;234 const shade = 0.25 + q.Z * 0.75;235 if (p.land) {236 ctx.fillStyle = `rgba(150, 255, 214, ${(0.25 + shade * 0.6).toFixed(3)})`;237 const s = 1.15 + q.Z * 0.6;238 ctx.fillRect(q.X - s / 2, q.Y - s / 2, s, s);239 } else {240 ctx.fillStyle = `rgba(120, 170, 190, ${(shade * 0.14).toFixed(3)})`;241 ctx.fillRect(q.X - 0.5, q.Y - 0.5, 1, 1);242 }243 }244 // arcs245 ctx.lineCap = "round";246 for (const a of arcs) {247 const P = a.seg.map(proj);248 ctx.beginPath();249 let started = false;250 for (const q of P) {251 if (q.Z < -0.02) {252 started = false;253 continue;254 }255 if (!started) ctx.moveTo(q.X, q.Y);256 else ctx.lineTo(q.X, q.Y);257 started = true;258 }259 ctx.strokeStyle = "rgba(94, 234, 180, 0.16)";260 ctx.lineWidth = 1;261 ctx.stroke();262 // pulse263 const head = (((a.phase + t * a.speed) % 1) + 1) % 1;264 const len = 0.22;265 ctx.beginPath();266 started = false;267 for (let i = 0; i < P.length; i++) {268 const u = i / (P.length - 1);269 if (u < head - len || u > head) continue;270 const q = P[i];271 if (q.Z < -0.02) {272 started = false;273 continue;274 }275 if (!started) ctx.moveTo(q.X, q.Y);276 else ctx.lineTo(q.X, q.Y);277 started = true;278 }279 const hq = P[Math.min(P.length - 1, Math.floor(head * (P.length - 1)))];280 const tq = P[Math.max(0, Math.floor((head - len) * (P.length - 1)))];281 const lg = ctx.createLinearGradient(tq.X, tq.Y, hq.X, hq.Y);282 lg.addColorStop(0, "rgba(255, 214, 102, 0)");283 lg.addColorStop(1, "rgba(255, 228, 140, 0.95)");284 ctx.strokeStyle = lg;285 ctx.lineWidth = 1.8;286 ctx.stroke();287 if (hq.Z > 0) {288 ctx.fillStyle = "rgba(255, 236, 170, 0.95)";289 ctx.beginPath();290 ctx.arc(hq.X, hq.Y, 2, 0, Math.PI * 2);291 ctx.fill();292 }293 }294 // nodes295 for (let i = 0; i < nv.length; i++) {296 const q = proj(nv[i]);297 if (q.Z < 0) continue;298 const pulse = (t * 0.6 + i * 0.13) % 1;299 ctx.strokeStyle = `rgba(94, 234, 180, ${(0.6 * (1 - pulse) * q.Z).toFixed(3)})`;300 ctx.lineWidth = 1;301 ctx.beginPath();302 ctx.arc(q.X, q.Y, 3 + pulse * 12, 0, Math.PI * 2);303 ctx.stroke();304 ctx.fillStyle = "#5EEAB4";305 ctx.beginPath();306 ctx.arc(q.X, q.Y, 2.6, 0, Math.PI * 2);307 ctx.fill();308 }309 // rim light310 const rim = ctx.createRadialGradient(ox, oy, R * 0.82, ox, oy, R);311 rim.addColorStop(0, "rgba(94,234,180,0)");312 rim.addColorStop(1, "rgba(94,234,180,0.18)");313 ctx.fillStyle = rim;314 ctx.beginPath();315 ctx.arc(ox, oy, R, 0, Math.PI * 2);316 ctx.fill();317 318 // scroll + counters319 const r = el.getBoundingClientRect();320 const p = Math.min(1, Math.max(0, -r.top / Math.max(1, r.height)));321 if (globeWrap.current) globeWrap.current.style.transform = `translate3d(0, ${(-p * 120).toFixed(1)}px, 0) scale(${(1 + p * 0.35).toFixed(4)})`;322 if (copy.current) {323 copy.current.style.transform = `translate3d(0, ${(p * -60).toFixed(1)}px, 0)`;324 copy.current.style.opacity = `${(1 - p * 1.5).toFixed(3)}`;325 }326 if (now - lastText > 120) {327 lastText = now;328 const secs = (Date.now() / 1000) % 86400;329 metrics.forEach((m, i) => {330 const n = vals.current[i];331 if (!n) return;332 const wobble = m.unit === "MW" ? Math.sin(t * 0.9) * 22 + Math.sin(t * 2.3) * 6 : 0;333 const v = m.base + m.perSecond * (m.unit === "MW" ? t : secs) + wobble;334 n.textContent = v.toLocaleString("en-US", { minimumFractionDigits: m.decimals ?? 0, maximumFractionDigits: m.decimals ?? 0 });335 });336 }337 };338 339 if (reduced) {340 draw(t0 + 1);341 return () => {342 ro.disconnect();343 el.removeEventListener("pointermove", onMove);344 };345 }346 let raf = 0;347 let visible = true;348 const loop = (now: number) => {349 raf = 0;350 draw(now);351 if (visible && !document.hidden) raf = requestAnimationFrame(loop);352 };353 const start = () => {354 if (!raf && visible && !document.hidden) {355 last = performance.now();356 raf = requestAnimationFrame(loop);357 }358 };359 const io = new IntersectionObserver(([e]) => {360 visible = e.isIntersecting;361 if (visible) start();362 });363 io.observe(el);364 document.addEventListener("visibilitychange", start);365 start();366 return () => {367 cancelAnimationFrame(raf);368 io.disconnect();369 ro.disconnect();370 document.removeEventListener("visibilitychange", start);371 el.removeEventListener("pointermove", onMove);372 };373 }, [metrics, nodes]);374 375 return (376 <section ref={root} className="fd-grd" aria-label="Gridline">377 <style>{css}</style>378 <div className="fd-grd-stars" aria-hidden="true" />379 <div ref={globeWrap} className="fd-grd-globe">380 <canvas ref={cvs} className="fd-grd-canvas" aria-hidden="true" />381 </div>382 <div ref={copy} className="fd-grd-copy">383 <p className="fd-grd-eyebrow">384 <span className="fd-grd-live" aria-hidden="true" />385 {eyebrow}386 </p>387 <h1 className="fd-grd-h1">{title}</h1>388 <p className="fd-grd-lede">{lede}</p>389 <div className="fd-grd-ctas">390 <a className="fd-grd-btn" href={primaryCta.href}>391 {primaryCta.label}392 </a>393 <a className="fd-grd-ghost" href={secondaryCta.href}>394 <span className="fd-grd-live" aria-hidden="true" />395 {secondaryCta.label}396 </a>397 </div>398 </div>399 <dl className="fd-grd-metrics">400 {metrics.map((m, i) => (401 <div key={m.label} className="fd-grd-m" style={{ animationDelay: `${700 + i * 120}ms` }}>402 <dt>{m.label}</dt>403 <dd>404 <span ref={(n) => { vals.current[i] = n; }}>405 {m.base.toLocaleString("en-US", { minimumFractionDigits: m.decimals ?? 0, maximumFractionDigits: m.decimals ?? 0 })}406 </span>407 <small>{m.unit}</small>408 </dd>409 <svg className="fd-grd-spark" viewBox="0 0 120 24" preserveAspectRatio="none" aria-hidden="true">410 <path d={SPARKS[i % SPARKS.length]} fill="none" stroke="currentColor" strokeWidth="1.4" vectorEffect="non-scaling-stroke" pathLength="1" />411 </svg>412 </div>413 ))}414 </dl>415 </section>416 );417}418 419const SPARKS = [420 "M0 18 L10 16 L20 17 L30 12 L40 14 L50 9 L60 11 L70 7 L80 9 L90 5 L100 7 L110 3 L120 4",421 "M0 22 L15 20 L30 18 L45 16 L60 13 L75 11 L90 8 L105 6 L120 3",422 "M0 14 L12 14 L24 12 L36 13 L48 10 L60 11 L72 9 L84 9 L96 7 L108 7 L120 5",423];424 425const css = `426.fd-grd, .fd-grd *, .fd-grd *::before, .fd-grd *::after { box-sizing: border-box; }427.fd-grd {428 --fd-ease-out: cubic-bezier(0.22, 1, 0.36, 1);429 --fd-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);430 --fd-spring: cubic-bezier(0.34, 1.56, 0.64, 1);431 --fd-dur-1: 120ms; --fd-dur-2: 240ms; --fd-dur-3: 420ms; --fd-dur-4: 700ms;432 --fd-grd-bg: #030807;433 --fd-grd-accent: #5EEAB4;434 --fd-grd-accent-2: #FFE48C;435 --fd-grd-sans: "SF Pro Display", ui-sans-serif, -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif;436 --fd-grd-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;437 position: relative; display: block; overflow: hidden; margin: 0; padding: 0; min-height: 880px;438 background: radial-gradient(80% 60% at 70% 50%, #08201b 0%, var(--fd-grd-bg) 65%); color: #E8F5F0;439 font: 400 1rem/1.5 var(--fd-grd-sans); isolation: isolate;440}441.fd-grd-stars {442 position: absolute; inset: 0; pointer-events: none; opacity: 0.6;443 background-image: radial-gradient(1px 1px at 12% 18%, #fff 50%, transparent), radial-gradient(1px 1px at 32% 72%, #cfe 50%, transparent), radial-gradient(1px 1px at 58% 12%, #fff 50%, transparent), radial-gradient(1px 1px at 84% 30%, #dfe 50%, transparent), radial-gradient(1px 1px at 92% 82%, #fff 50%, transparent), radial-gradient(1px 1px at 6% 88%, #fff 50%, transparent), radial-gradient(1.5px 1.5px at 46% 46%, #fff 50%, transparent);444}445.fd-grd-globe { position: absolute; right: -6%; top: 50%; width: min(980px, 72vw); aspect-ratio: 1; margin-top: max(-490px, -36vw); will-change: transform; animation: fd-grd-globein 2000ms var(--fd-ease-out) both; }446.fd-grd-canvas { display: block; width: 100%; height: 100%; }447.fd-grd-copy { position: relative; z-index: 2; max-width: 1240px; margin: 0 auto; padding: 150px 48px 0; will-change: transform, opacity; }448.fd-grd-eyebrow {449 display: inline-flex; align-items: center; gap: 10px; margin: 0 0 24px; padding: 0;450 font: 500 0.75rem/1 var(--fd-grd-mono); letter-spacing: 0.12em; text-transform: uppercase; color: var(--fd-grd-accent);451 animation: fd-grd-rise var(--fd-dur-4) var(--fd-ease-out) 100ms both;452}453.fd-grd-live { position: relative; display: block; width: 7px; height: 7px; border-radius: 9999px; background: var(--fd-grd-accent); box-shadow: 0 0 12px var(--fd-grd-accent); animation: fd-grd-blink 2s var(--fd-ease-in-out) infinite; }454.fd-grd-h1 {455 margin: 0; padding: 0; max-width: 13ch; color: #F2FBF7;456 font: 600 clamp(2.5rem, 5.4vw, 4.875rem)/1 var(--fd-grd-sans); letter-spacing: -0.045em; text-wrap: balance;457 animation: fd-grd-blur 1100ms var(--fd-ease-out) 180ms both;458}459.fd-grd-glow { background: linear-gradient(90deg, #5EEAB4, #C8FFE6 60%, #FFE48C); -webkit-background-clip: text; background-clip: text; color: transparent; }460.fd-grd-lede { margin: 24px 0 0; padding: 0; max-width: 29rem; color: rgba(214, 236, 228, 0.7); font: 400 clamp(1rem, 1.2vw, 1.125rem)/1.55 var(--fd-grd-sans); letter-spacing: -0.01em; text-wrap: pretty; animation: fd-grd-rise var(--fd-dur-4) var(--fd-ease-out) 380ms both; }461.fd-grd-ctas { display: flex; flex-wrap: wrap; gap: 12px; margin: 34px 0 0; animation: fd-grd-rise var(--fd-dur-4) var(--fd-ease-out) 480ms both; }462.fd-grd-btn {463 display: inline-flex; align-items: center; margin: 0; height: 48px; padding: 0 22px; border-radius: 9999px; border: 0;464 background: var(--fd-grd-accent); color: #02130D; text-decoration: none; font: 600 0.9375rem/1 var(--fd-grd-sans); letter-spacing: -0.01em;465 box-shadow: 0 0 0 1px rgba(94,234,180,0.4), 0 10px 40px -8px rgba(94,234,180,0.55);466 transition: transform var(--fd-dur-2) var(--fd-spring), box-shadow var(--fd-dur-2);467}468.fd-grd-btn:hover { box-shadow: 0 0 0 1px rgba(94,234,180,0.6), 0 14px 50px -6px rgba(94,234,180,0.7); }469.fd-grd-ghost { display: inline-flex; align-items: center; gap: 10px; margin: 0; height: 48px; padding: 0 20px; border-radius: 9999px; border: 1px solid rgba(94,234,180,0.25); background: rgba(94,234,180,0.05); color: #E8F5F0; text-decoration: none; font: 500 0.9375rem/1 var(--fd-grd-sans); transition: transform var(--fd-dur-2) var(--fd-spring), border-color var(--fd-dur-1); }470.fd-grd-ghost:hover { border-color: rgba(94,234,180,0.5); }471.fd-grd-btn:active, .fd-grd-ghost:active { transform: scale(0.96); transition-duration: var(--fd-dur-1); }472.fd-grd-btn:focus-visible, .fd-grd-ghost:focus-visible { outline: 2px solid rgba(94,234,180,0.7); outline-offset: 2px; }473.fd-grd-metrics {474 position: absolute; z-index: 2; left: 50%; bottom: 40px; transform: translateX(-50%); width: min(1144px, calc(100% - 96px)); margin: 0; padding: 0;475 display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0;476 border-top: 1px solid rgba(94,234,180,0.16);477}478.fd-grd-m { position: relative; margin: 0; padding: 22px 24px 0 0; animation: fd-grd-rise var(--fd-dur-4) var(--fd-ease-out) both; }479.fd-grd-m + .fd-grd-m { padding-left: 24px; border-left: 1px solid rgba(94,234,180,0.1); }480.fd-grd-m dt { margin: 0; padding: 0; font: 500 0.6875rem/1.3 var(--fd-grd-mono); letter-spacing: 0.08em; text-transform: uppercase; color: rgba(214,236,228,0.55); }481.fd-grd-m dd { display: flex; align-items: baseline; gap: 8px; margin: 10px 0 0; padding: 0; font: 500 clamp(1.5rem, 2.6vw, 2.25rem)/1 var(--fd-grd-mono); letter-spacing: -0.04em; color: #F2FBF7; font-variant-numeric: tabular-nums; }482.fd-grd-m dd small { font: 500 0.8125rem/1 var(--fd-grd-mono); letter-spacing: 0; color: var(--fd-grd-accent); }483.fd-grd-spark { position: absolute; right: 24px; top: 26px; width: 96px; height: 22px; display: block; color: var(--fd-grd-accent); opacity: 0.7; }484.fd-grd-spark path { stroke-dasharray: 1; stroke-dashoffset: 1; animation: fd-grd-draw 1600ms var(--fd-ease-out) 1000ms forwards; }485 486@keyframes fd-grd-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }487@keyframes fd-grd-blur { from { opacity: 0; filter: blur(12px); transform: translateY(12px); } to { opacity: 1; filter: none; transform: none; } }488@keyframes fd-grd-globein { from { opacity: 0; transform: scale(0.82) rotate(-8deg); filter: blur(12px); } to { opacity: 1; transform: none; filter: none; } }489@keyframes fd-grd-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }490@keyframes fd-grd-draw { to { stroke-dashoffset: 0; } }491 492@media (max-width: 900px) {493 .fd-grd { min-height: 844px; }494 .fd-grd-globe { right: auto; left: 50%; top: auto; bottom: -34vw; width: 120vw; margin: 0 0 0 -60vw; opacity: 0.9; }495 .fd-grd-copy { text-shadow: 0 1px 12px #030807; }496 .fd-grd-copy { padding: 104px 20px 0; }497 .fd-grd-metrics { width: calc(100% - 40px); bottom: 24px; grid-template-columns: 1fr 1fr; }498 .fd-grd-m { padding: 16px 12px 0 0; }499 .fd-grd-m + .fd-grd-m { padding-left: 12px; }500 .fd-grd-m:nth-child(3) { display: none; }501 .fd-grd-spark { display: none; }502}503@media (prefers-reduced-motion: reduce) { .fd-grd *, .fd-grd *::before { animation: none !important; } .fd-grd-spark path { stroke-dashoffset: 0; } }504`;505 506/* ───────────────────────── Demo ───────────────────────── */507 508export function Demo() {509 return (510 <div className="fd-grd-demo">511 <style>{demoCss}</style>512 <header className="fd-grd-demo-nav">513 <a className="fd-grd-demo-logo" href="#top">514 <svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true">515 <circle cx="10" cy="10" r="8" fill="none" stroke="#5EEAB4" strokeWidth="1.6" />516 <path d="M2 10h16M10 2c3 3 3 13 0 16M10 2c-3 3-3 13 0 16" fill="none" stroke="#5EEAB4" strokeWidth="1.2" />517 </svg>518 Gridline519 </a>520 <nav aria-label="Primary">521 <a href="#platform">Platform</a>522 <a href="#utilities">Utilities</a>523 <a href="#owners">Asset owners</a>524 <a href="#impact">Impact report</a>525 </nav>526 <a className="fd-grd-demo-cta" href="#demo">Book a demo</a>527 </header>528 <HeroClimateData />529 </div>530 );531}532 533const demoCss = `534.fd-grd-demo { position: relative; margin: 0; padding: 0; min-height: 100vh; background: #030807; }535.fd-grd-demo *, .fd-grd-demo *::before, .fd-grd-demo *::after { box-sizing: border-box; }536.fd-grd-demo-nav { position: absolute; z-index: 10; top: 0; left: 0; right: 0; max-width: 1240px; height: 72px; margin: 0 auto; padding: 0 48px; display: flex; align-items: center; justify-content: space-between; gap: 20px; }537.fd-grd-demo-logo { display: inline-flex; align-items: center; gap: 10px; margin: 0; color: #F2FBF7; text-decoration: none; font: 600 1.0625rem/1 "SF Pro Display", ui-sans-serif, -apple-system, "Inter", sans-serif; letter-spacing: -0.02em; }538.fd-grd-demo-logo svg { display: block; }539.fd-grd-demo-nav nav { display: flex; gap: 30px; }540.fd-grd-demo-nav nav a { color: rgba(214,236,228,0.65); text-decoration: none; font: 500 0.875rem/1 ui-sans-serif, -apple-system, "Inter", sans-serif; transition: color 120ms; }541.fd-grd-demo-nav nav a:hover { color: #F2FBF7; }542.fd-grd-demo-cta { display: inline-flex; align-items: center; height: 36px; padding: 0 16px; border-radius: 9999px; color: #F2FBF7; text-decoration: none; border: 1px solid rgba(94,234,180,0.3); font: 500 0.875rem/1 ui-sans-serif, -apple-system, "Inter", sans-serif; }543.fd-grd-demo-nav a:focus-visible { outline: 2px solid rgba(94,234,180,.7); outline-offset: 2px; border-radius: 8px; }544@media (max-width: 900px) { .fd-grd-demo-nav { padding: 0 20px; height: 64px; } .fd-grd-demo-nav nav { display: none; } }545`;A photoreal CSS-3D metal card that tilts to the cursor, then flips and docks on scroll while spend stats count up.
A terminal types a prompt and streams an agent's plan while the generated dashboard assembles itself beside it.
A giant serif masthead and procedural duotone fabric frames revealed by clip-path wipes, drifting apart on scroll.