// web/screens.jsx — v2 enhanced: Home, Menu, Loyalty, About

const { useState: wsS, useEffect: wsE, useRef: wsR } = React;

// =========== Scroll reveal hook ===========
function useReveal() {
  const ref = wsR(null);
  wsE(() => {
    const el = ref.current; if (!el) return;
    const obs = new IntersectionObserver(([e]) => { if (e.isIntersecting) { el.classList.add('in'); obs.disconnect(); } }, { threshold: 0.12 });
    obs.observe(el);
    return () => obs.disconnect();
  }, []);
  return ref;
}
function Reveal({ children, delay = 0, as = 'div', ...p }) {
  const ref = useReveal();
  const cls = 'lc-rv' + (delay ? ` lc-rv-${delay}` : '');
  return React.createElement(as, { ref, className: cls, ...p }, children);
}

// =========== Data ===========
// [GOAL LONG-TERM 2026-05-16] Canonical data layer is loaded via data/menu.js
// (before this script). window.LC.menu.* is SSOT mirror of FoodKing central system
// (post menu-reset 2026-05-13 + heal-light V2 2026-05-14, 11 cats / 41 items).
// W_CATS / W_ITEMS / W_DIET are exposed as backwards-compat globals on window.
// We re-read them here so this file's scope can use them directly.
const W_CATS = window.W_CATS;
const W_ITEMS = window.W_ITEMS;
const W_DIET = window.W_DIET;

// =========== ItemCard ===========
// [WEB-86-SYNC 2026-07-19] `soldOut` grise/désactive la carte quand le produit est en
// RUPTURE (86) posée depuis la gestion (caisse/KDS/admin), miroir de la borne. `reason`
// = motif backend (availability_reason) — affiché en libellé lisible.
function ItemCard({ item, onClick, soldOut, reason }) {
  // Motif lisible : les codes techniques (out_of_stock, snake_case) → message générique ;
  // un texte libre déjà lisible (« Rupture frigo ») est conservé.
  const humanReason = (!reason || /[_]/.test(String(reason)) || String(reason).length > 40)
    ? 'Épuisé pour le moment' : String(reason);
  return (
    <div className={'lc-card-item' + (soldOut ? ' is-soldout' : '')} onClick={onClick}
      aria-disabled={soldOut ? 'true' : undefined}
      style={soldOut ? { opacity: 0.6 } : undefined}>
      <div className="lc-card-item-thumb" style={{ position: 'relative', overflow: 'hidden', filter: soldOut ? 'grayscale(1)' : 'none' }}>
        {/* [GOAL LONG-TERM 2026-05-16 + MASSIVE-LOGIC HEAL 2026-05-17 P1] Real product
            photo with emoji fallback on 404 (instead of blank white space). */}
        {item.image && <img src={item.image} alt={item.name} loading="lazy"
          style={{ width: '100%', height: '100%', objectFit: 'contain', padding: '6%', display: 'block' }}
          onError={(e) => {
            e.currentTarget.style.display = 'none';
            // Reveal sibling emoji fallback by adding visible class on parent
            const fb = e.currentTarget.nextElementSibling;
            if (fb) fb.style.display = 'flex';
          }}/>}
        <span className="lc-card-item-thumb-emoji" style={{
          display: item.image ? 'none' : 'flex',
          alignItems: 'center', justifyContent: 'center',
          width: '100%', height: '100%', fontSize: 56,
        }}>{item.emoji || '🍽'}</span>
        {soldOut
          ? <div className="lc-card-item-badge" style={{ background: 'var(--ink,#1A1A1A)', color: '#fff', letterSpacing: '0.04em' }}>ÉPUISÉ</div>
          : (item.badge && <div className={'lc-card-item-badge' + (item.badge==='TOP'?' lc-card-item-badge--top':'')}>{item.badge}</div>)}
      </div>
      <div className="lc-card-item-body">
        {/* [HEAL T1-09 2026-06-05] nested-interactive: card container lost role/tabIndex/
            onKeyDown; the item NAME is now the real focusable control (button reset to
            look identical to .lc-card-item-name). */}
        <button type="button" className="lc-card-item-name" aria-label={soldOut ? `${item.name} — épuisé` : `Voir ${item.name}`} onClick={e=>{ e.stopPropagation(); onClick(); }} style={{ background: 'transparent', border: 0, padding: 0, color: 'inherit', cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left', display: 'block', width: '100%' }}>{item.name}</button>
        <div className="lc-card-item-desc">{soldOut ? humanReason : item.desc}</div>
        <div className="lc-card-item-foot">
          <span className="lc-card-item-price">{item.price.toFixed(2).replace('.', ',')} €</span>
          <span className="lc-card-item-add">{soldOut ? <span style={{ fontSize: 11, fontWeight: 800, letterSpacing: '0.04em' }}>86</span> : <WC_I.plus s={16}/>}</span>
        </div>
      </div>
    </div>
  );
}

// [HONESTY 2026-07-19] Horaires réels 18h–00h : l'indicateur « OUVERT / Ouvert maintenant »
// n'est vrai QUE pendant le service. Fermé ⇒ libellé neutre « Horaires 18h – 00h » (ni pastille
// verte, ni « OUVERT »/« maintenant »). Ouvert de 18:00 à 00:59 inclus (h>=18 || h===0).
function isOpenNow() { const h = new Date().getHours(); return h >= 18 || h === 0; }

// =========== HOME v2 ===========
function WebHome({ onRoute, onItem, unavail = {} }) {
  const WHY = [
    { n: '01', emoji: '🔥', t: 'Fait maison', d: 'Cuit sur la plancha, jamais sous vide. La viande arrive le matin.' },
    { n: '02', emoji: '⚡', t: '30 sec.', d: 'Commande en 3 taps, paye en caisse au retrait. Pas de friction.' },
    { n: '03', emoji: '🎯', t: 'À emporter', d: 'On te ping quand c\'est prêt. Ou en livraison via Uber Eats.' },
    { n: '04', emoji: '🎁', t: '1€ = 10pts', d: 'Programme fidélité automatique sur chaque commande.' },
  ];
  // [HEAL T1-03 2026-06-05] Removed fabricated TESTI array (named reviews) +
  // TESTIMONIALS section render — invented Google social proof, no real reviews.
  const HOURS = [
    { d: 'Lundi',    h: '18h — 00h', today: new Date().getDay() === 1 },
    { d: 'Mardi',    h: '18h — 00h', today: new Date().getDay() === 2 },
    { d: 'Mercredi', h: '18h — 00h', today: new Date().getDay() === 3 },
    { d: 'Jeudi',    h: '18h — 00h', today: new Date().getDay() === 4 },
    { d: 'Vendredi', h: '18h — 00h', today: new Date().getDay() === 5 },
    { d: 'Samedi',   h: '18h — 00h', today: new Date().getDay() === 6 },
    { d: 'Dimanche', h: '18h — 00h', today: new Date().getDay() === 0 },
  ];
  return (
    <div>
      {/* HERO */}
      <section className="lc-hero">
        <div className="lc-container lc-hero-grid">
          <Reveal className="lc-hero-text">
            <div className="lc-hero-eyebrow lc-mono">{isOpenNow()
              ? <React.Fragment><span style={{ width: 6, height: 6, borderRadius: 999, background: '#1FA653', boxShadow: '0 0 6px #1FA653' }}/> <b style={{ color: 'var(--orange-text)' }}>OUVERT</b> · HÉNIN-BEAUMONT 62110</React.Fragment>
              : <React.Fragment>Horaires 18h – 00h · HÉNIN-BEAUMONT 62110</React.Fragment>}</div>
            <h1>Sandwich. <span>Tacos.</span><br/>Bols. <em>Galette.</em></h1>
            <p>Du peuple, pour le peuple. Fait maison chaque jour à Hénin-Beaumont. Commande en 30 sec, viens chercher, mange. Envie d'être livré ? On est aussi sur Uber Eats.</p>
            <div className="lc-hero-ctas">
              <button className="lc-btn lc-btn--orange" onClick={()=>onRoute('menu')}>
                Commander maintenant <WC_I.arrow s={16} className="lc-btn-arrow"/>
              </button>
              <button className="lc-btn lc-btn--ghost" onClick={()=>onRoute('loyalty')}>
                Programme fidélité
              </button>
            </div>
            {/* Quick search hero */}
            <div className="lc-search-hero">
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none"><circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="1.8"/><path d="M16 16l4 4" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>
              <input placeholder="Cherche un plat, un ingrédient…" onClick={()=>onRoute('menu')} readOnly aria-label="Rechercher un plat — ouvre le menu"/>
              <button className="lc-search-hero-btn" onClick={()=>onRoute('menu')}>Voir <WC_I.arrow s={14}/></button>
            </div>
            <div className="lc-hero-stats">
              <div className="lc-hero-stat"><div className="lc-hero-stat-num"><span>30</span> sec</div><div className="lc-hero-stat-lbl">Pour commander</div></div>
              <div className="lc-hero-stat"><div className="lc-hero-stat-num">18h<span>–</span>00h</div><div className="lc-hero-stat-lbl">7 jours / 7</div></div>
              <div className="lc-hero-stat"><div className="lc-hero-stat-num">1<span>€</span> = 10pts</div><div className="lc-hero-stat-lbl">Sur chaque commande</div></div>
            </div>
          </Reveal>
          <Reveal delay={2} className="lc-hero-art">
            <span className="lc-hero-art-tag">// NOTRE SIGNATURE</span>
            {/* [HERO-PHOTO 2026-07-20 owner] La photo hero précédente (signature/cayenne-hero.png)
                montrait un mauvais sandwich (sauce blanche, fond transparent). On utilise la VRAIE
                photo produit Le Cayenne — sauce Cayenne signature + fond de marque (logo + éclairage). */}
            <img src="assets/menu/sandwich-cayenne.png" alt="Sandwich Le Cayenne signature — poulet grillé, sauce Cayenne" className="lc-hero-art-svg" style={{ width: '100%', height: '100%', objectFit: 'contain' }} onError={(e)=>{ e.currentTarget.style.display = 'none'; }}/>
            <div className="lc-hero-art-floating lc-hero-art-floating--tl">
              <span style={{ width: 28, height: 28, borderRadius: 8, background: '#FF5A1F', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>🔥</span>
              Fait maison<br/>chaque jour
            </div>
            <div className="lc-hero-art-floating lc-hero-art-floating--br">
              <span style={{ width: 28, height: 28, borderRadius: 8, background: '#FFD93D', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#0A0A0A' }}>10</span>
              pts par euro<br/>dépensé
            </div>
          </Reveal>
        </div>
        <div className="lc-container">
          <div className="lc-marquee">
            <div className="lc-marquee-track">
              {[...Array(3)].map((_,k) => (
                <React.Fragment key={k}>
                  {['🌶 Sauce Cayenne maison','🥖 Sandwichs faluche','🌮 Tacos M & L','🥣 Bols Frites/Riz','🍔 Burgers brioché','🍟 Frites Cheddar','🧒 Menu enfant','⚡ Prêt en ~10-15 min'].map((t,i) => (
                    <React.Fragment key={i}><span>{t}</span><i/></React.Fragment>
                  ))}
                </React.Fragment>
              ))}
            </div>
          </div>
        </div>
      </section>

      {/* [FIX 2026-07-14 offre mensongère] Bloc « DAILY SPECIAL » SUPPRIMÉ : offre bidon
          (Sandwich Cayenne + Menu « à 9,00 € » alors que le wizard facture 9,90 € ; faux prix
          barré 10,00 € ; faux compte à rebours codé en dur « Fin dans 4 h 12 min »). Owner a
          explicitement demandé son retrait — aucune promo réelle en base. */}

      {/* WHY US */}
      <section className="lc-section" style={{ paddingTop: 40 }}>
        <div className="lc-container">
          <Reveal className="lc-section-head">
            <div className="lc-section-head-text">
              <div className="lc-eyebrow">// Pourquoi Le Cayenne</div>
              <h2>La promesse en <span>4 points.</span></h2>
              <p>On a 4 obsessions, pas 40. La bouffe doit être bonne, rapide, honnête, et te rapporter.</p>
            </div>
          </Reveal>
          <div className="lc-why">
            {WHY.map((w, i) => (
              <Reveal key={i} delay={i+1} className="lc-why-card">
                <div className="lc-why-num">{w.n}</div>
                <div className="lc-why-icon" style={{ fontSize: 24 }}>{w.emoji}</div>
                <div className="lc-why-title">{w.t}</div>
                <div className="lc-why-desc">{w.d}</div>
              </Reveal>
            ))}
          </div>
        </div>
      </section>

      {/* FEATURED */}
      <section className="lc-section">
        <div className="lc-container">
          <Reveal className="lc-featured">
            <div className="lc-featured-body">
              <div className="lc-featured-pill">★ Signature</div>
              <h3>Le <span>Méga</span><br/>XL · 2 viandes</h3>
              <p className="lc-featured-desc">Sandwich XL · 2 viandes au choix · Cheddar + Œuf · Oignons rouges, tomates, salade · Sauce au choix. Le sandwich qui fait honneur à la faim.</p>
              <div className="lc-featured-bottom">
                <span className="lc-featured-price">{(() => { const m = W_ITEMS.find(i => i.slug === 'mega'); return m && m.price != null ? m.price.toFixed(2).replace('.', ',') + ' €' : ''; })()}</span>
                <button className="lc-btn lc-btn--orange" onClick={()=>onItem('mega')}>
                  Commander le Méga <WC_I.arrow s={16} className="lc-btn-arrow"/>
                </button>
              </div>
            </div>
            <div className="lc-featured-art">
              {(() => { const bc = W_ITEMS.find(i => i.slug === 'mega'); return bc && bc.image
                ? <img src={bc.image} alt="Méga"
                    style={{ width: '100%', height: '100%', objectFit: 'contain', padding: '7%', display: 'block' }}
                    onError={(e) => { e.currentTarget.style.display = 'none'; const sib = e.currentTarget.nextElementSibling; if (sib) sib.style.display = 'block'; }}/>
                : null; })()}
              <span className="lc-featured-art-emoji" style={{ display: W_ITEMS.find(i => i.slug === 'mega')?.image ? 'none' : 'block' }}>🥖</span>
            </div>
          </Reveal>

          <Reveal className="lc-section-head" style={{ marginTop: 60 }}>
            <div className="lc-section-head-text">
              <div className="lc-eyebrow">// Menu</div>
              <h2>Les <span>envies</span> du moment</h2>
              <p>Notre sélection de la semaine — fait maison sur la plancha, servi sans chichis.</p>
            </div>
            <button className="lc-btn lc-btn--ink" onClick={()=>onRoute('menu')}>Voir tout le menu <WC_I.arrow s={14}/></button>
          </Reveal>
          <div className="lc-menu-grid">
            {W_ITEMS.slice(0, 4).map((it, i) => <Reveal key={it.id} delay={i+1}><ItemCard item={it} onClick={()=>onItem(it.id)} soldOut={!!unavail[it.id]} reason={unavail[it.id]}/></Reveal>)}
          </div>
        </div>
      </section>

      {/* [HEAL T1-03 2026-06-05] Removed TESTIMONIALS section — fabricated Google
          social proof ("Plus de 800 avis Google · 4,8 étoiles · 87%") + invented
          named testimonials. No real reviews to display. */}

      {/* GALLERY */}
      <section className="lc-section" style={{ paddingTop: 0 }}>
        <div className="lc-container">
          {/* [SOCIAL-FACEBOOK 2026-07-20 owner] Section réseaux réparée : pointée sur la page
              Facebook officielle (au lieu d'un @insta mort) + vraies photos produit avec fond de
              marque (au lieu des emojis non-fonctionnels). Chaque tuile est un lien vers Facebook. */}
          <Reveal className="lc-section-head">
            <div className="lc-section-head-text">
              <div className="lc-eyebrow">// Facebook</div>
              <h2>Suis-nous sur <span>Le Cayenne</span></h2>
              <p>Photos du jour, plats du moment, coulisses de la plancha. Tag-nous, on republie les meilleures.</p>
            </div>
            <a className="lc-btn lc-btn--ink" target="_blank" rel="noopener noreferrer" href="https://www.facebook.com/LeCayenne">Le Cayenne sur Facebook <WC_I.arrow s={14}/></a>
          </Reveal>
          <div className="lc-gallery">
            {[
              { img: 'assets/menu/sandwich-cayenne.png',    alt: 'Sandwich Le Cayenne' },
              { img: 'assets/menu/cheese-burger.png',       alt: 'Cheese Burger' },
              { img: 'assets/menu/sandwich-mega.png',       alt: 'Sandwich Méga' },
              { img: 'assets/menu/sandwich-terminator.png', alt: 'Sandwich Terminator' },
              { img: 'assets/menu/chicken_burger.png',      alt: 'Chicken Burger' },
            ].map((t, i) => (
              <a
                key={i}
                className="lc-gallery-tile"
                href="https://www.facebook.com/LeCayenne"
                target="_blank"
                rel="noopener noreferrer"
                aria-label={`${t.alt} — voir sur notre page Facebook`}
                style={{ display: 'block', overflow: 'hidden', padding: 0, background: '#0A0A0A' }}
              >
                <img
                  src={t.img}
                  alt={t.alt}
                  loading="lazy"
                  style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
                  onError={(e) => { e.currentTarget.style.display = 'none'; }}
                />
              </a>
            ))}
          </div>
        </div>
      </section>

      {/* HOURS / LOCATION */}
      <section className="lc-section" style={{ paddingTop: 0 }}>
        <div className="lc-container">
          <Reveal className="lc-hours">
            <div className="lc-hours-grid">
              <div>
                <div className="lc-hours-status" style={isOpenNow() ? undefined : { background: 'rgba(255,255,255,0.06)', color: 'rgba(255,255,255,0.72)', border: '1px solid rgba(255,255,255,0.16)' }}>{isOpenNow() ? <React.Fragment><i/> Ouvert maintenant</React.Fragment> : 'Horaires 18h – 00h'}</div>
                <h2 className="lc-hours-title">437 Rue<br/><span>Élie Gruyelle,</span><br/>62110.</h2>
                <p style={{ color: 'rgba(255,255,255,0.6)', maxWidth: 380, lineHeight: 1.55 }}>Tu pousses la porte, on te connaît déjà. Parking gratuit à 50m, accessible PMR, click & collect.</p>
                <div style={{ marginTop: 22, display: 'flex', gap: 10, flexWrap: 'wrap' }}>
                  {/* [TERRAIN-HEAL 2026-07-16 · WEB-ITIN-WRONGADDR] href pointait vers « 14 rue de la
                      République » alors que l'adresse réelle (titre ci-dessus + brand.address + footer)
                      = 437 Rue Élie Gruyelle → le client était guidé au mauvais endroit. Lien reconstruit
                      depuis l'adresse canonique. */}
                  <a href={'https://www.google.com/maps/dir/?api=1&destination=' + encodeURIComponent('437 Rue Élie Gruyelle 62110 Hénin-Beaumont')} target="_blank" rel="noopener noreferrer" className="lc-btn lc-btn--orange">Itinéraire <WC_I.arrow s={14}/></a>
                  <a href="tel:0365678291" className="lc-btn lc-btn--ghost" style={{ background: 'transparent', borderColor: 'rgba(255,255,255,0.2)', color: '#fff' }}>03 65 67 82 91</a>
                </div>
              </div>
              <div className="lc-hours-list">
                {HOURS.map((h, i) => (
                  <div key={i} className={'lc-hours-row' + (h.today?' is-today':'')}>
                    <span>{h.d}{h.today && ' · Aujourd\'hui'}</span>
                    <b>{h.h}</b>
                  </div>
                ))}
              </div>
            </div>
          </Reveal>
        </div>
      </section>

      {/* APP DOWNLOAD CTA */}
      <section className="lc-section" style={{ paddingTop: 0 }}>
        <div className="lc-container">
          <Reveal>
            <div className="lc-app-cta">
              <div>
                <div className="lc-eyebrow" style={{ color: 'var(--yellow)' }}>// Mobile</div>
                <h3>L'<span>app</span> Le Cayenne.<br/>Toujours dans ta poche.</h3>
                <p>Commande en 3 taps. Reçois les push quand c'est prêt. Ta carte fidélité partout. Suivi des points en temps réel.</p>
                {/* [TERRAIN-HEAL 2026-07-16 · WEB-APP-DEADLINK] Ancres href="#" mortes supprimées
                    (aucune app publiée) → note honnête, plus de lien cassé. */}
                <div className="lc-app-cta-stores">
                  <span className="lc-app-store" style={{ opacity: 0.7, cursor: 'default' }}>
                    <span className="lc-app-store-icon">📱</span>
                    <span><small>Bientôt sur</small>iOS &amp; Android</span>
                  </span>
                </div>
              </div>
              <div className="lc-app-phone">
                <div className="lc-app-phone-screen">
                  <div className="lc-app-phone-mark">LE</div>
                  <div className="lc-app-phone-big">CAYENNE</div>
                  <div className="lc-app-phone-tag">62110</div>
                </div>
              </div>
            </div>
          </Reveal>
        </div>
      </section>

      {/* STATS COUNTERS */}
      <section className="lc-section" style={{ paddingTop: 60 }}>
        <div className="lc-container">
          <Reveal>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 14 }} className="lc-stats-grid">
              {/* [TERRAIN-HEAL 2026-07-16 · WEB-FAKE-COUNTERS] Retiré « 12 500+ commandes servies » et
                  « 1 200+ inscrits » = chiffres FABRIQUÉS (aucun backend, DB purgée) — même classe que
                  les faux Google/presse déjà retirés. Remplacés par des FAITS véridiques (fidélité réelle
                  1€=1pt, horaires réels) ; « Plats au menu » reste calculé du SSOT W_ITEMS. */}
              {[
                { text: '1€ = 10pts', l: 'Fidélité, chaque commande' },
                { text: '7j/7',     l: 'Ouvert 18h – 00h' },
                { n: W_ITEMS.length, s: '',  l: 'Plats au menu' },
              ].map((s, i) => (
                <div key={i} style={{ background: 'var(--paper)', borderRadius: 22, padding: 24, boxShadow: 'var(--shadow-1)', textAlign: 'center' }}>
                  <div style={{ fontFamily: 'var(--font-display)', fontSize: 'clamp(36px, 4.5vw, 56px)', color: 'var(--ink)', lineHeight: 1, letterSpacing: '-0.01em' }}>
                    {s.text != null ? s.text : <><AnimCounter to={s.n}/>{s.s}</>}
                  </div>
                  <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'var(--gray-3)', marginTop: 8 }}>{s.l}</div>
                </div>
              ))}
            </div>
          </Reveal>
        </div>
      </section>

      {/* [HEAL T1-02 2026-06-05] Removed PRESS section — fabricated citations
          (Voix du Nord / TF1 / France 3 / Time Out / Foodora). No real press. */}

      {/* COMPARISON */}
      <section className="lc-section" style={{ paddingTop: 40 }}>
        <div className="lc-container"><Reveal><Compare/></Reveal></div>
      </section>

      {/* FAQ */}
      <section className="lc-section" style={{ paddingTop: 40 }}>
        <div className="lc-container">
          <Reveal className="lc-section-head">
            <div className="lc-section-head-text">
              <div className="lc-eyebrow">// FAQ</div>
              <h2>Tu te poses des <span>questions ?</span></h2>
              <p>Les réponses aux 5 questions qu'on nous pose chaque semaine en caisse.</p>
            </div>
          </Reveal>
          <Reveal><FAQ items={FAQ_HOME}/></Reveal>
        </div>
      </section>
    </div>
  );
}

// =========== MENU v2 ===========
function WebMenu({ onItem, unavail = {} }) {
  const [cat, setCat] = wsS('all');
  const [q, setQ] = wsS('');
  const [diet, setDiet] = wsS([]);
  const toggleDiet = (id) => setDiet(d => d.includes(id) ? d.filter(x=>x!==id) : [...d, id]);
  // [W1-HEAL 2026-07-20 P3] Un filtre régime sans AUCUN produit correspondant (ex. Épicé : aucun
  // item tagué is_spicy) est MASQUÉ — un chip mort promettait une sélection inexistante.
  const DIET_HAS = { spicy: W_ITEMS.some(i => i.is_spicy), veggie: W_ITEMS.some(i => i.is_vegetarian), top: W_ITEMS.some(i => i.badge === 'TOP') };

  let items = W_ITEMS;
  if (cat !== 'all') items = items.filter(i => i.cat === cat);
  if (q.trim()) {
    // [HONESTY/UX 2026-07-19] Recherche INSENSIBLE aux accents (même normalisation NFD que
    // api.js:212) : « mega » trouve « Méga », « creme » trouve « crème ». Aussi sur la description.
    const norm = s => (s || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');
    const nq = norm(q);
    // [W1-HEAL 2026-07-20 P3] La recherche IGNORE la catégorie active (balaye TOUT le catalogue) :
    // « mega » tapé depuis « Menu enfant » doit trouver Méga.
    items = W_ITEMS.filter(i => norm(i.name).includes(nq) || norm(i.desc).includes(nq));
  }
  if (diet.length) {
    // [E2E-AUDIT 2026-07-04] 'top' aligné sur le BADGE affiché (i.badge==='TOP') et non sur tags:['TOP'] :
    // Double Cheese porte un badge TOP (via is_featured) mais pas le tag → il était badgé mais exclu du filtre.
    const DIET_PRED = { spicy: i => i.is_spicy, veggie: i => i.is_vegetarian, top: i => i.badge === 'TOP' };
    items = items.filter(i => diet.every(d => DIET_PRED[d] ? DIET_PRED[d](i) : false));
  }

  const counts = W_CATS.map(c => c.id === 'all' ? W_ITEMS.length : W_ITEMS.filter(i => i.cat === c.id).length);

  // [OWNER 2026-07-20] Boissons regroupées : en vue « Tout » (sans recherche/filtre) on SORT les
  //   boissons de la grille principale (sinon ~15 canettes noient les plats) → section dédiée
  //   « Boissons » (5 aperçus + « voir toutes » → page boisson). Desserts inchangés (restent normaux).
  const DRINK_CAT = 'boissons';
  const isDefaultView = cat === 'all' && !q.trim() && diet.length === 0;
  const drinkItems = W_ITEMS.filter(i => i.cat === DRINK_CAT);
  const gridItems = isDefaultView ? items.filter(i => i.cat !== DRINK_CAT) : items;

  return (
    <div className="lc-section">
      <div className="lc-container">
        <Reveal className="lc-section-head">
          <div className="lc-section-head-text">
            <div className="lc-eyebrow">// Menu complet</div>
            <h1>Tout ce qu'on <span>cuisine.</span></h1>
            <p>{W_CATS.length-1} catégories · {W_ITEMS.length} créations. Tous faits maison, prêts en ~10-15 min.</p>
          </div>
        </Reveal>

        <div className="lc-menu-layout">
          <aside className="lc-menu-side">
            {/* [HEAL A11Y-HEADING-01 2026-06-04] h4→h2: page <h1> then sidebar
                sections at h2 — no level skipped. Caption styling preserved via CSS. */}
            <h2>// Catégories</h2>
            <div className="lc-menu-side-list">
              {W_CATS.map((c, i) => (
                <button key={c.id} onClick={()=>setCat(c.id)} className={'lc-menu-side-link' + (cat===c.id?' is-on':'')}>
                  <span><i style={{ fontStyle: 'normal', marginRight: 6 }}>{c.icon}</i>{c.label}</span>
                  <span>{counts[i]}</span>
                </button>
              ))}
            </div>
            <h2>// Filtres</h2>
            <div className="lc-menu-side-list">
              {W_DIET.filter(d => DIET_HAS[d.id] !== false).map(d => (
                <button key={d.id} onClick={()=>toggleDiet(d.id)} className={'lc-menu-side-link' + (diet.includes(d.id)?' is-on':'')}>
                  <span>{d.label}</span>
                  {diet.includes(d.id) && <WC_I.check s={12}/>}
                </button>
              ))}
            </div>
          </aside>

          <div>
            <div className="lc-menu-search">
              <span className="lc-menu-search-icon">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none"><circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="1.8"/><path d="M16 16l4 4" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>
              </span>
              <input value={q} onChange={e=>setQ(e.target.value)} placeholder="Cherche un plat, un ingrédient…" aria-label="Cherche un plat ou un ingrédient" type="search"/>
              {q && <button onClick={()=>setQ('')} aria-label="Effacer la recherche" style={{ background: 'transparent', border: 0, color: 'var(--gray-3)' }}><WC_I.close s={14}/></button>}
            </div>

            {/* Mobile: category chips */}
            <div className="lc-show-mobile" style={{ marginBottom: 14 }}>
              <div className="lc-cat-tabs">
                {W_CATS.map((c, i) => (
                  <button key={c.id} onClick={()=>setCat(c.id)} className={'lc-cat-tab' + (cat===c.id?' is-on':'')}>
                    <i>{c.icon}</i> {c.label} <span style={{ opacity: 0.6, marginLeft: 4 }}>{counts[i]}</span>
                  </button>
                ))}
              </div>
              <div className="lc-diet-chips">
                {W_DIET.filter(d => DIET_HAS[d.id] !== false).map(d => (
                  <button key={d.id} onClick={()=>toggleDiet(d.id)} className={'lc-diet-chip' + (diet.includes(d.id)?' is-on':'')}>
                    {d.label}
                  </button>
                ))}
              </div>
            </div>

            <div style={{ marginBottom: 18, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
              <div style={{ fontSize: 13, color: 'var(--gray-3)' }}>
                <b style={{ color: 'var(--ink)' }}>{items.length}</b> résultat{items.length>1?'s':''}{diet.length>0 && ` · ${diet.length} filtre${diet.length>1?'s':''} actif${diet.length>1?'s':''}`}
              </div>
              {/* [HEAL A11Y-CONTRAST-01 2026-06-04] 12px reset text → AA token
                  --orange-text (#C73E18 5.18:1) instead of --orange (3.12:1 FAIL). */}
              {(diet.length > 0 || q || cat !== 'all') && (
                <button onClick={()=>{ setDiet([]); setQ(''); setCat('all'); }} style={{ background: 'transparent', border: 0, color: 'var(--orange-text)', fontSize: 12, fontWeight: 700, letterSpacing: '0.06em', textTransform: 'uppercase' }}>Réinitialiser</button>
              )}
            </div>

            {items.length === 0 ? (
              <div className="lc-menu-empty">
                <div className="lc-menu-empty-icon">🔍</div>
                <div className="lc-menu-empty-title">Rien trouvé</div>
                <div className="lc-menu-empty-desc">Essaye avec d'autres mots-clés ou retire un filtre.</div>
              </div>
            ) : (
              <>
                <div className="lc-menu-grid">
                  {gridItems.map((it, i) => <Reveal key={it.id} delay={(i%4)+1}><ItemCard item={it} onClick={()=>onItem(it.id)} soldOut={!!unavail[it.id]} reason={unavail[it.id]}/></Reveal>)}
                </div>
                {isDefaultView && drinkItems.length > 0 && (
                  <div style={{ marginTop: 44 }}>
                    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', flexWrap: 'wrap', gap: 8, marginBottom: 16 }}>
                      <h2 style={{ fontSize: 22, fontWeight: 800, letterSpacing: '-0.01em', margin: 0 }}>🥤 Boissons <span style={{ fontSize: 14, fontWeight: 600, color: 'var(--gray-3)' }}>· {drinkItems.length} au frais</span></h2>
                      <button onClick={()=>{ setCat(DRINK_CAT); if (typeof window!=='undefined') window.scrollTo({ top: 0, behavior: 'smooth' }); }} style={{ background: 'transparent', border: 0, color: 'var(--orange-text)', fontSize: 13, fontWeight: 700, letterSpacing: '0.04em', textTransform: 'uppercase', cursor: 'pointer' }}>Voir toutes →</button>
                    </div>
                    <div className="lc-menu-grid">
                      {drinkItems.slice(0, 5).map((it, i) => <Reveal key={it.id} delay={(i%4)+1}><ItemCard item={it} onClick={()=>onItem(it.id)} soldOut={!!unavail[it.id]} reason={unavail[it.id]}/></Reveal>)}
                    </div>
                  </div>
                )}
              </>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

// =========== LOYALTY v2 ===========
// [B1-W 2026-06-10 — décision D6=A] Catalogue de récompenses à paliers SUPPRIMÉ
// (ex-REWARDS r1-r5 : 200/300/800/1000/2000 pts). Redeem = modèle mobile CONTINU
// via data/loyalty.js (LC.loyalty) : 100 pts = 1 €, minimum 100 pts, déduction
// au taux de 100 pts = 1 €. Earn inchangé (1 pt/€, SSOT data/menu.js PEPPER_CLUB).
// [GOAL LONG-TERM 2026-05-16] Pepper Club paliers aligned data/menu.js PEPPER_CLUB
const TIERS = [
  { id: 'novice',  name: 'Novice',  min: 0 },
  { id: 'pepper',  name: 'Pepper',  min: 500 },
  { id: 'master',  name: 'Master',  min: 1500 },
  { id: 'legend',  name: 'Légende', min: 5000 },
];
// [HEAL LOY-04 2026-06-04] Status-tier achievements derive their thresholds from
// TIERS (single source) instead of contradictory hardcoded numbers (was 100/500/
// 2000 vs TIERS 500/1500/5000). Name "Master" (not "Master Chef"). `un` (unlocked)
// is computed from the live points balance below so badges stay consistent.
const TIER_MIN = id => TIERS.find(t => t.id === id).min;
const ACHI = [
  // [HONESTY 2026-07-19] plus de `un:true` en dur : un compte NEUF (0 commande) affichait
  // « Premier pas » + « Habitué / 5 commandes » DÉBLOQUÉS = compteur fabriqué (même classe que
  // les blocs referral/streak/1200-inscrits retirés). a1 se débloque quand des points sont
  // crédités (≥1 commande RETIRÉE). a2 « Habitué » et a8 « Tout testé » RETIRÉS : sans compteur
  // de commandes/plats câblé, ils restaient un:false sans seuil ⇒ INDÉBLOCABLES (faussaient « X sur N »).
  { id: 'a1', icon: '🎉', n: 'Premier pas', m: 'Première commande', ptsMin: 1 },
  { id: 'a3', icon: '🌶', n: 'Pepper', tier: 'pepper', m: `${TIER_MIN('pepper')} points` },
  { id: 'a4', icon: '⭐', n: 'Master', tier: 'master', m: `${TIER_MIN('master')} points` },
  { id: 'a5', icon: '👑', n: 'Légende', tier: 'legend', m: `${TIER_MIN('legend')} points` },
  // [GOAL-SYNC 2026-07-08] a6 « Parrain » (parrainage) et a7 « Régulier » (streak)
  // SUPPRIMÉS — aucun backend parrainage/streak en V1 (cohérence avec le retrait
  // des blocs referral/streak/défi/leaderboard fictifs plus bas).
];

// [GOAL-SYNC 2026-07-08] Historique fidélité RÉEL — libellés FR par type backend
// (GET /api/frontend/loyalty/history → lignes {type, points, balance_after, description, date}).
const HIST_TYPE_FR = {
  earn: 'Points gagnés',
  redeem: 'Points utilisés',
  manual_add: 'Ajout manuel (caisse)',
  manual_deduct: 'Retrait manuel (caisse)',
  expire: 'Points expirés',
};
// Types qui DÉBITENT le solde (affichage signe −).
const HIST_NEG_TYPES = { redeem: true, manual_deduct: true, expire: true };
// Date FR courte ("8 juil. · 19h47") ; repli sur la chaîne brute si non parsable.
function fmtHistDate(raw) {
  if (!raw) return '';
  const d = new Date(raw);
  if (isNaN(d.getTime())) return String(raw);
  const day = d.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' });
  const hm = d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }).replace(':', 'h');
  return `${day} · ${hm}`;
}

// [GOAL-SYNC 2026-07-08 intégration] onLogout remonté à l'App (reset isAuth + route home) —
// l'ancien no-op ()=>{} est remplacé par le vrai callback parent.
function WebLoyalty({ isAuth, onAccount, onLogout }) {
  const [tab, setTab] = wsS('rewards');
  // [HEAL LOY-01/LOY-02/DATA-LOYALTY-01 2026-06-04] ONE points value drives the
  // whole wallet/progression. Everything below is computed from `points` + TIERS
  // (single source) — no hardcoded contradictory numbers, no dead branches.
  // [WEB-WIREUP 2026-06-26] Solde RÉEL depuis le backend (GET /api/profile → loyalty_points).
  // Remplace l'ancien solde démo figé (347). 0 par défaut (nouveau guest) jusqu'au chargement.
  const [basePoints, setBasePoints] = wsS(0);
  // [GOAL-SYNC 2026-07-08] loyalty_code RÉEL (profile() ou minté par WebQR) —
  // remplace l'identifiant figé « LECAY-347-A9F2C ».
  const [loyaltyCode, setLoyaltyCode] = wsS('');
  // [GOAL-SYNC 2026-07-08] Taux RÉELS depuis GET /api/frontend/loyalty/config (public),
  // cachés en state — plus AUCUN taux hardcodé (contrat §2). Défauts = parité DB confirmée.
  const [cfg, setCfg] = wsS({ points_per_euro: 10, points_for_1_euro_discount: 100, min_redeem_points: 50 });
  // [GOAL-SYNC 2026-07-08] Redeem RÉEL (remplace l'ex-simulation demoRedeem locale).
  const [redeemUi, setRedeemUi] = wsS({ busy: false, done: null, error: '' });
  // [GOAL-SYNC 2026-07-08] Historique RÉEL (remplace les 4 lignes fictives de mai).
  const [histPage, setHistPage] = wsS(1);
  const [hist, setHist] = wsS({ loading: false, error: '', rows: null, page: 1, lastPage: 1 });
  wsE(() => {
    if (!isAuth) return;
    const api = (typeof window !== 'undefined' && window.LC && window.LC.api) || null;
    if (!api || !api.isAuthed()) return;
    let alive = true;
    api.profile().then(u => {
      if (!alive || !u) return;
      setBasePoints(parseInt(u.loyalty_points, 10) || 0);
      if (u.loyalty_code) setLoyaltyCode(String(u.loyalty_code));
    }).catch(e => {
      if (!alive) return;
      // [HONESTY 2026-07-19] Token invité périmé (401 sur requête authentifiée ⇒ kind:'auth')
      // ne doit PAS afficher un faux « 0 pt » à un utilisateur encore « connecté » : on rouvre
      // la connexion (onAccount) pour re-authentifier au lieu d'un solde silencieusement faux.
      const authExpired = e && (e.kind === 'auth' || e.status === 401);
      if (authExpired && typeof onAccount === 'function') onAccount();
    });
    return () => { alive = false; };
  }, [isAuth]);
  // Config fidélité backend (endpoint PUBLIC) — API down ⇒ on garde les défauts locaux.
  wsE(() => {
    const api = (typeof window !== 'undefined' && window.LC && window.LC.api) || null;
    if (!api || typeof api.loyaltyConfig !== 'function') return;
    let alive = true;
    api.loyaltyConfig().then(c => {
      if (!alive || !c) return;
      setCfg({
        points_per_euro: parseInt(c.points_per_euro, 10) || 1,
        points_for_1_euro_discount: parseInt(c.points_for_1_euro_discount, 10) || 100,
        min_redeem_points: parseInt(c.min_redeem_points, 10) || 100,
      });
    }).catch(() => {});
    return () => { alive = false; };
  }, []);
  // Historique : fetch à l'ouverture de l'onglet + à chaque changement de page.
  wsE(() => {
    if (tab !== 'history' || !isAuth) return;
    const api = (typeof window !== 'undefined' && window.LC && window.LC.api) || null;
    if (!api || typeof api.loyaltyHistory !== 'function' || !api.isAuthed()) {
      setHist({ loading: false, error: 'Historique indisponible pour le moment.', rows: [], page: 1, lastPage: 1 });
      return;
    }
    let alive = true;
    setHist(h => ({ ...h, loading: true, error: '' }));
    api.loyaltyHistory(histPage, 20).then(r => {
      if (!alive) return;
      const rows = (r && r.data) || [];
      const meta = (r && r.meta) || {};
      setHist({
        loading: false, error: '', rows: rows,
        page: parseInt(meta.current_page, 10) || histPage,
        lastPage: parseInt(meta.last_page, 10) || 1,
      });
    }).catch(e => {
      if (!alive) return;
      setHist({ loading: false, error: (e && e.message) || 'Impossible de charger l\'historique — réessayez.', rows: [], page: histPage, lastPage: 1 });
    });
    return () => { alive = false; };
  }, [tab, histPage, isAuth]);
  const points = basePoints;
  // Redeem backend = multiples de points_for_1_euro_discount UNIQUEMENT (100 pts = 1 €) :
  // le maximum utilisable est arrondi au multiple inférieur (250 pts → 200 pts → 2,00 €).
  const redeemRate = cfg.points_for_1_euro_discount;
  const usablePoints = (() => {
    const p = Math.floor(points / redeemRate) * redeemRate;
    return p >= cfg.min_redeem_points ? p : 0;
  })();
  const usableEuros = usablePoints / redeemRate;
  // Redeem RÉEL : POST /api/frontend/loyalty/redeem (X-Idempotency-Key auto côté api.js),
  // puis re-fetch du solde. 422 kill-switch / 400 multiple ⇒ message FR propre, pas de crash.
  const doRedeem = () => {
    const api = (typeof window !== 'undefined' && window.LC && window.LC.api) || null;
    if (!api || typeof api.loyaltyRedeem !== 'function' || !api.isAuthed()) {
      setRedeemUi({ busy: false, done: null, error: 'Service indisponible — réessayez plus tard.' });
      return;
    }
    if (!loyaltyCode) {
      setRedeemUi({ busy: false, done: null, error: 'Identifiant fidélité introuvable — rechargez la page.' });
      return;
    }
    if (usablePoints <= 0) return;
    const ptsAsked = usablePoints;
    setRedeemUi({ busy: true, done: null, error: '' });
    api.loyaltyRedeem(loyaltyCode, ptsAsked)
      .then(() => api.profile().catch(() => null))
      .then(u => {
        if (u) {
          setBasePoints(parseInt(u.loyalty_points, 10) || 0);
          if (u.loyalty_code) setLoyaltyCode(String(u.loyalty_code));
        } else {
          // Redeem OK mais re-fetch KO → repli local cohérent.
          setBasePoints(p => Math.max(0, p - ptsAsked));
        }
        setRedeemUi({ busy: false, done: { pointsUsed: ptsAsked, euros: ptsAsked / redeemRate }, error: '' });
      })
      .catch(e => {
        setRedeemUi({ busy: false, done: null, error: (e && e.message) || 'Utilisation impossible — réessayez.' });
      });
  };
  // Current status tier = highest tier whose threshold is reached.
  const curIdx = TIERS.reduce((acc, t, i) => points >= t.min ? i : acc, 0);
  const curTier = TIERS[curIdx];
  const nextTier = TIERS[curIdx + 1] || null;       // null = max tier reached
  const ptsToNext = nextTier ? nextTier.min - points : 0;
  // Progress within the current tier band toward the next threshold (0–100).
  const tierProgressPct = nextTier
    ? Math.round(((points - curTier.min) / (nextTier.min - curTier.min)) * 100)
    : 100;
  // [HEAL LOY-04/DATA-LOYALTY-01] Resolve each achievement's unlocked state: tier
  // badges unlock when the live points reach their TIERS threshold; others keep
  // their static `un`. The "X débloqués sur N" count is derived from this — no
  // hardcoded "3 débloqués".
  const achievements = ACHI.map(a => ({ ...a, un: a.tier ? points >= TIER_MIN(a.tier) : (a.ptsMin != null ? points >= a.ptsMin : !!a.un) }));
  const achiUnlocked = achievements.filter(a => a.un).length;
  if (!isAuth) {
    return (
      <div className="lc-section">
        <div className="lc-container" style={{ textAlign: 'center', padding: '60px 0' }}>
          <div className="lc-eyebrow" style={{ display: 'inline-block' }}>// Fidélité</div>
          <h1 className="lc-display" style={{ fontSize: 'var(--fs-h1)', margin: '14px 0' }}>Connecte-toi<br/>pour <span style={{ color: 'var(--orange-text)' }}>cumuler.</span></h1>
          {/* [B1-W 2026-06-10 D6=A] redeem CONTINU : 100 pts = 1 €, pas de paliers.
              [GOAL-SYNC 2026-07-08] taux affichés depuis GET /loyalty/config (state cfg). */}
          <p style={{ color: 'var(--gray-4)', fontSize: 16, maxWidth: 480, margin: '0 auto 30px' }}>1 € dépensé = {cfg.points_per_euro} point{cfg.points_per_euro > 1 ? 's' : ''}. {cfg.points_for_1_euro_discount} points = 1 € de réduction, utilisables dès {cfg.min_redeem_points} points.</p>
          <button className="lc-btn lc-btn--orange" onClick={onAccount}>Créer mon compte <WC_I.arrow s={16}/></button>
        </div>
      </div>
    );
  }
  return (
    <div className="lc-section">
      <div className="lc-container">
        <Reveal className="lc-section-head">
          <div className="lc-section-head-text">
            <div className="lc-eyebrow">// Fidélité · Pepper Club</div>
            <h1>Mon <span>compte</span><br/>Le Cayenne</h1>
            <p>Présente ton QR à la caisse pour cumuler. Utilise tes points dès {cfg.min_redeem_points} points : {cfg.points_for_1_euro_discount} pts = 1 € de réduction.</p>
          </div>
        </Reveal>

        <div className="lc-loyalty">
          <Reveal className="lc-wallet">
            <div className="lc-wallet-head">
              <div className="lc-wallet-mark">LE CAYENNE · CLUB</div>
              {/* [GOAL-SYNC 2026-07-08] Badge « DÉMO V1 » RETIRÉ — wallet câblé au réel
                  (solde profile(), QR signé /loyalty/qr, redeem /loyalty/redeem). */}
              {/* [HEAL LOY-01 2026-06-04] Status tier from points (single source). */}
              <div className="lc-wallet-tier">★ {curTier.name}</div>
            </div>
            <div className="lc-wallet-balance">
              <div className="lc-wallet-bal-lbl">Mon solde</div>
              <div className="lc-wallet-bal-num">{points}<span>pts</span></div>
              {/* [GOAL-SYNC 2026-07-08] Utilisable = multiple de {redeemRate} pts (règle
                  backend /redeem : 250 pts → 200 pts → 2,00 €), taux depuis cfg.
                  Ligne fictive « +33 pts gagnés ces 7 derniers jours » RETIRÉE. */}
              <div className="lc-wallet-bal-sub" style={{ color: 'var(--yellow)', fontWeight: 700 }}>Vos points : {points} → utilisable : {window.LC.loyalty.formatEuros(usableEuros)} · {redeemRate} pts = 1 €</div>
            </div>
            {/* [HEAL LOY-01 2026-06-04] Progress + label computed from points + TIERS
                (was hardcoded (points/500) fill and "153 pts → Master Chef"). */}
            <div className="lc-wallet-progress"><div className="lc-wallet-progress-fill" style={{ width: `${tierProgressPct}%` }}/></div>
            <div className="lc-wallet-progress-labels"><span>{curTier.name}</span><span>{nextTier ? `${ptsToNext} pts → ${nextTier.name}` : 'Palier max atteint'}</span></div>
            {/* [GOAL-SYNC 2026-07-08] QR RÉEL (token signé lqr.* via WebQR mint-on-display)
                + loyalty_code réel (profile() ou minté par le /qr) — remplace le mock,
                l'identifiant figé « LECAY-347-A9F2C », le badge « DÉMO V1 » et le bouton
                « Partager » sans action. */}
            <div className="lc-wallet-code">
              <WebQR onCode={c => setLoyaltyCode(c)}/>
              <div className="lc-wallet-code-body">
                <div className="lc-wallet-code-title">// Identifiant</div>
                <div className="lc-wallet-code-id">{loyaltyCode || '—'}</div>
                <div style={{ fontSize: 12, color: 'var(--gray-4)', marginTop: 6 }}>Présentez ce QR ou dictez votre numéro en caisse.</div>
              </div>
            </div>
          </Reveal>

          <Reveal delay={2}>
            {/* Tier progression */}
            {/* [HEAL LOY-01/LOY-02 2026-06-04] Level, track fill and stop states all
                computed from curIdx (single source). The previous classNames had a
                DEAD 'current' branch (i<2 already caught i===1) so no stop was ever
                marked current — now exactly stop[curIdx] is current. */}
            <div className="lc-tier-bar">
              <div className="lc-tier-bar-head">
                <h4>// Ma progression</h4>
                <b>Niveau {curIdx + 1} / {TIERS.length}</b>
              </div>
              <div className="lc-tier-track">
                <div className="lc-tier-track-fill" style={{ width: `${Math.min(100, Math.round(((curIdx + tierProgressPct / 100) / (TIERS.length - 1)) * 100))}%` }}/>
                <div className="lc-tier-stops">
                  {TIERS.map((t, i) => (
                    <div key={t.id} className={'lc-tier-stop' + (i < curIdx ? ' passed' : i === curIdx ? ' current' : '')}>
                      <span className="lc-tier-stop-label">{t.name}</span>
                    </div>
                  ))}
                </div>
              </div>
            </div>

            {/* [GOAL-SYNC 2026-07-08] Blocs FICTIFS supprimés : <Challenge/> (défi hebdo
                « 2/3 commandes »), streak « 4/8 semaines », parrainage « CAYENNE-IKYES »
                et <Leaderboard/> (top fidèles inventés) — AUCUN backend V1 pour ces
                mécaniques (aucune route leaderboard/challenge/referral). */}
          </Reveal>
        </div>

        {/* Achievements */}
        <Reveal className="lc-section-head" style={{ marginTop: 60, marginBottom: 16 }}>
          <div className="lc-section-head-text">
            <div className="lc-eyebrow">// Achievements</div>
            <h2>Mes <span>trophées.</span></h2>
            <p>{achiUnlocked} débloqués sur {achievements.length} — continue pour décrocher tous les badges.</p>
          </div>
        </Reveal>
        <div className="lc-achievements">
          {achievements.map((a, i) => (
            <Reveal key={a.id} delay={(i%4)+1} className={'lc-achi' + (a.un?'':' is-locked')}>
              <div className="lc-achi-icon">{a.un ? a.icon : '🔒'}</div>
              <div className="lc-achi-name">{a.n}</div>
              <div className="lc-achi-meta">{a.m}</div>
            </Reveal>
          ))}
        </div>

        {/* Tabs: redeem / history / how */}
        <Reveal className="lc-section-head" style={{ marginTop: 60, marginBottom: 16 }}>
          <div className="lc-section-head-text">
            <div className="lc-eyebrow">// Cumul & réductions</div>
            <h2>Utilise tes <span>points.</span></h2>
          </div>
        </Reveal>
        {/* [HEAL A11Y-TABS-01 2026-06-04] tablist/tab/aria-selected semantics. */}
        <div className="lc-cat-tabs" role="tablist" aria-label="Cumul & réductions" style={{ marginBottom: 18 }}>
          {[{id:'rewards',l:'Mes réductions'},{id:'history',l:'Historique'},{id:'how',l:'Comment ça marche'},{id:'profile',l:'Mon compte'}].map(t => (
            <button key={t.id} role="tab" aria-selected={tab===t.id} onClick={()=>setTab(t.id)} className={'lc-cat-tab' + (tab===t.id?' is-on':'')}>{t.l}</button>
          ))}
        </div>
        {/* [GOAL-SYNC 2026-07-08] Redeem RÉEL — POST /api/frontend/loyalty/redeem
            (points multiples de {redeemRate}, X-Idempotency-Key auto api.js) puis
            re-fetch du solde. Remplace l'ex-simulation locale demoRedeem (DÉMO V1).
            Modèle CONTINU 100 pts = 1 € (D6=A) — AUCUN catalogue de récompenses. */}
        {tab === 'rewards' && (() => {
          const L = window.LC.loyalty;
          const canUse = usablePoints > 0 && !redeemUi.busy && !!loyaltyCode;
          return (
            <div className="lc-rewards" style={{ maxWidth: 720 }}>
              <div className={'lc-reward-row' + (usablePoints > 0 ? '' : ' is-locked')}>
                <div className="lc-reward-icon">🎁</div>
                <div className="lc-reward-body">
                  <div className="lc-reward-name">Vos points : {points} → utilisable : {L.formatEuros(usableEuros)}</div>
                  <div className="lc-reward-meta">
                    {usablePoints > 0
                      ? `${usablePoints} pts déduits · ${redeemRate} pts = 1 € · minimum ${cfg.min_redeem_points} pts`
                      : `Minimum ${cfg.min_redeem_points} pts — encore ${Math.max(0, cfg.min_redeem_points - points)} pts pour débloquer 1,00 € de réduction`}
                  </div>
                </div>
                <button
                  className={'lc-reward-cta' + (canUse ? '' : ' lc-reward-cta--locked')}
                  disabled={!canUse}
                  aria-disabled={!canUse}
                  onClick={() => { if (canUse) doRedeem(); }}
                >{redeemUi.busy ? 'En cours…' : usablePoints > 0 ? `Utiliser ${L.formatEuros(usableEuros)}` : '🔒'}</button>
              </div>
              {redeemUi.error && (
                <div className="lc-reward-row" role="alert">
                  <div className="lc-reward-icon" style={{ background: '#FCE6E2', color: '#D72638' }}>!</div>
                  <div className="lc-reward-body">
                    <div className="lc-reward-name">Utilisation impossible</div>
                    {/* 422 kill-switch / 400 multiple → message backend remappé FR (api.js), informatif. */}
                    <div className="lc-reward-meta">{redeemUi.error}</div>
                  </div>
                </div>
              )}
              {redeemUi.done && (
                <div className="lc-reward-row" role="status">
                  <div className="lc-reward-icon" style={{ background: '#E8F8ED', color: '#1FA653' }}>✓</div>
                  <div className="lc-reward-body">
                    <div className="lc-reward-name">−{L.formatEuros(redeemUi.done.euros)} appliqués · {redeemUi.done.pointsUsed} pts déduits</div>
                    <div className="lc-reward-meta">Nouveau solde : {points} pts — remise enregistrée côté caisse.</div>
                  </div>
                </div>
              )}
            </div>
          );
        })()}
        {/* [GOAL-SYNC 2026-07-08] Historique RÉEL — GET /api/frontend/loyalty/history
            (paginé) ; remplace les 4 lignes fictives de mai. États loading/vide/erreur FR. */}
        {tab === 'history' && (
          <div className="lc-rewards" style={{ maxWidth: 720 }}>
            {hist.loading && (
              <div className="lc-reward-row" role="status">
                <div className="lc-reward-icon">⏳</div>
                <div className="lc-reward-body">
                  <div className="lc-reward-name">Chargement de l'historique…</div>
                </div>
              </div>
            )}
            {!hist.loading && hist.error && (
              <div className="lc-reward-row" role="alert">
                <div className="lc-reward-icon" style={{ background: '#FCE6E2', color: '#D72638' }}>!</div>
                <div className="lc-reward-body">
                  <div className="lc-reward-name">Historique indisponible</div>
                  <div className="lc-reward-meta">{hist.error}</div>
                </div>
              </div>
            )}
            {!hist.loading && !hist.error && hist.rows && hist.rows.length === 0 && (
              <div className="lc-reward-row">
                <div className="lc-reward-icon">🕐</div>
                <div className="lc-reward-body">
                  <div className="lc-reward-name">Aucune opération pour le moment</div>
                  <div className="lc-reward-meta">Vos points apparaîtront ici après votre première commande.</div>
                </div>
              </div>
            )}
            {!hist.loading && !hist.error && (hist.rows || []).map((h, i) => {
              const neg = !!HIST_NEG_TYPES[h.type];
              const amt = Math.abs(parseInt(h.points, 10) || 0);
              const label = HIST_TYPE_FR[h.type] || 'Opération';
              const balanceAfter = parseInt(h.balance_after, 10);
              return (
                <div key={i} className="lc-reward-row">
                  <div className="lc-reward-icon" style={{ background: neg ? '#FCE6E2' : '#E8F8ED', color: neg ? '#D72638' : '#1FA653', fontFamily: 'var(--font-display)', fontSize: 20 }}>{neg ? '−' : '+'}</div>
                  <div className="lc-reward-body">
                    <div className="lc-reward-name">{h.description || label}</div>
                    <div className="lc-reward-meta">{label} · {fmtHistDate(h.date)}{isNaN(balanceAfter) ? '' : ` · solde après : ${balanceAfter} pts`}</div>
                  </div>
                  <div className="lc-mono" style={{ fontWeight: 700, fontSize: 16, color: neg ? '#D72638' : '#1FA653' }}>{(neg ? '−' : '+') + amt}</div>
                </div>
              );
            })}
            {!hist.loading && !hist.error && hist.lastPage > 1 && (
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 14, marginTop: 14 }}>
                <button className="lc-cat-tab" disabled={hist.page <= 1} onClick={() => setHistPage(p => Math.max(1, p - 1))}>← Précédent</button>
                <span className="lc-mono" style={{ fontSize: 12 }}>Page {hist.page} / {hist.lastPage}</span>
                <button className="lc-cat-tab" disabled={hist.page >= hist.lastPage} onClick={() => setHistPage(p => p + 1)}>Suivant →</button>
              </div>
            )}
          </div>
        )}
        {tab === 'how' && (
          <div style={{ background: 'var(--paper)', borderRadius: 22, padding: 24, maxWidth: 720 }}>
            <h3 className="lc-display" style={{ fontSize: 24, margin: 0 }}>Comment ça marche</h3>
            {/* [GOAL-SYNC 2026-07-08] Taux affichés depuis GET /loyalty/config (state cfg) —
                plus aucun taux hardcodé. Mention « +25 pts à l'inscription » RETIRÉE
                (le backend /loyalty/register crée le compte à 0 point). */}
            <ol style={{ paddingLeft: 22, lineHeight: 1.9, marginTop: 14 }}>
              <li><b>1 € dépensé = {cfg.points_per_euro} point{cfg.points_per_euro > 1 ? 's' : ''}</b> gagné automatiquement après la commande</li>
              {/* [HEAL T2-06 2026-06-05] state redeem ratio in plain language (was only earn rate). */}
              <li><b>{cfg.points_for_1_euro_discount} points = 1 € de réduction</b> sur ta commande</li>
              <li>Présente ton QR ou dicte ton numéro en caisse</li>
              {/* [B1-W 2026-06-10 D6=A] redeem continu : plus de catalogue de récompenses à paliers. */}
              <li>Utilise tes points dès <b>{cfg.min_redeem_points} points</b>, au taux de {cfg.points_for_1_euro_discount} pts = 1 € — déduction directe sur l'addition</li>
              <li>Débloque des badges et gravis les tiers (Pepper · Master · Légende)</li>
            </ol>
            <div style={{ marginTop: 14, padding: 14, background: 'var(--yellow)', borderRadius: 12, fontSize: 13 }}>
              Tes points expirent au bout de 12 mois d'inactivité · Conforme RGPD.
            </div>
          </div>
        )}
        {/* [GOAL-SYNC 2026-07-08 intégration] câblé au vrai logout de l'App (ex no-op). */}
        {tab === 'profile' && <LoyaltyProfileTab onLogout={onLogout || (()=>{})}/>}
      </div>
    </div>
  );
}

Object.assign(window, { WebHome, WebMenu, WebLoyalty, ItemCard, W_CATS, W_ITEMS, Reveal });
