/* global React, VT_HERO, VT_HERO_EQUIPMENTS, VT_HERO_TRANSCRIPT, VT_CATEGORIES, Badge, Btn, Dot, Icon, RefChip, Sigil, EquipIcon, Waveform, VideoFrame, Confidence, RoofMap, formatTS, formatDuration, fmtMWh, fmtEur, fmtInt, fmtPct, EquipmentCard */
// Visite tabs — equipments, transcript, satellite, report, audit

const { useState: _vtbUseS, useEffect: _vtbUseE, useRef: _vtbUseR, useMemo: _vtbUseM } = React;

// ─── Equipments Tab ──────────────────────────────
// Branche sur /api/visite/:id/equipments si backend dispo, sinon fixtures VT_HERO_EQUIPMENTS
// `bk` est fourni par VisiteHub (instance unique partagée avec les autres tabs) — fallback local si absent
function EquipmentsTab({ v, bk }) {
  const backend = bk || ((typeof window !== 'undefined' && window.useBackendVisite) ? window.useBackendVisite(v) : { hasBackend: false, equipments: [], backendId: null, pipelineStatus: null, refresh: () => {} });
  const API = (window.AE_API && window.AE_API.BASE) || '';
  const [filter, setFilter] = _vtbUseS('all');
  const [analyzing, setAnalyzing] = _vtbUseS(false);

  // Normalise les équipements backend au format attendu par l'UI
  const equipments = _vtbUseM(() => {
    if (!backend.hasBackend) return VT_HERO_EQUIPMENTS;
    return backend.equipments.map((e, i) => ({
      id: e.id,
      category: e.category || 'autre',
      brand: e.brand || '',
      model: e.model || '',
      power: e.power_kw ? e.power_kw + ' kW' : '',
      confidence: e.confidence || 0.5,
      validated: !!e.validated,
      frameTimestamp: e.frameTimestamp,
      source: e.source,
      productCatalogId: e.productCatalogId,
      fostCodesSuggested: e.fostCodesSuggested || [],
      serialNumber: e.serialNumber,
      refrigerant: e.refrigerant,
      energyClass: e.energyClass,
      rawOcrText: e.rawOcrText,
      notes: e.notes,
      _backend: true,
    }));
  }, [backend.hasBackend, backend.equipments]);

  const [localSelected, setLocalSelected] = _vtbUseS(null);
  const selected = localSelected || (equipments[0] && equipments[0].id);

  const filtered = equipments.filter(e => filter === 'all' || (filter === 'pending' && !e.validated) || (filter === 'validated' && e.validated));
  const cur = equipments.find(e => e.id === selected) || equipments[0];
  const stats = {
    total: equipments.length,
    validated: equipments.filter(e => e.validated).length,
    pending: equipments.filter(e => !e.validated).length,
    avgConfidence: equipments.length ? equipments.reduce((s, e) => s + (e.confidence || 0), 0) / equipments.length : 0,
  };

  async function validateEquipment(eqId) {
    if (!backend.hasBackend) return;
    await fetch(`${API}/api/visite/${backend.backendId}/equipments/${eqId}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ validated: true, validatedAt: new Date().toISOString() }),
    }).catch(() => {});
    backend.refresh();
  }

  async function relaunchAnalysis() {
    if (!backend.hasBackend) return;
    setAnalyzing(true);
    try {
      await fetch(`${API}/api/visite/${backend.backendId}/analyze`, { method: 'POST' });
    } catch (_) {}
    setTimeout(() => { setAnalyzing(false); backend.refresh(); }, 2000);
  }

  return (
    <div>
      {/* Pipeline status bar (mode backend) */}
      {backend.hasBackend && (
        <div style={{ marginBottom: 12, padding: '10px 14px', background: 'var(--paper)', border: '1px solid var(--line)', borderRadius: 6, display: 'flex', alignItems: 'center', gap: 10 }}>
          <div style={{ fontSize: 11, color: 'var(--ink-3)', fontWeight: 600 }}>
            Pipeline IA ·
          </div>
          {backend.pipelineStatus ? (
            <>
              <Badge tone={backend.pipelineStatus.status === 'completed' ? 'signal' : backend.pipelineStatus.status === 'error' ? 'rouge' : 'plasma'}
                icon={backend.pipelineStatus.status === 'running' ? <Dot tone="plasma" pulse size={6}/> : null}>
                {backend.pipelineStatus.status}
              </Badge>
              <span style={{ fontSize: 10, color: 'var(--ink-4)', fontFamily: 'var(--font-mono)' }}>
                {Object.entries(backend.pipelineStatus.steps || {}).map(([k, s]) => `${k}:${s}`).join(' · ')}
              </span>
            </>
          ) : (
            <span style={{ fontSize: 11, color: 'var(--ink-4)' }}>aucune analyse lancée</span>
          )}
          <span style={{ flex: 1 }}/>
          <Btn variant="outline" size="sm" icon={<Icon.refresh/>} onClick={backend.refresh}>Rafraîchir</Btn>
          <Btn variant="signal" size="sm" icon={<Icon.bolt/>} onClick={relaunchAnalysis} disabled={analyzing || backend.pipelineStatus?.status === 'running'}>
            {analyzing ? 'Lancement…' : 'Relancer analyse IA'}
          </Btn>
        </div>
      )}

    <div style={{ display: 'grid', gridTemplateColumns: '340px 1fr', gap: 16 }}>
      {/* Left list */}
      <div>
        <div style={{ display: 'flex', gap: 6, marginBottom: 12 }}>
          {[
            { v: 'all', l: 'Tous', n: stats.total },
            { v: 'pending', l: 'À valider', n: stats.pending, tone: 'amber' },
            { v: 'validated', l: 'Validés', n: stats.validated, tone: 'signal' },
          ].map(f => (
            <button key={f.v} onClick={() => setFilter(f.v)} style={{
              flex: 1, padding: '8px 10px', fontSize: 11, fontWeight: 500,
              background: filter === f.v ? 'var(--ink)' : 'var(--paper)',
              color: filter === f.v ? 'var(--paper)' : 'var(--ink-2)',
              border: filter === f.v ? '1px solid var(--ink)' : '1px solid var(--line-2)',
              borderRadius: 5,
              display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5,
            }}>
              {f.l}
              <span className="mono" style={{ fontSize: 10, opacity: 0.7 }}>{f.n}</span>
            </button>
          ))}
        </div>

        <div style={{ border: '1px solid var(--line)', borderRadius: 6, background: 'var(--paper)', overflow: 'hidden' }}>
          {filtered.map((eq, i) => {
            const cat = VT_CATEGORIES[eq.category] || { label: eq.category };
            const isSel = selected === eq.id;
            return (
              <button key={eq.id} onClick={() => setLocalSelected(eq.id)} style={{
                width: '100%', padding: '10px 12px',
                display: 'flex', alignItems: 'center', gap: 10,
                borderBottom: i < filtered.length - 1 ? '1px solid var(--hairline)' : 'none',
                background: isSel ? 'var(--paper-2)' : 'transparent',
                borderLeft: isSel ? '2px solid var(--signal)' : '2px solid transparent',
                textAlign: 'left', cursor: 'pointer',
              }}>
                <div style={{ width: 30, height: 30, borderRadius: 4, background: 'var(--paper-2)', border: '1px solid var(--line)', display: 'grid', placeItems: 'center', color: `var(--${cat.tone === 'neutral' || cat.tone === 'muted' ? 'ink-3' : cat.tone})` }}>
                  <EquipIcon cat={eq.category} size={16}/>
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--ink)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{eq.brand} {eq.model}</div>
                  <div style={{ fontSize: 10, color: 'var(--ink-4)' }}>{cat.label} · @{formatTS(eq.frameTimestamp || 0)}</div>
                </div>
                <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 3 }}>
                  <Confidence value={eq.confidence} width={30} label={false}/>
                  {eq.validated ? <Dot tone="signal" size={5}/> : <Dot tone="amber" size={5}/>}
                </div>
              </button>
            );
          })}
        </div>

        {/* Add equipment */}
        <button onClick={async () => {
          if (!backend.hasBackend) { alert('Ajout manuel disponible uniquement sur visites backend'); return; }
          const category = prompt('Catégorie (pac_air_air, pac_air_eau, chaudiere, ballon_thermo, panneau_pv, clim, groupe_froid, cta, vmc) :', 'pac_air_eau');
          if (!category) return;
          const brand = prompt('Marque :') || '';
          const model = prompt('Modèle :') || '';
          const power = prompt('Puissance (kW) :');
          await fetch(`${API}/api/visite/${backend.backendId}/equipments`, {
            method: 'POST', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ category, brand, model, power_kw: power ? parseFloat(power) : null, source: 'manual', validated: true }),
          }).catch(() => {});
          backend.refresh();
        }} style={{ marginTop: 10, width: '100%', padding: 10, border: '1px dashed var(--line-2)', borderRadius: 5, fontSize: 12, color: 'var(--ink-3)', background: 'transparent', cursor: 'pointer' }}>
          + Ajouter équipement manuel
        </button>

        {/* Confidence distribution */}
        <div style={{ marginTop: 14, padding: 12, border: '1px solid var(--line)', borderRadius: 6, background: 'var(--paper)' }}>
          <div style={{ fontSize: 10, color: 'var(--ink-4)', textTransform: 'uppercase', letterSpacing: 0.5, fontWeight: 600, marginBottom: 8 }}>Qualité détection</div>
          <div style={{ display: 'flex', alignItems: 'flex-end', gap: 2, height: 48 }}>
            {Array.from({ length: 10 }).map((_, i) => {
              const low = i / 10, high = (i + 1) / 10;
              const count = equipments.filter(e => e.confidence >= low && e.confidence < high).length;
              const h = Math.max(2, (count / Math.max(1, Math.max(...Array.from({ length: 10 }).map((_, j) => equipments.filter(e => e.confidence >= j / 10 && e.confidence < (j + 1) / 10).length)))) * 48);
              const color = high > 0.85 ? 'var(--signal)' : high > 0.7 ? 'var(--copper)' : 'var(--amber)';
              return <div key={i} style={{ flex: 1, height: h, background: color, minHeight: 2 }}/>;
            })}
          </div>
          <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 9, color: 'var(--ink-4)', marginTop: 5, fontFamily: 'var(--font-mono)' }}>
            <span>0%</span><span>50%</span><span>100%</span>
          </div>
        </div>
      </div>

      {/* Right — detail */}
      <div>
        {cur && (
          <>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 360px', gap: 16, marginBottom: 16 }}>
              {/* Frame preview with bounding box */}
              <div style={{ aspectRatio: '16/9', border: '1px solid var(--line)', borderRadius: 6, overflow: 'hidden', position: 'relative', background: '#000' }}>
                <VideoFrame
                  scene={cur.category === 'pac_air_eau' ? 'utility_room' : cur.category === 'ballon_thermo' ? 'utility_room' : cur.category === 'compteur' ? 'meter_box' : cur.category === 'chaudiere' ? 'boiler' : cur.category === 'panneau_pv' ? 'roof_drone' : 'utility_room'}
                  timestamp={cur.frameTimestamp}
                  overlay={cur.boundingBox && (
                    <div style={{
                      position: 'absolute',
                      left: `${cur.boundingBox.x * 100}%`,
                      top: `${cur.boundingBox.y * 100}%`,
                      width: `${cur.boundingBox.w * 100}%`,
                      height: `${cur.boundingBox.h * 100}%`,
                      border: '2px solid var(--signal)',
                    }}>
                      {/* Corner marks */}
                      {['tl','tr','bl','br'].map(c => (
                        <div key={c} style={{
                          position: 'absolute',
                          [c.includes('t') ? 'top' : 'bottom']: -3,
                          [c.includes('l') ? 'left' : 'right']: -3,
                          width: 9, height: 9,
                          borderTop: c.includes('t') ? '2px solid var(--signal)' : 'none',
                          borderBottom: c.includes('b') ? '2px solid var(--signal)' : 'none',
                          borderLeft: c.includes('l') ? '2px solid var(--signal)' : 'none',
                          borderRight: c.includes('r') ? '2px solid var(--signal)' : 'none',
                          background: 'var(--signal)',
                        }}/>
                      ))}
                    </div>
                  )}
                />
                <div style={{ position: 'absolute', bottom: 10, left: 10, padding: '3px 8px', background: 'rgba(0,0,0,0.7)', color: '#fff', fontSize: 10, fontFamily: 'var(--font-mono)' }}>
                  frame t={formatTS(cur.frameTimestamp || 0)}
                </div>
              </div>

              {/* Spec sheet */}
              <div style={{ border: '1px solid var(--line)', borderRadius: 6, background: 'var(--paper)', padding: 16 }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
                  <div style={{ width: 40, height: 40, borderRadius: 5, background: 'var(--ink)', color: 'var(--signal)', display: 'grid', placeItems: 'center' }}>
                    <EquipIcon cat={cur.category} size={22}/>
                  </div>
                  <div style={{ flex: 1 }}>
                    <div style={{ fontSize: 10, color: 'var(--ink-4)', textTransform: 'uppercase', letterSpacing: 0.5, fontWeight: 600 }}>{(VT_CATEGORIES[cur.category] || {}).label}</div>
                    <div style={{ fontSize: 16, fontWeight: 600 }}>{cur.brand}</div>
                    <div style={{ fontSize: 12, color: 'var(--ink-3)' }}>{cur.model}</div>
                  </div>
                </div>
                <Specs>
                  {cur.serialNumber && <Spec label="S/N" value={cur.serialNumber} mono/>}
                  {cur.power_kw && <Spec label="Puissance" value={`${cur.power_kw} kW`}/>}
                  {cur.refrigerant && <Spec label="Fluide" value={cur.refrigerant}/>}
                  {cur.yearEstimated && <Spec label="Année" value={cur.yearEstimated}/>}
                  {cur.energyClass && <Spec label="Classe" value={<Badge tone="signal">{cur.energyClass}</Badge>}/>}
                  <Spec label="Source" value={({ vision: 'Vision IA', ocr: 'OCR plaque', audio_nlp: 'NLP audio', manual: 'Saisie tech.' })[cur.source]}/>
                  <Spec label="Confiance" value={<Confidence value={cur.confidence}/>}/>
                </Specs>
                <div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid var(--hairline)' }}>
                  {cur.validated ? (
                    <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: 'var(--signal-deep)' }}>
                      <Icon.check/> Validé par {cur.validatedBy}
                      <span style={{ flex: 1 }}/>
                      <button style={{ fontSize: 11, color: 'var(--ink-4)' }}>Dévalider</button>
                    </div>
                  ) : (
                    <div style={{ display: 'flex', gap: 6 }}>
                      <Btn variant="signal" size="sm" icon={<Icon.check/>} onClick={() => validateEquipment(cur.id)}>Valider</Btn>
                      <Btn variant="ghost" size="sm" icon={<Icon.cross/>} onClick={async () => {
                        if (!backend.hasBackend) return;
                        if (!confirm('Rejeter (supprimer) cet équipement détecté ?')) return;
                        await fetch(`${API}/api/visite/${backend.backendId}/equipments/${cur.id}`, { method: 'DELETE' }).catch(() => {});
                        backend.refresh();
                      }}>Rejeter</Btn>
                      <Btn variant="outline" size="sm">Éditer specs</Btn>
                    </div>
                  )}
                </div>
              </div>
            </div>

            {/* OCR + FOST */}
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
              <div style={{ border: '1px solid var(--line)', borderRadius: 6, background: 'var(--paper)', padding: 16 }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 10 }}>
                  <span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)' }}>Plaque signalétique · lecture OCR</span>
                  {cur.rawOcrText && <Badge tone="plasma">Claude Vision</Badge>}
                </div>
                {cur.rawOcrText ? (
                  <div style={{
                    padding: 12, background: 'var(--ink)', color: 'var(--signal)',
                    fontFamily: 'var(--font-mono)', fontSize: 11, lineHeight: 1.7,
                    borderRadius: 3, whiteSpace: 'pre-wrap',
                    border: '1px solid rgba(127,255,183,0.2)',
                  }}>{cur.rawOcrText}</div>
                ) : (
                  <div style={{ padding: 16, background: 'var(--paper-2)', fontSize: 12, color: 'var(--ink-4)', textAlign: 'center', borderRadius: 5 }}>
                    Pas de plaque lisible — détecté via {cur.source === 'audio_nlp' ? 'mention orale' : 'vision'}.
                  </div>
                )}
                {cur.notes && (
                  <div style={{ marginTop: 10, padding: 10, background: 'var(--paper-2)', fontSize: 12, color: 'var(--ink-2)', borderRadius: 5, borderLeft: '2px solid var(--copper)' }}>
                    <span style={{ color: 'var(--ink-4)', fontSize: 10, textTransform: 'uppercase', letterSpacing: 0.5, fontWeight: 600 }}>Note technicien</span>
                    <div style={{ marginTop: 4 }}>{cur.notes}</div>
                  </div>
                )}
              </div>
              <div style={{ border: '1px solid var(--line)', borderRadius: 6, background: 'var(--paper)', padding: 16 }}>
                <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)', marginBottom: 10 }}>
                  FOST suggérées · {cur.fostCodesSuggested.length}
                </div>
                {cur.fostCodesSuggested.length ? cur.fostCodesSuggested.map(f => (
                  <div key={f.code} style={{ padding: 10, border: '1px solid var(--line)', borderRadius: 5, background: 'var(--paper-2)', marginBottom: 6, display: 'flex', alignItems: 'center', gap: 10 }}>
                    <RefChip r={f.code}/>
                    <span style={{ flex: 1, fontSize: 12 }}>{f.label}</span>
                    <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--signal-deep)' }}>{fmtMWh(f.kwhCumac)}</span>
                    <Btn variant="outline" size="sm" iconRight={<Icon.arrow/>}>Ajouter</Btn>
                  </div>
                )) : (
                  <div style={{ padding: 16, background: 'var(--paper-2)', fontSize: 12, color: 'var(--ink-4)', textAlign: 'center', borderRadius: 5 }}>
                    Aucune fiche FOST applicable directement — cet équipement est conservé tel quel.
                  </div>
                )}
              </div>
            </div>
          </>
        )}
      </div>
    </div>
    </div>
  );
}

function Specs({ children }) {
  return <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>{children}</div>;
}
function Spec({ label, value, mono }) {
  return (
    <div>
      <div style={{ fontSize: 9, color: 'var(--ink-4)', textTransform: 'uppercase', letterSpacing: 0.5, fontWeight: 600 }}>{label}</div>
      <div className={mono ? 'mono' : ''} style={{ fontSize: 12, fontWeight: mono ? 500 : 600, color: 'var(--ink-2)', marginTop: 3, letterSpacing: mono ? '-0.01em' : 0 }}>{value}</div>
    </div>
  );
}

// ─── Transcript Tab ──────────────────────────────
function TranscriptTab({ v, bk }) {
  const [playheadT, setPlayheadT] = _vtbUseS(47);
  const [filterEntity, setFilterEntity] = _vtbUseS('all');
  const [filterSpeaker, setFilterSpeaker] = _vtbUseS('all');
  const [query, setQuery] = _vtbUseS('');

  // ── Normalise backend transcripts vs fixture ──────────────────────────────
  const hasBackendT = !!(bk && bk.hasBackend && bk.transcripts && bk.transcripts.length > 0);
  const activeSegments = _vtbUseM(() => {
    if (!hasBackendT) return VT_HERO_TRANSCRIPT.segments;
    return bk.transcripts
      .flatMap(t => (t.segments || []).map(s => ({ ...s, entities: s.entities || [] })))
      .sort((a, b) => a.start - b.start);
  }, [bk]);
  const activeSpeakers = _vtbUseM(() => {
    if (!hasBackendT) return VT_HERO_TRANSCRIPT.speakers;
    const m = new Map();
    bk.transcripts.forEach(t => (t.speakers || []).forEach(s => m.set(s.id, s)));
    return [...m.values()];
  }, [bk]);
  const activeSentiment = _vtbUseM(() => {
    if (!hasBackendT) return VT_HERO_TRANSCRIPT.sentiment;
    return bk.transcripts[0]?.sentiment || { overall: 'neutre', score: 0.5 };
  }, [bk]);

  const entities = {};
  activeSegments.forEach(s => (s.entities || []).forEach(e => {
    if (!entities[e.type]) entities[e.type] = [];
    entities[e.type].push({ ...e, segment: s });
  }));

  const ENTITY_META = {
    equipment:  { label: 'Équipements', tone: 'signal',  color: 'var(--signal-deep)' },
    date:       { label: 'Dates',       tone: 'neutral', color: 'var(--ink-2)' },
    budget:     { label: 'Budget',      tone: 'copper',  color: 'var(--copper)' },
    spec:       { label: 'Specs',       tone: 'plasma',  color: 'var(--plasma)' },
    action:     { label: 'Actions',     tone: 'signal',  color: 'var(--signal-deep)' },
    problem:    { label: 'Problèmes',   tone: 'rouge',   color: 'var(--rouge)' },
    subvention: { label: 'Subventions', tone: 'amber',   color: '#8C6510' },
  };

  const filteredSegments = activeSegments.filter(s =>
    (filterSpeaker === 'all' || s.speaker === filterSpeaker) &&
    (filterEntity === 'all' || (s.entities || []).some(e => e.type === filterEntity)) &&
    (!query || s.text.toLowerCase().includes(query.toLowerCase()))
  );

  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1fr 320px', gap: 16 }}>
      <div>
        {/* Audio player strip */}
        <div style={{ padding: 14, background: 'var(--ink)', borderRadius: 6, marginBottom: 14, display: 'flex', alignItems: 'center', gap: 14 }}>
          <button style={{ width: 36, height: 36, borderRadius: '50%', background: 'var(--signal)', color: '#0B1F12', display: 'grid', placeItems: 'center' }}>
            <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4 3l9 5-9 5z"/></svg>
          </button>
          <div style={{ flex: 1 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 6 }}>
              <span className="mono" style={{ fontSize: 11, color: 'var(--signal)' }}>{formatTS(playheadT)} / {formatTS(v.durationSeconds)}</span>
              <span style={{ flex: 1 }}/>
              <span style={{ fontSize: 11, color: 'rgba(255,255,255,0.6)' }}>Whisper-large-v3 · diarization pyannote-3</span>
            </div>
            <Waveform seed={v.ref + '-transcript'} width={620} height={34} bars={100} color="var(--signal)" bg="rgba(255,255,255,0.12)" progress={playheadT / v.durationSeconds}/>
          </div>
        </div>

        {/* Search + filters */}
        <div style={{ display: 'flex', gap: 8, marginBottom: 12, alignItems: 'center' }}>
          <div style={{ position: 'relative', flex: 1, minWidth: 200 }}>
            <Icon.search style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', color: 'var(--ink-4)' }}/>
            <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Rechercher dans la transcription…" style={{ width: '100%', padding: '8px 10px 8px 32px', border: '1px solid var(--line-2)', borderRadius: 5, fontSize: 13, background: 'var(--paper)' }}/>
          </div>
          <select value={filterSpeaker} onChange={e => setFilterSpeaker(e.target.value)} style={{ appearance: 'none', padding: '7px 10px', fontSize: 12, background: 'var(--paper)', border: '1px solid var(--line-2)', borderRadius: 5 }}>
            <option value="all">Tous locuteurs</option>
            {activeSpeakers.map(s => <option key={s.id} value={s.id}>{s.label}</option>)}
          </select>
          <select value={filterEntity} onChange={e => setFilterEntity(e.target.value)} style={{ appearance: 'none', padding: '7px 10px', fontSize: 12, background: 'var(--paper)', border: '1px solid var(--line-2)', borderRadius: 5 }}>
            <option value="all">Toutes entités</option>
            {Object.entries(entities).map(([k, arr]) => <option key={k} value={k}>{ENTITY_META[k]?.label || k} ({arr.length})</option>)}
          </select>
        </div>

        {/* Segments */}
        <div style={{ border: '1px solid var(--line)', borderRadius: 6, background: 'var(--paper)' }}>
          {(bk?.hasBackend && !hasBackendT) ? (
            <div style={{ padding: 48, textAlign: 'center', color: 'var(--ink-4)', fontSize: 13 }}>
              {bk?.pipelineStatus?.status === 'running'
                ? <><Dot tone="plasma" pulse size={8}/> <span style={{ marginLeft: 8 }}>Transcription Whisper en cours…</span></>
                : '🎤 Aucune transcription disponible — lancez l\'analyse IA pour transcrire l\'audio.'}
            </div>
          ) : filteredSegments.map((s, i) => {
            const speaker = activeSpeakers.find(sp => sp.id === s.speaker);
            const isCurrent = s.start <= playheadT && s.end >= playheadT;
            return (
              <div key={i} onClick={() => setPlayheadT(s.start)} style={{
                padding: '14px 16px', borderBottom: i < filteredSegments.length - 1 ? '1px solid var(--hairline)' : 'none',
                background: isCurrent ? 'var(--signal-tint)' : 'transparent',
                borderLeft: isCurrent ? '2px solid var(--signal)' : '2px solid transparent',
                cursor: 'pointer', display: 'grid', gridTemplateColumns: '60px 28px 1fr', gap: 10,
              }}>
                <div className="mono" style={{ fontSize: 11, color: isCurrent ? 'var(--signal-deep)' : 'var(--ink-4)', fontWeight: isCurrent ? 600 : 400 }}>
                  {formatTS(s.start)}
                </div>
                <div style={{ width: 22, height: 22, borderRadius: '50%', background: s.speaker === 'S1' ? 'var(--ink)' : 'var(--copper-tint)', color: s.speaker === 'S1' ? 'var(--paper)' : 'var(--copper)', display: 'grid', placeItems: 'center', fontSize: 9, fontWeight: 700 }}>
                  {speaker?.label.split(' ').map(x => x[0]).join('').slice(0, 2) || s.speaker}
                </div>
                <div>
                  <div style={{ fontSize: 10, color: 'var(--ink-4)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 3 }}>{speaker?.label} <span style={{ textTransform: 'none', fontWeight: 400, letterSpacing: 0 }}>· {speaker?.role === 'tech' ? 'technicien' : 'bénéficiaire'}</span></div>
                  <div style={{ fontSize: 14, color: 'var(--ink)', lineHeight: 1.55 }}>
                    {highlightEntities(s.text, s.entities || [], ENTITY_META)}
                  </div>
                  {(s.entities || []).length > 0 && (
                    <div style={{ marginTop: 6, display: 'flex', flexWrap: 'wrap', gap: 4 }}>
                      {(s.entities || []).map((e, j) => (
                        <span key={j} style={{ fontSize: 10, padding: '2px 6px', borderRadius: 3, background: 'var(--paper-2)', color: ENTITY_META[e.type]?.color || 'var(--ink-3)', border: `1px solid ${ENTITY_META[e.type]?.color || 'var(--line-2)'}`, fontFamily: 'var(--font-mono)' }}>
                          {ENTITY_META[e.type]?.label?.slice(0, -1) || e.type}: {e.value}
                        </span>
                      ))}
                    </div>
                  )}
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {/* Right column */}
      <div>
        <div style={{ border: '1px solid var(--line)', borderRadius: 6, background: 'var(--paper)', padding: 14, marginBottom: 14 }}>
          <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)', marginBottom: 10 }}>Locuteurs · diarisation</div>
          {activeSpeakers.map(s => {
            const segs = activeSegments.filter(seg => seg.speaker === s.id);
            const total = segs.reduce((sum, seg) => sum + (seg.end - seg.start), 0);
            const pct = (total / (activeSegments.reduce((sum, seg) => sum + (seg.end - seg.start), 0) || 1)) * 100;
            return (
              <div key={s.id} style={{ marginBottom: 10, padding: 10, background: 'var(--paper-2)', borderRadius: 5 }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                  <div style={{ width: 26, height: 26, borderRadius: '50%', background: s.role === 'tech' ? 'var(--ink)' : 'var(--copper-tint)', color: s.role === 'tech' ? 'var(--paper)' : 'var(--copper)', display: 'grid', placeItems: 'center', fontSize: 10, fontWeight: 700 }}>
                    {s.label.split(' ').map(x => x[0]).join('').slice(0, 2)}
                  </div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 12, fontWeight: 600 }}>{s.label}</div>
                    <div style={{ fontSize: 10, color: 'var(--ink-4)' }}>{s.role === 'tech' ? 'Technicien' : 'Bénéficiaire'} · {segs.length} tours</div>
                  </div>
                  <span className="mono" style={{ fontSize: 11, color: 'var(--ink-2)', fontWeight: 600 }}>{pct.toFixed(0)}%</span>
                </div>
                <div style={{ marginTop: 6, height: 3, background: 'var(--line)', borderRadius: 2, overflow: 'hidden' }}>
                  <div style={{ width: `${pct}%`, height: '100%', background: s.role === 'tech' ? 'var(--signal)' : 'var(--copper)' }}/>
                </div>
              </div>
            );
          })}
        </div>

        <div style={{ border: '1px solid var(--line)', borderRadius: 6, background: 'var(--paper)', padding: 14, marginBottom: 14 }}>
          <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)', marginBottom: 10 }}>Entités extraites · NLP</div>
          {Object.entries(entities).map(([k, arr]) => (
            <div key={k} style={{ padding: '8px 0', borderBottom: '1px solid var(--hairline)', fontSize: 12 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
                <span style={{ width: 8, height: 8, borderRadius: 2, background: ENTITY_META[k]?.color || 'var(--ink-4)' }}/>
                <span style={{ fontWeight: 600, color: ENTITY_META[k]?.color || 'var(--ink-2)' }}>{ENTITY_META[k]?.label || k}</span>
                <span style={{ flex: 1 }}/>
                <span className="mono" style={{ fontSize: 10, color: 'var(--ink-4)' }}>{arr.length}</span>
              </div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 3, paddingLeft: 14 }}>
                {arr.slice(0, 5).map((e, j) => (
                  <span key={j} style={{ fontSize: 10, padding: '1px 5px', background: 'var(--paper-2)', color: 'var(--ink-2)', borderRadius: 2, fontFamily: 'var(--font-mono)' }}>{e.value}</span>
                ))}
                {arr.length > 5 && <span style={{ fontSize: 10, color: 'var(--ink-4)' }}>+{arr.length - 5}</span>}
              </div>
            </div>
          ))}
        </div>

        <div style={{ border: '1px solid var(--line)', borderRadius: 6, background: 'var(--paper)', padding: 14 }}>
          <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)', marginBottom: 10 }}>Analyse sentiment</div>
          <div style={{ padding: 10, background: 'var(--signal-tint)', borderRadius: 5, display: 'flex', alignItems: 'center', gap: 10 }}>
            <div style={{ width: 42, height: 42, borderRadius: '50%', background: 'var(--signal)', color: '#0B1F12', display: 'grid', placeItems: 'center', fontSize: 18, fontWeight: 700 }}>
              +{Math.round((activeSentiment.score || 0) * 100)}
            </div>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--signal-deep)', textTransform: 'capitalize' }}>{activeSentiment.overall}</div>
              <div style={{ fontSize: 11, color: 'var(--ink-3)' }}>Ouverture forte sur les travaux, budget mentionné spontanément.</div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function highlightEntities(text, entities, meta) {
  if (!entities || !entities.length) return text;
  // sort by length desc to avoid substring conflicts
  const ents = [...entities].sort((a, b) => b.value.length - a.value.length);
  const parts = [{ text, ent: null }];
  ents.forEach(e => {
    const next = [];
    parts.forEach(p => {
      if (p.ent) { next.push(p); return; }
      const lower = p.text.toLowerCase();
      const idx = lower.indexOf(e.value.toLowerCase());
      if (idx === -1) { next.push(p); return; }
      if (idx > 0) next.push({ text: p.text.slice(0, idx), ent: null });
      next.push({ text: p.text.slice(idx, idx + e.value.length), ent: e });
      if (idx + e.value.length < p.text.length) next.push({ text: p.text.slice(idx + e.value.length), ent: null });
    });
    parts.splice(0, parts.length, ...next);
  });
  return parts.map((p, i) => p.ent
    ? <mark key={i} style={{ background: `${meta[p.ent.type]?.color || 'var(--ink-3)'}22`, color: 'var(--ink)', padding: '0 2px', borderBottom: `1.5px solid ${meta[p.ent.type]?.color || 'var(--ink-3)'}` }}>{p.text}</mark>
    : <React.Fragment key={i}>{p.text}</React.Fragment>
  );
}

// ─── Satellite Tab (toit + calpinage + PVGIS) — câblé backend ────────────────
// Dual mode : Leaflet + IGN WMTS (réseau) → RoofMap SVG (fallback)
// POST /api/visite/:id/satellite → BAN → IGN Cadastre → PVGIS → satelliteData
function SatelliteTab({ v, bk }) {
  const backend = bk || ((typeof window !== 'undefined' && window.useBackendVisite)
    ? window.useBackendVisite(v)
    : { hasBackend: false, satelliteData: null, backendId: null, full: null, refresh: () => {} });
  const API = (window.AE_API && window.AE_API.BASE) || '';

  // Analyse satellite
  const [analyzing, setAnalyzing] = _vtbUseS(false);
  const [analyzeError, setAnalyzeError] = _vtbUseS(null);

  // Calpinage controls
  const calp = v.calpinage || {};
  const [rows, setRows] = _vtbUseS(calp.rows || 2);
  const [cols, setCols] = _vtbUseS(calp.cols || 8);

  // ── Normalize backend satelliteData vs fixture ────────────────────────────
  // Backend structure: { geocode:{lat,lon,label}, rooftop:{estimatedRooftopArea,commune,parcelRef,parcelSurface}, altitude, pvgis:{peakpower_kWc,annualProduction_kWh,irradiation_kWh_m2}, analyzedAt }
  // Fixture structure: v.satellite = { roofArea, usableArea, pvPotentialKwh, pvgisSource, orientation, tilt, shadowFactor }
  const rawSat = backend.satelliteData;
  const hasSatData = !!(rawSat?.analyzedAt || rawSat?.pvgis?.annualProduction_kWh);

  // GPS — prefer backend.full (updated after satellite analysis) then fixture
  const satLat = backend.full?.lat || v.lat || null;
  const satLon = backend.full?.lon || v.lon || null;

  // Metrics (backend takes priority)
  const rooftopArea     = rawSat?.rooftop?.estimatedRooftopArea || v.satellite?.roofArea || null;
  const pvgisKwc        = rawSat?.pvgis?.peakpower_kWc || null;
  const pvgisAnnual     = rawSat?.pvgis?.annualProduction_kWh || null;
  const pvgisIrr        = rawSat?.pvgis?.irradiation_kWh_m2 || null;
  const altitude        = typeof rawSat?.altitude === 'number' ? rawSat.altitude : null;
  const commune         = rawSat?.rooftop?.commune || null;
  const parcelRef       = rawSat?.rooftop?.parcelRef || null;
  const parcelSurface   = rawSat?.rooftop?.parcelSurface || null;
  const orientation     = v.satellite?.orientation || 'Sud';
  const tiltAngle       = v.satellite?.tilt || 25;
  const shadowFactor    = v.satellite?.shadowFactor || 0.06;
  const selfCons        = calp.selfConsumption || 0.62;

  // ── Calpinage calc ────────────────────────────────────────────────────────
  const panelW  = calp.panelW || 400; // Wc
  const panels  = rows * cols;
  const kwc     = +(panels * panelW / 1000).toFixed(2);

  const annualProd = _vtbUseM(() => {
    if (pvgisAnnual && pvgisKwc && pvgisKwc > 0) {
      return Math.round(pvgisAnnual * kwc / pvgisKwc);  // scale from PVGIS peakpower
    }
    if (pvgisIrr) {
      return Math.round(kwc * pvgisIrr * 0.85);         // 85% perf ratio
    }
    const fallbackKwh = v.satellite?.pvPotentialKwh || 5600;
    return Math.round(kwc * (fallbackKwh / (calp.totalKwc || kwc || 1)));
  }, [pvgisAnnual, pvgisKwc, pvgisIrr, kwc]);

  // Seasonal profile (12 months), normalized to annualProd
  const SEASONAL = [0.048, 0.074, 0.133, 0.185, 0.228, 0.258, 0.272, 0.250, 0.191, 0.125, 0.066, 0.044];
  const pvgisMonthly = _vtbUseM(() => SEASONAL.map(f => Math.round(annualProd * f)), [annualProd]);
  const months = ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'];

  // ── Leaflet map refs ──────────────────────────────────────────────────────
  const mapContainerRef = _vtbUseR(null);
  const leafletMapRef   = _vtbUseR(null);
  const markerRef       = _vtbUseR(null);
  const leafletAvail    = typeof L !== 'undefined';

  // Initialize Leaflet once on mount
  _vtbUseE(() => {
    const container = mapContainerRef.current;
    if (!container || !leafletAvail || leafletMapRef.current) return;

    const initLat  = satLat || 46.6034;
    const initLon  = satLon || 1.8883;
    const initZoom = (satLat && satLon) ? 18 : 5;

    const map = L.map(container, { zoomControl: false, attributionControl: false })
      .setView([initLat, initLon], initZoom);

    // IGN WMTS orthophoto (primary)
    const ignTile = L.tileLayer(
      'https://data.geopf.fr/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0' +
      '&LAYER=ORTHOIMAGERY.ORTHOPHOTOS&STYLE=normal&TILEMATRIXSET=PM' +
      '&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=image/jpeg',
      { maxZoom: 20, tileSize: 256 }
    ).addTo(map);

    // OSM fallback on repeated tile errors
    let osmAdded = false, errCount = 0;
    ignTile.on('tileerror', () => {
      if (!osmAdded && ++errCount > 3) {
        osmAdded = true;
        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
          maxZoom: 19, attribution: '© OpenStreetMap contributors',
        }).addTo(map);
      }
    });

    L.control.zoom({ position: 'bottomright' }).addTo(map);
    L.control.attribution({ position: 'bottomleft', prefix: '© IGN Géoportail' }).addTo(map);

    // Building marker
    if (satLat && satLon) {
      markerRef.current = L.circleMarker([satLat, satLon], {
        radius: 11, color: '#22c55e', fillColor: '#22c55e', fillOpacity: 0.30, weight: 2,
      }).addTo(map);
    }

    leafletMapRef.current = map;
    return () => {
      map.remove();
      leafletMapRef.current = null;
      markerRef.current = null;
    };
  }, []); // eslint-disable-line react-hooks/exhaustive-deps — intentionally runs once

  // Update map position when GPS changes (after satellite analysis)
  _vtbUseE(() => {
    const map = leafletMapRef.current;
    if (!map || !satLat || !satLon) return;
    map.setView([satLat, satLon], 18);
    if (markerRef.current) {
      markerRef.current.setLatLng([satLat, satLon]);
    } else {
      markerRef.current = L.circleMarker([satLat, satLon], {
        radius: 11, color: '#22c55e', fillColor: '#22c55e', fillOpacity: 0.30, weight: 2,
      }).addTo(map);
    }
  }, [satLat, satLon]);

  // ── Trigger satellite analysis (avec cache IndexedDB PWA) ─────────────────
  // P3.1 : on regarde d'abord dans le cache local (lat/lon ou adresse) avant
  // de relancer l'appel BAN/IGN/PVGIS. Cache invalidé après 7 jours.
  async function runSatelliteAnalysis() {
    if (!backend.hasBackend) return;
    setAnalyzing(true);
    setAnalyzeError(null);
    try {
      // 1) Tentative cache IndexedDB
      const cacheKey = (window.AE_PWA_DB && satLat && satLon)
        ? window.AE_PWA_DB.sat.keyForLatLon(satLat, satLon)
        : (v.clientAddress ? `addr:${(v.clientAddress || '').toLowerCase().trim()}` : null);
      let cached = null;
      if (cacheKey && window.AE_PWA_DB) {
        cached = await window.AE_PWA_DB.sat.get(cacheKey);
      }
      if (cached) {
        // Hit : pousse les données vers le backend pour synchroniser et refresh
        try {
          await fetch(`${API}/api/visite/${backend.backendId}`, {
            method: 'PATCH', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ satelliteData: cached }),
          });
        } catch (_) {}
        backend.refresh();
        setAnalyzing(false);
        return;
      }

      // 2) Cache miss → vrai appel backend (BAN → IGN → PVGIS)
      const r = await fetch(`${API}/api/visite/${backend.backendId}/satellite`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ address: v.clientAddress }),
      });
      const data = await r.json();
      if (data.status === 'ok') {
        // 3) Stocke dans le cache local pour les prochaines fois (TTL 7 jours)
        if (cacheKey && window.AE_PWA_DB && data.satelliteData) {
          await window.AE_PWA_DB.sat.set(cacheKey, data.satelliteData);
        }
        backend.refresh();
      } else {
        setAnalyzeError(data.message || 'Erreur analyse satellite');
      }
    } catch (e) {
      setAnalyzeError(e.message);
    }
    setAnalyzing(false);
  }

  // ── Render ────────────────────────────────────────────────────────────────
  return (
    <div>
      {/* ── Trigger bar : pas encore analysé ── */}
      {backend.hasBackend && !hasSatData && (
        <div style={{ marginBottom: 14, padding: '12px 16px', background: 'var(--paper)', border: '1px dashed var(--line-2)', borderRadius: 6, display: 'flex', alignItems: 'center', gap: 12 }}>
          <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="var(--plasma)" strokeWidth="1.5">
            <circle cx="8" cy="8" r="2.5"/>
            <path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.05 3.05l1.42 1.42M11.53 11.53l1.42 1.42M11.53 3.05l-1.42 1.42M3.05 11.53l1.42 1.42"/>
          </svg>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 12, fontWeight: 600 }}>Aucune analyse satellite · BAN + Cadastre IGN + PVGIS</div>
            <div style={{ fontSize: 11, color: 'var(--ink-3)' }}>
              {v.clientAddress ? v.clientAddress : 'Renseignez une adresse pour lancer'}
            </div>
          </div>
          {analyzeError && (
            <span style={{ fontSize: 10, color: 'var(--rouge)', fontFamily: 'var(--font-mono)' }}>{analyzeError}</span>
          )}
          <Btn variant="signal" size="sm" onClick={runSatelliteAnalysis} disabled={analyzing || !v.clientAddress}
            icon={<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6"><circle cx="8" cy="8" r="3"/><path d="M8 1v1.5M8 13.5V15M1 8h1.5M13.5 8H15"/></svg>}>
            {analyzing ? 'Analyse…' : '🛰 Analyser satellite'}
          </Btn>
        </div>
      )}

      {/* ── Status bar : déjà analysé ── */}
      {backend.hasBackend && hasSatData && (
        <div style={{ marginBottom: 14, padding: '8px 14px', background: 'var(--paper)', border: '1px solid var(--line)', borderRadius: 6, display: 'flex', alignItems: 'center', gap: 10, fontSize: 11 }}>
          <Dot tone="signal" size={6}/>
          <span style={{ color: 'var(--ink-3)' }}>
            Analysé {rawSat.analyzedAt && `le ${new Date(rawSat.analyzedAt).toLocaleDateString('fr-FR')}`}
            {commune && <> · <strong>{commune}</strong></>}
            {parcelRef && <> · parcelle <span className="mono">{parcelRef}</span></>}
            {altitude !== null && <> · alt. {Math.round(altitude)} m</>}
          </span>
          <span style={{ flex: 1 }}/>
          {analyzeError && (
            <span style={{ color: 'var(--rouge)', fontFamily: 'var(--font-mono)', fontSize: 10 }}>{analyzeError}</span>
          )}
          <Btn variant="outline" size="sm" onClick={runSatelliteAnalysis} disabled={analyzing}>
            {analyzing ? 'Analyse…' : '↺ Relancer'}
          </Btn>
        </div>
      )}

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 360px', gap: 16 }}>
        {/* ── LEFT ── */}
        <div>
          <div style={{ border: '1px solid var(--line)', borderRadius: 8, background: 'var(--paper)', padding: 16 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
              <div>
                <div style={{ fontSize: 10, color: 'var(--ink-4)', textTransform: 'uppercase', letterSpacing: 0.5, fontWeight: 600 }}>Vue satellite · croisée PVGIS</div>
                <div style={{ fontSize: 14, fontWeight: 600, marginTop: 2 }}>
                  {commune || (v.clientAddress || '').split(',')[0] || 'Localisation non renseignée'}
                </div>
              </div>
              <span style={{ flex: 1 }}/>
              <Badge tone="plasma">IGN Géoportail</Badge>
              {hasSatData
                ? <Badge tone="signal">PVGIS v5.3</Badge>
                : <Badge tone="neutral">Non analysé</Badge>
              }
            </div>

            {/* Map : Leaflet si disponible, RoofMap SVG sinon */}
            {leafletAvail ? (
              <div style={{ position: 'relative', borderRadius: 5, overflow: 'hidden', height: 280, border: '1px solid var(--line)' }}>
                <div ref={mapContainerRef} style={{ height: '100%', width: '100%' }}/>
                <div style={{
                  position: 'absolute', bottom: 4, left: 4, zIndex: 900,
                  background: 'rgba(0,0,0,0.55)', color: '#fff', fontSize: 9,
                  padding: '2px 6px', borderRadius: 2, fontFamily: 'var(--font-mono)',
                  pointerEvents: 'none',
                }}>
                  IGN ORTHOPHOTO
                  {satLat && satLon && ` · ${satLat.toFixed(5)}, ${satLon.toFixed(5)}`}
                </div>
              </div>
            ) : (
              <div style={{ display: 'flex', justifyContent: 'center', padding: 20, background: 'var(--paper-2)', borderRadius: 5 }}>
                <div style={{ transform: 'scale(1.6)', transformOrigin: 'center' }}>
                  <RoofMap width={360} height={220} panels={panels} rows={rows} cols={cols}
                    orientation={orientation} tilt={tiltAngle} highlightPanel={null}/>
                </div>
              </div>
            )}

            {/* Metrics grid */}
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 1, background: 'var(--line)', border: '1px solid var(--line)', borderRadius: 5, marginTop: 14 }}>
              {[
                {
                  label: 'Surface toit',
                  value: rooftopArea ? `${rooftopArea} m²` : '— m²',
                  sub: hasSatData ? 'plan cadastral IGN' : 'non analysé',
                },
                {
                  label: pvgisIrr ? 'Irradiation' : 'Orientation',
                  value: pvgisIrr ? `${Math.round(pvgisIrr)} kWh/m²` : orientation,
                  sub: pvgisIrr ? `pente ~${tiltAngle}°` : `pente ${tiltAngle}°`,
                },
                {
                  label: 'Installation',
                  value: `${kwc} kWc`,
                  sub: `${panels} panneaux · ${panelW}Wc`,
                },
                {
                  label: 'Production',
                  value: `${fmtInt(annualProd)} kWh/an`,
                  sub: hasSatData ? 'PVGIS réel' : 'saisonnière estimée',
                  tone: hasSatData ? 'signal' : null,
                },
              ].map((s, i) => (
                <div key={i} style={{ padding: 12, background: 'var(--paper)' }}>
                  <div style={{ fontSize: 9, color: 'var(--ink-4)', textTransform: 'uppercase', letterSpacing: 0.5, fontWeight: 600 }}>{s.label}</div>
                  <div style={{ fontSize: 15, fontWeight: 600, marginTop: 2, color: s.tone ? 'var(--signal-deep)' : 'var(--ink)' }}>{s.value}</div>
                  <div style={{ fontSize: 10, color: 'var(--ink-4)', marginTop: 2, fontFamily: 'var(--font-mono)' }}>{s.sub}</div>
                </div>
              ))}
            </div>

            {/* Calpinage controls */}
            <div style={{ marginTop: 14, padding: 12, background: 'var(--paper-2)', borderRadius: 5, border: '1px solid var(--hairline)' }}>
              <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)', marginBottom: 10 }}>
                Calpinage interactif · {calp.panelModel || 'Panneau 400 Wc'}
              </div>
              <div style={{ display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap' }}>
                <div>
                  <div style={{ fontSize: 10, color: 'var(--ink-4)', marginBottom: 4 }}>Rangées</div>
                  <div style={{ display: 'flex', gap: 2 }}>
                    {[1, 2, 3, 4].map(r => (
                      <button key={r} onClick={() => setRows(r)} style={{
                        width: 32, height: 28, fontSize: 12, fontWeight: 600, cursor: 'pointer',
                        background: rows === r ? 'var(--ink)' : 'var(--paper)',
                        color: rows === r ? 'var(--paper)' : 'var(--ink-2)',
                        border: '1px solid var(--line-2)', borderRadius: 4,
                      }}>{r}</button>
                    ))}
                  </div>
                </div>
                <div>
                  <div style={{ fontSize: 10, color: 'var(--ink-4)', marginBottom: 4 }}>Colonnes</div>
                  <div style={{ display: 'flex', gap: 2 }}>
                    {[6, 8, 10, 12, 16].map(c => (
                      <button key={c} onClick={() => setCols(c)} style={{
                        width: 32, height: 28, fontSize: 12, fontWeight: 600, cursor: 'pointer',
                        background: cols === c ? 'var(--ink)' : 'var(--paper)',
                        color: cols === c ? 'var(--paper)' : 'var(--ink-2)',
                        border: '1px solid var(--line-2)', borderRadius: 4,
                      }}>{c}</button>
                    ))}
                  </div>
                </div>
                {rooftopArea && (
                  <div style={{ fontSize: 10, color: 'var(--ink-4)', fontFamily: 'var(--font-mono)' }}>
                    Dispo ≈ {Math.round(rooftopArea * 0.7)} m²<br/>
                    Max ~{Math.floor(rooftopArea * 0.7 / 1.76)} panneaux
                  </div>
                )}
                <div style={{ flex: 1 }}/>
                <div style={{ textAlign: 'right' }}>
                  <div className="mono" style={{ fontSize: 11, color: 'var(--ink-4)' }}>→ {panels} panneaux = {kwc} kWc</div>
                  <div className="mono" style={{ fontSize: 11, color: 'var(--signal-deep)', fontWeight: 600 }}>{fmtInt(annualProd)} kWh/an</div>
                </div>
              </div>
            </div>
          </div>

          {/* PVGIS monthly chart */}
          <div style={{ marginTop: 16, border: '1px solid var(--line)', borderRadius: 8, background: 'var(--paper)', padding: 16 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>
              <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)' }}>Production mensuelle estimée · kWh</div>
              {hasSatData
                ? <Badge tone="plasma">PVGIS</Badge>
                : <Badge tone="neutral">Profil saisonnier</Badge>
              }
            </div>
            <div style={{ display: 'flex', alignItems: 'flex-end', gap: 6, height: 140 }}>
              {pvgisMonthly.map((p, i) => {
                const maxV = Math.max(...pvgisMonthly);
                const h = (p / maxV) * 120;
                return (
                  <div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
                    <span className="mono" style={{ fontSize: 9, color: 'var(--ink-4)' }}>{p}</span>
                    <div style={{ width: '100%', height: h, background: 'linear-gradient(to top, var(--signal-deep), var(--signal))', borderRadius: '2px 2px 0 0' }}/>
                    <span className="mono" style={{ fontSize: 10, color: 'var(--ink-3)', fontWeight: 600 }}>{months[i]}</span>
                  </div>
                );
              })}
            </div>
          </div>
        </div>

        {/* ── RIGHT ── */}
        <div>
          {/* Bilan énergie */}
          <div style={{ border: '1px solid var(--line)', borderRadius: 8, background: 'var(--paper)', padding: 14, marginBottom: 14 }}>
            <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)', marginBottom: 10 }}>Bilan énergie</div>
            <Row label="Conso actuelle (estim.)" value={`${fmtInt(8400)} kWh/an`}/>
            <Row label="Production PV" value={`${fmtInt(annualProd)} kWh/an`} tone="signal"/>
            <Row label="Autoconsommation" value={`${fmtInt(Math.round(annualProd * selfCons))} kWh`} sub={`${Math.round(selfCons * 100)}%`}/>
            <Row label="Injection surplus" value={`${fmtInt(Math.round(annualProd * (1 - selfCons)))} kWh`}/>
            <div style={{ paddingTop: 10, marginTop: 10, borderTop: '1px solid var(--hairline)' }}>
              <Row label="Gain annuel estimé"
                value={fmtEur(annualProd * 0.24 * selfCons + annualProd * 0.10 * (1 - selfCons))}
                tone="signal" big/>
            </div>
          </div>

          {/* Données brutes PVGIS (backend) */}
          {hasSatData && (
            <div style={{ border: '1px solid var(--line)', borderRadius: 8, background: 'var(--paper)', padding: 14, marginBottom: 14 }}>
              <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)', marginBottom: 10 }}>Données brutes · BAN + Cadastre + PVGIS</div>
              <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--ink-2)', background: 'var(--paper-2)', padding: 10, borderRadius: 4, lineHeight: 1.9 }}>
                {rawSat?.geocode?.label && <div>BAN : <span style={{ color: 'var(--ink-3)' }}>{rawSat.geocode.label}</span></div>}
                {parcelRef && <div>Parcelle : <strong>{parcelRef}</strong>{parcelSurface && ` · ${parcelSurface} m²`}</div>}
                {rooftopArea && <div>Toiture estimée : <strong>{rooftopArea} m²</strong></div>}
                {altitude !== null && <div>Altitude IGN : <strong>{Math.round(altitude)} m</strong></div>}
                {pvgisKwc && <div>Peakpower PVGIS : <strong>{pvgisKwc} kWc</strong></div>}
                {pvgisAnnual && <div>Production : <strong>{Math.round(pvgisAnnual)} kWh/an</strong></div>}
                {pvgisIrr && <div>Irradiation : <strong>{Math.round(pvgisIrr)} kWh/m²/an</strong></div>}
              </div>
            </div>
          )}

          {/* Ombrages */}
          <div style={{ border: '1px solid var(--line)', borderRadius: 8, background: 'var(--paper)', padding: 14, marginBottom: 14 }}>
            <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)', marginBottom: 10 }}>Ombrages · analyse 3D</div>
            <div style={{ padding: 10, background: 'var(--paper-2)', borderRadius: 5 }}>
              {[
                { h: '9h',  pct: 12 },
                { h: '12h', pct:  3 },
                { h: '15h', pct:  6 },
                { h: '18h', pct: 35 },
              ].map(s => (
                <div key={s.h} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '4px 0' }}>
                  <span className="mono" style={{ fontSize: 11, width: 30, color: 'var(--ink-3)' }}>{s.h}</span>
                  <div style={{ flex: 1, height: 8, background: 'var(--line)', borderRadius: 2, overflow: 'hidden' }}>
                    <div style={{ width: `${s.pct}%`, height: '100%', background: s.pct > 25 ? 'var(--rouge)' : s.pct > 10 ? 'var(--amber)' : 'var(--signal)' }}/>
                  </div>
                  <span className="mono" style={{ fontSize: 10, color: 'var(--ink-4)', width: 30, textAlign: 'right' }}>{s.pct}%</span>
                </div>
              ))}
            </div>
            <div style={{ marginTop: 8, fontSize: 11, color: 'var(--ink-3)', lineHeight: 1.5 }}>
              Ombrage moyen sur production annuelle : {Math.round(shadowFactor * 100)} % — source principale : cheminée voisine en fin d'après-midi.
            </div>
          </div>

          {/* Synthèse + CTA Studio solaire */}
          <div style={{ border: '1px solid var(--line)', borderRadius: 8, background: 'var(--signal-tint)', padding: 14 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
              <svg width="14" height="14" viewBox="0 0 16 16" stroke="var(--signal-deep)" strokeWidth="1.6" fill="none">
                <circle cx="8" cy="8" r="3"/>
                <path d="M8 1v1.5M8 13.5V15M1 8h1.5M13.5 8H15"/>
              </svg>
              <span style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--signal-deep)' }}>Synthèse studio solaire</span>
            </div>
            <p style={{ fontSize: 12, color: 'var(--ink-2)', lineHeight: 1.55, margin: 0 }}>
              {hasSatData
                ? <>Toiture analysée : <strong>{rooftopArea || '—'} m²</strong> · potentiel <strong>{pvgisKwc || kwc} kWc</strong> · <strong>{pvgisAnnual ? fmtInt(Math.round(pvgisAnnual)) : fmtInt(annualProd)} kWh/an</strong> via PVGIS. Irradiation {pvgisIrr ? <strong>{Math.round(pvgisIrr)} kWh/m²/an</strong> : 'à calculer'}. Éligible <strong>BAR-EN-101</strong>.</>
                : <>Toiture optimale pour installation <strong>{kwc} kWc</strong>. Orientation {orientation}, pente idéale, ombrage limité. Autoconsommation <strong>{Math.round(selfCons * 100)} %</strong> viable. Éligible <strong>BAR-TH-137</strong>.</>
              }
            </p>
            <Btn variant="signal" size="sm" icon={<Icon.sun/>} style={{ marginTop: 10 }} iconRight={<Icon.arrow/>}>
              Ouvrir dans Studio solaire
            </Btn>
          </div>
        </div>
      </div>
    </div>
  );
}

function Row({ label, value, sub, tone, big }) {
  return (
    <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, padding: '4px 0' }}>
      <span style={{ fontSize: 11, color: 'var(--ink-4)', flex: 1 }}>{label}</span>
      <span style={{ fontSize: big ? 17 : 13, fontWeight: 600, color: tone ? `var(--${tone}-deep)` : 'var(--ink)' }}>{value}</span>
      {sub && <span className="mono" style={{ fontSize: 10, color: 'var(--ink-4)' }}>· {sub}</span>}
    </div>
  );
}

// ─── Report Tab — câblé backend ──────────────────────────────
// Dual mode : iframe /report/preview (backend) → A4 mock statique (demo)
// POST /report → génère PDF · GET /report → download · POST /crm/push → Odoo
// Interconnexion : POST /api/signatures (si rapport + contact) + Studio Solaire
function ReportTab({ v, bk }) {
  const backend = bk || ((typeof window !== 'undefined' && window.useBackendVisite)
    ? window.useBackendVisite(v)
    : { hasBackend: false, full: null, reports: [], equipments: [], satelliteData: null, backendId: null, refresh: () => {} });
  const API = (window.AE_API && window.AE_API.BASE) || '';

  // Demo mode state
  const [previewTab, setPreviewTab] = _vtbUseS('cover');

  // Backend actions state
  const [reportFormat, setReportFormat]     = _vtbUseS('pdf');
  const [generating, setGenerating]         = _vtbUseS(false);
  const [genError, setGenError]             = _vtbUseS(null);
  const [pushing, setPushing]               = _vtbUseS(false);
  const [pushResult, setPushResult]         = _vtbUseS(null);
  const [pushError, setPushError]           = _vtbUseS(null);
  const [sigRequesting, setSigRequesting]   = _vtbUseS(false);
  const [sigResult, setSigResult]           = _vtbUseS(null);
  const [archiving, setArchiving]           = _vtbUseS(false);
  const [archiveResult, setArchiveResult]   = _vtbUseS(null);
  const [archiveError, setArchiveError]     = _vtbUseS(null);
  const [sections, setSections]             = _vtbUseS({
    cover: true, summary: true, equipments: true, satellite: true,
    recommendations: true, transcript: false, photos: true, rgpd: true,
  });

  // Derived from backend
  const latestReport       = (backend.reports || [])[0] || null;
  const reportGeneratedAt  = backend.full?.reportGeneratedAt || latestReport?.createdAt || null;
  const reportUrl          = backend.full?.reportUrl || null;
  const crmSynced          = backend.full?.crmSynced || false;
  const crmSyncedAt        = backend.full?.crmSyncedAt || null;
  const contactId          = backend.full?.contactId || null;
  const hasReport          = !!(reportGeneratedAt || latestReport);
  const hasSatData         = !!(backend.satelliteData?.pvgis?.annualProduction_kWh);
  const validatedEquipments = _vtbUseM(
    () => (backend.equipments || []).filter(e => e.validated),
    [backend.equipments]
  );

  // ── Actions ──────────────────────────────────────────────────────────────

  async function generateReport() {
    if (!backend.hasBackend) return;
    setGenerating(true);
    setGenError(null);
    try {
      const r = await fetch(`${API}/api/visite/${backend.backendId}/report`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ format: reportFormat, type: 'full' }),
      });
      const data = await r.json();
      if (data.status === 'ok') {
        backend.refresh();
      } else {
        setGenError(data.message || 'Erreur génération rapport');
      }
    } catch (e) {
      setGenError(e.message);
    }
    setGenerating(false);
  }

  async function pushToCrm() {
    if (!backend.hasBackend) return;
    setPushing(true);
    setPushError(null);
    try {
      const r = await fetch(`${API}/api/visite/${backend.backendId}/crm/push`, { method: 'POST' });
      const data = await r.json();
      if (data.status === 'ok') {
        setPushResult(data.result);
        backend.refresh();
      } else {
        setPushError(data.message || 'Erreur push CRM');
      }
    } catch (e) {
      setPushError(e.message);
    }
    setPushing(false);
  }

  // Interconnexion modules/signatures — signature électronique native
  async function requestSignature() {
    if (!backend.hasBackend || !hasReport) return;
    setSigRequesting(true);
    try {
      const r = await fetch(`${API}/api/signatures`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          title: `Rapport visite ${v.ref}`,
          documentUrl: reportUrl || `${API}/api/visite/${backend.backendId}/report`,
          relatedEntity: 'TechVisit',
          relatedEntityId: backend.backendId,
          contactId,
          requestedBy: backend.full?.technicianEmail || v.technicianEmail || '',
          signers: contactId ? [{ contactId, role: 'client' }] : [],
          metadata: { visitRef: v.ref, type: 'rapport_visite' },
        }),
      });
      const data = await r.json();
      if (data.status === 'ok' || data.id || data.request) {
        setSigResult(data.request || data);
      }
    } catch (_) {}
    setSigRequesting(false);
  }

  // Archivage dans la GED org-admin — POST /api/org/documents/upload (base64)
  async function archiveToGED() {
    if (!backend.hasBackend || !hasReport) return;
    setArchiving(true);
    setArchiveError(null);
    try {
      const reportResp = await fetch(`${API}/api/visite/${backend.backendId}/report`);
      if (!reportResp.ok) throw new Error(`HTTP ${reportResp.status} sur /report`);
      const blob = await reportResp.blob();
      const reader = new FileReader();
      const b64 = await new Promise((resolve, reject) => {
        reader.onloadend = () => resolve(String(reader.result).split(',')[1] || '');
        reader.onerror = () => reject(new Error('FileReader erreur'));
        reader.readAsDataURL(blob);
      });
      const r = await fetch(`${API}/api/org/documents/upload`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          filename: `rapport-visite-${v.ref}.${latestReport?.format || 'pdf'}`,
          dataBase64: b64,
          mime: blob.type || 'application/pdf',
          size: blob.size,
          title: `Rapport visite ${v.ref}`,
          description: `Rapport technique de la visite ${v.ref} — ${backend.full?.clientAddress || ''}`,
          category: 'rapport_visite',
          tags: ['visite', 'rapport', v.ref],
          linkedEntity: { kind: 'visite', id: backend.backendId },
        }),
      });
      const data = await r.json();
      if (r.ok && (data.status === 'ok' || data.id || data.document)) {
        setArchiveResult(data.document || data);
      } else {
        setArchiveError(data.message || data.error || `HTTP ${r.status}`);
      }
    } catch (e) {
      setArchiveError(e.message);
    }
    setArchiving(false);
  }

  // ── Render ────────────────────────────────────────────────────────────────
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1fr 360px', gap: 16 }}>

      {/* ── LEFT : preview ── */}
      <div style={{ border: '1px solid var(--line)', borderRadius: 8, background: 'var(--paper-2)', padding: 28, display: 'flex', flexDirection: 'column', alignItems: 'center', minHeight: 500 }}>
        {backend.hasBackend ? (
          /* Backend mode — iframe aperçu HTML live */
          <>
            <div style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
              <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)' }}>Aperçu rapport · live</div>
              <span style={{ flex: 1 }}/>
              {hasReport && (
                <Badge tone="signal">
                  Généré {new Date(reportGeneratedAt).toLocaleDateString('fr-FR')}
                </Badge>
              )}
              {!hasReport && <Badge tone="neutral">Pas encore généré</Badge>}
            </div>
            <iframe
              key={backend.backendId}
              src={`${API}/api/visite/${backend.backendId}/report/preview`}
              style={{
                width: '100%', minHeight: 580, border: 'none',
                borderRadius: 5, background: '#fff', boxShadow: 'var(--shadow-2)',
              }}
              title={`Rapport ${v.ref}`}
            />
          </>
        ) : (
          /* Demo mode — A4 mock statique */
          <>
            <div style={{ width: 440, minHeight: 623, background: 'white', boxShadow: 'var(--shadow-3)', padding: 34, border: '1px solid var(--line)', fontFamily: 'var(--font-serif, Georgia, serif)', color: '#111', position: 'relative' }}>
              {previewTab === 'cover'       && <ReportCover v={v}/>}
              {previewTab === 'summary'     && <ReportSummary v={v}/>}
              {previewTab === 'equipments'  && <ReportEquipList v={v}/>}
              {previewTab === 'reco'        && <ReportRecoPage v={v}/>}
              <div style={{ position: 'absolute', bottom: 12, left: 34, right: 34, fontSize: 8, color: '#666', display: 'flex', borderTop: '0.5px solid #ccc', paddingTop: 6, fontFamily: 'monospace' }}>
                <span>Audits-Énergies · {v.ref}</span>
                <span style={{ flex: 1 }}/>
                <span>Page {['cover','summary','equipments','reco'].indexOf(previewTab) + 1} / 4</span>
              </div>
            </div>
            <div style={{ display: 'flex', gap: 2, padding: 4, background: 'var(--paper)', border: '1px solid var(--line)', borderRadius: 5, marginTop: 14 }}>
              {[
                { k: 'cover',       l: 'Couverture' },
                { k: 'summary',     l: 'Synthèse' },
                { k: 'equipments',  l: 'Équipements' },
                { k: 'reco',        l: 'Reco CEE' },
              ].map(t => (
                <button key={t.k} onClick={() => setPreviewTab(t.k)} style={{
                  padding: '5px 10px', fontSize: 11, fontWeight: 500,
                  background: previewTab === t.k ? 'var(--ink)' : 'transparent',
                  color: previewTab === t.k ? 'var(--paper)' : 'var(--ink-4)',
                  borderRadius: 4,
                }}>{t.l}</button>
              ))}
            </div>
          </>
        )}
      </div>

      {/* ── RIGHT : actions ── */}
      <div>

        {/* Rapport — génération + téléchargement */}
        <div style={{ border: '1px solid var(--line)', borderRadius: 8, background: 'var(--paper)', padding: 14, marginBottom: 14 }}>
          <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)', marginBottom: 10 }}>Rapport</div>
          <div style={{ fontSize: 12, color: 'var(--ink-3)', marginBottom: 10 }}>
            Modèle <strong>Rapport visite technique v3.1</strong>
            {backend.hasBackend && (
              hasReport
                ? <> · <span style={{ color: 'var(--signal-deep)', fontWeight: 600 }}>généré ✓</span></>
                : <> · <span style={{ color: 'var(--amber)' }}>à générer</span></>
            )}
          </div>

          {/* Format picker (backend + pas de rapport) */}
          {backend.hasBackend && !hasReport && (
            <div style={{ marginBottom: 10 }}>
              <div style={{ fontSize: 10, color: 'var(--ink-4)', marginBottom: 5 }}>Format</div>
              <div style={{ display: 'flex', gap: 3 }}>
                {['pdf', 'html', 'json'].map(f => (
                  <button key={f} onClick={() => setReportFormat(f)} style={{
                    padding: '5px 10px', fontSize: 11, fontWeight: 600, cursor: 'pointer',
                    background: reportFormat === f ? 'var(--ink)' : 'var(--paper)',
                    color:      reportFormat === f ? 'var(--paper)' : 'var(--ink-2)',
                    border: '1px solid var(--line-2)', borderRadius: 4,
                  }}>{f.toUpperCase()}</button>
                ))}
              </div>
            </div>
          )}

          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
            {backend.hasBackend ? (
              <>
                {hasReport ? (
                  <Btn variant="signal" size="sm" icon={<Icon.download/>}
                    onClick={() => window.open(`${API}/api/visite/${backend.backendId}/report`, '_blank')}>
                    Télécharger {(latestReport?.format || 'PDF').toUpperCase()}
                  </Btn>
                ) : (
                  <Btn variant="signal" size="sm" icon={<Icon.doc/>}
                    onClick={generateReport} disabled={generating}>
                    {generating ? 'Génération…' : `Générer rapport ${reportFormat.toUpperCase()}`}
                  </Btn>
                )}
                {hasReport && (
                  <Btn variant="outline" size="sm" icon={<Icon.doc/>}
                    onClick={generateReport} disabled={generating}>
                    {generating ? 'Régénération…' : 'Régénérer'}
                  </Btn>
                )}
                {genError && (
                  <div style={{ fontSize: 10, color: 'var(--rouge)', padding: '6px 8px', background: 'var(--rouge-tint,#fef2f2)', borderRadius: 4, fontFamily: 'var(--font-mono)' }}>
                    {genError}
                  </div>
                )}
              </>
            ) : (
              <>
                <Btn variant="signal" size="sm" icon={<Icon.download/>}>Télécharger PDF</Btn>
                <Btn variant="outline" size="sm" icon={<Icon.doc/>}>Exporter Word</Btn>
              </>
            )}
            <Btn variant="ghost" size="sm">Éditer modèle</Btn>
          </div>

          {backend.hasBackend && hasReport && (
            <div style={{ marginTop: 10, padding: 8, background: 'var(--paper-2)', fontSize: 10, color: 'var(--ink-4)', borderRadius: 4, fontFamily: 'var(--font-mono)' }}>
              {new Date(reportGeneratedAt).toLocaleString('fr-FR')}
              {latestReport?.storageKey && <><br/>{latestReport.storageKey.split('/').pop()}</>}
            </div>
          )}
          {!backend.hasBackend && (
            <div style={{ marginTop: 10, padding: 8, background: 'var(--paper-2)', fontSize: 10, color: 'var(--ink-4)', borderRadius: 4, fontFamily: 'var(--font-mono)' }}>
              sha256: f82a1c9b…4e7d · généré automatiquement 14 min après la clôture
            </div>
          )}
        </div>

        {/* Sections */}
        <div style={{ border: '1px solid var(--line)', borderRadius: 8, background: 'var(--paper)', padding: 14, marginBottom: 14 }}>
          <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)', marginBottom: 10 }}>Sections incluses</div>
          {[
            { k: 'cover',           l: 'Couverture + identification' },
            { k: 'summary',         l: 'Synthèse IA + constats' },
            { k: 'equipments',      l: `Équipements${backend.hasBackend && validatedEquipments.length ? ` (${validatedEquipments.length} validés)` : ' + OCR'}` },
            { k: 'satellite',       l: `Schéma toiture${hasSatData ? ' + PVGIS' : ' + calpinage'}` },
            { k: 'recommendations', l: 'Recommandations CEE' },
            { k: 'transcript',      l: 'Transcription intégrale' },
            { k: 'photos',          l: 'Photos annotées HD' },
            { k: 'rgpd',            l: 'Consentement RGPD' },
          ].map(s => (
            <label key={s.k} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0', cursor: 'pointer' }}>
              <input
                type="checkbox"
                checked={sections[s.k] !== undefined ? sections[s.k] : true}
                onChange={e => setSections(prev => ({ ...prev, [s.k]: e.target.checked }))}
                style={{ accentColor: 'var(--signal-deep)' }}
              />
              <span style={{ fontSize: 12, color: 'var(--ink-2)' }}>{s.l}</span>
            </label>
          ))}
        </div>

        {/* CRM Push */}
        <div style={{ border: '1px solid var(--line)', borderRadius: 8, background: 'var(--paper)', padding: 14, marginBottom: 14 }}>
          <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)', marginBottom: 10 }}>
            Sync CRM · Odoo
          </div>
          {crmSynced ? (
            <div style={{ padding: '8px 10px', background: 'var(--signal-tint)', borderRadius: 5, display: 'flex', alignItems: 'center', gap: 8 }}>
              <Icon.check style={{ color: 'var(--signal-deep)', flexShrink: 0 }}/>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--signal-deep)' }}>Synchronisé</div>
                {crmSyncedAt && (
                  <div style={{ fontSize: 10, color: 'var(--ink-3)', fontFamily: 'var(--font-mono)' }}>
                    {new Date(crmSyncedAt).toLocaleString('fr-FR')}
                  </div>
                )}
              </div>
              <Btn variant="outline" size="sm" onClick={pushToCrm} disabled={pushing}>↺ Re-sync</Btn>
            </div>
          ) : (
            <>
              <div style={{ fontSize: 12, color: 'var(--ink-3)', marginBottom: 8, lineHeight: 1.5 }}>
                Met à jour l'opportunité Odoo, attache le rapport, crée la note technique.
              </div>
              <Btn variant="signal" size="sm" icon={<Icon.refresh/>}
                onClick={pushToCrm}
                disabled={pushing || !backend.hasBackend}>
                {pushing ? 'Push en cours…' : 'Injecter CRM Odoo'}
              </Btn>
              {pushResult && (
                <div style={{ marginTop: 8, fontSize: 10, color: 'var(--signal-deep)', padding: '6px 8px', background: 'var(--signal-tint)', borderRadius: 4, fontFamily: 'var(--font-mono)', wordBreak: 'break-all' }}>
                  ✓ {typeof pushResult === 'string' ? pushResult : JSON.stringify(pushResult).slice(0, 120)}
                </div>
              )}
              {pushError && (
                <div style={{ marginTop: 6, fontSize: 10, color: 'var(--rouge)', fontFamily: 'var(--font-mono)' }}>{pushError}</div>
              )}
            </>
          )}
        </div>

        {/* ── Interconnexion signatures (rapport prêt + contact connu) ── */}
        {(backend.hasBackend && hasReport && contactId) && (
          <div style={{ border: '1px solid var(--line)', borderRadius: 8, background: 'var(--paper)', padding: 14, marginBottom: 14 }}>
            <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)', marginBottom: 8 }}>
              Signature native ↗ <span style={{ color: 'var(--plasma)', fontWeight: 400, fontSize: 10 }}>signatures</span>
            </div>
            {sigResult ? (
              <div style={{ padding: '8px 10px', background: 'var(--signal-tint)', borderRadius: 5, display: 'flex', alignItems: 'center', gap: 8, fontSize: 12 }}>
                <Icon.check style={{ color: 'var(--signal-deep)' }}/>
                <div>
                  <div style={{ fontWeight: 600, color: 'var(--signal-deep)' }}>Demande envoyée</div>
                  {sigResult.ref && (
                    <div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--ink-3)' }}>Réf : {sigResult.ref}</div>
                  )}
                </div>
              </div>
            ) : (
              <>
                <div style={{ fontSize: 12, color: 'var(--ink-3)', marginBottom: 8, lineHeight: 1.5 }}>
                  Envoyer le rapport au bénéficiaire pour signature électronique native (sans DocuSign).
                </div>
                <Btn variant="outline" size="sm" icon={<Icon.sign/>}
                  onClick={requestSignature} disabled={sigRequesting}>
                  {sigRequesting ? 'Envoi…' : 'Demander signature client'}
                </Btn>
              </>
            )}
          </div>
        )}

        {/* ── Interconnexion Études Régl. 3CL-DPE (si surface toiture + satellite) ── */}
        {(backend.hasBackend && hasSatData && backend.satelliteData?.rooftop?.estimatedRooftopArea) && (
          <div style={{ border: '1px solid var(--line)', borderRadius: 8, background: 'var(--paper)', padding: 14, marginBottom: 14 }}>
            <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)', marginBottom: 8 }}>
              Étude 3CL-DPE ↗ <span style={{ color: 'var(--copper)', fontWeight: 400, fontSize: 10 }}>thermique réglementaire</span>
            </div>
            <div style={{ fontSize: 12, color: 'var(--ink-3)', marginBottom: 8, lineHeight: 1.5 }}>
              Surface toiture {backend.satelliteData.rooftop.estimatedRooftopArea} m² détectée — poursuivre avec une étude 3CL-DPE (maison individuelle) ou audit tertiaire OPQIBI.
            </div>
            <Btn variant="outline" size="sm" icon={<Icon.doc/>} iconRight={<Icon.arrow/>}
              onClick={() => {
                if (typeof window.AE_NAV === 'function') {
                  window.AE_NAV('etudes-reg', {
                    sourceVisiteId: backend.backendId,
                    visitRef: v.ref,
                    contactId,
                    clientAddress: backend.full?.clientAddress,
                    prefill: {
                      rooftopArea: backend.satelliteData.rooftop.estimatedRooftopArea,
                      lat: backend.full?.lat,
                      lon: backend.full?.lon,
                      commune: backend.satelliteData.rooftop.commune,
                      altitude: backend.satelliteData.altitude,
                    },
                  });
                }
              }}>
              Poursuivre en étude 3CL-DPE
            </Btn>
          </div>
        )}

        {/* ── Interconnexion GED org-admin (si rapport généré) ── */}
        {(backend.hasBackend && hasReport) && (
          <div style={{ border: '1px solid var(--line)', borderRadius: 8, background: 'var(--paper)', padding: 14, marginBottom: 14 }}>
            <div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--ink-3)', marginBottom: 8 }}>
              Archiver dans la GED ↗ <span style={{ color: 'var(--plasma)', fontWeight: 400, fontSize: 10 }}>org-admin</span>
            </div>
            {archiveResult ? (
              <div style={{ padding: '8px 10px', background: 'var(--signal-tint)', borderRadius: 5, fontSize: 12, color: 'var(--signal-deep)', display: 'flex', alignItems: 'center', gap: 8 }}>
                <Icon.check/>
                <div>
                  <div style={{ fontWeight: 600 }}>Archivé dans Mon organisation</div>
                  {archiveResult.id && (
                    <div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--ink-3)' }}>doc #{archiveResult.id}</div>
                  )}
                </div>
              </div>
            ) : (
              <>
                <div style={{ fontSize: 12, color: 'var(--ink-3)', marginBottom: 8, lineHeight: 1.5 }}>
                  Classer le rapport PDF dans la GED interne — accessible depuis « Mon organisation » (tag visite, lié à l'entité).
                </div>
                <Btn variant="outline" size="sm" icon={<Icon.folder/>}
                  onClick={archiveToGED} disabled={archiving}>
                  {archiving ? 'Archivage…' : 'Archiver le rapport'}
                </Btn>
                {archiveError && (
                  <div style={{ marginTop: 6, fontSize: 10, color: 'var(--rouge)', fontFamily: 'var(--font-mono)' }}>{archiveError}</div>
                )}
              </>
            )}
          </div>
        )}

        {/* ── Interconnexion Studio Solaire (si données satellite analysées) ── */}
        {hasSatData && (
          <div style={{ border: '1px solid var(--line)', borderRadius: 8, background: 'var(--signal-tint)', padding: 14 }}>
            <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5, color: 'var(--signal-deep)', marginBottom: 6 }}>
              Studio Solaire ↗
            </div>
            <div style={{ fontSize: 12, color: 'var(--ink-2)', marginBottom: 8, lineHeight: 1.5 }}>
              Données PVGIS disponibles — ouvrir dans le Studio Solaire pour affiner le calpinage et générer le devis PV.
            </div>
            <Btn variant="signal" size="sm" icon={<Icon.sun/>} iconRight={<Icon.arrow/>}
              onClick={() => {
                if (typeof window.AE_NAV === 'function') {
                  window.AE_NAV('solar', {
                    sourceVisiteId: backend.backendId,
                    visitRef: v.ref,
                    contactId,
                    clientAddress: backend.full?.clientAddress,
                    prefill: {
                      lat: backend.full?.lat,
                      lon: backend.full?.lon,
                      rooftopArea: backend.satelliteData?.rooftop?.estimatedRooftopArea,
                      annualProduction_kWh: backend.satelliteData?.pvgis?.annualProduction_kWh,
                      peakpower_kWc: backend.satelliteData?.pvgis?.peakpower_kWc,
                    },
                  });
                }
              }}>
              Ouvrir dans Studio solaire
            </Btn>
          </div>
        )}
      </div>
    </div>
  );
}

function ReportCover({ v }) {
  return (
    <div>
      <div style={{ fontSize: 9, color: '#666', letterSpacing: 2, fontFamily: 'monospace' }}>AUDITS-ÉNERGIES</div>
      <div style={{ fontSize: 8, color: '#999', fontFamily: 'monospace', marginTop: 2 }}>{v.ref} — rapport de visite technique</div>
      <div style={{ height: 1, background: '#111', margin: '16px 0' }}/>
      <div style={{ fontSize: 28, fontWeight: 400, lineHeight: 1.1, marginBottom: 4 }}>Visite technique</div>
      <div style={{ fontSize: 22, fontWeight: 400, lineHeight: 1.1, fontStyle: 'italic', color: '#444' }}>chez {v.client}</div>
      <div style={{ height: 30 }}/>
      <div style={{ fontSize: 10, color: '#333', lineHeight: 1.7 }}>
        <div><strong>Adresse</strong> — {v.clientAddress}</div>
        <div><strong>Zone climatique</strong> — {v.zone}</div>
        <div><strong>Date de visite</strong> — {new Date(v.startedAt).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })}</div>
        <div><strong>Technicien</strong> — {v.technicianName} (camion {v.truck})</div>
        <div><strong>Durée</strong> — {formatDuration(v.durationSeconds)}</div>
        <div><strong>Dossier rattaché</strong> — {v.dossierRef}</div>
      </div>
      <div style={{ height: 30 }}/>
      <div style={{ padding: 12, background: '#f5f3ee', fontSize: 9, color: '#444', fontFamily: 'monospace', borderLeft: '2px solid #0d3b2c' }}>
        Rapport généré automatiquement à partir de l'analyse vidéo + audio. Les données techniques sont issues de la lecture de plaques (OCR Claude Vision) et de la détection d'objets. Toute décision finale relève de l'expertise du technicien signataire.
      </div>
      <div style={{ position: 'absolute', bottom: 40, right: 34, width: 60, height: 60 }}>
        <Sigil seed={v.ref} size={60}/>
      </div>
    </div>
  );
}

function ReportSummary({ v }) {
  return (
    <div>
      <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 10, color: '#0d3b2c' }}>Synthèse</div>
      <p style={{ fontSize: 10, lineHeight: 1.7, color: '#111', marginBottom: 14 }}>{v.aiSummary}</p>
      <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 8, marginTop: 14 }}>Potentiel CEE</div>
      <div style={{ border: '0.5px solid #ccc' }}>
        {v.recommendations.map((r, i) => (
          <div key={r.fostCode} style={{ display: 'flex', padding: 6, fontSize: 9, borderBottom: i < v.recommendations.length - 1 ? '0.5px solid #eee' : 'none' }}>
            <span style={{ fontFamily: 'monospace', width: 60, color: '#555' }}>{r.fostCode}</span>
            <span style={{ flex: 1 }}>{r.label}</span>
            <span style={{ fontWeight: 600, fontFamily: 'monospace' }}>{fmtMWh(r.kwhCumac)}</span>
          </div>
        ))}
      </div>
      <div style={{ height: 16 }}/>
      <div style={{ fontSize: 12, fontWeight: 600, marginBottom: 8 }}>Consentement RGPD</div>
      <p style={{ fontSize: 9, color: '#333', lineHeight: 1.6 }}>
        Le bénéficiaire a donné son accord explicite pour l'enregistrement audio et vidéo à {new Date(v.consentTimestamp).toLocaleTimeString('fr-FR')}, avant le début de la captation. Les données sont conservées conformément au RGPD et aux obligations PNCEE.
      </p>
    </div>
  );
}

function ReportEquipList({ v }) {
  return (
    <div>
      <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 10, color: '#0d3b2c' }}>Équipements détectés</div>
      {VT_HERO_EQUIPMENTS.filter(e => e.validated).map((e, i) => (
        <div key={e.id} style={{ display: 'flex', gap: 10, padding: 8, background: i % 2 ? '#f9f8f4' : 'transparent', fontSize: 9 }}>
          <div style={{ width: 40, height: 40, background: '#0d3b2c', color: '#7fffb7', display: 'grid', placeItems: 'center', flexShrink: 0 }}>
            <EquipIcon cat={e.category} size={20}/>
          </div>
          <div style={{ flex: 1 }}>
            <div style={{ fontWeight: 700 }}>{e.brand} {e.model}</div>
            <div style={{ fontSize: 8, color: '#555' }}>{(VT_CATEGORIES[e.category] || {}).label}</div>
            <div style={{ display: 'flex', gap: 10, marginTop: 3, fontSize: 8, fontFamily: 'monospace' }}>
              {e.serialNumber && <span>S/N {e.serialNumber}</span>}
              {e.power_kw && <span>{e.power_kw} kW</span>}
              {e.yearEstimated && <span>{e.yearEstimated}</span>}
              {e.energyClass && <span>classe {e.energyClass}</span>}
            </div>
          </div>
        </div>
      ))}
    </div>
  );
}

function ReportRecoPage({ v }) {
  return (
    <div>
      <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 14, color: '#0d3b2c' }}>Recommandations prioritaires</div>
      {v.recommendations.map((r, i) => (
        <div key={r.fostCode} style={{ marginBottom: 12, padding: 10, border: '0.5px solid #111' }}>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
            <span style={{ fontSize: 18, fontWeight: 700 }}>P{r.priority}</span>
            <span style={{ fontFamily: 'monospace', fontSize: 10, background: '#0d3b2c', color: 'white', padding: '2px 5px' }}>{r.fostCode}</span>
            <span style={{ fontSize: 11 }}>{r.label}</span>
          </div>
          <div style={{ marginTop: 6, fontSize: 9, color: '#333', lineHeight: 1.5 }}>
            Volume estimé : <strong>{fmtMWh(r.kwhCumac)}</strong> cumac. Prime valorisable : <strong>{fmtEur(r.kwhCumac * 0.0078)}</strong>.
          </div>
        </div>
      ))}
    </div>
  );
}

// ─── Audit Tab ─────────────────────────────
function AuditTab({ v, bk }) {
  // ── Build events from backend when available, else fixture ────────────────
  const EVENTS = _vtbUseM(() => {
    const full = bk && bk.hasBackend && bk.full;
    if (!full) {
      // Fixture fallback
      return [
        { at: v.startedAt, by: v.technicianName, type: 'consent', label: 'Consentement RGPD enregistré', meta: 'SHA-256 a7f2…91cb' },
        { at: v.startedAt, by: v.technicianName, type: 'start', label: 'Début enregistrement', meta: `GPS ${(v.lat||0).toFixed(3)}, ${(v.lon||0).toFixed(3)} · ${v.network}` },
        { at: new Date(new Date(v.startedAt).getTime() + 47000).toISOString(), by: 'Vision IA', type: 'ai', label: 'Détection PAC Daikin 94 %', meta: 'Claude Vision · 11ms' },
        { at: new Date(new Date(v.startedAt).getTime() + 113000).toISOString(), by: 'Vision IA', type: 'ai', label: 'Détection ballon Atlantic 88 %', meta: 'Claude Vision · 9ms' },
        { at: new Date(new Date(v.startedAt).getTime() + 149000).toISOString(), by: 'OCR', type: 'ai', label: 'Lecture plaque Linky', meta: 'Claude Vision · 14ms' },
        { at: new Date(new Date(v.startedAt).getTime() + 250000).toISOString(), by: 'NLP audio', type: 'ai', label: 'Extraction 18 entités', meta: 'Whisper + custom NER' },
        { at: v.completedAt, by: v.technicianName, type: 'end', label: 'Fin enregistrement', meta: formatDuration(v.durationSeconds) },
        { at: new Date(new Date(v.completedAt).getTime() + 120000).toISOString(), by: 'Système', type: 'process', label: 'Analyse IA complète', meta: 'coût compute 0,042 €' },
        { at: new Date(new Date(v.completedAt).getTime() + 180000).toISOString(), by: 'Système', type: 'sync', label: 'Sync CRM Odoo · opportunité mise à jour', meta: 'Opportunity #4821' },
        { at: new Date(new Date(v.completedAt).getTime() + 210000).toISOString(), by: 'Système', type: 'sync', label: 'Fiche dossier CEE enrichie', meta: v.dossierRef },
        { at: new Date(new Date(v.completedAt).getTime() + 300000).toISOString(), by: 'Système', type: 'report', label: 'Rapport PDF généré', meta: 'modèle v3.1 · sha256 f82a1c9b…' },
      ];
    }
    // Build from real backend data
    const evts = [];
    const tech = full.technicianName || v.technicianName || 'Technicien';
    const start = full.startedAt || v.startedAt;
    const end   = full.completedAt || v.completedAt;
    // Consent (if audio transcript has consent recorded)
    const consentT = (bk.transcripts || []).find(t => t.consentRecorded);
    if (consentT) {
      evts.push({ at: consentT.consentTimestamp || start, by: tech, type: 'consent', label: 'Consentement RGPD enregistré', meta: `consentTimestamp: ${consentT.consentTimestamp ? new Date(consentT.consentTimestamp).toLocaleTimeString('fr-FR') : '—'}` });
    }
    // Start
    if (start) {
      evts.push({ at: start, by: tech, type: 'start', label: 'Début enregistrement', meta: `GPS ${(full.lat||v.lat||0).toFixed(3)}, ${(full.lon||v.lon||0).toFixed(3)}` });
    }
    // AI equipment detections (only those with a frameTimestamp)
    (bk.equipments || []).forEach(eq => {
      if (eq.frameTimestamp != null && start) {
        const ts = new Date(new Date(start).getTime() + eq.frameTimestamp * 1000).toISOString();
        const srcLabel = eq.source === 'vision' ? 'Vision IA' : eq.source === 'ocr' ? 'OCR' : eq.source === 'audio_nlp' ? 'NLP audio' : 'IA';
        evts.push({ at: ts, by: srcLabel, type: 'ai',
          label: `Détection ${[eq.brand, eq.model || eq.category].filter(Boolean).join(' ')} ${Math.round((eq.confidence||0)*100)}%`,
          meta: `${eq.source} · conf. ${(eq.confidence||0).toFixed(2)}` });
      }
    });
    // End recording
    if (end) {
      evts.push({ at: end, by: tech, type: 'end', label: 'Fin enregistrement', meta: formatDuration(full.durationSeconds || v.durationSeconds || 0) });
    }
    // Pipeline completed
    if (full.status === 'completed' && end) {
      evts.push({ at: new Date(new Date(end).getTime() + 60000).toISOString(), by: 'Système', type: 'process', label: 'Analyse IA complète', meta: `${(bk.equipments||[]).length} équipements détectés` });
    }
    // CRM sync
    if (full.crmSynced) {
      evts.push({ at: full.crmSyncedAt || end || start, by: 'Système', type: 'sync', label: 'Sync CRM Odoo · opportunité mise à jour', meta: `Opportunity #${full.opportunityId || '—'}` });
    }
    // Reports generated
    (bk.reports || []).forEach(r => {
      const rAt = r.createdAt || end;
      if (rAt) evts.push({ at: rAt, by: 'Système', type: 'report', label: `Rapport ${r.type || 'PDF'} généré`, meta: `format ${r.format || 'pdf'}` });
    });
    evts.sort((a, b) => new Date(a.at) - new Date(b.at));
    return evts;
  }, [bk]);

  const TYPE_META = {
    consent: { color: 'var(--plasma)', icon: 'lock' },
    start:   { color: 'var(--signal)', icon: 'bolt' },
    ai:      { color: 'var(--plasma)', icon: 'copilot' },
    end:     { color: 'var(--signal)', icon: 'check' },
    process: { color: 'var(--ink-3)', icon: 'refresh' },
    sync:    { color: 'var(--copper)', icon: 'refresh' },
    report:  { color: 'var(--signal)', icon: 'doc' },
  };

  return (
    <div style={{ maxWidth: 820 }}>
      <div style={{ padding: '10px 14px', background: 'var(--ink)', color: 'var(--signal)', borderRadius: 5, fontFamily: 'var(--font-mono)', fontSize: 10, marginBottom: 18, display: 'flex', alignItems: 'center', gap: 10 }}>
        <Dot tone="signal" pulse size={6}/>
        <span>audit_trail · {bk?.hasBackend ? 'backend temps réel' : 'fixture démo'} · {EVENTS.length} événements · intégrité OK</span>
      </div>
      <div style={{ position: 'relative', paddingLeft: 24 }}>
        <div style={{ position: 'absolute', left: 7, top: 8, bottom: 8, width: 1, background: 'var(--line-2)' }}/>
        {EVENTS.map((e, i) => {
          const meta = TYPE_META[e.type];
          return (
            <div key={i} style={{ position: 'relative', paddingBottom: 16 }}>
              <div style={{ position: 'absolute', left: -22, top: 2, width: 14, height: 14, borderRadius: '50%', background: 'var(--paper)', border: `2px solid ${meta.color}`, display: 'grid', placeItems: 'center' }}>
                <div style={{ width: 5, height: 5, borderRadius: '50%', background: meta.color }}/>
              </div>
              <div style={{ padding: 10, border: '1px solid var(--hairline)', borderRadius: 5, background: 'var(--paper)' }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                  <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ink)' }}>{e.label}</span>
                  <span style={{ flex: 1 }}/>
                  <span className="mono" style={{ fontSize: 10, color: 'var(--ink-4)' }}>
                    {new Date(e.at).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
                  </span>
                </div>
                <div style={{ fontSize: 11, color: 'var(--ink-4)', marginTop: 3, display: 'flex', gap: 6 }}>
                  <span>par {e.by}</span>
                  <span>·</span>
                  <span className="mono">{e.meta}</span>
                </div>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

Object.assign(window, { EquipmentsTab, TranscriptTab, SatelliteTab, ReportTab, AuditTab });
