// web/funnel.jsx — Complete ordering funnel v4
// Steps: cart → checkout (pickup time) → payment → (account if guest) → confirmation → tracking

const { useState: wfvS, useEffect: wfvE, useRef: wfvR } = React;

// ============================================================
// [GOAL-SYNC 2026-07-08] points_per_euro RÉEL — GET /api/frontend/loyalty/config via
// LC.api.loyaltyConfig() (cache module partagé + state). Plus de ratio 1pt/€ hardcodé ;
// fallback 1 si l'API est indisponible. Earn affiché = Math.floor(total × ppe) (règle backend).
// ============================================================
let wfvPpeCache = null;
let wfvPpePromise = null;
function usePointsPerEuro() {
  const [ppe, setPpe] = wfvS(wfvPpeCache != null ? wfvPpeCache : 1);
  wfvE(() => {
    if (wfvPpeCache != null) return;
    const api = (typeof window !== 'undefined' && window.LC && window.LC.api) || null;
    if (!api || !api.loyaltyConfig) return;
    let alive = true;
    if (!wfvPpePromise) wfvPpePromise = api.loyaltyConfig().catch(() => null);
    wfvPpePromise.then(cfg => {
      const v = cfg && Number(cfg.points_per_euro);
      wfvPpeCache = (isFinite(v) && v > 0) ? v : 1;
      if (alive) setPpe(wfvPpeCache);
    });
    return () => { alive = false; };
  }, []);
  return ppe;
}

// ============================================================
// Mini QR for ticket
// ============================================================
function TicketQR({ value }) {
  // [ULTRA-AUDIT 2026-07-19 P2] Le QR peignait un motif DÉCORATIF (n'encodait RIEN) alors que la
  // confirmation dit « Présente ce QR à la caisse » → la caisse ne pouvait rien scanner. On encode
  // désormais le VRAI code de retrait via window.qrcode (même lib que la fidélité, déjà chargée).
  // Fallback motif si la lib manque — le numéro lisible imprimé sous le QR reste la source de vérité.
  const code = (value != null ? String(value) : '').replace(/^#/, '');
  let svgHtml = '';
  if (code && typeof window !== 'undefined' && typeof window.qrcode === 'function') {
    try {
      const q = window.qrcode(0, 'M');
      q.addData(code);
      q.make();
      svgHtml = q.createSvgTag({ cellSize: 4, margin: 0, scalable: true })
        .replace('<svg ', '<svg style="width:100%;height:auto;display:block" ');
    } catch (e) { svgHtml = ''; }
  }
  if (svgHtml) {
    return (
      <div
        className="lcf-ticket-qr-real"
        role="img"
        aria-label={'QR de retrait — commande ' + code}
        style={{ width: '100%', maxWidth: 220, margin: '0 auto', background: '#fff', padding: 6, borderRadius: 8, boxSizing: 'border-box' }}
        dangerouslySetInnerHTML={{ __html: svgHtml }}
      />
    );
  }
  const cells = [];
  for (let y=0; y<13; y++) for (let x=0; x<13; x++) {
    const TL = x<3 && y<3, TR = x>=10 && y<3, BL = x<3 && y>=10;
    let on = false;
    if (TL || TR || BL) {
      const lx = TR ? x-10 : x, ly = BL ? y-10 : y;
      on = lx===0||lx===2||ly===0||ly===2 || (lx===1&&ly===1);
    } else on = ((x*7 + y*11 + 3) % 5) < 2;
    cells.push(on);
  }
  return (
    <div className="lcf-ticket-qr-mock">
      {cells.map((on, i) => <i key={i} className={on?'':'off'}/>)}
    </div>
  );
}

// Confetti
function Confetti() {
  const colors = ['#FF5A1F', '#FFD93D', '#0A0A0A', '#1FA653'];
  return (
    <div className="lcf-confetti">
      {Array.from({length: 28}).map((_,i) => (
        <i key={i} style={{
          left: `${(i*3.5) % 100}%`,
          background: colors[i % colors.length],
          animationDelay: `${(i % 12) * 0.12}s`,
        }}/>
      ))}
    </div>
  );
}

// Funnel stepper
function FunnelSteps({ step, fulfillment }) {
  // [STEP-LABEL 2026-07-07] Step 1 label follows the chosen mode : « Livraison » en mode
  // livraison, « Retrait » sinon (avant, « Retrait » s'affichait même en livraison).
  const labels = [
    { id: 'checkout', label: fulfillment === 'delivery' ? 'Livraison' : 'Retrait' },
    { id: 'payment',  label: 'Paiement' },
    { id: 'confirm',  label: 'Confirmé' },
  ];
  const idx = labels.findIndex(l => l.id === step);
  return (
    <div className="lcf-steps">
      {labels.map((l, i) => (
        <React.Fragment key={l.id}>
          <div className={'lcf-step' + (i < idx ? ' is-done' : i === idx ? ' is-current' : '')}>
            <div className="lcf-step-num"><span>{i+1}</span></div>
            <span>{l.label}</span>
          </div>
          {i < labels.length - 1 && <div className="lcf-step-arrow">→</div>}
        </React.Fragment>
      ))}
    </div>
  );
}

// Order summary card
function OrderSummary({ cart, discount = 0, points = 0, deliveryFee = null }) {
  const subtotal = cart.reduce((s,i) => s + i.price * i.qty, 0);
  const total = subtotal - discount + (deliveryFee != null ? deliveryFee : 0);
  // [GOAL-SYNC 2026-07-08] ratio earn depuis la config backend (fallback 1).
  const ppe = usePointsPerEuro();
  return (
    <div className="lcf-summary">
      <div className="lcf-summary-head">
        <h3>Récap</h3>
        <span className="lcf-summary-count">{cart.length} article{cart.length>1?'s':''}</span>
      </div>
      <div className="lcf-summary-rows">
        {cart.map((it, i) => (
          <div key={i} className="lcf-summary-row">
            <div className="lcf-summary-row-thumb">
              {it.image && <img src={it.image} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain' }} onError={(e) => { e.currentTarget.style.display = 'none'; const s = e.currentTarget.nextElementSibling; if (s) s.style.display = 'flex'; }}/>}
              <span style={{ display: it.image ? 'none' : 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>{it.emoji}</span>
            </div>
            <div className="lcf-summary-row-body">
              <div className="lcf-summary-row-name">{it.name}</div>
              {it.subs && <div className="lcf-summary-row-meta">+ {it.subs}</div>}
              <div className="lcf-summary-row-q">× {it.qty}</div>
            </div>
            <div className="lcf-summary-row-price">{(it.price * it.qty).toFixed(2).replace('.', ',')} €</div>
          </div>
        ))}
      </div>
      <div className="lcf-summary-totals">
        <div className="lcf-summary-totals-row"><span>Sous-total</span><span>{subtotal.toFixed(2).replace('.', ',')} €</span></div>
        {discount > 0 && <div className="lcf-summary-totals-row" style={{ color: 'var(--green-text)' }}><span>Promo</span><span>−{discount.toFixed(2).replace('.', ',')} €</span></div>}
        {deliveryFee != null && <div className="lcf-summary-totals-row"><span>Livraison</span><span>{deliveryFee === 0 ? <b style={{ color: 'var(--green-text)' }}>Offerte</b> : (deliveryFee.toFixed(2).replace('.', ',') + ' €')}</span></div>}
        <div className="lcf-summary-totals-row is-total"><span>Total</span><b>{total.toFixed(2).replace('.', ',')} €</b></div>
      </div>
      <div className="lcf-summary-loyalty">
        <span className="ic">🎁</span>
        <div className="lcf-summary-loyalty-text">
          {/* [GOAL-SYNC 2026-07-08] floor(total × points_per_euro) — aligné sur l'earn backend. */}
          Tu gagnes <b>+{Math.floor(total * ppe)} pts</b> sur cette commande
        </div>
      </div>
    </div>
  );
}

// ============================================================
// CHECKOUT PAGE — Pickup time + notes + promo
// ============================================================
function CheckoutPage({ cart, onBack, onNext, ctx, setCtx }) {
  // [OWNER 2026-07-28 · Wave D — RÉINSTAURE les créneaux, annule la décision du 2026-07-19] Le
  // backend PLANIFIE désormais réellement (scheduled_at + is_advance_order=1, release KDS T-20)
  // et l'attente « dès que prêt » vient de la FILE CUISINE RÉELLE (GET order/wait-estimate :
  // base 15 min, +5/tranche de 3 commandes devant, plafond 30-35). Plus jamais d'heure inventée.
  const [est, setEst] = wfvS(null); // { wait_low, wait_high, closing_time, queue_count }
  wfvE(() => {
    let on = true;
    const api = (typeof window !== 'undefined' && window.LC && window.LC.api) || null;
    if (api && api.waitEstimate) {
      api.waitEstimate().then(r => { if (on && r && r.wait_low != null) setEst(r); }).catch(() => {});
    }
    return () => { on = false; };
  }, []);
  const asapLabel = est ? ('~' + est.wait_low + '-' + est.wait_high + ' min') : '~15-20 min';
  // Heure programmée : bornée [maintenant + attente haute, fermeture − 15 min] par pas de 15.
  const schedFromDelay = (min) => {
    const d = new Date(Date.now() + min * 60000);
    const p = (n) => String(n).padStart(2, '0');
    return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':00';
  };
  const schedFromTime = (hhmm) => {
    const d = new Date();
    return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') + ' ' + hhmm + ':00';
  };
  const minPick = (() => { const d = new Date(Date.now() + ((est && est.wait_high) || 20) * 60000); const p = n => String(n).padStart(2, '0'); return p(d.getHours()) + ':' + p(Math.ceil(d.getMinutes() / 5) * 5 % 60); })();
  const maxPick = (() => {
    if (!est || !est.closing_time) return '23:45';
    const m = est.closing_time.split(':'); const t = (parseInt(m[0], 10) * 60 + parseInt(m[1], 10)) - 15;
    const p = n => String(n).padStart(2, '0'); return p(Math.floor(((t % 1440) + 1440) % 1440 / 60)) + ':' + p(t % 60);
  })();
  const slots = [
    { id: 'asap',   label: 'Dès que prêt', time: asapLabel },
    { id: 'plus30', label: 'Dans 30 min',  time: '+30 min', delay: 30 },
    { id: 'plus40', label: 'Dans 40 min',  time: '+40 min', delay: 40 },
    { id: 'custom', label: 'Choisir une heure', time: ctx.slotHour || '—' },
  ];
  const days = [
    { id: 'today',    d: 'AUJ', n: "Aujourd'hui" },
  ];
  const subtotal = cart.reduce((s,i) => s + i.price * i.qty, 0);
  // [T2-03 HEAL 2026-06-05] Running total surfaced on the CTA (summary is below the fold on mobile).
  const total = subtotal - (ctx.discount || 0);
  // [WEB-WIREUP 2026-06-26] Real backend coupon validation (surface=web). The discount is
  // computed + scope-enforced server-side; we store coupon_id and pass it to placeOrder so the
  // order's discount is the SSOT one (no client-invented -10%).
  const [promoBusy, setPromoBusy] = wfvS(false);
  const applyPromo = async () => {
    if (promoBusy) return;
    const api = (typeof window !== 'undefined' && window.LC && window.LC.api) || null;
    const code = (ctx.promoInput || '').trim();
    // [ULTRA-AUDIT 2026-07-19 P3] Code vidé + « Appliquer » = RETRAIT du coupon (avant : laissait
    // discount/couponId appliqués, aucun moyen de retirer une remise). On réinitialise proprement.
    if (!code) { setCtx(c => ({ ...c, discount: 0, couponId: null, promoApplied: null, promoErr: null })); return; }
    if (!api) { setCtx(c => ({ ...c, promoErr: 'API indisponible.' })); return; }
    setPromoBusy(true);
    try {
      const res = await api.checkCoupon(code, subtotal);
      const disc = Number(res.discount || 0);
      setCtx(c => ({ ...c, promoApplied: res.code || code, couponId: res.id, discount: disc, promoErr: null }));
    } catch (e) {
      setCtx(c => ({ ...c, promoErr: (e && e.message) || 'Code invalide', couponId: null, promoApplied: null, discount: 0 }));
    } finally { setPromoBusy(false); }
  };

  // [DELIVERY 2026-06-27] Mode à emporter / livraison + devis géocodé.
  const fulfillment = ctx.fulfillment || 'pickup';
  const [geoBusy, setGeoBusy] = wfvS(false);
  const api = (typeof window !== 'undefined' && window.LC && window.LC.api) || null;
  // [DROP-FIX 2026-07-19 · WEB-CHECKOUT-CRASH] `deliveryEnabled` était référencé ici (gate section
  //   « Mode » lignes 225/246) mais n'était défini QUE dans PaymentPage (regression bfe8768 2026-07-16)
  //   → ReferenceError au render de CheckoutPage → page BLANCHE → AUCUNE commande web possible depuis
  //   le 16/07. Défini ici avec le MÊME pattern que PaymentPage (api.config.deliveryEnabled, OFF par
  //   défaut = mandat V1 « 100% à emporter »). Découvert en e2e navigateur réel.
  const deliveryEnabled = !!(api && api.config && api.config.deliveryEnabled);
  // [ADDRESS-SSOT 2026-07-07 / owner-corrigé 2026-07-12] Adresse canonique du restaurant =
  // source unique (data/menu.js brand) : 437 Rue Élie Gruyelle, 62110 Hénin-Beaumont. Le fallback
  // littéral reste synchronisé avec la valeur brand.
  const brand = (typeof window !== 'undefined' && window.LC && window.LC.menu && window.LC.menu.brand) || {};
  const pickupFull = brand.address || '437 Rue Élie Gruyelle, 62110 Hénin-Beaumont';
  const pickupStreet = pickupFull.split(',')[0].trim();
  const pickupZip = brand.zip || '62110';
  const pickupMapsUrl = 'https://www.google.com/maps/dir/?api=1&destination=' + encodeURIComponent(pickupFull);
  const setMode = (m) => setCtx(c => ({ ...c, fulfillment: m, deliveryErr: null }));
  const computeDelivery = async () => {
    if (geoBusy || !api) return;
    const addr = (ctx.deliveryAddress || '').trim();
    if (addr.length < 6) { setCtx(c => ({ ...c, deliveryErr: 'Entre ton adresse complète (rue, code postal, ville).' })); return; }
    setGeoBusy(true); setCtx(c => ({ ...c, deliveryErr: null }));
    try {
      const q = await api.deliveryQuoteForAddress(addr, subtotal);
      setCtx(c => ({ ...c, deliveryQuote: q, deliveryErr: null }));
    } catch (e) {
      setCtx(c => ({ ...c, deliveryQuote: null, deliveryErr: (e && e.message) || 'Adresse introuvable.' }));
    } finally { setGeoBusy(false); }
  };
  const dq = ctx.deliveryQuote;
  // [TERRAIN-HEAL 2026-07-16 · WEB-SLOT-STUB] CTA gating : pickup = retrait IMMÉDIAT toujours possible
  // (V1 mono-resto, pas de planification réelle) → plus de blocage sur un créneau fake ; delivery →
  // besoin d'un devis (adresse géocodée). Un créneau choisi reste purement indicatif.
  // [Wave D] créneau « Choisir une heure » sans heure choisie → on ne peut pas continuer.
  const canProceed = (fulfillment === 'delivery' ? !!dq : true) && !(ctx.slot === 'custom' && !ctx.slotHour);
  return (
    <div className="lcf-page">
      <div className="lc-container">
        <FunnelSteps step="checkout" fulfillment={fulfillment}/>
        <div className="lcf-grid">
          <div className="lcf-card">
            <div className="lcf-card-head">
              <div>
                <div className="lc-eyebrow">// Étape 1 sur 2</div>
                <h2>{fulfillment === 'delivery' ? <>Où livrer<br/>ta commande ?</> : <>Quand récupérer<br/>ta commande ?</>}</h2>
              </div>
            </div>

            {/* [DELIVERY 2026-06-27 · COMING-SOON 2026-07-27] Choix du mode : à emporter / livraison.
                Directive owner 2026-07-27 : lancement « à emporter » seul, la livraison reste VISIBLE
                mais désactivée avec « Ça arrive bientôt » (remplace le masquage TERRAIN-HEAL 2026-07-16).
                Quand feature-delivery passera à 1, la tuile redevient active (barème 4 € ≤3 km puis +2 €/km). */}
            <div className="lcf-section">
              <h3>// Mode</h3>
              {/* [TERRAIN-HEAL 2026-07-16 · WEB-A11Y-RADIOGROUP] les 2 boutons se comportent en radios
                  (pastille lcf-paymethod-radio) mais étaient annoncés « bouton » : role=radiogroup +
                  role=radio + aria-checked → le lecteur d'écran annonce « radio, coché ». */}
              <div className="lcf-paymethods" role="radiogroup" aria-label="Mode de retrait" style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1fr) minmax(0,1fr)', gap: 10 }}>
                <button role="radio" aria-checked={fulfillment==='pickup'} onClick={()=>setMode('pickup')} className={'lcf-paymethod' + (fulfillment==='pickup'?' is-on':'')}>
                  <div className="lcf-paymethod-icon lcf-paymethod-icon--cash" aria-hidden="true">🛍️</div>
                  <div className="lcf-paymethod-body"><div className="lcf-paymethod-name">À emporter</div><div className="lcf-paymethod-meta">Tu viens chercher</div></div>
                  <div className="lcf-paymethod-radio" aria-hidden="true"><i/></div>
                </button>
                {deliveryEnabled ? (
                <button role="radio" aria-checked={fulfillment==='delivery'} onClick={()=>setMode('delivery')} className={'lcf-paymethod' + (fulfillment==='delivery'?' is-on':'')}>
                  <div className="lcf-paymethod-icon lcf-paymethod-icon--cash" aria-hidden="true">🛵</div>
                  <div className="lcf-paymethod-body"><div className="lcf-paymethod-name">Livraison</div><div className="lcf-paymethod-meta">Dès 4 € · selon distance</div></div>
                  <div className="lcf-paymethod-radio" aria-hidden="true"><i/></div>
                </button>
                ) : (
                /* [OWNER 2026-07-28 · GOAL WEB] Renvoi Uber Eats tant que NOTRE livraison n'est pas
                   lancée (feature-delivery=0). URL surchargeable via <meta name="uber-eats-url">
                   (gate G1 owner — placeholder = recherche Le Cayenne Hénin-Beaumont sur Uber Eats).
                   Quand feature-delivery passera à 1, la tuile Livraison native revient (zéro code). */
                <a role="link" href={(api && api.config && api.config.uberEatsUrl) || 'https://www.ubereats.com/fr/search?q=Le%20Cayenne%20H%C3%A9nin-Beaumont'} target="_blank" rel="noopener noreferrer" className="lcf-paymethod" style={{ textDecoration: 'none' }} aria-label="Commander en livraison sur Uber Eats (nouvel onglet)">
                  <div className="lcf-paymethod-icon lcf-paymethod-icon--cash" aria-hidden="true">🛵</div>
                  <div className="lcf-paymethod-body"><div className="lcf-paymethod-name">Livraison</div><div className="lcf-paymethod-meta">Sur Uber Eats →</div></div>
                </a>
                )}
              </div>
            </div>

            {deliveryEnabled && fulfillment === 'delivery' && (
              <div className="lcf-section">
                <h3>// Adresse de livraison</h3>
                <div className="lc-promo">
                  <input value={ctx.deliveryAddress || ''} onChange={e=>setCtx(c=>({ ...c, deliveryAddress: e.target.value, deliveryQuote: null }))} placeholder="N° rue, code postal, ville" aria-label="Adresse de livraison"/>
                  <button onClick={computeDelivery} disabled={geoBusy}>{geoBusy ? '…' : 'Calculer'}</button>
                </div>
                {ctx.deliveryErr && <div className="lcf-field-error" role="alert" aria-live="assertive" style={{ marginTop: 8 }}><WC_I.close s={12}/> {ctx.deliveryErr}</div>}
                {dq && (
                  <div role="status" aria-live="polite" style={{ marginTop: 10, padding: '12px 14px', background: 'var(--cream-2)', borderRadius: 12, fontSize: 13 }}>
                    <div style={{ fontWeight: 700 }}>📍 {dq.distanceKm} km · <span>Livraison <b>{dq.estimate.fee.toFixed(2).replace('.', ',')} €</b></span></div>
                    {/* [BARÈME 2026-07-27] offerte ≥30 € COUPÉE backend (Settings free_delivery_above=0,
                        migration 2026_07_27_093000) — grille affichée = exemples owner exacts, pas de
                        formule « +2 €/km » qui contredirait 4 km → 5 €. */}
                    <div style={{ fontSize: 11.5, color: 'var(--gray-3)', marginTop: 4 }}>Frais selon distance — ex. : ≤3 km 4 €, 4 km 5 €, 5 km 7 €, 6 km 9 €.</div>
                  </div>
                )}
              </div>
            )}

            {fulfillment === 'pickup' && (<>
            <div className="lcf-section">
              <h3>// Jour</h3>
              {/* [TERRAIN-HEAL 2026-07-16 · WEB-A11Y-RADIOGROUP] jour de retrait = radios */}
              <div className="lcf-pickup-cal" role="radiogroup" aria-label="Jour de retrait">
                {days.map(d => (
                  <button key={d.id} role="radio" aria-checked={ctx.day===d.id} onClick={()=>setCtx(c => ({ ...c, day: d.id }))} className={'lcf-pickup-day' + (ctx.day===d.id?' is-on':'')}>
                    <div className="lcf-pickup-day-d">{d.d}</div>
                    <div className="lcf-pickup-day-n">{d.n}</div>
                  </button>
                ))}
              </div>
            </div>

            <div className="lcf-section">
              <h3>// Heure</h3>
              <div className="lcf-pickup">
                {/* [TERRAIN-HEAL 2026-07-16 · WEB-A11Y-RADIOGROUP] créneau horaire = radios */}
                <div className="lcf-pickup-rows" role="radiogroup" aria-label="Créneau de retrait">
                  {slots.map(s => (
                    <button key={s.id} role="radio" aria-checked={ctx.slot===s.id}
                      onClick={()=>setCtx(c => ({
                        ...c, slot: s.id,
                        slotTime: s.id === 'custom' ? (c.slotHour || s.time) : s.time,
                        // [Wave D] scheduledAt ('YYYY-MM-DD HH:mm:ss') : null en ASAP ; +30/+40 =
                        // délai relatif ; custom = l'heure choisie (posée par l'input ci-dessous).
                        scheduledAt: s.delay ? schedFromDelay(s.delay) : (s.id === 'custom' && c.slotHour ? schedFromTime(c.slotHour) : null),
                      }))}
                      className={'lcf-pickup-slot' + (ctx.slot===s.id?' is-on':'')}>
                      <div className="lcf-pickup-slot-head">{s.label}</div>
                      <div className="lcf-pickup-slot-time">{s.time}</div>
                    </button>
                  ))}
                </div>
                {ctx.slot === 'custom' && (
                  <div style={{ marginTop: 10, display: 'flex', alignItems: 'center', gap: 10 }}>
                    <label htmlFor="lcf-pickup-hour" style={{ fontSize: 12, fontWeight: 700 }}>Heure de retrait</label>
                    <input id="lcf-pickup-hour" type="time" step={300} min={minPick} max={maxPick}
                      value={ctx.slotHour || ''}
                      onChange={e => { const v = e.target.value; setCtx(c => ({ ...c, slotHour: v, slotTime: v, scheduledAt: v ? schedFromTime(v) : null })); }}
                      style={{ padding: '8px 10px', borderRadius: 10, border: '1px solid var(--gray-1)', fontSize: 14 }}/>
                    <span style={{ fontSize: 11, color: 'var(--gray-3)' }}>entre {minPick} et {maxPick}</span>
                  </div>
                )}
                {est && est.queue_count > 0 && (
                  <div style={{ marginTop: 8, fontSize: 11.5, color: 'var(--gray-3)' }}>
                    {est.queue_count} commande{est.queue_count > 1 ? 's' : ''} en cuisine devant toi — estimation temps réel.
                  </div>
                )}
              </div>
            </div>
            </>)}

            <div className="lcf-section">
              <h3>// Code promo</h3>
              <div className="lc-promo">
                <input
                  value={ctx.promoInput || ''}
                  onChange={e => setCtx(c => ({ ...c, promoInput: e.target.value }))}
                  placeholder="Ex: CAYENNE10"
                  aria-label="Code promo"
                />
                <button onClick={applyPromo} disabled={promoBusy}>{promoBusy ? '…' : 'Appliquer'}</button>
              </div>
              {ctx.promoApplied && <div style={{ marginTop: 8, fontSize: 12, color: 'var(--green-text)', fontWeight: 700 }}>✓ {ctx.promoApplied} · −{(ctx.discount || 0).toFixed(2).replace('.', ',')} € appliqué</div>}
              {/* [FULL-FLOW HEAL 2026-05-18 P0-3 WCAG SC 4.1.3] promo error aria-live alert */}
              {ctx.promoErr && <div className="lcf-field-error" role="alert" aria-live="assertive"><WC_I.close s={12}/> {ctx.promoErr}</div>}
            </div>

            <div className="lcf-section">
              <h3>// Note pour la cuisine (optionnel)</h3>
              <textarea
                className="lc-notes"
                value={ctx.notes || ''}
                onChange={e => setCtx(c => ({ ...c, notes: e.target.value.slice(0, 190) }))}
                placeholder="Une précision ? Sauce à part, bien cuit, sans oignons…"
                maxLength={190}
                aria-label="Note pour la cuisine (optionnel)"
                aria-describedby="notes-counter"
              />
              {/* [FULL-FLOW HEAL 2026-05-18 P1-1] 190 char limit aligned with mobile spec */}
              <div id="notes-counter" aria-live="polite" style={{ fontSize: 10, color: 'var(--gray-3)', textAlign: 'right', marginTop: 2 }}>
                {(ctx.notes || '').length} / 190
              </div>
            </div>

            {fulfillment === 'pickup' && (
            <div className="lcf-section">
              <h3>// Lieu de retrait</h3>
              <div style={{ background: 'var(--cream-2)', borderRadius: 14, padding: 16, display: 'flex', gap: 14, alignItems: 'center' }}>
                <div style={{ width: 44, height: 44, borderRadius: 12, background: 'var(--ink)', color: 'var(--yellow)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>📍</div>
                <div style={{ flex: 1 }}>
                  <div style={{ fontWeight: 700, fontSize: 14 }}>Le Cayenne · Hénin-Beaumont</div>
                  <div style={{ fontSize: 12, color: 'var(--gray-3)', marginTop: 2 }}>{pickupStreet} · {pickupZip}</div>
                </div>
                <button onClick={() => window.open(pickupMapsUrl, '_blank', 'noopener')} style={{ background: 'var(--paper)', border: '1px solid var(--gray-1)', padding: '8px 12px', borderRadius: 999, fontSize: 11, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', cursor: 'pointer' }}>Itinéraire</button>
              </div>
            </div>
            )}

            <div className="lcf-cta-bar">
              <button className="lcf-cta-bar-back" onClick={onBack}>
                <WC_I.back s={14}/> Retour
              </button>
              <button className="lcf-cta-bar-next" onClick={onNext} disabled={!canProceed}>
                Continuer vers paiement <WC_I.arrow s={16}/>
                <b>{(total + (fulfillment === 'delivery' && dq ? dq.estimate.fee : 0)).toFixed(2).replace('.', ',')} €</b>
              </button>
            </div>
          </div>
          <OrderSummary cart={cart} discount={ctx.discount || 0} deliveryFee={ctx.fulfillment === 'delivery' && ctx.deliveryQuote ? ctx.deliveryQuote.estimate.fee : null}/>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// PAYMENT PAGE
// ============================================================
function PaymentPage({ cart, onBack, onNext, ctx, setCtx, isAuth, onAccount }) {
  // [GOAL-SYNC 2026-07-08] Formulaire carte VIDE par défaut (l'ancien pré-remplissage démo
  // 4242… + nom réel en dur est supprimé). Ce formulaire n'existe que derrière le flag ON.
  const [card, setCard] = wfvS({ num: '', exp: '', cvc: '', name: '' });
  // [RT-07 HEAL 2026-06-04] Brief processing state on submit so the user sees the action
  // registered (and double-fire is prevented) before the confirm transition.
  const [submitting, setSubmitting] = wfvS(false);
  // [T2-02 HEAL 2026-06-05] Decline path: the mock previously always succeeded. A Stripe-style
  // sentinel card (4000 0000 0000 0002) now produces a recoverable "refusé" state instead of
  // confirming. Only the card method can decline; cash/Apple/Google keep the happy path.
  const [payError, setPayError] = wfvS(false);
  // [WEB-WIREUP 2026-06-26] Real order placement via window.LC.api → backend caisse.
  //   Inline guest-OTP gate (phone → code) so an anonymous visitor can authenticate to order.
  const [apiError, setApiError] = wfvS(null);
  const [authStep, setAuthStep] = wfvS('none');   // none | phone | otp
  const [authPhone, setAuthPhone] = wfvS('');
  // [EMAIL-CODE 2026-07-29 · directive owner] Le client donne téléphone + EMAIL ;
  // le code part par EMAIL (Brevo, 0 SMS). Backend vague C : /guest-signup/email-otp
  // (l'email est persisté sur le compte au succès du verify — contrat inchangé).
  const [authEmail, setAuthEmail] = wfvS('');
  const [authOtp, setAuthOtp] = wfvS('');
  const [authErr, setAuthErr] = wfvS(null);
  const [authBusy, setAuthBusy] = wfvS(false);
  // [W16 DEV-OTP 2026-07-20] Code OTP renvoyé par le backend hors prod quand le SMS n'est pas câblé
  // (staging). Affiché sous le champ seulement si window.LC.isDev ; vide/absent en production.
  const [authDevCode, setAuthDevCode] = wfvS('');
  // [HEAL 2026-07-09 P2 · durci 2026-07-16 WEB-FUNNEL-DUP-NAV] Clé d'idempotence STABLE pour toute
  // la session paiement. Une nouvelle tentative (retry après timeout réseau, ou re-soumission)
  // réutilise la MÊME clé → le backend dédoublonne au lieu de créer une 2e commande.
  // ⚠️ AVANT : régénérée à chaque remontage du composant (retour Compte→paiement, changement de
  // route) → un client incertain (« ça a marché ? ») qui revenait et re-soumettait créait une
  // 2e commande. On persiste désormais la clé en sessionStorage : le remontage la RÉUTILISE
  // (dédup réel), et on ne la purge qu'après une commande RÉUSSIE (nouvelle commande = clé neuve).
  const [idemKey] = wfvS(() => {
    const gen = () => 'web' + Date.now().toString(36) + Math.random().toString(36).slice(2, 12);
    // [ULTRA-AUDIT 2026-07-19 P1] Clé scopée à la SIGNATURE du panier. AVANT : la clé persistée était
    // réutilisée quel que soit le panier → si la réponse était perdue (timeout) puis que le client
    // MODIFIAIT le panier et re-soumettait, même clé + payload différent = 409 « bloqué à vie » (commande
    // corrigée impossible dans l'onglet). Désormais : même panier → même clé (dédup retry), panier
    // modifié (remontage) → clé NEUVE. (goCheckout purge aussi la clé au retour au checkout.)
    let sig = '0';
    try { sig = (cart || []).map(i => (i.id != null ? i.id : i.slug) + 'x' + i.qty + '@' + i.price + (i.subs ? '~' + i.subs : '')).join('|'); } catch (_) {}
    try {
      const raw = sessionStorage.getItem('lc.funnel.idem');
      if (raw) { const o = JSON.parse(raw); if (o && o.sig === sig && o.key) return o.key; }
      const k = gen();
      sessionStorage.setItem('lc.funnel.idem', JSON.stringify({ sig: sig, key: k }));
      return k;
    } catch (_) { return gen(); }
  });
  const api = (typeof window !== 'undefined' && window.LC && window.LC.api) || null;
  // [GOAL-SYNC 2026-07-08] Flag « paiement en ligne » (meta feature-online-card, OFF par défaut).
  // OFF ⇒ seul le règlement au comptoir existe dans le DOM (aucune fausse option carte/wallet).
  const onlineCard = !!(api && api.config && api.config.onlineCardEnabled);
  // [TERRAIN-HEAL 2026-07-16 · WEB-DELIVERY-CONTRADICTION] Livraison gatée OFF par défaut (mandat V1
  // « 100% à emporter » — la copie du site le dit 4×). OFF ⇒ le toggle Mode disparaît, tout le monde
  // est en retrait (fulfillment='pickup'). Activation future : <meta name="feature-delivery" content="1">.
  const deliveryEnabled = !!(api && api.config && api.config.deliveryEnabled);
  // [GOAL-SYNC 2026-07-08] ratio earn depuis la config backend (fallback 1).
  const ppe = usePointsPerEuro();

  const placeRealOrder = async () => {
    // [GOAL-SYNC 2026-07-08] payment_method honnête : 4 (carte) UNIQUEMENT derrière le flag ON ;
    // sinon toujours 1 (comptoir) — plus de commandes UNPAID étiquetées « carte » en DB.
    const pm = (onlineCard && ctx.method === 'card') ? 4 : 1;
    // [TERRAIN-HEAL 2026-07-16 · WEB-NOTE-DEAD] La note cuisine (allergies !) était saisie mais
    // jamais transmise. On la passe à placeOrder qui la rattache à la 1ère ligne (canal instruction).
    // [DROP-FIX 2026-07-19] `expectedTotal` = LE grand total TTC affiché sur le bouton « Payer »
    //   (subtotal − remise + livraison, cf. `total` ci-dessous). Transmis à placeOrder → envoyé
    //   comme `expected_total` : le backend rejette 422 si son total SSOT diverge de +1 centime
    //   (garde anti « panier ≠ payé »). On envoie LA valeur exacte que le client voit, pas un
    //   recalcul qui pourrait dériver.
    // [Wave D] scheduledAt posé au choix du créneau (+30/+40/heure) — null en « dès que prêt ».
    const orderOpts = { cart, paymentMethod: pm, couponId: ctx.couponId || null, loyaltyCode: ctx.loyaltyCode || null, idempotencyKey: idemKey, note: (ctx.notes || '').trim() || null, expectedTotal: total, scheduledAt: ctx.scheduledAt || null };
    // [DELIVERY 2026-06-27 · barème 2026-07-27] Livraison : sauve l'adresse géocodée (→ address_id)
    // puis envoie les paramètres delivery. Le backend RECALCULE le fee depuis l'adresse (SSOT,
    // 4 € ≤3 km puis +2 €/km — plus d'« offerte ≥30 € »).
    if (ctx.fulfillment === 'delivery' && ctx.deliveryQuote) {
      const dq = ctx.deliveryQuote;
      const saved = await api.saveAddress({ label: 'Livraison', latitude: dq.lat, longitude: dq.lng, address: ctx.deliveryAddress });
      orderOpts.orderType = 5;
      orderOpts.addressId = saved.id;
      orderOpts.deliveryDistanceKm = dq.distanceKm;
      orderOpts.deliveryTime = 'ASAP';
      orderOpts.deliveryCharge = dq.estimate.fee;
    }
    const order = await api.placeOrder(orderOpts);
    // [ULTRA-AUDIT 2026-07-19 P1] Renommé `total`→`paidTotal` : ce `const` était HOISTÉ par Babel-standalone
    // (ES5) au-dessus de son usage `expectedTotal: total` (L422), qui lisait donc `undefined` → api.js
    // repliait sur le SOUS-TOTAL brut → 422 garanti dès qu'un coupon existe (remise ≠ 0). Désormais L422
    // lie le grand total OUTER (L523 = ce que montre le bouton « Payer ») ; ce paidTotal = total scellé backend.
    const paidTotal = Number(order.total != null ? order.total : (order.convert_price || 0));
    setCtx(c => ({
      ...c,
      orderId: order.order_serial_no || ('#' + order.id),
      orderDbId: order.id,
      orderQueue: order.queue_number || null,
      orderTotal: paidTotal,
      // [GOAL-SYNC 2026-07-08] floor(total × points_per_euro) — même règle que l'earn backend.
      earnedPoints: Math.floor((paidTotal || 0) * ppe),
    }));
    // [WEB-FUNNEL-DUP-NAV 2026-07-16] Commande RÉUSSIE → on purge la clé d'idempotence persistée :
    // la PROCHAINE commande obtiendra une clé neuve (sinon elle serait dédoublonnée à tort).
    try { sessionStorage.removeItem('lc.funnel.idem'); } catch (_) {}

    // [W-MOLLIE 2026-07-20] Paiement carte EN LIGNE (flag onlineCard + méthode 'card') : la commande
    // vient d'être créée UNPAID (pm=4) ; on lance le checkout Mollie et on REDIRIGE le client vers la
    // page de paiement hébergée Mollie. Seul le webhook la passera PAID (jamais le client). Le retour
    // Mollie ramène sur {site}?order={id} — traité au mount de l'App (index.html). REPLI FAIL-SAFE :
    // Mollie indisponible (503 non configuré / 502 compte non activé / réseau) ⇒ on NE bloque PAS le
    // client, la commande reste UNPAID encaissable au comptoir → confirmation normale.
    if (onlineCard && ctx.method === 'card') {
      try {
        // persister la commande en attente : le redirect plein-écran perd l'état React.
        try { sessionStorage.setItem('lc.mollie.pending', JSON.stringify({ orderDbId: order.id, orderId: order.order_serial_no || ('#' + order.id), orderQueue: order.queue_number || null, orderTotal: paidTotal, earnedPoints: Math.floor((paidTotal || 0) * ppe) })); } catch (_) {}
        const co = await api.mollieCheckout(order.id);
        if (co && co.checkout_url) { window.location.href = co.checkout_url; return; }  // → Mollie, on quitte l'app
        try { sessionStorage.removeItem('lc.mollie.pending'); } catch (_) {}             // réponse sans URL → repli comptoir
      } catch (e) {
        try { sessionStorage.removeItem('lc.mollie.pending'); } catch (_) {}             // 503/502/réseau → repli comptoir
      }
    }
    onNext();
  };

  const submit = async () => {
    if (submitting || authBusy || !ctx.method) return;
    if (payError) setPayError(false);
    if (apiError) setApiError(null);
    // [GOAL-SYNC 2026-07-08] Sentinelle de refus 4000…0002 : UNIQUEMENT derrière le flag ON
    // (flag OFF, la méthode carte n'existe plus dans le DOM).
    if (onlineCard && ctx.method === 'card' && card.num.replace(/\s/g, '').startsWith('4000000000000002')) {
      setPayError(true); return;
    }
    if (!api) { setApiError('API indisponible.'); return; }
    if (!api.isAuthed()) { setAuthStep('phone'); return; } // open OTP gate
    setSubmitting(true);
    try {
      await placeRealOrder();
    } catch (e) {
      setSubmitting(false);
      // [SEC-WEB-01 2026-07-15] Token périmé (api.js a purgé le token + levé kind:'auth')
      // → rouvrir l'OTP au lieu d'un message brut, pour que le client re-valide et que sa
      // commande aboutisse (au lieu d'être perdue en boucle « Unauthenticated »).
      if (e && e.kind === 'auth') { setAuthStep('phone'); setApiError((e && e.message) || 'Session expirée. Reconnecte-toi.'); return; }
      setApiError((e && e.message) || 'La commande a échoué. Réessaie ou paie en caisse.');
    }
  };

  const sendOtp = async () => {
    if (authBusy) return;
    const phone = authPhone.replace(/[^0-9]/g, '');
    if (phone.length < 6) { setAuthErr('Numéro invalide.'); return; }
    // [EMAIL-CODE 2026-07-29] email requis — le code part par EMAIL (plus de SMS).
    const email = String(authEmail || '').trim();
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email)) { setAuthErr('Email invalide — le code de confirmation y est envoyé.'); return; }
    setAuthErr(null); setAuthBusy(true);
    try { const r = await api.guestEmailOtp(phone, email); setAuthDevCode(r && r.dev_code ? String(r.dev_code) : ''); setAuthStep('otp'); }
    catch (e) { setAuthErr((e && e.message) || 'Envoi du code impossible.'); }
    finally { setAuthBusy(false); }
  };
  const verifyOtp = async () => {
    if (authBusy) return;
    const phone = authPhone.replace(/[^0-9]/g, '');
    setAuthErr(null); setAuthBusy(true);
    // [HEAL 2026-07-09 P1] Deux échecs distincts, deux affichages distincts :
    //  (1) OTP faux → la modale reste ouverte, on montre authErr dedans.
    //  (2) OTP OK mais placeRealOrder échoue → la modale est DÉJÀ fermée, donc authErr
    //      serait invisible (échec silencieux). On surface l'erreur via apiError, affichée
    //      sur la page paiement.
    try {
      await api.guestVerify(phone, authOtp.trim());
    } catch (e) {
      setAuthBusy(false);
      setAuthErr((e && e.message) || 'Code invalide.');
      return;
    }
    setAuthStep('none'); setAuthBusy(false);
    setSubmitting(true);
    try {
      await placeRealOrder();
    } catch (e) {
      setSubmitting(false);
      setApiError((e && e.message) || 'La commande a échoué. Réessaie ou paie en caisse.');
    }
  };
  // [GOAL-SYNC 2026-07-08] Honnêteté paiement : flag OFF (défaut) ⇒ SEULE la méthode comptoir
  // existe dans le DOM — les fausses options Carte/Apple Pay/Google Pay sont SUPPRIMÉES (aucun
  // débit en ligne n'existe en V1). Flag ON (test) ⇒ option carte en ligne SANS promesse
  // Stripe/3D-Secure ; le formulaire carte ci-dessous reste un simple placeholder du futur vrai
  // flux (le serveur bloque de toute façon : config/payment.php).
  const methods = [
    { id: 'counter', name: 'Payer sur place', meta: 'Espèces ou carte au comptoir — ton plat t\'attend', icon: '💵', cls: 'cash', recommended: true },
  ];
  if (onlineCard) {
    methods.push({ id: 'card', name: 'Carte bancaire (en ligne)', meta: 'Parcours de test — aucun débit réel pour l\'instant', icon: 'VISA', cls: 'card' });
  }
  const subtotal = cart.reduce((s,i) => s + i.price * i.qty, 0);
  // [DELIVERY 2026-06-27] Le total payé inclut le frais de livraison (le même devis que le checkout).
  const deliveryFee = ctx.fulfillment === 'delivery' && ctx.deliveryQuote ? ctx.deliveryQuote.estimate.fee : 0;
  const total = subtotal - (ctx.discount || 0) + deliveryFee;
  return (
    <div className="lcf-page">
      <div className="lc-container">
        <FunnelSteps step="payment" fulfillment={ctx.fulfillment}/>
        <div className="lcf-grid">
          <div className="lcf-card">
            <div className="lcf-card-head">
              <div>
                <div className="lc-eyebrow">// Étape 2 sur 2</div>
                <h2>Comment<br/>tu paies ?</h2>
              </div>
            </div>

            <div className="lcf-section">
              <h3>// Mode de paiement</h3>
              {/* [TERRAIN-HEAL 2026-07-16 · WEB-A11Y-RADIOGROUP] mode de paiement = radios */}
              <div className="lcf-paymethods" role="radiogroup" aria-label="Mode de paiement">
                {methods.map(m => (
                  <button key={m.id} role="radio" aria-checked={ctx.method===m.id} onClick={()=>setCtx(c => ({ ...c, method: m.id }))} className={'lcf-paymethod' + (ctx.method===m.id?' is-on':'')} style={{ position: 'relative' }}>
                    {m.recommended && <span className="lcf-recommended">★ Conseillé</span>}
                    {/* [GOAL-SYNC 2026-07-08] Icônes Apple/Google Pay supprimées (méthodes retirées du DOM). */}
                    <div className={'lcf-paymethod-icon lcf-paymethod-icon--' + m.cls}>
                      {m.cls === 'cash' && m.icon}
                      {m.cls === 'card' && m.icon}
                    </div>
                    <div className="lcf-paymethod-body">
                      <div className="lcf-paymethod-name">{m.name}</div>
                      <div className="lcf-paymethod-meta">{m.meta}</div>
                    </div>
                    <div className="lcf-paymethod-radio"><i/></div>
                  </button>
                ))}
              </div>

              {ctx.method === 'card' && (
                <div className="lcf-cardform">
                  {/* [W-MOLLIE 2026-07-20] PLUS de formulaire carte LOCAL. La carte est saisie sur la
                      page de paiement HÉBERGÉE Mollie (sécurisée, PCI-DSS) après « Payer ». Collecter
                      le numéro ici serait une DOUBLE saisie (redemandée par Mollie), inutile (jamais
                      transmise) et hors-PCI. On explique la redirection au lieu de fausses inputs. */}
                  <div role="note" style={{ display: 'flex', gap: 12, alignItems: 'flex-start', padding: '14px 16px', background: 'rgba(31,102,83,0.08)', border: '1px solid rgba(31,102,83,0.25)', borderRadius: 12 }}>
                    <svg width="22" height="22" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0, marginTop: 1, color: 'var(--green-text, #1FA653)' }}><path d="M12 3l8 3v6c0 4-3.5 7.5-8 9-4.5-1.5-8-5-8-9V6l8-3z" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round"/><path d="M9 12l2 2 4-4" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>
                    <div>
                      <div style={{ fontWeight: 700, fontSize: 14 }}>Paiement sécurisé Mollie</div>
                      <div style={{ fontSize: 12.5, color: 'var(--gray-4)', marginTop: 3, lineHeight: 1.5 }}>
                        En appuyant sur « Payer », tu es redirigé vers la page de paiement sécurisée : ta
                        carte est saisie chez Mollie, jamais sur ce site. Ta commande t'attend au retour.
                      </div>
                    </div>
                  </div>
                </div>
              )}

              <div className="lcf-security">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M12 3l8 3v6c0 4-3.5 7.5-8 9-4.5-1.5-8-5-8-9V6l8-3z" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round"/><path d="M9 12l2 2 4-4" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>
                {/* [W-MOLLIE 2026-07-20] Copie du bandeau sécurité HONNÊTE selon la méthode : carte en
                    ligne → « sécurisé via Mollie » ; comptoir → « réglé au comptoir » (V1). Plus de
                    « paiement au comptoir » affiché quand le client paie en ligne (affiché ≠ réalité). */}
                {ctx.method === 'card' ? (
                  <>
                    <span>Paiement <b>sécurisé en ligne</b> via Mollie · Données protégées (RGPD)</span>
                    <span title="paiement carte en ligne sécurisé" aria-label="paiement carte en ligne sécurisé" style={{ display: 'inline-block', marginLeft: 8, fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, letterSpacing: '0.16em', textTransform: 'uppercase', background: 'var(--green, #1FA653)', color: '#fff', padding: '3px 8px', borderRadius: 6 }}>Paiement en ligne</span>
                  </>
                ) : (
                  <>
                    <span>Commande <b>envoyée en direct</b> à la caisse · Données protégées (RGPD)</span>
                    <span title="paiement réglé au comptoir au retrait" aria-label="paiement réglé au comptoir au retrait" style={{ display: 'inline-block', marginLeft: 8, fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, letterSpacing: '0.16em', textTransform: 'uppercase', background: 'var(--yellow)', color: 'var(--ink)', padding: '3px 8px', borderRadius: 6 }}>Paiement au comptoir</span>
                  </>
                )}
              </div>
            </div>

            {!isAuth && (
              <div className="lcf-section">
                <h3>// Compte (recommandé)</h3>
                <div style={{ background: 'var(--yellow)', borderRadius: 16, padding: 16, display: 'flex', gap: 12, alignItems: 'center' }}>
                  <div style={{ fontSize: 32 }}>🎁</div>
                  <div style={{ flex: 1 }}>
                    {/* [GOAL-SYNC 2026-07-08] floor(total × points_per_euro) — aligné sur l'earn backend. */}
                    <div style={{ fontWeight: 700, fontSize: 14 }}>Cumule +{Math.floor(total * ppe)} pts sur cette commande</div>
                    <div style={{ fontSize: 12, color: 'var(--gray-4)', marginTop: 2 }}>Connecte-toi ou crée un compte (30 sec) pour cumuler tes points fidélité.</div>
                  </div>
                  <button onClick={onAccount} className="lc-btn lc-btn--ink" style={{ padding: '12px 18px' }}>Compte</button>
                </div>
              </div>
            )}

            {/* [GOAL-SYNC 2026-07-08] Bloc « Débit à la préparation · réservé sur ta carte »
                SUPPRIMÉ : aucun débit ni pré-autorisation n'existe — copy mensongère. */}

            {/* [WEB-WIREUP 2026-06-26] API error surface */}
            {apiError && (
              <div className="lcf-field-error" role="alert" aria-live="assertive" style={{ marginTop: 10 }}>
                <WC_I.close s={12}/> {apiError}
              </div>
            )}

            {/* [WEB-WIREUP 2026-06-26] Inline guest-OTP gate — required to send the order to the caisse */}
            {authStep !== 'none' && (
              <div className="lcf-section" style={{ marginTop: 10, padding: 16, background: 'var(--cream-2)', borderRadius: 14 }}>
                <h3>// Confirme tes coordonnées</h3>
                <div style={{ fontSize: 12, color: 'var(--gray-4)', marginBottom: 10 }}>
                  {/* [EMAIL-CODE 2026-07-29] téléphone + email ; le code part par EMAIL (0 SMS). */}
                  Pour envoyer ta commande à la caisse : ton téléphone + ton email — <b>tu reçois le code par email</b>.
                </div>
                {authStep === 'phone' && (
                  <>
                    <div className="lcf-cardform-field">
                      <label htmlFor="auth-phone">Téléphone</label>
                      <input id="auth-phone" inputMode="tel" autoComplete="tel" value={authPhone}
                        onChange={e => setAuthPhone(e.target.value)} placeholder="06 12 34 56 78"/>
                    </div>
                    <div className="lcf-cardform-field" style={{ marginTop: 8 }}>
                      <label htmlFor="auth-email">Email</label>
                      <input id="auth-email" type="email" inputMode="email" autoComplete="email" value={authEmail}
                        onChange={e => setAuthEmail(e.target.value)} placeholder="ton.email@exemple.fr"/>
                    </div>
                    <button className="lc-btn lc-btn--ink" disabled={authBusy} onClick={sendOtp} style={{ marginTop: 10, width: '100%' }}>
                      {authBusy ? 'Envoi…' : 'Recevoir le code par email'}
                    </button>
                  </>
                )}
                {authStep === 'otp' && (
                  <>
                    <div className="lcf-cardform-field">
                      <label htmlFor="auth-otp">Code reçu par email <span style={{ fontWeight: 400, color: 'var(--gray-3)' }}>(vérifie aussi les spams)</span></label>
                      <input id="auth-otp" inputMode="numeric" autoComplete="one-time-code" value={authOtp}
                        onChange={e => setAuthOtp(e.target.value)} placeholder="1234"/>
                    </div>
                    {/* [W16 DEV-OTP 2026-07-20] Staging sans SMS : code renvoyé par le backend, affiché
                        SEULEMENT si ?dev (window.LC.isDev) ET dev_code présent (absent en prod = rien). */}
                    {typeof window !== 'undefined' && window.LC && window.LC.isDev && authDevCode && (
                      <div className="lc-otp-devhint" role="note" style={{ marginTop: 6, padding: '6px 10px', background: 'rgba(255,90,31,0.10)', border: '1px dashed #d8410a', borderRadius: 8, fontSize: 13, textAlign: 'center' }}>
                        Code (dev) : <b style={{ letterSpacing: '3px' }}>{authDevCode}</b>
                      </div>
                    )}
                    <button className="lc-btn lc-btn--orange" disabled={authBusy} onClick={verifyOtp} style={{ marginTop: 10, width: '100%' }}>
                      {authBusy ? 'Vérification…' : 'Valider et envoyer la commande'}
                    </button>
                  </>
                )}
                {authErr && (
                  <div className="lcf-field-error" role="alert" aria-live="assertive" style={{ marginTop: 8 }}>
                    <WC_I.close s={12}/> {authErr}
                  </div>
                )}
              </div>
            )}

            <div className="lcf-cta-bar">
              <button className="lcf-cta-bar-back" onClick={onBack}>
                <WC_I.back s={14}/> Retour
              </button>
              <button className="lcf-cta-bar-next" onClick={submit} disabled={!ctx.method || submitting} aria-busy={submitting}>
                {submitting
                  ? (ctx.method === 'counter' ? 'Confirmation…' : 'Paiement…')
                  : (ctx.method === 'counter' ? 'Confirmer la commande' : 'Payer')}
                <b>{total.toFixed(2).replace('.', ',')} €</b>
              </button>
            </div>
          </div>
          <OrderSummary cart={cart} discount={ctx.discount || 0} deliveryFee={ctx.fulfillment === 'delivery' && ctx.deliveryQuote ? ctx.deliveryQuote.estimate.fee : null}/>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// CONFIRMATION
// ============================================================
function ConfirmationPage({ cart, ctx, onTrack, onHome }) {
  // [PAY-002 HEAL 2026-06-04] Prefer the order total snapshotted at payment confirm
  // (ctx.orderTotal) — the cart is cleared on confirm for both payment paths, so a live
  // cart calc would read €0,00. Fall back to live cart if no snapshot (legacy/direct render).
  const subtotal = cart.reduce((s,i) => s + i.price * i.qty, 0);
  const total = ctx.orderTotal != null ? ctx.orderTotal : subtotal - (ctx.discount || 0);
  const orderId = ctx.orderId || '—';   // [HONESTY 2026-07-19] pas de faux numéro « C-0000 » en repli (ne devrait jamais arriver post-commande, mais un placeholder neutre vaut mieux qu'un ticket fabriqué)
  return (
    <div className="lcf-page">
      <div className="lc-container">
        <FunnelSteps step="confirm" fulfillment={ctx.fulfillment}/>
        <div className="lcf-confirm">
          <Confetti/>
          <div className="lcf-confirm-check">
            <svg width="48" height="48" viewBox="0 0 24 24" fill="none">
              <path d="M5 12.5l4 4 10-10" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
          </div>
          <h1>C'est <span>parti !</span></h1>
          <p className="lcf-confirm-sub">
            {/* [W-MOLLIE 2026-07-20] Copy « Paiement confirmé » RÉSERVÉE au paiement carte en ligne
                RÉELLEMENT vérifié serveur au retour Mollie (ctx.mollieReturn==='paid', payment_status=PAID).
                Sinon (V1 comptoir OU carte non finalisée), règlement au comptoir — jamais de fausse promesse. */}
            {ctx.mollieReturn === 'paid'
              ? (<>Paiement confirmé <b>✓</b> — ta commande <b>#{String(orderId).replace(/^#/, '')}</b> est en préparation. Présente ce QR à la caisse pour la récupérer.</>)
              : ctx.mollieReturn === 'checking'
              ? (<>Vérification du paiement en cours… ta commande <b>#{String(orderId).replace(/^#/, '')}</b> est bien enregistrée. Présente ce QR à la caisse.</>)
              : (<>Ta commande <b>#{String(orderId).replace(/^#/, '')}</b> est envoyée. Présente ce QR à la caisse pour la récupérer.{' Tu paies sur place.'}</>)}
          </p>

          <div className="lcf-ticket">
            <div className="lcf-ticket-head">
              <h4>// Ticket retrait</h4>
              <em>HÉNIN-BEAUMONT</em>
            </div>
            <div className="lcf-ticket-qr">
              <TicketQR value={orderId}/>
            </div>
            <div className="lcf-ticket-code">{orderId}</div>
            <div className="lcf-ticket-foot">
              <div className="lcf-ticket-cell">
                <div className="lcf-ticket-cell-lbl">Prêt dans</div>
                <div className="lcf-ticket-cell-val">{ctx.slotTime || '~15-20 min'}</div>
              </div>
              <div className="lcf-ticket-cell">
                <div className="lcf-ticket-cell-lbl">Total</div>
                <div className="lcf-ticket-cell-val">{total.toFixed(2).replace('.', ',')} €</div>
              </div>
            </div>
          </div>

          {/* [WEB-WIREUP 2026-06-26] La commande est réellement transmise à la caisse / cuisine (KDS). */}
          <div style={{ marginTop: 14, textAlign: 'center' }}>
            <span title="commande envoyée à la caisse" aria-label="commande envoyée à la caisse" style={{ display: 'inline-block', fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, letterSpacing: '0.16em', textTransform: 'uppercase', background: 'var(--green, #1FA653)', color: '#fff', padding: '3px 8px', borderRadius: 6 }}>✓ Envoyée en cuisine</span>
          </div>

          <div className="lcf-confirm-ctas">
            <button className="lc-btn lc-btn--ink" onClick={onTrack}>Suivre ma commande <WC_I.arrow s={14}/></button>
            <button className="lc-btn" style={{ background: 'transparent', color: 'var(--ink)', border: '1.5px solid var(--ink)' }} onClick={onHome}>Retour à l'accueil</button>
          </div>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// TRACKING
// ============================================================
// [WEB-WIREUP 2026-06-26] Backend OrderStatus → tracking step (0=reçu,1=cuisine,2=prêt,3=récupéré).
function statusToStep(status) {
  switch (Number(status)) {
    case 1: case 4: return 0;       // PENDING / ACCEPT → reçu
    case 7: return 1;               // PREPARING → cuisine
    case 8: case 10: return 2;      // PREPARED / OUT_FOR_DELIVERY → prêt
    case 13: return 3;              // DELIVERED → récupéré
    default: return 0;
  }
}
function TrackingPage({ cart, ctx, onHome, onTicket }) {
  const [stepIdx, setStepIdx] = wfvS(0); // 0=received, 1=cooking, 2=ready, 3=picked
  const [statusName, setStatusName] = wfvS(null);
  const [canceled, setCanceled] = wfvS(false);
  // [PAY-002 HEAL 2026-06-04] Prefer snapshotted order total (cart cleared on confirm).
  const subtotal = cart.reduce((s,i) => s + i.price * i.qty, 0);
  const total = ctx.orderTotal != null ? ctx.orderTotal : subtotal - (ctx.discount || 0);
  // [WEB-WIREUP 2026-06-26] Poll the REAL order status from the backend caisse (no more timer mock).
  wfvE(() => {
    const api = (typeof window !== 'undefined' && window.LC && window.LC.api) || null;
    if (!api || !ctx.orderDbId) return;
    let alive = true; let timer = null; let fails = 0;
    const tick = () => {
      api.getOrder(ctx.orderDbId).then(o => {
        if (!alive || !o) return;
        fails = 0;
        setStatusName(o.status_name || null);
        const st = Number(o.status);
        if ([16, 19, 22].indexOf(st) !== -1) { setCanceled(true); return; } // terminal: stop
        setStepIdx(statusToStep(st));
        if (st === 13) return;                       // delivered → stop polling
        timer = setTimeout(tick, 20000);
      }).catch(() => {
        if (!alive) return;
        fails++;
        if (fails >= 3) return;                      // order gone / persistent error → give up quietly
        timer = setTimeout(tick, 30000);
      });
    };
    tick();
    return () => { alive = false; if (timer) clearTimeout(timer); };
  }, [ctx.orderDbId]);
  const steps = [
    { id: 'received', label: 'Reçu',      icon: '✓' },
    { id: 'cooking',  label: 'Cuisine',   icon: '🍳' },
    { id: 'ready',    label: 'Prêt',      icon: '🛍' },
    { id: 'picked',   label: 'Récupéré',  icon: '✨' },
  ];
  const fillPct = (stepIdx / (steps.length - 1)) * 100;
  return (
    <div className="lcf-page">
      <div className="lc-container">
        <div className="lcf-track">
          <div className="lcf-track-status"><i/> {canceled ? 'ANNULÉE' : (statusName || 'EN PRÉPARATION').toUpperCase()}</div>
          <h2>{ctx.orderId ? ('#' + String(ctx.orderId).replace(/^#/, '')) : '~12 MIN'}</h2>
          <p>{canceled ? "Cette commande a été annulée. Contacte-nous si besoin." : "Ta commande est en caisse / cuisine. Le statut se met à jour automatiquement."}</p>

          <div className="lcf-track-steps">
            <div className="lcf-track-bar">
              <div className="lcf-track-bar-fill" style={{ width: fillPct + '%' }}/>
            </div>
            {steps.map((s, i) => (
              <div key={s.id} className={'lcf-track-step' + (i < stepIdx ? ' is-done' : i === stepIdx ? ' is-current' : '')}>
                <div className="lcf-track-step-dot">{i <= stepIdx ? s.icon : i+1}</div>
                <div className="lcf-track-step-label">{s.label}</div>
              </div>
            ))}
          </div>

          <div className="lcf-track-info">
            <div>
              <div className="lcf-track-info-cell-lbl">Numéro</div>
              <div className="lcf-track-info-cell-val">{ctx.orderId || '—'}</div>
            </div>
            <div style={{ textAlign: 'right' }}>
              <div className="lcf-track-info-cell-lbl">Total</div>
              <div className="lcf-track-info-cell-val amount">{total.toFixed(2).replace('.', ',')} €</div>
            </div>
          </div>

          <button className="lcf-track-cta" onClick={onTicket}>
            Voir le QR de retrait <WC_I.arrow s={16}/>
          </button>
        </div>

        {/* Tips while waiting */}
        <div style={{ marginTop: 24, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
          <div style={{ background: 'var(--paper)', borderRadius: 18, padding: 20, boxShadow: 'var(--shadow-1)' }}>
            <div style={{ fontSize: 28 }}>🎁</div>
            <div style={{ fontWeight: 700, fontSize: 14, marginTop: 6 }}>+{ctx.earnedPoints != null ? ctx.earnedPoints : ((window.LC && window.LC.loyalty && window.LC.loyalty.earnPoints) ? window.LC.loyalty.earnPoints(total) : Math.floor(total))} pts crédités</div>
            <div style={{ fontSize: 12, color: 'var(--gray-3)', marginTop: 4 }}>Visible dans ta carte fidélité dès le retrait.</div>
            {/* [TRACK-02 2026-07-15] La commande est réelle (en caisse) ET le suivi ci-dessus
                poll le VRAI statut backend toutes les 20 s (funnel.jsx ~726 : api.getOrder →
                statusName/step). L'ancienne mention « illustratif / pas de live-status » était
                fausse (trompeuse). Points fidélité crédités au retrait. */}
            <span title="Statut mis à jour en direct depuis la caisse" aria-label="Statut mis à jour en direct depuis la caisse" style={{ display: 'inline-block', marginTop: 8, fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, letterSpacing: '0.16em', textTransform: 'uppercase', background: 'var(--cream-2, #eee)', color: 'var(--gray-4, #555)', padding: '3px 8px', borderRadius: 6 }}>Suivi en direct</span>
          </div>
          <div style={{ background: 'var(--paper)', borderRadius: 18, padding: 20, boxShadow: 'var(--shadow-1)' }}>
            <div style={{ fontSize: 28 }}>📞</div>
            <div style={{ fontWeight: 700, fontSize: 14, marginTop: 6 }}>Une question ?</div>
            <div style={{ fontSize: 12, color: 'var(--gray-3)', marginTop: 4 }}>03 65 67 82 91 · réponse immédiate</div>
          </div>
        </div>

        <div style={{ marginTop: 20, textAlign: 'center' }}>
          <button onClick={onHome} style={{ background: 'transparent', border: 0, color: 'var(--gray-4)', fontSize: 12, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase' }}>Retour à l'accueil</button>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { CheckoutPage, PaymentPage, ConfirmationPage, TrackingPage, OrderSummary, FunnelSteps });
