// FOOTPASS Messenger v2 · Écran Messages avec le système de filtres « Modèle 4 »
// (Substitution · fil d'Ariane horizontal). Repris du Labo et branché sur l'app réelle :
//  • Rangée de familles colorées : Agents · Clubs · Joueurs · Réseau
//  • Sous-rangée à substitution (cascade géo pour Clubs/Joueurs, structures pour Agents,
//    métiers pour Réseau) avec code couleur par sous-catégorie (une teinte par famille,
//    déclinée en tons OKLCH).
//  • Liste « Tous » mélangée et triée par récence (boîte de réception).
const { useState: useStateM4, useEffect: useEffectM4, useRef: useRefM4 } = React;

const M4D = window.FP_M4_DATA || { clubs: {}, agents: {}, msgs: [], staff: [], pros: [] };
const M4_CLUBS = M4D.clubs || {};
const M4_AGENTS = M4D.agents || {};
const M4_MSGS = M4D.msgs || [];
const M4_STAFF = M4D.staff || [];
const M4_PROS = M4D.pros || [];

// On retire la mention « (Fictif) » des noms, partout (lignes, sous-filtres, feuille +).
function m4Clean(s) { return typeof s === 'string' ? s.replace(/\s*\(fictif\)/ig, '').trim() : s; }
[...M4_MSGS, ...M4_STAFF, ...M4_PROS].forEach((o) => { if (o && o.name) o.name = m4Clean(o.name); });
Object.values(M4_AGENTS).forEach((a) => { if (a && a.label) a.label = m4Clean(a.label); });

// ─────────────── HELPERS ARBRE ───────────────
function m4NodeUnread(n) {
  if (!n.children) return n.unread || 0;
  return n.children.reduce((s, c) => s + m4NodeUnread(c), 0);
}
function m4CollectLeaves(n) {
  if (!n.children) return [{ node: n }];
  return n.children.flatMap((c) => m4CollectLeaves(c));
}
function m4IsLeaf(n) { return !n.children; }
function m4LeafMsgs(node) { return m4CollectLeaves(node).map((l) => l.node.msg).filter(Boolean); }

const m4Initials = (n) => (n || '?').split(' ').map((w) => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase();

// ─────────────── CODE COULEUR ───────────────
// Couleur de base de chaque famille (= rangée du haut)
const M4_FAM_BASE = { agents: '#A07CEA', clubs: '#E0A93E', joueurs: '#5C9DF5', reseau: '#EC6F77' };
// Teinte du rôle dans la liste (P1 · Spectre équilibré) · variante assombrie en mode clair (contraste sur fond blanc)
const M4_FAM_TINT = { joueur: '#5C9DF5', agent: '#A07CEA', staff: '#E0A93E', reseau: '#EC6F77' };
const M4_FAM_TINT_LIGHT = { joueur: '#2C6BC9', agent: '#6F45C0', staff: '#8C6512', reseau: '#C0454E' };
const m4IsLight = () => {
  if (document.querySelector('.hm.light, .app.light')) return true;
  const r = document.querySelector('.app, .hm');
  return !!r && !r.classList.contains('dark');
};
const m4RoleColor = (fam) => (m4IsLight() ? (M4_FAM_TINT_LIGHT[fam] || '#57575E') : (M4_FAM_TINT[fam] || '#C7C7CF'));
// Rampe de tons par sous-catégorie : une hue par famille, luminosité décroissante
// + éventail de hue ±15°, calé en OKLCH (cf. audit « Code couleur des sous-catégories »).
const M4_FAM_RAMP = {
  agents:  { C: 0.155, H: 300 },
  clubs:   { C: 0.130, H: 78  },
  joueurs: { C: 0.140, H: 262 },
  reseau:  { C: 0.160, H: 18  },
};
function m4Tone(famKey, i, n) {
  const b = M4_FAM_RAMP[famKey];
  if (!b || i < 0) return null;
  const t = n < 2 ? 0.5 : i / (n - 1);
  const L = 0.78 - t * 0.24;
  const H = b.H - 15 + t * 30;
  return `oklch(${L.toFixed(3)} ${b.C} ${H.toFixed(1)})`;
}

// ─────────────── ARBRES PAR FAMILLE ───────────────
// Agents → structures (agences). Sélectionner une structure filtre ses agents.
function m4BuildAgents() {
  const map = {};
  Object.keys(M4_AGENTS).forEach((ak) => {
    const org = M4_AGENTS[ak].org || '-';
    map[org] = map[org] || { key: 'st_' + org, label: org, agents: [], unread: 0 };
    map[org].agents.push(ak);
    map[org].unread += (M4_AGENTS[ak].unread || 0);
  });
  const children = Object.values(map).sort((a, b) => (b.unread - a.unread) || a.label.localeCompare(b.label));
  return { key: 'agents', label: 'Agents', children };
}
// Cascade géographique : Territoire → Pays → Championnat → Club → (staff | joueurs)
function m4BuildCascade(items, leafFn, rootKey, rootLabel) {
  const order = ['Europe', 'Asie', 'Afrique', 'Amériques'];
  const tr = {};
  items.forEach((it) => {
    const c = M4_CLUBS[it.club]; if (!c) return;
    tr[c.terr] = tr[c.terr] || {};
    tr[c.terr][c.country] = tr[c.terr][c.country] || {};
    tr[c.terr][c.country][c.league] = tr[c.terr][c.country][c.league] || {};
    tr[c.terr][c.country][c.league][it.club] = tr[c.terr][c.country][c.league][it.club] || { label: c.label, items: [] };
    tr[c.terr][c.country][c.league][it.club].items.push(it);
  });
  const node = (key, label, children) => ({ key, label, children });
  const children = Object.keys(tr).sort((a, b) => order.indexOf(a) - order.indexOf(b)).map((terr) =>
    node(rootKey + '_t_' + terr, terr, Object.keys(tr[terr]).sort().map((country) =>
      node(rootKey + '_c_' + country, country, Object.keys(tr[terr][country]).sort().map((league) =>
        node(rootKey + '_l_' + league, league, Object.keys(tr[terr][country][league]).map((ck) => {
          const cl = tr[terr][country][league][ck];
          return node(rootKey + '_club_' + ck, cl.label, cl.items.map(leafFn));
        })))))));
  return { key: rootKey, label: rootLabel, children };
}
// Réseau → métier → personne
function m4BuildPros() {
  const order = ['Avocats', 'Médecins', 'Journalistes', 'Sponsors', 'Parents'];
  const map = {};
  M4_PROS.forEach((p) => {
    const cat = p.cat || 'Autres';
    map[cat] = map[cat] || { key: 'cat_' + cat, label: cat, children: [] };
    map[cat].children.push({ key: p.id, label: p.name, unread: p.unread, msg: p });
  });
  const children = Object.keys(map).sort((a, b) => order.indexOf(a) - order.indexOf(b)).map((k) => map[k]);
  return { key: 'reseau', label: 'Réseau', children };
}

const m4StaffLeaf = (s) => ({ key: s.id, label: s.name, unread: s.unread, msg: s });
const m4PlayerLeaf = (p) => ({ key: p.id, label: p.name, unread: p.unread, msg: p });

const M4_AGENTS_TREE = m4BuildAgents();
const M4_CLUBS_TREE = m4BuildCascade(M4_STAFF, m4StaffLeaf, 'clubs', 'Clubs');
const M4_JOUEURS_TREE = m4BuildCascade(M4_MSGS, m4PlayerLeaf, 'joueurs', 'Joueurs');
const M4_PROS_TREE = m4BuildPros();
const M4_TREES = { agents: M4_AGENTS_TREE, clubs: M4_CLUBS_TREE, joueurs: M4_JOUEURS_TREE, reseau: M4_PROS_TREE };
const M4_FAMS = [
  { id: 'clubs',   label: 'Clubs',   flat: false },
  { id: 'agents',  label: 'Agents',  flat: true },
  { id: 'joueurs', label: 'Joueurs', flat: false },
  { id: 'reseau',  label: 'Réseau',  flat: false },
].filter((f) => M4_TREES[f.id] && M4_TREES[f.id].children.length);

// ─────────────── CONVERSATIONS ───────────────
const m4ClubLabel = (ck) => (M4_CLUBS[ck] ? M4_CLUBS[ck].label : '');
const m4ConvAgent = (ak) => ({ id: 'ag_' + ak, name: M4_AGENTS[ak].label, role: 'Agent', org: M4_AGENTS[ak].org, preview: M4_AGENTS[ak].preview, time: M4_AGENTS[ak].time, unread: M4_AGENTS[ak].unread, photo: M4_AGENTS[ak].photo, fam: 'agent' });
const m4ConvStaff = (s) => ({ id: s.id, name: s.name, role: s.role, org: m4ClubLabel(s.club), preview: s.preview, time: s.time, unread: s.unread, photo: s.photo, fam: 'staff' });
const m4ConvPlayer = (p) => ({ id: p.id, name: p.name, role: 'Joueur', org: m4ClubLabel(p.club), preview: p.preview, time: p.time, unread: p.unread, fam: 'joueur' });
const m4ConvPro = (p) => ({ id: p.id, name: p.name, role: p.role, org: p.org, preview: p.preview, time: p.time, unread: p.unread, fam: 'reseau' });
const m4AgentKeys = () => Object.keys(M4_AGENTS);

// Récence pour la boîte de réception « Tous » (mélange les familles)
const M4_MONTHS = { janv: 0, 'févr': 1, fevr: 1, mars: 2, avr: 3, mai: 4, juin: 5, juil: 6, 'août': 7, aout: 7, sept: 8, oct: 9, nov: 10, 'déc': 11, dec: 11 };
const M4_DOW = { dim: 0, lun: 1, mar: 2, mer: 3, jeu: 4, ven: 5, sam: 6 };
const M4_TODAY = new Date(2026, 5, 17);
function m4Recency(t) {
  if (!t) return 99999;
  const s = String(t).trim().toLowerCase();
  if (/^\d{1,2}:\d{2}$/.test(s)) { const [h, m] = s.split(':').map(Number); return (1 - (h * 60 + m) / 1440) * 0.9; }
  if (s.startsWith('aujourd')) return 0;
  if (s.startsWith('hier')) return 1;
  const dow = M4_DOW[s.slice(0, 3)];
  if (dow != null && !/\d/.test(s)) { return ((M4_TODAY.getDay() - dow + 7) % 7) || 7; }
  const m = s.match(/^(\d{1,2})\s+([a-zéûô.]+)/);
  if (m) {
    const day = parseInt(m[1], 10);
    const key = m[2].replace(/\./g, '');
    const mon = M4_MONTHS[key] != null ? M4_MONTHS[key] : M4_MONTHS[key.slice(0, 3)];
    if (mon == null) return 99999;
    let d = new Date(2026, mon, day);
    if (d > M4_TODAY) d = new Date(2025, mon, day);
    return Math.round((M4_TODAY - d) / 86400000) + 2;
  }
  return 99999;
}

// Boîte de réception générée, triée par récence (mélange les familles)
const M4_MIX = [...m4AgentKeys().map(m4ConvAgent), ...M4_STAFF.map(m4ConvStaff), ...M4_MSGS.map(m4ConvPlayer), ...M4_PROS.map(m4ConvPro)]
  .sort((a, b) => m4Recency(a.time) - m4Recency(b.time));

// Mes vraies conversations (annuaire v2) · toujours en tête de « Tous », vrais fils de chat.
const M4_ROLE_FAM = { 'Joueur': 'joueur', 'Agent': 'agent', 'Directeur sportif': 'staff', 'Recruteur': 'staff', 'Entraîneur': 'staff', 'Club': 'staff', 'Juriste': 'reseau' };
const M4_REAL = (window.FP_CONVOS || []).map((m) => {
  const p = (window.FP_PEERS && window.FP_PEERS[m.id]) || {};
  return { id: m.id, name: m4Clean(p.name), role: p.role, org: p.org, preview: m.preview, time: m.time, unread: m.unread, photo: p.photo, online: p.online, fam: M4_ROLE_FAM[p.role] || 'staff' };
}).filter((c) => c.name);

const M4_ALL = [...M4_REAL, ...M4_MIX];

// ── Liste VIVANTE ── fpTouchConvo (chat) et openChat (main) synchronisent M4_ALL et notifient (fp:convos).
// Sans ça, la liste resterait l'instantané du chargement : nouvelles conversations invisibles, aperçus figés.
window.m4TouchConvo = (pid, preview, time) => {
  let e = M4_ALL.find((c) => c.id === pid);
  if (!e) {
    const p = (window.FP_PEERS || {})[pid];
    if (!p) return;
    e = { id: pid, name: m4Clean(p.name), role: p.role, org: p.org, preview: '', time: '', unread: 0, photo: p.photo, online: p.online, fam: p.fam || M4_ROLE_FAM[p.role] || 'staff' };
  }
  e.preview = preview;
  e.time = time;
  const i = M4_ALL.indexOf(e);
  if (i > -1) M4_ALL.splice(i, 1);
  M4_ALL.unshift(e);
  window.dispatchEvent(new CustomEvent('fp:convos'));
};
window.m4ReadConvo = (pid) => {
  const e = M4_ALL.find((c) => c.id === pid);
  if (e && e.unread) { e.unread = 0; window.dispatchEvent(new CustomEvent('fp:convos')); }
};

// Quelles conversations afficher selon le filtre + le niveau courant
function m4List(mainSel, drill, leaf) {
  if (mainSel === 'all') return M4_ALL;
  if (mainSel === 'agents') {
    if (leaf && leaf.agents) return leaf.agents.map(m4ConvAgent);
    return m4AgentKeys().map(m4ConvAgent);
  }
  if (mainSel === 'clubs') {
    if (leaf && leaf.msg) return [m4ConvStaff(leaf.msg)];
    const node = drill.length ? drill[drill.length - 1] : M4_CLUBS_TREE;
    return m4LeafMsgs(node).map(m4ConvStaff);
  }
  if (mainSel === 'joueurs') {
    if (leaf && leaf.msg) return [m4ConvPlayer(leaf.msg)];
    const node = drill.length ? drill[drill.length - 1] : M4_JOUEURS_TREE;
    return m4LeafMsgs(node).map(m4ConvPlayer);
  }
  if (mainSel === 'reseau') {
    if (leaf && leaf.msg) return [m4ConvPro(leaf.msg)];
    const node = drill.length ? drill[drill.length - 1] : M4_PROS_TREE;
    return m4LeafMsgs(node).map(m4ConvPro);
  }
  return M4_ALL;
}

// ─────────────── FILS SYNTHÉTISÉS (annuaire Labo) ───────────────
// Les vrais fils (lucas, amadou…) vivent dans FP_THREADS. Les centaines de contacts de
// l'annuaire Labo n'en ont pas : on fabrique pour chacun un court historique crédible,
// orienté par rôle, qui se termine sur son dernier message (l'aperçu de la liste, le
// message non lu). Ainsi « Reprendre » / un résultat de recherche ouvre une conversation
// avec un historique réel · plus jamais d'écran vide.
function m4DayLabel(time) {
  const s = String(time || '').trim();
  if (!s || /^\d{1,2}:\d{2}$/.test(s)) return "AUJOURD'HUI";
  return s.toUpperCase();
}
function m4Hash(str) { let h = 0; for (let i = 0; i < String(str).length; i++) h = (h * 31 + str.charCodeAt(i)) | 0; return Math.abs(h); }
// Chaque modèle : 3 répliques (eux → moi → eux) ; l'aperçu vient se poser en dernier.
const M4_THREAD_TPL = {
  joueur: [
    [{ s: 'received', t: 'Bonjour Alexandre, merci de suivre mon dossier de près.' },
     { s: 'sent', t: 'Bonjour {first}. Je m\u2019en occupe : votre profil intéresse plusieurs clubs. On fait le point cette semaine\u00a0?' },
     { s: 'received', t: 'Avec plaisir, dites-moi quand vous êtes dispo.' }],
    [{ s: 'received', t: 'Salut Alexandre, des nouvelles côté mercato\u00a0?' },
     { s: 'sent', t: 'Ça bouge. Je sécurise les bons interlocuteurs avant de vous engager sur quoi que ce soit.' },
     { s: 'received', t: 'Parfait, je reste concentré sur le terrain.' }],
  ],
  agent: [
    [{ s: 'received', t: 'Alexandre, on échange sur un dossier commun\u00a0?' },
     { s: 'sent', t: 'Avec plaisir. Tout passe par FOOTPASS, traçable. Envoie-moi les éléments.' },
     { s: 'received', t: 'Je te prépare ça et je reviens vers toi.' }],
    [{ s: 'received', t: 'Salut, j\u2019ai un profil qui pourrait t\u2019intéresser.' },
     { s: 'sent', t: 'Je regarde. Si l\u2019autorisation est claire, on peut avancer vite.' },
     { s: 'received', t: 'Autorisation en règle de mon côté, je te partage le contact.' }],
  ],
  staff: [
    [{ s: 'received', t: 'Bonjour Alexandre, merci pour votre retour rapide.' },
     { s: 'sent', t: 'Bonjour. Je vous transmets les éléments dans la journée via le coffre sécurisé.' },
     { s: 'received', t: 'Parfait, on attend ça pour avancer en interne.' }],
    [{ s: 'received', t: 'Bonjour, le staff a revu le profil avec attention.' },
     { s: 'sent', t: 'Très bien. Je reste à disposition pour organiser un échange avec la direction.' },
     { s: 'received', t: 'On revient vers vous très vite.' }],
  ],
  reseau: [
    [{ s: 'received', t: 'Bonjour Alexandre, je reviens vers vous sur le dossier.' },
     { s: 'sent', t: 'Bonjour, merci. Tout est centralisé côté FOOTPASS, n\u2019hésitez pas.' },
     { s: 'received', t: 'Noté, je vous tiens informé.' }],
    [{ s: 'received', t: 'Bonjour, merci de votre message.' },
     { s: 'sent', t: 'Avec plaisir. Je vous laisse regarder et revenir vers moi.' },
     { s: 'received', t: 'Je m\u2019en occupe et je reviens vers vous.' }],
  ],
};
function m4BuildThread(c) {
  const tpls = M4_THREAD_TPL[c.fam] || M4_THREAD_TPL.staff;
  const tpl = tpls[m4Hash(c.id) % tpls.length];
  const first = (c.name || '').split(' ')[0];
  const clock = ['09:12', '09:31', '10:04', '10:22'];
  const out = [{ id: 1, kind: 'day', label: m4DayLabel(c.time) }];
  tpl.forEach((m, i) => {
    out.push({ id: out.length + 1, kind: 'text', side: m.s, time: clock[i] || '10:30',
      status: m.s === 'sent' ? 'read' : undefined, text: m.t.replace('{first}', first) });
  });
  if (c.preview) {
    const last = (c.time && /^\d{1,2}:\d{2}$/.test(c.time)) ? c.time : '11:42';
    out.push({ id: out.length + 1, kind: 'text', side: 'received', time: last, text: c.preview });
  }
  return out;
}

// ─────────────── ENREGISTREMENT DES PEERS (pour ouvrir les chats) ───────────────
// On mémorise quels fils sont « vrais » (contenu rédigé, varié) AVANT d'en synthétiser :
// seuls ceux-là sont indexés en plein-texte (les fils synthétisés sont du remplissage
// répété · on n'en indexe que le dernier message, unique, pour la recherche « Messages »).
const M4_GENUINE_THREADS = new Set(Object.keys(window.FP_THREADS || {}));
(function registerM4Peers() {
  if (!window.FP_PEERS) return;
  const add = (c) => {
    if (!window.FP_PEERS[c.id]) window.FP_PEERS[c.id] = { id: c.id, name: c.name, role: c.role, org: c.org, initials: m4Initials(c.name), photo: c.photo, passNo: 'FP · PASS ID' };
    if (!window.FP_PEERS[c.id].fam) window.FP_PEERS[c.id].fam = c.fam;   // teinte du rôle (même code couleur que la liste)
  };
  M4_ALL.forEach((c) => {
    add(c);
    // fil synthétisé pour tout contact sans vrai historique (les vrais fils sont préservés)
    if (window.FP_THREADS && !window.FP_THREADS[c.id]) window.FP_THREADS[c.id] = m4BuildThread(c);
  });
})();

// ─────────────── FEUILLE « + PLUS » ───────────────
const M4Dot = () => (<svg width="10" height="10" viewBox="0 0 10 10"><circle cx="5" cy="5" r="3.4" fill="currentColor"></circle></svg>);
const M4Folder = () => (<svg width="15" height="15" viewBox="0 0 16 16" fill="none"><path d="M1.5 4.2c0-.7.6-1.2 1.2-1.2h3l1.3 1.5h6.3c.7 0 1.2.5 1.2 1.2v6.4c0 .7-.5 1.2-1.2 1.2H2.7c-.6 0-1.2-.5-1.2-1.2V4.2z" stroke="currentColor" strokeWidth="1.3"></path></svg>);

function M4PlusSheet({ parent, pinned, onPin, onPick, onClose, toneFor }) {
  const [q, setQ] = useStateM4('');
  const norm = (s) => s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
  const kids = parent.children || [];
  const list = q.trim() ? kids.filter((k) => norm(k.label).includes(norm(q))) : kids;
  return (
    <div className="pg-sheetwrap">
      <div className="pg-scrim" onClick={onClose}></div>
      <div className="pg-sheet drill">
        <span className="pg-grab"></span>
        <div className="pg-sheet-title">Tout {parent.label}</div>
        <div className="hm-search" style={{ margin: '0 0 12px' }}>
          <svg width="16" height="16" viewBox="0 0 18 18" fill="none"><circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.6"></circle><path d="M12.5 12.5L16 16" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"></path></svg>
          <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Rechercher une sous-catégorie…" />
        </div>
        <div className="drill-list m4-pluslist">
          {list.map((n) => {
            const tc = toneFor && toneFor(n);
            return (
              <div key={n.key} className="m4-plusrow">
                <button className="m4-plusmain" onClick={() => { onPick(n); onClose(); }}>
                  <span className="hm-row-ic">{m4IsLeaf(n) ? <M4Dot /> : <M4Folder />}</span>
                  <span className="hm-row-txt"><b>{n.label}</b>{!m4IsLeaf(n) ? <em>{n.children.length} sous-niveaux</em> : null}</span>
                  {m4NodeUnread(n) ? <span className="m4-dot" style={tc ? { background: tc } : undefined}></span> : null}
                </button>
                <button className={`m4-pinbtn ${pinned.includes(n.key) ? 'on' : ''}`} onClick={() => onPin(n.key)} aria-label="Épingler">{pinned.includes(n.key) ? '★' : '☆'}</button>
              </div>
            );
          })}
          {list.length === 0 ? <div className="hm-empty">Aucun résultat.</div> : null}
        </div>
      </div>
    </div>
  );
}

// ─────────────── LIGNE DE CONVERSATION ───────────────
function M4ConvRow({ c, onOpenChat }) {
  return (
    <button className="hm-convo" onClick={() => onOpenChat(c.id)}>
      <span className="hm-convo-av"><FPAvatar size={46} initials={m4Initials(c.name)} src={c.photo} /></span>
      <span className="hm-convo-main">
        <span className="hm-convo-top"><b>{window.fpUpperLast(c.name)}</b><span className={`hm-convo-time ${c.unread ? 'acc' : ''}`}>{c.time}</span></span>
        <span className="hm-convo-id"><span className="ci-role" style={{ color: m4RoleColor(c.fam) }}>{c.role}</span>{c.org ? <span className="ci-sep"> · </span> : null}<span className="ci-org">{c.org}</span></span>
        <span className="hm-convo-prev"><em>{c.preview}</em>{c.unread ? <span className="hm-unread">{c.unread}</span> : null}</span>
      </span>
    </button>
  );
}

const M4_LIST_CAP = 80;

// ─────────────── RECHERCHE SPOTLIGHT ───────────────
const M4_FAM_META = {
  agent:  { label: 'Agents',  color: M4_FAM_BASE.agents },
  staff:  { label: 'Clubs',   color: M4_FAM_BASE.clubs },
  joueur: { label: 'Joueurs', color: M4_FAM_BASE.joueurs },
  reseau: { label: 'Réseau',  color: M4_FAM_BASE.reseau },
};
const M4_FAM_ORDER = ['joueur', 'agent', 'staff', 'reseau'];
const m4SearchKey = 'fp_v2_recent_search';
function m4ReadRecent() { try { return JSON.parse(localStorage.getItem(m4SearchKey) || '[]'); } catch (e) { return []; } }
function m4PushRecent(term) {
  if (!term || !term.trim()) return;
  const t = term.trim();
  let r = m4ReadRecent().filter((x) => x.toLowerCase() !== t.toLowerCase());
  r.unshift(t); r = r.slice(0, 6);
  try { localStorage.setItem(m4SearchKey, JSON.stringify(r)); } catch (e) {}
}

function M4SpotRow({ c, onOpenChat, term, idx }) {
  const meta = M4_FAM_META[c.fam] || {};
  // surligne la portion correspondante du nom
  const hl = (txt) => {
    if (!term) return txt;
    const n = txt.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
    const qn = term.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
    const i = n.indexOf(qn);
    if (i < 0) return txt;
    return <React.Fragment>{txt.slice(0, i)}<mark>{txt.slice(i, i + qn.length)}</mark>{txt.slice(i + qn.length)}</React.Fragment>;
  };
  return (
    <button className="m4-spotrow" onClick={() => onOpenChat(c.id)} style={idx != null ? { animationDelay: Math.min(idx, 10) * 22 + 'ms' } : undefined}>
      <span className="m4-spotrow-av"><FPAvatar size={42} initials={m4Initials(c.name)} src={c.photo} /></span>
      <span className="m4-spotrow-main">
        <span className="m4-spotrow-name">{hl(window.fpUpperLast(c.name))}</span>
        <span className="m4-spotrow-id"><span style={{ color: m4RoleColor(c.fam) }}>{c.role}</span>{c.org ? <React.Fragment> · {c.org}</React.Fragment> : null}</span>
      </span>
      {c.unread ? <span className="m4-spotrow-badge" style={{ background: meta.color }}>{c.unread}</span> : null}
    </button>
  );
}

// Chips de portée (innovation WhatsApp #1) · rôles Footpass + Non lues
const M4_SCOPES = [
  { key: 'all', label: 'Tous' },
  { key: 'joueur', label: 'Joueurs', color: M4_FAM_TINT.joueur },
  { key: 'agent', label: 'Agents', color: M4_FAM_TINT.agent },
  { key: 'staff', label: 'Clubs', color: M4_FAM_TINT.staff },
  { key: 'reseau', label: 'Réseau', color: M4_FAM_TINT.reseau },
  { key: 'unread', label: 'Non lues', color: '#00C49A' },
];

// Catégories · objets métier Footpass (innovation #2)
const M4_CATS = [
  { key: 'passid', label: 'PassID', color: '#00C49A', count: '2 480' },
  { key: 'documents', label: 'Documents', color: '#6FD3C4', count: '316' },
  { key: 'autorisations', label: 'Autorisations', color: '#A07CEA', count: '41' },
  { key: 'agenda', label: 'Agenda', color: '#E0A93E', count: '12' },
  { key: 'liens', label: 'Liens', color: '#5C9DF5', count: '64' },
  { key: 'audio', label: 'Audio', color: '#EC6F77', count: '38' },
  { key: 'video', label: 'Vidéo', color: '#6FD3C4', count: '27' },
  { key: 'nonlues', label: 'Non lues', color: '#00C49A' },
  { key: 'contacts', label: 'Contacts', color: '#5C9DF5' },
];
const M4_CONTENT_CATS = ['documents', 'agenda', 'liens', 'audio', 'video'];

function M4CatIcon({ k, s = 21 }) {
  const p = { fill: 'none', stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round', strokeLinejoin: 'round' };
  const G = {
    passid: <React.Fragment><rect x="3" y="5" width="18" height="14" rx="2.5"></rect><circle cx="8.5" cy="11" r="2.2"></circle><path d="M5.5 16c.5-1.6 1.6-2.3 3-2.3s2.5.7 3 2.3M14 9.5h4M14 13h3"></path></React.Fragment>,
    documents: <React.Fragment><path d="M6 2h8l4 4v16H6z"></path><path d="M14 2v4h4M9 13h6M9 17h6"></path></React.Fragment>,
    autorisations: <React.Fragment><circle cx="6" cy="12" r="2.7"></circle><circle cx="18" cy="6" r="2.7"></circle><circle cx="18" cy="18" r="2.7"></circle><path d="M8.5 10.7 15.5 7.3M8.5 13.3 15.5 16.7"></path></React.Fragment>,
    agenda: <React.Fragment><rect x="3" y="4.5" width="18" height="16" rx="2.5"></rect><path d="M3 9h18M8 2.5v4M16 2.5v4"></path></React.Fragment>,
    liens: <React.Fragment><path d="M10.5 7l1.8-1.8a3.8 3.8 0 0 1 5.4 5.4L15.5 12M13.5 17l-1.8 1.8a3.8 3.8 0 0 1-5.4-5.4L8.5 12"></path><path d="M9 15l6-6"></path></React.Fragment>,
    audio: <path d="M4 10v4M8 7v10M12 4v16M16 8v8M20 11v2"></path>,
    video: <React.Fragment><rect x="3" y="6" width="13" height="12" rx="2.5"></rect><path d="M16 10l5-3v10l-5-3z"></path></React.Fragment>,
    nonlues: <React.Fragment><path d="M20 11.5a8 8 0 0 1-11.5 7.2L4 20l1.3-4.5A8 8 0 1 1 20 11.5z"></path><circle cx="17.5" cy="6.5" r="2.6" fill="currentColor" stroke="none"></circle></React.Fragment>,
    contacts: <React.Fragment><rect x="5" y="3" width="14" height="18" rx="2.5"></rect><path d="M5 8H3M5 12H3M5 16H3"></path><circle cx="12" cy="10" r="2.3"></circle><path d="M8.5 16.5c.6-1.8 2-2.6 3.5-2.6s2.9.8 3.5 2.6"></path></React.Fragment>,
  };
  return <svg width={s} height={s} viewBox="0 0 24 24" {...p}>{G[k]}</svg>;
}

const M4Lock = ({ s = 11 }) => (<svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"></rect><path d="M8 11V8a4 4 0 0 1 8 0v3"></path></svg>);

// Vue « Autorisations » · contacts liés à une entité, personnalisée par compte (écran 3)
const M4_AUTORISATIONS = {
  entity: { name: 'Rayo Vallecano CF', cr: 'RV', count: 8, concern: 2 },
  groups: [
    { label: 'Vous concernent', dot: '#00C49A', items: [
      { title: 'Autorisation de représentation · K. Mbappé', status: 'actif', you: true, from: 'Vous · Atlas Sports', arrow: '→', to: 'Rayo Vallecano', meta: ['Signée 12/03/2026', 'Expire 30/06/2026'], sec: 'Chiffré · Pass ID ×2' },
      { title: 'Autorisation de contact · U19 Rayo', status: 'actif', you: true, from: 'D. Cobeño · Rayo', arrow: '↔', to: 'Vous', meta: ['Émis 02/05/2026', 'Permanent'], sec: 'Chiffré · traçable' },
    ] },
    { label: 'Dans votre réseau', dot: '#5A5A62', items: [
      { title: 'Procuration commission · Rayo × NTS', status: 'attente', from: 'A. Mbaye', arrow: '→', to: 'NTS Agency', meta: ['Envoyé 14/06/2026', '1 signature manquante'] },
      { title: 'Autorisation 2023–2024 · Ciss × Rayo', status: 'expire', from: 'A. Mbaye', arrow: '→', to: 'Rayo Vallecano', meta: ['Close 30/06/2024'], sec: 'Archivé' },
    ] },
  ],
};
const M4_STAT = { actif: ['Actif', 'actif'], attente: ['Attente', 'wait'], expire: ['Expiré', 'exp'] };

function M4AutorisationsView() {
  const L = M4_AUTORISATIONS;
  return (
    <div className="m4-spot-results">
      <div className="m4-ctxstrip">
        <span className="m4-ctx-cr">{L.entity.cr}</span>
        <div className="m4-ctx-info"><b>{L.entity.name}</b><span><em>{L.entity.count} autorisations</em> dans votre réseau · {L.entity.concern} vous concernent</span></div>
      </div>
      {L.groups.map((g, gi) => (
        <React.Fragment key={gi}>
          <div className="m4-agrp"><span className="m4-gdot2" style={{ background: g.dot }}></span>{g.label} · {g.items.length}</div>
          {g.items.map((it, ii) => {
            const st = M4_STAT[it.status];
            return (
              <div key={ii} className={`m4-acard ${it.status === 'actif' ? 'live' : ''}`}>
                <div className="m4-acard-h"><div className="m4-ti">{it.title}</div><span className="m4-apills">{it.you ? <span className="m4-youpill">Vous</span> : null}<span className={`m4-statpill ${st[1]}`}>{st[0]}</span></span></div>
                <div className="m4-parties"><span className="m4-pchip">{it.from}</span><span className="m4-parrow">{it.arrow}</span><span className="m4-pchip">{it.to}</span></div>
                <div className="m4-ameta">{it.meta.map((m, mi) => <span key={mi} className="m4-m">{m}</span>)}{it.sec ? <span className="m4-m sec"><M4Lock />{it.sec}</span> : null}</div>
              </div>
            );
          })}
        </React.Fragment>
      ))}
    </div>
  );
}

// Index plein-texte des messages (recherche « dans n'importe quelle conversation », comme WhatsApp)
const M4_MSG_INDEX = (() => {
  const T = window.FP_THREADS || {};
  const out = [];
  Object.keys(T).forEach((cid) => {
    const peer = M4_ALL.find((c) => c.id === cid) || (window.FP_PEERS && window.FP_PEERS[cid]) || null;
    if (!peer) return;
    (T[cid] || []).forEach((m, idx, arr) => {
      if (!(m && m.kind === 'text' && m.text)) return;
      // fil synthétisé : seul le dernier message (l'aperçu, unique) est indexé
      if (!M4_GENUINE_THREADS.has(cid) && idx !== arr.length - 1) return;
      out.push({ cid, text: m.text, time: m.time || '', side: m.side, peer });
    });
  });
  return out;
})();

function m4Snippet(text, term) {
  const nm = (s) => s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
  const n = nm(text);
  const qn = nm(term);
  const i = n.indexOf(qn);
  if (i < 0) return text.length > 64 ? text.slice(0, 64) + '…' : text;
  const start = Math.max(0, i - 22);
  const pre = (start > 0 ? '…' : '') + text.slice(start, i);
  const mid = text.slice(i, i + qn.length);
  const end = Math.min(text.length, i + qn.length + 34);
  const post = text.slice(i + qn.length, end) + (end < text.length ? '…' : '');
  return <React.Fragment>{pre}<mark>{mid}</mark>{post}</React.Fragment>;
}

// Carte « Meilleur résultat » · profil vivant + actions contextuelles (innovation #3)
function M4BestCard({ c, onOpen }) {
  const color = m4RoleColor(c.fam);
  const rel = c.unread
    ? `${c.unread} message${c.unread > 1 ? 's' : ''} non lu${c.unread > 1 ? 's' : ''}${c.time ? ' · ' + c.time : ''}`
    : `Conversation active${c.time ? ' · ' + c.time : ''}`;
  return (
    <div className="m4-best" style={{ '--hit': color }}>
      <button className="m4-best-top" onClick={() => onOpen(c.id)}>
        <span className="m4-best-av"><FPAvatar size={54} initials={m4Initials(c.name)} src={c.photo} /></span>
        <span className="m4-best-id">
          <span className="m4-best-name">{window.fpUpperLast(c.name)}<span className="m4-best-v" title="Pass ID vérifié"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#04130F" strokeWidth="3.4" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12l4 4L19 7"></path></svg></span></span>
          <span className="m4-best-role"><span style={{ color }}>{c.role}</span>{c.org ? ' · ' + c.org : ''}</span>
        </span>
        <span className="m4-best-go">›</span>
      </button>
      <div className="m4-best-rel">
        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20 11.5a8 8 0 0 1-11.5 7.2L4 20l1.3-4.5A8 8 0 1 1 20 11.5z"></path></svg>
        {rel}
      </div>
      <div className="m4-best-cta">
        <button className="m4-bbtn pri" onClick={() => onOpen(c.id)}>Reprendre</button>
        <button className="m4-bbtn sec" onClick={() => onOpen(c.id)}>Voir le profil</button>
      </div>
    </div>
  );
}

// ── Numéro Pass ID d'un contact : réel si connu, sinon dérivé de façon déterministe ──
function m4PassId(c) {
  const peer = window.FP_PEERS && window.FP_PEERS[c.id];
  if (peer && peer.passNo && /\d{3,}/.test(peer.passNo)) return peer.passNo;
  const h = m4Hash(c.id);
  const a = 1000 + (h % 9000);
  const b = 1000 + (((h >> 5) ^ (h >> 11)) >>> 0) % 9000;
  return `FP-${a}-${b}-${m4Initials(c.name)}`;
}

// Formule du membre (preuve d'appartenance au cercle pro), déterministe par id
function m4Plan(c) {
  return ((m4Hash(c.id + 'plan') % 100) < (c.fam === 'joueur' ? 45 : 70)) ? 'pro' : 'start';
}
const m4TierLabel = (p) => (p === 'pro' ? 'PRO' : 'START');

// Ligne « identité vérifiée » · affiche le Pass ID de chaque membre (catégorie PassID).
// tierStyle : A = pastille à droite · B = tag en ligne (rôle) · C = liseré avatar + badge
function M4PassRow({ c, idx, onOpen, tierStyle = 'A' }) {
  const color = m4RoleColor(c.fam);
  const plan = m4Plan(c);
  const tag = <span className={`m4-rtier ${plan}`}>{m4TierLabel(plan)}</span>;
  return (
    <button className={`m4-passrow tier-${tierStyle} ${tierStyle === 'C' ? 'tierav-' + plan : ''}`} onClick={() => onOpen(c)} style={idx != null ? { animationDelay: Math.min(idx, 12) * 20 + 'ms' } : undefined}>
      <span className="m4-passrow-av">
        <FPAvatar size={44} initials={m4Initials(c.name)} src={c.photo} ring />
        {tierStyle === 'C' ? <span className={`m4-avtier ${plan}`}>{m4TierLabel(plan)}</span> : null}
      </span>
      <span className="m4-passrow-main">
        <span className="m4-passrow-name">{window.fpUpperLast(c.name)}<span className="m4-passrow-v" title="Pass ID vérifié"><svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="#04130F" strokeWidth="3.6" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12l4 4L19 7"></path></svg></span></span>
        <span className="m4-passrow-num"><FPIcon.Shield s={11} />{m4PassId(c)}</span>
        <span className="m4-passrow-role"><span style={{ color }}>{c.role}</span>{c.org ? ' · ' + c.org : ''}{tierStyle === 'B' ? <React.Fragment> {tag}</React.Fragment> : null}</span>
      </span>
      {tierStyle === 'A' ? tag : null}
      <span className="m4-passrow-go">›</span>
    </button>
  );
}

function M4PassView({ list, onOpen, tierStyle }) {
  const rows = list.slice().sort((a, b) => a.name.localeCompare(b.name)).slice(0, 60);
  const n = list.length;
  return (
    <div className="m4-spot-results">
      <div className="m4-spot-count">{n} identité{n > 1 ? 's' : ''} vérifiée{n > 1 ? 's' : ''} · Pass ID</div>
      {rows.map((c, i) => <M4PassRow key={c.id} c={c} idx={i} onOpen={onOpen} tierStyle={tierStyle} />)}
      {n > rows.length ? <div className="m4-spot-more">+ {n - rows.length} autres membres</div> : null}
    </div>
  );
}

// Feuille « Pass ID » · la carte d'identité vérifiée d'un membre (vue au clic sur une ligne)
function M4PassSheet({ c, onClose, onOpenChat }) {
  const [copied, setCopied] = useStateM4(false);
  const [cardTier, setCardTier] = useStateM4(() => { try { return localStorage.getItem('fp_cardtier') || '3'; } catch (e) { return '3'; } });
  useEffectM4(() => {
    const h = (e) => setCardTier((e.detail && e.detail.v) || '3');
    window.addEventListener('fp:cardtier', h);
    return () => window.removeEventListener('fp:cardtier', h);
  }, []);
  const num = m4PassId(c);
  const Pass = window.ObPassCard;
  const plan = m4Plan(c);
  const planName = plan === 'pro' ? 'PassPRO' : 'PassSTART';
  // Proposition d'affichage de la formule sur la carte :
  //  1 · Matière   · carte OR pour PRO, carte standard sombre pour START (le support = le statut)
  //  2 · Pastille · carte premium pour tous + jeton PRO/START sous le logo
  //  3 · Les deux · matière différenciée + jeton (le plus marqué)
  const goldCard = cardTier === '2' ? true : (plan === 'pro');
  const showChip = cardTier === '2' || cardTier === '3';
  // 4e champ de la carte : statut du membre (preuve d'appartenance au cercle pro)
  const f4 = { label: 'Status', value: plan === 'pro' ? 'Professional' : 'Amateur' };
  const link = 'footpass.app/p/' + num.replace(/[^A-Za-z0-9]/g, '').toLowerCase();
  const copy = () => {
    try { navigator.clipboard && navigator.clipboard.writeText(link); } catch (e) {}
    setCopied(true); setTimeout(() => setCopied(false), 1600);
  };
  return (
    <div className="pg-sheetwrap m4-passsheet">
      <div className="pg-scrim" onClick={onClose}></div>
      <div className="pg-sheet m4-passsheet-panel">
        <span className="pg-grab"></span>
        <div className={`pg-cardwrap ${plan === 'start' ? 'm4-amateurcard' : ''}`}>
          {Pass ? <Pass name={c.name} role={c.role} org={c.org} agent={f4.value} agentLabel={f4.label} number={num} verified gold={goldCard} photo={c.photo} initials={m4Initials(c.name)} clubLabel={c.fam === 'agent' ? 'Agence' : 'Club'} tier={showChip && plan === 'pro' ? 'PRO' : undefined} tierKind={plan} /> : null}
        </div>
        <div className="pg-info">
          <div className="pg-inforow"><span>Statut</span><b className="ok"><FPIcon.Shield s={11} /> Vérifié</b></div>
          <div className="pg-inforow"><span>Rôle certifié</span><b>{c.role}</b></div>
          {c.org ? <div className="pg-inforow"><span>{c.fam === 'agent' ? 'Structure' : 'Club'}</span><b>{c.org}</b></div> : null}
          <div className="pg-inforow m4-passid-row"><span>PassID</span><b><span className={`m4-tierpill ${plan === 'pro' ? 'pro' : ''}`}>{plan === 'pro' ? 'PRO' : 'START'}</span></b></div>
        </div>
        <div className="pg-sheet-actions">
          <button className="pg-btn primary" onClick={() => onOpenChat(c.id)}>Ouvrir la conversation</button>
          <button className="pg-btn" onClick={copy}>{copied ? 'Lien sécurisé copié ✓' : 'Partager hors de l’app'}</button>
        </div>
        <div className="m4-passnote"><FPIcon.Shield s={10} /> Le QR de vérification n’apparaît qu’au partage hors de l’app : il prouve l’authenticité du Pass et renvoie vers FOOTPASS.</div>
      </div>
    </div>
  );
}

function M4Spotlight({ onClose, onOpenChat, lang }) {
  const [q, setQ] = useStateM4('');
  const [scope, setScope] = useStateM4('all');
  const [passCard, setPassCard] = useStateM4(null);
  const [tierStyle, setTierStyle] = useStateM4(() => { try { return localStorage.getItem('fp_tierstyle') || 'C'; } catch (e) { return 'C'; } });
  useEffectM4(() => {
    const h = (e) => setTierStyle((e.detail && e.detail.style) || 'C');
    window.addEventListener('fp:tierstyle', h);
    return () => window.removeEventListener('fp:tierstyle', h);
  }, []);
  const [cat, setCat] = useStateM4(null);
  const [closing, setClosing] = useStateM4(false);
  const [recent, setRecent] = useStateM4(m4ReadRecent);
  const inputRef = useRefM4(null);
  useEffectM4(() => { const id = setTimeout(() => inputRef.current && inputRef.current.focus(), 60); return () => clearTimeout(id); }, []);
  useEffectM4(() => {
    const onKey = (e) => { if (e.key === 'Escape') doClose(); };
    window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey);
  }, []);
  const doClose = () => { setClosing(true); setTimeout(onClose, 230); };
  const pick = (id, term) => { m4PushRecent(term || q); onOpenChat(id); };

  const norm = (s) => s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
  const term = q.trim();
  const tn = norm(term);
  const inScope = (c) => scope === 'all' ? true : scope === 'unread' ? !!c.unread : c.fam === scope;
  const isAutorisations = cat && cat.key === 'autorisations';
  const isContentCat = cat && M4_CONTENT_CATS.includes(cat.key);
  const isPassId = cat && cat.key === 'passid';
  const catOk = (c) => (cat && cat.key === 'nonlues') ? !!c.unread : true;
  const pass = (c) => inScope(c) && catOk(c);
  // Score de pertinence (liste à plat, type recherche iMessage) :
  // nom exact > début de nom > début d'un mot du nom > nom contient > rôle/structure contient.
  // Les non-lus remontent légèrement à score égal.
  const scoreOf = (c) => {
    const n = norm(c.name);
    let s = 0;
    if (n === tn) s = 1000;
    else if (n.startsWith(tn)) s = 800;
    else if (n.split(' ').some((w) => w.startsWith(tn))) s = 600;
    else if (n.includes(tn)) s = 400;
    else if (norm(c.org || '').includes(tn)) s = 250;
    else if (norm(c.role || '').includes(tn)) s = 200;
    return s > 0 ? s + Math.min(c.unread || 0, 9) : 0;
  };
  const matches = term
    ? M4_ALL.filter(pass).map((c) => ({ c, s: scoreOf(c) })).filter((x) => x.s > 0)
        .sort((a, b) => b.s - a.s || a.c.name.localeCompare(b.c.name)).map((x) => x.c)
    : [];
  const best = matches[0] || null;
  const rest = matches.slice(1, 40);

  const msgMatches = (term && !isContentCat && !isAutorisations)
    ? M4_MSG_INDEX.filter((m) => m.peer && pass(m.peer) && norm(m.text).includes(tn))
    : [];
  const shownMsgs = msgMatches.slice(0, 20);

  const idlePool = M4_ALL.filter(pass).sort((a, b) => (b.unread || 0) - (a.unread || 0));
  const idleSugg = idlePool.slice(0, 6);
  const idleLabel = scope === 'all' ? 'À traiter' : (M4_SCOPES.find((s) => s.key === scope) || {}).label;
  const unreadN = M4_ALL.filter((c) => c.unread).length;
  const catCount = (k) => k === 'nonlues' ? unreadN : k === 'contacts' ? M4_ALL.length : (M4_CATS.find((c) => c.key === k) || {}).count;

  return (
    <div className={`m4-spot ${closing ? 'out' : 'in'}`}>
      <div className="m4-spot-scrim" onClick={doClose}></div>
      <div className="m4-spot-panel">
        <div className="m4-spot-bar">
          <HMI.Search s={18} />
          {cat ? (
            <button className="m4-spot-tk" style={{ '--tk': cat.color }} onClick={() => setCat(null)} title="Retirer le filtre">
              <M4CatIcon k={cat.key} s={13} />{cat.label}
              <span className="m4-spot-tk-x"><svg width="10" height="10" viewBox="0 0 22 22" fill="none"><path d="M16 6L6 16M6 6l10 10" stroke="currentColor" strokeWidth="2.8" strokeLinecap="round"></path></svg></span>
            </button>
          ) : null}
          <input ref={inputRef} value={q} onChange={(e) => setQ(e.target.value)} placeholder={cat ? `Filtrer ${cat.label}…` : 'Profil, document, autorisation…'} />
          {q ? (
            <button className="m4-spot-x" onClick={() => setQ('')} aria-label="Effacer">
              <svg width="13" height="13" viewBox="0 0 22 22" fill="none"><path d="M16 6L6 16M6 6l10 10" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round"></path></svg>
            </button>
          ) : null}
          <button className="m4-spot-cancel" onClick={doClose}>Annuler</button>
        </div>

        <div className="m4-spot-chips">
          {M4_SCOPES.map((s) => (
            <button key={s.key} className={`m4-chip ${scope === s.key ? 'on' : ''}`} onClick={() => setScope(s.key)}>
              {s.color && scope !== s.key ? <span className="m4-chip-dot" style={{ background: s.color }}></span> : null}
              {s.label}
            </button>
          ))}
        </div>

        <div className="m4-spot-body">
          {isAutorisations ? (
            <M4AutorisationsView />
          ) : isPassId ? (
            <M4PassView list={term ? matches : M4_ALL.filter(pass)} onOpen={(c) => setPassCard(c)} tierStyle={tierStyle} />
          ) : isContentCat ? (
            <div className="m4-spot-none">
              <div className="m4-spot-none-ic" style={{ color: cat.color }}><M4CatIcon k={cat.key} s={26} /></div>
              <p>{cat.label} · bientôt indexés</p>
              <span>Cette famille de contenus sera consultable dans une prochaine version.</span>
            </div>
          ) : !term ? (
            cat ? (
              <div className="m4-spot-results">
                <div className="m4-spot-count">{idlePool.length} {cat.key === 'nonlues' ? ('conversation' + (idlePool.length > 1 ? 's' : '')) : ('profil' + (idlePool.length > 1 ? 's' : ''))}</div>
                {idlePool.slice(0, 40).map((c, i) => <M4SpotRow key={c.id} c={c} onOpenChat={(id) => pick(id)} term="" idx={i} />)}
              </div>
            ) : (
            <div className="m4-spot-idle">
              {recent.length ? (
                <div className="m4-spot-sec">
                  <div className="m4-spot-sechead"><span>Recherches récentes</span><button onClick={() => { setRecent([]); try { localStorage.removeItem(m4SearchKey); } catch (e) {} }}>Effacer</button></div>
                  <div className="m4-spot-recents">
                    {recent.map((r) => (
                      <button key={r} className="m4-spot-chip" onClick={() => setQ(r)}>
                        <svg width="13" height="13" viewBox="0 0 18 18" fill="none"><circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5"></circle><path d="M8 5v3.2l2 1.3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"></path></svg>
                        {r}
                      </button>
                    ))}
                  </div>
                </div>
              ) : null}
              <div className="m4-spot-sec">
                <div className="m4-spot-sechead"><span>Catégories</span></div>
                <div className="m4-cats">
                  {M4_CATS.map((ct) => (
                    <button key={ct.key} className="m4-cat" onClick={() => setCat(ct)}>
                      <span className="m4-cat-ic" style={{ color: ct.color }}><M4CatIcon k={ct.key} /></span>
                      <span className="m4-cat-lbl">{ct.label}</span>
                      <span className="m4-cat-count">{catCount(ct.key)}</span>
                      <span className="m4-cat-go">›</span>
                    </button>
                  ))}
                </div>
              </div>
              <div className="m4-spot-sec">
                <div className="m4-spot-sechead"><span>{idleLabel}</span></div>
                {idleSugg.map((c) => <M4SpotRow key={c.id} c={c} onOpenChat={(id) => pick(id)} term="" />)}
              </div>
            </div>
            )
          ) : (best || shownMsgs.length) ? (
            <div className="m4-spot-results">
              {best ? (
                <React.Fragment>
                  <div className="m4-spot-sechead bestlbl"><span>Meilleur résultat</span></div>
                  <M4BestCard c={best} onOpen={(id) => pick(id, term)} />
                  {rest.length ? (
                    <div className="m4-spot-sec">
                      <div className="m4-spot-count">{matches.length} profil{matches.length > 1 ? 's' : ''}</div>
                      {rest.map((c, i) => <M4SpotRow key={c.id} c={c} onOpenChat={(id) => pick(id, term)} term={term} idx={i} />)}
                    </div>
                  ) : null}
                </React.Fragment>
              ) : null}
              {shownMsgs.length ? (
                <div className="m4-spot-sec">
                  <div className="m4-spot-sechead"><span>Messages</span>{msgMatches.length > shownMsgs.length ? <em className="m4-sh-em">{msgMatches.length}</em> : null}</div>
                  {shownMsgs.map((m, i) => (
                    <button key={m.cid + '_' + i} className="m4-spotrow m4-msgrow" onClick={() => pick(m.peer.id, term)} style={{ animationDelay: Math.min(i, 10) * 22 + 'ms' }}>
                      <span className="m4-spotrow-av"><FPAvatar size={42} initials={m4Initials(m.peer.name)} src={m.peer.photo} /></span>
                      <span className="m4-spotrow-main">
                        <span className="m4-spotrow-name"><span className="m4-msg-who">{window.fpUpperLast(m.peer.name)}</span><span className="m4-msg-time">{m.time}</span></span>
                        <span className="m4-msg-snip">{m.side === 'sent' ? <span className="m4-msg-you">Vous : </span> : null}{m4Snippet(m.text, term)}</span>
                      </span>
                    </button>
                  ))}
                </div>
              ) : null}
            </div>
          ) : (
            <div className="m4-spot-none">
              <div className="m4-spot-none-ic"><HMI.Search s={26} /></div>
              <p>Aucun résultat pour « {term} »</p>
              <span>Essayez un nom, un club, un rôle ou un mot.</span>
              {window.FPInviteEmpty ? <FPInviteEmpty term={term} /> : null}
            </div>
          )}
        </div>
      </div>
      {passCard ? <M4PassSheet c={passCard} onClose={() => setPassCard(null)} onOpenChat={(id) => { setPassCard(null); pick(id, term); }} /> : null}
    </div>
  );
}

// ─────────────── ÉCRAN MESSAGES ───────────────
function HMMessagesM4({ lang, onOpenChat, onSheet }) {
  const [q, setQ] = useStateM4('');
  // Re-rendu quand une conversation bouge (envoi, réception, lecture) — liste vivante
  const [, setCvTick] = useStateM4(0);
  useEffectM4(() => {
    const f = () => setCvTick((t) => t + 1);
    window.addEventListener('fp:convos', f);
    return () => window.removeEventListener('fp:convos', f);
  }, []);
  const [showSearch, setShowSearch] = useStateM4(false);
  const searchRef = useRefM4(null);
  useEffectM4(() => { if (showSearch && searchRef.current) searchRef.current.focus(); }, [showSearch]);
  const toggleSearch = () => setShowSearch((s) => { if (s) setQ(''); return !s; });
  const [mainSel, setMainSel] = useStateM4('all');
  const [drill, setDrill] = useStateM4([]);
  const [leaf, setLeaf] = useStateM4(null);
  const [pins, setPins] = useStateM4({});
  const [plus, setPlus] = useStateM4(false);
  const [unreadOnly, setUnreadOnly] = useStateM4(false);

  const root = M4_TREES[mainSel] || null;
  const isTree = !!root;
  const fam = M4_FAMS.find((f) => f.id === mainSel);
  const flat = fam ? fam.flat : false;          // agents : sélection simple, pas de cascade
  const parent = drill.length ? drill[drill.length - 1] : root;

  const clickMain = (key) => { setMainSel(mainSel === key ? 'all' : key); setDrill([]); setLeaf(null); setPlus(false); };
  const clickKid = (n) => {
    if (flat) { setLeaf(leaf && leaf.key === n.key ? null : n); return; }
    if (m4IsLeaf(n)) setLeaf(n); else { setDrill((d) => [...d, n]); setLeaf(null); }
  };
  const goUp = () => { if (leaf) setLeaf(null); else if (drill.length) setDrill((d) => d.slice(0, -1)); };
  const pinKey = parent ? parent.key : '';
  const pinned = pins[pinKey] || [];
  const togglePin = (k) => setPins((p) => { const cur = p[pinKey] || []; return { ...p, [pinKey]: cur.includes(k) ? cur.filter((x) => x !== k) : [...cur, k] }; });

  // épinglés d'abord, puis non-lus, top 3
  const kids = parent && parent.children ? parent.children : [];
  const pinnedNodes = kids.filter((k) => pinned.includes(k.key));
  const topNodes = kids.filter((k) => !pinned.includes(k.key)).sort((a, b) => m4NodeUnread(b) - m4NodeUnread(a)).slice(0, 3);
  const visible = [...pinnedNodes, ...topNodes];

  // ton d'un sous-filtre : on reprend EXACTEMENT la couleur de la famille (= la teinte
  // du rôle dans la liste), pour que filtres et rôles parlent la même couleur.
  const chipTone = () => M4_FAM_BASE[mainSel] || null;

  // liste + recherche
  const norm = (s) => s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
  let listItems = m4List(mainSel, drill, leaf);
  if (q.trim()) listItems = listItems.filter((c) => norm(`${c.name} ${c.role || ''} ${c.org || ''}`).includes(norm(q.trim())));
  const unreadCount = listItems.filter((c) => c.unread).length;
  if (unreadOnly) listItems = listItems.filter((c) => c.unread);
  const shown = listItems.slice(0, M4_LIST_CAP);

  const t = (k) => (window.hmT ? window.hmT(lang, k) : k);

  return (
    <div className="hm-page" data-screen-label="Messages · liste">
      <div className="hm-pagehead">
        <h1 className="hm-h1">Messages</h1>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <button className={`hm-iconbtn ${showSearch ? 'on' : ''}`} onClick={toggleSearch} aria-label="Rechercher"><HMI.Search s={17} /></button>
          <button className="hm-iconbtn" onClick={() => onSheet && onSheet({ kind: 'newmsg' })} aria-label="Nouveau message"><HMI.Pen s={16} /></button>
        </div>
      </div>

      {showSearch ? (
        <M4Spotlight onClose={toggleSearch} onOpenChat={(id) => { toggleSearch(); onOpenChat(id); }} lang={lang} />
      ) : null}

      <div className="hm-filters">
        {/* Rangée principale · familles */}
        <div className="hm-chips">
          <button className={`hm-chip ${mainSel === 'all' ? 'on' : ''}`} onClick={() => clickMain('all')}>Tous</button>
          <button className={`hm-chip m4-unread ${unreadOnly ? 'on' : ''}`} onClick={() => setUnreadOnly((v) => !v)} aria-pressed={unreadOnly}>
            <span className="m4-unread-dot"></span>
            <span>Non lus</span>
            {unreadCount ? <span className="m4-badge m4-unread-badge">{unreadCount}</span> : null}
          </button>
          {M4_FAMS.map((f) => {
            const base = M4_FAM_BASE[f.id];
            const un = m4NodeUnread(M4_TREES[f.id]);
            const on = mainSel === f.id;
            return (
              <button key={f.id} className={`hm-chip ${on ? 'on' : ''}`} onClick={() => clickMain(f.id)}>
                <span className="m4-cdot" style={{ background: base }}></span>
                <span>{f.label}</span>
                {un ? <span className="m4-badge" style={{ background: base, color: '#0A0A0C' }}>{un}</span> : null}
              </button>
            );
          })}
        </div>

        {/* Sous-rangée à substitution */}
        {isTree ? (
          <div className="hm-chips hm-mf-sub m4-sub" key={parent.key}>
            {drill.length > 0 ? (
              <div style={{ display: 'contents' }}>
                <button className="hm-chip parent" onClick={goUp}><span className="m4-back">‹</span><span>{parent.label}</span></button>
                <span className="m4-sep"></span>
              </div>
            ) : null}
            {visible.map((n) => {
              const tc = chipTone(n);
              const on = leaf && leaf.key === n.key;
              const pinnedN = pinned.includes(n.key);
              const chipStyle = on && tc ? { background: tc, borderColor: tc, color: '#0A0A0C' } : undefined;
              return (
                <button key={n.key} className={`hm-chip sub m4-kid ${on ? 'on' : ''}`} onClick={() => clickKid(n)} style={chipStyle}>
                  {pinnedN ? <span className="m4-pin">★</span> : (tc ? <span className="m4-cdot" style={{ background: on ? '#0A0A0C' : tc }}></span> : null)}
                  <span>{n.label}</span>
                  {m4NodeUnread(n) ? <span className="m4-badge" style={tc ? { background: on ? 'rgba(10,10,12,0.18)' : tc, color: '#0A0A0C' } : undefined}>{m4NodeUnread(n)}</span> : null}
                  {!m4IsLeaf(n) && !flat ? <span className="m4-chev">›</span> : null}
                </button>
              );
            })}
            <button className="hm-chip add m4-kid" onClick={() => setPlus(true)}>+ Plus</button>
          </div>
        ) : null}
      </div>

      <div className="hm-scroll">
        <div className="hm-convos">
          {mainSel === 'all' && !q.trim() && !unreadOnly && window.FPReqZone ? <FPReqZone onOpenChat={onOpenChat} /> : null}
          {shown.map((c) => <M4ConvRow key={c.id} c={c} onOpenChat={onOpenChat} />)}
          {listItems.length === 0 ? (
            <div className="hm-empty">{q.trim() ? ('Aucun membre ne correspond à « ' + q + ' ».') : (unreadOnly ? 'Aucun message non lu. Vous êtes à jour.' : 'Aucune conversation dans ce filtre.')}</div>
          ) : null}
          {listItems.length === 0 && q.trim() && window.FPInviteEmpty ? <FPInviteEmpty term={q} /> : null}
        </div>
        {listItems.length > M4_LIST_CAP ? <div className="hm-listfoot">+ {listItems.length - M4_LIST_CAP} autres conversations</div> : (
          <div className="hm-listfoot"><FPIcon.Shield s={10} /> Chiffré de bout en bout · identités vérifiées PASS ID</div>
        )}
      </div>

      {plus && parent ? (
        <M4PlusSheet parent={parent} pinned={pinned} onPin={togglePin} onPick={clickKid} onClose={() => setPlus(false)} toneFor={chipTone} />
      ) : null}
    </div>
  );
}

// Total des conversations non lues (tous filtres confondus) · alimente le badge d'onglet
const m4GlobalUnread = () => M4_ALL.filter((c) => c.unread).length;

Object.assign(window, { HMMessagesM4, m4RoleColor, m4GlobalUnread });
