/* ============ Genius — RÉSEAUX SOCIAUX, refonte guidée (20/09/2026) ============
   1 · Idée (brief, 3 points de départ, 3 suggestions + « Voir d'autres idées »)  2 · Création (réseaux, format, angle, style, génération, éditeur
   par réseau, carrousel réordonnable, versions)  3 · Diffusion (récapitulatif, copier / télécharger, passage vers la publicité).
   Aucune publication réelle : aucune page/profil n'est connecté à Genius (les comptes publicitaires ne donnent pas ce droit). Aucun faux succès.
   Les brouillons (étape, saisies, résultats) sont conservés dans le navigateur, par espace. */
const SOCIAL_NETS = [
  { k: 'facebook', label: 'Facebook', fmt: '1200 × 630 · 1,91:1', hint: 'Court, chaleureux, une question' },
  { k: 'linkedin', label: 'LinkedIn', fmt: '1200 × 627 · 1,91:1', hint: 'Pro, un point de vue, « voir plus »' },
  { k: 'instagram', label: 'Instagram', fmt: '1080 × 1350 · 4:5', hint: 'Accroche, emojis, hashtags' },
];
const SOCIAL_TONES = ['Chaleureux', 'Professionnel', 'Fun et léger', 'Pédagogique', 'Inspirant'];
const SOCIAL_TYPES = { 'actualité': 'coral', 'calendrier': 'amber', 'conseil': 'green', 'coulisses': 'blue', 'question': 'blue', 'chiffre': 'navy', 'offre': 'coral', 'explication': 'green' };
const SOCIAL_FORMATS = [['image', 'Image', 'Un visuel par réseau, au bon format.'], ['carousel', 'Carrousel', '5 à 6 diapositives carrées à faire défiler.'], ['video', 'Vidéo', 'Une vidéo verticale de 8 s (Seedance 2.5 via Higgsfield) pour tous les réseaux.'], ['text', 'Texte seul', 'Facebook et LinkedIn — Instagram exige un visuel.']];
const SOCIAL_ANGLES = [['conseil', 'Conseil'], ['offre', 'Présentation d’une offre'], ['coulisses', 'Coulisses'], ['question', 'Question'], ['quiz', 'Quiz vrai / faux'], ['explication', 'Explication pédagogique']];
const SOCIAL_STYLES = [['photo', 'Photo', 'photo'], ['photo_text', 'Photo avec texte', 'banner'], ['graphic', 'Composition graphique', 'banner'], ['inspiration', 'Inspiration importée', 'banner']];
const SOCIAL_HKEY = 'genius-social-history:' + (window.geniusTenant ? window.geniusTenant() : '');
const SOCIAL_DKEY = 'genius-social-draft:' + (window.geniusTenant ? window.geniusTenant() : '');
const soHist = () => { try { return JSON.parse(localStorage.getItem(SOCIAL_HKEY) || '[]') || []; } catch (e) { return []; } };
const soSave = (h) => { try { localStorage.setItem(SOCIAL_HKEY, JSON.stringify(h.slice(0, 20))); } catch (e) {} };
const soPost = (action, body) => fetch(GAPI() + '?action=' + action, { method: 'POST', headers: GHEAD(), body: JSON.stringify(Object.assign({ tenant: window.geniusTenant() }, body)) }).then(r => r.json());
async function soDownload(urls, name) {
  const r = await (window.__geniusNativeFetch || fetch)(GAPI() + '?action=img_download', { method: 'POST', headers: GHEAD(), body: JSON.stringify({ tenant: window.geniusTenant(), files: urls, name }) });
  if (!r.ok) { let m = 'téléchargement impossible'; try { const j = await r.json(); m = j.message || j.error || m; } catch (e) {} throw new Error(m); }
  const blob = await r.blob(); const ext = urls.length > 1 ? 'zip' : 'jpg';
  const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name + '.' + ext; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 30000);
}
window.soDownload = soDownload;
const soIcon = (k) => <img src={'brand/net-' + k + '.png'} alt="" />;
/* (audit C-A38) copie asynchrone, résolue à true seulement si le presse-papiers a accepté */
function soCopy(txt) { try { if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(txt).then(() => true, () => false); } catch (e) {} try { const ta = document.createElement('textarea'); ta.value = txt; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); const ok = document.execCommand('copy'); ta.remove(); return Promise.resolve(!!ok); } catch (e) { return Promise.resolve(false); } }

/* 22/09/2026 : un échec de requête affichait toujours « réseau », même quand le serveur avait renvoyé une vraie raison
   (ex. « Genius n’a pas répondu à temps (4 min) ») — on remonte le message réel, seul « réseau » restait indiagnosticable. */
const soNetErr = e => (e && e.message) ? String(e.message).slice(0, 160) : 'réseau';
const soSlug = s => String(s || 'post').toLowerCase().replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').slice(0, 40);
/* combinaisons compatibles : quiz et explication = carrousel ; texte seul sans Instagram ; vidéo réservée à Higgsfield connecté */
function soCompat(s, hf) {
  const out = { ...s };   /* quiz / explication sont des angles : image (une question, une réponse dans le texte) ou carrousel (diapositives) */
  if (out.format === 'text') out.nets = out.nets.filter(n => n !== 'instagram'); if (!out.nets.length) out.nets = ['facebook'];
  if (out.style === 'inspiration' && !out.ref) out.style = 'photo_text';
  return out;
}
const soServerFormat = s => s.format === 'video' ? 'video' : (s.format === 'carousel' ? (s.angle === 'quiz' ? 'quiz' : (s.angle === 'explication' ? 'explainer' : 'carousel')) : 'single');
/* ce qui sera réellement généré, sans approximation */
function soPlan(s) {
  const nets = s.nets; const texts = nets.length; let photos = 0, media = 0, label = '';
  if (s.format === 'text') { label = texts + ' texte' + (texts > 1 ? 's' : '') + ', aucun média'; }
  else if (s.format === 'video') { media = 1; label = texts + ' texte' + (texts > 1 ? 's' : '') + ' · 1 vidéo Seedance 2.5 · 8 s · 9:16'; return { texts, photos: 0, media, label, cost: s._video_est ? s._video_est : 'vidéo : estimation Higgsfield demandée avant la génération' }; }
  else if (s.format === 'carousel') { const illu = s._da && s._da.illu; photos = illu ? 6 : 1; media = 6; label = texts + ' texte' + (texts > 1 ? 's' : '') + (illu ? ' · 5 à 6 diapositives ILLUSTRÉES (une image générée par diapositive, textes dessinés dedans)' : ' · 1 photo générée · 5 à 6 diapositives composées'); }
  else if (s.campaign && s.reuse_photo) { media = nets.length; label = texts + ' texte' + (texts > 1 ? 's' : '') + ' · photo de la campagne réutilisée · ' + media + ' recadrage' + (media > 1 ? 's' : ''); }
  else { const sizes = new Set(nets.map(n => n === 'instagram' ? 'portrait' : 'landscape')); photos = sizes.size; media = nets.length; label = texts + ' texte' + (texts > 1 ? 's' : '') + ' · ' + photos + ' photo' + (photos > 1 ? 's' : '') + ' générée' + (photos > 1 ? 's' : '') + ' · ' + media + ' visuel' + (media > 1 ? 's' : '') + ' recadré' + (media > 1 ? 's' : ''); }
  const da = s._da || {};
  return { texts, photos, media, label,
    da: da.label ? (da.label + (da.refs ? ' · ' + da.refs + ' visuel' + (da.refs > 1 ? 's' : '') + ' de référence envoyé' + (da.refs > 1 ? 's' : '') + ' au modèle' : ' · aucune référence déposée') + (da.text_in_image ? ' · textes dessinés dans l’image' : '')) : '',
    cost: photos ? (photos + ' image' + (photos > 1 ? 's' : '') + ' générée' + (photos > 1 ? 's' : '') + ' (' + (da.model || 'gpt-image') + ', coût réel relevé après génération)') : 'aucun coût média' };
}

/* ---------- Étape 1 : idée ---------- */
function SoIdeaCard({ i, on, onPick }) {
  return <button type="button" className={'so-idea' + (on ? ' on' : '')} aria-pressed={on} onClick={() => onPick(i)}>
    <div className="so-idea-top"><Badge tone={SOCIAL_TYPES[i.type] || 'navy'}>{i.type || 'post'}</Badge>{i.timing === 'imminent' && <span className="so-tim so-tim-now">Imminent</span>}{i.timing === 'saisonnier' && <span className="so-tim">À préparer</span>}{on && <span className="so-idea-check"><Icon name="check" size={12} w={3} /> Sélectionnée</span>}</div>
    <div className="so-idea-t">{i.title}</div>
    <div className="so-idea-a">{i.angle}</div>
    {i.why && <div className="so-idea-why"><Icon name="target" size={12} /> {i.why}</div>}
    {i.source && <div className="so-idea-s">↳ {i.source}{i.source_date ? ' · ' + i.source_date : ''}</div>}
    {(i.needs || []).length > 0 && <div className="so-idea-needs">À compléter : {i.needs.join(', ')}</div>}
  </button>;
}
function SoStep1({ d, set, ideas, ideasBusy, ideasErr, loadIdeas, camps, campsBusy, loadCamps }) {
  const start = d.start;
  return <>
    <div className="card so-home">
      <h2>Que souhaitez-vous partager ?</h2>
      <textarea className="so-brief" rows={3} value={d.brief} onChange={e => set({ brief: e.target.value })} placeholder="En quelques mots : le sujet, ce qu’il faut dire, ce qu’il faut éviter…" />
      <div className="so-starts" role="radiogroup" aria-label="Point de départ">{[['mine', 'Mon idée', 'bulb', 'Je pars de mon brief.'], ['campaign', 'Une campagne existante', 'send', 'Décliner une campagne Genius.'], ['genius', 'Les suggestions de Genius', 'spark', 'Trois idées pertinentes pour votre activité.']].map(([k, l, ic, sub]) => <button key={k} type="button" role="radio" aria-checked={start === k} className={'so-start' + (start === k ? ' on' : '')} onClick={() => { if (k !== d.start) set({ start: k, idea: null, campaign: null, reuse_photo: false });   /* (audit C-A10) */ if (k === 'genius' && !ideas && !ideasBusy) loadIdeas(false); if (k === 'campaign' && !camps && !campsBusy) loadCamps(); }}>{ic === 'spark' ? <Spark size={18} /> : <Icon name={ic} size={18} />}<span><b>{l}</b><small>{sub}</small></span>{start === k && <Icon name="check" size={14} w={3} />}</button>)}</div>
    </div>
    {start === 'genius' && <div className="card so-sec">
      <div className="list-head"><div><div className="sec-title">Suggestions de Genius</div><div className="sec-sub">Sujets utiles à votre audience, actualités datées avec leur source. Votre sélection est conservée si vous en demandez d’autres.</div></div><Btn size="sm" kind="ghost" icon="refresh" onClick={() => loadIdeas(true)} disabled={ideasBusy}>{ideasBusy ? 'Genius cherche…' : 'Voir d’autres idées'}</Btn></div>
      {ideasErr && <div className="err-txt">Idées non chargées : {ideasErr}</div>}
      {ideasBusy && !ideas && <Skeleton rows={3} />}
      {ideas && <div className="so-ideas">{ideas.map(i => <SoIdeaCard key={i.id} i={i} on={!!(d.idea && d.idea.id === i.id)} onPick={i => set({ idea: d.idea && d.idea.id === i.id ? null : i, needs_inputs: {} })} />)}</div>}
      {ideasBusy && ideas && <div className="sec-sub" style={{ marginTop: 8 }}><span className="gspin" /> Genius cherche d’autres idées…</div>}
    </div>}
    {start === 'campaign' && <div className="card so-sec">
      <div className="list-head"><div><div className="sec-title">Une campagne existante</div><div className="sec-sub">Le titre, l’accroche, l’offre et le visuel de la campagne servent de base.</div></div></div>
      {campsBusy && <Skeleton rows={2} />}
      {camps && !camps.length && <div className="sec-sub">Aucune campagne enregistrée.</div>}
      {camps && camps.length > 0 && <div className="so-camps">{camps.map(c => { const on = !!(d.campaign && d.campaign.id === c.id); return <button type="button" key={c.id} className={'so-camp' + (on ? ' on' : '')} aria-pressed={on} onClick={() => set({ campaign: c, idea: { id: 'camp_' + c.id, title: c.title, angle: c.subject || '', type: 'offre', needs: [] } })}>{c.image ? <img src={c.image} alt="" /> : <span className="so-camp-ph"><Spark size={16} /></span>}<span><b>{c.title}</b><small>{c.status === 'draft' ? 'Brouillon' : 'Programmée / envoyée'}</small></span>{on && <Icon name="check" size={14} w={3} />}</button>; })}</div>}
    </div>}
    {d.idea && (d.idea.needs || []).length > 0 && <div className="card so-sec">
      <div className="sec-title">À compléter</div><div className="sec-sub" style={{ marginBottom: 8 }}>Genius n’invente pas ces éléments : renseignez-les, ou laissez vide et le post contiendra « [À compléter] » à cet endroit.</div>
      {d.idea.needs.map((nd, i) => <div key={i} className="st-field"><span>{nd}</span><input value={(d.needs_inputs || {})[i] || ''} onChange={e => set({ needs_inputs: { ...(d.needs_inputs || {}), [i]: e.target.value } })} placeholder="Facultatif" /></div>)}
    </div>}
    <div className="card so-sec">
      <div className="sec-title">Objectif <small className="sec-sub">(facultatif)</small></div>
      <div className="chips" style={{ marginTop: 8 }}>{[['', 'Sans objectif précis'], ['notoriete', 'Faire connaître'], ['contact', 'Générer des demandes'], ['fidelite', 'Fidéliser'], ['recrutement', 'Recruter']].map(([k, l]) => <button key={k} type="button" className={'chip' + ((d.objective || '') === k ? ' on' : '')} onClick={() => set({ objective: k })}>{l}</button>)}</div>
    </div>
  </>;
}

/* ---------- Étape 2 : création ---------- */
function SoSettings({ d, set, hf, brandTone, onRef, refBusy, refErr }) {
  const s = d; const fileRef = React.useRef(null);
  const toggleNet = k => set(soCompat({ ...s, nets: s.nets.includes(k) ? (s.nets.length > 1 ? s.nets.filter(x => x !== k) : s.nets) : s.nets.concat([k]) }, hf));
  const fmtDisabled = f => false;
  return <div className="card so-sec">
    <div className="so-set-row"><span>Réseaux</span><div className="so-nets">{SOCIAL_NETS.map(n => { const on = s.nets.includes(n.k); const off = s.format === 'text' && n.k === 'instagram'; return <button key={n.k} type="button" aria-pressed={on} className={'so-netbtn' + (on ? ' on' : '')} disabled={off} title={off ? 'Instagram exige un visuel' : n.hint} onClick={() => toggleNet(n.k)}><span className={'so-ic so-ic-' + n.k}>{soIcon(n.k)}</span><span><b>{n.label}</b><small>{n.hint}</small></span>{on && <Icon name="check" size={13} w={3} />}</button>; })}</div></div>
    <div className="so-set-row"><span>Format</span><div className="chips">{SOCIAL_FORMATS.map(([k, l, sub]) => <button key={k} type="button" className={'chip' + (s.format === k ? ' on' : '')} disabled={fmtDisabled(k)} title={sub} onClick={() => set(soCompat({ ...s, format: k }, hf))}>{l}</button>)}<span className="so-note">{(SOCIAL_FORMATS.find(f => f[0] === s.format) || [])[2]}{s.format === 'video' && !hf ? ' Higgsfield (non connecté) ajouterait l’animation d’une image existante.' : ''}</span></div></div>
    <div className="so-set-row"><span>Angle</span><div className="chips">{SOCIAL_ANGLES.map(([k, l]) => <button key={k} type="button" className={'chip' + (s.angle === k ? ' on' : '')} onClick={() => set(soCompat({ ...s, angle: s.angle === k ? '' : k }, hf))}>{l}</button>)}<span className="so-note">{s.angle === 'quiz' ? (s.format === 'carousel' ? 'Question, réponse puis invitation en diapositives.' : 'La question dans le texte et le visuel, la réponse dans le post.') : s.angle === 'explication' ? (s.format === 'carousel' ? 'Un point par diapositive.' : 'Explication en quelques lignes.') : 'Facultatif : Genius choisit sinon selon l’idée.'}</span></div></div>
    {s.format !== 'text' && s.format !== 'video' && <div className="so-set-row"><span>Style visuel</span><div className="chips">{SOCIAL_STYLES.map(([k, l]) => <button key={k} type="button" className={'chip' + (s.style === k ? ' on' : '')} onClick={() => { if (k === 'inspiration' && !s.ref) { fileRef.current && fileRef.current.click(); return; } set(soCompat({ ...s, style: k }, hf)); }}>{l}</button>)}
      <input ref={fileRef} type="file" accept="image/*" hidden onChange={e => { onRef(e.target.files && e.target.files[0]); e.target.value = ''; }} />
      {refBusy && <GenStatus label="Genius analyse la direction artistique…" size={16} />}{refErr && <span className="err-txt">{refErr}</span>}
      {s.ref && <span className="so-ref-mini">{s.ref.preview ? <img src={s.ref.preview} alt="" /> : null}<span>{s.ref.style.summary || 'style repris'}{s.ref.style.kind === 'illustration' ? ' · illustration' : ''}{s.ref.style.ref_url ? ' · envoyé au modèle et ajouté à la direction artistique de la marque' : ''}</span><button type="button" className="attr-x" title="Retirer" onClick={() => set(soCompat({ ...s, ref: null, style: 'photo_text' }, hf))}>×</button></span>}
      <span className="so-note">{s.style === 'photo' ? 'Photo seule, recadrée par réseau.' : s.style === 'graphic' ? 'Photo + panneau aux couleurs de la marque, titre, sous-titre et bouton.' : s.style === 'inspiration' ? 'Même direction artistique que le visuel importé.' : 'Photo avec le titre du post aux couleurs de la marque.'}</span></div></div>}
    {s._da && s._da.label && <div className="so-set-row"><span>Direction artistique</span><div><span className="chip on">{s._da.label}</span>{s._da.refs ? <span className="so-note" style={{ marginLeft: 8 }}>{s._da.refs} visuel{s._da.refs > 1 ? 's' : ''} de référence envoyé{s._da.refs > 1 ? 's' : ''} au modèle{s._da.text_in_image ? ' · textes dessinés dans l’image' : ''}</span> : <span className="so-note" style={{ marginLeft: 8 }}>Aucune référence : déposez 2 à 4 visuels de la marque dans <b>Réglages → Marque → Direction artistique</b> pour que les visuels ressemblent vraiment à votre charte.</span>}</div></div>}
    <details className="st-adv"><summary>Personnaliser</summary>
      <div className="so-set-row"><span>Ton</span><div className="chips"><button type="button" className={'chip' + (!s.tone ? ' on' : '')} onClick={() => set({ ...s, tone: '' })}>Ton de la marque{brandTone ? ' · ' + brandTone : ''}</button>{SOCIAL_TONES.map(t => <button key={t} type="button" className={'chip' + (s.tone === t ? ' on' : '')} onClick={() => set({ ...s, tone: t })}>{t}</button>)}</div></div>
      {s.format !== 'text' && <div className="so-set-row"><span>Logo</span><Toggle on={s.logo !== false} onChange={v => set({ ...s, logo: v })} label={s.logo !== false ? 'Logo de la marque sur le visuel' : 'Sans logo'} /></div>}
      {s.campaign && s.format === 'image' && <div className="so-set-row"><span>Photo</span><Toggle on={!!s.reuse_photo} onChange={v => set({ ...s, reuse_photo: v })} label={s.reuse_photo ? 'Réutiliser la photo de la campagne (aucune génération)' : 'Nouvelle photo générée'} /></div>}
      {s.format === 'video' && <div className="so-set-row"><span>Moteur vidéo</span><div><span className="chip on">Seedance 2.5 (Higgsfield) · 8 s · 9:16 · 720p</span>{hf && hf.blocker ? <div className="err-txt" style={{ marginTop: 6 }}>{hf.blocker}</div> : <span className="so-note" style={{ marginLeft: 8 }}>Estimation affichée avant la génération.</span>}</div></div>}
      <div className="so-set-row"><span>Fournisseur</span><div className="chips"><span className="chip on">{s.format === 'video' ? 'Higgsfield · Seedance 2.5' : 'Genius (images OpenAI)'}</span></div><span className="so-note">Le moteur reste un détail technique : le résultat et le style priment.</span></div>
    </details>
  </div>;
}
/* éditeur d'un réseau : aperçu à droite, panneau compact à gauche, versions et actions rapides */
function SoEditor({ post, res, onChange, onApplyOthers, onVersions, onRetryVideo }) {
  const [busy, setBusy] = React.useState(''); const [err, setErr] = React.useState(''); const [ins, setIns] = React.useState(''); const [copied, setCopied] = React.useState(false); const [slide, setSlide] = React.useState(0); const [imgIns, setImgIns] = React.useState(false);
  const text = post.text || ''; const tags = (post.hashtags || []); const L = post.limits || {}; const slides = post.slides && post.slides.length ? post.slides : null; const img = slides ? { url: slides[Math.min(slide, slides.length - 1)].url, w: 1080, h: 1080, photo: res.photo } : post.image;
  const isIg = post.network === 'instagram', isLi = post.network === 'linkedin'; const folded = L.fold && text.length > L.fold;
  const brandName = (window.tenantLabel && window.tenantLabel(window.__genius_status)) || 'Votre marque'; const initials = String(brandName).split(/\s+/).filter(Boolean).slice(0, 2).map(x => x[0]).join('').toUpperCase() || 'G';
  const commit = (patch) => onChange({ ...post, ...patch }, true);
  const quick = (instruction) => { setBusy('text'); setErr(''); soPost('social_revise', { network: post.network, text, hashtags: tags, instruction }).then(j => { setBusy(''); if (!j || !j.ok) { setErr((j && (j.detail || j.error)) || 'échec'); return; } commit({ text: j.text, hashtags: j.hashtags || tags }); setIns(''); }).catch(e => { setBusy(''); setErr(soNetErr(e)); }); };
  const newImage = (opt) => { setBusy('img'); setErr(''); const body = { networks: [post.network], scene: opt.scene || (img && img.scene) || res.scene || '', headline: res.headline || '', subline: res.subline || '', cta: res.cta || '', image_mode: res.imageMode || 'banner', logo: ('logo' in opt) ? opt.logo : (res.logo !== false), style: res.style || null }; if (opt.reframe && img && img.photo) body.photo = img.photo;
    soPost('social_image', body).then(j => { setBusy(''); if (!j || !j.ok || !j.images || !j.images[post.network]) { setErr((j && (j.image_error || j.error)) || 'visuel non généré'); return; } commit({ image: { ...j.images[post.network], scene: j.scene || body.scene, logo: body.logo } }); setImgIns(false); setIns(''); }).catch(e => { setBusy(''); setErr(soNetErr(e)); }); };
  const moveSlide = (i, dir) => { const t = res.slideTexts.slice(); const j = i + dir; if (j < 0 || j >= t.length) return; [t[i], t[j]] = [t[j], t[i]]; const sl = slides.slice(); [sl[i], sl[j]] = [sl[j], sl[i]]; onChange({ ...post, slides: sl }, true, { slideTexts: t, slidesDirty: true }); setSlide(j); };
  const editSlide = (i, patch) => { const t = res.slideTexts.map((x, k) => k === i ? { ...x, ...patch } : x); onChange(post, false, { slideTexts: t, slidesDirty: true }); };
  const renderSlides = () => { setBusy('slides'); setErr(''); soPost('social_slides_render', { photo: res.photo || (img && img.photo) || '', slides: res.slideTexts, style: res.style || null, logo: res.logo !== false }).then(j => { setBusy(''); if (!j || !j.ok) { setErr((j && j.error) || 'rendu impossible'); return; } const ok = j.slides.filter(x => x.ok); onChange({ ...post, slides: ok, image: ok[0] ? { url: ok[0].url, photo: res.photo, w: 1080, h: 1080, ratio: '1:1' } : post.image }, true, { slidesDirty: false, allSlides: ok }); }).catch(e => { setBusy(''); setErr(soNetErr(e)); }); };
  const copy = () => { soCopy(text.trim() + (tags.length ? '\n\n' + tags.map(t => '#' + t).join(' ') : '')).then(ok => { if (!ok) return; setCopied(true); setTimeout(() => setCopied(false), 1500); }); };
  const cur = res.slideTexts && res.slideTexts[slide];
  return <div className="so-edit">
    <div className="so-edit-l">
      <div className="so-ed-head"><span className="so-lbl2" style={{ margin: 0 }}>Texte {post.label}</span><span className="so-ed-ver">{post.versions && post.versions.length ? <button type="button" className="lnk lnk-muted" onClick={onVersions}>Annuler (v{post.versions.length})</button> : null}</span></div>
      <textarea className="so-ta" value={text} onChange={e => onChange({ ...post, text: e.target.value }, false)} onBlur={() => onChange(post, true)} rows={isLi ? 9 : 7} spellCheck />
      <div className={'so-count' + (L.max && text.length > L.max ? ' bad' : (L.reco && text.length > L.reco ? ' warn' : ''))}>{text.length.toLocaleString('fr-FR')} caractères{L.reco ? ' · conseillé ≤ ' + L.reco.toLocaleString('fr-FR') : ''}{folded ? ' · coupé après ' + L.fold + ' (« voir plus »)' : ''}{/\[À compléter/.test(text) ? ' · contient un « À compléter »' : ''}</div>
      <input className="so-tags" value={tags.join(' ')} onChange={e => onChange({ ...post, hashtags: e.target.value.split(/[\s,]+/).map(t => t.replace(/^#/, '')).filter(Boolean) }, false)} onBlur={() => onChange(post, true)} placeholder="hashtags" aria-label="Hashtags" />
      <div className="chips so-chips">{['Plus court', 'Autre accroche', 'Plus concret', isLi ? 'Sans emoji' : 'Plus chaleureux'].map(c => <button key={c} type="button" className="chip" disabled={!!busy} onClick={() => quick(c === 'Autre accroche' ? 'Propose une autre première ligne (accroche), garde le reste' : c)}>{c}</button>)}{img && !slides && <button type="button" className={'chip' + (imgIns ? ' on' : '')} disabled={!!busy} onClick={() => setImgIns(v => !v)}>Modifier le visuel</button>}</div>
      {imgIns && <div className="so-ins"><input value={ins} onChange={e => setIns(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && ins.trim()) newImage({ scene: ins.trim() }); }} placeholder="Ce qui doit changer sur le visuel (nouvelle photo générée)…" /><Btn size="sm" kind="ghost" onClick={() => newImage({ scene: ins.trim() })} disabled={!!busy || !ins.trim()}>Générer</Btn>{img && img.photo && <Btn size="sm" kind="ghost" onClick={() => newImage({ reframe: true })} disabled={!!busy} title="Même photo, nouveau cadrage (instantané)">Recadrer</Btn>}</div>}
      {!imgIns && <div className="so-ins"><input value={ins} onChange={e => setIns(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && ins.trim()) quick(ins.trim()); }} placeholder="Demander un changement du texte…" /><Btn size="sm" kind="ghost" onClick={() => quick(ins.trim())} disabled={!!busy || !ins.trim()}>Appliquer</Btn></div>}
      {slides && cur && <div className="so-slide-ed"><div className="so-lbl2">Diapositive {slide + 1} · {cur.kind}</div><input value={cur.title || ''} onChange={e => editSlide(slide, { title: e.target.value })} placeholder="Titre" /><textarea rows={2} value={cur.text || ''} onChange={e => editSlide(slide, { text: e.target.value })} placeholder="Texte" />{res.slidesDirty && <Btn size="sm" onClick={renderSlides} disabled={!!busy}>{busy === 'slides' ? 'Rendu…' : 'Mettre à jour les diapositives'}</Btn>}<span className="so-note">Même photo, nouveau rendu : aucune génération payante.</span></div>}
      {busy === 'img' && <ImageGenerationLoader compact title="Genius prépare le visuel…" sub="Le visuel actuel reste affiché jusqu’au remplacement." />}
      {busy === 'text' && <GenStatus label="Genius retravaille le texte…" size={18} />}
      {err && <div className="err-txt">Échec : {err}</div>}
      <div className="so-bar"><Btn size="sm" icon={copied ? 'check' : 'copy'} onClick={copy}>{copied ? 'Copié' : 'Copier le texte'}</Btn><Btn size="sm" kind="ghost" onClick={onApplyOthers} title="Copie ce texte dans les autres réseaux (ils gardent leur visuel)">Appliquer aux autres réseaux</Btn></div>
    </div>
    <div className="so-edit-r">
      <div className="so-prev-lbl">Aperçu {post.label} · {slides ? ('carrousel ' + slides.length + ' diapos') : (L.ratio || '')}</div>
      {slides && <div className="so-slides">{slides.map((sl, i) => <div key={sl.url + i} className={'so-slide-th' + (i === slide ? ' on' : '')}><button type="button" onClick={() => setSlide(i)} aria-label={'Diapositive ' + (i + 1)}><img src={sl.url} alt="" /><span>{i + 1}</span></button><span className="so-slide-mv"><button type="button" onClick={() => moveSlide(i, -1)} disabled={i === 0} aria-label="Avancer">‹</button><button type="button" onClick={() => moveSlide(i, 1)} disabled={i === slides.length - 1} aria-label="Reculer">›</button></span></div>)}</div>}
      <div className={'so-prev so-prev-' + post.network}>
        <div className="so-prev-head"><span className="so-prev-av">{initials}</span><span className="so-prev-who"><b>{brandName}</b><small>{isLi ? 'Entreprise · 1 j' : (isIg ? 'Publication' : 'Il y a 1 h · 🌐')}</small></span></div>
        {!res.videoTask && res.videoError && <div className="err-txt">Vidéo non lancée : {res.videoError}</div>}
        {res.videoTask && <div className="so-prev-video">{res.videoTask.status === 'completed' && (res.videoTask.media || [])[0] ? <VideoPlayer src={res.videoTask.media[0].url} poster={res.videoTask.media[0].poster} width={270} height={480} /> : ['queued', 'in_progress'].includes(res.videoTask.status) ? <VideoGenerationLoader phase={res.videoTask.status === 'queued' ? 'prep' : 'gen'} title="Vidéo Seedance 2.5 en cours…" sub={'Les textes sont prêts. ' + (res.videoTask.progress ? 'Avancement ' + res.videoTask.progress + ' %.' : 'Quelques minutes.')} /> : <div className="err-txt">Vidéo : {(ST_STATUS[res.videoTask.status] || [res.videoTask.status])[0]}{res.videoTask.error ? ' — ' + res.videoTask.error : ''}{res.videoTask.status === 'download_failed' && onRetryVideo && <div style={{ marginTop: 6 }}><Btn size="sm" icon="refresh" onClick={onRetryVideo}>Rapatrier la vidéo</Btn></div>}</div>}</div>}
        {isIg && !res.videoTask && <div className={'so-prev-img' + (img ? '' : ' ph')} style={img ? { aspectRatio: img.w + '/' + img.h } : { aspectRatio: '4/5' }}>{img && img.url ? <img src={img.url} alt={post.alt || ''} /> : <span>Sans visuel</span>}</div>}
        <div className="so-prev-txt">{folded ? <>{text.slice(0, L.fold)}<span className="so-prev-more">… voir plus</span></> : text}{tags.length ? <span className="so-prev-tags">{'\n' + tags.map(t => '#' + t).join(' ')}</span> : null}</div>
        {!isIg && !res.videoTask && img && img.url && <div className="so-prev-img" style={{ aspectRatio: img.w + '/' + img.h }}><img src={img.url} alt={post.alt || ''} /></div>}
        <div className="so-prev-acts">{(isLi ? ['J’aime', 'Commenter', 'Republier', 'Envoyer'] : ['J’aime', 'Commenter', 'Partager']).map(x => <span key={x}>{x}</span>)}</div>
      </div>
    </div>
  </div>;
}


/* ---------- Comptes connectés : Pages Facebook / Instagram pro (Meta) + LinkedIn (profil / pages entreprise) — 20/09/2026 ---------- */
const SO_PLAT = { meta: { label: 'Facebook & Instagram', hint: 'Connexion Facebook avec permissions de publication : pages Facebook et comptes Instagram professionnels reliés.' }, linkedin: { label: 'LinkedIn', hint: 'Profil et pages entreprise dont vous êtes administrateur.' } };
function SocialAccounts({ onChange, compact }) {
  const [st, setSt] = React.useState(null); const [err, setErr] = React.useState(''); const [busy, setBusy] = React.useState('');
  const load = React.useCallback((runQueue) => soPost('social_accounts', runQueue ? { run_queue: 1 } : {}).then(j => { if (j && j.ok) { setSt(j); onChange && onChange(j); } else setErr((j && j.error) || 'erreur'); }).catch(e => setErr(soNetErr(e))), []);
  const popupRef = React.useRef(null);
  React.useEffect(() => { load(true); const onMsg = e => { if (!e.data || !e.data.genius_social) return; let apiOrigin = ''; try { apiOrigin = new URL(window.GENIUS_API || '', location.href).origin; } catch (_) {} if (e.origin !== apiOrigin || (popupRef.current && e.source !== popupRef.current)) return; load(); if (e.data.genius_social === 'error') setErr(e.data.message || 'connexion refusée'); }; window.addEventListener('message', onMsg); return () => window.removeEventListener('message', onMsg); }, [load]);
  const connect = p => { setBusy(p); setErr(''); const w = window.open('about:blank', 'genius_social_oauth', 'width=560,height=760'); popupRef.current = w;
    soPost('social_oauth_start', { platform: p }).then(j => { if (!j || !j.ok) { if (w) w.close(); setErr((j && (j.message || j.error)) || 'connexion impossible'); return; } if (j.connected) { if (w) w.close(); load(); return; } if (w) w.location.href = j.url; else setErr('Autorisez les fenêtres surgissantes pour vous connecter.'); }).catch(e => { if (w) w.close(); setErr(soNetErr(e)); }).finally(() => setBusy('')); };
  const disconnect = p => { if (!window.confirm('Déconnecter ' + SO_PLAT[p].label + ' de cet espace ? Les publications programmées sur ces comptes échoueront.')) return; soPost('social_disconnect', { platform: p }).then(() => load()); };
  const refresh = p => { setBusy(p + ':r'); soPost('social_refresh', { platform: p }).then(j => { if (j && !j.ok) setErr(j.error || 'vérification impossible'); load(); }).catch(e => setErr(soNetErr(e))).finally(() => setBusy('')); };
  const select = (p, net, id) => { setSt(x => x ? { ...x, platforms: x.platforms.map(q => q.platform === p ? { ...q, selected: { ...q.selected, [net]: id } } : q) } : x); soPost('social_select', { platform: p, selected: { [net]: id } }).then(() => load()).catch(e => setErr(soNetErr(e))); };
  if (err && !st) return <div className="err-txt">Comptes non chargés : {err}</div>;
  if (!st) return <Skeleton rows={3} />;
  return <div className="ads-conn">
    <div className="sec-sub" style={{ marginBottom: 10 }}>Chaque espace de travail connecte ses propres pages. Les jetons restent sur le serveur, chiffrés. Distinct des comptes publicitaires.{st.mode === 'mock' ? <span className="so-pill-warn" style={{ marginLeft: 8 }}>Mode test · rien n’est publié</span> : st.mode === 'off' ? <span className="so-pill-warn" style={{ marginLeft: 8 }}>Désactivé (aucune application configurée)</span> : null}</div>
    {err && <div className="err-txt" style={{ marginBottom: 10 }}>{err}</div>}
    {st.platforms.map(p => { const conn = p.status === 'connected'; const nets = p.platform === 'meta' ? ['facebook', 'instagram'] : ['linkedin']; const appOk = st.apps[p.platform] && st.apps[p.platform].configured; return <div key={p.platform} className={'ads-plat' + (conn ? ' on' : '')}>
      <div className="ads-plat-h">
        <div><div className="ads-plat-t">{SO_PLAT[p.platform].label}</div><div className="sec-sub">{conn ? <>Connecté{p.user ? ' · ' + p.user : ''}{p.connected_at ? ' · le ' + new Date(p.connected_at).toLocaleDateString('fr-FR') : ''}{p.expires_at ? ' · autorisation jusqu’au ' + new Date(p.expires_at).toLocaleDateString('fr-FR') : ''}{p.mock ? ' · simulé' : ''}</> : p.status === 'expired' ? <span style={{ color: 'var(--red)' }}>Autorisation expirée ou révoquée — reconnectez-vous.</span> : SO_PLAT[p.platform].hint}</div></div>
        <div className="head-actions">{conn ? <><Btn size="sm" kind="ghost" icon="refresh" onClick={() => refresh(p.platform)} disabled={busy === p.platform + ':r'}>Vérifier</Btn><Btn size="sm" kind="ghost" onClick={() => disconnect(p.platform)}>Déconnecter</Btn></> : <Btn size="sm" icon="link" onClick={() => connect(p.platform)} disabled={(!appOk && st.mode !== 'mock') || busy === p.platform}>{p.status === 'expired' ? 'Reconnecter' : 'Connecter'}</Btn>}</div>
      </div>
      {!appOk && st.mode !== 'mock' && <div className="st-notice"><Icon name="alert" size={14} /> <span>{p.platform === 'meta' ? 'Application Meta non configurée sur le serveur (App ID / secret, permissions pages_manage_posts et instagram_content_publish).' : 'Application LinkedIn non configurée sur le serveur (client ID / secret, produit Community Management API).'}{st.can_configure ? ' Renseignez-la ci-dessous.' : ' Demandez à Groupe Verlaine.'}</span></div>}
      {conn && <div className="ads-accts">
        {!p.targets.length && <div className="sec-sub">Aucune page ou compte accessible avec cette autorisation.{p.error ? ' ' + p.error : ''}</div>}
        {nets.map(net => { const list = p.targets.filter(t => t.network === net); const cur = (p.selected || {})[net] || ''; const lbl = (SOCIAL_NETS.find(x => x.k === net) || {}).label; return <div key={net} className="so-target"><span className={'so-ic so-ic-' + net}>{soIcon(net)}</span><div style={{ flex: 1 }}><div className="so-lbl2">{lbl} — {list.length ? 'publier sur' : 'aucune cible'}</div>{list.length > 0 && <select value={cur} onChange={e => select(p.platform, net, e.target.value)}><option value="">— non utilisé —</option>{list.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}</select>}{!list.length && net === 'instagram' && <div className="sec-sub">Reliez un compte Instagram professionnel à votre page Facebook (Meta Business Suite) puis « Vérifier ».</div>}</div></div>; })}
        {p.error && <div className="sec-sub" style={{ color: 'var(--red)' }}>{p.error}</div>}
        <div className="sec-sub" style={{ marginTop: 6 }}>Autorisations : {(p.scopes || []).join(', ') || '—'}{p.checked_at ? ' · vérifié le ' + new Date(p.checked_at).toLocaleString('fr-FR', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }) : ''}</div>
      </div>}
    </div>; })}
    {!compact && st.can_configure && <SocialAppsConfig onChange={() => load()} />}
    {!compact && <SocialQueue queue={st.queue} onChange={() => load()} />}
  </div>;
}
/* 22/09/2026 (Felix : « comment je fais pour créer l'app ? ») : le mode d'emploi est DANS l'écran — étapes numérotées,
   liens vers les bonnes pages des consoles, et la valeur exacte à coller. */
function SocialAppHowTo({ uri }) {
  const steps = {
    meta: [
      ['Créer l’application', <>Ouvrez <a href="https://developers.facebook.com/apps/create/" target="_blank" rel="noreferrer">developers.facebook.com</a>, cliquez <b>Créer une application</b>, choisissez le type <b>Entreprise</b> et donnez-lui un nom (« Genius — Groupe Verlaine » par exemple).</>],
      ['Ajouter la connexion Facebook', <>Dans l’application : <b>Ajouter un produit</b> → <b>Connexion Facebook</b> → <b>Paramètres</b>. Dans « URI de redirection OAuth valides », collez <code>{uri}</code> puis <b>Enregistrer</b>.</>],
      ['Copier les identifiants', <>Menu <b>Paramètres de l’app → De base</b> : copiez l’<b>identifiant de l’application</b> et la <b>clé secrète</b>, collez-les ci-dessous, puis <b>Enregistrer Meta</b>.</>],
      ['Publier sur vos pages', <>Tant que l’application est en <b>mode développement</b>, la publication marche sur les pages dont <b>vous</b> êtes administrateur. Pour l’ouvrir à d’autres personnes : vérification d’entreprise + revue des permissions <code>pages_manage_posts</code> et <code>instagram_content_publish</code>.</>],
      ['Instagram', <>Le compte Instagram doit être un compte <b>professionnel</b> (ou créateur) <b>relié à la page Facebook</b> — sinon il n’apparaîtra pas dans la liste.</>],
    ],
    linkedin: [
      ['Créer l’application', <>Ouvrez <a href="https://www.linkedin.com/developers/apps/new" target="_blank" rel="noreferrer">linkedin.com/developers</a> et associez l’application à la <b>Page LinkedIn</b> de l’entreprise (vous devez en être administrateur).</>],
      ['Ajouter les produits', <>Onglet <b>Products</b> : ajoutez <b>Sign In with LinkedIn using OpenID Connect</b> et <b>Share on LinkedIn</b> (immédiats). Pour publier <b>au nom de la page</b>, demandez en plus <b>Community Management API</b> — LinkedIn valide en quelques jours.</>],
      ['Déclarer l’URL de retour', <>Onglet <b>Auth</b> → « Authorized redirect URLs » : collez <code>{uri}</code> puis <b>Update</b>.</>],
      ['Copier les identifiants', <>Toujours dans <b>Auth</b> : copiez <b>Client ID</b> et <b>Client Secret</b>, collez-les ci-dessous, puis <b>Enregistrer LinkedIn</b>.</>],
    ],
  };
  const [open, setOpen] = React.useState('');
  return <div className="so-howto">
    {['meta', 'linkedin'].map(k => <div key={k}>
      <button type="button" className="lnk" onClick={() => setOpen(open === k ? '' : k)}>{open === k ? '▾' : '▸'} Comment créer l’application {k === 'meta' ? 'Meta (Facebook + Instagram)' : 'LinkedIn'} ?</button>
      {open === k && <ol className="so-howto-list">{steps[k].map(([t, d], i) => <li key={i}><b>{t}.</b> {d}</li>)}</ol>}
    </div>)}
  </div>;
}
/* identifiants d'application (Groupe Verlaine uniquement) */
function SocialAppsConfig({ onChange }) {
  const [c, setC] = React.useState(null); const [form, setForm] = React.useState({ meta: {}, linkedin: {} }); const [msg, setMsg] = React.useState('');
  const [chk, setChk] = React.useState(null); const [chkBusy, setChkBusy] = React.useState(false);   /* 22/09 : contrôle des identifiants auprès des plateformes */
  const check = () => { setChkBusy(true); setChk(null); soPost('social_app_check', {}).then(j => { setChkBusy(false); if (j && j.ok) setChk(j.checks); else setChk({ meta: { ok: false, message: (j && (j.message || j.error)) || 'contrôle impossible' } }); }).catch(e => { setChkBusy(false); setChk({ meta: { ok: false, message: soNetErr(e) } }); }); };
  const load = () => soPost('social_config', {}).then(j => { if (j && j.ok) setC(j); });
  React.useEffect(() => { load(); }, []);
  if (!c) return null;
  const save = (set) => soPost('social_config', { set }).then(j => { if (j && j.ok) { setC(j); setForm({ meta: {}, linkedin: {} }); setMsg('Enregistré.'); setTimeout(() => setMsg(''), 2500); onChange && onChange(); } else setMsg((j && j.error) || 'erreur'); });
  return <details className="st-adv ads-apps"><summary>Applications sociales (identifiants serveur, Groupe Verlaine)</summary>
    <div className="sec-sub">Ces identifiants servent UNIQUEMENT à publier des posts. Ils n’ont rien à voir avec « Applications publicitaires » (lecture des dépenses Meta / Google Ads) ni avec « Studio créatif — Higgsfield » (fabrication des vidéos) : vous pouvez laisser ces deux-là de côté.</div>
    <div className="sec-sub">URL de redirection OAuth à déclarer dans les deux consoles : <code>{c.redirect_uri}</code> <button type="button" className="lnk lnk-muted" onClick={() => soCopy(c.redirect_uri)}>Copier</button></div>
    <SocialAppHowTo uri={c.redirect_uri} />
    <div className="st-set-row"><span>Mode</span><div className="chips">{[['live', 'Plateformes réelles'], ['mock', 'Mode test (simulé)'], ['off', 'Désactivé']].map(([k, l]) => <button key={k} type="button" className={'chip' + (c.mode === k ? ' on' : '')} onClick={() => save({ mode: k })}>{l}</button>)}</div></div>
    <div className="ads-apps-grid">
      <div><div className="sec-title" style={{ fontSize: 14 }}>Meta (Graph {c.meta.version})</div><div className="sec-sub">{c.meta.from === 'ads' && c.meta.configured ? 'Application des Connexions publicitaires réutilisée. ' : ''}Permissions : {c.meta.scopes.join(', ')}. Au-delà des rôles de l’app (admin, développeur, testeur) : App Review + vérification d’entreprise.</div>
        <StField label="App ID (laisser vide pour réutiliser l’app publicitaire)" value={form.meta.app_id != null ? form.meta.app_id : (c.meta.from === 'social' ? c.meta.app_id : '')} onChange={v => setForm(f => ({ ...f, meta: { ...f.meta, app_id: v } }))} />
        <StField type="password" autoComplete="new-password" label={'App secret' + (c.meta.has_secret && c.meta.from === 'social' ? ' (déjà enregistré)' : '')} value={form.meta.app_secret || ''} onChange={v => setForm(f => ({ ...f, meta: { ...f.meta, app_secret: v } }))} placeholder={c.meta.has_secret && c.meta.from === 'social' ? '••••••••' : ''} />
        <Btn size="sm" kind="ghost" onClick={() => save({ meta: { app_id: form.meta.app_id != null ? form.meta.app_id : 'GNSKEEP', app_secret: form.meta.app_secret || 'GNSKEEP' } })}>Enregistrer Meta</Btn></div>
      <div><div className="sec-title" style={{ fontSize: 14 }}>LinkedIn (version {c.linkedin.version})</div><div className="sec-sub">Application LinkedIn Developers avec les produits « Sign In with LinkedIn using OpenID Connect », « Share on LinkedIn » et « Community Management API » (pages entreprise). Scopes : {c.linkedin.scopes.join(' ')}.</div>
        <StField label="Client ID" value={form.linkedin.client_id != null ? form.linkedin.client_id : c.linkedin.client_id} onChange={v => setForm(f => ({ ...f, linkedin: { ...f.linkedin, client_id: v } }))} />
        <StField type="password" autoComplete="new-password" label={'Client secret' + (c.linkedin.has_secret ? ' (déjà enregistré)' : '')} value={form.linkedin.client_secret || ''} onChange={v => setForm(f => ({ ...f, linkedin: { ...f.linkedin, client_secret: v } }))} placeholder={c.linkedin.has_secret ? '••••••••' : ''} />
        <Btn size="sm" kind="ghost" onClick={() => save({ linkedin: { client_id: form.linkedin.client_id != null ? form.linkedin.client_id : 'GNSKEEP', client_secret: form.linkedin.client_secret || 'GNSKEEP' } })}>Enregistrer LinkedIn</Btn></div>
    </div>
    <div className="so-set-row"><span>Contrôle</span><div><Btn size="sm" kind="ghost" onClick={check} disabled={chkBusy}>{chkBusy ? 'Vérification…' : 'Vérifier les identifiants'}</Btn>
      <span className="so-note" style={{ marginLeft: 8 }}>Interroge Meta et LinkedIn sans rien publier.</span>
      {chk && <div style={{ marginTop: 8 }}>{['meta', 'linkedin'].map(k => chk[k] ? <div key={k} className={chk[k].ok ? 'ok-txt' : 'err-txt'}>{k === 'meta' ? 'Meta' : 'LinkedIn'} — {chk[k].message}</div> : null)}</div>}</div></div>
    {msg && <div className="ok-txt">{msg}</div>}
  </details>;
}
/* file des publications (programmées, publiées, échouées) */
const SO_QSTATUS = { scheduled: 'Programmée', running: 'En cours', published: 'Publiée', failed: 'Échec', canceled: 'Annulée' };
function SocialQueue({ queue, onChange }) {
  const [busy, setBusy] = React.useState('');
  const cancel = id => { if (!window.confirm('Annuler cette publication programmée ?')) return; setBusy(id); soPost('social_queue_cancel', { id }).then(() => onChange && onChange()).finally(() => setBusy('')); };
  if (!queue || !queue.length) return null;
  return <div className="so-queue"><div className="so-lbl2" style={{ marginBottom: 6 }}>Publications</div>
    {queue.map(q => <div key={q.id} className={'so-qrow so-q-' + q.status}><span className={'so-ic so-ic-' + q.network}>{soIcon(q.network)}</span><div style={{ flex: 1, minWidth: 0 }}><div className="so-qt">{q.title || (q.text || '').slice(0, 70)}</div><div className="sec-sub">{SO_QSTATUS[q.status] || q.status} · {new Date(q.when).toLocaleString('fr-FR', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })} · {q.target_name}{q.mock ? ' · simulé' : ''}{q.status === 'failed' && q.result ? ' — ' + q.result.error : ''}{q.status === 'published' && q.result && q.result.url ? <> — <a href={q.result.url} target="_blank" rel="noopener">voir</a></> : null}</div></div>{q.status === 'scheduled' && <Btn size="sm" kind="ghost" onClick={() => cancel(q.id)} disabled={busy === q.id}>Annuler</Btn>}</div>)}
  </div>;
}
/* Publier maintenant / Programmer : confirmation explicite, un réseau à la fois */
function SoPublishModal({ res, post, mode, onClose, onDone }) {
  const [when, setWhen] = React.useState(() => { const d = new Date(Date.now() + 3600000); d.setMinutes(0, 0, 0); return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0, 16); });
  const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState('');
  const vt = res.videoTask; const video = vt && vt.status === 'completed' && (vt.media || [])[0] ? vt.media[0].url : '';
  const images = video ? [] : (post.slides && post.slides.length ? post.slides.map(x => x.url) : (post.image && post.image.url ? [post.image.url] : []));
  const go = () => { setBusy(true); setErr(''); soPost('social_publish', { network: post.network, text: post.text || '', hashtags: post.hashtags || [], images, video, when: mode === 'schedule' ? new Date(when).toISOString() : '', title: res.topic || '' }).then(j => { setBusy(false); if (!j || !j.ok) { setErr((j && (j.message || j.error)) || 'échec'); return; } onDone(j); }).catch(e => { setBusy(false); setErr(soNetErr(e)); }); };
  return <div className="st-modal-bg" onClick={onClose}><div className="st-modal" onClick={e => e.stopPropagation()}>
    <div className="sec-title">{mode === 'schedule' ? 'Programmer' : 'Publier maintenant'} · {post.label}</div>
    <div className="st-modal-rows">{[['Cible', post._target || '—'], ['Texte', (post.text || '').length + ' caractères · ' + (post.hashtags || []).length + ' hashtags'], ['Médias', video ? '1 vidéo' : images.length ? images.length + ' visuel' + (images.length > 1 ? 's' : '') : 'aucun']].map(([k, v]) => <div key={k}><span>{k}</span><b>{v}</b></div>)}</div>
    {mode === 'schedule' && <div className="st-field" style={{ marginTop: 10 }}><span>Date et heure (heure de Paris)</span><input type="datetime-local" value={when} onChange={e => setWhen(e.target.value)} /></div>}
    <div className="sec-sub" style={{ marginTop: 10 }}>{mode === 'schedule' ? 'Genius publiera à l’heure dite (la file est vérifiée toutes les 5 minutes).' : 'La publication est immédiate et visible publiquement.'}</div>
    {err && <div className="err-txt" style={{ marginTop: 8 }}>{err}</div>}
    <div className="st-modal-a"><Btn kind="ghost" onClick={onClose} disabled={busy}>Annuler</Btn><Btn onClick={go} icon="send" disabled={busy}>{busy ? 'Envoi…' : (mode === 'schedule' ? 'Programmer' : 'Publier')}</Btn></div>
  </div></div>;
}
/* ---------- Étape 3 : diffusion ---------- */
function SoStep3({ res, onAds, adsBusy }) {
  const [copied, setCopied] = React.useState(''); const [dl, setDl] = React.useState('');
  const [acc, setAcc] = React.useState(null); const [showAcc, setShowAcc] = React.useState(false); const [pub, setPub] = React.useState(null); const [flash, setFlash] = React.useState('');
  const ready = (acc && acc.ready) || []; const targetName = net => { if (!acc) return ''; const p = acc.platforms.find(x => x.platform === (net === 'linkedin' ? 'linkedin' : 'meta')); const id = p && p.selected ? p.selected[net] : ''; const t = p && p.targets.find(x => x.id === id); return t ? t.name : ''; };
  const done = j => { setPub(null); setFlash(j.scheduled ? 'Publication programmée' + (j.note ? ' — ' + j.note : '') : (j.mock ? 'Simulé (mode test) : rien n’a été publié' : 'Publié' + (j.url ? ' — ' : ''))); if (j.url) window.open(j.url, '_blank', 'noopener'); setShowAcc(true); setTimeout(() => setFlash(''), 6000); };
  const copy = p => { soCopy((p.text || '').trim() + ((p.hashtags || []).length ? '\n\n' + p.hashtags.map(t => '#' + t).join(' ') : '')).then(ok => { if (!ok) return; setCopied(p.network); setTimeout(() => setCopied(''), 1500); }); };
  const download = p => { const vt = res.videoTask; if (vt && vt.status === 'completed' && (vt.media || [])[0]) { window.open(vt.media[0].url, '_blank', 'noopener'); return; } const urls = p.slides && p.slides.length ? p.slides.map(s => s.url) : (p.image && p.image.url ? [p.image.url] : []); if (!urls.length) return; setDl(p.network); soDownload(urls, p.network + '-' + soSlug(res.topic)).catch(() => {}).then(() => setDl('')); };
  return <>
    {acc && !ready.length && <div className="st-notice"><Icon name="alert" size={15} /> <span>Aucune page ou profil n’est connecté pour cet espace : connectez Facebook / Instagram / LinkedIn ci-dessous pour publier ou programmer directement. Sinon, copiez le texte et téléchargez les médias.</span></div>}
    {flash && <div className="ok-txt" style={{ marginBottom: 10 }}>{flash}</div>}
    <div className="card so-sec"><div className="list-head" style={{ alignItems: 'center' }}><div><div className="sec-title">Comptes connectés</div><div className="sec-sub">{acc ? (ready.length ? 'Prêt à publier sur : ' + ready.map(n => (SOCIAL_NETS.find(x => x.k === n) || {}).label + (targetName(n) ? ' (' + targetName(n) + ')' : '')).join(', ') : 'Aucune page choisie.') : 'Chargement…'}</div></div><Btn size="sm" kind="ghost" icon={showAcc ? 'x' : 'link'} onClick={() => setShowAcc(v => !v)}>{showAcc ? 'Masquer' : 'Gérer les comptes'}</Btn></div>
      <div hidden={!showAcc}><SocialAccounts onChange={setAcc} /></div>{!showAcc && !acc && <SocialAccounts onChange={setAcc} compact />}</div>
    {pub && <SoPublishModal res={res} post={{ ...pub.post, _target: targetName(pub.post.network) }} mode={pub.mode} onClose={() => setPub(null)} onDone={done} />}
    <div className="so-recap">{res.posts.map(p => <div key={p.network} className="card so-recap-card">
      <div className="so-recap-h"><span className={'so-ic so-ic-' + p.network}>{soIcon(p.network)}</span><b>{p.label}</b><span className="sec-sub">{(p.text || '').length} car. · {(p.hashtags || []).length} hashtags · {p.slides && p.slides.length ? p.slides.length + ' diapositives' : (p.image && p.image.url ? '1 visuel ' + (p.image.ratio || '') : 'sans visuel')}</span></div>
      {res.videoTask && res.videoTask.status === 'completed' && (res.videoTask.media || [])[0] && <img className="so-recap-img" src={res.videoTask.media[0].poster || ''} alt="" />}
      {res.videoTask && res.videoTask.status !== 'completed' && <div className="sec-sub">Vidéo : {(ST_STATUS[res.videoTask.status] || [res.videoTask.status])[0]}</div>}
      {!res.videoTask && p.image && p.image.url && <img className="so-recap-img" src={p.image.url} alt="" />}
      <div className="so-recap-txt">{p.text}</div>
      <div className="so-bar"><Btn size="sm" icon={copied === p.network ? 'check' : 'copy'} onClick={() => copy(p)}>{copied === p.network ? 'Copié' : 'Copier le texte'}</Btn>{((p.image && p.image.url) || (res.videoTask && res.videoTask.status === 'completed')) && <Btn size="sm" kind="ghost" icon="download" onClick={() => download(p)} disabled={dl === p.network}>{dl === p.network ? 'Préparation…' : 'Télécharger ' + (res.videoTask ? 'la vidéo' : (p.slides && p.slides.length ? 'les médias (zip)' : 'le visuel'))}</Btn>}{(() => { const can = ready.includes(p.network) && !(res.videoTask && res.videoTask.status !== 'completed'); const why = !ready.includes(p.network) ? 'Connectez une page ' + p.label + ' (Comptes connectés)' : 'Attendez la fin de la vidéo'; return <><Btn size="sm" kind="ghost" icon="calendar" disabled={!can} title={can ? '' : why} onClick={() => setPub({ post: p, mode: 'schedule' })}>Programmer</Btn><Btn size="sm" icon="send" disabled={!can} title={can ? '' : why} onClick={() => setPub({ post: p, mode: 'now' })}>Publier</Btn></>; })()}</div>
    </div>)}</div>
    <div className="card so-sec"><div className="list-head" style={{ alignItems: 'center' }}><div><div className="sec-title">Utiliser dans une publicité</div><div className="sec-sub">Crée une campagne Genius à partir de ce post et ouvre le parcours Publicité (Meta Ads / Google Ads) avec ses propres réglages et validations. Aucune campagne payante n’est lancée ici.</div></div><Btn size="sm" kind="ghost" icon="send" onClick={onAds} disabled={adsBusy}>{adsBusy ? 'Création…' : 'Ouvrir le parcours Publicité'}</Btn></div></div>
  </>;
}

/* ---------- page ---------- */
const SO_DEFAULT = { step: 0, start: 'genius', brief: '', idea: null, campaign: null, objective: '', needs_inputs: {}, nets: ['facebook', 'linkedin', 'instagram'], format: 'image', angle: '', style: 'photo_text', tone: '', logo: true, ref: null, reuse_photo: false };
function SocialPage() {
  const [d, setD] = React.useState(() => { try { const j = JSON.parse(localStorage.getItem(SOCIAL_DKEY) || 'null'); return j && typeof j === 'object' ? { ...SO_DEFAULT, ...j } : { ...SO_DEFAULT }; } catch (e) { return { ...SO_DEFAULT }; } });
  const set = patch => setD(x => ({ ...x, ...patch }));
  const [res, setRes] = React.useState(() => { try { return JSON.parse(localStorage.getItem(SOCIAL_DKEY + ':res') || 'null'); } catch (e) { return null; } });
  React.useEffect(() => { try { localStorage.setItem(SOCIAL_DKEY, JSON.stringify({ ...d, ref: d.ref ? { style: d.ref.style, preview: '' } : null })); } catch (e) {} }, [d]);
  React.useEffect(() => { try { if (res) localStorage.setItem(SOCIAL_DKEY + ':res', JSON.stringify(res)); else localStorage.removeItem(SOCIAL_DKEY + ':res'); } catch (e) {} }, [res]);
  const [ideas, setIdeas] = React.useState(null); const [ideasBusy, setIdeasBusy] = React.useState(false); const [ideasErr, setIdeasErr] = React.useState(''); const seen = React.useRef([]);
  const [camps, setCamps] = React.useState(null); const [campsBusy, setCampsBusy] = React.useState(false);
  const [hf, setHf] = React.useState(null); const [brandTone, setBrandTone] = React.useState('');
  const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(''); const [confirm, setConfirm] = React.useState(false); const [tab, setTab] = React.useState('facebook'); const [hist, setHist] = React.useState(soHist); const [adsBusy, setAdsBusy] = React.useState(false);
  const [refBusy, setRefBusy] = React.useState(false); const [refErr, setRefErr] = React.useState('');
  const late = useLateNotice(busy, 120000);
  const [da, setDa] = React.useState(null);   /* 22/09 : direction artistique de la marque (Réglages → Marque) : pilote la NATURE des visuels */
  React.useEffect(() => { soPost('studio_status', {}).then(j => { if (j && j.ok) setHf({ live: j.mode === 'live' || j.mode === 'mock', blocker: j.blocker || '', defaults: j.defaults }); }).catch(() => {}); const st = window.__genius_status; if (st && st.tone) setBrandTone(st.tone);
    soPost('status', {}).then(j => { if (j && j.ok) setDa({ ...(j.da || {}), model: j.image_model, illu: ['illustration', 'illustration_3d', 'affiche'].includes((j.da || {}).style) }); }).catch(() => {}); }, []);
  React.useEffect(() => { if (da) setD(x => ({ ...x, _da: { ...da, illu: da.illu || (x.ref && x.ref.style && x.ref.style.kind === 'illustration') } })); }, [da, d.ref && d.ref.style && d.ref.style.kind]);
  /* 20/09 : estimation Seedance 2.5 (paramètres exacts : 8 s, 9:16, 720p) affichée dans le plan et la confirmation */
  React.useEffect(() => { if (d.format !== 'video' || !hf || !hf.live) return; let stop = false; soPost('studio_estimate', { endpoint: (hf.defaults && hf.defaults.video && hf.defaults.video.endpoint) || 'bytedance/seedance-2.5/text-to-video', params: { prompt: 'estimate', aspect_ratio: '9:16', duration: 8, resolution: '720p', generate_audio: true } }).then(j => { if (stop) return; setD(x => ({ ...x, _video_est: j && j.ok && j.known ? ('vidéo ≈ ' + Number(j.usd).toFixed(3) + ' $ (' + (j.eur != null ? '≈ ' + j.eur + ' €, ' : '') + 'estimation Higgsfield indicative)') : ('vidéo : estimation indisponible' + (j && j.reason ? ' — ' + j.reason : '')) })); }).catch(() => {}); return () => { stop = true; }; }, [d.format, hf && hf.live]);
  React.useEffect(() => { if (d.step === 0 && d.start === 'genius' && !ideas && !ideasBusy) loadIdeas(false); }, [d.step, d.start]);
  const loadIdeas = React.useCallback((more) => { setIdeasBusy(true); setIdeasErr(''); soPost('social_ideas', { n: 3, seen: more ? seen.current : [], brief: d.brief }).then(j => { setIdeasBusy(false); if (!j || !j.ok) { setIdeasErr((j && (j.detail || j.error)) || 'échec'); return; } const nw = j.ideas || []; seen.current = seen.current.concat(nw.map(i => i.title)).slice(-30); setIdeas(prev => more && prev ? prev.concat(nw) : nw); }).catch(e => { setIdeasBusy(false); setIdeasErr(soNetErr(e)); }); }, [d.brief]);
  const loadCamps = () => { setCampsBusy(true); soPost('campaign_list', {}).then(j => { setCampsBusy(false); setCamps(j && j.ok ? (j.rows || []) : []); }).catch(() => { setCampsBusy(false); setCamps([]); }); };
  /* 22/09/2026 : un visuel importé en inspiration est AUSSI enregistré dans la direction artistique de la marque
     (sinon il fallait le réimporter à chaque post — et les campagnes/e-mails ne le voyaient jamais). */
  const saveRefToBrand = (style) => { if (!style || !style.ref_url) return;
    fetch(GAPI() + '?action=config_get', { method: 'POST', headers: GHEAD(), body: JSON.stringify({ tenant: window.geniusTenant() }) }).then(r => r.json()).then(j => {
      const da = (((j && j.config) || {}).brand || {}).da || {}; const refs = (da.refs || []).filter(u => u !== style.ref_url);
      if ((da.refs || []).includes(style.ref_url)) return;
      const next = { ...da, refs: [style.ref_url].concat(refs).slice(0, 4) };
      return fetch(GAPI() + '?action=config_save', { method: 'POST', headers: GHEAD(), body: JSON.stringify({ tenant: window.geniusTenant(), config: { brand: { da: next } } }) })
        .then(r => r.json()).then(() => soPost('status', {}).then(k => { if (k && k.ok) setDa({ ...(k.da || {}), model: k.image_model, illu: ['illustration', 'illustration_3d', 'affiche'].includes((k.da || {}).style) }); }));
    }).catch(() => {}); };
  const onRef = (file) => { if (!file || !/^image\//.test(file.type)) return; setRefBusy(true); setRefErr(''); const rd = new FileReader(); rd.onload = () => { const im = new Image(); im.onload = () => { const k = Math.min(1, 1024 / Math.max(im.width, im.height)); const c = document.createElement('canvas'); c.width = Math.round(im.width * k); c.height = Math.round(im.height * k); c.getContext('2d').drawImage(im, 0, 0, c.width, c.height); const data = c.toDataURL('image/jpeg', 0.85); soPost('social_style_analyze', { image: data }).then(j => { setRefBusy(false); if (!j || !j.ok) { setRefErr((j && (j.detail || j.error)) || 'analyse impossible'); return; } set({ ref: { preview: data, style: j.style }, style: 'inspiration' }); saveRefToBrand(j.style); }).catch(e => { setRefBusy(false); setRefErr(soNetErr(e)); }); }; im.src = rd.result; }; rd.readAsDataURL(file); };
  React.useEffect(() => { const vt = res && res.videoTask; if (!vt || ['completed', 'failed', 'nsfw', 'canceled', 'download_failed', 'submit_uncertain'].includes(vt.status)) return; let stop = false, delay = 4000, timer = null;
    const tick = () => soPost('studio_task', { task: vt.id }).then(j => { if (stop) return; if (j && j.ok) setRes(r => r ? { ...r, videoTask: j.task } : r); delay = Math.min(10000, delay * 1.3); timer = setTimeout(tick, delay); }).catch(() => { if (!stop) timer = setTimeout(tick, 8000); });
    timer = setTimeout(tick, delay); return () => { stop = true; clearTimeout(timer); }; }, [res && res.videoTask && (res.videoTask.id + ':' + res.videoTask.status)]);
  const hasIdea = d.start === 'mine' ? d.brief.trim().length > 3 : !!d.idea;
  const plan = soPlan(d);
  const generate = () => {
    setConfirm(false); setBusy(true); setErr('');
    const styleMode = (SOCIAL_STYLES.find(x => x[0] === d.style) || [])[2] || 'banner'; const style = d.style === 'inspiration' && d.ref ? d.ref.style : (d.style === 'graphic' ? { overlay: 'solid', layout: 'left' } : null);
    const body = { networks: d.nets, tone: d.tone || '', with_image: d.format !== 'text' && d.format !== 'video', image_mode: styleMode, logo: d.logo !== false, format: soServerFormat(d), style, angle: d.angle || '', needs_inputs: d.needs_inputs || {}, objective: d.objective || '', client_key: 'so' + Date.now().toString(36) + Math.random().toString(36).slice(2, 10) };
    if (d.start === 'mine') body.prompt = d.brief.trim(); else { body.idea = d.idea; if (d.brief.trim()) body.prompt = d.brief.trim(); if (d.start === 'campaign' && d.campaign) { if (camps && !camps.some(c => c.id === d.campaign.id)) { setBusy(false); setErr('Cette campagne n’existe plus : choisissez-en une autre.'); set({ campaign: null, idea: null, step: 0 }); return; } body.campaign_id = d.campaign.id; body.reuse_campaign_photo = !!d.reuse_photo; } }
    soPost('social_posts', body).then(j => { setBusy(false); if (!j || !j.ok) { setErr((j && (j.message || j.detail || j.error)) || 'échec'); return; }
      const r = { at: Date.now(), topic: j.topic, format: j.format || 'single', videoTask: j.video_task || null, videoError: j.video_error || '', style: j.style || null, headline: j.headline, subline: j.subline || '', cta: j.cta || '', scene: j.scene, imageMode: j.image_mode, logo: j.logo !== false, photo: j.photo || '', slideTexts: j.slide_texts || [], slidesDirty: false, posts: (j.posts || []).map(p => ({ ...p, versions: [] })), imageError: j.image_error || '' };
      setRes(r); setTab(r.posts[0] ? r.posts[0].network : 'facebook'); const h = [r].concat(hist).slice(0, 20); setHist(h); soSave(h);
    }).catch(e => { setBusy(false); setErr(soNetErr(e)); });
  };
  /* édition d'un réseau : versions conservées, autres réseaux intacts ; un changement de texte ne touche jamais les images */
  const updPost = (p, snapshot, extra) => setRes(r => { if (!r) return r; const posts = r.posts.map(x => { if (x.network !== p.network) return extra && extra.allSlides ? { ...x, slides: extra.allSlides, image: p.image } : x; const prev = r.posts.find(y => y.network === p.network); const changed = snapshot && prev && (prev.text !== p.text || JSON.stringify(prev.hashtags) !== JSON.stringify(p.hashtags) || (prev.image && p.image && prev.image.url !== p.image.url)); return { ...p, versions: changed ? [...(prev.versions || []), { text: prev.text, hashtags: prev.hashtags, image: prev.image, slides: prev.slides }].slice(-10) : (p.versions || prev.versions || []) }; }); const nr = { ...r, posts, ...(extra || {}) }; delete nr.allSlides; setHist(h0 => { const h = h0.map(x => x.at === nr.at ? nr : x); soSave(h); return h; }); return nr; });   /* (audit C-A11/C-A16) */
  const undo = (net) => setRes(r => { if (!r) return r; return { ...r, posts: r.posts.map(x => { if (x.network !== net || !(x.versions || []).length) return x; const v = x.versions[x.versions.length - 1]; return { ...x, ...v, versions: x.versions.slice(0, -1) }; }) }; });
  const applyOthers = (net) => { if (!res) return; const src = res.posts.find(x => x.network === net); if (!src || !window.confirm('Copier ce texte et ces hashtags dans les autres réseaux ? Chaque réseau garde son visuel ; vous pourrez annuler par réseau.')) return; res.posts.filter(x => x.network !== net).forEach(x => updPost({ ...x, text: src.text, hashtags: src.hashtags }, true)); };
  const toAds = () => { if (!res) return; setAdsBusy(true); const p0 = res.posts[0]; soPost('campaign_save', { title: 'Post — ' + (res.topic || 'réseaux sociaux'), status: 'draft', step: 1, draft: { title: res.topic || '', hook: res.headline || (p0 && (p0.text || '').split('\n')[0]) || '', offer: res.subline || '', cta: res.cta || '', _gen: { image: p0 && p0.image ? p0.image.url : '', photo: res.photo || (p0 && p0.image ? p0.image.photo : '') }, source: 'social' } }).then(j => { setAdsBusy(false); if (j && j.ok && window.geniusOpenCampaign) window.geniusOpenCampaign(j.id, 'ads'); else setErr((j && j.error) || 'campagne non créée'); }).catch(e => { setAdsBusy(false); setErr(soNetErr(e)); }); };
  const reset = () => { setD({ ...SO_DEFAULT }); setRes(null); setIdeas(null); };
  const retryVideo = () => { const vt = res && res.videoTask; if (!vt) return; soPost('studio_retry_download', { task: vt.id }).then(j => { if (j && j.task) setRes(r => r ? { ...r, videoTask: j.task } : r); else setErr((j && (j.message || j.error)) || 'rapatriement impossible'); }).catch(e => setErr(soNetErr(e))); };
  const openHist = h => { setRes({ ...h, posts: (h.posts || []).map(p => ({ versions: [], ...p })) }); set({ step: 1 }); setTab(h.posts && h.posts[0] ? h.posts[0].network : 'facebook'); };
  const step = d.step || 0; const curPost = res && (res.posts.find(p => p.network === tab) || res.posts[0]);
  const primary = step === 0 ? { label: 'Continuer', on: () => set({ step: 1 }), ok: hasIdea, why: d.start === 'mine' ? 'Décrivez votre idée (quelques mots suffisent).' : d.start === 'campaign' ? 'Choisissez une campagne.' : 'Choisissez une suggestion.' }
    : step === 1 ? (res ? { label: 'Passer à la diffusion', on: () => set({ step: 2 }), ok: true } : { label: busy ? 'Génération…' : 'Générer', on: () => setConfirm(true), ok: !busy && d.nets.length > 0, why: 'Choisissez au moins un réseau.' })
    : { label: 'Nouveau post', on: reset, ok: true };
  const summary = step === 0 ? (d.idea ? 'Idée : ' + d.idea.title : (d.start === 'mine' && d.brief.trim() ? 'Mon idée : ' + d.brief.trim().slice(0, 60) : 'Aucune idée choisie')) : (d.nets.map(n => (SOCIAL_NETS.find(x => x.k === n) || {}).label).join(', ') + ' · ' + (SOCIAL_FORMATS.find(f => f[0] === d.format) || [])[1] + (d.angle ? ' · ' + (SOCIAL_ANGLES.find(a => a[0] === d.angle) || [])[1] : ''));
  return <div className="page so-page">
    <PageHead eyebrow="Réseaux sociaux" title={step === 0 ? 'Que souhaitez-vous partager ?' : step === 1 ? (res ? 'Vos posts' : 'Création') : 'Diffusion'} sub={step === 0 ? 'Genius connaît votre marque, son ton et ses visuels : dites-lui juste l’idée.' : step === 1 ? 'Réseaux, format, angle et style — puis chaque réseau reçoit sa version.' : 'Récapitulatif, export, et passage vers la publicité si besoin.'} right={<Stepper steps={['Idée', 'Création', 'Diffusion']} current={step} onJump={i => { if (i < step || (i === 1 && hasIdea) || (i === 2 && res)) set({ step: i }); }} />} />
    {step === 0 && <><SoStep1 d={d} set={set} ideas={ideas} ideasBusy={ideasBusy} ideasErr={ideasErr} loadIdeas={loadIdeas} camps={camps} campsBusy={campsBusy} loadCamps={loadCamps} />
      {hist.length > 0 && <div className="card so-sec"><div className="list-head"><div><div className="sec-title">Reprendre un travail</div><div className="sec-sub">Conservé sur cet ordinateur.</div></div><button className="lnk lnk-muted" onClick={() => { setHist([]); soSave([]); }}>Effacer</button></div><div className="so-hist">{hist.map(h => <button key={h.at} type="button" className="so-hist-row" onClick={() => openHist(h)}><span className="so-hist-nets">{(h.posts || []).map(p => <span key={p.network} className={'so-ic so-ic-' + p.network}>{soIcon(p.network)}</span>)}</span><span className="so-hist-t">{h.topic || 'Posts'}</span><span className="so-hist-d">{new Date(h.at).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}</span></button>)}</div></div>}</>}
    {step === 1 && <>
      <div className="so-ctx"><span><b>Idée</b> {d.idea ? d.idea.title : d.brief.trim().slice(0, 80)}</span><button type="button" className="lnk lnk-muted" onClick={() => set({ step: 0 })}>Modifier</button></div>
      {!res && <SoSettings d={d} set={setD} hf={hf} brandTone={brandTone} onRef={onRef} refBusy={refBusy} refErr={refErr} />}
      {busy && <CampaignGenerationLoader variant="compact" title={d.format === 'carousel' ? 'Genius rédige les posts et compose les diapositives…' : 'Genius rédige les posts' + (d.format !== 'text' ? ' et prépare les visuels…' : '…')} sub={late ? 'Toujours en cours — vos réglages sont conservés.' : plan.label} />}
      {err && <div className="err-txt">Échec : {err} — vos réglages sont conservés, relancez.</div>}
      {res && <>
        {res.imageError && <div className="st-notice"><Icon name="alert" size={14} /> <span>Visuel non généré ({res.imageError}) : les textes sont prêts, utilisez « Modifier le visuel ».</span></div>}
        <div className="subtabs so-nettabs" role="tablist">{res.posts.map(p => <button key={p.network} type="button" role="tab" aria-selected={tab === p.network} className={tab === p.network ? 'on' : ''} onClick={() => setTab(p.network)}><span className={'so-ic so-ic-' + p.network}>{soIcon(p.network)}</span>{p.label}{(p.versions || []).length ? <small> · v{p.versions.length + 1}</small> : null}</button>)}<span className="spacer" /><button type="button" className="lnk lnk-muted" onClick={() => { if (window.confirm('Repartir des réglages ? Les posts actuels restent dans « Reprendre un travail ».')) setRes(null); }}>Régler et régénérer</button></div>
        {curPost && <Card pad={0} className="so-card"><SoEditor key={curPost.network + res.at} post={curPost} res={res} onChange={updPost} onApplyOthers={() => applyOthers(curPost.network)} onVersions={() => undo(curPost.network)} onRetryVideo={retryVideo} /></Card>}
      </>}
    </>}
    {step === 2 && res && <SoStep3 res={res} onAds={toAds} adsBusy={adsBusy} />}
    {step === 2 && !res && <div className="card"><EmptyState title="Rien à diffuser" sub="Générez d’abord vos posts à l’étape Création." action={<Btn size="sm" kind="ghost" onClick={() => set({ step: 1 })}>← Création</Btn>} /></div>}
    <div className="so-actbar" role="region" aria-label="Actions">
      <div className="so-act-sum"><b>{summary}</b><small>{step === 1 && !res ? plan.label + ' · ' + plan.cost : step === 1 && res ? res.posts.length + ' post' + (res.posts.length > 1 ? 's' : '') + ' prêt' + (res.posts.length > 1 ? 's' : '') : step === 2 && res ? 'Publier, programmer, copier ou télécharger' : ''}{!primary.ok && primary.why ? ' · ' + primary.why : ''}</small></div>
      <div className="so-act-btns">{step > 0 && <Btn kind="ghost" onClick={() => set({ step: step - 1 })}>Retour</Btn>}<Btn onClick={primary.on} disabled={!primary.ok} icon={step === 1 && !res ? 'spark' : undefined}>{primary.label}</Btn></div>
    </div>
    {confirm && <div className="st-modal-bg" onClick={() => setConfirm(false)}><div className="st-modal" onClick={e => e.stopPropagation()}>
      <div className="sec-title">Générer ces posts ?</div>
      <div className="st-modal-rows">{[['Réseaux', d.nets.map(n => (SOCIAL_NETS.find(x => x.k === n) || {}).label).join(', ')], ['Format', (SOCIAL_FORMATS.find(f => f[0] === d.format) || [])[1] + (d.angle ? ' · ' + (SOCIAL_ANGLES.find(a => a[0] === d.angle) || [])[1] : '')], ['Textes', plan.texts + ' (un par réseau, rédigés par OpenAI gpt-5.5, facturé)'], ['Médias', d.format === 'text' ? 'aucun' : d.format === 'video' ? '1 vidéo 8 s 9:16 720p — fournisseur appelé : Higgsfield (Seedance 2.5)' + (d._video_est ? ' — ' + d._video_est : '') : (d.campaign && d.reuse_photo && d.format === 'image' ? 'photo de la campagne réutilisée, ' + plan.media + ' recadrage(s)' : plan.photos + ' photo(s) générée(s) (OpenAI images) → ' + plan.media + ' visuel(s)')], ['Coût', plan.cost]].map(([k, v]) => <div key={k}><span>{k}</span><b>{v}</b></div>)}</div>
      <div className="st-modal-a"><Btn kind="ghost" onClick={() => setConfirm(false)}>Annuler</Btn><Btn onClick={generate} icon="spark">Générer</Btn></div>
    </div></div>}
  </div>;
}
Object.assign(window, { SocialPage });
