1/**
2 * Noir — luxury perfume house landing page (Maison Vérane).
3 *
4 * Cinematic black, bone and a single amber. Giant italic serif type; no
5 * web fonts or network. The hero flacon is a pre-rendered photograph
6 * (Demo image: /img/lux/perfume-noir.webp, included in the repo's public/img/lux —
7 * copy it next to your page or swap in your own); the smaller flacons stay SVG so they can be tinted.
8 * The smoke is a canvas of ~1k particles on
9 * a drifting noise field that the cursor stirs.
10 *
11 * Motion
12 * - Hero: procedural smoke / ink fluid. Pointer velocity drags particles and
13 * adds a swirl; the title rises letter by letter out of a mask.
14 * - Manifesto: pinned, a giant italic sentence lights up word by word.
15 * - Flacon: pinned SVG bottle, two bands of light sweep across the glass as
16 * you scroll while the liquid glows and the bottle tilts; captions arrive
17 * in turn over an outlined background word.
18 * - Notes: pinned horizontal scroll through top / heart / base, each with its
19 * own colour atmosphere that crossfades; giant words counter-parallax.
20 * - Quiz: a pinned "discover your scent" with an animated result (name rises
21 * letter by letter, bars fill, the flacon fills with its own colour).
22 * - Collection rows with hover-lift bottles; reveal-on-scroll elsewhere.
23 *
24 * Reduced motion: smoke is a single pre-rendered frame, nothing pins, the
25 * notes stack vertically, every word and caption is fully visible.
26 * SSR-safe: no window/document access during render.
27 */
28import { useEffect, useRef, useState, type CSSProperties, type FormEvent } from "react";
29
30const sv = (o: Record<string, string | number>) => o as CSSProperties;
31const clamp = (v: number, a = 0, b = 1) => Math.min(b, Math.max(a, v));
32const prefersReduced = () => typeof window !== "undefined" && !!window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
33const NOOP = (e: { preventDefault: () => void }) => e.preventDefault();
34
35/* ------------------------------------------------------------------ */
36/* Data */
37/* ------------------------------------------------------------------ */
38const MANIFESTO = "Some things are not worn. They are remembered — a coat left on a chair, a door closing in another room, the last warm hour of a long day.";
39const AMBER_WORDS = new Set(["remembered", "warm", "hour"]);
40
41const NOTES = [
42 { k: "Top", n: "I", when: "0 – 15 minutes", word: "Top", line: "The first breath. Bright, cold, gone before you can hold it.", atm: "#9DB5A6", list: [["Black bergamot", "Calabria, cold-pressed at dawn"], ["Pink pepper", "Sharp, almost metallic"], ["Green cardamom", "Crushed between the fingers"]] },
43 { k: "Heart", n: "II", when: "15 minutes – 3 hours", word: "Heart", line: "The conversation. Velvet petals over a low smoulder.", atm: "#B04A5C", list: [["Rose absolute", "Bulgarian, steam-distilled twice"], ["Labdanum", "Resin of the cistus, dark and leathery"], ["Smoked iris", "Orris butter touched with birch tar"]] },
44 { k: "Base", n: "III", when: "3 – 12 hours", word: "Base", line: "The memory. What stays on the collar long after you leave.", atm: "#E0A04A", list: [["Amber accord", "Warm, resinous, faintly sweet"], ["Aged oud", "Eleven years in oak"], ["Tonka & vetiver", "Dry hay, smoke, the end of summer"]] },
45];
46
47const QUIZ = [
48 { q: "Where does the evening find you?", o: [
49 ["A velvet bar, past midnight", [3, 1, 0, 0]], ["A coastal road at dusk", [0, 0, 3, 1]], ["An empty cathedral", [1, 0, 1, 3]], ["A garden after the rain", [0, 3, 1, 1]]] },
50 { q: "Choose a texture.", o: [
51 ["Suede, worn soft", [3, 1, 0, 0]], ["Raw silk", [0, 3, 0, 1]], ["Cold stone", [0, 0, 2, 3]], ["Linen, line-dried", [0, 0, 3, 1]]] },
52 { q: "How long should it linger?", o: [
53 ["A whisper, three hours", [0, 0, 2, 2]], ["An evening, eight hours", [1, 2, 1, 1]], ["A memory, twelve and more", [3, 2, 0, 0]], ["Until someone asks", [2, 1, 0, 2]]] },
54] as { q: string; o: [string, number[]][] }[];
55
56const SCENTS = [
57 { id: "nox", num: "19", name: "Nox Ambre", tint: "#E0A04A", desc: "Warm, resinous and unhurried. Amber and aged oud in a slow, dark dance. A scent for rooms with the lights turned low.", bars: [92, 78, 96], notes: ["Black bergamot, pink pepper", "Rose absolute, labdanum", "Amber, oud, tonka"] },
58 { id: "rose", num: "22", name: "Rose Fumée", tint: "#B04A5C", desc: "A rose that has been near a fire. Smoked petals and birch tar over a floor of patchouli. Romantic, a little dangerous.", bars: [74, 88, 82], notes: ["Saffron, cardamom", "Rose absolute, birch tar", "Patchouli, guaiac wood"] },
59 { id: "vet", num: "31", name: "Vétiver Pâle", tint: "#9DB5A6", desc: "Cool, mineral and clean-edged. Vetiver root and cypress under a flash of grapefruit. Salt air and linen.", bars: [58, 46, 64], notes: ["Grapefruit zest, juniper", "Vetiver, cypress", "Oakmoss, white musk"] },
60 { id: "iris", num: "08", name: "Iris Cendre", tint: "#C9C2D6", desc: "Powdery, grey and quietly austere. Orris butter, violet leaf and cold ash. The smell of stone after a long silence.", bars: [62, 54, 70], notes: ["Aldehydes, mandarin", "Orris butter, violet leaf", "Cedar, ash, sandalwood"] },
61];
62
63const COLLECTION = [
64 { n: "Nox Ambre", no: "Nº 19", notes: "Amber · Oud · Tonka", price: "€265", tint: "#E0A04A" },
65 { n: "Rose Fumée", no: "Nº 22", notes: "Smoked rose · Birch tar · Patchouli", price: "€265", tint: "#B04A5C" },
66 { n: "Vétiver Pâle", no: "Nº 31", notes: "Vetiver · Cypress · Grapefruit", price: "€245", tint: "#9DB5A6" },
67 { n: "Iris Cendre", no: "Nº 08", notes: "Orris · Violet leaf · Ash", price: "€285", tint: "#C9C2D6" },
68];
69
70/* ------------------------------------------------------------------ */
71/* Noise for the smoke */
72/* ------------------------------------------------------------------ */
73const hash2 = (x: number, y: number) => {
74 let h = Math.imul(x, 374761393) ^ Math.imul(y, 668265263);
75 h = Math.imul(h ^ (h >>> 13), 1274126177);
76 return ((h ^ (h >>> 16)) >>> 0) / 4294967295;
77};
78const vnoise = (x: number, y: number) => {
79 const xi = Math.floor(x), yi = Math.floor(y), xf = x - xi, yf = y - yi;
80 const u = xf * xf * (3 - 2 * xf), v = yf * yf * (3 - 2 * yf);
81 const a = hash2(xi, yi), b = hash2(xi + 1, yi), c = hash2(xi, yi + 1), d = hash2(xi + 1, yi + 1);
82 return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
83};
84
85/* ------------------------------------------------------------------ */
86/* Flacon */
87/* ------------------------------------------------------------------ */
88const BODY = "M104 252Q104 226 130 226H270Q296 226 296 252V556Q296 586 266 586H134Q104 586 104 556Z";
89const LIQ = "M118 270Q118 264 126 264H274Q282 264 282 270V550Q282 572 262 572H138Q118 572 118 550Z";
90
91function Defs() {
92 return (
93 <svg width="0" height="0" style={{ position: "absolute" }} aria-hidden="true" focusable="false">
94 <defs>
95 <linearGradient id="nrg-glass" x1="0" y1="0" x2="1" y2="0">
96 <stop offset="0" stopColor="#1d1a16" /><stop offset=".12" stopColor="#0d0c0a" /><stop offset=".55" stopColor="#14120f" /><stop offset=".9" stopColor="#0a0908" /><stop offset="1" stopColor="#24201a" />
97 </linearGradient>
98 <linearGradient id="nrg-shade" x1="0" y1="0" x2="0" y2="1">
99 <stop offset="0" stopColor="#fff" stopOpacity=".22" /><stop offset=".25" stopColor="#fff" stopOpacity="0" /><stop offset=".7" stopColor="#000" stopOpacity="0" /><stop offset="1" stopColor="#000" stopOpacity=".55" />
100 </linearGradient>
101 <linearGradient id="nrg-sweep" x1="0" y1="0" x2="1" y2="0">
102 <stop offset="0" stopColor="#fff" stopOpacity="0" /><stop offset=".5" stopColor="#fff" stopOpacity=".7" /><stop offset="1" stopColor="#fff" stopOpacity="0" />
103 </linearGradient>
104 <linearGradient id="nrg-cap" x1="0" y1="0" x2="1" y2="0">
105 <stop offset="0" stopColor="#2a2621" /><stop offset=".18" stopColor="#0b0a09" /><stop offset=".5" stopColor="#1b1814" /><stop offset=".8" stopColor="#090807" /><stop offset="1" stopColor="#2b2721" />
106 </linearGradient>
107 <linearGradient id="nrg-metal" x1="0" y1="0" x2="1" y2="0">
108 <stop offset="0" stopColor="#7a5524" /><stop offset=".35" stopColor="#f0c27a" /><stop offset=".6" stopColor="#b57f34" /><stop offset="1" stopColor="#5e3f17" />
109 </linearGradient>
110 <radialGradient id="nrg-glow"><stop offset="0" stopColor="#E0A04A" stopOpacity=".9" /><stop offset=".5" stopColor="#E0A04A" stopOpacity=".25" /><stop offset="1" stopColor="#E0A04A" stopOpacity="0" /></radialGradient>
111 <clipPath id="nrg-photo"><rect x="104" y="226" width="192" height="360" rx="30" /><rect x="146" y="120" width="108" height="130" rx="10" /></clipPath>
112 <clipPath id="nrg-clip">
113 <path d={BODY} /><rect x="176" y="176" width="48" height="54" /><rect x="164" y="162" width="72" height="20" rx="3" /><rect x="146" y="48" width="108" height="118" rx="10" />
114 </clipPath>
115 </defs>
116 </svg>
117 );
118}
119
120type FlaconProps = {
121 tint?: string; big?: boolean; level?: number; className?: string;
122 sweepRef?: (el: SVGRectElement | null) => void; sweep2Ref?: (el: SVGRectElement | null) => void;
123 glowRef?: (el: SVGCircleElement | null) => void; tiltRef?: (el: SVGGElement | null) => void;
124};
125function Flacon({ tint = "#E0A04A", big = false, level = 1, className, sweepRef, sweep2Ref, glowRef, tiltRef }: FlaconProps) {
126 if (big)
127 return (
128 <svg className={className} viewBox="0 0 400 640" role="img" aria-label="Vérane Nº 19 flacon: amber glass, black lacquer cap, photographed in golden light" style={{ overflow: "visible" }}>
129 <circle ref={glowRef} cx="200" cy="420" r="260" fill="url(#nrg-glow)" opacity={0.3} style={{ mixBlendMode: "screen" }} />
130 <g ref={tiltRef}>
131 <image
132 href="/img/lux/perfume-noir.webp"
133 x="-70" y="20" width="540" height="675"
134 preserveAspectRatio="xMidYMid slice"
135 style={{ WebkitMaskImage: "radial-gradient(ellipse 62% 60% at 50% 50%, #000 52%, transparent 100%)", maskImage: "radial-gradient(ellipse 62% 60% at 50% 50%, #000 52%, transparent 100%)" }}
136 />
137 <g clipPath="url(#nrg-photo)" style={{ mixBlendMode: "screen" }}>
138 <rect ref={sweepRef} x="0" y="0" width="90" height="640" fill="url(#nrg-sweep)" transform="translate(-200 0) skewX(-16)" />
139 <rect ref={sweep2Ref} x="0" y="0" width="26" height="640" fill="url(#nrg-sweep)" opacity=".8" transform="translate(-200 0) skewX(-16)" />
140 </g>
141 </g>
142 </svg>
143 );
144 return (
145 <svg className={className} viewBox="0 0 400 640" role="img" aria-label="Vérane flacon, hand-blown glass with an oak cap" style={{ overflow: "visible" }}>
146 <circle ref={glowRef} cx="200" cy="420" r="260" fill="url(#nrg-glow)" opacity={big ? 0.3 : 0.18} style={{ mixBlendMode: "screen" }} />
147 <g ref={tiltRef}>
148 <ellipse cx="200" cy="600" rx="150" ry="14" fill="#000" opacity=".7" />
149 {/* glass */}
150 <path d={BODY} fill="url(#nrg-glass)" stroke="#E9E2D3" strokeOpacity=".38" strokeWidth="1.2" />
151 {/* liquid */}
152 <g className="nr-liq" style={{ transform: `scaleY(${level})`, transformOrigin: "200px 572px", transformBox: "view-box" } as CSSProperties}>
153 <path d={LIQ} fill={tint} fillOpacity=".92" />
154 <path d={LIQ} fill="url(#nrg-shade)" />
155 <ellipse cx="200" cy="266" rx="82" ry="4" fill="#fff" opacity=".35" />
156 </g>
157 <path d="M104 252Q104 226 130 226H270Q296 226 296 252V556Q296 586 266 586H134Q104 586 104 556Z" fill="url(#nrg-shade)" opacity=".35" />
158 <rect x="112" y="240" width="5" height="330" rx="2.5" fill="#fff" opacity=".16" />
159 <rect x="104" y="548" width="192" height="3" fill="#fff" opacity=".08" />
160 {/* neck + collar + cap */}
161 <rect x="176" y="176" width="48" height="54" fill="url(#nrg-glass)" stroke="#E9E2D3" strokeOpacity=".3" />
162 <rect x="164" y="162" width="72" height="20" rx="3" fill="url(#nrg-metal)" />
163 <rect x="146" y="48" width="108" height="118" rx="10" fill="url(#nrg-cap)" stroke="#E9E2D3" strokeOpacity=".3" />
164 <path d="M172 52V162M200 52V162M228 52V162" stroke="#E9E2D3" strokeOpacity=".08" />
165 <rect x="150" y="56" width="4" height="100" rx="2" fill="#fff" opacity=".18" />
166 {/* label */}
167 <rect x="134" y="344" width="132" height="140" fill="none" stroke="#E9E2D3" strokeOpacity={big ? 0.45 : 0.35} strokeWidth=".8" />
168 {big && (
169 <>
170 <text x="200" y="372" textAnchor="middle" fontFamily="inherit" fontSize="10" letterSpacing="5" fill="#E9E2D3" fillOpacity=".85">VÉRANE</text>
171 <text x="200" y="430" textAnchor="middle" fontFamily="inherit" fontStyle="italic" fontSize="46" fill="#E9E2D3">Nº 19</text>
172 <line x1="176" y1="446" x2="224" y2="446" stroke="#E0A04A" strokeWidth="1" />
173 <text x="200" y="464" textAnchor="middle" fontFamily="inherit" fontSize="8" letterSpacing="3.5" fill="#E9E2D3" fillOpacity=".7">NOX AMBRE</text>
174 <text x="200" y="476" textAnchor="middle" fontFamily="inherit" fontSize="6" letterSpacing="2.5" fill="#E9E2D3" fillOpacity=".5">EAU DE PARFUM · 50 ML</text>
175 </>
176 )}
177 {!big && <line x1="170" y1="414" x2="230" y2="414" stroke="#E9E2D3" strokeOpacity=".5" strokeWidth=".8" />}
178 {/* light */}
179 {big && (
180 <g clipPath="url(#nrg-clip)" style={{ mixBlendMode: "screen" }}>
181 <rect ref={sweepRef} x="0" y="0" width="90" height="640" fill="url(#nrg-sweep)" transform="translate(-200 0) skewX(-16)" />
182 <rect ref={sweep2Ref} x="0" y="0" width="26" height="640" fill="url(#nrg-sweep)" opacity=".8" transform="translate(-200 0) skewX(-16)" />
183 </g>
184 )}
185 </g>
186 </svg>
187 );
188}
189
190/* ------------------------------------------------------------------ */
191/* Page */
192/* ------------------------------------------------------------------ */
193export function Page() {
194 const rootRef = useRef<HTMLDivElement>(null);
195 const heroRef = useRef<HTMLElement>(null);
196 const cvsRef = useRef<HTMLCanvasElement>(null);
197 const heroTxtRef = useRef<HTMLDivElement>(null);
198 const manRef = useRef<HTMLElement>(null);
199 const wordRefs = useRef<(HTMLSpanElement | null)[]>([]);
200 const botRef = useRef<HTMLElement>(null);
201 const sweepRef = useRef<SVGRectElement | null>(null);
202 const sweep2Ref = useRef<SVGRectElement | null>(null);
203 const glowRef = useRef<SVGCircleElement | null>(null);
204 const tiltRef = useRef<SVGGElement | null>(null);
205 const capRefs = useRef<(HTMLDivElement | null)[]>([]);
206 const bgWordRef = useRef<HTMLDivElement>(null);
207 const notesRef = useRef<HTMLElement>(null);
208 const trackRef = useRef<HTMLDivElement>(null);
209 const atmRefs = useRef<(HTMLDivElement | null)[]>([]);
210 const panelRefs = useRef<(HTMLDivElement | null)[]>([]);
211 const fillRef = useRef<HTMLSpanElement>(null);
212 const quizRef = useRef<HTMLElement>(null);
213 const qringRef = useRef<SVGGElement>(null);
214
215 const [note, setNote] = useState(0);
216 const [step, setStep] = useState(0);
217 const [picks, setPicks] = useState<number[][]>([]);
218 const [go, setGo] = useState(false);
219 const [reserved, setReserved] = useState(false);
220 const [navSolid, setNavSolid] = useState(false);
221 const [email, setEmail] = useState("");
222 const [joined, setJoined] = useState(false);
223
224 const words = MANIFESTO.split(" ");
225
226 /* reveals */
227 useEffect(() => {
228 const root = rootRef.current; if (!root) return;
229 root.classList.add("nr-js");
230 const els = Array.from(root.querySelectorAll<HTMLElement>("[data-rv]"));
231 if (typeof IntersectionObserver === "undefined") { els.forEach((e) => e.classList.add("in")); return; }
232 const io = new IntersectionObserver((es) => es.forEach((e) => { if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } }), { rootMargin: "0px 0px -10% 0px" });
233 els.forEach((e) => io.observe(e));
234 return () => io.disconnect();
235 }, []);
236
237 /* ───── smoke ───── */
238 useEffect(() => {
239 const cvs = cvsRef.current, hero = heroRef.current; if (!cvs || !hero) return;
240 const ctx = cvs.getContext("2d"); if (!ctx) return;
241 const rm = prefersReduced();
242 const SC = 0.5;
243 let W = 0, H = 0, N = 0;
244 let xs = new Float32Array(0), ys = new Float32Array(0), pxs = new Float32Array(0), pys = new Float32Array(0), life = new Float32Array(0), kind = new Uint8Array(0);
245 let mx = -9999, my = -9999, lx = -9999, ly = -9999, mvx = 0, mvy = 0, hot = 0;
246 let visible = true, raf = 0, last = performance.now(), T = 0;
247 const R = 120;
248
249 const spawn = (i: number, anywhere: boolean) => {
250 if (anywhere || Math.random() < 0.3) { xs[i] = Math.random() * W; ys[i] = Math.random() * H; }
251 else { xs[i] = W * 0.5 + (Math.random() + Math.random() + Math.random() - 1.5) * W * 0.32; ys[i] = H + 8; }
252 pxs[i] = xs[i]; pys[i] = ys[i];
253 life[i] = 220 + Math.random() * 380;
254 kind[i] = Math.random() < 0.16 ? 1 : 0;
255 };
256 const resize = () => {
257 W = Math.ceil(hero.clientWidth * SC); H = Math.ceil(hero.clientHeight * SC);
258 cvs.width = W; cvs.height = H;
259 ctx.fillStyle = "#070605"; ctx.fillRect(0, 0, W, H);
260 N = Math.round(clamp((W * H) / 520, 420, 1500));
261 xs = new Float32Array(N); ys = new Float32Array(N); pxs = new Float32Array(N); pys = new Float32Array(N); life = new Float32Array(N); kind = new Uint8Array(N);
262 for (let i = 0; i < N; i++) spawn(i, true);
263 };
264 const step = (dt: number, fade: number) => {
265 T += dt * 0.016;
266 ctx.globalCompositeOperation = "source-over";
267 ctx.fillStyle = `rgba(7,6,5,${fade})`; ctx.fillRect(0, 0, W, H);
268 ctx.globalCompositeOperation = "lighter";
269 ctx.lineWidth = 1.3; ctx.lineCap = "round";
270 const sp = Math.hypot(mvx, mvy);
271 // pointer injects fresh amber ink
272 if (hot > 0.02 && sp > 0.6) { for (let k = 0; k < 10; k++) { const i = (Math.random() * N) | 0; xs[i] = mx + (Math.random() - 0.5) * 16; ys[i] = my + (Math.random() - 0.5) * 16; pxs[i] = xs[i]; pys[i] = ys[i]; kind[i] = Math.random() < 0.55 ? 1 : 0; life[i] = 120 + Math.random() * 160; } }
273 for (let i = 0; i < N; i++) {
274 let x = xs[i], y = ys[i];
275 const a = vnoise(x * 0.0042 + T * 0.35, y * 0.0042 - T * 0.2) * 12.5 + vnoise(x * 0.011 - T * 0.5, y * 0.011 + T * 0.3) * 3;
276 let vx = Math.cos(a) * 0.85, vy = Math.sin(a) * 0.85 - 0.42;
277 let near = 0;
278 if (hot > 0.02) {
279 const dx = x - mx, dy = y - my, d2 = dx * dx + dy * dy;
280 if (d2 < R * R) {
281 const d = Math.sqrt(d2) + 0.001, f = (1 - d / R) * (1 - d / R) * hot;
282 vx += mvx * f * 0.45 - (dy / d) * f * 2.2; vy += mvy * f * 0.45 + (dx / d) * f * 2.2; near = f;
283 }
284 }
285 x += vx * dt; y += vy * dt;
286 const al = (kind[i] ? 0.2 : 0.15) * (1 + near * 1.4) * clamp(life[i] / 60);
287 ctx.strokeStyle = kind[i] ? `rgba(224,160,74,${al})` : `rgba(233,226,211,${al})`;
288 ctx.beginPath(); ctx.moveTo(pxs[i], pys[i]); ctx.lineTo(x, y); ctx.stroke();
289 pxs[i] = x; pys[i] = y; xs[i] = x; ys[i] = y;
290 life[i] -= dt;
291 if (life[i] <= 0 || x < -20 || x > W + 20 || y < -20 || y > H + 20) spawn(i, false);
292 }
293 mvx *= 0.9; mvy *= 0.9; hot *= 0.985;
294 };
295 const frame = (now: number) => {
296 raf = requestAnimationFrame(frame);
297 const dt = Math.min(2.2, (now - last) / 16.67); last = now;
298 if (!visible) return;
299 step(dt, 0.05);
300 };
301 const onMove = (e: PointerEvent) => {
302 const r = hero.getBoundingClientRect();
303 const x = (e.clientX - r.left) * SC, y = (e.clientY - r.top) * SC;
304 if (lx > -9000) { mvx = mvx * 0.5 + (x - lx) * 0.9; mvy = mvy * 0.5 + (y - ly) * 0.9; }
305 mx = x; my = y; lx = x; ly = y; hot = 1;
306 };
307 const onLeave = () => { lx = ly = -9999; };
308 resize();
309 if (rm) {
310 for (let k = 0; k < 340; k++) step(1, 0.03);
311 const onR = () => { resize(); for (let k = 0; k < 340; k++) step(1, 0.03); };
312 window.addEventListener("resize", onR);
313 return () => window.removeEventListener("resize", onR);
314 }
315 const io = typeof IntersectionObserver !== "undefined" ? new IntersectionObserver(([e]) => { visible = e.isIntersecting; }) : null;
316 io?.observe(hero);
317 hero.addEventListener("pointermove", onMove); hero.addEventListener("pointerleave", onLeave);
318 window.addEventListener("resize", resize);
319 raf = requestAnimationFrame(frame);
320 return () => { cancelAnimationFrame(raf); io?.disconnect(); hero.removeEventListener("pointermove", onMove); hero.removeEventListener("pointerleave", onLeave); window.removeEventListener("resize", resize); };
321 }, []);
322
323 /* ───── scroll choreography ───── */
324 useEffect(() => {
325 const rm = prefersReduced();
326 let raf = 0, cur = -1, solid = false;
327 const prog = (el: HTMLElement | null, vh: number) => { if (!el) return null; const r = el.getBoundingClientRect(); if (r.bottom < -80 || r.top > vh + 80) return null; return clamp(-r.top / Math.max(1, r.height - vh)); };
328 const WN = words.length;
329 const frame = () => {
330 raf = requestAnimationFrame(frame);
331 const vh = window.innerHeight, vw = window.innerWidth, sy = window.scrollY;
332 const sol = sy > 40; if (sol !== solid) { solid = sol; setNavSolid(sol); }
333 if (heroTxtRef.current) { const u = clamp(sy / (vh * 0.9)); heroTxtRef.current.style.transform = `translate3d(0,${(u * 140).toFixed(1)}px,0)`; heroTxtRef.current.style.opacity = String(1 - u * 0.9); }
334 const pm = prog(manRef.current, vh);
335 if (pm !== null) wordRefs.current.forEach((w, i) => { if (!w) return; const s = (i / WN) * 0.8 + 0.06; const u = clamp((pm - s) / 0.06); w.style.opacity = String(0.13 + 0.87 * u); w.style.transform = `translate3d(0,${((1 - u) * 14).toFixed(1)}px,0)`; });
336 const pb = prog(botRef.current, vh);
337 if (pb !== null) {
338 const x = -150 + pb * 640;
339 sweepRef.current?.setAttribute("transform", `translate(${x.toFixed(1)} 0) skewX(-16)`);
340 sweep2Ref.current?.setAttribute("transform", `translate(${(x - 135).toFixed(1)} 0) skewX(-16)`);
341 glowRef.current?.setAttribute("opacity", (0.2 + 0.55 * Math.sin(pb * Math.PI)).toFixed(3));
342 tiltRef.current?.setAttribute("transform", `rotate(${(-3 + pb * 6).toFixed(2)} 200 590)`);
343 [0.18, 0.5, 0.82].forEach((c, i) => { const el = capRefs.current[i]; if (el) { const o = clamp(1.7 - Math.abs(pb - c) / 0.1); el.style.opacity = String(o); el.style.transform = `translate3d(0,${((1 - o) * 24).toFixed(1)}px,0)`; } });
344 if (bgWordRef.current) bgWordRef.current.style.transform = `translate3d(${(-pb * 26).toFixed(2)}vw,0,0)`;
345 }
346 const pn = prog(notesRef.current, vh);
347 if (pn !== null && trackRef.current) {
348 const pos = pn * 2;
349 trackRef.current.style.transform = `translate3d(${(-pos * vw).toFixed(1)}px,0,0)`;
350 atmRefs.current.forEach((el, i) => { if (el) el.style.opacity = String(clamp(1.15 - Math.abs(pos - i) * 1.1)); });
351 panelRefs.current.forEach((el, i) => {
352 if (!el) return; const off = i - pos;
353 const word = el.querySelector<HTMLElement>(".nr-gw"), body = el.querySelector<HTMLElement>(".nr-pbody");
354 if (word) word.style.transform = `translate3d(${(off * vw * 0.32).toFixed(1)}px,0,0)`;
355 if (body) { body.style.transform = `translate3d(${(off * vw * 0.1).toFixed(1)}px,0,0)`; body.style.opacity = String(clamp(1.25 - Math.abs(off) * 1.3)); }
356 });
357 if (fillRef.current) fillRef.current.style.transform = `scaleX(${pn.toFixed(3)})`;
358 const n = Math.round(pos); if (n !== cur) { cur = n; setNote(n); }
359 }
360 const pq = prog(quizRef.current, vh);
361 if (pq !== null) qringRef.current?.setAttribute("transform", `rotate(${(pq * 140).toFixed(1)})`);
362 };
363 if (rm) {
364 wordRefs.current.forEach((w) => { if (w) w.style.opacity = "1"; });
365 return;
366 }
367 raf = requestAnimationFrame(frame);
368 return () => cancelAnimationFrame(raf);
369 }, [words.length]);
370
371 /* result animation trigger */
372 useEffect(() => {
373 if (step < QUIZ.length) { setGo(false); return; }
374 const id = window.setTimeout(() => setGo(true), 80);
375 return () => window.clearTimeout(id);
376 }, [step]);
377
378 const scent = (() => {
379 if (step < QUIZ.length) return SCENTS[0];
380 const tot = [0, 0, 0, 0]; picks.forEach((p) => p.forEach((v, i) => { tot[i] += v; }));
381 let b = 0; tot.forEach((v, i) => { if (v > tot[b]) b = i; });
382 return SCENTS[b];
383 })();
384 const choose = (w: number[]) => { setPicks((p) => [...p, w]); setStep((s) => s + 1); };
385 const back = () => { setPicks((p) => p.slice(0, -1)); setStep((s) => Math.max(0, s - 1)); };
386 const retake = () => { setPicks([]); setStep(0); setReserved(false); };
387
388 const goTo = (id: string) => (e: { preventDefault: () => void }) => {
389 e.preventDefault();
390 document.getElementById(id)?.scrollIntoView({ behavior: prefersReduced() ? "auto" : "smooth", block: "start" });
391 };
392
393 return (
394 <div ref={rootRef} className="nr" id="nr-top">
395 <style>{CSS}</style>
396 <Defs />
397 <div className="nr-curtain" aria-hidden="true" />
398
399 <header className={"nr-nav" + (navSolid ? " solid" : "")}>
400 <a href="#nr-top" className="nr-logo" onClick={goTo("nr-top")}>Vérane</a>
401 <nav className="nr-nav-links" aria-label="Primary">
402 <a href="#nr-flacon" onClick={goTo("nr-flacon")}>The flacon</a>
403 <a href="#nr-notes" onClick={goTo("nr-notes")}>Notes</a>
404 <a href="#nr-quiz" onClick={goTo("nr-quiz")}>Find your scent</a>
405 <a href="#nr-collection" onClick={goTo("nr-collection")}>Collection</a>
406 </nav>
407 <a href="#nr-collection" onClick={goTo("nr-collection")} className="nr-nav-bag">Boutique<span>0</span></a>
408 </header>
409
410 {/* ───────────── hero ───────────── */}
411 <section className="nr-hero" ref={heroRef} aria-label="Nox Ambre, Eau de Parfum">
412 <canvas ref={cvsRef} className="nr-smoke" aria-hidden="true" />
413 <div className="nr-vig" aria-hidden="true" />
414 <div className="nr-hero-in" ref={heroTxtRef}>
415 <div className="nr-kicker">Maison de parfum · Grasse — Paris · Nº 19</div>
416 <h1 aria-label="Smoke and Amber">
417 <span className="nr-ln" aria-hidden="true">{"Smoke".split("").map((c, i) => <span key={i} style={sv({ "--i": i })}>{c}</span>)}</span>
418 <span className="nr-ln amber2" aria-hidden="true">{"& Amber".split("").map((c, i) => <span key={i} style={sv({ "--i": i + 6 })}>{c === " " ? " " : c}</span>)}</span>
419 </h1>
420 </div>
421 <div className="nr-hero-foot">
422 <div className="nr-hero-meta">
423 <b>Nox Ambre</b>
424 <span>Eau de Parfum · 50 ml</span>
425 <a href="#nr-quiz" onClick={goTo("nr-quiz")}>Discover your scent →</a>
426 </div>
427 <div className="nr-hero-hint"><i />Move through the smoke</div>
428 </div>
429 </section>
430
431 {/* ───────────── manifesto ───────────── */}
432 <section className="nr-man" ref={manRef} aria-label="Manifesto">
433 <div className="nr-man-stick">
434 <div className="nr-label">Manifeste</div>
435 <p className="nr-man-p" aria-label={MANIFESTO}>
436 {words.map((w, i) => (
437 <span key={i} aria-hidden="true" ref={(el) => { wordRefs.current[i] = el; }} className={AMBER_WORDS.has(w.replace(/[.,—]/g, "")) ? "am" : ""}>{w}</span>
438 ))}
439 </p>
440 </div>
441 </section>
442
443 {/* ───────────── flacon ───────────── */}
444 <section className="nr-bot" id="nr-flacon" ref={botRef}>
445 <div className="nr-bot-stick">
446 <div className="nr-bgword" ref={bgWordRef} aria-hidden="true">Le Flacon</div>
447 <Flacon big className="nr-flacon" sweepRef={(el) => { sweepRef.current = el; }} sweep2Ref={(el) => { sweep2Ref.current = el; }} glowRef={(el) => { glowRef.current = el; }} tiltRef={(el) => { tiltRef.current = el; }} />
448 {[
449 ["01", "The glass", "Hand-blown in the Vallée de la Bresle. 480 grams of lead-free crystal, so thick it bends the light before it reaches the juice.", "l t"],
450 ["02", "The juice", "Thirty-two percent absolute, macerated twelve weeks on oak. Nothing added to make it louder. Only time.", "r m"],
451 ["03", "The cap", "Smoked oak, turned by one family in the Jura, finished with a ring of brushed amber brass.", "l b"],
452 ].map(([n, t, b, pos], i) => (
453 <div key={n} className={"nr-cap " + pos} ref={(el) => { capRefs.current[i] = el; }}>
454 <span>{n}</span><h3>{t}</h3><p>{b}</p>
455 </div>
456 ))}
457 </div>
458 </section>
459
460 {/* ───────────── notes ───────────── */}
461 <section className="nr-notes" id="nr-notes" ref={notesRef} aria-label="Notes: top, heart and base">
462 <div className="nr-notes-stick">
463 {NOTES.map((n, i) => (
464 <div key={n.k} className="nr-atm" ref={(el) => { atmRefs.current[i] = el; }} style={sv({ "--atm": n.atm })} aria-hidden="true" />
465 ))}
466 <div className="nr-track" ref={trackRef}>
467 {NOTES.map((n, i) => (
468 <div key={n.k} className="nr-panel" ref={(el) => { panelRefs.current[i] = el; }} style={sv({ "--atm": n.atm })}>
469 <div className="nr-gw" aria-hidden="true">{n.word}</div>
470 <div className="nr-pbody">
471 <div className="nr-pk"><b>{n.n}</b><span>{n.when}</span></div>
472 <h2>{n.k} <em>notes</em></h2>
473 <p className="nr-pline">{n.line}</p>
474 <ul>
475 {n.list.map(([a, d]) => (<li key={a}><span>{a}</span><small>{d}</small></li>))}
476 </ul>
477 </div>
478 <div className="nr-orb" aria-hidden="true"><i /><i /><i /></div>
479 </div>
480 ))}
481 </div>
482 <div className="nr-tl" aria-hidden="true">
483 <div className="nr-tl-bar"><span ref={fillRef} /></div>
484 <div className="nr-tl-labels">{NOTES.map((n, i) => <span key={n.k} className={i === note ? "on" : ""}>{n.k} · {n.when}</span>)}</div>
485 </div>
486 </div>
487 </section>
488
489 {/* ───────────── quiz ───────────── */}
490 <section className="nr-quiz" id="nr-quiz" ref={quizRef}>
491 <div className="nr-quiz-stick">
492 <svg className="nr-qring" viewBox="-400 -400 800 800" aria-hidden="true">
493 <g ref={qringRef}>
494 {[380, 300, 220].map((r, i) => <circle key={r} r={r} fill="none" stroke="#E9E2D3" strokeOpacity={0.1 - i * 0.02} strokeDasharray={i === 1 ? "2 10" : "none"} />)}
495 <circle cx="380" cy="0" r="5" fill="#E0A04A" /><circle cx="-220" cy="0" r="3" fill="#E9E2D3" />
496 </g>
497 </svg>
498 <div className="nr-quiz-in">
499 {step < QUIZ.length ? (
500 <div key={step} className="nr-qstep">
501 <div className="nr-label">Discover your scent · 0{step + 1} / 0{QUIZ.length}</div>
502 <h2 className="nr-qh">{QUIZ[step].q}</h2>
503 <div className="nr-opts">
504 {QUIZ[step].o.map(([t, w], i) => (
505 <button key={t} type="button" className="nr-opt" onClick={() => choose(w)}>
506 <small>{["I", "II", "III", "IV"][i]}</small><span>{t}</span>
507 </button>
508 ))}
509 </div>
510 <div className="nr-qnav">
511 <div className="dots" aria-hidden="true">{QUIZ.map((_, i) => <i key={i} className={i <= step ? "on" : ""} />)}</div>
512 {step > 0 && <button type="button" onClick={back} className="nr-linkbtn">← Back</button>}
513 </div>
514 </div>
515 ) : (
516 <div className={"nr-res" + (go ? " go" : "")} role="status" aria-live="polite">
517 <div className="nr-res-l">
518 <div className="nr-label">Your signature · Nº {scent.num}</div>
519 <h2 className="nr-res-name" aria-label={scent.name}>
520 {scent.name.split("").map((c, i) => <span key={i} aria-hidden="true" style={sv({ "--i": i })}>{c === " " ? " " : c}</span>)}
521 </h2>
522 <p className="nr-res-d">{scent.desc}</p>
523 <div className="nr-bars">
524 {["Warmth", "Sillage", "Longevity"].map((l, i) => (
525 <div key={l}><span>{l}</span><div><i style={sv({ "--w": scent.bars[i] + "%", "--k": i })} /></div><b>{scent.bars[i]}</b></div>
526 ))}
527 </div>
528 <div className="nr-res-notes">{["Top", "Heart", "Base"].map((l, i) => <div key={l}><small>{l}</small><span>{scent.notes[i]}</span></div>)}</div>
529 <div className="nr-res-cta">
530 <button type="button" className="nr-btn" onClick={() => setReserved(true)} disabled={reserved}>{reserved ? "Sample reserved ✓" : "Reserve a 2 ml sample"}</button>
531 <button type="button" className="nr-linkbtn" onClick={retake}>Retake the quiz</button>
532 </div>
533 </div>
534 <div className="nr-res-r" style={sv({ "--tint": scent.tint })}>
535 <div className="nr-res-halo" aria-hidden="true" />
536 <Flacon tint={scent.tint} level={go ? 1 : 0.02} className="nr-res-bottle" />
537 </div>
538 </div>
539 )}
540 </div>
541 </div>
542 </section>
543
544 {/* ───────────── collection ───────────── */}
545 <section className="nr-coll" id="nr-collection">
546 <div className="nr-wrap">
547 <div className="nr-label" data-rv>The collection</div>
548 <h2 className="nr-h2" data-rv>Four ways to <em>disappear.</em></h2>
549 <ul className="nr-rows">
550 {COLLECTION.map((c, i) => (
551 <li key={c.n} data-rv style={sv({ "--d": i * 0.08 + "s", "--tint": c.tint })}>
552 <a href="#" onClick={NOOP} className="nr-row">
553 <span className="no">{c.no}</span>
554 <span className="nm">{c.n}</span>
555 <span className="nt">{c.notes}</span>
556 <span className="pr">{c.price}<small>50 ml</small></span>
557 <span className="bt" aria-hidden="true"><Flacon tint={c.tint} /></span>
558 </a>
559 </li>
560 ))}
561 </ul>
562 </div>
563 </section>
564
565 {/* ───────────── footer ───────────── */}
566 <footer className="nr-foot">
567 <div className="nr-wrap nr-foot-grid">
568 <div data-rv>
569 <div className="nr-label">The letter</div>
570 <p className="nr-foot-p">One letter a season. New accords, rare restocks, nothing else.</p>
571 <form className="nr-sub" onSubmit={(e: FormEvent) => { e.preventDefault(); if (email.includes("@")) setJoined(true); }}>
572 {joined ? <div className="nr-thanks">Merci. The next letter finds you in spring.</div> : (
573 <>
574 <label className="nr-sr" htmlFor="nr-email">Email</label>
575 <input id="nr-email" type="email" required placeholder="your@email.com" value={email} onChange={(e) => setEmail(e.target.value)} />
576 <button type="submit">Subscribe</button>
577 </>
578 )}
579 </form>
580 </div>
581 <div data-rv style={sv({ "--d": ".1s" })}>
582 <div className="nr-label">Maison</div>
583 <address>14 rue du Faubourg Saint-Honoré<br />75008 Paris<br />Tue – Sat, 11 – 19h</address>
584 </div>
585 <div data-rv style={sv({ "--d": ".2s" })}>
586 <div className="nr-label">House</div>
587 <ul className="nr-foot-links">{["Our story", "The atelier in Grasse", "Refills", "Stockists", "Contact"].map((l) => <li key={l}><a href="#" onClick={NOOP}>{l}</a></li>)}</ul>
588 </div>
589 </div>
590 <div className="nr-giant" aria-hidden="true">Vérane</div>
591 <div className="nr-wrap nr-legal"><span>© 2026 Maison Vérane SAS</span><span>Privacy · Terms · Shipping</span></div>
592 </footer>
593 </div>
594 );
595}
596
597export default Page;
598
599/* ------------------------------------------------------------------ */
600/* Styles */
601/* ------------------------------------------------------------------ */
602const CSS = `
603.nr{--nr-black:#070605;--nr-ink:#0E0C0A;--nr-bone:#E9E2D3;--nr-mute:#A8A196;--nr-amber:#E0A04A;--nr-line:rgba(233,226,211,.14);
604--nr-serif:"Cormorant Garamond","Playfair Display","Iowan Old Style","Palatino Linotype",Palatino,"Book Antiqua",Georgia,serif;
605--nr-sans:"Helvetica Neue","Inter",ui-sans-serif,system-ui,-apple-system,"Segoe UI",Arial,sans-serif;
606--nr-out:cubic-bezier(.22,1,.36,1);
607position:relative;box-sizing:border-box;width:100%;background:var(--nr-black);color:var(--nr-bone);font-family:var(--nr-sans);font-size:16px;line-height:1.6;font-weight:300;text-align:left;overflow-x:clip;-webkit-font-smoothing:antialiased}
608.nr *,.nr *::before,.nr *::after{box-sizing:border-box}
609:where(.nr) :where(h1,h2,h3,p,ul,li,form,address){margin:0;padding:0;font:inherit;letter-spacing:inherit;font-style:normal}
610:where(.nr) :where(ul){list-style:none}
611:where(.nr) :where(a){color:inherit;text-decoration:none}
612:where(.nr) :where(button,input){font:inherit;color:inherit;margin:0;border:0;background:none;padding:0;appearance:none;-webkit-appearance:none;border-radius:0;text-align:inherit}
613:where(.nr) :where(button){cursor:pointer}
614.nr :focus-visible{outline:1px solid var(--nr-amber);outline-offset:4px}
615:where(.nr) :where(svg){display:block;max-width:100%}
616.nr .nr-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
617.nr-wrap{width:100%;max-width:1280px;margin:0 auto;padding:0 48px}
618.nr-label{font-family:var(--nr-sans);font-size:11px;letter-spacing:.34em;text-transform:uppercase;color:var(--nr-amber);font-weight:400}
619.nr-h2{margin-top:22px;font-family:var(--nr-serif);font-style:italic;font-weight:400;font-size:clamp(3rem,9vw,8.4rem);line-height:.92;letter-spacing:-.025em}
620.nr-h2 em{color:var(--nr-amber)}
621.nr-btn{display:inline-flex;align-items:center;justify-content:center;height:54px;padding:0 34px;border:1px solid var(--nr-amber);color:var(--nr-black);background:var(--nr-amber);font-size:12px;letter-spacing:.26em;text-transform:uppercase;font-weight:500;transition:background .4s,color .4s,transform .5s var(--nr-out)}
622.nr-btn:hover:not(:disabled){background:transparent;color:var(--nr-amber)}
623.nr-btn:disabled{background:transparent;color:var(--nr-amber);cursor:default}
624.nr-linkbtn{font-size:12px;letter-spacing:.22em;text-transform:uppercase;color:var(--nr-mute);border-bottom:1px solid transparent;transition:color .3s,border-color .3s;padding:4px 0}
625.nr-linkbtn:hover{color:var(--nr-bone);border-color:var(--nr-bone)}
626
627.nr-curtain{position:fixed;inset:0;z-index:100;background:var(--nr-black);pointer-events:none;animation:nrCurtain 1.8s .15s var(--nr-out) forwards}
628@keyframes nrCurtain{to{opacity:0;visibility:hidden}}
629
630/* nav */
631.nr-nav{position:fixed;z-index:60;top:0;left:0;right:0;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;padding:22px 48px;transition:background .5s,padding .5s;mix-blend-mode:normal}
632.nr-nav.solid{background:rgba(7,6,5,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);padding-top:14px;padding-bottom:14px;border-bottom:1px solid var(--nr-line)}
633.nr-logo{font-family:var(--nr-serif);font-size:26px;letter-spacing:.34em;text-transform:uppercase;font-weight:500}
634.nr-nav-links{display:flex;gap:38px;font-size:11px;letter-spacing:.26em;text-transform:uppercase}
635.nr-nav-links a,.nr-nav-bag{opacity:.78;transition:opacity .3s,color .3s}.nr-nav-links a:hover,.nr-nav-bag:hover{opacity:1;color:var(--nr-amber)}
636.nr-nav-bag{justify-self:end;font-size:11px;letter-spacing:.26em;text-transform:uppercase;display:inline-flex;gap:10px;align-items:center}
637.nr-nav-bag span{width:22px;height:22px;border:1px solid var(--nr-line);border-radius:50%;display:grid;place-items:center;font-size:10px;letter-spacing:0}
638
639/* hero */
640.nr-hero{position:relative;height:100vh;min-height:620px;overflow:hidden;background:var(--nr-black);cursor:crosshair}
641.nr-smoke{position:absolute;inset:0;width:100%;height:100%}
642.nr-vig{position:absolute;inset:0;pointer-events:none;background:radial-gradient(70% 45% at 50% 105%,rgba(224,160,74,.16),transparent 70%),radial-gradient(75% 70% at 50% 50%,transparent 35%,rgba(7,6,5,.8) 100%),linear-gradient(180deg,rgba(7,6,5,.55),transparent 22%,transparent 70%,rgba(7,6,5,.8))}
643.nr-hero-in{position:absolute;left:0;right:0;bottom:15vh;padding:0 48px;pointer-events:none;will-change:transform,opacity}
644.nr-kicker{font-size:11px;letter-spacing:.34em;text-transform:uppercase;color:var(--nr-mute);margin-bottom:2vh;opacity:0;animation:nrFade 1.4s 1.6s var(--nr-out) forwards}
645.nr-hero h1{font-family:var(--nr-serif);font-style:italic;font-weight:300;font-size:clamp(5.2rem,21vw,21rem);line-height:.8;letter-spacing:-.04em;color:var(--nr-bone)}
646.nr-ln{display:block;overflow:hidden;padding:.06em .1em .12em 0;margin-right:-.1em}
647.nr-ln span{display:inline-block;transform:translate3d(0,112%,0);animation:nrRise 1.5s var(--nr-out) calc(var(--i) * 70ms + 1s) forwards}
648.nr-ln.amber2{color:var(--nr-amber);padding-left:.3em}
649@keyframes nrRise{to{transform:none}}
650@keyframes nrFade{to{opacity:1}}
651.nr-hero-foot{position:absolute;left:0;right:0;bottom:0;display:flex;justify-content:space-between;align-items:flex-end;padding:0 48px 34px;gap:20px;opacity:0;animation:nrFade 1.4s 2.1s var(--nr-out) forwards;pointer-events:none}
652.nr-hero-meta{display:grid;gap:4px;font-size:12px;letter-spacing:.2em;text-transform:uppercase;color:var(--nr-mute);pointer-events:auto}
653.nr-hero-meta b{font-family:var(--nr-serif);font-style:italic;font-weight:400;font-size:28px;letter-spacing:0;text-transform:none;color:var(--nr-bone)}
654.nr-hero-meta a{margin-top:8px;color:var(--nr-amber);transition:letter-spacing .4s var(--nr-out)}.nr-hero-meta a:hover{letter-spacing:.3em}
655.nr-hero-hint{display:flex;align-items:center;gap:14px;font-size:11px;letter-spacing:.3em;text-transform:uppercase;color:var(--nr-mute)}
656.nr-hero-hint i{width:46px;height:1px;background:var(--nr-amber);transform-origin:left;animation:nrLine 2.6s ease-in-out infinite}
657@keyframes nrLine{50%{transform:scaleX(.2)}}
658
659.nr.nr-js [data-rv]{opacity:0;transform:translate3d(0,40px,0);transition:opacity 1.2s var(--nr-out) var(--d,0s),transform 1.2s var(--nr-out) var(--d,0s)}
660.nr.nr-js [data-rv].in{opacity:1;transform:none}
661
662/* manifesto */
663.nr-man{position:relative;height:300vh;background:var(--nr-black)}
664.nr-man-stick{position:sticky;top:0;height:100vh;display:flex;flex-direction:column;justify-content:center;padding:0 48px;max-width:1380px;margin:0 auto}
665.nr-man-p{margin-top:28px;font-family:var(--nr-serif);font-style:italic;font-weight:400;font-size:clamp(2.1rem,6.4vw,6.4rem);line-height:1.02;letter-spacing:-.025em}
666.nr-man-p span{display:inline-block;margin-right:.26em;opacity:.13;will-change:opacity,transform}
667.nr-man-p span.am{color:var(--nr-amber)}
668
669/* flacon */
670.nr-bot{position:relative;height:360vh;background:radial-gradient(60% 50% at 50% 55%,#15110c,var(--nr-black) 70%)}
671.nr-bot-stick{position:sticky;top:0;height:100vh;overflow:hidden;display:grid;place-items:center}
672.nr-bgword{position:absolute;left:0;top:50%;margin-top:-.5em;white-space:nowrap;font-family:var(--nr-serif);font-style:italic;font-size:clamp(10rem,36vw,36rem);line-height:1;color:transparent;-webkit-text-stroke:1px rgba(233,226,211,.13);padding-left:12vw;will-change:transform}
673.nr-flacon{position:relative;height:min(82vh,760px);width:auto;aspect-ratio:400/640;font-family:var(--nr-serif)}
674.nr-cap{position:absolute;max-width:320px;opacity:0;will-change:opacity,transform}
675.nr-cap.l{left:max(48px,6vw)}.nr-cap.r{right:max(48px,6vw)}
676.nr-cap.t{top:20vh}.nr-cap.m{top:42vh}.nr-cap.b{bottom:15vh}
677.nr-cap span{font-size:11px;letter-spacing:.34em;color:var(--nr-amber)}
678.nr-cap h3{margin-top:10px;font-family:var(--nr-serif);font-style:italic;font-weight:400;font-size:clamp(2rem,3.4vw,3.2rem);line-height:1}
679.nr-cap p{margin-top:14px;color:var(--nr-mute);font-size:15px;line-height:1.65}
680.nr-cap.r{text-align:right}
681
682/* notes */
683.nr-notes{position:relative;height:420vh;background:var(--nr-black)}
684.nr-notes-stick{position:sticky;top:0;height:100vh;overflow:hidden}
685.nr-atm{position:absolute;inset:0;opacity:0;background:radial-gradient(70% 80% at 72% 55%,color-mix(in srgb,var(--atm) 30%,transparent),transparent 70%),radial-gradient(60% 60% at 10% 100%,color-mix(in srgb,var(--atm) 16%,transparent),transparent 70%)}
686.nr-atm:first-child{opacity:1}
687.nr-track{position:absolute;left:0;top:0;height:100%;width:300vw;display:flex;will-change:transform}
688.nr-panel{position:relative;flex:0 0 100vw;height:100%;overflow:hidden;display:flex;align-items:center;padding:0 7vw}
689.nr-gw{position:absolute;left:0;bottom:-.12em;font-family:var(--nr-serif);font-style:italic;font-weight:300;font-size:clamp(11rem,38vw,40rem);line-height:.8;letter-spacing:-.05em;color:color-mix(in srgb,var(--atm) 20%,transparent);white-space:nowrap;padding-left:3vw;will-change:transform}
690.nr-pbody{position:relative;z-index:2;max-width:640px;will-change:transform,opacity}
691.nr-pk{display:flex;align-items:center;gap:18px;font-size:11px;letter-spacing:.3em;text-transform:uppercase;color:var(--atm)}
692.nr-pk b{font-family:var(--nr-serif);font-style:italic;font-weight:400;font-size:28px;letter-spacing:0}
693.nr-panel h2{margin-top:14px;font-family:var(--nr-serif);font-style:italic;font-weight:400;font-size:clamp(3.4rem,9vw,8rem);line-height:.9;letter-spacing:-.03em}
694.nr-panel h2 em{color:var(--atm)}
695.nr-pline{margin-top:20px;max-width:28em;font-family:var(--nr-serif);font-style:italic;font-size:clamp(1.2rem,2vw,1.7rem);line-height:1.3;color:var(--nr-bone);opacity:.85}
696.nr-panel ul{margin-top:32px;border-top:1px solid var(--nr-line)}
697.nr-panel li{display:flex;justify-content:space-between;align-items:baseline;gap:20px;padding:15px 0;border-bottom:1px solid var(--nr-line)}
698.nr-panel li span{font-family:var(--nr-serif);font-size:clamp(1.3rem,2vw,1.75rem);color:var(--nr-bone)}
699.nr-panel li small{font-size:12px;letter-spacing:.06em;color:var(--nr-mute);text-align:right}
700.nr-orb{position:absolute;right:8vw;top:50%;width:min(36vw,520px);aspect-ratio:1;margin-top:calc(min(36vw,520px) / -2);pointer-events:none}
701.nr-orb i{position:absolute;border-radius:50%;filter:blur(36px);background:radial-gradient(circle,color-mix(in srgb,var(--atm) 55%,transparent),transparent 70%);animation:nrDrift 14s ease-in-out infinite}
702.nr-orb i:nth-child(1){inset:8%}.nr-orb i:nth-child(2){inset:30% 0 0 34%;animation-delay:-5s;animation-duration:18s}.nr-orb i:nth-child(3){inset:0 38% 42% 0;animation-delay:-9s;opacity:.6}
703@keyframes nrDrift{50%{transform:translate3d(8%,-6%,0) scale(1.15)}}
704.nr-tl{position:absolute;left:7vw;right:7vw;bottom:34px;z-index:3}
705.nr-tl-bar{height:1px;background:var(--nr-line)}
706.nr-tl-bar span{display:block;height:1px;background:var(--nr-amber);transform:scaleX(0);transform-origin:left}
707.nr-tl-labels{display:flex;justify-content:space-between;margin-top:14px;font-size:10px;letter-spacing:.24em;text-transform:uppercase;color:#6f6a62}
708.nr-tl-labels .on{color:var(--nr-amber)}
709
710/* quiz */
711.nr-quiz{position:relative;height:190vh;background:var(--nr-black);border-top:1px solid var(--nr-line)}
712.nr-quiz-stick{position:sticky;top:0;height:100vh;overflow:hidden;display:flex;align-items:center}
713.nr-qring{position:absolute;left:50%;top:50%;width:max(130vh,900px);transform:translate(-50%,-50%);pointer-events:none}
714.nr-quiz-in{position:relative;width:100%;max-width:1280px;margin:0 auto;padding:0 48px}
715.nr-qstep{animation:nrStep .9s var(--nr-out)}
716@keyframes nrStep{from{opacity:0;transform:translate3d(0,24px,0)}}
717.nr-qh{margin-top:24px;max-width:11em;font-family:var(--nr-serif);font-style:italic;font-weight:400;font-size:clamp(2.8rem,7vw,6.6rem);line-height:.96;letter-spacing:-.03em}
718.nr-opts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 48px;margin-top:44px;max-width:980px}
719.nr-opt{display:flex;align-items:baseline;gap:20px;padding:20px 0;border-top:1px solid var(--nr-line);font-family:var(--nr-serif);font-size:clamp(1.3rem,2.2vw,2rem);line-height:1.15;transition:padding .5s var(--nr-out),color .3s,border-color .3s}
720.nr-opt small{font-family:var(--nr-sans);font-size:11px;letter-spacing:.2em;color:var(--nr-amber);min-width:2.2em}
721.nr-opt:hover,.nr-opt:focus-visible{padding-left:18px;color:var(--nr-amber);border-color:var(--nr-amber)}
722.nr-qnav{display:flex;align-items:center;gap:30px;margin-top:34px}
723.nr-qnav .dots{display:flex;gap:8px}.nr-qnav .dots i{width:26px;height:1px;background:var(--nr-line)}.nr-qnav .dots i.on{background:var(--nr-amber)}
724.nr-res{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(0,.75fr);gap:5vw;align-items:center}
725.nr-res-name{margin-top:14px;font-family:var(--nr-serif);font-style:italic;font-weight:400;font-size:clamp(3.4rem,8vw,7.6rem);line-height:.9;letter-spacing:-.03em;color:var(--nr-bone)}
726.nr-res-name span{display:inline-block;opacity:0;transform:translate3d(0,40%,0) rotate(4deg);transition:opacity 1s var(--nr-out) calc(var(--i) * 55ms),transform 1.2s var(--nr-out) calc(var(--i) * 55ms)}
727.nr-res.go .nr-res-name span{opacity:1;transform:none}
728.nr-res-d{margin-top:18px;max-width:34em;color:var(--nr-mute);font-size:15.5px;opacity:0;transition:opacity 1s .5s}
729.nr-res.go .nr-res-d,.nr-res.go .nr-res-notes,.nr-res.go .nr-res-cta{opacity:1}
730.nr-bars{margin-top:24px;display:grid;gap:10px;max-width:440px}
731.nr-bars>div{display:grid;grid-template-columns:90px 1fr 28px;align-items:center;gap:14px;font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--nr-mute)}
732.nr-bars>div>div{height:1px;background:var(--nr-line);position:relative}
733.nr-bars i{position:absolute;left:0;top:-1px;height:3px;width:var(--w);background:var(--nr-amber);transform:scaleX(0);transform-origin:left;transition:transform 1.8s var(--nr-out) calc(.5s + var(--k) * .15s)}
734.nr-res.go .nr-bars i{transform:scaleX(1)}
735.nr-bars b{font-weight:400;color:var(--nr-bone);letter-spacing:0;text-align:right}
736.nr-res-notes{margin-top:22px;display:grid;grid-template-columns:repeat(3,1fr);gap:18px;max-width:640px;opacity:0;transition:opacity 1s .9s}
737.nr-res-notes small{display:block;font-size:10px;letter-spacing:.3em;text-transform:uppercase;color:var(--nr-amber);margin-bottom:4px}
738.nr-res-notes span{font-family:var(--nr-serif);font-style:italic;font-size:1.1rem;line-height:1.2;color:var(--nr-bone)}
739.nr-res-cta{margin-top:26px;display:flex;align-items:center;gap:28px;flex-wrap:wrap;opacity:0;transition:opacity 1s 1.1s}
740.nr-res-r{position:relative;justify-self:center;width:100%;display:grid;place-items:center}
741.nr-res-halo{position:absolute;width:120%;aspect-ratio:1;border-radius:50%;background:radial-gradient(circle,color-mix(in srgb,var(--tint) 40%,transparent),transparent 65%);filter:blur(10px)}
742.nr-res-bottle{position:relative;height:min(56vh,520px);width:auto;aspect-ratio:400/640}
743.nr .nr-liq{transition:transform 2.4s var(--nr-out)}
744
745/* collection */
746.nr-coll{padding:170px 0 120px;background:var(--nr-black);border-top:1px solid var(--nr-line)}
747.nr-rows{margin-top:70px;border-top:1px solid var(--nr-line)}
748.nr-row{position:relative;display:grid;grid-template-columns:80px minmax(0,1.3fr) minmax(0,1.2fr) 150px;align-items:center;gap:24px;padding:34px 0;border-bottom:1px solid var(--nr-line);transition:padding .6s var(--nr-out)}
749.nr-row .no{font-size:11px;letter-spacing:.3em;color:var(--nr-amber)}
750.nr-row .nm{font-family:var(--nr-serif);font-style:italic;font-size:clamp(2.4rem,6.4vw,6rem);line-height:.95;letter-spacing:-.03em;transition:color .4s,transform .7s var(--nr-out)}
751.nr-row .nt{font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--nr-mute)}
752.nr-row .pr{text-align:right;font-family:var(--nr-serif);font-size:1.7rem}
753.nr-row .pr small{display:block;font-family:var(--nr-sans);font-size:10px;letter-spacing:.24em;color:var(--nr-mute);text-transform:uppercase}
754.nr-row .bt{position:absolute;right:190px;top:50%;width:62px;margin-top:-52px;opacity:0;transform:translate3d(0,24px,0) rotate(6deg);transition:opacity .5s,transform .8s var(--nr-out);pointer-events:none}
755.nr-row:hover .nm,.nr-row:focus-visible .nm{color:var(--tint);transform:translate3d(18px,0,0)}
756.nr-row:hover .bt,.nr-row:focus-visible .bt{opacity:1;transform:none}
757
758/* footer */
759.nr-foot{position:relative;padding:110px 0 0;background:var(--nr-black);border-top:1px solid var(--nr-line);overflow:hidden}
760.nr-foot-grid{display:grid;grid-template-columns:1.5fr 1fr 1fr;gap:60px}
761.nr-foot-p{margin-top:16px;font-family:var(--nr-serif);font-style:italic;font-size:1.7rem;line-height:1.2;max-width:15em}
762.nr-sub{display:flex;margin-top:26px;max-width:440px;border-bottom:1px solid var(--nr-bone)}
763.nr-sub input{flex:1;min-width:0;height:48px;font-size:15px}.nr-sub input::placeholder{color:#6f6a62}
764.nr-sub button{font-size:11px;letter-spacing:.26em;text-transform:uppercase;color:var(--nr-amber);padding-left:16px}
765.nr-thanks{height:48px;display:flex;align-items:center;color:var(--nr-amber);font-family:var(--nr-serif);font-style:italic;font-size:1.2rem}
766.nr-foot address{margin-top:16px;color:var(--nr-mute);line-height:1.9}
767.nr-foot-links{margin-top:16px;display:grid;gap:6px}.nr-foot-links a{color:var(--nr-mute);transition:color .3s}.nr-foot-links a:hover{color:var(--nr-amber)}
768.nr-giant{margin-top:70px;text-align:center;font-family:var(--nr-serif);font-style:italic;font-weight:300;font-size:clamp(7rem,30vw,32rem);line-height:.74;letter-spacing:-.05em;white-space:nowrap;color:transparent;-webkit-text-stroke:1px rgba(233,226,211,.22);background:linear-gradient(180deg,rgba(224,160,74,.5),transparent 70%);-webkit-background-clip:text;background-clip:text;padding-bottom:.02em;height:.62em;overflow:hidden}
769.nr-legal{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;padding-top:22px;padding-bottom:30px;font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:#6f6a62;border-top:1px solid var(--nr-line);position:relative;background:var(--nr-black)}
770
771/* responsive */
772@media (max-width:900px){
773 .nr-wrap,.nr-nav,.nr-hero-in,.nr-hero-foot,.nr-man-stick,.nr-quiz-in{padding-left:22px;padding-right:22px}
774 .nr-nav{grid-template-columns:1fr auto;padding-top:16px;padding-bottom:16px}.nr-nav-links{display:none}.nr-logo{font-size:21px}
775 .nr-hero-hint{display:none}.nr-hero-in{bottom:19vh}
776 .nr-cap{max-width:none;left:22px!important;right:22px!important;text-align:left!important}
777 .nr-cap.t,.nr-cap.m,.nr-cap.b{top:auto;bottom:7vh}
778 .nr-cap p{font-size:14px}
779 .nr-flacon{height:min(56vh,520px);margin-top:-10vh}
780 .nr-panel{padding:0 22px;align-items:flex-start;padding-top:15vh}
781 .nr-orb{opacity:.7;right:-20vw;width:70vw;margin-top:-10vh}
782 .nr-panel ul{margin-top:20px}.nr-panel li{padding:11px 0;flex-direction:column;gap:2px}.nr-panel li small{text-align:left}
783 .nr-pline{margin-top:12px}
784 .nr-tl{left:22px;right:22px;bottom:20px}.nr-tl-labels{font-size:9px;letter-spacing:.12em}.nr-tl-labels span:not(.on){opacity:.5}
785 .nr-quiz{height:auto}.nr-quiz-stick{position:static;height:auto;min-height:100svh;padding:90px 0 70px}
786 .nr-opts{grid-template-columns:1fr;margin-top:26px}.nr-opt{padding:15px 0}
787 .nr-res{grid-template-columns:1fr;gap:30px}.nr-res-bottle{height:240px}.nr-res-r{order:-1}
788 .nr-res-notes{grid-template-columns:1fr 1fr 1fr;gap:10px}.nr-res-notes span{font-size:.95rem}
789 .nr-coll{padding:110px 0 80px}
790 .nr-row{grid-template-columns:1fr auto;gap:6px 14px;padding:26px 0}.nr-row .no{grid-column:1/-1}.nr-row .nt{grid-column:1;grid-row:3}.nr-row .nm{grid-column:1;grid-row:2}.nr-row .pr{grid-column:2;grid-row:2/4;align-self:end}
791 .nr-row .bt{right:84px;top:34px;margin:0;width:38px;opacity:.9;transform:none}
792 .nr-foot-grid{grid-template-columns:1fr;gap:36px}
793}
794@media (min-width:901px) and (max-height:700px){
795 .nr-quiz{height:auto}.nr-quiz-stick{position:static;height:auto;min-height:100vh;padding:80px 0}
796}
797
798@media (prefers-reduced-motion:reduce){
799 .nr *{animation:none!important}
800 .nr-curtain{display:none}
801 .nr-ln span,.nr-kicker,.nr-hero-foot{transform:none!important;opacity:1!important}
802 .nr.nr-js [data-rv]{opacity:1;transform:none;transition:none}
803 .nr-man{height:auto;padding:120px 0}.nr-man-stick{position:static;height:auto}.nr-man-p span{opacity:1!important;transform:none!important}
804 .nr-bot{height:auto;padding:80px 0 100px}.nr-bot-stick{position:static;height:auto;display:block;overflow:visible;padding:0 48px}
805 .nr-flacon{display:block;margin:0 auto 50px}
806 .nr-cap{position:static;opacity:1!important;transform:none!important;max-width:560px;margin:0 auto 34px;text-align:left!important}
807 .nr-bgword{display:none}
808 .nr-notes{height:auto}.nr-notes-stick{position:static;height:auto;overflow:visible}
809 .nr-atm{display:none}.nr-track{position:static;width:100%;flex-direction:column;transform:none!important}
810 .nr-panel{flex:none;height:auto;min-height:100vh;padding-top:12vh;padding-bottom:12vh;background:radial-gradient(70% 70% at 72% 50%,color-mix(in srgb,var(--atm) 24%,transparent),transparent 70%)}
811 .nr-gw,.nr-pbody{transform:none!important;opacity:1!important}.nr-tl{display:none}
812 .nr-quiz{height:auto}.nr-quiz-stick{position:static;height:auto;min-height:100vh;padding:100px 0}
813 .nr-res-name span{opacity:1;transform:none;transition:none}.nr-bars i{transform:scaleX(1);transition:none}
814 .nr-res-d,.nr-res-notes,.nr-res-cta{opacity:1;transition:none}.nr .nr-liq{transition:none}
815}
816`;