1/**
2 * Prism — AI product-photography landing page with Apple-style Liquid Glass.
3 *
4 * Product imagery: three real studio photographs in public/img/prism/ —
5 * parfum.webp, balm.webp, tube.webp (each with a -480 variant), grey seamless
6 * backdrop, soft light, reflective floor. Every "campaign variation" on the
7 * page is the same three photos re-lit and re-cropped with CSS only (blend-mode
8 * tints, filters, object-position + scale), so there are no other image files.
9 * Copy them next to your page or swap in your own photos of the same style.
10 *
11 * A complete, self-contained single page: React is the only import and every
12 * style lives in the <style> block at the bottom of this file (classes are
13 * prefixed `fd-prs-`). Drop it into any React 18+ project and render <Page />.
14 *
15 * Liquid Glass: the in-file <Glass> helper renders a frosted surface
16 * everywhere and, in Chromium, upgrades to true refraction — a per-element
17 * SVG filter (feImage displacement map generated on a canvas from the
18 * element's own size and radius, three chromatic displacement passes, slight
19 * blur and saturation) applied with `backdrop-filter: url(#id)`. Following
20 * Apple's HIG, glass is used only for the control layer that floats above
21 * content (nav, prompt bar, toggles, sliders, carousel buttons) — never for
22 * content cards.
23 *
24 * Motion: drifting mesh backdrop, nav that condenses into a glass capsule,
25 * hero lighting cycle over a fan of the three photos, pointer + scroll
26 * parallax, draggable refraction lens with spring follow, sticky feature
27 * scenes with parallax depth layers, column-parallax gallery, before/after
28 * reveal, count-up stats, momentum carousel (drag, fling, spring snap),
29 * spring-thumb pricing toggle, staggered reveals.
30 *
31 * Browser support: evergreen (the photo crops use container query units).
32 * Refraction is Chromium-only (frosted fallback elsewhere). Accessibility:
33 * landmarks, real buttons/inputs, keyboard carousel, aria-live, reduced-motion
34 * / reduced-transparency / more-contrast fallbacks. SSR-safe: nothing touches
35 * window during render.
36 */
37import {
38 useCallback,
39 useEffect,
40 useId,
41 useRef,
42 useState,
43 version,
44 type CSSProperties,
45 type ReactNode,
46} from "react";
47
48
49/* ─────────────────────────── helpers ─────────────────────────── */
50
51const reducedMotion = () =>
52 typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
53const clamp = (n: number, a: number, b: number) => (n < a ? a : n > b ? b : n);
54const clamp01 = (n: number) => clamp(n, 0, 1);
55
56function useReveal(root: React.RefObject<HTMLElement | null>) {
57 useEffect(() => {
58 const el = root.current;
59 if (!el) return;
60 el.setAttribute("data-js", "");
61 const items = Array.from(el.querySelectorAll<HTMLElement>("[data-reveal]"));
62 if (typeof IntersectionObserver === "undefined" || reducedMotion()) {
63 items.forEach((i) => i.setAttribute("data-in", ""));
64 return;
65 }
66 const io = new IntersectionObserver(
67 (entries) => {
68 for (const e of entries)
69 if (e.isIntersecting) {
70 e.target.setAttribute("data-in", "");
71 io.unobserve(e.target);
72 }
73 },
74 { rootMargin: "0px 0px -8% 0px" },
75 );
76 items.forEach((i) => io.observe(i));
77 return () => io.disconnect();
78 }, [root]);
79}
80
81function useScrollFrame(ref: React.RefObject<HTMLElement | null>, fn: () => void) {
82 const fnRef = useRef(fn);
83 fnRef.current = fn;
84 useEffect(() => {
85 const el = ref.current;
86 if (!el) return;
87 let raf = 0;
88 let active = true;
89 const run = () => {
90 raf = 0;
91 if (active) fnRef.current();
92 };
93 const queue = () => {
94 if (!raf) raf = requestAnimationFrame(run);
95 };
96 const io =
97 typeof IntersectionObserver !== "undefined"
98 ? new IntersectionObserver(
99 ([e]) => {
100 active = e.isIntersecting;
101 if (active) queue();
102 },
103 { rootMargin: "200px 0px" },
104 )
105 : undefined;
106 io?.observe(el);
107 queue();
108 window.addEventListener("scroll", queue, { passive: true });
109 window.addEventListener("resize", queue);
110 return () => {
111 io?.disconnect();
112 window.removeEventListener("scroll", queue);
113 window.removeEventListener("resize", queue);
114 if (raf) cancelAnimationFrame(raf);
115 };
116 }, [ref]);
117}
118
119/* ─────────────────────────── Liquid Glass helper ─────────────────────────── */
120
121function supportsSvgBackdrop() {
122 if (typeof navigator === "undefined" || typeof window === "undefined") return false;
123 const ua = navigator.userAgent;
124 const chromium = /(Chrome|Chromium)\/\d+/.test(ua) && !/Firefox|FxiOS|CriOS|EdgiOS/.test(ua);
125 const transparencyOk = !window.matchMedia("(prefers-reduced-transparency: reduce)").matches;
126 return chromium && transparencyOk;
127}
128
129/**
130 * Displacement map for a rounded rectangle: neutral grey inside, and a convex
131 * bezel of width `bezel` whose red/green channels push samples outward along
132 * the edge normal (strongest at the rim) — the "liquid" lensing at the edge.
133 */
134function buildDisplacementMap(w: number, h: number, radius: number, bezel: number) {
135 const W = Math.max(2, Math.round(w));
136 const H = Math.max(2, Math.round(h));
137 const c = document.createElement("canvas");
138 c.width = W;
139 c.height = H;
140 const ctx = c.getContext("2d");
141 if (!ctx) return "";
142 const img = ctx.createImageData(W, H);
143 const hw = W / 2;
144 const hh = H / 2;
145 const r = Math.min(radius, hw, hh);
146 for (let j = 0; j < H; j++) {
147 for (let i = 0; i < W; i++) {
148 const x = i + 0.5 - hw;
149 const y = j + 0.5 - hh;
150 const qx = Math.abs(x) - (hw - r);
151 const qy = Math.abs(y) - (hh - r);
152 const ox = Math.max(qx, 0);
153 const oy = Math.max(qy, 0);
154 const sd = Math.hypot(ox, oy) + Math.min(Math.max(qx, qy), 0) - r;
155 const d = -sd;
156 let nx = 0;
157 let ny = 0;
158 let mag = 0;
159 if (d > 0 && d < bezel) {
160 if (qx > 0 && qy > 0) {
161 const l = Math.hypot(qx, qy) || 1;
162 nx = (qx / l) * Math.sign(x);
163 ny = (qy / l) * Math.sign(y);
164 } else if (qx > qy) nx = Math.sign(x);
165 else ny = Math.sign(y);
166 const t = d / bezel;
167 mag = Math.pow(1 - t, 2.4);
168 }
169 const k = (j * W + i) * 4;
170 img.data[k] = 128 + nx * mag * 127;
171 img.data[k + 1] = 128 + ny * mag * 127;
172 img.data[k + 2] = 128;
173 img.data[k + 3] = 255;
174 }
175 }
176 ctx.putImageData(img, 0, 0);
177 return c.toDataURL();
178}
179
180type GlassProps = {
181 className?: string;
182 style?: CSSProperties;
183 children?: ReactNode;
184 /** Corner radius in px used for the displacement map (match your CSS). Use a big number for capsules/circles. */
185 radius?: number;
186 /** Width of the refracting rim in px. */
187 bezel?: number;
188 /** Displacement strength in px. */
189 strength?: number;
190 /** Set false for frosted-only glass (e.g. surfaces that change size via clip-path). */
191 refract?: boolean;
192 /** Receives the glass element (for callers that animate it). */
193 glassRef?: React.MutableRefObject<HTMLDivElement | null>;
194} & Omit<React.HTMLAttributes<HTMLDivElement>, "style" | "className" | "children">;
195
196function Glass({ className = "", style, children, radius = 9999, bezel = 18, strength = 48, refract = true, glassRef, ...rest }: GlassProps) {
197 const ref = useRef<HTMLDivElement | null>(null);
198 const setRef = useCallback(
199 (node: HTMLDivElement | null) => {
200 ref.current = node;
201 if (glassRef) glassRef.current = node;
202 },
203 [glassRef],
204 );
205 const rawId = useId();
206 const id = `fd-prs-g${rawId.replace(/[^a-zA-Z0-9_-]/g, "")}`;
207 const [map, setMap] = useState<{ w: number; h: number; url: string } | null>(null);
208
209 useEffect(() => {
210 const el = ref.current;
211 if (!el || !refract || !supportsSvgBackdrop()) return;
212 let raf = 0;
213 let last = "";
214 const build = () => {
215 raf = 0;
216 const w = el.offsetWidth;
217 const h = el.offsetHeight;
218 const key = `${w}x${h}`;
219 if (!w || !h || key === last) return;
220 last = key;
221 setMap({ w, h, url: buildDisplacementMap(w, h, Math.min(radius, h / 2, w / 2), Math.min(bezel, h / 2, w / 2)) });
222 };
223 const ro = new ResizeObserver(() => {
224 if (!raf) raf = requestAnimationFrame(build);
225 });
226 ro.observe(el);
227 build();
228 return () => {
229 ro.disconnect();
230 if (raf) cancelAnimationFrame(raf);
231 };
232 }, [refract, radius, bezel]);
233
234 const on = !!map?.url;
235 return (
236 <div
237 ref={setRef}
238 className={`fd-prs-glass ${className}`}
239 data-refract={on || undefined}
240 style={on ? { ...style, backdropFilter: `url(#${id})`, WebkitBackdropFilter: `url(#${id})` } : style}
241 {...rest}
242 >
243 {on && map && (
244 <svg className="fd-prs-glass-svg" width="0" height="0" aria-hidden="true" focusable="false">
245 <filter id={id} x="0" y="0" width={map.w} height={map.h} filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
246 <feImage href={map.url} x="0" y="0" width={map.w} height={map.h} preserveAspectRatio="none" result="map" />
247 <feDisplacementMap in="SourceGraphic" in2="map" scale={strength} xChannelSelector="R" yChannelSelector="G" result="dr" />
248 <feColorMatrix in="dr" type="matrix" values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" result="r" />
249 <feDisplacementMap in="SourceGraphic" in2="map" scale={strength * 0.93} xChannelSelector="R" yChannelSelector="G" result="dg" />
250 <feColorMatrix in="dg" type="matrix" values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0" result="g" />
251 <feDisplacementMap in="SourceGraphic" in2="map" scale={strength * 0.86} xChannelSelector="R" yChannelSelector="G" result="db" />
252 <feColorMatrix in="db" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0" result="b" />
253 <feBlend in="r" in2="g" mode="screen" result="rg" />
254 <feBlend in="rg" in2="b" mode="screen" result="rgb" />
255 <feGaussianBlur in="rgb" stdDeviation="0.7" result="soft" />
256 <feColorMatrix in="soft" type="saturate" values="1.45" />
257 </filter>
258 </svg>
259 )}
260 {children}
261 </div>
262 );
263}
264
265/* ─────────────────────────── product photography ─────────────────────────── */
266
267// React 19 knows `fetchPriority`; React 18 only passes the lowercase DOM attribute through.
268const FETCH_PRIORITY = Number.parseInt(version, 10) >= 19 ? "fetchPriority" : "fetchpriority";
269
270type ProductId = "parfum" | "balm" | "tube";
271
272const PRODUCTS: Record<ProductId, { name: string; w: number; h: number; w480: number; alt: string }> = {
273 parfum: { name: "Eau de Parfum Nº4", w: 570, h: 818, w480: 480, alt: "Prism Eau de Parfum Nº4: an amber glass bottle with a brushed-silver cap on a grey studio backdrop" },
274 balm: { name: "Fieldnote Balm", w: 568, h: 818, w480: 480, alt: "Prism Fieldnote Balm: a frosted blue glass jar with a navy lid on a grey studio backdrop" },
275 tube: { name: "SPF 30", w: 568, h: 818, w480: 480, alt: "Prism SPF 30: a teal squeeze tube with a dark green cap on a grey studio backdrop" },
276};
277
278/**
279 * A grade is pure CSS over the untouched photo: `bg` is blended over the whole
280 * frame (the grey seamless takes the tint, white labels stay white), `all` is a
281 * second soft pass, `f` is a filter on the photo itself and lx/ly place the
282 * key-light highlight.
283 */
284type Grade = { name: string; dot: string; bg: string; bgo: number; bgm: string; all: string; allo: number; allm: string; f: string; lx: string; ly: string };
285
286const GRADES = {
287 natural: { name: "Original", dot: "#8b8b97", bg: "#ffffff", bgo: 0, bgm: "overlay", all: "#ffffff", allo: 0, allm: "soft-light", f: "none", lx: "30%", ly: "16%" },
288 golden: { name: "Golden hour", dot: "#e8862a", bg: "#ffcf9f", bgo: 0.6, bgm: "overlay", all: "#ff9a3c", allo: 0.18, allm: "soft-light", f: "saturate(1.04)", lx: "16%", ly: "14%" },
289 north: { name: "North-light studio", dot: "#5b8dff", bg: "#cfe0ff", bgo: 0.55, bgm: "overlay", all: "#7fa8ff", allo: 0.12, allm: "soft-light", f: "none", lx: "82%", ly: "12%" },
290 dusk: { name: "Neon dusk", dot: "#ff4f86", bg: "#e6b0ff", bgo: 0.6, bgm: "overlay", all: "#ff3d8b", allo: 0.16, allm: "soft-light", f: "none", lx: "86%", ly: "30%" },
291 green: { name: "Greenhouse", dot: "#1fae7c", bg: "#bff0d6", bgo: 0.6, bgm: "overlay", all: "#2fbf8a", allo: 0.14, allm: "soft-light", f: "none", lx: "50%", ly: "4%" },
292 rose: { name: "Rose studio", dot: "#ff7fa8", bg: "#ffd1e0", bgo: 0.6, bgm: "overlay", all: "#ff8fb4", allo: 0.14, allm: "soft-light", f: "none", lx: "24%", ly: "20%" },
293 ink: { name: "Night edit", dot: "#3d4470", bg: "#2b3150", bgo: 0.8, bgm: "overlay", all: "#6b7bff", allo: 0.1, allm: "soft-light", f: "contrast(1.04)", lx: "70%", ly: "18%" },
294 mono: { name: "Mono", dot: "#16161d", bg: "#ffffff", bgo: 0, bgm: "overlay", all: "#ffffff", allo: 0, allm: "soft-light", f: "grayscale(1) contrast(1.1)", lx: "30%", ly: "16%" },
295 duo: { name: "Duotone", dot: "#5b3df5", bg: "#ffb3d1", bgo: 0.55, bgm: "overlay", all: "#5b3df5", allo: 0.8, allm: "color", f: "grayscale(1) contrast(1.06)", lx: "30%", ly: "16%" },
296} satisfies Record<string, Grade>;
297
298type GradeKey = keyof typeof GRADES;
299
300/** Where the photo sits in its frame: ox/oy = object-position (0-1), fx/fy = zoom focus (0-1), z = zoom (>= 1). */
301type Crop = { ox?: number; oy?: number; fx?: number; fy?: number; z?: number };
302
303function Pic({
304 id,
305 grade = "natural",
306 crop,
307 className = "",
308 eager = false,
309 sizes = "(max-width: 700px) 90vw, 560px",
310 alt,
311 children,
312}: {
313 id: ProductId;
314 grade?: GradeKey;
315 crop?: Crop;
316 className?: string;
317 eager?: boolean;
318 sizes?: string;
319 alt?: string;
320 children?: ReactNode;
321}) {
322 const p = PRODUCTS[id];
323 const g: Grade = GRADES[grade];
324 const c = crop ?? {};
325 const style = {
326 "--bg": g.bg,
327 "--bgo": g.bgo,
328 "--bgm": g.bgm,
329 "--all": g.all,
330 "--allo": g.allo,
331 "--allm": g.allm,
332 "--f": g.f,
333 "--lx": g.lx,
334 "--ly": g.ly,
335 "--ox": c.ox ?? 0.5,
336 "--oy": c.oy ?? 0.5,
337 "--fx": c.fx ?? 0.5,
338 "--fy": c.fy ?? 0.5,
339 "--z": c.z ?? 1,
340 } as CSSProperties;
341 const priority = eager ? { [FETCH_PRIORITY]: "high" } : {};
342 return (
343 <div className={`fd-prs-shot ${className}`} style={style}>
344 <div className="fd-prs-shot-in">
345 <img
346 className="fd-prs-shot-img"
347 src={`/img/prism/${id}.webp`}
348 srcSet={`/img/prism/${id}-480.webp ${p.w480}w, /img/prism/${id}.webp ${p.w}w`}
349 sizes={sizes}
350 width={p.w}
351 height={p.h}
352 alt={alt ?? p.alt}
353 decoding="async"
354 loading={eager ? "eager" : "lazy"}
355 {...priority}
356 />
357 </div>
358 <i className="fd-prs-g-bg" aria-hidden="true" />
359 <i className="fd-prs-g-all" aria-hidden="true" />
360 <i className="fd-prs-g-temp" aria-hidden="true" />
361 <i className="fd-prs-g-light" aria-hidden="true" />
362 {children}
363 </div>
364 );
365}
366
367
368/* ─────────────────────────── icons ─────────────────────────── */
369
370function Logo() {
371 return (
372 <svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" className="fd-prs-logo">
373 <path d="M12 3l9 16H3z" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" />
374 <path d="M12 3l-2.5 16M14 12l7 2M14 12l7 4.5" stroke="currentColor" strokeWidth="1.2" opacity="0.55" />
375 </svg>
376 );
377}
378function Spark() {
379 return (
380 <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
381 <path d="M8 1.5l1.4 4.1 4.1 1.4-4.1 1.4L8 12.5 6.6 8.4 2.5 7l4.1-1.4z" fill="currentColor" />
382 </svg>
383 );
384}
385function Chevron({ dir = "right" }: { dir?: "left" | "right" }) {
386 return (
387 <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" style={{ transform: dir === "left" ? "scaleX(-1)" : undefined }}>
388 <path d="M6 3.5L10.5 8 6 12.5" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
389 </svg>
390 );
391}
392function Check() {
393 return (
394 <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
395 <path d="M3.5 8.5l3 3 6-7" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
396 </svg>
397 );
398}
399
400/* ─────────────────────────── backdrop ─────────────────────────── */
401
402function Mesh() {
403 const ref = useRef<HTMLDivElement>(null);
404 useEffect(() => {
405 const el = ref.current;
406 if (!el || reducedMotion()) return;
407 let raf = 0;
408 const run = () => {
409 raf = 0;
410 const max = Math.max(1, document.documentElement.scrollHeight - window.innerHeight);
411 el.style.setProperty("--m", clamp01(window.scrollY / max).toFixed(4));
412 };
413 const q = () => {
414 if (!raf) raf = requestAnimationFrame(run);
415 };
416 q();
417 window.addEventListener("scroll", q, { passive: true });
418 window.addEventListener("resize", q);
419 return () => {
420 window.removeEventListener("scroll", q);
421 window.removeEventListener("resize", q);
422 if (raf) cancelAnimationFrame(raf);
423 };
424 }, []);
425 return (
426 <div className="fd-prs-mesh" aria-hidden="true" ref={ref}>
427 <i className="fd-prs-blob fd-prs-b1" />
428 <i className="fd-prs-blob fd-prs-b2" />
429 <i className="fd-prs-blob fd-prs-b3" />
430 <i className="fd-prs-blob fd-prs-b4" />
431 <i className="fd-prs-blob fd-prs-b5" />
432 <i className="fd-prs-grain" />
433 </div>
434 );
435}
436
437/* ─────────────────────────── nav ─────────────────────────── */
438
439const LINKS = [
440 ["#lens", "Optics"],
441 ["#studio", "Studio"],
442 ["#gallery", "Gallery"],
443 ["#stories", "Customers"],
444 ["#pricing", "Pricing"],
445];
446
447function Nav() {
448 const [condensed, setCondensed] = useState(false);
449 const [open, setOpen] = useState(false);
450 useEffect(() => {
451 const on = () => setCondensed(window.scrollY > 40);
452 on();
453 window.addEventListener("scroll", on, { passive: true });
454 return () => window.removeEventListener("scroll", on);
455 }, []);
456 return (
457 <header className="fd-prs-nav" data-condensed={condensed || undefined} data-open={open || undefined}>
458 <Glass className="fd-prs-nav-glass" refract={false} aria-hidden="true" />
459 <div className="fd-prs-nav-in">
460 <a href="#top" className="fd-prs-brand" aria-label="Prism home">
461 <Logo />
462 <span>Prism</span>
463 </a>
464 <nav className="fd-prs-nav-links" aria-label="Primary">
465 {LINKS.map(([h, l]) => (
466 <a key={h} href={h}>
467 {l}
468 </a>
469 ))}
470 </nav>
471 <div className="fd-prs-nav-right">
472 <a href="#top" className="fd-prs-nav-signin">
473 Sign in
474 </a>
475 <a href="#pricing" className="fd-prs-btn fd-prs-btn-sm">
476 Try free
477 </a>
478 <button type="button" className="fd-prs-burger" aria-expanded={open} aria-label={open ? "Close menu" : "Open menu"} onClick={() => setOpen((o) => !o)}>
479 <span />
480 <span />
481 </button>
482 </div>
483 </div>
484 <div className="fd-prs-sheet" hidden={!open}>
485 {LINKS.map(([h, l]) => (
486 <a key={h} href={h} onClick={() => setOpen(false)}>
487 {l}
488 </a>
489 ))}
490 </div>
491 </header>
492 );
493}
494
495/* ─────────────────────────── hero ─────────────────────────── */
496
497const ORDER: ProductId[] = ["parfum", "balm", "tube"];
498
499const LIGHTS: { grade: GradeKey; prompt: string; angle: number; lens: number }[] = [
500 { grade: "golden", prompt: "Golden hour on travertine, long soft shadow, 50mm", angle: 32, lens: 50 },
501 { grade: "north", prompt: "North-light studio, seamless grey, crisp reflections", angle: 38, lens: 85 },
502 { grade: "dusk", prompt: "Neon dusk rooftop, magenta rim light, shallow depth", angle: 44, lens: 35 },
503 { grade: "green", prompt: "Greenhouse morning, dappled leaves, dew on glass", angle: 50, lens: 50 },
504];
505
506function Hero() {
507 const [idx, setIdx] = useState(0);
508 const [front, setFront] = useState(0);
509 const [value, setValue] = useState("");
510 const [busy, setBusy] = useState(false);
511 const [temp, setTemp] = useState(5600);
512 const stage = useRef<HTMLDivElement>(null);
513 const sec = useRef<HTMLElement>(null);
514
515 const generate = useCallback(() => {
516 setBusy(true);
517 window.setTimeout(() => {
518 setIdx((i) => (i + 1) % LIGHTS.length);
519 setBusy(false);
520 setValue("");
521 }, reducedMotion() ? 0 : 900);
522 }, []);
523
524 // Auto-cycle lighting while the user hasn't typed.
525 useEffect(() => {
526 if (reducedMotion() || value) return;
527 const t = window.setInterval(() => {
528 if (!document.hidden) setIdx((i) => (i + 1) % LIGHTS.length);
529 }, 5200);
530 return () => window.clearInterval(t);
531 }, [value]);
532
533 // Pointer parallax on the stage layers.
534 useEffect(() => {
535 const el = stage.current;
536 if (!el || reducedMotion()) return;
537 let raf = 0;
538 let px = 0;
539 let py = 0;
540 const apply = () => {
541 raf = 0;
542 el.style.setProperty("--px", px.toFixed(3));
543 el.style.setProperty("--py", py.toFixed(3));
544 };
545 const move = (e: PointerEvent) => {
546 const r = el.getBoundingClientRect();
547 px = clamp((e.clientX - (r.left + r.width / 2)) / r.width, -0.5, 0.5);
548 py = clamp((e.clientY - (r.top + r.height / 2)) / r.height, -0.5, 0.5);
549 if (!raf) raf = requestAnimationFrame(apply);
550 };
551 const leave = () => {
552 px = 0;
553 py = 0;
554 if (!raf) raf = requestAnimationFrame(apply);
555 };
556 const zone = sec.current ?? el;
557 zone.addEventListener("pointermove", move);
558 zone.addEventListener("pointerleave", leave);
559 return () => {
560 zone.removeEventListener("pointermove", move);
561 zone.removeEventListener("pointerleave", leave);
562 if (raf) cancelAnimationFrame(raf);
563 };
564 }, []);
565
566 // Scroll: the stage sinks back and the controls drift up at different depths.
567 useScrollFrame(sec, () => {
568 const el = sec.current;
569 if (!el || reducedMotion()) return;
570 const r = el.getBoundingClientRect();
571 const p = clamp01(-r.top / Math.max(1, r.height));
572 el.style.setProperty("--sp", p.toFixed(4));
573 });
574
575 const light = LIGHTS[idx];
576 const g = GRADES[light.grade];
577 return (
578 <section className="fd-prs-hero" id="top" ref={sec}>
579 <div className="fd-prs-wrap fd-prs-hero-in">
580 <div className="fd-prs-hero-copy">
581 <p className="fd-prs-kicker fd-prs-in" style={{ "--d": "0ms" } as CSSProperties}>
582 Prism 3 · AI product photography
583 </p>
584 <h1 className="fd-prs-h1">
585 <span className="fd-prs-in" style={{ "--d": "80ms" } as CSSProperties}>
586 Light,
587 </span>{" "}
588 <span className="fd-prs-in" style={{ "--d": "170ms" } as CSSProperties}>
589 on demand.
590 </span>
591 </h1>
592 <p className="fd-prs-lede fd-prs-in" style={{ "--d": "260ms" } as CSSProperties}>
593 Upload one product photo. Prism relights, restages and resizes it into a full campaign — with shadows,
594 reflections and refraction that obey real optics.
595 </p>
596 <form
597 className="fd-prs-in fd-prs-prompt-wrap"
598 style={{ "--d": "360ms" } as CSSProperties}
599 onSubmit={(e) => {
600 e.preventDefault();
601 generate();
602 }}
603 >
604 <Glass className="fd-prs-prompt" radius={9999} bezel={16} strength={40}>
605 <Spark />
606 <label className="fd-prs-sr" htmlFor="fd-prs-prompt-input">
607 Describe a scene
608 </label>
609 <input
610 id="fd-prs-prompt-input"
611 value={value}
612 onChange={(e) => setValue(e.target.value)}
613 placeholder={light.prompt}
614 autoComplete="off"
615 />
616 <button type="submit" className="fd-prs-btn fd-prs-btn-sm" data-busy={busy || undefined}>
617 {busy ? "Rendering…" : "Generate"}
618 </button>
619 </Glass>
620 </form>
621 <p className="fd-prs-fine fd-prs-in" style={{ "--d": "440ms" } as CSSProperties}>
622 Free for 25 renders · No credit card · Commercial licence included
623 </p>
624 </div>
625
626 <div
627 className="fd-prs-hero-stage fd-prs-in"
628 style={{ "--d": "220ms", "--t": ((temp - 5100) / 2400).toFixed(3) } as CSSProperties}
629 ref={stage}
630 >
631 <div className="fd-prs-stage-layer fd-prs-depth-1">
632 <div className="fd-prs-fan">
633 {ORDER.map((pid, i) => (
634 <button
635 key={pid}
636 type="button"
637 className="fd-prs-fan-card"
638 data-pos={(i - front + ORDER.length) % ORDER.length}
639 aria-pressed={i === front}
640 aria-label={`Show ${PRODUCTS[pid].name}`}
641 onClick={() => setFront(i)}
642 >
643 <Pic id={pid} grade={light.grade} eager={i === 0} sizes="(max-width: 700px) 60vw, 340px" />
644 <i className="fd-prs-render-sweep" data-busy={busy || undefined} aria-hidden="true" />
645 </button>
646 ))}
647 </div>
648 </div>
649 <div className="fd-prs-stage-layer fd-prs-depth-2">
650 <Glass className="fd-prs-chip fd-prs-chip-scene" radius={9999} bezel={12} strength={28}>
651 <span className="fd-prs-chip-dot" style={{ background: g.dot }} aria-hidden="true" />
652 <span aria-live="polite">{g.name}</span>
653 </Glass>
654 </div>
655 <div className="fd-prs-stage-layer fd-prs-depth-3">
656 <Glass className="fd-prs-chip fd-prs-chip-temp" radius={22} bezel={14} strength={32}>
657 <label htmlFor="fd-prs-temp" className="fd-prs-chip-label">
658 <span>Key light</span>
659 <b>{temp.toLocaleString("en-US")} K</b>
660 </label>
661 <input
662 id="fd-prs-temp"
663 className="fd-prs-slider"
664 type="range"
665 min={2700}
666 max={7500}
667 step={100}
668 value={temp}
669 onChange={(e) => setTemp(Number(e.target.value))}
670 style={{ "--v": `${((temp - 2700) / 4800) * 100}%` } as CSSProperties}
671 />
672 </Glass>
673 </div>
674 <div className="fd-prs-stage-layer fd-prs-depth-4">
675 <Glass className="fd-prs-chip fd-prs-chip-angle" radius={9999} bezel={12} strength={28}>
676 <span className="fd-prs-chip-label">
677 <span>Angle</span>
678 <b>{light.angle}°</b>
679 </span>
680 <span className="fd-prs-chip-sep" aria-hidden="true" />
681 <span className="fd-prs-chip-label">
682 <span>Lens</span>
683 <b>{light.lens} mm</b>
684 </span>
685 </Glass>
686 </div>
687 </div>
688 </div>
689 </section>
690 );
691}
692
693
694/* ─────────────────────────── logos ─────────────────────────── */
695
696function Logos() {
697 const names = ["MAISON VALE", "Fieldnote", "otto audio", "Sable & Co", "PALE BLUE", "Hearth", "Kinfolk Eyewear", "Brightside"];
698 return (
699 <section className="fd-prs-logos" aria-label="Brands using Prism">
700 <p className="fd-prs-mini" data-reveal="">
701 Shot on Prism by 11,000 brands this season
702 </p>
703 <div className="fd-prs-marquee">
704 <ul>
705 {[...names, ...names].map((n, i) => (
706 <li key={i} aria-hidden={i >= names.length || undefined} className={`fd-prs-wm-${i % 4}`}>
707 {n}
708 </li>
709 ))}
710 </ul>
711 </div>
712 </section>
713 );
714}
715
716/* ─────────────────────────── refraction lens ─────────────────────────── */
717
718function Lens() {
719 const area = useRef<HTMLDivElement>(null);
720 const lens = useRef<HTMLDivElement | null>(null);
721 const [strength, setStrength] = useState(64);
722 const [mode, setMode] = useState<"pending" | "refract" | "frost">("pending");
723
724 useEffect(() => {
725 setMode(supportsSvgBackdrop() ? "refract" : "frost");
726 }, []);
727
728 // Spring-follow drag (k 380, c 30) + idle drift.
729 useEffect(() => {
730 const a = area.current;
731 const l = lens.current;
732 if (!a || !l) return;
733 const s = { x: 0, y: 0, vx: 0, vy: 0, tx: 0, ty: 0 };
734 let dragging = false;
735 let touched = false;
736 let raf = 0;
737 let last = 0;
738 let t0 = 0;
739 let visible = true;
740 const reduced = reducedMotion();
741 const place = () => {
742 const r = a.getBoundingClientRect();
743 s.tx = r.width * 0.5;
744 s.ty = r.height * 0.5;
745 if (!s.x) {
746 s.x = s.tx;
747 s.y = s.ty;
748 }
749 };
750 place();
751 const step = (now: number) => {
752 raf = 0;
753 const dt = Math.min(0.032, last ? (now - last) / 1000 : 0.016);
754 last = now;
755 const r = a.getBoundingClientRect();
756 if (!touched && !reduced) {
757 t0 += dt;
758 s.tx = r.width * (0.5 + 0.28 * Math.sin(t0 * 0.55));
759 s.ty = r.height * (0.5 + 0.18 * Math.sin(t0 * 0.9 + 1));
760 }
761 const k = 380;
762 const c = 30;
763 s.vx += (k * (s.tx - s.x) - c * s.vx) * dt;
764 s.vy += (k * (s.ty - s.y) - c * s.vy) * dt;
765 s.x += s.vx * dt;
766 s.y += s.vy * dt;
767 const speed = Math.min(1, Math.hypot(s.vx, s.vy) / 1600);
768 const ang = Math.atan2(s.vy, s.vx);
769 l.style.transform = `translate3d(${(s.x - l.offsetWidth / 2).toFixed(1)}px, ${(s.y - l.offsetHeight / 2).toFixed(1)}px, 0) rotate(${ang}rad) scale(${1 + speed * 0.12}, ${1 - speed * 0.08}) rotate(${-ang}rad)`;
770 const settled = touched && !dragging && Math.abs(s.tx - s.x) < 0.2 && Math.abs(s.ty - s.y) < 0.2 && Math.abs(s.vx) < 1 && Math.abs(s.vy) < 1;
771 if (visible && !document.hidden && !settled) raf = requestAnimationFrame(step);
772 else last = 0;
773 };
774 const kick = () => {
775 if (!raf) raf = requestAnimationFrame(step);
776 };
777 const toLocal = (e: PointerEvent) => {
778 const r = a.getBoundingClientRect();
779 s.tx = clamp(e.clientX - r.left, 0, r.width);
780 s.ty = clamp(e.clientY - r.top, 0, r.height);
781 };
782 const down = (e: PointerEvent) => {
783 dragging = true;
784 touched = true;
785 a.setPointerCapture(e.pointerId);
786 toLocal(e);
787 kick();
788 };
789 const move = (e: PointerEvent) => {
790 if (!dragging && e.pointerType === "touch") return;
791 if (!dragging && !touched) touched = true;
792 toLocal(e);
793 kick();
794 };
795 const up = () => {
796 dragging = false;
797 };
798 const key = (e: KeyboardEvent) => {
799 const d = e.shiftKey ? 40 : 12;
800 if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) return;
801 e.preventDefault();
802 touched = true;
803 const r = a.getBoundingClientRect();
804 if (e.key === "ArrowLeft") s.tx = clamp(s.tx - d, 0, r.width);
805 if (e.key === "ArrowRight") s.tx = clamp(s.tx + d, 0, r.width);
806 if (e.key === "ArrowUp") s.ty = clamp(s.ty - d, 0, r.height);
807 if (e.key === "ArrowDown") s.ty = clamp(s.ty + d, 0, r.height);
808 kick();
809 };
810 const io =
811 typeof IntersectionObserver !== "undefined"
812 ? new IntersectionObserver(([e]) => {
813 visible = e.isIntersecting;
814 if (visible) kick();
815 })
816 : undefined;
817 io?.observe(a);
818 a.addEventListener("pointerdown", down);
819 a.addEventListener("pointermove", move);
820 a.addEventListener("pointerup", up);
821 a.addEventListener("pointercancel", up);
822 l.addEventListener("keydown", key);
823 window.addEventListener("resize", place);
824 kick();
825 return () => {
826 io?.disconnect();
827 a.removeEventListener("pointerdown", down);
828 a.removeEventListener("pointermove", move);
829 a.removeEventListener("pointerup", up);
830 a.removeEventListener("pointercancel", up);
831 l.removeEventListener("keydown", key);
832 window.removeEventListener("resize", place);
833 if (raf) cancelAnimationFrame(raf);
834 };
835 }, []);
836
837 return (
838 <section className="fd-prs-sec" id="lens">
839 <div className="fd-prs-wrap">
840 <div className="fd-prs-head" data-reveal="">
841 <p className="fd-prs-kicker">Optics engine</p>
842 <h2 className="fd-prs-h2">
843 Glass that bends light <span className="fd-prs-mute">the way glass does.</span>
844 </h2>
845 <p className="fd-prs-p">
846 Prism ray-traces every reflective and transparent surface in your shot, so perfume bottles, sunglasses and
847 serum droppers come out with honest highlights instead of plastic sheen. Drag the lens to feel it.
848 </p>
849 </div>
850 <div className="fd-prs-lens-stage" ref={area} data-reveal="">
851 <div className="fd-prs-lens-bg" aria-hidden="true">
852 <div className="fd-prs-lens-stripes" />
853 <p className="fd-prs-lens-word">
854 Refraction
855 <br />
856 index 1.52
857 </p>
858 <div className="fd-prs-lens-grid">
859 {Array.from({ length: 6 }, (_, i) => (
860 <span key={i}>
861 {["Crown glass", "n = 1.52", "Abbe 64.2", "Dispersion", "Caustics", "Fresnel"][i]}
862 </span>
863 ))}
864 </div>
865 <div className="fd-prs-lens-shot">
866 <Pic id="parfum" grade="north" crop={{ oy: 0.58, fy: 0.6, z: 1.3 }} sizes="(max-width: 720px) 80vw, 390px" />
867 </div>
868 </div>
869 <Glass
870 className="fd-prs-lens"
871 glassRef={lens}
872 radius={9999}
873 bezel={46}
874 strength={strength}
875 tabIndex={0}
876 role="img"
877 aria-label="Refraction lens. Drag, or focus and use the arrow keys to move it."
878 />
879 <div className="fd-prs-lens-ctrl">
880 <Glass className="fd-prs-chip fd-prs-chip-strength" radius={22} bezel={14} strength={28}>
881 <label htmlFor="fd-prs-strength" className="fd-prs-chip-label">
882 <span>Refraction</span>
883 <b>{strength}</b>
884 </label>
885 <input
886 id="fd-prs-strength"
887 className="fd-prs-slider"
888 type="range"
889 min={0}
890 max={120}
891 value={strength}
892 onChange={(e) => setStrength(Number(e.target.value))}
893 style={{ "--v": `${(strength / 120) * 100}%` } as CSSProperties}
894 />
895 </Glass>
896 <span className="fd-prs-mode" data-mode={mode}>
897 {mode === "refract" ? "Live SVG refraction" : mode === "frost" ? "Frosted fallback (this browser)" : "Detecting…"}
898 </span>
899 </div>
900 </div>
901 </div>
902 </section>
903 );
904}
905
906/* ─────────────────────────── sticky studio scenes ─────────────────────────── */
907
908type Scene = { k: string; t: string; d: string; id: ProductId; grade: GradeKey; crop: Crop; clip: string; label: string; az: number; dial: number };
909
910const SCENES: Scene[] = [
911 { k: "Relight", t: "Move the sun.", d: "Drag the key light around your product and watch shadows, caustics and reflections follow — no reshoot, no lighting kit.", id: "parfum", grade: "golden", crop: { oy: 0.55 }, clip: "inset(0% 0% 0% 0% round 28px)", label: "1:1 · Shopify PDP", az: 214, dial: -50 },
912 { k: "Restage", t: "Change the world around it.", d: "Swap travertine for terrazzo, studio for street. Prism keeps your product pixel-exact and composites the scene around it.", id: "balm", grade: "dusk", crop: { oy: 0.62, z: 1.12, fy: 0.62 }, clip: "inset(0% 10% 0% 10% round 28px)", label: "4:5 · Instagram feed", az: 296, dial: 30 },
913 { k: "Resize", t: "Every format, one click.", d: "214 crops for every channel — from a 9:16 story to a 21:9 billboard — each re-composed by the model, not just cropped.", id: "tube", grade: "north", crop: { oy: 0.5, z: 1.05 }, clip: "inset(0% 21% 0% 21% round 28px)", label: "9:16 · Stories & Reels", az: 180, dial: 0 },
914];
915
916function Studio() {
917 const sec = useRef<HTMLElement>(null);
918 const [i, setI] = useState(0);
919 const iRef = useRef(0);
920 useScrollFrame(sec, () => {
921 const el = sec.current;
922 if (!el) return;
923 const r = el.getBoundingClientRect();
924 const travel = Math.max(1, r.height - window.innerHeight);
925 const p = reducedMotion() ? 0.5 : clamp01(-r.top / travel);
926 el.style.setProperty("--p", p.toFixed(4));
927 const n = Math.min(SCENES.length - 1, Math.floor(p * SCENES.length));
928 if (n !== iRef.current) {
929 iRef.current = n;
930 setI(n);
931 }
932 });
933 const s = SCENES[i];
934 const sg = GRADES[s.grade];
935 return (
936 <section className="fd-prs-studio" id="studio" ref={sec}>
937 <div className="fd-prs-studio-stick">
938 <div className="fd-prs-wrap fd-prs-studio-grid">
939 <div className="fd-prs-studio-copy">
940 <p className="fd-prs-kicker">The studio</p>
941 <ol className="fd-prs-scene-list">
942 {SCENES.map((sc, n) => (
943 <li key={sc.k} data-on={n === i || undefined}>
944 <span className="fd-prs-scene-k">
945 0{n + 1} · {sc.k}
946 </span>
947 <h3>{sc.t}</h3>
948 <p>{sc.d}</p>
949 </li>
950 ))}
951 </ol>
952 <div className="fd-prs-scene-dots" aria-hidden="true">
953 {SCENES.map((sc, n) => (
954 <i key={sc.k} data-on={n === i || undefined} />
955 ))}
956 </div>
957 </div>
958 <div className="fd-prs-studio-stage" aria-label={`${s.k}: ${s.label}`} role="group">
959 <div className="fd-prs-depth" data-depth="-60" style={{ "--depth": -60 } as CSSProperties}>
960 <div className="fd-prs-studio-frame" style={{ clipPath: s.clip }}>
961 {SCENES.map((sc, n) => (
962 <div key={sc.k} className="fd-prs-stage-shot" data-on={n === i || undefined}>
963 <Pic id={sc.id} grade={sc.grade} crop={sc.crop} sizes="(max-width: 1000px) 90vw, 560px" alt={`${PRODUCTS[sc.id].name} in ${GRADES[sc.grade].name.toLowerCase()} light, ${sc.label}`} />
964 </div>
965 ))}
966 </div>
967 </div>
968 <div className="fd-prs-depth fd-prs-studio-c1" style={{ "--depth": 70 } as CSSProperties}>
969 <Glass className="fd-prs-chip" radius={9999} bezel={12} strength={26}>
970 <span className="fd-prs-chip-dot" style={{ background: sg.dot }} />
971 <span>{sg.name}</span>
972 </Glass>
973 </div>
974 <div className="fd-prs-depth fd-prs-studio-c2" style={{ "--depth": 140 } as CSSProperties}>
975 <Glass className="fd-prs-chip fd-prs-sun-dial" radius={9999} bezel={16} strength={30}>
976 <span className="fd-prs-dial" style={{ "--a": `${s.dial}deg` } as CSSProperties} aria-hidden="true">
977 <i />
978 </span>
979 <span className="fd-prs-chip-label">
980 <span>Sun azimuth</span>
981 <b>{s.az}°</b>
982 </span>
983 </Glass>
984 </div>
985 <div className="fd-prs-depth fd-prs-studio-c3" style={{ "--depth": 110 } as CSSProperties}>
986 <Glass className="fd-prs-chip" radius={9999} bezel={12} strength={26}>
987 <span className="fd-prs-chip-label">
988 <b>{s.label}</b>
989 </span>
990 </Glass>
991 </div>
992 </div>
993 </div>
994 </div>
995 </section>
996 );
997}
998
999
1000/* ─────────────────────────── gallery (column parallax) ─────────────────────────── */
1001
1002const SHOTS: { id: ProductId; grade: GradeKey; crop: Crop; cap: string; tall?: boolean }[] = [
1003 { id: "parfum", grade: "golden", crop: { oy: 0.5 }, cap: "Nº4 · launch hero", tall: true },
1004 { id: "balm", grade: "dusk", crop: { oy: 0.7, fy: 0.66, z: 1.4 }, cap: "Fieldnote Balm · label detail" },
1005 { id: "tube", grade: "north", crop: { oy: 0.5 }, cap: "SPF 30 · Reels", tall: true },
1006 { id: "balm", grade: "ink", crop: { oy: 0.6, fy: 0.62, z: 1.15 }, cap: "Fieldnote · night edit" },
1007 { id: "parfum", grade: "mono", crop: { oy: 0.2, fy: 0.22, z: 1.4 }, cap: "Nº4 · cap detail, mono" },
1008 { id: "tube", grade: "golden", crop: { oy: 0.5, fy: 0.5, z: 1.12 }, cap: "SPF 30 · summer set", tall: true },
1009 { id: "parfum", grade: "green", crop: { oy: 0.6, fy: 0.6, z: 1.3 }, cap: "Nº4 · greenhouse" },
1010 { id: "tube", grade: "duo", crop: { oy: 0.5 }, cap: "SPF 30 · duotone teaser", tall: true },
1011 { id: "balm", grade: "rose", crop: { oy: 0.55 }, cap: "Fieldnote · holiday set" },
1012];
1013
1014function Gallery() {
1015 const sec = useRef<HTMLElement>(null);
1016 useScrollFrame(sec, () => {
1017 const el = sec.current;
1018 if (!el || reducedMotion()) return;
1019 const r = el.getBoundingClientRect();
1020 const p = clamp01((window.innerHeight - r.top) / (r.height + window.innerHeight));
1021 el.style.setProperty("--g", (p - 0.5).toFixed(4));
1022 });
1023 const cols = [0, 1, 2].map((c) => SHOTS.filter((_, n) => n % 3 === c));
1024 return (
1025 <section className="fd-prs-sec" id="gallery" ref={sec}>
1026 <div className="fd-prs-wrap">
1027 <div className="fd-prs-head fd-prs-head-row" data-reveal="">
1028 <div>
1029 <p className="fd-prs-kicker">Made with Prism</p>
1030 <h2 className="fd-prs-h2">
1031 One photo in. <span className="fd-prs-mute">A season of campaigns out.</span>
1032 </h2>
1033 </div>
1034 <p className="fd-prs-p">Every image below is cut from the same three studio photos — relit, regraded and recomposed by Prism.</p>
1035 </div>
1036 <div className="fd-prs-gal">
1037 {cols.map((col, c) => (
1038 <div key={c} className="fd-prs-gal-col" style={{ "--speed": [-60, 50, -110][c] } as CSSProperties}>
1039 {col.map((s, n) => (
1040 <figure key={s.cap} className="fd-prs-gal-item" data-tall={s.tall || undefined} data-reveal="" style={{ "--i": n + c } as CSSProperties}>
1041 <Pic id={s.id} grade={s.grade} crop={s.crop} sizes="(max-width: 720px) 45vw, 370px" alt={`${PRODUCTS[s.id].name}, ${s.cap}, ${GRADES[s.grade].name.toLowerCase()} grade`} />
1042 <figcaption>{s.cap}</figcaption>
1043 </figure>
1044 ))}
1045 </div>
1046 ))}
1047 </div>
1048 </div>
1049 </section>
1050 );
1051}
1052
1053/* ─────────────────────────── before / after ─────────────────────────── */
1054
1055const COMPARE_GRADES: GradeKey[] = ["golden", "dusk", "green", "mono"];
1056
1057function Compare() {
1058 const [pid, setPid] = useState<ProductId>("parfum");
1059 const [grade, setGrade] = useState<GradeKey>("golden");
1060 const [pos, setPos] = useState(50);
1061 const g = GRADES[grade];
1062 return (
1063 <section className="fd-prs-sec" id="compare">
1064 <div className="fd-prs-wrap fd-prs-cmp">
1065 <div className="fd-prs-cmp-copy" data-reveal="">
1066 <p className="fd-prs-kicker">Before and after</p>
1067 <h2 className="fd-prs-h2">
1068 Same photo. <span className="fd-prs-mute">A different light.</span>
1069 </h2>
1070 <p className="fd-prs-p">
1071 Drag the handle to compare your original upload with the relit render. The label, the glass and the cap
1072 stay pixel-exact; only the light and the world around them change.
1073 </p>
1074 <div className="fd-prs-cmp-ctrl">
1075 <span className="fd-prs-cmp-lab" id="fd-prs-cmp-prod">
1076 Product
1077 </span>
1078 <Glass className="fd-prs-pills" radius={9999} bezel={14} strength={30} role="group" aria-labelledby="fd-prs-cmp-prod">
1079 {ORDER.map((o) => (
1080 <button key={o} type="button" aria-pressed={o === pid} onClick={() => setPid(o)}>
1081 {PRODUCTS[o].name}
1082 </button>
1083 ))}
1084 </Glass>
1085 <span className="fd-prs-cmp-lab" id="fd-prs-cmp-light">
1086 Light
1087 </span>
1088 <Glass className="fd-prs-pills" radius={9999} bezel={14} strength={30} role="group" aria-labelledby="fd-prs-cmp-light">
1089 {COMPARE_GRADES.map((k) => (
1090 <button key={k} type="button" aria-pressed={k === grade} onClick={() => setGrade(k)}>
1091 {GRADES[k].name}
1092 </button>
1093 ))}
1094 </Glass>
1095 </div>
1096 </div>
1097 <div className="fd-prs-cmp-stage" data-reveal="">
1098 <Pic id={pid} grade="natural" sizes="(max-width: 1000px) 90vw, 520px" alt={`${PRODUCTS[pid].name}, original upload`} />
1099 <div className="fd-prs-cmp-after" style={{ clipPath: `inset(0 0 0 ${pos}%)` }}>
1100 <Pic id={pid} grade={grade} sizes="(max-width: 1000px) 90vw, 520px" alt={`${PRODUCTS[pid].name}, ${g.name.toLowerCase()} grade`} />
1101 </div>
1102 <span className="fd-prs-cmp-tag fd-prs-cmp-tag-l">Original</span>
1103 <span className="fd-prs-cmp-tag fd-prs-cmp-tag-r">{g.name}</span>
1104 <input
1105 className="fd-prs-cmp-range"
1106 type="range"
1107 min={0}
1108 max={100}
1109 value={pos}
1110 onChange={(e) => setPos(Number(e.target.value))}
1111 aria-label="Reveal the relit version"
1112 />
1113 <div className="fd-prs-cmp-line" style={{ left: `${pos}%` }} aria-hidden="true">
1114 <Glass className="fd-prs-cmp-knob" radius={9999} bezel={14} strength={28}>
1115 <Chevron dir="left" />
1116 <Chevron />
1117 </Glass>
1118 </div>
1119 </div>
1120 </div>
1121 </section>
1122 );
1123}
1124
1125
1126/* ─────────────────────────── stats ─────────────────────────── */
1127
1128function CountUp({ to, dec = 0, suf = "", pre = "" }: { to: number; dec?: number; suf?: string; pre?: string }) {
1129 const ref = useRef<HTMLSpanElement>(null);
1130 useEffect(() => {
1131 const el = ref.current;
1132 if (!el) return;
1133 const fmt = (n: number) => pre + n.toLocaleString("en-US", { minimumFractionDigits: dec, maximumFractionDigits: dec }) + suf;
1134 if (reducedMotion() || typeof IntersectionObserver === "undefined") {
1135 el.textContent = fmt(to);
1136 return;
1137 }
1138 el.textContent = fmt(0);
1139 let raf = 0;
1140 const io = new IntersectionObserver(
1141 ([e]) => {
1142 if (!e.isIntersecting) return;
1143 io.disconnect();
1144 const start = performance.now();
1145 const tick = (now: number) => {
1146 const t = clamp01((now - start) / 1500);
1147 el.textContent = fmt(t >= 1 ? to : to * (1 - Math.pow(2, -10 * t)));
1148 if (t < 1) raf = requestAnimationFrame(tick);
1149 };
1150 raf = requestAnimationFrame(tick);
1151 },
1152 { threshold: 0.6 },
1153 );
1154 io.observe(el);
1155 return () => {
1156 io.disconnect();
1157 cancelAnimationFrame(raf);
1158 };
1159 }, [to, dec, suf, pre]);
1160 return (
1161 <span ref={ref}>
1162 {pre}
1163 {to.toLocaleString("en-US", { minimumFractionDigits: dec, maximumFractionDigits: dec })}
1164 {suf}
1165 </span>
1166 );
1167}
1168
1169function Stats() {
1170 const items = [
1171 { to: 4.8, dec: 1, suf: "s", label: "median render, 4K, any scene" },
1172 { to: 214, suf: "", label: "channel-ready formats per shot" },
1173 { to: 92, suf: "%", label: "lower cost than a studio day" },
1174 { to: 11000, suf: "+", label: "brands shooting on Prism" },
1175 ];
1176 return (
1177 <section className="fd-prs-stats" aria-label="Prism in numbers">
1178 <div className="fd-prs-wrap">
1179 <dl className="fd-prs-stats-row">
1180 {items.map((s, n) => (
1181 <div key={s.label} data-reveal="" style={{ "--i": n } as CSSProperties}>
1182 <dt>
1183 <CountUp to={s.to} dec={s.dec} suf={s.suf} />
1184 </dt>
1185 <dd>{s.label}</dd>
1186 </div>
1187 ))}
1188 </dl>
1189 </div>
1190 </section>
1191 );
1192}
1193
1194/* ─────────────────────────── momentum carousel ─────────────────────────── */
1195
1196const QUOTES: { q: string; n: string; r: string; m: string; id: ProductId; grade: GradeKey; crop: Crop }[] = [
1197 { q: "We shot our entire spring launch — 340 assets — in an afternoon. The bottle reflections are better than our last studio day.", n: "Inès Duval", r: "Creative Director, Maison Vale", m: "340 assets · 1 afternoon", id: "parfum", grade: "golden", crop: { oy: 0.58, fy: 0.58, z: 1.1 } },
1198 { q: "Our Amazon conversion went up 18% after we replaced flat white-background shots with Prism lifestyle scenes.", n: "Marcus Bell", r: "Head of E-commerce, Fieldnote", m: "+18% conversion", id: "balm", grade: "north", crop: { oy: 0.62, fy: 0.62, z: 1.15 } },
1199 { q: "Refraction through tinted lenses was the thing every other tool got wrong. Prism gets it right on the first render.", n: "Hana Sato", r: "Brand Lead, Kinfolk Eyewear", m: "0 reshoots this year", id: "parfum", grade: "mono", crop: { oy: 0.62, fy: 0.62, z: 1.2 } },
1200 { q: "I briefed a billboard on Tuesday and approved it on Wednesday. The agency still doesn't believe me.", n: "Tom Achterberg", r: "CMO, Pale Blue", m: "48 h brief to billboard", id: "tube", grade: "dusk", crop: { oy: 0.5, fy: 0.45, z: 1.2 } },
1201 { q: "We localise every campaign for nine markets. Prism restages each one with local light and architecture.", n: "Leila Haddad", r: "Global Marketing, Sable & Co", m: "9 markets · 1 shoot", id: "balm", grade: "rose", crop: { oy: 0.62, fy: 0.62, z: 1.15 } },
1202];
1203
1204
1205function Carousel() {
1206 const viewport = useRef<HTMLDivElement>(null);
1207 const track = useRef<HTMLDivElement>(null);
1208 const [active, setActive] = useState(0);
1209 const st = useRef({ x: 0, v: 0, target: 0, raf: 0, last: 0, step: 0, min: 0, dragging: false, moved: 0 });
1210
1211 const render = useCallback(() => {
1212 const t = track.current;
1213 if (t) t.style.transform = `translate3d(${st.current.x.toFixed(2)}px,0,0)`;
1214 }, []);
1215
1216 const measure = useCallback(() => {
1217 const t = track.current;
1218 const v = viewport.current;
1219 if (!t || !v) return;
1220 const first = t.children[0] as HTMLElement | undefined;
1221 const second = t.children[1] as HTMLElement | undefined;
1222 st.current.step = first && second ? second.offsetLeft - first.offsetLeft : v.clientWidth;
1223 st.current.min = Math.min(0, v.clientWidth - t.scrollWidth);
1224 }, []);
1225
1226 const animateTo = useCallback(
1227 (target: number, v0 = 0) => {
1228 const s = st.current;
1229 s.target = clamp(target, s.min, 0);
1230 s.v = v0;
1231 if (reducedMotion()) {
1232 s.x = s.target;
1233 render();
1234 return;
1235 }
1236 cancelAnimationFrame(s.raf);
1237 s.last = 0;
1238 const tick = (now: number) => {
1239 const dt = Math.min(0.032, s.last ? (now - s.last) / 1000 : 0.016);
1240 s.last = now;
1241 const k = 380;
1242 const c = 30;
1243 s.v += (k * (s.target - s.x) - c * s.v) * dt;
1244 s.x += s.v * dt;
1245 render();
1246 if (Math.abs(s.target - s.x) > 0.3 || Math.abs(s.v) > 3) s.raf = requestAnimationFrame(tick);
1247 else {
1248 s.x = s.target;
1249 render();
1250 }
1251 };
1252 s.raf = requestAnimationFrame(tick);
1253 },
1254 [render],
1255 );
1256
1257 const go = useCallback(
1258 (n: number) => {
1259 measure();
1260 const s = st.current;
1261 const maxIdx = Math.max(0, Math.round(-s.min / (s.step || 1)));
1262 const idx = clamp(n, 0, Math.min(QUOTES.length - 1, maxIdx));
1263 setActive(idx);
1264 animateTo(Math.max(s.min, -idx * s.step));
1265 },
1266 [animateTo, measure],
1267 );
1268
1269 useEffect(() => {
1270 const v = viewport.current;
1271 if (!v) return;
1272 measure();
1273 const s = st.current;
1274 let startX = 0;
1275 let startPos = 0;
1276 let lastX = 0;
1277 let lastT = 0;
1278 let vel = 0;
1279 const down = (e: PointerEvent) => {
1280 if (e.button !== 0) return;
1281 cancelAnimationFrame(s.raf);
1282 measure();
1283 s.dragging = true;
1284 s.moved = 0;
1285 startX = lastX = e.clientX;
1286 startPos = s.x;
1287 lastT = performance.now();
1288 vel = 0;
1289 v.setPointerCapture(e.pointerId);
1290 v.setAttribute("data-drag", "");
1291 };
1292 const move = (e: PointerEvent) => {
1293 if (!s.dragging) return;
1294 const now = performance.now();
1295 const dx = e.clientX - startX;
1296 s.moved = Math.max(s.moved, Math.abs(dx));
1297 let x = startPos + dx;
1298 if (x > 0) x *= 0.35;
1299 if (x < s.min) x = s.min + (x - s.min) * 0.35;
1300 const dt = Math.max(1, now - lastT);
1301 vel = 0.8 * ((e.clientX - lastX) / dt) + 0.2 * vel;
1302 lastX = e.clientX;
1303 lastT = now;
1304 s.x = x;
1305 render();
1306 };
1307 const up = () => {
1308 if (!s.dragging) return;
1309 s.dragging = false;
1310 v.removeAttribute("data-drag");
1311 const projected = s.x + vel * 260;
1312 const idx = clamp(Math.round(-projected / (s.step || 1)), 0, QUOTES.length - 1);
1313 setActive(idx);
1314 animateTo(Math.max(s.min, -idx * s.step), vel * 1000);
1315 };
1316 const click = (e: MouseEvent) => {
1317 if (s.moved > 6) {
1318 e.preventDefault();
1319 e.stopPropagation();
1320 }
1321 };
1322 const onResize = () => {
1323 measure();
1324 s.x = clamp(s.x, s.min, 0);
1325 render();
1326 };
1327 v.addEventListener("pointerdown", down);
1328 v.addEventListener("pointermove", move);
1329 v.addEventListener("pointerup", up);
1330 v.addEventListener("pointercancel", up);
1331 v.addEventListener("click", click, true);
1332 window.addEventListener("resize", onResize);
1333 return () => {
1334 v.removeEventListener("pointerdown", down);
1335 v.removeEventListener("pointermove", move);
1336 v.removeEventListener("pointerup", up);
1337 v.removeEventListener("pointercancel", up);
1338 v.removeEventListener("click", click, true);
1339 window.removeEventListener("resize", onResize);
1340 cancelAnimationFrame(s.raf);
1341 };
1342 }, [animateTo, measure, render]);
1343
1344 return (
1345 <section className="fd-prs-sec" id="stories" aria-roledescription="carousel" aria-label="Customer stories">
1346 <div className="fd-prs-wrap">
1347 <div className="fd-prs-head fd-prs-head-row" data-reveal="">
1348 <div>
1349 <p className="fd-prs-kicker">Customers</p>
1350 <h2 className="fd-prs-h2">
1351 Studios on deadline <span className="fd-prs-mute">switched first.</span>
1352 </h2>
1353 </div>
1354 <div className="fd-prs-car-btns">
1355 <Glass className="fd-prs-round" radius={9999} bezel={14} strength={30}>
1356 <button type="button" onClick={() => go(active - 1)} aria-label="Previous story" disabled={active === 0}>
1357 <Chevron dir="left" />
1358 </button>
1359 </Glass>
1360 <Glass className="fd-prs-round" radius={9999} bezel={14} strength={30}>
1361 <button type="button" onClick={() => go(active + 1)} aria-label="Next story" disabled={active === QUOTES.length - 1}>
1362 <Chevron />
1363 </button>
1364 </Glass>
1365 </div>
1366 </div>
1367 </div>
1368 <div
1369 className="fd-prs-car"
1370 ref={viewport}
1371 tabIndex={0}
1372 onKeyDown={(e) => {
1373 if (e.key === "ArrowRight") {
1374 e.preventDefault();
1375 go(active + 1);
1376 }
1377 if (e.key === "ArrowLeft") {
1378 e.preventDefault();
1379 go(active - 1);
1380 }
1381 }}
1382 aria-label="Stories. Drag or use arrow keys."
1383 >
1384 <div className="fd-prs-car-track" ref={track}>
1385 {QUOTES.map((q, n) => (
1386 <figure key={q.n} className="fd-prs-quote" data-active={n === active || undefined} aria-roledescription="slide" aria-label={`${n + 1} of ${QUOTES.length}`}>
1387 <div className="fd-prs-quote-shot" aria-hidden="true">
1388 <Pic id={q.id} grade={q.grade} crop={q.crop} sizes="(max-width: 720px) 82vw, 432px" alt={`${PRODUCTS[q.id].name}, ${GRADES[q.grade].name.toLowerCase()} grade`} />
1389 <span className="fd-prs-quote-metric">{q.m}</span>
1390 </div>
1391 <blockquote>“{q.q}”</blockquote>
1392 <figcaption>
1393 <b>{q.n}</b>
1394 <span>{q.r}</span>
1395 </figcaption>
1396 </figure>
1397 ))}
1398 </div>
1399 </div>
1400 <div className="fd-prs-car-dots" aria-hidden="true">
1401 {QUOTES.map((q, n) => (
1402 <i key={q.n} data-on={n === active || undefined} />
1403 ))}
1404 </div>
1405 </section>
1406 );
1407}
1408
1409/* ─────────────────────────── pricing ─────────────────────────── */
1410
1411const PLANS = [
1412 { name: "Starter", m: 0, y: 0, blurb: "Try every feature on your own products.", feats: ["25 renders / month", "1080p exports", "12 scene presets", "Personal licence"], cta: "Start free" },
1413 { name: "Studio", m: 36, y: 29, blurb: "For brands shipping campaigns every month.", feats: ["1,500 renders / month", "4K + 214 channel formats", "Custom scenes & brand kits", "Ray-traced glass & liquids", "Commercial licence"], cta: "Start 14-day trial", hot: true },
1414 { name: "Agency", m: 149, y: 119, blurb: "Multiple brands, client review, SSO.", feats: ["Unlimited renders", "8 brand workspaces", "Client approval links", "SSO & audit log", "Priority render queue"], cta: "Talk to us" },
1415];
1416
1417function Pricing() {
1418 const [yearly, setYearly] = useState(true);
1419 return (
1420 <section className="fd-prs-sec" id="pricing">
1421 <div className="fd-prs-wrap">
1422 <div className="fd-prs-head fd-prs-head-center" data-reveal="">
1423 <p className="fd-prs-kicker">Pricing</p>
1424 <h2 className="fd-prs-h2">
1425 Cheaper than one studio day. <span className="fd-prs-mute">Every month.</span>
1426 </h2>
1427 <Glass className="fd-prs-seg" radius={9999} bezel={14} strength={30} role="radiogroup" aria-label="Billing period" data-yearly={yearly || undefined}>
1428 <i className="fd-prs-seg-thumb" aria-hidden="true" />
1429 <button type="button" role="radio" aria-checked={!yearly} onClick={() => setYearly(false)}>
1430 Monthly
1431 </button>
1432 <button type="button" role="radio" aria-checked={yearly} onClick={() => setYearly(true)}>
1433 Yearly <em>save 20%</em>
1434 </button>
1435 </Glass>
1436 </div>
1437 <div className="fd-prs-plans">
1438 {PLANS.map((p, n) => (
1439 <article key={p.name} className="fd-prs-plan" data-hot={p.hot || undefined} data-reveal="" style={{ "--i": n } as CSSProperties}>
1440 <div className="fd-prs-plan-top">
1441 <h3>{p.name}</h3>
1442 {p.hot && <span className="fd-prs-plan-tag">Most popular</span>}
1443 </div>
1444 <p className="fd-prs-plan-blurb">{p.blurb}</p>
1445 <div className="fd-prs-price">
1446 <span className="fd-prs-price-n" key={yearly ? "y" : "m"}>
1447 ${yearly ? p.y : p.m}
1448 </span>
1449 <span className="fd-prs-price-per">{p.m === 0 ? "forever" : "/ month"}</span>
1450 </div>
1451 <p className="fd-prs-plan-bill">{p.m === 0 ? "No card required" : yearly ? `$${p.y * 12} billed yearly` : "Billed monthly, cancel anytime"}</p>
1452 <a href="#top" className={`fd-prs-btn ${p.hot ? "" : "fd-prs-btn-soft"}`}>
1453 {p.cta}
1454 </a>
1455 <ul>
1456 {p.feats.map((f) => (
1457 <li key={f}>
1458 <Check />
1459 {f}
1460 </li>
1461 ))}
1462 </ul>
1463 </article>
1464 ))}
1465 </div>
1466 </div>
1467 </section>
1468 );
1469}
1470
1471/* ─────────────────────────── faq ─────────────────────────── */
1472
1473const FAQ: [string, string][] = [
1474 ["Do I need a studio photo to start?", "No. A phone photo on any surface works: Prism cuts the product out, rebuilds the backdrop and lights it from scratch. Clean, evenly lit starting shots, like the grey seamless photos on this page, give the sharpest labels."],
1475 ["Will my label and logo stay readable?", "Yes. Packaging is locked pixel-exact. Type, foil and embossing are composited on top of every relit scene, so a new light never rewrites your label."],
1476 ["Can I keep one look across a whole season?", "Brand kits store your palettes, backdrops and lens choices, so a launch hero, a PDP crop and a holiday set all land in the same grade."],
1477 ["Who owns the renders?", "You do. Studio and Agency plans include a commercial licence, and every file carries C2PA content credentials."],
1478 ["How fast is a render?", "The median 4K render takes 4.8 seconds. Batches of 214 channel formats run in parallel, so a full set is usually ready before you have finished your coffee."],
1479];
1480
1481function Faq() {
1482 return (
1483 <section className="fd-prs-sec" id="faq">
1484 <div className="fd-prs-wrap">
1485 <div className="fd-prs-head fd-prs-head-center" data-reveal="">
1486 <p className="fd-prs-kicker">Questions</p>
1487 <h2 className="fd-prs-h2">
1488 Good to know <span className="fd-prs-mute">before your first render.</span>
1489 </h2>
1490 </div>
1491 <div className="fd-prs-faq" data-reveal="">
1492 {FAQ.map(([q, a]) => (
1493 <details key={q}>
1494 <summary>
1495 <span>{q}</span>
1496 <Chevron />
1497 </summary>
1498 <p>{a}</p>
1499 </details>
1500 ))}
1501 </div>
1502 </div>
1503 </section>
1504 );
1505}
1506
1507
1508/* ─────────────────────────── CTA + footer ─────────────────────────── */
1509
1510function Cta() {
1511 return (
1512 <section className="fd-prs-cta">
1513 <div className="fd-prs-wrap">
1514 <div className="fd-prs-cta-card" data-reveal="">
1515 <div className="fd-prs-cta-copy">
1516 <h2 className="fd-prs-cta-h">Your next shoot is a sentence away.</h2>
1517 <p className="fd-prs-p">Upload a phone photo of any product. Your first 25 renders are on us.</p>
1518 <div className="fd-prs-cta-btns">
1519 <a href="#top" className="fd-prs-btn">
1520 Try Prism free
1521 </a>
1522 <a href="#gallery" className="fd-prs-btn fd-prs-btn-soft">
1523 See the gallery
1524 </a>
1525 </div>
1526 </div>
1527 <div className="fd-prs-cta-shot">
1528 <Pic id="parfum" grade="golden" crop={{ oy: 0.6 }} sizes="(max-width: 1000px) 90vw, 500px" />
1529 </div>
1530 </div>
1531 </div>
1532 </section>
1533 );
1534}
1535
1536function Footer() {
1537 const cols: [string, string[]][] = [
1538 ["Product", ["Studio", "Optics engine", "Brand kits", "API", "What’s new"]],
1539 ["Use cases", ["Beauty", "Eyewear", "Food & drink", "Home", "Agencies"]],
1540 ["Company", ["About", "Careers", "Press kit", "Privacy", "Terms"]],
1541 ];
1542 return (
1543 <footer className="fd-prs-foot">
1544 <div className="fd-prs-wrap">
1545 <div className="fd-prs-foot-top">
1546 <div>
1547 <a href="#top" className="fd-prs-brand" aria-label="Prism home">
1548 <Logo />
1549 <span>Prism</span>
1550 </a>
1551 <p className="fd-prs-foot-p">AI product photography with real optics. Made in Copenhagen and Lisbon.</p>
1552 </div>
1553 {cols.map(([h, items]) => (
1554 <div key={h}>
1555 <h4>{h}</h4>
1556 <ul>
1557 {items.map((it) => (
1558 <li key={it}>
1559 <a href="#top">{it}</a>
1560 </li>
1561 ))}
1562 </ul>
1563 </div>
1564 ))}
1565 </div>
1566 <div className="fd-prs-foot-bot">
1567 <span>© 2026 Prism Imaging ApS</span>
1568 <span>All renders carry C2PA content credentials.</span>
1569 </div>
1570 </div>
1571 </footer>
1572 );
1573}
1574
1575/* ─────────────────────────── page ─────────────────────────── */
1576
1577export function Page() {
1578 const root = useRef<HTMLDivElement>(null);
1579 useReveal(root);
1580 // Depth layers inside sticky scenes: translate by (progress - 0.5) * depth.
1581 useEffect(() => {
1582 const el = root.current;
1583 if (!el || reducedMotion()) return;
1584 let raf = 0;
1585 const run = () => {
1586 raf = 0;
1587 el.querySelectorAll<HTMLElement>(".fd-prs-studio").forEach((s) => {
1588 const p = Number(getComputedStyle(s).getPropertyValue("--p") || 0.5);
1589 s.querySelectorAll<HTMLElement>(".fd-prs-depth").forEach((d) => {
1590 const depth = Number(d.style.getPropertyValue("--depth") || 0);
1591 d.style.transform = `translate3d(0, ${((p - 0.5) * -depth).toFixed(1)}px, 0)`;
1592 });
1593 });
1594 };
1595 const q = () => {
1596 if (!raf) raf = requestAnimationFrame(run);
1597 };
1598 q();
1599 window.addEventListener("scroll", q, { passive: true });
1600 return () => {
1601 window.removeEventListener("scroll", q);
1602 if (raf) cancelAnimationFrame(raf);
1603 };
1604 }, []);
1605 return (
1606 <div className="fd-prs" ref={root}>
1607 <style>{css}</style>
1608 <Mesh />
1609 <Nav />
1610 <main>
1611 <Hero />
1612 <Logos />
1613 <Lens />
1614 <Studio />
1615 <Gallery />
1616 <Compare />
1617 <Stats />
1618 <Carousel />
1619 <Pricing />
1620 <Faq />
1621 <Cta />
1622 </main>
1623 <Footer />
1624 </div>
1625 );
1626}
1627
1628export default Page;
1629
1630/* ─────────────────────────── styles ─────────────────────────── */
1631
1632const css = `
1633.fd-prs {
1634 --fd-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
1635 --fd-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
1636 --fd-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
1637 --fd-dur-1: 120ms;
1638 --fd-dur-2: 240ms;
1639 --fd-dur-3: 420ms;
1640 --fd-dur-4: 700ms;
1641
1642 --fd-prs-ink: #16161d;
1643 --fd-prs-ink-2: #2a2a33;
1644 --fd-prs-mute: #4f4f5c;
1645 --fd-prs-paper: #f6f4fb;
1646 --fd-prs-card: #ffffff;
1647 --fd-prs-line: rgba(22, 22, 29, 0.08);
1648 --fd-prs-accent: #5b3df5;
1649 --fd-prs-glass-tint: rgba(255, 255, 255, 0.22);
1650 --fd-prs-glass-frost: rgba(255, 255, 255, 0.58);
1651 --fd-prs-sans: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Inter Variable", "Inter", "Segoe UI", Roboto, sans-serif;
1652
1653 position: relative; display: block; margin: 0; padding: 0; isolation: isolate;
1654 background: var(--fd-prs-paper); color: var(--fd-prs-ink);
1655 font: 400 1rem/1.5 var(--fd-prs-sans); letter-spacing: -0.01em; text-align: left;
1656 -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1657 overflow-x: clip; clip-path: inset(0); color-scheme: light;
1658}
1659.fd-prs, .fd-prs *, .fd-prs *::before, .fd-prs *::after { box-sizing: border-box; }
1660.fd-prs :where(h1, h2, h3, h4, p, ul, ol, li, dl, dt, dd, figure, figcaption, blockquote, main, header, footer, section, nav, article, label, form) {
1661 margin: 0; padding: 0; border: 0; font: inherit; color: inherit; letter-spacing: inherit; background: none;
1662}
1663.fd-prs :where(ul, ol) { list-style: none; }
1664.fd-prs :where(a) { color: inherit; text-decoration: none; }
1665.fd-prs :where(button, input) { appearance: none; -webkit-appearance: none; margin: 0; padding: 0; border: 0; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-align: inherit; line-height: inherit; border-radius: 0; box-shadow: none; }
1666.fd-prs :where(button) { cursor: pointer; }
1667.fd-prs :where(button:disabled) { cursor: default; }
1668.fd-prs :where(svg) { display: block; overflow: visible; }
1669.fd-prs :where(b, strong) { font-weight: 600; }
1670.fd-prs :where(em) { font-style: normal; }
1671.fd-prs :where(a, button, input, [tabindex]):focus-visible { outline: 2px solid rgba(91, 61, 245, 0.7); outline-offset: 2px; }
1672.fd-prs-sr { position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
1673
1674.fd-prs-wrap { width: 100%; max-width: 1200px; margin: 0 auto; padding: 0 32px; }
1675.fd-prs-kicker { font: 600 0.875rem/1.3 var(--fd-prs-sans); letter-spacing: -0.005em; color: var(--fd-prs-accent); }
1676.fd-prs-h2 { margin-top: 12px; max-width: 18em; font: 600 clamp(2rem, 4.6vw, 3.5rem)/1.05 var(--fd-prs-sans); letter-spacing: -0.035em; text-wrap: balance; }
1677.fd-prs-mute { color: #6e6e7a; }
1678.fd-prs-p { margin-top: 18px; max-width: 36em; font: 400 1.125rem/1.5 var(--fd-prs-sans); color: var(--fd-prs-mute); }
1679.fd-prs-mini { font: 500 0.875rem/1.4 var(--fd-prs-sans); color: var(--fd-prs-mute); text-align: center; }
1680.fd-prs-head-row { display: flex; justify-content: space-between; align-items: flex-end; gap: 32px; flex-wrap: wrap; }
1681.fd-prs-head-row .fd-prs-p { max-width: 24em; margin: 0; }
1682.fd-prs-head-center { display: flex; flex-direction: column; align-items: center; text-align: center; }
1683.fd-prs-head-center .fd-prs-h2 { margin-left: auto; margin-right: auto; }
1684.fd-prs-sec { position: relative; padding: 140px 0; }
1685
1686/* backdrop */
1687.fd-prs-mesh { position: fixed; inset: -10vh -10vw; z-index: -1; pointer-events: none; overflow: hidden; }
1688.fd-prs-blob { position: absolute; display: block; width: 60vmax; height: 60vmax; border-radius: 50%; opacity: 0.9; will-change: transform; }
1689/* The mesh drifts with scroll (--m: 0→1 over the page), not with time: SVG-refraction glass re-rasterizes whenever its backdrop changes, so a time-based drift would cost a filter pass every frame. */
1690.fd-prs-mesh { --m: 0; }
1691.fd-prs-b1 { left: -12vmax; top: -18vmax; background: radial-gradient(closest-side, #ffc2d4, rgba(255,194,212,0)); transform: translate3d(calc(var(--m) * 28vw), calc(var(--m) * 30vh), 0) scale(calc(1 + var(--m) * 0.25)); }
1692.fd-prs-b2 { right: -16vmax; top: -6vmax; background: radial-gradient(closest-side, #c6b6ff, rgba(198,182,255,0)); transform: translate3d(calc(var(--m) * -24vw), calc(var(--m) * 36vh), 0); }
1693.fd-prs-b3 { left: 18vw; top: 34vh; background: radial-gradient(closest-side, #b3e1ff, rgba(179,225,255,0)); transform: translate3d(calc(var(--m) * 20vw), calc(var(--m) * -30vh), 0) scale(calc(1 + var(--m) * 0.3)); }
1694.fd-prs-b4 { right: 4vw; bottom: -24vmax; background: radial-gradient(closest-side, #ffdcb3, rgba(255,220,179,0)); transform: translate3d(calc(var(--m) * -30vw), calc(var(--m) * -26vh), 0); }
1695.fd-prs-b5 { left: -20vmax; bottom: -10vmax; background: radial-gradient(closest-side, #bff3dd, rgba(191,243,221,0)); transform: translate3d(calc(var(--m) * 36vw), calc(var(--m) * -20vh), 0) scale(calc(1 - var(--m) * 0.15)); }
1696.fd-prs-grain { position: absolute; inset: 0; opacity: 0.35; mix-blend-mode: soft-light; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='0.5'/></svg>"); }
1697
1698/* glass */
1699.fd-prs-glass {
1700 position: relative; isolation: isolate;
1701 background: var(--fd-prs-glass-frost);
1702 -webkit-backdrop-filter: blur(18px) saturate(180%); backdrop-filter: blur(18px) saturate(180%);
1703 box-shadow:
1704 inset 0 1px 0 rgba(255,255,255,0.95), inset 0 -1px 0 rgba(255,255,255,0.35),
1705 inset 1px 0 0 rgba(255,255,255,0.5), inset -1px 0 0 rgba(255,255,255,0.5),
1706 0 1px 2px rgba(22,22,29,0.06), 0 12px 32px -12px rgba(22,22,29,0.22);
1707}
1708.fd-prs-glass[data-refract] { background: var(--fd-prs-glass-tint); }
1709.fd-prs-glass::after { content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; background: linear-gradient(160deg, rgba(255,255,255,0.55), rgba(255,255,255,0) 38%, rgba(255,255,255,0) 70%, rgba(255,255,255,0.22)); mix-blend-mode: soft-light; z-index: 1; }
1710.fd-prs-glass > * { position: relative; z-index: 2; }
1711.fd-prs-glass-svg { position: absolute !important; width: 0; height: 0; overflow: hidden; pointer-events: none; }
1712
1713/* buttons */
1714.fd-prs-btn {
1715 display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 48px; padding: 0 24px; border-radius: 9999px;
1716 background: var(--fd-prs-ink); color: #fff; font: 500 1rem/1 var(--fd-prs-sans); letter-spacing: -0.01em; white-space: nowrap;
1717 transition: transform var(--fd-dur-1) var(--fd-ease-out), background-color var(--fd-dur-2) var(--fd-ease-out);
1718}
1719.fd-prs-btn:hover { background: #000; }
1720.fd-prs-btn:active { transform: scale(0.96); }
1721.fd-prs-btn-sm { height: 36px; padding: 0 16px; font-size: 0.875rem; }
1722.fd-prs-btn-soft { background: rgba(22,22,29,0.06); color: var(--fd-prs-ink); }
1723.fd-prs-btn-soft:hover { background: rgba(22,22,29,0.1); }
1724.fd-prs-btn[data-busy] { background: var(--fd-prs-accent); }
1725
1726/* nav */
1727.fd-prs-nav { position: sticky; top: 0; z-index: 50; display: block; height: 72px; }
1728.fd-prs-nav-glass {
1729 position: absolute; inset: 0; border-radius: 9999px; opacity: 0;
1730 clip-path: inset(0 0 0 0 round 0px);
1731 transition: opacity var(--fd-dur-3) var(--fd-ease-out), clip-path var(--fd-dur-4) var(--fd-ease-out);
1732}
1733.fd-prs-nav[data-condensed] .fd-prs-nav-glass, .fd-prs-nav[data-open] .fd-prs-nav-glass { opacity: 1; clip-path: inset(10px max(16px, calc((100% - 940px) / 2)) 10px max(16px, calc((100% - 940px) / 2)) round 9999px); }
1734.fd-prs-nav[data-open] .fd-prs-nav-glass { clip-path: inset(10px 16px -320px 16px round 28px); }
1735.fd-prs-nav-in { position: relative; z-index: 1; display: flex; align-items: center; gap: 24px; max-width: 1200px; height: 72px; margin: 0 auto; padding: 0 32px; transition: max-width var(--fd-dur-4) var(--fd-ease-out), padding var(--fd-dur-4) var(--fd-ease-out); }
1736.fd-prs-nav[data-condensed] .fd-prs-nav-in { max-width: 940px; padding: 0 24px 0 28px; }
1737.fd-prs-brand { display: inline-flex; align-items: center; gap: 8px; font: 600 1.125rem/1 var(--fd-prs-sans); letter-spacing: -0.02em; }
1738.fd-prs-nav-links { display: flex; gap: 2px; margin: 0 auto; }
1739.fd-prs-nav-links a { display: inline-flex; align-items: center; height: 34px; padding: 0 14px; border-radius: 9999px; font: 500 0.875rem/1 var(--fd-prs-sans); color: var(--fd-prs-ink-2); transition: background-color var(--fd-dur-1) linear; }
1740.fd-prs-nav-links a:hover { background: rgba(22,22,29,0.06); }
1741.fd-prs-nav-right { display: flex; align-items: center; gap: 6px; }
1742.fd-prs-nav-signin { padding: 0 12px; font: 500 0.875rem/1 var(--fd-prs-sans); color: var(--fd-prs-ink-2); }
1743.fd-prs-burger { display: none; position: relative; width: 40px; height: 40px; border-radius: 9999px; }
1744.fd-prs-burger span { position: absolute; left: 12px; right: 12px; height: 1.5px; border-radius: 2px; background: var(--fd-prs-ink); transition: transform var(--fd-dur-3) var(--fd-spring); }
1745.fd-prs-burger span:first-child { top: 16px; }
1746.fd-prs-burger span:last-child { top: 23px; }
1747.fd-prs-nav[data-open] .fd-prs-burger span:first-child { transform: translateY(3.5px) rotate(45deg); }
1748.fd-prs-nav[data-open] .fd-prs-burger span:last-child { transform: translateY(-3.5px) rotate(-45deg); }
1749.fd-prs-sheet { position: relative; z-index: 1; display: grid; padding: 8px 40px 24px; }
1750.fd-prs-sheet[hidden] { display: none; }
1751.fd-prs-sheet a { padding: 13px 0; border-bottom: 1px solid var(--fd-prs-line); font: 600 1.25rem/1.2 var(--fd-prs-sans); letter-spacing: -0.02em; }
1752
1753/* hero */
1754.fd-prs-hero { --sp: 0; position: relative; padding: 72px 0 120px; }
1755.fd-prs-hero-in { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); gap: 56px; align-items: center; }
1756.fd-prs-in { animation: fd-prs-in 1000ms var(--fd-ease-out) both; animation-delay: var(--d, 0ms); }
1757@keyframes fd-prs-in { from { opacity: 0; transform: translateY(20px) scale(0.985); filter: blur(8px); } to { opacity: 1; transform: none; filter: none; } }
1758.fd-prs-h1 { margin-top: 18px; font: 650 clamp(3.25rem, 7.4vw, 6.5rem)/0.95 var(--fd-prs-sans); letter-spacing: -0.05em; }
1759.fd-prs-h1 > span { display: block; white-space: nowrap; }
1760.fd-prs-lede { margin-top: 24px; max-width: 30em; font: 400 clamp(1.0625rem, 1.5vw, 1.25rem)/1.5 var(--fd-prs-sans); color: var(--fd-prs-mute); }
1761.fd-prs-prompt-wrap { margin-top: 36px; max-width: 540px; }
1762.fd-prs-prompt { display: flex; align-items: center; gap: 10px; height: 60px; padding: 0 10px 0 20px; border-radius: 9999px; color: var(--fd-prs-accent); }
1763.fd-prs-prompt input { flex: 1; min-width: 0; height: 100%; font: 450 1rem/1 var(--fd-prs-sans); color: var(--fd-prs-ink); outline: none; }
1764.fd-prs-prompt input::placeholder { color: #5d5d6a; opacity: 1; }
1765.fd-prs-prompt:focus-within { box-shadow: inset 0 1px 0 rgba(255,255,255,0.95), 0 0 0 3px rgba(91,61,245,0.25), 0 12px 32px -12px rgba(22,22,29,0.22); }
1766.fd-prs-fine { margin-top: 16px; font: 400 0.8125rem/1.4 var(--fd-prs-sans); color: var(--fd-prs-mute); }
1767
1768.fd-prs-hero-stage { --px: 0; --py: 0; position: relative; aspect-ratio: 1 / 1.02; transform: translate3d(0, calc(var(--sp) * 60px), 0); }
1769.fd-prs-stage-layer { position: absolute; transition: transform var(--fd-dur-4) var(--fd-ease-out); }
1770.fd-prs-depth-1 { inset: 0; transform: translate3d(calc(var(--px) * -14px), calc(var(--py) * -14px), 0); }
1771.fd-prs-depth-1 { z-index: 1; }
1772.fd-prs-depth-2 { z-index: 2; left: 0; top: 10%; transform: translate3d(calc(var(--px) * 26px), calc(var(--py) * 26px + var(--sp) * -80px), 0); }
1773.fd-prs-depth-3 { z-index: 3; right: 0; top: 40%; transform: translate3d(calc(var(--px) * 40px), calc(var(--py) * 40px + var(--sp) * -150px), 0); }
1774.fd-prs-depth-4 { z-index: 4; left: 2%; bottom: 0; transform: translate3d(calc(var(--px) * 32px), calc(var(--py) * 32px + var(--sp) * -110px), 0); }
1775.fd-prs-stage-shot { position: absolute; inset: 0; opacity: 0; transition: opacity 900ms var(--fd-ease-in-out); }
1776.fd-prs-stage-shot[data-on] { opacity: 1; }
1777.fd-prs-render-sweep { position: absolute; inset: 0; z-index: 3; display: block; pointer-events: none; background: linear-gradient(100deg, transparent 30%, rgba(255,255,255,0.65) 50%, transparent 70%); transform: translateX(-120%); }
1778.fd-prs-render-sweep[data-busy] { animation: fd-prs-sweep 900ms var(--fd-ease-in-out); }
1779@keyframes fd-prs-sweep { to { transform: translateX(120%); } }
1780
1781.fd-prs-chip { display: inline-flex; align-items: center; gap: 10px; min-height: 48px; padding: 8px 24px 8px 20px; border-radius: 9999px; font: 500 0.875rem/1.2 var(--fd-prs-sans); color: var(--fd-prs-ink); white-space: nowrap; }
1782.fd-prs-chip-dot { width: 10px; height: 10px; border-radius: 50%; box-shadow: 0 0 0 3px rgba(255,255,255,0.7); transition: background-color var(--fd-dur-4) var(--fd-ease-in-out); }
1783.fd-prs-chip-label { display: flex; flex-direction: column; gap: 2px; }
1784.fd-prs-chip-label span { font: 500 0.6875rem/1.1 var(--fd-prs-sans); color: var(--fd-prs-mute); letter-spacing: 0; }
1785.fd-prs-chip-label b { font: 600 0.9375rem/1.1 var(--fd-prs-sans); font-variant-numeric: tabular-nums; }
1786.fd-prs-chip-sep { width: 1px; align-self: stretch; margin: 2px 4px; background: rgba(22,22,29,0.12); }
1787.fd-prs-chip-temp, .fd-prs-chip-strength { flex-direction: column; align-items: stretch; gap: 10px; width: 220px; padding: 14px 18px 16px; border-radius: 22px; }
1788.fd-prs-chip-temp .fd-prs-chip-label, .fd-prs-chip-strength .fd-prs-chip-label { flex-direction: row; justify-content: space-between; align-items: baseline; }
1789.fd-prs-chip-temp .fd-prs-chip-label span, .fd-prs-chip-strength .fd-prs-chip-label span { font-size: 0.8125rem; }
1790.fd-prs-slider { --v: 50%; display: block; width: 100%; height: 22px; background: transparent; cursor: pointer; }
1791.fd-prs-slider::-webkit-slider-runnable-track { height: 6px; border-radius: 9999px; background: linear-gradient(to right, var(--fd-prs-ink) var(--v), rgba(22,22,29,0.14) var(--v)); }
1792.fd-prs-slider::-moz-range-track { height: 6px; border-radius: 9999px; background: linear-gradient(to right, var(--fd-prs-ink) var(--v), rgba(22,22,29,0.14) var(--v)); }
1793.fd-prs-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 26px; height: 18px; margin-top: -6px; border-radius: 9999px; background: #fff; box-shadow: 0 0 0 0.5px rgba(0,0,0,0.12), 0 3px 8px rgba(0,0,0,0.18); transition: transform var(--fd-dur-2) var(--fd-spring); }
1794.fd-prs-slider:active::-webkit-slider-thumb { transform: scale(1.18); }
1795.fd-prs-slider::-moz-range-thumb { width: 26px; height: 18px; border: 0; border-radius: 9999px; background: #fff; box-shadow: 0 3px 8px rgba(0,0,0,0.18); }
1796
1797/* product photography: the real photo, graded and cropped with CSS only */
1798.fd-prs-shot { position: absolute; inset: 0; overflow: hidden; isolation: isolate; container-type: size; background: #8d8e96; }
1799.fd-prs-shot-in {
1800 position: absolute; left: 0; top: 0; width: max(100cqw, calc(100cqh * 0.6968)); aspect-ratio: 570 / 818;
1801 transform-origin: calc(var(--fx) * 100%) calc(var(--fy) * 100%);
1802 transform: translate(calc((100cqw - 100%) * var(--ox)), calc((100cqh - 100%) * var(--oy))) scale(var(--z));
1803}
1804.fd-prs-shot-img { display: block; width: 100%; height: 100%; max-width: none; filter: var(--f); transition: filter 900ms var(--fd-ease-in-out); }
1805.fd-prs-shot > i { position: absolute; inset: 0; display: block; pointer-events: none; }
1806.fd-prs-g-bg { background: var(--bg); opacity: var(--bgo); mix-blend-mode: var(--bgm); transition: background-color 900ms var(--fd-ease-in-out), opacity 900ms var(--fd-ease-in-out); }
1807.fd-prs-g-all { background: var(--all); opacity: var(--allo); mix-blend-mode: var(--allm); transition: background-color 900ms var(--fd-ease-in-out), opacity 900ms var(--fd-ease-in-out); }
1808.fd-prs-g-temp { mix-blend-mode: soft-light; background: linear-gradient(rgba(80,140,255,calc(var(--t, 0) * 0.6)), rgba(80,140,255,calc(var(--t, 0) * 0.6))), linear-gradient(rgba(255,140,40,calc(var(--t, 0) * -0.7)), rgba(255,140,40,calc(var(--t, 0) * -0.7))); transition: background 200ms linear; }
1809.fd-prs-g-light { background: radial-gradient(70% 60% at var(--lx) var(--ly), rgba(255,255,255,0.5), rgba(255,255,255,0) 70%); mix-blend-mode: soft-light; }
1810
1811/* hero fan */
1812.fd-prs-fan { position: absolute; inset: 0; }
1813.fd-prs-fan-card { position: absolute; left: 50%; top: 50%; display: block; width: 58%; aspect-ratio: 570 / 818; border-radius: 26px; overflow: hidden; transform: translate(-50%, -50%); box-shadow: 0 40px 80px -36px rgba(60,30,120,0.5), 0 0 0 1px rgba(255,255,255,0.75); transition: transform 800ms var(--fd-ease-out); }
1814.fd-prs-fan-card[data-pos="0"] { z-index: 3; cursor: default; }
1815.fd-prs-fan-card[data-pos="1"] { z-index: 2; transform: translate(calc(-50% + 48%), calc(-50% + 3%)) rotate(7deg) scale(0.78); }
1816.fd-prs-fan-card[data-pos="2"] { z-index: 1; transform: translate(calc(-50% - 48%), calc(-50% + 3%)) rotate(-7deg) scale(0.78); }
1817.fd-prs-fan-card[data-pos="1"]:hover { transform: translate(calc(-50% + 50%), calc(-50% + 1%)) rotate(8deg) scale(0.8); }
1818.fd-prs-fan-card[data-pos="2"]:hover { transform: translate(calc(-50% - 50%), calc(-50% + 1%)) rotate(-8deg) scale(0.8); }
1819
1820/* logos */
1821.fd-prs-logos { padding: 32px 0 40px; }
1822.fd-prs-marquee { margin-top: 22px; overflow: hidden; -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent); mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent); }
1823.fd-prs-marquee ul { display: flex; width: max-content; gap: 64px; padding-right: 64px; animation: fd-prs-marq 42s linear infinite; }
1824.fd-prs-marquee li { white-space: nowrap; color: #3d3d48; }
1825@keyframes fd-prs-marq { to { transform: translateX(-50%); } }
1826.fd-prs-wm-0 { font: 600 1.125rem/1 var(--fd-prs-sans); letter-spacing: 0.24em; }
1827.fd-prs-wm-1 { font: italic 500 1.375rem/1 Georgia, "Times New Roman", serif; }
1828.fd-prs-wm-2 { font: 700 1.375rem/1 var(--fd-prs-sans); letter-spacing: -0.05em; }
1829.fd-prs-wm-3 { font: 400 1.25rem/1 var(--fd-prs-sans); letter-spacing: 0.02em; }
1830
1831/* lens */
1832.fd-prs-lens-stage { position: relative; height: 560px; margin-top: 56px; border-radius: 36px; overflow: hidden; touch-action: none; cursor: grab; user-select: none; -webkit-user-select: none; box-shadow: 0 40px 90px -50px rgba(60,30,120,0.45), 0 0 0 1px rgba(255,255,255,0.8); }
1833.fd-prs-lens-stage:active { cursor: grabbing; }
1834.fd-prs-lens-bg { position: absolute; inset: 0; background: #fbfaff; }
1835.fd-prs-lens-stripes { position: absolute; inset: 0; background: repeating-linear-gradient(90deg, rgba(91,61,245,0.08) 0 2px, transparent 2px 28px), repeating-linear-gradient(0deg, rgba(22,22,29,0.05) 0 1px, transparent 1px 28px); }
1836.fd-prs-lens-word { position: absolute; left: 6%; top: 12%; font: 700 clamp(3rem, 8.5vw, 7.5rem)/0.9 var(--fd-prs-sans); letter-spacing: -0.055em; color: var(--fd-prs-ink); }
1837.fd-prs-lens-word::first-line { color: var(--fd-prs-accent); }
1838.fd-prs-lens-grid { position: absolute; left: 6%; bottom: 8%; display: grid; grid-template-columns: repeat(3, auto); gap: 8px 28px; font: 500 0.875rem/1.3 ui-monospace, "SF Mono", Menlo, monospace; color: var(--fd-prs-mute); }
1839.fd-prs-lens-shot { position: absolute; right: 5%; top: 8%; bottom: 8%; width: 34%; border-radius: 28px; overflow: hidden; box-shadow: 0 30px 60px -36px rgba(30,20,60,0.5); }
1840.fd-prs-lens { position: absolute; left: 0; top: 0; width: 240px; height: 240px; border-radius: 50%; will-change: transform; cursor: grab; touch-action: none; background: rgba(255,255,255,0.3); box-shadow: inset 0 1px 1px rgba(255,255,255,0.95), inset 0 -2px 6px rgba(255,255,255,0.4), inset 0 0 0 1px rgba(255,255,255,0.55), 0 24px 50px -16px rgba(30,20,60,0.4), 0 2px 6px rgba(30,20,60,0.12); }
1841.fd-prs-lens[data-refract] { background: rgba(255,255,255,0.06); }
1842.fd-prs-lens:not([data-refract]) { -webkit-backdrop-filter: blur(6px) saturate(200%) contrast(1.05); backdrop-filter: blur(6px) saturate(200%) contrast(1.05); background: rgba(255,255,255,0.28); }
1843.fd-prs-lens-ctrl { position: absolute; right: 20px; bottom: 20px; display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
1844.fd-prs-mode { display: inline-flex; align-items: center; gap: 8px; height: 28px; padding: 0 12px; border-radius: 9999px; background: rgba(22,22,29,0.78); color: #fff; font: 500 0.75rem/1 var(--fd-prs-sans); }
1845.fd-prs-mode::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: #9aa0a6; }
1846.fd-prs-mode[data-mode="refract"]::before { background: #5df2a5; }
1847
1848/* studio */
1849.fd-prs-studio { --p: 0; position: relative; height: 330vh; }
1850.fd-prs-studio-stick { position: sticky; top: 0; height: 100vh; display: flex; align-items: center; overflow: hidden; }
1851.fd-prs-studio-grid { display: grid; grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); gap: 64px; align-items: center; }
1852.fd-prs-scene-list { position: relative; display: grid; margin-top: 18px; }
1853.fd-prs-scene-list li { grid-area: 1 / 1; opacity: 0; transform: translateY(24px); transition: opacity var(--fd-dur-4) var(--fd-ease-out), transform var(--fd-dur-4) var(--fd-ease-out); pointer-events: none; }
1854.fd-prs-scene-list li[data-on] { opacity: 1; transform: none; pointer-events: auto; }
1855.fd-prs-scene-k { font: 600 0.875rem/1.2 var(--fd-prs-sans); color: var(--fd-prs-mute); }
1856.fd-prs-scene-list h3 { margin-top: 10px; font: 600 clamp(2rem, 4.2vw, 3.25rem)/1.04 var(--fd-prs-sans); letter-spacing: -0.035em; }
1857.fd-prs-scene-list p { margin-top: 18px; max-width: 26em; font: 400 1.125rem/1.5 var(--fd-prs-sans); color: var(--fd-prs-mute); }
1858.fd-prs-scene-dots { display: flex; gap: 8px; margin-top: 36px; }
1859.fd-prs-scene-dots i { width: 8px; height: 8px; border-radius: 9999px; background: rgba(22,22,29,0.18); transition: width var(--fd-dur-3) var(--fd-spring), background-color var(--fd-dur-3); }
1860.fd-prs-scene-dots i[data-on] { width: 28px; background: var(--fd-prs-ink); }
1861.fd-prs-studio-stage { position: relative; height: min(76vh, 640px); }
1862.fd-prs-depth { position: absolute; will-change: transform; }
1863.fd-prs-studio-stage > .fd-prs-depth:first-child { inset: 0; }
1864.fd-prs-studio-frame { position: absolute; inset: 0; border-radius: 28px; overflow: hidden; transition: clip-path 900ms var(--fd-ease-out); box-shadow: 0 50px 100px -40px rgba(60,30,120,0.35); }
1865.fd-prs-studio-c1 { left: -4%; top: 12%; }
1866.fd-prs-studio-c2 { right: -3%; top: 46%; }
1867.fd-prs-studio-c3 { left: 8%; bottom: 6%; }
1868.fd-prs-sun-dial { gap: 12px; padding-left: 10px; }
1869.fd-prs-dial { position: relative; width: 32px; height: 32px; border-radius: 50%; background: rgba(22,22,29,0.08); box-shadow: inset 0 0 0 1px rgba(22,22,29,0.1); }
1870.fd-prs-dial i { position: absolute; left: 50%; top: 50%; width: 2px; height: 13px; margin-left: -1px; border-radius: 2px; background: var(--fd-prs-ink); transform-origin: 50% 0; transform: rotate(calc(var(--a) + 180deg)); transition: transform var(--fd-dur-4) var(--fd-spring); }
1871.fd-prs-dial::after { content: ""; position: absolute; left: 50%; top: 50%; width: 6px; height: 6px; margin: -3px; border-radius: 50%; background: #ffb547; }
1872
1873/* gallery */
1874.fd-prs-gal { --g: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; margin-top: 64px; align-items: start; }
1875.fd-prs-gal-col { display: grid; gap: 20px; transform: translate3d(0, calc(var(--g) * var(--speed) * 1px), 0); will-change: transform; }
1876.fd-prs-gal-col:nth-child(2) { margin-top: 64px; }
1877.fd-prs-gal-item { position: relative; border-radius: 26px; overflow: hidden; aspect-ratio: 4 / 4.2; box-shadow: 0 30px 60px -36px rgba(60,30,120,0.4); }
1878.fd-prs-gal-item[data-tall] { aspect-ratio: 4 / 5.4; }
1879.fd-prs-gal-item figcaption { position: absolute; left: 14px; bottom: 14px; z-index: 2; display: inline-flex; align-items: center; height: 30px; padding: 0 12px; border-radius: 9999px; background: rgba(255,255,255,0.86); font: 500 0.75rem/1 var(--fd-prs-sans); color: var(--fd-prs-ink); }
1880
1881/* stats */
1882.fd-prs-stats { padding: 40px 0 0; }
1883.fd-prs-stats-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; }
1884.fd-prs-stats-row div { padding: 28px; border-radius: 26px; background: rgba(255,255,255,0.72); box-shadow: 0 0 0 1px rgba(255,255,255,0.9), 0 20px 40px -30px rgba(60,30,120,0.35); }
1885.fd-prs-stats-row dt { font: 650 clamp(2.25rem, 4vw, 3.5rem)/1 var(--fd-prs-sans); letter-spacing: -0.05em; font-variant-numeric: tabular-nums; }
1886.fd-prs-stats-row dd { margin-top: 10px; font: 400 0.9375rem/1.4 var(--fd-prs-sans); color: var(--fd-prs-mute); }
1887
1888/* carousel */
1889.fd-prs-car-btns { display: flex; gap: 10px; }
1890.fd-prs-round { width: 52px; height: 52px; border-radius: 50%; }
1891.fd-prs-round button { display: grid; place-items: center; width: 100%; height: 100%; border-radius: 50%; color: var(--fd-prs-ink); transition: transform var(--fd-dur-1) var(--fd-ease-out), opacity var(--fd-dur-2); }
1892.fd-prs-round button:active { transform: scale(0.92); }
1893.fd-prs-round button:disabled { opacity: 0.3; }
1894.fd-prs-car { margin-top: 48px; padding: 8px 0 40px; overflow: hidden; touch-action: pan-y; cursor: grab; user-select: none; -webkit-user-select: none; outline-offset: -4px; }
1895.fd-prs-car[data-drag] { cursor: grabbing; }
1896.fd-prs-car-track { display: flex; gap: 20px; width: max-content; padding: 0 max(32px, calc((100vw - 1200px) / 2 + 32px)); will-change: transform; }
1897.fd-prs-quote { display: flex; flex-direction: column; width: min(460px, 82vw); padding: 14px 14px 28px; border-radius: 32px; background: rgba(255,255,255,0.8); box-shadow: 0 0 0 1px rgba(255,255,255,0.9), 0 30px 60px -40px rgba(60,30,120,0.45); transition: transform var(--fd-dur-4) var(--fd-ease-out), opacity var(--fd-dur-4) var(--fd-ease-out); opacity: 0.6; transform: scale(0.97); }
1898.fd-prs-quote[data-active] { opacity: 1; transform: none; }
1899.fd-prs-quote-shot { position: relative; height: 220px; border-radius: 22px; overflow: hidden; }
1900.fd-prs-quote-metric { position: absolute; left: 12px; top: 12px; z-index: 2; display: inline-flex; align-items: center; height: 30px; padding: 0 12px; border-radius: 9999px; background: rgba(22,22,29,0.82); color: #fff; font: 600 0.8125rem/1 var(--fd-prs-sans); }
1901.fd-prs-quote blockquote { margin: 24px 14px 0; font: 500 1.25rem/1.4 var(--fd-prs-sans); letter-spacing: -0.02em; }
1902.fd-prs-quote figcaption { display: flex; flex-direction: column; gap: 2px; margin: 22px 14px 0; }
1903.fd-prs-quote figcaption b { font: 600 0.9375rem/1.3 var(--fd-prs-sans); }
1904.fd-prs-quote figcaption span { font: 400 0.875rem/1.3 var(--fd-prs-sans); color: var(--fd-prs-mute); }
1905.fd-prs-car-dots { display: flex; justify-content: center; gap: 8px; }
1906.fd-prs-car-dots i { width: 7px; height: 7px; border-radius: 9999px; background: rgba(22,22,29,0.2); transition: width var(--fd-dur-3) var(--fd-spring), background-color var(--fd-dur-3); }
1907.fd-prs-car-dots i[data-on] { width: 24px; background: var(--fd-prs-ink); }
1908
1909/* pricing */
1910.fd-prs-seg { position: relative; display: inline-grid; grid-template-columns: 1fr 1fr; margin-top: 36px; padding: 5px; border-radius: 9999px; }
1911.fd-prs-seg button { position: relative; z-index: 3; display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 42px; padding: 0 22px; border-radius: 9999px; font: 500 0.9375rem/1 var(--fd-prs-sans); color: var(--fd-prs-ink); }
1912.fd-prs-seg em { font-size: 0.75rem; color: var(--fd-prs-accent); font-weight: 600; }
1913.fd-prs-seg-thumb { position: absolute; z-index: 2; top: 5px; bottom: 5px; left: 5px; width: calc(50% - 5px); border-radius: 9999px; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.08), 0 6px 16px -6px rgba(30,20,60,0.3); transition: transform var(--fd-dur-3) var(--fd-spring); }
1914.fd-prs-seg[data-yearly] .fd-prs-seg-thumb { transform: translateX(100%); }
1915.fd-prs-plans { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; margin-top: 56px; align-items: stretch; }
1916.fd-prs-plan { display: flex; flex-direction: column; padding: 32px; border-radius: 32px; background: rgba(255,255,255,0.82); box-shadow: 0 0 0 1px rgba(255,255,255,0.95), 0 30px 60px -40px rgba(60,30,120,0.4); }
1917.fd-prs-plan[data-hot] { background: var(--fd-prs-ink); color: #fff; box-shadow: 0 40px 80px -36px rgba(60,30,120,0.6); }
1918.fd-prs-plan-top { display: flex; justify-content: space-between; align-items: center; }
1919.fd-prs-plan h3 { font: 600 1.375rem/1.2 var(--fd-prs-sans); letter-spacing: -0.02em; }
1920.fd-prs-plan-tag { display: inline-flex; align-items: center; height: 26px; padding: 0 10px; border-radius: 9999px; background: rgba(255,255,255,0.14); font: 600 0.75rem/1 var(--fd-prs-sans); }
1921.fd-prs-plan-blurb { margin-top: 8px; font: 400 0.9375rem/1.45 var(--fd-prs-sans); color: var(--fd-prs-mute); }
1922.fd-prs-plan[data-hot] .fd-prs-plan-blurb, .fd-prs-plan[data-hot] .fd-prs-plan-bill, .fd-prs-plan[data-hot] .fd-prs-price-per { color: #b9b9c6; }
1923.fd-prs-price { display: flex; align-items: baseline; gap: 8px; margin-top: 30px; }
1924.fd-prs-price-n { display: inline-block; font: 650 3.5rem/1 var(--fd-prs-sans); letter-spacing: -0.055em; font-variant-numeric: tabular-nums; animation: fd-prs-pop var(--fd-dur-3) var(--fd-spring); }
1925@keyframes fd-prs-pop { from { opacity: 0; transform: translateY(10px) scale(0.92); } to { opacity: 1; transform: none; } }
1926.fd-prs-price-per { font: 400 1rem/1 var(--fd-prs-sans); color: var(--fd-prs-mute); }
1927.fd-prs-plan-bill { margin-top: 8px; font: 400 0.8125rem/1.4 var(--fd-prs-sans); color: var(--fd-prs-mute); }
1928.fd-prs-plan .fd-prs-btn { margin-top: 28px; width: 100%; }
1929.fd-prs-plan[data-hot] .fd-prs-btn { background: #fff; color: var(--fd-prs-ink); }
1930.fd-prs-plan ul { display: grid; gap: 12px; margin-top: 28px; }
1931.fd-prs-plan li { display: flex; gap: 10px; align-items: center; font: 400 0.9375rem/1.35 var(--fd-prs-sans); }
1932.fd-prs-plan li svg { flex: none; color: var(--fd-prs-accent); }
1933.fd-prs-plan[data-hot] li svg { color: #b7a8ff; }
1934
1935/* before / after */
1936.fd-prs-cmp { display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 1fr); gap: 64px; align-items: center; }
1937.fd-prs-cmp-ctrl { display: grid; justify-items: start; gap: 10px; margin-top: 32px; }
1938.fd-prs-cmp-lab { margin-top: 6px; font: 600 0.8125rem/1.2 var(--fd-prs-sans); color: var(--fd-prs-mute); }
1939.fd-prs-pills { display: inline-flex; flex-wrap: wrap; gap: 4px; padding: 5px; border-radius: 26px; }
1940.fd-prs-pills button { display: inline-flex; align-items: center; height: 40px; padding: 0 16px; border-radius: 9999px; font: 500 0.9375rem/1 var(--fd-prs-sans); color: var(--fd-prs-ink); transition: background-color var(--fd-dur-2) var(--fd-ease-out), box-shadow var(--fd-dur-2) var(--fd-ease-out); }
1941.fd-prs-pills button:hover { background: rgba(22,22,29,0.06); }
1942.fd-prs-pills button[aria-pressed="true"] { background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.08), 0 6px 16px -6px rgba(30,20,60,0.3); }
1943.fd-prs-cmp-stage { position: relative; width: 100%; max-width: 540px; aspect-ratio: 4 / 5; justify-self: end; border-radius: 36px; overflow: hidden; isolation: isolate; box-shadow: 0 50px 100px -40px rgba(60,30,120,0.4), 0 0 0 1px rgba(255,255,255,0.8); }
1944.fd-prs-cmp-after { position: absolute; inset: 0; }
1945.fd-prs-cmp-tag { position: absolute; top: 16px; z-index: 3; display: inline-flex; align-items: center; height: 30px; padding: 0 12px; border-radius: 9999px; background: rgba(22,22,29,0.82); color: #fff; font: 600 0.8125rem/1 var(--fd-prs-sans); pointer-events: none; }
1946.fd-prs-cmp-tag-l { left: 16px; }
1947.fd-prs-cmp-tag-r { right: 16px; }
1948.fd-prs-cmp-range { position: absolute; inset: 0; z-index: 5; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: ew-resize; touch-action: pan-y; }
1949.fd-prs-cmp-line { position: absolute; top: 0; bottom: 0; z-index: 4; width: 2px; margin-left: -1px; background: rgba(255,255,255,0.9); box-shadow: 0 0 0 1px rgba(22,22,29,0.12); pointer-events: none; }
1950.fd-prs-cmp-knob { position: absolute; left: 50%; top: 50%; display: flex; align-items: center; justify-content: center; width: 56px; height: 56px; margin: -28px 0 0 -28px; border-radius: 50%; color: var(--fd-prs-ink); }
1951.fd-prs-cmp-knob svg { width: 14px; height: 14px; margin: 0 -2px; }
1952.fd-prs-cmp-range:focus-visible + .fd-prs-cmp-line .fd-prs-cmp-knob { outline: 2px solid rgba(91, 61, 245, 0.9); outline-offset: 3px; }
1953
1954/* faq */
1955.fd-prs-faq { display: grid; gap: 12px; max-width: 760px; margin: 56px auto 0; }
1956.fd-prs-faq details { border-radius: 24px; background: rgba(255,255,255,0.8); box-shadow: 0 0 0 1px rgba(255,255,255,0.95), 0 20px 40px -32px rgba(60,30,120,0.4); }
1957.fd-prs-faq summary { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 22px 26px; border-radius: 24px; font: 600 1.0625rem/1.3 var(--fd-prs-sans); letter-spacing: -0.015em; cursor: pointer; list-style: none; }
1958.fd-prs-faq summary::-webkit-details-marker { display: none; }
1959.fd-prs-faq summary:focus-visible { outline: 2px solid rgba(91, 61, 245, 0.7); outline-offset: 2px; }
1960.fd-prs-faq summary svg { flex: none; color: var(--fd-prs-mute); transform: rotate(90deg); transition: transform var(--fd-dur-3) var(--fd-spring); }
1961.fd-prs-faq details[open] summary svg { transform: rotate(-90deg); }
1962.fd-prs-faq details p { max-width: 38em; padding: 0 26px 24px; font: 400 1rem/1.55 var(--fd-prs-sans); color: var(--fd-prs-mute); }
1963
1964/* cta */
1965.fd-prs-cta { padding: 40px 0 140px; }
1966.fd-prs-cta-card { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr); align-items: stretch; border-radius: 40px; overflow: hidden; background: rgba(255,255,255,0.78); box-shadow: 0 0 0 1px rgba(255,255,255,0.95), 0 50px 100px -50px rgba(60,30,120,0.5); }
1967.fd-prs-cta-copy { padding: 64px; }
1968.fd-prs-cta-h { font: 650 clamp(2.25rem, 4.6vw, 3.75rem)/1 var(--fd-prs-sans); letter-spacing: -0.045em; text-wrap: balance; }
1969.fd-prs-cta-btns { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 32px; }
1970.fd-prs-cta-shot { position: relative; min-height: 380px; }
1971
1972/* footer */
1973.fd-prs-foot { padding: 64px 0 32px; background: rgba(255,255,255,0.6); border-top: 1px solid rgba(255,255,255,0.9); }
1974.fd-prs-foot-top { display: grid; grid-template-columns: 1.6fr repeat(3, minmax(0, 1fr)); gap: 32px; }
1975.fd-prs-foot-p { margin-top: 14px; max-width: 22em; font: 400 0.875rem/1.5 var(--fd-prs-sans); color: var(--fd-prs-mute); }
1976.fd-prs-foot h4 { font: 600 0.8125rem/1 var(--fd-prs-sans); }
1977.fd-prs-foot ul { display: grid; gap: 10px; margin-top: 16px; }
1978.fd-prs-foot ul a { font: 400 0.875rem/1.3 var(--fd-prs-sans); color: var(--fd-prs-mute); transition: color var(--fd-dur-1) linear; }
1979.fd-prs-foot ul a:hover { color: var(--fd-prs-ink); }
1980.fd-prs-foot-bot { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-top: 56px; padding-top: 20px; border-top: 1px solid var(--fd-prs-line); font: 400 0.8125rem/1.3 var(--fd-prs-sans); color: var(--fd-prs-mute); }
1981
1982/* reveal */
1983.fd-prs[data-js] [data-reveal] { opacity: 0; transform: translateY(26px) scale(0.985); transition: opacity var(--fd-dur-4) var(--fd-ease-out), transform 900ms var(--fd-ease-out); transition-delay: calc(var(--i, 0) * 80ms); }
1984.fd-prs[data-js] [data-reveal][data-in] { opacity: 1; transform: none; }
1985.fd-prs-gal-item[data-reveal] { transform: translateY(40px) scale(0.96); }
1986
1987/* responsive */
1988@media (max-width: 1000px) {
1989 .fd-prs-nav-links, .fd-prs-nav-signin { display: none; }
1990 .fd-prs-burger { display: block; }
1991 .fd-prs-nav[data-condensed] .fd-prs-nav-in { max-width: none; padding: 0 32px; }
1992 .fd-prs-nav[data-condensed] .fd-prs-nav-glass { clip-path: inset(10px 16px 10px 16px round 9999px); }
1993 .fd-prs-hero-in { grid-template-columns: minmax(0, 1fr); gap: 40px; }
1994 .fd-prs-hero-stage { max-width: 620px; width: 100%; justify-self: center; }
1995 .fd-prs-studio-grid { grid-template-columns: minmax(0, 1fr); gap: 28px; }
1996 .fd-prs-studio-stage { height: 52vh; }
1997 .fd-prs-scene-list h3 { font-size: 2rem; }
1998 .fd-prs-scene-list p { margin-top: 10px; font-size: 1rem; }
1999 .fd-prs-scene-dots { margin-top: 16px; }
2000 .fd-prs-stats-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2001 .fd-prs-plans { grid-template-columns: 1fr; max-width: 520px; margin-left: auto; margin-right: auto; }
2002 .fd-prs-cta-card { grid-template-columns: 1fr; }
2003 .fd-prs-cmp { grid-template-columns: minmax(0, 1fr); gap: 36px; }
2004 .fd-prs-cmp-stage { justify-self: center; max-width: 560px; }
2005 .fd-prs-cta-shot { min-height: 300px; }
2006}
2007@media (max-width: 720px) {
2008 .fd-prs-wrap, .fd-prs-nav-in, .fd-prs-nav[data-condensed] .fd-prs-nav-in { padding: 0 20px; }
2009 .fd-prs-nav-right .fd-prs-btn-sm { display: none; }
2010 .fd-prs-sheet { padding: 4px 32px 24px; }
2011 .fd-prs-sec { padding: 96px 0; }
2012 .fd-prs-hero { padding: 40px 0 88px; }
2013 .fd-prs-prompt { height: 56px; padding-left: 16px; }
2014 .fd-prs-prompt input { font-size: 0.9375rem; }
2015 .fd-prs-chip { min-height: 42px; padding: 6px 14px; font-size: 0.8125rem; }
2016 .fd-prs-chip-temp { width: 168px; padding: 12px 14px 14px; }
2017 .fd-prs-depth-3 { right: -2px; }
2018 .fd-prs-depth-3 { top: 3%; }
2019 .fd-prs-lens-stage { height: 520px; border-radius: 28px; }
2020 .fd-prs-lens { width: 170px; height: 170px; }
2021 .fd-prs-lens-shot { left: 6%; right: 6%; top: auto; bottom: 26%; width: auto; height: 34%; }
2022 .fd-prs-lens-grid { display: none; }
2023 .fd-prs-lens-ctrl { left: 16px; right: 16px; bottom: 16px; align-items: stretch; }
2024 .fd-prs-chip-strength { width: auto; }
2025 .fd-prs-mode { align-self: flex-start; }
2026 .fd-prs-studio { height: 300vh; }
2027 .fd-prs-studio-stage { height: 46vh; }
2028 .fd-prs-studio-c1 { left: 2%; top: 4%; }
2029 .fd-prs-studio-c2 { right: 2%; top: 56%; }
2030 .fd-prs-gal { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
2031 .fd-prs-gal-col { gap: 12px; }
2032 .fd-prs-gal-col:nth-child(3) { display: none; }
2033 .fd-prs-gal-col:nth-child(2) { margin-top: 40px; }
2034 .fd-prs-gal-item { border-radius: 20px; }
2035 .fd-prs-gal-item figcaption { left: 8px; bottom: 8px; height: 26px; padding: 0 9px; font-size: 0.6875rem; max-width: calc(100% - 16px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; line-height: 26px; }
2036 .fd-prs-stats-row div { padding: 20px; }
2037 .fd-prs-quote blockquote { font-size: 1.0625rem; }
2038 .fd-prs-quote-shot { height: 180px; }
2039 .fd-prs-car-track { padding: 0 20px; gap: 12px; }
2040 .fd-prs-cta { padding-bottom: 96px; }
2041 .fd-prs-cta-copy { padding: 36px 28px; }
2042 .fd-prs-foot-top { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2043 .fd-prs-foot-top > div:first-child { grid-column: 1 / -1; }
2044}
2045@media (prefers-reduced-motion: reduce) {
2046 .fd-prs *, .fd-prs *::before, .fd-prs *::after { animation: none !important; }
2047 .fd-prs[data-js] [data-reveal] { transform: none !important; transition: opacity 120ms linear; transition-delay: 0ms; }
2048 .fd-prs-hero-stage, .fd-prs-stage-layer, .fd-prs-gal-col, .fd-prs-depth { transform: none !important; }
2049 .fd-prs-studio { height: auto; padding: 96px 0; }
2050 .fd-prs-studio-stick { position: relative; height: auto; }
2051 .fd-prs-scene-list { gap: 32px; }
2052 .fd-prs-scene-list li { grid-area: auto; opacity: 1; transform: none; }
2053 .fd-prs-scene-dots { display: none; }
2054 .fd-prs-quote { transition: none; }
2055 .fd-prs-fan-card, .fd-prs-shot-img, .fd-prs-g-bg, .fd-prs-g-all { transition: none; }
2056 .fd-prs-faq summary svg { transition: none; }
2057}
2058@media (prefers-reduced-transparency: reduce) {
2059 .fd-prs-glass, .fd-prs-glass[data-refract] { background: rgba(250,250,252,0.97); -webkit-backdrop-filter: none !important; backdrop-filter: none !important; }
2060 .fd-prs-lens { background: rgba(250,250,252,0.5); }
2061}
2062@media (prefers-contrast: more) {
2063 .fd-prs { --fd-prs-mute: #2f2f38; }
2064 .fd-prs-glass, .fd-prs-glass[data-refract] { background: #fff; -webkit-backdrop-filter: none !important; backdrop-filter: none !important; box-shadow: 0 0 0 1px #16161d; }
2065 .fd-prs-mute { color: #3a3a44; }
2066}
2067`;