/* global React, L */
// S'fontaine 1: one-page site for the gîte, in a rounded listing-page layout:
// title + photo grid, two columns with a sticky booking card, then reviews, map, area, host, FAQ.
// Colours: forest / terracotta / cream (CSS variables and classes live in index.html).
// Reads window.VALMOREL (valmorel-data.js + content/*.json); every text exists in fr, en and nl.

const { useState, useRef, useEffect, useContext, useCallback } = React;
const V_ALPINE = window.VALMOREL;
const trA = window.tr;

const LightboxCtx = React.createContext(null);

// Placeholder colours for photo slots that have no photo yet
const TONES = ['warm', 'cream', 'forest'];
const TONE = {
  warm:   { bg: '#e8dcc4', stripe: 'rgba(120,90,60,0.10)', text: '#8a7050' },
  forest: { bg: '#3a4a37', stripe: 'rgba(255,255,255,0.06)', text: '#a8b8a3' },
  cream:  { bg: '#efe7d6', stripe: 'rgba(120,90,60,0.08)', text: '#9a8166' },
};
const stripes = (t, gap = 14) => `repeating-linear-gradient(135deg, ${t.stripe} 0 2px, transparent 2px ${gap}px)`;

const LOCALE = { fr: 'fr-FR', en: 'en-GB', nl: 'nl-NL' };
const fmtEur = (p, lang) => p.toLocaleString(LOCALE[lang], {
  style: 'currency', currency: 'EUR', minimumFractionDigits: p % 1 ? 2 : 0, maximumFractionDigits: 2 });
const fmtOne = (v, lang) => v.toLocaleString(LOCALE[lang], { minimumFractionDigits: 1, maximumFractionDigits: 1 });
const telHref = (phone) => 'tel:' + phone.replace(/[^+\d]/g, '');

// Holds the width of the longest translation, so switching language doesn't shift the layout
function StableText({ k, lang }) {
  return (
    <span style={{ display: 'inline-grid', justifyItems: 'center' }}>
      {['fr', 'en', 'nl'].map(l => (
        <span key={l} style={{ gridArea: '1 / 1', visibility: l === lang ? 'visible' : 'hidden' }}>{trA(V_ALPINE.i18n[k], l)}</span>
      ))}
    </span>
  );
}

// Phone layout switch for the few things CSS can't do (calendar months); the rest is in index.html
const MOBILE_Q = '(max-width: 760px)';
function useMobile() {
  const [m, setM] = useState(() => window.matchMedia(MOBILE_Q).matches);
  useEffect(() => {
    const mq = window.matchMedia(MOBILE_Q);
    const on = () => setM(mq.matches);
    mq.addEventListener('change', on);
    return () => mq.removeEventListener('change', on);
  }, []);
  return m;
}

const scrollToId = (id) => {
  const el = document.getElementById(id);
  if (!el) return;
  window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - 96, behavior: 'smooth' });
};

// ─── ICONS (1-stroke, currentColor) ─────────────────────────────────────
const IconA = {
  wifi: (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round">
      <path d="M5 12.5a10 10 0 0 1 14 0"/><path d="M8 15.5a6 6 0 0 1 8 0"/><circle cx="12" cy="18.5" r="1" fill="currentColor"/>
    </svg>
  ),
  garage: (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
      <path d="M3 10 12 4l9 6v10H3z"/><path d="M6 14h12M6 18h12"/>
    </svg>
  ),
  ski: (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round">
      <path d="m4 18 16-6M5 20l14-5"/><path d="m9 11 4-7M11 13l4-7"/>
    </svg>
  ),
  boot: (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round">
      <path d="M8 4h4v10l5 3 2 4H5v-3l3-2z"/>
    </svg>
  ),
  terrace: (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round">
      <path d="M3 19h18M5 19V9l7-5 7 5v10"/><path d="M10 19v-5h4v5"/>
    </svg>
  ),
  view: (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round">
      <path d="m3 18 5-7 4 5 3-4 6 8z"/><circle cx="17" cy="6" r="2"/>
    </svg>
  ),
  kitchen: (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round">
      <rect x="5" y="3" width="14" height="18" rx="1"/><path d="M5 10h14M9 6h2M9 14h6M9 17h6"/>
    </svg>
  ),
  dish: (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round">
      <ellipse cx="12" cy="14" rx="9" ry="3"/><path d="M3 14v4c0 1.5 4 3 9 3s9-1.5 9-3v-4"/>
    </svg>
  ),
  wash: (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <rect x="4" y="3" width="16" height="18" rx="1"/><circle cx="12" cy="14" r="4"/><circle cx="8" cy="6.5" r=".5" fill="currentColor"/>
    </svg>
  ),
  linen: (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round">
      <path d="M3 11h18v9H3z"/><path d="M3 11V7l9-3 9 3v4"/><path d="M12 4v7"/>
    </svg>
  ),
  smoke: (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="2.5" fill="currentColor"/>
    </svg>
  ),
  heat: (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round">
      <path d="M12 3c-2 3 2 4 0 7s2 4 0 7"/><path d="M7 6c-1.5 2 1.5 3 0 5s1.5 3 0 5M17 6c-1.5 2 1.5 3 0 5s1.5 3 0 5"/>
    </svg>
  ),
};

// Interface icons
const svg = (d, w = 16, extra = {}) => (
  <svg width={w} height={w} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8"
       strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...extra}>{d}</svg>
);
const Ico = {
  globe: svg(<><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.5 2.7 3.8 5.7 3.8 9s-1.3 6.3-3.8 9c-2.5-2.7-3.8-5.7-3.8-9S9.5 5.7 12 3z"/></>, 18),
  menu: svg(<path d="M4 7h16M4 12h16M4 17h16"/>, 18),
  close: svg(<path d="M6 6l12 12M18 6L6 18"/>, 18),
  grid: svg(<><rect x="4" y="4" width="6" height="6" rx="1"/><rect x="14" y="4" width="6" height="6" rx="1"/><rect x="4" y="14" width="6" height="6" rx="1"/><rect x="14" y="14" width="6" height="6" rx="1"/></>, 16),
  left: svg(<path d="m15 18-6-6 6-6"/>, 16),
  right: svg(<path d="m9 18 6-6-6-6"/>, 16),
  down: svg(<path d="m6 9 6 6 6-6"/>, 16),
  route: svg(<><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M19 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5"/></>, 16),
  star: <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.5l2.9 6.1 6.6.8-4.9 4.6 1.3 6.6L12 17.3l-5.9 3.3 1.3-6.6-4.9-4.6 6.6-.8z"/></svg>,
  play: <svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" style={{ marginLeft: 3 }}><path d="M8 5v14l11-7z"/></svg>,
};

// ─── PHOTO — real photo or a placeholder; opens the lightbox when idx is set ──
function PhotoA({ src, idx, label, tone = 'warm', ratio, radius = 12, showLabel }) {
  const open = useContext(LightboxCtx);
  const t = TONE[tone] || TONE.warm;
  const realSrc = src || (idx !== undefined && V_ALPINE.photos.gallery[idx]?.photo);
  const clickable = idx !== undefined && !!open;
  return (
    <div className={clickable ? 'vm-clickable' : undefined}
         role={clickable ? 'button' : undefined} tabIndex={clickable ? 0 : undefined} aria-label={clickable ? label : undefined}
         onClick={clickable ? () => open(idx) : undefined}
         onKeyDown={clickable ? (e) => e.key === 'Enter' && open(idx) : undefined}
         style={{ position: 'relative', overflow: 'hidden', borderRadius: radius, aspectRatio: ratio,
                  height: ratio ? undefined : '100%', background: t.bg, backgroundImage: realSrc ? 'none' : stripes(t),
                  cursor: clickable ? 'pointer' : 'default' }}>
      {realSrc && <img className="vm-photo-img" src={realSrc} alt={label || ''} loading="lazy" decoding="async"/>}
      {label && (!realSrc || showLabel) && (
        <span style={{ position: 'absolute', left: 12, ...(realSrc ? { bottom: 12 } : { top: 12 }), padding: '4px 10px', borderRadius: 999,
                       background: realSrc ? 'rgba(255,252,247,0.92)' : 'transparent', color: realSrc ? '#2d3e2c' : t.text,
                       font: '600 12px/1.3 "Manrope", sans-serif', pointerEvents: 'none' }}>{label}</span>
      )}
    </div>
  );
}

// Owner photo, or initials while there is none
function Avatar({ size = 48 }) {
  const c = V_ALPINE.contact, src = V_ALPINE.photos.owner;
  const initials = c.name.split(/\s+/).map(w => w[0]).join('').slice(0, 2);
  return src
    ? <img src={src} alt={c.name} style={{ width: size, height: size, borderRadius: '50%', objectFit: 'cover', flexShrink: 0 }}/>
    : <span aria-hidden="true" style={{ width: size, height: size, borderRadius: '50%', background: 'var(--ink)', color: 'var(--bg)',
                                        display: 'grid', placeItems: 'center', flexShrink: 0,
                                        font: `600 ${Math.round(size * 0.36)}px/1 "Fraunces", serif` }}>{initials}</span>;
}

// ─── HEADER ─────────────────────────────────────────────────────────────
function TopBarA({ lang, setLang }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  const [active, setActive] = useState('top');
  const [scrolled, setScrolled] = useState(false);
  const [menu, setMenu] = useState(null); // 'nav' | 'lang' | null
  const go = (id) => { setMenu(null); scrollToId(id); };
  const LANGS = [['fr', '🇫🇷', 'Français'], ['en', '🇬🇧', 'English'], ['nl', '🇳🇱', 'Nederlands']];
  const navItems = [
    { k: 'nav_overview', id: 'top' },
    { k: 'nav_house',    id: 'le-gite' },
    { k: 'nav_reviews',  id: 'avis' },
    { k: 'nav_location', id: 'localisation' },
    { k: 'nav_area',     id: 'le-domaine' },
    { k: 'nav_host',     id: 'hote' },
  ];

  // Which section is under the header (ids in page order), and a soft blur once scrolled
  useEffect(() => {
    const ids = ['top', 'le-gite', 'reserver', 'avis', 'localisation', 'le-domaine', 'hote'];
    const onScroll = () => {
      let current = ids[0];
      for (const id of ids) {
        const el = document.getElementById(id);
        if (el && el.getBoundingClientRect().top <= 120) current = id;
      }
      setActive(current);
      setScrolled(window.scrollY > 8);
    };
    window.addEventListener('scroll', onScroll, { passive: true });
    onScroll();
    return () => window.removeEventListener('scroll', onScroll);
  }, []);
  useEffect(() => {
    if (!menu) return;
    const onKey = (e) => e.key === 'Escape' && setMenu(null);
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [menu]);

  return (
    <header style={{ position: 'sticky', top: 0, zIndex: 50, borderBottom: '1px solid var(--line)',
                     background: scrolled ? 'rgba(245,241,234,0.92)' : 'var(--bg)',
                     backdropFilter: scrolled ? 'saturate(1.4) blur(14px)' : 'none',
                     WebkitBackdropFilter: scrolled ? 'saturate(1.4) blur(14px)' : 'none' }}>
      {menu && <div onClick={() => setMenu(null)} style={{ position: 'fixed', inset: 0 }}/>}
      <div className="vm-wrap" style={{ position: 'relative', height: 72, display: 'flex', alignItems: 'center',
                                        justifyContent: 'space-between', gap: 16 }}>
        <button className="vm-brand" onClick={() => go('top')} style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer',
                                                   textAlign: 'left', color: 'inherit' }}>
          <div style={{ font: '600 19px/1 "Fraunces", serif', whiteSpace: 'nowrap' }}>S'fontaine 1</div>
          <div className="vm-muted" style={{ font: '500 12px/1 "Manrope", sans-serif', marginTop: 5, whiteSpace: 'nowrap' }}>
            Réf. {V_ALPINE.brand.ref}
          </div>
        </button>

        <nav className="vm-nav" style={{ display: 'flex', gap: 2, whiteSpace: 'nowrap', font: '500 15px/1 "Manrope", sans-serif' }}>
          {navItems.map(item => (
            <a key={item.k} href={'#' + item.id} onClick={(e) => { e.preventDefault(); go(item.id); }}
               style={{ position: 'relative', color: active === item.id ? 'var(--ink)' : 'var(--muted)', textDecoration: 'none' }}>
              <StableText k={item.k} lang={lang}/>
              {active === item.id && (
                <span style={{ position: 'absolute', left: 14, right: 14, bottom: 3, height: 2, borderRadius: 2, background: 'var(--accent)' }}/>
              )}
            </a>
          ))}
        </nav>

        <div className="vm-header-right" style={{ position: 'relative', display: 'flex', alignItems: 'center', gap: 8 }}>
          <button className="vm-icon-btn" aria-label="Langue / Language / Taal" aria-expanded={menu === 'lang'}
                  onClick={() => setMenu(m => m === 'lang' ? null : 'lang')}>{Ico.globe}</button>
          <button className="vm-btn vm-btn-primary vm-pill" onClick={() => go('reserver')}
                  style={{ padding: '10px 18px', fontSize: 15 }}><StableText k="nav_book" lang={lang}/></button>
          <button className="vm-icon-btn vm-burger" aria-label="Menu" aria-expanded={menu === 'nav'}
                  onClick={() => setMenu(m => m === 'nav' ? null : 'nav')}>{menu === 'nav' ? Ico.close : Ico.menu}</button>
          {menu === 'lang' && (
            <div className="vm-menu" role="menu">
              {LANGS.map(([L, flag, name]) => (
                <button key={L} className="vm-menu-item" aria-current={lang === L} onClick={() => { setLang(L); setMenu(null); }}>
                  <span style={{ fontSize: 20, lineHeight: 1 }}>{flag}</span>{name}
                </button>
              ))}
            </div>
          )}
          {menu === 'nav' && (
            <div className="vm-menu" style={{ minWidth: 250 }}>
              {navItems.map(item => (
                <button key={item.k} className="vm-menu-item" aria-current={active === item.id} onClick={() => go(item.id)}
                        style={{ font: '600 17px/1.2 "Fraunces", serif', padding: '14px 20px' }}>{t(item.k)}</button>
              ))}
            </div>
          )}
        </div>
      </div>
    </header>
  );
}

// ─── TITLE + PHOTO GRID ─────────────────────────────────────────────────
function TitleA({ lang, rv }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  const b = V_ALPINE.brand;
  return (
    <section id="top" style={{ paddingTop: 28 }}>
      <h1 className="vm-h1">
        {t('hero_title_a')} <span style={{ color: 'var(--accent)' }}>{t('hero_title_b')}</span>
      </h1>
      <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '6px 20px', marginTop: 10,
                    font: '500 15px/1.4 "Manrope", sans-serif' }}>
        {rv.count > 0 && (
          <button className="vm-link" onClick={() => scrollToId('avis')}
                  style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
            <span style={{ color: 'var(--accent)', display: 'flex' }}>{Ico.star}</span>{rv.title}
          </button>
        )}
        <button className="vm-link" onClick={() => scrollToId('localisation')}>{b.town}, {b.region}</button>
      </div>
      <PhotoGridA lang={lang}/>
    </section>
  );
}

function PhotoGridA({ lang }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  const open = useContext(LightboxCtx);
  const [playing, setPlaying] = useState(false);
  const g = V_ALPINE.photos.gallery;
  return (
    <div className="vm-photos">
      <div style={{ position: 'relative', background: '#3a4a37' }}>
        {playing ? (
          <iframe src="https://www.youtube.com/embed/j9epP4FZvoQ?autoplay=1&rel=0&modestbranding=1&playsinline=1"
                  title="Visite du gîte S'fontaine 1, Valmorel"
                  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen"
                  allowFullScreen referrerPolicy="strict-origin-when-cross-origin"
                  style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', border: 'none', background: '#000' }}/>
        ) : (
          <button className="vm-play-tile" onClick={() => setPlaying(true)} aria-label={t('hero_video')}
                  style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', border: 'none', padding: 0, cursor: 'pointer',
                           background: `#3a4a37 url(${V_ALPINE.photos.hero}) center top/cover` /* top: keeps the Gîtes de France logo in view */ }}>
            <span style={{ position: 'absolute', inset: 0,
                           background: 'radial-gradient(ellipse at center, rgba(20,28,20,0.35) 0%, rgba(20,28,20,0.12) 70%)' }}/>
            <span style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column',
                           alignItems: 'center', justifyContent: 'center', gap: 16 }}>
              <span className="vm-play" style={{ width: 76, height: 76, borderRadius: '50%', background: 'var(--accent)', color: 'var(--bg)',
                                                 display: 'grid', placeItems: 'center',
                                                 boxShadow: '0 12px 32px rgba(0,0,0,0.35), 0 0 0 8px rgba(245,241,234,0.2)' }}>{Ico.play}</span>
              <span style={{ padding: '8px 14px', borderRadius: 999, background: 'rgba(255,252,247,0.92)', color: 'var(--ink)',
                             font: '600 14px/1 "Manrope", sans-serif' }}>{t('hero_video')}</span>
            </span>
          </button>
        )}
      </div>
      <div className="vm-photo-small">
        {g.slice(0, 4).map((p, i) => (
          <PhotoA key={i} idx={i} label={trA(p.caption, lang)} tone={TONES[i % 3]} radius={0}/>
        ))}
      </div>
      {g.length > 0 && (
        <button className="vm-btn vm-btn-light vm-btn-sm vm-all-photos" onClick={() => open(0)}>
          {Ico.grid}{t('gal_all')}
        </button>
      )}
    </div>
  );
}

// ─── LEFT COLUMN: about the gîte ────────────────────────────────────────
function OverviewA({ lang }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  const b = V_ALPINE.brand, c = V_ALPINE.contact;
  const amen = (key) => trA(V_ALPINE.amenities.find(a => a.key === key), lang);
  const highlights = [
    [IconA.ski, `${b.distToLift} m ${t('stat_lift')}`],
    [IconA.garage, amen('garage')],
    [IconA.terrace, amen('terrace')],
  ];
  return (
    <>
      <div className="vm-sub" id="le-gite">
        <h2 className="vm-h2" style={{ marginBottom: 8 }}>{t('intro_title')}</h2>
        <p className="vm-p">
          {b.capacity} {t('stat_guests')}, {b.bedrooms} {t('stat_rooms')}, {b.bathrooms} {t('stat_baths')}, {b.surface} m²
        </p>
      </div>
      <div className="vm-sub" style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
        <Avatar size={48}/>
        <div>
          <div style={{ font: '600 16px/1.3 "Manrope", sans-serif' }}>{t('own_host')}: {c.name}</div>
          <div className="vm-small vm-muted">{t('own_since')} 2022</div>
        </div>
      </div>
      <div className="vm-sub" style={{ display: 'grid', gap: 20 }}>
        {highlights.map(([icon, text], i) => (
          <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 20 }}>
            <span style={{ width: 26, height: 26, flexShrink: 0 }}>{icon}</span>
            <span style={{ font: '600 16px/1.35 "Manrope", sans-serif' }}>{text}</span>
          </div>
        ))}
      </div>
      <div className="vm-sub">
        <p className="vm-p" style={{ fontWeight: 600, marginBottom: 16 }}>{t('hero_lede')}</p>
        <p className="vm-p">{t('intro_body')}</p>
      </div>
    </>
  );
}

function FloorsA({ lang }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  const [zoom, setZoom] = useState(null);
  const plans = [
    { src: 'images/plan-rdc.jpg', label: t('floor_gf'),
      note: lang === 'fr' ? 'Sur 2 niveaux : salon, cuisine, 3 chambres (1, 2 et 3), 2 terrasses, parking 2 places'
          : lang === 'nl' ? 'Over 2 niveaus: salon, keuken, 3 slaapkamers (1, 2 en 3), 2 terrassen, parking 2 plekken'
          : 'Across 2 levels: living, kitchen, 3 bedrooms (1, 2 and 3), 2 terraces, parking for 2' },
    { src: 'images/plan-etage.jpg', label: t('floor_1'),
      note: lang === 'fr' ? 'Chambre 4 (2 lits 80×200), salle d’eau privée (SDB 4)'
          : lang === 'nl' ? 'Slaapkamer 4 (2 bedden 80×200), privé-badkamer (SDB 4)'
          : 'Bedroom 4 (2 single beds 80×200), private bathroom (SDB 4)' },
  ];
  const floors = [['floor_1', 'floor_1_desc'], ['floor_gf', 'floor_gf_desc'], ['floor_bs', 'floor_bs_desc'], ['floor_base', 'floor_base_desc']];
  return (
    <div className="vm-sub">
      <h2 className="vm-h2">{t('floors_title')}</h2>
      <div className="vm-grid-2">
        {plans.map((p, i) => (
          <button key={i} onClick={() => setZoom(p)} className="vm-card"
                  style={{ padding: 0, overflow: 'hidden', cursor: 'zoom-in', textAlign: 'left', color: 'inherit' }}>
            <div style={{ background: '#fffdf8', padding: 16, borderBottom: '1px solid var(--line)' }}>
              <img src={p.src} alt={p.label} loading="lazy" decoding="async"
                   style={{ display: 'block', width: '100%', height: 180, objectFit: 'contain' }}/>
            </div>
            <div style={{ padding: '14px 16px' }}>
              <div style={{ font: '600 16px/1.3 "Manrope", sans-serif' }}>{p.label}</div>
              <div className="vm-small vm-muted" style={{ marginTop: 4 }}>{p.note}</div>
            </div>
          </button>
        ))}
      </div>
      <div className="vm-grid-2" style={{ marginTop: 16 }}>
        {floors.map(([label, desc]) => (
          <div key={label} className="vm-card" style={{ padding: 20 }}>
            <div style={{ font: '600 16px/1.3 "Manrope", sans-serif' }}>{t(label)}</div>
            <div className="vm-small vm-muted" style={{ marginTop: 6 }}>{t(desc)}</div>
          </div>
        ))}
      </div>
      {zoom && (
        <div onClick={() => setZoom(null)} style={{ position: 'fixed', inset: 0, zIndex: 200, background: 'rgba(31,44,30,0.94)',
                                                    display: 'flex', flexDirection: 'column', alignItems: 'center',
                                                    justifyContent: 'center', padding: 24, cursor: 'zoom-out' }}>
          <div style={{ font: '600 20px/1.2 "Fraunces", serif', color: 'var(--bg)', marginBottom: 16 }}>{zoom.label}</div>
          <img src={zoom.src} alt={zoom.label} style={{ maxWidth: '92vw', maxHeight: '78vh', objectFit: 'contain', background: '#fffdf8',
                                                       padding: 20, borderRadius: 12, boxShadow: '0 24px 60px rgba(0,0,0,0.4)' }}/>
          <div className="vm-small" style={{ marginTop: 16, color: '#a8b8a3' }}>
            {lang === 'fr' ? 'Cliquer pour fermer' : lang === 'nl' ? 'Klik om te sluiten' : 'Click to close'}
          </div>
        </div>
      )}
    </div>
  );
}

function AmenitiesA({ lang }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  return (
    <div className="vm-sub">
      <h2 className="vm-h2">{t('amen_title')}</h2>
      <div className="vm-grid-2" style={{ gap: '0 24px' }}>
        {V_ALPINE.amenities.map((a, i) => (
          <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 16, padding: '10px 0' }}>
            <span style={{ width: 24, height: 24, flexShrink: 0 }}>{IconA[a.key]}</span>
            <span className="vm-p">{trA(a, lang)}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

// ─── LIVE DATA: Gîtes de France (ITEA widgets) ─────────────────────────
// The widget endpoint returns its HTML as JSONP (CORS allowed). We parse that HTML
// and render the data in the page's own style instead of the widget's look.
// Language follows Accept-Language; the documented `langue` param is ignored by the server.
const ITEA_URL = (widget) =>
  `https://widget.itea.fr/widget.php?key=tftr8_fc&numGite=${V_ALPINE.brand.ref}&widget=${widget}&callback=cb`;
const ITEA_HOST = { fr: 'widget', en: 'widget-uk', nl: 'widget-nl' }; // booking pages open in this language

const iteaCache = {};
function useItea(widget, lang) {
  const [doc, setDoc] = useState(null); // null = loading, false = failed
  useEffect(() => {
    let alive = true;
    const k = widget + lang;
    iteaCache[k] = iteaCache[k] || fetch(ITEA_URL(widget), { headers: { 'Accept-Language': lang } })
      .then(r => r.text())
      .then(s => new DOMParser().parseFromString(
        JSON.parse(s.slice(s.indexOf('(') + 1, s.lastIndexOf(')'))).html, 'text/html'));
    iteaCache[k].then(d => alive && setDoc(d), () => alive && setDoc(false));
    return () => { alive = false; };
  }, [widget, lang]);
  return doc;
}

const unshout = (s) => s === s.toUpperCase() ? s.toLowerCase().replace(/(^|[\s'-])\p{L}/gu, c => c.toUpperCase()) : s;
const iteaText = (el, sel) => (el.querySelector(sel)?.textContent || '').replace(/\s+/g, ' ').trim();
const fmtDay = (y, m, d, add, lang) =>
  new Date(y, m - 1, +d + add).toLocaleDateString(LOCALE[lang], { day: 'numeric', month: 'short' });

// Weekly prices per season. Live from Gîtes de France: only free weeks carry a price, grouped per price level
// with the months it applies to. Season names come from V_ALPINE.rates (matched on price), which is also the
// fallback while loading or offline.
function useRates(lang) {
  const doc = useItea('prix', lang);
  const live = {};
  if (doc) doc.querySelectorAll('table[data-duree-tarif="semaine"] td.libre').forEach(td => {
    const price = parseFloat(td.dataset.prix.replace(',', '.'));
    (live[price] = live[price] || new Set()).add(+td.dataset.deb.split('/')[1]);
  });
  return Object.keys(live).length
    ? Object.keys(live).map(Number).sort((a, b) => a - b).map(price => {
        const known = V_ALPINE.rates.find(r => r.price === price);
        const months = [...live[price]].sort((a, b) => a - b)
          .map(m => new Date(2000, m - 1).toLocaleDateString(LOCALE[lang], { month: 'short' })).join(', ');
        return known ? { season: trA(known.season, lang), months, price } : { season: months, months: '', price };
      })
    : V_ALPINE.rates.map(r => ({ season: trA(r.season, lang), months: trA(r.months, lang), price: r.price }));
}

// Guest reviews collected by Gîtes de France
function useReviews(lang) {
  const doc = useItea('avis', lang);
  const reviews = doc ? [...doc.querySelectorAll('.div_widget_itea_avis_detailAvisClient')].map(el => {
    const title = iteaText(el, '.p_widget_itea_avis_titreAvis');
    return {
      // No all-caps text on the site: sentence-case shouted titles, title-case shouted names
      title: title === title.toUpperCase() ? title.charAt(0) + title.slice(1).toLowerCase() : title,
      name: unshout(iteaText(el, '.p_widget_itea_avis_pseudoClient')),
      type: iteaText(el, '.p_widget_itea_avis_typeSejourAvis'),
      date: (iteaText(el, '.dateDepotAvis').match(/\d{2}\/\d{2}\/\d{4}/) || [''])[0],
      body: iteaText(el, '.p_widget_itea_avis_descriptionAvis'),
      stars: el.querySelectorAll('.p_widget_itea_avis_noteGlobale .active').length,
      notes: [...el.querySelectorAll('.div_widget_itea_avis_note')]
        .map(n => [iteaText(n, '.sp_widget_itea_avis_libelleQuest'), n.querySelectorAll('.active').length]),
    };
  }) : [];
  const avg = reviews.length ? reviews.reduce((a, r) => a + r.stars, 0) / reviews.length : 0;
  const title = trA(V_ALPINE.i18n.rev_title, lang).replace('{avg}', fmtOne(avg, lang)).replace('{n}', reviews.length);
  return { reviews, avg, title, count: reviews.length };
}

// Availability + the chosen stay, shared by the calendar and the booking card.
// Any stay of free nights can be booked: Saturday → Saturday in a free week is confirmed instantly,
// other dates go to the owner as a request (tested on the Gîtes de France booking page).
function useBooking(lang) {
  const doc = useItea('dispo', lang);
  const [sel, setSel] = useState({ from: null, to: null }); // arrival / departure, yyyy-mm-dd
  const [hover, setHover] = useState(null);

  // Arrival days bookable online ("A" days) → Gîtes de France booking page, keyed by yyyy-mm-dd
  const links = {};
  if (doc) doc.querySelectorAll('a > span.canlendrier_classe_by_lettre_A').forEach(sp => {
    links[sp.dataset.date] = `https://${ITEA_HOST[lang]}.itea.fr${sp.parentNode.getAttribute('href')}`;
  });
  // 18 months, Monday-first weeks
  const months = doc ? [...doc.querySelectorAll('li.calendItea_li[data-mois]')].map(li => ({
    label: iteaText(li, 'caption'),
    heads: [...li.querySelectorAll('thead th')].map(th => th.textContent.trim()),
    days: [...li.querySelectorAll('tbody td > span, tbody td > a > span')].map(sp => ({
      day: +sp.textContent, date: sp.dataset.date, pad: sp.style.display === 'none',
      past: sp.classList.contains('spanCalendJour_avantDatePossible'),
      booked: sp.classList.contains('spanCalendJour_occupe'),
    })),
  })) : [];

  const info = {};
  months.forEach(m => m.days.forEach(d => { if (!d.pad) info[d.date] = d; }));
  const isFree = (iso) => info[iso] && !info[iso].past && !info[iso].booked;
  const addDays = (iso, n) => { const [y, m, d] = iso.split('-').map(Number); return new Date(Date.UTC(y, m - 1, d + n)).toISOString().slice(0, 10); };
  const nightsFree = (a, b) => { for (let x = a; x < b; x = addDays(x, 1)) if (!isFree(x)) return false; return true; };
  const validEnd = (iso) => sel.from && !sel.to && iso > sel.from && nightsFree(sel.from, iso);
  const pick = (iso) => {
    if (validEnd(iso)) setSel({ from: sel.from, to: iso });
    else if (isFree(iso)) setSel({ from: iso, to: null });
  };
  const nights = sel.to ? Math.round((Date.parse(sel.to) - Date.parse(sel.from)) / 864e5) : 0;
  const instant = nights > 0 && nights % 7 === 0 && !!links[sel.from];
  const resaBase = doc && doc.querySelector('a[href*="etape1"]')?.getAttribute('href').replace(/&jour=.*$/, '');
  const bookUrl = () => {
    const [y, m, d] = sel.from.split('-');
    return `https://${ITEA_HOST[lang]}.itea.fr${resaBase}&jour=${+d}&mois=${+m}&annee=${y}&duree=${nights}`;
  };
  const label = (iso, add = 0) => { const [y, m, d] = iso.split('-'); return fmtDay(y, m, d, add, lang); };
  return {
    doc, links, months, sel, setSel, hover, setHover, isFree, validEnd, pick, nights, instant, bookUrl, label,
    end: sel.to || (hover && validEnd(hover) ? hover : null),
    next: Object.keys(links).sort()[0],
  };
}

// ─── LEFT COLUMN: calendar, rates, why Gîtes de France ─────────────────
function CalendarA({ lang, bk }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  const [offset, setOffset] = useState(0);
  const per = useMobile() ? 1 : 2; // months in view
  const { months, sel, setSel, setHover, isFree, validEnd, end, pick, nights, instant, label, doc } = bk;
  const shown = months.slice(offset, offset + per);
  const nightsText = `${nights} ${t(nights === 1 ? 'book_night' : 'book_nights')}, ${t(instant ? 'book_instant' : 'book_on_request')}`;
  return (
    <div className="vm-sub" id="reserver">
      <h2 className="vm-h2" style={{ marginBottom: 6 }}>
        {sel.to ? `${label(sel.from)} → ${label(sel.to)}` : sel.from ? `${label(sel.from)} → …` : t('book_how_title')}
      </h2>
      <p className="vm-small vm-muted" style={{ margin: '0 0 24px', maxWidth: 560 }}>
        {sel.to ? nightsText : t(sel.from ? 'book_pick_departure' : 'book_how')}
      </p>
      <div style={{ position: 'relative' }} onMouseLeave={() => setHover(null)}>
        {months.length > per && (
          <>
            <button className="vm-round-btn" aria-label={months[offset - 1]?.label} disabled={offset === 0}
                    onClick={() => setOffset(o => o - 1)} style={{ position: 'absolute', left: 0, top: 0 }}>{Ico.left}</button>
            <button className="vm-round-btn" aria-label={months[offset + per]?.label} disabled={offset + per >= months.length}
                    onClick={() => setOffset(o => Math.min(o + 1, months.length - per))}
                    style={{ position: 'absolute', right: 0, top: 0 }}>{Ico.right}</button>
          </>
        )}
        <div style={{ display: 'grid', gridTemplateColumns: `repeat(${per}, minmax(0, 1fr))`, gap: 40,
                      minHeight: doc === null ? 320 : undefined }}>
          {shown.map(mo => (
            <div key={mo.label}>
              <div style={{ textAlign: 'center', font: '600 16px/32px "Manrope", sans-serif', marginBottom: 8 }}>{mo.label}</div>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)' }}>
                {mo.heads.map((d, i) => (
                  <div key={'h' + i} className="vm-muted" style={{ textAlign: 'center', padding: '6px 0', font: '600 12px/1 "Manrope", sans-serif' }}>{d}</div>
                ))}
                {mo.days.map((d, i) => {
                  if (d.pad) return <div key={i}/>;
                  const edge = d.date === sel.from || (end && d.date === end);
                  const hoverEnd = !sel.to && end && d.date === end;            // previewing a departure day
                  const inRange = sel.from && end && d.date > sel.from && d.date < end;
                  const clickable = isFree(d.date) || validEnd(d.date);
                  const style = {
                    background: edge && !hoverEnd ? 'var(--ink)' : inRange ? 'var(--soft)' : 'transparent',
                    color: edge && !hoverEnd ? 'var(--bg)' : d.booked ? 'var(--muted)' : 'var(--ink)',
                    borderColor: hoverEnd ? 'var(--ink)' : undefined,
                    borderRadius: edge ? '50%' : inRange ? 0 : undefined,
                    textDecoration: d.booked ? 'line-through' : 'none',
                    opacity: d.past ? 0.3 : d.booked ? 0.55 : 1,
                  };
                  return clickable
                    ? <button key={i} className="vm-day vm-day-free" onClick={() => pick(d.date)}
                              onMouseEnter={() => setHover(d.date)} aria-pressed={!!edge} style={style}>{d.day}</button>
                    : <div key={i} className="vm-day" style={style}>{d.day}</div>;
                })}
              </div>
            </div>
          ))}
        </div>
      </div>
      <div className="vm-small vm-muted" style={{ display: 'flex', alignItems: 'center', gap: '8px 20px', flexWrap: 'wrap', marginTop: 20 }}>
        <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <span style={{ width: 14, height: 14, borderRadius: '50%', border: '1px solid var(--ink)' }}/>{t('book_free')}
        </span>
        <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <span style={{ textDecoration: 'line-through', fontWeight: 600 }}>12</span>{t('rates_booked')}
        </span>
        <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <span style={{ width: 14, height: 14, borderRadius: '50%', background: 'var(--ink)' }}/>{t('book_yours')}
        </span>
        {sel.from && (
          <button className="vm-link" onClick={() => setSel({ from: null, to: null })}
                  style={{ marginLeft: 'auto', color: 'var(--ink)' }}>{t('book_clear')}</button>
        )}
      </div>
    </div>
  );
}

function BookingCardA({ lang, bk, price }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  const c = V_ALPINE.contact;
  const { sel, nights, instant, bookUrl, label, links, next } = bk;
  const dateBox = (k, iso) => (
    <button onClick={() => scrollToId('reserver')} style={{ flex: 1, minWidth: 0, textAlign: 'left', padding: '10px 14px',
                                                            border: 'none', background: 'transparent', cursor: 'pointer', color: 'inherit' }}>
      <div style={{ font: '700 12px/1.3 "Manrope", sans-serif' }}>{t(k)}</div>
      <div className="vm-small" style={{ color: iso ? 'var(--ink)' : 'var(--muted)', marginTop: 2 }}>{iso ? label(iso) : t('book_add_date')}</div>
    </button>
  );
  return (
    <div className="vm-card" style={{ padding: 24, boxShadow: 'var(--shadow)' }}>
      <div className="vm-small vm-muted">{t('hero_from')}</div>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 6, marginBottom: 20 }}>
        <span style={{ font: '600 26px/1.15 "Fraunces", serif' }}>{fmtEur(price, lang)}</span>
        <span className="vm-muted">{t('hero_per_week')}</span>
      </div>
      <div style={{ display: 'flex', border: '1px solid #b9b1a2', borderRadius: 8, marginBottom: 16 }}>
        {dateBox('book_arrival', sel.from)}
        <span style={{ width: 1, background: '#b9b1a2' }}/>
        {dateBox('book_departure', sel.to)}
      </div>
      {sel.to
        ? <a className="vm-btn vm-btn-primary vm-btn-block" href={bookUrl()} target="_blank" rel="noopener">{t('book_go')}</a>
        : <button className="vm-btn vm-btn-primary vm-btn-block" onClick={() => scrollToId('reserver')}>{t('hero_check')}</button>}
      <p className="vm-small vm-muted" style={{ textAlign: 'center', margin: '12px 0 0' }}>
        {sel.to
          ? `${nights} ${t(nights === 1 ? 'book_night' : 'book_nights')}, ${t(instant ? 'book_instant' : 'book_on_request')}`
          : next && <a href={links[next]} target="_blank" rel="noopener" style={{ color: 'inherit' }}>
              {t('book_next')}: {label(next)} → {label(next, 7)}
            </a>}
      </p>
      <div style={{ textAlign: 'center', marginTop: 8 }}>
        <button className="vm-link vm-small" onClick={() => scrollToId('via-gites-de-france')}>{t('book_why_title')}</button>
      </div>
      <div style={{ borderTop: '1px solid var(--line)', marginTop: 20, paddingTop: 16 }}>
        <div className="vm-small vm-muted">{t('book_help')}</div>
        <div className="vm-small" style={{ display: 'flex', flexWrap: 'wrap', gap: '4px 16px', marginTop: 6, fontWeight: 600 }}>
          <a href={telHref(c.phone)} style={{ color: 'inherit' }}>{c.phone}</a>
          <a href={`mailto:${c.email}`} style={{ color: 'inherit', overflowWrap: 'anywhere' }}>{c.email}</a>
        </div>
        <div className="vm-small vm-muted" style={{ marginTop: 10 }}>
          {lang === 'fr' ? 'Paiements : carte bancaire, chèques-vacances ANCV, cartes cadeaux.'
            : lang === 'nl' ? 'Betalen: creditcard, ANCV vouchers, cadeaubonnen.'
            : 'Payment: credit card, ANCV vouchers, gift cards.'}
        </div>
      </div>
    </div>
  );
}

function RatesA({ lang, rows }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  return (
    <div className="vm-sub" id="tarifs">
      <h2 className="vm-h2" style={{ marginBottom: 6 }}>{t('rates_title')}</h2>
      <p className="vm-small vm-muted" style={{ margin: '0 0 20px' }}>{t('rates_note')}</p>
      <div className="vm-card" style={{ overflow: 'hidden' }}>
        {rows.map((r, i) => (
          <div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16, padding: '14px 20px',
                                borderTop: i ? '1px solid var(--line)' : 'none' }}>
            <div style={{ minWidth: 0 }}>
              <div style={{ font: '600 16px/1.3 "Manrope", sans-serif' }}>{r.season}</div>
              {r.months && <div className="vm-small vm-muted">{r.months}</div>}
            </div>
            <div style={{ font: '600 18px/1.2 "Fraunces", serif', whiteSpace: 'nowrap' }}>{fmtEur(r.price, lang)}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

function WhyA({ lang }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  return (
    <div className="vm-sub" id="via-gites-de-france">
      <h2 className="vm-h2" style={{ marginBottom: 8 }}>{t('book_why_title')}</h2>
      <p className="vm-p vm-muted" style={{ marginBottom: 24 }}>{t('book_why_intro')}</p>
      <div className="vm-grid-2" style={{ gap: 24 }}>
        {V_ALPINE.i18n.book_why.map(({ title, text }, i) => (
          <div key={i} style={{ display: 'flex', gap: 14 }}>
            <span style={{ width: 32, height: 32, borderRadius: '50%', background: 'var(--soft)', color: 'var(--accent)', flexShrink: 0,
                           display: 'grid', placeItems: 'center', font: '700 14px/1 "Manrope", sans-serif' }}>{i + 1}</span>
            <div>
              <div style={{ font: '600 16px/1.3 "Manrope", sans-serif', marginBottom: 4 }}>{trA(title, lang)}</div>
              <div className="vm-small vm-muted">{trA(text, lang)}</div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ─── REVIEWS ────────────────────────────────────────────────────────────
function ReviewsA({ lang, rv }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  const [all, setAll] = useState(false);
  if (!rv.count) return null; // loading, unreachable, or no reviews yet
  // Average score per question across all reviews
  const byLabel = {};
  rv.reviews.forEach(r => r.notes.forEach(([l, v]) => (byLabel[l] = byLabel[l] || []).push(v)));
  const scores = Object.entries(byLabel).map(([l, vs]) => ({ l, v: vs.reduce((a, b) => a + b, 0) / vs.length }));
  const shown = all ? rv.reviews : rv.reviews.slice(0, 6);
  return (
    <section className="vm-sec" id="avis">
      <h2 className="vm-h2" style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
        <span style={{ color: 'var(--accent)', display: 'flex' }}>{Ico.star}</span>{rv.title}
      </h2>
      <p className="vm-small vm-muted" style={{ display: 'flex', gap: 8, margin: '0 0 32px' }}>
        <span aria-hidden="true" style={{ color: 'var(--accent)', fontWeight: 700 }}>✓</span>{t('rev_verified')}
      </p>
      {scores.length > 0 && (
        <div className="vm-scores" style={{ display: 'grid', gridTemplateColumns: `repeat(${Math.min(scores.length, 4)}, minmax(0, 1fr))`,
                                            marginBottom: 40, paddingBottom: 32, borderBottom: '1px solid var(--line)' }}>
          {scores.map((s, i) => (
            <div key={i} style={{ borderLeft: i ? '1px solid var(--line)' : 'none', paddingLeft: i ? 24 : 0 }}>
              <div className="vm-small">{s.l}</div>
              <div style={{ font: '600 20px/1.3 "Fraunces", serif', marginTop: 4 }}>{fmtOne(s.v, lang)}</div>
            </div>
          ))}
        </div>
      )}
      <div className="vm-reviews" style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '40px 80px' }}>
        {shown.map(r => <ReviewCardA key={r.name + r.date} r={r} lang={lang}/>)}
      </div>
      {rv.count > 6 && !all && (
        <button className="vm-btn vm-btn-outline" style={{ marginTop: 40 }} onClick={() => setAll(true)}>
          {t('rev_show_all').replace('{n}', rv.count)}
        </button>
      )}
    </section>
  );
}

function ReviewCardA({ r, lang }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  const [original, setOriginal] = useState(false);
  // Translations are added by the owners in Pages CMS (content/reviews.json), matched on name + date
  const tr = V_ALPINE.reviews.find(x => x.name === r.name && x.date === r.date);
  const hasTr = !!tr?.body?.[lang];
  const showTr = hasTr && !original;
  return (
    <article>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
        <span aria-hidden="true" style={{ width: 44, height: 44, borderRadius: '50%', background: 'var(--soft)', flexShrink: 0,
                                          display: 'grid', placeItems: 'center', font: '600 18px/1 "Fraunces", serif' }}>{r.name.charAt(0)}</span>
        <div>
          <div style={{ font: '600 16px/1.3 "Manrope", sans-serif' }}>{r.name}</div>
          <div className="vm-small vm-muted">{r.type}</div>
        </div>
      </div>
      <div className="vm-small" style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
        <span aria-label={`${r.stars}/5`} style={{ color: 'var(--accent)', letterSpacing: 1 }}>
          {'★'.repeat(r.stars)}<span style={{ opacity: 0.3 }}>{'★'.repeat(5 - r.stars)}</span>
        </span>
        <span className="vm-muted">{r.date}</span>
      </div>
      <div style={{ font: '600 16px/1.4 "Manrope", sans-serif', marginBottom: 4 }}>{showTr ? tr.title[lang] || r.title : r.title}</div>
      <p className="vm-p">{showTr ? tr.body[lang] : r.body}</p>
      {hasTr && (
        <div className="vm-small vm-muted" style={{ marginTop: 10 }}>
          {showTr && <span style={{ marginRight: 10 }}>{t('rev_translated')}</span>}
          <button className="vm-link" style={{ color: 'var(--ink)' }} onClick={() => setOriginal(o => !o)}>
            {showTr ? t('rev_show_original') : t('rev_show_translation')}
          </button>
        </div>
      )}
    </article>
  );
}

// ─── LOCATION ───────────────────────────────────────────────────────────
function LocationA({ lang }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  const b = V_ALPINE.brand;
  const fmt = (m) => m >= 1000 ? `${(m / 1000).toFixed(1).replace('.0', '')} km` : `${m} m`;
  const onSite = lang === 'fr' ? 'sur place' : lang === 'nl' ? 'ter plaatse' : 'on site';
  const [dropdown, setDropdown] = useState(false);
  const dropdownRef = useRef(null);

  // Close the directions menu on outside click or Esc
  useEffect(() => {
    if (!dropdown) return;
    const onDown = (e) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target)) setDropdown(false); };
    const onKey = (e) => { if (e.key === 'Escape') setDropdown(false); };
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown', onKey);
    return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); };
  }, [dropdown]);

  const lat = b.lat, lng = b.lng;
  const mapApps = [
    { key: 'osm', label: 'OpenStreetMap',
      sub: lang === 'fr' ? 'Dans le navigateur' : lang === 'nl' ? 'In de browser' : 'In your browser',
      url: `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}#map=16/${lat}/${lng}` },
    { key: 'google', label: 'Google Maps',
      sub: lang === 'fr' ? 'Itinéraire, tous appareils' : lang === 'nl' ? 'Route, alle apparaten' : 'Directions, any device',
      url: `https://www.google.com/maps/search/?api=1&query=${lat}%2C${lng}` },
    { key: 'apple', label: 'Apple Plans', sub: 'iPhone, iPad, Mac',
      url: `https://maps.apple.com/?ll=${lat},${lng}&q=${encodeURIComponent(`${b.name}, ${b.town}`)}` },
    { key: 'waze', label: 'Waze',
      sub: lang === 'fr' ? 'Navigation auto' : lang === 'nl' ? 'Auto-navigatie' : 'Driving navigation',
      url: `https://www.waze.com/ul?ll=${lat}%2C${lng}&navigate=yes` },
  ];

  const mapEl = useRef(null);
  const mapInst = useRef(null);
  useEffect(() => {
    if (!mapEl.current || mapInst.current || typeof L === 'undefined') return;
    const map = L.map(mapEl.current, { zoomControl: false, scrollWheelZoom: false, dragging: !L.Browser.mobile }).setView([lat, lng], 15);
    L.control.zoom({ position: 'bottomright' }).addTo(map);
    mapInst.current = map;
    // OpenStreetMap tiles: no API key needed; attribution is mandatory
    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
      maxZoom: 19, attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
    }).addTo(map);
    const houseIcon = L.divIcon({
      className: 'vm-house-marker',
      html: `<div style="position:relative;transform:translate(-11px,-11px)">
        <div style="width:22px;height:22px;border-radius:50%;background:#c44d2e;border:3px solid #fffcf7;box-shadow:0 0 0 8px rgba(196,77,46,0.2),0 2px 6px rgba(0,0,0,0.2)"></div>
        <div style="position:absolute;left:30px;top:-1px;font:600 13px/1 Manrope,sans-serif;color:#2d3e2c;background:#fffcf7;padding:7px 12px;border-radius:999px;white-space:nowrap;box-shadow:0 2px 10px rgba(45,62,44,0.2)">S'fontaine 1</div>
      </div>`,
      iconSize: [0, 0], iconAnchor: [0, 0],
    });
    L.marker([lat, lng], { icon: houseIcon, keyboard: false }).addTo(map);
    return () => { map.remove(); mapInst.current = null; };
  }, [lat, lng]);

  return (
    <section className="vm-sec" id="localisation">
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16, flexWrap: 'wrap', marginBottom: 24 }}>
        <div>
          <h2 className="vm-h2" style={{ marginBottom: 6 }}>{t('loc_title')}</h2>
          <p className="vm-p vm-muted">{b.name}, {b.postalCode} {b.town}, {b.region}, {b.country}</p>
        </div>
        <div ref={dropdownRef} style={{ position: 'relative' }}>
          <button className="vm-btn vm-btn-outline vm-btn-sm" aria-expanded={dropdown} onClick={() => setDropdown(d => !d)}>
            {Ico.route}
            {lang === 'fr' ? 'Itinéraire' : lang === 'nl' ? 'Route openen' : 'Get directions'}
            <span style={{ display: 'flex', transform: dropdown ? 'rotate(180deg)' : 'none', transition: 'transform .2s' }}>{Ico.down}</span>
          </button>
          {dropdown && (
            <div role="menu" className="vm-menu" style={{ minWidth: 260 }}>
              {mapApps.map(app => (
                <a key={app.key} href={app.url} target="_blank" rel="noopener noreferrer" className="vm-menu-item"
                   onClick={() => setDropdown(false)} style={{ justifyContent: 'space-between' }}>
                  <span>
                    <span style={{ display: 'block', fontWeight: 600 }}>{app.label}</span>
                    <span className="vm-small vm-muted">{app.sub}</span>
                  </span>
                  <span style={{ color: 'var(--accent)' }}>↗</span>
                </a>
              ))}
            </div>
          )}
        </div>
      </div>
      <div className="vm-map" style={{ position: 'relative', height: 480, borderRadius: 12, overflow: 'hidden', isolation: 'isolate', background: 'var(--soft)' }}>
        <div ref={mapEl} style={{ position: 'absolute', inset: 0 }}/>
      </div>
      <div style={{ marginTop: 40 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', flexWrap: 'wrap', gap: 8, marginBottom: 8 }}>
          <h3 className="vm-h3">
            {lang === 'fr' ? 'Loisirs & commodités' : lang === 'nl' ? 'Voorzieningen in de buurt' : 'Leisure & amenities'}
          </h3>
          <span className="vm-small vm-muted">
            {lang === 'fr' ? 'Distances à vol d\'oiseau' : lang === 'nl' ? 'Hemelsbreed' : 'As the crow flies'}
          </span>
        </div>
        <div className="vm-dist" style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', columnGap: 40 }}>
          {V_ALPINE.distances.map((d, i) => (
            <div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 12,
                                  padding: '12px 0', borderBottom: '1px solid var(--line)' }}>
              <span style={{ font: '400 15px/1.3 "Manrope", sans-serif' }}>{trA(d, lang)}</span>
              <span style={{ font: '600 15px/1.3 "Manrope", sans-serif', whiteSpace: 'nowrap' }}>{d.m === 0 ? onSite : fmt(d.m)}</span>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

// ─── SKI AREA ───────────────────────────────────────────────────────────
function SkiAreaA({ lang }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  const stats = [
    { n: '165', l: { fr: 'km de pistes', en: 'km of slopes', nl: 'km piste' } },
    { n: '2 832', l: { fr: 'm au Cheval Noir', en: 'm at Cheval Noir', nl: 'm Cheval Noir' } },
    { n: '57', l: { fr: 'remontées', en: 'lifts', nl: 'liften' } },
  ];
  return (
    <section className="vm-sec" id="le-domaine">
      <div className="vm-ski" style={{ background: 'var(--ink)', color: 'var(--bg)', borderRadius: 24, padding: 48, display: 'grid',
                                       gridTemplateColumns: 'minmax(0, 1.2fr) minmax(0, 1fr)', gap: 48, alignItems: 'center' }}>
        <div>
          <h2 className="vm-h2" style={{ color: 'var(--bg)' }}>{t('ski_title')}</h2>
          <p className="vm-p" style={{ color: '#c5d1bf' }}>{t('ski_body')}</p>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 12, marginTop: 32 }}>
            {stats.map((s, i) => (
              <div key={i} style={{ background: 'rgba(245,241,234,0.08)', borderRadius: 12, padding: '14px 16px' }}>
                <div style={{ font: '600 24px/1.1 "Fraunces", serif', color: '#d4a574', whiteSpace: 'nowrap' }}>{s.n}</div>
                <div className="vm-small" style={{ color: '#a8b8a3', marginTop: 4 }}>{trA(s.l, lang)}</div>
              </div>
            ))}
          </div>
        </div>
        <PhotoA src={V_ALPINE.photos.ski} ratio="4/5" radius={16} label="Grand Domaine" tone="forest"/>
      </div>
    </section>
  );
}

// ─── THINGS TO DO ───────────────────────────────────────────────────────
function ThingsA({ lang }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  const [season, setSeason] = useState('winter');
  const name = { winter: { fr: 'Hiver', en: 'Winter', nl: 'Winter' }, summer: { fr: 'Été', en: 'Summer', nl: 'Zomer' } };
  return (
    <section className="vm-sec">
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16, flexWrap: 'wrap', marginBottom: 24 }}>
        <h2 className="vm-h2" style={{ margin: 0 }}>{t('todo_title')}</h2>
        <div role="tablist" style={{ display: 'inline-flex', background: 'var(--soft)', borderRadius: 999, padding: 4 }}>
          {['winter', 'summer'].map(s => (
            <button key={s} role="tab" aria-selected={season === s} onClick={() => setSeason(s)} style={{
              border: 'none', borderRadius: 999, padding: '9px 20px', cursor: 'pointer', color: 'var(--ink)',
              background: season === s ? 'var(--surface)' : 'transparent', boxShadow: season === s ? 'var(--shadow)' : 'none',
              font: '600 14px/1 "Manrope", sans-serif', transition: 'background-color .2s, box-shadow .2s' }}>{trA(name[s], lang)}</button>
          ))}
        </div>
      </div>
      <div className="vm-todo" style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 16 }}>
        {V_ALPINE.todo[season].map((item, i) => (
          <div key={i} className="vm-card" style={{ padding: 20 }}>
            <div style={{ width: 32, height: 32, borderRadius: '50%', background: 'var(--soft)', color: 'var(--accent)', marginBottom: 14,
                          display: 'grid', placeItems: 'center', font: '700 14px/1 "Manrope", sans-serif' }}>{i + 1}</div>
            <div style={{ font: '600 16px/1.35 "Manrope", sans-serif' }}>{trA(item, lang)}</div>
          </div>
        ))}
      </div>
    </section>
  );
}

// ─── HOST ───────────────────────────────────────────────────────────────
function HostA({ lang }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  const c = V_ALPINE.contact;
  const row = (label, value, first) => (
    <div style={{ padding: '12px 0', borderTop: first ? 'none' : '1px solid var(--line)' }}>
      <div className="vm-small vm-muted">{label}</div>
      <div style={{ font: '600 15px/1.35 "Manrope", sans-serif', overflowWrap: 'anywhere' }}>{value}</div>
    </div>
  );
  return (
    <section className="vm-sec" id="hote">
      <h2 className="vm-h2">{t('own_title')}</h2>
      <div className="vm-host" style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 380px) minmax(0, 1fr)', gap: 64, alignItems: 'start' }}>
        <div style={{ background: 'var(--surface)', borderRadius: 24, boxShadow: 'var(--shadow-lg)', padding: '28px 28px 16px' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 20, marginBottom: 12 }}>
            <Avatar size={88}/>
            <div>
              <div style={{ font: '600 24px/1.15 "Fraunces", serif' }}>{c.name}</div>
              <div className="vm-small vm-muted" style={{ marginTop: 4 }}>{t('own_host')}</div>
            </div>
          </div>
          {row(t('own_since'), '2022', true)}
          {row(t('own_languages'), 'Français, English, Nederlands')}
        </div>
        <div>
          <p className="vm-p" style={{ marginBottom: 20 }}>{t('own_body')}</p>
          {row(t('own_phone'), c.phone, true)}
          {row(t('own_email'), c.email)}
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 12, marginTop: 20 }}>
            <a className="vm-btn vm-btn-dark" href={telHref(c.phone)}>{t('own_call')}</a>
            <a className="vm-btn vm-btn-outline" href={`mailto:${c.email}`}>{t('own_mail')}</a>
          </div>
        </div>
      </div>
    </section>
  );
}

// ─── FAQ ────────────────────────────────────────────────────────────────
function FaqA({ lang }) {
  const t = (k) => trA(V_ALPINE.i18n[k], lang);
  const [open, setOpen] = useState(0);
  return (
    <section className="vm-sec">
      <h2 className="vm-h2">{t('faq_title')}</h2>
      <div style={{ maxWidth: 760 }}>
        {V_ALPINE.faq.map((f, i) => {
          const isOpen = open === i;
          return (
            <div key={i} style={{ borderBottom: '1px solid var(--line)' }}>
              <button onClick={() => setOpen(isOpen ? -1 : i)} aria-expanded={isOpen} style={{
                width: '100%', padding: '20px 0', background: 'transparent', border: 'none', cursor: 'pointer',
                display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16, textAlign: 'left',
                font: '600 16px/1.4 "Manrope", sans-serif', color: 'var(--ink)' }}>
                <span>{trA(f.q, lang)}</span>
                <span style={{ display: 'flex', flexShrink: 0, transform: isOpen ? 'rotate(180deg)' : 'none', transition: 'transform .2s' }}>{Ico.down}</span>
              </button>
              {isOpen && <p className="vm-p vm-muted" style={{ paddingBottom: 20 }}>{trA(f.a, lang)}</p>}
            </div>
          );
        })}
      </div>
    </section>
  );
}

// ─── FOOTER ─────────────────────────────────────────────────────────────
function FooterA() {
  const c = V_ALPINE.contact;
  const head = { font: '600 14px/1.3 "Manrope", sans-serif', color: '#d4a574', marginBottom: 12 };
  return (
    <footer style={{ background: '#1f2c1e', color: '#a8b8a3', marginTop: 48, padding: '48px 0 28px' }}>
      <div className="vm-wrap">
        <div className="vm-footer-grid" style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr', gap: 48, paddingBottom: 32,
                                                 borderBottom: '1px solid rgba(168,184,163,0.2)' }}>
          <div>
            <div style={{ font: '600 21px/1.1 "Fraunces", serif', color: 'var(--bg)' }}>S'fontaine 1</div>
            <div className="vm-small" style={{ marginTop: 10 }}>Les Avanchers-Valmorel, Savoie, 1 350 m</div>
          </div>
          <div>
            <div style={head}>Contact</div>
            <div className="vm-small" style={{ lineHeight: 1.8, overflowWrap: 'anywhere' }}>
              <a href={`mailto:${c.email}`} style={{ color: 'inherit' }}>{c.email}</a><br/>
              <a href={telHref(c.phone)} style={{ color: 'inherit' }}>{c.phone}</a>
            </div>
          </div>
          <div>
            <div style={head}>Légal</div>
            <div className="vm-small" style={{ lineHeight: 1.8 }}>Mentions légales<br/>CGV<br/>Cookies</div>
          </div>
        </div>
        <div className="vm-small" style={{ display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12, marginTop: 20, color: '#6b7d68' }}>
          <span>© 2026 S'fontaine 1, réf. {V_ALPINE.brand.ref}</span>
          <span>45.45°N, 6.41°E, 1 350 m</span>
        </div>
      </div>
    </footer>
  );
}

// ─── LIGHTBOX ───────────────────────────────────────────────────────────
function LightboxA({ index, lang, onClose, onPrev, onNext, onJump }) {
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') onClose();
      else if (e.key === 'ArrowRight') onNext();
      else if (e.key === 'ArrowLeft') onPrev();
    };
    window.addEventListener('keydown', onKey);
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = prev; };
  }, [onClose, onPrev, onNext]);

  const photos = V_ALPINE.photos.gallery;
  const img = photos[index];
  if (!img) return null;
  const label = trA(img.caption, lang);
  const tone = TONE[TONES[index % 3]];
  const roundBtn = { width: 48, height: 48, borderRadius: '50%', border: 'none', background: 'rgba(255,252,247,0.92)',
                     color: 'var(--ink)', cursor: 'pointer', display: 'grid', placeItems: 'center', boxShadow: 'var(--shadow)' };

  return (
    <div style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(20,25,18,0.95)', display: 'flex',
                  flexDirection: 'column', animation: 'lbFadeIn .22s ease' }}>
      <style>{`@keyframes lbFadeIn { from {opacity:0} to {opacity:1} }`}</style>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '14px 20px', color: 'var(--bg)', flexShrink: 0, gap: 16 }}>
        <div style={{ display: 'flex', gap: 12, alignItems: 'center', minWidth: 0 }}>
          <span style={{ padding: '6px 12px', borderRadius: 999, background: 'rgba(245,241,234,0.12)', font: '600 13px/1 "Manrope", sans-serif' }}>
            {index + 1} / {photos.length}
          </span>
          <span style={{ font: '600 17px/1.2 "Fraunces", serif', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{label}</span>
        </div>
        <button onClick={onClose} aria-label="Close" style={roundBtn}>{Ico.close}</button>
      </div>
      <div className="vm-lb-stage" style={{ flex: 1, position: 'relative', display: 'flex', alignItems: 'center',
                                            justifyContent: 'center', padding: '4px 88px 96px', minHeight: 0 }}>
        <button onClick={onPrev} aria-label="Previous" style={{ ...roundBtn, position: 'absolute', left: 20, top: '50%', transform: 'translateY(-50%)', zIndex: 5 }}>{Ico.left}</button>
        <div style={{ width: '100%', height: '100%', minHeight: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          {img.photo ? (
            <img src={img.photo} alt={label} style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', display: 'block',
                                                     borderRadius: 12, boxShadow: '0 30px 80px rgba(0,0,0,0.45)' }}/>
          ) : (
            <div style={{ height: '100%', aspectRatio: '3/2', maxWidth: '100%', borderRadius: 12, position: 'relative',
                          background: tone.bg, backgroundImage: stripes(tone) }}>
              <div style={{ position: 'absolute', bottom: 20, left: 24, font: '600 13px/1.2 "Manrope", sans-serif', color: tone.text }}>{label}</div>
            </div>
          )}
        </div>
        <button onClick={onNext} aria-label="Next" style={{ ...roundBtn, position: 'absolute', right: 20, top: '50%', transform: 'translateY(-50%)', zIndex: 5 }}>{Ico.right}</button>
      </div>
      <div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '24px 20px 16px', display: 'flex', gap: 8,
                    justifyContent: 'center', flexWrap: 'wrap', pointerEvents: 'none',
                    background: 'linear-gradient(to top, rgba(16,20,14,0.9), rgba(16,20,14,0))' }}>
        {photos.map((g, i) => {
          const gt = TONE[TONES[i % 3]];
          const active = i === index;
          return (
            <button key={i} onClick={() => onJump(i)} aria-label={trA(g.caption, lang)} style={{
              width: 72, height: 50, flexShrink: 0, pointerEvents: 'auto', padding: 0, cursor: 'pointer', borderRadius: 8,
              backgroundColor: gt.bg, backgroundImage: g.photo ? `url(${g.photo})` : stripes(gt, 8),
              backgroundSize: 'cover', backgroundPosition: 'center',
              border: active ? '2px solid var(--bg)' : '2px solid transparent', opacity: active ? 1 : 0.55,
              transition: 'opacity .18s, border-color .18s' }}/>
          );
        })}
      </div>
    </div>
  );
}

// ─── PAGE ───────────────────────────────────────────────────────────────
function VariantAlpine() {
  const [lang, setLang] = useState('fr');
  const [lightboxIdx, setLightboxIdx] = useState(null);
  const n = V_ALPINE.photos.gallery.length;
  const open = useCallback((i) => setLightboxIdx(i), []);
  const close = useCallback(() => setLightboxIdx(null), []);
  const goNext = useCallback(() => setLightboxIdx(i => (i + 1) % n), [n]);
  const goPrev = useCallback(() => setLightboxIdx(i => (i - 1 + n) % n), [n]);
  const bk = useBooking(lang);
  const rv = useReviews(lang);
  const rates = useRates(lang);
  const price = Math.min(...rates.map(r => r.price));
  return (
    <LightboxCtx.Provider value={open}>
      <TopBarA lang={lang} setLang={setLang}/>
      <main className="vm-wrap">
        <TitleA lang={lang} rv={rv}/>
        <div className="vm-two-col">
          <div>
            <OverviewA lang={lang}/>
            <FloorsA lang={lang}/>
            <AmenitiesA lang={lang}/>
            <CalendarA lang={lang} bk={bk}/>
            <div className="vm-sub vm-only-m"><BookingCardA lang={lang} bk={bk} price={price}/></div>
            <RatesA lang={lang} rows={rates}/>
            <WhyA lang={lang}/>
          </div>
          <aside className="vm-only-d">
            <div className="vm-sticky"><BookingCardA lang={lang} bk={bk} price={price}/></div>
          </aside>
        </div>
        <ReviewsA lang={lang} rv={rv}/>
        <LocationA lang={lang}/>
        <SkiAreaA lang={lang}/>
        <ThingsA lang={lang}/>
        <HostA lang={lang}/>
        <FaqA lang={lang}/>
      </main>
      <FooterA/>
      {lightboxIdx !== null && (
        <LightboxA index={lightboxIdx} lang={lang} onClose={close} onPrev={goPrev} onNext={goNext} onJump={setLightboxIdx}/>
      )}
    </LightboxCtx.Provider>
  );
}

window.VariantAlpine = VariantAlpine;
