/* global React, ReactDOM, CaseHero, CaseFacts, CaseGallery, CaseQuote, CaseResults */ const { useEffect: useCSEffect, useRef: useCSRef } = React; // Per-case overrides, keyed by ?id=… from the marketing site grid. // Any case not listed falls back to the Baumanière reference template. const CASE_DATA = { oustau: { cover: "../assets/photo-beaumaniere-1.jpg", cat: "Restaurant · 3★", year: "2025", title: "L'Oustau de Baumanière", client: "Maison Charial · Glenn Viel & J.A Charial", loc: "Les Baux-de-Provence", deliverables: "Photo · vidéo · motion · social", team: "S. Jullien (DA), 2 op. cadreurs", lede: "Six mois de reportage chez Glenn Viel pour traduire en images la table 3★, la maison et le savoir-faire au service.", }, phebus: { cover: "../assets/photo-phebus-1.jpg", cat: "Hôtellerie & Spa · Restaurant · 1★", year: "2024", title: "Le Phébus & SPA — 40 ans", client: "Xavier Mathieu", loc: "Joucas · Vaucluse", deliverables: "Photo · vidéo · motion · social", team: "S. Jullien (DA), 2 op. cadreurs", lede: "Quarante ans de la maison Mathieu mis en images : dîners à 8 mains, spa et hôtellerie, capturés en reportage discret pendant la saison.", }, sevin: { cover: "../assets/photo-sevin.jpg", cat: "Restaurant · 1★", year: "2024", title: "Restaurant SEVIN", client: "Guilhem Sevin", loc: "Avignon · Vaucluse", deliverables: "Photo · vidéo · social", team: "S. Jullien (DA), 1 op. cadreur", lede: "Vitrine digitale de l'étoilé Sevin : portraits, dressage, service. Une charte sobre, dense, fidèle à la cuisine de Guilhem.", }, grizzly: { cover: "../assets/photo-portrait-1.jpg", cat: "Restaurant · 2★", year: "2025", title: "Le Grizzly", client: "Édouard Loubet", loc: "Provence", deliverables: "Photo · vidéo · motion", team: "S. Jullien (DA), 1 op. cadreur", lede: "Reportage long-cours dans la maison d'Édouard Loubet — la nature, le geste, la table.", }, "altera-roma": { cover: "../assets/photo-altera-4.jpg", cat: "Hôtellerie · Boutique-hôtel", year: "2026", title: "Altera Roma", client: "Altera Roma", loc: "Rome · Italie", deliverables: "Photo · vidéo · social", team: "S. Jullien (DA), 1 op. cadreur", lede: "Un boutique-hôtel romain à la palette terracotta, jungle et terrazzo — capter en images l'identité chromatique forte et l'esprit d'évasion des chambres.", photos: [ { src: "../assets/photo-altera-4.jpg", cap: "Chambre · fresque jungle & terracotta" }, { src: "../assets/photo-altera-1.jpg", cap: "Chambre · fresque palmiers" }, { src: "../assets/photo-altera-2.jpg", cap: "Salle de bain · terrazzo & terracotta" }, { src: "../assets/photo-altera-3.jpg", cap: "Vasque · terrazzo signature" }, { src: "../assets/photo-altera-5.jpg", cap: "Détail · chapeau & robinetterie noire" }, { src: "../assets/photo-altera-6.jpg", cap: "Suite mansardée · pierre & poutres" }, { src: "../assets/photo-altera-7.jpg", cap: "Tête de lit · pierre apparente" }, ], }, }; const DEFAULT_ID = "oustau"; function pickId() { try { const p = new URLSearchParams(window.location.search).get("id"); if (!p) return DEFAULT_ID; return p; } catch { return DEFAULT_ID; } } // Merge content.json caseStudyPages over the legacy CASE_DATA so existing pages // keep working until they're filled in via the back-office. function resolveData(id) { const fromJson = (window.SITE_CONTENT && window.SITE_CONTENT.caseStudyPages && window.SITE_CONTENT.caseStudyPages[id]) || null; const fromLegacy = CASE_DATA[id] || null; if (!fromJson && !fromLegacy) return null; // Json takes precedence; legacy fills gaps. return Object.assign({}, fromLegacy || {}, fromJson || {}); } const PARAM_ID = pickId(); // DATA is resolved inside App() — by then the async loader has populated SITE_CONTENT. const DEFAULT_PHOTOS = [ { src: "assets/photo-beaumaniere-1.jpg", cap: "Cuisine · service du soir" }, { src: "assets/photo-beaumaniere-2.jpg", cap: "Plat signature" }, { src: "assets/photo-beaumaniere-3.jpg", cap: "Brigade · mise en place" }, { src: "assets/photo-portrait-1.jpg", cap: "Portrait · chef" }, { src: "assets/photo-aerial-DJI.jpg", cap: "Aérien DJI · domaine" }, { src: "assets/photo-phebus-2.jpg", cap: "Détail · dressage" }, ]; const DEFAULT_STATS = [ { n: "516 188", label: "vues organiques" }, { n: "18 907", label: "j'aime" }, { n: "703", label: "enregistrements" }, { n: "341 550", label: "comptes touchés" }, ]; // Ambient desert dunes — on a case-study page we keep the dunes visible from // just below the hero to the bottom of the page. Fade-out near the CTA. function CSDesertDunesBackground() { useCSEffect(() => { let raf = 0; let smoothWind = 0; let smoothOp = 0; const update = () => { raf = 0; const max = Math.max(1, document.documentElement.scrollHeight - window.innerHeight); const p = Math.min(1, Math.max(0, window.scrollY / max)); smoothWind = smoothWind + (p - smoothWind) * 0.35; // Visible band: from just past the hero (~80vh) until the CTA section const vh = window.innerHeight; const cta = document.querySelector('.cs-cta'); const heroEnd = vh * 0.7; let target = 0; if (window.scrollY > heroEnd) { target = Math.min(1, (window.scrollY - heroEnd) / (vh * 0.45)); } if (cta) { const ctaTop = cta.getBoundingClientRect().top; if (ctaTop < vh * 0.4) target *= Math.max(0, ctaTop / (vh * 0.4)); } smoothOp = smoothOp + (target - smoothOp) * 0.22; const gust = 0.4 + Math.pow(smoothWind, 0.8) * 2.4; const root = document.documentElement; root.style.setProperty('--wind', smoothWind.toFixed(3)); root.style.setProperty('--gust', gust.toFixed(2)); root.style.setProperty('--dunes-opacity', smoothOp.toFixed(3)); if (Math.abs(target - smoothOp) > 0.001) raf = requestAnimationFrame(update); }; const onScroll = () => { if (!raf) raf = requestAnimationFrame(update); }; window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onScroll); update(); return () => { window.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onScroll); if (raf) cancelAnimationFrame(raf); }; }, []); return (