// app.jsx — App staff (white-label) : design V2 + TOUS les outils de la V1 (actions fonctionnelles).
const { useState, useEffect, useRef } = React;
// ── Config salle (white-label) : valeurs depuis /config.js (window.__CFG), repli NEUTRE OnPush. ──
// Les replis ne doivent JAMAIS contenir la marque d'un client réel (risque white-label au déploiement).
const CFG = (typeof window !== 'undefined' && window.__CFG) || {};
const BRAND = CFG.name || 'OnPush';
const BRAND_FULL = CFG.fullName || 'OnPush';
const GYM_PHONE = CFG.phone || '';
const GYM_PHONE_DISPLAY = CFG.phoneDisplay || '';
const APP_DOMAIN = CFG.domain || 'app.onpush.app';
const GOOGLE_REVIEW = CFG.reviewLink || '';
const PERTE_MINEUR_AN = (CFG.minFee4w || 29) * 13; // 1 mineur refusé = tarif mini x 13 périodes de 4 sem = manque à gagner /an
const ax = { fontFamily:'Archivo, sans-serif' };
const hk = { fontFamily:'"Hanken Grotesk", sans-serif' };
const mo = { fontFamily:'"Space Mono", monospace' };

// ---- Auth + API ----
const LS = { me:'4ds_me', code:'4ds_code', codeFor:'4ds_codeFor' };
const meName = () => localStorage.getItem(LS.me) || '';
// Vue admin "voir comme" : nom du membre dont on regarde l'interface (lecture seule).
// L'auth reste celle de l'admin (en-têtes), seul le "who" des données change.
const viewAsName = () => sessionStorage.getItem('4ds_viewas') || '';
const effName = () => viewAsName() || meName();
function authHeaders() { return { 'x-user': meName(), 'x-access-code': localStorage.getItem(LS.code) || '' }; }
async function api(path, opts = {}) {
  const r = await fetch(path, { ...opts, headers: { ...(opts.headers||{}), ...authHeaders() } });
  if (r.status === 401) { const e = new Error('auth'); e.auth = true; throw e; }
  if (!r.ok) throw new Error((await r.json().catch(()=>({}))).error || 'Erreur');
  return r.json();
}
// ── Push OS (Web Push) ──
function urlB64ToU8(base64) {
  const pad = '='.repeat((4 - base64.length % 4) % 4);
  const b64 = (base64 + pad).replace(/-/g, '+').replace(/_/g, '/');
  const raw = atob(b64); const arr = new Uint8Array(raw.length);
  for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
  return arr;
}
function pushSupported() { return ('serviceWorker' in navigator) && ('PushManager' in window) && ('Notification' in window); }
async function enablePush(opts = {}) {
  if (!pushSupported()) { alert("Ton appareil ne supporte pas les notifications.\nSur iPhone : ajoute d'abord l'app à l'écran d'accueil, puis ouvre-la depuis l'icône."); return false; }
  // skipConfirm : utilisé par la fenêtre bloquante (PushGate) qui sert déjà de confirmation.
  if (!opts.skipConfirm && !confirm("🔔 Les notifications sont OBLIGATOIRES pour le staff.\n\nElles te préviennent des relances avis, des messages du manager et de ce qu'il y a à traiter, même quand l'app est fermée.\n\nOn les active maintenant ?")) return false;
  try {
    const perm = await Notification.requestPermission();
    if (perm !== 'granted') { alert('Notifications refusées. Tu peux les réactiver dans les réglages de ton téléphone.'); return false; }
    const reg = await navigator.serviceWorker.ready;
    const { key } = await fetch('/api/push/key').then(r => r.json());
    if (!key) { alert('Push pas encore configuré côté serveur.'); return false; }
    let sub = await reg.pushManager.getSubscription();
    if (!sub) sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlB64ToU8(key) });
    const r = await api('/api/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sub }) });
    if (r && r.ok) { try { await api('/api/push/test', { method: 'POST' }); } catch (_) {} alert('✅ Notifications activées ! Tu vas recevoir une notif de test.'); return true; }
    alert('Échec : ' + ((r && (r.error || r.reason)) || '?')); return false;
  } catch (e) { alert('Échec : ' + e.message); return false; }
}

async function act(id, action, payload = {}) {
  return api('/api/lead', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ id, action, payload, who: meName() }) });
}

// ---- Dates ----
const pad = (n) => String(n).padStart(2,'0');
const todayISO = () => { const d=new Date(); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; };
const isSunday = (iso) => !!iso && new Date(iso+'T00:00:00').getDay()===0;
function plusDaysISO(n){ const d=new Date(); d.setDate(d.getDate()+n); if(d.getDay()===0) d.setDate(d.getDate()+1); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; }
// Rappel intra-journée : datetime ISO complet (avec heure) pour "rappeler aujourd'hui à Xh" / "dans Xh".
const todayAtISO = (h, m=0) => { const d=new Date(); d.setHours(h, m, 0, 0); return d.toISOString(); };
const inHoursISO = (h) => new Date(Date.now() + h*3600000).toISOString();
const inMinutesISO = (m) => new Date(Date.now() + m*60000).toISOString();
const hasTime = (iso) => typeof iso==='string' && iso.includes('T');
const fmtFR = (iso) => iso ? iso.slice(0,10).split('-').reverse().join('/') : '';
const ddmm = () => { const d=new Date(); return pad(d.getDate())+'/'+pad(d.getMonth()+1); };
const hhmm = () => { const d=new Date(); return pad(d.getHours())+'h'+pad(d.getMinutes()); };
const noteSign = () => (meName()||'Phoning') + ' · ' + ddmm() + ' ' + hhmm();

// ---- WhatsApp (mêmes messages que la V1) ----
const waNumber = (l) => {
  let d = (l.phone||'').replace(/\D/g,'');
  if (d.startsWith('0')) return '33'+d.slice(1);
  if (d.startsWith('33')) return d;
  return d ? '33'+d : '';
};
const waLink = (l, text) => 'https://wa.me/'+waNumber(l)+'?text='+encodeURIComponent(text);
// Repli "message classique" (SMS natif) quand le numéro n'est pas sur WhatsApp. Vide si pas de numéro.
const smsLink = (l, text) => { const n = waNumber(l); return n ? 'sms:+'+n+'?&body='+encodeURIComponent(text) : ''; };
const GYM_ADDR = CFG.addr || '27 ancienne route de Caumont au Thor';
const WA_TEMPLATES = (l) => { const me = meName(); const p = (l.name||'').split(' ')[0];
  // Si le lead a une séance d'essai (Calendly ou fixée par l'équipe) -> messages dédiés en tête :
  // bienvenue à la réservation + confirmation veille + rappel jour J (mêmes que l'onglet Séances).
  let seBlock = [];
  if (l.seanceEssai) {
    const se = l.seanceEssai, d = new Date(se);
    const h = hasTime(se) ? d.toLocaleTimeString('fr-FR',{hour:'2-digit',minute:'2-digit'}).replace(':','h') : '';
    const at = h ? ` à ${h}` : '';
    const jr = se.slice(0,10);
    const j = jr===todayISO() ? "aujourd'hui" : jr===plusDaysISO(1) ? 'demain' : 'le '+fmtFR(jr).slice(0,5);
    seBlock = [
      { label:'✅ Réservation reçue (à l\'inscription)', text:`Salut ${p}, super, ta séance d'essai ${j}${at} est bien réservée 💪 Si tu as la moindre question d'ici là, écris-moi ici, je suis là. À très vite !` },
      { label:'📅 Confirmer (la veille)', text:`Salut ${p}, c'est ${me} de ${BRAND}. Je te confirme ta séance d'essai ${j}${at}. On t'attend au ${GYM_ADDR}, viens comme tu es 💪` },
      { label:'⏰ Rappel du jour J', text:`Salut ${p}, on se voit${at} tout à l'heure pour ta séance d'essai 💪 On t'attend, viens comme tu es. À toute !` },
    ];
  }
  return [
  ...seBlock,
  { label:'👋 Premier contact', text:`Salut ${p}, c'est ${me} de ${BRAND_FULL}. Tu nous as laissé tes infos pour une séance d'essai, je reviens vers toi. On te la cale quand tu veux, tu penses à quel jour ?` },
  { label:'📞 Pas de réponse', text:`Salut ${p}, c'est ${me} de ${BRAND}. J'ai essayé de t'appeler mais je t'ai loupé. Pas de pression, dis-moi quand je peux te rappeler et on en parle tranquille.` },
  { label:'🔁 Relance', text:`Salut ${p}, c'est ${me} de ${BRAND}. Je reviens vers toi pour ta séance d'essai. Toujours motivé ? Si oui je te trouve un créneau qui t'arrange.` },
  { label:'✅ Confirmation', text:`Salut ${p}, c'est ${me}. C'est noté pour ta séance d'essai 💪 On t'attend au ${GYM_ADDR}, viens comme tu es. À très vite !` },
  { label:'📵 Absent (no-show)', text:`Salut ${p}, c'est ${me} de ${BRAND}. On t'a loupé aujourd'hui, ça arrive. On la replace quand tu veux, je te garde ton créneau.` },
  { label:'⭐ Demander un avis Google', text:`Salut ${p}, c'est ${me} de ${BRAND}. Si tu te plais chez nous, ça nous aiderait vraiment que tu nous laisses un petit avis Google, ça prend 2 min 🙏 ${GOOGLE_REVIEW}` },
]; };

// ---- Suivi des nouveaux adhérents : un message WhatsApp pré-rempli par étape du 1er mois ----
// Le vendeur qui l'a converti garde la relation. À personnaliser avant d'envoyer (ce n'est qu'une base).
const SUIVI_WA = (l) => { const me = meName(); const p = (l.name||'').split(' ')[0];
  return ({
    'J+2': { sub:'Premiers jours', text:`Salut ${p}, c'est ${me} de ${BRAND}. Je prends de tes nouvelles, comment s'est passée ta première séance ? Si tu as une question ou besoin de quoi que ce soit, écris-moi, je suis là.` },
    'J+7': { sub:'1ère semaine', text:`Salut ${p}, c'est ${me} de ${BRAND}. Ça fait une semaine que tu nous as rejoints, je voulais savoir comment ça se passe pour toi. Si tu as besoin de quoi que ce soit, je reste dispo.` },
    'J+14': { sub:'2 semaines', text:`Salut ${p}, c'est ${me} de ${BRAND}. Déjà deux semaines, je prends de tes nouvelles. Tu t'y retrouves bien ? Si tu as une question, je suis là pour t'aider.` },
    'J+30': { sub:'1 mois', text:`Salut ${p}, c'est ${me} de ${BRAND}. Ça fait un mois que tu es avec nous, le plus dur est passé. Je voulais savoir si tu te plais chez nous. Si c'est le cas, un avis Google nous aiderait beaucoup. ${GOOGLE_REVIEW}` },
  }[l.suiviEtape] || { sub:'Suivi', text:`Salut ${p}, c'est ${me} de ${BRAND}. Je prends de tes nouvelles, comment se passent tes débuts à la salle ?` });
};
const SUIVI_STEPS = ['J+2','J+7','J+14','J+30'];   // les 4 relances du 1er mois (miroir de core.mjs SUIVI_STEPS)

// ---- Signaler un bug : tampon des dernières erreurs + capture d'écran (html2canvas chargé à la demande, autorisé par la CSP unpkg) ----
const BUG_ERRORS = [];
(function(){ try {
  const log = (m) => { try { const s = String(m).slice(0,200); if (s) { BUG_ERRORS.push(s); if (BUG_ERRORS.length > 12) BUG_ERRORS.shift(); } } catch(_){} };
  window.addEventListener('error', (e)=> log((e.message||'erreur') + (e.filename ? ` @${String(e.filename).split('/').pop()}:${e.lineno||''}` : '')));
  window.addEventListener('unhandledrejection', (e)=> log('promesse rejetée : ' + ((e.reason && (e.reason.message||e.reason)) || '?')));
  const ce = console.error; console.error = function(){ try{ log(Array.prototype.map.call(arguments, String).join(' ')); }catch(_){} return ce.apply(console, arguments); };
} catch(_){} })();
let _h2cP = null;
function loadH2C(){ if (window.html2canvas) return Promise.resolve(window.html2canvas);
  if (!_h2cP) _h2cP = new Promise((res, rej)=>{ const s=document.createElement('script'); s.src='https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js'; s.onload=()=>res(window.html2canvas); s.onerror=()=>{ _h2cP=null; rej(new Error('chargement capture')); }; document.head.appendChild(s); });
  return _h2cP;
}
async function captureScreen(){
  const h2c = await loadH2C();
  const el = document.getElementById('root') || document.body;
  const w = Math.round(el.getBoundingClientRect().width) || window.innerWidth;
  const h = Math.round(el.getBoundingClientRect().height) || window.innerHeight;
  const bg = getComputedStyle(document.body).backgroundColor || '#0C0D10';
  const canvas = await h2c(el, {
    backgroundColor: bg, scale: Math.min(2, window.devicePixelRatio || 1.5), logging: false, useCORS: true, allowTaint: true,
    width: w, height: h, windowWidth: w, windowHeight: h, scrollX: 0, scrollY: 0,   // cadre EXACTEMENT l'écran visible
    ignoreElements: (e)=> e && e.getAttribute && e.getAttribute('data-bughide') === '1',  // ne pas capturer le bouton 🐛 lui-même
    // le conteneur #root est centré (max-width:480 + margin:auto) ; on le neutralise sur le clone pour que la capture ne soit pas décalée
    onclone: (doc)=>{ const r = doc.getElementById('root'); if (r) { r.style.maxWidth='none'; r.style.width='100%'; r.style.margin='0'; r.style.left='0'; r.style.right='auto'; } },
  });
  return canvas.toDataURL('image/jpeg', 0.82);
}

// ---- Mapping Notion -> design ----
function relWhen(iso) {
  if (!iso) return '';
  const d = new Date(iso+'T00:00:00'), now = new Date();
  const days = Math.round((new Date(now.toDateString()) - new Date(d.toDateString()))/86400000);
  if (days<=0) return "aujourd'hui"; if (days===1) return 'hier'; return `il y a ${days} j`;
}
// Date relative fine (horodatage complet) : "à l'instant", "il y a 12 min", "il y a 3 h", "hier 14h02"...
function relTime(iso) {
  if (!iso) return 'jamais';
  const d = new Date(iso), now = new Date(); const s = Math.floor((now - d)/1000);
  if (s < 60) return "à l'instant";
  if (s < 3600) return `il y a ${Math.floor(s/60)} min`;
  if (s < 86400 && d.toDateString()===now.toDateString()) return `aujourd'hui ${String(d.getHours()).padStart(2,'0')}h${String(d.getMinutes()).padStart(2,'0')}`;
  const days = Math.round((new Date(now.toDateString()) - new Date(d.toDateString()))/86400000);
  if (days===1) return `hier ${String(d.getHours()).padStart(2,'0')}h${String(d.getMinutes()).padStart(2,'0')}`;
  if (days < 7) return `il y a ${days} j`;
  return d.toLocaleDateString('fr-FR', { day:'2-digit', month:'2-digit' });
}
const ADMIN = CFG.admin || 'Ismail';
function mapStatus(s){ if(s==='Nouveau')return'nouveau'; if(s==='RDV pris')return'essai'; if(s==='Appelé')return'contacte'; return'contacte'; }
function mapLead(l){ return { id:l.id, name:[l.prenom,l.nom].filter(Boolean).join(' ')||'Lead', plan:l.source||'Lead', status:mapStatus(l.statut), statutRaw:l.statut||'', when:relWhen(l.dateEntree), phone:l.telephone||'', email:l.email||'', relanceLe:l.relanceLe||'', urgent:l.statut==='Nouveau', converted:l.conversion==='Oui', convType:l.convType||'', convPar:l.convPar||'', seanceEssai:l.seanceEssai||'', seanceProspect:!!l.seanceProspect, seanceConfirmee:!!l.seanceConfirmee, seanceAnnulee:!!l.seanceAnnulee, assigne:l.assigne||'', dateEntree:l.dateEntree||'', dateConversion:l.dateConversion||'', suiviEtape:l.suiviEtape||'', suiviProchain:l.suiviProchain||'', suiviEtat:l.suiviEtat||'', suiviDue:!!l.suiviDue }; }
const fmtSeance = (iso)=>{ if(!iso) return ''; const d=new Date(iso); return d.toLocaleDateString('fr-FR',{weekday:'short',day:'2-digit',month:'2-digit'})+' à '+d.toLocaleTimeString('fr-FR',{hour:'2-digit',minute:'2-digit'}).replace(':','h'); };

// ---- Couleur d'accent de l'app (persistante) ----
function hexRgba(hex, a){ const h=hex.replace('#',''); const r=parseInt(h.slice(0,2),16),g=parseInt(h.slice(2,4),16),b=parseInt(h.slice(4,6),16); return `rgba(${r},${g},${b},${a})`; }
// Mélange une base sombre avec la couleur d'accent (t = dose d'accent) -> fond/cartes teintés.
function tint(baseHex, accHex, t){ const b=baseHex.replace('#',''), a=accHex.replace('#',''); const f=(i)=>Math.round(parseInt(b.substr(i,2),16)*(1-t)+parseInt(a.substr(i,2),16)*t); const h=(n)=>n.toString(16).padStart(2,'0'); return '#'+h(f(0))+h(f(2))+h(f(4)); }
const ACCENTS = [
  { name:'Blanc', hex:'#F3F5F1', ink:'#0C0D10' },
  { name:'Vert', hex:'#22C55E', ink:'#0C0D10' },
  { name:'Rose', hex:'#EC4899', ink:'#ffffff' },
  { name:'Bleu', hex:'#2563EB', ink:'#ffffff' },
];
const DEFAULT_ACCENT = { name:'Bleu', hex:'#2563EB', ink:'#ffffff' };
function lum(hex){ const h=hex.replace('#',''); const r=parseInt(h.slice(0,2),16),g=parseInt(h.slice(2,4),16),b=parseInt(h.slice(4,6),16); return (0.299*r+0.587*g+0.114*b)/255; }
const getMode = () => localStorage.getItem('4ds_mode') || 'night';
const currentAccent = () => { try{ const a=JSON.parse(localStorage.getItem('4ds_accent')||'null'); return (a && ACCENTS.some(x=>x.name===a.name)) ? a : DEFAULT_ACCENT; }catch(_){ return DEFAULT_ACCENT; } };

// Applique le thème complet : couleur d'accent + mode (jour/nuit).
function applyTheme(a, mode){
  const s=document.documentElement.style;
  const day = mode==='day';
  // En mode jour, une couleur d'accent trop claire devient sombre (sinon illisible sur fond clair).
  const eff = (day && lum(a.hex)>0.62) ? '#17181C' : (!day && lum(a.hex)<0.10 ? '#F3F5F1' : a.hex);
  const ink = lum(eff)>0.6 ? '#0C0D10' : '#ffffff';
  s.setProperty('--volt', eff);
  s.setProperty('--volt-ink', ink);
  s.setProperty('--volt-dim', hexRgba(eff, day?0.14:0.16));
  s.setProperty('--volt-line', hexRgba(eff, day?0.5:0.36));
  // amber (état "en attente") par défaut, surchargé par les palettes sociales
  // En mode jour, l'ambre clair (#F6C254) est illisible sur fond blanc : on le fonce (lisible WCAG AA).
  s.setProperty('--amber', day ? '#B45309' : '#F6C254');
  s.setProperty('--amber-dim', 'rgba(246,194,84,0.14)');
  if(day){
    s.setProperty('--bg', tint('#F2F3F0', a.hex, 0.04));
    s.setProperty('--surface', '#ffffff');
    s.setProperty('--elev', '#ffffff');
    s.setProperty('--line', 'rgba(12,13,16,0.10)');
    s.setProperty('--text', '#0C0D10');
    s.setProperty('--muted', 'rgba(12,13,16,0.56)');
    s.setProperty('--faint', 'rgba(12,13,16,0.40)');
    s.setProperty('--navbg', 'rgba(255,255,255,0.88)');
    s.setProperty('--track', 'rgba(12,13,16,0.08)');
    // pas de palette : le fond et les cartes sont du même monde -> texte sur fond = texte des cartes
    s.setProperty('--on-bg', '#0C0D10');
    s.setProperty('--on-bg-muted', 'rgba(12,13,16,0.56)');
  } else {
    s.setProperty('--bg', tint('#0A0B0D', a.hex, 0.06));
    s.setProperty('--surface', tint('#15171C', a.hex, 0.10));
    s.setProperty('--elev', tint('#1C1F26', a.hex, 0.13));
    s.setProperty('--line', hexRgba(a.hex, 0.16));
    s.setProperty('--text', '#F3F5F1');
    s.setProperty('--muted', 'rgba(243,245,241,0.56)');
    s.setProperty('--faint', 'rgba(243,245,241,0.34)');
    s.setProperty('--navbg', 'rgba(12,13,16,0.92)');
    s.setProperty('--track', 'rgba(255,255,255,0.08)');
    s.setProperty('--on-bg', '#F3F5F1');
    s.setProperty('--on-bg-muted', 'rgba(243,245,241,0.56)');
  }
}

// ---- Palettes "réseaux" (thème complet : fond coloré + cartes claires). 2 niveaux de texte. ----
const PALETTES = [
  { id:'instagram', name:'Instagram', dot:'linear-gradient(135deg,#FEDA75,#FA7E1E,#D62976,#962FBF,#4F5BD5)',
    bg:'linear-gradient(160deg,#FEDA75 0%,#FA7E1E 20%,#D62976 50%,#962FBF 75%,#4F5BD5 100%)', onBg:'#FFFFFF', onBgMuted:'rgba(255,255,255,.82)',
    surface:'#FFFFFF', text:'#262626', muted:'rgba(38,38,38,.56)', faint:'rgba(38,38,38,.36)', line:'rgba(0,0,0,.08)',
    accent:'#E1306C', accentDim:'rgba(225,48,108,.12)', amber:'#C77A0A', navBg:'rgba(255,255,255,.92)' },
  { id:'facebook', name:'Facebook', dot:'#1877F2',
    bg:'#1877F2', onBg:'#FFFFFF', onBgMuted:'rgba(255,255,255,.82)',
    surface:'#FFFFFF', text:'#050505', muted:'rgba(5,5,5,.55)', faint:'rgba(5,5,5,.36)', line:'rgba(0,0,0,.08)',
    accent:'#1877F2', accentDim:'rgba(24,119,242,.12)', amber:'#C77A0A', navBg:'rgba(255,255,255,.95)' },
  { id:'snapchat', name:'Snapchat', dot:'#FFFC00',
    bg:'#FFFC00', onBg:'#0B0B0B', onBgMuted:'rgba(11,11,11,.66)',
    surface:'#FFFFFF', text:'#111111', muted:'rgba(17,17,17,.56)', faint:'rgba(17,17,17,.36)', line:'rgba(0,0,0,.08)',
    accent:'#0B0B0B', accentDim:'rgba(255,252,0,.7)', amber:'#B07A1E', navBg:'rgba(255,255,255,.95)' },
  { id:'whatsapp', name:'WhatsApp', dot:'#128C7E',
    bg:'#0B5C50', onBg:'#FFFFFF', onBgMuted:'rgba(255,255,255,.8)',
    surface:'#FFFFFF', text:'#0B141A', muted:'rgba(11,20,26,.55)', faint:'rgba(11,20,26,.36)', line:'rgba(0,0,0,.08)',
    accent:'#128C7E', accentDim:'rgba(18,140,126,.13)', amber:'#C77A0A', navBg:'rgba(255,255,255,.95)' },
  { id:'tiktok', name:'TikTok', dot:'linear-gradient(135deg,#FE2C55,#000,#25F4EE)',
    bg:'#000000', onBg:'#FFFFFF', onBgMuted:'rgba(255,255,255,.62)',
    surface:'#161616', text:'#FFFFFF', muted:'rgba(255,255,255,.56)', faint:'rgba(255,255,255,.36)', line:'rgba(255,255,255,.1)',
    accent:'#FE2C55', accentDim:'rgba(254,44,85,.16)', amber:'#F6C254', navBg:'rgba(0,0,0,.9)' },
];
function applyPalette(p){
  const s=document.documentElement.style;
  const ink = lum((p.accent||'#000').replace ? p.accent : '#000') > 0.6 ? '#0C0D10' : '#ffffff';
  s.setProperty('--bg', p.bg);
  s.setProperty('--surface', p.surface);
  s.setProperty('--elev', p.surface);
  s.setProperty('--line', p.line);
  s.setProperty('--text', p.text);
  s.setProperty('--muted', p.muted);
  s.setProperty('--faint', p.faint);
  s.setProperty('--on-bg', p.onBg);
  s.setProperty('--on-bg-muted', p.onBgMuted);
  s.setProperty('--navbg', p.navBg);
  s.setProperty('--track', hexRgba(p.text.length>=7?p.text:'#000000', 0.08));
  s.setProperty('--amber', p.amber);
  s.setProperty('--amber-dim', hexRgba(p.amber, 0.14));
  s.setProperty('--volt', p.accent);
  s.setProperty('--volt-ink', ink);
  s.setProperty('--volt-dim', p.accentDim);
  s.setProperty('--volt-line', hexRgba(p.accent, 0.45));
}
const currentPalette = () => { const id=localStorage.getItem('4ds_palette'); return id ? PALETTES.find(p=>p.id===id) : null; };
function setPalette(p){ if(p){ localStorage.setItem('4ds_palette', p.id); applyPalette(p); } else { localStorage.removeItem('4ds_palette'); applyTheme(currentAccent(), getMode()); } }
function applyAccent(a){ localStorage.removeItem('4ds_palette'); applyTheme(a, getMode()); localStorage.setItem('4ds_accent', JSON.stringify(a)); }
function setMode(m){ localStorage.setItem('4ds_mode', m); localStorage.removeItem('4ds_palette'); applyTheme(currentAccent(), m); }
async function setGlobalAccent(a){ applyAccent(a); try{ await api('/api/accent', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ accent:a }) }); }catch(_){} }
// Thèmes "réseaux" (PALETTES/applyPalette ci-dessus) gardés dormants : pas exposés dans l'UI pour l'instant,
// réactivables plus tard (cf. note mémoire). Version de base = couleur d'accent + mode jour/nuit.
(function boot(){ localStorage.removeItem('4ds_palette'); applyTheme(currentAccent(), getMode());
  if ('serviceWorker' in navigator) { try { navigator.serviceWorker.register('/sw.js').catch(()=>{}); } catch(_){} }
})();

// ════════ UI de base ════════
function Screen({ children, active, go }) {
  const admin = meName()===ADMIN && !viewAsName(); // en vue "voir comme", on garde la nav vendeur
  return <div style={{ height:'100%', display:'flex', flexDirection:'column', background:T.bg, color:T.text, ...hk }}>
    <div style={{ flex:1, overflowY:'auto', paddingTop:20 }}>{children}</div>
    <BottomNav active={active} onNav={go} admin={admin} />
  </div>;
}
const SectionLabel = ({ children, action, onAction }) => (
  <div style={{ display:'flex', alignItems:'baseline', justifyContent:'space-between', margin:'0 0 12px' }}>
    <span style={{ ...mo, fontSize:11, letterSpacing:1, textTransform:'uppercase', color:T.onBgMuted }}>{children}</span>
    {action && <span onClick={onAction} style={{ ...hk, fontSize:13, color:T.volt, fontWeight:600, cursor:'pointer' }}>{action}</span>}
  </div>
);
const btn = (bg, color, extra={}) => ({ border:'none', cursor:'pointer', borderRadius:13, padding:'13px 0', ...hk, fontSize:14, fontWeight:700, background:bg, color, display:'flex', alignItems:'center', justifyContent:'center', gap:7, ...extra });
// Vert "conversion" pour les boutons d'action principaux (indépendant du thème).
const GREEN = '#22C55E', GINK = '#06270F';
const gbtn = (extra={}) => btn(GREEN, GINK, extra);
const obtn = { border:`1px solid ${T.line}`, cursor:'pointer', borderRadius:13, padding:'12px 0', ...hk, fontSize:13.5, fontWeight:600, background:'transparent', color:T.text };

// Boutons d'action d'un lead (tous les outils de la V1)
function LeadButtons({ lead, H }) {
  const [more, setMore] = useState(false); // replie les actions rares (mauvais num / mineur / Deciplus)
  return <div>
    <div style={{ display:'flex', gap:12, marginBottom:14 }}>
      <button onClick={()=>H.call(lead)} style={{ flex:1, ...gbtn({ padding:'15px 0' }) }}><Icon name="phone" size={18} color={GINK} />Appeler</button>
      <button onClick={()=>H.wa(lead)} style={{ flex:1, ...btn('#25D366','#fff', { padding:'15px 0' }) }}>📲 WhatsApp</button>
    </div>
    {lead.converted
      ? <div style={{ marginBottom:14 }}>
          <div style={{ display:'flex', alignItems:'center', gap:8, background:'rgba(34,197,94,0.14)', border:'1px solid rgba(34,197,94,0.4)', borderRadius:12, padding:'10px 12px', marginBottom:8, ...hk, fontSize:13, fontWeight:600 }}>
            <span style={{ color:GREEN }}>✅ Converti</span>
            <span style={{ color:T.muted }}>{lead.convType?`· ${lead.convType}`:''}{lead.convPar?` · par ${lead.convPar}`:''}</span>
          </div>
          {lead.convType==='Retour offert' && <button onClick={()=>H.conv(lead)} style={{ width:'100%', marginBottom:8, ...btn('linear-gradient(135deg,#f59e0b,#fcd34d)','#1a1300', { padding:'13px 0' }), fontWeight:800 }}>💰 Passé en abonnement payant (déclenche la prime)</button>}
          <button onClick={()=>H.deconv(lead)} style={{ width:'100%', ...obtn, color:'#ff8a8a', borderColor:'rgba(255,138,138,0.4)' }}>↩️ Annuler la conversion</button>
        </div>
      : <button onClick={()=>H.conv(lead)} style={{ width:'100%', ...btn('linear-gradient(135deg,#f59e0b,#fcd34d)','#1a1300', { padding:'15px 0' }), marginBottom:14, fontWeight:800 }}>💰 Converti</button>}
    <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10 }}>
      <button onClick={()=>H.open(lead,'rdv')} style={obtn}>✅ RDV pris</button>
      <button onClick={()=>H.open(lead,'relance')} style={obtn}>🔁 À relancer</button>
      <button onClick={()=>H.noAnswer(lead)} style={{ ...obtn, gridColumn:'1 / span 2', color:T.amber, borderColor:'rgba(246,194,84,0.4)' }}>📵 Pas de réponse</button>
      <button onClick={()=>H.open(lead,'pasinteresse')} style={{ ...obtn, color:'#ff8a8a' }}>✖ Pas intéressé</button>
      <button onClick={()=>H.open(lead,'note')} style={obtn}>💬 Note</button>
      <button onClick={()=>setMore(m=>!m)} style={{ ...obtn, gridColumn:'1 / span 2', color:T.muted }}>{more ? 'Moins d\'options ⌃' : 'Plus d\'options ⌄'}</button>
      {more && <>
        <button onClick={()=>H.mauvais(lead)} style={obtn}>📵 Mauvais numéro</button>
        <button onClick={()=>H.mineur(lead)} style={{ ...obtn, color:T.muted }}>🔞 Mineur</button>
        <button onClick={()=>H.deciplus(lead)} style={{ ...obtn, gridColumn:'1 / span 2', color:'#7dd3fc', borderColor:'rgba(125,211,252,0.4)' }}>📧 Passé sur Deciplus (emailing)</button>
      </>}
    </div>
    {meName()===ADMIN && !viewAsName() && <button onClick={()=>H.delLead(lead)} style={{ width:'100%', marginTop:14, ...obtn, color:'#ff6b6b', borderColor:'rgba(239,68,68,0.4)' }}>🗑️ Supprimer ce lead (admin)</button>}
  </div>;
}

// Compteur animé (la cagnotte qui monte).
function useCountUp(target, dur=1000){
  const [v,setV]=useState(0);
  useEffect(()=>{ let raf; const t0=performance.now();
    const tick=(now)=>{ const p=Math.min(1,(now-t0)/dur); setV(Math.round(target*(1-Math.pow(1-p,3)))); if(p<1) raf=requestAnimationFrame(tick); };
    raf=requestAnimationFrame(tick); return ()=>cancelAnimationFrame(raf);
  },[target]); return v;
}

// Record battu : fanfare montante + accord final + grosse vibration (plus fort que la fête de palier).
function recordFx() {
  try {
    if (navigator.vibrate) { navigator.vibrate([0, 80, 60, 80, 60, 80, 60, 220]); }
    else { for (let k=0; k<6; k++) setTimeout(iosHapticTick, k*150); }
  } catch(_){}
  try {
    const AC = window.AudioContext || window.webkitAudioContext;
    if (!AC) return;
    const ctx = new AC();
    if (ctx.state === 'suspended') ctx.resume();
    const now = ctx.currentTime;
    const seq = [523.25, 659.25, 783.99, 1046.5, 1318.5]; // montée triomphante
    seq.forEach((f, i) => {
      const o = ctx.createOscillator(), g = ctx.createGain();
      o.type = 'triangle'; o.frequency.value = f;
      const t = now + i * 0.12;
      g.gain.setValueAtTime(0.0001, t);
      g.gain.exponentialRampToValueAtTime(0.25, t + 0.02);
      g.gain.exponentialRampToValueAtTime(0.0001, t + 0.40);
      o.connect(g); g.connect(ctx.destination); o.start(t); o.stop(t + 0.42);
    });
    const chord = [523.25, 659.25, 783.99], tc = now + seq.length * 0.12 + 0.05; // accord final qui tient
    chord.forEach((f) => {
      const o = ctx.createOscillator(), g = ctx.createGain();
      o.type = 'triangle'; o.frequency.value = f;
      g.gain.setValueAtTime(0.0001, tc);
      g.gain.exponentialRampToValueAtTime(0.20, tc + 0.03);
      g.gain.exponentialRampToValueAtTime(0.0001, tc + 0.95);
      o.connect(g); g.connect(ctx.destination); o.start(tc); o.stop(tc + 1.0);
    });
    setTimeout(() => { try { ctx.close(); } catch(_){} }, 2600);
  } catch(_){}
}

// Overlay plein écran quand un conseiller bat son record de prime.
// Célébration plein écran GÉNÉRIQUE (base de l'animation "record", déclinée par palier).
function BigCelebration({ emoji='🎉', kicker, big, sub, accent='#CBFB45', confColors, onClose }) {
  const cols = confColors || ['#CBFB45','#FACC15','#FFD700','#22c55e','#ffffff','#fde68a'];
  return <div onClick={onClose} style={{ position:'fixed', inset:0, zIndex:120, display:'flex', alignItems:'center', justifyContent:'center', padding:24, background:`radial-gradient(circle at 50% 38%, ${hexRgba(accent,0.22)}, rgba(0,0,0,0.96))`, animation:'recordbg .4s ease' }}>
    {Array.from({length:46}).map((_,i)=>(<span key={i} className="confetti-full" style={{ left:`${(i*2.17)%100}%`, background:cols[i%cols.length], animation:`confettifull ${1.6+(i%5)*0.25}s ease-out ${(i%8)*0.09}s forwards` }} />))}
    <div onClick={e=>e.stopPropagation()} style={{ position:'relative', textAlign:'center', maxWidth:360 }}>
      <div style={{ fontSize:84, animation:'recordpop .7s cubic-bezier(.2,.9,.3,1.4) both' }}>{emoji}</div>
      {kicker && <div style={{ ...mo, fontSize:13, letterSpacing:3, color:accent, marginTop:8, animation:'recordglow 1.6s ease infinite' }}>{kicker}</div>}
      <div style={{ ...ax, fontSize:58, fontWeight:900, color:'#fff', letterSpacing:-2, margin:'4px 0', animation:'recordpop .7s cubic-bezier(.2,.9,.3,1.4) .1s both' }}>{big}</div>
      {sub && <div style={{ ...hk, fontSize:15, color:'rgba(255,255,255,0.85)', lineHeight:1.5 }}>{sub}</div>}
      <button onClick={onClose} style={{ marginTop:22, border:'1px solid rgba(255,255,255,0.25)', background:'transparent', color:'#fff', borderRadius:14, padding:'12px 28px', ...hk, fontSize:15, fontWeight:700, cursor:'pointer' }}>Continuer 🚀</button>
    </div>
  </div>;
}

// Son DISTINCT par palier (notes différentes) + vibration. Réutilise iosHapticTick.
function milestoneFx(cfg = {}) {
  const notes = cfg.notes || [523.25, 659.25, 783.99];
  try { if (navigator.vibrate) navigator.vibrate(cfg.vib || [0,60,45,60,45,160]); else { for (let k=0;k<4;k++) setTimeout(iosHapticTick, k*150); } } catch(_){}
  try {
    const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return;
    const ctx = new AC(); if (ctx.state==='suspended') ctx.resume();
    const now = ctx.currentTime;
    notes.forEach((f,i)=>{ const o=ctx.createOscillator(), g=ctx.createGain(); o.type='triangle'; o.frequency.value=f; const t=now+i*0.11; g.gain.setValueAtTime(0.0001,t); g.gain.exponentialRampToValueAtTime(0.24,t+0.02); g.gain.exponentialRampToValueAtTime(0.0001,t+0.38); o.connect(g); g.connect(ctx.destination); o.start(t); o.stop(t+0.4); });
    if (cfg.chord) { const tc=now+notes.length*0.11+0.05; [523.25,659.25,783.99].forEach(f=>{ const o=ctx.createOscillator(), g=ctx.createGain(); o.type='triangle'; o.frequency.value=f; g.gain.setValueAtTime(0.0001,tc); g.gain.exponentialRampToValueAtTime(0.2,tc+0.03); g.gain.exponentialRampToValueAtTime(0.0001,tc+0.95); o.connect(g); g.connect(ctx.destination); o.start(tc); o.stop(tc+1.0); }); }
    setTimeout(()=>{ try{ctx.close();}catch(_){} }, 2400);
  } catch(_){}
}

// Une animation différente par palier d'AVIS (30 / 50 / 100).
const AVIS_MILESTONES = {
  30:  { emoji:'⭐', kicker:'PALIER DÉBLOQUÉ', sub:'+50€ pour toute l\'équipe 🙌', accent:'#FACC15', confColors:['#FACC15','#fde68a','#f59e0b','#ffffff'], notes:[523.25,659.25,783.99] },
  50:  { emoji:'🌟', kicker:'ÇA MONTE',        sub:'+70€ chacun, on lâche rien 🔥', accent:'#a78bfa', confColors:['#a78bfa','#c4b5fd','#f0abfc','#ffffff'], notes:[587.33,739.99,880] },
  100: { emoji:'🏆', kicker:'LÉGENDES',        sub:'+100€ chacun 👑', accent:'#FFD700', confColors:['#FFD700','#FACC15','#fff7cc','#ffffff'], notes:[523.25,659.25,783.99,1046.5,1318.5], chord:true },
};
function avisMilestoneCfg(seuil, prime) {
  const m = AVIS_MILESTONES[seuil] || { emoji:'⭐', kicker:'PALIER DÉBLOQUÉ', sub:`+${prime}€ pour toute l'équipe 🙌`, accent:'#FACC15', notes:[523.25,659.25,783.99] };
  return { ...m, big:`${seuil} avis !` };
}
// Animation CAGNOTTE : le style CHANGE à chaque palier de 100€ (cycle de 4 looks).
const CAG_STYLES = [
  { emoji:'🔥', accent:'#f59e0b', confColors:['#f59e0b','#fbbf24','#fde68a','#ffffff'], notes:[523.25,659.25,783.99] },
  { emoji:'🚀', accent:'#38bdf8', confColors:['#38bdf8','#7dd3fc','#a5f3fc','#ffffff'], notes:[587.33,739.99,880] },
  { emoji:'💪', accent:'#22c55e', confColors:['#22c55e','#86efac','#bbf7d0','#ffffff'], notes:[659.25,783.99,987.77] },
  { emoji:'💎', accent:'#e879f9', confColors:['#e879f9','#f0abfc','#f5d0fe','#ffffff'], notes:[698.46,880,1046.5], chord:true },
];
function cagMilestoneCfg(palier, firstGoal=300) {
  const idx = Math.max(0, Math.round((palier - firstGoal)/100)) % CAG_STYLES.length;
  return { ...CAG_STYLES[idx], kicker:'PALIER CAGNOTTE', big:`${palier} €`, sub:'atteint, prochain objectif en vue 🔝' };
}

// Record battu = la grande animation préférée d'Ismail (déclinaison de BigCelebration).
function RecordOverlay({ amount, onClose }) {
  return <BigCelebration emoji="🏆" kicker="NOUVEAU RECORD" big={`${amount} €`} sub="Ta meilleure prime, jamais atteinte. Énorme 💪" accent="#CBFB45" onClose={onClose} />;
}

// Carte cagnotte premium : relief, contour gradient animé, reflet, compteur qui monte.
const MOIS_FR = ['JANVIER','FÉVRIER','MARS','AVRIL','MAI','JUIN','JUILLET','AOÛT','SEPTEMBRE','OCTOBRE','NOVEMBRE','DÉCEMBRE'];
function CagnotteCard({ validee=0, attente=0, goal=300, avis=0, growth=null }){
  const val = useCountUp(validee); // le GROS chiffre = la prime réelle (validée), pas le potentiel
  // Paliers PROGRESSIFS : objectif de base (300€), puis +100€ à chaque palier atteint.
  const step = 100;
  const firstGoal = goal || 300;
  const passed = validee < firstGoal ? 0 : Math.floor((validee - firstGoal)/step) + 1; // nb de paliers franchis
  const floor = passed === 0 ? 0 : firstGoal + (passed-1)*step;     // dernier palier atteint
  const target = passed === 0 ? firstGoal : firstGoal + passed*step; // prochain objectif
  const span = Math.max(1, target - floor);
  const pctVal = Math.min(100, ((validee-floor)/span)*100);
  const pctAtt = Math.min(100-pctVal, (attente/span)*100);
  const reste = Math.max(0, target - validee);
  const mois = MOIS_FR[new Date().getMonth()];
  // Fête de palier cagnotte = le seul grand moment plein écran. Le record est suivi en silence (l'overlay se déclenchait à chaque nouveau plus-haut, ça dévalorisait l'effet).
  const [cagFx, setCagFx] = useState(null);
  useEffect(()=>{
    let recordFired = false;
    try {
      const raw = localStorage.getItem('cag_record');
      const rec = raw == null ? null : Number(raw);
      if (rec == null || validee > rec) localStorage.setItem('cag_record', String(validee)); // record suivi en silence (plus d'overlay plein écran)
    } catch(_){}
    try {
      const seen = Number(localStorage.getItem('cag_palier_seen') ?? '-1');
      if (!recordFired && seen >= 0 && passed > seen) { setCagFx(floor); milestoneFx(cagMilestoneCfg(floor, firstGoal)); setTimeout(()=>setCagFx(null), 7000); }
      localStorage.setItem('cag_palier_seen', String(passed));
    } catch(_){}
  }, [validee]);
  return <>
    {cagFx != null && <BigCelebration {...cagMilestoneCfg(cagFx, firstGoal)} onClose={()=>setCagFx(null)} />}
    <div className="cag-wrap" data-tour="cagnotte">
    <div className="cag-card">
      <div className="cag-shine" />
      <div className="cag-coin" style={{ top:56, right:20, fontSize:54 }}>💰</div>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between' }}>
        <span style={{ ...mo, fontSize:11, letterSpacing:1.5, textTransform:'uppercase', color:'rgba(255,255,255,0.6)' }}>Prime du mois · {mois}</span>
      </div>
      <div className="cag-amount" style={ax}>{val.toLocaleString('fr-FR')} €</div>
      {growth!=null && growth>0 && <div style={{ ...hk, fontSize:12, fontWeight:700, color:'#86efac', margin:'-8px 0 10px' }}>↗ +{growth}% par rapport à ta prime du mois dernier</div>}
      <div style={{ display:'flex', gap:16, margin:'2px 0 12px' }}>
        <span style={{ ...hk, fontSize:12.5, color:'rgba(255,255,255,0.85)', display:'flex', alignItems:'center', gap:6 }}>
          <i style={{ width:9, height:9, borderRadius:999, background:GREEN, boxShadow:'0 0 8px rgba(34,197,94,.6)' }} /> Validée <b style={{ color:'#fff' }}>{validee} €</b>
        </span>
        <span style={{ ...hk, fontSize:12.5, color:'rgba(255,255,255,0.85)', display:'flex', alignItems:'center', gap:6 }}>
          <i style={{ width:9, height:9, borderRadius:999, background:'rgba(250,204,21,0.85)' }} /> En attente <b style={{ color:'#fff' }}>{attente} €</b>
        </span>
      </div>
      <div className="cag-bar" style={{ display:'flex' }}>
        <i style={{ width:`${pctVal}%`, borderRadius:0, background:'linear-gradient(90deg,#16A34A,#22C55E)', boxShadow:'0 0 14px rgba(34,197,94,.6)', transition:'width 1.1s cubic-bezier(.22,1,.36,1)' }} />
        <i style={{ width:`${pctAtt}%`, borderRadius:0, background:'linear-gradient(90deg,#FACC15,#FDE68A)', boxShadow:'none', transition:'width 1.1s cubic-bezier(.22,1,.36,1)' }} />
      </div>
      <div style={{ ...hk, fontSize:12.5, color:'rgba(255,255,255,0.62)', marginTop:10 }}>
        Objectif {target} € · plus que {reste} €{passed>0?` · palier ${floor}€ ✅`:''}
      </div>
      {avis>0 && <div style={{ ...hk, fontSize:11.5, color:'rgba(203,251,69,0.85)', marginTop:4 }}>⭐ dont {avis} € de primes avis Google (déjà validé)</div>}
    </div>
    </div>
  </>;
}

// Hero de taux (% RDV pris, % convertis sur RDV) — repris du bilan V1.
function RateHero({ pct, cap, sub, warn }){
  return <div style={{ flex:1, background:T.surface, border:`1px solid ${warn?'#ef4444':T.line}`, borderRadius:18, padding:'16px 14px', textAlign:'center' }}>
    <div style={{ ...ax, fontSize:38, fontWeight:900, lineHeight:1, color:warn?'#f87171':T.text }}>{pct}<span style={{ fontSize:17, opacity:0.6 }}>%</span></div>
    <div style={{ ...hk, fontSize:12, color:T.muted, fontWeight:700, marginTop:3 }}>{cap}</div>
    <div style={{ height:6, borderRadius:999, background:T.line, margin:'10px 0 7px', overflow:'hidden' }}><i style={{ display:'block', height:'100%', borderRadius:999, width:Math.min(pct,100)+'%', background:warn?'#f87171':GREEN }} /></div>
    <div style={{ ...hk, fontSize:11, color:T.faint }}>{sub}</div>
  </div>;
}

// ════════ ACCUEIL ADMIN — cockpit de pilotage (patron, pas de cagnotte perso) ════════
const NUDGE_DEFAULT = "Let's go l'équipe 🔥\n\nQuand vous demandez un avis, voilà le type de message à envoyer (les gens savent quoi écrire et Google met les avis détaillés en avant) :\n\n« Tu écris ce que tu veux : les coachs, la salle ouverte 24h/24, l'ambiance, le parking, les cours collectifs… ce qui te plaît le plus chez nous. Merci beaucoup 💚 »\n\nChaque avis = prime pour l'équipe";
// ════════ CAISSE / ENCAISSEMENTS (admin only) ════════
// Saisie à 2 niveaux : catégorie -> articles. (Articles = libellés propres ; le prix vient du catalogue/historique.)
// Fallback si l'admin n'a pas encore configuré ses formules (sinon la saisie lit d.formules, éditable).
const CAISSE_CATS = [
  { cat:'Abo', icon:'🎟️', articles:[{a:'Wake up',p:66.9},{a:'Level up',p:96.9},{a:'1 mois',p:69.9},{a:'Regul',p:42.9},{a:'Basic',p:91.9},{a:'Off peak',p:81.9},{a:'Weekend',p:91.9},{a:'Change your body',p:259.8},{a:"Frais d'inscription",p:42}] },
  { cat:'Conso', icon:'🥤', articles:[{a:'Monster',p:3},{a:'Eau',p:1},{a:'Red Bull',p:2.5},{a:'Powerade',p:2},{a:'Barre',p:5}] },
  { cat:'Complément', icon:'💊', articles:[{a:'Whey',p:69.9},{a:'Iso',p:69.9},{a:'Créatine',p:24.9},{a:'Omega 3',p:19.9},{a:'Gainer',p:39.9},{a:'Shaker',p:9.9}] },
  { cat:'Séance', icon:'📅', articles:[{a:'Séance',p:10},{a:'4 séances',p:40},{a:'5 séances',p:50},{a:'10 séances',p:80}] },
  { cat:'Textile', icon:'👕', articles:[{a:'T-shirt',p:39.9},{a:'Casquette',p:19},{a:'Short',p:0},{a:'Legging',p:0}] },
];
const CAISSE_PAIEMENTS = ['CB','Espèces','Chèque','Virement'];
const fmtEur = (n) => { const v = Math.round((Number(n)||0)*100)/100; return v.toLocaleString('fr-FR',{ minimumFractionDigits: v%1?2:0, maximumFractionDigits:2 }) + ' €'; };

function CaisseSheet({ me, onClose }){
  const [period, setPeriod] = useState('mois');
  const [d, setD] = useState(null);
  const [add, setAdd] = useState(false);
  const [cloture, setCloture] = useState(false);   // clôture de caisse (compter les espèces)
  const [formOpen, setFormOpen] = useState(false); // gérer mes formules (catégories + articles + prix)
  const [q, setQ] = useState('');                  // recherche dans la liste
  const [selBar, setSelBar] = useState(null);   // barre du graphe touchée -> affiche son montant
  const [from, setFrom] = useState('');         // plage perso (date à date)
  const [to, setTo] = useState('');
  const load = () => {
    if (period==='perso' && (!from || !to)) { setD({ ok:true, needDates:true }); return; }
    setD(null);
    const q = period==='perso' ? `period=perso&from=${from}&to=${to}` : `period=${period}`;
    api('/api/caisse?'+q).then(setD).catch(()=>setD({ ok:false, err:true }));
  };
  useEffect(()=>{ load(); }, [period, from, to]);
  const PERIODS = [['jour','Jour'],['semaine','Semaine'],['mois','Mois'],['annee','Année'],['perso','Perso']];
  const maxBar = (d && d.series && d.series.length) ? Math.max(1, ...d.series.map(s=>s.value)) : 1;
  const Break = (title, arr, volt) => (arr && arr.length>0) ? (()=>{ const mx=Math.max(1,...arr.map(x=>x.v)); return (
    <div key={title} style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:16, padding:16, marginBottom:12 }}>
      <div style={{ ...ax, fontSize:15, fontWeight:800, marginBottom:14 }}>{title}</div>
      {arr.map(x=>(
        <div key={x.k} style={{ display:'flex', alignItems:'center', gap:10, marginBottom:10 }}>
          <div style={{ width:84, flexShrink:0, minWidth:0 }}><div style={{ ...hk, fontSize:13, fontWeight:600, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{x.k}</div>{x.n?<div style={{ ...hk, fontSize:10, color:T.faint }}>panier {Math.round(x.v/x.n)}€</div>:null}</div>
          <div style={{ flex:1, height:9, background:'rgba(255,255,255,.06)', borderRadius:999, overflow:'hidden' }}><div style={{ height:'100%', width:Math.round(x.v/mx*100)+'%', borderRadius:999, background: volt?`linear-gradient(90deg,${T.volt},#8fb52e)`:'linear-gradient(90deg,#22C55E,#86efac)' }} /></div>
          <div style={{ width:66, flexShrink:0, textAlign:'right', ...ax, fontSize:13.5, fontWeight:800 }}>{fmtEur(x.v)}</div>
        </div>
      ))}
    </div>); })() : null;
  return <Sheet onClose={onClose} tall>
    {sheetTitle('💶 Caisse')}
    <div style={{ display:'flex', gap:5, background:T.surface, border:`1px solid ${T.line}`, borderRadius:12, padding:4, marginBottom:14 }}>
      {PERIODS.map(([k,lb])=>(<button key={k} onClick={()=>{ setSelBar(null); if(k==='perso' && (!from||!to)){ const t=new Date(), p2=n=>String(n).padStart(2,'0'); setFrom(`${t.getFullYear()}-${p2(t.getMonth()+1)}-01`); setTo(`${t.getFullYear()}-${p2(t.getMonth()+1)}-${p2(t.getDate())}`); } setPeriod(k); }} style={{ flex:1, border:'none', cursor:'pointer', borderRadius:9, padding:'8px 1px', ...hk, fontSize:12, fontWeight:700, background:period===k?T.volt:'transparent', color:period===k?T.ink:T.muted }}>{lb}</button>))}
</div>
    {period==='perso' && <div style={{ display:'flex', gap:8, marginBottom:14 }}>
      {[['Du',from,setFrom],['Au',to,setTo]].map(([lb,val,set])=>(
        <div key={lb} style={{ flex:1 }}>
          <div style={{ ...mo, fontSize:9, letterSpacing:1, color:T.muted, textTransform:'uppercase', marginBottom:4 }}>{lb}</div>
          <input type="date" value={val} onChange={e=>{ setSelBar(null); set(e.target.value); }} style={{ width:'100%', background:T.surface, border:`1px solid ${T.line}`, borderRadius:10, padding:'9px 10px', color:T.text, ...hk, fontSize:13.5, outline:'none', boxSizing:'border-box', colorScheme:'dark' }} />
        </div>
      ))}
    </div>}
    {d && d.needsConnect && <div style={{ ...hk, fontSize:13, color:T.amber, background:T.surface, border:`1px solid ${T.line}`, borderRadius:14, padding:14, marginBottom:14, lineHeight:1.5 }}>⚠️ La base « Encaissements » n'est pas encore reliée à l'app. Dans Notion : ouvre la base, puis ••• → Connexions → ajoute « 4D Gym Phoning ». Puis <span onClick={()=>load()} style={{ textDecoration:'underline', cursor:'pointer' }}>recharge</span>.</div>}
    {!d && <div style={{ ...hk, fontSize:13, color:T.faint, padding:'22px 0', textAlign:'center' }}>Chargement…</div>}
    {d && d.needDates && <div style={{ ...hk, fontSize:13.5, color:T.muted, padding:'22px 0', textAlign:'center', lineHeight:1.5 }}>Choisis une date de <b style={{ color:T.text }}>début</b> et de <b style={{ color:T.text }}>fin</b> ci-dessus pour voir la caisse sur cette période.</div>}
    {d && d.ok && !d.needDates && <>
      <div style={{ borderRadius:20, padding:18, marginBottom:12, background:'radial-gradient(130% 130% at 0% 0%,#16331f,#0c170f 60%,#080b09)', border:'1px solid rgba(34,197,94,0.22)' }}>
        <div style={{ ...mo, fontSize:10, letterSpacing:1.5, color:'#7fd9a3', textTransform:'uppercase' }}>Encaissé · {d.periodLabel}{d.demo?' · démo':''}</div>
        <div style={{ ...ax, fontWeight:900, fontSize:38, letterSpacing:-1.5, lineHeight:1, margin:'7px 0 8px', background:'linear-gradient(180deg,#f0fff5,#86efac 70%,#4ade80)', WebkitBackgroundClip:'text', backgroundClip:'text', color:'transparent' }}>{fmtEur(d.total)}</div>
        {d.delta!=null && <span style={{ display:'inline-flex', alignItems:'center', gap:5, fontSize:13, fontWeight:700, color:d.delta>=0?GREEN:'#f87171', background:d.delta>=0?'rgba(34,197,94,.12)':'rgba(248,113,113,.12)', border:`1px solid ${d.delta>=0?'rgba(34,197,94,.3)':'rgba(248,113,113,.3)'}`, padding:'3px 9px', borderRadius:999 }}>{d.delta>=0?'▲ +':'▼ '}{d.delta}% vs mois précédent</span>}
        <div style={{ ...hk, fontSize:13, color:T.muted, marginTop:10 }}><b style={{ color:T.text }}>{d.count}</b> encaissement{d.count>1?'s':''} · panier moyen <b style={{ color:T.text }}>{fmtEur(d.panier)}</b></div>
      </div>
      {d.objectif!=null && <div style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:16, padding:'14px 16px', marginBottom:12 }}>
        <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', ...hk, fontSize:12.5, marginBottom:8 }}><span style={{ color:T.muted }}>Objectif du mois</span><span style={{ fontWeight:800 }}>{Math.round(d.total/d.objectif*100)}% <span style={{ color:T.faint, fontWeight:500 }}>de {fmtEur(d.objectif)}</span></span></div>
        <div style={{ height:10, background:'rgba(255,255,255,.06)', borderRadius:999, overflow:'hidden' }}><div style={{ height:'100%', width:Math.min(100,Math.round(d.total/d.objectif*100))+'%', borderRadius:999, background: d.total>=d.objectif?GREEN:`linear-gradient(90deg,${T.volt},#8fb52e)` }} /></div>
        {d.projection!=null && <div style={{ ...hk, fontSize:11.5, color:T.faint, marginTop:8 }}>Projection fin de mois à ce rythme : <b style={{ color: d.projection>=d.objectif?GREEN:T.text }}>{fmtEur(d.projection)}</b></div>}
      </div>}
      <div style={{ display:'flex', gap:8, marginBottom:14 }}>
        <button onClick={()=>setAdd(true)} style={{ flex:1, ...gbtn(), padding:'14px 0', fontSize:15 }}>+ Encaissement</button>
        <button onClick={()=>setCloture(true)} title="Clôture de caisse (espèces)" style={{ flex:'0 0 auto', ...obtn, padding:'14px 15px', fontSize:14, color:T.text }}>🧮 Clôture</button>
        <button onClick={()=>setFormOpen(true)} title="Gérer mes formules (articles + prix)" style={{ flex:'0 0 auto', ...obtn, padding:'14px 14px', fontSize:15, color:T.text }}>⚙️</button>
      </div>
      {d.series && d.series.length>0 && <div style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:16, padding:14, marginBottom:12 }}>
        <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', gap:8, marginBottom:12 }}>
          <span style={{ ...mo, fontSize:10, letterSpacing:1, color:T.faint, textTransform:'uppercase' }}>Par {d.unit||(period==='annee'?'mois':'jour')} · {d.periodLabel}</span>
          {selBar!=null && d.series[selBar]
            ? <span style={{ ...hk, fontSize:13.5, fontWeight:800, color:T.volt, whiteSpace:'nowrap' }}>{(d.series[selBar].date && d.series[selBar].date.length>=10) ? fmtFR(d.series[selBar].date) : d.series[selBar].label} · {fmtEur(d.series[selBar].value)}</span>
            : <span style={{ ...hk, fontSize:10.5, color:T.faint }}>touche une barre</span>}
        </div>
        <div style={{ display:'flex', alignItems:'flex-end', gap:3, height:90 }}>
          {d.series.map((s,i)=>{ const sel=selBar===i; const top=sel || (selBar==null && s.value===maxBar && maxBar>1);
            return <div key={i} onClick={()=>setSelBar(sel?null:i)} style={{ flex:1, height:'100%', display:'flex', alignItems:'flex-end', cursor:'pointer' }}>
              <div style={{ width:'100%', height:Math.max(3,Math.round(s.value/maxBar*88))+'px', borderRadius:'3px 3px 0 0', background: top?`linear-gradient(180deg,${T.volt},#8fb52e)`:'linear-gradient(180deg,#3a8f5a,#1c5a35)', transition:'background .15s' }} />
            </div>; })}
        </div>
      </div>}
      {d.bestWeekday && d.count>2 && <div style={{ ...hk, fontSize:12.5, color:T.muted, textAlign:'center', margin:'0 0 12px' }}>📈 Jour le plus rentable : <b style={{ color:T.text }}>{d.bestWeekday.jour}</b> ({fmtEur(d.bestWeekday.v)})</div>}
      {Break('Par catégorie', d.byCategorie, false)}
      {Break('Par moyen de paiement', d.byPaiement, false)}
      {Break('Top articles', d.byArticle, true)}
      {Break('Par vendeur', d.byVendeur, false)}
      {d.entries && d.entries.length>0 && <div style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:16, padding:'14px 16px', marginBottom:12 }}>
        <div style={{ ...ax, fontSize:15, fontWeight:800 }}>Encaissements · {d.periodLabel}</div>
        <div style={{ ...hk, fontSize:11.5, color:T.faint, margin:'2px 0 8px' }}>Touche une ligne pour la modifier ou la supprimer.</div>
        <input value={q} onChange={ev=>setQ(ev.target.value)} placeholder="Rechercher (client, article, vendeur…)" style={{ width:'100%', background:T.bg, border:`1px solid ${T.line}`, borderRadius:10, padding:'9px 12px', color:T.text, ...hk, fontSize:13, outline:'none', boxSizing:'border-box', marginBottom:10 }} />
        {(q ? d.entries.filter(e=>((e.article||'')+' '+(e.nom||'')+' '+(e.prenom||'')+' '+(e.vendeur||'')+' '+(e.paiement||'')).toLowerCase().includes(q.toLowerCase())) : d.entries).map((e,i)=>(
          <div key={e.id||i} onClick={()=>{ if(e.id) setAdd(e); }} style={{ display:'flex', alignItems:'center', gap:9, padding:'9px 0', borderTop:i?`1px solid ${T.line}`:'none', cursor:e.id?'pointer':'default' }}>
            <div style={{ flex:1, minWidth:0 }}>
              <div style={{ ...hk, fontSize:13.5, fontWeight:700, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{e.article||'—'}{(e.prenom||e.nom)?<span style={{ color:T.muted, fontWeight:500 }}> · {[e.prenom,e.nom].filter(Boolean).join(' ')}</span>:null}</div>
              <div style={{ ...hk, fontSize:11, color:T.faint }}>{e.vendeur||''}{e.source?' · '+e.source:''}{period!=='jour'&&e.date?' · '+fmtFR(e.date):''}</div>
            </div>
            <span style={{ ...mo, fontSize:9, fontWeight:700, color:T.muted, border:`1px solid ${T.line}`, borderRadius:999, padding:'2px 7px', whiteSpace:'nowrap' }}>{e.paiement||''}</span>
            <span style={{ ...ax, fontWeight:800, fontSize:14, color:GREEN, whiteSpace:'nowrap' }}>{fmtEur(e.montant)}</span>
            <span style={{ ...ax, fontSize:15, color:T.faint }}>›</span>
          </div>
        ))}
      </div>}
      {d.cumulYear!=null && <div style={{ ...hk, fontSize:12.5, color:T.muted, textAlign:'center', margin:'4px 0 8px' }}>Cumul {new Date().getFullYear()} : <b style={{ color:GREEN }}>{fmtEur(d.cumulYear)}</b> <span style={{ color:T.faint }}>(onglet Année)</span></div>}
    </>}
    {d && !d.ok && !d.needsConnect && <div style={{ ...hk, fontSize:13, color:'#f87171', padding:'16px 0', textAlign:'center' }}>Erreur de chargement{d.reason?' : '+d.reason:''}. <span onClick={()=>load()} style={{ textDecoration:'underline', cursor:'pointer' }}>Réessayer</span></div>}
    <button onClick={onClose} style={{ width:'100%', marginTop:6, ...obtn, color:T.muted }}>Fermer</button>
    {add && <CaisseAdd me={me} entry={add===true?null:add} formules={d&&d.formules} onClose={()=>setAdd(false)} onDone={()=>{ setAdd(false); load(); }} />}
    {cloture && <CaisseCloture onClose={()=>setCloture(false)} />}
    {formOpen && <FormulesManager onClose={()=>setFormOpen(false)} onSaved={()=>{ setFormOpen(false); load(); }} />}
  </Sheet>;
}

function CaisseAdd({ me, entry, formules, onClose, onDone }){
  const cats = (formules && formules.length) ? formules : CAISSE_CATS;
  const catOf = (a) => (cats.find(c=>(c.articles||[]).some(x=>x.a===a))||{}).cat || 'Autre';
  const editing = !!(entry && entry.id);
  const known = entry ? cats.some(c=>(c.articles||[]).some(x=>x.a===entry.article)) : false;
  const [selCat, setSelCat] = useState(entry ? catOf(entry.article) : null);
  const [montant, setMontant] = useState(entry ? String(entry.montant ?? '') : '');
  const [article, setArticle] = useState(entry ? (known ? entry.article : 'Autre') : '');
  const [artFree, setArtFree] = useState(entry && !known ? (entry.article||'') : '');
  const [paiement, setPaiement] = useState((entry && entry.paiement) || 'CB');
  const [nom, setNom] = useState(entry ? [entry.prenom, entry.nom].filter(Boolean).join(' ') : '');
  const [source, setSource] = useState((entry && entry.source) || '');
  const [busy, setBusy] = useState(false);
  const finalArticle = selCat==='Autre' ? artFree.trim() : article;
  const valid = (Number(String(montant).replace(',','.'))>0) && !!finalArticle;
  const save = async () => {
    if(!valid || busy) return; setBusy(true);
    try {
      const parts = nom.trim().split(' ').filter(Boolean);
      const prenom = parts.shift() || '';
      const payload = { montant, article: finalArticle, paiement, source, prenom, nom: parts.join(' ') };
      const r = editing
        ? await api('/api/caisse', { method:'PATCH', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ id: entry.id, ...payload }) })
        : await api('/api/caisse', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ ...payload, vendeur: me }) });
      if(r && r.ok) onDone();
      else alert('Échec : '+((r&&r.reason)||'?'));
    } catch(e){ alert('Échec : '+e.message); } finally { setBusy(false); }
  };
  const del = async () => {
    if(!editing || busy) return;
    if(!confirm('Supprimer cet encaissement ?\nIl part à la corbeille Notion (récupérable).')) return;
    setBusy(true);
    try {
      const r = await api('/api/caisse?id='+encodeURIComponent(entry.id), { method:'DELETE' });
      if(r && r.ok) onDone();
      else alert('Échec : '+((r&&r.reason)||'?'));
    } catch(e){ alert('Échec : '+e.message); } finally { setBusy(false); }
  };
  return <Sheet onClose={()=>{ if(!busy) onClose(); }} tall>
    {sheetTitle(editing ? '✏️ Modifier l\'encaissement' : '+ Encaissement')}
    <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.muted, textTransform:'uppercase', marginBottom:6 }}>Montant</div>
    <div style={{ display:'flex', alignItems:'center', background:T.surface, border:`1px solid ${T.volt}`, borderRadius:12, padding:'6px 14px', marginBottom:14 }}>
      <input value={montant} onChange={e=>setMontant(e.target.value)} inputMode="decimal" placeholder="0" style={{ flex:1, background:'none', border:'none', outline:'none', color:T.text, ...ax, fontWeight:800, fontSize:30, width:'100%' }} />
      <span style={{ ...ax, fontWeight:800, fontSize:24, color:T.muted }}>€</span>
    </div>
    <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.muted, textTransform:'uppercase', marginBottom:6 }}>Catégorie</div>
    <div style={{ display:'flex', flexWrap:'wrap', gap:6, marginBottom:10 }}>
      {cats.concat([{cat:'Autre',icon:'✏️'}]).map(c=>{ const on=selCat===c.cat; return <button key={c.cat} onClick={()=>{ setSelCat(c.cat); if(c.cat!=='Autre') setArticle(''); }} style={{ ...hk, fontSize:12.5, fontWeight:700, padding:'8px 13px', borderRadius:999, cursor:'pointer', background:on?T.volt:T.surface, color:on?T.ink:T.muted, border:`1px solid ${on?T.volt:T.line}` }}>{c.icon} {c.cat}</button>; })}
    </div>
    {selCat && selCat!=='Autre' && <>
      <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.muted, textTransform:'uppercase', marginBottom:6 }}>Article</div>
      <div style={{ display:'flex', flexWrap:'wrap', gap:6, marginBottom:14 }}>
        {((cats.find(c=>c.cat===selCat)||{}).articles||[]).map(it=>{ const on=article===it.a; return <button key={it.a} onClick={()=>{ setArticle(it.a); if(it.p) setMontant(String(it.p)); }} style={{ ...hk, fontSize:12.5, fontWeight:600, padding:'7px 12px', borderRadius:999, cursor:'pointer', background:on?T.volt:T.surface, color:on?T.ink:T.muted, border:`1px solid ${on?T.volt:T.line}` }}>{it.a}{it.p?<span style={{ opacity:.6, fontWeight:500 }}> · {it.p}€</span>:''}</button>; })}
      </div>
    </>}
    {selCat==='Autre' && <input value={artFree} onChange={e=>setArtFree(e.target.value)} placeholder="Quel article / formule ?" style={{ width:'100%', background:T.surface, border:`1px solid ${T.line}`, borderRadius:12, padding:'11px 12px', color:T.text, ...hk, fontSize:14, outline:'none', boxSizing:'border-box', marginBottom:14 }} />}
    <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.muted, textTransform:'uppercase', marginBottom:6 }}>Moyen de paiement</div>
    <div style={{ display:'flex', gap:6, background:T.surface, border:`1px solid ${T.line}`, borderRadius:12, padding:4, marginBottom:14 }}>
      {CAISSE_PAIEMENTS.map(p=>(<button key={p} onClick={()=>setPaiement(p)} style={{ flex:1, border:'none', cursor:'pointer', borderRadius:9, padding:'9px 2px', ...hk, fontSize:12.5, fontWeight:700, background:paiement===p?T.volt:'transparent', color:paiement===p?T.ink:T.muted }}>{p}</button>))}
    </div>
    <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.muted, textTransform:'uppercase', marginBottom:6 }}>Client (optionnel)</div>
    <input value={nom} onChange={e=>setNom(e.target.value)} placeholder="Prénom Nom" style={{ width:'100%', background:T.surface, border:`1px solid ${T.line}`, borderRadius:12, padding:'11px 12px', color:T.text, ...hk, fontSize:14, outline:'none', boxSizing:'border-box', marginBottom:14 }} />
    <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.muted, textTransform:'uppercase', marginBottom:6 }}>Source (optionnel)</div>
    <div style={{ display:'flex', flexWrap:'wrap', gap:6, marginBottom:18 }}>
      {['parrainage','reabo','ads','internet','passage','site'].map(s=>{ const on=source===s; return <button key={s} onClick={()=>setSource(on?'':s)} style={{ ...hk, fontSize:12, fontWeight:600, padding:'6px 11px', borderRadius:999, cursor:'pointer', background:on?T.amber:T.surface, color:on?'#1a1205':T.muted, border:`1px solid ${on?T.amber:T.line}` }}>{s}</button>; })}
    </div>
    <button onClick={save} disabled={!valid||busy} style={{ width:'100%', ...gbtn(), padding:'15px 0', fontSize:15.5, opacity:(!valid||busy)?0.55:1 }}>{busy?'…':editing?'Enregistrer les modifications':'Enregistrer dans la caisse'}</button>
    {editing && <button onClick={del} disabled={busy} style={{ width:'100%', marginTop:10, ...obtn, color:'#f87171', borderColor:'rgba(248,113,113,0.4)', opacity:busy?0.5:1 }}>🗑 Supprimer cet encaissement</button>}
  </Sheet>;
}

// Clôture de caisse : compter les espèces du soir vs ce qui devrait être en caisse (ventes espèces du jour + fond).
function CaisseCloture({ onClose }){
  const [d, setD] = useState(null);
  const [fond, setFond] = useState('');
  const [compte, setCompte] = useState('');
  useEffect(()=>{ api('/api/caisse?period=jour').then(setD).catch(()=>setD({ ok:false })); }, []);
  const especes = (d && d.ok && (d.byPaiement||[]).find(x=>x.k==='Espèces') || {}).v || 0;
  const fondN = Number(String(fond).replace(',','.'))||0;
  const compteN = Number(String(compte).replace(',','.'))||0;
  const attendu = Math.round((especes + fondN)*100)/100;
  const ecart = compte!=='' ? Math.round((compteN - attendu)*100)/100 : null;
  const num = { width:74, textAlign:'right', background:T.bg, border:`1px solid ${T.line}`, borderRadius:8, padding:'5px 8px', color:T.text, ...ax, fontSize:15, fontWeight:700, outline:'none' };
  return <Sheet onClose={onClose} tall>
    {sheetTitle('🧮 Clôture de caisse')}
    <div style={{ ...hk, fontSize:13, color:T.muted, marginBottom:14, lineHeight:1.5 }}>Compte tes espèces en caisse ce soir : l'app calcule l'écart avec ce qui devrait y être (ventes espèces du jour + fond de caisse).</div>
    {!d && <div style={{ ...hk, fontSize:13, color:T.faint, padding:'14px 0', textAlign:'center' }}>Chargement du jour…</div>}
    {d && <>
      <div style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:14, padding:'14px 16px', marginBottom:14 }}>
        <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', ...hk, fontSize:14, marginBottom:9 }}><span style={{ color:T.muted }}>Ventes espèces du jour</span><b style={{ color:GREEN }}>{fmtEur(especes)}</b></div>
        <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', ...hk, fontSize:14 }}><span style={{ color:T.muted }}>+ Fond de caisse</span><span style={{ display:'flex', alignItems:'center', gap:5 }}><input value={fond} onChange={e=>setFond(e.target.value)} inputMode="decimal" placeholder="0" style={num} /><b>€</b></span></div>
        <div style={{ borderTop:`1px solid ${T.line}`, marginTop:11, paddingTop:11, display:'flex', justifyContent:'space-between', ...hk, fontSize:14.5, fontWeight:800 }}><span>Attendu en caisse</span><span>{fmtEur(attendu)}</span></div>
      </div>
      <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.muted, textTransform:'uppercase', marginBottom:6 }}>Compté (ce que tu as physiquement)</div>
      <div style={{ display:'flex', alignItems:'center', background:T.surface, border:`1px solid ${T.volt}`, borderRadius:12, padding:'6px 14px', marginBottom:16 }}>
        <input value={compte} onChange={e=>setCompte(e.target.value)} inputMode="decimal" placeholder="0" style={{ flex:1, background:'none', border:'none', outline:'none', color:T.text, ...ax, fontWeight:800, fontSize:30, width:'100%' }} /><span style={{ ...ax, fontWeight:800, fontSize:24, color:T.muted }}>€</span>
      </div>
      {ecart!=null && <div style={{ borderRadius:16, padding:'18px', textAlign:'center', background: ecart===0?'rgba(34,197,94,.12)':'rgba(248,113,113,.10)', border:`1px solid ${ecart===0?'rgba(34,197,94,.4)':'rgba(248,113,113,.35)'}` }}>
        <div style={{ ...mo, fontSize:10, letterSpacing:1.5, color:T.muted, textTransform:'uppercase', marginBottom:6 }}>{ecart===0?'Caisse juste 🎯':ecart>0?'Excédent':'Manquant'}</div>
        <div style={{ ...ax, fontWeight:900, fontSize:34, color: ecart===0?GREEN:'#f87171' }}>{ecart===0?'✅':(ecart>0?'+':'')+fmtEur(ecart)}</div>
        {ecart!==0 && <div style={{ ...hk, fontSize:12.5, color:T.muted, marginTop:6 }}>Compté {fmtEur(compteN)} · attendu {fmtEur(attendu)}</div>}
      </div>}
    </>}
    <button onClick={onClose} style={{ width:'100%', marginTop:14, ...obtn, color:T.muted }}>Fermer</button>
  </Sheet>;
}

// Gérer ses formules : catégories + articles + prix (édités par l'admin, stockés en Notion).
function FormulesManager({ onClose, onSaved }){
  const [cats, setCats] = useState(null);
  const [busy, setBusy] = useState(false);
  useEffect(()=>{ api('/api/caisse/formules').then(r=>setCats((r&&r.formules)||[])).catch(()=>setCats([])); }, []);
  const upd = (fn) => setCats(prev => { const n = JSON.parse(JSON.stringify(prev||[])); fn(n); return n; });
  const save = async () => {
    setBusy(true);
    try {
      const clean = (cats||[]).filter(c=>c.cat&&String(c.cat).trim()).map(c=>({ cat:String(c.cat).trim(), icon:c.icon||'', articles:(c.articles||[]).filter(x=>x.a&&String(x.a).trim()).map(x=>({ a:String(x.a).trim(), p:Number(String(x.p).replace(',','.'))||0 })) }));
      const r = await api('/api/caisse/formules', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ formules: clean }) });
      if(r && r.ok) onSaved(); else alert('Échec : '+((r&&r.reason)||'?'));
    } catch(e){ alert('Échec : '+(e.message||e)); } finally { setBusy(false); }
  };
  const inp = { background:T.bg, border:`1px solid ${T.line}`, borderRadius:8, color:T.text, outline:'none' };
  return <Sheet onClose={onClose} tall>
    {sheetTitle('⚙️ Mes formules')}
    <div style={{ ...hk, fontSize:13, color:T.muted, marginBottom:14, lineHeight:1.5 }}>Tes catégories et articles avec leur prix. Ce sont eux qui s'affichent à la saisie. Modifie, puis Enregistre.</div>
    {!cats && <div style={{ ...hk, fontSize:13, color:T.faint, padding:'16px 0', textAlign:'center' }}>Chargement…</div>}
    {cats && <>
      {cats.map((c,ci)=>(
        <div key={ci} style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:14, padding:'12px 14px', marginBottom:12 }}>
          <div style={{ display:'flex', alignItems:'center', gap:7, marginBottom:10 }}>
            <input value={c.icon||''} onChange={e=>upd(n=>{ n[ci].icon=e.target.value; })} placeholder="🏷️" style={{ ...inp, width:44, textAlign:'center', padding:'7px 0', fontSize:16 }} />
            <input value={c.cat} onChange={e=>upd(n=>{ n[ci].cat=e.target.value; })} placeholder="Nom catégorie" style={{ ...inp, flex:1, padding:'8px 10px', ...hk, fontSize:14, fontWeight:700 }} />
            <button onClick={()=>upd(n=>{ n.splice(ci,1); })} title="Supprimer la catégorie" style={{ ...obtn, padding:'8px 10px', fontSize:13, color:'#f87171', borderColor:'rgba(248,113,113,0.4)' }}>🗑</button>
          </div>
          {(c.articles||[]).map((x,xi)=>(
            <div key={xi} style={{ display:'flex', alignItems:'center', gap:6, marginBottom:6 }}>
              <input value={x.a} onChange={e=>upd(n=>{ n[ci].articles[xi].a=e.target.value; })} placeholder="Article" style={{ ...inp, flex:1, padding:'7px 10px', ...hk, fontSize:13.5 }} />
              <input value={x.p} onChange={e=>upd(n=>{ n[ci].articles[xi].p=e.target.value; })} inputMode="decimal" placeholder="0" style={{ ...inp, width:62, textAlign:'right', padding:'7px 8px', ...ax, fontWeight:700, fontSize:14 }} />
              <span style={{ ...hk, fontSize:13, color:T.muted }}>€</span>
              <button onClick={()=>upd(n=>{ n[ci].articles.splice(xi,1); })} title="Retirer" style={{ ...obtn, padding:'6px 9px', fontSize:12, color:T.faint }}>✕</button>
            </div>
          ))}
          <button onClick={()=>upd(n=>{ n[ci].articles=n[ci].articles||[]; n[ci].articles.push({a:'',p:''}); })} style={{ ...hk, fontSize:12.5, color:T.volt, background:'transparent', border:'none', cursor:'pointer', padding:'6px 0', fontWeight:700 }}>+ Article</button>
        </div>
      ))}
      <button onClick={()=>upd(n=>{ n.push({ cat:'', icon:'🏷️', articles:[{a:'',p:''}] }); })} style={{ width:'100%', ...obtn, padding:'11px 0', fontSize:13.5, color:T.text, marginBottom:14 }}>+ Catégorie</button>
      <button onClick={save} disabled={busy} style={{ width:'100%', ...gbtn(), padding:'14px 0', fontSize:15, opacity:busy?0.6:1 }}>{busy?'Enregistrement…':'Enregistrer mes formules'}</button>
    </>}
    <button onClick={onClose} style={{ width:'100%', marginTop:10, ...obtn, color:T.muted }}>Fermer</button>
  </Sheet>;
}

function ScreenAdminDashboard({ d, go, H }) {
  const ts = d.teamStats || null;
  const [caisseOpen, setCaisseOpen] = useState(false);   // caisse / encaissements (admin only)
  const [relSheet, setRelSheet] = useState(false);   // composeur "message à l'équipe" (+ raccourci relance avis)
  const [relMsg, setRelMsg] = useState('');
  const [relBusy, setRelBusy] = useState(false);
  const [relTitle, setRelTitle] = useState('Message à l\'équipe');
  const [relTargets, setRelTargets] = useState([]);  // destinataires du message
  const sendRelance = async () => {
    if (!relMsg.trim() || !relTargets.length) return;
    setRelBusy(true);
    try {
      const r = await api('/api/nudgeavis', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ names: relTargets, msg: relMsg }) });
      if (r && r.ok) { setRelSheet(false); alert('✅ Message envoyé à '+relTargets.join(', ')+'.\nÇa s\'affiche sur leur accueil (et en notif s\'ils ont activé les notifications).'); }
      else alert('Échec : '+((r&&(r.error||r.reason))||'?'));
    } catch(e){ alert('Échec : '+e.message); } finally { setRelBusy(false); }
  };
  const lb = [...(d.leaderboard||[])].sort((a,b)=>(b.total||0)-(a.total||0));
  const sellers = (d.team||[]).filter(n => n!==ADMIN && n!=='Dev'); // l'équipe (sans le patron ni le technique)
  const toggleTarget = (n) => setRelTargets(t => t.includes(n) ? t.filter(x=>x!==n) : [...t, n]);
  const openMessage = () => { setRelTitle('Message à l\'équipe'); setRelMsg(''); setRelTargets(sellers); setRelSheet(true); };           // composeur libre, toute l'équipe par défaut
  const openAvisRelance = () => { setRelTitle('Relancer pour les avis'); setRelMsg(NUDGE_DEFAULT); setRelTargets((ts&&ts.avisInactive)||[]); setRelSheet(true); }; // raccourci pré-rempli
  const today = todayISO();
  const all = d.allLeads||[];
  // Nouveaux adhérents à suivre aujourd'hui, toute l'équipe (vue pilotage du décrochage).
  const [suiviDue, setSuiviDue] = useState(0);
  useEffect(()=>{ api('/api/suivis?who='+encodeURIComponent(d.me)+'&all=1').then(r=>setSuiviDue(r.due||0)).catch(()=>{}); },[d.me]);
  // Pouls équipe : à traiter aujourd'hui (toute l'équipe) = nouveaux jamais appelés + relances dues (aujourd'hui ou en retard). + séances du jour.
  const aAppelerList = all.filter(l=> !l.converted && l.statutRaw!=='MORT' && l.statutRaw!=='NUM PAS BON' &&
    ((l.urgent && !l.seanceEssai) || (l.relanceLe && l.relanceLe.slice(0,10) <= today)));
  const seancesJourList = (d.seances||[]).filter(l=> l.seanceEssai && l.seanceEssai.slice(0,10)===today);
  const seancesAVenir = (d.seances||[]).filter(l=> l.seanceEssai && l.seanceEssai.slice(0,10) > today && !l.converted);
  const aRattraperList = (d.seances||[]).filter(l=> l.seanceEssai && l.seanceEssai.slice(0,10) < today && !l.converted);
  const aRattraper = aRattraperList.length;
  // Suivi : séances passées PAS notées le jour J, regroupées par vendeur (qui oublie de noter).
  const rattraperBySeller = (()=>{ const m={}; aRattraperList.forEach(l=>{ const k=l.assigne||'Non attribué'; m[k]=(m[k]||0)+1; }); return Object.entries(m).sort((a,b)=>b[1]-a[1]); })();
  const taux = ts && ts.closingRate != null ? ts.closingRate : 0;
  const kpi = (label, val, color) => (
    <div style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:16, padding:'14px' }}>
      <div style={{ ...ax, fontSize:24, fontWeight:900, color }}>{val}</div>
      <div style={{ ...hk, fontSize:11.5, color:T.muted, marginTop:3 }}>{label}</div>
    </div>
  );
  return <Screen active="home" go={go}>
    <div style={{ padding:'0 20px 24px' }}>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:12, marginBottom:20 }}>
        <div style={{ minWidth:0 }}><div style={{ ...hk, fontSize:14, color:T.onBgMuted }}>Pilotage</div><div style={{ ...ax, fontSize:26, fontWeight:800, letterSpacing:-0.5, color:T.onBg, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{BRAND} · {d.me}</div></div>
        <div style={{ display:'flex', alignItems:'center', gap:10, flexShrink:0 }}>
          <button onClick={openMessage} title="Écrire à l'équipe" style={{ ...btn(T.amber,'#1a1205'), padding:'9px 13px', fontSize:12.5, fontWeight:800, borderRadius:999, whiteSpace:'nowrap' }}>📣 Push une notif</button>
          <Avatar name={d.me} size={44} ring pulse />
        </div>
      </div>

      <div onClick={()=>setCaisseOpen(true)} style={{ display:'flex', alignItems:'center', gap:12, background:'linear-gradient(135deg, rgba(34,197,94,0.16), rgba(34,197,94,0.05))', border:'1px solid rgba(34,197,94,0.4)', borderRadius:16, padding:'14px 16px', marginBottom:18, cursor:'pointer' }}>
        <div style={{ fontSize:24 }}>💶</div>
        <div style={{ flex:1, minWidth:0 }}><div style={{ ...hk, fontSize:15, fontWeight:800, color:T.onBg }}>Caisse · encaissements</div><div style={{ ...hk, fontSize:11.5, color:T.onBgMuted }}>Saisie + dashboard du jour (hors abos auto)</div></div>
        <div style={{ ...ax, fontSize:22, color:GREEN, fontWeight:800 }}>›</div>
      </div>

      <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.onBgMuted, margin:'0 0 10px' }}>DEPUIS LE LANCEMENT · ÉQUIPE</div>
      {!ts && <div onClick={()=>go('home')} style={{ ...hk, fontSize:13, color:T.faint, marginBottom:16, cursor:'pointer' }}>Chargement… (touche pour réessayer)</div>}
      {ts && ts.ok && <>
        {/* Conversions = le chiffre qui compte (nouveaux membres). En héros, le reste en contexte. */}
        <div style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:16, padding:'16px 18px', marginBottom:10, display:'flex', alignItems:'center', justifyContent:'space-between' }}>
          <div>
            <div style={{ ...ax, fontSize:40, fontWeight:900, lineHeight:1, color:GREEN }}>{ts.totalConvs}</div>
            <div style={{ ...hk, fontSize:12, color:T.muted, marginTop:4 }}>Conversions depuis le lancement</div>
          </div>
          <div style={{ textAlign:'right' }}>
            <div style={{ ...ax, fontSize:26, fontWeight:900, color: taux>=30?GREEN:T.text }}>{taux}%</div>
            <div style={{ ...hk, fontSize:11.5, color:T.muted, marginTop:2 }}>taux de conversion</div>
          </div>
        </div>
        <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10, marginBottom:10 }}>
          {kpi('Leads entrants', ts.totalLeads, T.text)}
          {kpi('RDV pris (actifs)', ts.totalRdv, T.volt)}
        </div>
        {/* Primes : dissociées (CA conversions + prime avis) + total à payer en bas */}
        <div style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:16, padding:'14px 16px', marginBottom:10 }}>
          <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.onBgMuted, marginBottom:10 }}>PRIMES À VERSER · CE MOIS</div>
          <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', padding:'8px 0', borderBottom:`1px solid ${T.line}` }}>
            <span style={{ ...hk, fontSize:14, color:T.text }}>Prime CA <span style={{ color:T.faint, fontSize:11.5 }}>· conversions</span></span>
            <span style={{ ...ax, fontSize:18, fontWeight:800, color:T.amber }}>{fmt(ts.totalPrimes)}</span>
          </div>
          <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', padding:'8px 0', borderBottom:`1px solid ${T.line}` }}>
            <span style={{ ...hk, fontSize:14, color:T.text }}>Prime avis <span style={{ color:T.faint, fontSize:11.5 }}>· équipe</span></span>
            <span style={{ ...ax, fontSize:18, fontWeight:800, color:T.amber }}>{fmt(ts.totalAvisPrime||0)}</span>
          </div>
          <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', padding:'10px 0 2px' }}>
            <span style={{ ...ax, fontSize:14, fontWeight:900, color:T.text, textTransform:'uppercase', letterSpacing:.5 }}>Total à payer</span>
            <span style={{ ...ax, fontSize:22, fontWeight:900, color:GREEN }}>{fmt(ts.totalAPayer!=null ? ts.totalAPayer : ((ts.totalPrimes||0)+(ts.totalAvisPrime||0)))}</span>
          </div>
        </div>
        {ts.teamTraites!=null && <div style={{ ...hk, fontSize:11.5, color:T.muted, textAlign:'center', marginBottom: ts.totalMineurs>0?10:18 }}>Conversion équipe · {ts.teamConvertis}/{ts.teamTraites} traités</div>}
        {ts.totalMineurs>0 && <div style={{ background:'rgba(239,68,68,0.07)', border:'1px solid rgba(239,68,68,0.22)', borderRadius:16, padding:'13px 16px', marginBottom:18, display:'flex', alignItems:'center', gap:12 }}>
          <span style={{ fontSize:22 }}>💸</span>
          <div style={{ flex:1, minWidth:0 }}><div style={{ ...ax, fontSize:18, fontWeight:900, color:'#f87171' }}>≈ {fmt(ts.totalMineurs*PERTE_MINEUR_AN)} de manque à gagner</div><div style={{ ...hk, fontSize:11.5, color:T.muted }}>{ts.totalMineurs} mineur{ts.totalMineurs>1?'s':''} refusé{ts.totalMineurs>1?'s':''} cette année · {(CFG.minFee4w||29)}€/4 sem. sur 1 an chacun</div></div>
        </div>}
      </>}

      <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.onBgMuted, margin:'0 0 10px' }}>À TRAITER · ÉQUIPE</div>
      <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:10, marginBottom:18 }}>
        <div onClick={()=> aAppelerList.length && H.openCat('aappeler-eq','📞 À appeler / relancer · équipe', aAppelerList)} style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:14, padding:'13px 11px', cursor: aAppelerList.length?'pointer':'default' }}>
          <div style={{ ...ax, fontSize:23, fontWeight:900, color: aAppelerList.length?'#f87171':T.text }}>{aAppelerList.length}</div>
          <div style={{ ...hk, fontSize:10.5, color:T.muted, marginTop:3 }}>📞 À appeler{aAppelerList.length?' ›':''}</div>
        </div>
        <div onClick={()=>go('sessions')} style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:14, padding:'13px 11px', cursor:'pointer' }}>
          <div style={{ ...ax, fontSize:23, fontWeight:900, color:T.volt }}>{seancesJourList.length}</div>
          <div style={{ ...hk, fontSize:10.5, color:T.muted, marginTop:3 }}>📅 Séances aujourd'hui ›</div>
        </div>
        <div onClick={()=>go('sessions')} style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:14, padding:'13px 11px', cursor:'pointer' }}>
          <div style={{ ...ax, fontSize:23, fontWeight:900, color:'#a78bfa' }}>{seancesAVenir.length}</div>
          <div style={{ ...hk, fontSize:10.5, color:T.muted, marginTop:3 }}>📅 Séances à venir ›</div>
        </div>
      </div>

      {ts && ts.totalOrphelins > 0 && <div onClick={()=>H.orphans && H.orphans()} style={{ background:'rgba(245,158,11,0.12)', border:'1px solid rgba(245,158,11,0.35)', borderRadius:14, padding:'12px 16px', marginBottom:12, display:'flex', alignItems:'center', gap:12, cursor:'pointer' }}>
        <span style={{ fontSize:22 }}>🧩</span>
        <div style={{ flex:1, minWidth:0 }}><div style={{ ...ax, fontSize:16, fontWeight:800, color:T.amber }}>{ts.totalOrphelins} lead{ts.totalOrphelins>1?'s':''} que personne n'appelle</div><div style={{ ...hk, fontSize:11.5, color:T.muted }}>Aucun vendeur dessus · touche pour répartir</div></div>
        <span style={{ ...hk, fontSize:18, color:T.amber }}>›</span>
      </div>}
      {aRattraper > 0 && <div onClick={()=>go('sessions')} style={{ background:'rgba(239,68,68,0.10)', border:'1px solid rgba(239,68,68,0.32)', borderRadius:14, padding:'12px 16px', marginBottom:18, display:'flex', alignItems:'center', gap:12, cursor:'pointer' }}>
        <span style={{ fontSize:22 }}>🔴</span>
        <div style={{ flex:1, minWidth:0 }}><div style={{ ...ax, fontSize:16, fontWeight:800, color:'#ff6b6b' }}>{aRattraper} séance{aRattraper>1?'s':''} pas notée{aRattraper>1?'s':''} le jour J</div><div style={{ ...hk, fontSize:11.5, color:T.muted }}>{rattraperBySeller.map(([n,c])=>`${n} ${c}`).join(' · ')} · touche pour relancer</div></div>
        <span style={{ ...hk, fontSize:18, color:'#ff6b6b' }}>›</span>
      </div>}
      {ts && ts.totalAnciens > 0 && <div onClick={()=>H.openCat('anciens','🗂️ Leads anciens (+30j) à vérifier')} style={{ background:'rgba(96,165,250,0.10)', border:'1px solid rgba(96,165,250,0.32)', borderRadius:14, padding:'12px 16px', marginBottom:18, display:'flex', alignItems:'center', gap:12, cursor:'pointer' }}>
        <span style={{ fontSize:22 }}>🗂️</span>
        <div style={{ flex:1, minWidth:0 }}><div style={{ ...ax, fontSize:16, fontWeight:800, color:'#7dd3fc' }}>{ts.totalAnciens} lead{ts.totalAnciens>1?'s':''} ancien{ts.totalAnciens>1?'s':''} à vérifier</div><div style={{ ...hk, fontSize:11.5, color:T.muted }}>Entrés il y a +30j, pas convertis · à passer sur Deciplus</div></div>
        <span style={{ ...hk, fontSize:18, color:'#7dd3fc' }}>›</span>
      </div>}

      {suiviDue > 0 && <div onClick={()=>H.suivi()} style={{ background:'rgba(34,197,94,0.10)', border:'1px solid rgba(34,197,94,0.34)', borderRadius:14, padding:'12px 16px', marginBottom:18, display:'flex', alignItems:'center', gap:12, cursor:'pointer' }}>
        <span style={{ fontSize:22 }}>🌱</span>
        <div style={{ flex:1, minWidth:0 }}><div style={{ ...ax, fontSize:16, fontWeight:800, color:GREEN }}>{suiviDue} nouvel{suiviDue>1?'s':''} adhérent{suiviDue>1?'s':''} à suivre</div><div style={{ ...hk, fontSize:11.5, color:T.muted }}>Rétention du 1er mois · touche pour voir qui relancer sur WhatsApp</div></div>
        <span style={{ ...hk, fontSize:18, color:GREEN }}>›</span>
      </div>}

      {ts && ts.avisInactive && ts.avisInactive.length > 0 && <div onClick={openAvisRelance} style={{ background:'rgba(245,158,11,0.10)', border:'1px solid rgba(245,158,11,0.32)', borderRadius:14, padding:'12px 16px', marginBottom:18, display:'flex', alignItems:'center', gap:12, cursor:'pointer' }}>
        <span style={{ fontSize:22 }}>⭐</span>
        <div style={{ flex:1, minWidth:0 }}><div style={{ ...ax, fontSize:16, fontWeight:800, color:T.amber }}>{ts.avisInactive.join(', ')} : 0 avis cette semaine</div><div style={{ ...hk, fontSize:11.5, color:T.muted }}>Touche pour les relancer (push sur leur session)</div></div>
        <span style={{ ...hk, fontSize:18, color:T.amber }}>›</span>
      </div>}

      <div style={{ display:'flex', alignItems:'baseline', justifyContent:'space-between', margin:'0 0 10px' }}>
        <span style={{ ...mo, fontSize:10, letterSpacing:1, color:T.onBgMuted }}>PERFORMANCE ÉQUIPE</span>
        <span onClick={()=>go('gains')} style={{ ...hk, fontSize:12.5, color:T.volt, fontWeight:700, cursor:'pointer' }}>Tout voir ›</span>
      </div>
      <div style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:16, padding:'4px 14px', marginBottom:18 }}>
        {lb.length===0 && <div style={{ ...hk, fontSize:13, color:T.faint, padding:'12px 0' }}>Chargement…</div>}
        {lb.map((m,i)=>(
          <div key={m.name} onClick={()=>H.setViewAs && H.setViewAs(m.name)} style={{ display:'flex', alignItems:'center', gap:12, padding:'11px 0', borderBottom: i<lb.length-1?`1px solid ${T.line}`:'none', cursor:'pointer' }}>
            <span style={{ ...ax, fontSize:14, fontWeight:900, color:T.faint, width:18 }}>{i+1}</span>
            <Avatar name={m.name} size={34} rank={i+1} />
            <div style={{ flex:1, minWidth:0 }}>
              <div style={{ ...hk, fontSize:14, fontWeight:700 }}>{m.name}</div>
              <div style={{ ...hk, fontSize:11.5, color:T.muted }}>{m.signed||0} inscrits · {m.taux||0}% de conversion</div>
            </div>
            <div style={{ ...ax, fontSize:16, fontWeight:900, color:T.volt }}>{fmt(m.total||0)}</div>
          </div>
        ))}
      </div>
    </div>
    {relSheet && <Sheet onClose={()=>setRelSheet(false)}>
      {sheetTitle('📣 '+relTitle)}
      <div style={{ ...hk, fontSize:13.5, color:T.muted, lineHeight:1.5, marginBottom:10 }}>S'affiche sur l'accueil des personnes choisies (et en notif s'ils ont activé les notifications). Choisis qui le reçoit :</div>
      <div style={{ display:'flex', gap:7, flexWrap:'wrap', marginBottom:14 }}>
        {sellers.map(n=>{ const on=relTargets.includes(n); return (
          <button key={n} onClick={()=>toggleTarget(n)} style={{ ...hk, fontSize:13, fontWeight:700, padding:'7px 13px', borderRadius:999, cursor:'pointer',
            background:on?T.amber:'transparent', color:on?'#1a1205':T.muted, border:`1px solid ${on?T.amber:T.line}` }}>{on?'✓ ':''}{n}</button>); })}
        {sellers.length>1 && <button onClick={()=>setRelTargets(relTargets.length===sellers.length?[]:sellers)} style={{ ...hk, fontSize:12.5, fontWeight:700, padding:'7px 12px', borderRadius:999, cursor:'pointer', background:'transparent', color:T.volt, border:`1px solid ${T.line}` }}>{relTargets.length===sellers.length?'Aucun':'Tout le monde'}</button>}
      </div>
      <textarea value={relMsg} onChange={e=>setRelMsg(e.target.value)} rows={6} placeholder="Écris ton message à l'équipe…" style={{ width:'100%', background:T.bg, border:`1px solid ${T.line}`, borderRadius:12, padding:'12px 14px', color:T.text, ...hk, fontSize:14, outline:'none', boxSizing:'border-box', resize:'vertical', lineHeight:1.55, marginBottom:14 }} />
      <button onClick={sendRelance} disabled={relBusy||!relMsg.trim()||!relTargets.length} style={{ width:'100%', ...gbtn(), padding:'14px 0', fontSize:15, opacity:(relBusy||!relMsg.trim()||!relTargets.length)?0.6:1 }}>{relBusy?'Envoi…':`📣 Envoyer${relTargets.length?' ('+relTargets.length+')':''}`}</button>
    </Sheet>}
    {caisseOpen && <CaisseSheet me={d.me} onClose={()=>setCaisseOpen(false)} />}
  </Screen>;
}

// ════════ ÉQUIPE ADMIN — classement + activité (remplace l'onglet Primes côté patron) ════════
function ScreenEquipe({ d, go, H }) {
  const ts = d.teamStats || null;
  const lb = [...(d.leaderboard||[])].sort((a,b)=>(b.total||0)-(a.total||0));
  const activity = d.activity || [];
  return <Screen active="gains" go={go}>
    <div style={{ padding:'0 20px 24px' }}>
      <div style={{ ...ax, fontSize:26, fontWeight:800, letterSpacing:-0.5, color:T.onBg, marginBottom:6 }}>Équipe</div>
      <div style={{ ...hk, fontSize:13.5, color:T.onBgMuted, marginBottom:20 }}>Performance de tes vendeurs depuis le lancement. Touche un vendeur pour ouvrir son interface.</div>

      {ts && ts.ok && <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10, marginBottom:20 }}>
        {[['Conversions', ts.totalConvs, GREEN],['CA primes · ce mois', fmt(ts.totalPrimes), '#facc15']].map(([l,v,c])=>(
          <div key={l} style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:16, padding:'14px' }}>
            <div style={{ ...ax, fontSize:24, fontWeight:900, color:c }}>{v}</div>
            <div style={{ ...hk, fontSize:11.5, color:T.muted, marginTop:3 }}>{l}</div>
          </div>
        ))}
      </div>}

      <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.onBgMuted, margin:'0 0 10px' }}>CLASSEMENT DU MOIS</div>
      <div style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:16, padding:'4px 14px', marginBottom:22 }}>
        {lb.length===0 && <div style={{ ...hk, fontSize:13, color:T.faint, padding:'12px 0' }}>Chargement…</div>}
        {lb.map((m,i)=>(
          <div key={m.name} onClick={()=>H.setViewAs && H.setViewAs(m.name)} style={{ display:'flex', alignItems:'center', gap:12, padding:'12px 0', borderBottom:i<lb.length-1?`1px solid ${T.line}`:'none', cursor:'pointer' }}>
            <span style={{ ...ax, fontSize:15, fontWeight:900, color: i===0?'#facc15':T.faint, width:20 }}>{i+1}</span>
            <Avatar name={m.name} size={38} rank={i+1} />
            <div style={{ flex:1, minWidth:0 }}>
              <div style={{ ...hk, fontSize:15, fontWeight:700 }}>{m.name}</div>
              <div style={{ ...hk, fontSize:12, color:T.muted }}>{m.signed||0} inscrits · {m.convertis||0}/{m.traites||0} · {m.taux||0}% de conversion</div>
            </div>
            <div style={{ textAlign:'right' }}>
              <div style={{ ...ax, fontSize:17, fontWeight:900, color:T.volt }}>{fmt(m.total||0)}</div>
              <div style={{ ...hk, fontSize:10, color:T.faint }}>👁 voir</div>
            </div>
          </div>
        ))}
      </div>

      <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.onBgMuted, margin:'0 0 10px' }}>ACTIVITÉ ÉQUIPE</div>
      <div style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:16, padding:'6px 14px' }}>
        {activity.length===0 && <div style={{ ...hk, fontSize:13, color:T.faint, padding:'12px 0' }}>Chargement…</div>}
        {activity.map((m,i)=>{ const recent = m.lastSeen && (Date.now()-new Date(m.lastSeen).getTime() < 86400000); return (
          <div key={m.name} style={{ display:'flex', alignItems:'center', gap:12, padding:'11px 0', borderBottom:i<activity.length-1?`1px solid ${T.line}`:'none' }}>
            <Avatar name={m.name} size={32} />
            <div style={{ flex:1 }}><div style={{ ...hk, fontSize:14, fontWeight:600 }}>{m.name}</div><div style={{ ...hk, fontSize:11, color:T.faint }}>{m.count>0?`${m.count} connexion${m.count>1?'s':''}`:'pas encore connecté'}</div></div>
            <div style={{ display:'flex', alignItems:'center', gap:6 }}>
              <span style={{ width:7, height:7, borderRadius:7, background: m.lastSeen?(recent?GREEN:T.amber):T.faint }} />
              <span style={{ ...hk, fontSize:12, color:m.lastSeen?T.muted:T.faint }}>{relTime(m.lastSeen)}</span>
            </div>
          </div>); })}
      </div>
    </div>
  </Screen>;
}

// ════════ ACCUEIL — bilan du vendeur (inspiré V1) ════════
function ScreenDashboard({ d, go, H }) {
  const s = d.stats || {};
  const [nudgeSeen, setNudgeSeen] = useState(()=>{ try{ return localStorage.getItem('avis_nudge_seen'); }catch(_){ return null; } });
  // Le message du manager expire tout seul après 7 jours (un "bravo / relance" ne doit pas rester affiché indéfiniment).
  const nudgeFresh = s.nudge && s.nudge.at && (Date.now() - Date.parse(s.nudge.at) < 7*86400000);
  const nudge = (nudgeFresh && nudgeSeen !== s.nudge.at) ? s.nudge : null;
  // "OK c'est parti" : on masque tout de suite (local), ET on efface le message dans la fiche du vendeur côté serveur
  // -> il expire pour de bon dès qu'il interagit (ne revient plus sur un autre appareil). Pas en vue admin (lecture seule).
  const dismissNudge = ()=>{ try{ localStorage.setItem('avis_nudge_seen', s.nudge.at); }catch(_){} setNudgeSeen(s.nudge.at); if(!viewAsName()) api('/api/nudge/clear', { method:'POST' }).catch(()=>{}); };
  const [pushAsk, setPushAsk] = useState(()=>{ try{ return pushSupported() && Notification.permission!=='granted'; }catch(_){ return false; } });
  // Nouveaux adhérents à suivre aujourd'hui (rétention 1er mois).
  const [suiviDue, setSuiviDue] = useState(0);
  useEffect(()=>{ api('/api/suivis?who='+encodeURIComponent(d.me)).then(r=>setSuiviDue(r.due||0)).catch(()=>{}); },[d.me]);
  const warn = s.rdv>=3 && (s.pctRdv - s.pctRdvConv) >= 25; // beaucoup de RDV mais peu convertis
  const t = Math.max(s.total||0, 1);
  // Convertis = vraies conversions du mois (Converti par), pas seulement les leads assignés récents
  const convN = s.oui||0; // Convertis de l'entonnoir = conversions des leads entrés depuis le lancement (même cohorte que Leads/RDV), PAS les closes du mois
  const wRdv = Math.round(((s.rdv||0)/t)*100), wConv = Math.min(100, Math.round((convN/t)*100));
  const fn = (lbl, w, n, color, cat) => (
    <div onClick={cat?()=>H.openCat(cat, lbl):undefined} style={{ display:'flex', alignItems:'center', gap:10, marginBottom:9, cursor:cat?'pointer':'default' }}>
      <span style={{ ...hk, fontSize:12, fontWeight:700, color:T.muted, width:74, flex:'0 0 auto' }}>{lbl}{cat?' ›':''}</span>
      <div style={{ flex:1, background:T.line, borderRadius:999, height:16, overflow:'hidden' }}><i style={{ display:'block', height:'100%', borderRadius:999, width:Math.max(w,3)+'%', background:color }} /></div>
      <span style={{ ...ax, fontSize:13, fontWeight:900, width:42, textAlign:'right', flex:'0 0 auto' }}>{n}</span>
    </div>
  );
  return <Screen active="home" go={go}>
    <div style={{ padding:'0 20px 24px' }}>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:20 }}>
        <div><div style={{ ...hk, fontSize:14, color:T.onBgMuted }}>Salut,</div><div style={{ ...ax, fontSize:26, fontWeight:800, letterSpacing:-0.5, color:T.onBg }}>{d.me} · {BRAND}</div></div>
        <Avatar name={d.me} size={44} ring pulse />
      </div>

      <CagnotteCard validee={d.cagnotte} attente={s.potentiel||0} goal={d.goal} avis={d.reviews?.prime||0} growth={s.growth} />

      {suiviDue > 0 && <div onClick={()=>H.suivi()} style={{ background:'linear-gradient(135deg, rgba(34,197,94,0.16), rgba(34,197,94,0.06))', border:'1px solid rgba(34,197,94,0.45)', borderRadius:16, padding:'14px 16px', margin:'14px 0 4px', display:'flex', alignItems:'center', gap:12, cursor:'pointer' }}>
        <span style={{ fontSize:22 }}>🌱</span>
        <div style={{ flex:1, minWidth:0 }}>
          <div style={{ ...hk, fontSize:14.5, fontWeight:800, color:T.onBg }}>{suiviDue} nouvel{suiviDue>1?'s':''} adhérent{suiviDue>1?'s':''} à suivre</div>
          <div style={{ ...hk, fontSize:11.5, color:T.onBgMuted }}>Le 1er mois décide tout. Prends de leurs nouvelles sur WhatsApp 💬</div>
        </div>
        <span style={{ ...hk, fontSize:18, color:GREEN }}>›</span>
      </div>}

      {/* Un seul bandeau d'alerte à la fois, du plus urgent au moins urgent : message manager > activer notifs > prime avis. Évite l'empilement. */}
      {nudge ? <div style={{ background:'linear-gradient(135deg, rgba(246,194,84,0.18), rgba(245,158,11,0.10))', border:'1px solid rgba(246,194,84,0.5)', borderRadius:16, padding:'14px 16px', margin:'14px 0 4px' }}>
        <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:6 }}><span style={{ fontSize:18 }}>📣</span><span style={{ ...mo, fontSize:10, letterSpacing:1, color:T.amber }}>MESSAGE DU MANAGER</span></div>
        <div style={{ ...hk, fontSize:15, fontWeight:600, color:T.onBg, lineHeight:1.6, whiteSpace:'pre-line' }}>{nudge.msg}</div>
        <button onClick={dismissNudge} style={{ marginTop:10, ...btn(T.amber,'#1a1205'), padding:'10px 0', width:'100%', fontSize:13.5 }}>OK, c'est parti 💪</button>
      </div>
      : pushAsk ? <div onClick={async()=>{ const ok=await enablePush(); if(ok) setPushAsk(false); }} style={{ background:'rgba(239,68,68,0.12)', border:'1px solid rgba(239,68,68,0.32)', borderRadius:14, padding:'12px 14px', margin:'14px 0 4px', display:'flex', alignItems:'center', gap:10, cursor:'pointer' }}>
        <span style={{ fontSize:20 }}>🔔</span>
        <div style={{ flex:1, minWidth:0 }}><div style={{ ...hk, fontSize:13.5, fontWeight:800, color:'#f87171' }}>Active tes notifications (obligatoire)</div><div style={{ ...hk, fontSize:11.5, color:T.onBgMuted }}>Relances avis, messages du manager, leads à traiter. Touche pour activer.</div></div>
        <span style={{ ...hk, fontSize:18, color:'#f87171' }}>›</span>
      </div>
      : s.avisWeek===0 ? <div style={{ ...hk, fontSize:12.5, fontWeight:700, color:'#f87171', background:'rgba(239,68,68,0.12)', border:'1px solid rgba(239,68,68,0.3)', borderRadius:14, padding:'11px 14px', margin:'14px 0 4px', lineHeight:1.5 }}>⭐ La prime avis est pour TOUTE l'équipe, et tu n'as demandé <b>aucun avis cette semaine</b>. Descends et touche "Demander un avis", chaque avis nous rapproche de la prime 🙏</div>
      : null}

      {s.doubleFrom > 0 && (d.inscrits >= s.doubleFrom
        ? <div style={{ ...hk, fontSize:12.5, fontWeight:800, color:'#06270F', background:GREEN, borderRadius:12, padding:'10px 12px', margin:'14px 0 4px', textAlign:'center' }}>🔥 Bonus actif : tes primes sont DOUBLÉES (dès {s.doubleFrom} inscriptions)</div>
        : <div style={{ ...hk, fontSize:12, fontWeight:700, color:T.muted, background:T.surface, border:`1px solid ${T.line}`, borderRadius:12, padding:'10px 12px', margin:'14px 0 4px', textAlign:'center' }}>🔥 Plus que <b style={{ color:T.text }}>{s.doubleFrom - d.inscrits}</b> inscriptions pour débloquer la prime <b style={{ color:T.text }}>×2</b></div>)}

      <div data-tour="rates" style={{ display:'flex', gap:12, margin:'14px 0 12px' }}>
        <RateHero pct={s.pctRdv||0} cap="RDV pris" sub={`${s.rdv||0} sur ${s.traites||0} traités`} />
        <RateHero pct={s.pctRdvConv||0} cap="convertis sur RDV" sub={`${s.oui||0} sur ${s.rdv||0} RDV`} warn={warn} />
      </div>
      {warn && <div style={{ ...hk, fontSize:12, fontWeight:700, color:'#f87171', background:'rgba(239,68,68,0.12)', border:'1px solid rgba(239,68,68,0.3)', borderRadius:12, padding:'9px 12px', marginBottom:12, textAlign:'center' }}>⚠ Beaucoup de RDV mais peu de signatures : c'est au moment de l'essai que ça se joue, accroche-toi pour les convertir.</div>}

      <div data-tour="funnel" style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:16, padding:14, marginBottom:12 }}>
        <div style={{ ...mo, fontSize:10.5, letterSpacing:1, color:T.faint, textTransform:'uppercase', marginBottom:11 }}>Entonnoir</div>
        {fn('Leads', 100, s.total||0, T.text)}
        {fn('RDV pris', wRdv, s.rdv||0, GREEN, 'rdv')}
        {fn('Convertis', wConv, convN, '#FACC15', 'convertis')}
      </div>

      <div data-tour="stats" style={{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:10, marginBottom:18 }}>
        {[['Leads actifs', d.leads.length, false, 'actifs'],['En cours', s.enCours||0, false, 'encours'],['Perdus', s.perdus||0, true, 'perdus']].map(([k,v,danger,cat])=>(
          <div key={k} onClick={()=> cat==='actifs' ? H.openCat('actifs', 'Leads à appeler', d.leads) : H.openCat(cat, k)}
            style={{ background:T.surface, borderRadius:14, padding:'13px 10px', textAlign:'center', border:`1px solid ${danger?'rgba(239,68,68,0.3)':T.line}`, cursor:'pointer' }}>
            <div style={{ ...ax, fontSize:22, fontWeight:900, color:danger?'#f87171':T.text }}>{v}</div>
            <div style={{ ...hk, fontSize:11, color:T.muted, marginTop:2 }}>{k} ›</div>
          </div>
        ))}
      </div>

      <div data-tour="avis"><AvisCard r={d.reviews} /></div>
      <div data-tour="scratch-card"><ScratchCard name={d.me} /></div>
    </div>
  </Screen>;
}

// ════════ LEADS — Nouveaux (urgent) + Relances + recherche ════════
const RED = '#ef4444'; // rouge "alerte" unique (fills/bordures) ; #f87171 = rouge texte sur fond sombre
function LeadRow({ lead, urgent, onClick, assignee }) {
  return <div onClick={onClick} style={{ display:'flex', alignItems:'center', gap:12, padding:'12px 14px', marginBottom:10, cursor:'pointer',
    background:T.surface, borderRadius:14, border: urgent?`1.5px solid ${RED}`:`1px solid ${T.line}`, boxShadow: urgent?`0 0 0 3px rgba(239,68,68,0.12)`:'none' }}>
    <Avatar name={lead.name} size={40} />
    <div style={{ flex:1, minWidth:0 }}>
      <div style={{ ...hk, fontSize:15, fontWeight:600, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{lead.name}</div>
      <div style={{ ...hk, fontSize:12, color:T.muted }}>{assignee ? `👤 ${assignee}${lead.when?` · ${lead.when}`:''}` : lead.plan}</div>
    </div>
    {lead.converted
      ? <span style={{ ...mo, fontSize:10, fontWeight:700, color:'#15803d', background:'rgba(34,197,94,0.16)', padding:'4px 9px', borderRadius:999 }}>✅ CONVERTI</span>
      : lead.seanceEssai
      ? <span style={{ ...mo, fontSize:10, fontWeight:700, color:T.amber, background:T.amberDim, padding:'4px 9px', borderRadius:999, whiteSpace:'nowrap' }}>📅 {fmtSeance(lead.seanceEssai)}</span>
      : lead.relanceLe
      ? <span style={{ ...mo, fontSize:10, fontWeight:700, color:T.blue, background:T.blueDim, padding:'4px 9px', borderRadius:999, whiteSpace:'nowrap' }}>🔁 {fmtFR(lead.relanceLe)}{hasTime(lead.relanceLe)?' '+new Date(lead.relanceLe).toLocaleTimeString('fr-FR',{hour:'2-digit',minute:'2-digit'}).replace(':','h'):''}</span>
      : urgent
      ? <span style={{ ...mo, fontSize:10, fontWeight:700, letterSpacing:0.5, color:'#fff', background:RED, padding:'4px 9px', borderRadius:999 }}>🚨 NOUVEAU</span>
      : <span style={{ ...mo, fontSize:10, fontWeight:700, color:T.muted, background:T.line, padding:'4px 9px', borderRadius:999 }}>{lead.statutRaw||'—'}</span>}
  </div>;
}

function ScreenLeads({ d, go, H, ver }) {
  const [q, setQ] = useState('');
  const [res, setRes] = useState(null);
  const [day, setDay] = useState(()=>todayISO()); // défaut : relances DU JOUR (tâches du jour). 'all' | date ISO
  useEffect(()=>{ if(q.trim().length<2){ setRes(null); return; } const t=setTimeout(async()=>{
    try{ const out=await api('/api/search?q='+encodeURIComponent(q)); setRes((out.leads||[]).map(mapLead)); }catch{ setRes([]); }
  },300); return ()=>clearTimeout(t); },[q, ver]);
  // Leads anciens (+30j) du conseiller -> à passer sur Deciplus.
  const [ancCount, setAncCount] = useState(0);
  useEffect(()=>{ api('/api/category?cat=anciens&who='+encodeURIComponent(d.me)).then(r=>setAncCount(r.total ?? (r.leads||[]).length)).catch(()=>{}); },[ver]);
  // Relances dues d'un AUTRE vendeur (collègue absent) -> visibles par toute l'équipe.
  const [relEqCount, setRelEqCount] = useState(0);
  useEffect(()=>{ api('/api/category?cat=relancesequipe&who='+encodeURIComponent(d.me)).then(r=>setRelEqCount(r.total ?? (r.leads||[]).length)).catch(()=>{}); },[ver]);

  const searching = q.trim().length>=2;
  const me = d.me, today = todayISO();
  const tomorrow = (()=>{ const t=new Date(); t.setDate(t.getDate()+1); return `${t.getFullYear()}-${pad(t.getMonth()+1)}-${pad(t.getDate())}`; })();
  // Séances fixées : leads avec séance à venir, message de confirmation PAS encore envoyé (rappel).
  // Dès que le message WhatsApp est envoyé (seanceConfirmee), le lead quitte les Leads et ne vit que dans l'onglet Séances.
  const seances = (d.seances||[]).filter(l=> l.seanceEssai && !l.seanceConfirmee && l.seanceEssai.slice(0,10) >= today).sort((a,b)=>a.seanceEssai.localeCompare(b.seanceEssai));
  const seanceIds = new Set(seances.map(l=>l.id));
  // ORPHELINS : leads actifs attribués à personne (pool de l'équipe). Visible par tous, sortis des "Nouveaux"/"Relances".
  // Dès qu'un conseiller agit dessus, l'auto-attribution (côté serveur) le lui donne -> il quitte le pool.
  // Orphelin = non attribué ET entré il y a +48h (personne ne l'a pris). Avant 48h, il reste dans "Nouveaux à appeler" (pool partagé).
  const orphelins = (d.allLeads||[]).filter(l=> !l.assigne && !l.converted && l.statutRaw!=='MORT' && l.statutRaw!=='NUM PAS BON' && !seanceIds.has(l.id) && l.dateEntree && (Date.now()-new Date(l.dateEntree).getTime())>48*3600000)
    .sort((a,b)=>{ if(a.urgent!==b.urgent) return a.urgent?-1:1; return (a.dateEntree||'').localeCompare(b.dateEntree||''); });
  const orphIds = new Set(orphelins.map(l=>l.id));
  // Une séance d'essai ACTIVE (posée, PAS annulée) sort le lead des Leads -> il vit dans l'onglet Séances,
  // peu importe que "À relancer ?" soit resté coché (cas Calendly / saisie Notion à la main).
  const activeSeance = (l)=> l.seanceEssai && !l.seanceAnnulee;
  const nouveaux = d.leads.filter(l=>l.urgent && !activeSeance(l) && !seanceIds.has(l.id) && !orphIds.has(l.id));
  const relancesAll = d.leads.filter(l=>!l.urgent && !activeSeance(l) && !seanceIds.has(l.id) && !orphIds.has(l.id));
  // À RAPPELER MAINTENANT : relances avec une HEURE déjà passée aujourd'hui (rappel intra-journée dû). Urgent.
  const nowMs = Date.now();
  const aRappeler = relancesAll.filter(l=> hasTime(l.relanceLe) && new Date(l.relanceLe).getTime() <= nowMs).sort((a,b)=>a.relanceLe.localeCompare(b.relanceLe));
  const aRappelerIds = new Set(aRappeler.map(l=>l.id));
  const relancesPipe = relancesAll.filter(l=> !aRappelerIds.has(l.id));
  // FILET A REPRENDRE : leads "Nouveau" assignés à un AUTRE conseiller, entrés avant aujourd'hui (pas traités) -> anti-perte si l'assigné est absent
  const aReprendre = (d.allLeads||[]).filter(l=> l.urgent && !l.seanceEssai && l.assigne && l.assigne!==me && l.dateEntree && l.dateEntree.slice(0,10) < today);
  // Relances filtrées par le jour choisi, puis groupées par date
  const relances = day==='all' ? relancesPipe : relancesPipe.filter(l=> (l.relanceLe||'').slice(0,10)===day);
  const groups = (()=>{ const by={}; relances.forEach(l=>{ const k=(l.relanceLe||'').slice(0,10)||'zzz'; (by[k]=by[k]||[]).push(l); }); return Object.keys(by).sort().map(k=>({ date:k, leads:by[k] })); })();
  const dayLabel = (iso)=> iso==='zzz'?'Sans date' : iso===today?"Aujourd'hui" : iso===tomorrow?'Demain' : new Date(iso+'T00:00:00').toLocaleDateString('fr-FR',{ weekday:'short', day:'2-digit', month:'2-digit' });

  const Header = ({ red, label, count }) => (
    <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'10px 14px', borderRadius:12, marginBottom:12,
      background: red?RED:'var(--track)', color: red?'#fff':T.onBg }}>
      <span style={{ ...hk, fontWeight:800, fontSize:14 }}>{label}</span>
      <span style={{ ...mo, fontSize:12, fontWeight:700, background: red?'rgba(255,255,255,0.22)':'var(--track)', borderRadius:999, padding:'2px 9px' }}>{count}</span>
    </div>
  );
  const Chip = ({ id, label }) => (
    <button onClick={()=>setDay(id)} style={{ ...hk, fontSize:13, fontWeight:700, padding:'7px 14px', borderRadius:999, flexShrink:0,
      border:`1px solid ${day===id?'transparent':T.line}`, background: day===id?T.volt:'transparent', color: day===id?T.ink:T.onBgMuted, cursor:'pointer' }}>{label}</button>
  );
  const customDay = day!=='all' && day!==today && day!==tomorrow;

  return <Screen active="leads" go={go}>
    <div style={{ padding:'0 20px 20px' }}>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:12 }}>
        <div style={{ ...ax, fontSize:26, fontWeight:800, letterSpacing:-0.5, color:T.onBg }}>Leads</div>
        <button onClick={()=>H.walkin()} style={{ ...btn(T.volt, 'var(--volt-ink)'), padding:'9px 14px', fontSize:13.5, fontWeight:800, borderRadius:999 }}>➕ Sur place</button>
      </div>
      <input value={q} onChange={e=>setQ(e.target.value)} placeholder="🔍 Retrouver un lead (nom ou téléphone)…"
        style={{ width:'100%', background:T.surface, border:`1px solid ${T.line}`, borderRadius:13, padding:'12px 14px', color:T.text, ...hk, fontSize:14, outline:'none', marginBottom:16 }} />

      {searching ? (
        <>
          <SectionLabel>{res?`${res.length} résultat${res.length>1?'s':''}`:'Recherche…'}</SectionLabel>
          {(res||[]).map(l=>(<LeadRow key={l.id} lead={l} urgent={l.urgent} onClick={()=>H.openLead(l, res)} />))}
          {res && res.length===0 && <div style={{ ...hk, fontSize:13, color:T.onBgMuted, padding:'12px 0' }}>Aucun lead trouvé.</div>}
        </>
      ) : (!nouveaux.length && !relancesAll.length && !aReprendre.length && !seances.length && !orphelins.length) ? (
        <div style={{ padding:'40px 0', textAlign:'center', color:T.onBgMuted }}><div style={{ fontSize:40, marginBottom:10 }}>✅</div>Tout est traité, bravo. Dès qu'un lead tombe, appelle-le tout de suite : c'est dans la première heure qu'il signe.</div>
      ) : (
        <>
          <Header red label="🚨 Nouveaux à appeler" count={nouveaux.length} />
          {nouveaux.length ? nouveaux.map(l=>(<LeadRow key={l.id} lead={l} urgent onClick={()=>H.openLead(l, nouveaux)} />))
            : <div style={{ ...hk, fontSize:13, color:T.onBgMuted, padding:'4px 0 16px' }}>Aucun nouveau lead pour l'instant. Enchaîne tes relances : chaque jour d'attente, un lead refroidit.</div>}
          <div style={{ height:12 }} />
          {seances.length>0 && <>
            <Header red label="🚨 Nouveaux à appeler · séance déjà fixée" count={seances.length} />
            <div style={{ ...hk, fontSize:12, color:T.muted, margin:'-4px 0 10px' }}>Séance déjà réservée. Envoie-leur le message de confirmation WhatsApp.</div>
            {seances.map(l=>(<LeadRow key={l.id} lead={l} urgent={false} onClick={()=>H.openLead(l, seances)} />))}
            <div style={{ height:10 }} />
          </>}
          {aRappeler.length>0 && <>
            <Header red label="🔴 À rappeler maintenant" count={aRappeler.length} />
            <div style={{ ...hk, fontSize:12, color:T.muted, margin:'-4px 0 10px' }}>Rappels de la journée dont l'heure est passée. À faire en priorité.</div>
            {aRappeler.map(l=>(<LeadRow key={l.id} lead={l} urgent onClick={()=>H.openLead(l, aRappeler)} />))}
            <div style={{ height:10 }} />
          </>}
          {aReprendre.length>0 && <>
            <Header red label="🆘 À reprendre" count={aReprendre.length} />
            <div style={{ ...hk, fontSize:12, color:T.muted, margin:'-4px 0 10px' }}>Leads que leur vendeur n'a pas encore appelés. Prends-les avant qu'ils refroidissent.</div>
            {aReprendre.map(l=>(<LeadRow key={l.id} lead={l} urgent assignee={l.assigne} onClick={()=>H.openLead(l, aReprendre)} />))}
            <div style={{ height:10 }} />
          </>}
          {orphelins.length>0 && <>
            <Header label="🧩 Personne ne les appelle" count={orphelins.length} />
            <div style={{ ...hk, fontSize:12, color:T.muted, margin:'-4px 0 10px' }}>Personne dessus depuis 2 jours. Appelle : dès que tu agis, le lead devient le tien.</div>
            {orphelins.map(l=>(<LeadRow key={l.id} lead={l} urgent={l.urgent} onClick={()=>H.openLead(l, orphelins)} />))}
            <div style={{ height:10 }} />
          </>}

          <div style={{ height:8 }} />
          <Header label="🔁 Relances" count={relancesPipe.length} />
          <div style={{ display:'flex', gap:7, overflowX:'auto', paddingBottom:6, marginBottom:14, WebkitOverflowScrolling:'touch' }}>
            <Chip id="all" label="Tout" />
            <Chip id={today} label="Aujourd'hui" />
            <Chip id={tomorrow} label="Demain" />
            <label style={{ ...hk, fontSize:13, fontWeight:700, padding:'7px 14px', borderRadius:999, flexShrink:0, position:'relative', cursor:'pointer',
              border:`1px solid ${customDay?'transparent':T.line}`, background: customDay?T.volt:'transparent', color: customDay?T.ink:T.onBgMuted }}>
              📅 {customDay?fmtFR(day):'Un jour'}
              <input type="date" onChange={e=>e.target.value&&setDay(e.target.value)} style={{ position:'absolute', inset:0, opacity:0, cursor:'pointer' }} />
            </label>
          </div>
          {groups.length ? groups.map(g=>(
            <div key={g.date}>
              <div style={{ display:'flex', alignItems:'center', gap:8, margin:'14px 0 8px' }}>
                <span style={{ ...mo, fontSize:11.5, letterSpacing:0.5, textTransform:'uppercase', fontWeight:800, color:T.onBg }}>{dayLabel(g.date)}</span>
                {g.date!=='zzz' && <span style={{ ...hk, fontSize:11, color:T.faint }}>{fmtFR(g.date)}</span>}
                <span style={{ flex:1, height:1, background:T.line }} />
                <span style={{ ...mo, fontSize:11, fontWeight:700, color:T.muted }}>{g.leads.length}</span>
              </div>
              {g.leads.map(l=>(<LeadRow key={l.id} lead={l} urgent={false} onClick={()=>H.openLead(l, relances)} />))}
            </div>
          )) : <div style={{ ...hk, fontSize:13, color:T.faint, padding:'4px 0' }}>Aucune relance{day!=='all'?' ce jour-là':''}.</div>}
        </>
      )}

      {!searching && relEqCount>0 && <div onClick={()=>H.openCat('relancesequipe','🔥 Relances à rappeler vite')} style={{ background:'rgba(239,68,68,0.12)', border:'1px solid rgba(239,68,68,0.4)', borderRadius:14, padding:'12px 14px', marginTop:18, display:'flex', alignItems:'center', gap:10, cursor:'pointer' }}>
        <span style={{ fontSize:20 }}>🔥</span>
        <div style={{ flex:1, minWidth:0 }}><div style={{ ...hk, fontSize:13.5, fontWeight:800, color:'#ff6b6b' }}>{relEqCount} relance{relEqCount>1?'s':''} à rappeler vite</div><div style={{ ...hk, fontSize:11.5, color:T.onBgMuted }}>En retard ou pour aujourd'hui. Plus tu attends, plus le lead refroidit et part ailleurs. Touche pour les prendre.</div></div>
      </div>}
      {!searching && ancCount>0 && <div onClick={()=>H.openCat('anciens','🗂️ Vieux leads à mettre sur Deciplus')} style={{ background:'rgba(96,165,250,0.10)', border:'1px solid rgba(96,165,250,0.32)', borderRadius:14, padding:'12px 14px', marginTop:18, display:'flex', alignItems:'center', gap:10, cursor:'pointer' }}>
        <span style={{ fontSize:20 }}>🗂️</span>
        <div style={{ flex:1, minWidth:0 }}><div style={{ ...hk, fontSize:13.5, fontWeight:800, color:'#7dd3fc' }}>{ancCount} contact{ancCount>1?'s':''} de plus d'un mois</div><div style={{ ...hk, fontSize:11.5, color:T.onBgMuted }}>Jamais inscrits. Tout le monde peut les mettre sur Deciplus. Touche pour voir.</div></div>
        <span style={{ ...hk, fontSize:18, color:'#7dd3fc' }}>›</span>
      </div>}
    </div>
  </Screen>;
}

// ════════ PRIMES ════════
function WalletInner({ d, H }) {
  const [filter, setFilter] = useState('tout');
  const [hist, setHist] = useState(null);
  useEffect(()=>{ api('/api/primehistory?who='+encodeURIComponent(d.me)).then(r=>setHist(r.items||[])).catch(()=>setHist([])); }, [d.me]);
  const monthLabel = (key)=>{ const [y,m]=key.split('-'); const lbl=MOIS_FR[Number(m)-1]||''; return lbl.charAt(0)+lbl.slice(1).toLowerCase()+' '+y; };
  // En cours = exactement la cagnotte de l'accueil (conversions du mois + prime avis), pour que tout colle.
  const enCours = d.cagnotte || 0;
  const validee = hist ? hist.filter(h=>h.paid).reduce((s,h)=>s+(h.total||0),0) : 0;
  const montant = filter==='valide' ? validee : filter==='attente' ? enCours : (validee+enCours);
  const montantAnim = useCountUp(montant);
  const montantLabel = filter==='valide' ? 'Validée · total payé' : filter==='attente' ? 'En cours · ce mois' : 'Cagnotte totale';
  const shown = (d.conversions||[]).filter(l => filter==='tout' || (filter==='valide' ? l.paid : !l.paid));
  // Conversion -> objet "lead" pour ouvrir la fiche (notes, annuler conversion, etc.).
  const asLead = (l)=> ({ id:l.id, name:l.name, phone:l.phone||'', email:l.email||'', converted:true, convType:(l.type==='abo'?'Abonnement MRR':l.type==='comptant'?'Comptant':''), convPar:l.convPar||'', statutRaw:l.statut||'', seanceEssai:l.seanceEssai||'', seanceProspect:false, relanceLe:'', urgent:false, plan:'Inscrit' });
  const openConv = (l)=> H && H.openLead(asLead(l), shown.map(asLead));
  return (
    <div style={{ padding:'0 20px' }}>
      <div style={{ ...ax, fontSize:26, fontWeight:800, letterSpacing:-0.5, marginBottom:16, color:T.onBg }}>Ma cagnotte</div>
      {/* En-tête sobre : on garde le total (incl. "Cagnotte totale" depuis toujours) + le filtre, sans le décorum animé de la carte d'accueil. */}
      <div style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:18, padding:'18px 20px', marginBottom:14 }}>
        <div style={{ ...mo, fontSize:11, letterSpacing:1.5, textTransform:'uppercase', color:T.muted }}>{montantLabel}</div>
        <div style={{ ...ax, fontSize:34, fontWeight:900, lineHeight:1.1, marginTop:4, color: filter==='valide' ? GREEN : T.volt }}>{montantAnim.toLocaleString('fr-FR')} €</div>
      </div>
      <div style={{ display:'flex', gap:6, background:T.surface, borderRadius:12, padding:4, marginBottom:14, border:`1px solid ${T.line}` }}>
        {[['tout','Tout'],['valide','Validé'],['attente','En attente']].map(([id,lb])=>(
          <button key={id} onClick={()=>setFilter(id)} style={{ flex:1, border:'none', cursor:'pointer', borderRadius:9, padding:'8px 0', ...hk, fontSize:13, fontWeight:600, background:filter===id?T.volt:'transparent', color:filter===id?'var(--volt-ink)':T.muted }}>{lb}</button>
        ))}
      </div>
      <div style={{ display:'flex', flexDirection:'column', paddingBottom:20 }}>
        {filter!=='attente' && d.reviews?.prime>0 && <div style={{ display:'flex', alignItems:'center', gap:12, padding:'12px 0', borderBottom:`1px solid ${T.line}` }}>
          <div style={{ width:38, height:38, borderRadius:11, background:T.amberDim, display:'flex', alignItems:'center', justifyContent:'center', fontSize:18 }}>⭐</div>
          <div style={{ flex:1, minWidth:0 }}><div style={{ ...hk, fontSize:14, fontWeight:600, color:T.onBg }}>Prime avis Google</div><div style={{ ...hk, fontSize:12, color:T.onBgMuted }}>Bonus équipe du mois</div></div>
          <div style={{ ...ax, fontSize:17, fontWeight:800, color:T.onBg }}>+{d.reviews.prime}€</div>
        </div>}
        {shown.length===0 && !(filter!=='attente' && d.reviews?.prime>0) && <div style={{ ...hk, fontSize:13, color:T.onBgMuted, padding:'18px 0', textAlign:'center' }}>Aucune conversion ici.</div>}
        {shown.slice(0,80).map(l=>(
          <div key={l.id} onClick={()=>openConv(l)} style={{ display:'flex', alignItems:'center', gap:12, padding:'12px 0', borderBottom:`1px solid ${T.line}`, cursor:'pointer' }}>
            <div style={{ width:38, height:38, borderRadius:11, background:l.prime!=null?T.voltDim:T.amberDim, display:'flex', alignItems:'center', justifyContent:'center' }}><Icon name={l.prime!=null?'check':'check'} size={18} color={l.prime!=null?T.volt:T.amber} /></div>
            <div style={{ flex:1, minWidth:0 }}><div style={{ ...hk, fontSize:14, fontWeight:600, color:T.onBg }}>{l.name}</div><div style={{ ...hk, fontSize:12, color:T.onBgMuted }}>{l.type==='abo'?'Abonnement MRR':l.type==='comptant'?'Comptant':'Inscrit'}{l.date?` · ${relWhen(l.date)}`:''}</div></div>
            <div style={{ display:'flex', alignItems:'center', gap:8 }}><div style={{ ...ax, fontSize:17, fontWeight:800, color:l.prime!=null?T.onBg:T.onBgMuted }}>{l.prime!=null?`+${l.prime}€`:'✓'}</div><Icon name="arrow" size={16} color={T.onBgMuted} /></div>
          </div>
        ))}
      </div>
      <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.onBgMuted, margin:'4px 0 10px' }}>HISTORIQUE DES PAIES</div>
      <div style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:16, padding:'4px 14px', marginBottom:26 }}>
        {!hist && <div style={{ ...hk, fontSize:13, color:T.faint, padding:'13px 0' }}>Chargement…</div>}
        {hist && hist.length===0 && <div style={{ ...hk, fontSize:13, color:T.faint, padding:'13px 0' }}>Pas encore d'historique.</div>}
        {hist && hist.map((h,i)=>(
          <div key={h.key} style={{ display:'flex', alignItems:'center', gap:10, padding:'12px 0', borderBottom: i<hist.length-1?`1px solid ${T.line}`:'none' }}>
            <div style={{ flex:1, minWidth:0 }}>
              <div style={{ ...hk, fontSize:14, fontWeight:600, color:T.onBg }}>{monthLabel(h.key)}</div>
              <div style={{ ...hk, fontSize:11.5, color:T.onBgMuted }}>{h.manual?'reporté':(h.count!=null?`${h.count} inscription${h.count>1?'s':''}`:'')}</div>
            </div>
            <span style={{ ...mo, fontSize:9.5, fontWeight:700, padding:'3px 8px', borderRadius:999, background:h.paid?'rgba(34,197,94,0.16)':T.amberDim, color:h.paid?GREEN:T.amber }}>{h.paid?'✅ Payé':'⏳ En cours'}</span>
            <div style={{ ...ax, fontSize:17, fontWeight:800, color:T.onBg, minWidth:50, textAlign:'right' }}>{fmt(h.paid ? h.total : (d.cagnotte||h.total))}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

// Carte avis Google : total en direct + objectif par paliers (gamification équipe, reprise V1)
const REVIEW_MSG = `Salut ! Si tu te plais à ${BRAND}, ça nous aiderait vraiment que tu laisses un petit avis Google. Ça te prend 2 min depuis ton tél 🙏

👉 ${GOOGLE_REVIEW}

Tu écris ce que tu veux : les coachs, la salle ouverte 24h/24, l'ambiance, le parking, les cours collectifs… ce qui te plaît le plus chez nous. Merci beaucoup 💚`;
// Version courte pour le partage natif (Snap/Insta/WhatsApp…) : le lien part séparément (mieux géré par Insta/Snap).
const REVIEW_SHARE_TEXT = `Salut ! Si tu te plais à ${BRAND}, ça nous aiderait vraiment que tu laisses un petit avis Google 🙏 Ça prend 2 min, merci 💚`;

// Hack iOS : Safari interdit navigator.vibrate, mais "cliquer" un interrupteur natif
// (input switch, iOS 17.4+) déclenche un petit retour haptique. Pas garanti, mais c'est tout ce qu'iOS permet.
let _hapticSwitch = null;
function iosHapticTick() {
  try {
    if (!_hapticSwitch) {
      const label = document.createElement('label');
      label.setAttribute('aria-hidden', 'true');
      label.style.cssText = 'position:absolute;left:-9999px;top:0;width:0;height:0;opacity:0;pointer-events:none;';
      const input = document.createElement('input');
      input.type = 'checkbox';
      input.setAttribute('switch', '');
      label.appendChild(input);
      document.body.appendChild(label);
      _hapticSwitch = label;
    }
    _hapticSwitch.click();
  } catch(_){}
}

// Petit "ta-da" + vibration quand un palier d'avis tombe (synthétisé, aucun fichier à charger).
function celebrateFx() {
  try {
    if (navigator.vibrate) { navigator.vibrate([0, 55, 45, 55, 45, 150]); }
    else { iosHapticTick(); setTimeout(iosHapticTick, 170); setTimeout(iosHapticTick, 360); setTimeout(iosHapticTick, 560); } // iOS : suite de tics pour imiter le rythme
  } catch(_){}
  try {
    const AC = window.AudioContext || window.webkitAudioContext;
    if (!AC) return;
    const ctx = new AC();
    if (ctx.state === 'suspended') ctx.resume();
    const now = ctx.currentTime;
    const notes = [523.25, 659.25, 783.99, 1046.5]; // do-mi-sol-do (arpège joyeux)
    notes.forEach((f, i) => {
      const o = ctx.createOscillator(), g = ctx.createGain();
      o.type = 'triangle'; o.frequency.value = f;
      const t = now + i * 0.10;
      g.gain.setValueAtTime(0.0001, t);
      g.gain.exponentialRampToValueAtTime(0.22, t + 0.02);
      g.gain.exponentialRampToValueAtTime(0.0001, t + 0.34);
      o.connect(g); g.connect(ctx.destination);
      o.start(t); o.stop(t + 0.36);
    });
    setTimeout(() => { try { ctx.close(); } catch(_){} }, 1400);
  } catch(_){}
}

function AvisCard({ r }) {
  if (!r || !r.ok) return null;
  const tiers = r.tiers||[];
  const [ask, setAsk] = useState(false);
  const [qr, setQr] = useState(false);
  const [copied, setCopied] = useState(false);
  const [revOpen, setRevOpen] = useState(false);
  // Date précise depuis le timestamp Google (au lieu du vague "il y a moins d'une semaine").
  const revWhen = (v) => v.time ? relDay(new Date(v.time*1000).toISOString().slice(0,10)) : (v.when||'');
  const latest = Array.isArray(r.latest) ? r.latest : [];
  const notifyAvis = (ch) => api('/api/invite', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ who: meName(), channel: ch }) }).catch(()=>{});
  const copyMsg = async () => { try{ await navigator.clipboard.writeText(REVIEW_MSG); }catch(_){} notifyAvis('avis-copy'); setCopied(true); setTimeout(()=>setCopied(false),1800); };
  // Partage natif iOS/Android : ouvre le menu du téléphone avec TOUTES les apps (Snap, Insta, WhatsApp, SMS…).
  // C'est le seul moyen d'envoyer vers Snap/Insta : ces apps n'acceptent pas de message pré-rempli via un lien.
  const shareMsg = async () => {
    notifyAvis('avis-share');
    // On met le message ET le lien dans `text` (sans `url` séparé) : sinon iOS ne garde que l'url et jette le message.
    try { if (navigator.share) { await navigator.share({ text: REVIEW_SHARE_TEXT + '\n\n' + GOOGLE_REVIEW }); return; } } catch(_){}
    copyMsg(); // pas de partage natif (ex. ordi) -> on copie le message en secours
  };
  // ── Jauge PROGRESSIVE : on vise un palier à la fois (30 → 50 → 100) ──
  const sortedTiers = [...tiers].sort((a,b)=>a.seuil-b.seuil);
  const allDone = sortedTiers.length && sortedTiers.every(t=>t.unlocked);
  const doneTiers = sortedTiers.filter(t=>t.unlocked);
  const nextTier = sortedTiers.find(t=>!t.unlocked) || null;
  const floor = doneTiers.length ? doneTiers[doneTiers.length-1].seuil : 0;        // dernier palier atteint
  const target = nextTier ? nextTier.seuil : (sortedTiers.length ? sortedTiers[sortedTiers.length-1].seuil : 0);
  const span = Math.max(1, target - floor);
  const inBand = Math.max(0, Math.min(span, r.gained - floor));
  const pct = allDone ? 100 : Math.round(inBand/span*100);
  // Remplissage animé depuis 0 à l'ouverture de la carte
  const [barW, setBarW] = useState(0);
  const gaugeRef = useRef(null);
  // La barre part TOUJOURS de 0 et se remplit quand la carte avis arrive à l'écran (au scroll). Elle se vide quand on s'éloigne -> le remplissage se rejoue à chaque passage.
  useEffect(()=>{
    const el = gaugeRef.current;
    if (!el || typeof IntersectionObserver === 'undefined') { const t=setTimeout(()=>setBarW(pct), 150); return ()=>clearTimeout(t); }
    const io = new IntersectionObserver((ents)=>{
      if (ents.some(e=>e.isIntersecting)) setTimeout(()=>setBarW(pct), 120);
      else setBarW(0);
    }, { threshold: 0.4 });
    io.observe(el);
    return ()=>io.disconnect();
  }, [pct]);
  // Fête quand un palier vient d'être franchi (mémorisé en local : ne se relance pas à chaque rechargement)
  const [celebrate, setCelebrate] = useState(null);
  useEffect(()=>{
    try {
      const seen = Number(localStorage.getItem('avis_tiers_seen') ?? '-1');
      if (seen >= 0 && doneTiers.length > seen) { const tier=doneTiers[doneTiers.length-1]; setCelebrate(tier); milestoneFx(avisMilestoneCfg(tier.seuil, tier.prime)); setTimeout(()=>setCelebrate(null), 7000); }
      localStorage.setItem('avis_tiers_seen', String(doneTiers.length));
    } catch(_){}
  }, [doneTiers.length]);
  return <div style={{ position:'relative', background:T.surface, border:`1px solid ${T.line}`, borderRadius:18, padding:16, marginBottom:14, overflow:'hidden' }}>
    {celebrate && <BigCelebration {...avisMilestoneCfg(celebrate.seuil, celebrate.prime)} onClose={()=>setCelebrate(null)} />}
    <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:10 }}>
      <span style={{ ...hk, fontWeight:800, fontSize:14 }}>⭐ Avis Google</span>
      <span style={{ ...ax, fontWeight:900, fontSize:26, color:T.amber }}>{r.total}<span style={{ ...hk, fontSize:12, color:T.muted, fontWeight:600 }}> au total</span></span>
    </div>
    <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', marginBottom:6 }}>
      <span style={{ ...mo, fontSize:11, color:T.muted, textTransform:'uppercase' }}>{(allDone||!nextTier)?'Tous les paliers atteints':`Prochain palier · +${nextTier.prime}€`}</span>
      <span style={{ ...ax, fontWeight:800, color:T.text }}>{r.gained}<span style={{ color:T.muted, fontWeight:600 }}>/{target}</span></span>
    </div>
    <div ref={gaugeRef} style={{ position:'relative', height:9, borderRadius:8, background:'var(--track)', marginBottom:10 }}>
      <div className="gauge-fill" style={{ width:`${barW}%`, height:'100%', borderRadius:8, background:allDone?'#22c55e':'linear-gradient(90deg,#f59e0b,#fcd34d)' }} />
    </div>
    <div style={{ display:'flex', gap:8, flexWrap:'wrap', marginBottom:6 }}>
      {sortedTiers.map((t,i)=>{ const isTarget = !t.unlocked && nextTier && t.seuil===nextTier.seuil; return (
        <span key={i} style={{ ...mo, fontSize:10.5, fontWeight:800, padding:'3px 8px', borderRadius:999,
          background:t.unlocked?'#13371f':(isTarget?T.amberDim:'var(--track)'),
          color:t.unlocked?'#4ade80':(isTarget?T.amber:T.muted),
          border:isTarget?'1px solid rgba(246,194,84,0.5)':'1px solid transparent' }}>{t.unlocked?'✅':(isTarget?'🎯':'🔒')} {t.seuil} = {t.prime}€</span>);
      })}
    </div>
    <div style={{ ...hk, fontSize:11.5, color:T.faint, marginBottom:12 }}>{(allDone||!r.next)?`🎉 Tous les paliers débloqués, +${r.prime}€ chacun !`:`Plus que ${r.next.remaining} avis pour débloquer +${r.next.prime}€ chacun`}</div>
    <button onClick={()=>setAsk(true)} style={{ width:'100%', ...btn(T.amber, '#1a1205'), padding:'11px 0', fontSize:13.5 }}>📣 Demander un avis</button>
    {latest.length>0 && <div style={{ marginTop:14 }}>
      <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.faint, marginBottom:8 }}>DERNIERS AVIS REÇUS</div>
      <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
        {(revOpen ? latest : latest.slice(0,1)).map((v,i)=>(
          <div key={i} style={{ background:T.elev, border:`1px solid ${T.line}`, borderRadius:12, padding:'10px 12px' }}>
            <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:8, marginBottom:4 }}>
              <span style={{ ...hk, fontSize:13, fontWeight:700, color:T.text, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{v.author||'Client'}</span>
              <span style={{ ...mo, fontSize:11, color:T.amber, flex:'0 0 auto' }}>{'★'.repeat(Math.max(0,Math.round(v.rating||0)))}<span style={{ color:T.faint }}>{revWhen(v)?` · ${revWhen(v)}`:''}</span></span>
            </div>
            {v.text && <div style={{ ...hk, fontSize:12.5, color:T.muted, lineHeight:1.45 }}>{v.text.length>160?v.text.slice(0,160)+'…':v.text}</div>}
          </div>
        ))}
      </div>
      {latest.length>1 && <button onClick={()=>setRevOpen(o=>!o)} style={{ width:'100%', marginTop:8, background:'transparent', border:'none', cursor:'pointer', ...hk, fontSize:12.5, fontWeight:700, color:T.amber }}>{revOpen?'Réduire ▲':`Dérouler les ${latest.length-1} autres ▼`}</button>}
    </div>}
    {ask && <Sheet onClose={()=>setAsk(false)}>
      {sheetTitle('⭐ Récolter un avis Google')}
      <button onClick={()=>{ notifyAvis('avis-qr'); setAsk(false); setQr(true); }} style={{ width:'100%', ...gbtn(), padding:'15px 0', fontSize:15.5, marginBottom:12 }}>📱 Montrer le QR code (face à face)</button>
      <div style={{ ...hk, fontSize:13.5, color:T.muted, lineHeight:1.55, marginBottom:14 }}>Ou envoie-lui le message sur WhatsApp, ou partage-le ailleurs. Chaque avis rapproche l'équipe de la prime 💚</div>
      <div style={{ background:T.elev, border:`1px solid ${T.line}`, borderRadius:12, padding:12, ...hk, fontSize:13, color:T.text, lineHeight:1.5, whiteSpace:'pre-wrap', maxHeight:'26vh', overflowY:'auto', marginBottom:12 }}>{REVIEW_MSG}</div>
      <a href={'https://wa.me/?text='+encodeURIComponent(REVIEW_MSG)} target="_blank" rel="noopener" onClick={()=>notifyAvis('avis-wa')} style={{ display:'block', textAlign:'center', textDecoration:'none', ...gbtn(), padding:'15px 0', fontSize:15.5, marginBottom:9 }}>📲 Envoyer sur WhatsApp</a>
      <div style={{ display:'flex', gap:9, marginBottom:9 }}>
        <button onClick={shareMsg} style={{ flex:1, ...obtn, padding:'12px 0' }}>📤 Partager</button>
        <button onClick={copyMsg} style={{ flex:1, ...obtn, padding:'12px 0', color:copied?GREEN:T.text, borderColor:copied?GREEN:T.line }}>{copied?'✓ Copié':'Copier'}</button>
      </div>
      <a href={GOOGLE_REVIEW} target="_blank" rel="noopener" style={{ display:'block', textAlign:'center', textDecoration:'none', ...obtn, padding:'12px 0', color:T.amber, borderColor:'rgba(246,194,84,0.4)' }}>⭐ Ouvrir le lien d'avis</a>
    </Sheet>}
    {qr && <div onClick={()=>setQr(false)} style={{ position:'fixed', inset:0, zIndex:90, background:'rgba(0,0,0,0.85)', display:'flex', alignItems:'center', justifyContent:'center', padding:24 }}>
      <div onClick={e=>e.stopPropagation()} style={{ background:'#fff', borderRadius:24, padding:'26px 24px', textAlign:'center', maxWidth:360, width:'100%', boxShadow:'0 20px 60px rgba(0,0,0,0.5)' }}>
        <div style={{ ...ax, fontWeight:900, fontSize:20, color:'#0C0D10', marginBottom:4 }}>Scanne pour nous noter ⭐</div>
        <div style={{ ...hk, fontSize:13.5, color:'rgba(12,13,16,0.6)', marginBottom:16 }}>Vise ce QR code avec ton appareil photo, ça ouvre direct l'avis Google.</div>
        <img src="/qr-avis.png" alt={'QR avis Google '+BRAND} style={{ width:'100%', maxWidth:280, aspectRatio:'1', display:'block', margin:'0 auto', borderRadius:12 }} />
        <div style={{ ...mo, fontSize:11, letterSpacing:1, color:'rgba(12,13,16,0.45)', marginTop:14 }}>{BRAND_FULL.toUpperCase()}</div>
        <button onClick={()=>setQr(false)} style={{ width:'100%', marginTop:18, border:'none', borderRadius:14, padding:'13px 0', background:'#0C0D10', color:'#fff', ...hk, fontSize:15, fontWeight:700, cursor:'pointer' }}>Fermer</button>
      </div>
    </div>}
  </div>;
}

// ════════ TICKET À GRATTER — offre aléatoire de la semaine pour l'entourage ════════
// Pool pondéré : gagnant à tous les coups, gros lots rares (marge maîtrisée). w = poids (chance relative).
const SCRATCH_POOL = [
  { id:'frais',       label:"Frais d'inscription offerts (57€)",        tier:'common', w:10 },
  { id:'essai',       label:"Séance d'essai gratuite + bilan",          tier:'common', w:10 },
  { id:'sem1',        label:"1 semaine d'essai gratuite",               tier:'common', w:9 },
  { id:'m1_20',       label:"-20% sur ton 1er mois",                    tier:'common', w:9 },
  { id:'moins10',     label:"-10€ sur ton inscription",                 tier:'common', w:8 },
  { id:'bilan',       label:"Bilan Anovator offert (balance à impédancemètre)", tier:'common', w:8 },
  { id:'duo',         label:"Pass duo : amène qui tu veux gratuit 1 semaine", tier:'common', w:7 },
  { id:'m1_30',       label:"-30% sur ton 1er mois",                    tier:'common', w:7 },
  { id:'j15',         label:"15 jours offerts",                         tier:'common', w:6 },
  { id:'frais_coach', label:"Frais offerts + 1 séance coaching",        tier:'common', w:6 },
  { id:'j10',         label:"10 jours offerts",                         tier:'common', w:6 },
  { id:'coach2',      label:"2 séances coaching découverte offertes",   tier:'common', w:5 },
  { id:'m1_50',       label:"-50% sur ton 1er mois",                    tier:'common', w:5 },
  { id:'m2_25',       label:"-25% sur tes 2 premiers mois",             tier:'common', w:5 },
  { id:'sem3',        label:"3 semaines offertes",                      tier:'rare',   w:3 },
  { id:'mois1',       label:"1 mois offert",                            tier:'rare',   w:3 },
  { id:'mois1_coach', label:"1 mois offert + bilan coaching",           tier:'rare',   w:2 },
  { id:'mois2',       label:"2 mois offerts",                           tier:'jackpot',w:1 },
  { id:'mois3',       label:"3 mois offerts",                           tier:'jackpot',w:1 },
];
const TIER = {
  common:  { badge:'OFFRE',      col:T.text,  bg:'var(--track)' },
  rare:    { badge:'RARE ✨',    col:T.amber, bg:'rgba(246,194,84,0.12)' },
  jackpot: { badge:'JACKPOT 🎰', col:GREEN,   bg:'rgba(34,197,94,0.14)' },
};
function scratchWeekKey(){ const d=new Date(); const o=new Date(Date.UTC(d.getFullYear(),d.getMonth(),d.getDate())); const day=o.getUTCDay()||7; o.setUTCDate(o.getUTCDate()+4-day); const ys=new Date(Date.UTC(o.getUTCFullYear(),0,1)); const wk=Math.ceil((((o-ys)/86400000)+1)/7); return o.getUTCFullYear()+'-W'+wk; }
// RNG seedé : même (user+semaine) -> même tirage, toujours. Impossible de re-rouler en vidant le cache.
function seededRng(str){ let h=1779033703 ^ str.length; for(let i=0;i<str.length;i++){ h=Math.imul(h ^ str.charCodeAt(i),3432918353); h=h<<13 | h>>>19; } return ()=>{ h=Math.imul(h ^ h>>>16,2246822507); h=Math.imul(h ^ h>>>13,3266489909); return ((h ^= h>>>16)>>>0)/4294967296; }; }
// Tirage déterministe (offre + code) à partir du prénom et de la semaine.
function scratchTicket(name, wk){ const rng=seededRng((name||'?')+'|'+wk); const tot=SCRATCH_POOL.reduce((s,o)=>s+o.w,0); let r=rng()*tot; let off=SCRATCH_POOL[0]; for(const o of SCRATCH_POOL){ if((r-=o.w)<0){ off=o; break; } } const A='ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; const pfx=(BRAND.match(/[A-Za-z0-9]/g)||['G']).slice(0,2).join('').toUpperCase(); const code=pfx+'-'+Array.from({length:4},()=>A[Math.floor(rng()*A.length)]).join(''); return { offer:off, code }; }

function ScratchCard({ name }) {
  const wk = scratchWeekKey();
  const DKEY = '4ds_scr_'+(name||'')+'_'+wk+'_done'; // état "gratté" par user + semaine
  const ticket = useState(()=> scratchTicket(name, wk))[0]; // 1 ticket déterministe / user / semaine
  const offer = ticket.offer, code = ticket.code;
  const tier = TIER[offer.tier] || TIER.common;
  const [done, setDone] = useState(()=>{ try{ return localStorage.getItem(DKEY)==='1'; }catch(_){ return false; } });
  const [boom, setBoom] = useState(false); // animation de dévoilement (déclenchée au grattage, pas si déjà fait)
  const cvRef = useRef(null), wrapRef = useRef(null);
  const glow = offer.tier==='jackpot' ? 'rgba(34,197,94,.45)' : offer.tier==='rare' ? 'rgba(246,194,84,.4)' : 'rgba(255,255,255,.22)';
  const confetti = useState(()=> Array.from({ length: offer.tier==='jackpot'?20:offer.tier==='rare'?12:0 }, ()=>({
    left: Math.round(Math.random()*98), delay: (Math.random()*0.3).toFixed(2), dur: (0.8+Math.random()*0.5).toFixed(2),
    col: ['#e30613','#ffffff','#22C55E','#F6C254','#7FC8FF'][Math.floor(Math.random()*5)],
  })))[0];
  const msg = `Salut ! Je bosse à ${BRAND_FULL}, ouverte 24h/24. Cette semaine je peux te faire gagner : ${offer.label}. Tu présentes ce code à la salle : ${code}, valable jusqu'à dimanche. Tu passes ? ${GYM_PHONE_DISPLAY}, dis que tu viens de la part de ${name}.`;
  const notifyShare = ()=> api('/api/invite', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ who:name, channel:'scratch', detail:`${offer.label} (code ${code})` }) }).catch(()=>{});

  useEffect(()=>{
    if(done) return;
    const cv=cvRef.current, wrap=wrapRef.current; if(!cv||!wrap) return;
    const w=wrap.clientWidth, h=wrap.clientHeight, dpr=window.devicePixelRatio||1;
    cv.width=w*dpr; cv.height=h*dpr; cv.style.width=w+'px'; cv.style.height=h+'px';
    const ctx=cv.getContext('2d'); ctx.scale(dpr,dpr);
    // Panneau DORÉ (même or que le bouton "Demander un avis" : #F6C254)
    const g=ctx.createLinearGradient(0,0,w,h); g.addColorStop(0,'#FBE6A6'); g.addColorStop(.5,'#F6C254'); g.addColorStop(1,'#E0A93D');
    ctx.fillStyle=g; ctx.fillRect(0,0,w,h);
    // reflets dorés diagonaux
    ctx.strokeStyle='rgba(255,255,255,0.16)'; ctx.lineWidth=2.5; for(let x=-h;x<w;x+=13){ ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x+h,h); ctx.stroke(); }
    ctx.fillStyle='#5A3E0C'; ctx.textAlign='center'; ctx.font='900 17px "Hanken Grotesk", sans-serif';
    ctx.fillText('GRATTE ICI', w/2, h/2-2); ctx.font='700 11.5px "Hanken Grotesk", sans-serif'; ctx.fillStyle='rgba(90,62,12,0.78)'; ctx.fillText('découvre ton offre surprise', w/2, h/2+17);
    ctx.globalCompositeOperation='destination-out';
    let drawing=false, last=null, ended=false;
    const pos=(e)=>{ const r=cv.getBoundingClientRect(); const t=e.touches&&e.touches[0]?e.touches[0]:e; return { x:t.clientX-r.left, y:t.clientY-r.top }; };
    // pinceau bien plus gros -> on gratte beaucoup moins longtemps
    const scratch=(p)=>{ ctx.beginPath(); ctx.arc(p.x,p.y,34,0,7); ctx.fill(); if(last){ ctx.lineWidth=66; ctx.lineCap='round'; ctx.beginPath(); ctx.moveTo(last.x,last.y); ctx.lineTo(p.x,p.y); ctx.stroke(); } last=p; };
    const finish=()=>{ if(ended) return; ended=true; removeListeners();
      setBoom(true);
      cv.style.pointerEvents='none'; cv.style.animation='scratchcover .45s ease forwards';
      setTimeout(()=>{ try{ localStorage.setItem(DKEY,'1'); }catch(_){} setDone(true); }, 430);
    };
    // révélation dès ~38% gratté, vérifiée en continu pendant le grattage
    const check=()=>{ try{ const px=ctx.getImageData(0,0,cv.width,cv.height).data; let clear=0,n=0; for(let i=3;i<px.length;i+=4*60){ n++; if(px[i]===0) clear++; } if(clear/n>0.38) finish(); }catch(_){} };
    let mc=0;
    const down=(e)=>{ drawing=true; last=null; scratch(pos(e)); e.preventDefault(); };
    const move=(e)=>{ if(!drawing) return; scratch(pos(e)); if((++mc%5)===0) check(); e.preventDefault(); };
    const up=()=>{ if(!drawing) return; drawing=false; check(); };
    cv.addEventListener('mousedown',down); cv.addEventListener('mousemove',move); window.addEventListener('mouseup',up);
    cv.addEventListener('touchstart',down,{passive:false}); cv.addEventListener('touchmove',move,{passive:false}); window.addEventListener('touchend',up);
    function removeListeners(){ cv.removeEventListener('mousedown',down); cv.removeEventListener('mousemove',move); window.removeEventListener('mouseup',up); cv.removeEventListener('touchstart',down); cv.removeEventListener('touchmove',move); window.removeEventListener('touchend',up); }
    return removeListeners;
  },[done]);

  const revealed = done; // contenu visible
  return <div style={{ position:'relative', overflow:'hidden', background:`linear-gradient(135deg, ${T.voltDim}, ${T.surface})`, border:`1px solid var(--volt-line)`, borderRadius:18, padding:16, marginBottom:14 }}>
    <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:4 }}>
      <span style={{ ...ax, fontWeight:800, fontSize:15 }}>🎟️ Ticket à gratter</span>
      <span style={{ ...mo, fontSize:9.5, fontWeight:800, color:T.muted, border:`1px solid ${T.line}`, borderRadius:999, padding:'2px 8px' }}>SEMAINE</span>
    </div>
    <div style={{ ...hk, fontSize:12.5, color:T.muted, lineHeight:1.5, marginBottom:11 }}>Une offre surprise par semaine pour tes proches. Gratte, puis envoie le code.</div>
    <div ref={wrapRef} className={boom?'scratch-reveal':undefined} style={{ '--reveal-glow':glow, position:'relative', height:120, borderRadius:14, overflow:'hidden', marginBottom:revealed?12:0, border:`1px solid ${T.line}`, background:tier.bg, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', textAlign:'center', padding:'0 14px' }}>
      <span style={{ ...mo, fontSize:9.5, fontWeight:800, color:tier.col, marginBottom:5, letterSpacing:1 }}>{tier.badge}</span>
      <span style={{ ...ax, fontSize:18, fontWeight:900, color:tier.col, lineHeight:1.2 }}>{offer.label}</span>
      {revealed && <span style={{ ...mo, fontSize:12, color:T.text, marginTop:7, background:'var(--track)', padding:'3px 10px', borderRadius:8, letterSpacing:1, animation: boom?'scratchpop .5s cubic-bezier(.2,1.3,.5,1) .12s both':undefined }}>CODE : {code}</span>}
      {boom && <div className="scratch-shine" />}
      {boom && confetti.map((c,i)=>(<span key={i} className="confetti" style={{ left:c.left+'%', background:c.col, animationDelay:c.delay+'s', animationDuration:c.dur+'s' }} />))}
      {!revealed && <canvas ref={cvRef} style={{ position:'absolute', inset:0, cursor:'pointer', touchAction:'none' }} />}
    </div>
    {revealed && <a href={'https://wa.me/?text='+encodeURIComponent(msg)} target="_blank" rel="noopener" onClick={notifyShare} style={{ display:'block', textAlign:'center', textDecoration:'none', ...gbtn(), padding:'12px 0', animation: boom?'scratchpop .5s cubic-bezier(.2,1.3,.5,1) .22s both':undefined }}>📲 Envoyer à un proche</a>}
  </div>;
}

// ════════ CLASSEMENT ════════
function RankInner({ d }) {
  const [mode, setMode] = useState('cagnotte'); // 'cagnotte' (€ du mois) | 'perf' (taux de transfo)
  const val = (p) => mode==='cagnotte' ? (p.total||0) : (p.taux||0);
  const big = (p) => mode==='cagnotte' ? `${p.total||0}€` : `${p.taux||0}%`;
  const sub = (p) => mode==='cagnotte' ? `${p.signed||0} inscrits ce mois` : `${p.convertis||0} convertis / ${p.traites||0}`;
  const lb = [...(d.leaderboard||[])].sort((a,b)=> val(b)-val(a) || (b.signed||0)-(a.signed||0));
  lb.forEach((p,i)=> p._rank = i+1);
  const top3 = lb.slice(0,3);
  const podium = top3.length>=3 ? [top3[1],top3[0],top3[2]] : top3;
  const heights=[104,134,88];
  const meRow = lb.find(x=>x.me);
  const isLeader = !!meRow && meRow._rank===1;
  const anyValue = lb.some(p=>val(p)>0);
  const toFirst = (lb.length&&meRow) ? Math.max(0, val(lb[0])-val(meRow)) : 0;
  const unit = mode==='cagnotte' ? '€' : ' pts';
  const pct = Math.min(100, Math.round((d.cagnotte/d.goal)*100));
  const Seg = ({ id, label }) => (
    <button onClick={()=>setMode(id)} style={{ flex:1, border:'none', cursor:'pointer', borderRadius:10, padding:'9px 0', ...hk, fontSize:13.5, fontWeight:700, background:mode===id?T.volt:'transparent', color:mode===id?T.ink:T.muted }}>{label}</button>
  );
  if (!d.leaderboard || !d.leaderboard.length) return <div style={{ padding:'70px 20px', textAlign:'center', color:T.muted, ...hk }}>Chargement du classement…</div>;
  return (
    <div style={{ padding:'0 20px' }}>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:14 }}>
        <div style={{ ...ax, fontSize:26, fontWeight:800, letterSpacing:-0.5, color:T.onBg }}>Classement</div>
        <span style={{ ...mo, fontSize:12, color:T.onBgMuted, border:`1px solid ${T.line}`, padding:'6px 12px', borderRadius:999 }}>Équipe</span>
      </div>
      <div style={{ display:'flex', gap:6, background:T.surface, borderRadius:12, padding:4, marginBottom:16, border:`1px solid ${T.line}` }}>
        <Seg id="cagnotte" label="💰 Cagnotte" /><Seg id="perf" label="🎯 Performance" />
      </div>
      {mode==='perf' && <div style={{ ...hk, fontSize:11.5, color:T.onBgMuted, marginTop:-8, marginBottom:14, textAlign:'center' }}>Taux de transformation (convertis ÷ traités), équitable quels que soient les jours travaillés.</div>}

      <div style={{ display:'flex', alignItems:'flex-end', justifyContent:'center', gap:10, marginBottom:18 }}>
        {podium.map((p,k)=>{ const top=p._rank===1; return <div key={p.name} style={{ flex:1, display:'flex', flexDirection:'column', alignItems:'center' }}>
          <Avatar name={p.name} size={top?56:44} ring={p.me} rank={p._rank} />
          <div style={{ ...hk, fontSize:12, fontWeight:700, marginTop:8, color:p.me?T.volt:T.onBg }}>{p.me?'Toi':p.name.split(' ')[0]}</div>
          <div style={{ ...ax, fontSize:18, fontWeight:800, color:top?T.volt:T.onBg }}>{big(p)}</div>
          <div style={{ width:'100%', height:heights[k]??90, background:top?T.voltDim:T.surface, border:`1px solid ${top?'var(--volt-line)':T.line}`, borderRadius:'12px 12px 0 0', marginTop:8, display:'flex', alignItems:'flex-start', justifyContent:'center', paddingTop:10 }}><span style={{ ...ax, fontSize:30, fontWeight:800, color:top?T.volt:T.faint }}>{p._rank}</span></div>
        </div>; })}
      </div>
      <div style={{ background:T.surface, border:'1px solid var(--volt-line)', borderRadius:18, padding:16, marginBottom:14 }}>
        <div style={{ display:'flex', alignItems:'center', gap:10 }}><Icon name="bolt" size={18} color={T.volt} />
          <span style={{ ...hk, fontSize:14, fontWeight:600 }}>{!anyValue ? <b style={{ color:T.volt }}>La compétition démarre 🔥</b> : isLeader ? <b style={{ color:T.volt }}>Tu es en tête 🔥</b> : <>Plus que <b style={{ color:T.volt }}>{toFirst}{unit}</b> pour la 1<sup>re</sup> place</>}</span></div>
      </div>
      <SectionLabel>Équipe {BRAND}</SectionLabel>
      <div style={{ display:'flex', flexDirection:'column', gap:8, paddingBottom:20 }}>
        {lb.map(p=>(<div key={p.name} style={{ display:'flex', alignItems:'center', gap:12, padding:'10px 14px', borderRadius:14, background:p.me?T.voltDim:T.surface, border:`1px solid ${p.me?'var(--volt-line)':T.line}` }}>
          <span style={{ ...ax, fontSize:16, fontWeight:800, width:22, color:p.me?T.volt:T.faint }}>{p._rank}</span>
          <Avatar name={p.name} size={34} ring={p.me} rank={p._rank} />
          <div style={{ flex:1 }}><div style={{ ...hk, fontSize:14, fontWeight:600 }}>{p.me?`Toi · ${p.name}`:p.name}</div><div style={{ ...hk, fontSize:11, color:T.muted }}>{sub(p)}</div></div>
          <span style={{ ...ax, fontSize:16, fontWeight:800, color:p.me?T.volt:T.text }}>{big(p)}</span>
        </div>))}
      </div>
    </div>
  );
}

// Onglet combiné Primes + Classement (toggle en haut).
function ScreenGains({ d, go, H }) {
  const [view, setView] = useState('primes');
  return <Screen active="gains" go={go}>
    <div style={{ padding:'12px 20px 0' }}>
      <div style={{ display:'flex', gap:6, background:T.surface, borderRadius:12, padding:4, border:`1px solid ${T.line}` }}>
        {[['primes','💰 Mes primes'],['rank','🏆 Classement']].map(([id,lb])=>(
          <button key={id} onClick={()=>setView(id)} style={{ flex:1, border:'none', cursor:'pointer', borderRadius:9, padding:'10px 0', ...hk, fontSize:14, fontWeight:700, background:view===id?T.volt:'transparent', color:view===id?T.ink:T.muted }}>{lb}</button>
        ))}
      </div>
    </div>
    {view==='primes' ? <WalletInner d={d} H={H} /> : <RankInner d={d} />}
  </Screen>;
}

// ════════ SÉANCES D'ESSAI DU JOUR ════════
function ScreenSessions({ d, go, H }) {
  const today = todayISO();
  const tomorrow = plusDaysISO(1);
  const all = (d.seances||[]).filter(l=> l.seanceEssai).map(l=>({ ...l, jour:l.seanceEssai.slice(0,10) }));
  const future = all.filter(l=> l.jour >= today).sort((a,b)=> a.seanceEssai.localeCompare(b.seanceEssai));
  // À RATTRAPER : séances passées sans inscription (no-show ou venu mais pas signé). Plus récent en haut.
  const past = all.filter(l=> l.jour < today && !l.converted).sort((a,b)=> b.seanceEssai.localeCompare(a.seanceEssai));
  const todays = future.filter(l=> l.jour===today);
  const tomos = future.filter(l=> l.jour===tomorrow);
  const later = future.filter(l=> l.jour>tomorrow);
  const [waLead, setWaLead] = useState(null);
  const heureOf = (l)=> hasTime(l.seanceEssai) ? new Date(l.seanceEssai).toLocaleTimeString('fr-FR',{hour:'2-digit',minute:'2-digit'}).replace(':','h') : '';
  const dayPhrase = (l)=> l.jour===today ? "aujourd'hui" : l.jour===tomorrow ? 'demain' : 'le '+fmtFR(l.jour).slice(0,5);
  // 2 propositions courtes, ton 4D (tutoiement, pas de tournure bancale), date-aware (veille / jour J).
  const waMsgs = (l)=>{ const p=l.name.split(' ')[0]; const h=heureOf(l); const j=dayPhrase(l); const at=h?` à ${h}`:'';
    if (l.jour < today) return [
      { label:'📵 Pas venu, on te replace', text:`Salut ${p}, c'est ${d.me} de ${BRAND}. On t'a loupé pour ta séance d'essai, ça arrive ! On te la replace quand tu veux, tu me dis quel jour t'arrange ?` },
      { label:'🔁 Toujours partant ?', text:`Salut ${p}, c'est ${d.me} de ${BRAND}. Toujours motivé pour ta séance d'essai offerte ? Je te bloque un créneau dès que tu me dis 💪` },
    ];
    return [
    { label:'✅ Confirmer (à envoyer la veille)', text:`Salut ${p}, c'est ${d.me} de ${BRAND_FULL}. Je te confirme ta séance d'essai ${j}${at}. On t'attend au ${GYM_ADDR}, viens comme tu es 💪` },
    { label:'📅 Rappel du jour J', text:`Salut ${p}, on se voit${h?` à ${h}`:''} tout à l'heure pour ta séance d'essai 💪 On t'attend, viens comme tu es. À toute !` },
  ]; };
  const Row = (l)=>{
    const isPast = l.jour < today && !l.converted;
    const canVenu = l.jour <= today && !l.converted; // bouton "Venu" dès le jour J (et les jours passés), jamais pour le futur
    const heure = isPast ? fmtFR(l.jour).slice(0,5) : (heureOf(l) || '—');
    return <div key={l.id} onClick={()=>H.openLead(l, all)} style={{ display:'flex', alignItems:'center', gap:12, padding:'12px 0', borderBottom:`1px solid ${T.line}`, cursor:'pointer' }}>
      <div style={{ width:54, textAlign:'center', flex:'0 0 auto' }}><div style={{ ...ax, fontSize:isPast?14:17, fontWeight:900, color:isPast?RED:T.onBg }}>{heure}</div></div>
      <div style={{ flex:1, minWidth:0 }}>
        <div style={{ ...hk, fontSize:14.5, fontWeight:700, color:T.onBg, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{l.name}</div>
        <div style={{ display:'flex', gap:6, marginTop:3, flexWrap:'wrap' }}>
          {l.seanceProspect
            ? <span style={{ ...mo, fontSize:9.5, fontWeight:700, padding:'2px 7px', borderRadius:999, background:'rgba(124,58,237,0.16)', color:'#a78bfa' }}>📅 Calendly</span>
            : l.assigne
              ? <span style={{ ...mo, fontSize:9.5, fontWeight:700, padding:'2px 7px', borderRadius:999, background:T.amberDim, color:T.amber }}>👤 {l.assigne}</span>
              : <span style={{ ...mo, fontSize:9.5, fontWeight:700, padding:'2px 7px', borderRadius:999, background:'rgba(124,58,237,0.16)', color:'#a78bfa' }}>🧩 à prendre</span>}
          {l.converted && <span style={{ ...mo, fontSize:9.5, fontWeight:700, padding:'2px 7px', borderRadius:999, background:'rgba(34,197,94,0.16)', color:GREEN }}>✅ inscrit</span>}
          {isPast && <span style={{ ...mo, fontSize:9.5, fontWeight:700, padding:'2px 7px', borderRadius:999, background:'rgba(239,68,68,0.16)', color:RED }}>🔴 pas venu ?</span>}
          {!isPast && l.seanceConfirmee && !l.converted && <span style={{ ...mo, fontSize:9.5, fontWeight:700, padding:'2px 7px', borderRadius:999, background:'rgba(34,197,94,0.16)', color:GREEN }}>✓ confirmée</span>}
        </div>
      </div>
      <div style={{ display:'flex', gap:6, flex:'0 0 auto', alignItems:'center' }} onClick={e=>e.stopPropagation()}>
        {canVenu && <button onClick={async()=>{
            let note;
            if (isPast) { note = window.prompt('Séance passée non notée le jour J pour « '+l.name+' ».\nQue s\'est-il passé ? (obligatoire)\nEx : venu pas encore signé · pas venu (no-show) · oublié de noter, il a signé…'); if (note===null) return; if (!note.trim()) { alert('Une note est obligatoire pour une séance traitée en retard.'); return; } }
            else { if(!confirm('Marquer « '+l.name+' » comme VENU ?\nLa séance est résolue et le lead repart en relance le lendemain pour le closer. Ce n\'est PAS une inscription.')) return; }
            try{ await act(l.id,'rattrape',{ date: plusDaysISO(1), note: note?note.trim():undefined }); H&&H.reload&&H.reload(); }catch(e){ alert('Échec : '+e.message); }
          }} title="Venu" style={{ height:38, padding:'0 11px', borderRadius:11, border:'none', cursor:'pointer', background:isPast?RED:GREEN, color:isPast?'#fff':GINK, ...hk, fontWeight:800, fontSize:12.5 }}>{isPast?'❓ Venu ?':'✅ Venu'}</button>}
        <button onClick={()=>H.call(l)} title="Appeler" style={{ width:38, height:38, borderRadius:11, border:'none', cursor:'pointer', background:canVenu?T.surface:GREEN, border:canVenu?`1px solid ${T.line}`:'none', display:'flex', alignItems:'center', justifyContent:'center' }}><Icon name="phone" size={17} color={canVenu?T.onBg:GINK} /></button>
        <button onClick={()=>setWaLead(l)} title="WhatsApp" style={{ width:38, height:38, borderRadius:11, border:'none', cursor:'pointer', background:'#25D366', display:'flex', alignItems:'center', justifyContent:'center' }}><Icon name="whatsapp" size={19} color="#fff" /></button>
      </div>
    </div>;
  };
  return <Screen active="sessions" go={go}>
    <div style={{ padding:'0 20px 24px' }}>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:4 }}>
        <div style={{ ...ax, fontSize:26, fontWeight:800, letterSpacing:-0.5, color:T.onBg }}>Séances d'essai</div>
        <button onClick={()=>H.walkin()} style={{ ...btn(T.volt, 'var(--volt-ink)'), padding:'9px 14px', fontSize:13.5, fontWeight:800, borderRadius:999 }}>➕ Sur place</button>
      </div>
      <div style={{ ...hk, fontSize:13, color:T.onBgMuted, marginBottom:18 }}>Qui vient tester la salle. Confirme la veille ET le matin par WhatsApp : un essai pas confirmé, c'est un essai qui ne vient pas.</div>

      {past.length>0 && <>
        <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'10px 14px', borderRadius:12, marginBottom:8, background:RED, color:'#fff' }}>
          <span style={{ ...hk, fontWeight:800, fontSize:14 }}>🔴 À rattraper</span>
          <span style={{ ...mo, fontSize:12, fontWeight:700, background:'rgba(255,255,255,0.22)', borderRadius:999, padding:'2px 9px' }}>{past.length}</span>
        </div>
        <div style={{ ...hk, fontSize:12, color:T.onBgMuted, margin:'0 0 10px' }}>Séances passées sans inscription (no-show ou pas encore signé). Ce sont des leads chauds : relance-les, propose un nouveau créneau.</div>
        <div style={{ marginBottom:24 }}>{past.map(Row)}</div>
      </>}

      <SectionLabel>🎯 Aujourd'hui · {todays.length}</SectionLabel>
      {todays.length ? <div style={{ marginBottom:24 }}>{todays.map(Row)}</div>
        : <div style={{ ...hk, fontSize:13.5, color:T.faint, background:T.surface, border:`1px solid ${T.line}`, borderRadius:14, padding:'18px 16px', textAlign:'center', marginBottom:24 }}>Aucune séance d'essai aujourd'hui.</div>}

      {tomos.length>0 && <><SectionLabel>Demain · {tomos.length}</SectionLabel><div style={{ marginBottom:24 }}>{tomos.map(Row)}</div></>}
      {later.length>0 && <><SectionLabel>Plus tard · {later.length}</SectionLabel><div>{later.map(Row)}</div></>}
    </div>
    {waLead && <Sheet onClose={()=>setWaLead(null)}>
      {sheetTitle(waLead.jour < today ? '📵 Relancer (pas venu)' : 'Confirmer la séance')}
      <div style={{ ...hk, fontSize:13, color:T.muted, marginBottom:12 }}>Choisis le message, WhatsApp s'ouvre pré-rempli.</div>
      <div style={{ display:'flex', flexDirection:'column', gap:10 }}>
        {waMsgs(waLead).map(t=>(
          <a key={t.label} href={'https://wa.me/'+(waLead.phone||'').replace(/[^0-9]/g,'').replace(/^0/,'33')+'?text='+encodeURIComponent(t.text)} target="_blank" rel="noopener" onClick={()=>{ if(waLead.jour >= today && waLead.seanceEssai && !waLead.seanceConfirmee) act(waLead.id,'confirmseance').catch(()=>{}); else if(waLead.jour < today) act(waLead.id,'rattrape',{ date: plusDaysISO(1) }).then(()=>H&&H.reload&&H.reload()).catch(()=>{}); setWaLead(null); }} style={{ ...obtn, textAlign:'left', padding:'12px 14px', textDecoration:'none', display:'block' }}>
            <div style={{ ...hk, fontSize:13.5, fontWeight:700, marginBottom:5 }}>{t.label}</div>
            <div style={{ ...hk, fontSize:12.5, color:T.muted, lineHeight:1.45 }}>{t.text}</div>
          </a>
        ))}
      </div>
    </Sheet>}
  </Screen>;
}

// ════════ NOUVEAUTÉS (changelog) ════════
// Le plus récent en haut. À chaque grosse mise à jour, on ajoute un bloc.
// Date relative façon App Store pour les versions ("aujourd'hui", "hier", "il y a 6 j", "il y a 2 sem.").
function relDay(iso){ if(!iso) return ''; const d=new Date(iso+'T12:00:00'); const days=Math.round((Date.now()-d.getTime())/86400000); if(days<=0) return "aujourd'hui"; if(days===1) return 'hier'; if(days<7) return `il y a ${days} j`; const w=Math.floor(days/7); if(w<5) return `il y a ${w} sem.`; const mo=Math.floor(days/30); if(mo<12) return `il y a ${mo} mois`; return `il y a ${Math.floor(days/365)} an(s)`; }
const CHANGELOG = [
  { iso:'2026-06-22', date:'22 juin 2026',
    staff:[],
    news:[
      'Nouveau (admin only) : la Caisse arrive dans ton pilotage — carte « 💶 Caisse ». Tu saisis un encaissement en 10 sec (montant, article, paiement, client, source) et tu as le dashboard : total jour/semaine/mois/année, graphe par jour, répartition par moyen de paiement (pour réconcilier ta caisse), par article et par vendeur, panier moyen, et comparaison vs mois précédent. Hors abonnements MRR (le registre légal reste Deciplus). Visible par toi seul, pas par le staff. Tu peux corriger une erreur : touche une ligne d\'encaissement pour la modifier ou la supprimer. Périodes Jour/Semaine/Mois/Année + plage perso (date à date), et le graphe est tactile (touche une barre = date + montant du jour).',
      'Technique : base Notion « Encaissements 4D GYM » (Article/Montant/Paiement/Vendeur/Source/Nom/Prénom/Date). core.mjs addEncaissement + getCaisseDashboard (agrégats calculés depuis l\'année en cache 60s ; branche démo si pas de token). Routes /api/caisse GET+POST gardées admin (x-user===ADMIN). UI CaisseSheet (sélecteur période, hero+delta façon Stripe, graphe façon Square, répartitions) + CaisseAdd (saisie). Changelog rendu robuste (bloc sans ligne masqué dans la vue). À activer 1× : connecter l\'intégration « 4D Gym Phoning » à la base Notion. Env ENCAISSEMENTS_DB_ID (posé en local + Vercel). Modif/suppression : getCaisseDashboard renvoie d.entries (lignes de la période) ; updateEncaissement (PATCH) + deleteEncaissement (archive Notion, DELETE) gardés admin ; CaisseAdd passe en mode édition au tap d\'une ligne. Perf : getCaisseDashboard lit par PLAGE (mois courant + précédent pour Jour/Semaine/Mois ; année entière seulement pour la vue Année) + cache 3 min → ~1,6 s au lieu de ~5,4 s sur les vues courantes ; le cumul année ne s\'affiche que sur l\'onglet Année.',
    ],
  },
  { iso:'2026-06-21', date:'21 juin 2026',
    staff:[
      'Nouveau : un bouton 🐛 en bas à gauche, sur tous les écrans. Si un truc bug, tu le touches : l\'app prend la capture de l\'écran toute seule, tu écris vite ce qui cloche, et ça part direct au manager avec la capture. Plus besoin de tout réexpliquer.',
      'Suivi des inscrits revu : chaque nouvel inscrit affiche ses 4 relances du 1er mois (S1→S4 : J+2, J+7, J+14, J+30) d\'un coup d\'œil. Celle du jour est en vert, tu l\'envoies en 1 touche ; une fois faite, elle passe en gris ✓ et la suivante se débloque toute seule à sa date. La fiche ne disparaît plus : tu vois où en est chacun. Les 4 messages sont réécrits dans un ton posé (un commercial qui prend des nouvelles, sans rien supposer).',
      'Le bouton du manager dans son pilotage s\'appelle maintenant « 📣 Push une notif » (avant « Message ») : c\'est plus clair, ça t\'envoie bien une notification sur ton téléphone.',
    ],
    news:[
      'Signaler un bug : bouton flottant 🐛 (BugButton, data-bughide pour s\'auto-exclure de la capture) → capture l\'écran courant via html2canvas (chargé à la demande depuis unpkg, déjà autorisé par la CSP ; JPEG 0.82, scale ≤2) + description libre + écran/version/appareil/dernières erreurs JS (tampon BUG_ERRORS branché sur window error/unhandledrejection/console.error). POST /api/bug (checkAuth) → core.sendBug envoie sur le Telegram du manager (sendPhoto multipart fait main, repli sendMessage si pas d\'image). Caché pendant le PushGate et l\'onboarding. Réutilise TELEGRAM_BOT_TOKEN/CHAT_ID déjà en place (mêmes notifs Telegram que l\'audit), aucun env à ajouter. Cadrage de la capture corrigé ensuite : width/windowWidth = largeur réelle de l\'écran + onclone neutralisant le conteneur centré (#root max-width:480 + margin:auto) → capture plein écran, non décalée.',
      'Suivi inscrits (SuiviSheet) : carte refondue en stepper 4 étapes (SUIVI_STEPS côté client = miroir core.mjs). État dérivé de suiviEtape — faite (i<idx) gris ✓, du jour (i==idx && suiviDue) vert, prochaine non due contour volt, future pointillé. mark() n\'enlève plus la carte : il avance suiviEtape en place + load(true) silencieux pour recaler la date, et ne retire la fiche qu\'au bouclage (après J+30). CTA WhatsApp = « Relance {étape} ». Titre « Inscrits à suivre », section « À relancer aujourd\'hui ».',
      'Pilotage admin : bouton « 📣 Message » renommé « 📣 Push une notif » (le libellé dit ce qu\'il fait vraiment). En-tête rendu robuste pour le libellé plus long : titre BRAND · prénom tronqué avec … si besoin (minWidth:0 + ellipsis), groupe bouton+avatar en flexShrink:0 / whiteSpace:nowrap → plus aucun risque de casse sur petit écran. Composeur (« Message à l\'équipe ») et envoi inchangés.',
    ],
  },
  { iso:'2026-06-20', date:'20 juin 2026',
    staff:[
      'Moins d\'interruptions : l\'animation « nouveau record » ne saute plus à chaque petit progrès. On garde la grande fête quand la cagnotte passe un vrai palier.',
      'Onglet Primes épuré : la carte du haut affiche toujours ta cagnotte totale, en plus sobre (la grosse carte animée reste sur l\'accueil).',
      'Guide d\'accueil revu : plus court et plus clair (8 étapes au lieu de 12), et aux couleurs de l\'app.',
      'Compteurs fiables : quand tu ouvres « relances à rappeler » ou « adhérents à suivre », la liste correspond bien au chiffre affiché (même en mode « voir comme un membre »).',
      'Le message du manager : dès que tu appuies sur « OK, c\'est parti », il disparaît pour de bon, même si tu rouvres l\'app sur un autre téléphone. Et s\'il reste sans réponse, il s\'efface tout seul au bout d\'une semaine.',
      'Le manager peut maintenant t\'écrire un message quand il veut (bouton 📣 dans son pilotage) : ça s\'affiche sur ton accueil et en notif.',
    ],
    news:[
      'Sécurité (audit pré-commercialisation) : en-têtes posés sur chaque réponse (server.mjs) — CSP (frame-ancestors none = anti-clickjacking), X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy, Permissions-Policy, HSTS. Anti-brute-force login renforcé : seuil 8→5, cooldown 15 min, + délai croissant par essai raté (350ms × n, plafonné 2,5s) qui freine même en serverless (prouvé : 5 essais → lock, 6e bloqué).',
      'Login durci : on TAPE son prénom (plus de liste de noms exposée), et /api/config (équipe + photos) est désormais protégé par mot de passe (plus renvoyé avant connexion).',
      'Minification : shared.jsx/app.jsx pré-compilés + minifiés par esbuild (shared.min.js/app.min.js), Babel retiré du navigateur (≈3 Mo de moins + démarrage plus rapide + warning disparu). CSP durcie en conséquence : unsafe-eval retiré. Workflow : `npm run bundle` après modif des .jsx (npm start/dev rebuildent tout seuls). Vérifié : app + login OK depuis le bundle, 0 erreur console, 0 violation CSP.',
      'Pilotage admin : bouton « 📣 Message » dans l\'en-tête → composeur « Message à l\'équipe » (destinataires en chips, toute l\'équipe par défaut + « Tout le monde / Aucun ») qui réutilise /api/nudgeavis (bandeau MESSAGE DU MANAGER + push). La relance avis devient un raccourci pré-rempli du même composeur (relTargets/relTitle).',
      'Nudge manager : « OK c\'est parti » appelle POST /api/nudge/clear (clearNudge, x-user) qui vide « Nudge avis » dans la fiche du vendeur → expiration à l\'interaction, ne réapparaît plus (autre appareil / cache vidé). Non déclenché en vue admin (lecture seule). L\'expiration 7 jours côté client reste le filet pour ceux qui n\'ouvrent jamais l\'app.',
      'Correctif compteur/liste (vue admin & « voir comme ») : CategorySheet et SuiviSheet interrogeaient meName() (admin réel) au lieu de effName() (identité affichée) → divergence badge/liste. Alignés sur effName(). SuiviSheet démarre sur all=isAdmin (le badge pilotage utilise all=1). Vérifié live : suivi admin 0 → 42 (= badge), relancesequipe Enzo 41 → 35 (= badge).',
      'Message du manager (nudge avis) : expire après 7 jours côté client (Date.parse(at)). Les nudges « bravo 30 avis » obsolètes ont été effacés dans Notion.',
      'Guide (Tour) : 12 → 8 étapes (fusion des étapes redondantes : taux/entonnoir, avis paliers+comment, ticket à gratter retirés du tour). Montants prime (MRR/comptant) et paliers avis tirés de la config via props (plus de 5/10 ni 30/50/100 en dur). Couleurs 100% thématisées var(--volt)/--volt-line (fini les glows rouges 225,29,42 / #ff8694 sur un accent non-rouge), TourCeleb compris.',
      'Gamification : RecordOverlay plein écran retiré (il se déclenchait à chaque nouveau plus-haut, ça dévalorisait l\'effet) ; record toujours suivi en silence (cag_record) ; palier cagnotte +100€ et palier avis restent les seuls grands moments.',
      'WalletInner : carte cagnotte « héros » remplacée par un en-tête sobre (montantLabel/montantAnim restent réactifs au filtre Tout/Validé/En attente), montant thématisé var(--volt) au lieu du lime #CBFB45 (white-label).',
      'Décision actée : la carte cagnotte reste verte = couleur sémantique « argent » (fixe, non thématisée par l\'accent), au même titre que le rouge danger et le vert succès.',
    ],
  },
  { iso:'2026-06-19', date:'19 juin 2026',
    staff:[
      'Nouveau : suivi des nouveaux adhérents (le 1er mois décide tout). Dès qu\'un adhérent est converti, il entre dans une boucle de suivi — tu prends de ses nouvelles sur WhatsApp à J+2, J+7, J+14 et J+30, avec un message déjà prêt à personnaliser. Tu reçois une notif le matin et une pastille sur l\'accueil. Après chaque message, tu dis s\'il répond, est silencieux ou à risque.',
      'Notifications : alerte dès qu\'un nouveau lead arrive, rappel des relances à faire le matin, et rappel d\'envoyer le message des séances d\'essai (la veille à 19h, le jour J à 9h).',
      'Pas de réponse : tu choisis quand te rappeler (15 min, 30 min, 1h ou 2h) et tu reçois une notification au bon moment.',
      'Une séance d\'essai posée sort le lead des relances : il passe dans l\'onglet Séances.',
      'Les vieux leads (plus d\'un mois) à mettre sur Deciplus sont partagés par toute l\'équipe : chacun peut les traiter.',
      'Les relances en retard d\'un collègue sont visibles par tout le monde. Plus on attend, plus le lead refroidit et part ailleurs : rappelle-les vite.',
      'Passer un lead sur Deciplus ne le retire plus à son vendeur. Une note garde la trace de qui l\'a fait et quand.',
      'Tableau de bord fiabilisé : un vrai entonnoir (Leads, puis RDV pris, puis Convertis), compté depuis le lancement de l\'app, avec des chiffres cohérents. La liste qui s\'ouvre au clic correspond pile au chiffre.',
      'File de leads réorganisée (nouveaux à appeler en haut) et nouveau type de conversion « Retour offert » (3 mois offerts, prime au passage en abonnement payant).',
      'Écran d\'accueil allégé : un seul rappel à la fois en haut, le plus urgent, au lieu d\'en empiler plusieurs.',
      'Profil rangé : menu plus clair, doublon retiré, et le bouton WhatsApp est désormais le même partout dans l\'app.',
      'Vocabulaire unifié : on dit « conversion » partout (fini le mot « closing »), comme le bouton « Converti ».',
      'Mode jour plus lisible : les montants et infos en orange ne sont plus délavés sur fond blanc.',
      'Boutons plus faciles à toucher : les flèches de la fiche lead et les libellés de la barre du bas.',
    ],
    news:[
      'Push : scanNewLeads (chaque minute), scanRelances (10h), scanRelancesDue (rappel intra-journée), scanSeancesVeille/JourJ (19h/9h, garde parisHour, crons 0 17,18 et 0 7,8 UTC). Anti-doublon via colonnes Notion.',
      'getStats / getTeamStats : entonnoir sur la cohorte depuis STATS_START ; RDV = séance non annulée / RDV pris / En cours / Oui ; Convertis = leads de la cohorte convertis, donc Leads ≥ RDV ≥ Convertis. Primes restent du mois (monthConvs).',
      'getLeadsByCategory : anciens (global), relancesequipe (relances dues des 7 derniers jours d\'un autre vendeur), rdv/convertis/encours/perdus alignés sur la cohorte (liste = chiffre affiché).',
      'deciplus retiré de CLAIM_ACTIONS (pas de réattribution) + note auto postComment. Note de version sans emoji, regroupée par jour.',
      'Revue : push relances du matin borné à 30 j (plus de vieux backlog) ; relances équipe n\'affiche plus un rappel dont l\'heure n\'est pas passée ; plusieurs textes clarifiés (orphelins = "personne ne les appelle", messages d\'urgence).',
      'Audit pré-commercialisation : couleur d\'app par défaut corrigée (fini la pastille fantôme « Volt » au 1er lancement) ; entrée menu « Notifications / Bientôt » périmée supprimée ; panneau de test réservé au dev (isDev) et non plus à tout admin (white-label).',
      'Cohérence visuelle : accueil vendeur = un seul bandeau prioritaire (manager > notifs > avis) ; Pilotage admin = KPI hiérarchisés (Conversions en héros + taux), AvisCard retirée (action vendeur) ; rouge alerte unifié #ef4444 ; vert WhatsApp unifié #25D366 + vraie icône WhatsApp (fini l\'emoji à côté des icônes SVG).',
      'Audit expert (5 domaines, vérif adverse) : white-label = 4D/Le Thor retirés du header vendeur, QR avis, messages WhatsApp bug/aide, préfixe code ticket et citations coaching dérivés de BRAND ; prime de conversion ConvSheet lit primeMrr/primeComptant (plus de 5/10 en dur).',
      'Robustesse : gardes ||[] sur les spreads leaderboard/conversions, garde r.next dans AvisCard, /api/stats et /api/conversions dégradables (un hoquet Notion ne vide plus toute l\'app).',
      'A11y mode jour : --amber foncé en jour (#B45309, WCAG AA), montants primes lisibles, libellés nav 11px + plus contrastés ; rouge alerte unifié sur le bandeau relances équipe ; vocabulaire « closing » → « conversion » sur tout l\'UI ; AvisCard en double retirée du Classement.',
      'Suivi adhérents 1er mois : colonnes Notion "Étape suivi" (J+2/J+7/J+14/J+30/Terminé), "Prochain suivi le", "Dernier état suivi", "Suivi notifié" (créées via scripts/ensure-suivi-columns.mjs). getSuivis (du vendeur ou ?all=1 admin, fenêtre 40j, robuste si colonnes absentes), scanSuivis (cron /api/push/suivis à 9h), action buildProps "suivi" (avance l\'étape + état). La conversion entre l\'adhérent dans la boucle (best-effort) ; l\'annulation l\'en sort. Envoi WhatsApp manuel pré-rempli (l\'API WhatsApp automatique pourra se brancher sur la même mécanique).',
    ],
  },
  { v:'1.8', iso:'2026-06-18', date:'18 juin 2026', title:'Anims par palier, relance avis & ménage',
    staff:[
      'Anim plein écran par palier (avis 30/50/100, cagnotte tous les 100€, record battu)',
      'Notifs OS pour les relances, à activer dans Profil',
      'Patron : push de relance avis en 1 touche depuis le cockpit',
      'Patron : primes dissociées (Prime CA + Prime avis) + Total à payer en bas',
      'Séance jour J non traitée → bouton rouge "Venu ?" pour ne plus l\'oublier',
      'Séance jour J traitée en retard : laisse une note (le patron voit qui et pourquoi)',
      'Classement équipe : le patron ne compte plus (vendeurs vs vendeurs)',
      'Bouton "Passé sur Deciplus" sur les fiches (sort de la file, pas une conversion)',
      'Leads 30-60j pas convertis : "À inscrire sur Deciplus" (vendeur) et "à vérifier" sur le cockpit admin',
      'Admin : suppression de lead (archivage Notion, récupérable)',
      'Écran "Demander un avis" refait (QR + WhatsApp en gros)',
      'Relance avis (manager) : vraie fenêtre avec grand champ texte',
      'Carte cagnotte animée, le chiffre suit le bouton',
      'App verrouillée en portrait sur mobile',
    ],
    news:[
      'Push interne manager → vendeur via champ Notion "Nudge avis" (bandeau + refresh 90s)',
      'Cockpit admin : carte "Primes à verser" — Prime CA + Prime avis (équipe) + Total à payer',
      'getTeamStats : totalAvisPrime (prime avis × nb vendeurs hors patron) + totalAPayer',
      'Bouton "Venu" disponible jour J (jour ≤ aujourd\'hui), exclu pour les séances futures',
      'Séance passée non traitée : bouton "Venu ?" en rouge (alerte oubli)',
      'Séance jour J en retard : note obligatoire + suivi admin par vendeur',
      'CLAIM_EXCLUDE retire le patron du classement vendeurs',
      'Anims palier refondues (BigCelebration + son distinct), panneau de test admin',
      'Barre d\'avis : remplissage IntersectionObserver à chaque entrée',
      'Relance avis manager : Sheet + textarea (au lieu d\'un prompt tronqué)',
      'Avis/push : message manager plus lisible (+ référencement Google), notifs présentées comme obligatoires staff',
      'DesktopBlock minimal : icône + titre "Application réservée au mobile"',
      '"Mes primes" harmonisé avec l\'accueil (En cours = conversions + prime avis)',
      'screen.orientation.lock(\'portrait\') sur l\'app',
      '/api/deletelead : archivage Notion réversible, admin only',
      'Action "deciplus" : Statut=Deciplus, Conversion=Non, hors file',
    ],
    fixes:[
      'Audit api/ : 22 endpoints morts supprimés, 2 colonnes Notion retirées',
      'Cagnotte : un seul bloc, montant suit le bouton (Tout/Validé/En attente)',
      'Séances à rattraper : bouton "Venu" (la personne est venue mais pas encore inscrite) → sort de "à rattraper" et repart en relance le lendemain dans tes leads pour la closer. Et le WhatsApp "pas venu" fait pareil pour les no-shows. Attribué à toi, plus de liste polluée',
      'Leads anciens : fenêtre resserrée à 30-60j (ignore le vieux backlog 2025)',
      'Suppression lead masquée en mode "Voir comme un membre"',
    ],
  },
  { v:'1.7', iso:'2026-06-17', date:'17 juin 2026', title:'No-shows, avis en direct & orphelins',
    staff:[
      'Avis par palier : jauge sur le prochain seuil + fête à chaque palier',
      'Alerte si tu n\'as demandé aucun avis de la semaine',
      'Historique des paies mois par mois (auto "Payé" le 1er)',
      'Anim "nouveau record" plein écran quand tu bats ta meilleure prime',
      'Cagnotte par palier (300 → 400 → 500…)',
      'Séance sans personne → "à prendre"',
      '"À rattraper" : les no-shows relançables en 1 touche',
      'Dernier avis Google visible dans la carte (auteur, note, message)',
      '"Orphelins" sur l\'écran Leads (auto-attribution dès que tu agis)',
      'App uniquement mobile / tablette',
    ],
    news:[
      'Section "À rattraper" : no-shows visibles 21 jours + messages WhatsApp dédiés',
      'Cockpit admin : bloc "À traiter · équipe" (nouveaux + relances + séances)',
      'Cockpit : manque à gagner des mineurs en CA (377€/an chacun)',
      'Dernier avis Google sous la jauge (API Places, refresh 30 min)',
      'Auto-attribution des orphelins dès la 1ère action',
      'Jauge progressive : 1 palier à la fois, anim depuis 0',
    ],
    fixes:[
      'Cagnotte : jauge par palier de 100€ au lieu d\'un objectif fixe',
      '"Conversion globale" → "Taux de closing équipe" (vrai % sous 100)',
      '"staff" → "à prendre" sur les séances non attribuées',
      '"À appeler" : ne compte plus les leads avec essai déjà booké',
      'Conversions sur lead orphelin comptées pour le closeur',
    ],
  },
  { v:'1.4', iso:'2026-06-16', date:'16 juin 2026', title:'Cockpit admin, séances & sécurité',
    staff:[
      'Cockpit patron : chiffres équipe + à traiter en temps réel',
      'Changer ton mot de passe depuis l\'app',
      'Onglet "Séances" du jour avec confirmation',
      '"Sur place" pour ceux qui débarquent',
      'Bouton "Mineur" : capture coordonnées, hors stats',
      'Lien de parrainage unique par inscription',
      'Messages WhatsApp séance prêts',
      'Partage des avis Snap / Insta / WhatsApp',
      'Email du lead visible sur la fiche',
      'Couleur "Vert" de retour',
      'Carte cagnotte refaite + ticket à gratter plus rapide',
    ],
    news:[
      'Cockpit admin : chiffres du mois + à traiter + perf équipe + avis',
      '"Primes" → "Équipe" côté admin (classement + activité)',
      'Changement mdp self-service depuis Profil',
      'Suivi mineurs : case Notion, hors stats, compteur annuel',
      'Onglet Séances : Calendly + fixées, appel + WhatsApp en 1 touche',
      '"Prospect sur place" (passé / essai / inscrit direct)',
      'Parrainage à la conversion : page filleul → Calendly + Telegram',
      'WhatsApp séance dans chaque fiche (réservé / veille / jour J)',
      'Partage natif des avis (Snap, Insta, WhatsApp, SMS)',
      '"Voir comme un membre" en lecture seule, sans compter de connexion',
      'Refresh auto à l\'ouverture + toutes les 90 s',
      'Calendly pré-rempli depuis les optins du site',
    ],
    fixes:[
      'Séances : converti masqué, essai sur "RDV pris" remonte',
      'Séances annulées (case ou Calendly) masquées, re-réservation réactive',
      'Annuler une conversion : lead remis exactement dans son état',
      '"Convertis" affiche les vraies conversions du mois (plafond 100%)',
      'Lien parrainage unique même avec 2 prénoms identiques',
      'Prénom/Nom inversés corrigés sur les 4 optins',
      'Conversions cliquables dans "Primes" → ouvre la fiche',
      'Croix de fermeture ajoutée sur toutes les fenêtres',
    ],
  },
  { v:'1.0', iso:'2026-06-15', date:'15 juin 2026', title:'Version 1.0 · Lancement',
    staff:[
      'Nouvelle app phoning, design sombre',
      'File du jour, appel, WhatsApp, notes',
      'Cagnotte du mois + primes au closeur',
      'Classement Cagnotte (€) et Performance (taux)',
      'Avis Google + ticket à gratter hebdo',
      'Séances Calendly automatiques',
      'Photo, couleur, mode jour / nuit',
    ],
    news:[
      'App "staff" complète sur leads.4dgymclub.com (PWA)',
      'Phoning : file du jour + appel + WhatsApp + statuts',
      'Cagnotte animée + primes MRR 5€ / comptant 10€',
      '2 taux (RDV pris, closing) + entonnoir cliquable',
      'Classement Cagnotte (€) + Performance (taux)',
      'Avis Google par paliers 30 / 50 / 100',
      'Récolter un avis : QR + WhatsApp',
      'Ticket à gratter hebdo',
      'Séances Calendly auto → "Séances fixées"',
      'Rappels intra-journée + relances par date',
      '"À reprendre" : leads de conseillers absents',
      'Photo + couleur + mode jour/nuit',
      'Guide interactif au 1er lancement',
      'Notif Telegram au patron',
      'Suivi connexions équipe (admin)',
    ],
    fixes:[
      'WhatsApp PWA : n\'éjecte plus l\'app',
      'Relances : 1 lead = 1 entrée',
      'Performance : compte même les vieux closings',
      'Cagnotte : inclut les primes avis (objectif 300€)',
      'Mode jour : éléments lisibles',
      'Séances : distingue Calendly et fixée employé',
    ],
  },
];

// ════════ CHANGER MOT DE PASSE ════════
function PwdSheet({ who, onClose }) {
  const [oldP, setOldP] = useState('');
  const [newP, setNewP] = useState('');
  const [newP2, setNewP2] = useState('');
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState('');
  const [done, setDone] = useState(false);
  const submit = async () => {
    setErr('');
    if (!oldP) { setErr('Saisis ton ancien mot de passe.'); return; }
    if (newP.length < 4) { setErr('Le nouveau mot de passe doit faire au moins 4 caractères.'); return; }
    if (newP !== newP2) { setErr('Les deux nouveaux mots de passe ne correspondent pas.'); return; }
    setBusy(true);
    try {
      const r = await api('/api/changepwd', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ oldPwd: oldP, newPwd: newP }) });
      if (r && r.ok) { setDone(true); }
      else { setErr(r?.reason === 'ancien mdp incorrect' ? 'Ancien mot de passe incorrect.' : (r?.reason || 'Erreur, réessaie.')); }
    } catch(e) { setErr(e.message || 'Erreur réseau.'); }
    finally { setBusy(false); }
  };
  return <Sheet onClose={onClose}>
    {sheetTitle('🔑 Changer mon mot de passe')}
    {done ? (
      <div style={{ textAlign:'center', padding:'20px 0' }}>
        <div style={{ fontSize:44, marginBottom:12 }}>✅</div>
        <div style={{ ...ax, fontSize:18, fontWeight:800, marginBottom:8 }}>Mot de passe mis à jour</div>
        <div style={{ ...hk, fontSize:14, color:T.muted, marginBottom:20 }}>Utilise-le à ta prochaine connexion.</div>
        <button onClick={onClose} style={{ width:'100%', ...gbtn(), padding:'14px 0' }}>Fermer</button>
      </div>
    ) : (<>
      <div style={{ ...hk, fontSize:13.5, color:T.muted, marginBottom:20 }}>Ton nouveau mot de passe sera actif immédiatement, partout.</div>
      {[['Ancien mot de passe', oldP, setOldP], ['Nouveau mot de passe', newP, setNewP], ['Confirmer le nouveau', newP2, setNewP2]].map(([label, val, set]) => (
        <div key={label} style={{ marginBottom:14 }}>
          <div style={{ ...hk, fontSize:12, color:T.muted, marginBottom:5 }}>{label}</div>
          <input type="password" value={val} onChange={e=>set(e.target.value)} placeholder="••••••••"
            style={{ width:'100%', background:T.elev, border:`1px solid ${T.line}`, borderRadius:13, padding:'13px 15px', color:T.text, fontSize:16, outline:'none', boxSizing:'border-box' }} />
        </div>
      ))}
      {err && <div style={{ ...hk, fontSize:13, color:'#f87171', marginBottom:12 }}>{err}</div>}
      <button onClick={submit} disabled={busy} style={{ width:'100%', ...gbtn(), padding:'14px 0', opacity:busy?0.6:1, marginTop:4 }}>{busy ? 'Enregistrement…' : 'Changer mon mot de passe'}</button>
    </>)}
  </Sheet>;
}

// ════════ PROFIL ════════
function ScreenProfile({ d, go, H, onLogout }) {
  const sortedM = [...(d.leaderboard||[])].sort((a,b)=>(b.total||0)-(a.total||0));
  const myRank = sortedM.findIndex(x=>x.me)+1; const rank = myRank>0 ? '#'+myRank : '—';
  const [acc, setAcc] = useState(()=>currentAccent().name);
  const [mode, setModeS] = useState(getMode());
  const [info, setInfo] = useState(null);
  const [news, setNews] = useState(false);
  const [notesDetail, setNotesDetail] = useState(false); // admin : vue détaillée (debug) vs vue staff
  const [openV, setOpenV] = useState(null);               // carte dépliée (numéro de version)
  const [activity, setActivity] = useState(null);
  const [teamStats, setTeamStats] = useState(null);
  const [seenSheet, setSeenSheet] = useState(null);
  const [pwdSheet, setPwdSheet] = useState(false);
  const [photoVer, setPhotoVer] = useState(0);
  const [photoBusy, setPhotoBusy] = useState(false);
  const [testBig, setTestBig] = useState(null); // admin : aperçu plein écran d'une animation de palier
  const fileRef = useRef(null);
  const isAdmin = d.me === ADMIN;
  const isDev = d.me === 'Dev'; // outils de test : réservés au dev, jamais visibles par un gérant client (white-label)
  // Rejoue une animation plein écran (avec son distinct) pour la tester.
  const runTest = (cfg) => { milestoneFx(cfg); setTestBig(null); setTimeout(()=>setTestBig(cfg), 30); };
  const RECORD_TEST = { emoji:'🏆', kicker:'NOUVEAU RECORD', big:'420 €', sub:'Ta meilleure prime, jamais atteinte. Énorme 💪', accent:'#CBFB45', notes:[523.25,659.25,783.99,1046.5,1318.5], chord:true };
  const canSeeNotes = isAdmin || d.me === 'Dev';
  const onPickPhoto = (e) => {
    const file = e.target.files && e.target.files[0]; if(!file) return;
    if(!/^image\//.test(file.type)) { alert('Choisis une image.'); return; }
    setPhotoBusy(true);
    const img = new Image();
    img.onload = () => {
      const S=128, cv=document.createElement('canvas'); cv.width=S; cv.height=S; const ctx=cv.getContext('2d');
      const r=Math.max(S/img.width, S/img.height), w=img.width*r, h=img.height*r;
      ctx.drawImage(img, (S-w)/2, (S-h)/2, w, h);
      let q=0.6, url=cv.toDataURL('image/jpeg', q);
      while(url.length>42000 && q>0.3){ q-=0.1; url=cv.toDataURL('image/jpeg', q); }
      URL.revokeObjectURL(img.src);
      api('/api/photo', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ dataUrl:url }) })
        .then(res=>{ if(res&&res.ok){ window.__photos = { ...(window.__photos||{}), [d.me]: url }; setPhotoVer(v=>v+1); } else { alert('Échec : '+((res&&res.reason)||'envoi')); } })
        .catch(()=>alert('Échec de l\'envoi'))
        .finally(()=>setPhotoBusy(false));
    };
    img.onerror = ()=>{ setPhotoBusy(false); alert('Image illisible.'); };
    img.src = URL.createObjectURL(file);
  };
  useEffect(()=>{ if(isAdmin){ api('/api/activity').then(a=>setActivity(a.items||[])).catch(()=>{}); } },[]);
  const s = d.stats || {};
  const objectifsBody = <div style={{ display:'flex', flexDirection:'column', gap:12 }}>
    <div><b style={{ color:T.volt }}>💰 Cagnotte</b><br/>Objectif <b>{d.goal} €</b> ce mois.</div>
    {s.doubleFrom > 0 && <div><b style={{ color:GREEN }}>🔥 Bonus ×2</b><br/>Dès <b>{s.doubleFrom} inscriptions</b>, tes primes doublent : MRR <b>{(s.primeMrr||5)*2}€</b>, comptant <b>{(s.primeComptant||10)*2}€</b>.</div>}
    <div><b style={{ color:T.amber }}>⭐ Avis Google (équipe)</b><br/>{(d.reviews?.tiers||[]).map(t=>`${t.seuil} avis = ${t.prime}€`).join(' · ') || '—'} pour chaque membre.</div>
  </div>;
  const menu = [
    { label:'🎯 Objectifs', onClick:()=>setInfo({ title:'Tes objectifs', body:objectifsBody }) },
    { label:'💸 Historique versements', onClick:()=>go('gains') },
    { label:'🗂 Leads archivés', onClick:()=>H.openCat('perdus','Leads archivés') },
    { label:'🔔 Activer les notifications', onClick:()=>enablePush() },
    { label:'🔑 Changer mon mot de passe', onClick:()=>setPwdSheet(true) },
    { label:'🧭 Revoir le guide', onClick:()=>H.replayTour() },
    { label:'📋 Notes de version', onClick:()=>setNews(true) },
    { label:'🐞 Signaler un bug', href:'https://wa.me/'+GYM_PHONE+'?text='+encodeURIComponent(`🐞 Bug app ${BRAND}\nDe : ${d.me}\nÉcran : \nCe qui s'est passé : \n`) },
    { label:'💬 Aide & contact manager', href:'https://wa.me/'+GYM_PHONE+'?text='+encodeURIComponent(`Salut, j'ai une question sur l'appli ${BRAND}`) },
  ];
  return <Screen active="me" go={go}>
    <div style={{ padding:'0 20px' }}>
      <div style={{ display:'flex', alignItems:'center', gap:14, marginBottom:24 }}>
        <div onClick={()=>!photoBusy && fileRef.current && fileRef.current.click()} style={{ position:'relative', cursor:'pointer', opacity:photoBusy?0.5:1 }}>
          <Avatar name={d.me} size={64} ring pulse key={photoVer} />
          <div style={{ position:'absolute', right:-2, bottom:-2, width:24, height:24, borderRadius:24, background:T.volt, border:`2px solid ${T.bg}`, display:'flex', alignItems:'center', justifyContent:'center' }}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--volt-ink)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
          </div>
          <input ref={fileRef} type="file" accept="image/*" onChange={onPickPhoto} style={{ display:'none' }} />
        </div>
        <div><div style={{ ...ax, fontSize:24, fontWeight:800, color:T.onBg }}>{d.me}</div><div style={{ ...hk, fontSize:13, color:T.onBgMuted }}>{photoBusy?'Envoi de la photo…':'Touche ta photo pour la changer'}</div></div>
      </div>
      <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:10, marginBottom:26 }}>
        {[['Prime du mois',fmt(d.cagnotte),T.volt],['Inscrits',String(d.inscrits),T.text],['Rang',rank,T.text]].map(([k,v,c])=>(<div key={k} style={{ background:T.surface, borderRadius:16, padding:'14px 12px', border:`1px solid ${T.line}` }}><div style={{ ...ax, fontSize:22, fontWeight:800, color:c }}>{v}</div><div style={{ ...hk, fontSize:11, color:T.muted, marginTop:2 }}>{k}</div></div>))}
      </div>
      {isAdmin && <>
        <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.onBgMuted, margin:'4px 0 10px' }}>ACTIVITÉ ÉQUIPE · ADMIN</div>
        <div style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:16, padding:'6px 14px', marginBottom:22 }}>
          {!activity && <div style={{ ...hk, fontSize:13, color:T.faint, padding:'12px 0' }}>Chargement…</div>}
          {activity && activity.map((m,i)=>{ const recent = m.lastSeen && (Date.now()-new Date(m.lastSeen).getTime() < 86400000); return (
            <div key={m.name} onClick={()=> (m.count>0||(m.pushLog&&m.pushLog.length)) && setSeenSheet(m)} style={{ display:'flex', alignItems:'center', gap:12, padding:'11px 0', borderBottom: i<activity.length-1?`1px solid ${T.line}`:'none', cursor: (m.count>0||(m.pushLog&&m.pushLog.length))?'pointer':'default' }}>
              <Avatar name={m.name} size={34} />
              <div style={{ flex:1, minWidth:0 }}>
                <div style={{ ...hk, fontSize:14, fontWeight:600 }}>{m.name}</div>
                <div style={{ ...hk, fontSize:11, color:T.faint }}>{m.count>0?`${m.count} connexion${m.count>1?'s':''}${m.count>0?' · voir ›':''}`:'pas encore connecté'}</div>
              </div>
              <div style={{ display:'flex', alignItems:'center', gap:6 }}>
                <span style={{ width:7, height:7, borderRadius:7, background: m.lastSeen?(recent?GREEN:T.amber):T.faint }} />
                <span style={{ ...hk, fontSize:12, color: m.lastSeen?T.muted:T.faint }}>{relTime(m.lastSeen)}</span>
              </div>
            </div>); })}
        </div>
        <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.onBgMuted, margin:'4px 0 10px' }}>VOIR L'INTERFACE D'UN MEMBRE · ADMIN</div>
        <div style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:16, padding:'6px 14px', marginBottom:22 }}>
          {(d.team||[]).filter(m=>m!==ADMIN).map((m,i,arr)=>(
            <div key={m} onClick={()=>H.setViewAs(m)} style={{ display:'flex', alignItems:'center', gap:12, padding:'11px 0', borderBottom:i<arr.length-1?`1px solid ${T.line}`:'none', cursor:'pointer' }}>
              <Avatar name={m} size={34} />
              <div style={{ flex:1, ...hk, fontSize:14, fontWeight:600 }}>{m}</div>
              <span style={{ ...hk, fontSize:12.5, fontWeight:700, color:'#a78bfa', display:'flex', alignItems:'center', gap:5 }}>👁 Voir ›</span>
            </div>
          ))}
          <div style={{ ...hk, fontSize:11, color:T.faint, padding:'8px 0 10px' }}>Lecture seule, sans compter de connexion.</div>
        </div>
        {isDev && <>
        <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.onBgMuted, margin:'4px 0 10px' }}>TEST · DEV</div>
        <div style={{ background:T.surface, border:`1px solid ${T.line}`, borderRadius:16, padding:'14px', marginBottom:22 }}>
          <div style={{ ...hk, fontSize:13, color:T.muted, marginBottom:12, lineHeight:1.5 }}>Rejoue chaque animation de palier (chacune a son look + son + vibration), pour voir ce que l'équipe verra.</div>
          <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.faint, marginBottom:8 }}>AVIS</div>
          <div style={{ display:'flex', gap:8, marginBottom:12 }}>
            <button onClick={()=>runTest(avisMilestoneCfg(30,50))} style={{ flex:1, ...obtn, padding:'12px 0' }}>⭐ 30</button>
            <button onClick={()=>runTest(avisMilestoneCfg(50,70))} style={{ flex:1, ...obtn, padding:'12px 0' }}>🌟 50</button>
            <button onClick={()=>runTest(avisMilestoneCfg(100,100))} style={{ flex:1, ...obtn, padding:'12px 0' }}>🏆 100</button>
          </div>
          <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.faint, marginBottom:8 }}>CAGNOTTE (palier de 100€)</div>
          <div style={{ display:'flex', gap:8, flexWrap:'wrap', marginBottom:12 }}>
            {[300,400,500,600].map(v=>(<button key={v} onClick={()=>runTest(cagMilestoneCfg(v,300))} style={{ flex:'1 1 22%', ...obtn, padding:'12px 0' }}>{cagMilestoneCfg(v,300).emoji} {v}€</button>))}
          </div>
          <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.faint, marginBottom:8 }}>RECORD</div>
          <button onClick={()=>runTest(RECORD_TEST)} style={{ width:'100%', ...gbtn(), padding:'13px 0', fontSize:14.5 }}>🏆 Tester le record battu</button>
          <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.faint, margin:'14px 0 8px' }}>NOTIF PUSH</div>
          <button onClick={async()=>{ try{ const r=await api('/api/push/relancetest',{method:'POST'}); alert(r&&r.ok?'✅ Notif "relances du jour" envoyée sur ton appareil.':'⚠ Échec : '+((r&&r.reason)||'active d\'abord tes notifications')); }catch(_){ alert('⚠ Échec de l\'envoi.'); } }} style={{ width:'100%', ...gbtn(), padding:'13px 0', fontSize:14.5 }}>🔁 M'envoyer la notif relances</button>
        </div>
        </>}
      </>}
      <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.onBgMuted, margin:'4px 0 10px' }}>AFFICHAGE</div>
      <div style={{ display:'flex', gap:8, background:T.surface, borderRadius:12, padding:4, marginBottom:22, border:`1px solid ${T.line}` }}>
        {[['night','🌙 Nuit'],['day','☀️ Jour']].map(([id,lb])=>(
          <button key={id} onClick={()=>{ setMode(id); setModeS(id); }} style={{ flex:1, border:'none', cursor:'pointer', borderRadius:9, padding:'10px 0', ...hk, fontSize:14, fontWeight:700, background:mode===id?T.volt:'transparent', color:mode===id?T.ink:T.muted }}>{lb}</button>
        ))}
      </div>
      <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.onBgMuted, margin:'4px 0 10px' }}>COULEUR DE L'APP</div>
      <div style={{ display:'flex', gap:12, marginBottom:22, flexWrap:'wrap' }}>
        {ACCENTS.map(a=>(<button key={a.name} title={a.name} onClick={()=>{ applyAccent(a); setAcc(a.name); }}
          style={{ width:40, height:40, borderRadius:999, background:a.hex, cursor:'pointer',
            border: acc===a.name?'3px solid #fff':`2px solid ${T.line}`, boxShadow: acc===a.name?`0 0 0 3px ${hexRgba(a.hex,0.4)}`:'none' }} />))}
      </div>
      <div style={{ ...hk, fontSize:11.5, color:T.onBgMuted, marginBottom:20 }}>Juste pour toi, sur ton téléphone.</div>

      <div style={{ display:'flex', flexDirection:'column', gap:2, marginBottom:20 }}>
        {menu.map(it=> it.href
          ? <a key={it.label} href={it.href} target="_blank" rel="noopener" style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'15px 0', borderBottom:`1px solid ${T.line}`, ...hk, fontSize:15, cursor:'pointer', color:T.onBg, textDecoration:'none' }}>{it.label}<Icon name="arrow" size={18} color={T.onBgMuted} /></a>
          : <div key={it.label} onClick={it.onClick} style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'15px 0', borderBottom:`1px solid ${T.line}`, ...hk, fontSize:15, cursor:'pointer', color:T.onBg }}>{it.label}<Icon name="arrow" size={18} color={T.onBgMuted} /></div>)}
      </div>
      <button onClick={onLogout} style={{ width:'100%', border:`1px solid ${T.line}`, background:'transparent', color:T.onBgMuted, borderRadius:14, padding:'14px 0', ...hk, fontSize:15, fontWeight:600, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', gap:8, marginBottom:20 }}><Icon name="logout" size={18} color={T.onBgMuted} />Se déconnecter</button>
    </div>
    {testBig && <BigCelebration {...testBig} onClose={()=>setTestBig(null)} />}
    {news && <Sheet onClose={()=>setNews(false)}>
      {sheetTitle('📋 Notes de version')}
      <div style={{ ...hk, fontSize:13, color:T.muted, marginBottom:14 }}>Les nouveautés de l'app, mise à jour après mise à jour.</div>
      {isAdmin && <div style={{ display:'flex', background:T.elev, borderRadius:11, padding:4, marginBottom:16 }}>
        {[[false,'👥 Vue staff'],[true,'🛠 Vue détaillée']].map(([id,lb])=>(
          <button key={String(id)} onClick={()=>setNotesDetail(id)} style={{ flex:1, border:'none', cursor:'pointer', borderRadius:8, padding:'8px 0', ...hk, fontSize:12.5, fontWeight:700, background:notesDetail===id?T.volt:'transparent', color:notesDetail===id?T.ink:T.muted }}>{lb}</button>
        ))}
      </div>}
      <div style={{ display:'flex', flexDirection:'column', maxHeight:'60vh', overflowY:'auto' }}>
        {CHANGELOG.map((v,i)=>{
          const detail = isAdmin && notesDetail;
          const lines = detail
            ? [...(v.news||[]).map(t=>({t,dim:false})), ...(v.fixes||[]).map(t=>({t:'🔧 '+t,dim:true}))]
            : (v.staff||v.news||[]).map(t=>({t,dim:false}));
          if (!lines.length) return null;   // bloc vide dans la vue courante (ex. nouveauté admin-only en vue staff) -> masqué
          const open = openV===v.iso;
          const shown = open ? lines : lines.slice(0,3);
          const rest = lines.length-3;
          return (
          <div key={i} style={{ padding:'4px 0 16px', borderBottom:i<CHANGELOG.length-1?`1px solid ${T.line}`:'none', marginBottom:16 }}>
            <div style={{ display:'flex', alignItems:'baseline', justifyContent:'space-between', marginBottom:10 }}>
              <span style={{ ...ax, fontSize:18, fontWeight:800, color:T.text }}>{v.date}</span>
              <span style={{ ...hk, fontSize:12.5, color:T.faint }}>{relDay(v.iso)}</span>
            </div>
            {shown.map((ln,k)=>(<div key={k} style={{ display:'flex', gap:8, ...hk, fontSize:13.5, color:ln.dim?T.muted:T.text, lineHeight:1.5, marginBottom:7 }}><span style={{ color:ln.dim?T.faint:T.volt }}>•</span><span>{ln.t}</span></div>))}
            {rest>0 && <span onClick={()=>setOpenV(open?null:v.iso)} style={{ ...hk, fontSize:13, fontWeight:700, color:T.volt, cursor:'pointer', display:'inline-block', marginTop:2 }}>{open?'moins':`plus (${rest})`}</span>}
          </div>);
        })}
      </div>
      <button onClick={()=>setNews(false)} style={{ width:'100%', marginTop:16, ...obtn, color:T.muted }}>Fermer</button>
    </Sheet>}
    {info && <Sheet onClose={()=>setInfo(null)}>
      {sheetTitle(info.title)}
      <div style={{ ...hk, fontSize:14.5, color:T.text, lineHeight:1.6 }}>{info.body}</div>
      <button onClick={()=>setInfo(null)} style={{ width:'100%', marginTop:16, ...obtn, color:T.muted }}>Fermer</button>
    </Sheet>}
    {seenSheet && <Sheet onClose={()=>setSeenSheet(null)}>
      <div style={{ display:'flex', alignItems:'center', gap:12, marginBottom:14 }}><Avatar name={seenSheet.name} size={44} ring /><div><div style={{ ...ax, fontSize:18, fontWeight:800 }}>{seenSheet.name}</div><div style={{ ...hk, fontSize:12.5, color:T.muted }}>{seenSheet.count} connexion{seenSheet.count>1?'s':''} au total</div></div></div>
      <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.faint, marginBottom:8 }}>DERNIÈRES SESSIONS</div>
      <div style={{ display:'flex', flexDirection:'column', maxHeight:'52vh', overflowY:'auto' }}>
        {(seenSheet.history||[]).length===0 && <div style={{ ...hk, fontSize:13, color:T.faint, padding:'10px 0' }}>Aucun historique.</div>}
        {(seenSheet.history||[]).map((iso,i)=>{ const dt=new Date(iso); return (
          <div key={i} style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'10px 0', borderBottom:`1px solid ${T.line}` }}>
            <span style={{ ...hk, fontSize:14, color:T.text }}>{dt.toLocaleDateString('fr-FR',{weekday:'short', day:'2-digit', month:'2-digit'})}</span>
            <span style={{ ...mo, fontSize:13, color:T.muted }}>{String(dt.getHours()).padStart(2,'0')}h{String(dt.getMinutes()).padStart(2,'0')}</span>
          </div>); })}
      </div>
      <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.faint, margin:'18px 0 8px' }}>NOTIFICATIONS PUSH ({(seenSheet.pushLog||[]).length})</div>
      <div style={{ display:'flex', flexDirection:'column', maxHeight:'40vh', overflowY:'auto' }}>
        {(seenSheet.pushLog||[]).length===0 && <div style={{ ...hk, fontSize:13, color:T.faint, padding:'10px 0' }}>Aucune notif envoyée.</div>}
        {(seenSheet.pushLog||[]).map((p,i)=>{ const dt=new Date(p.at); return (
          <div key={i} style={{ padding:'10px 0', borderBottom:`1px solid ${T.line}` }}>
            <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:8 }}>
              <span style={{ ...hk, fontSize:13.5, fontWeight:700, color:T.text, minWidth:0, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{p.title||'Notif'}</span>
              <span style={{ ...mo, fontSize:12, color:T.muted, flex:'0 0 auto' }}>{dt.toLocaleDateString('fr-FR',{day:'2-digit', month:'2-digit'})} · {String(dt.getHours()).padStart(2,'0')}h{String(dt.getMinutes()).padStart(2,'0')}</span>
            </div>
            {p.body && <div style={{ ...hk, fontSize:12, color:T.muted, marginTop:3, lineHeight:1.4 }}>{p.body}</div>}
          </div>); })}
      </div>
      <button onClick={()=>setSeenSheet(null)} style={{ width:'100%', marginTop:16, ...obtn, color:T.muted }}>Fermer</button>
    </Sheet>}
    {pwdSheet && <PwdSheet who={d.me} onClose={()=>setPwdSheet(false)} />}
  </Screen>;
}

// ════════ Feuilles (sheets) ════════
function Sheet({ children, onClose, tall }) {
  return <div onClick={onClose} style={{ position:'fixed', inset:0, zIndex:60, background:'rgba(0,0,0,0.6)', display:'flex', alignItems:'flex-end', justifyContent:'center', paddingTop:'env(safe-area-inset-top)', boxSizing:'border-box' }}>
    <div onClick={e=>e.stopPropagation()} style={{ position:'relative', width:'100%', maxWidth:480, background:T.surface, borderRadius:'22px 22px 0 0', padding:'16px 20px calc(20px + env(safe-area-inset-bottom))', border:`1px solid ${T.line}`, ...hk, color:T.text, maxHeight:'calc(100dvh - env(safe-area-inset-top) - 8px)', overflowY:'auto' }}>
      {onClose && <div style={{ position:'sticky', top:0, zIndex:6, display:'flex', justifyContent:'flex-end', height:0, marginBottom:-6, pointerEvents:'none' }}>
        <button onClick={onClose} aria-label="Fermer" style={{ pointerEvents:'auto', width:36, height:36, borderRadius:999, border:`1px solid ${T.line}`, background:T.elev||T.bg, color:T.muted, fontSize:18, lineHeight:1, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', boxShadow:'0 2px 8px rgba(0,0,0,0.3)' }}>✕</button>
      </div>}
      {children}
    </div>
  </div>;
}
const sheetTitle = (t) => <div style={{ ...ax, fontSize:18, fontWeight:800, marginBottom:14 }}>{t}</div>;

// "Pas de réponse" : choix rapide du délai de rappel. Pose une relance AVEC heure (now+délai)
// -> le scan minute (scanRelancesDue) push le conseiller "c'est le moment d'appeler" quand l'heure arrive.
function NoAnswerSheet({ lead, onClose, onDone }) {
  const [busy, setBusy] = useState(false);
  const pick = async (minutes) => {
    if (busy) return; setBusy(true);
    try { await act(lead.id, 'relance', { date: inMinutesISO(minutes) }); onDone(lead.id); }
    catch (e) { setBusy(false); alert('Échec : ' + e.message); }
  };
  const opts = [['15 min', 15], ['30 min', 30], ['1 heure', 60], ['2 heures', 120]];
  const b = { background:T.elev, border:`1px solid ${T.line}`, borderRadius:13, padding:'15px 14px', color:T.text, ...hk, fontSize:15.5, fontWeight:700, cursor:'pointer', textAlign:'left', display:'flex', alignItems:'center', gap:10 };
  return <Sheet onClose={onClose}>
    {sheetTitle('📵 Pas de réponse')}
    <div style={{ ...hk, fontSize:13.5, color:T.muted, marginBottom:14, lineHeight:1.5 }}>Te rappeler d'appeler <b style={{ color:T.text }}>{lead.name || 'ce lead'}</b> dans combien de temps ? Tu recevras une notif au bon moment.</div>
    <div style={{ display:'grid', gap:10, opacity: busy?0.5:1, pointerEvents: busy?'none':'auto' }}>
      {opts.map(([lb, m]) => (<button key={m} onClick={()=>pick(m)} style={b}><span style={{ fontSize:18 }}>⏰</span> Dans {lb}</button>))}
    </div>
  </Sheet>;
}

// Fiche d'un lead, SWIPEABLE : glisse gauche/droite pour passer au lead suivant/précédent.
function LeadSheet({ state, setSheet, H, onClose }) {
  const { list, index } = state;
  const lead = list[index];
  const [dir, setDir] = useState('next');
  const [notes, setNotes] = useState(null);
  const [copied, setCopied] = useState('');
  const startX = useRef(null);
  const copy = (val, key) => { try{ navigator.clipboard.writeText(val); }catch(_){}; setCopied(key); setTimeout(()=>setCopied(''), 1500); };
  useEffect(()=>{ if(!lead) return; setNotes(null);
    api('/api/notes?id='+encodeURIComponent(lead.id)).then(j=>setNotes((j.notes||[]).filter(n=>n.text&&n.text.trim()))).catch(()=>setNotes([]));
  },[lead && lead.id]);
  if (!lead) return null;
  const go = (delta) => { const ni = index + delta; if (ni >= 0 && ni < list.length) { setDir(delta>0?'next':'prev'); setSheet({ list, index: ni }); } };
  const onStart = (x) => { startX.current = x; };
  const onEnd = (x) => { if (startX.current == null) return; const dx = x - startX.current; startX.current = null; if (dx < -45) go(1); else if (dx > 45) go(-1); };
  return <Sheet onClose={onClose} tall>
    <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:12, paddingRight:42 }}>
      <button onClick={()=>go(-1)} disabled={index<=0} style={{ background:'none', border:'none', color:index>0?T.text:T.line, fontSize:24, cursor:'pointer', width:42, height:42, display:'flex', alignItems:'center', justifyContent:'center' }}>‹</button>
      <span style={{ ...mo, fontSize:11, color:T.faint, letterSpacing:1 }}>{index+1} / {list.length} · glisse ← →</span>
      <button onClick={()=>go(1)} disabled={index>=list.length-1} style={{ background:'none', border:'none', color:index<list.length-1?T.text:T.line, fontSize:24, cursor:'pointer', width:42, height:42, display:'flex', alignItems:'center', justifyContent:'center' }}>›</button>
    </div>
    <div key={lead.id} style={{ animation:`${dir==='next'?'lsnext':'lsprev'} .18s ease` }}>
      {/* Zone de swipe = haut de la carte (les boutons gardent un clic normal) */}
      <div onTouchStart={e=>onStart(e.touches[0].clientX)} onTouchEnd={e=>onEnd(e.changedTouches[0].clientX)}
        onPointerDown={e=>{ onStart(e.clientX); try{ e.currentTarget.setPointerCapture(e.pointerId); }catch(_){} }}
        onPointerUp={e=>onEnd(e.clientX)} style={{ touchAction:'pan-y', cursor:'grab' }}>
        <div style={{ display:'flex', alignItems:'center', gap:12, marginBottom:16 }}>
          <Avatar name={lead.name} size={48} ring />
          <div style={{ minWidth:0 }}>
            <div style={{ ...ax, fontSize:18, fontWeight:800, display:'flex', alignItems:'center', gap:8 }}>{lead.name}
              {lead.urgent && <span style={{ ...mo, fontSize:9, fontWeight:700, color:'#fff', background:RED, padding:'2px 7px', borderRadius:999 }}>NOUVEAU</span>}</div>
            <div style={{ ...hk, fontSize:13, color:T.muted }}>{lead.phone||'pas de numéro'}{lead.plan?` · ${lead.plan}`:''}</div>
            {lead.email && <div onClick={()=>copy(lead.email,'em')} style={{ ...hk, fontSize:13, fontWeight:600, color: copied==='em'?GREEN:T.volt, cursor:'pointer', marginTop:3, wordBreak:'break-all' }}>✉️ {lead.email}{copied==='em'?'  ✓ copié':''}</div>}
          </div>
        </div>
        <div style={{ marginBottom:14 }}>
          <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.faint, marginBottom:6 }}>HISTORIQUE</div>
          {notes===null && <div style={{ ...hk, fontSize:12, color:T.faint }}>Chargement des notes…</div>}
          {notes && notes.length===0 && <div style={{ ...hk, fontSize:12, color:T.faint }}>Aucune note pour l'instant.</div>}
          {notes && notes.length>0 && <div style={{ display:'flex', flexDirection:'column', gap:6, maxHeight:150, overflowY:'auto' }}>
            {notes.map((n,i)=>(<div key={i} style={{ ...hk, fontSize:12.5, lineHeight:1.4, color:T.text, background:T.bg, border:`1px solid ${T.line}`, borderRadius:10, padding:'8px 10px' }}>{n.text}</div>))}
          </div>}
        </div>
      </div>
      <LeadButtons lead={lead} H={H} />
    </div>
    <button onClick={onClose} style={{ width:'100%', marginTop:12, ...obtn, color:T.muted }}>Fermer</button>
  </Sheet>;
}

// RDV / relance / pas intéressé / note
function ActionSheet({ state, onClose, onDone }) {
  const { lead, mode } = state;
  const isRdv = mode==='rdv', isRel = mode==='relance', dated = isRdv||isRel;
  const [date, setDate] = useState(isRdv?plusDaysISO(1):isRel?plusDaysISO(1):'');
  const [time, setTime] = useState('18:00');
  const [note, setNote] = useState('');
  const [busy, setBusy] = useState(false);
  const titles = { rdv:'Séance d’essai le', relance:'Relancer le', pasinteresse:'Pas intéressé', note:'Note rapide' };
  const quicks = isRdv ? [['Aujourd’hui',0],['Demain',1],['Dans 2 j',2]] : isRel ? [['Demain',1],['Dans 2 j',2],['Dans 1 sem',7]] : [];
  const submit = async (skipDate) => {
    if (dated && !skipDate && isSunday(date)) { setDate(plusDaysISO(1)); return; }
    setBusy(true);
    const sig = note.trim() ? (noteSign()+' : '+note.trim()) : '';
    try {
      if (isRdv) { const p={}; if(!skipDate&&date) p.date = time?new Date(`${date}T${time}`).toISOString():date; if(sig)p.note=sig; await act(lead.id,'rdv',p); }
      else if (isRel) { if(!date){setBusy(false);return;} const p={date}; if(sig)p.note=sig; await act(lead.id,'relance',p); }
      else if (mode==='pasinteresse') { await act(lead.id,'pasinteresse', sig?{note:sig}:{}); }
      else { if(!sig){setBusy(false);return;} await act(lead.id,'note',{note:sig}); }
      onDone((isRdv||mode==='pasinteresse') ? lead.id : null);
    } catch(e){ setBusy(false); alert('Échec : '+e.message); }
  };
  return <Sheet onClose={onClose}>
    {sheetTitle(titles[mode])}
    {dated && <>
      {isRel && <>
        <div style={{ ...mo, fontSize:10, letterSpacing:0.5, color:T.faint, marginBottom:6 }}>RAPPELER AUJOURD'HUI À</div>
        <div style={{ display:'flex', gap:8, marginBottom:10, flexWrap:'wrap' }}>
          {[['Dans 1h',inHoursISO(1)],['🕛 Midi',todayAtISO(12)],['14h',todayAtISO(14)],['16h',todayAtISO(16)],['17h',todayAtISO(17)]].map(([lb,iso])=>(
            <button key={lb} onClick={()=>setDate(iso)} style={{ ...obtn, padding:'8px 12px', fontSize:13, flex:'1 0 28%', borderColor: date===iso?T.volt:T.line, color: date===iso?T.volt:T.text }}>{lb}</button>))}
        </div>
        <div style={{ ...mo, fontSize:10, letterSpacing:0.5, color:T.faint, marginBottom:6 }}>OU UN AUTRE JOUR</div>
      </>}
      <div style={{ display:'flex', gap:8, marginBottom:12, flexWrap:'wrap' }}>
        {quicks.map(([lb,n])=>(<button key={lb} onClick={()=>setDate(plusDaysISO(n))} style={{ ...obtn, flex:1, padding:'9px 0', fontSize:13 }}>{lb}</button>))}
      </div>
      <div style={{ display:'flex', gap:10, marginBottom:12 }}>
        <input type="date" value={(date||'').slice(0,10)} onChange={e=>setDate(e.target.value)} style={{ flex:1, background:T.elev, border:`1px solid ${T.line}`, borderRadius:12, padding:'12px', color:T.text, ...hk, fontSize:15, colorScheme:'dark' }} />
        {isRdv && <input type="time" value={time} onChange={e=>setTime(e.target.value)} style={{ width:110, background:T.elev, border:`1px solid ${T.line}`, borderRadius:12, padding:'12px', color:T.text, ...hk, fontSize:15, colorScheme:'dark' }} />}
      </div>
      {isSunday(date) && <div style={{ color:T.amber, fontSize:12, marginBottom:10 }}>Fermé le dimanche, choisis un autre jour.</div>}
    </>}
    <div style={{ ...mo, fontSize:11, color:T.faint, marginBottom:6 }}>{noteSign()}</div>
    <textarea value={note} onChange={e=>setNote(e.target.value)} rows={3} placeholder="Ce qui s'est dit, objection, à rappeler…" style={{ width:'100%', background:T.elev, border:`1px solid ${T.line}`, borderRadius:12, padding:'12px', color:T.text, ...hk, fontSize:14, resize:'none', marginBottom:14, outline:'none' }} />
    <div style={{ display:'flex', gap:10 }}>
      <button onClick={onClose} style={{ ...obtn, flex:1, color:T.muted }}>Annuler</button>
      {isRdv && <button onClick={()=>submit(true)} disabled={busy} style={{ ...obtn, flex:1 }}>Sans date</button>}
      <button onClick={()=>submit(false)} disabled={busy} style={{ flex:1.4, ...gbtn() }}>{busy?'…':'Valider'}</button>
    </div>
  </Sheet>;
}

function ConvSheet({ lead, team, prime={}, onClose, onDone }) {
  const [type, setType] = useState(null);
  const [who, setWho] = useState(meName());
  const [busy, setBusy] = useState(false);
  const submit = async () => {
    if (!type) return; setBusy(true);
    try { await act(lead.id,'converti',{ type, convertiPar: who }); onDone(type==='Abonnement MRR'?(prime.mrr||5):type==='Comptant'?(prime.comptant||10):0, who); }
    catch(e){ setBusy(false); alert('Échec : '+e.message); }
  };
  const TypeBtn = ({ val, lb, sub }) => (
    <button onClick={()=>setType(val)} style={{ flex:1, cursor:'pointer', borderRadius:14, padding:'14px 0', ...hk, fontWeight:800, fontSize:15,
      background: type===val?T.volt:'transparent', color: type===val?'var(--volt-ink)':T.text, border:`1px solid ${type===val?T.volt:T.line}` }}>{lb}<br/><small style={{ fontWeight:600, opacity:0.7 }}>{sub}</small></button>
  );
  return <Sheet onClose={onClose}>
    {sheetTitle('Conversion 💰')}
    <div style={{ ...hk, fontSize:13, color:T.muted, marginBottom:12 }}>Type d'offre et qui a conclu.</div>
    <div style={{ display:'flex', gap:10, marginBottom:10 }}><TypeBtn val="Abonnement MRR" lb="Abonnement" sub="+5€" /><TypeBtn val="Comptant" lb="Comptant" sub="+10€" /></div>
    <div style={{ marginBottom:16 }}><TypeBtn val="Retour offert" lb="Retour offert (3 mois gratuits)" sub="0€ · pas de prime tant qu'il n'est pas en abonnement payant" /></div>
    <div style={{ ...mo, fontSize:11, color:T.faint, marginBottom:8 }}>CONCLU PAR</div>
    <div style={{ display:'flex', gap:8, flexWrap:'wrap', marginBottom:16 }}>
      {team.map(n=>(<button key={n} onClick={()=>setWho(n)} style={{ cursor:'pointer', borderRadius:999, padding:'8px 14px', ...hk, fontSize:13, fontWeight:600, background:who===n?T.voltDim:'transparent', color:who===n?T.volt:T.muted, border:`1px solid ${who===n?'var(--volt-line)':T.line}` }}>{n}</button>))}
    </div>
    <div style={{ display:'flex', gap:10 }}>
      <button onClick={onClose} style={{ ...obtn, flex:1, color:T.muted }}>Annuler</button>
      <button onClick={submit} disabled={busy||!type} style={{ flex:1.4, ...btn(type?GREEN:T.line, type?GINK:T.faint) }}>{busy?'…':'Valider'}</button>
    </div>
  </Sheet>;
}

function WaSheet({ lead, onClose, onSent }) {
  // Une séance fixée reste dans l'onglet Leads en rappel JUSQU'À l'envoi d'un message lié à la séance.
  // Les messages "séance" (réservation reçue, confirmer la veille, rappel jour J) cochent "Séance confirmée".
  const SEANCE_LABELS = ["✅ Réservation reçue (à l'inscription)", '📅 Confirmer (la veille)', '⏰ Rappel du jour J'];
  return <Sheet onClose={onClose}>
    {sheetTitle('Message WhatsApp')}
    <div style={{ ...hk, fontSize:13, color:T.muted, marginBottom:12 }}>Choisis le cas, WhatsApp s'ouvre pré-rempli.</div>
    <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
      {WA_TEMPLATES(lead).map(t=>(<a key={t.label} href={waLink(lead,t.text)} target="_blank" rel="noopener" onClick={()=>{ if(lead.seanceEssai && !lead.seanceConfirmee && SEANCE_LABELS.includes(t.label)){ act(lead.id,'confirmseance').catch(()=>{}); onSent && onSent(lead.id); } onClose(); }} style={{ ...obtn, textAlign:'left', padding:'13px 14px', textDecoration:'none', display:'block' }}>{t.label}</a>))}
    </div>
    <button onClick={onClose} style={{ width:'100%', marginTop:12, ...obtn, color:T.muted }}>Annuler</button>
  </Sheet>;
}

// Ajout d'un prospect "Sur place" (walk-in) — 3 cas : juste passé / séance d'essai / inscrit direct.
function WalkinSheet({ onClose, onDone }) {
  const [prenom, setPrenom] = useState('');
  const [nom, setNom] = useState('');
  const [phone, setPhone] = useState('');
  const [email, setEmail] = useState('');
  const [mode, setMode] = useState('passe');
  const [type, setType] = useState('Abonnement MRR');
  const [seance, setSeance] = useState(()=>{ const d=new Date(); d.setHours(d.getHours()+1,0,0,0); return d.toISOString().slice(0,16); });
  const [note, setNote] = useState('');
  const [busy, setBusy] = useState(false);
  const inp = { width:'100%', background:T.bg, border:`1px solid ${T.line}`, borderRadius:12, padding:'12px 14px', color:T.text, ...hk, fontSize:14, outline:'none', boxSizing:'border-box' };
  const submit = async () => {
    if(mode!=='mineur' && !prenom.trim() && !phone.trim()){ alert('Mets au moins un prénom ou un téléphone.'); return; }
    setBusy(true);
    try {
      const sig = note.trim() ? (noteSign()+' : '+note.trim()) : '';
      const body = { prenom:prenom.trim(), nom:nom.trim(), phone:phone.trim(), email:email.trim(), mode, note:sig };
      if(mode==='essai') body.seance = new Date(seance).toISOString();
      if(mode==='inscrit') body.type = type;
      const r = await api('/api/walkin', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) });
      if(r && r.ok){
        if(mode==='mineur'){ alert(r.count ? `🚫 Refus enregistré.\nC'est le ${r.count}ᵉ mineur refusé cette année.` : '🚫 Refus enregistré.'); }
        onDone && onDone(mode, { name:[prenom.trim(),nom.trim()].filter(Boolean).join(' '), phone:phone.trim(), type });
      } else { alert('Échec : '+((r&&r.reason)||'enregistrement')); setBusy(false); }
    } catch(e){ alert('Échec : '+(e.message||'')); setBusy(false); }
  };
  const Seg = ({ id, label }) => (
    <button onClick={()=>setMode(id)} style={{ flex:1, border:'none', cursor:'pointer', borderRadius:9, padding:'10px 4px', ...hk, fontSize:12.5, fontWeight:700, background:mode===id?T.volt:'transparent', color:mode===id?T.ink:T.muted }}>{label}</button>
  );
  return <Sheet onClose={onClose}>
    {sheetTitle('➕ Prospect sur place')}
    <div style={{ display:'flex', flexDirection:'column', gap:10, marginBottom:14 }}>
      <div style={{ display:'flex', gap:10 }}>
        <input value={prenom} onChange={e=>setPrenom(e.target.value)} placeholder="Prénom" style={{ ...inp, flex:1 }} />
        <input value={nom} onChange={e=>setNom(e.target.value)} placeholder="Nom" style={{ ...inp, flex:1 }} />
      </div>
      <input value={email} onChange={e=>setEmail(e.target.value)} placeholder="Adresse mail" inputMode="email" autoCapitalize="off" style={inp} />
      <input value={phone} onChange={e=>setPhone(e.target.value)} placeholder="Téléphone" inputMode="tel" style={inp} />
    </div>
    <div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.faint, marginBottom:8 }}>SON ÉTAT</div>
    <div style={{ display:'flex', gap:6, background:T.surface, borderRadius:12, padding:4, marginBottom:14, border:`1px solid ${T.line}`, flexWrap:'wrap' }}>
      <Seg id="passe" label="👋 Juste passé" /><Seg id="essai" label="📅 Séance d'essai" /><Seg id="inscrit" label="✅ Inscrit" /><Seg id="mineur" label="🚫 Mineur" />
    </div>
    {mode==='mineur' && <div style={{ ...hk, fontSize:13, color:T.muted, lineHeight:1.5, background:T.surface, border:`1px solid ${T.line}`, borderRadius:12, padding:'12px 14px', marginBottom:14 }}>On n'accepte pas les mineurs pour l'instant. Prends quand même ses coordonnées (nom, tél, email) : on les garde pour le recontacter si on accepte les mineurs plus tard. Il sort de la file et ne compte pas dans tes stats. On voit le total de l'année.</div>}
    {mode==='essai' && <div style={{ marginBottom:14 }}>
      <div style={{ ...hk, fontSize:12.5, color:T.muted, marginBottom:6 }}>Date et heure de la séance</div>
      <input type="datetime-local" value={seance} onChange={e=>setSeance(e.target.value)} style={inp} />
    </div>}
    {mode==='inscrit' && <div style={{ marginBottom:14 }}>
      <div style={{ ...hk, fontSize:12.5, color:T.muted, marginBottom:6 }}>Type d'inscription (prime créditée à ton nom)</div>
      <div style={{ display:'flex', gap:8 }}>
        {[['Abonnement MRR','Abonnement (MRR)'],['Comptant','Comptant']].map(([id,lb])=>(
          <button key={id} onClick={()=>setType(id)} style={{ flex:1, ...obtn, padding:'12px 0', background:type===id?T.voltDim:'transparent', borderColor:type===id?'var(--volt-line)':T.line, color:type===id?T.volt:T.text, fontWeight:700 }}>{lb}</button>
        ))}
      </div>
    </div>}
    <div style={{ ...mo, fontSize:11, color:T.faint, marginBottom:6 }}>{noteSign()}</div>
    <textarea value={note} onChange={e=>setNote(e.target.value)} rows={2} placeholder={mode==='essai' ? "Veut tester la muscu · vient après le boulot · amène un pote…" : mode==='inscrit' ? "Abonnement 12 mois · payé en 1 fois · objectif perte de poids…" : mode==='mineur' ? "Optionnel : 15 ans, voulait la muscu, venu avec un parent…" : "Reviendra plus tard · venu avec sa femme · veut réfléchir…"} style={{ ...inp, marginBottom:14, resize:'none' }} />
    <button onClick={submit} disabled={busy} style={{ width:'100%', ...gbtn(), padding:'14px 0', fontSize:15, opacity:busy?0.6:1 }}>{busy?'Enregistrement…':mode==='inscrit'?'✅ Enregistrer l\'inscription':mode==='essai'?'📅 Enregistrer la séance':mode==='mineur'?'🚫 Enregistrer le refus':'Enregistrer le prospect'}</button>
    <button onClick={onClose} style={{ width:'100%', marginTop:10, ...obtn, color:T.muted }}>Annuler</button>
  </Sheet>;
}

// Liste des leads d'une catégorie (clic sur une chip / l'entonnoir de l'accueil).
function CategorySheet({ cat, title, who, presetLeads, H, onClose }) {
  const [leads, setLeads] = useState(presetLeads || null);
  useEffect(()=>{ if(presetLeads){ setLeads(presetLeads); return; } setLeads(null);
    api('/api/category?who='+encodeURIComponent(who)+'&cat='+encodeURIComponent(cat))
      .then(r=>setLeads((r.leads||[]).map(mapLead))).catch(()=>setLeads([]));
  },[cat]);
  return <Sheet onClose={onClose}>
    {sheetTitle(title + (leads ? ` · ${leads.length}` : ''))}
    {leads===null && <div style={{ ...hk, fontSize:13, color:T.faint, padding:'12px 0' }}>Chargement…</div>}
    {leads && leads.length===0 && <div style={{ ...hk, fontSize:13, color:T.faint, padding:'12px 0' }}>Aucun lead dans cette catégorie.</div>}
    {leads && leads.map(l=>(<LeadRow key={l.id} lead={l} urgent={l.urgent} onClick={()=>H.openLead(l, leads)} />))}
    <button onClick={onClose} style={{ width:'100%', marginTop:8, ...obtn, color:T.muted }}>Fermer</button>
  </Sheet>;
}

// Leads orphelins (attribués à personne) -> répartition côté admin.
function OrphansSheet({ team, onClose }) {
  const [leads, setLeads] = useState(null);
  const [busy, setBusy] = useState('');
  useEffect(()=>{ api('/api/category?who=&cat=orphelins').then(r=>setLeads((r.leads||[]).map(mapLead))).catch(()=>setLeads([])); },[]);
  const assign = async (lead, to) => { setBusy(lead.id); try{ await act(lead.id,'assign',{to}); setLeads(ls=>ls.filter(l=>l.id!==lead.id)); }catch(e){ alert('Échec : '+e.message); } finally{ setBusy(''); } };
  return <Sheet onClose={onClose}>
    {sheetTitle('🧩 Leads que personne n\'appelle'+(leads?` · ${leads.length}`:''))}
    <div style={{ ...hk, fontSize:13, color:T.muted, marginBottom:14 }}>Aucun vendeur dessus. Assigne chacun à un vendeur : il devient propriétaire et le lead compte dans ses stats.</div>
    {leads===null && <div style={{ ...hk, fontSize:13, color:T.faint, padding:'12px 0' }}>Chargement…</div>}
    {leads && leads.length===0 && <div style={{ ...hk, fontSize:13.5, color:GREEN, padding:'12px 0' }}>✅ Aucun lead orphelin, tout est attribué.</div>}
    {leads && leads.map(l=>(
      <div key={l.id} style={{ borderBottom:`1px solid ${T.line}`, padding:'12px 0' }}>
        <div style={{ ...hk, fontSize:15, fontWeight:700, color:T.text }}>{l.name}</div>
        <div style={{ ...hk, fontSize:12, color:T.muted, marginBottom:9 }}>{l.statutRaw||l.status} · {l.plan}{l.when?` · ${l.when}`:''}</div>
        <div style={{ display:'flex', gap:8, flexWrap:'wrap' }}>
          {team.map(m=>(<button key={m} disabled={busy===l.id} onClick={()=>assign(l,m)} style={{ ...obtn, padding:'8px 14px', fontSize:13, opacity:busy===l.id?0.5:1 }}>{busy===l.id?'…':'→ '+m}</button>))}
        </div>
      </div>
    ))}
    <button onClick={onClose} style={{ width:'100%', marginTop:14, ...obtn, color:T.muted }}>Fermer</button>
  </Sheet>;
}

// Suivi des nouveaux adhérents (rétention du 1er mois) : la boucle où le vendeur qui a converti
// garde la relation et écrit sur WhatsApp à J+2, J+7, J+14, J+30. Liste + message pré-rempli par
// étape + bouton pour enregistrer l'état (et avancer à l'étape suivante).
const SUIVI_ETATS = [
  { id:'Répond', label:'💬 Il répond', color:GREEN },
  { id:'Silence', label:'🤐 Pas de réponse', color:'#9ca3af' },
  { id:'À risque', label:'⚠️ À risque', color:'#f87171' },
];
function SuiviSheet({ me, isAdmin, onClose, onDone }){
  const [data, setData] = useState(null);   // { suivis:[...] }
  const [all, setAll] = useState(isAdmin);   // admin : démarre sur toute l'équipe (= le compteur du pilotage), sinon ses propres adhérents
  const [busy, setBusy] = useState('');
  const load = (silent) => { if(!silent) setData(null); api('/api/suivis?who='+encodeURIComponent(me)+(all&&isAdmin?'&all=1':''))
    .then(r=>setData({ suivis:(r.suivis||[]).map(mapLead) })).catch(()=>{ if(!silent) setData({ suivis:[] }); }); };
  useEffect(()=>{ load(); }, [all]);
  const mark = async (l, etat) => { setBusy(l.id); try{
    await act(l.id,'suivi',{ etape:l.suiviEtape, etat, convDate:l.dateConversion });
    const next = SUIVI_STEPS[SUIVI_STEPS.indexOf(l.suiviEtape)+1];
    if (next) {
      // l'adhérent avance d'une étape : la carte RESTE, l'étape faite passe en gris ✓, la suivante se débloque à sa date
      setData(d=>({ suivis:d.suivis.map(x=> x.id===l.id ? { ...x, suiviEtape:next, suiviEtat:etat, suiviDue:false } : x) }));
      load(true); // recale en silence la date de prochain contact
    } else {
      // J+30 fait -> 1er mois bouclé, il sort de la boucle
      setData(d=>({ suivis:d.suivis.filter(x=>x.id!==l.id) }));
    }
    onDone && onDone();
  }catch(e){ alert('Échec : '+e.message); } finally{ setBusy(''); } };
  const suivis = data ? data.suivis : null;
  const due = suivis ? suivis.filter(l=>l.suiviDue) : [];
  const later = suivis ? suivis.filter(l=>!l.suiviDue) : [];
  // états d'une étape : déjà faite (gris ✓) / du jour (vert) / prochaine pas encore due (contour volt) / verrouillée (pointillé)
  const stepStyle = (state) => {
    const base = { borderRadius:11, padding:'8px 3px 7px', textAlign:'center', position:'relative', ...mo, fontSize:11, fontWeight:700, lineHeight:1.15 };
    if (state==='done')   return { ...base, background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.06)', color:'#6b7280' };
    if (state==='active') return { ...base, background:GREEN, border:`1px solid ${GREEN}`, color:GINK, boxShadow:'0 6px 18px rgba(34,197,94,.30)' };
    if (state==='next')   return { ...base, background:'transparent', border:`1px solid ${T.voltLine}`, color:T.volt };
    return { ...base, background:'transparent', border:`1px dashed ${T.line}`, color:T.faint };  // lock
  };
  const Card = (l) => {
    const wa = SUIVI_WA(l);
    const idx = Math.max(0, SUIVI_STEPS.indexOf(l.suiviEtape));
    return <div key={l.id} style={{ background:T.surface, border:`1px solid ${l.suiviEtat==='À risque'?'rgba(248,113,113,0.5)':T.line}`, borderRadius:14, padding:'13px 14px', marginBottom:10 }}>
      <div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:2 }}>
        <Avatar name={l.name} size={38} />
        <div style={{ flex:1, minWidth:0 }}>
          <div style={{ ...hk, fontSize:15, fontWeight:700, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{l.name}</div>
          <div style={{ ...hk, fontSize:11.5, color:T.muted }}>✅ Inscrit{l.dateConversion?` le ${fmtFR(l.dateConversion)}`:''}{all&&l.convPar?` · ${l.convPar}`:''}</div>
        </div>
      </div>
      {/* Les 4 relances du 1er mois : faite = gris ✓, du jour = vert, à venir = pointillé. Avance toute seule à chaque relance enregistrée. */}
      <div style={{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:6, margin:'11px 0 9px' }}>
        {SUIVI_STEPS.map((st,i)=>{ const state = i<idx ? 'done' : i===idx ? (l.suiviDue?'active':'next') : 'lock';
          return <div key={st} style={stepStyle(state)}>
            <span>S{i+1}</span>
            <span style={{ display:'block', fontSize:9.5, fontWeight:700, opacity:.85, marginTop:1 }}>{st}</span>
            {state==='done' && <span style={{ position:'absolute', top:3, right:5, fontSize:9 }}>✓</span>}
          </div>;
        })}
      </div>
      <div style={{ ...hk, fontSize:12, color:T.muted, marginBottom:8 }}>{wa.sub} · {l.suiviDue ? <b style={{ color:GREEN }}>à relancer aujourd'hui</b> : `prévu le ${fmtFR(l.suiviProchain)}`}{l.suiviEtat?` · dernier : ${l.suiviEtat}`:''}</div>
      {waNumber(l)
        ? <div style={{ display:'flex', gap:8, marginBottom:8 }}>
            <a href={waLink(l, wa.text)} target="_blank" rel="noopener" style={{ flex:1, textAlign:'center', textDecoration:'none', ...btn('#25D366','#fff'), padding:'11px 0', fontSize:14 }}>📲 Relance {l.suiviEtape}</a>
            <a href={smsLink(l, wa.text)} style={{ flex:'0 0 auto', textAlign:'center', textDecoration:'none', ...obtn, padding:'11px 16px', fontSize:14, color:T.text }}>💬 SMS</a>
          </div>
        : <div style={{ ...hk, fontSize:12.5, color:T.amber, background:T.surface, border:`1px solid ${T.line}`, borderRadius:12, padding:'10px 12px', marginBottom:8, lineHeight:1.45 }}>☎️ Pas de numéro sur cette fiche. Ajoute son téléphone dans Notion pour pouvoir lui écrire.</div>}
      <div style={{ display:'flex', gap:6 }}>
        {SUIVI_ETATS.map(e=>(<button key={e.id} disabled={busy===l.id} onClick={()=>mark(l,e.id)} style={{ flex:1, ...obtn, padding:'9px 2px', fontSize:11.5, color:e.color, borderColor:T.line, opacity:busy===l.id?0.5:1 }}>{busy===l.id?'…':e.label}</button>))}
      </div>
    </div>;
  };
  return <Sheet onClose={onClose} tall>
    {sheetTitle('🌱 Inscrits à suivre'+(due.length?` · ${due.length}`:''))}
    <div style={{ ...hk, fontSize:13, color:T.muted, marginBottom:14, lineHeight:1.5 }}>Le 1er mois décide tout. 4 relances prévues par inscrit (J+2, J+7, J+14, J+30). Chaque relance faite passe en gris, la suivante s'active toute seule le jour prévu.</div>
    {isAdmin && <div style={{ display:'flex', gap:6, background:T.surface, borderRadius:11, padding:4, marginBottom:14, border:`1px solid ${T.line}` }}>
      {[['Mes adhérents',false],["Toute l'équipe",true]].map(([lb,v])=>(<button key={lb} onClick={()=>setAll(v)} style={{ flex:1, border:'none', cursor:'pointer', borderRadius:8, padding:'9px 4px', ...hk, fontSize:12.5, fontWeight:700, background:all===v?T.volt:'transparent', color:all===v?T.ink:T.muted }}>{lb}</button>))}
    </div>}
    {suivis===null && <div style={{ ...hk, fontSize:13, color:T.faint, padding:'12px 0' }}>Chargement…</div>}
    {suivis && suivis.length===0 && <div style={{ ...hk, fontSize:13.5, color:GREEN, padding:'12px 0' }}>✅ Personne à suivre pour l'instant. Chaque nouvelle conversion arrivera ici.</div>}
    {due.length>0 && <><div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.faint, textTransform:'uppercase', margin:'2px 0 10px' }}>À relancer aujourd'hui · {due.length}</div>{due.map(Card)}</>}
    {later.length>0 && <><div style={{ ...mo, fontSize:10, letterSpacing:1, color:T.faint, textTransform:'uppercase', margin:'12px 0 10px' }}>À venir · {later.length}</div>{later.map(Card)}</>}
    <button onClick={onClose} style={{ width:'100%', marginTop:10, ...obtn, color:T.muted }}>Fermer</button>
  </Sheet>;
}

// Compresse une image (128px JPEG) et l'envoie comme photo de profil. Renvoie le data URL.
function uploadProfilePhoto(file, name){
  return new Promise((resolve, reject)=>{
    if(!file || !/^image\//.test(file.type)) return reject('image');
    const img = new Image();
    img.onload = ()=>{
      const S=128, cv=document.createElement('canvas'); cv.width=S; cv.height=S; const ctx=cv.getContext('2d');
      const r=Math.max(S/img.width, S/img.height), w=img.width*r, h=img.height*r;
      ctx.drawImage(img, (S-w)/2, (S-h)/2, w, h);
      let q=0.6, url=cv.toDataURL('image/jpeg', q);
      while(url.length>42000 && q>0.3){ q-=0.1; url=cv.toDataURL('image/jpeg', q); }
      URL.revokeObjectURL(img.src);
      api('/api/photo', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ dataUrl:url }) })
        .then(res=>{ if(res&&res.ok){ window.__photos = { ...(window.__photos||{}), [name]: url }; resolve(url); } else reject((res&&res.reason)||'envoi'); })
        .catch(()=>reject('envoi'));
    };
    img.onerror = ()=>reject('illisible');
    img.src = URL.createObjectURL(file);
  });
}

// Rappel fun au lancement si l'employé n'a pas de photo de profil.
function PhotoNudge({ name, onClose }){
  const ref = useRef(null);
  const [busy, setBusy] = useState(false);
  const pick = (e)=>{ const f=e.target.files&&e.target.files[0]; if(!f) return; setBusy(true);
    uploadProfilePhoto(f, name).then(()=>{ setBusy(false); onClose(true); }).catch(()=>{ setBusy(false); alert('Échec, réessaie.'); }); };
  return <div onClick={()=>onClose(false)} style={{ position:'fixed', inset:0, zIndex:88, background:'rgba(0,0,0,0.78)', display:'flex', alignItems:'center', justifyContent:'center', padding:24 }}>
    <div onClick={e=>e.stopPropagation()} style={{ background:T.elev, border:`1px solid var(--volt-line)`, borderRadius:22, padding:'26px 22px', maxWidth:360, width:'100%', textAlign:'center', boxShadow:'0 20px 60px rgba(0,0,0,0.55)' }}>
      <div style={{ fontSize:46, marginBottom:8 }}>🫥</div>
      <div style={{ ...ax, fontSize:20, fontWeight:900, marginBottom:8 }}>Hé {name}, on voit pas ta tête !</div>
      <div style={{ ...hk, fontSize:14, color:T.muted, lineHeight:1.5, marginBottom:18 }}>Pour l'instant t'es un joli rond gris 🤡. Mets une photo de profil, ça motive et ça fait classe dans le classement.</div>
      <input ref={ref} type="file" accept="image/*" onChange={pick} style={{ display:'none' }} />
      <button onClick={()=>!busy && ref.current && ref.current.click()} style={{ width:'100%', ...gbtn(), padding:'14px 0', fontSize:15, marginBottom:10, opacity:busy?0.6:1 }}>{busy?'Envoi…':'📸 Mettre ma photo'}</button>
      <button onClick={()=>onClose(false)} style={{ background:'none', border:'none', color:T.faint, ...hk, fontSize:13, cursor:'pointer' }}>Plus tard…</button>
    </div>
  </div>;
}

function WinPopup({ win, onClose }) {
  const lead = win.lead;
  const p = (lead?.name||'').split(' ')[0];
  const me = meName();
  // Lien de parrainage UNIQUE par membre : on génère un code stable (hash de l'id/téléphone) puis on
  // encode {prénom, ref} dans un token opaque (base64url). Deux membres au même prénom ont des liens
  // différents, et le lien ne montre pas juste le prénom en clair. Le ref sert au tracking dans Notion.
  const refCode = (l)=>{ const seed=`${l?.id||''}|${l?.phone||''}|${l?.name||''}`; let h=5381; for(let i=0;i<seed.length;i++){ h=((h*33)^seed.charCodeAt(i))>>>0; } const A='abcdefghijkmnpqrstuvwxyz23456789'; let c='',n=h; for(let i=0;i<7;i++){ c+=A[n%A.length]; n=Math.floor(n/A.length)+(i+1)*131; } return c; };
  const ref = refCode(lead);
  const tok = (()=>{ try{ return btoa(unescape(encodeURIComponent(JSON.stringify({n:p||'',r:ref})))).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,''); }catch(_){ return ''; } })();
  // wa.me direct (PAS raccourci) pour ouvrir le partage de façon fluide ; le filleul ouvre une vraie page web.
  const optinLink = 'https://invite.4dgymclub.com/parrainage.html?p='+tok;
  const shareText = `Je viens de m'inscrire à ${BRAND_FULL} 💪 Viens tester avec moi, ta séance d'essai est offerte et on gagne chacun 1 mois. Inscris-toi ici 👉 ${optinLink}`;
  const shareLink = 'https://wa.me/?text='+encodeURIComponent(shareText);
  // Ce qu'on envoie AU nouveau membre (sur SON WhatsApp), avec le lien court cliquable dedans.
  const memberMsg = `Salut ${p}, bienvenue dans la team 💪 Petit cadeau : invite un pote et vous gagnez chacun 1 mois offert (et 2 mois pour toi si tu laisses un avis Google). Tu l'invites en 1 clic ici, choisis qui tu veux 👉 ${shareLink}`;
  const parrLink = (lead && lead.phone) ? waLink(lead, memberMsg) : 'https://wa.me/?text='+encodeURIComponent(memberMsg);
  const sendParr = () => api('/api/invite', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ who: me, channel:'parrainage', detail: lead?.name||'' }) }).catch(()=>{});
  return <div style={{ position:'fixed', inset:0, zIndex:70, background:'rgba(0,0,0,0.7)', display:'flex', alignItems:'center', justifyContent:'center', padding:24 }}>
    <div style={{ background:T.surface, border:`1px solid var(--volt-line)`, borderRadius:24, padding:'30px 24px', textAlign:'center', maxWidth:340, ...hk, animation:'splashin .3s ease' }}>
      <div style={{ fontSize:46 }}>🎉</div>
      <div style={{ ...ax, fontSize:22, fontWeight:800, color:T.text, marginTop:8 }}>Félicitations</div>
      <div style={{ ...ax, fontSize:42, fontWeight:900, color:T.volt, margin:'8px 0' }}>+{win.gain}€</div>
      <div style={{ ...hk, fontSize:14.5, color:T.muted, marginBottom:16, lineHeight:1.4 }}>
        {win.total!=null
          ? <>Bravo <b style={{ color:T.text }}>{win.who}</b> ! Tu es à <b style={{ color:T.volt }}>{win.total}€</b> ce mois 🔥</>
          : <>Bravo <b style={{ color:T.text }}>{win.who}</b>, une conversion de plus 🔥</>}
      </div>
      <div style={{ ...hk, fontSize:13, color:T.text, fontWeight:700, marginBottom:4 }}>Dernière étape : envoie-lui le parrainage 👇</div>
      <div style={{ ...hk, fontSize:12, color:T.faint, marginBottom:12, lineHeight:1.4 }}>On envoie le message à <b style={{ color:T.text }}>{p||'lui'}</b>. Il clique le lien dedans et invite ses propres potes. 1 mois offert pour chacun.</div>
      <a href={parrLink} target="_blank" rel="noopener" onClick={()=>{ sendParr(); onClose(); }} style={{ display:'block', textDecoration:'none', textAlign:'center', ...btn('#25D366','#fff'), padding:'15px 0', fontSize:15, fontWeight:800 }}>📲 Envoyer le parrainage à {p||'lui'}</a>
      <div onClick={onClose} style={{ ...hk, fontSize:12.5, color:T.faint, marginTop:12, cursor:'pointer', textDecoration:'underline' }}>Plus tard</div>
    </div>
  </div>;
}

// ════════ SPLASH de motivation (slogans, repris de la V1) ════════
const SLOGANS = [
  (n)=>`${n}, aujourd'hui tu vas tout arracher 🔥`,
  (n)=>`Allez ${n}, mode gorille activé 🦍`,
  (n)=>`${n}, chaque appel te rapproche d'un RDV. Fonce 💪`,
  (n)=>`C'est ta journée ${n}. Décroche, souris, signe.`,
  (n)=>`${n}, transforme les "peut-être" en "c'est calé" ✅`,
  (n)=>`On lâche rien ${n}, on fait péter le score 🚀`,
  (n)=>`${n}, le téléphone c'est ton arme. Charge.`,
  (n)=>`Un appel de plus que les autres ${n}, c'est ça la diff.`,
  (n)=>`${n}, les leads n'attendent que toi. Go.`,
  (n)=>`Personne ne décroche comme toi ${n}. Montre-leur 🏆`,
  (n)=>`${n}, un "non" te rapproche du prochain "oui".`,
  (n)=>`Aujourd'hui ${n}, t'es en mission. Que des signatures.`,
  (n)=>`${n}, ta voix au téléphone c'est ta meilleure pub.`,
  (n)=>`Chaque inscription c'est ta prime qui monte ${n} 💸`,
  (n)=>`${n}, chaque appel peut devenir une inscription 🔥`,
  (n)=>`Réveille le closer en toi ${n}.`,
  (n)=>`${n}, on appelle, on cale, on encaisse.`,
  (n)=>`Le premier appel est le plus dur ${n}. Après c'est du plaisir.`,
  (n)=>`${n}, souris au téléphone, ça s'entend.`,
  (n)=>`Today ${n} ne lâche aucun lead.`,
  (n)=>`${n}, t'as le téléphone, t'as le talent. Action.`,
  (n)=>`Objectif du jour ${n} : remplir le planning d'essais.`,
  (n)=>`${n}, un lead appelé dans l'heure c'est un lead converti.`,
  (n)=>`La gagne c'est un état d'esprit ${n}. Active-le.`,
  (n)=>`${n}, chaque "j'ai pas le temps" cache un "convaincs-moi".`,
  (n)=>`On va faire trembler le classement ${n} 📈`,
  (n)=>`${n}, ta cagnotte se construit appel par appel.`,
  (n)=>`Le silence ne signe pas ${n}. Décroche.`,
  (n)=>`${n}, sois la raison pour laquelle quelqu'un s'inscrit aujourd'hui.`,
  (n)=>`Pas de lead froid pour toi ${n}, juste des futurs membres.`,
  (n)=>`${n}, relance, relance, relance. C'est là que ça signe.`,
  (n)=>`T'es chaud ${n} ? Le téléphone aussi. Go.`,
  (n)=>`${n}, transforme la salle un appel à la fois.`,
  (n)=>`Aujourd'hui ${n}, t'écris ton record.`,
  (n)=>`${n}, le meilleur vendeur c'est celui qui rappelle.`,
  (n)=>`Un essai calé c'est une victoire ${n}. Empile-les.`,
  (n)=>`${n}, ils cherchent une raison de venir. Donne-la-leur.`,
  (n)=>`Une fiche, un appel, une inscription. Go ${n} 🔥`,
  (n)=>`${n}, énergie à fond, sourire branché, on y va.`,
  (n)=>`Chaque seconde au tel ${n}, c'est de l'argent.`,
  (n)=>`${n}, tu connais ta salle mieux que personne. Vends-la.`,
  (n)=>`On respire, on appelle, on signe ${n}.`,
  (n)=>`${n}, la concurrence dort, toi tu décroches.`,
  (n)=>`T'es pas là pour appeler ${n}, t'es là pour convertir.`,
  (n)=>`${n}, fais-en un de plus. Toujours un de plus.`,
  (n)=>`La régularité bat le talent ${n}. Et toi t'as les deux.`,
  (n)=>`${n}, ton planning d'essais ne va pas se remplir tout seul.`,
  (n)=>`Aujourd'hui ${n}, chaque lead repart avec une date.`,
  (n)=>`${n}, le classement adore les acharnés. Sois-en un.`,
  (n)=>`On démarre fort ${n}. Premier appel, maintenant 📞`,
  (n)=>`${n}, t'as un don pour mettre les gens à l'aise. Sers-t'en.`,
  (n)=>`Tes futurs membres n'attendent que toi ${n}. Va les chercher.`,
  (n)=>`${n}, un sourire, une date d'essai, et c'est plié.`,
  (n)=>`Le succès aime les tenaces ${n}. Rappelle encore.`,
  (n)=>`${n}, aujourd'hui on remplit la salle 🏋️`,
  // Prendre soin des adhérents (fidélisation, accueil, service) — pas que les leads
  (n)=>`${n}, un membre qu'on appelle par son prénom, c'est un membre qui reste.`,
  (n)=>`Aujourd'hui ${n}, fais sourire au moins un adhérent 😊`,
  (n)=>`${n}, prends 2 min pour montrer un exo à quelqu'un, ça change tout.`,
  (n)=>`Un adhérent bien accueilli ${n}, c'est un parrainage qui arrive.`,
  (n)=>`${n}, on signe des leads, mais on chouchoute ceux qui sont déjà là.`,
  (n)=>`T'as pas vu un habitué depuis 15 jours ${n} ? Un petit message et il revient.`,
  (n)=>`${n}, le sourire à l'accueil vaut autant qu'une signature.`,
  (n)=>`Prends des nouvelles ${n}, demande comment se passent les séances.`,
  (n)=>`${n}, une salle propre et une équipe présente, c'est ça qui fidélise.`,
  (n)=>`On garde les anciens heureux ${n}, pas que les nouveaux.`,
  (n)=>`${n}, un conseil au bon moment, c'est un abonnement qui se renouvelle.`,
  (n)=>`Repère le membre qui galère ${n} et file-lui un coup de main.`,
  (n)=>`${n}, chaque adhérent doit se sentir chez lui à ${BRAND}.`,
  (n)=>`Un membre content ${n}, c'est ta meilleure pub.`,
  (n)=>`${n}, fidéliser coûte moins cher que recruter. Bichonne les adhérents.`,
  (n)=>`${n}, l'accueil d'aujourd'hui c'est la réinscription de demain.`,
  // Bonne humeur / bien-être de l'équipe — kiffer, profiter, être heureux
  (n)=>`${n}, kiffe ta journée, le reste suivra ✨`,
  (n)=>`Bonne humeur d'abord ${n}, les résultats après.`,
  (n)=>`${n}, prends du plaisir, ça s'entend au tel et à l'accueil.`,
  (n)=>`Profite ${n}, tu bosses dans une salle de ouf, savoure 😎`,
  (n)=>`${n}, un sourire pour toi avant un sourire pour les autres.`,
  (n)=>`Détends-toi ${n}, t'es exactement là où il faut.`,
  (n)=>`La bonne énergie ${n}, ça se travaille comme un muscle 💚`,
  (n)=>`${n}, fais une pause, respire, repars plus fort.`,
  (n)=>`Aujourd'hui ${n}, sois heureux d'abord, performant ensuite.`,
  (n)=>`Le boulot c'est mieux quand on rigole ${n}. Lâche-toi.`,
  (n)=>`Kiffe le moment ${n}, tu te souviendras de ces journées.`,
  (n)=>`${n}, prends soin de toi autant que des autres.`,
  (n)=>`Bonne vibe ${n} : tu donnes le ton de la salle aujourd'hui.`,
  (n)=>`${n}, profite de la vie. On bosse pour vivre, pas l'inverse.`,
  (n)=>`Un café, une bonne playlist, et c'est parti ${n} 🎶`,
  // Citations de grandes personnalités (affichées avec l'auteur)
  ()=>({ q:"La discipline est le pont entre les objectifs et les réussites.", by:"Jim Rohn" }),
  ()=>({ q:"Le succès c'est tomber sept fois, se relever huit.", by:"Proverbe japonais" }),
  ()=>({ q:"Tout le monde a un plan jusqu'au premier coup de poing.", by:"Mike Tyson" }),
  ()=>({ q:"Le seul endroit où le succès vient avant le travail, c'est le dictionnaire.", by:"Vince Lombardi" }),
  ()=>({ q:"La motivation te lance, l'habitude te fait tenir.", by:"Jim Ryun" }),
  ()=>({ q:"On ne perd jamais : soit on gagne, soit on apprend.", by:"Nelson Mandela" }),
  ()=>({ q:"Le succès, c'est aller d'échec en échec sans perdre l'enthousiasme.", by:"Winston Churchill" }),
  ()=>({ q:"Fais ce que tu peux, avec ce que tu as, là où tu es.", by:"Theodore Roosevelt" }),
  ()=>({ q:"La chance sourit aux audacieux.", by:"Virgile" }),
  ()=>({ q:"Les gens oublieront ce que tu as dit, mais jamais ce que tu leur as fait ressentir.", by:"Maya Angelou" }),
  ()=>({ q:"Sois toi-même, les autres sont déjà pris.", by:"Oscar Wilde" }),
  ()=>({ q:"Le bonheur n'est pas une destination, c'est une façon de voyager.", by:"Margaret Lee Runbeck" }),
  ()=>({ q:"Fais de ta vie un rêve, et d'un rêve une réalité.", by:"Saint-Exupéry" }),
  ()=>({ q:"Ce n'est pas la montagne qu'on conquiert, mais soi-même.", by:"Edmund Hillary" }),
];
function Splash({ text, onDone }) {
  useEffect(()=>{ const t=setTimeout(onDone, 3000); return ()=>clearTimeout(t); },[]);
  return <div onClick={onDone} style={{ position:'fixed', inset:0, zIndex:80, background:T.bg, color:T.text, ...hk, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:20, padding:30 }}>
    <img className="splash-logo" src="/icon-512.png" alt={BRAND} width={104} height={104} style={{ borderRadius:24, boxShadow:'0 14px 40px rgba(0,0,0,0.55)' }} />
    {text && typeof text === 'object' && text.q
      ? <div style={{ maxWidth:360, textAlign:'center', animation:'splashin .35s ease' }}>
          <div style={{ ...ax, fontSize:21, fontWeight:700, fontStyle:'italic', lineHeight:1.4 }}>« {text.q} »</div>
          <div style={{ ...mo, fontSize:12.5, color:T.muted, marginTop:12, letterSpacing:0.5, textTransform:'uppercase' }}>— {text.by}</div>
        </div>
      : <div style={{ ...ax, fontSize:23, fontWeight:800, textAlign:'center', lineHeight:1.3, maxWidth:340, animation:'splashin .35s ease' }}>{text}</div>}
    <div style={{ width:120, height:4, borderRadius:4, background:'var(--track)', overflow:'hidden' }}><div style={{ height:'100%', background:T.volt, animation:'splashbar 3s linear forwards' }} /></div>
  </div>;
}

// Tour guidé au premier lancement : projecteur sur les vrais éléments + bulle. Impossible à quitter.
function Tour({ name, prime, tiers, onDone }) {
  // Montants tirés de la config (white-label) : pas de prime/palier codé en dur.
  const P = prime || {}, MRR = P.mrr || 5, CPT = P.comptant || 10;
  const tierStr = (tiers && tiers.length) ? tiers.map(t=>`${t.seuil} avis = ${t.prime}€`).join(', ') : null;
  const steps = [
    { sel:null, emo:'👋', label:'Bienvenue', title:`Salut ${name}, bienvenue dans ton app`, text:`Tes leads, ta cagnotte, tes essais, tes avis : tout au même endroit, sans Excel ni feuille volante. Je te montre l'essentiel, ça va vite.` },
    { sel:'cagnotte', emo:'💰', label:'Cagnotte', title:'Ta prime du mois, en direct', text:`Elle monte à chaque inscription : ${MRR}€ par abonnement, ${CPT}€ par comptant. Pas de plafond. Plus tu convertis, plus elle grimpe.` },
    { sel:'stats', emo:'📊', label:'Tes leads', title:'Ton tableau de bord', text:`Tes leads actifs, en cours, perdus : touche un chiffre pour ouvrir la liste. Au-dessus, l'entonnoir montre où ça bloque entre le lead, le RDV et la conversion.` },
    { sel:'nav-leads', emo:'📞', label:'Les appels', title:'Tes leads à appeler', text:`Les nouveaux en rouge, c'est urgent : à rappeler vite avant que le lead refroidisse. Touche une fiche pour Appeler, WhatsApp, poser un RDV ou convertir.` },
    { sel:'nav-sessions', emo:'📅', label:'Séances', title:'Les essais à confirmer', text:`Tes séances d'essai du jour, triées par heure. Un WhatsApp en 1 touche, la veille ET le matin : un essai pas confirmé, c'est un essai qui ne vient pas.` },
    { sel:'avis', emo:'⭐', label:'Avis', title:'La prime avis, pour toute l\'équipe', text:`Plus on récolte d'avis Google, plus la prime monte pour tout le monde${tierStr?` (${tierStr})`:''}. Touche « Demander un avis » : QR code en face à face, ou lien sur WhatsApp.` },
    { sel:'nav-gains', emo:'🏆', label:'Classement', title:'Tes primes et le classement', text:`Le détail de tes conversions, l'historique de tes paies et le classement de l'équipe. Un peu de compétition, ça motive.` },
    { sel:'nav-me', emo:'🤳', label:'Profil', title:`Allez ${name}, mets ta photo`, text:`Touche ton avatar et mets TA photo, pas un rond gris. Tu y choisis aussi la couleur de l'app et le mode jour ou nuit. C'est parti !` },
  ];
  const [i, setI] = useState(0);
  const [rect, setRect] = useState(null);
  const s = steps[i], last = i===steps.length-1, first = i===0;
  const pct = Math.round(((i+1)/steps.length)*100);
  useEffect(()=>{
    if(!s.sel){ setRect(null); return; }
    const el = document.querySelector(`[data-tour="${s.sel}"]`);
    if(!el){ setRect(null); return; }
    // Centre la cible en faisant defiler le conteneur scrollable interne (pas toute la page).
    let p = el.parentElement;
    while(p){ const o=getComputedStyle(p).overflowY; if(o==='auto'||o==='scroll'){ const er=el.getBoundingClientRect(), pr=p.getBoundingClientRect(); p.scrollTop += (er.top - pr.top) - (p.clientHeight/2 - er.height/2); break; } p=p.parentElement; }
    const m = ()=> setRect(el.getBoundingClientRect());
    const raf = requestAnimationFrame(m);
    const t1 = setTimeout(m, 120), t2 = setTimeout(m, 360);
    return ()=>{ cancelAnimationFrame(raf); clearTimeout(t1); clearTimeout(t2); };
  },[i]);
  const pad=8;
  const hole = rect ? { top:Math.max(6,rect.top-pad), left:Math.max(6,rect.left-pad), width:rect.width+pad*2, height:rect.height+pad*2 } : null;
  let tip;
  if(!rect) tip = { top:'50%', transform:'translateY(-50%)' };
  else if(rect.top > window.innerHeight*0.5) tip = { bottom:(window.innerHeight - rect.top) + 16 };
  else tip = { top: rect.bottom + 16 };
  const [celeb, setCeleb] = useState(false);
  const finish = () => { setCeleb(true); setTimeout(onDone, 2800); };
  if(celeb) return <TourCeleb name={name} />;
  return <>
    <div style={{ position:'fixed', inset:0, zIndex:84 }} /> {/* bloque toute interaction avec l'app */}
    {hole
      ? <div style={{ position:'fixed', top:hole.top, left:hole.left, width:hole.width, height:hole.height, borderRadius:14, boxShadow:'0 0 0 9999px rgba(8,9,11,0.82)', border:'2px solid var(--volt)', zIndex:85, pointerEvents:'none', transition:'all .3s ease' }} />
      : <div style={{ position:'fixed', inset:0, background:'radial-gradient(ellipse at center, rgba(8,9,11,0.80), rgba(8,9,11,0.94))', zIndex:85, pointerEvents:'none', backdropFilter:'blur(4px)' }} />}
    <div style={{ position:'fixed', left:14, right:14, maxWidth:460, margin:'0 auto', zIndex:86, background:T.elev, border:'2px solid var(--volt)', borderRadius:22, padding:0, boxShadow:'0 0 0 8px var(--volt-line), 0 30px 80px rgba(0,0,0,.65)', animation:'splashin .25s ease', overflow:'hidden', ...tip }}>
      {/* Barre de progression */}
      <div style={{ height:4, background:T.line, position:'relative' }}>
        <div style={{ position:'absolute', left:0, top:0, bottom:0, width:pct+'%', background:'var(--volt)', borderRadius:'0 4px 4px 0', boxShadow:'0 0 14px var(--volt-line)', transition:'width .35s cubic-bezier(.4,0,.2,1)' }} />
      </div>
      {/* Bouton skip */}
      <button onClick={finish} aria-label="Passer" style={{ position:'absolute', top:14, right:14, background:'rgba(0,0,0,.3)', border:`1px solid ${T.line}`, color:T.muted, width:32, height:32, borderRadius:'50%', cursor:'pointer', fontSize:14, zIndex:2, display:'flex', alignItems:'center', justifyContent:'center' }}>✕</button>
      {/* Hero emoji */}
      <div style={{ display:'flex', alignItems:'center', justifyContent:'center', padding:'22px 0 4px', background:'linear-gradient(180deg, var(--volt-line) 0%, transparent 100%)' }}>
        <span style={{ fontSize:54, lineHeight:1, animation:'tourBounce 1.6s ease-in-out infinite' }}>{s.emo}</span>
      </div>
      {/* Label section */}
      <div style={{ ...hk, fontSize:11, fontWeight:900, letterSpacing:1, textTransform:'uppercase', color:'var(--volt)', textAlign:'center', margin:'6px 0 0' }}>{s.label} · {i+1}/{steps.length}</div>
      {/* Body */}
      <div style={{ padding:'8px 22px 18px' }}>
        <h4 style={{ ...ax, fontSize:22, fontWeight:900, lineHeight:1.25, letterSpacing:'-.4px', margin:'8px 0 8px', color:T.text, textAlign:'center' }}>{s.title}</h4>
        <p style={{ fontSize:14.5, color:T.text, opacity:.78, lineHeight:1.55, margin:0, textAlign:'center', ...hk }}>{s.text}</p>
      </div>
      {/* Foot */}
      <div style={{ display:'flex', alignItems:'center', gap:10, padding:'16px 22px 20px', borderTop:`1px solid ${T.line}` }}>
        <button onClick={()=>!first&&setI(i-1)} disabled={first} aria-label="Précédent" style={{ background:'transparent', border:`1px solid ${T.line}`, color:T.muted, borderRadius:999, width:42, height:42, fontSize:18, cursor:first?'not-allowed':'pointer', flex:'0 0 auto', ...ax, fontWeight:900, opacity:first?.3:1 }}>‹</button>
        <div style={{ display:'flex', gap:6, flex:1, justifyContent:'center', flexWrap:'wrap' }}>
          {steps.map((_,k)=>(<span key={k} style={{ width:k===i?22:7, height:7, borderRadius:7, background:k===i?'var(--volt)':T.line, transition:'all .25s', boxShadow:k===i?'0 0 8px var(--volt-line)':'none' }} />))}
        </div>
        <button onClick={()=> last?finish():setI(i+1)} style={{ background:'var(--volt)', color:T.ink, border:'none', borderRadius:999, padding:'12px 22px', ...ax, fontWeight:900, fontSize:14, cursor:'pointer', flex:'0 0 auto', boxShadow:'0 6px 20px var(--volt-line)', minWidth:120 }}>{last?'Let\'s go 🔥':'Suivant ›'}</button>
      </div>
    </div>
    <style>{`@keyframes tourBounce { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-8px) scale(1.08); } }`}</style>
  </>;
}

// Mini-célébration fin de guide : overlay plein écran + confettis + texte motivant.
function TourCeleb({ name }) {
  const emojis = ['🎉','✨','🎊','⭐','🔥','💪','🚀','💥'];
  const conf = [];
  for(let i=0; i<22; i++){
    const e = emojis[i % emojis.length];
    const left = Math.round(Math.random()*100);
    const cx = Math.round((Math.random()-.5)*240);
    const delay = (Math.random()*.6).toFixed(2);
    const dur = (1.8 + Math.random()*1.0).toFixed(2);
    conf.push({ e, left, cx, delay, dur, k:i });
  }
  return <div style={{ position:'fixed', inset:0, background:'radial-gradient(ellipse at center, var(--volt-line), rgba(0,0,0,.92))', display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', zIndex:120, overflow:'hidden', animation:'tcIn .35s ease-out' }}>
    <div style={{ width:140, height:140, borderRadius:'50%', border:'5px solid var(--volt)', boxShadow:'0 0 0 10px var(--volt-line), 0 20px 60px var(--volt-line)', background:'var(--volt)', display:'flex', alignItems:'center', justifyContent:'center', fontSize:74, animation:'tcPop .8s cubic-bezier(.2,.9,.3,1.6) both, tcBounce 1.6s ease-in-out .8s infinite' }}>🚀</div>
    <div style={{ ...ax, fontFamily:'inherit', fontWeight:900, fontSize:42, color:'#fff', letterSpacing:'-1px', marginTop:24, textShadow:'0 4px 18px rgba(0,0,0,.5)', animation:'tcUp .45s ease-out .25s both' }}>Let's go {name} 🔥</div>
    <div style={{ ...hk, fontWeight:700, fontSize:20, color:'#fff', marginTop:6, opacity:.9, animation:'tcUp .45s ease-out .45s both' }}>T'es prêt(e), à toi de jouer.</div>
    {conf.map(c => (
      <div key={c.k} style={{ position:'absolute', left:c.left+'%', top:'-40px', fontSize:32, pointerEvents:'none', ['--cx']:c.cx+'px', animation:`tcConfetti ${c.dur}s ease-out ${c.delay}s forwards` }}>{c.e}</div>
    ))}
    <style>{`
      @keyframes tcIn { 0% { opacity:0 } 100% { opacity:1 } }
      @keyframes tcPop { 0% { transform: scale(0) rotate(-30deg); opacity:0; } 60% { transform: scale(1.15) rotate(5deg); opacity:1; } 100% { transform: scale(1) rotate(0); opacity:1; } }
      @keyframes tcBounce { 0%,100% { transform: scale(1); } 50% { transform: scale(1.05); } }
      @keyframes tcUp { 0% { transform: translateY(20px); opacity:0; } 100% { transform: translateY(0); opacity:1; } }
      @keyframes tcConfetti { 0% { transform: translate(0, -30vh) rotate(0); opacity:1; } 100% { transform: translate(var(--cx, 0), 100vh) rotate(720deg); opacity:0; } }
    `}</style>
  </div>;
}

// ════════ LOGIN ════════
function Login({ onDone }) {
  // Durci : on TAPE son prénom (plus de liste de noms exposée). La liste de l'équipe n'est
  // plus chargée avant connexion -> /api/config est protégé par mot de passe côté serveur.
  const [who, setWho] = useState(meName()); const [pwd, setPwd] = useState('');
  const [err, setErr] = useState(''); const [busy, setBusy] = useState(false); const [showPwd, setShowPwd] = useState(false);
  const submit = async () => {
    const name = who.trim();
    if (!name || !pwd) { setErr('Entre ton prénom et ton mot de passe'); return; }
    setBusy(true); setErr('');
    localStorage.setItem(LS.me,name); localStorage.setItem(LS.code,pwd); localStorage.setItem(LS.codeFor,name);
    try { await api('/api/stats?who='+encodeURIComponent(name)); onDone(); }
    catch(e){ setErr('Prénom ou mot de passe incorrect'); } finally { setBusy(false); }
  };
  const inpStyle = { width:'100%', background:T.surface, border:`1px solid ${err?'#ff6b6b':T.line}`, borderRadius:14, padding:'14px 16px', color:T.text, ...hk, fontSize:16, outline:'none', boxSizing:'border-box' };
  return <div style={{ minHeight:'100%', background:T.bg, color:T.text, ...hk, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', padding:'40px 24px' }}>
    <img src="/icon-512.png" alt={BRAND} width={92} height={92} style={{ borderRadius:22, marginBottom:10, boxShadow:'0 12px 34px rgba(0,0,0,0.55)' }} />
    <div style={{ ...mo, fontSize:19, fontWeight:700, letterSpacing:3, textTransform:'uppercase', color:T.text, marginBottom:30 }}>App staff</div>
    <div style={{ ...hk, fontSize:16, marginBottom:16 }}>Connecte-toi</div>
    <div style={{ display:'flex', flexDirection:'column', gap:10, width:'100%', maxWidth:320 }}>
      <input value={who} autoFocus autoCapitalize="words" autoCorrect="off" spellCheck={false} onChange={e=>setWho(e.target.value)} onKeyDown={e=>{ if(e.key==='Enter') submit(); }} placeholder="Ton prénom" style={inpStyle} />
      <div style={{ position:'relative' }}>
        <input type={showPwd?'text':'password'} value={pwd} onChange={e=>setPwd(e.target.value)} onKeyDown={e=>{ if(e.key==='Enter') submit(); }} placeholder="Mot de passe" style={{ ...inpStyle, paddingRight:46 }} />
        <button type="button" onClick={()=>setShowPwd(v=>!v)} aria-label={showPwd?'Masquer le mot de passe':'Afficher le mot de passe'} style={{ position:'absolute', right:6, top:'50%', transform:'translateY(-50%)', background:'none', border:'none', cursor:'pointer', padding:8, color:showPwd?T.text:T.muted, display:'flex', alignItems:'center' }}>
          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round">
            <path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" /><circle cx="12" cy="12" r="3" />
            {!showPwd && <path d="M4 4l16 16" />}
          </svg>
        </button>
      </div>
      {err && <div style={{ color:'#ff8a8a', fontSize:13 }}>{err}</div>}
      <button onClick={submit} disabled={busy} style={{ width:'100%', ...gbtn(), fontSize:16, padding:'14px 0', marginTop:2 }}>{busy?'…':'Se connecter'}</button>
    </div>
    <div style={{ marginTop:36, display:'flex', flexDirection:'column', alignItems:'center', gap:7, opacity:0.55 }}>
      <div style={{ ...mo, fontSize:9, letterSpacing:2, textTransform:'uppercase', color:T.faint }}>Propulsé par</div>
      <img src="/assets/onpush-logo-blanc.png" alt="OnPush" style={{ height:15, width:'auto', display:'block' }} />
    </div>
  </div>;
}

// ════════ APP ════════
// Fenêtre bloquante : tant que le vendeur n'a pas activé les notifs OS, l'app est grisée derrière
// et il ne peut RIEN faire d'autre que les activer (pas de bouton "passer"). Demande du patron :
// les notifs (nouveau lead, relances, manager) sont obligatoires pour le staff.
function PushGate({ onEnabled }) {
  const [busy, setBusy] = useState(false);
  const supported = pushSupported();
  let denied = false; try { denied = ('Notification' in window) && Notification.permission === 'denied'; } catch (_) {}
  const activate = async () => {
    if (busy) return; setBusy(true);
    const ok = await enablePush({ skipConfirm: true }); setBusy(false);
    if (ok) { onEnabled(); return; }
    try { if (Notification.permission === 'granted') onEnabled(); } catch (_) {} // déjà accordé entre-temps
  };
  return <div style={{ position:'fixed', inset:0, zIndex:130, background:'rgba(0,0,0,0.86)', backdropFilter:'blur(5px)', WebkitBackdropFilter:'blur(5px)', display:'flex', alignItems:'center', justifyContent:'center', padding:24, boxSizing:'border-box' }}>
    <div style={{ background:T.elev||T.surface, border:`1px solid ${T.line}`, borderRadius:24, padding:'30px 24px 26px', maxWidth:380, width:'100%', textAlign:'center', boxShadow:'0 24px 70px rgba(0,0,0,0.6)' }}>
      <div style={{ fontSize:54, lineHeight:1, marginBottom:14 }}>🔔</div>
      <div style={{ ...ax, fontSize:22, fontWeight:900, color:T.text, marginBottom:10 }}>Active tes notifications</div>
      <div style={{ ...hk, fontSize:14.5, lineHeight:1.5, color:T.muted, marginBottom:22 }}>
        Elles sont <b style={{ color:T.text }}>obligatoires</b> pour le staff : tu es prévenu dès qu'un <b style={{ color:T.text }}>nouveau lead</b> arrive (à rappeler vite), des relances et des messages du manager, même quand l'app est fermée.
      </div>
      {supported ? (
        <button onClick={activate} disabled={busy} style={{ ...gbtn({ width:'100%', padding:'15px 0', fontSize:15.5, opacity:busy?0.6:1 }) }}>
          {busy ? 'Activation…' : '🔔 Activer maintenant'}
        </button>
      ) : (
        <div style={{ ...hk, fontSize:13.5, lineHeight:1.5, color:T.text, background:T.accentDim, border:`1px solid ${T.line}`, borderRadius:14, padding:'14px 16px', textAlign:'left' }}>
          📲 Sur iPhone, ajoute d'abord l'app à ton <b>écran d'accueil</b> : bouton <b>Partager</b> ↑ puis <b>« Sur l'écran d'accueil »</b>. Rouvre ensuite l'app depuis son icône.
          <button onClick={()=>{ try{ if(pushSupported()&&Notification.permission==='granted') onEnabled(); else location.reload(); }catch(_){ location.reload(); } }} style={{ ...gbtn({ width:'100%', padding:'13px 0', marginTop:14, fontSize:14 }) }}>J'ai installé l'app → réessayer</button>
        </div>
      )}
      {denied && supported && <div style={{ ...hk, fontSize:12.5, lineHeight:1.45, color:T.accent, marginTop:14 }}>
        Tu as déjà refusé : va dans les <b>réglages</b> de ton téléphone → cette app → Notifications → Autoriser, puis reviens.
      </div>}
    </div>
  </div>;
}

// Bouton flottant « Signaler un bug » : capture l'écran courant + un mot, et ça part au manager.
const SCREEN_LABELS = { home:'Accueil / Pilotage', leads:'Leads', sessions:"Séances d'essai", gains:'Primes / Équipe', me:'Profil' };
function BugButton({ tab }){
  const [open, setOpen] = useState(false);
  const [capturing, setCapturing] = useState(false);
  const [shot, setShot] = useState('');
  const [capFail, setCapFail] = useState(false);
  const [text, setText] = useState('');
  const [busy, setBusy] = useState(false);
  const [sent, setSent] = useState(false);
  const screen = SCREEN_LABELS[tab] || tab || '';
  const start = async () => {
    if (capturing) return;
    setShot(''); setCapFail(false); setText(''); setSent(false); setBusy(false); setCapturing(true);
    let url = '';
    try { url = await captureScreen(); } catch(_){ setCapFail(true); }   // capture AVANT d'ouvrir la fenêtre, sinon on capturerait la fenêtre
    setShot(url); setCapturing(false); setOpen(true);
  };
  const send = async () => {
    if (!text.trim() || busy) return;
    setBusy(true);
    const ver = (typeof CHANGELOG !== 'undefined' && CHANGELOG[0] && (CHANGELOG[0].v || CHANGELOG[0].date)) || '';
    try {
      const r = await api('/api/bug', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ text: text.trim(), dataUrl: shot, screen, ver, ua: navigator.userAgent, errors: BUG_ERRORS.slice(-8) }) });
      if (r && r.ok) { setSent(true); setTimeout(()=>setOpen(false), 1500); }
      else alert("Échec de l'envoi : " + ((r && r.reason) || 'réessaie'));
    } catch(e){ alert('Échec : ' + (e.message||e)); } finally { setBusy(false); }
  };
  return <>
    <button data-bughide="1" onClick={start} title="Signaler un bug" aria-label="Signaler un bug"
      style={{ position:'fixed', left:14, bottom:'calc(78px + env(safe-area-inset-bottom))', zIndex:60, width:42, height:42, borderRadius:999,
        background:'rgba(28,31,38,0.82)', border:`1px solid ${T.line}`, color:T.text, fontSize:18, cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center',
        backdropFilter:'blur(6px)', WebkitBackdropFilter:'blur(6px)', boxShadow:'0 6px 18px rgba(0,0,0,0.4)', opacity:0.82 }}>{capturing ? '⏳' : '🐛'}</button>
    {open && <Sheet onClose={()=>{ if(!busy) setOpen(false); }} tall>
      {sheetTitle('🐛 Signaler un bug')}
      {sent
        ? <div style={{ ...hk, fontSize:14.5, color:GREEN, padding:'22px 0', textAlign:'center', lineHeight:1.5 }}>✅ Envoyé au manager, merci !<br/><span style={{ color:T.muted, fontSize:13 }}>Le bug est parti avec la capture.</span></div>
        : <>
          <div style={{ ...hk, fontSize:13, color:T.muted, marginBottom:12, lineHeight:1.5 }}>La capture de l'écran est prise automatiquement. Explique vite ce qui cloche, ça part direct au manager.</div>
          <div style={{ borderRadius:14, overflow:'hidden', border:`1px solid ${T.line}`, marginBottom:12, background:T.surface, minHeight:130, maxHeight:300, display:'flex', alignItems:'center', justifyContent:'center' }}>
            {shot ? <img src={shot} alt="capture de l'écran" style={{ width:'100%', display:'block' }} />
              : capFail ? <div style={{ ...hk, fontSize:12.5, color:T.amber, padding:'18px', textAlign:'center', lineHeight:1.5 }}>Capture impossible sur cet écran. Décris quand même le bug, je l'envoie sans image.</div>
              : <div style={{ ...hk, fontSize:13, color:T.faint, padding:'26px' }}>Capture en cours…</div>}
          </div>
          <textarea value={text} onChange={(e)=>setText(e.target.value)} placeholder="C'est quoi le souci ? (ex : le bouton Converti ne réagit pas)" rows={4}
            style={{ width:'100%', resize:'none', borderRadius:12, border:`1px solid ${T.line}`, background:T.surface, color:T.text, padding:'12px', fontSize:14, ...hk, marginBottom:12, boxSizing:'border-box', outline:'none' }} />
          <button onClick={send} disabled={busy || !text.trim()} style={{ width:'100%', ...gbtn(), padding:'14px 0', fontSize:15, opacity:(busy||!text.trim())?0.55:1 }}>{busy ? 'Envoi…' : '📨 Envoyer au manager'}</button>
        </>}
    </Sheet>}
  </>;
}

function App() {
  const [authed, setAuthed] = useState(!!localStorage.getItem(LS.code));
  const [tab, setTab] = useState('home');
  const [d, setD] = useState(null);
  const [err, setErr] = useState('');
  const [leadSheet, setLeadSheet] = useState(null);
  const [actionSheet, setActionSheet] = useState(null);
  const [noAnsSheet, setNoAnsSheet] = useState(null);
  const [convSheet, setConvSheet] = useState(null);
  const [waSheet, setWaSheet] = useState(null);
  const [win, setWin] = useState(null);
  const [splash, setSplash] = useState(null);
  const [ver, setVer] = useState(0);
  const [catSheet, setCatSheet] = useState(null);
  const [onboard, setOnboard] = useState(false);
  const [photoNudge, setPhotoNudge] = useState(false);
  const [walkin, setWalkin] = useState(false);
  const [orphansSheet, setOrphansSheet] = useState(false);
  const [suiviSheet, setSuiviSheet] = useState(false);
  // Notifs OS obligatoires : true tant que la permission n'est pas accordée -> fenêtre bloquante.
  const [needPush, setNeedPush] = useState(()=>{ try { return ('Notification' in window) && Notification.permission!=='granted'; } catch(_){ return false; } });
  const [viewAs, setViewAsState] = useState(viewAsName());
  const isAdmin = meName() === ADMIN;
  const setViewAs = (name) => {
    if (name) { sessionStorage.setItem('4ds_viewas', name); } else { sessionStorage.removeItem('4ds_viewas'); }
    setViewAsState(name||''); setTab('home'); setD(null); setErr('');
  };
  // Garde-fou : aucune action d'écriture en vue admin (on regarde, on ne modifie pas l'identité du membre).
  const guardRO = () => { if (viewAsName()) { alert('👁 Tu es en vue admin (lecture seule).\nReviens sur ton profil pour agir.'); return true; } return false; };

  const load = async (opts={}) => {
    const me = effName();
    const impersonating = !!viewAsName();
    const light = !!opts.light; // refresh léger : pas de log connexion, pas de gros scan classement
    try {
      const [stats, leadsR, convR, revR, cfgR, seancesR] = await Promise.all([
        api('/api/stats?who='+encodeURIComponent(me)).catch(()=>({})), api('/api/leads'),
        api('/api/conversions?who='+encodeURIComponent(me)).catch(()=>({items:[]})),
        api('/api/reviews').catch(()=>null), api('/api/config').catch(()=>null),
        api('/api/seances').catch(()=>null),
      ]);
      if (cfgR && cfgR.photos) window.__photos = cfgR.photos;
      const allMapped = (leadsR.leads||[]).map(mapLead);
      const seances = ((seancesR&&seancesR.seances)||[]).map(mapLead);
      const myLeads = allMapped.filter(l=>l.assigne===me||!l.assigne)
        // nouveaux (urgent) d'abord, puis relances triées par date (la plus en retard / ancienne en premier)
        .sort((a,b)=>{ if(a.urgent!==b.urgent) return a.urgent?-1:1; if(a.urgent) return 0; return (a.relanceLe||'9999').localeCompare(b.relanceLe||'9999'); });
      setD(prev => ({ me, stats, goal:300, cagnotte:(stats.cagnotte||0) + (revR?.prime||0), inscrits:stats.closes||0, pctConv:stats.pctConv||0,
        leads:myLeads, allLeads:allMapped, seances, conversions:convR.items||[], leaderboard:(prev&&prev.leaderboard)||[], team:(cfgR&&cfgR.team)||[], reviews:revR,
        teamStats:(prev&&prev.teamStats)||null, activity:(prev&&prev.activity)||[] }));
      setVer(v=>v+1);
      // Enregistre la connexion du membre (throttlé côté serveur). PAS en vue admin ni en refresh léger.
      if (!impersonating && !light) api('/api/seen', { method:'POST' }).catch(()=>{});
      // Classement (scan lourd des leads) : seulement en chargement complet, pas à chaque refresh léger.
      if (!light) api('/api/leaderboard?me='+encodeURIComponent(me)).then(lb=> setD(prev=> prev ? { ...prev, leaderboard: lb.items||[] } : prev)).catch(()=>{});
      // Données de pilotage admin (chiffres globaux + activité équipe) : seulement sur le cockpit admin, pas en vue impersonation.
      if (!light && isAdmin && !impersonating) {
        api('/api/teamstats').then(t=> setD(prev=> prev ? { ...prev, teamStats: t } : prev)).catch(()=>{});
        api('/api/activity').then(a=> setD(prev=> prev ? { ...prev, activity: a.items||[] } : prev)).catch(()=>{});
      }
    } catch(e){ if(e.auth){ logout(); return; } setErr(e.message||'Erreur'); }
  };
  const logout = () => { localStorage.removeItem(LS.code); sessionStorage.removeItem('4ds_splash'); setAuthed(false); setD(null); setTab('home'); };
  useEffect(()=>{ if(authed) load(); },[authed, viewAs]);
  // Refresh léger auto toutes les 90s, uniquement si l'app est au premier plan et qu'aucune fenêtre n'est ouverte.
  useEffect(()=>{ if(!authed) return;
    const id = setInterval(()=>{ if(document.visibilityState==='visible' && !leadSheet && !actionSheet && !noAnsSheet && !convSheet && !walkin && !catSheet) load({light:true}); }, 90000);
    return ()=>clearInterval(id);
  },[authed, leadSheet, actionSheet, noAnsSheet, convSheet, walkin, catSheet]);
  // Refresh quand l'app revient au premier plan (rouvre le raccourci / rebascule dessus).
  useEffect(()=>{ if(!authed) return;
    const onVis = ()=>{ if(document.visibilityState==='visible') load({light:true}); };
    document.addEventListener('visibilitychange', onVis); window.addEventListener('focus', onVis);
    return ()=>{ document.removeEventListener('visibilitychange', onVis); window.removeEventListener('focus', onVis); };
  },[authed]);
  // Re-vérifie la permission notifs au retour au premier plan (le vendeur a pu l'activer dans les réglages).
  useEffect(()=>{ if(!authed) return;
    const recheck = ()=>{ try { if(('Notification' in window) && Notification.permission==='granted') setNeedPush(false); } catch(_){} };
    document.addEventListener('visibilitychange', recheck); window.addEventListener('focus', recheck);
    return ()=>{ document.removeEventListener('visibilitychange', recheck); window.removeEventListener('focus', recheck); };
  },[authed]);
  // Splash de motivation : une fois par session, au lancement (pas à chaque refresh de données)
  useEffect(()=>{ if(authed && !sessionStorage.getItem('4ds_splash')){ const n=meName(); const QN=14; /* citations en fin de liste */ const idx = Math.random()<0.3 ? (SLOGANS.length-QN+Math.floor(Math.random()*QN)) : Math.floor(Math.random()*(SLOGANS.length-QN)); setSplash(SLOGANS[idx](n)); sessionStorage.setItem('4ds_splash','1'); } },[authed]);
  // Tour guidé : une seule fois par appareil, au premier lancement (force l'Accueil).
  useEffect(()=>{ if(authed && !localStorage.getItem('4ds_onboarded')){ setTab('home'); setOnboard(true); } },[authed]);
  // Rappel photo de profil (fun) : 1×/session si pas de photo, et seulement après le tour de bienvenue.
  useEffect(()=>{ if(authed && d && !onboard && !viewAs && !sessionStorage.getItem('4ds_photonudge') && !(window.__photos||{})[d.me]){ sessionStorage.setItem('4ds_photonudge','1'); setPhotoNudge(true); } },[d, authed, onboard]);

  const closeAll = () => { setLeadSheet(null); setActionSheet(null); setNoAnsSheet(null); setConvSheet(null); setWaSheet(null); setCatSheet(null); };
  const dropLead = (id) => setD(prev => prev ? { ...prev, leads: prev.leads.filter(l=>l.id!==id) } : prev);
  const afterAction = (dropId) => { closeAll(); if (dropId) dropLead(dropId); load(); };

  // Handlers passés à tous les écrans/cartes
  const H = {
    reload: () => load(),
    call: (l) => { if(l.phone) window.location.href='tel:'+l.phone.replace(/\s/g,''); },
    wa: (l) => { setLeadSheet(null); setWaSheet({ lead:l }); },
    conv: (l) => { if(guardRO())return; setLeadSheet(null); setConvSheet({ lead:l }); },
    open: (l, mode) => { if(guardRO())return; setLeadSheet(null); setActionSheet({ lead:l, mode }); },
    mauvais: async (l) => { if(guardRO())return; try{ await act(l.id,'mauvaisnum',{}); afterAction(l.id); }catch(e){ alert('Échec : '+e.message); } },
    mineur: async (l) => { if(guardRO())return; if(!confirm('Marquer comme mineur ?\nIl sort de la file et de tes stats, mais ses coordonnées (nom, tél, email) sont gardées pour plus tard.')) return; try{ await act(l.id,'mineur',{}); afterAction(l.id); }catch(e){ alert('Échec : '+e.message); } },
    deciplus: async (l) => { if(guardRO())return; if(!confirm('Passer sur Deciplus ?\nLe lead sort de la file d\'appels et part en emailing. Ce n\'est PAS une conversion.')) return; try{ await act(l.id,'deciplus',{}); afterAction(l.id); }catch(e){ alert('Échec : '+e.message); } },
    delLead: async (l) => { if(guardRO())return; if(!confirm('Supprimer ce lead ?\n\n« '+(l.name||'Ce lead')+' » part à la corbeille Notion (récupérable). Action réservée à l\'admin.')) return; try{ const r=await api('/api/deletelead',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ id:l.id }) }); if(r&&r.ok){ afterAction(l.id); } else { alert('Échec : '+((r&&(r.error||r.reason))||'?')); } }catch(e){ alert('Échec : '+e.message); } },
    noAnswer: (l) => { if(guardRO())return; setLeadSheet(null); setNoAnsSheet({ lead:l }); },
    deconv: async (l) => { if(guardRO())return; try{ await act(l.id,'deconverti',{}); closeAll(); load(); }catch(e){ alert('Échec : '+e.message); } },
    openLead: (l, list) => { const L = (list&&list.length)?list:(d?d.leads:[l]); const i = Math.max(0, L.findIndex(x=>x.id===l.id)); setLeadSheet({ list:L, index:i }); },
    openCat: (cat, title, presetLeads) => setCatSheet({ cat, title, presetLeads }),
    replayTour: () => { closeAll(); setTab('home'); setOnboard(true); },
    walkin: () => { if(guardRO())return; setWalkin(true); },
    orphans: () => setOrphansSheet(true),
    suivi: () => setSuiviSheet(true),
    isAdmin, viewAs, setViewAs,
  };

  if (!authed) return <Login onDone={()=>setAuthed(true)} />;
  if (err) return <div style={{ minHeight:'100%', background:T.bg, color:T.text, ...hk, display:'flex', flexDirection:'column', gap:12, alignItems:'center', justifyContent:'center', padding:24 }}><div>Erreur : {err}</div><button onClick={()=>{ setErr(''); load(); }} style={{ ...gbtn(), padding:'10px 18px' }}>Réessayer</button></div>;
  if (!d) return <><div style={{ minHeight:'100%', background:T.bg, color:T.muted, ...hk, display:'flex', alignItems:'center', justifyContent:'center' }}>Chargement…</div>{splash && <Splash text={splash} onDone={()=>setSplash(null)} />}</>;

  const go = (t) => { setTab(t); load(); };
  const props = { d, go, H, ver };
  return <div style={{ height:'100%', display:'flex', flexDirection:'column', paddingTop:'env(safe-area-inset-top)', boxSizing:'border-box' }}>
    {viewAs && <div onClick={()=>setViewAs('')} style={{ flex:'0 0 auto', background:'#7c3aed', color:'#fff', ...hk, fontSize:12.5, fontWeight:700, padding:'10px 16px', display:'flex', alignItems:'center', justifyContent:'space-between', cursor:'pointer' }}>
      <span>👁 Vue de <b>{viewAs}</b> · lecture seule</span>
      <span style={{ textDecoration:'underline' }}>Revenir à mon profil</span>
    </div>}
    <div style={{ flex:1, minHeight:0, position:'relative' }}>
    {tab==='home' && ((isAdmin && !viewAs) ? <ScreenAdminDashboard {...props} /> : <ScreenDashboard {...props} />)}
    {tab==='leads' && <ScreenLeads {...props} />}
    {tab==='sessions' && <ScreenSessions d={d} go={go} H={H} />}
    {tab==='gains' && ((isAdmin && !viewAs) ? <ScreenEquipe d={d} go={go} H={H} /> : <ScreenGains d={d} go={go} H={H} />)}
    {tab==='me' && <ScreenProfile d={d} go={go} H={H} onLogout={logout} />}
    </div>
    {catSheet && <CategorySheet cat={catSheet.cat} title={catSheet.title} who={effName()} presetLeads={catSheet.presetLeads} H={H} onClose={()=>setCatSheet(null)} />}
    {leadSheet && <LeadSheet state={leadSheet} setSheet={setLeadSheet} H={H} onClose={()=>setLeadSheet(null)} />}
    {actionSheet && <ActionSheet state={actionSheet} onClose={()=>setActionSheet(null)} onDone={afterAction} />}
    {noAnsSheet && <NoAnswerSheet lead={noAnsSheet.lead} onClose={()=>setNoAnsSheet(null)} onDone={()=>{ closeAll(); load(); }} />}
    {convSheet && <ConvSheet lead={convSheet.lead} team={d.team} prime={{ mrr:(d.stats&&d.stats.primeMrr)||5, comptant:(d.stats&&d.stats.primeComptant)||10 }} onClose={()=>setConvSheet(null)} onDone={(gain, who)=>{ const lead=convSheet.lead; const id=lead.id; closeAll(); dropLead(id); if(gain>0){ const total = who===meName() ? (d.cagnotte||0)+gain : null; setWin({ gain, who, total, lead }); } else { alert('✅ Retour offert enregistré.\nPas de prime tant qu\'il n\'est pas passé en abonnement payant. Reconvertis-le en Abonnement / Comptant à ce moment-là.'); } load(); }} />}
    {waSheet && <WaSheet lead={waSheet.lead} onClose={()=>setWaSheet(null)} onSent={(id)=>{ setD(prev=> prev ? { ...prev, seances: (prev.seances||[]).map(s=> s.id===id ? { ...s, seanceConfirmee:true } : s), leads: (prev.leads||[]).map(s=> s.id===id ? { ...s, seanceConfirmee:true } : s) } : prev); setTimeout(()=>load({light:true}), 1200); }} />}
    {orphansSheet && <OrphansSheet team={(d.team||[]).filter(m=>m!=='Ismail'&&m!=='Dev')} onClose={()=>{ setOrphansSheet(false); load(); }} />}
    {suiviSheet && <SuiviSheet me={effName()} isAdmin={isAdmin && !viewAs} onClose={()=>{ setSuiviSheet(false); load(); }} onDone={()=>{}} />}
    {walkin && <WalkinSheet onClose={()=>setWalkin(false)} onDone={(mode, info)=>{ setWalkin(false);
      if(mode==='inscrit'){ const gain = (info&&info.type==='Comptant') ? (d.stats?.primeComptant||10) : (d.stats?.primeMrr||5); setWin({ gain, who:meName(), total:(d.cagnotte||0)+gain, lead:{ name:(info&&info.name)||'', phone:(info&&info.phone)||'' } }); setTab('home'); }
      else { setTab(mode==='essai'?'sessions':'leads'); }
      load(); }} />}
    {win && <WinPopup win={win} onClose={()=>setWin(null)} />}
    {splash && <Splash text={splash} onDone={()=>setSplash(null)} />}
    {onboard && tab==='home' && <Tour name={d.me} prime={{ mrr:(d.stats&&d.stats.primeMrr)||5, comptant:(d.stats&&d.stats.primeComptant)||10 }} tiers={(d.reviews&&d.reviews.tiers)||[]} onDone={()=>{ localStorage.setItem('4ds_onboarded','1'); setOnboard(false); }} />}
    {photoNudge && <PhotoNudge name={d.me} onClose={(done)=>{ setPhotoNudge(false); if(done) setVer(v=>v+1); }} />}
    {/* Fenêtre bloquante notifs : pas en vue admin (lecture seule), sinon dès que la permission manque. */}
    {!viewAs && needPush && <PushGate onEnabled={()=>setNeedPush(false)} />}
    {!needPush && !onboard && <BugButton tab={tab} />}
  </div>;
}

// App réservée au mobile + tablette. On bloque les ordinateurs.
// iPad se fait passer pour un Mac (user-agent "Macintosh") -> on le rattrape via maxTouchPoints (écran tactile).
const IS_DESKTOP = (() => {
  try {
    const h = location.hostname || '';
    if (h === 'localhost' || h === '127.0.0.1' || h.endsWith('.local')) return false; // dev local : pas de blocage (pour développer/tester)
    const ua = navigator.userAgent || '';
    const mobileUA = /Android|iPhone|iPod|iPad|Mobile|Tablet|Silk|Kindle|Opera Mini|Windows Phone|BlackBerry|Mobi/i.test(ua);
    const touch = (navigator.maxTouchPoints || 0) > 1; // iPad/tablette tactile = >1, Mac/PC de bureau = 0
    return !(mobileUA || touch);
  } catch (_) { return false; }
})();

function DesktopBlock() {
  return <div style={{ minHeight:'100%', background:T.bg, color:T.text, ...hk, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', textAlign:'center', padding:'30px 26px', gap:18 }}>
    <div style={{ fontSize:54 }}>📱</div>
    <div style={{ ...ax, fontSize:22, fontWeight:900, letterSpacing:2, textTransform:'uppercase', color:T.onBg }}>Application réservée au mobile</div>
  </div>;
}

ReactDOM.createRoot(document.getElementById('root')).render(IS_DESKTOP ? <DesktopBlock /> : <App />);
