// FRZN — sections (i18n aware)
const { useState, useEffect, useRef } = React;
// Scroll reveal hook
const useReveal = (stagger = false) => {
const ref = useRef(null);
useEffect(() => {
const el = ref.current; if (!el) return;
const io = new IntersectionObserver(([e]) => {
if (e.isIntersecting) { el.classList.add('in'); io.unobserve(el); }
}, { threshold: 0.15 });
el.classList.add(stagger ? 'reveal-stagger' : 'reveal');
io.observe(el);
return () => io.disconnect();
}, []);
return ref;
};
const Reveal = ({ children, stagger, as: As = 'div', ...rest }) => {
const ref = useReveal(stagger);
return {children};
};
// render array headline with italic accent words
const renderTitle = (arr, accentIdx = [1]) => arr.map((w, i) =>
accentIdx.includes(i)
? {w}{' '}
: {w}{' '}
);
// ------------ HERO ------------
const Hero = ({ t, copyKey, onWaitlist }) => {
const [waterTemp, setWaterTemp] = useState(4);
useEffect(() => {
const id = setInterval(() => {
setWaterTemp(x => Math.max(1.5, Math.min(6.5, x + (Math.random() - 0.5) * 0.4)));
}, 1600);
return () => clearInterval(id);
}, []);
const h = t.hero.headline[copyKey];
const eyebrow = t.hero.eyebrow[copyKey];
const sub = t.hero.sub[copyKey];
const markerPos = `${Math.min(100, Math.max(0, (waterTemp / 30) * 100))}%`;
const accentIdx = copyKey === 'thermal' ? 1 : copyKey === 'vitality' ? 5 : 1;
return (
{t.hero.metaSpec}{t.hero.metaSpecV}
{t.hero.metaCond}{t.hero.metaCondV}
{t.hero.metaTarget}{t.hero.metaTargetV}
{t.hero.metaRelease}{t.hero.metaReleaseV}
{eyebrow}
{h.map((w, i) => {
if (i === accentIdx) return {w} ;
if (i === 4 && copyKey === 'thermal') return {w} ;
return {w} ;
})}
{sub}
{t.hero.prop1Title}
{t.hero.prop1}
{t.hero.prop2Title}
{t.hero.prop2}
FR-001 / SHORTS
THERMAL CAPTURE · 3.2 μm
REV 04
{waterTemp.toFixed(1)}°C AMB
{t.liveLabel} · THERMAL READ{waterTemp.toFixed(1)}°C · CLASS III
0°C10°C20°C30°C
{Array.from({ length: 2 }).map((_, r) => (
{t.marquee.map((m, i) => {m})}
))}
);
};
// ------------ SECTION HEAD ------------
const SectionHead = ({ num, label, title, accentIdx = [1] }) => (
§ {num}{label}
{renderTitle(title, accentIdx)}
);
// ------------ SCIENCE ------------
const Science = ({ t }) => (
{t.science.cards.map((c, i) => (
{c.stat[0]}{c.stat.slice(1)}
{c.title}
))}
);
// ------------ MECHANISM ------------
const Mechanism = ({ t }) => {
const [active, setActive] = useState(3);
return (
{t.mech.layers.map((l, i) => (
))}
);
};
// ------------ PRODUCT CARD ------------
const ProductCard = ({ product, onAdd, lang, t }) => {
const [size, setSize] = useState(product.sizes.find(s => !product.unavailable.includes(s)));
const [added, setAdded] = useState(false);
const Thermal = window[product.visual];
const handleAdd = () => {
onAdd({ ...product, size });
setAdded(true); setTimeout(() => setAdded(false), 1200);
};
const title = product.name;
const tagline = product.tagline[lang] || product.tagline.en;
const desc = product.desc[lang] || product.desc.en;
return (
{product.id} · REV 04
{product.thermal} · {product.gsm}
{tagline}{product.lat}
{title}
{desc}
{product.sizes.map(s => (
))}
{product.was && €{product.was}}€{product.price}
{t.products.stock}
);
};
// ------------ PRODUCTS ------------
const Products = ({ t, lang, onAdd }) => {
const top3 = CATALOG.slice(0, 3);
return (
{top3.map(p => )}
{t.products.refillTitle}
{t.products.refill}
);
};
// ------------ RITUAL ------------
const Ritual = ({ t }) => (
{t.ritual.phases.map((p, i) => (
{t.ritual.phase} {p[0]}
{p[4]}
{p[3]}
))}
);
// ------------ AMBASSADORS ------------
const Ambassadors = ({ t }) => (
{t.field.quotes.map((q, i) => (
))}
{['Men\u2019s Health','GQ','Monocle','WIRED','Kinfolk','Hypebeast'].map(n => ({n}))}
);
// ------------ GUARANTEE ------------
const Guarantee = ({ t }) => (
§ 06{t.guarantee.title.toUpperCase()}
{t.guarantee.title}
{t.guarantee.items.map((g, i) => (
{['60','∞','↻','−CO₂'][i]}
))}
);
// ------------ SPECS ------------
const Specs = ({ t }) => (
{t.specs.rows.map((r, i) => (
{r[0]}
{r[1]}
{r[2]}
{r[3]}
))}
);
// ------------ FAQ ------------
const FAQ = ({ t }) => {
const [open, setOpen] = useState(0);
return (
{t.faq.items.map((it, i) => (
{it[1]}
))}
);
};
// ------------ WAITLIST ------------
const Waitlist = ({ t, onSubmit }) => {
const [email, setEmail] = useState('');
return (
{renderTitle(t.waitlist.title, [1])}
{t.waitlist.sub}
{t.waitlist.reserved}{t.waitlist.left}
);
};
// ------------ FOOTER ------------
const Footer = ({ t }) => (
);
Object.assign(window, { Hero, Science, Mechanism, Products, Ritual, Ambassadors, Guarantee, Specs, FAQ, Waitlist, Footer, ProductCard, useReveal, Reveal, renderTitle, SectionHead });