Atelier Nord. Fashion / E-commerce landing page.

A monochrome high-fashion storefront for a Stockholm label. A giant condensed ATELIER NORD masthead is fitted to the viewport and scrubbed apart letter by letter as you scroll, blending over the cover image. A zoom-parallax lookbook starts inside one photograph and pulls back until seven editorial frames settle into an asymmetric grid. Product cards have colour swatches that cross-fade the fabric, a quick-add bar that morphs into a size picker and then an Added state, a spring-physics cart drawer, a cursor-following View label and a press-quote marquee. All garments are procedural SVG with fold shading, herringbone, cable-knit and check textures under a film-grain layer.

  • Letter scrub masthead
  • Zoom-parallax lookbook
  • Quick-add morph
  • Swatch cross-fade
  • Spring cart drawer
  • Cursor-follow label
  • Quote marquee
Full screen

The preview is interactive — scroll inside it to run the animations.

The whole page. One file, nothing left out.

1,135 lines · 71.5 KB · imports React only. This is the exact file behind the preview above — every section, interaction and style.

  1. 01Save it as atelier-nord.tsx in any React 18+ project.
  2. 02Render <Page /> on a route of its own — it brings its own nav and footer.
  3. 03Swap the copy and tokens. Keep the DESIGN.md next to it so your AI agent follows the system.
atelier-nord.tsx
1/**
2 * Atelier Nord — Scandinavian fashion e-commerce.
3 *
4 * Monochrome high-fashion storefront: giant condensed type, an art-directed
5 * asymmetric grid and pre-rendered fabric photography (draped herringbone,
6 * cable knit, twill, poplin, check and leather, recoloured per swatch) with a
7 * CSS film-grain layer. No web fonts.
8 *
9 * Demo images: /img/lux/{dr-coat,dr-knit,dr-trouser,dr-shirt,dr-scarf,dr-bag,dunes-mono,still-grey}.webp
10 * (+ -800 variants), included in the repo's public/img/lux — copy them next to your page or swap in your own.
11 *
12 * Motion
13 * - Masthead: each letter of ATELIER / NORD scrubs on scroll at its own
14 * speed and tilt; the cover image drifts and scales underneath.
15 * - Zoom-parallax lookbook: a pinned stage starts zoomed into one image and
16 * pulls back until it settles into an asymmetric grid; tiles arrive at
17 * different depths.
18 * - Product cards: swatch changes cross-fade the fabric colour; "Quick add"
19 * morphs (clip-path) into a size picker, then into an "Added" state.
20 * - Cart drawer slides in on a real spring (stiffness 380, damping 30).
21 * - A cursor-following "View" label springs after the pointer over product
22 * imagery (mouse only).
23 * - Editorial press-quote marquee; reveal-on-scroll everywhere else.
24 *
25 * Reduced motion: no scrub, no pinning, no marquee, drawer/label snap.
26 * SSR-safe: no window/document access during render.
27 * Accessibility: real buttons with aria-pressed / aria-label, dialog with
28 * focus management + Escape, aria-live cart status, AA contrast.
29 */
30import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
31
32/* ------------------------------------------------------------------ */
33/* Photography (pre-rendered, see /img/lux) */
34/* ------------------------------------------------------------------ */
35type GKind = "coat" | "knit" | "trouser" | "shirt" | "scarf" | "bag";
36type SceneK = "figure" | "fjord" | "birch" | "stack" | "detail" | "window" | "still";
37
38/* Fabric studies, rendered in neutral grey. The product image is multiplied
39 over the swatch colour (mix-blend-mode: overlay) so every colourway keeps
40 the same folds, weave and light. */
41const FABRIC: Record<GKind, string> = { coat: "dr-coat", knit: "dr-knit", trouser: "dr-trouser", shirt: "dr-shirt", scarf: "dr-scarf", bag: "dr-bag" };
42const SCENE_IMG: Record<SceneK, { f: string; w: number; h: number; pos: string; scale?: number }> = {
43 figure: { f: "dr-coat", w: 1200, h: 1500, pos: "50% 40%" },
44 fjord: { f: "dunes-mono", w: 1920, h: 1200, pos: "60% 50%" },
45 birch: { f: "dr-trouser", w: 1200, h: 1500, pos: "50% 50%" },
46 stack: { f: "dr-knit", w: 1200, h: 1500, pos: "50% 70%" },
47 detail: { f: "dr-coat", w: 1200, h: 1500, pos: "30% 30%", scale: 1.9 },
48 window: { f: "still-grey", w: 1200, h: 1500, pos: "50% 55%" },
49 still: { f: "dr-bag", w: 1200, h: 1500, pos: "50% 50%" },
50};
51
52function ProductShot({ kind, color, label }: { kind: GKind; color: string; label: string }) {
53 const f = FABRIC[kind];
54 return (
55 <div className="an-shot" role={label ? "img" : undefined} aria-label={label || undefined} style={{ background: color }}>
56 <img src={`/img/lux/${f}-800.webp`} srcSet={`/img/lux/${f}-800.webp 800w, /img/lux/${f}.webp 1200w`} sizes="(max-width: 700px) 90vw, 480px" width={1200} height={1500} alt={label ? "" : `Grey ${kind} fabric study showing folds and weave in studio light`} loading="lazy" decoding="async" />
57 </div>
58 );
59}
60
61function Scene({ k, label, eager = false }: { k: SceneK; label: string; eager?: boolean }) {
62 const s = SCENE_IMG[k];
63 return (
64 <img
65 className="an-scene"
66 src={`/img/lux/${s.f}-800.webp`}
67 srcSet={`/img/lux/${s.f}-800.webp 800w, /img/lux/${s.f}.webp ${s.w}w`}
68 sizes="(max-width: 700px) 100vw, 50vw"
69 width={s.w}
70 height={s.h}
71 alt={label}
72 loading={eager ? "eager" : "lazy"}
73 decoding="async"
74 style={{ objectPosition: s.pos, ...(s.scale ? { transform: `scale(${s.scale})`, transformOrigin: s.pos } : null) }}
75 />
76 );
77}
78
79/* ------------------------------------------------------------------ */
80/* Data */
81/* ------------------------------------------------------------------ */
82type Swatch = { name: string; hex: string };
83const SW = {
84 ecru: { name: "Ecru", hex: "#E6E0D3" },
85 stone: { name: "Stone", hex: "#A8A39A" },
86 charcoal: { name: "Charcoal", hex: "#3A3A3A" },
87 black: { name: "Black", hex: "#141414" },
88 fog: { name: "Fog", hex: "#C9C9C6" },
89 moss: { name: "Moss", hex: "#55574C" },
90};
91const PRODUCTS: { name: string; kind: GKind; price: number; tag?: string; swatches: Swatch[]; fabric: string; layout: string }[] = [
92 { name: "Sund Overcoat", kind: "coat", price: 690, tag: "New", swatches: [SW.charcoal, SW.stone, SW.black], fabric: "Wool herringbone, 680 g/m", layout: "an-p-a" },
93 { name: "Vind Cable Crew", kind: "knit", price: 230, swatches: [SW.ecru, SW.fog, SW.charcoal], fabric: "Extrafine merino", layout: "an-p-b" },
94 { name: "Ström Pleated Trouser", kind: "trouser", price: 260, swatches: [SW.black, SW.stone, SW.moss], fabric: "Wool twill", layout: "an-p-c" },
95 { name: "Ljus Poplin Shirt", kind: "shirt", price: 160, tag: "Restocked", swatches: [SW.ecru, SW.fog, SW.black], fabric: "Organic cotton poplin", layout: "an-p-d" },
96 { name: "Frost Check Scarf", kind: "scarf", price: 140, swatches: [SW.fog, SW.charcoal, SW.ecru], fabric: "Lambswool", layout: "an-p-e" },
97 { name: "Båt Leather Tote", kind: "bag", price: 320, swatches: [SW.black, SW.stone], fabric: "Vegetable-tanned leather", layout: "an-p-f" },
98];
99const SIZES = ["XS", "S", "M", "L", "XL"];
100const ONE_SIZE: GKind[] = ["scarf", "bag"];
101
102const LOOKS: { k: SceneK; label: string; cap: string }[] = [
103 { k: "birch", label: "Charcoal wool twill falling in raking light", cap: "Look 03 — Ström Trouser" },
104 { k: "stack", label: "Merino cable knit, draped over a plinth", cap: "Vind Cable Crew" },
105 { k: "detail", label: "Close-up of the herringbone weave on the overcoat", cap: "Herringbone, 680 g/m" },
106 { k: "fjord", label: "Wind-sculpted dunes at first light, in monochrome", cap: "Look 01 — Råbjerg Mile, 06:40" },
107 { k: "window", label: "Ceramics and a dried branch in window light", cap: "Frost Check Scarf" },
108 { k: "figure", label: "Charcoal herringbone wool, studio light", cap: "Look 07" },
109 { k: "still", label: "Vegetable-tanned black leather, close-up", cap: "Båt Tote" },
110];
111const LOOK_CENTER = 3;
112
113const QUOTES = [
114 { q: "The quietest coat of the season — and the one everyone asked about.", s: "Kvarter Magazine" },
115 { q: "Scandinavian restraint, cut with a tailor's arrogance.", s: "The Northern Review" },
116 { q: "Atelier Nord makes clothes you stop noticing, which is the point.", s: "Form & Fabric" },
117 { q: "A knit you will hand down, not throw out.", s: "Studio Weekly" },
118];
119
120const FABRICS = [
121 { name: "Herringbone wool", from: "Kvarnby Spinneri, Halland", spec: "680 g/m · 100% wool", cls: "an-tx-hb" },
122 { name: "Cable merino", from: "Filatura Orsi, Biella", spec: "19.5 µm · RWS certified", cls: "an-tx-knit" },
123 { name: "Bouclé alpaca", from: "Andean co-op, Arequipa", spec: "62% alpaca · 38% wool", cls: "an-tx-boucle" },
124 { name: "Cotton poplin", from: "Tessitura Vela, Como", spec: "120/2 · GOTS organic", cls: "an-tx-poplin" },
125];
126
127const JOURNAL = [
128 { k: "fjord" as SceneK, date: "14 Oct 2026", t: "Six days on Lysefjord: shooting AW26 in four hours of light a day" },
129 { k: "stack" as SceneK, date: "2 Oct 2026", t: "Why our cable crew takes eleven hours to knit — and why we refuse to speed it up" },
130 { k: "window" as SceneK, date: "19 Sep 2026", t: "Inside the Södermalm atelier: repairs, alterations and a very old Pfaff" },
131];
132
133const fmt = (n: number) => `€${n.toLocaleString("en-IE")}`;
134type CartItem = { key: string; name: string; kind: GKind; color: Swatch; size: string; price: number; qty: number };
135
136/* ------------------------------------------------------------------ */
137/* Page */
138/* ------------------------------------------------------------------ */
139export function Page() {
140 const rootRef = useRef<HTMLDivElement>(null);
141 const drawerRef = useRef<HTMLDivElement>(null);
142 const closeRef = useRef<HTMLButtonElement>(null);
143 const lastFocus = useRef<HTMLElement | null>(null);
144 const [small, setSmall] = useState(false);
145 const [menu, setMenu] = useState(false);
146 const [colors, setColors] = useState<number[]>(PRODUCTS.map(() => 0));
147 const [picking, setPicking] = useState<number | null>(null);
148 const [added, setAdded] = useState<number | null>(null);
149 const [cart, setCart] = useState<CartItem[]>([
150 { key: "ström-black-M", name: "Ström Pleated Trouser", kind: "trouser", color: SW.black, size: "M", price: 260, qty: 1 },
151 ]);
152 const [open, setOpen] = useState(false);
153 const [bump, setBump] = useState(0);
154 const [status, setStatus] = useState("");
155 const [email, setEmail] = useState("");
156 const [joined, setJoined] = useState(false);
157
158 const count = cart.reduce((a, c) => a + c.qty, 0);
159 const subtotal = cart.reduce((a, c) => a + c.qty * c.price, 0);
160 const FREE = 400;
161
162 /* Scroll engine, reveals, cursor label. */
163 useEffect(() => {
164 const root = rootRef.current;
165 if (!root) return;
166 const doc = root.ownerDocument;
167 const win = doc.defaultView;
168 if (!win) return;
169 const reduce = win.matchMedia("(prefers-reduced-motion: reduce)").matches;
170 root.classList.add("an-js");
171
172 // Hosts with overflow:hidden on html AND body make <body> a scroll container
173 // that never scrolls, which breaks position:sticky. `clip` looks the same.
174 let restoreBody: (() => void) | null = null;
175 const fixRaf = win.requestAnimationFrame(() => {
176 const b = doc.body;
177 const bs = win.getComputedStyle(b);
178 const hs = win.getComputedStyle(doc.documentElement);
179 if (hs.overflowX !== "visible" && (bs.overflowX === "hidden" || bs.overflowY === "auto")) {
180 const prev = [b.style.overflowX, b.style.overflowY];
181 b.style.overflowX = "clip";
182 b.style.overflowY = "visible";
183 restoreBody = () => {
184 b.style.overflowX = prev[0];
185 b.style.overflowY = prev[1];
186 };
187 }
188 });
189
190 const mast = root.querySelector<HTMLElement>(".an-mast");
191 const look = root.querySelector<HTMLElement>(".an-look");
192 const grid = root.querySelector<HTMLElement>(".an-look-grid");
193 const stage = root.querySelector<HTMLElement>(".an-look-stage");
194 const center = root.querySelector<HTMLElement>(`.an-tile-${LOOK_CENTER}`);
195 let s0 = 1;
196 let ox = 0;
197 let oy = 0;
198 let lookOn = false;
199
200 // Fit the masthead word to the content width whatever condensed face the OS supplies.
201 const word = root.querySelector<HTMLElement>(".an-word");
202 const fitWord = () => {
203 if (!word || !mast) return;
204 const lines = Array.from(word.querySelectorAll<HTMLElement>(".an-word-line"));
205 word.style.fontSize = "100px";
206 const widest = Math.max(...lines.map((l) => l.scrollWidth));
207 const avail = mast.clientWidth - parseFloat(win.getComputedStyle(mast).paddingLeft) * 2;
208 if (widest <= 0) return;
209 const byWidth = ((100 * avail) / widest) * 0.995;
210 // Keep the CTA row above the fold on short desktop viewports.
211 const byHeight = win.innerWidth > 760 ? (win.innerHeight - 330) / 1.56 : Infinity;
212 word.style.fontSize = `${Math.floor(Math.max(64, Math.min(byWidth, byHeight)))}px`;
213 };
214 doc.fonts?.ready.then(() => { fitWord(); }).catch(() => {});
215
216 const measure = () => {
217 fitWord();
218 lookOn = !reduce && win.innerWidth > 760;
219 if (!grid || !center || !stage) return;
220 grid.style.transform = "";
221 if (!lookOn) {
222 look?.classList.remove("is-zoom");
223 return;
224 }
225 look?.classList.add("is-zoom");
226 const cw = center.offsetWidth;
227 const ch = center.offsetHeight;
228 const cx = center.offsetLeft + cw / 2;
229 const cy = center.offsetTop + ch / 2;
230 grid.style.transformOrigin = `${cx}px ${cy}px`;
231 s0 = Math.max(win.innerWidth / cw, win.innerHeight / ch) * 1.02;
232 // Where the centre tile sits relative to the stage centre when settled.
233 ox = stage.clientWidth / 2 - (grid.offsetLeft + cx);
234 oy = stage.clientHeight / 2 - (grid.offsetTop + cy);
235 };
236
237 let raf = 0;
238 let lastSmall = false;
239 const update = () => {
240 raf = 0;
241 const vh = win.innerHeight;
242 const y = win.scrollY || doc.documentElement.scrollTop;
243 const sm = y > 40;
244 if (sm !== lastSmall) {
245 lastSmall = sm;
246 setSmall(sm);
247 }
248 if (mast && !reduce) {
249 const r = mast.getBoundingClientRect();
250 const p = Math.min(1, Math.max(0, -r.top / Math.max(1, r.height)));
251 mast.style.setProperty("--p", p.toFixed(4));
252 }
253 if (look && grid && lookOn) {
254 const r = look.getBoundingClientRect();
255 const p = Math.min(1, Math.max(0, -r.top / Math.max(1, r.height - vh)));
256 const k = Math.min(1, p / 0.78);
257 const e = 1 - Math.pow(1 - k, 3);
258 const s = s0 + (1 - s0) * e;
259 grid.style.transform = `translate3d(${(ox * (1 - e)).toFixed(1)}px, ${(oy * (1 - e)).toFixed(1)}px, 0) scale(${s.toFixed(4)})`;
260 look.style.setProperty("--e", e.toFixed(4));
261 }
262 };
263 const onScroll = () => {
264 if (!raf) raf = win.requestAnimationFrame(update);
265 };
266 const onResize = () => {
267 measure();
268 onScroll();
269 };
270 measure();
271 update();
272 win.addEventListener("scroll", onScroll, { passive: true });
273 win.addEventListener("resize", onResize);
274
275 const io = new win.IntersectionObserver(
276 (es) => es.forEach((e) => {
277 if (e.isIntersecting) {
278 e.target.classList.add("is-in");
279 io.unobserve(e.target);
280 }
281 }),
282 { rootMargin: "0px 0px -10% 0px", threshold: 0.12 },
283 );
284 root.querySelectorAll("[data-an-rv]").forEach((el) => io.observe(el));
285
286 // Cursor "View" label — spring-follow (stiffness 380, damping 30).
287 const label = root.querySelector<HTMLElement>(".an-cursor");
288 let tx = 0, ty = 0, x = 0, y2 = 0, vx = 0, vy = 0, active = false, cRaf = 0, last = 0;
289 const step = (now: number) => {
290 const dt = Math.min(0.032, (now - (last || now)) / 1000) || 0.016;
291 last = now;
292 const ax = 380 * (tx - x) - 30 * vx;
293 const ay = 380 * (ty - y2) - 30 * vy;
294 vx += ax * dt; vy += ay * dt;
295 x += vx * dt; y2 += vy * dt;
296 if (label) label.style.transform = `translate3d(${x.toFixed(1)}px, ${y2.toFixed(1)}px, 0)`;
297 if (Math.abs(tx - x) + Math.abs(ty - y2) + Math.abs(vx) + Math.abs(vy) > 0.3) cRaf = win.requestAnimationFrame(step);
298 else { cRaf = 0; last = 0; }
299 };
300 const onMove = (e: PointerEvent) => {
301 if (e.pointerType !== "mouse" || !label) return;
302 const t = e.target as Element | null;
303 // Show over imagery, but step aside for real controls (quick add, hotspots).
304 const hit = t?.closest?.("[data-an-view]") && !t.closest("button");
305 const rr = root.getBoundingClientRect();
306 tx = e.clientX - rr.left;
307 ty = e.clientY - rr.top;
308 if (hit && !active) {
309 active = true;
310 if (reduce || !label.classList.contains("is-on")) { x = tx; y2 = ty; vx = vy = 0; }
311 label.classList.add("is-on");
312 } else if (!hit && active) {
313 active = false;
314 label.classList.remove("is-on");
315 }
316 if (reduce) {
317 label.style.transform = `translate3d(${tx}px, ${ty}px, 0)`;
318 return;
319 }
320 if (!cRaf) cRaf = win.requestAnimationFrame(step);
321 };
322 root.addEventListener("pointermove", onMove, { passive: true });
323 const onLeave = () => { active = false; label?.classList.remove("is-on"); };
324 root.addEventListener("pointerleave", onLeave);
325
326 return () => {
327 win.cancelAnimationFrame(fixRaf);
328 if (raf) win.cancelAnimationFrame(raf);
329 if (cRaf) win.cancelAnimationFrame(cRaf);
330 win.removeEventListener("scroll", onScroll);
331 win.removeEventListener("resize", onResize);
332 root.removeEventListener("pointermove", onMove);
333 root.removeEventListener("pointerleave", onLeave);
334 io.disconnect();
335 restoreBody?.();
336 root.classList.remove("an-js");
337 };
338 }, []);
339
340 /* Cart drawer — spring slide + focus management. */
341 const springX = useRef({ x: 104, v: 0 });
342 useEffect(() => {
343 const el = drawerRef.current;
344 const win = el?.ownerDocument.defaultView;
345 if (!el || !win) return;
346 const reduce = win.matchMedia("(prefers-reduced-motion: reduce)").matches;
347 const target = open ? 0 : 104;
348 const st = springX.current;
349 let raf = 0;
350 let last = 0;
351 const paint = () => { el.style.transform = `translate3d(${st.x.toFixed(2)}%, 0, 0)`; };
352 if (reduce) {
353 st.x = target; st.v = 0; paint();
354 } else {
355 const tick = (now: number) => {
356 const dt = Math.min(0.032, (now - (last || now)) / 1000) || 0.016;
357 last = now;
358 const a = 380 * (target - st.x) - 30 * st.v;
359 st.v += a * dt;
360 st.x += st.v * dt;
361 paint();
362 if (Math.abs(target - st.x) > 0.05 || Math.abs(st.v) > 0.05) raf = win.requestAnimationFrame(tick);
363 else { st.x = target; st.v = 0; paint(); }
364 };
365 raf = win.requestAnimationFrame(tick);
366 }
367 if (open) {
368 lastFocus.current = el.ownerDocument.activeElement as HTMLElement | null;
369 win.setTimeout(() => closeRef.current?.focus({ preventScroll: true }), 60);
370 } else if (lastFocus.current) {
371 lastFocus.current.focus?.({ preventScroll: true });
372 lastFocus.current = null;
373 }
374 const onKey = (e: KeyboardEvent) => {
375 if (!open) return;
376 if (e.key === "Escape") setOpen(false);
377 if (e.key === "Tab") {
378 const f = Array.from(el.querySelectorAll<HTMLElement>("button, a[href], input")).filter((n) => !n.hasAttribute("disabled"));
379 if (!f.length) return;
380 const first = f[0];
381 const lastEl = f[f.length - 1];
382 const act = el.ownerDocument.activeElement;
383 if (e.shiftKey && act === first) { e.preventDefault(); lastEl.focus(); }
384 else if (!e.shiftKey && act === lastEl) { e.preventDefault(); first.focus(); }
385 }
386 };
387 el.ownerDocument.addEventListener("keydown", onKey);
388 return () => {
389 win.cancelAnimationFrame(raf);
390 el.ownerDocument.removeEventListener("keydown", onKey);
391 };
392 }, [open]);
393
394 const addToCart = (pi: number, size: string) => {
395 const p = PRODUCTS[pi];
396 const c = p.swatches[colors[pi]];
397 const key = `${p.name}-${c.name}-${size}`;
398 setCart((cs) => {
399 const ex = cs.find((x) => x.key === key);
400 if (ex) return cs.map((x) => (x.key === key ? { ...x, qty: x.qty + 1 } : x));
401 return [{ key, name: p.name, kind: p.kind, color: c, size, price: p.price, qty: 1 }, ...cs];
402 });
403 setPicking(null);
404 setAdded(pi);
405 setBump((b) => b + 1);
406 setStatus(`${p.name}, ${c.name}, size ${size} added to your bag.`);
407 const win = rootRef.current?.ownerDocument.defaultView;
408 win?.setTimeout(() => setOpen(true), 650);
409 win?.setTimeout(() => setAdded((a) => (a === pi ? null : a)), 2600);
410 };
411 const changeQty = (key: string, d: number) =>
412 setCart((cs) => cs.map((x) => (x.key === key ? { ...x, qty: x.qty + d } : x)).filter((x) => x.qty > 0));
413
414 const navL = ["Women", "Men", "Knitwear", "Lookbook", "Journal"];
415 const word1 = "ATELIER".split("");
416 const word2 = "NORD".split("");
417 const dyOf = (i: number, w: number) => (w === 0 ? -1 : 1) * (40 + ((i * 53 + w * 31) % 110));
418 const rotOf = (i: number, w: number) => (((i + w) % 3) - 1) * (2 + (i % 2) * 2);
419
420 return (
421 <div ref={rootRef} className="an">
422 <style>{css}</style>
423 <a className="an-skip" href="#an-main">Skip to content</a>
424 <span className="an-cursor" aria-hidden="true">View</span>
425
426 <p className="an-announce">Complimentary shipping & returns across the EU over €400 · Repairs free for life</p>
427
428 {/* NAV */}
429 <header className={`an-nav${small ? " is-small" : ""}${menu ? " is-open" : ""}`}>
430 <div className="an-nav-in">
431 <nav className="an-nav-l" aria-label="Primary">
432 <button type="button" className="an-burger" aria-expanded={menu} aria-label={menu ? "Close menu" : "Open menu"} onClick={() => setMenu((m) => !m)}><span /><span /></button>
433 {navL.map((l) => <a key={l} href={l === "Lookbook" ? "#an-look" : l === "Journal" ? "#an-journal" : "#an-shop"}>{l}</a>)}
434 </nav>
435 <a href="#an-top" className="an-logo" aria-label="Atelier Nord — home">Atelier&nbsp;Nord</a>
436 <div className="an-nav-r">
437 <a href="#an-top" className="an-nav-txt">Search</a>
438 <a href="#an-top" className="an-nav-txt">Account</a>
439 <button type="button" className="an-bag-btn" onClick={() => setOpen(true)} aria-label={`Bag, ${count} ${count === 1 ? "item" : "items"}`}>
440 Bag <span key={bump} className={`an-bag-n${bump ? " is-bump" : ""}`}>{count}</span>
441 </button>
442 </div>
443 </div>
444 <div className="an-sheet" hidden={!menu}>
445 {navL.map((l) => <a key={l} href={l === "Lookbook" ? "#an-look" : l === "Journal" ? "#an-journal" : "#an-shop"} onClick={() => setMenu(false)}>{l}</a>)}
446 </div>
447 </header>
448
449 <main id="an-main">
450 {/* MASTHEAD */}
451 <section id="an-top" className="an-mast" aria-labelledby="an-mast-title">
452 <div className="an-mast-meta">
453 <p>Autumn / Winter 2026<br />Collection 07 — <em>Stillhet</em></p>
454 <p className="an-mast-meta-r">Designed in Stockholm<br />Made in Portugal & Italy</p>
455 </div>
456 <figure className="an-mast-img" data-an-view>
457 <div className="an-mast-img-in an-grain"><Scene k="figure" eager label="Charcoal herringbone wool, draped in soft studio light" /></div>
458 <figcaption>Look 07 · Sund Overcoat, Charcoal</figcaption>
459 </figure>
460 <h1 id="an-mast-title" className="an-word" aria-label="Atelier Nord">
461 <span className="an-word-line" aria-hidden="true">
462 {word1.map((c, i) => (
463 <span key={i} className="an-ch" style={{ "--dy": dyOf(i, 0), "--rot": rotOf(i, 0), "--i": i } as CSSProperties}><span>{c}</span></span>
464 ))}
465 </span>
466 <span className="an-word-line an-word-line-2" aria-hidden="true">
467 {word2.map((c, i) => (
468 <span key={i} className="an-ch" style={{ "--dy": dyOf(i, 1), "--rot": rotOf(i, 1), "--i": i + 7 } as CSSProperties}><span>{c}</span></span>
469 ))}
470 </span>
471 </h1>
472 <div className="an-mast-foot">
473 <p>Clothes for long winters and short light. Seven-year coats, knits you repair, nothing you need to explain.</p>
474 <a href="#an-shop" className="an-btn an-btn-ink">Shop the collection <span aria-hidden="true">→</span></a>
475 </div>
476 </section>
477
478 {/* STATEMENT */}
479 <section className="an-statement" aria-label="About the collection">
480 <p className="an-statement-n" data-an-rv>(01)</p>
481 <p className="an-statement-t" data-an-rv>
482 Stillhet is Swedish for <em>stillness</em>. Thirty-one pieces in five colours, cut from cloth woven within
483 <span className="an-inline-img an-grain" aria-hidden="true"><Scene k="detail" label="" /></span>
484 a day of where it was spun — built to be worn hard and mended, not replaced.
485 </p>
486 </section>
487
488 {/* LOOKBOOK ZOOM */}
489 <section id="an-look" className="an-look" aria-labelledby="an-look-title">
490 <div className="an-look-stage">
491 <div className="an-look-head">
492 <h2 id="an-look-title" className="an-h2">Lookbook — AW26</h2>
493 <p>Shot on Lysefjord and in Tyresta, October 2026</p>
494 </div>
495 <ul className="an-look-grid">
496 {LOOKS.map((l, i) => (
497 <li key={l.k} className={`an-tile an-tile-${i}`} style={{ "--k": [140, -90, 200, 0, -160, 120, -60][i] } as CSSProperties} data-an-view>
498 <div className="an-tile-img an-grain"><Scene k={l.k} label={l.label} /></div>
499 <p className="an-tile-cap">{l.cap}</p>
500 </li>
501 ))}
502 </ul>
503 <p className="an-look-over" aria-hidden="true">Stillhet</p>
504 </div>
505 </section>
506
507 {/* SHOP */}
508 <section id="an-shop" className="an-shop" aria-labelledby="an-shop-title">
509 <div className="an-shop-head">
510 <h2 id="an-shop-title" className="an-h2 an-h2-xl" data-an-rv>New in</h2>
511 <p data-an-rv>Six pieces from Collection 07. Every garment ships with a repair card — send it back any time and we'll mend it free.</p>
512 <a href="#an-shop" className="an-link" data-an-rv>View all 31 pieces</a>
513 </div>
514 <p className="an-sr" aria-live="polite">{status}</p>
515 <ul className="an-grid">
516 {PRODUCTS.map((p, i) => {
517 const c = p.swatches[colors[i]];
518 const isPick = picking === i;
519 const isAdded = added === i;
520 const oneSize = ONE_SIZE.includes(p.kind);
521 return (
522 <li key={p.name} className={`an-card ${p.layout}`} data-an-rv>
523 <div className="an-card-media" data-an-view>
524 <a href="#an-shop" className="an-card-img an-grain" aria-label={`${p.name} in ${c.name}, ${fmt(p.price)}`}>
525 <ProductShot kind={p.kind} color={c.hex} label={`${p.name} in ${c.name}`} />
526 </a>
527 {p.tag && <span className="an-tag">{p.tag}</span>}
528 <div className={`an-qa${isPick ? " is-pick" : ""}${isAdded ? " is-added" : ""}`}>
529 <button type="button" className="an-qa-main" aria-expanded={isPick} onClick={() => (oneSize ? addToCart(i, "One size") : setPicking(isPick ? null : i))} disabled={isAdded}>
530 <span className="an-qa-plus" aria-hidden="true" />
531 <span className="an-qa-txt">{isAdded ? "Added" : oneSize ? "Add to bag" : "Quick add"}</span>
532 </button>
533 <div className="an-qa-sizes" role="group" aria-label={`Choose size for ${p.name}`} aria-hidden={!isPick}>
534 {SIZES.map((s, si) => (
535 <button key={s} type="button" tabIndex={isPick ? 0 : -1} disabled={p.kind === "coat" && si === 0} onClick={() => addToCart(i, s)} aria-label={`Size ${s}${p.kind === "coat" && si === 0 ? ", sold out" : ""}`}>{s}</button>
536 ))}
537 </div>
538 </div>
539 </div>
540 <div className="an-card-info">
541 <div>
542 <h3 className="an-card-name">{p.name}</h3>
543 <p className="an-card-fab">{p.fabric} · <span className="an-card-col">{c.name}</span></p>
544 </div>
545 <p className="an-card-price">{fmt(p.price)}</p>
546 </div>
547 <div className="an-sw" role="group" aria-label={`Colour for ${p.name}`}>
548 {p.swatches.map((s, si) => (
549 <button key={s.name} type="button" aria-label={s.name} aria-pressed={colors[i] === si} className={colors[i] === si ? "is-on" : ""} style={{ "--c": s.hex } as CSSProperties} onClick={() => setColors((cs) => cs.map((v, k) => (k === i ? si : v)))} />
550 ))}
551 </div>
552 </li>
553 );
554 })}
555 </ul>
556 </section>
557
558 {/* PRESS MARQUEE */}
559 <section className="an-press" aria-label="Press">
560 <div className="an-marquee">
561 <div className="an-marquee-track">
562 {[0, 1].map((k) => (
563 <ul key={k} className="an-marquee-set" aria-hidden={k === 1}>
564 {QUOTES.map((q) => (
565 <li key={q.s}><blockquote>“{q.q}”</blockquote><cite>— {q.s}</cite><span className="an-star" aria-hidden="true">✳</span></li>
566 ))}
567 </ul>
568 ))}
569 </div>
570 </div>
571 </section>
572
573 {/* FEATURE */}
574 <section className="an-feature" aria-labelledby="an-feature-title">
575 <div className="an-feature-media" data-an-view>
576 <div className="an-feature-img an-grain">
577 <ProductShot kind="coat" color={SW.charcoal.hex} label="Sund Overcoat, charcoal, front view" />
578 {[
579 { x: 47, y: 24, t: "Herringbone twill, 680 g/m wool" },
580 { x: 57, y: 55, t: "Brushed by hand for a soft, even nap" },
581 { x: 30, y: 70, t: "Lined in Bemberg cupro" },
582 ].map((h, i) => (
583 <button key={i} type="button" className="an-hot" style={{ left: `${h.x}%`, top: `${h.y}%` }} aria-label={h.t}>
584 <span aria-hidden="true">{i + 1}</span>
585 <em role="tooltip">{h.t}</em>
586 </button>
587 ))}
588 </div>
589 </div>
590 <div className="an-feature-text">
591 <p className="an-eyebrow" data-an-rv>The Sund Overcoat</p>
592 <h2 id="an-feature-title" className="an-h2 an-h2-xl" data-an-rv>Seven winters, minimum.</h2>
593 <p className="an-body" data-an-rv>A single-breasted coat in 680-gram herringbone, woven in Halland from wool shorn forty kilometres away. Canvassed through the chest, cut long enough to cover a blazer and roomy enough for a cable knit underneath.</p>
594 <dl className="an-specs" data-an-rv>
595 <div><dt>Cloth</dt><dd>100% wool herringbone, Kvarnby Spinneri</dd></div>
596 <div><dt>Lining</dt><dd>Bemberg cupro, body & sleeves</dd></div>
597 <div><dt>Fit</dt><dd>Relaxed. Model is 186 cm wearing M</dd></div>
598 <div><dt>Made</dt><dd>Guimarães, Portugal — 14 hours per coat</dd></div>
599 <div><dt>Care</dt><dd>Brush, air, steam. Free repairs for life</dd></div>
600 </dl>
601 <div className="an-feature-cta" data-an-rv>
602 <button type="button" className="an-btn an-btn-ink" onClick={() => addToCart(0, "M")}>Add to bag — {fmt(690)}</button>
603 <a href="#an-shop" className="an-link">Size guide</a>
604 </div>
605 </div>
606 </section>
607
608 {/* MATERIALS */}
609 <section className="an-mat" aria-labelledby="an-mat-title">
610 <div className="an-mat-head">
611 <p className="an-eyebrow" data-an-rv>(04) Materials</p>
612 <h2 id="an-mat-title" className="an-h2" data-an-rv>Four cloths. Four mills. We have visited every one.</h2>
613 </div>
614 <ul className="an-mat-grid">
615 {FABRICS.map((f, i) => (
616 <li key={f.name} className="an-mat-card" data-an-rv style={{ "--d": `${i * 80}ms` } as CSSProperties}>
617 <div className="an-mat-sw"><span className={`an-tx ${f.cls}`} /></div>
618 <h3 className="an-mat-name">{f.name}</h3>
619 <p className="an-mat-from">{f.from}</p>
620 <p className="an-mat-spec">{f.spec}</p>
621 </li>
622 ))}
623 </ul>
624 </section>
625
626 {/* JOURNAL */}
627 <section id="an-journal" className="an-journal" aria-labelledby="an-journal-title">
628 <div className="an-journal-head">
629 <h2 id="an-journal-title" className="an-h2 an-h2-xl" data-an-rv>Journal</h2>
630 <a href="#an-journal" className="an-link" data-an-rv>All stories</a>
631 </div>
632 <ul className="an-journal-grid">
633 {JOURNAL.map((j, i) => (
634 <li key={j.t} className="an-story" data-an-rv style={{ "--d": `${i * 90}ms` } as CSSProperties}>
635 <a href="#an-journal">
636 <div className="an-story-img an-grain"><Scene k={j.k} label={LOOKS.find((l) => l.k === j.k)?.label ?? "Atelier Nord journal story cover"} /></div>
637 <p className="an-story-date">{j.date}</p>
638 <h3 className="an-story-t">{j.t}</h3>
639 </a>
640 </li>
641 ))}
642 </ul>
643 </section>
644 </main>
645
646 {/* FOOTER */}
647 <footer className="an-foot">
648 <div className="an-foot-top">
649 <form className="an-news" onSubmit={(e) => { e.preventDefault(); if (/\S+@\S+\.\S+/.test(email)) setJoined(true); }}>
650 <h2 className="an-news-t">Letters from the atelier</h2>
651 <p className="an-news-s">New collections a week early, sample sales in Stockholm and Copenhagen. Twice a season, never more.</p>
652 <label htmlFor="an-email" className="an-news-l">Email</label>
653 <div className="an-news-row">
654 <input id="an-email" type="email" required autoComplete="email" placeholder="name@domain.com" value={email} onChange={(e) => setEmail(e.target.value)} disabled={joined} />
655 <button type="submit" disabled={joined}>{joined ? "Subscribed ✓" : "Subscribe"}</button>
656 </div>
657 <p className="an-news-note" aria-live="polite">{joined ? `Tack — the next letter goes to ${email}.` : "By subscribing you accept our privacy policy."}</p>
658 </form>
659 <div className="an-foot-cols">
660 <div><p className="an-foot-h">Stores</p><ul><li>Hornsgatan 54, Stockholm</li><li>Ravnsborggade 9, København</li><li>Markveien 35, Oslo</li></ul></div>
661 <div><p className="an-foot-h">Help</p><ul>{["Shipping & returns", "Repairs", "Size guide", "Care", "Contact"].map((x) => <li key={x}><a href="#an-top">{x}</a></li>)}</ul></div>
662 <div><p className="an-foot-h">Atelier</p><ul>{["About", "Mills & makers", "Impact report 2025", "Careers", "Stockists"].map((x) => <li key={x}><a href="#an-top">{x}</a></li>)}</ul></div>
663 <div><p className="an-foot-h">Follow</p><ul>{["Instagram", "Pinterest", "Spotify — Stillhet playlist"].map((x) => <li key={x}><a href="#an-top">{x}</a></li>)}</ul></div>
664 </div>
665 </div>
666 <p className="an-foot-word" aria-hidden="true">ATELIER NORD</p>
667 <div className="an-foot-legal">
668 <span>© 2026 Atelier Nord AB · Org. nr 559301-4471 · Stockholm</span>
669 <span><a href="#an-top">Privacy</a><a href="#an-top">Terms</a><a href="#an-top">Cookies</a><a href="#an-top">EUR € / English</a></span>
670 </div>
671 </footer>
672
673 {/* CART DRAWER */}
674 <div className={`an-scrim${open ? " is-on" : ""}`} onClick={() => setOpen(false)} aria-hidden="true" />
675 <div ref={drawerRef} className={`an-drawer${open ? " is-open" : ""}`} role="dialog" aria-modal="true" aria-labelledby="an-bag-title" aria-hidden={!open} style={{ transform: "translate3d(104%,0,0)" }}>
676 <div className="an-drawer-head">
677 <h2 id="an-bag-title" className="an-drawer-t">Bag <span>({count})</span></h2>
678 <button ref={closeRef} type="button" className="an-x" onClick={() => setOpen(false)} aria-label="Close bag" tabIndex={open ? 0 : -1}><span /><span /></button>
679 </div>
680 <div className="an-ship">
681 <p>{subtotal >= FREE ? "You've unlocked free shipping." : `You're ${fmt(FREE - subtotal)} away from free shipping.`}</p>
682 <span className="an-ship-bar"><i style={{ transform: `scaleX(${Math.min(1, subtotal / FREE)})` }} /></span>
683 </div>
684 <ul className="an-lines">
685 {cart.length === 0 && <li className="an-empty">Your bag is empty.</li>}
686 {cart.map((c) => (
687 <li key={c.key} className="an-line">
688 <div className="an-line-img"><ProductShot kind={c.kind} color={c.color.hex} label="" /></div>
689 <div className="an-line-info">
690 <p className="an-line-name">{c.name}</p>
691 <p className="an-line-meta">{c.color.name} · {c.size}</p>
692 <div className="an-qty">
693 <button type="button" onClick={() => changeQty(c.key, -1)} aria-label={`Remove one ${c.name}`} tabIndex={open ? 0 : -1}>−</button>
694 <span>{c.qty}</span>
695 <button type="button" onClick={() => changeQty(c.key, 1)} aria-label={`Add one ${c.name}`} tabIndex={open ? 0 : -1}>+</button>
696 </div>
697 </div>
698 <p className="an-line-price">{fmt(c.price * c.qty)}</p>
699 </li>
700 ))}
701 </ul>
702 <div className="an-drawer-foot">
703 <p className="an-sub"><span>Subtotal</span><span>{fmt(subtotal)}</span></p>
704 <p className="an-sub-note">Taxes included. Shipping calculated at checkout.</p>
705 <button type="button" className="an-btn an-btn-ink an-btn-block" disabled={!cart.length} tabIndex={open ? 0 : -1}>Checkout</button>
706 </div>
707 </div>
708 </div>
709 );
710}
711
712export default Page;
713
714/* ------------------------------------------------------------------ */
715/* Styles */
716/* ------------------------------------------------------------------ */
717const GRAIN = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .55 0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E")`;
718
719const css = String.raw`
720.an {
721 --an-paper: #F2F1EE;
722 --an-white: #FFFFFF;
723 --an-studio: #E4E2DD;
724 --an-ink: #0E0E0E;
725 --an-graphite: #2A2A2A;
726 --an-grey: #66645F;
727 --an-line: rgba(14, 14, 14, 0.14);
728 --an-cond: "HelveticaNeue-CondensedBold", "Helvetica Neue Condensed Bold", "Helvetica Neue Condensed", "Arial Narrow", "Roboto Condensed", "Nimbus Sans Narrow", "TeX Gyre Heros Cn", "Liberation Sans Narrow", ui-sans-serif, sans-serif;
729 --an-sans: "Helvetica Neue", Helvetica, ui-sans-serif, -apple-system, "Inter", "Segoe UI", Roboto, Arial, sans-serif;
730 --fd-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
731 --fd-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
732 --fd-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
733 --fd-dur-1: 120ms;
734 --fd-dur-2: 240ms;
735 --fd-dur-3: 420ms;
736 --fd-dur-4: 700ms;
737 --an-gut: 24px;
738 position: relative;
739 display: block;
740 width: 100%;
741 margin: 0;
742 padding: 0;
743 overflow-x: clip;
744 background: var(--an-paper);
745 color: var(--an-ink);
746 font: 400 14px/1.5 var(--an-sans);
747 letter-spacing: 0;
748 text-align: left;
749 -webkit-font-smoothing: antialiased;
750 -moz-osx-font-smoothing: grayscale;
751}
752.an *, .an *::before, .an *::after {
753 box-sizing: border-box;
754 margin: 0;
755 padding: 0;
756 border: 0 solid transparent;
757 font: inherit;
758 color: inherit;
759 letter-spacing: inherit;
760 text-decoration: none;
761 list-style: none;
762 background: none;
763 -webkit-appearance: none;
764 appearance: none;
765 min-width: 0;
766 font-style: normal;
767}
768.an :where(svg) { display: block; }
769.an :where(button) { cursor: pointer; text-align: inherit; line-height: inherit; border-radius: 0; }
770.an :where(button:disabled) { cursor: not-allowed; }
771.an :focus-visible { outline: 2px solid rgba(14, 14, 14, 0.7); outline-offset: 2px; }
772.an [hidden] { display: none !important; }
773.an-skip { position: absolute; left: 12px; top: -60px; z-index: 200; padding: 12px 16px; background: var(--an-ink); color: #fff; font: 500 13px/1 var(--an-sans); }
774.an-skip:focus { top: 12px; }
775.an-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
776.an-grain { position: relative; }
777.an-grain::after { content: ""; position: absolute; inset: 0; pointer-events: none; background-image: ${GRAIN}; background-size: 160px 160px; opacity: 0.2; mix-blend-mode: multiply; }
778.an-shot, .an-scene { display: block; width: 100%; height: 100%; max-width: none; }
779.an-shot { position: relative; overflow: hidden; }
780.an-shot img { position: absolute; inset: 0; display: block; width: 100%; height: 100%; max-width: none; object-fit: cover; mix-blend-mode: overlay; }
781.an-scene { object-fit: cover; }
782.an-fab { transition: fill var(--fd-dur-4) var(--fd-ease-in-out); }
783.an-fab-stroke { transition: stroke var(--fd-dur-4) var(--fd-ease-in-out); }
784
785.an-eyebrow { font: 500 12px/1.4 var(--an-sans); letter-spacing: 0.1em; text-transform: uppercase; color: var(--an-grey); }
786.an-h2 { font: 700 clamp(40px, 5vw, 76px)/0.88 var(--an-cond); font-stretch: condensed; letter-spacing: -0.015em; text-transform: uppercase; color: var(--an-ink); }
787.an-h2-xl { font-size: clamp(64px, 9vw, 150px); line-height: 0.82; letter-spacing: -0.025em; }
788.an-body { font: 400 16px/1.6 var(--an-sans); color: var(--an-graphite); }
789.an-link { display: inline-flex; align-items: center; min-height: 44px; font: 500 13px/1 var(--an-sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--an-ink); background-image: linear-gradient(currentColor, currentColor); background-size: 100% 1px; background-repeat: no-repeat; background-position: 0 calc(50% + 10px); transition: background-size var(--fd-dur-3) var(--fd-ease-out); }
790.an-link:hover { background-size: 0% 1px; background-position: 100% calc(50% + 10px); }
791.an-btn { display: inline-flex; align-items: center; justify-content: center; gap: 12px; min-height: 52px; padding: 0 28px; font: 500 13px/1 var(--an-sans); letter-spacing: 0.1em; text-transform: uppercase; white-space: nowrap; transition: background-color var(--fd-dur-2) var(--fd-ease-out), color var(--fd-dur-2) var(--fd-ease-out), transform var(--fd-dur-2) var(--fd-spring); }
792.an-btn span { transition: transform var(--fd-dur-3) var(--fd-ease-out); }
793.an-btn:hover span { transform: translateX(4px); }
794.an-btn:active { transform: scale(0.97); transition-duration: var(--fd-dur-1); }
795.an-btn-ink { background: var(--an-ink); color: #FFFFFF; }
796.an-btn-ink:hover { background: #333; }
797.an-btn-ink:disabled { background: #9A9893; }
798.an-btn-block { width: 100%; }
799
800/* Cursor label */
801.an-cursor { position: absolute; left: 0; top: 0; z-index: 90; display: grid; place-items: center; width: 84px; height: 84px; margin: -42px 0 0 -42px; border-radius: 50%; background: var(--an-ink); color: #FFFFFF; font: 500 12px/1 var(--an-sans); letter-spacing: 0.12em; text-transform: uppercase; pointer-events: none; scale: 0; transition: scale var(--fd-dur-3) var(--fd-spring); will-change: transform; }
802.an-cursor.is-on { scale: 1; }
803
804/* Announcement + NAV */
805.an-announce { padding: 10px var(--an-gut); background: var(--an-ink); color: #EDEDEA; font: 400 12px/1.4 var(--an-sans); letter-spacing: 0.06em; text-align: center; }
806.an-nav { position: sticky; top: 0; z-index: 70; }
807.an-nav-in { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; height: 72px; padding: 0 var(--an-gut); background: var(--an-paper); border-bottom: 1px solid transparent; transition: height var(--fd-dur-3) var(--fd-ease-out), border-color var(--fd-dur-3) var(--fd-ease-out), background-color var(--fd-dur-3) var(--fd-ease-out); }
808.an-nav.is-small .an-nav-in { height: 52px; border-bottom-color: var(--an-line); background: rgba(242, 241, 238, 0.9); -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px); }
809.an-nav-l { display: flex; align-items: center; gap: 22px; }
810.an-nav-l a, .an-nav-txt { display: inline-flex; align-items: center; min-height: 44px; font: 500 12px/1 var(--an-sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--an-ink); }
811.an-nav-l a:hover, .an-nav-txt:hover { text-decoration: underline; text-underline-offset: 4px; }
812.an-logo { font: 700 30px/1 var(--an-cond); font-stretch: condensed; letter-spacing: 0.04em; text-transform: uppercase; color: var(--an-ink); white-space: nowrap; transition: font-size var(--fd-dur-3) var(--fd-ease-out), letter-spacing var(--fd-dur-3) var(--fd-ease-out); }
813.an-nav.is-small .an-logo { font-size: 22px; letter-spacing: 0.08em; }
814.an-nav-r { display: flex; align-items: center; justify-content: flex-end; gap: 22px; }
815.an-bag-btn { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; font: 500 12px/1 var(--an-sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--an-ink); }
816.an-bag-n { display: inline-grid; place-items: center; min-width: 22px; height: 22px; padding: 0 6px; border-radius: 9999px; background: var(--an-ink); color: #FFFFFF; font: 500 11px/1 var(--an-sans); letter-spacing: 0; }
817.an-bag-n.is-bump { animation: an-bump 600ms var(--fd-spring); }
818@keyframes an-bump { 0% { transform: scale(1); } 30% { transform: scale(1.5); } 100% { transform: scale(1); } }
819.an-burger { display: none; position: relative; width: 44px; height: 44px; margin-left: -10px; }
820.an-burger span { position: absolute; left: 11px; right: 11px; height: 1.5px; background: var(--an-ink); transition: transform var(--fd-dur-3) var(--fd-ease-out); }
821.an-burger span:first-child { top: 18px; }
822.an-burger span:last-child { top: 25px; }
823.an-nav.is-open .an-burger span:first-child { transform: translateY(3.5px) rotate(45deg); }
824.an-nav.is-open .an-burger span:last-child { transform: translateY(-3.5px) rotate(-45deg); }
825.an-sheet { display: flex; flex-direction: column; padding: 8px var(--an-gut) 24px; background: var(--an-paper); border-bottom: 1px solid var(--an-line); }
826.an-sheet a { padding: 10px 0; font: 700 44px/1 var(--an-cond); font-stretch: condensed; text-transform: uppercase; color: var(--an-ink); }
827
828/* MASTHEAD */
829.an-mast { position: relative; display: grid; grid-template-columns: 1fr; grid-template-rows: auto 1fr auto auto; min-height: calc(100vh - 110px); min-height: calc(100svh - 110px); padding: 20px var(--an-gut) 28px; --p: 0; overflow: hidden; }
830.an-mast-meta { display: flex; justify-content: space-between; gap: 24px; font: 500 12px/1.5 var(--an-sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--an-ink); position: relative; z-index: 3; }
831.an-mast > .an-word { isolation: auto; }
832.an-mast-meta em { font-style: italic; }
833.an-mast-meta-r { text-align: right; color: var(--an-grey); }
834.an-mast-img { position: absolute; right: 9vw; top: 84px; z-index: 2; width: 24vw; margin: 0; transform: translate3d(0, calc(var(--p) * 120px), 0); }
835.an-mast-img-in { aspect-ratio: 3 / 4; overflow: hidden; background: var(--an-studio); }
836.an-mast-img-in .an-scene { transform: scale(calc(1.12 + var(--p) * 0.12)); transform-origin: 50% 30%; }
837.an-mast-img figcaption { margin-top: 10px; font: 500 11px/1.4 var(--an-sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--an-grey); }
838.an-word { grid-row: 3; position: relative; z-index: 3; mix-blend-mode: difference; color: #FFFFFF; display: flex; flex-direction: column; font: 700 22.6vw/0.78 var(--an-cond); font-stretch: condensed; letter-spacing: -0.02em; text-transform: uppercase; margin: 0; }
839.an-word-line { display: flex; width: max-content; white-space: nowrap; }
840.an-word-line-2 { justify-content: flex-start; padding-left: 0; }
841.an-ch { display: inline-block; overflow: hidden; padding: 0.04em 0 0.02em; margin: -0.04em 0 -0.02em; transform: translate3d(0, calc(var(--p) * var(--dy) * 1px), 0) rotate(calc(var(--p) * var(--rot) * 1deg)); will-change: transform; }
842.an-ch > span { display: inline-block; animation: an-rise 1.1s var(--fd-ease-out) both; animation-delay: calc(var(--i) * 45ms + 120ms); }
843@keyframes an-rise { from { transform: translateY(102%); } to { transform: none; } }
844.an-mast-foot { grid-row: 4; position: relative; z-index: 3; display: flex; align-items: flex-end; justify-content: space-between; gap: 32px; margin-top: 24px; padding-top: 18px; border-top: 1px solid var(--an-ink); }
845.an-mast-foot p { max-width: 420px; font: 400 16px/1.5 var(--an-sans); color: var(--an-graphite); }
846
847/* STATEMENT */
848.an-statement { display: grid; grid-template-columns: 1fr 3fr; gap: var(--an-gut); padding: 160px var(--an-gut) 180px; }
849.an-statement-n { font: 500 12px/1.6 var(--an-sans); letter-spacing: 0.08em; color: var(--an-grey); }
850.an-statement-t { font: 400 clamp(28px, 3.4vw, 52px)/1.12 var(--an-sans); letter-spacing: -0.03em; color: var(--an-ink); text-indent: 12%; }
851.an-statement-t em { font-style: italic; }
852.an-inline-img { display: inline-block; width: 1.9em; height: 0.82em; margin: 0 0.12em; vertical-align: -0.06em; overflow: hidden; border-radius: 9999px; background: #2E2E2E; }
853.an-inline-img .an-scene { width: 100%; height: 100%; }
854
855/* REVEALS */
856.an-js [data-an-rv] { opacity: 0; transform: translate3d(0, 32px, 0); transition: opacity 900ms var(--fd-ease-out), transform 1s var(--fd-ease-out); transition-delay: var(--d, 0ms); }
857.an-js [data-an-rv].is-in { opacity: 1; transform: none; }
858
859/* LOOKBOOK */
860.an-look { position: relative; background: var(--an-paper); --e: 1; }
861.an-look.is-zoom { height: 320vh; height: 320svh; --e: 0; }
862.an-look-stage { position: relative; padding: 80px var(--an-gut); }
863.an-look.is-zoom .an-look-stage { position: sticky; top: 0; height: 100vh; height: 100svh; padding: 0 var(--an-gut); overflow: hidden; display: flex; align-items: center; justify-content: center; }
864.an-look-head { position: relative; z-index: 3; display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; margin-bottom: 32px; font: 500 12px/1.4 var(--an-sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--an-grey); }
865.an-look.is-zoom .an-look-head { position: absolute; left: var(--an-gut); right: var(--an-gut); top: 72px; margin: 0; opacity: calc((var(--e) - 0.75) * 4); transform: translate3d(0, calc((1 - var(--e)) * 30px), 0); }
866.an-look-grid { position: relative; display: grid; grid-template-columns: repeat(12, 1fr); grid-template-rows: repeat(6, minmax(0, 1fr)); gap: 14px; width: 100%; max-width: 1400px; height: min(66vh, 640px); will-change: transform; }
867.an-tile { position: relative; margin: 0; transform: translate3d(0, calc((1 - var(--e)) * var(--k) * 1px), 0); }
868.an-tile-img { width: 100%; height: 100%; overflow: hidden; background: var(--an-studio); }
869.an-tile-cap { position: absolute; left: 0; bottom: -22px; font: 500 11px/1.4 var(--an-sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--an-grey); white-space: nowrap; opacity: calc((var(--e) - 0.8) * 5); }
870.an-tile-0 { grid-column: 1 / 3; grid-row: 1 / 4; }
871.an-tile-1 { grid-column: 1 / 4; grid-row: 4 / 7; }
872.an-tile-2 { grid-column: 3 / 5; grid-row: 1 / 4; }
873.an-tile-3 { grid-column: 5 / 9; grid-row: 2 / 6; }
874.an-tile-4 { grid-column: 9 / 11; grid-row: 1 / 4; }
875.an-tile-5 { grid-column: 11 / 13; grid-row: 1 / 5; }
876.an-tile-6 { grid-column: 9 / 11; grid-row: 4 / 7; }
877.an-tile-1, .an-tile-6 { margin-top: 18px; }
878.an-look-over { position: absolute; z-index: 2; left: 50%; top: 50%; font: 700 15vw/0.8 var(--an-cond); font-stretch: condensed; text-transform: uppercase; letter-spacing: -0.02em; color: #FFFFFF; mix-blend-mode: difference; pointer-events: none; white-space: nowrap; transform: translate3d(-50%, -50%, 0) scale(calc(1 + (1 - var(--e)) * 0.25)); opacity: calc(1 - var(--e) * 2.4); }
879.an-look:not(.is-zoom) .an-look-over { display: none; }
880
881/* SHOP */
882.an-shop { padding: 140px var(--an-gut) 120px; background: var(--an-white); }
883.an-shop-head { display: grid; grid-template-columns: 1.4fr 1fr auto; align-items: end; gap: var(--an-gut); margin-bottom: 72px; }
884.an-shop-head p { max-width: 380px; font: 400 15px/1.55 var(--an-sans); color: var(--an-graphite); }
885.an-grid { display: grid; grid-template-columns: repeat(12, 1fr); column-gap: var(--an-gut); row-gap: 72px; align-items: start; }
886.an-p-a { grid-column: 1 / 6; }
887.an-p-b { grid-column: 7 / 10; margin-top: 160px; }
888.an-p-c { grid-column: 10 / 13; margin-top: 40px; }
889.an-p-d { grid-column: 2 / 5; }
890.an-p-e { grid-column: 6 / 9; margin-top: 120px; }
891.an-p-f { grid-column: 9 / 13; margin-top: -60px; }
892.an-card-media { position: relative; overflow: hidden; background: var(--an-studio); cursor: none; }
893.an-card-img { display: block; aspect-ratio: 4 / 5; overflow: hidden; }
894.an-card-img .an-shot { transition: transform 1.2s var(--fd-ease-out); }
895.an-card-media:hover .an-card-img .an-shot { transform: scale(1.04); }
896.an-tag { position: absolute; left: 14px; top: 14px; padding: 6px 10px; background: var(--an-white); font: 500 11px/1 var(--an-sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--an-ink); }
897.an-card-info { display: flex; justify-content: space-between; gap: 16px; margin-top: 14px; }
898.an-card-name { font: 500 14px/1.35 var(--an-sans); letter-spacing: 0.04em; text-transform: uppercase; color: var(--an-ink); }
899.an-card-fab { margin-top: 3px; font: 400 13px/1.4 var(--an-sans); color: var(--an-grey); }
900.an-card-col { display: inline-block; animation: an-fadein var(--fd-dur-3) var(--fd-ease-out); }
901.an-card-price { font: 500 14px/1.35 var(--an-sans); color: var(--an-ink); font-variant-numeric: tabular-nums; }
902.an-sw { display: flex; gap: 4px; margin-top: 8px; margin-left: -8px; }
903.an-sw button { position: relative; width: 36px; height: 36px; border-radius: 50%; }
904.an-sw button::before { content: ""; position: absolute; inset: 10px; border-radius: 50%; background: var(--c); box-shadow: inset 0 0 0 1px rgba(14, 14, 14, 0.18); transition: inset var(--fd-dur-2) var(--fd-spring); }
905.an-sw button::after { content: ""; position: absolute; inset: 6px; border-radius: 50%; box-shadow: 0 0 0 1px var(--an-ink); opacity: 0; transform: scale(0.7); transition: opacity var(--fd-dur-2) var(--fd-ease-out), transform var(--fd-dur-3) var(--fd-spring); }
906.an-sw button.is-on::after { opacity: 1; transform: none; }
907.an-sw button:hover::before { inset: 9px; }
908@keyframes an-fadein { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
909
910/* Quick add morph */
911.an-qa { position: absolute; left: 12px; right: 12px; bottom: 12px; height: 48px; display: flex; align-items: stretch; background: var(--an-white); clip-path: inset(0 calc(100% - 132px) 0 0); transform: translateY(8px); opacity: 0; transition: clip-path var(--fd-dur-3) var(--fd-ease-out), transform var(--fd-dur-3) var(--fd-ease-out), opacity var(--fd-dur-2) var(--fd-ease-out), background-color var(--fd-dur-3) var(--fd-ease-out); }
912.an-card-media:hover .an-qa, .an-qa:focus-within, .an-qa.is-pick, .an-qa.is-added { opacity: 1; transform: none; }
913.an-qa.is-pick { clip-path: inset(0 0 0 0); }
914.an-qa.is-added { clip-path: inset(0 calc(100% - 132px) 0 0); background: var(--an-ink); color: #FFFFFF; }
915.an-qa-main { flex: none; display: flex; align-items: center; gap: 10px; width: 132px; padding: 0 16px; font: 500 12px/1 var(--an-sans); letter-spacing: 0.08em; text-transform: uppercase; color: inherit; }
916.an-qa-main:disabled { cursor: default; }
917.an-qa-plus { position: relative; width: 12px; height: 12px; flex: none; transition: transform var(--fd-dur-3) var(--fd-ease-out); }
918.an-qa-plus::before, .an-qa-plus::after { content: ""; position: absolute; left: 0; top: 5.25px; width: 12px; height: 1.5px; background: currentColor; transition: transform var(--fd-dur-3) var(--fd-ease-out), width var(--fd-dur-3) var(--fd-ease-out); }
919.an-qa-plus::after { transform: rotate(90deg); }
920.an-qa.is-pick .an-qa-plus { transform: rotate(45deg); }
921.an-qa.is-added .an-qa-plus { transform: rotate(-45deg) translate(1px, -1px); }
922.an-qa.is-added .an-qa-plus::before { width: 6px; top: 8px; left: 0; transform: rotate(90deg) translateX(-3px); transform-origin: left; }
923.an-qa.is-added .an-qa-plus::after { width: 12px; top: 8px; transform: none; }
924.an-qa-sizes { flex: 1; display: grid; grid-template-columns: repeat(5, 1fr); border-left: 1px solid var(--an-line); }
925.an-qa-sizes button { font: 500 12px/1 var(--an-sans); letter-spacing: 0.06em; color: var(--an-ink); text-align: center; opacity: 0; transform: translateX(-8px); transition: opacity var(--fd-dur-2) var(--fd-ease-out), transform var(--fd-dur-3) var(--fd-ease-out), background-color var(--fd-dur-1) var(--fd-ease-out); }
926.an-qa.is-pick .an-qa-sizes button { opacity: 1; transform: none; }
927.an-qa.is-pick .an-qa-sizes button:nth-child(2) { transition-delay: 30ms; }
928.an-qa.is-pick .an-qa-sizes button:nth-child(3) { transition-delay: 60ms; }
929.an-qa.is-pick .an-qa-sizes button:nth-child(4) { transition-delay: 90ms; }
930.an-qa.is-pick .an-qa-sizes button:nth-child(5) { transition-delay: 120ms; }
931.an-qa-sizes button:hover:not(:disabled) { background: var(--an-paper); }
932.an-qa-sizes button:disabled { color: #8D8B86; text-decoration: line-through; }
933
934/* PRESS */
935.an-press { padding: 56px 0; background: var(--an-ink); color: #F2F1EE; overflow: hidden; }
936.an-marquee-track { display: flex; width: max-content; animation: an-marq 60s linear infinite; }
937.an-press:hover .an-marquee-track { animation-play-state: paused; }
938.an-marquee-set { display: flex; }
939.an-marquee-set li { display: flex; align-items: baseline; gap: 18px; padding-right: 18px; white-space: nowrap; }
940.an-marquee-set blockquote { font: 700 clamp(40px, 5vw, 72px)/1 var(--an-cond); font-stretch: condensed; letter-spacing: -0.01em; text-transform: uppercase; }
941.an-marquee-set cite { font: 500 12px/1 var(--an-sans); letter-spacing: 0.1em; text-transform: uppercase; color: #B9B7B2; }
942.an-star { font: 400 36px/1 var(--an-sans); color: #8C8A85; margin-left: 40px; }
943@keyframes an-marq { to { transform: translateX(-50%); } }
944
945/* FEATURE */
946.an-feature { display: grid; grid-template-columns: 7fr 5fr; gap: var(--an-gut); padding: 140px var(--an-gut); background: var(--an-paper); }
947.an-feature-media { position: sticky; top: 76px; align-self: start; cursor: none; }
948.an-feature-img { aspect-ratio: 4 / 5; overflow: hidden; background: var(--an-studio); max-height: calc(100vh - 100px); margin: 0 auto; }
949.an-hot { position: absolute; z-index: 2; display: grid; place-items: center; width: 44px; height: 44px; margin: -22px 0 0 -22px; cursor: pointer; }
950.an-hot span { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 50%; background: #FFFFFF; color: var(--an-ink); font: 500 12px/1 var(--an-sans); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.6); animation: an-ping 2.4s var(--fd-ease-out) infinite; }
951@keyframes an-ping { 0% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.6); } 80%, 100% { box-shadow: 0 0 0 14px rgba(255, 255, 255, 0); } }
952.an-hot em { position: absolute; left: 50%; bottom: calc(100% + 4px); width: 220px; padding: 10px 12px; background: #FFFFFF; color: var(--an-ink); font: 400 13px/1.4 var(--an-sans); text-align: left; opacity: 0; transform: translate(-50%, 6px); pointer-events: none; transition: opacity var(--fd-dur-2) var(--fd-ease-out), transform var(--fd-dur-3) var(--fd-ease-out); }
953.an-hot:hover em, .an-hot:focus-visible em { opacity: 1; transform: translate(-50%, 0); }
954.an-feature-text { padding: 40px 0 0 4vw; }
955.an-feature-text .an-h2 { margin: 20px 0 32px; }
956.an-feature-text .an-body { max-width: 460px; }
957.an-specs { margin-top: 48px; border-top: 1px solid var(--an-ink); }
958.an-specs div { display: grid; grid-template-columns: 110px 1fr; gap: 16px; padding: 16px 0; border-bottom: 1px solid var(--an-line); }
959.an-specs dt { font: 500 12px/1.5 var(--an-sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--an-grey); }
960.an-specs dd { font: 400 15px/1.5 var(--an-sans); color: var(--an-ink); }
961.an-feature-cta { display: flex; align-items: center; gap: 28px; margin-top: 40px; }
962
963/* MATERIALS */
964.an-mat { padding: 140px var(--an-gut); background: var(--an-white); }
965.an-mat-head { display: grid; grid-template-columns: 1fr 2fr; gap: var(--an-gut); margin-bottom: 64px; }
966.an-mat-head .an-h2 { max-width: 14ch; }
967.an-mat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--an-gut); }
968.an-mat-sw { aspect-ratio: 1; overflow: hidden; background: var(--an-studio); }
969.an-tx { display: block; width: 100%; height: 100%; transition: transform 1.2s var(--fd-ease-out); }
970.an-mat-card:hover .an-tx { transform: scale(1.5); }
971.an-tx-hb { background-color: #3A3A3A; }
972.an-tx-hb { background-image: linear-gradient(90deg, transparent 50%, rgba(0,0,0,.06) 50%), repeating-linear-gradient(45deg, rgba(255,255,255,.1) 0 2px, transparent 2px 6px), repeating-linear-gradient(-45deg, rgba(255,255,255,.1) 0 2px, transparent 2px 6px); background-size: 28px 100%, 28px 28px, 28px 28px; }
973.an-tx-knit { background-color: #E6E0D3; background-image: radial-gradient(ellipse 5px 9px at 50% 50%, rgba(0,0,0,.14) 60%, transparent 62%), radial-gradient(ellipse 5px 9px at 50% 50%, rgba(255,255,255,.5) 40%, transparent 42%); background-size: 12px 14px, 12px 14px; background-position: 0 0, 6px 7px; }
974.an-tx-boucle { background-color: #C9C9C6; background-image: radial-gradient(circle at 30% 30%, rgba(255,255,255,.7) 0 2px, transparent 3px), radial-gradient(circle at 70% 60%, rgba(0,0,0,.18) 0 3px, transparent 4px), radial-gradient(circle at 50% 80%, rgba(255,255,255,.5) 0 2.5px, transparent 3.5px), radial-gradient(circle at 20% 70%, rgba(0,0,0,.12) 0 2px, transparent 3px); background-size: 11px 11px, 13px 13px, 9px 9px, 15px 15px; }
975.an-tx-poplin { background-color: #F1EEE7; background-image: repeating-linear-gradient(0deg, rgba(0,0,0,.05) 0 1px, transparent 1px 3px), repeating-linear-gradient(90deg, rgba(0,0,0,.04) 0 1px, transparent 1px 3px), linear-gradient(120deg, rgba(255,255,255,.6), rgba(0,0,0,.06)); }
976.an-mat-name { margin-top: 18px; font: 500 14px/1.35 var(--an-sans); letter-spacing: 0.04em; text-transform: uppercase; color: var(--an-ink); }
977.an-mat-from { margin-top: 4px; font: 400 14px/1.4 var(--an-sans); color: var(--an-graphite); }
978.an-mat-spec { margin-top: 2px; font: 400 13px/1.4 var(--an-sans); color: var(--an-grey); }
979
980/* JOURNAL */
981.an-journal { padding: 140px var(--an-gut); background: var(--an-paper); }
982.an-journal-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; margin-bottom: 56px; }
983.an-journal-grid { display: grid; grid-template-columns: 5fr 3fr 4fr; gap: var(--an-gut); align-items: start; }
984.an-story:nth-child(2) { margin-top: 120px; }
985.an-story:nth-child(3) { margin-top: 40px; }
986.an-story a { display: block; color: var(--an-ink); }
987.an-story-img { aspect-ratio: 4 / 5; overflow: hidden; background: var(--an-studio); }
988.an-story:first-child .an-story-img { aspect-ratio: 5 / 4; }
989.an-story-img .an-scene { transition: transform 1.2s var(--fd-ease-out); }
990.an-story a:hover .an-story-img .an-scene { transform: scale(1.05); }
991.an-story-date { margin-top: 16px; font: 500 12px/1.4 var(--an-sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--an-grey); }
992.an-story-t { margin-top: 8px; font: 400 22px/1.25 var(--an-sans); letter-spacing: -0.02em; color: var(--an-ink); max-width: 30ch; }
993.an-story a:hover .an-story-t { text-decoration: underline; text-underline-offset: 4px; text-decoration-thickness: 1px; }
994
995/* FOOTER */
996.an-foot { padding: 96px var(--an-gut) 28px; background: var(--an-ink); color: #EDEDEA; overflow: hidden; }
997.an-foot-top { display: grid; grid-template-columns: 5fr 7fr; gap: 64px; }
998.an-news-t { font: 700 44px/0.9 var(--an-cond); font-stretch: condensed; text-transform: uppercase; color: #FFFFFF; }
999.an-news-s { margin-top: 14px; max-width: 420px; font: 400 15px/1.55 var(--an-sans); color: #BDBBB6; }
1000.an-news-l { display: block; margin-top: 28px; font: 500 12px/1 var(--an-sans); letter-spacing: 0.08em; text-transform: uppercase; color: #BDBBB6; }
1001.an-news-row { display: flex; margin-top: 10px; border-bottom: 1px solid #EDEDEA; }
1002.an-news-row input { flex: 1; height: 52px; font: 400 18px/1 var(--an-sans); color: #FFFFFF; outline: none; }
1003.an-news-row input::placeholder { color: #8C8A85; }
1004.an-news-row button { padding: 0 4px 0 16px; font: 500 12px/1 var(--an-sans); letter-spacing: 0.1em; text-transform: uppercase; color: #FFFFFF; min-height: 52px; }
1005.an-news-row:focus-within { border-bottom-color: #FFFFFF; box-shadow: 0 1px 0 #FFFFFF; }
1006.an-news-note { margin-top: 10px; font: 400 12px/1.5 var(--an-sans); color: #A7A5A0; }
1007.an-foot-cols { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--an-gut); }
1008.an-foot-h { margin-bottom: 14px; font: 500 12px/1 var(--an-sans); letter-spacing: 0.08em; text-transform: uppercase; color: #A7A5A0; }
1009.an-foot-cols li { margin-bottom: 8px; font: 400 14px/1.45 var(--an-sans); color: #EDEDEA; }
1010.an-foot-cols a:hover { text-decoration: underline; text-underline-offset: 3px; }
1011.an-foot-word { margin: 96px -0.02em 0; font: 700 16.2vw/0.78 var(--an-cond); font-stretch: condensed; letter-spacing: -0.02em; color: #FFFFFF; white-space: nowrap; text-align: center; }
1012.an-foot-legal { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px; margin-top: 28px; padding-top: 18px; border-top: 1px solid rgba(237, 237, 234, 0.2); font: 400 12px/1.5 var(--an-sans); color: #A7A5A0; }
1013.an-foot-legal span:last-child { display: flex; flex-wrap: wrap; gap: 18px; }
1014.an-foot-legal a:hover { color: #FFFFFF; }
1015
1016/* DRAWER */
1017.an-scrim { position: fixed; inset: 0; z-index: 95; background: rgba(14, 14, 14, 0.4); opacity: 0; pointer-events: none; transition: opacity var(--fd-dur-3) var(--fd-ease-out); }
1018.an-scrim.is-on { opacity: 1; pointer-events: auto; }
1019.an-drawer { position: fixed; top: 0; right: 0; bottom: 0; z-index: 100; display: flex; flex-direction: column; width: min(440px, 100vw); background: var(--an-white); box-shadow: -30px 0 60px -30px rgba(0, 0, 0, 0.35); will-change: transform; visibility: hidden; transition: visibility 0s linear 600ms; }
1020.an-drawer.is-open { visibility: visible; transition: none; }
1021.an-drawer-head { display: flex; align-items: center; justify-content: space-between; height: 72px; padding: 0 var(--an-gut); border-bottom: 1px solid var(--an-line); }
1022.an-drawer-t { font: 700 32px/1 var(--an-cond); font-stretch: condensed; text-transform: uppercase; color: var(--an-ink); }
1023.an-drawer-t span { color: var(--an-grey); }
1024.an-x { position: relative; width: 44px; height: 44px; margin-right: -10px; }
1025.an-x span { position: absolute; left: 12px; right: 12px; top: 21px; height: 1.5px; background: var(--an-ink); transform: rotate(45deg); }
1026.an-x span:last-child { transform: rotate(-45deg); }
1027.an-ship { padding: 16px var(--an-gut); border-bottom: 1px solid var(--an-line); font: 400 13px/1.4 var(--an-sans); color: var(--an-graphite); }
1028.an-ship-bar { position: relative; display: block; height: 2px; margin-top: 10px; background: var(--an-line); }
1029.an-ship-bar i { position: absolute; inset: 0; background: var(--an-ink); transform-origin: left; transition: transform var(--fd-dur-4) var(--fd-ease-out); }
1030.an-lines { flex: 1; overflow-y: auto; padding: 8px var(--an-gut); }
1031.an-empty { padding: 40px 0; font: 400 15px/1.5 var(--an-sans); color: var(--an-grey); }
1032.an-line { display: grid; grid-template-columns: 84px 1fr auto; gap: 16px; padding: 16px 0; border-bottom: 1px solid var(--an-line); animation: an-fadein var(--fd-dur-3) var(--fd-ease-out); }
1033.an-line-img { aspect-ratio: 4 / 5; overflow: hidden; background: var(--an-studio); }
1034.an-line-name { font: 500 13px/1.35 var(--an-sans); letter-spacing: 0.04em; text-transform: uppercase; color: var(--an-ink); }
1035.an-line-meta { margin-top: 4px; font: 400 13px/1.4 var(--an-sans); color: var(--an-grey); }
1036.an-qty { display: inline-flex; align-items: center; margin-top: 12px; border: 1px solid var(--an-line); }
1037.an-qty button { width: 36px; height: 36px; font: 400 16px/1 var(--an-sans); color: var(--an-ink); text-align: center; }
1038.an-qty button:hover { background: var(--an-paper); }
1039.an-qty span { min-width: 24px; font: 500 13px/1 var(--an-sans); text-align: center; font-variant-numeric: tabular-nums; }
1040.an-line-price { font: 500 13px/1.35 var(--an-sans); color: var(--an-ink); font-variant-numeric: tabular-nums; }
1041.an-drawer-foot { padding: 20px var(--an-gut) 24px; border-top: 1px solid var(--an-line); }
1042.an-sub { display: flex; justify-content: space-between; font: 500 14px/1 var(--an-sans); letter-spacing: 0.06em; text-transform: uppercase; color: var(--an-ink); }
1043.an-sub-note { margin: 8px 0 16px; font: 400 12px/1.4 var(--an-sans); color: var(--an-grey); }
1044
1045/* RESPONSIVE */
1046@media (max-width: 1080px) {
1047 .an-nav-l a, .an-nav-txt { display: none; }
1048 .an-burger { display: block; }
1049 .an-mast-img { width: 32vw; right: 6vw; }
1050 .an-shop-head { grid-template-columns: 1fr 1fr; }
1051 .an-shop-head .an-link { grid-column: 2; }
1052 .an-mat-grid { grid-template-columns: repeat(2, 1fr); row-gap: 40px; }
1053 .an-foot-top { grid-template-columns: 1fr; }
1054}
1055@media (max-width: 760px) {
1056 .an { --an-gut: 16px; }
1057 .an-announce { font-size: 11px; }
1058 .an-nav-in { height: 60px; }
1059 .an-logo { font-size: 24px; }
1060 .an-nav.is-small .an-logo { font-size: 20px; }
1061 .an-mast { min-height: 0; padding-top: 16px; grid-template-rows: auto auto auto auto; }
1062 .an-mast-meta-r { display: none; }
1063 .an-mast-img { position: relative; right: auto; top: auto; grid-row: 2; width: 72%; margin: 20px 0 0 auto; transform: translate3d(0, calc(var(--p) * 60px), 0); }
1064 .an-word { font-size: 25.4vw; margin-top: -18vw; }
1065 .an-mast-foot { flex-direction: column; align-items: flex-start; gap: 18px; }
1066 .an-mast-foot .an-btn { width: 100%; }
1067 .an-statement { grid-template-columns: 1fr; padding: 96px var(--an-gut); }
1068 .an-statement-t { text-indent: 0; font-size: 28px; }
1069 .an-look-stage { padding: 72px var(--an-gut); }
1070 .an-look-head { flex-direction: column; align-items: flex-start; gap: 8px; }
1071 .an-look-grid { grid-template-columns: repeat(2, 1fr); grid-template-rows: none; grid-auto-rows: 200px; height: auto; gap: 10px; }
1072 .an-tile { grid-column: auto !important; grid-row: auto !important; margin: 0 !important; transform: none; }
1073 .an-tile-3 { grid-column: 1 / -1 !important; grid-row: span 2 !important; order: -1; }
1074 .an-tile-cap { position: static; margin-top: 6px; opacity: 1; }
1075 .an-look-grid { row-gap: 34px; }
1076 .an-shop { padding: 96px var(--an-gut); }
1077 .an-shop-head { grid-template-columns: 1fr; gap: 16px; margin-bottom: 48px; }
1078 .an-shop-head .an-link { grid-column: auto; }
1079 .an-grid { grid-template-columns: 1fr 1fr; column-gap: 10px; row-gap: 40px; }
1080 .an-card { grid-column: auto !important; margin-top: 0 !important; }
1081 .an-p-a, .an-p-f { grid-column: 1 / -1 !important; }
1082 .an-card-name { font-size: 12px; }
1083 .an-card-fab { font-size: 12px; }
1084 .an-card-info { flex-direction: column; gap: 4px; }
1085 .an-qa { opacity: 1; transform: none; left: 8px; right: 8px; bottom: 8px; height: 44px; clip-path: inset(0 calc(100% - 44px) 0 0); }
1086 .an-qa-main { width: 44px; padding: 0 16px; }
1087 .an-qa-txt { display: none; }
1088 .an-qa.is-pick { clip-path: inset(0 0 0 0); height: auto; flex-wrap: wrap; }
1089 .an-qa.is-pick .an-qa-sizes { flex-basis: 100%; grid-template-columns: repeat(3, 1fr); border-left: 0; border-top: 1px solid var(--an-line); }
1090 .an-qa.is-pick .an-qa-sizes button { min-height: 44px; }
1091 .an-qa.is-added { clip-path: inset(0 calc(100% - 44px) 0 0); }
1092 .an-mast-img figcaption { display: none; }
1093 .an-journal-head .an-link { white-space: nowrap; }
1094 .an-card-media { cursor: auto; }
1095 .an-feature { grid-template-columns: 1fr; padding: 96px var(--an-gut); }
1096 .an-feature-media { position: relative; top: auto; }
1097 .an-feature-text { padding: 32px 0 0; }
1098 .an-feature-cta { flex-direction: column; align-items: stretch; gap: 8px; }
1099 .an-hot em { width: 180px; }
1100 .an-mat { padding: 96px var(--an-gut); }
1101 .an-mat-head { grid-template-columns: 1fr; gap: 16px; margin-bottom: 40px; }
1102 .an-mat-grid { gap: 10px; row-gap: 32px; }
1103 .an-journal { padding: 96px var(--an-gut); }
1104 .an-journal-grid { grid-template-columns: 1fr; gap: 40px; }
1105 .an-story:nth-child(n) { margin-top: 0; }
1106 .an-story-t { font-size: 20px; }
1107 .an-foot { padding-top: 72px; }
1108 .an-foot-cols { grid-template-columns: 1fr 1fr; row-gap: 32px; }
1109 .an-foot-word { margin-top: 64px; }
1110 .an-specs div { grid-template-columns: 90px 1fr; }
1111}
1112@media (hover: none) {
1113 .an-cursor { display: none; }
1114 .an-card-media, .an-feature-media { cursor: auto; }
1115 .an-qa { opacity: 1; transform: none; }
1116}
1117@media (prefers-reduced-motion: reduce) {
1118 .an *, .an *::before, .an *::after { animation-duration: 1ms !important; animation-iteration-count: 1 !important; animation-delay: 0ms !important; transition-duration: 120ms !important; transition-delay: 0ms !important; }
1119 .an-ch, .an-mast-img, .an-mast-img-in .an-scene, .an-tile { transform: none !important; }
1120 .an-js [data-an-rv] { transform: none; }
1121 .an-marquee-track { animation: none !important; width: auto; }
1122 .an-marquee-set[aria-hidden="true"] { display: none; }
1123 .an-marquee-set { flex-direction: column; gap: 24px; padding: 0 var(--an-gut); }
1124 .an-marquee-set li { white-space: normal; flex-wrap: wrap; }
1125 .an-star { display: none; }
1126}
1127@media (prefers-contrast: more) {
1128 .an-nav.is-small .an-nav-in { background: var(--an-paper); -webkit-backdrop-filter: none; backdrop-filter: none; border-bottom-color: var(--an-ink); }
1129 .an-grain::after { display: none; }
1130}
1131@media (prefers-reduced-transparency: reduce) {
1132 .an-nav.is-small .an-nav-in { background: var(--an-paper); -webkit-backdrop-filter: none; backdrop-filter: none; }
1133}
1134`;

The design brief. For you and your AI agent.

Tokens, type, layout, components and motion — written from the real values in the file, so a model can extend the page without drifting.

Atelier Nord — DESIGN.md

A Scandinavian fashion e-commerce storefront: monochrome high fashion with giant condensed uppercase type, an art-directed asymmetric 12-column grid, and pre-rendered fabric photography (draped herringbone, cable knit, twill, poplin, check and leather, plus a CSS film-grain layer). It uses no web fonts.

All CSS lives in Page.tsx under the .an prefix. Every element is reset explicitly, so the page is independent of host resets.

1. Palette

RoleTokenHex
Page paper--an-paper#F2F1EE
White bands (shop, materials, drawer)--an-white#FFFFFF
Studio backdrop behind products--an-studio#E4E2DD
Ink (type, buttons, footer, marquee)--an-ink#0E0E0E
Body text--an-graphite#2A2A2A
Meta / captions--an-grey#66645F
Hairlines--an-linergba(14,14,14,0.14)
Footer secondary text—#A7A5A0 / #BDBBB6

Product colourways (the only "colour", all desaturated):

ColourwayHex
Ecru#E6E0D3
Stone#A8A39A
Charcoal#3A3A3A
Black#141414
Fog#C9C9C6
Moss#55574C

Contrast: ink on paper is about 18:1, --an-grey on paper about 5.4:1, #A7A5A0 on ink about 7.6:1.

2. Typography

  • Condensed display (--an-cond): "HelveticaNeue-CondensedBold", "Helvetica Neue Condensed Bold", "Helvetica Neue Condensed", "Arial Narrow", "Roboto Condensed", "Nimbus Sans Narrow", "TeX Gyre Heros Cn", "Liberation Sans Narrow", ui-sans-serif, sans-serif. Weight 700, font-stretch: condensed, uppercase.
  • Text (--an-sans): "Helvetica Neue", Helvetica, ui-sans-serif, -apple-system, "Inter", "Segoe UI", Roboto, Arial, sans-serif.
StyleSpec
Masthead wordcondensed 700 / 0.78, −0.02em. Fitted by JS to the content width (min(width-fit, (vh − 330) / 1.56) on desktop; CSS fallback 22.6vw)
H2 XL (New in, Journal, feature)clamp(64px, 9vw, 150px) / 0.82, −0.025em
H2clamp(40px, 5vw, 76px) / 0.88, −0.015em
Marquee quoteclamp(40px, 5vw, 72px) / 1
Footer wordmark16.2vw / 0.78
Statementsans 400, clamp(28px, 3.4vw, 52px) / 1.12, −0.03em, 12% text-indent
Product namesans 500, 14px, 0.04em, uppercase
Labels / nav / buttonssans 500, 12–13px, 0.08–0.1em, uppercase
Bodysans 400, 15–16px / 1.55–1.6

3. Grid, spacing, shape

  • Full-bleed with a gutter of --an-gut (24px; 16px at ≤760px).
  • New in uses an art-directed 12-column grid. Each card has an explicit column span and vertical offset (.an-p-a … .an-p-f): coat 1/6, knit 7/10 +160px, trouser 10/13 +40px, shirt 2/5, scarf 6/9 +120px, bag 9/13 −60px.
  • The Lookbook grid is 12 columns × 6 rows, with 7 tiles of different spans around a 4×4 centre tile.
  • Journal is 5fr / 3fr / 4fr with staggered offsets.
  • Shape is square: radius 0 everywhere. The exceptions are the bag count, swatches and cursor label (circles) and the inline statement image (capsule).
  • No shadows except the cart drawer (-30px 0 60px -30px rgba(0,0,0,.35)).
  • Film grain: an SVG feTurbulence data-URI at 20% multiply over imagery (.an-grain).

4. Motion tokens

--fd-ease-out    cubic-bezier(0.22, 1, 0.36, 1)
--fd-ease-in-out cubic-bezier(0.65, 0, 0.35, 1)
--fd-spring      cubic-bezier(0.34, 1.56, 0.64, 1)
--fd-dur-1 120ms · --fd-dur-2 240ms · --fd-dur-3 420ms · --fd-dur-4 700ms
Physics spring (drawer, cursor label): stiffness 380, damping 30, mass 1 — integrated in rAF.

Choreography

  1. Load. Masthead letters rise out of their masks (1.1s, 45ms stagger per letter across both words).
  2. Masthead scrub. --p is the scroll progress through the masthead. Every letter has its own --dy (ATELIER moves up and NORD moves down, 40–150px) and --rot (−4°…+4°), so the word shears apart as you leave. The word uses mix-blend-mode: difference (white), so it reads as ink on paper and inverts where it crosses the cover photo. The cover image drifts down 120px and its scene scales from 1.12 to 1.24.
  3. Statement. Paragraph reveal; an inline capsule image of the coat lapel sits inside the sentence.
  4. Zoom-parallax lookbook. The section is 320vh with a sticky 100vh stage.
  5. At p=0 the grid is scaled so the centre tile fills the viewport, and translated to centre it.
  6. Over the first 78% of scroll (easeOutCubic e) it pulls back to scale 1 and settles into the asymmetric grid.
  7. Surrounding tiles arrive from different depths (--k: −160…+200px × (1−e)).
  8. A "Stillhet" overlay in difference blend fades out, and the heading and captions fade in at the end.
  9. Product cards.
  10. Swatch → fabric fill cross-fades (700ms in-out); the colour name fades in.
  11. Hover zooms the image 1.04 (1.2s) and reveals Quick add (fade plus 8px rise).
  12. Quick-add morph: a white bar is clipped to 132px; on click clip-path opens to full width (420ms) and the sizes slide in (30ms stagger) while the + rotates to ×.
  13. Choosing a size collapses the bar back to 132px in ink with a check ("Added"), bounces the bag count (600ms spring), then opens the drawer 650ms later.
  14. Cart drawer. Its translateX goes from 104% to 0 through the physics spring (with a slight overshoot). The scrim fades (420ms). Focus moves to Close, Tab is trapped, Escape closes, and focus returns to the opener. The free-shipping bar scales to the subtotal against €400.
  15. Cursor "View" label. An 84px ink circle follows the mouse through the spring over [data-an-view] imagery (scale 0→1 with the spring). It hides over buttons so it never covers controls, and is disabled on touch.
  16. Press marquee. Two sets translate −50% over 60s; pauses on hover.
  17. Feature hotspots. Pulsing white dots (2.4s ping); the tooltip rises in on hover or focus.
  18. Materials. Fabric swatches zoom 1.5 on hover (1.2s), like a loupe.

5. Sections

  1. Announcement bar (ink).
  2. Sticky condensing nav. 72px → 52px, translucent paper with a hairline; the logo shrinks from 30px to 22px and its tracking widens. Bag button with an animated count.
  3. Masthead. Collection meta, cover image (Look 07), the fitted giant word, an intro line and CTA.
  4. Statement. (01) "Stillhet" paragraph with an inline image.
  5. Lookbook. Zoom-parallax pinned grid of 7 editorial scenes (studio, fjord, birch forest, knit stack, lapel detail, window still, tote).
  6. New in. Six products in the asymmetric grid with swatches and quick add.
  7. Press marquee. Four editorial quotes.
  8. Feature. Sticky Sund Overcoat image with 3 hotspots, a spec table and an add-to-bag button.
  9. Materials. Four mills with CSS-pattern swatches.
  10. Journal. Three stories in a staggered grid.
  11. Footer. Newsletter (underline input), stores, help, atelier, follow, a giant wordmark, and legal.
  12. Cart drawer (dialog).

6. Components and states

  • Buttons. 52px, square, uppercase 13px. Ink fill; hover #333; press 0.97; disabled #9A9893.
  • Text link. 1px underline that wipes out to the right on hover.
  • Swatch. 36px hit area with a 16px dot and a ring that springs in when aria-pressed="true".
  • Quick add. States: hidden (desktop, no hover) → bar → .is-pick (sizes, sold-out XS on the coat struck through) → .is-added (ink + check). One-size items (scarf, bag) add directly.
  • Cart line. Thumbnail, name, colour · size, quantity stepper (removing the last unit deletes the line), price.
  • Newsletter. Underline field; after success the note reads back the address.

7. Responsive

  • ≤1080px: burger replaces the text nav; the shop header becomes 2 columns; materials go 2-up; the footer stacks.
  • ≤760px:
  • the cover image moves into the flow (72% width, right-aligned) with the word overlapping it from below
  • the lookbook stops pinning and becomes a 2-column grid with the fjord tile full-width first
  • the shop becomes 2 columns with coat and bag full-width
  • quick add shows as a 44px + button and opens into a 3×2 size grid with 44px cells
  • the feature image is no longer sticky
  • the journal goes to 1 column.
  • (hover: none): no cursor label; quick add is always visible.

8. Accessibility and reduced motion

  • Skip link and landmarks. The H1 is aria-label="Atelier Nord", with its split letters aria-hidden.
  • Product links carry name, colour and price. Swatches have aria-label and aria-pressed; size buttons are labelled (including "sold out").
  • An aria-live region announces bag additions.
  • The drawer is role="dialog" with aria-modal, focus trap, Escape to close and focus return.
  • Focus ring: 2px ink at 70%.
  • prefers-reduced-motion:
  • no letter scrub, cover drift or lookbook pin/zoom
  • the marquee becomes a static list
  • the drawer and cursor snap instead of springing
  • transitions are capped at 120ms.
  • prefers-contrast: more removes the grain and makes the nav opaque.
  • Fixes the html+body overflow host quirk that breaks position: sticky, and restores it on unmount.

9. Do / Don't

  • Do let type be the image: one giant condensed word per major section, with everything else small and tracked.
  • Do keep it monochrome. Colour exists only in the product colourways, and only in greys and naturals.
  • Do break the grid on purpose (offsets, spans) while keeping every edge on a column line.
  • Don't round corners or add card shadows.
  • Don't let the cursor label cover a control, or show it on touch devices.
  • Don't animate layout; scrub and zoom are transform-only and driven by one rAF scroll loop.

Assets

Product and editorial images are pre-rendered fabric studies in public/img/lux/: dr-coat, dr-knit, dr-trouser, dr-shirt, dr-scarf, dr-bag (neutral grey, recoloured per swatch with mix-blend-mode: overlay over the swatch colour), plus dunes-mono and still-grey for editorial tiles (each with a -800 variant). Swap in real packshots by replacing the files; people portraits, if you add any, live in public/img/people/p1…p9.webp.

Questions about Atelier Nord

Scandinavian fashion e-commerce — monochrome, condensed, art-directed. It is a free fashion / e-commerce landing page for React with a matching DESIGN.md brief.

Open the page, click Copy full source or Download .tsx, and drop the single Page file into your React project. Copy the public/img folder next to it for the demo photography, or swap in your own images.

Yes. It is MIT-licensed: free for personal and commercial projects, no signup and no attribution required.

Yes. The DESIGN.md describes tokens, type scale, layout, components and motion so Claude Code, Cursor, Lovable or v0 can extend the page without drifting from its look.