// UI primitives for HOS

const { useState, useEffect, useRef, useCallback, useMemo, createContext, useContext } = React;

/* ───────────── Currency ───────────── */
const CURRENCY_META = {
  CAD: { symbol: '$',  label: 'CAD', locale: 'en-CA', flagUrl: 'https://flagcdn.com/24x18/ca.png' },
  USD: { symbol: '$',  label: 'USD', locale: 'en-US', flagUrl: 'https://flagcdn.com/24x18/us.png' },
  EUR: { symbol: '€',  label: 'EUR', locale: 'fr-FR', flagUrl: 'https://flagcdn.com/24x18/eu.png' },
  TND: { symbol: 'DT', label: 'TND', locale: 'fr-TN', flagUrl: 'https://flagcdn.com/24x18/tn.png' },
};
// Detect the visitor's currency from their timezone (offline, no API):
// Canada → CAD, Tunisia → TND, Europe → EUR, USA & everywhere else → USD.
const CANADA_TZ = ['America/Toronto', 'America/Montreal', 'America/Vancouver', 'America/Edmonton', 'America/Winnipeg', 'America/Halifax', 'America/St_Johns', 'America/Regina', 'America/Moncton', 'America/Glace_Bay', 'America/Goose_Bay', 'America/Whitehorse', 'America/Dawson', 'America/Iqaluit', 'America/Yellowknife', 'America/Blanc-Sablon', 'America/Rankin_Inlet', 'America/Cambridge_Bay'];
function detectCurrency(enabled, base) {
  let cur = 'USD';
  try {
    const tz = (Intl.DateTimeFormat().resolvedOptions().timeZone) || '';
    const lang = (typeof navigator !== 'undefined' && (navigator.language || '')).toUpperCase();
    if (tz === 'Africa/Tunis' || lang.endsWith('-TN')) cur = 'TND';
    else if (CANADA_TZ.includes(tz) || lang.endsWith('-CA')) cur = 'CAD';
    else if (tz.startsWith('Europe/')) cur = 'EUR';
    else cur = 'USD';
  } catch { cur = base; }
  return enabled.includes(cur) ? cur : (enabled.includes('USD') ? 'USD' : base);
}
function CurrencyFlag({ code, size = 16 }) {
  const m = CURRENCY_META[code] || {};
  if (m.flagUrl) return <img src={m.flagUrl} alt="" width={size} height={Math.round(size * 0.75)} style={{ borderRadius: 2, objectFit: 'cover', display: 'inline-block' }} />;
  // USD = worldwide → globe icon (renders everywhere, unlike emoji flags)
  return <svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth="1.5" style={{ display: 'inline-block' }}><circle cx="12" cy="12" r="9" /><path d="M3 12h18M12 3c2.5 2.5 2.5 15 0 18M12 3c-2.5 2.5-2.5 15 0 18" /></svg>;
}
// Reference cross-rates expressed per 1 CAD — used only as a safety net when a configured
// rate is missing or obviously mis-entered, so a live customer never sees a broken price.
// The sane rate for ANY base = SANE_RATES[currency] / SANE_RATES[base].
const SANE_RATES = { CAD: 1, USD: 0.73, EUR: 0.62, TND: 2.08 };
// Convert a BASE-currency amount into `currency` using rates (rate = units of `currency` per 1 base).
function convertPrice(baseAmount, { currency, base = 'CAD', rates = {} } = {}) {
  const amt = Number(baseAmount || 0);
  if (!currency || currency === base) return amt;
  let rate = Number(rates && rates[currency]);
  // LIVE SAFETY NET: a mis-entered rate (e.g. 0.62 typed as 62, or left blank) must never
  // reach a shopper as a 10–100x-wrong price. We derive a sane cross-rate for the store's
  // actual base currency and keep the configured rate only when it's within 4x of it;
  // otherwise we fall back to the sane value. Works for any base (CAD, TND, …).
  const def = (Number(SANE_RATES[base]) > 0 && Number(SANE_RATES[currency]) > 0)
    ? Number(SANE_RATES[currency]) / Number(SANE_RATES[base]) : 0;
  if (!(rate > 0)) {
    rate = def > 0 ? def : 0;
  } else if (def > 0) {
    if (rate > def * 4 || rate < def / 4) rate = def;
  } else if (rate > 20 || rate < 0.02) {
    rate = 0;
  }
  return rate > 0 ? amt * rate : amt;
}
// Effective price in the selected currency: a pinned per-zone override wins, otherwise we
// auto-convert the base amount. `overrides` is a map like { EUR: 55, TND: 199 } (only the
// currencies the admin chose to fix); anything missing/zero falls back to conversion.
function effectivePrice(baseAmount, overrides, opts = {}) {
  const currency = opts.currency || opts.base || 'CAD';
  const ov = overrides && typeof overrides === 'object' ? Number(overrides[currency]) : NaN;
  if (ov > 0) return ov;
  return convertPrice(baseAmount, opts);
}
function formatPrice(baseAmount, opts = {}, overrides = null) {
  const currency = opts.currency || opts.base || 'CAD';
  const meta = CURRENCY_META[currency] || { symbol: '$', label: currency, locale: 'en-US' };
  const value = effectivePrice(baseAmount, overrides, opts);
  // Show cents when the converted amount isn't a whole number (e.g. 62,01 €), but keep round
  // prices clean (80 €, 199 DT). Never round 62.01 up to a wrong-looking 62 or 620.
  let num;
  try {
    num = new Intl.NumberFormat(meta.locale, { minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(value);
  } catch {
    num = (Math.round(value * 100) / 100).toString();
  }
  return meta.symbol === 'DT' ? `${num} DT` : `${meta.symbol}${num}`;
}

const CurrencyCtx = createContext({ currency: 'CAD', setCurrency: () => {}, base: 'CAD', rates: {}, enabled: ['CAD'] });

function CurrencyProvider({ children }) {
  const { siteData } = (typeof useSiteData === 'function' ? useSiteData() : { siteData: {} });
  const s = (siteData && siteData.settings) || {};
  const base = s.baseCurrency || 'CAD';
  const rates = s.currencyRates || {};
  const enabled = (s.enabledCurrencies && s.enabledCurrencies.length) ? s.enabledCurrencies : ['CAD', 'EUR', 'USD', 'TND'];
  const [currency, setCurrencyState] = useState(() => {
    // Honour a returning visitor's manual choice; otherwise auto-detect from their location.
    try {
      const saved = localStorage.getItem('hos.currency');
      if (saved) return saved;
    } catch {}
    return detectCurrency(['CAD', 'EUR', 'USD', 'TND'], base);
  });
  // Keep the selected currency valid against what the store enables.
  useEffect(() => {
    if (!enabled.includes(currency)) setCurrencyState(base);
  }, [enabled.join(','), base]);
  const setCurrency = useCallback((c) => {
    try { localStorage.setItem('hos.currency', c); } catch {}
    setCurrencyState(c);
  }, []);
  return <CurrencyCtx.Provider value={{ currency, setCurrency, base, rates, enabled }}>{children}</CurrencyCtx.Provider>;
}

function useCurrency() {
  const ctx = useContext(CurrencyCtx);
  // fmt(amount, overrides?) — overrides is an optional per-zone price map for this item.
  const fmt = useCallback((baseAmount, overrides) => formatPrice(baseAmount, { currency: ctx.currency, base: ctx.base, rates: ctx.rates }, overrides), [ctx.currency, ctx.base, ctx.rates]);
  // priceOf(amount, overrides?) — the raw number in the selected currency (for maths/sums).
  const priceOf = useCallback((baseAmount, overrides) => effectivePrice(baseAmount, overrides, { currency: ctx.currency, base: ctx.base, rates: ctx.rates }), [ctx.currency, ctx.base, ctx.rates]);
  // fmtRaw(value) — format a number that is ALREADY in the selected currency (no conversion).
  // Use for line totals / subtotals after summing per-unit override-aware prices × qty.
  const fmtRaw = useCallback((value) => formatPrice(value, { currency: ctx.currency, base: ctx.currency, rates: ctx.rates }), [ctx.currency, ctx.rates]);
  return { ...ctx, fmt, priceOf, fmtRaw };
}

// Drop-in price display — shows a base amount in the visitor's selected currency.
// Pass `overrides` (a per-zone price map) to honour a pinned price for the active currency.
function Price({ amount, overrides, className }) {
  const { fmt } = useCurrency();
  return <span className={className}>{fmt(amount, overrides)}</span>;
}

function CurrencySwitcher({ tone = 'deep', className = '' }) {
  const { currency, setCurrency, enabled } = useCurrency();
  const c = tone === 'ivory' ? 'text-ivory/85' : 'text-deep';
  if (!enabled || enabled.length < 2) return null;
  const cur = CURRENCY_META[currency] || { label: currency };
  return (
    <span className={`relative inline-flex items-center ${className}`}>
      <span className={`pointer-events-none inline-flex items-center gap-1.5 font-sans text-[11px] tracking-widest-2 uppercase ${c}`}>
        <CurrencyFlag code={currency} size={16} />{cur.label}
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" className="w-3 h-3 opacity-60"><path d="M6 9l6 6 6-6" strokeLinecap="round" strokeLinejoin="round"/></svg>
      </span>
      <select value={currency} onChange={(e) => setCurrency(e.target.value)} aria-label="Currency"
        className="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
        {enabled.map((code) => {
          const m = CURRENCY_META[code] || { label: code };
          return <option key={code} value={code}>{m.label}</option>;
        })}
      </select>
    </span>
  );
}

/* ───────────── Language Context ───────────── */
const LangCtx = createContext({ lang: 'en', setLang: () => {} });

function LangProvider({ children }) {
  const [lang, setLangState] = useState(() => {
    try { return localStorage.getItem('hos.lang') || 'fr'; } catch { return 'fr'; }
  });
  const setLang = useCallback((l) => {
    try { localStorage.setItem('hos.lang', l); } catch {}
    setLangState(l);
  }, []);
  useEffect(() => {
    document.documentElement.lang = lang;
  }, [lang]);
  return <LangCtx.Provider value={{ lang, setLang }}>{children}</LangCtx.Provider>;
}

function useLang() {
  const { lang, setLang } = useContext(LangCtx);
  const t = useCallback((en, fr) => (lang === 'fr' && fr ? fr : en), [lang]);
  return { lang, setLang, t };
}

/* ───────────── Language Switcher ───────────── */
function LangSwitcher({ tone = 'deep', className = '' }) {
  const { lang, setLang } = useLang();
  const c = tone === 'ivory' ? 'text-ivory/80' : 'text-deep';
  return (
    <div className={`inline-flex items-center font-sans text-[11px] tracking-widest-2 uppercase ${c} ${className}`}>
      <button onClick={() => setLang('en')}
              className={`px-1.5 py-1 transition-opacity ${lang==='en' ? 'opacity-100' : 'opacity-40 hover:opacity-70'}`}>
        EN
      </button>
      <span className="opacity-30">/</span>
      <button onClick={() => setLang('fr')}
              className={`px-1.5 py-1 transition-opacity ${lang==='fr' ? 'opacity-100' : 'opacity-40 hover:opacity-70'}`}>
        FR
      </button>
    </div>
  );
}


/* ───────────── Cart Context ───────────── */
const CartCtx = createContext(null);
const CART_KEY = 'roudaina.cart.v1';
const WISH_KEY = 'roudaina.wish.v1';

function CartProvider({ children }) {
  const [items, setItems] = useState(() => {
    try { return JSON.parse(localStorage.getItem(CART_KEY) || '[]'); } catch { return []; }
  });
  const [wishlist, setWishlist] = useState(() => {
    try { return new Set(JSON.parse(localStorage.getItem(WISH_KEY) || '[]')); } catch { return new Set(); }
  });
  const [open, setOpen] = useState(false);
  const [toast, setToast] = useState(null);

  useEffect(() => {
    try { localStorage.setItem(CART_KEY, JSON.stringify(items)); } catch {}
  }, [items]);

  useEffect(() => {
    try { localStorage.setItem(WISH_KEY, JSON.stringify([...wishlist])); } catch {}
  }, [wishlist]);

  const add = useCallback((p) => {
    setItems(curr => {
      const ex = curr.find(x => x.id === p.id);
      if (ex) return curr.map(x => x.id === p.id ? {...x, qty: x.qty + 1} : x);
      return [...curr, { ...p, qty: 1 }];
    });
    setToast({ name: p.name, ts: Date.now() });
  }, []);

  const remove = useCallback((id) => setItems(c => c.filter(x => x.id !== id)), []);
  const setQty = useCallback((id, qty) => setItems(c => c.map(x => x.id === id ? {...x, qty: Math.max(1, qty)} : x)), []);

  const toggleWish = useCallback((id) => {
    setWishlist(curr => {
      const next = new Set(curr);
      if (next.has(id)) next.delete(id); else next.add(id);
      return next;
    });
  }, []);

  const subtotal = useMemo(() => items.reduce((s,x) => s + x.price*x.qty, 0), [items]);
  const count    = useMemo(() => items.reduce((s,x) => s + x.qty, 0), [items]);

  useEffect(() => {
    if (!toast) return;
    const t = setTimeout(() => setToast(null), 2200);
    return () => clearTimeout(t);
  }, [toast]);

  return (
    <CartCtx.Provider value={{ items, add, remove, setQty, subtotal, count, open, setOpen, wishlist, toggleWish, toast }}>
      {children}
    </CartCtx.Provider>
  );
}
const useCart = () => useContext(CartCtx);

/* ───────────── Reveal on scroll ───────────── */
function useReveal(dep) {
  useEffect(() => {
    // Wait a tick so newly-mounted elements are in the DOM
    const t = setTimeout(() => {
      const els = document.querySelectorAll('.reveal:not(.in)');
      const io = new IntersectionObserver((entries) => {
        entries.forEach(e => {
          if (e.isIntersecting) {
            e.target.classList.add('in');
            io.unobserve(e.target);
          }
        });
      }, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
      els.forEach(el => io.observe(el));
      // Store on closure so cleanup can reach it
      useReveal._io = io;
    }, 40);
    return () => {
      clearTimeout(t);
      if (useReveal._io) { useReveal._io.disconnect(); useReveal._io = null; }
    };
  }, [dep]);
}

/* ───────────── HOS Logo (SVG-like via type) ───────────── */
function Logo({ size = 'md', tone = 'rose', mark = true }) {
  const sizes = {
    sm: { mark: 'text-[22px]', sub: 'text-[8px]' },
    md: { mark: 'text-[34px]', sub: 'text-[10px]' },
    lg: { mark: 'text-[80px]', sub: 'text-[14px]' },
    xl: { mark: 'text-[140px]', sub: 'text-[18px]' },
  };
  const s = sizes[size] || sizes.md;
  const color = tone === 'ivory' ? 'text-ivory' : tone === 'deep' ? 'text-deep' : 'text-rose';
  return (
    <div className={`inline-flex flex-col items-center leading-none select-none ${color}`}>
      <span className={`font-display tracking-[.16em] ${s.mark}`} style={{lineHeight: 1}}>HOS</span>
      {mark && (
        <>
          <span className={`font-sans tracking-widest-2 mt-2 ${s.sub}`}>HOUSE&nbsp;OF&nbsp;SHEYLAS</span>
          <div className="flex items-center gap-2 mt-1.5 opacity-80">
            <span className="block h-px w-8" style={{background:'currentColor'}}></span>
            <Diamond />
            <span className="block h-px w-8" style={{background:'currentColor'}}></span>
          </div>
        </>
      )}
    </div>
  );
}

function Diamond({ size = 8 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 10 10" fill="none">
      <path d="M5 0 L6 4 L10 5 L6 6 L5 10 L4 6 L0 5 L4 4 Z" fill="currentColor"/>
    </svg>
  );
}

/* ───────────── Color swatch helpers ─────────────
   A color is { name, hex, hex2? }. With hex2 the swatch renders as a
   half / half split (the "two-tone bull"), e.g. half red, half blue. */
function _isLightHex(hex) {
  if (!hex || hex.length < 7) return false;
  const r = parseInt(hex.slice(1,3),16), g = parseInt(hex.slice(3,5),16), b = parseInt(hex.slice(5,7),16);
  return (0.2126*r + 0.7152*g + 0.0722*b) > 208;
}
function swatchBg(c) {
  if (!c) return 'transparent';
  if (c.hex2) return `linear-gradient(135deg, ${c.hex} 0 50%, ${c.hex2} 50% 100%)`;
  return c.hex;
}
function swatchStyle(c) {
  const light = c && (_isLightHex(c.hex) || (c.hex2 && _isLightHex(c.hex2)));
  return { background: swatchBg(c), border: light ? '1px solid rgba(74,45,51,.28)' : 'none' };
}

/* ───────────── Icons ───────────── */
const I = {
  search:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" {...p}><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5" strokeLinecap="round"/></svg>,
  user:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" {...p}><circle cx="12" cy="8" r="4"/><path d="M4 21c1.5-4 5-6 8-6s6.5 2 8 6" strokeLinecap="round"/></svg>,
  bag:     (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" {...p}><path d="M5 8h14l-1 12H6L5 8Z"/><path d="M9 8a3 3 0 0 1 6 0" strokeLinecap="round"/></svg>,
  heart:   (props) => {
    const filled = !!(props && props.filled);
    return <svg viewBox="0 0 24 24" fill={filled ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="1.4" className={props && props.className} style={props && props.style}><path d="M12 21s-7-4.5-9.3-9A5 5 0 0 1 12 6.3 5 5 0 0 1 21.3 12C19 16.5 12 21 12 21Z" strokeLinejoin="round"/></svg>;
  },
  close:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" {...p}><path d="M6 6 18 18 M18 6 6 18" strokeLinecap="round"/></svg>,
  menu:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" {...p}><path d="M3 7h18 M3 12h18 M3 17h12" strokeLinecap="round"/></svg>,
  plus:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" {...p}><path d="M12 5v14 M5 12h14" strokeLinecap="round"/></svg>,
  minus:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" {...p}><path d="M5 12h14" strokeLinecap="round"/></svg>,
  arrowR:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" {...p}><path d="M4 12h16 M14 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  arrowDown:(p)=> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" {...p}><path d="M12 4v16 M6 14l6 6 6-6" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  star:    (p) => <svg viewBox="0 0 24 24" fill="currentColor" {...p}><path d="m12 2 2.9 6.6 7.1.7-5.4 4.8 1.6 7L12 17.8 5.8 21l1.6-7L2 9.3l7.1-.7L12 2Z"/></svg>,
  check:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" {...p}><path d="m5 12 5 5 9-11" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  insta:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" {...p}><rect x="3" y="3" width="18" height="18" rx="5"/><circle cx="12" cy="12" r="4"/><circle cx="17.5" cy="6.5" r=".8" fill="currentColor"/></svg>,
  tiktok:  (p) => <svg viewBox="0 0 24 24" fill="currentColor" {...p}><path d="M14 3v10.5a2.5 2.5 0 1 1-2.5-2.5h.5v-3h-.5A5.5 5.5 0 1 0 17 13.5V8.2a6.6 6.6 0 0 0 4 1.3v-3a3.6 3.6 0 0 1-3.6-3.5H14Z"/></svg>,
  pin:     (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" {...p}><path d="M12 21V14 M12 14a4 4 0 1 0-4-4 4 4 0 0 0 4 4Z" strokeLinecap="round"/></svg>,
  facebook:(p) => <svg viewBox="0 0 24 24" fill="currentColor" {...p}><path d="M14 8.5h2V5.7h-2.3c-2 0-3.2 1.2-3.2 3.3v1.8H8.4V13h2.1v6.3h2.8V13h2.1l.4-2.7h-2.5V9.2c0-.5.3-.7.9-.7Z"/></svg>,
};

/* ───────────── Button ───────────── */
function Button({ children, variant = 'primary', size = 'md', className = '', as: As = 'button', ...rest }) {
  const base = 'btn-sweep inline-flex items-center justify-center gap-2 font-sans uppercase tracking-widest-2 transition-all duration-500 select-none';
  const sizes = {
    sm: 'text-[11px] px-5 py-2.5',
    md: 'text-[12px] px-7 py-3.5',
    lg: 'text-[12px] px-9 py-4',
  };
  const variants = {
    primary: 'bg-deep text-ivory hover:bg-rose',
    rose:    'bg-rose text-ivory hover:bg-deep',
    ivory:   'bg-ivory text-deep hover:bg-deep hover:text-ivory border border-deep/15',
    ghost:   'bg-transparent text-deep border border-deep/25 hover:bg-deep hover:text-ivory',
    ghostLight:'bg-transparent text-ivory border border-ivory/40 hover:bg-ivory hover:text-deep',
    link:    'text-deep border-b border-deep/30 hover:border-deep px-0 py-1 tracking-wider-2',
  };
  return (
    <As className={`${base} ${sizes[size]} ${variants[variant]} ${className}`} {...rest}>
      {children}
    </As>
  );
}

/* ───────────── SectionLabel (eyebrow) ───────────── */
function Eyebrow({ children, tone = 'rose', className = '' }) {
  const c = tone === 'ivory' ? 'text-ivory/80' : 'text-rose';
  return (
    <div className={`inline-flex items-center gap-3 font-sans text-[11px] tracking-widest-2 uppercase ${c} ${className}`}>
      <span className="block h-px w-6 bg-current opacity-60"></span>
      <span>{children}</span>
      <span className="block h-px w-6 bg-current opacity-60"></span>
    </div>
  );
}

/* ───────────── ProductCard ───────────── */
const TAG_FR = { 'New':'Nouveau', 'Just in':'Arrivage', 'Limited':'Édition limitée', 'Bestseller':'Best-seller', 'Most loved':'Plus aimé', 'Restocked':'Réassorti' };
function ProductCard({ p, idx = 0, reveal = true }) {
  const cart = useCart();
  const { t, lang } = useLang();
  const { fmt } = useCurrency();
  const liked = cart.wishlist.has(p.id);
  const [activeColor, setActiveColor] = useState(0);
  const [imgLoaded, setImgLoaded] = useState(false);
  const tagLabel = p.tag ? (lang === 'fr' ? (TAG_FR[p.tag] || p.tag) : p.tag) : null;
  const soldOut = p.stock === 0 || p.stock === '0';
  return (
    <article className={`${reveal ? `reveal reveal-delay-${(idx % 4) + 1} ` : ''}group`}>
      <a href={`#product/${p.id}`} className="relative overflow-hidden bg-blush-2 aspect-[3/4] rounded-lg block transition-shadow duration-500 group-hover:shadow-[0_24px_55px_-32px_rgba(74,45,51,0.55)]">
        {!imgLoaded && (
          <div className="absolute inset-0" style={{background:'linear-gradient(110deg,#F8E4E7 30%, #F6D6DA 50%, #F8E4E7 70%)', backgroundSize:'200% 100%'}} />
        )}
        <img
          src={p.img}
          alt={p.name}
          onLoad={() => setImgLoaded(true)}
          className={`img-zoom absolute inset-0 w-full h-full object-cover transition-opacity duration-700 ${imgLoaded?'opacity-100':'opacity-0'} ${soldOut ? 'grayscale-[0.35] opacity-80' : ''}`}
          loading="lazy"
        />
        {/* Tag / Sold out */}
        {soldOut ? (
          <span className="absolute top-3 left-3 bg-deep text-ivory text-[10px] tracking-widest-2 uppercase px-2.5 py-1.5">
            {t('Sold out', 'Épuisé')}
          </span>
        ) : tagLabel && (
          <span className="absolute top-3 left-3 bg-ivory/90 backdrop-blur-sm text-deep text-[10px] tracking-widest-2 uppercase px-2.5 py-1.5">
            {tagLabel}
          </span>
        )}
        {/* Wish */}
        <button
          aria-label={t('Add to wishlist', 'Ajouter aux favoris')}
          onClick={(e) => { e.preventDefault(); e.stopPropagation(); cart.toggleWish(p.id); }}
          className={`absolute top-3 right-3 w-9 h-9 rounded-full flex items-center justify-center transition-all duration-300
            ${liked ? 'bg-rose text-ivory scale-110' : 'bg-ivory/90 text-deep hover:bg-ivory'}`}
        >
          <I.heart filled={liked} className="w-[18px] h-[18px]"/>
        </button>
        {/* Quick add — slides up on hover (disabled when sold out) */}
        <div className="absolute inset-x-3 bottom-3 translate-y-[140%] group-hover:translate-y-0 transition-transform duration-500 ease-out">
          <button
            disabled={soldOut}
            onClick={(e) => { e.preventDefault(); e.stopPropagation(); if (soldOut) return; cart.add({ id:p.id, name:p.name, price:p.price, overrides:p.priceOverrides, img:p.img, color: p.colors?.[activeColor]?.name }); }}
            className={`w-full font-sans uppercase tracking-widest-2 text-[11px] py-3.5 transition-colors ${soldOut ? 'bg-deep/40 text-ivory/80 cursor-not-allowed' : 'bg-deep text-ivory hover:bg-rose'}`}
          >
            {soldOut ? t('Sold out', 'Épuisé') : `+ ${t('Add to bag', 'Ajouter au panier')}`}
          </button>
        </div>
      </a>
      <div className="pt-4">
        <h3 className="font-serif text-[19px] leading-snug text-deep tracking-[0.01em]">
          <a href={`#product/${p.id}`} className="hover:text-rose transition-colors">{p.name}</a>
        </h3>
        <div className="mt-2 flex items-baseline justify-between">
          <span className="font-sans text-[13px] text-deep-2 tracking-[0.08em]">{fmt(p.price, p.priceOverrides)}</span>
          {p.colors && (
            <div className="flex items-center gap-1.5">
              {p.colors.map((c, i) => (
                <button
                  key={i}
                  onClick={(e) => { e.preventDefault(); e.stopPropagation(); setActiveColor(i); }}
                  aria-label={c.name}
                  title={c.name}
                  className={`w-3.5 h-3.5 rounded-full transition-all duration-300 ${activeColor===i ? 'ring-1 ring-offset-2 ring-offset-ivory ring-deep scale-110' : 'opacity-70 hover:opacity-100'}`}
                  style={swatchStyle(c)}
                />
              ))}
            </div>
          )}
        </div>
      </div>
    </article>
  );
}

/* ───────────── Toast (added to bag) ───────────── */
function CartToast() {
  const cart = useCart();
  const { t } = useLang();
  if (!cart.toast) return null;
  return (
    <div className="fixed top-[88px] right-6 z-[90] animate-slide-in-down">
      <div className="bg-deep text-ivory shadow-card px-5 py-4 flex items-center gap-3 max-w-[320px]">
        <span className="w-7 h-7 rounded-full bg-rose flex items-center justify-center">
          <I.check className="w-4 h-4"/>
        </span>
        <div>
          <div className="font-sans uppercase text-[10px] tracking-widest-2 text-ivory/60">{t('Added to bag', 'Ajouté au panier')}</div>
          <div className="font-serif text-[16px] leading-tight">{cart.toast.name}</div>
        </div>
      </div>
    </div>
  );
}

// Expose
Object.assign(window, {
  React, ReactDOM, useState, useEffect, useRef, useCallback, useMemo, createContext, useContext,
  CartProvider, useCart, useReveal,
  LangProvider, useLang, LangSwitcher,
  CurrencyProvider, useCurrency, CurrencySwitcher, Price, formatPrice, convertPrice, effectivePrice,
  Logo, Diamond, I, Button, Eyebrow, ProductCard, CartToast,
  swatchBg, swatchStyle,
  TAG_FR, CAT_FR_GLOBAL: (typeof CAT_FR !== 'undefined' ? CAT_FR : null),
});
