/* global React */ const { useEffect, useRef, useState } = React; /* ---------- Nav ---------- */ function Nav() { const { t } = useLang(); return ( ); } /* ---------- Hero (parallax landscape) ---------- */ function Hero() { const { t } = useLang(); const sceneRef = useRef(null); useEffect(() => { const scene = sceneRef.current; if (!scene) return; const field = scene.querySelector(".px-layer--vineyard"); if (!field) return; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; let raf = 0; let mx = 0, my = 0, tmx = 0, tmy = 0; // Only the field (vineyard) animates — it drifts and scales from its centre. const apply = () => { mx += (tmx - mx) * 0.08; my += (tmy - my) * 0.08; const sc = window.scrollY || window.pageYOffset || 0; const scrollShift = sc * 0.1; const grow = Math.min(sc, 600) / 600 * 0.06; const mxShift = mx * 22; const myShift = my * 10; field.style.transform = `translate3d(${mxShift.toFixed(1)}px, ${(myShift - scrollShift).toFixed(1)}px, 0) scale(${(1.08 + grow).toFixed(3)})`; const settling = Math.abs(tmx - mx) > 0.001 || Math.abs(tmy - my) > 0.001; raf = settling ? requestAnimationFrame(apply) : 0; }; const kick = () => { if (!raf) raf = requestAnimationFrame(apply); }; const onScroll = () => kick(); const onMove = (e) => { if (reduce) return; const r = scene.getBoundingClientRect(); tmx = (e.clientX - r.left) / r.width - 0.5; tmy = (e.clientY - r.top) / r.height - 0.5; kick(); }; apply(); requestAnimationFrame(() => scene.classList.add("is-in")); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); if (!reduce) window.addEventListener("mousemove", onMove); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); window.removeEventListener("mousemove", onMove); if (raf) cancelAnimationFrame(raf); }; }, []); return (
Vineyard landscape A studio looking out over the vineyard

{t.hero.headPre}{t.hero.headRoman}.

{t.hero.lede}

{t.hero.cta}
{t.hero.illusCredit}
); } /* ---------- Marquee ---------- */ function Marquee() { const { t } = useLang(); const items = t.marquee; const icons = [ "images/icon-bottle.png", "images/icon-grapes.png", "images/icon-drop.png", "images/icon-glass.png"]; const row = <> {items.map((w, i) => {w} )} ; return (
{row}{row}{row}
); } /* ---------- Section header helper ---------- */ /* Splits heading children on
into individually-masked lines */ function MaskLines({ children }) { const lines = []; let cur = []; React.Children.forEach(children, (ch) => { if (ch && ch.type === "br") { lines.push(cur); cur = []; } else { cur.push(ch); } }); if (cur.length) lines.push(cur); return lines.map((line, i) => {line} ); } function SectionHead({ num, children }) { return (
{num}

{children}

); } /* ---------- About ---------- */ /* ---------- About + Week (merged section) ---------- */ const PROCESS = [ { day: "Monday", title: "Immersion", desc: "We walk the vines at dawn, sit with the story, and work out who the wine is really for. ", loc: "Day one" }, { day: "Tuesday", title: "Strategy", desc: "We settle on what the wine is and — more usefully — what it isn't. ", loc: "Day two" }, { day: "Wednesday", title: "Identity", desc: "Logo and identity, drawn by hand, then built into type, grid and palette. A name turns up too, if you need one.", loc: "Day three" }, { day: "Thursday", title: "Packaging", desc: "Label, capsule, neck, case. We mock the bottle up at full size and carry it through every light in the building.", loc: "Day four" }, { day: "Friday", title: "Refinement", desc: "Edits made standing up. Proofs pulled, papers tested wet, the whole system written into a printed dossier left on your table.", loc: "Day five" }, { day: "Sat / Sun", title: "Celebration", desc: "The work rests. We open a bottle of yours, mocked up at scale in its new dress, with the family and the harvest crew. A long table, a short speech.", loc: "Day six & seven", weekend: true }]; function ProcessCarousel() { const { t } = useLang(); const PROCESS = t.week.cards; const trackRef = useRef(null); const scrollBy = (dir) => { const el = trackRef.current; if (!el) return; const step = el.clientWidth * 0.7; el.scrollBy({ left: dir * step, behavior: "smooth" }); }; return (
{t.week.eyebrowPrefix}{t.week.taglineB}

{t.week.title}

{PROCESS.map((row, i) =>
{row.day} {row.title}. {row.desc} {row.loc}
)}
{t.week.end.day} {t.week.end.title}. {t.week.end.desc} {t.week.end.loc}
{t.week.hint}
); } function About() { const { t } = useLang(); return (
{brLines(t.about.heading)}
{t.about.ledeA}{t.about.ledeEm}{t.about.ledeB}

{t.about.p1}

{t.about.p2}

{t.about.mono}

); } /* ---------- Philosophy ---------- */ function Philosophy() { const { t } = useLang(); return (
{t.philosophy.l1a}{t.philosophy.em1.replace(/ /g, "\u00a0")}.
{t.philosophy.l2a}{t.philosophy.em2.replace(/ /g, "\u00a0")}.
{t.philosophy.attrib}
); } /* ---------- Team ---------- */ function Team() { const { t } = useLang(); return (
{brLines(t.team.heading)}

{t.team.ledePre} andstudio {t.team.ledePost1}
{t.team.ledePost2}

{t.team.sub}

The andstudio team
{t.team.figcaption}
); } /* ---------- Comparison (Wine Exchange vs Agency) ---------- */ function Comparison() { const { t } = useLang(); const COMPARE = t.compare.rows; return (
{brLines(t.compare.heading)}
{t.compare.oursHead}
    {COMPARE.map((r, i) =>
  • {r.ours}
  • )}
{t.compare.theirsHead}
    {COMPARE.map((r, i) =>
  • {r.theirs}
  • )}
); } /* ---------- Two Paths (winery / studio open call) ---------- */ function TwoPaths() { const { t } = useLang(); return (
{t.paths.heading}
{t.paths.tag}

{t.paths.lede}

    {t.paths.list.map((item, i) =>
  • {String(i + 1).padStart(2, "0")} {item}
  • )}
{t.paths.footLabel} {t.paths.footLine} {t.paths.cta}
); } /* ---------- CTA ---------- */ function CtaBlock() { const { t } = useLang(); return (
{t.cta.eyebrow}

{t.cta.headPre}{t.cta.headRoman}
{t.cta.headPost}.

{t.cta.p}

{t.cta.small}
{t.cta.cta}
); } /* ---------- Footer ---------- */ function Footer() { const { t } = useLang(); return ( ); } /* ---------- Tweaks ---------- */ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "palette": "light", "grain": 0.04 } /*EDITMODE-END*/; function TweaksUI() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); useEffect(() => { if (t.palette === "light") document.body.removeAttribute("data-palette");else document.body.setAttribute("data-palette", t.palette); }, [t.palette]); useEffect(() => { document.documentElement.style.setProperty("--grain", t.grain); }, [t.grain]); return ( setTweak("palette", v)} options={[ { value: "light", label: "Light" }, { value: "black", label: "Black" }, { value: "oxblood", label: "Oxblood" }] } /> setTweak("grain", v / 100)} min={0} max={20} step={1} unit="%" /> ); } /* ---------- Cookie / Privacy Banner ---------- */ function PrivacyBanner() { const { t } = useLang(); const [dismissed, setDismissed] = useState(() => { try { return localStorage.getItem("we-privacy-ack") === "1"; } catch (_) { return false; } }); const [open, setOpen] = useState(false); if (dismissed) return null; const accept = () => { try {localStorage.setItem("we-privacy-ack", "1");} catch (_) {} setDismissed(true); }; return (

{t.privacy.copy}

{open &&

{t.privacy.detail} hello@winedesign.exchange.

}
); } /* ---------- App ---------- */ function App() { // Apply defaults on first mount useEffect(() => { if (TWEAK_DEFAULTS.palette !== "light") { document.body.setAttribute("data-palette", TWEAK_DEFAULTS.palette); } document.documentElement.style.setProperty("--grain", TWEAK_DEFAULTS.grain); }, []); // Scroll-driven reveals + title parallax (rAF; reliable across environments) useEffect(() => { const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const fades = Array.from(document.querySelectorAll(".reveal")); const titles = Array.from(document.querySelectorAll("[data-reveal]")); const parallax = Array.from(document.querySelectorAll("[data-parallax]")); let raf = 0; const tick = () => { const vh = window.innerHeight; const trigger = vh * 0.88; fades.forEach((el) => { if (!el.classList.contains("in") && el.getBoundingClientRect().top < trigger) { el.classList.add("in"); } }); titles.forEach((el) => { if (!el.classList.contains("is-in") && el.getBoundingClientRect().top < trigger) { el.classList.add("is-in"); } }); if (!reduce) { parallax.forEach((n) => { const f = parseFloat(n.getAttribute("data-parallax")) || 0.04; const r = n.getBoundingClientRect(); const center = r.top + r.height / 2; const off = (center - vh / 2) * -f; n.style.setProperty("--py", `${off.toFixed(1)}px`); }); } raf = 0; }; const onScroll = () => { if (!raf) raf = requestAnimationFrame(tick); }; // Defer the first reveal pass two frames so on-load (above-the-fold) // titles paint their hidden state first, then animate in — otherwise the // class lands in the same frame as first paint and the transition is skipped. requestAnimationFrame(() => requestAnimationFrame(tick)); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); // Safety net: if rAF is throttled (e.g. tab not focused on load), make sure // titles never stay hidden — reveal everything after a short delay. When the // tab is active this still animates (adding the class triggers the transition). const safety = setTimeout(() => { document.querySelectorAll("[data-reveal]").forEach((el) => el.classList.add("is-in")); document.querySelectorAll(".reveal").forEach((el) => el.classList.add("in")); }, 1400); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); clearTimeout(safety); if (raf) cancelAnimationFrame(raf); }; }, []); return ( <>