// ══ FloodsFlow — puiser dans « Contenu Source » ════════════════════════════════════════
//
// LA DEMANDE, MOT POUR MOT : « quand on appuie sur choisir, on peut soit sélectionner un
// dossier et ça va récupérer les vidéos dans le dossier, soit... on peut choisir de prendre
// une modèle dans le contenu source, et après choisir le dossier vidéo et après hop, comme ça
// on peut les récupérer ».
//
// CE QUE ÇA REMPLACE. « Approvisionner » n'offrait qu'un sélecteur de fichiers du système, qui
// TÉLÉVERSE un par un. Or le contenu est déjà sur ce disque : la bibliothèque porte des
// modèles de cent à cent vingt vidéos. On les référence, on ne les recopie pas.
//
// LE VOCABULAIRE EST CELUI DE L'OPÉRATEUR, pas celui du code. Il dit « modèle » (parfois
// « pilote ») et « dossier ». Le code, lui, dit « pack » et « catégorie » — ces deux mots
// n'apparaissent nulle part à l'écran.
//
// POURQUOI ON LIT LE DISQUE ET NON LA LISTE DES CATÉGORIES. La route `/api/source/packs`
// compte les fichiers des seules catégories DÉCLARÉES sur la machine. Mesuré ici : elle
// annonce 17 fichiers pour « pilote pacmi » alors que le dossier en porte 20 — les 3 de
// « Publication » sont invisibles parce que cette catégorie n'est pas déclarée sur ce poste.
// Un modèle peut donc paraître vide en contenant cent vidéos. On passe par
// `/api/media/browse`, qui fait un `os.listdir` et ne ment pas.

const FB_RACINE = "Contenu Source";

const fbBase = () => (window.Backend && window.Backend.base) || "";

async function fbParcourir(rel) {
  const r = await fetch(fbBase() + "/api/media/browse?path=" + encodeURIComponent(rel || ""),
    { cache: "no-store" });
  return r.json();
}

// Ce qu'un format sait publier — le même partage que côté serveur, pour que l'écran n'annonce
// jamais un nombre que l'import refusera ensuite.
const FB_VIDEO = [".mp4", ".mov", ".m4v", ".webm", ".avi", ".mkv"];
const FB_IMAGE = [".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif", ".bmp"];
const FB_EXTS = {
  reels: FB_VIDEO, reels_essai: FB_VIDEO,
  story: FB_VIDEO.concat(FB_IMAGE), photo: FB_IMAGE,
};
const fbPublisable = (nom, fmt) => {
  const p = String(nom || "").lastIndexOf(".");
  const ext = p < 0 ? "" : String(nom).slice(p).toLowerCase();
  return (FB_EXTS[fmt] || FB_VIDEO).indexOf(ext) >= 0;
};

const FB_VOILE = {
  position: "fixed", inset: 0, zIndex: 9200, background: "rgba(3,7,15,.75)",
  display: "flex", alignItems: "center", justifyContent: "center", padding: 20,
};
const FB_BOITE = {
  width: "min(720px, 100%)", maxHeight: "88vh", display: "flex", flexDirection: "column",
  background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 15,
  padding: "18px 20px", boxShadow: "0 24px 60px rgba(0,0,0,.55)",
};
const FB_BTN = (accent) => ({
  display: "inline-flex", alignItems: "center", gap: 6, padding: "7px 13px", borderRadius: 9,
  fontSize: 12.5, fontWeight: 600, cursor: "pointer", font: "inherit",
  background: accent ? "var(--accent)" : "var(--surface-2, rgba(255,255,255,.05))",
  color: accent ? "#06281c" : "var(--text)",
  border: "1px solid " + (accent ? "var(--accent)" : "var(--border)"),
});
const FB_LIGNE = (actif) => ({
  display: "flex", alignItems: "center", gap: 10, padding: "9px 12px", fontSize: 13,
  borderTop: "1px solid var(--border)", cursor: "pointer",
  background: actif ? "rgba(0,224,138,.09)" : "transparent",
});

// ═══════════════════════════════════════════════════════════════════════════════════════
function DialogueBibliotheque({ concept, format, surFermer, surAvis }) {
  const { useState, useEffect } = React;
  const [fmt, setFmt] = useState(format || "reels");
  const [modeles, setModeles] = useState(null);
  const [modele, setModele] = useState(null);     // {nom, rel}
  const [dossiers, setDossiers] = useState(null); // [{nom, rel, n}]
  const [dossier, setDossier] = useState(null);   // {nom, rel}
  const [fichiers, setFichiers] = useState(null);
  const [ecartes, setEcartes] = useState([]);     // décochés à la main
  const [deja, setDeja] = useState([]);           // déjà en banque, non décochables
  const [erreur, setErreur] = useState("");
  const [occupe, setOccupe] = useState(false);
  const [toutVoir, setToutVoir] = useState(false);

  const libelle = ((window.FF_FORMATS || []).find((f) => f.id === fmt) || {}).label || fmt;

  // ── niveau 1 : les modèles, avec leur VRAI volume ────────────────────────────────────
  // `browse` sur la racine rend `files: 0` pour chaque modèle (il ne compte que le premier
  // niveau) : on redescend donc dans chacun, en parallèle. Douze requêtes locales valent
  // mieux qu'une liste de « 9 dossiers » qui ne dit jamais où sont les vidéos.
  useEffect(() => {
    let vivant = true;
    (async () => {
      try {
        const d = await fbParcourir(FB_RACINE);
        if (!vivant) return;
        const bruts = (d.folders || []).map((f) => ({ nom: f.name, rel: f.path }));
        const pleins = await Promise.all(bruts.map(async (m) => {
          try {
            const s = await fbParcourir(m.rel);
            const sous = await Promise.all((s.folders || []).map(async (x) => {
              const c = await fbParcourir(x.path);
              return { nom: x.name, rel: x.path, fichiers: (c.files || []) };
            }));
            return Object.assign({}, m, { sous: sous, vrac: s.files || [] });
          } catch (e) { return Object.assign({}, m, { sous: [], vrac: [] }); }
        }));
        if (!vivant) return;
        setModeles(pleins);
      } catch (e) { if (vivant) setErreur("bibliothèque illisible — " + e.message); }
    })();
    return () => { vivant = false; };
  }, []);

  // Ce que le format retient d'un dossier : c'est ce nombre-là qu'on affiche, pas le nombre
  // de fichiers. Un dossier de 40 photos vaut 0 pour les Reels, et le dire évite d'y entrer.
  const compter = (liste) => (liste || []).filter((f) => fbPublisable(f.name, fmt)).length;
  const volume = (m) => compter(m.vrac) + (m.sous || []).reduce((a, s) => a + compter(s.fichiers), 0);

  const ouvrir = async (m) => {
    setModele(m); setDossier(null); setFichiers(null); setErreur("");
    const liste = (m.sous || []).map((s) => ({ nom: s.nom, rel: s.rel, n: compter(s.fichiers) }));
    if (compter(m.vrac)) {
      // LE VRAC N'EST PAS PERDU. Des fichiers posés directement dans le modèle, hors dossier,
      // existent vraiment — les taire ferait chercher longtemps.
      liste.unshift({ nom: "à la racine du modèle", rel: m.rel, n: compter(m.vrac), vrac: true });
    }
    setDossiers(liste);
  };

  const entrer = async (d) => {
    setDossier(d); setFichiers(null); setEcartes([]); setErreur("");
    try {
      const b = await fbParcourir(d.rel);
      const dispo = (b.files || []).filter((f) => fbPublisable(f.name, fmt));
      setFichiers(dispo);
      // CE QUI EST DÉJÀ EN BANQUE EST MARQUÉ SUR PLACE, pas annoncé après coup. Reprendre un
      // modèle est le geste le plus naturel du monde ; découvrir « 12 doublons » une fois le
      // clic passé ne dit pas LESQUELS.
      const r = await fetch(fbBase() + "/api/floodsflow/concepts/" + concept.id
        + "/banque-chemins/" + fmt, { cache: "no-store" });
      const j = await r.json().catch(() => ({}));
      const vus = new Set(j.chemins || []);
      setDeja(dispo.filter((f) => vus.has(d.rel + "/" + f.name)).map((f) => f.name));
    } catch (e) { setErreur("dossier illisible — " + e.message); setFichiers([]); }
  };

  const aPrendre = (fichiers || [])
    .filter((f) => deja.indexOf(f.name) < 0 && ecartes.indexOf(f.name) < 0)
    .map((f) => f.name);

  const importer = async () => {
    if (!dossier || !aPrendre.length || occupe) return;
    setOccupe(true);
    try {
      const r = await fetch(fbBase() + "/api/floodsflow/concepts/" + concept.id + "/bibliotheque", {
        method: "POST", headers: { "content-type": "application/json" },
        body: JSON.stringify({ dossier: fmt, source: dossier.rel, noms: aPrendre }),
      });
      const d = await r.json();
      if (!d.ok) { setErreur(d.error || "import refusé"); setOccupe(false); return; }
      surAvis({ ton: "ok", texte: d.importes + " " + libelle + " ajouté(s) depuis « "
        + modele.nom + " » — référencés, aucun fichier copié"
        + (d.doublons ? " · " + d.doublons + " déjà en banque, ignorée(s)" : "")
        + (d.tronque ? " · " + d.tronque + " au-delà du plafond, relance pour les prendre" : "") });
      surFermer(true);
    } catch (e) {
      setErreur("import impossible — " + e.message);
      setOccupe(false);
    }
  };

  const fil = [
    { t: FB_RACINE, aller: () => { setModele(null); setDossier(null); setFichiers(null); } },
    modele && { t: modele.nom, aller: () => { setDossier(null); setFichiers(null); } },
    dossier && { t: dossier.nom, aller: null },
  ].filter(Boolean);

  // ── rendu ────────────────────────────────────────────────────────────────────────────
  const pleins = (dossiers || []).filter((d) => d.n > 0);
  const vides = (dossiers || []).filter((d) => !d.n);

  return (
    <div style={FB_VOILE} onMouseDown={(e) => { if (e.target === e.currentTarget) surFermer(false); }}>
      <div style={FB_BOITE}>
        <div style={{ fontSize: 16, fontWeight: 800, marginBottom: 3 }}>
          Prendre du contenu déjà en bibliothèque
        </div>

        {/* LE FORMAT RESTE VISIBLE ET MODIFIABLE. Se tromper de ligne en ouvrant la fenêtre
            est l'erreur la plus facile ; sans ce sélecteur il faudrait tout refaire. */}
        <div style={{ display: "flex", alignItems: "center", gap: 7, flexWrap: "wrap",
          margin: "8px 0 12px" }}>
          <span style={{ fontSize: 11.5, color: "var(--faint)" }}>à ranger dans</span>
          {(window.FF_FORMATS || []).map((f) => (
            <button key={f.id} onClick={() => { setFmt(f.id); setDossier(null); setFichiers(null); }}
              style={{ ...FB_BTN(fmt === f.id), padding: "4px 10px", fontSize: 12 }}>
              {f.emoji} {f.label}
            </button>
          ))}
        </div>

        <div style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 12,
          color: "var(--faint)", marginBottom: 8, flexWrap: "wrap" }}>
          {fil.map((f, i) => (
            <React.Fragment key={i}>
              {i > 0 && <span>›</span>}
              <span onClick={f.aller || undefined}
                style={{ cursor: f.aller ? "pointer" : "default",
                  color: i === fil.length - 1 ? "var(--text)" : "var(--muted)" }}>{f.t}</span>
            </React.Fragment>
          ))}
        </div>

        {erreur ? (
          <div style={{ padding: "10px 13px", borderRadius: 9, fontSize: 12.5, marginBottom: 10,
            background: "rgba(255,92,108,.08)", border: "1px solid rgba(255,92,108,.25)",
            color: "var(--danger, #ff5c6c)" }}>⚠ {erreur}</div>
        ) : null}

        <div style={{ flex: 1, minHeight: 0, overflowY: "auto",
          border: "1px solid var(--border)", borderRadius: 11 }}>

          {/* ── les modèles ──────────────────────────────────────────────────────── */}
          {!modele && (modeles === null ? (
            <div style={{ padding: 16, fontSize: 12.5, color: "var(--faint)" }}>lecture…</div>
          ) : !modeles.length ? (
            <div style={{ padding: 16, fontSize: 12.5, color: "var(--faint)" }}>
              Aucun modèle dans « {FB_RACINE} ». Dépose-les depuis l'écran Dossiers.
            </div>
          ) : modeles.slice().sort((a, b) => volume(b) - volume(a)).map((m) => {
            const n = volume(m);
            return (
              <div key={m.rel} onClick={() => ouvrir(m)} style={{ ...FB_LIGNE(false),
                opacity: n ? 1 : .5 }}>
                <span style={{ fontSize: 15 }}>🎞</span>
                <span style={{ flex: 1, minWidth: 0, overflow: "hidden",
                  textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{m.nom}</span>
                <span style={{ fontSize: 11.5, color: n ? "var(--accent)" : "var(--faint)" }}>
                  {n ? n + " " + libelle.toLowerCase() : "rien pour ce format"}
                </span>
                <span style={{ color: "var(--faint)" }}>›</span>
              </div>
            );
          }))}

          {/* ── les dossiers du modèle ───────────────────────────────────────────── */}
          {modele && !dossier && (dossiers === null ? (
            <div style={{ padding: 16, fontSize: 12.5, color: "var(--faint)" }}>lecture…</div>
          ) : (
            <React.Fragment>
              {!pleins.length && !vides.length && (
                <div style={{ padding: 16, fontSize: 12.5, color: "var(--faint)" }}>
                  Ce modèle ne contient aucun dossier.
                </div>
              )}
              {pleins.map((d) => (
                <div key={d.rel} onClick={() => entrer(d)} style={FB_LIGNE(false)}>
                  <span style={{ fontSize: 15 }}>{d.vrac ? "📄" : "📁"}</span>
                  <span style={{ flex: 1, minWidth: 0, overflow: "hidden",
                    textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{d.nom}</span>
                  <span style={{ fontSize: 11.5, color: "var(--accent)" }}>{d.n}</span>
                  <span style={{ color: "var(--faint)" }}>›</span>
                </div>
              ))}
              {/* LES DOSSIERS VIDES SONT NOMMÉS, PAS ALIGNÉS. Sur un modèle réel, sept
                  dossiers sur neuf sont vides pour un format donné : les afficher comme les
                  autres, c'est le bruit que l'opérateur subit déjà ailleurs. */}
              {vides.length > 0 && (
                <div onClick={() => setToutVoir(!toutVoir)}
                  style={{ padding: "9px 12px", fontSize: 11.5, color: "var(--faint)",
                    borderTop: "1px solid var(--border)", cursor: "pointer" }}>
                  {toutVoir ? "▾ " : "▸ "}{vides.length} dossier(s) sans {libelle.toLowerCase()} —
                  {" "}{vides.map((d) => d.nom).join(", ")}
                </div>
              )}
              {toutVoir && vides.map((d) => (
                <div key={d.rel} onClick={() => entrer(d)}
                  style={{ ...FB_LIGNE(false), opacity: .45 }}>
                  <span style={{ fontSize: 15 }}>📁</span>
                  <span style={{ flex: 1 }}>{d.nom}</span>
                  <span style={{ fontSize: 11.5, color: "var(--faint)" }}>0</span>
                </div>
              ))}
            </React.Fragment>
          ))}

          {/* ── les fichiers ─────────────────────────────────────────────────────── */}
          {dossier && (fichiers === null ? (
            <div style={{ padding: 16, fontSize: 12.5, color: "var(--faint)" }}>lecture…</div>
          ) : !fichiers.length ? (
            <div style={{ padding: 16, fontSize: 12.5, color: "var(--busy, #f5b942)" }}>
              Aucun fichier utilisable en {libelle} dans ce dossier. Les {libelle.toLowerCase()}
              {" "}attendent {(FB_EXTS[fmt] || []).slice(0, 4).join(" ")}.
            </div>
          ) : fichiers.map((f) => {
            const pris = deja.indexOf(f.name) >= 0;
            const on = !pris && ecartes.indexOf(f.name) < 0;
            return (
              <div key={f.name}
                onClick={() => !pris && setEcartes((l) => (on ? l.concat(f.name)
                  : l.filter((x) => x !== f.name)))}
                style={{ ...FB_LIGNE(on), opacity: pris ? .45 : 1,
                  cursor: pris ? "default" : "pointer" }}>
                <span style={{ width: 14, height: 14, borderRadius: 4, flexShrink: 0,
                  fontSize: 10, lineHeight: "13px", textAlign: "center", fontWeight: 900,
                  color: "#06281c",
                  border: "1px solid " + (on ? "var(--accent)" : "var(--border)"),
                  background: on ? "var(--accent)" : "transparent" }}>{on ? "✓" : ""}</span>
                <span style={{ flex: 1, minWidth: 0, overflow: "hidden",
                  textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{f.name}</span>
                {pris && <span style={{ fontSize: 10.5, color: "var(--faint)" }}>DÉJÀ EN BANQUE</span>}
              </div>
            );
          }))}
        </div>

        {/* ── le pied ──────────────────────────────────────────────────────────────── */}
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 12,
          flexWrap: "wrap" }}>
          <div style={{ flex: 1, minWidth: 200, fontSize: 11.5, color: "var(--faint)",
            lineHeight: 1.5 }}>
            {/* CE QUE L'OPÉRATEUR DOIT SAVOIR AVANT DE CLIQUER, et qui n'est écrit nulle part
                ailleurs : rien n'est copié. Une vidange de ce concept ne touchera pas ses
                originaux — c'est la question qu'il se posera le jour où il videra. */}
            {dossier ? (
              <React.Fragment>
                <b style={{ color: aPrendre.length ? "var(--accent)" : "var(--faint)" }}>
                  {aPrendre.length}</b> à ajouter en {libelle}
                {deja.length ? " · " + deja.length + " déjà en banque" : ""}
                {aPrendre.length > (window.FF_BIBLIO_MAX || 200)
                  ? " · seules les " + (window.FF_BIBLIO_MAX || 200) + " premières partiront"
                  : ""}
                <div>Rien n'est copié : la banque pointe sur tes fichiers d'origine, une
                  vidange de ce concept ne les touchera pas.</div>
              </React.Fragment>
            ) : (
              <span>Le Panier n'est pas proposé : il est consommable, et un autre écran peut
                le vider pendant qu'un concept s'appuie dessus.</span>
            )}
          </div>
          <button onClick={() => surFermer(false)} style={FB_BTN(false)}>Annuler</button>
          <button onClick={importer} disabled={!aPrendre.length || occupe}
            style={{ ...FB_BTN(!!aPrendre.length && !occupe),
              opacity: aPrendre.length && !occupe ? 1 : .45,
              cursor: aPrendre.length && !occupe ? "pointer" : "default" }}>
            {occupe ? "…" : "Ajouter " + (aPrendre.length || "")}
          </button>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { DialogueBibliotheque, FB_RACINE, fbPublisable, FF_BIBLIO_MAX: 200 });

// ══ VOIR CE QU'ON A APPROVISIONNÉ ══════════════════════════════════════════════════════
//
// LA DEMANDE : « quand on clique sur approvisionner, il faudrait un bouton où on puisse
// appuyer sur voir, et comme ça on peut voir le contenu qui a été choisi — si c'est du
// contenu source, ou voir des visuels si c'est du contenu d'un dossier sur mon PC ».
//
// DEUX PROVENANCES, DEUX MÉCANIQUES, UN SEUL ÉCRAN. Ce qui vient du PC n'existe encore que
// dans le navigateur : ce sont des objets `File`, on en fabrique une URL locale. Ce qui est
// en banque vit sur le disque du backend : on demande sa vignette au serveur. L'opérateur,
// lui, n'a pas à connaître cette différence — il veut voir ses vidéos.
//
// LES URL LOCALES SE RÉVOQUENT. `URL.createObjectURL` retient le fichier entier tant qu'on ne
// l'a pas relâché : ouvrir la fenêtre dix fois sur deux cents vidéos, sans révocation, c'est
// plusieurs gigaoctets qui ne redescendent jamais.

const FA_CASE = {
  position: "relative", aspectRatio: "9 / 16", borderRadius: 8, overflow: "hidden",
  background: "var(--surface-2, rgba(255,255,255,.04))", border: "1px solid var(--border)",
  cursor: "pointer",
};

function FaVignette({ src, video, local, nom, sur }) {
  const [rate, setRate] = React.useState(false);
  const plein = { width: "100%", height: "100%", objectFit: "cover", display: "block" };
  return (
    <div style={FA_CASE} onClick={sur} title={nom}>
      {rate || !src ? (
        <div style={{ display: "grid", placeItems: "center", height: "100%", fontSize: 22,
          color: "var(--faint)" }}>{video ? "🎬" : "🖼"}</div>
      ) : video && local ? (
        // UNE VIDÉO DU PC N'A PAS DE VIGNETTE : elle n'est pas encore sur le serveur, donc
        // personne ne peut lui en fabriquer une. On laisse le navigateur décoder sa PREMIÈRE
        // IMAGE — `preload="metadata"` suffit, le fichier entier n'est pas lu. Sans ça,
        // l'écran rendait une grille de pictogrammes identiques : ce n'est pas « voir ».
        // ET ON POUSSE LA TÊTE DE LECTURE. `preload="metadata"` charge la durée et les
        // dimensions, mais ne décode aucune image : la case restait noire. Un saut de
        // 0,1 s force le décodage de la première image, et c'est tout ce qu'on veut voir.
        <video src={src} muted playsInline preload="metadata"
          onLoadedMetadata={(ev) => { try { ev.target.currentTime = 0.1; } catch (x) {} }}
          onError={() => setRate(true)} style={plein} />
      ) : (
        <img src={src} alt="" loading="lazy" onError={() => setRate(true)} style={plein} />
      )}
      <div style={{ position: "absolute", left: 0, right: 0, bottom: 0, padding: "3px 5px",
        fontSize: 9.5, lineHeight: 1.3, color: "#fff", background: "rgba(0,0,0,.62)",
        overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{nom}</div>
    </div>
  );
}

function VueApercuContenu({ concept, format, fichiers, surFermer }) {
  const { useState, useEffect } = React;
  const [banque, setBanque] = useState(null);
  const [erreur, setErreur] = useState("");
  const [grand, setGrand] = useState(null);      // {src, video, nom}
  const [locaux, setLocaux] = useState([]);

  const libelle = ((window.FF_FORMATS || []).find((f) => f.id === format) || {}).label || format;

  // Les fichiers du PC, pas encore envoyés. Les URL sont créées une fois, et rendues ensuite.
  useEffect(() => {
    const liste = (fichiers || []).map((f) => ({
      nom: f.name, video: /^video\//.test(f.type || "") || /\.(mp4|mov|m4v|webm)$/i.test(f.name),
      src: URL.createObjectURL(f),
    }));
    setLocaux(liste);
    return () => liste.forEach((x) => { try { URL.revokeObjectURL(x.src); } catch (e) {} });
  }, [fichiers]);

  useEffect(() => {
    let vivant = true;
    (async () => {
      try {
        const r = await fetch(fbBase() + "/api/floodsflow/concepts/" + concept.id
          + "/banque-chemins/" + format, { cache: "no-store" });
        const d = await r.json();
        if (!vivant) return;
        if (d && d.ok) setBanque(d.chemins || []); else setErreur(d.error || "banque illisible");
      } catch (e) { if (vivant) setErreur("backend injoignable — " + e.message); }
    })();
    return () => { vivant = false; };
  }, [concept.id, format]);

  const url = (rel, quoi) => fbBase() + "/api/media/" + quoi + "?path=" + encodeURIComponent(rel);
  const estVideo = (n) => /\.(mp4|mov|m4v|webm|avi|mkv)$/i.test(n || "");
  // D'OÙ VIENT CHAQUE CONTENU, DIT EN UN MOT. Un chemin sous « Contenu Source » est une
  // référence — l'original vit dans un modèle ; les autres ont été téléversés ici.
  const depuisBiblio = (rel) => String(rel || "").indexOf(FB_RACINE + "/") === 0;
  const modele = (rel) => String(rel).split("/")[1] || "";

  const grille = { display: "grid", gap: 7,
    gridTemplateColumns: "repeat(auto-fill, minmax(96px, 1fr))" };
  const titre = { fontSize: 11, fontWeight: 700, letterSpacing: ".06em",
    textTransform: "uppercase", color: "var(--muted)", margin: "14px 0 7px" };

  const refs = (banque || []).filter(depuisBiblio);
  const propres = (banque || []).filter((r) => !depuisBiblio(r));

  return (
    <div style={FB_VOILE} onMouseDown={(e) => { if (e.target === e.currentTarget) surFermer(); }}>
      <div style={{ ...FB_BOITE, width: "min(860px, 100%)" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <div style={{ fontSize: 16, fontWeight: 800, flex: 1 }}>
            {libelle} de « {concept.nom} »
          </div>
          <button onClick={surFermer} style={FB_BTN(false)}>Fermer</button>
        </div>
        <div style={{ fontSize: 11.5, color: "var(--faint)", marginTop: 4 }}>
          Clique une vignette pour la regarder en grand.
        </div>

        {erreur ? (
          <div style={{ padding: "10px 13px", borderRadius: 9, fontSize: 12.5, marginTop: 10,
            background: "rgba(255,92,108,.08)", border: "1px solid rgba(255,92,108,.25)",
            color: "var(--danger, #ff5c6c)" }}>⚠ {erreur}</div>
        ) : null}

        <div style={{ flex: 1, minHeight: 0, overflowY: "auto", marginTop: 4 }}>
          {locaux.length > 0 && (
            <React.Fragment>
              <div style={titre}>
                Choisis sur cette machine — {locaux.length}, pas encore envoyés
              </div>
              <div style={grille}>
                {locaux.map((f, i) => (
                  <FaVignette key={i} src={f.src} video={f.video} local nom={f.nom}
                    sur={() => setGrand(f)} />
                ))}
              </div>
            </React.Fragment>
          )}

          {refs.length > 0 && (
            <React.Fragment>
              <div style={titre}>
                Pris dans un modèle — {refs.length} référencé(s), aucun fichier copié
              </div>
              <div style={grille}>
                {refs.map((rel) => (
                  <FaVignette key={rel} src={url(rel, "thumb")} video={estVideo(rel)}
                    nom={rel.split("/").pop() + " · " + modele(rel)}
                    sur={() => setGrand({ src: url(rel, "file"), video: estVideo(rel),
                      nom: rel.split("/").pop() })} />
                ))}
              </div>
            </React.Fragment>
          )}

          {propres.length > 0 && (
            <React.Fragment>
              <div style={titre}>Déposés ici — {propres.length}</div>
              <div style={grille}>
                {propres.map((rel) => (
                  <FaVignette key={rel} src={url(rel, "thumb")} video={estVideo(rel)}
                    nom={rel.split("/").pop()}
                    sur={() => setGrand({ src: url(rel, "file"), video: estVideo(rel),
                      nom: rel.split("/").pop() })} />
                ))}
              </div>
            </React.Fragment>
          )}

          {banque !== null && !locaux.length && !banque.length && (
            <div style={{ padding: "26px 0", textAlign: "center", fontSize: 12.5,
              color: "var(--faint)" }}>
              Rien en {libelle} pour l'instant. « Choisir… » prend des fichiers de cette
              machine, « 🎞 Modèle… » puise dans « {FB_RACINE} ».
            </div>
          )}
          {banque === null && !locaux.length && (
            <div style={{ padding: 26, textAlign: "center", fontSize: 12.5,
              color: "var(--faint)" }}>lecture…</div>
          )}
        </div>

        {/* LA VUE EN GRAND. Une vidéo se joue vraiment : c'est la seule façon de vérifier
            qu'on a pris le bon lot avant de le spoofer sur soixante comptes. */}
        {grand && (
          <div onClick={() => setGrand(null)}
            style={{ position: "fixed", inset: 0, zIndex: 9300, background: "rgba(3,7,15,.9)",
              display: "flex", alignItems: "center", justifyContent: "center", padding: 24,
              flexDirection: "column", gap: 10 }}>
            {grand.video ? (
              <video src={grand.src} controls autoPlay loop
                style={{ maxWidth: "100%", maxHeight: "82vh", borderRadius: 10 }} />
            ) : (
              <img src={grand.src} alt=""
                style={{ maxWidth: "100%", maxHeight: "82vh", borderRadius: 10 }} />
            )}
            <div style={{ fontSize: 12, color: "var(--muted)" }}>
              {grand.nom} — clique pour fermer
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

Object.assign(window, { VueApercuContenu, FaVignette });
