/* MKT PRODUÇÃO DE CONTEÚDOS — UI segue padrão de Campanhas (data-theme=light + kanban colapsável).
   Fluxo: List (Kanban por fase) → Detail (Tabs) → Contexto → Prompts → Imagens → Copies → Peça Final.
   Real-time via SSE endpoint /producao/campanha/:id/stream.
*/

// ═══════════════════════════════════════════════════════════════════════════════
// API wrapper
// ═══════════════════════════════════════════════════════════════════════════════
async function ctdJson(r) {
  const text = await r.text();
  const trimmed = String(text || '').trim();
  if (!trimmed || trimmed[0] === '<') {
    const err = new Error('O servidor fechou a ligação. A alteração não ficou gravada. Tenta outra vez.');
    err.ctdRetry = true;
    throw err;
  }
  let data;
  try { data = JSON.parse(trimmed); }
  catch { throw new Error('Resposta inválida do servidor.'); }
  if (!r.ok) throw new Error(data?.error || data?.message || `Erro ${r.status}`);
  return data;
}
async function ctdSend(url, options, attempt = 0) {
  try {
    const r = await fetch(url, options);
    return await ctdJson(r);
  } catch (e) {
    if (attempt < 2 && (e?.ctdRetry || e?.name === 'TypeError')) {
      await new Promise(resolve => setTimeout(resolve, 1500));
      return ctdSend(url, options, attempt + 1);
    }
    throw e;
  }
}
const CtdAPI = {
  list:            (params = {})           => fetch(`/api/marketing/producao/campanhas?${new URLSearchParams(params)}`).then(r => r.json()),
  oneShot:         (body)                  => ctdSend('/api/marketing/producao/one-shot', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }),
  oneShotBriefings:(slug)                  => ctdSend(`/api/marketing/briefings/para-one-shot?brand_slug=${encodeURIComponent(slug || '')}`),
  oneShotBriefing: (id, briefingId)        => ctdSend(`/api/marketing/campanhas/${id}/one-shot-briefing`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ briefing_id: briefingId }) }),
  oneShotPeca:     (id, body)              => ctdSend(`/api/marketing/campanhas/${id}/one-shot-pecas`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }),
  oneShotPecasAccao: (id, body)            => ctdSend(`/api/marketing/campanhas/${id}/one-shot-pecas/accao`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }),
  brevoTemplates:  ()                      => ctdSend('/api/marketing/brevo/templates'),
  brevoTemplate:   (id)                    => ctdSend(`/api/marketing/brevo/templates/${id}`),
  oneShotEmail:    (id, body)              => ctdSend(`/api/marketing/campanhas/${id}/one-shot-email`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }),
  oneShotEmailImagem: (id, file)           => { const fd = new FormData(); fd.append('image', file); return ctdSend(`/api/marketing/campanhas/${id}/one-shot-email-imagem`, { method: 'POST', body: fd }); },
  oneShotEmailGerarTexto: (id, body)       => ctdSend(`/api/marketing/campanhas/${id}/one-shot-email-gerar-texto`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }),
  oneShotEmailGerarImagem: (id, body)      => ctdSend(`/api/marketing/campanhas/${id}/one-shot-email-gerar-imagem`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }),
  oneShotEmailDocumento: (id, file, fields) => { const fd = new FormData(); fd.append('file', file); Object.entries(fields || {}).forEach(([k, v]) => fd.append(k, v)); return ctdSend(`/api/marketing/campanhas/${id}/one-shot-email-documento`, { method: 'POST', body: fd }); },
  oneShotEmailDocumentoSeccoes: (id, body) => ctdSend(`/api/marketing/campanhas/${id}/one-shot-email-documento/seccoes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }),
  oneShotEmailDocumentoRever: (id, body) => ctdSend(`/api/marketing/campanhas/${id}/one-shot-email-documento/rever`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }),
  oneShotEmailDocumentoCorrigir: (id, body) => ctdSend(`/api/marketing/campanhas/${id}/one-shot-email-documento/corrigir`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }),
  oneShotEmailTeste: (id, body) => ctdSend(`/api/marketing/campanhas/${id}/one-shot-email-teste`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }),
  destinatariosTeste: () => ctdSend('/api/marketing/activacao/destinatarios-teste'),
  arquivo:         (id)                    => fetch(`/api/marketing/campanhas/${id}/arquivo`).then(r => r.json()),
  arquivar:        (id, actor = {})        => ctdSend(`/api/marketing/campanhas/${id}/arquivar`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor) }),
  restaurar:       (id)                    => ctdSend(`/api/marketing/campanhas/${id}/restaurar`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }),
  renomear:        (id, titulo)            => ctdSend(`/api/marketing/campanhas/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ titulo }) }),
  campanha:        (id)                    => fetch(`/api/marketing/producao/campanha/${id}`).then(r => r.json()),
  campanhaFull:    (id)                    => fetch(`/api/marketing/campanhas/${id}`).then(r => r.json()),
  validacao:       (id, body)              => ctdSend(`/api/marketing/producao/campanha/${id}/validacao`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }),
  tabs:            (id)                    => fetch(`/api/marketing/producao/campanha/${id}/tabs`).then(r => r.json()),
  canalDetail:     (id, canal)             => fetch(`/api/marketing/producao/campanha/${id}/tab/${canal}`).then(r => r.json()),
  kpis:            ()                      => fetch('/api/marketing/producao/kpis').then(r => r.json()),
  brands:          ()                      => fetch('/api/marketing/brands').then(r => r.json()).catch(() => []),

  // Copies — fonte única `conteudo_copy` do módulo Campanhas (2026-09-07 refactor).
  // Endpoint devolve `copy` (master + multi-market) + `idiomas` (traduções legacy).
  // Unimos ambos numa lista única para o Tab Copies.
  copies:          (id) => fetch(`/api/marketing/campanhas/${id}`, { cache: 'no-store' }).then(r => r.json()).then(d => ({
    copies: [...(d?.copy || []), ...(d?.idiomas || [])],
  })),
  // SSE: gera copies para todos os mercados definidos em variants_by_market
  generateCopies:  (id, body = {}, onStep) => {
    return new Promise(async (resolve, reject) => {
      try {
        const res = await fetch(`/api/marketing/campanhas/${id}/generate-copy`, {
          method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
        });
        if (!res.ok) {
          let msg = `HTTP ${res.status}`;
          try { const j = await res.json(); msg = j.error || msg; } catch {}
          return reject(new Error(msg));
        }
        const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = '';
        while (true) {
          const { done, value } = await reader.read();
          if (value) {
            buf += decoder.decode(value, { stream: !done });
            const lines = buf.split('\n'); buf = done ? '' : (lines.pop() ?? '');
            for (const line of lines) {
              if (!line.startsWith('data: ')) continue;
              let evt; try { evt = JSON.parse(line.slice(6)); } catch { continue; }
              if (evt.type === 'status' && onStep) onStep(evt.step || 0, evt.total || 0, evt.message || '');
              else if (evt.type === 'done')  resolve(evt.data);
              else if (evt.type === 'error') reject(new Error(evt.message));
            }
          }
          if (done) {
            if (buf.startsWith('data: ')) { try { const e = JSON.parse(buf.slice(6)); if (e.type === 'done') resolve(e.data); } catch {} }
            break;
          }
        }
      } catch (e) { reject(e); }
    });
  },
  // SSE: adapta copies existentes para outros mercados (não tradução literal — adaptação real)
  generateIdiomas: (id, extraLinguas = [], onStep) => {
    return new Promise(async (resolve, reject) => {
      try {
        const res = await fetch(`/api/marketing/campanhas/${id}/generate-idiomas`, {
          method: 'POST', headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ extra_linguas: extraLinguas }),
        });
        if (!res.ok) {
          let msg = `HTTP ${res.status}`;
          try { const j = await res.json(); msg = j.error || msg; } catch {}
          return reject(new Error(msg));
        }
        const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = '';
        while (true) {
          const { done, value } = await reader.read();
          if (value) {
            buf += decoder.decode(value, { stream: !done });
            const lines = buf.split('\n'); buf = done ? '' : (lines.pop() ?? '');
            for (const line of lines) {
              if (!line.startsWith('data: ')) continue;
              let evt; try { evt = JSON.parse(line.slice(6)); } catch { continue; }
              if (evt.type === 'status' && onStep) onStep(evt.step || 0, evt.total || 0, evt.message || '');
              else if (evt.type === 'done')  resolve(evt.data);
              else if (evt.type === 'error') reject(new Error(evt.message));
            }
          }
          if (done) {
            if (buf.startsWith('data: ')) { try { const e = JSON.parse(buf.slice(6)); if (e.type === 'done') resolve(e.data); } catch {} }
            break;
          }
        }
      } catch (e) { reject(e); }
    });
  },
  editCopy:         (id, copyId, body)     => ctdSend(`/api/marketing/copy/${copyId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }),
  approveCopy:      (id, copyId, actor={}) => ctdSend(`/api/marketing/copy/${copyId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'aprovado', ...actor }) }),
  approveWaTemplate:(copyId, actor={})     => ctdSend(`/api/marketing/copy/${copyId}/wa-template/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor) }),
  refreshWaMeta:    (copyId)               => ctdSend(`/api/marketing/copy/${copyId}/wa-meta-refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }),
  adaptWaHeader:    (copyId, actor={})     => ctdSend(`/api/marketing/copy/${copyId}/wa-header`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor) }),
  generateWaPitchHeader: (copyId, actor = {}, onStep = () => {}) => new Promise(async (resolve, reject) => {
    let settled = false;
    const ok = (v) => { if (!settled) { settled = true; resolve(v); } };
    const fail = (e) => { if (!settled) { settled = true; reject(e instanceof Error ? e : new Error(e)); } };
    try {
      const res = await fetch(`/api/marketing/copy/${copyId}/wa-header-pitch`, {
        method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor),
      });
      if (!res.ok) {
        let msg = `HTTP ${res.status}`;
        try { const j = await res.json(); msg = j.error || msg; } catch {}
        return fail(new Error(msg));
      }
      const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = '';
      while (true) {
        const { done, value } = await reader.read();
        if (value) {
          buf += decoder.decode(value, { stream: !done });
          const lines = buf.split('\n'); buf = done ? '' : (lines.pop() ?? '');
          for (const line of lines) {
            if (!line.startsWith('data: ')) continue;
            let evt; try { evt = JSON.parse(line.slice(6)); } catch { continue; }
            if (evt.type === 'status') onStep(evt.step || 0, evt.total || 0, evt.message || '');
            else if (evt.type === 'done') ok(evt.copy);
            else if (evt.type === 'error') fail(new Error(evt.message || 'Não consegui gerar a imagem'));
          }
        }
        if (done) break;
      }
      if (!settled) fail(new Error('A geração do cabeçalho terminou sem imagem'));
    } catch (e) { fail(e); }
  }),
  submitWaTemplate: (campanhaId, copyId, actor={}) => ctdSend(`/api/marketing/activacao/wa/submit-template`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ campanha_id: campanhaId, copy_id: copyId, ...actor }) }),
  submitWaBatch: (campanhaId, copyIds, actor={}) => ctdSend(`/api/marketing/activacao/wa/submit-batch`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ campanha_id: campanhaId, copy_ids: copyIds, ...actor }) }),
  generateWaLibrary: (id, lingua = 'pt', actor = {}) => new Promise(async (resolve, reject) => {
    try {
      const res = await fetch(`/api/marketing/producao/campanha/${id}/wa/generate`, {
        method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ lingua, actor_email: actor.actor_email || null }),
      });
      if (!res.ok) {
        let msg = `HTTP ${res.status}`;
        try { const j = await res.json(); msg = j.error || msg; } catch {}
        return reject(new Error(msg));
      }
      const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = ''; let event = '';
      while (true) {
        const { done, value } = await reader.read();
        if (value) {
          buf += decoder.decode(value, { stream: !done });
          const lines = buf.split('\n'); buf = done ? '' : (lines.pop() ?? '');
          for (const line of lines) {
            if (line.startsWith('event: ')) event = line.slice(7).trim();
            else if (line.startsWith('data: ')) {
              let data = {}; try { data = JSON.parse(line.slice(6)); } catch { event = ''; continue; }
              if (event === 'done') resolve(data);
              else if (event === 'error') reject(new Error(data.message || 'Erro ao gerar a biblioteca'));
              event = '';
            }
          }
        }
        if (done) break;
      }
    } catch (e) { reject(e); }
  }),
  approveAllCopies: (id, actor={})         => fetch(`/api/marketing/campanhas/${id}/copy/approve-all`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor) }).then(r => r.json()),
  regenerateCopy:   (copyId)               => fetch(`/api/marketing/copy/${copyId}/regenerate`, { method: 'POST' }).then(r => r.json()),
  copyVersions:     (copyId)               => fetch(`/api/marketing/copy/${copyId}/versions`).then(r => r.json()),
  copiesStale:      (id)                   => fetch(`/api/marketing/campanhas/${id}/copies/stale`).then(r => r.json()),
  // SSE: gerar copy para 1 peça específica (per-item, com escolha de modelo)
  generateCopyItem: (id, body = {}, onStep) => {
    return new Promise(async (resolve, reject) => {
      try {
        const res = await fetch(`/api/marketing/campanhas/${id}/generate-copy-item`, {
          method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
        });
        if (!res.ok) {
          let msg = `HTTP ${res.status}`;
          try { const j = await res.json(); msg = j.error || msg; } catch {}
          return reject(new Error(msg));
        }
        const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = '';
        while (true) {
          const { done, value } = await reader.read();
          if (value) {
            buf += decoder.decode(value, { stream: !done });
            const lines = buf.split('\n'); buf = done ? '' : (lines.pop() ?? '');
            for (const line of lines) {
              if (!line.startsWith('data: ')) continue;
              let evt; try { evt = JSON.parse(line.slice(6)); } catch { continue; }
              if (evt.type === 'status' && onStep) onStep(evt.step || 0, evt.total || 0, evt.message || '');
              else if (evt.type === 'progress' && onStep) onStep(evt.step || 0, evt.total || 0, evt.message || '⏳');
              else if (evt.type === 'done')  resolve(evt.data);
              else if (evt.type === 'error') reject(new Error(evt.message));
            }
          }
          if (done) {
            if (buf.startsWith('data: ')) { try { const e = JSON.parse(buf.slice(6)); if (e.type === 'done') resolve(e.data); } catch {} }
            break;
          }
        }
      } catch (e) { reject(e); }
    });
  },

  // Prompts
  prompts:         (id)                    => fetch(`/api/marketing/producao/campanha/${id}/prompts`).then(r => r.json()),
  generatePrompts: (id, body = {}, onStep) => {
    return new Promise(async (resolve, reject) => {
      try {
        const res = await fetch(`/api/marketing/producao/campanha/${id}/prompts/generate`, {
          method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
        });
        if (!res.ok) {
          let msg = `HTTP ${res.status}`;
          try { const j = await res.json(); msg = j.error || msg; } catch {}
          return reject(new Error(msg));
        }
        const reader = res.body.getReader();
        const decoder = new TextDecoder();
        let buf = '';
        while (true) {
          const { done, value } = await reader.read();
          if (value) {
            buf += decoder.decode(value, { stream: !done });
            const lines = buf.split('\n');
            buf = done ? '' : (lines.pop() ?? '');
            for (const line of lines) {
              if (!line.startsWith('data: ')) continue;
              let evt; try { evt = JSON.parse(line.slice(6)); } catch { continue; }
              if (evt.type === 'status' && onStep) onStep(evt.step, evt.total || 0, evt.message || '');
              else if (evt.type === 'done')  resolve(evt.data);
              else if (evt.type === 'error') reject(new Error(evt.message));
            }
          }
          if (done) {
            if (buf.startsWith('data: ')) {
              try { const e = JSON.parse(buf.slice(6)); if (e.type === 'done') resolve(e.data); } catch {}
            }
            break;
          }
        }
      } catch (e) { reject(e); }
    });
  },
  // SSE: gerar prompt para 1 peça específica (per-item, via query param commPlanIndex)
  generatePromptItem: (id, commPlanIndex, body = {}, onStep) => {
    return new Promise(async (resolve, reject) => {
      let settled = false;
      const ok = (v) => { if (!settled) { settled = true; resolve(v); } };
      const fail = (e) => { if (!settled) { settled = true; reject(e instanceof Error ? e : new Error(e)); } };
      const dropped = () => fail(new Error('A ligação caiu a meio. O prompt e a imagem anteriores mantêm-se. Tenta outra vez.'));
      try {
        const res = await fetch(`/api/marketing/producao/campanha/${id}/prompts/generate?commPlanIndex=${encodeURIComponent(commPlanIndex)}`, {
          method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
        });
        if (!res.ok) {
          let msg = `HTTP ${res.status}`;
          try { const j = await res.json(); msg = j.error || msg; } catch {}
          return fail(new Error(msg));
        }
        const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = '';
        while (true) {
          const { done, value } = await reader.read();
          if (value) {
            buf += decoder.decode(value, { stream: !done });
            const lines = buf.split('\n'); buf = done ? '' : (lines.pop() ?? '');
            for (const line of lines) {
              if (!line.startsWith('data: ')) continue;
              let evt; try { evt = JSON.parse(line.slice(6)); } catch { continue; }
              if (evt.type === 'status' && onStep) onStep(evt.step || 0, evt.total || 0, evt.message || '');
              else if (evt.type === 'progress' && onStep) onStep(evt.step || 0, evt.total || 0, evt.message || '⏳');
              else if (evt.type === 'done')  ok(evt.data);
              else if (evt.type === 'error') fail(new Error(evt.message));
            }
          }
          if (done) {
            if (buf.startsWith('data: ')) { try { const e = JSON.parse(buf.slice(6)); if (e.type === 'done') ok(e.data); else if (e.type === 'error') fail(new Error(e.message)); } catch {} }
            break;
          }
        }
        if (!settled) dropped();
      } catch (e) {
        const raw = String(e?.message || '');
        if (/network error|failed to fetch|load failed|networkerror/i.test(raw)) dropped();
        else fail(e);
      }
    });
  },
  editPrompt:      (id, pid, body)         => fetch(`/api/marketing/producao/campanha/${id}/prompts/${pid}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(r => r.json()),
  approvePrompt:   (id, pid, actor = {})   => fetch(`/api/marketing/producao/campanha/${id}/prompts/${pid}/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor) }).then(r => r.json()),
  approveAllPrompts:  (id, actor = {})     => fetch(`/api/marketing/producao/campanha/${id}/prompts/approve-all`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor) }).then(r => r.json()),
  sendPromptsToExec:  (id, actor = {})     => fetch(`/api/marketing/producao/campanha/${id}/prompts/send-to-executive`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor) }).then(r => r.json()),
  promptReadiness:    (id, commPlanIndex) => fetch(`/api/marketing/producao/campanha/${id}/prompt-readiness${commPlanIndex != null ? `?commPlanIndex=${commPlanIndex}` : ''}`).then(r => r.json()),
  promptApplication:  (id, commPlanIndex) => fetch(`/api/marketing/producao/campanha/${id}/prompt-application${commPlanIndex != null ? `?commPlanIndex=${encodeURIComponent(commPlanIndex)}` : ''}`).then(r => r.json()),
  generateImage: (id, pid, actor = {}, onStep = () => {}) => {
    return new Promise(async (resolve, reject) => {
      let settled = false;
      const ok = (v) => { if (!settled) { settled = true; resolve(v); } };
      const fail = (e) => { if (!settled) { settled = true; reject(e instanceof Error ? e : new Error(e)); } };
      try {
        const res = await fetch(`/api/marketing/producao/campanha/${id}/prompts/${pid}/generate-image`, {
          method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor),
        });
        if (!res.ok) {
          let msg = `HTTP ${res.status}`;
          try { const j = await res.json(); msg = j.error || msg; } catch {}
          return fail(new Error(msg));
        }
        const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = '';
        while (true) {
          const { done, value } = await reader.read();
          if (value) {
            buf += decoder.decode(value, { stream: !done });
            const lines = buf.split('\n'); buf = done ? '' : (lines.pop() ?? '');
            for (const line of lines) {
              if (line.startsWith(':')) continue;
              if (!line.startsWith('data: ')) continue;
              let evt; try { evt = JSON.parse(line.slice(6)); } catch { continue; }
              if (evt.type === 'status') onStep(evt.message || '');
              else if (evt.type === 'done') ok(evt);
              else if (evt.type === 'error') fail(new Error(evt.message || 'Erro ao gerar imagem'));
            }
          }
          if (done) {
            if (!settled && buf) {
              const raw = buf.startsWith('data: ') ? buf.slice(6) : buf.replace(/^data: /, '');
              try {
                const e = JSON.parse(raw);
                if (e.type === 'done') ok(e);
                else if (e.type === 'error') fail(new Error(e.message || 'Erro ao gerar imagem'));
              } catch {}
            }
            break;
          }
        }
        if (!settled) fail(new Error('Ligação encerrada sem resultado'));
      } catch (e) { fail(e); }
    });
  },
  approveImage:    (id, pid, actor = {})   => fetch(`/api/marketing/producao/campanha/${id}/prompts/${pid}/image/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor) }).then(r => r.json()),
  restoreImage:    (id, pid, url)          => fetch(`/api/marketing/producao/campanha/${id}/prompts/${pid}/restore-image`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) }).then(async r => {
    const data = await r.json();
    if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`);
    return data;
  }),
  generateVideoPrompt: (id, pid, actor = {}, onStep = () => {}) => {
    return new Promise(async (resolve, reject) => {
      let settled = false;
      const ok = (v) => { if (!settled) { settled = true; resolve(v); } };
      const fail = (e) => { if (!settled) { settled = true; reject(e instanceof Error ? e : new Error(e)); } };
      try {
        const res = await fetch(`/api/marketing/producao/campanha/${id}/prompts/${pid}/generate-video-prompt`, {
          method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor),
        });
        if (!res.ok) {
          let msg = `HTTP ${res.status}`;
          try { const j = await res.json(); msg = j.error || msg; } catch {}
          return fail(new Error(msg));
        }
        const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = '';
        while (true) {
          const { done, value } = await reader.read();
          if (value) {
            buf += decoder.decode(value, { stream: !done });
            const lines = buf.split('\n'); buf = done ? '' : (lines.pop() ?? '');
            for (const line of lines) {
              if (line.startsWith(':')) continue;
              if (!line.startsWith('data: ')) continue;
              let evt; try { evt = JSON.parse(line.slice(6)); } catch { continue; }
              if (evt.type === 'status') onStep(evt.step || 0, evt.total || 0, evt.message || '');
              else if (evt.type === 'done') ok(evt);
              else if (evt.type === 'error') fail(new Error(evt.message || 'Erro ao gerar prompt de vídeo'));
            }
          }
          if (done) {
            if (!settled && buf) {
              const raw = buf.startsWith('data: ') ? buf.slice(6) : buf.replace(/^data: /, '');
              try {
                const e = JSON.parse(raw);
                if (e.type === 'done') ok(e);
                else if (e.type === 'error') fail(new Error(e.message || 'Erro ao gerar prompt de vídeo'));
              } catch {}
            }
            break;
          }
        }
        if (!settled) fail(new Error('Ligação encerrada sem resultado'));
      } catch (e) { fail(e); }
    });
  },
  generateVideo: (id, pid, actor = {}, onStep = () => {}) => {
    return new Promise(async (resolve, reject) => {
      let settled = false;
      const ok = (v) => { if (!settled) { settled = true; resolve(v); } };
      const fail = (e) => { if (!settled) { settled = true; reject(e instanceof Error ? e : new Error(e)); } };
      try {
        const res = await fetch(`/api/marketing/producao/campanha/${id}/prompts/${pid}/generate-video`, {
          method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor),
        });
        if (!res.ok) {
          let msg = `HTTP ${res.status}`;
          try { const j = await res.json(); msg = j.error || msg; } catch {}
          return fail(new Error(msg));
        }
        const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = '';
        while (true) {
          const { done, value } = await reader.read();
          if (value) {
            buf += decoder.decode(value, { stream: !done });
            const lines = buf.split('\n'); buf = done ? '' : (lines.pop() ?? '');
            for (const line of lines) {
              if (line.startsWith(':')) continue;
              if (!line.startsWith('data: ')) continue;
              let evt; try { evt = JSON.parse(line.slice(6)); } catch { continue; }
              if (evt.type === 'status') onStep(evt.step || 0, evt.total || 0, evt.message || '');
              else if (evt.type === 'done') ok(evt);
              else if (evt.type === 'error') fail(new Error(evt.message || 'Erro ao gerar vídeo'));
            }
          }
          if (done) {
            if (!settled && buf) {
              const raw = buf.startsWith('data: ') ? buf.slice(6) : buf.replace(/^data: /, '');
              try {
                const e = JSON.parse(raw);
                if (e.type === 'done') ok(e);
                else if (e.type === 'error') fail(new Error(e.message || 'Erro ao gerar vídeo'));
              } catch {}
            }
            break;
          }
        }
        if (!settled) fail(new Error('Ligação encerrada sem resultado'));
      } catch (e) { fail(e); }
    });
  },
  approveVideo:    (id, pid, actor = {})   => fetch(`/api/marketing/producao/campanha/${id}/prompts/${pid}/video/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor) }).then(r => r.json()),
  approveAllVideos:(id, actor = {})        => fetch(`/api/marketing/producao/campanha/${id}/prompts/approve-all-videos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor) }).then(r => r.json()),
  restoreVideo:    (id, pid, url)          => fetch(`/api/marketing/producao/campanha/${id}/prompts/${pid}/restore-video`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) }).then(async r => {
    const data = await r.json();
    if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`);
    return data;
  }),
  approveAllImages:(id, actor = {})        => fetch(`/api/marketing/producao/campanha/${id}/prompts/approve-all-images`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor) }).then(r => r.json()),
  downloadUrl:     (url, name)             => `/api/marketing/producao/download?url=${encodeURIComponent(url)}&name=${encodeURIComponent(name || 'imagem.png')}`,
  generateVariations: (id, pid, body = {}, onStep) => {
    return new Promise(async (resolve, reject) => {
      try {
        const res = await fetch(`/api/marketing/producao/campanha/${id}/prompts/${pid}/variations`, {
          method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
        });
        if (!res.ok) {
          let msg = `HTTP ${res.status}`;
          try { const j = await res.json(); msg = j.error || msg; } catch {}
          return reject(new Error(msg));
        }
        const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = '';
        let settled = false;
        while (true) {
          const { done, value } = await reader.read();
          if (value) {
            buf += decoder.decode(value, { stream: !done });
            const lines = buf.split('\n'); buf = done ? '' : (lines.pop() ?? '');
            for (const line of lines) {
              if (!line.startsWith('data: ')) continue;
              let evt; try { evt = JSON.parse(line.slice(6)); } catch { continue; }
              if (evt.type === 'status' && onStep) onStep(evt.step, evt.total || 0, evt.message || '');
              else if (evt.type === 'done') { settled = true; resolve(evt.data); }
              else if (evt.type === 'error') { settled = true; reject(new Error(evt.message || 'Erro ao gerar variações')); }
            }
          }
          if (done) {
            if (buf.startsWith('data: ')) {
              try {
                const e = JSON.parse(buf.slice(6));
                if (e.type === 'done') { settled = true; resolve(e.data); }
                else if (e.type === 'error') { settled = true; reject(new Error(e.message || 'Erro ao gerar variações')); }
              } catch {}
            }
            if (!settled) reject(new Error('Ligação encerrada sem resultado'));
            break;
          }
        }
      } catch (e) { reject(e); }
    });
  },
  editImage: (id, pid, editPrompt, onStep) => {
    return new Promise(async (resolve, reject) => {
      try {
        const res = await fetch(`/api/marketing/producao/campanha/${id}/prompts/${pid}/edit`, {
          method: 'POST', headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ edit_prompt: editPrompt }),
        });
        if (!res.ok) {
          let msg = `HTTP ${res.status}`;
          try { const j = await res.json(); msg = j.error || msg; } catch {}
          return reject(new Error(msg));
        }
        const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = '';
        while (true) {
          const { done, value } = await reader.read();
          if (value) {
            buf += decoder.decode(value, { stream: !done });
            const lines = buf.split('\n'); buf = done ? '' : (lines.pop() ?? '');
            for (const line of lines) {
              if (!line.startsWith('data: ')) continue;
              let evt; try { evt = JSON.parse(line.slice(6)); } catch { continue; }
              if (evt.type === 'status' && onStep) onStep(evt.message || '');
              else if (evt.type === 'done')  resolve(evt.data);
              else if (evt.type === 'error') reject(new Error(evt.message));
            }
          }
          if (done) {
            if (buf.startsWith('data: ')) { try { const e = JSON.parse(buf.slice(6)); if (e.type === 'done') resolve(e.data); } catch {} }
            break;
          }
        }
      } catch (e) { reject(e); }
    });
  },
  generateAllImages: (id, body = {}, onStep) => {
    return new Promise(async (resolve, reject) => {
      try {
        const res = await fetch(`/api/marketing/producao/campanha/${id}/prompts/generate-all-images`, {
          method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
        });
        if (!res.ok) {
          let msg = `HTTP ${res.status}`;
          try { const j = await res.json(); msg = j.error || msg; } catch {}
          return reject(new Error(msg));
        }
        const reader = res.body.getReader(); const decoder = new TextDecoder(); let buf = '';
        while (true) {
          const { done, value } = await reader.read();
          if (value) {
            buf += decoder.decode(value, { stream: !done });
            const lines = buf.split('\n'); buf = done ? '' : (lines.pop() ?? '');
            for (const line of lines) {
              if (!line.startsWith('data: ')) continue;
              let evt; try { evt = JSON.parse(line.slice(6)); } catch { continue; }
              if (evt.type === 'status' && onStep) onStep(evt.step, evt.total || 0, evt.message || '');
              else if (evt.type === 'done')  resolve(evt.data);
              else if (evt.type === 'error') reject(new Error(evt.message));
            }
          }
          if (done) {
            if (buf.startsWith('data: ')) { try { const e = JSON.parse(buf.slice(6)); if (e.type === 'done') resolve(e.data); } catch {} }
            break;
          }
        }
      } catch (e) { reject(e); }
    });
  },

  imagens:         (id)                    => fetch(`/api/marketing/producao/campanha/${id}/imagens`).then(r => r.json()),

  generateCanal:   (id, canal, body = {})  => fetch(`/api/marketing/producao/campanha/${id}/tab/${canal}/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(r => r.json()),
  approveCanal:    (id, canal, actor = {}) => fetch(`/api/marketing/producao/campanha/${id}/tab/${canal}/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor) }).then(r => r.json()),

  peca:            (id)                    => fetch(`/api/marketing/producao/peca/${id}`).then(r => r.json()),
  approvePeca:     (id, actor = {})        => fetch(`/api/marketing/producao/peca/${id}/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor) }).then(r => r.json()),
  regeneratePeca:  (id, actor = {})        => fetch(`/api/marketing/producao/peca/${id}/regenerate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(actor) }).then(r => r.json()),

  stream: (id, onEvent) => {
    const es = new EventSource(`/api/marketing/producao/campanha/${id}/stream`);
    es.onmessage = (e) => { try { onEvent(JSON.parse(e.data)); } catch {} };
    return es;
  },
};

// ═══════════════════════════════════════════════════════════════════════════════
// Config — Kanban colunas + tabs (padrão Campanhas: cor + statuses)
// ═══════════════════════════════════════════════════════════════════════════════
const CTD_VALIDADORES = [
  'fabio.costa@digidelta.pt',
  'rui.leitao@digidelta.pt',
  'armando.mota@digidelta.pt',
  'bruno.rosa@digidelta.pt',
  'bruno.rosa@netscreen.pt',
];
const ctdPodeValidar = (email) => CTD_VALIDADORES.includes(String(email || window.currentUser?.email || '').toLowerCase());
const CTD_PASSO_RANK = { 'Por começar': 0, Copy: 1, Prompt: 2, Imagem: 3, 'Vídeo': 4, 'Aprovação Meta': 5, Fechado: 9 };
function ctdPorComecar(canal) {
  if (!canal || canal.fechado) return 0;
  const m = String(canal.rotulo || '').match(/^(.*?)\s+(\d+)\/(\d+)$/);
  if (!m) return canal.passo === 'Por começar' ? Number(canal.total || 0) : 0;
  const feitos = Number(m[2]);
  const base = Number(m[3]);
  if (m[1].trim() === 'Fechado') return Math.max(0, base - feitos);
  return Math.max(0, Number(canal.total || 0) - base);
}
function ctdCanalAtrasado(canais) {
  const list = Array.isArray(canais) ? canais : [];
  const abertos = list.filter(c => !c.fechado);
  const pool = abertos.length ? abertos : list;
  return pool.slice().sort((a, b) => {
    const d = (CTD_PASSO_RANK[a.passo] ?? 9) - (CTD_PASSO_RANK[b.passo] ?? 9);
    if (d) return d;
    const ia = CTD_CANAL_ORDER.indexOf(a.canal);
    const ib = CTD_CANAL_ORDER.indexOf(b.canal);
    return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib);
  })[0] || null;
}
const CTD_KANBAN_COLS = [
  { id: 'por_fazer', label: 'Por fazer', color: '#64748b', match: c => c.coluna === 'por_fazer' },
  { id: 'em_producao', label: 'Em produção', color: '#f59e0b', match: c => c.coluna === 'em_producao' },
  { id: 'em_validacao', label: 'Em validação', color: '#3859D0', match: c => c.coluna === 'em_validacao' },
  { id: 'aprovado', label: 'Aprovado', color: '#22c55e', match: c => c.coluna === 'aprovado' },
  { id: 'activacao', label: 'Activação', color: '#0f766e', match: c => c.coluna === 'activacao' || c.camp_status === 'em_activacao' },
];

// ─── Helper: nº de peças prontas por campanha (copy+imagem aprovadas) ─────────
const _ctdN = (v) => Number(v || 0);
const ctdPecasReady = (c) => Math.max(
  _ctdN(c.pecas_aprovadas),
  Math.min(_ctdN(c.copies_aprovadas), _ctdN(c.imagens_aprovadas)),
);

const CTD_DETAIL_TABS = [
  { id: 'contexto', label: 'Plano de Produção' },
  { id: 'copies',   label: 'Copies' },
  { id: 'prompts',  label: 'Prompts' },
  { id: 'imagens',  label: 'Imagens' },
  { id: 'video',    label: 'Video' },
  { id: 'peca',     label: 'Peça Final' },
];
const CTD_QUAL_TABS = [
  { id: 'copies',  label: 'Copies' },
  { id: 'prompts', label: 'Aprovação Meta' },
  { id: 'peca',    label: 'Peça Final' },
];
function ctdPieceTabs(waGroup) {
  if (waGroup === 'qualificacao' || waGroup === 'abertura') return CTD_QUAL_TABS;
  return CTD_DETAIL_TABS;
}

// Mapping coluna kanban → tab do detalhe (1:1).
const CTD_COL_TO_TAB = {
  plano_producao: 'contexto',
  prompts:        'prompts',
  imagens:        'imagens',
  copies:         'copies',
  peca_final:     'peca',
};

// Detecta em que coluna kanban a campanha cai e devolve a tab correspondente.
function ctdTabForCampanha(c) {
  const col = CTD_KANBAN_COLS.find(k => k.match(c));
  return col ? (CTD_COL_TO_TAB[col.id] || 'contexto') : 'contexto';
}

// ═══════════════════════════════════════════════════════════════════════════════
// Helpers
// ═══════════════════════════════════════════════════════════════════════════════
function ctdFmtDate(iso) {
  if (!iso) return '—';
  const d = new Date(iso);
  if (isNaN(d)) return '—';
  return d.toLocaleDateString('pt-PT', { day: '2-digit', month: 'short' });
}

const ctdBrandColor = (slug) => {
  const map = { mimaki: '#0066cc', biond: '#4a7c2e', decal: '#e63946', alldecor: '#f77f00', sensek: '#7209b7', netscreen: '#118ab2', digidelta: '#112954' };
  return map[(slug || '').toLowerCase()] || '#3859D0';
};
const ctdInkOn = (hex) => {
  const h = String(hex || '#111111').replace('#', '');
  const r = parseInt(h.slice(0, 2), 16) || 0;
  const g = parseInt(h.slice(2, 4), 16) || 0;
  const b = parseInt(h.slice(4, 6), 16) || 0;
  return (0.2126 * r + 0.7152 * g + 0.0722 * b) < 150 ? '#FFFFFF' : '#111111';
};

const CTD_BRAND_PAGE = {
  biond: { name: 'BIOND', domain: 'biond.pt' },
  mimaki: { name: 'Mimaki', domain: 'mimaki.pt' },
  decal: { name: 'Decal', domain: 'decal.pt' },
  alldecor: { name: 'AllDecor', domain: 'alldecor.pt' },
  sensek: { name: 'Sensek', domain: 'sensek.pt' },
  netscreen: { name: 'Netscreen', domain: 'netscreen.pt' },
  digidelta: { name: 'Digidelta Internacional', domain: 'digidelta.pt' },
};
const ctdBrandPage = (slug, fallbackName) => {
  const key = String(slug || '').toLowerCase();
  const known = CTD_BRAND_PAGE[key];
  if (known) return known;
  const name = fallbackName || (key ? key.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) : 'Digidelta Internacional');
  return { name, domain: (key || 'digidelta') + '.pt' };
};

function ctdParseJson(v) {
  if (!v) return null;
  if (typeof v === 'object') return v;
  try { return JSON.parse(v); } catch { return null; }
}
function ctdTokens(s) {
  return String(s || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '')
    .split(/[^a-z0-9%]+/).filter(w => w.length >= 3);
}
function ctdScore(a, b) {
  const ta = ctdTokens(a), tb = ctdTokens(b);
  if (!ta.length || !tb.length) return 0;
  let hits = 0;
  for (const w of ta) {
    if (tb.some(x => x === w || (w.length >= 5 && x.startsWith(w)) || (x.length >= 5 && w.startsWith(x)))) hits += 1;
  }
  const phrases = ['print & cut', 'print and cut', 'ok biobased', 'bio-based', 'testing roll', 'a2-s1'];
  const qa = String(a || '').toLowerCase(), qb = String(b || '').toLowerCase();
  for (const ph of phrases) if (qa.includes(ph) && qb.includes(ph)) hits += 2;
  return hits;
}
function ctdBestMatchStr(query, list, minScore = 2) {
  const arr = (list || []).filter(Boolean);
  if (!arr.length || !query) return '';
  let best = '', bestScore = 0;
  for (const item of arr) {
    const sc = ctdScore(query, item);
    if (sc > bestScore) { bestScore = sc; best = item; }
  }
  return bestScore >= minScore ? best : '';
}
const CTD_GENERIC = new Set(['maquina','producao','trabalho','trabalhos','cliente','clientes','tempo','hora','horas','parar','enquanto','tenho','fico','perco','perda','todo','toda','todos','outro','outra','noutro','ainda','muito','fazer','mesmo','unica','unico','resultado','direto','directo','diretamente','para','pela','pelo','pelas','pelos','como','mais','menos','esta','esse','essa','isto','isso','quando','onde','porque','sobre','entre','desde','cada','numa','tambem','apenas','sempre','nunca','depois','antes','durante']);
function ctdSpecificTokens(s) {
  return ctdTokens(s).filter(w => !CTD_GENERIC.has(w) && w.length >= 4);
}
function ctdTokenHit(a, b) {
  return a === b || (a.length >= 5 && b.length >= 5 && (a.startsWith(b) || b.startsWith(a)));
}
function ctdSpecificScore(query, candidate) {
  const q = ctdSpecificTokens(query);
  const c = ctdSpecificTokens(candidate);
  if (!q.length || !c.length) return 0;
  let hits = 0;
  for (const w of q) if (c.some(x => ctdTokenHit(w, x))) hits += 1;
  return hits;
}
function ctdRestates(a, b) {
  const ta = ctdSpecificTokens(a);
  const tb = ctdSpecificTokens(b);
  if (ta.length < 3 || tb.length < 3) return false;
  const inter = ta.filter(t => tb.some(x => ctdTokenHit(t, x))).length;
  return inter / Math.min(ta.length, tb.length) >= 0.6;
}
function ctdBestAngle(uspRaw, angles) {
  const list = Array.isArray(angles) ? angles : [];
  let best = null, bestScore = 0;
  for (const a of list) {
    const sc = ctdSpecificScore(uspRaw, a?.usp_principal || '');
    if (sc > bestScore) { bestScore = sc; best = a; }
  }
  return bestScore >= 1 ? best : null;
}
function ctdAngleByUspIndex(angles, peca) {
  const n = Number(peca?.usp_index);
  if (!Number.isFinite(n) || n < 1) return null;
  return (Array.isArray(angles) ? angles : [])[n - 1] || null;
}
function ctdAnguloForAd(localAngle, baseAngle, usp, country) {
  const local = String(localAngle?.angulo || '').trim();
  const base = String(baseAngle?.angulo || '').trim();
  if (local && !ctdRestates(local, usp)) return local;
  if (String(country || 'PT').toUpperCase() !== 'ES' && base && !ctdRestates(base, usp)) return base;
  return '';
}
function ctdPitchBaseline(ep) {
  const m = String(ep || '').match(/depend[eê]ncia de\s+([^—–.]{8,90})/i);
  return m ? m[1].trim() : '';
}
function ctdCertName(cert) {
  return String(cert || '').split(/[\s(—–-]/)[0].toLowerCase();
}
function ctdCertForAd(peca, usp, angle, certs, ep) {
  const hay = [usp, angle?.usp_principal].filter(Boolean).join('\n').toLowerCase();
  const inUsp = (certs || []).find(c => {
    const name = ctdCertName(c);
    return name.length >= 4 && hay.includes(name);
  });
  if (inUsp) return inUsp;
  if (Number(peca?.usp_index) !== 1) return '';
  const pitch = String(ep || '').toLowerCase();
  return (certs || []).find(c => {
    const name = ctdCertName(c);
    return name.length >= 4 && pitch.includes(name);
  }) || '';
}
function ctdBestPain(uspRaw, pains) {
  const list = (pains || []).filter(Boolean);
  let best = '', bestScore = 0;
  for (const p of list) {
    const sc = ctdSpecificScore(uspRaw, p);
    if (sc > bestScore) { bestScore = sc; best = p; }
  }
  return bestScore >= 2 ? best : '';
}
function ctdPainClause(usp) {
  const s = String(usp || '').replace(/\s+/g, ' ').trim();
  const m = s.match(/antes que\s+([^.;]{8,90})/i);
  if (!m) return '';
  const bit = m[1].trim().replace(/\bcontamine\b/i, 'contamina');
  return bit.charAt(0).toUpperCase() + bit.slice(1);
}
function ctdBestMatchObj(query, list, getter, minScore = 2) {
  const arr = Array.isArray(list) ? list : [];
  if (!arr.length || !query) return null;
  let best = null, bestScore = 0;
  for (const item of arr) {
    const sc = ctdScore(query, getter(item) || '');
    if (sc > bestScore) { bestScore = sc; best = item; }
  }
  return bestScore >= minScore ? best : null;
}
function ctdSanitizeProof(s) {
  return String(s || '')
    .replace(/80[,.]96\s*%/gi, '80–96%')
    .replace(/\b87\s*%/g, '80–96%')
    .replace(/T[ÜU]V\s*Rheinland/gi, 'TÜV AUSTRIA')
    .replace(/T[ÜU]V\s*S[ÜU]D/gi, 'TÜV AUSTRIA')
    .replace(/[ \t]+/g, ' ')
    .trim();
}
function ctdAnglesForMarket(proposta, country) {
  const variants = proposta?.variants_by_market || [];
  const cc = String(country || '').toUpperCase();
  const v = variants.find(x => String(x.country || '').toUpperCase() === cc);
  if (v?.messaging_angles_local?.length) return v.messaging_angles_local;
  return proposta?.messaging_angles || [];
}
function ctdMetaAdOrdinal(commPlan, idx) {
  const plan = Array.isArray(commPlan) ? commPlan : [];
  let n = 0;
  for (let i = 0; i < plan.length; i++) {
    const p = plan[i] || {};
    const isMeta = p.canal === 'meta_ads' || p.content_type === 'meta_ads';
    if (!isMeta) continue;
    if (i === idx) return n;
    n += 1;
  }
  return null;
}
function ctdMimakiTalk(s) {
  let t = String(s || '');
  const rules = [
    [/a fila de trabalhos cresce/gi, 'os trabalhos para imprimir acumulam-se'],
    [/\bda fila de trabalhos\b/gi, 'dos trabalhos para imprimir'],
    [/\bna fila de trabalhos\b/gi, 'nos trabalhos para imprimir'],
    [/\ba fila de trabalhos\b/gi, 'os trabalhos para imprimir'],
    [/\bfila de trabalhos\b/gi, 'trabalhos para imprimir'],
    [/\ba fila de trabalho\b/gi, 'o que tenho para imprimir'],
    [/\bfila de trabalho\b/gi, 'o que tenho para imprimir'],
    [/a fila de produção não para/gi, 'os trabalhos para imprimir não param'],
    [/\bda fila de produção\b/gi, 'dos trabalhos para imprimir'],
    [/\ba fila de produção\b/gi, 'os trabalhos para imprimir'],
    [/\bfila de produção\b/gi, 'trabalhos para imprimir'],
    [/\bfilas de espera\b/gi, 'trabalhos à espera de imprimir'],
    [/\bfila bloqueada\b/gi, 'trabalhos parados'],
    [/\bfila desbloqueada\b/gi, 'trabalhos a imprimir'],
  ];
  for (const [re, to] of rules) t = t.replace(re, to);
  return t;
}
function ctdAdFicha({ peca, commPlanIndex, campFull, country }) {
  const proposta = campFull?.proposta_json || {};
  const briefing = campFull?.briefing || {};
  const usps = [].concat(briefing.usps || []).filter(Boolean);
  const pains = [].concat(briefing.pain_points || []).filter(Boolean);
  const certs = [].concat(briefing.certifications || []).filter(Boolean);
  const angles = ctdAnglesForMarket(proposta, country);
  const uspRaw = peca?.body || peca?.subtitulo || '';
  let angle = ctdBestAngle(uspRaw, angles) || ctdAngleByUspIndex(angles, peca);
  if (!angle) {
    const ord = ctdMetaAdOrdinal(proposta.comm_plan, commPlanIndex);
    if (ord != null && angles[ord] && ctdSpecificScore(uspRaw, angles[ord].usp_principal || '') >= 1) angle = angles[ord];
  }
  const baseAngle = ctdAngleByUspIndex(proposta?.messaging_angles, peca);
  const usp = ctdSanitizeProof(uspRaw || angle?.usp_principal || ctdBestMatchStr(peca?.titulo, usps, 1) || '');
  const angleFits = angle && ctdSpecificScore(uspRaw || usp, angle.usp_principal || '') >= 2;
  const painLibrary = (angleFits && String(angle.dor_principal || '').trim())
    ? angle.dor_principal
    : ctdBestPain(uspRaw || usp, pains);
  const pain = ctdSanitizeProof(painLibrary || ctdPainClause(angle?.usp_principal || usp) || '');
  const persona = (angle?.persona && String(angle.persona).length > String(briefing.decision_maker || '').length)
    ? angle.persona
    : (briefing.decision_maker || angle?.persona || '');
  const angulo = ctdAnguloForAd(angle, baseAngle, uspRaw || usp, country);
  const ep = String(proposta.core_promise || briefing.elevator_pitch || '');
  const cert = ctdSanitizeProof(ctdCertForAd(peca, usp, angle, certs, ep) || '');
  const cp = ep.replace(/^["'\s]+|["'\s]+$/g, '');
  const cb = proposta.comparison_baseline;
  const baselineRaw = (cb && typeof cb === 'object') ? (cb.primary || cb.baseline || '') : (cb || '');
  const offer = ctdParseJson(campFull?.commercial_offer) || ctdParseJson(briefing.commercial_offer);
  const baseline = String(baselineRaw || '').replace(/\.+$/, '').trim() || ctdPitchBaseline(ep);
  const corePromise = ctdSanitizeProof(cp);
  const saved = proposta?.copy_base?.pieces?.[String(commPlanIndex)] || {};
  const campSaved = proposta?.copy_base?.campaign || {};
  const pick = (key, fallback) => (Object.prototype.hasOwnProperty.call(saved, key) ? saved[key] : fallback);
  const pickCamp = (key, fallback) => (Object.prototype.hasOwnProperty.call(campSaved, key) ? campSaved[key] : fallback);
  const talk = (s) => String(campFull?.brand_slug || '').toLowerCase() === 'mimaki' ? ctdMimakiTalk(s) : s;
  const uspOut = talk(pick('usp', usp));
  const painOut = talk(pick('pain', pain));
  const anguloOut = talk(pick('angulo', angulo));
  return {
    usp: uspOut,
    angulo: anguloOut,
    pain: painOut,
    persona: pick('persona', persona),
    corePromise: pickCamp('core_promise', corePromise),
    baseline: pickCamp('baseline', baseline),
    cert: pick('cert', cert),
    brief: { usp: uspOut, angulo: anguloOut, pain: painOut, persona, corePromise, baseline, cert },
    offer,
    allowPrice: /preço|barato|€\s*\/\s*m|eur\/m|custa menos|mais competitivo|menos €/i.test(uspRaw),
    adTitle: peca?.titulo || '',
  };
}
function ctdNormTitle(s) {
  return String(s || '').normalize('NFC').trim().toLowerCase().replace(/\s+/g, ' ');
}
function ctdPieceCode(s) {
  const m = String(s || '').normalize('NFC').toUpperCase().match(/\b(?:AD|INT|EM|WA|BL|PC)-\d+\b/);
  return m ? m[0] : '';
}
function ctdNavRowsSameCanal(rows, peca) {
  const canal = ctdCanalLabel(peca || {});
  const group = peca?.waGroup || null;
  const order = ['PT', 'ES', 'FR', 'IT', 'DE', 'EN'];
  return (rows || [])
    .filter(r => r?.peca && !['campanha_paid', 'paid_campaign'].includes(r.peca.content_type))
    .filter(r => ctdCanalLabel(r.peca) === canal)
    .filter(r => !group || r.peca?.waGroup === group)
    .sort((a, b) => {
      const ia = order.indexOf(a.country), ib = order.indexOf(b.country);
      const pa = ia === -1 ? 99 : ia, pb = ib === -1 ? 99 : ib;
      if (pa !== pb) return pa - pb;
      return (a.groupPos || a.idx) - (b.groupPos || b.idx);
    });
}
function ctdListRank(r) {
  if (r?.peca?.waGroup === 'qualificacao') return 1;
  if (r?.peca?.waGroup === 'abertura') return 2;
  if (r?.peca?.waGroup === 'usp') return 3;
  return 0;
}
function ctdWaGrupoTitulo(grupo, country) {
  const cc = country || 'PT';
  if (grupo === 'qualificacao') return `Qualificação LEADS · ${cc}`;
  if (grupo === 'abertura') return `Daily Prospect · ${cc}`;
  if (grupo === 'usp') return `SDR AI · ${cc}`;
  return '';
}
function ctdGroupTitle(r) {
  const wa = ctdWaGrupoTitulo(r?.peca?.waGroup, r?.country || r?.peca?.country);
  if (wa) return wa;
  return `Mercado · ${r.country}`;
}
function ctdPromptChoices(p) {
  let c = p?.sonnet_choices;
  if (typeof c === 'string') {
    try { c = JSON.parse(c); } catch { c = {}; }
  }
  return c && typeof c === 'object' ? c : {};
}
function ctdPromptApp(p) {
  const slug = ctdPromptChoices(p).chosen_application || '';
  return CTD_APP_LABELS[slug] ? slug : '';
}
function ctdPromptSceneLabel(p) {
  return ctdPromptChoices(p).chosen_scene_label || '';
}
function ctdPromptPaletteLabel(p) {
  const raw = ctdPromptChoices(p).chosen_palette_label || '';
  if (raw === 'Nike volt') return 'Lima eléctrico';
  return raw;
}
function ctdPromptLayoutLabel(p) {
  return ctdPromptChoices(p).chosen_layout_label || '';
}
function ctdImageHistory(p) {
  const h = p?.magnific_history;
  return Array.isArray(h) ? h.filter(x => x && x.url) : [];
}
function ctdImageArchive(p) {
  const current = p?.magnific_url || p?.image_url || null;
  const hist = ctdImageHistory(p);
  const out = [];
  const seen = new Set();
  if (current) {
    const fromHist = hist.find(h => h.url === current);
    out.push(fromHist || { url: current, source: 'current', created_at: p?.updated_at || null });
    seen.add(current);
  }
  for (const h of hist) {
    if (!h.url || seen.has(h.url)) continue;
    out.push(h);
    seen.add(h.url);
  }
  return out;
}
function ctdVideoArchive(p) {
  const current = p?.video_url || null;
  const hist = Array.isArray(p?.video_history) ? p.video_history.filter(x => x && x.url) : [];
  const out = [];
  const seen = new Set();
  if (current) {
    const fromHist = hist.find(h => h.url === current);
    out.push(fromHist || { url: current, source: 'current', created_at: p?.updated_at || null });
    seen.add(current);
  }
  for (const h of hist) {
    if (!h.url || seen.has(h.url)) continue;
    out.push(h);
    seen.add(h.url);
  }
  return out;
}
function ctdHistorySourceLabel(source) {
  if (source === 'current') return 'Actual';
  if (source === 'prompt_regen') return 'Prompt anterior';
  if (source === 'edit') return 'Edição';
  if (source === 'replaced') return 'Substituída';
  return 'Geração';
}
const CTD_PROMPT_PT_SECTIONS = [
  ['Enquadramento', 'Género, formato, hora e lugar'],
  ['Assunto', 'O que a fotografia mostra'],
  ['Aplicação', 'Como o filme ou o produto está aplicado'],
  ['Paleta', 'Cores'],
  ['Layout', 'Formas e composição'],
  ['Letra', 'Tipo de letra do claim'],
  ['Headline', 'Frase exacta no criativo'],
  ['Sub', 'Segunda frase no criativo'],
  ['Marca', 'Wordmark e selos'],
  ['Contexto', 'Rua, pessoas, arquitectura'],
  ['Fundo', 'Fundo da peça'],
  ['Ambiente', 'Estúdio'],
  ['Câmara', 'Luz, câmara e lente'],
  ['Exclusões', 'O que a imagem não pode ter'],
];
function ctdParsePromptPt(text) {
  const src = String(text || '').replace(/\r\n/g, '\n');
  const keys = CTD_PROMPT_PT_SECTIONS.map(([k]) => k);
  const re = new RegExp('(?:^|\\n)\\s*(' + keys.join('|') + ')\\s*:', 'gi');
  const hits = [...src.matchAll(re)];
  if (hits.length < 4) return null;
  const byKey = {};
  hits.forEach((hit, i) => {
    const key = keys.find(k => k.toLowerCase() === hit[1].toLowerCase());
    const start = hit.index + hit[0].length;
    const end = i + 1 < hits.length ? hits[i + 1].index : src.length;
    if (key) byKey[key] = src.slice(start, end).trim();
  });
  const optional = new Set(['Letra', 'Fundo', 'Ambiente']);
  return CTD_PROMPT_PT_SECTIONS
    .filter(([key]) => !optional.has(key) || String(byKey[key] || '').trim())
    .map(([key, hint]) => ({ key, hint, value: byKey[key] || '' }));
}
function ctdJoinPromptPt(sections) {
  return sections.map(s => `${s.key}: ${String(s.value || '').trim()}`).join('\n\n');
}
function ctdSetPromptSection(text, key, value) {
  const src = String(text || '').replace(/\r\n/g, '\n');
  const keys = CTD_PROMPT_PT_SECTIONS.map(([k]) => k);
  if (!keys.includes(key)) return text;
  const re = new RegExp('(?:^|\\n)\\s*(' + keys.join('|') + ')\\s*:', 'gi');
  const hits = [...src.matchAll(re)];
  if (hits.length < 4) return text;
  const byKey = {};
  const order = [];
  hits.forEach((hit, i) => {
    const found = keys.find(k => k.toLowerCase() === hit[1].toLowerCase());
    const start = hit.index + hit[0].length;
    const end = i + 1 < hits.length ? hits[i + 1].index : src.length;
    if (found && !order.includes(found)) {
      order.push(found);
      byKey[found] = src.slice(start, end).trim();
    }
  });
  byKey[key] = value;
  if (!order.includes(key)) {
    const at = keys.indexOf(key);
    let insertAt = order.length;
    for (let i = 0; i < order.length; i++) {
      if (keys.indexOf(order[i]) > at) { insertAt = i; break; }
    }
    order.splice(insertAt, 0, key);
  }
  return order.map(k => `${k}: ${byKey[k]}`).join('\n\n');
}
function ctdPromptFp(p) {
  return String(p?.prompt_text_pt || p?.prompt_text || '').replace(/\s+/g, ' ').trim().slice(0, 220);
}
const CTD_APP_LABELS = {
  'retail-graphics': 'Retail',
  'outdoor-billboard': 'Outdoor',
  'architectural': 'Architectural',
  'window-graphics': 'Window',
  'wayfinding': 'Wayfinding',
  'stands-expositions': 'Stands',
  'fleet-graphics': 'Fleet',
  'floor-graphics': 'Floor',
  'print-and-cut': 'Print & Cut',
  'indoor-decor': 'Indoor',
  'texture-detail': 'Textura',
  'product-roll': 'Rolo',
};
function ctdAppLabel(slug) {
  if (!slug) return '';
  return CTD_APP_LABELS[slug] || '';
}
function ctdCopySourceFp(copy) {
  const norm = (s) => String(s || '').replace(/\s+/g, ' ').trim().toLowerCase();
  return ['copy_imagem', 'copy_imagem_sub', 'body', 'cta'].map(k => norm(copy?.[k])).join('\n');
}
function ctdCopyAfterPrompt(prompt, copy) {
  if (!copy?.created_at) return false;
  const promptAt = prompt?.updated_at || prompt?.created_at;
  if (!promptAt) return false;
  return new Date(copy.created_at) > new Date(promptAt);
}
function ctdPromptIsStale(prompt, copy) {
  if (!prompt || !copy || copy.status !== 'aprovado') return false;
  if (ctdPromptChoices(prompt).needs_regen) return true;
  if (ctdCopyAfterPrompt(prompt, copy)) return true;
  const choices = ctdPromptChoices(prompt);
  const norm = (s) => String(s || '').replace(/\s+/g, ' ').trim().toLowerCase();
  const claim = norm(copy.copy_imagem);
  const sub = norm(copy.copy_imagem_sub);
  if (choices.source_copy_fp) {
    const stored = norm(choices.source_copy_fp);
    if (stored === ctdCopySourceFp(copy)) return false;
    if (claim.length >= 8 && stored.includes(claim) && (sub.length < 8 || stored.includes(sub.slice(0, 24)))) return false;
    return true;
  }
  const blob = `${prompt.prompt_text || ''} ${prompt.prompt_text_pt || ''} ${choices.chosen_headline || ''}`.toLowerCase();
  if (claim.length >= 8 && !blob.includes(claim.slice(0, 24))) return true;
  if (sub.length >= 8 && !blob.includes(sub.slice(0, 24))) return true;
  if (copy.created_at && prompt.created_at && new Date(copy.created_at) > new Date(prompt.created_at)) return true;
  return false;
}
function ctdPromptForPeca(prompts, commPlanIndex, peca, copy) {
  const list = Array.isArray(prompts) ? prompts : [];
  if (!list.length) return null;
  const idx = Number(commPlanIndex);
  const canal = peca?.canal || copy?.canal || '';
  const titulo = ctdNormTitle(peca?.titulo || peca?.title || copy?.item_title || '');
  const claim = String(copy?.copy_imagem || peca?.copy_imagem || '').trim();

  // Título da peça é estável; plano_item_index no batch antigo era o índice visual (0,1,2…),
  // não o índice real no comm_plan — por isso NÃO se começa pelo índice.
  if (titulo) {
    const byTitulo = list.find(p => ctdNormTitle(p.titulo) === titulo);
    if (byTitulo) return byTitulo;
  }
  if (claim.length >= 10) {
    const needle = claim.toLowerCase().slice(0, 28);
    const byClaim = list.find(p => {
      const choices = ctdPromptChoices(p);
      const blob = `${p.prompt_text || ''} ${p.prompt_text_pt || ''} ${choices.chosen_headline || ''}`.toLowerCase();
      return blob.includes(needle);
    });
    if (byClaim) return byClaim;
  }
  if (Number.isFinite(idx)) {
    const canalHead = String(canal).split('+')[0];
    const byCanalIdx = list.find(p => Number(p.plano_item_index) === idx && (!canal || !p.canal || p.canal === canal || p.canal === canalHead || String(canal).startsWith(p.canal)));
    if (byCanalIdx) return byCanalIdx;
  }
  return null;
}
function ctdImageForPeca(prompts, imagens, commPlanIndex, peca, copy) {
  const prompt = ctdPromptForPeca(prompts, commPlanIndex, peca, copy);
  const url = prompt?.magnific_url || prompt?.image_url || prompt?.composed_url || null;
  if (url) return url;
  const img = (imagens || []).find(i => prompt && ((i.id && i.id === prompt.id) || (i.url && i.url === prompt.magnific_url)));
  return img?.url || img?.image_url || img?.composed_url || null;
}

// Cross-channel reuse rules — 1 criativo → múltiplas publicações finais.
// Alinhado com skill portal/skills/channels/SKILL.md.
const CTD_REUSE_MAP = {
  meta_ads:          { paid: ['Meta Ads (FB + IG placement)'], organic: ['Facebook', 'Instagram', 'LinkedIn'] },
  facebook_ads:      { paid: ['Facebook Ads'],                 organic: ['Facebook', 'Instagram', 'LinkedIn'] },
  instagram_ads:     { paid: ['Instagram Ads'],                organic: ['Facebook', 'Instagram', 'LinkedIn'] },
  linkedin_ads:      { paid: ['LinkedIn Ads'],                 organic: ['LinkedIn'] },
  google_ads_search: { paid: ['Google Search'],                organic: [] },
  google_ads_display:{ paid: ['Google Display'],               organic: [] },
  google_ads:        { paid: ['Google Ads'],                   organic: [] },
  email:             { paid: [],                                organic: ['Email'] },
  whatsapp:          { paid: [],                                organic: ['WhatsApp Broadcast'] },
  blog:              { paid: [],                                organic: ['Blog site'] },
  website:           { paid: [],                                organic: ['Website'] },
  led:               { paid: [],                                organic: ['Muppi DOOH'] },
  muppi_led:         { paid: [],                                organic: ['Muppi DOOH'] },
};

const ctdCanalBreakdown = (canal, formatosCount = 1) => {
  const rule = CTD_REUSE_MAP[canal] || { paid: [canal], organic: [] };
  const paidCount    = rule.paid.length * Math.max(formatosCount, 1);
  const organicCount = rule.organic.length * Math.max(formatosCount, 1);
  return { rule, paidCount, organicCount, total: paidCount + organicCount };
};

const ctdStatusLabel = (status) => {
  const map = {
    ready_for_production: 'POR INICIAR', pending_production: 'POR INICIAR',
    prompts_pending: 'PROMPTS PENDENTE', prompts_in_progress: 'PROMPTS EM CURSO', prompts_generated: 'PROMPTS GERADOS',
    images_pending: 'IMAGENS PENDENTES', images_in_progress: 'IMAGENS EM CURSO', images_generated: 'IMAGENS GERADAS',
    copies_pending: 'COPIES PENDENTES', copies_in_progress: 'COPIES EM CURSO', copies_generated: 'COPIES GERADOS',
    awaiting_approval: 'AGUARDA APROVAÇÃO', ready_review: 'PARA REVISÃO',
    completed: 'CONCLUÍDA', published: 'PUBLICADA',
  };
  return map[status] || (status ? status.toUpperCase() : 'PENDENTE');
};

// ═══════════════════════════════════════════════════════════════════════════════
// CtdDropdown — dropdown genérico (padrão CampDropdown)
// ═══════════════════════════════════════════════════════════════════════════════
const CtdDropdown = ({ label, value, onChange, options }) => {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  React.useEffect(() => {
    const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, []);
  const active = options.find(o => o.value === value);
  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <button onClick={() => setOpen(!open)} style={{
        background: '#fff', border: '1px solid #e2e8f0', borderRadius: 6,
        padding: '5px 10px', fontSize: 11, cursor: 'pointer',
        display: 'flex', alignItems: 'center', gap: 6,
        color: 'var(--text, #283252)', fontFamily: 'var(--font-body, Inter, sans-serif)',
      }}>
        <span style={{ color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.06em', fontSize: 10, fontFamily: 'var(--font-mono, monospace)' }}>{label}</span>
        <span style={{ fontWeight: 500 }}>{active?.label || '—'}</span>
        <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="6 9 12 15 18 9"/></svg>
      </button>
      {open && (
        <div style={{
          position: 'absolute', top: '100%', left: 0, marginTop: 4, minWidth: 160,
          background: '#fff', border: '1px solid #e2e8f0', borderRadius: 6,
          boxShadow: '0 4px 12px rgba(0,0,0,0.08)', zIndex: 20, maxHeight: 300, overflowY: 'auto',
        }}>
          {options.map(o => (
            <div key={o.value} onClick={() => { onChange(o.value); setOpen(false); }} style={{
              padding: '6px 10px', fontSize: 12, cursor: 'pointer',
              background: o.value === value ? '#f1f5f9' : 'transparent',
              color: 'var(--text, #283252)',
            }}
            onMouseEnter={e => e.currentTarget.style.background = '#f8fafc'}
            onMouseLeave={e => e.currentTarget.style.background = o.value === value ? '#f1f5f9' : 'transparent'}
            >{o.label}</div>
          ))}
        </div>
      )}
    </div>
  );
};

// ═══════════════════════════════════════════════════════════════════════════════
// CtdCollapsedColStrip — coluna compacta (0 items + não expandida)
// ═══════════════════════════════════════════════════════════════════════════════
const CtdCollapsedColStrip = ({ col, count, onExpand }) => (
  <div onClick={onExpand} title={`Expandir ${col.label}`} style={{
    width: 52, borderRadius: 8, cursor: 'pointer', overflow: 'hidden',
    background: '#ffffff', border: '1px solid #e2e8f0',
    display: 'flex', flexDirection: 'column', alignItems: 'center',
    padding: '10px 0 8px', gap: 6, marginBottom: 10,
  }}>
    <div style={{
      width: 24, height: 24, borderRadius: '50%',
      background: count > 0 ? col.color : 'transparent',
      border: `1.5px solid ${col.color}`,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    }}>
      <span style={{ fontSize: 11, fontWeight: 700, color: count > 0 ? '#fff' : col.color, fontFamily: 'monospace', lineHeight: 1 }}>{count}</span>
    </div>
    <div style={{
      writingMode: 'vertical-rl', textOrientation: 'mixed', transform: 'rotate(180deg)',
      fontSize: 10, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase',
      color: col.color, fontFamily: 'var(--font-mono, monospace)', userSelect: 'none',
    }}>
      {col.label}
    </div>
    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke={col.color} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
  </div>
);

// ═══════════════════════════════════════════════════════════════════════════════
// CtdExpandedColHeader — header da coluna quando expandida
// ═══════════════════════════════════════════════════════════════════════════════
const CtdExpandedColHeader = ({ col, count, total, onCollapse, subLine }) => {
  const pct = total > 0 ? count / total : 0;
  return (
    <div style={{
      background: '#ffffff', border: '1px solid #e2e8f0', borderRadius: 8,
      marginBottom: 10, overflow: 'hidden', cursor: 'pointer',
    }} onClick={onCollapse}>
      <div style={{ padding: '10px 14px 0' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
          <div style={{ fontSize: 9.5, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: '#64748b', fontFamily: 'var(--font-mono, monospace)' }}>{col.label}</div>
          <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#475569" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
        </div>
        <div style={{ fontSize: 22, fontWeight: 700, fontFamily: 'var(--font-display, Montserrat, sans-serif)', color: col.color, lineHeight: 1, paddingBottom: subLine ? 4 : 8 }}>{count}</div>
        {subLine && <div style={{ fontSize: 10, color: 'var(--text-muted, #4a5e7a)', fontFamily: 'var(--font-mono, monospace)', paddingBottom: 8 }}>{subLine}</div>}
        <div style={{ height: 3, background: '#f1f5f9', overflow: 'hidden' }}>
          <div style={{ height: '100%', width: `${pct * 100}%`, background: col.color, transition: 'width .4s' }} />
        </div>
      </div>
    </div>
  );
};

// ═══════════════════════════════════════════════════════════════════════════════
// CtdCampanhaCard — card rico (padrão Campanhas)
// ═══════════════════════════════════════════════════════════════════════════════
const CTD_ARCHIVE_OWNERS = ['fabio.costa@digidelta.pt', 'rui.leitao@digidelta.pt', 'joao.paulino@digidelta.pt'];
const ctdCanArchive = (userEmail) => CTD_ARCHIVE_OWNERS.includes(String(userEmail || window.currentUser?.email || '').toLowerCase());

const CtdArchiveModal = ({ campanha, userEmail, onClose, onDone }) => {
  const [impact, setImpact] = React.useState(null);
  const [loadError, setLoadError] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState('');
  React.useEffect(() => {
    let live = true;
    CtdAPI.arquivo(campanha.id).then(d => {
      if (!live) return;
      if (d?.error) setLoadError(d.error);
      else setImpact(d);
    }).catch(e => { if (live) setLoadError(e.message || 'Não consegui ler o impacto.'); });
    return () => { live = false; };
  }, [campanha.id]);
  const confirm = async () => {
    setBusy(true);
    setError('');
    try {
      await CtdAPI.arquivar(campanha.id, { actor_email: userEmail });
      onDone();
    } catch (e) {
      setError(e.message || 'Não arquivei a campanha.');
      setBusy(false);
    }
  };
  const metaOn = Number(impact?.meta_activas || 0);
  const emailsOn = Number(impact?.emails_activos || 0) + Number(impact?.workflows_activos || 0);
  const sessoes = Number(impact?.sessoes || 0);
  const leads = Number(impact?.leads || 0);
  return (
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(15,23,42,.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }} onClick={onClose}>
      <div onClick={e => e.stopPropagation()} style={{ background: '#fff', borderRadius: 14, width: '100%', maxWidth: 480, boxShadow: '0 24px 80px rgba(0,0,0,.2)' }}>
        <div style={{ padding: '18px 22px 12px', borderBottom: '1px solid #f1f5f9' }}>
          <div style={{ fontSize: 15, fontWeight: 700, color: '#1d2e38' }}>Arquivar campanha</div>
          <div style={{ fontSize: 13, color: '#334155', marginTop: 6, lineHeight: 1.4 }}>{campanha.titulo || 'Sem título'}</div>
        </div>
        <div style={{ padding: '16px 22px', display: 'flex', flexDirection: 'column', gap: 10, fontSize: 13, lineHeight: 1.45, color: '#334155' }}>
          <div>Arquivar tira a campanha deste quadro. Copies, imagens, custos e leads ficam nesta campanha. Anúncios na Meta e envios de email param-se à parte, na Activação.</div>
          {impact?.briefing_reabre && (
            <div>É a última campanha deste briefing. O briefing continua Aprovado, sem campanhas activas.</div>
          )}
          {!impact?.briefing_reabre && Number(impact?.outras_campanhas) > 0 && (
            <div>Este briefing tem outras campanhas. Elas ficam.</div>
          )}
          {impact?.sdr_activa && (
            <div>A Digi deixa de usar a ficha em conversas novas.{sessoes > 0 ? ` As ${sessoes} sessões já tidas ficam no histórico.` : ''}</div>
          )}
          {leads > 0 && <div>{leads} lead{leads === 1 ? '' : 's'} continuam a apontar para esta campanha.</div>}
          {metaOn > 0 && (
            <div style={{ padding: '8px 10px', borderRadius: 8, background: '#FEF3C7', color: '#92400E' }}>
              {metaOn === 1 ? 'Há 1 anúncio ACTIVE na Meta.' : `Há ${metaOn} anúncios ACTIVE na Meta.`} O gasto continua até o pausares na Activação.
            </div>
          )}
          {emailsOn > 0 && (
            <div style={{ padding: '8px 10px', borderRadius: 8, background: '#FEF3C7', color: '#92400E' }}>
              {emailsOn === 1 ? 'Há 1 envio de email activo.' : `Há ${emailsOn} envios de email activos.`} Continuam a sair até os parares na Activação.
            </div>
          )}
          {!impact && !loadError && <div style={{ color: '#94a3b8' }}>A ler o que esta campanha tem no ar…</div>}
          {loadError && <div style={{ color: '#991B1B' }}>{loadError}</div>}
          {error && <div style={{ color: '#991B1B' }}>{error}</div>}
        </div>
        <div style={{ padding: '12px 22px 16px', borderTop: '1px solid #f1f5f9', display: 'flex', justifyContent: 'space-between', gap: 8 }}>
          <button type="button" onClick={onClose} className="btn" style={{ fontSize: 12 }} disabled={busy}>Cancelar</button>
          <button type="button" onClick={confirm} disabled={busy || !!loadError} className="btn" style={{ fontSize: 12, background: '#1d2e38', color: '#fff', borderColor: '#1d2e38' }}>
            {busy ? 'A arquivar…' : 'Arquivar'}
          </button>
        </div>
      </div>
    </div>
  );
};

const CTD_ESTADO_ANTERIOR = {
  em_producao: 'Em produção',
  em_activacao: 'Activação',
  publicado: 'Publicado',
  conceito_gerado: 'Conceito',
  aprovado: 'Aprovado',
};

const ctdDataCurta = (v) => {
  if (!v) return '—';
  const d = new Date(v);
  if (Number.isNaN(d.getTime())) return '—';
  return d.toLocaleDateString('pt-PT', { day: '2-digit', month: 'short', year: 'numeric' });
};

const CtdRestaurarModal = ({ campanha, onClose, onDone }) => {
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState('');
  const anterior = campanha.status_antes_arquivo;
  const conhecido = anterior && !['arquivada', 'cancelada'].includes(anterior);
  const noQuadro = ['em_producao', 'em_activacao', 'publicado'].includes(anterior);
  const rotulo = CTD_ESTADO_ANTERIOR[anterior] || anterior;
  const confirm = async () => {
    setBusy(true);
    setError('');
    try {
      const res = await CtdAPI.restaurar(campanha.id);
      onDone({ status: res?.status || anterior, titulo: campanha.titulo, estado_inferido: res?.estado_inferido });
    } catch (e) {
      setError(e.message || 'Não restaurei a campanha.');
      setBusy(false);
    }
  };
  return (
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(15,23,42,.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }} onClick={onClose}>
      <div onClick={e => e.stopPropagation()} style={{ background: '#fff', borderRadius: 14, width: '100%', maxWidth: 460, boxShadow: '0 24px 80px rgba(0,0,0,.2)' }}>
        <div style={{ padding: '18px 22px 12px', borderBottom: '1px solid #f1f5f9' }}>
          <div style={{ fontSize: 15, fontWeight: 700, color: '#1d2e38' }}>Restaurar campanha</div>
          <div style={{ fontSize: 13, color: '#334155', marginTop: 6 }}>{campanha.titulo || 'Sem título'}</div>
        </div>
        <div style={{ padding: '16px 22px', fontSize: 13, lineHeight: 1.5, color: '#334155', display: 'flex', flexDirection: 'column', gap: 10 }}>
          {conhecido && noQuadro && <div>Volta ao quadro de produção, em {rotulo}. O briefing continua Aprovado.</div>}
          {conhecido && !noQuadro && <div>Volta à lista de Campanhas, em {rotulo}. O briefing continua Aprovado.</div>}
          {!conhecido && <div>O estado anterior não ficou registado. Volta à lista de Campanhas, em Conceito. O briefing continua Aprovado.</div>}
          {error && <div style={{ color: '#991B1B' }}>{error}</div>}
        </div>
        <div style={{ padding: '12px 22px 16px', borderTop: '1px solid #f1f5f9', display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
          <button type="button" onClick={onClose} className="btn" style={{ fontSize: 12 }} disabled={busy}>Cancelar</button>
          <button type="button" onClick={confirm} disabled={busy} className="btn" style={{ fontSize: 12, background: '#3859D0', color: '#fff', borderColor: '#3859D0' }}>{busy ? 'A restaurar…' : 'Restaurar'}</button>
        </div>
      </div>
    </div>
  );
};

const CtdArquivoLista = ({ linhas, canRestaurar, onOpen, onRestaurar }) => {
  const th = { textAlign: 'left', fontSize: 10, fontWeight: 600, color: '#64748b', textTransform: 'uppercase', letterSpacing: '.04em', padding: '8px 10px', borderBottom: '1px solid #e2e8f0', whiteSpace: 'nowrap', background: '#fff' };
  const td = { fontSize: 12, padding: '10px', borderBottom: '1px solid #e2e8f0', verticalAlign: 'middle', color: '#283252' };
  if (!linhas.length) {
    return <div style={{ padding: '36px 0', textAlign: 'center', fontSize: 13, color: '#64748b' }}>Ainda não há campanhas arquivadas que tenham passado pela produção.</div>;
  }
  return (
    <div style={{ background: '#fff', border: '1px solid #e2e8f0', borderRadius: 10, overflowX: 'auto' }}>
      <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 760 }}>
        <thead>
          <tr>
            <th style={th}>Campanha</th>
            <th style={th}>Marca</th>
            <th style={th}>Produto</th>
            <th style={th}>Estado anterior</th>
            <th style={th}>Arquivada</th>
            {canRestaurar && <th style={{ ...th, textAlign: 'right' }}>Acção</th>}
          </tr>
        </thead>
        <tbody>
          {linhas.map(c => (
            <tr key={c.id} onClick={() => onOpen(c)} style={{ cursor: 'pointer' }}
              onMouseEnter={e => { e.currentTarget.style.background = '#f8fafc'; }}
              onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}>
              <td style={{ ...td, fontWeight: 650, maxWidth: 320 }}>
                <div>{c.titulo || 'Sem título'}</div>
                {Number(c.irmas_vivas) > 0 && (
                  <div style={{ fontSize: 11, color: '#3859D0', fontWeight: 500, marginTop: 2 }}>
                    Mesmo briefing · mais {Number(c.irmas_vivas)} campanha{Number(c.irmas_vivas) === 1 ? '' : 's'} no quadro
                  </div>
                )}
              </td>
              <td style={td}>{c.brand_name || '—'}</td>
              <td style={td}>{c.commercial_name || c.product_name || '—'}</td>
              <td style={td}>{c.status_antes_arquivo ? (CTD_ESTADO_ANTERIOR[c.status_antes_arquivo] || c.status_antes_arquivo) : '—'}</td>
              <td style={td}>
                <div>{ctdDataCurta(c.arquivada_em)}</div>
                {c.arquivada_por && <div style={{ fontSize: 11, color: '#94a3b8', marginTop: 2 }}>{String(c.arquivada_por).split('@')[0]}</div>}
              </td>
              {canRestaurar && (
                <td style={{ ...td, textAlign: 'right' }} onClick={e => e.stopPropagation()}>
                  <button type="button" className="btn" style={{ height: 28, padding: '0 10px', fontSize: 12 }} onClick={() => onRestaurar(c)}>Restaurar</button>
                </td>
              )}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

const CtdRenameModal = ({ campanha, onClose, onDone }) => {
  const [nome, setNome] = React.useState(campanha.titulo || '');
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState('');
  const inputRef = React.useRef(null);
  React.useEffect(() => {
    inputRef.current?.focus();
    inputRef.current?.select();
  }, []);
  const guardar = async () => {
    const next = nome.trim().replace(/\s+/g, ' ');
    if (!next) { setError('O nome é obrigatório.'); return; }
    if (next === String(campanha.titulo || '').trim()) { onClose(); return; }
    setBusy(true);
    setError('');
    try {
      const row = await CtdAPI.renomear(campanha.id, next);
      onDone(row?.titulo || next);
    } catch (e) {
      setError(e.message || 'Não gravei o nome.');
      setBusy(false);
    }
  };
  return (
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(15,23,42,.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }} onClick={onClose}>
      <div role="dialog" aria-modal="true" aria-label="Editar nome" onClick={e => e.stopPropagation()} style={{ background: '#fff', borderRadius: 14, width: '100%', maxWidth: 440, boxShadow: '0 24px 80px rgba(0,0,0,.2)' }}>
        <div style={{ padding: '18px 22px 12px', borderBottom: '1px solid #f1f5f9' }}>
          <div style={{ fontSize: 15, fontWeight: 700, color: '#1d2e38' }}>Editar nome</div>
        </div>
        <div style={{ padding: '16px 22px' }}>
          <label style={{ display: 'block', fontSize: 12, fontWeight: 700, color: '#475569' }}>
            Nome da campanha
            <input
              ref={inputRef}
              value={nome}
              disabled={busy}
              maxLength={180}
              onChange={e => { setNome(e.target.value); setError(''); }}
              onKeyDown={e => {
                if (e.key === 'Enter') { e.preventDefault(); guardar(); }
                if (e.key === 'Escape') onClose();
              }}
              style={{ display: 'block', width: '100%', boxSizing: 'border-box', marginTop: 6, border: '1px solid #E3E6EB', borderRadius: 8, padding: '9px 10px', fontSize: 14, fontWeight: 600, color: '#112954' }}
            />
          </label>
          {error && <div style={{ color: '#991B1B', fontSize: 12, marginTop: 8 }}>{error}</div>}
        </div>
        <div style={{ padding: '12px 22px 16px', borderTop: '1px solid #f1f5f9', display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
          <button type="button" onClick={onClose} className="btn" style={{ fontSize: 12 }} disabled={busy}>Cancelar</button>
          <button type="button" onClick={guardar} disabled={busy} className="btn" style={{ fontSize: 12, background: '#3859D0', color: '#fff', borderColor: '#3859D0' }}>
            {busy ? 'A guardar…' : 'Guardar'}
          </button>
        </div>
      </div>
    </div>
  );
};

const CtdCardMenu = ({ canArchive, onEdit, onArchive }) => {
  const [open, setOpen] = React.useState(false);
  const [box, setBox] = React.useState(null);
  const btnRef = React.useRef(null);
  const menuRef = React.useRef(null);
  const place = React.useCallback(() => {
    const r = btnRef.current?.getBoundingClientRect();
    if (!r) return;
    setBox({ top: r.bottom + 4, left: Math.max(8, r.right - 168) });
  }, []);
  React.useEffect(() => {
    if (!open) return;
    place();
    const close = (e) => {
      if (btnRef.current?.contains(e.target) || menuRef.current?.contains(e.target)) return;
      setOpen(false);
    };
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', close);
    document.addEventListener('keydown', onKey);
    window.addEventListener('scroll', place, true);
    window.addEventListener('resize', place);
    return () => {
      document.removeEventListener('mousedown', close);
      document.removeEventListener('keydown', onKey);
      window.removeEventListener('scroll', place, true);
      window.removeEventListener('resize', place);
    };
  }, [open, place]);
  const item = (label, onClick) => (
    <button type="button" role="menuitem" onClick={(e) => { e.stopPropagation(); setOpen(false); onClick(); }} style={{
      display: 'block', width: '100%', textAlign: 'left', background: 'transparent', border: 0, borderRadius: 6,
      padding: '8px 10px', fontSize: 13, fontWeight: 600, color: '#112954', cursor: 'pointer', fontFamily: 'inherit',
    }}
      onMouseEnter={e => { e.currentTarget.style.background = '#F4F6FA'; }}
      onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}
    >{label}</button>
  );
  return (
    <>
      <button
        ref={btnRef}
        type="button"
        title="Opções da campanha"
        aria-haspopup="menu"
        aria-expanded={open}
        style={{ background: 'none', border: 0, cursor: 'pointer', color: open ? '#3859D0' : '#94a3b8', fontSize: 16, lineHeight: 1, padding: '2px 6px' }}
        onClick={(e) => { e.stopPropagation(); setOpen(v => !v); }}
      >⋯</button>
      {open && box && ReactDOM.createPortal(
        <div ref={menuRef} role="menu" onClick={e => e.stopPropagation()} style={{
          position: 'fixed', top: box.top, left: box.left, zIndex: 80, width: 168,
          background: '#fff', border: '1px solid #E3E6EB', borderRadius: 8, padding: 4,
          boxShadow: '0 12px 32px rgba(17,41,84,0.14)',
        }}>
          {item('Editar nome', onEdit)}
          {canArchive && item('Arquivar', onArchive)}
        </div>,
        document.body
      )}
    </>
  );
};

const CtdCampanhaCard = ({ campanha, onOpen, colId, canArchive, onArchive, onRename, fresh }) => {
  const brand         = campanha.brand_slug || 'digidelta';
  const color         = ctdBrandColor(brand);
  const totalPlano    = Number(campanha.total_plano_pecas || 0);
  const totalCopies   = Number(campanha.total_copies || totalPlano || 0);
  const copiesAprov   = Number(campanha.copies_aprovadas || 0);
  const totalPrompts  = Number(campanha.total_prompts || 0);
  const promptsAprov  = Number(campanha.prompts_aprovados || 0);
  const totalImagens  = Number(campanha.total_imagens || 0);
  const custo         = campanha.custo_total_usd;
  const ultAct        = campanha.ultima_actividade || campanha.created_at;
  const produtoTitulo = campanha.commercial_name || campanha.product_name;

  // Kick-off da Comunicação Interna
  const kickoffRaw  = campanha.data_kickoff || null;
  const kickoffDate = kickoffRaw ? (() => { const d = new Date(kickoffRaw); d.setHours(0,0,0,0); return d; })() : null;
  const todayMid    = (() => { const d = new Date(); d.setHours(0,0,0,0); return d; })();
  const daysKickoff = kickoffDate ? Math.round((kickoffDate - todayMid) / 86400000) : null;
  const kickoffFmt  = kickoffDate ? kickoffDate.toLocaleDateString('pt-PT', { day: '2-digit', month: 'short' }) : null;
  const kickoffColor = daysKickoff !== null
    ? (daysKickoff < 0 ? '#94a3b8' : daysKickoff <= 3 ? '#dc2626' : daysKickoff <= 7 ? '#f59e0b' : '#94a3b8')
    : '#94a3b8';
  const kickoffLabel = kickoffFmt
    ? (daysKickoff === 0 ? `Kick-off hoje` : daysKickoff > 0 && daysKickoff <= 7 ? `Kick-off em ${daysKickoff}d (${kickoffFmt})` : `Kick-off: ${kickoffFmt}`)
    : null;

  const canais = Array.isArray(campanha.canais) ? campanha.canais : [];

  return (
    <div
      data-campanha-id={campanha.id}
      onClick={() => onOpen(campanha)}
      style={{
        background: '#ffffff', border: '1px solid #e2e8f0',
        borderLeft: `3px solid ${color}`, borderRadius: 8,
        padding: 12, marginBottom: 8, cursor: 'pointer',
        transition: 'box-shadow 150ms',
        boxShadow: fresh ? '0 0 0 2px #3859D0' : 'none',
      }}
      onMouseEnter={e => e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.06)'}
      onMouseLeave={e => e.currentTarget.style.boxShadow = fresh ? '0 0 0 2px #3859D0' : 'none'}
    >
      {/* Header — marca + menu */}
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
        <span style={{ fontSize: 10, fontWeight: 600, letterSpacing: '0.08em', color, textTransform: 'uppercase', fontFamily: 'var(--font-mono, monospace)' }}>
          {(campanha.brand_name || brand).toUpperCase()}
        </span>
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
          <CtdCardMenu
            canArchive={canArchive}
            onEdit={() => onRename?.(campanha)}
            onArchive={() => onArchive?.(campanha)}
          />
        </span>
      </div>

      {/* Título + produto */}
      <div style={{ fontWeight: 700, fontSize: 13, color: 'var(--text, #283252)', marginBottom: 2, lineHeight: 1.3 }}>
        {campanha.titulo || 'Sem título'}
      </div>
      {produtoTitulo && (
        <div style={{ fontSize: 11, color: '#64748b', marginBottom: 4 }}>{produtoTitulo}</div>
      )}
      {(campanha.one_shot === true || String(campanha.one_shot) === 'true') && !campanha.briefing_id && (
        <div style={{ fontSize: 11, color: '#64748b', marginBottom: 4 }}>Sem campanha</div>
      )}
      {Number(campanha.irmas_vivas) > 0 && (
        <div style={{ fontSize: 11, color: '#3859D0', marginBottom: 8 }}>
          Mesmo briefing · mais {Number(campanha.irmas_vivas)} campanha{Number(campanha.irmas_vivas) === 1 ? '' : 's'}
        </div>
      )}
      {campanha.one_shot && ctdOneShotCanais(campanha).length > 0 && (
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, margin: '8px 0' }}>
          {ctdOneShotCanais(campanha).map(label => (
            <span key={label} style={{ fontSize: 10, fontWeight: 700, padding: '2px 6px', borderRadius: 4, background: '#ECF1FB', color: '#3859D0' }}>{label}</span>
          ))}
        </div>
      )}

      {canais.length > 0 ? (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 8 }}>
          {canais.map(ch => (
            <div key={ch.canal} style={{ display: 'flex', justifyContent: 'space-between', gap: 8 }}>
              <span style={{ fontSize: 11, fontWeight: 600, color: '#283252' }}>{ch.canal}</span>
              <span style={{ fontSize: 10, color: ch.fechado ? '#166534' : '#64748b', fontFamily: 'var(--font-mono, monospace)' }}>{ch.rotulo || ch.passo}</span>
            </div>
          ))}
        </div>
      ) : (
        <div style={{ fontSize: 10, color: '#94a3b8', marginBottom: 8 }}>Por começar</div>
      )}

      {/* Footer — kick-off + custo */}
      <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 10, fontFamily: 'var(--font-mono, monospace)', marginTop: 2 }}>
        <span style={{ color: kickoffColor, fontWeight: daysKickoff !== null && daysKickoff >= 0 && daysKickoff <= 7 ? 700 : 400 }}>
          {kickoffLabel || ctdFmtDate(ultAct)}
        </span>
        <span style={{ color: '#94a3b8' }}>{typeof custo === 'number' && custo > 0 ? `$ ${custo.toFixed(2)}` : '—'}</span>
      </div>
    </div>
  );
};

// ═══════════════════════════════════════════════════════════════════════════════
// Placeholders para tabs (implementados nas próximas iterações)
// ═══════════════════════════════════════════════════════════════════════════════
const CtdTabPlaceholder = ({ title, description }) => (
  <div style={{ padding: 48, textAlign: 'center' }}>
    <div className="font-display" style={{ fontSize: 18, color: 'var(--text, #283252)', marginBottom: 8, fontWeight: 600 }}>{title}</div>
    <div style={{ fontSize: 13, color: 'var(--text-muted, #64748b)', maxWidth: 480, margin: '0 auto' }}>{description}</div>
  </div>
);

// Mapping content_type do comm_plan → categoria visual.
// social_post = criativo principal (Meta Ads + reuso orgânico FB/IG/LinkedIn) — 1 por USP.
// campanha_paid = envelope de campanha (metadata para segmentação/copy) — sem criativo próprio.
const CTD_CATEGORIA = {
  briefing_interno: { label: 'COMUNICAÇÃO INTERNA', color: '#94a3b8' },
  campanha_paid:    { label: 'ENVELOPE CAMPANHA',   color: '#64748b' },
  paid_campaign:    { label: 'ENVELOPE CAMPANHA',   color: '#64748b' },
  email_html:       { label: 'EMAIL',               color: '#8b5cf6' },
  email:            { label: 'EMAIL',               color: '#8b5cf6' },
  social_post:      { label: 'META ADS',            color: '#3b82f6' },
  wa_message:       { label: 'WHATSAPP',            color: '#22c55e' },
  wa_template:      { label: 'WHATSAPP',            color: '#22c55e' },
  blog_post:        { label: 'BLOG',                color: '#f59e0b' },
  landing_page:     { label: 'LANDING PAGE',        color: '#0891b2' },
  site:             { label: 'SITE',                color: '#0f766e' },
  led:              { label: 'LED',                 color: '#b45309' },
};
const ctdGetCategoria = (contentType) =>
  CTD_CATEGORIA[contentType] || { label: (contentType || 'OUTRO').toUpperCase().replace(/_/g, ' '), color: '#64748b' };

// Subtítulo explicativo por categoria (aparece ao lado do header do grupo)
const CTD_CATEGORIA_SUBTITLE = {
  'META ADS':             '1 criativo por USP → Meta Ads (Facebook + Instagram pago) + reuso orgânico em Facebook, Instagram, LinkedIn — 4 publicações por criativo',
  'EMAIL':                '1 hero image por email — enviado via Brevo',
  'WHATSAPP':             '1 imagem por template — broadcast segmentado (utility ou marketing template)',
  'BLOG':                 '1 hero image por artigo — publicado no site',
  'LANDING PAGE':         '1 hero image por landing — página dedicada de conversão',
  'ENVELOPE CAMPANHA':    'Metadata (segmentação + copy). O criativo vem dos META ADS acima.',
  'COMUNICAÇÃO INTERNA':  '1 hero 16:9 no email interno — briefing curto para a equipa comercial',
};

// Destinos efectivos por peça — social_post explicita canal principal + reuso orgânico
const ctdGetDestinos = (peca) => {
  const ct = peca.content_type;
  if (ct === 'social_post')  return ['Meta Ads (Facebook + Instagram)', 'Reuso orgânico: Facebook, Instagram, LinkedIn'];
  if (ct === 'campanha_paid' || ct === 'paid_campaign') return ['Envelope da campanha (segmentação + copy) — criativo vem dos META ADS'];
  if (ct === 'email_html' || ct === 'email')            return ['Email'];
  if (ct === 'wa_message'  || ct === 'wa_template')     return ['WhatsApp Broadcast'];
  if (ct === 'briefing_interno')                        return ['Email interno · WhatsApp interno · Portal'];
  if (ct === 'blog_post')                               return ['Blog site'];
  if (ct === 'landing_page')                            return ['Website'];
  return [peca.canal || '—'];
};

// Label do canal principal (usado no badge do PromptCard).
// Substitui 'instagram'/'facebook' isolado por 'Meta Ads' para social_post.
function ctdOneShotCanais(c) {
  const tipos = Array.isArray(c?.plano_tipos) ? c.plano_tipos : [];
  const labels = [];
  tipos.forEach(ct => {
    const label = ctdCanalLabel({ content_type: ct });
    if (label && label !== '—' && !labels.includes(label)) labels.push(label);
  });
  return labels;
}
const ctdCanalLabel = (peca, promptCanal) => {
  const ct = peca?.content_type;
  const rawCanal = (promptCanal || peca?.canal || '');
  // Comunicação interna (email_interno + whatsapp_interno + portal_notificacao ou composite)
  if (ct === 'briefing_interno' || rawCanal.includes('interno') || rawCanal.includes('portal_notificacao')) return 'Comunicação Interna';
  if (ct === 'social_post') return 'Meta Ads';
  if (ct === 'email_html' || ct === 'email')            return 'Email';
  if (ct === 'wa_message'  || ct === 'wa_template')     return 'WhatsApp';
  if (ct === 'blog_post')                               return 'Blog';
  if (ct === 'landing_page')                            return 'Landing';
  if (ct === 'site')                                    return 'Site';
  if (ct === 'led' || rawCanal === 'muppi_led')         return 'LED';
  const raw = rawCanal.split('+')[0].split('_')[0] || '—';
  return raw.charAt(0).toUpperCase() + raw.slice(1);
};

// ─── Autor helper — avatar circular com iniciais + tooltip email ─────────────
const ctdAuthorChip = (email) => {
  if (!email) return <span style={{ fontSize: 11, color: 'var(--text-dim, #94a4c4)' }}>—</span>;
  const nick = String(email).split('@')[0] || '';
  const parts = nick.split('.').filter(Boolean);
  const initials = ((parts[0]?.[0] || '') + (parts[1]?.[0] || '')).toUpperCase() || nick.slice(0,2).toUpperCase();
  const short = parts.slice(0, 2).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(' ') || nick;
  return (
    <span title={email} style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
      <span style={{
        width: 22, height: 22, borderRadius: '50%',
        background: 'var(--ai-500, #3859D0)', color: '#fff',
        fontSize: 9, fontWeight: 700, letterSpacing: '0.02em',
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
        fontFamily: 'var(--font-mono, monospace)', flexShrink: 0,
      }}>{initials}</span>
      <span style={{ fontSize: 11.5, color: 'var(--text, #112954)', whiteSpace: 'nowrap' }}>{short}</span>
    </span>
  );
};

// Perspectives disponíveis para Magnific change-camera (Variations feature)
const CTD_PERSPECTIVES = [
  { key: 'ext_long_shot',   label: 'Ext. long shot' },
  { key: 'long_shot',       label: 'Long shot' },
  { key: 'medium_long',     label: 'Medium long' },
  { key: 'med_closeup',     label: 'Medium closeup' },
  { key: 'closeup',         label: 'Closeup da impressão' },
  { key: 'extreme_closeup', label: 'Closeup do painel' },
  { key: 'low_angle',       label: 'Low angle' },
  { key: 'high_angle',      label: 'High angle' },
  { key: 'back_view',       label: 'Back view' },
  { key: 'eye_level',       label: 'Eye level' },
  { key: 'three_quarter',   label: '3/4 view' },
  { key: 'profile',         label: 'Profile' },
  { key: 'aerial',          label: 'Aerial' },
  { key: 'ots',             label: 'Over the shoulder' },
  { key: 'pov',             label: 'POV' },
];
const CTD_PERSPECTIVES_DEFAULT = ['long_shot', 'three_quarter', 'low_angle', 'high_angle', 'back_view', 'closeup', 'extreme_closeup'];

// Pixels para cada aspect ratio (Nano Banana Pro só aceita 1:1, 4:5, 9:16, 16:9, 4:3, 3:4)
const CTD_FORMAT_PX = {
  '4:5':    '1080×1350',
  '16:9':   '1920×1080',
  '1:1':    '1080×1080',
  '9:16':   '1080×1920',
  '4:3':    '1440×1080',
  '3:4':    '1080×1440',
};

// ─── CtdKpiCards — 4 cards clicáveis, pixel-a-pixel igual ao Copies/Prompts ────
const CtdKpiCards = ({ cards, filterStatus, onFilter }) => (
  <div style={{ display: 'grid', gridTemplateColumns: cards.length > 4 ? 'repeat(auto-fit, minmax(140px, 1fr))' : 'repeat(4, 1fr)', gap: 12, marginBottom: 20 }}>
    {cards.map(card => {
      const isActive = filterStatus === card.key;
      return (
        <div key={card.key}
          onClick={() => onFilter && onFilter(isActive && card.key !== 'all' ? 'all' : card.key)}
          style={{
            background: isActive ? card.activeBg : 'var(--bg-surface, #fff)',
            border: '1px solid ' + (isActive ? card.accent + '50' : 'var(--border, #E3E6EB)'),
            borderRadius: 8, padding: '14px 16px',
            cursor: onFilter ? 'pointer' : 'default',
          }}>
          <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: isActive ? card.accent : 'var(--text-muted)', fontFamily: 'var(--font-mono, monospace)', marginBottom: 6 }}>{card.label}</div>
          <div style={{ fontSize: 28, fontWeight: 700, color: isActive ? card.accent : 'var(--text, #112954)', lineHeight: 1 }}>{card.value}</div>
          <div style={{ marginTop: 10, height: 3, background: isActive ? card.accent : 'var(--border, #E3E6EB)', borderRadius: 2, opacity: isActive ? 1 : 0.35 }} />
        </div>
      );
    })}
  </div>
);

// ─── CtdToolbar — barra de filtros + modelo + bulk actions (padrão universal) ──
const CtdToolbar = ({
  canais = [], countries = [], total = 0, filtered = 0,
  filterCanal, onFilterCanal, filterCountry, onFilterCountry, hideCanal = false,
  models = null,           // null = sem selector | [{ id, label }]
  activeModel, onModel,
  selectedCount = 0, onGenerateSelected, generatingSelected,
  pendingCount = 0, onGeneratePending, generatingPending,
  busy = false,
  rightExtra = null,       // JSX extra à direita (ex: botão APROVAR TODAS)
  onClearFilters = null,
}) => {
  const Icon = window.Icon;
  const countryOptions = countries.map(c => (
    c && typeof c === 'object' ? { value: c.value, label: c.label || c.value } : { value: c, label: c }
  ));
  const filtersActive = (filterCanal && filterCanal !== 'all') || (filterCountry && filterCountry !== 'all');
  return (
    <div style={{ display: 'flex', gap: 8, alignItems: 'center', padding: '10px 0', fontSize: 11, flexWrap: 'wrap', marginBottom: 4 }}>
      {!hideCanal && (
        <CtdDropdown
          label="Canal"
          value={filterCanal}
          onChange={onFilterCanal}
          options={[{ value: 'all', label: 'Todos os canais' }, ...canais.map(c => ({ value: c, label: c }))]}
        />
      )}
      {countryOptions.length > 0 && (
        <CtdDropdown
          label="Mercado"
          value={filterCountry}
          onChange={onFilterCountry}
          options={[{ value: 'all', label: 'Todos mercados' }, ...countryOptions]}
        />
      )}
      {!(hideCanal && countryOptions.length === 0 && filtered === total) && (
        <span style={{ color: 'var(--text-dim,#7a8fa8)', fontSize: 11 }}>{filtered} de {total}</span>
      )}
      {onClearFilters && filtersActive && (
        <button type="button" onClick={onClearFilters}
          style={{ background: 'transparent', border: 0, padding: '4px 2px', fontSize: 11, color: 'var(--color-primary,#3859D0)', cursor: 'pointer', fontFamily: 'var(--font-body, Inter, sans-serif)' }}>
          Limpar filtros
        </button>
      )}
      <div style={{ marginLeft: 'auto', display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
        {models && (
          <div style={{ display: 'flex', gap: 2, padding: '3px', background: 'var(--bg-surface,#fff)', borderRadius: 6, border: '1px solid var(--border-2,#E3E6EB)' }}>
            {models.map(m => {
              const active = activeModel === m.id;
              return <button key={m.id} onClick={() => !busy && onModel(m.id)} disabled={busy}
                style={{ padding: '4px 10px', fontSize: 10, fontWeight: 700, letterSpacing: '0.04em', background: active ? 'var(--color-primary,#3859D0)' : 'transparent', color: active ? '#fff' : 'var(--text-muted,#4a5e7a)', border: 0, borderRadius: 4, cursor: busy ? 'not-allowed' : 'pointer', fontFamily: 'var(--font-mono,monospace)', textTransform: 'uppercase' }}>{m.label}</button>;
            })}
          </div>
        )}
        {onGenerateSelected && (
          <button onClick={onGenerateSelected} disabled={busy || selectedCount === 0} className="btn btn-sm"
            style={{ fontSize: 11, opacity: (busy || selectedCount === 0) ? 0.5 : 1, display: 'inline-flex', alignItems: 'center', gap: 5 }}>
            <Icon name="sparkle" size={11} /> Gerar seleccionados ({selectedCount})
          </button>
        )}
        {onGeneratePending && (
          <button onClick={onGeneratePending} disabled={busy || pendingCount === 0}
            style={{ background: 'var(--color-primary,#3859D0)', color: '#fff', border: 0, padding: '6px 12px', borderRadius: 'var(--radius-sm,6px)', fontSize: 11, fontWeight: 700, cursor: busy ? 'not-allowed' : 'pointer', display: 'inline-flex', alignItems: 'center', gap: 5, opacity: (busy || pendingCount === 0) ? 0.5 : 1 }}>
            <Icon name="sparkle" size={11} /> Gerar todos pendentes ({pendingCount})
          </button>
        )}
        {rightExtra}
      </div>
    </div>
  );
};

// ─── CtdBreadcrumb — navegação inline, cada segmento é linkável ──────────────
// trail: array de { label, onClick?, badge? } (badge = { text, color })
const CtdBreadcrumb = ({ trail = [], onBack, currentPos, totalItems, onPrev, onNext }) => {
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape' && onBack) onBack(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onBack]);
  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',
      background: 'var(--bg-surface, #fff)', borderRadius: 'var(--radius-md, 8px)',
      boxShadow: 'var(--shadow-card, 0 1px 4px rgba(40,41,61,0.04), 0 4px 16px rgba(96,97,112,0.08))',
      border: '1px solid var(--border-1, #ECEFF5)', marginBottom: 14, flexWrap: 'wrap',
    }}>
      {trail.map((seg, i) => {
        const isLast = i === trail.length - 1;
        const clickable = !!seg.onClick && !isLast;
        return (
          <React.Fragment key={i}>
            {seg.badge && (
              <span style={{
                fontSize: 9, fontWeight: 700, letterSpacing: '0.06em',
                padding: '3px 7px', borderRadius: 4,
                background: `${seg.badge.color}18`, color: seg.badge.color,
                fontFamily: 'var(--font-mono, monospace)',
              }}>{seg.badge.text.toUpperCase()}</span>
            )}
            {clickable ? (
              <button onClick={seg.onClick}
                style={{ background: 'transparent', border: 0, cursor: 'pointer', padding: 0, fontSize: 12, color: 'var(--color-primary, #3859D0)', textDecoration: 'none' }}
                onMouseEnter={e => e.currentTarget.style.textDecoration = 'underline'}
                onMouseLeave={e => e.currentTarget.style.textDecoration = 'none'}>
                {seg.label}
              </button>
            ) : (
              <span style={{ fontSize: 12, color: 'var(--text, #112954)', fontWeight: isLast ? 700 : 400 }}>{seg.label}</span>
            )}
            {!isLast && <span style={{ color: 'var(--text-dim, #7a8fa8)', fontSize: 12 }}>›</span>}
          </React.Fragment>
        );
      })}
      {totalItems > 1 && (
        <div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 6 }}>
          <button onClick={onPrev} disabled={!onPrev}
            style={{ background: 'var(--bg-surface,#fff)', border: '1px solid var(--border-2,#E3E6EB)', padding: '2px 10px', borderRadius: 4, fontSize: 14, cursor: onPrev ? 'pointer' : 'not-allowed', opacity: onPrev ? 1 : 0.4, color: 'var(--text,#112954)' }}>←</button>
          <span style={{ fontSize: 12, color: 'var(--text-muted,#4a5e7a)', minWidth: 44, textAlign: 'center' }}>{currentPos} / {totalItems}</span>
          <button onClick={onNext} disabled={!onNext}
            style={{ background: 'var(--bg-surface,#fff)', border: '1px solid var(--border-2,#E3E6EB)', padding: '2px 10px', borderRadius: 4, fontSize: 14, cursor: onNext ? 'pointer' : 'not-allowed', opacity: onNext ? 1 : 0.4, color: 'var(--text,#112954)' }}>→</button>
        </div>
      )}
    </div>
  );
};

const CTD_ENVELOPE_TYPES = ['campanha_paid', 'paid_campaign'];
const CTD_MARKET_ORDER = ['GLOBAL', 'PT', 'ES', 'FR', 'IT', 'DE', 'EN'];
const CTD_CAT_ORDER = ['COMUNICAÇÃO INTERNA', 'META ADS', 'EMAIL', 'WHATSAPP', 'BLOG', 'LANDING PAGE', 'ENVELOPE CAMPANHA'];
const CTD_MARKET_LABEL = {
  GLOBAL: 'TRANSVERSAIS',
  PT: 'PORTUGAL',
  ES: 'ESPANHA',
  FR: 'FRANÇA',
  IT: 'ITÁLIA',
  DE: 'ALEMANHA',
  EN: 'INTERNACIONAL',
};
const CTD_MARKET_SUB = {
  GLOBAL: 'Peças sem prefixo de país — 1 copy é gerada por mercado através de variants_by_market.',
  PT: 'Peças específicas do mercado português.',
  ES: 'Peças específicas do mercado espanhol.',
};
const CTD_CANAL_KPI = {
  'Meta Ads': { accent: '#7C3AED', activeBg: '#F3EEF8' },
  'Email': { accent: '#0891B2', activeBg: '#E0F7FB' },
  'WhatsApp': { accent: '#059669', activeBg: '#ECFDF5' },
  'Blog': { accent: '#D97706', activeBg: '#FEF3C7' },
  'Landing': { accent: '#0E7490', activeBg: '#E0F7FB' },
  'Comunicação Interna': { accent: '#64748B', activeBg: '#F1F5F9' },
  'LED': { accent: '#B45309', activeBg: '#FEF3C7' },
  'Site': { accent: '#1D4ED8', activeBg: '#EFF3FF' },
};

const ctdDetectMarket = (p) => {
  if (p?.country) return String(p.country).toUpperCase();
  const m = String(p?.titulo || '').match(/^\[([A-Z]{2})\]/);
  return m ? m[1] : 'GLOBAL';
};

const ctdSortByOrder = (order) => (a, b) => {
  const ia = order.indexOf(a);
  const ib = order.indexOf(b);
  if (ia !== -1 && ib !== -1) return ia - ib;
  if (ia !== -1) return -1;
  if (ib !== -1) return 1;
  return String(a).localeCompare(String(b));
};

const CTD_OFFER_TYPE = {
  digirent: 'Digirent', printplan: 'PrintPlan', voucher: 'Voucher on Demand',
  direct_discount: 'Desconto Directo', trade_in: 'Trade-In', testing_roll: 'Testing Roll', sample_request: 'Amostra',
};
const CTD_OFFER_TIER = {
  with_deposit: 'Com caução', free: 'Gratuito', standard: 'Standard', premium: 'Premium',
  large: 'Large', enterprise: 'Enterprise', tailor_made: 'Tailor-made',
};

function ctdHumanizePlanText(text) {
  return String(text || '').replace(/\{[^{}]*\}/g, (blob) => {
    try {
      const o = JSON.parse(blob);
      if (!o || typeof o !== 'object' || Array.isArray(o)) return blob;
      const type = o.type ? (CTD_OFFER_TYPE[o.type] || String(o.type).replace(/_/g, ' ')) : '';
      const tier = o.tier ? (CTD_OFFER_TIER[o.tier] || String(o.tier).replace(/_/g, ' ')) : '';
      const details = o.details ? String(o.details).replace(/\s+/g, ' ').trim() : '';
      const parts = [type, tier, details].filter(Boolean);
      return parts.length ? parts.join(' · ') : blob;
    } catch { return blob; }
  }).replace(/\b([a-z]+(?:_[a-z0-9]+)+)\b/g, (_, w) => w.replace(/_/g, ' ')).replace(/\s+/g, ' ').trim();
}

function ctdPlanPieceCopy(peca) {
  const sub = String(peca?.subtitulo || '').replace(/\s+/g, ' ').trim();
  const body = ctdHumanizePlanText(peca?.body);
  const stem = sub.replace(/(?:\.{3}|…)\s*$/, '').trim();
  const subIsCut = !!stem && body.toLowerCase().startsWith(stem.toLowerCase()) && body.length > stem.length + 12;
  let title = sub;
  let extra = body;
  if (!sub || subIsCut) {
    const cut = body.match(/^(.+?[.!?])(?:\s+|$)/);
    const sentence = cut && cut[1].length <= 220 ? cut[1].trim() : '';
    title = sentence || (body.length > 180 ? body.slice(0, 180).replace(/\s+\S*$/, '').trim() : body) || String(peca?.titulo || '—');
    extra = body.slice(title.length).replace(/^[\s.:;—-]+/, '').trim();
  } else if (body.toLowerCase().startsWith(sub.toLowerCase())) {
    extra = body.slice(sub.length).replace(/^[\s.:;—-]+/, '').trim();
  }
  const code = String(peca?.titulo || '').trim();
  return {
    title: title || code || '—',
    code: code && code !== title ? code : '',
    extra,
  };
}

const CtdPlanoPecaCard = ({ peca, catColor, onOpen }) => {
  const [open, setOpen] = React.useState(false);
  const [hover, setHover] = React.useState(false);
  const { title, code, extra } = ctdPlanPieceCopy(peca);
  const canal = ctdCanalLabel(peca);
  const date = peca.planned_date ? ctdFmtDate(peca.planned_date) : '';
  return (
    <div
      role={onOpen ? 'button' : undefined}
      tabIndex={onOpen ? 0 : undefined}
      onClick={onOpen || undefined}
      onKeyDown={onOpen ? (e) => { if (e.key === 'Enter') onOpen(); } : undefined}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      style={{ padding: '10px 12px', background: hover && onOpen ? '#fff' : '#f8fafc', borderRadius: 6, borderLeft: `3px solid ${catColor}`, cursor: onOpen ? 'pointer' : 'default', boxShadow: hover && onOpen ? '0 1px 4px rgba(15,23,42,.06)' : 'none' }}
    >
      <div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 13, fontWeight: 600, color: '#112954', lineHeight: 1.4 }}>{title}</div>
          {code && (
            <div style={{ marginTop: 3, fontSize: 10, color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', lineHeight: 1.4 }}>{code}</div>
          )}
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0, paddingTop: 1 }}>
          {canal && canal !== '—' && (
            <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.04em', color: catColor, background: `${catColor}18`, borderRadius: 4, padding: '3px 6px', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>{canal}</span>
          )}
          {date && (
            <span style={{ fontSize: 10, fontWeight: 600, color: '#475569', background: '#fff', border: '1px solid #e2e8f0', borderRadius: 4, padding: '2px 6px', fontFamily: 'var(--font-mono, monospace)' }}>{date}</span>
          )}
        </div>
      </div>
      {extra && (
        <div style={{ marginTop: 6 }}>
          {open && <div style={{ fontSize: 12, color: '#64748b', lineHeight: 1.5, marginBottom: 4 }}>{extra}</div>}
          <button type="button" onClick={(e) => { e.stopPropagation(); setOpen(v => !v); }}
            style={{ border: 0, background: 'none', padding: 0, fontSize: 11, fontWeight: 600, color: '#3859D0', cursor: 'pointer' }}>
            {open ? 'menos' : 'ver mais'}
          </button>
        </div>
      )}
    </div>
  );
};

const CtdTabContexto = ({ campanha, campFull, onNavigate }) => {
  const [ctxFilterCanal,   setCtxFilterCanal]   = React.useState('all');
  const [ctxFilterCountry, setCtxFilterCountry] = React.useState('all');

  const markets    = (campFull?.estrategia_json?.markets) || [];
  const commPlan   = (campFull?.proposta_json?.comm_plan) || [];
  const loading    = !campFull;
  const brandColor = ctdBrandColor(campanha?.brand_slug);
  const elevatorPitch = String(
    campFull?.briefing?.elevator_pitch
    || campFull?.proposta_json?.core_promise
    || campFull?.elevator_pitch
    || ''
  ).replace(/^["'\s]+|["'\s]+$/g, '').trim();

  const planoPecas = React.useMemo(
    () => commPlan.filter(p => !CTD_ENVELOPE_TYPES.includes(p.content_type)),
    [commPlan],
  );

  // Agrupar por MERCADO e, dentro, por CATEGORIA — já com Canal e Mercado aplicados.
  const pecasByMarket = React.useMemo(() => {
    const marketGroups = new Map();
    planoPecas.forEach(p => {
      if (ctxFilterCanal !== 'all' && ctdCanalLabel(p) !== ctxFilterCanal) return;
      const mk = ctdDetectMarket(p);
      if (ctxFilterCountry !== 'all' && mk !== ctxFilterCountry) return;
      if (!marketGroups.has(mk)) marketGroups.set(mk, new Map());
      const catMap = marketGroups.get(mk);
      const cat = ctdGetCategoria(p.content_type);
      if (!catMap.has(cat.label)) catMap.set(cat.label, { color: cat.color, items: [] });
      catMap.get(cat.label).items.push(p);
    });

    return [...marketGroups.entries()]
      .sort((a, b) => ctdSortByOrder(CTD_MARKET_ORDER)(a[0], b[0]))
      .map(([mk, catMap]) => {
        const cats = [...catMap.entries()].sort((a, b) => ctdSortByOrder(CTD_CAT_ORDER)(a[0], b[0]));
        const totalItems = cats.reduce((n, [, v]) => n + v.items.length, 0);
        return [mk, cats, totalItems];
      });
  }, [planoPecas, ctxFilterCanal, ctxFilterCountry]);

  const canalCounts = React.useMemo(() => {
    const counts = {};
    planoPecas.forEach(p => {
      const label = ctdCanalLabel(p);
      if (!label) return;
      counts[label] = (counts[label] || 0) + 1;
    });
    return counts;
  }, [planoPecas]);

  const canaisPresent = Object.keys(canalCounts).sort(ctdSortByOrder(CTD_CANAL_ORDER));
  const countriesPresent = [...new Set(planoPecas.map(ctdDetectMarket))]
    .sort(ctdSortByOrder(CTD_MARKET_ORDER))
    .map(code => ({ value: code, label: CTD_MARKET_LABEL[code] || code }));
  const filteredCount = pecasByMarket.reduce((n, [, , totalItems]) => n + totalItems, 0);
  const filtersActive = ctxFilterCanal !== 'all' || ctxFilterCountry !== 'all';
  const clearFilters = () => { setCtxFilterCanal('all'); setCtxFilterCountry('all'); };

  const kpiCards = [
    { key: 'all', label: 'TOTAL PEÇAS', value: planoPecas.length, accent: '#3859D0', activeBg: '#EFF3FF' },
    ...canaisPresent.map(canal => {
      const style = CTD_CANAL_KPI[canal] || { accent: '#64748B', activeBg: '#F1F5F9' };
      return { key: canal, label: canal.toUpperCase(), value: canalCounts[canal] || 0, ...style };
    }),
  ];

  if (!campanha) return <CtdTabPlaceholder title="Contexto" description="Sem campanha seleccionada." />;

  return (
    <div style={{ padding: '20px 32px 32px', width: '100%' }}>

      {loading ? (
        <div style={{ padding: 40, textAlign: 'center', color: '#94a3b8', fontSize: 12 }}>A carregar contexto da campanha...</div>
      ) : (markets.length === 0 && commPlan.length === 0) ? (
        <div style={{ padding: 40, textAlign: 'center', fontSize: 12, color: '#94a3b8', fontStyle: 'italic', background: '#f8fafc', borderRadius: 8, border: '1px dashed #cbd5e1' }}>
          Esta campanha ainda não tem estratégia ou plano de comunicação aprovado. Volta a Campanhas → Planeamento → Aprovar.
        </div>
      ) : (
        <>
          {planoPecas.length > 0 && (
            <>
              <CtdToolbar
                canais={canaisPresent} countries={countriesPresent}
                total={planoPecas.length} filtered={filteredCount}
                filterCanal={ctxFilterCanal} onFilterCanal={setCtxFilterCanal}
                filterCountry={ctxFilterCountry} onFilterCountry={setCtxFilterCountry}
                onClearFilters={clearFilters}
              />
              <CtdKpiCards
                filterStatus={ctxFilterCanal}
                onFilter={setCtxFilterCanal}
                cards={kpiCards}
              />
              {elevatorPitch && (
                <div style={{ background: '#fff', border: '1px solid #e2e8f0', borderLeft: '3px solid #3859D0', borderRadius: 8, padding: '14px 16px', marginBottom: 16 }}>
                  <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', marginBottom: 6 }}>ELEVATOR PITCH</div>
                  <CtdClampText text={ctdPlainText(elevatorPitch)} lines={3} />
                </div>
              )}
              <div style={{ background: '#ffffff', border: '1px solid #e2e8f0', borderRadius: 8, padding: 20, marginBottom: 16 }}>

              {filteredCount === 0 ? (
                <div style={{ padding: 28, textAlign: 'center', fontSize: 12, color: '#94a3b8', background: '#f8fafc', borderRadius: 8, border: '1px dashed #cbd5e1' }}>
                  Nenhuma peça corresponde aos filtros seleccionados.
                  {filtersActive && (
                    <div style={{ marginTop: 10 }}>
                      <button type="button" onClick={clearFilters}
                        style={{ background: '#fff', border: '1px solid #cbd5e1', borderRadius: 6, padding: '6px 12px', fontSize: 11, cursor: 'pointer', color: '#3859D0' }}>
                        Limpar filtros
                      </button>
                    </div>
                  )}
                </div>
              ) : (
              <div style={{ display: 'flex', flexDirection: 'column', gap: 22 }}>
                {pecasByMarket.map(([mk, cats, totalItems]) => (
                  <div key={mk} style={{ borderTop: '2px solid #E3E6EB', paddingTop: 14 }}>
                    {/* Header mercado */}
                    <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12, flexWrap: 'wrap' }}>
                      <div style={{
                        display: 'inline-flex', alignItems: 'center', gap: 8,
                        padding: '6px 14px', background: brandColor, color: '#fff',
                        borderRadius: 6,
                      }}>
                        <span style={{
                          fontSize: 12, fontWeight: 800, letterSpacing: '0.08em',
                          fontFamily: 'var(--font-mono, monospace)',
                        }}>
                          {CTD_MARKET_LABEL[mk] || mk}
                        </span>
                        <span style={{
                          fontSize: 10, padding: '2px 6px', background: 'rgba(255,255,255,0.22)',
                          borderRadius: 3, fontFamily: 'var(--font-mono, monospace)',
                        }}>{totalItems} peças</span>
                      </div>
                      {CTD_MARKET_SUB[mk] && (
                        <span style={{ fontSize: 11, color: '#64748b', fontStyle: 'italic', flex: 1, minWidth: 200 }}>
                          {CTD_MARKET_SUB[mk]}
                        </span>
                      )}
                    </div>

                    {/* Categorias dentro do mercado */}
                    <div style={{ display: 'flex', flexDirection: 'column', gap: 14, paddingLeft: 8 }}>
                      {cats.map(([catLabel, { color: catColor, items }]) => (
                        <div key={catLabel}>
                          <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8, flexWrap: 'wrap' }}>
                            <div style={{
                              display: 'inline-flex', alignItems: 'center', gap: 6,
                              padding: '3px 9px', background: `${catColor}18`, borderRadius: 4,
                            }}>
                              <span style={{
                                fontSize: 9, fontWeight: 700, letterSpacing: '0.1em',
                                color: catColor, textTransform: 'uppercase',
                                fontFamily: 'var(--font-mono, monospace)',
                              }}>
                                {catLabel} · {items.length}
                              </span>
                            </div>
                            {CTD_CATEGORIA_SUBTITLE[catLabel] && (
                              <span style={{ fontSize: 10.5, color: '#94a3b8', fontStyle: 'italic', flex: 1, minWidth: 200 }}>
                                {CTD_CATEGORIA_SUBTITLE[catLabel]}
                              </span>
                            )}
                          </div>
                          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                            {items.map((p, i) => {
                              const idx = commPlan.indexOf(p);
                              return (
                                <CtdPlanoPecaCard
                                  key={p.id || `${p.titulo || ''}-${idx >= 0 ? idx : i}`}
                                  peca={p}
                                  catColor={catColor}
                                  onOpen={onNavigate && idx >= 0 ? () => onNavigate({
                                    tab: 'copies',
                                    idx,
                                    country: ctdDetectMarket(p) === 'GLOBAL' ? (p.country || 'PT') : ctdDetectMarket(p),
                                    titulo: p.titulo,
                                    contentType: p.content_type,
                                  }) : undefined}
                                />
                              );
                            })}
                          </div>
                        </div>
                      ))}
                    </div>
                  </div>
                ))}
              </div>
              )}
            </div>
            </>
          )}

        </>
      )}
    </div>
  );
};

// Match prompt → peça do comm_plan (por peca_id, titulo, ou canal+content_type)
const ctdMatchPeca = (prompt, commPlan) => {
  if (!prompt || !commPlan?.length) return null;
  if (prompt.peca_id)         return commPlan.find(p => p.id === prompt.peca_id);
  if (prompt.plano_item_id)   return commPlan.find(p => p.id === prompt.plano_item_id);
  if (prompt.titulo)          return commPlan.find(p => p.titulo === prompt.titulo);
  return commPlan.find(p =>
    (p.canal || '').includes(prompt.canal || '__nope__') && p.content_type === prompt.content_type
  );
};

// Content types que precisam de imagem (têm prompts)
// Fonte dos criativos: social_post (1 por USP) = imagem principal para Meta Ads.
// Cada criativo é REUSADO em Meta Ads (pago) + Facebook/Instagram/LinkedIn (orgânico).
// campanha_paid = envelope (metadata copy/segmentação) — sem prompt próprio.
// ─── CtdPromptCard — 1 prompt ligado à sua peça do comm_plan ──────────────────
const CtdPromptCard = ({ prompt, peca, campanha, userEmail, onRefresh, onOpenGenerate }) => {
  const [editing, setEditing]             = React.useState(false);
  // Prompt PT para o utilizador rever. prompt_text (EN) é o que vai ao Magnific.
  const [text, setText]                   = React.useState(prompt.prompt_text_pt || prompt.prompt_text || prompt.prompt || '');
  const [showEn, setShowEn]               = React.useState(false);
  const [saving, setSaving]               = React.useState(false);
  const [approving, setApproving]         = React.useState(false);
  const [generatingImg, setGeneratingImg] = React.useState(false);
  const [showNegative, setShowNegative]   = React.useState(false);
  const [showFullBody, setShowFullBody]   = React.useState(false);

  React.useEffect(() => { setText(prompt.prompt_text_pt || prompt.prompt_text || prompt.prompt || ''); }, [prompt.id, prompt.prompt_text_pt, prompt.prompt_text, prompt.prompt]);

  const status      = prompt.status || 'pending';
  const isApproved  = ['approved', 'image_generated', 'ready'].includes(status);
  const hasImage    = !!prompt.image_url || status === 'image_generated';
  const format      = prompt.formato || prompt.format || prompt.aspect_ratio || '';
  const contentType = prompt.content_type || peca?.content_type || '';
  const productRefs = prompt.product_refs || [];
  const cat         = ctdGetCategoria(contentType);
  const destinos    = ctdGetDestinos(peca || { content_type: contentType });

  const uspBody = peca?.body || '';
  const uspSnippet = uspBody.length > 150 && !showFullBody ? uspBody.slice(0, 150) + '...' : uspBody;

  const handleSave = async () => {
    setSaving(true);
    try {
      await CtdAPI.editPrompt(campanha.id, prompt.id, { prompt_text_pt: text });
      setEditing(false);
      onRefresh?.();
    } catch (e) { alert('Erro a guardar: ' + (e.message || 'desconhecido')); }
    setSaving(false);
  };

  const handleCancel = () => { setText(prompt.prompt_text_pt || prompt.prompt_text || prompt.prompt || ''); setEditing(false); };

  const handleApprove = async () => {
    setApproving(true);
    try {
      await CtdAPI.approvePrompt(campanha.id, prompt.id, { actor_email: userEmail });
      onRefresh?.();
    } catch (e) { alert('Erro a aprovar: ' + (e.message || 'desconhecido')); }
    setApproving(false);
  };

  const handleGenerateImage = async () => {
    setGeneratingImg(true);
    try {
      await CtdAPI.generateImage(campanha.id, prompt.id, { actor_email: userEmail });
      onRefresh?.();
    } catch (e) { alert('Erro a gerar imagem: ' + (e.message || 'desconhecido')); }
    setGeneratingImg(false);
  };

  return (
    <div style={{
      background: '#ffffff', border: '1px solid #e2e8f0',
      borderLeft: `3px solid ${cat.color}`, borderRadius: 8,
      padding: 14, marginBottom: 10,
    }}>
      {/* Header — CANAL prominente + peça + status */}
      {(() => {
        const canalLabel = ctdCanalLabel(peca, prompt.canal);
        const formatPx = CTD_FORMAT_PX[format] || '';
        return (
          <div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginBottom: 10 }}>
            {/* CANAL badge grande */}
            <div style={{
              padding: '8px 12px', background: cat.color, color: '#ffffff',
              borderRadius: 6, flexShrink: 0, minWidth: 96, textAlign: 'center',
            }}>
              <div style={{ fontSize: 8, letterSpacing: '0.1em', textTransform: 'uppercase', opacity: 0.85, fontFamily: 'var(--font-mono, monospace)' }}>CANAL</div>
              <div style={{ fontSize: 13, fontWeight: 700, marginTop: 2, letterSpacing: '0.02em' }}>{canalLabel}</div>
            </div>

            {/* Peça title + meta */}
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 12, fontWeight: 700, color: '#283252', marginBottom: 2, fontFamily: 'var(--font-mono, monospace)' }}>
                {peca?.titulo || contentType || 'Prompt'}
              </div>
              {peca?.subtitulo && (
                <div style={{ fontSize: 12, color: '#334155', fontWeight: 600, marginBottom: 3 }}>{peca.subtitulo}</div>
              )}
              <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', fontSize: 10, color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>
                <span style={{ color: cat.color, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase' }}>{cat.label}</span>
                {contentType && <span>· {contentType}</span>}
                {format && (
                  <span style={{ padding: '1px 5px', background: '#f1f5f9', borderRadius: 3, color: '#475569' }}>
                    {format}{formatPx && ` · ${formatPx}`}
                  </span>
                )}
                {peca?.planned_date && <span>· {ctdFmtDate(peca.planned_date)}</span>}
              </div>
              {destinos.length > 0 && (
                <div style={{ fontSize: 11, color: '#475569', marginTop: 5 }}>
                  <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', marginRight: 4 }}>PUBLICA EM:</span>
                  {destinos.join(' · ')}
                  {contentType === 'social_post' && <span style={{ color: '#94a3b8', marginLeft: 4, fontSize: 10 }}>(mesmo criativo, 4 publicações: 1 paid + 3 organic)</span>}
                </div>
              )}
            </div>

            {/* Status */}
            <span style={{
              fontSize: 9, fontWeight: 600, letterSpacing: '0.06em', textTransform: 'uppercase',
              padding: '3px 8px', borderRadius: 4, whiteSpace: 'nowrap', flexShrink: 0,
              background: isApproved ? '#dcfce7' : (status === 'rejected' ? '#fee2e2' : '#fef3c7'),
              color:      isApproved ? '#166534' : (status === 'rejected' ? '#991b1b' : '#92400e'),
              fontFamily: 'var(--font-mono, monospace)',
            }}>
              {status === 'image_generated' ? 'IMAGEM GERADA' : (isApproved ? 'APROVADO' : status === 'rejected' ? 'REJEITADO' : 'PENDENTE')}
            </span>
          </div>
        );
      })()}

      {/* USP / mensagem-alvo (do comm_plan.body) */}
      {uspBody && (
        <div style={{ padding: 10, background: `${cat.color}0d`, borderRadius: 6, marginBottom: 10, borderLeft: `2px solid ${cat.color}` }}>
          <div style={{ fontSize: 10, fontWeight: 700, color: cat.color, letterSpacing: '0.08em', textTransform: 'uppercase', marginBottom: 4, fontFamily: 'var(--font-mono, monospace)' }}>
            USP / MENSAGEM-ALVO
          </div>
          <div style={{ fontSize: 11, color: '#334155', lineHeight: 1.55 }}>
            {uspSnippet}
            {uspBody.length > 150 && (
              <button onClick={() => setShowFullBody(!showFullBody)} style={{ background: 'none', border: 0, color: cat.color, fontSize: 10, cursor: 'pointer', marginLeft: 4, padding: 0 }}>
                {showFullBody ? '(menos)' : '(ver mais)'}
              </button>
            )}
          </div>
        </div>
      )}

      {/* Dimensão da imagem — indicador visual do aspect ratio + pixels */}
      <div style={{ display: 'flex', gap: 12, marginBottom: 10 }}>
        <div style={{
          width: 80, flexShrink: 0,
          aspectRatio: format === '9:16' ? '9 / 16' : format === '4:5' ? '4 / 5' : format === '1.91:1' ? '1.91 / 1' : format === '16:9' ? '16 / 9' : '1 / 1',
          background: '#f1f5f9', border: '1px dashed #cbd5e1', borderRadius: 4,
          display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
          fontSize: 9, color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', textAlign: 'center', gap: 2, padding: 4,
        }}>
          <span style={{ fontSize: 8, letterSpacing: '0.06em', textTransform: 'uppercase' }}>DIMENSÃO</span>
          <span style={{ fontWeight: 700, color: '#64748b' }}>{format || '1:1'}</span>
          {CTD_FORMAT_PX[format] && (
            <span style={{ fontSize: 8, color: '#94a3b8' }}>{CTD_FORMAT_PX[format]}</span>
          )}
        </div>

        {/* Product refs — com metadata */}
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 10, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 6, fontFamily: 'var(--font-mono, monospace)' }}>
            REFERÊNCIA DO PRODUTO ({productRefs.length})
          </div>
          {productRefs.length === 0 ? (
            <div style={{ fontSize: 11, color: '#94a3b8', fontStyle: 'italic' }}>Sem referência de produto (Magnific pode inventar geometria)</div>
          ) : (
            <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
              {productRefs.slice(0, 6).map((ref, i) => (
                <div key={i} title={`${ref.name || ref.filename || 'ref'} · ${ref.tipo_shot || 'shot'}`} style={{
                  display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
                }}>
                  <div style={{
                    width: 44, height: 44, borderRadius: 4, overflow: 'hidden',
                    background: '#f1f5f9', border: '1px solid #e2e8f0',
                    display: 'flex', alignItems: 'center', justifyContent: 'center',
                  }}>
                    {ref.url ? (
                      <img src={ref.url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
                    ) : (
                      <span style={{ fontSize: 9, color: '#94a3b8' }}>#{ref.id || i + 1}</span>
                    )}
                  </div>
                  {ref.tipo_shot && (
                    <span style={{ fontSize: 8, color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase', letterSpacing: '0.04em' }}>
                      {ref.tipo_shot}
                    </span>
                  )}
                </div>
              ))}
            </div>
          )}
        </div>
      </div>

      {/* Prompt text */}
      <div style={{ fontSize: 10, fontWeight: 700, color: '#64748b', letterSpacing: '0.08em', textTransform: 'uppercase', marginBottom: 4, fontFamily: 'var(--font-mono, monospace)' }}>
        PROMPT (PT — para revisão)
      </div>
      {editing ? (
        <textarea value={text} onChange={e => setText(e.target.value)} rows={6}
          style={{ width: '100%', boxSizing: 'border-box', padding: 10, border: '1px solid #cbd5e1', borderRadius: 6, fontSize: 12, fontFamily: 'var(--font-body, Inter, sans-serif)', resize: 'vertical', outline: 'none', background: '#fafbfc' }} />
      ) : (
        <div style={{ padding: 10, background: '#f8fafc', borderRadius: 6, fontSize: 12, color: '#334155', lineHeight: 1.55, whiteSpace: 'pre-wrap', maxHeight: 200, overflowY: 'auto' }}>
          {text || <span style={{ color: '#94a3b8' }}>Sem prompt.</span>}
        </div>
      )}

      {/* Prompt EN (colapsável) — o que vai realmente para o Magnific */}
      {prompt.prompt_text && prompt.prompt_text_pt && (
        <div style={{ marginTop: 6 }}>
          <button onClick={() => setShowEn(!showEn)} style={{ background: 'none', border: 0, cursor: 'pointer', fontSize: 10, color: '#64748b', padding: 0, fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>
            {showEn ? '▾' : '▸'} Prompt EN (enviado ao Magnific)
          </button>
          {showEn && (
            <div style={{ marginTop: 6, padding: 8, background: '#f8fafc', borderRadius: 4, fontSize: 11, color: '#475569', fontFamily: 'var(--font-mono, monospace)', maxHeight: 200, overflowY: 'auto', whiteSpace: 'pre-wrap', border: '1px solid #e2e8f0' }}>
              {prompt.prompt_text}
            </div>
          )}
        </div>
      )}

      {/* Negative prompt (collapsible) */}
      {prompt.negative_prompt && (
        <div style={{ marginTop: 8 }}>
          <button onClick={() => setShowNegative(!showNegative)} style={{ background: 'none', border: 0, cursor: 'pointer', fontSize: 10, color: '#64748b', padding: 0, fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>
            {showNegative ? '▾' : '▸'} Negative prompt
          </button>
          {showNegative && (
            <div style={{ marginTop: 6, padding: 8, background: '#fef2f2', borderRadius: 4, fontSize: 11, color: '#7f1d1d', fontFamily: 'var(--font-mono, monospace)', maxHeight: 80, overflowY: 'auto' }}>
              {prompt.negative_prompt}
            </div>
          )}
        </div>
      )}

      {/* Actions */}
      <div style={{ display: 'flex', gap: 6, marginTop: 12, justifyContent: 'flex-end', flexWrap: 'wrap' }}>
        {editing ? (
          <>
            <button onClick={handleCancel} disabled={saving} className="btn btn-sm">Cancelar</button>
            <button onClick={handleSave} disabled={saving || !text.trim()} className="btn btn-sm btn-primary">
              {saving ? 'A guardar...' : 'Guardar'}
            </button>
          </>
        ) : (
          <>
            {!isApproved && <button onClick={() => setEditing(true)} className="btn btn-sm">Editar</button>}
            {onOpenGenerate && !isApproved && (
              <button onClick={() => onOpenGenerate(prompt, peca)} className="btn btn-sm" title="Regerar prompt desta peça">
                ✨ Regerar…
              </button>
            )}
            {!isApproved && (
              <button onClick={handleApprove} disabled={approving} className="btn btn-sm btn-ai">
                {approving ? 'A aprovar...' : '✓ Aprovar'}
              </button>
            )}
            {hasImage && <span style={{ fontSize: 11, color: '#166534', padding: '4px 8px' }}>✓ Imagem pronta</span>}
          </>
        )}
      </div>
    </div>
  );
};

// ─── CtdPromptsApprovalBar — status + acções aprovação (padrão Funil Campanhas) ──
const CtdPromptsApprovalBar = ({ campanhaId, prompts, visualPecas, userEmail, reloadPrompts, onNavigate }) => {
  const [busy, setBusy] = React.useState(null);

  const total       = prompts.length;
  const approved    = prompts.filter(p => p.status === 'approved').length;
  const pendingExec = prompts.filter(p => p.status === 'pending_executive').length;
  const draft       = prompts.filter(p => p.status === 'draft').length;
  const withImage   = prompts.filter(p => p.magnific_url).length;
  const pendingImage= approved - withImage;

  const isApproved  = total > 0 && approved === total;
  const isPending   = pendingExec > 0 && !isApproved;
  const canApprove  = draft > 0 || pendingExec > 0;

  const label = isApproved ? 'Prompts aprovados' : isPending ? 'Enviado para aprovação executiva' : 'Pronto para revisão';
  const bg    = isApproved ? '#E1F7E6' : isPending ? '#FEF3C7' : '#EBEFF9';
  const fg    = isApproved ? '#1F8A52' : isPending ? '#92400E' : '#3859D0';

  const lastApprovedAt = prompts
    .map(p => p.approved_at).filter(Boolean)
    .sort().slice(-1)[0];
  const lastApprovedBy = prompts.find(p => p.approved_at === lastApprovedAt)?.approved_by;

  const missing = visualPecas.length - total;

  const handleSendExec = async () => {
    setBusy('exec');
    try { await CtdAPI.sendPromptsToExec(campanhaId, { actor_email: userEmail }); reloadPrompts?.(); }
    catch (e) { alert(e.message || 'Erro ao enviar para aprovação'); }
    setBusy(null);
  };
  const handleApproveAll = async () => {
    setBusy('approve');
    try { await CtdAPI.approveAllPrompts(campanhaId, { approver_email: userEmail }); reloadPrompts?.(); }
    catch (e) { alert(e.message || 'Erro ao aprovar'); }
    setBusy(null);
  };

  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',
      background: '#fff', borderRadius: 8, boxShadow: '0 1px 3px rgba(17,41,84,.06)',
      border: '1px solid #ECEFF5', marginBottom: 14, flexWrap: 'wrap',
    }}>
      <span style={{
        fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em',
        padding: '3px 8px', borderRadius: 4, background: bg, color: fg, fontFamily: 'var(--font-mono, monospace)',
      }}>{label}</span>
      <span style={{ fontSize: 11, color: '#64748b' }}>
        {total}/{visualPecas.length} prompts
        {missing > 0 && ` · ${missing} em falta`}
        {approved > 0 && ` · ${approved} aprovados`}
        {pendingExec > 0 && ` · ${pendingExec} pendentes`}
        {lastApprovedAt && ` · última aprovação ${new Date(lastApprovedAt).toLocaleString('pt-PT')}${lastApprovedBy ? ` por ${String(lastApprovedBy).split('@')[0]}` : ''}`}
      </span>
      <div style={{ marginLeft: 'auto', display: 'flex', gap: 6, alignItems: 'center' }}>
        {!isApproved && canApprove && !isPending && (
          <button className="btn" style={{ height: 28, padding: '0 14px', fontSize: 12 }} disabled={!!busy} onClick={handleSendExec}>
            {busy === 'exec' ? 'A enviar…' : 'Enviar para Aprovação Executiva'}
          </button>
        )}
        {!isApproved && canApprove && (
          <button className="btn btn-ai" style={{ height: 28, padding: '0 14px', fontSize: 12 }} disabled={!!busy} onClick={handleApproveAll}>
            {busy === 'approve' ? 'A aprovar…' : 'Aprovar Internamente →'}
          </button>
        )}
        {isPending && !canApprove && (
          <span style={{ fontSize: 10, fontWeight: 700, padding: '3px 10px', borderRadius: 99, background: '#FEF3C7', color: '#92400E', fontFamily: 'var(--font-mono, monospace)' }}>
            Aguarda aprovação executiva
          </span>
        )}
        {isApproved && withImage === total && total > 0 && (
          <span style={{ fontSize: 10, fontWeight: 700, padding: '3px 10px', borderRadius: 99, background: '#E1F7E6', color: '#1F8A52', fontFamily: 'var(--font-mono, monospace)' }}>
            ✓ Todas as {total} imagens geradas
          </span>
        )}
        {isApproved && pendingImage > 0 && (
          <button className="btn btn-primary" style={{ height: 28, padding: '0 14px', fontSize: 12 }} onClick={() => onNavigate?.('imagens')}>
            → Ir para Imagens ({pendingImage} por gerar)
          </button>
        )}
      </div>
    </div>
  );
};

// ─── CtdGenProgress — barra de progresso durante geração de prompts (SSE) ─────
const CtdGenProgress = ({ step, total, message }) => {
  const pct = total > 0 ? Math.round((step / total) * 100) : 0;
  return (
    <div style={{
      padding: 16, background: 'linear-gradient(135deg, #eef2ff 0%, #f5f3ff 100%)',
      border: '1px solid #c7d2fe', borderRadius: 8,
    }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10, gap: 12 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0, flex: 1 }}>
          {/* Wave animation */}
          <div style={{ display:'flex', alignItems:'flex-end', gap:2, height:20, flexShrink:0 }}>
            {[0,1,2,3,4].map(function(i) {
              return React.createElement('div', { key:i, style:{
                width:3, height:16, background:'#6366f1', borderRadius:2,
                animation:'ctdWave 1s ease-in-out infinite',
                animationDelay: (i * 0.12) + 's',
                transformOrigin:'bottom',
              }});
            })}
          </div>
          <div style={{ fontSize: 13, color: '#3730a3', fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
            {message || 'A processar...'}
          </div>
        </div>
        <div style={{ fontSize: 12, color: '#6366f1', fontFamily: 'var(--font-mono, monospace)', fontWeight: 600, flexShrink: 0 }}>
          {total > 0 ? `${step}/${total}` : ''}
        </div>
      </div>
      <div style={{ height: 6, background: '#e0e7ff', borderRadius: 3, overflow: 'hidden' }}>
        <div style={{
          width: `${pct}%`, height: '100%',
          background: 'linear-gradient(90deg, #6366f1, #8b5cf6)',
          transition: 'width 0.3s ease',
        }} />
      </div>
      <style>{`@keyframes ctdWave { 0%,100%{transform:scaleY(0.3)} 50%{transform:scaleY(1)} }`}</style>
    </div>
  );
};

function CtdImageHistoryStrip({ prompt, currentUrl, disabled, onRestore, title }) {
  const items = ctdImageArchive(prompt);
  if (!items.length) return null;
  return (
    <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
      <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
        <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>{title || 'Imagens deste anúncio'}</span>
        <span style={{ fontSize: 9, color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>{items.length}</span>
      </div>
      <div style={{ padding: 10, display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6, maxHeight: 320, overflowY: 'auto' }}>
        {items.map((h, i) => {
          const isCurrent = !!(currentUrl && h.url === currentUrl);
          const when = h.created_at ? String(h.created_at).replace('T', ' ').slice(0, 16) : '';
          return (
            <button
              key={`${h.url}-${i}`}
              type="button"
              disabled={disabled || isCurrent}
              onClick={() => { if (!isCurrent) onRestore?.(h.url); }}
              title={isCurrent ? 'Imagem actual no mockup' : `Usar no mockup · ${ctdHistorySourceLabel(h.source)}`}
              style={{
                padding: 0, border: isCurrent ? '2px solid #3859D0' : '1px solid var(--border-2, #E3E6EB)',
                borderRadius: 6, overflow: 'hidden', background: '#111318', cursor: (disabled || isCurrent) ? 'default' : 'pointer',
                opacity: disabled ? 0.55 : 1, textAlign: 'left',
              }}
            >
              <div style={{ aspectRatio: '4 / 5', overflow: 'hidden', position: 'relative' }}>
                <img src={h.url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
                {isCurrent && (
                  <span style={{ position: 'absolute', top: 6, left: 6, fontSize: 8, fontWeight: 800, letterSpacing: '0.08em', textTransform: 'uppercase', background: '#3859D0', color: '#fff', borderRadius: 4, padding: '2px 6px', fontFamily: 'var(--font-mono, monospace)' }}>Actual</span>
                )}
              </div>
              <div style={{ padding: '5px 6px', background: '#fff' }}>
                <div style={{ fontSize: 9, fontWeight: 700, color: isCurrent ? '#3859D0' : '#112954', fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.02em' }}>
                  {isCurrent ? 'Actual' : ctdHistorySourceLabel(h.source)}
                </div>
                {when && (
                  <div style={{ fontSize: 8, color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', marginTop: 1 }}>{when}</div>
                )}
              </div>
            </button>
          );
        })}
      </div>
      {items.length > 1 && (
        <div style={{ padding: '0 12px 10px', fontSize: 10, color: '#94a3b8', lineHeight: 1.35 }}>
          Clica numa versão para a usar no mockup. Nada se apaga.
        </div>
      )}
    </div>
  );
}

// ─── CtdPromptDetailModal — modal idêntico ao CtdGenerateCopyItemModal, adaptado para prompts ──
const CtdPromptDetailModal = ({ campanhaId, row, userEmail, onClose, onDone, onPrev, onNext, currentPos, totalItems, inline = false, campFull = null, brandSlug, brandName, onNavigate }) => {
  const Icon = window.Icon;
  const { peca, prompt: initialPrompt, idx, copy: rowCopy } = row;

  const [prompt, setPrompt]     = React.useState(initialPrompt);
  const [editText, setEditText] = React.useState(initialPrompt?.prompt_text_pt || initialPrompt?.prompt_text || '');
  const [showEn, setShowEn]     = React.useState(false);
  const [modelo]                = React.useState('opus-4-7');
  const [userNote, setUserNote] = React.useState('');
  const [busy, setBusy]         = React.useState(false);
  const [busyImg, setBusyImg]   = React.useState(false);
  const [busyRestore, setBusyRestore] = React.useState(false);
  const [saving, setSaving]     = React.useState(false);
  const [step, setStep]         = React.useState({ step: 0, total: 0, message: '' });
  const [donePayload, setDone]  = React.useState(null);
  const [error, setError]       = React.useState(null);
  // Readiness check
  const [readiness, setReadiness]     = React.useState(null);
  const [showUpload, setShowUpload]   = React.useState(false);
  const [uploading, setUploading]     = React.useState(false);
  const [reopenPrompt, setReopenPrompt] = React.useState(false);
  const fileInputRef                  = React.useRef(null);
  const [appCatalog, setAppCatalog]     = React.useState([]);
  const [appUsed, setAppUsed]           = React.useState([]);
  const [pickApp, setPickApp]           = React.useState('');
  const [pickScene, setPickScene]       = React.useState('');
  const [pickPalette, setPickPalette]   = React.useState('');
  const [pickLayout, setPickLayout]     = React.useState('');
  const [pickLetter, setPickLetter]     = React.useState('');
  const [pickBackdrop, setPickBackdrop] = React.useState('');
  const [pickAmbiente, setPickAmbiente] = React.useState('');
  const [paletteCatalog, setPaletteCatalog] = React.useState([]);
  const [layoutCatalog, setLayoutCatalog]   = React.useState([]);
  const [letterCatalog, setLetterCatalog]   = React.useState([]);
  const [backdropCatalog, setBackdropCatalog] = React.useState([]);
  const [ambienteCatalog, setAmbienteCatalog] = React.useState([]);
  const [styleByApp, setStyleByApp]     = React.useState({});
  const [suggestedApp, setSuggestedApp] = React.useState('');
  const [suggestedScene, setSuggestedScene] = React.useState('');
  const [machineLabel, setMachineLabel] = React.useState('');

  // Ao abrir a modal: carregar estado fresco do prompt (pode ter magnific_url já gerado)
  // + reset state interno para evitar leaking entre navegação prev/next
  React.useEffect(() => {
    setPrompt(initialPrompt);
    setEditText(initialPrompt?.prompt_text_pt || initialPrompt?.prompt_text || '');
    setUserNote('');
    setDone(null);
    setError(null);
    setReopenPrompt(false);
    setStep({ step: 0, total: 0, message: '' });
    const choices = ctdPromptChoices(initialPrompt);
    const existingApp = ctdPromptApp(initialPrompt);
    const existingScene = choices.chosen_scene || '';
    setPickApp(existingApp || '');
    setPickScene(existingScene);
    setPickPalette(choices.chosen_palette || '');
    setPickLayout(choices.chosen_layout || '');
    setPickLetter(choices.chosen_letter || '');
    setPickBackdrop(choices.chosen_backdrop || '');
    setPickAmbiente(choices.chosen_ambiente || '');
    refreshPrompt();
    CtdAPI.promptReadiness(campanhaId, idx)
      .then(r => setReadiness(r))
      .catch(() => setReadiness({ ready: false, ref_image: false, product_name: null, product_slug: null, brand_slug: null, copies: [] }));
    CtdAPI.promptApplication(campanhaId, idx)
      .then(d => {
        setAppCatalog(Array.isArray(d.catalog) ? d.catalog : []);
        setAppUsed(Array.isArray(d.used) ? d.used : []);
        setPaletteCatalog(Array.isArray(d.palettes) ? d.palettes : []);
        setLayoutCatalog(Array.isArray(d.layouts) ? d.layouts : []);
        setLetterCatalog(Array.isArray(d.letters) ? d.letters : []);
        setBackdropCatalog(Array.isArray(d.backdrops) ? d.backdrops : []);
        setAmbienteCatalog(Array.isArray(d.ambientes) ? d.ambientes : []);
        setStyleByApp(d.styles && typeof d.styles === 'object' ? d.styles : {});
        setSuggestedApp(d.suggested_application || '');
        setSuggestedScene(d.suggested_scene || '');
        setMachineLabel(d.machine_label || '');
        const known = (list, id) => Array.isArray(list) && list.some(p => p.id === id);
        setPickApp(prev => prev || d.suggested_application || '');
        setPickScene(prev => prev || d.suggested_scene || '');
        setPickPalette(prev => known(d.palettes, prev) ? prev : (d.suggested_palette || ''));
        setPickLayout(prev => known(d.layouts, prev) ? prev : (d.suggested_layout || ''));
        setPickLetter(prev => known(d.letters, prev) ? prev : (d.suggested_letter || ''));
        setPickBackdrop(prev => known(d.backdrops, prev) ? prev : (d.suggested_backdrop || ''));
        setPickAmbiente(prev => known(d.ambientes, prev) ? prev : (d.suggested_ambiente || ''));
      })
      .catch(() => {});
  }, [campanhaId, idx, initialPrompt?.id]);

  const handleFileUpload = async (e) => {
    const file = e.target.files?.[0];
    if (!file || !readiness?.brand_slug) return;
    const _productSlug = readiness?.product_slug ||
      (readiness?.product_name || 'produto').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
    setUploading(true); setError(null);
    try {
      const fd = new FormData();
      fd.append('image', file);
      fd.append('brand_slug', readiness.brand_slug);
      fd.append('product_slug', _productSlug);
      fd.append('tipo_shot', 'hero');
      const res = await fetch('/api/marketing/producao/equipamento-imagens/upload', { method: 'POST', body: fd });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || 'Erro no upload');
      // Refresh readiness — passar idx para manter copies visíveis
      const r = await CtdAPI.promptReadiness(campanhaId, idx);
      setReadiness(r);
      setShowUpload(false);
    } catch (e) { setError(e.message || 'Erro ao fazer upload'); }
    setUploading(false);
    if (fileInputRef.current) fileInputRef.current.value = '';
  };

  const handleSaveRefUrl = async () => {
    if (!refUrl.trim() || !readiness) return;
    setSavingUrl(true);
    try {
      await CtdAPI.saveRefImageUrl({
        brand_slug: readiness.brand_slug,
        product_slug: readiness.product_slug,
        url: refUrl.trim(),
        tipo_shot: 'hero',
        descricao: `Upload manual — ${readiness.product_name}`,
        quality_score: 70,
      });
      setRefUrl('');
      const r = await CtdAPI.promptReadiness(campanhaId, idx);
      setReadiness(r);
    } catch (e) { setError('Erro ao guardar imagem: ' + e.message); }
    setSavingUrl(false);
  };

  const promptIsStale = ctdPromptIsStale(prompt, rowCopy);
  const isApproved = prompt?.status === 'approved' && !promptIsStale;
  const hasImage   = !!(prompt?.image_url || prompt?.magnific_url);
  const imageUrl   = prompt?.image_url || prompt?.magnific_url || null;
  const canalLabel = ctdCanalLabel(peca);
  const plannedDate = peca?.planned_date || null;
  const isMaterials = ['biond', 'decal', 'sensek', 'alldecor'].includes(String(brandSlug || campFull?.brand_slug || '').toLowerCase());
  const isMimaki = String(brandSlug || campFull?.brand_slug || '').toLowerCase() === 'mimaki';
  const isInternalPrompt = peca?.content_type === 'briefing_interno' || rowCopy?.content_type === 'briefing_interno'
    || String(peca?.canal || rowCopy?.canal || '').includes('interno')
    || String(peca?.canal || rowCopy?.canal || '').includes('portal_notificacao');
  const isMetaPrompt = peca?.content_type === 'social_post' || peca?.content_type === 'meta_ads' || peca?.canal === 'meta_ads' || rowCopy?.canal === 'meta_ads';

  const approvedCopyForImg = (readiness?.copies || []).find(c => c.status === 'aprovado' && c.copy_imagem);
  const activeCopyForUi = rowCopy || approvedCopyForImg || null;
  const mockForm = ctdCopyToPreviewForm(activeCopyForUi);
  const appSlug = ctdPromptApp(prompt);
  const sceneLabel = ctdPromptSceneLabel(prompt);
  const showStyleCard = (isMaterials || isMimaki) && !isInternalPrompt;
  const canEditApp = showStyleCard && (!prompt || !isApproved || reopenPrompt);
  const scenesForPick = (appCatalog.find(c => c.slug === pickApp) || {}).scenes || [];
  const styleForApp = styleByApp[pickApp] || {};
  const palettesForPick = (styleForApp.palettes?.length
    ? styleForApp.palettes.map(id => paletteCatalog.find(p => p.id === id)).filter(Boolean)
    : paletteCatalog);
  const layoutsForPick = (styleForApp.layouts?.length
    ? styleForApp.layouts.map(id => layoutCatalog.find(p => p.id === id)).filter(Boolean)
    : layoutCatalog);
  const writeStyleField = (key, id, catalog) => {
    const line = (catalog.find(item => item.id === id) || {}).line;
    if (!line) return;
    setEditText(prev => ctdSetPromptSection(prev, key, line));
  };
  const onPickApp = (slug) => {
    setPickApp(slug);
    const nextScenes = (appCatalog.find(c => c.slug === slug) || {}).scenes || [];
    setPickScene(prev => nextScenes.some(x => x.id === prev) ? prev : (nextScenes[0]?.id || ''));
    const rule = styleByApp[slug] || {};
    const nextPal = (rule.palettes || []).map(id => paletteCatalog.find(p => p.id === id)).filter(Boolean);
    const nextLay = (rule.layouts || []).map(id => layoutCatalog.find(p => p.id === id)).filter(Boolean);
    const palId = nextPal.some(p => p.id === pickPalette) ? pickPalette : (nextPal[0]?.id || '');
    const layId = nextLay.some(p => p.id === pickLayout) ? pickLayout : (nextLay[0]?.id || '');
    if (palId !== pickPalette) { setPickPalette(palId); writeStyleField('Paleta', palId, nextPal); }
    if (layId !== pickLayout) { setPickLayout(layId); writeStyleField('Layout', layId, nextLay); }
  };
  const appClash = (appUsed || []).find(u => u.slug && u.slug === pickApp);
  const appSelectSt = {
    width: '100%', boxSizing: 'border-box', border: '1px solid var(--border-2, #E3E6EB)',
    borderRadius: 6, padding: '8px 10px', fontSize: 12.5, fontWeight: 600, color: '#112954',
    background: (busy || busyImg || !canEditApp) ? 'var(--bg-subtle, #FAFAFA)' : '#fff',
    outline: 'none', fontFamily: 'var(--font-body, Inter, sans-serif)',
  };
  const chNow = ctdPromptChoices(prompt);
  const imagedFp = chNow.imaged_prompt_fp || '';
  const currentFp = ctdPromptFp(prompt);
  const imageStale = !!(hasImage && (
    promptIsStale ||
    (imagedFp && imagedFp !== currentFp)
  ));
  const showImageUrl = imageUrl || null;
  const imageApproved = !!(prompt?.image_approved_at) && !imageStale && !promptIsStale;
  const needsVideo = false;
  const hasVideo = !!(prompt?.video_url) && !promptIsStale;
  const videoApproved = !!(prompt?.video_approved_at) && !promptIsStale;
  const imageOk = !!(showImageUrl && imageApproved);
  const chainDone = imageOk && (!needsVideo || videoApproved);
  const promptState = (imageOk) || isApproved ? 'done' : prompt ? 'current' : 'wait';
  const imageState = imageOk ? 'done' : (isApproved || imageStale || showImageUrl) ? 'current' : 'wait';
  const imageDetail = imageOk ? 'Aprovada'
    : (showImageUrl && !imageStale) ? 'Gerada · por aprovar'
    : imageStale ? 'Prompt mudou · regenerar'
    : isApproved ? 'Aguarda geração' : 'Depois do prompt';
  const openPieceTab = (tab, extra) => onNavigate?.({
    tab, idx, country: rowCopy?.country || peca?.country || null,
    titulo: peca?.titulo || null, contentType: peca?.content_type || null, ...(extra || {}),
  });

  const resetState = () => {
    setDone(null); setError(null);
    setStep({ step: 0, total: 0, message: '' });
  };
  const handleNav = (navFn) => { if (busy || busyImg || busyRestore) return; resetState(); navFn?.(); };

  const refreshPrompt = async () => {
    try {
      const fresh = await CtdAPI.prompts(campanhaId);
      const list  = Array.isArray(fresh) ? fresh : (fresh?.prompts || []);
      const p = ctdPromptForPeca(list, idx, peca, rowCopy);
      if (p) {
        setPrompt(p);
        setEditText(p.prompt_text_pt || p.prompt_text || '');
        const savedApp = ctdPromptApp(p);
        const savedScene = ctdPromptChoices(p).chosen_scene;
        if (savedApp) setPickApp(savedApp);
        if (savedScene) setPickScene(savedScene);
      }
      return p || null;
    } catch (e) { console.error('[PromptModal] refresh error:', e); return null; }
  };

  const handleGenerate = async () => {
    setBusy(true); resetState();
    setStep({ step: 0, total: 1, message: `A iniciar (${modelo})…` });
    try {
      await CtdAPI.generatePromptItem(campanhaId, idx,
        { actor_email: userEmail, modelo: 'opus-4-7', user_note: userNote || undefined, chosen_application: pickApp || undefined, chosen_scene: pickScene || undefined, chosen_palette: pickPalette || undefined, chosen_layout: pickLayout || undefined, chosen_letter: pickLetter || undefined, chosen_backdrop: pickBackdrop || undefined, chosen_ambiente: pickAmbiente || undefined },
        (s, t, m) => setStep({ step: s, total: t, message: m }));
      await refreshPrompt();
      setDone({ ok: true });
      onDone?.();
    } catch (e) { setError(e.message || 'Erro ao gerar prompt'); }
    setBusy(false);
  };

  const handleSave = async () => {
    if (!prompt?.id) return;
    setSaving(true);
    try {
      await CtdAPI.editPrompt(campanhaId, prompt.id, { prompt_text_pt: editText.trim() });
      setPrompt(p => ({ ...p, prompt_text_pt: editText.trim() }));
    } catch (e) { setError(e.message); }
    setSaving(false);
  };

  const handleApprove = async () => {
    if (!prompt?.id) return;
    setBusyImg(true); setError(null);
    setStep({ step: 1, total: 1, message: 'A aprovar prompt…' });
    try {
      if (editText.trim() !== (prompt.prompt_text_pt || ''))
        await CtdAPI.editPrompt(campanhaId, prompt.id, { prompt_text_pt: editText.trim() });
      await CtdAPI.approvePrompt(campanhaId, prompt.id, { actor_email: userEmail });
      await refreshPrompt();
      onDone?.();
    } catch (e) { setError(e.message || 'Erro ao aprovar'); }
    setBusyImg(false);
    setStep({ step: 0, total: 0, message: '' });
  };

  const handleGenerateImage = async () => {
    if (!prompt?.id) return;
    setBusyImg(true); setError(null);
    try {
      let p = prompt;
      if (promptIsStale) {
        setStep({ step: 1, total: 2, message: 'A actualizar prompt com a copy nova…' });
        await CtdAPI.generatePromptItem(campanhaId, idx, { actor_email: userEmail, modelo: 'opus-4-7', chosen_application: pickApp || undefined, chosen_scene: pickScene || undefined, chosen_palette: pickPalette || undefined, chosen_layout: pickLayout || undefined, chosen_letter: pickLetter || undefined, chosen_backdrop: pickBackdrop || undefined, chosen_ambiente: pickAmbiente || undefined }, () => {});
        p = await refreshPrompt();
        if (!p?.id) throw new Error('Prompt desta peça não encontrado após regeneração');
        await CtdAPI.approvePrompt(campanhaId, p.id, { actor_email: userEmail });
        setStep({ step: 2, total: 2, message: 'A gerar imagem… (60-120s)' });
      } else {
        if (p.status !== 'approved') {
          await CtdAPI.approvePrompt(campanhaId, p.id, { actor_email: userEmail });
        }
        setStep({ step: 1, total: 1, message: 'A gerar imagem… (60-120s)' });
      }
      const result = await CtdAPI.generateImage(campanhaId, p.id, { actor_email: userEmail },
        (msg) => setStep(s => ({ ...s, message: msg })));
      const url = result?.url;
      if (!url) throw new Error('A geração terminou sem URL de imagem');
      const nextChoices = { ...ctdPromptChoices(p), imaged_prompt_fp: ctdPromptFp(p) };
      setPrompt(pp => ({
        ...pp, id: p.id, magnific_url: url, image_url: url, status: 'approved',
        sonnet_choices: nextChoices,
        prompt_text: p.prompt_text, prompt_text_pt: p.prompt_text_pt,
        magnific_history: result?.history || pp.magnific_history,
      }));
      await refreshPrompt();
      onDone?.();
    } catch (e) { setError(e.message || 'Erro ao gerar imagem'); }
    setBusyImg(false);
    setStep({ step: 0, total: 0, message: '' });
  };

  const closeReopen = () => {
    setReopenPrompt(false);
    setUserNote('');
    setEditText(prompt?.prompt_text_pt || prompt?.prompt_text || '');
    const choices = ctdPromptChoices(prompt);
    setPickApp(ctdPromptApp(prompt) || '');
    setPickScene(choices.chosen_scene || '');
    setPickPalette(choices.chosen_palette || '');
    setPickLayout(choices.chosen_layout || '');
    setPickLetter(choices.chosen_letter || '');
    setPickBackdrop(choices.chosen_backdrop || '');
    setPickAmbiente(choices.chosen_ambiente || '');
  };

  const styleSelects = (
    <>
      {!!machineLabel && (
        <div style={{ fontSize: 11, fontWeight: 600, color: '#64748B', marginBottom: 8 }}>{machineLabel}</div>
      )}
      <label style={{ display: 'block', fontSize: 10, fontWeight: 700, color: '#64748B', marginBottom: 4 }}>Aplicação</label>
      <select value={pickApp} disabled={busy || busyImg || busyRestore}
        onChange={e => onPickApp(e.target.value)}
        style={{ ...appSelectSt, marginBottom: 10 }}>
        {!pickApp && <option value="">Escolher…</option>}
        {(appCatalog.length ? appCatalog : (isMimaki ? [] : Object.keys(CTD_APP_LABELS).map(slug => ({ slug, label: CTD_APP_LABELS[slug], scenes: [] })))).map(c => (
          <option key={c.slug} value={c.slug}>{c.label}</option>
        ))}
      </select>
      {isMimaki && !appCatalog.length ? (
        <div style={{ fontSize: 12, color: '#64748B', lineHeight: 1.4 }}>Esta campanha não tem ficha de aplicações.</div>
      ) : null}
      {scenesForPick.length > 0 && (
        <>
          <label style={{ display: 'block', fontSize: 10, fontWeight: 700, color: '#64748B', marginBottom: 4 }}>Cena</label>
          <select value={pickScene} disabled={busy || busyImg || busyRestore || !pickApp}
            onChange={e => setPickScene(e.target.value)}
            style={{ ...appSelectSt, marginBottom: 10 }}>
            {!pickScene && <option value="">Escolher…</option>}
            {scenesForPick.map(s => (
              <option key={s.id} value={s.id}>{s.label}</option>
            ))}
          </select>
        </>
      )}
      <label style={{ display: 'block', fontSize: 10, fontWeight: 700, color: '#64748B', marginBottom: 4 }}>Paleta</label>
      <select value={pickPalette} disabled={busy || busyImg || busyRestore || !palettesForPick.length}
        onChange={e => { const id = e.target.value; setPickPalette(id); writeStyleField('Paleta', id, palettesForPick); }}
        style={{ ...appSelectSt, marginBottom: 10 }}>
        {!pickPalette && <option value="">Escolher…</option>}
        {palettesForPick.map(p => (
          <option key={p.id} value={p.id}>{p.label}</option>
        ))}
      </select>
      <label style={{ display: 'block', fontSize: 10, fontWeight: 700, color: '#64748B', marginBottom: 4 }}>Layout</label>
      <select value={pickLayout} disabled={busy || busyImg || busyRestore || !layoutsForPick.length}
        onChange={e => { const id = e.target.value; setPickLayout(id); writeStyleField('Layout', id, layoutsForPick); }}
        style={appSelectSt}>
        {!pickLayout && <option value="">Escolher…</option>}
        {layoutsForPick.map(p => (
          <option key={p.id} value={p.id}>{p.label}</option>
        ))}
      </select>
      {isMaterials && letterCatalog.length > 0 && (
        <>
          <label style={{ display: 'block', fontSize: 10, fontWeight: 700, color: '#64748B', margin: '10px 0 4px' }}>Letra</label>
          <select value={pickLetter} disabled={busy || busyImg || busyRestore}
            onChange={e => { const id = e.target.value; setPickLetter(id); writeStyleField('Letra', id, letterCatalog); }}
            style={appSelectSt}>
            {!pickLetter && <option value="">Escolher…</option>}
            {letterCatalog.map(p => (
              <option key={p.id} value={p.id}>{p.label}</option>
            ))}
          </select>
        </>
      )}
      {isMimaki && backdropCatalog.length > 0 && (
        <>
          <label style={{ display: 'block', fontSize: 10, fontWeight: 700, color: '#64748B', margin: '10px 0 4px' }}>Fundo</label>
          <select value={pickBackdrop} disabled={busy || busyImg || busyRestore}
            onChange={e => { const id = e.target.value; setPickBackdrop(id); writeStyleField('Fundo', id, backdropCatalog); }}
            style={appSelectSt}>
            {!pickBackdrop && <option value="">Escolher…</option>}
            {backdropCatalog.map(p => (
              <option key={p.id} value={p.id}>{p.label}</option>
            ))}
          </select>
          <label style={{ display: 'block', fontSize: 10, fontWeight: 700, color: '#64748B', margin: '10px 0 4px' }}>Ambiente</label>
          <select value={pickAmbiente} disabled={busy || busyImg || busyRestore}
            onChange={e => { const id = e.target.value; setPickAmbiente(id); writeStyleField('Ambiente', id, ambienteCatalog); }}
            style={appSelectSt}>
            {!pickAmbiente && <option value="">Escolher…</option>}
            {ambienteCatalog.map(p => (
              <option key={p.id} value={p.id}>{p.label}</option>
            ))}
          </select>
        </>
      )}
      {appClash && (
        <div style={{ marginTop: 8, fontSize: 11, color: '#92400E', background: '#FFFBEB', border: '1px solid #FDE68A', borderRadius: 6, padding: '7px 8px', lineHeight: 1.35 }}>
          Já usada noutro anúncio{appClash.titulo ? ` · ${String(appClash.titulo).replace(/\s+/g, ' ').slice(0, 42)}` : ''}. Podes manter.
        </div>
      )}
      <div style={{ marginTop: 8, fontSize: 11, color: '#64748B', lineHeight: 1.4 }}>A escolha actualiza já o campo do prompt. A próxima geração usa esta escolha.</div>
    </>
  );

  const generateForm = (
    <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
      <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
        <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--text-dim, #94a3b8)', fontFamily: 'var(--font-mono, monospace)' }}>
          {prompt ? 'Alterar prompt' : 'Gerar prompt'}
        </span>
        <span style={{ fontSize: 10, fontWeight: 700, padding: '3px 8px', borderRadius: 4, background: '#ECF1FB', color: '#3859D0', fontFamily: 'var(--font-mono, monospace)' }}>OPUS 4.7</span>
      </div>
      <div style={{ padding: 14 }}>
        {isApproved && (
          <div style={{ fontSize: 11, color: '#92400E', background: '#FFFBEB', border: '1px solid #FDE68A', borderRadius: 6, padding: '8px 10px', lineHeight: 1.4, marginBottom: 10 }}>
            Regenerar o prompt volta a rascunho. A imagem actual fica no arquivo, na tab Imagens.
          </div>
        )}
        <textarea value={userNote} onChange={e => setUserNote(e.target.value)} disabled={busy || busyImg || busyRestore}
          placeholder={isInternalPrompt ? 'Ex: "Hero 16:9, a gráfica a testar o filme."' : 'Ex: "Cena de farmácia, não boutique."'}
          style={{ width: '100%', resize: 'vertical', boxSizing: 'border-box', border: '1px solid var(--border-2, #E3E6EB)', borderRadius: 6, padding: '8px 10px', fontSize: 12, lineHeight: 1.55, color: 'var(--text, #112954)', background: busy ? 'var(--bg-subtle, #FAFAFA)' : '#fff', outline: 'none', fontFamily: 'var(--font-body, Inter, sans-serif)', minHeight: 48, marginBottom: 10 }} />
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
          <button onClick={handleGenerate} disabled={busy || busyImg || saving}
            style={{ background: 'transparent', color: 'var(--color-primary, #3859D0)', border: '1px solid var(--color-primary, #3859D0)', padding: 8, borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: busy ? 'not-allowed' : 'pointer', opacity: busy||busyImg||saving ? 0.5 : 1, gridColumn: prompt ? 'auto' : '1 / -1' }}>
            {busy ? 'A gerar…' : (prompt ? 'Regerar prompt' : 'Gerar')}
          </button>
          {prompt && (
            <button onClick={handleSave} disabled={busy || busyImg || saving}
              style={{ background: 'transparent', color: 'var(--text, #112954)', border: '1px solid var(--border-2, #E3E6EB)', padding: 8, borderRadius: 6, fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>
              {saving ? 'A guardar…' : 'Guardar'}
            </button>
          )}
        </div>
        {prompt && !isApproved && (
          <button onClick={handleApprove} disabled={busy || busyImg || saving}
            style={{ marginTop: 6, width: '100%', background: '#1F8A52', color: '#fff', border: 0, padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: 'pointer' }}>
            {busyImg ? 'A aprovar…' : 'Aprovar prompt'}
          </button>
        )}
        {!isMaterials && (
          <div style={{ marginTop: 10 }}>
            <input ref={fileInputRef} type="file" accept="image/jpeg,image/png,image/webp" style={{ display: 'none' }} onChange={handleFileUpload} />
            <button onClick={() => fileInputRef.current?.click()} disabled={uploading}
              style={{ background: 'none', border: 'none', padding: 0, fontSize: 11, fontWeight: 600, color: '#3859D0', cursor: 'pointer' }}>
              {uploading ? 'A carregar…' : readiness?.ref_image ? 'Trocar foto do produto' : 'Carregar foto do produto'}
            </button>
          </div>
        )}
      </div>
    </div>
  );

  const _cardEl = (
    <>
      <div onClick={inline ? undefined : e => e.stopPropagation()} style={inline ? {
        background: 'transparent',
        display: 'flex', flexDirection: 'column', width: '100%',
      } : {
        background: 'var(--bg-surface, #fff)', borderRadius: 'var(--radius-lg, 10px)',
        maxWidth: 1240, width: '94vw', maxHeight: '92vh', overflowY: 'auto',
        display: 'flex', flexDirection: 'column',
        boxShadow: 'var(--shadow-huge, 0 20px 60px rgba(0,0,0,0.3))',
      }}>
        {/* Header — só em modo modal (inline usa CtdBreadcrumb externo) */}
        {!inline && (
        <div style={{ padding: '16px 20px', borderBottom: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12, background: 'var(--bg-subtle, #FAFAFA)', position: 'sticky', top: 0, zIndex: 2 }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.08em', color: 'var(--color-primary, #3859D0)', fontFamily: 'var(--font-mono, monospace)', marginBottom: 4 }}>
              {prompt ? 'EDITAR / REGERAR PROMPT' : 'GERAR PROMPT'} · {canalLabel.toUpperCase()}
              {plannedDate && ` · ${ctdFmtDate(plannedDate)}`}
            </div>
            <div style={{ fontSize: 15, fontWeight: 700, color: 'var(--text, #112954)', lineHeight: 1.3, fontFamily: 'var(--font-mono, monospace)' }}>
              {peca.titulo || `Peça #${idx + 1}`}
            </div>
            {peca.subtitulo && (
              <div className="font-display" style={{ fontSize: 14, color: 'var(--text-muted, #4a5e7a)', marginTop: 4, fontWeight: 600 }}>
                {peca.subtitulo}
              </div>
            )}
          </div>
          {/* Navegação ← N/M → */}
          {totalItems > 1 && (
            <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginRight: 8 }}>
              <button onClick={() => handleNav(onPrev)} disabled={!onPrev || busy || busyImg}
                style={{ background: 'none', border: '1px solid var(--border-2,#E3E6EB)', borderRadius: 4, padding: '4px 10px', cursor: (!onPrev||busy||busyImg)?'not-allowed':'pointer', opacity: (!onPrev||busy||busyImg)?0.4:1, fontSize: 14 }}>←</button>
              <span style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted,#4a5e7a)', fontFamily: 'var(--font-mono,monospace)', minWidth: 44, textAlign: 'center' }}>{currentPos} / {totalItems}</span>
              <button onClick={() => handleNav(onNext)} disabled={!onNext || busy || busyImg}
                style={{ background: 'none', border: '1px solid var(--border-2,#E3E6EB)', borderRadius: 4, padding: '4px 10px', cursor: (!onNext||busy||busyImg)?'not-allowed':'pointer', opacity: (!onNext||busy||busyImg)?0.4:1, fontSize: 14 }}>→</button>
            </div>
          )}
          <button onClick={onClose} disabled={busy || busyImg} style={{ background: 'none', border: 0, cursor: busy||busyImg ? 'not-allowed' : 'pointer', opacity: busy||busyImg ? 0.4 : 1 }}>
            <Icon name="x" size={20} />
          </button>
        </div>
        )}

        {/* Body — 3 colunas: pipeline · prompt · mockup */}
        <div style={{ padding: inline ? 0 : 20 }}>
          <div style={{ display: 'grid', gridTemplateColumns: isInternalPrompt ? 'minmax(292px, 320px) minmax(0, 1fr)' : 'minmax(292px, 320px) minmax(0, 1fr) 300px', gap: 20, alignItems: 'start' }}>

          {/* Esquerda */}
          <div style={{ minWidth: 0, display: 'flex', flexDirection: 'column', gap: 12 }}>
            {isApproved && reopenPrompt && (
              <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
                <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
                  <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--text-dim, #94a3b8)', fontFamily: 'var(--font-mono, monospace)' }}>Alterar prompt</span>
                  <span style={{ fontSize: 10, fontWeight: 700, padding: '3px 8px', borderRadius: 4, background: '#ECF1FB', color: '#3859D0', fontFamily: 'var(--font-mono, monospace)' }}>OPUS 4.7</span>
                </div>
                <div style={{ padding: 14 }}>
                  {styleSelects}
                  <div style={{ fontSize: 11, color: '#92400E', background: '#FFFBEB', border: '1px solid #FDE68A', borderRadius: 6, padding: '8px 10px', lineHeight: 1.4, margin: '12px 0 10px' }}>
                    Regenerar o prompt volta a rascunho. A imagem actual fica no arquivo, na tab Imagens.
                  </div>
                  {(!showImageUrl || imageStale) && (
                    <button type="button" onClick={() => {
                      if (isMetaPrompt) {
                        openPieceTab('imagens', { generate: true });
                        return;
                      }
                      handleGenerateImage();
                    }} disabled={busy || busyImg || saving}
                      style={{ width: '100%', marginBottom: 10, background: '#3859D0', color: '#fff', border: 0, padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: (busy || busyImg) ? 'not-allowed' : 'pointer', opacity: (busy || busyImg) ? 0.55 : 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 7 }}>
                      <Icon name="sparkle" size={13} /> {busyImg ? 'A gerar…' : 'Gerar imagem'}
                    </button>
                  )}
                  <textarea value={userNote} onChange={e => setUserNote(e.target.value)} disabled={busy || busyImg || busyRestore}
                    placeholder={isInternalPrompt ? 'Ex: "Hero 16:9, a gráfica a testar o filme."' : 'Ex: "Cena de farmácia, não boutique."'}
                    style={{ width: '100%', resize: 'vertical', boxSizing: 'border-box', border: '1px solid var(--border-2, #E3E6EB)', borderRadius: 6, padding: '8px 10px', fontSize: 12, lineHeight: 1.55, color: 'var(--text, #112954)', background: '#fff', outline: 'none', fontFamily: 'var(--font-body, Inter, sans-serif)', minHeight: 48, marginBottom: 10 }} />
                  <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
                    <button onClick={handleGenerate} disabled={busy || busyImg || saving}
                      style={{ background: 'transparent', color: 'var(--color-primary, #3859D0)', border: '1px solid var(--color-primary, #3859D0)', padding: 8, borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: busy ? 'not-allowed' : 'pointer', opacity: busy || busyImg || saving ? 0.5 : 1 }}>
                      {busy ? 'A gerar…' : 'Regerar prompt'}
                    </button>
                    <button onClick={handleSave} disabled={busy || busyImg || saving}
                      style={{ background: 'transparent', color: 'var(--text, #112954)', border: '1px solid var(--border-2, #E3E6EB)', padding: 8, borderRadius: 6, fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>
                      {saving ? 'A guardar…' : 'Guardar'}
                    </button>
                  </div>
                  <button type="button" onClick={closeReopen} disabled={busy || busyImg}
                    style={{ marginTop: 8, border: 0, background: 'none', padding: 0, fontSize: 11, fontWeight: 600, color: 'var(--text-muted, #4a5e7a)', cursor: 'pointer' }}>
                    Cancelar
                  </button>
                </div>
              </div>
            )}
            {showStyleCard && !(isApproved && reopenPrompt) && (
              <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, padding: '10px 14px' }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8, marginBottom: 8 }}>
                  <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.08em', color: 'var(--text-dim, #94a3b8)', fontFamily: 'var(--font-mono, monospace)' }}>APLICAÇÃO DESTA IMAGEM</div>
                  {pickApp && pickApp === suggestedApp && (
                    <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.08em', color: '#3859D0', fontFamily: 'var(--font-mono, monospace)' }}>SUGERIDA</span>
                  )}
                </div>
                {canEditApp ? styleSelects : (
                  <>
                    <div style={{ fontSize: 14, fontWeight: 700, color: '#112954' }}>
                      {(appCatalog.find(c => c.slug === pickApp) || {}).label || ctdAppLabel(pickApp) || ctdAppLabel(appSlug) || '—'}
                      {(scenesForPick.find(s => s.id === pickScene)?.label || sceneLabel) ? ` · ${scenesForPick.find(s => s.id === pickScene)?.label || sceneLabel}` : ''}
                    </div>
                    {(ctdPromptPaletteLabel(prompt) || ctdPromptLayoutLabel(prompt)) && (
                      <div style={{ marginTop: 8, paddingTop: 8, borderTop: '1px solid var(--border-1, #ECEFF5)', fontSize: 11, color: '#334155', lineHeight: 1.45 }}>
                        {ctdPromptPaletteLabel(prompt) && <div><span style={{ color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', fontSize: 9, letterSpacing: '0.06em' }}>PALETA</span> {ctdPromptPaletteLabel(prompt)}</div>}
                        {ctdPromptLayoutLabel(prompt) && <div style={{ marginTop: 2 }}><span style={{ color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', fontSize: 9, letterSpacing: '0.06em' }}>LAYOUT</span> {ctdPromptLayoutLabel(prompt)}</div>}
                      </div>
                    )}
                  </>
                )}
              </div>
            )}
            {isApproved && !reopenPrompt ? (
              <div style={{ background: '#fff', border: imageStale || promptIsStale ? '1px solid #FDE68A' : '1px solid #BBF7D0', borderRadius: 8, overflow: 'hidden' }}>
                <div style={{ padding: '10px 14px', borderBottom: imageStale || promptIsStale ? '1px solid #FDE68A' : '1px solid #DCFCE7', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, background: imageStale || promptIsStale ? '#FFFBEB' : '#F0FDF4' }}>
                  <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: imageStale || promptIsStale ? '#92400E' : '#166534', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>
                    {imageStale ? 'Imagem desactualizada' : 'Prompt aprovado'}
                  </div>
                  <span style={{ fontSize: 10, fontWeight: 700, color: imageStale || promptIsStale ? '#92400E' : '#166534', display: 'inline-flex', alignItems: 'center', gap: 4 }}>
                    <Icon name="check" size={12} /> {chainDone ? 'Pronta para a peça final' : imageOk && needsVideo ? 'Pronta para vídeo' : showImageUrl && !imageStale ? 'Imagem por aprovar' : 'Pronta para imagem'}
                  </span>
                </div>
                <div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 12 }}>
                  <CtdPipelineStep n="1" label="Copy" detail={activeCopyForUi?.copy_imagem ? 'Aprovada nesta peça' : 'Sem copy aprovada'} state={activeCopyForUi?.copy_imagem ? 'done' : 'wait'} />
                  <CtdPipelineStep n="2" label="Prompt" detail={isApproved ? 'Aprovado' : 'Gerado · por aprovar'} state={promptState} />
                  <CtdPipelineStep n="3" label="Imagem" detail={imageDetail} state={imageState} onClick={showImageUrl ? () => openPieceTab('imagens') : null} />
                  {needsVideo && (
                    <CtdPipelineStep n="4" label="Vídeo"
                      detail={videoApproved ? 'Aprovado' : hasVideo ? 'Gerado · por aprovar' : imageOk ? 'Aguarda geração' : 'Depois da imagem aprovada'}
                      state={videoApproved ? 'done' : imageOk ? 'current' : 'wait'}
                      onClick={imageOk ? () => openPieceTab('video') : null} />
                  )}
                  {(!showImageUrl || imageStale) && (
                    <button type="button" onClick={() => {
                      if (isMetaPrompt) {
                        openPieceTab('imagens', { generate: true });
                        return;
                      }
                      handleGenerateImage();
                    }} disabled={busy || busyImg || busyRestore}
                      style={{ background: '#3859D0', color: '#fff', border: 0, width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: (busy||busyImg) ? 'not-allowed' : 'pointer', opacity: (busy||busyImg) ? 0.55 : 1, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 7 }}>
                      <Icon name="sparkle" size={13} /> {busyImg ? 'A gerar…' : 'Gerar imagem'}
                    </button>
                  )}
                  {showImageUrl && !imageStale && !imageApproved && (
                    <button type="button" onClick={() => openPieceTab('imagens')} style={{ background: '#1F8A52', color: '#fff', border: 0, width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: 'pointer' }}>Aprovar imagem</button>
                  )}
                  {imageOk && needsVideo && !videoApproved && (
                    <button type="button" onClick={() => openPieceTab('video')} style={{ background: '#3859D0', color: '#fff', border: 0, width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: 'pointer' }}>{hasVideo ? 'Rever e aprovar vídeo' : 'Gerar vídeo'}</button>
                  )}
                  {chainDone && (
                    <button type="button" onClick={() => openPieceTab('peca')} style={{ background: '#3859D0', color: '#fff', border: 0, width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: 'pointer' }}>Peça final</button>
                  )}
                  {showImageUrl && !imageStale && (
                    <button type="button" onClick={() => { if (isMetaPrompt) openPieceTab('imagens', { generate: true }); else handleGenerateImage(); }} disabled={busy || busyImg || busyRestore}
                      style={{ background: 'transparent', color: '#1F8A52', border: '1px solid #1F8A52', width: '100%', padding: '8px 10px', borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: (busy||busyImg) ? 'not-allowed' : 'pointer', opacity: (busy||busyImg) ? 0.55 : 1 }}>
                      {busyImg ? 'A gerar…' : 'Regerar imagem'}
                    </button>
                  )}
                  <div style={{ display: 'flex', gap: 6 }}>
                    {onNext && (
                      <button type="button" onClick={() => handleNav(onNext)} disabled={busy || busyImg}
                        style={{ flex: 1, background: 'transparent', color: 'var(--text, #112954)', border: '1px solid var(--border-2, #E3E6EB)', padding: '8px 10px', borderRadius: 6, fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>
                        Próxima peça →
                      </button>
                    )}
                  </div>
                  <button type="button" onClick={() => setReopenPrompt(true)}
                    style={{ border: 0, background: 'none', padding: 0, textAlign: 'left', fontSize: 11, fontWeight: 600, color: 'var(--text-muted, #4a5e7a)', cursor: 'pointer' }}>
                    Alterar prompt aprovado
                  </button>
                </div>
              </div>
            ) : !isApproved ? (
              <>
                {generateForm}
                {prompt && (
                  <div style={{ padding: 14, background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 10 }}>
                    <CtdPipelineStep n="1" label="Copy" detail={activeCopyForUi?.copy_imagem ? 'Aprovada nesta peça' : 'Sem copy aprovada'} state={activeCopyForUi?.copy_imagem ? 'done' : 'wait'} />
                    <CtdPipelineStep n="2" label="Prompt" detail="Gerado · por aprovar" state="current" />
                    <CtdPipelineStep n="3" label="Imagem" detail="Depois do prompt" state="wait" />
                  </div>
                )}
              </>
            ) : null}

            {!showStyleCard && !isInternalPrompt && (
            <div style={{ padding: '10px 14px', background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8 }}>
              <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.08em', color: 'var(--text-dim, #94a3b8)', fontFamily: 'var(--font-mono, monospace)', marginBottom: 6 }}>APLICAÇÃO DESTA IMAGEM</div>
              {appSlug ? (
                <>
                  <div style={{ fontSize: 14, fontWeight: 700, color: '#112954' }}>
                    {ctdAppLabel(appSlug)}{sceneLabel ? ` · ${sceneLabel}` : ''}
                  </div>
                </>
              ) : (
                <div style={{ fontSize: 12, fontWeight: 600, color: '#64748B' }}>Por atribuir — regenera para usar o catálogo</div>
              )}
            </div>
            )}
            {onNavigate && (
              <button type="button" onClick={() => onNavigate('copies')}
                style={{ background: 'none', border: 'none', padding: 0, fontSize: 11, fontWeight: 600, color: '#3859D0', cursor: 'pointer', textAlign: 'left' }}>
                Ir à tab Copies →
              </button>
            )}
          </div>

          {/* Centro — prompt */}
          <div style={{ minWidth: 0 }}>
            {donePayload && !busy && !isApproved && (
              <div style={{ padding: 10, background: '#DCFCE7', borderRadius: 6, color: '#166534', fontSize: 12, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
                <Icon name="check" size={14} /> Prompt gerado. Revê, clica <b>Aprovar</b> e depois <b>Gerar imagem</b>.
              </div>
            )}
            {error && <div style={{ padding: 12, background: '#FEE2E2', borderRadius: 6, color: '#991B1B', fontSize: 12, marginBottom: 12 }}>{error}</div>}
            {imageStale && hasImage && !busy && !busyImg && (
              <div style={{ padding: 10, background: '#FEF3C7', borderRadius: 6, color: '#92400E', fontSize: 12, fontWeight: 600, marginBottom: 12 }}>
                A imagem no mockup não corresponde a este prompt. Clica <b>Gerar imagem</b>.
              </div>
            )}

            {(busy || busyImg) && (
              <div style={{ padding: '48px 16px', background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 20 }}>
                <div style={{ display: 'flex', alignItems: 'flex-end', gap: 4, height: 48 }}>
                  {[0,1,2,3,4,5,6].map(i => <div key={i} style={{ width: 4, height: 32, background: 'var(--color-primary, #3859D0)', borderRadius: 4, animation: 'ctdWave 1s ease-in-out infinite', animationDelay: (i * 0.1) + 's', transformOrigin: 'bottom' }} />)}
                </div>
                <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.10em', color: 'var(--color-primary, #3859D0)', fontFamily: 'var(--font-mono, monospace)', textAlign: 'center' }}>
                  {step.message || (busyImg ? 'A GERAR IMAGEM' : 'A GERAR PROMPT')}
                </div>
                <style>{`@keyframes ctdWave { 0%,100%{transform:scaleY(0.25)} 50%{transform:scaleY(1)} }`}</style>
              </div>
            )}

            {prompt && !busy && !busyImg && (
              <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
                {activeCopyForUi?.copy_imagem && !isInternalPrompt && (
                  <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 10 }}>
                    <div style={{ minWidth: 0 }}>
                      <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.08em', color: 'var(--text-dim, #94a3b8)', fontFamily: 'var(--font-mono, monospace)', marginBottom: 4 }}>{isMimaki ? 'CLAIM' : 'CLAIM NO FILME'}</div>
                      <div style={{ fontSize: 13, fontWeight: 700, color: '#112954' }}>{activeCopyForUi.copy_imagem}</div>
                    </div>
                    {(activeCopyForUi.copy_imagem_cta || activeCopyForUi.cta) && (
                      <div style={{ fontSize: 11, fontWeight: 700, color: '#DC2626', flexShrink: 0 }}>{activeCopyForUi.copy_imagem_cta || activeCopyForUi.cta}</div>
                    )}
                  </div>
                )}
                <div style={{ padding: 14 }}>
                  <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: 'var(--text-dim, #94a3b8)', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase', marginBottom: 8 }}>Prompt · PT</div>
                  {ctdParsePromptPt(editText) ? (
                    <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
                      {ctdParsePromptPt(editText).filter(section => !(isMimaki && section.key === 'Letra')).map(section => (
                        <label key={section.key} style={{ display: 'block' }}>
                          <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.06em', color: '#112954', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>{section.key}</div>
                          <div style={{ fontSize: 10, color: '#94a3b8', margin: '2px 0 4px' }}>{section.hint}</div>
                          <textarea
                            value={section.value}
                            disabled={isApproved && !reopenPrompt}
                            rows={Math.min(6, Math.max(2, Math.ceil(section.value.length / 90)))}
                            onChange={e => {
                              const next = ctdParsePromptPt(editText).map(s => s.key === section.key ? { ...s, value: e.target.value } : s);
                              setEditText(ctdJoinPromptPt(next));
                            }}
                            style={{ width: '100%', border: '1px solid var(--border-2, #E3E6EB)', borderRadius: 6, padding: '8px 10px', fontSize: 12.5, lineHeight: 1.5, color: 'var(--text)', background: (isApproved && !reopenPrompt) ? 'var(--bg-subtle)' : '#fff', resize: 'vertical', fontFamily: 'var(--font-body)', boxSizing: 'border-box' }}
                          />
                        </label>
                      ))}
                    </div>
                  ) : (
                    <textarea value={editText} onChange={e => setEditText(e.target.value)} disabled={isApproved && !reopenPrompt} rows={22}
                      style={{ width: '100%', minHeight: 520, border: '1px solid var(--border-2, #E3E6EB)', borderRadius: 6, padding: '10px 12px', fontSize: 12, lineHeight: 1.55, color: 'var(--text)', background: (isApproved && !reopenPrompt) ? 'var(--bg-subtle)' : '#fff', resize: 'vertical', fontFamily: 'var(--font-body)', boxSizing: 'border-box', overflow: 'auto', whiteSpace: 'pre-wrap' }} />
                  )}
                  {prompt.prompt_text && (
                    <div style={{ marginTop: 8 }}>
                      <button onClick={() => setShowEn(s => !s)} style={{ fontSize: 10, color: 'var(--text-dim)', background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'var(--font-mono)' }}>
                        {showEn ? '▾' : '▸'} PROMPT EN (enviado ao Magnific)
                      </button>
                      {showEn && <div style={{ marginTop: 6, padding: '8px 12px', background: 'var(--bg-subtle)', borderRadius: 6, fontSize: 11, color: 'var(--text-muted)', lineHeight: 1.5, fontFamily: 'monospace', whiteSpace: 'pre-wrap' }}>{prompt.prompt_text}</div>}
                    </div>
                  )}
                </div>
              </div>
            )}

            {!prompt && !busy && !donePayload && (
              <div style={{ padding: 32, textAlign: 'center', color: 'var(--text-muted, #4a5e7a)', background: '#fff', borderRadius: 8, border: '1px dashed var(--border-2, #E3E6EB)', fontSize: 12 }}>
                {isInternalPrompt
                  ? <>Sem prompt ainda para o hero deste email. O formato é 16:9. Clica <b>Gerar</b>.</>
                  : <>Sem prompt ainda para esta peça. Escolhe a aplicação à esquerda (já vem sugerida) e clica <b>Gerar</b>.</>}
              </div>
            )}
          </div>

          {/* Direita — feed do anúncio. No email interno a pré-visualização fica na modal da copy. */}
          {!isInternalPrompt && <div style={{ position: 'sticky', top: inline ? 76 : 0, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 12 }}>
            {!isInternalPrompt && (
              <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
                <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
                  <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>Pré-visualização</span>
                  <span style={{ fontSize: 9, color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>Feed · 4:5</span>
                </div>
                <CtdMetaAdsFeedMockup
                  form={mockForm}
                  flush
                  brandName={brandName || campFull?.brand_name}
                  brandSlug={brandSlug || campFull?.brand_slug}
                  imageUrl={showImageUrl}
                  staleLabel={imageStale ? 'Imagem de outro prompt — Regenerar' : null}
                />
              </div>
            )}
          </div>}

          </div>
        </div>


        {/* Footer — só em modo modal */}
        {!inline && (
        <div style={{ padding: '12px 20px', borderTop: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'flex-end', gap: 8, background: 'var(--bg-subtle, #FAFAFA)' }}>
          <button onClick={onClose} disabled={busy||busyImg} className="btn btn-sm" style={{ fontSize: 12 }}>Fechar</button>
        </div>
        )}
      </div>
    </>
  );

  if (inline) return _cardEl;
  return ReactDOM.createPortal(
    <div onClick={busy || busyImg ? undefined : onClose}
      style={{ position: 'fixed', inset: 0, background: 'rgba(17,41,84,0.55)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>{_cardEl}</div>,
    document.body
  );
};

// ─── CtdTabPrompts — padrão idêntico a CtdTabCopies (CtdCopiesApprovalStrip + CtdCopiesPlanTable) ──
const CtdTabPrompts = ({ campanha, prompts, copies, campFull, userEmail, reloadPrompts, reloadImagens, reloadCopies, onNavigate, onBackToKanban, focusPiece, livePieceRef, onPiece, lockCanal }) => {
  const Icon = window.Icon;
  const commPlan   = (campFull?.proposta_json?.comm_plan) || [];
  const markets    = (campFull?.estrategia_json?.markets) || [];
  const marketList = markets.length
    ? markets.map(m => ({ country: m.country || m.pais || 'PT', lingua: m.lingua || (m.country === 'ES' ? 'es' : 'pt') }))
    : [{ country: 'PT', lingua: 'pt' }];
  const primaryCountry = marketList[0]?.country || 'PT';

  const [filterCanal, setFilterCanal]   = React.useState(lockCanal || 'all');
  React.useEffect(() => { if (lockCanal) setFilterCanal(lockCanal); }, [lockCanal]);
  const [filterCountry, setFilterCountry] = React.useState('all');
  const [filterStatus, setFilterStatus] = React.useState('all');
  const [modelo]                    = React.useState('opus-4-7');
  const [selected, setSelected]         = React.useState(new Set());
  const [rowBusy, setRowBusy]           = React.useState(new Set()); // set of row.key
  const [bulkBusy, setBulkBusy]         = React.useState(false);
  const [bulkProg, setBulkProg]         = React.useState({ done: 0, total: 0, current: '' });
  const [approvingIds, setApprovingIds] = React.useState(new Set());
  const [imagingIds, setImagingIds]     = React.useState(new Set());
  const [error, setError]               = React.useState(null);
  const [openItem, setOpenItem]         = React.useState(null); // peça aberta nesta tab
  if (livePieceRef && openItem) {
    livePieceRef.current = {
      idx: openItem.idx,
      country: openItem.country || null,
      titulo: openItem.peca?.titulo || null,
      contentType: openItem.peca?.content_type || null,
      waGroup: openItem.peca?.waGroup || null,
    };
  }
  React.useEffect(() => {
    if (!openItem) return;
    onPiece?.({
      idx: openItem.idx,
      country: openItem.country || null,
      titulo: openItem.peca?.titulo || null,
      contentType: openItem.peca?.content_type || null,
      waGroup: openItem.peca?.waGroup || null,
    });
  }, [openItem?.key]);

  // detectCountry: mesma lógica do CtdCopiesPlanTable
  const detectCountry = (peca) => {
    if (peca.country) return String(peca.country).toUpperCase();
    const m = String(peca.titulo || '').match(/^\[([A-Z]{2})\]/);
    if (m) return m[1];
    return primaryCountry;
  };

  // Linhas da tabela — 1 por peça visual (mesma lógica rows do CtdCopiesPlanTable)
  const ENVELOPE_TYPES = ['campanha_paid', 'paid_campaign'];
  const rows = React.useMemo(() => {
    const out = [];
    commPlan.forEach((peca, idx) => {
      if (ENVELOPE_TYPES.includes(peca.content_type)) return;
      if (ctdPecaArchived(peca)) return;
      if (ctdEmailBrevo(peca)) return;
      if (ctdHideWaFromPlan(peca)) return;
      const country = detectCountry(peca);
      const copyForPeca = (copies || []).find(c => Number(c.comm_plan_item_index) === idx);
      if (peca.item_type !== 'one_shot' && ctdIsWaPiece(peca, copyForPeca)) {
        const pack = ctdWaPackFromCopy(copyForPeca);
        const stale = ctdWaTemplateStale(pack);
        const status = !copyForPeca || copyForPeca.status !== 'aprovado' ? 'pendente' : stale ? 'desactualizado' : pack.approvedAt ? 'aprovado' : 'gerado';
        out.push({ key: `${idx}::${country}`, idx, country, peca, prompt: null, copy: copyForPeca, status, hasImage: false, stale, app: null, sceneLabel: null, wa: true });
        return;
      }
      const prompt  = ctdPromptForPeca(prompts, idx, peca, copyForPeca);
      const stale = ctdPromptIsStale(prompt, copyForPeca);
      const status  = stale ? 'desactualizado' : !prompt ? 'pendente' : (prompt.status === 'approved' ? 'aprovado' : 'gerado');
      const hasImage = !stale && !!(prompt && (prompt.image_url || prompt.magnific_url));
      const app = ctdPromptApp(prompt);
      const sceneLabel = ctdPromptSceneLabel(prompt);
      out.push({ key: `${idx}::${country}`, idx, country, peca, prompt, copy: copyForPeca, status, hasImage, stale, app, sceneLabel, wa: false });
    });
    ctdWaLibraryEntries(copies).forEach(e => {
      e.copyStale = ctdWaUspStale(e, copies);
      const stale = ctdWaTemplateStale(e.pack);
      const status = e.copy.status !== 'aprovado' ? 'pendente' : stale ? 'desactualizado' : e.pack.approvedAt ? 'aprovado' : 'gerado';
      out.push(ctdWaLibraryRow(e, status));
    });
    ctdWaPendingAberturas(copies, campFull).forEach(row => out.push(row));
    return out;
  }, [commPlan, prompts, copies, campFull]);
  React.useEffect(() => {
    if (!openItem?.key) return;
    const fresh = rows.find(r => r.key === openItem.key);
    if (fresh && fresh.copy !== openItem.copy) setOpenItem(fresh);
  }, [rows, openItem]);

  // Sort + filter
  const COUNTRY_ORDER_TBL = ['PT', 'ES', 'FR', 'IT', 'DE', 'EN'];
  const CANAL_ORDER_TBL   = ['Meta Ads', 'Email', 'WhatsApp', 'Blog', 'Landing', 'LED', 'Site', 'Comunicação Interna'];

  const canaisPresent    = React.useMemo(() => {
    const set = new Set(rows.map(r => ctdCanalLabel(r.peca)).filter(Boolean));
    return [...set].sort((a, b) => {
      const ia = CANAL_ORDER_TBL.indexOf(a), ib = CANAL_ORDER_TBL.indexOf(b);
      if (ia !== -1 && ib !== -1) return ia - ib;
      if (ia !== -1) return -1; if (ib !== -1) return 1;
      return a.localeCompare(b);
    });
  }, [rows]);
  const countriesPresent = React.useMemo(() => [...new Set(rows.map(r => r.country))], [rows]);

  const filtered = React.useMemo(() => {
    const list = rows.filter(r => {
      const canalEff = lockCanal || filterCanal;
      if (canalEff !== 'all' && ctdCanalLabel(r.peca) !== canalEff) return false;
      if (filterCountry !== 'all' && r.country !== filterCountry)              return false;
      if (filterStatus  === 'sem_imagem') {
        if (r.status !== 'aprovado' || r.hasImage) return false;
      } else if (filterStatus === 'pendente') {
        if (r.status !== 'pendente' && r.status !== 'desactualizado') return false;
      } else if (filterStatus !== 'all' && r.status !== filterStatus) return false;
      return true;
    });
    return list.sort((a, b) => {
      const ra = ctdListRank(a), rb = ctdListRank(b);
      if (ra !== rb) return ra - rb;
      const ia = COUNTRY_ORDER_TBL.indexOf(a.country), ib = COUNTRY_ORDER_TBL.indexOf(b.country);
      const pa = ia === -1 ? 99 : ia, pb = ib === -1 ? 99 : ib;
      if (pa !== pb) return pa - pb;
      return (a.groupPos || a.idx) - (b.groupPos || b.idx);
    });
  }, [rows, filterCanal, lockCanal, filterCountry, filterStatus]);

  const focusToken = focusPiece ? `${focusPiece.idx}:${focusPiece.country || ''}:${focusPiece.titulo || ''}:${ctdPieceCode(focusPiece.titulo)}` : '';
  const openedFocus = React.useRef('');
  React.useEffect(() => {
    if (!focusToken || openedFocus.current === focusToken) return;
    const idx = Number(focusPiece.idx);
    const wantTitle = ctdNormTitle(focusPiece.titulo);
    const wantCode = ctdPieceCode(focusPiece.titulo);
    const row = (wantCode && rows.find(r => ctdPieceCode(r.peca?.titulo) === wantCode))
      || (wantTitle && rows.find(r => ctdNormTitle(r.peca?.titulo) === wantTitle))
      || (!wantCode && !wantTitle && Number.isFinite(idx) && rows.find(r => r.idx === idx && (!focusPiece.country || r.country === focusPiece.country)))
      || (!wantCode && !wantTitle && Number.isFinite(idx) && rows.find(r => r.idx === idx))
      || null;
    if (!row) return;
    openedFocus.current = focusToken;
    setOpenItem(row);
  }, [focusToken, rows, focusPiece]);

  const counters = React.useMemo(() => {
    const plan = rows.filter(r => !r.library);
    return {
      total:     plan.length,
      pendente:  plan.filter(r => r.status === 'pendente' || r.status === 'desactualizado').length,
      gerado:    plan.filter(r => r.status === 'gerado').length,
      aprovado:  plan.filter(r => r.status === 'aprovado').length,
      semImagem: plan.filter(r => !r.wa && r.status === 'aprovado' && !r.hasImage).length,
    };
  }, [rows]);

  // Selecção
  const allFilteredSel = filtered.length > 0 && filtered.every(r => selected.has(r.key));
  const toggleAll = () => setSelected(prev => {
    const next = new Set(prev);
    if (allFilteredSel) filtered.forEach(r => next.delete(r.key));
    else filtered.forEach(r => next.add(r.key));
    return next;
  });
  const toggleOne = (key) => setSelected(prev => {
    const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next;
  });

  // Gerar 1 item
  const runOne = async (row) => {
    if (row.waPending) {
      setRowBusy(prev => { const n = new Set(prev); n.add(row.key); return n; });
      setError(null);
      try {
        await CtdAPI.generateWaLibrary(campanha.id, 'pt', { actor_email: userEmail });
        reloadCopies?.();
      } catch (e) { setError(e.message || 'Erro ao gerar os templates WhatsApp'); }
      setRowBusy(prev => { const n = new Set(prev); n.delete(row.key); return n; });
      return;
    }
    if (row.wa) { setOpenItem(row); return; }
    setRowBusy(prev => { const n = new Set(prev); n.add(row.key); return n; });
    setError(null);
    try {
      await CtdAPI.generatePromptItem(campanha.id, row.idx, { actor_email: userEmail, modelo }, () => {});
      reloadPrompts?.();
    } catch (e) { setError(`Erro na peça #${row.idx+1}: ${e.message}`); }
    setRowBusy(prev => { const n = new Set(prev); n.delete(row.key); return n; });
  };

  // Gerar em bulk (PT primeiro — mirror semântico igual ao Copies)
  const runBulk = async (targetRows) => {
    if (!targetRows.length) return;
    const sorted = [...targetRows].sort((a, b) => {
      const ap = a.country === 'PT' ? 0 : 1, bp = b.country === 'PT' ? 0 : 1;
      if (ap !== bp) return ap - bp; return a.idx - b.idx;
    });
    const visual = sorted.filter(r => !r.wa);
    setBulkBusy(true); setBulkProg({ done: 0, total: visual.length, current: '' });
    for (let i = 0; i < visual.length; i++) {
      const r = visual[i];
      setBulkProg({ done: i, total: visual.length, current: r.peca.titulo || `Peça #${r.idx+1}` });
      try { await CtdAPI.generatePromptItem(campanha.id, r.idx, { actor_email: userEmail, modelo }, () => {}); } catch {}
      reloadPrompts?.();
    }
    setBulkProg({ done: sorted.length, total: sorted.length, current: 'Concluído' });
    setBulkBusy(false); setSelected(new Set());
    setTimeout(() => setBulkProg({ done: 0, total: 0, current: '' }), 1200);
  };

  const runSelected  = () => runBulk(filtered.filter(r => selected.has(r.key)));
  const runAllPending = () => {
    const pend = rows.filter(r => r.status === 'pendente' && !r.library);
    if (pend.length && !confirm(`Gerar ${pend.length} prompts pendentes com Opus 4.7?`)) return;
    runBulk(pend);
  };

  // Aprovar prompt (não dispara Magnific)
  const runApprove = async (row) => {
    if (!row.prompt?.id) return;
    setApprovingIds(prev => { const n = new Set(prev); n.add(row.prompt.id); return n; });
    setError(null);
    try {
      await CtdAPI.approvePrompt(campanha.id, row.prompt.id, { actor_email: userEmail });
      reloadPrompts?.();
    } catch (e) { setError(e.message || 'Erro ao aprovar'); }
    setApprovingIds(prev => { const n = new Set(prev); n.delete(row.prompt.id); return n; });
  };

  const runGenerateImage = async (row) => {
    if (!row.prompt?.id) return;
    setImagingIds(prev => { const n = new Set(prev); n.add(row.prompt.id); return n; });
    setError(null);
    try {
      if (row.prompt.status !== 'approved') {
        await CtdAPI.approvePrompt(campanha.id, row.prompt.id, { actor_email: userEmail });
      }
      await CtdAPI.generateImage(campanha.id, row.prompt.id, { actor_email: userEmail });
      reloadPrompts?.(); reloadImagens?.();
    } catch (e) { setError(e.message || 'Erro ao gerar imagem'); }
    setImagingIds(prev => { const n = new Set(prev); n.delete(row.prompt.id); return n; });
  };

  const approveAllGerado = async () => {
    const gerados = rows.filter(r => r.status === 'gerado');
    for (const r of gerados) await runApprove(r);
  };

  const generateAllPendingImages = async () => {
    const pending = rows.filter(r => !r.wa && r.status === 'aprovado' && !r.hasImage);
    if (!pending.length) return;
    if (!confirm(`Gerar imagem para ${pending.length} prompt(s) aprovado(s)?`)) return;
    for (const r of pending) await runGenerateImage(r);
  };

  // Estilos idênticos ao CtdCopiesPlanTable
  const STATUS_STYLE = {
    pendente: { bg: '#F1F5F9', fg: '#475569', label: 'Pendente' },
    desactualizado: { bg: '#FEF3C7', fg: '#92400E', label: 'Copy mudou' },
    gerado:   { bg: '#FEF3C7', fg: '#92400E', label: 'Gerado'   },
    aprovado: { bg: '#DCFCE7', fg: '#166534', label: 'Aprovado' },
  };
  // Estilos idênticos pixel-a-pixel ao CtdCopiesPlanTable
  const thSt = { textAlign: 'left', padding: '8px 12px', fontSize: 10, fontFamily: 'var(--font-display)', fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--text-muted)', whiteSpace: 'nowrap', borderBottom: '1px solid var(--border)' };
  const tdSt = { padding: '10px 12px', verticalAlign: 'middle', borderBottom: '1px solid var(--border)', fontSize: 13, color: 'var(--text)' };
  const canalBadge = (peca) => {
    const label = ctdCanalLabel(peca);
    const cat   = ctdGetCategoria(peca.content_type);
    return <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.06em', padding: '3px 7px', borderRadius: 4, background: `${cat.color}18`, color: cat.color, fontFamily: 'var(--font-mono, monospace)' }}>{label.toUpperCase()}</span>;
  };

  // ── Approval strip (= CtdCopiesApprovalStrip adaptado) ───────────────────────
  const totalP  = counters.total, approvedP = counters.aprovado, pendingP = totalP - approvedP;
  const allApprP = totalP > 0 && approvedP === totalP;
  const labelP   = allApprP ? 'PROMPTS APROVADOS' : approvedP > 0 ? 'PROMPTS EM APROVAÇÃO' : totalP > 0 ? 'PROMPTS GERADOS' : 'PROMPTS';
  const bgP      = allApprP ? '#E1F7E6' : approvedP > 0 ? '#FEF3C7' : totalP > 0 ? '#ECF1FB' : '#F1F5F9';
  const fgP      = allApprP ? '#1F8A52' : approvedP > 0 ? '#92400E' : totalP > 0 ? '#3859D0' : '#475569';

  // Detail view inline (substitui a tabela quando um prompt está aberto)
  if (openItem) {
    const navRows = ctdNavRowsSameCanal(rows, openItem.peca);
    const rowIndex = Math.max(0, navRows.findIndex(r => r.key === openItem.key));
    const prevRow  = rowIndex > 0 ? navRows[rowIndex - 1] : null;
    const nextRow  = rowIndex < navRows.length - 1 ? navRows[rowIndex + 1] : null;
    const total    = navRows.length;
    const pecaTitle = openItem.peca?.titulo || `Peça #${(openItem.idx ?? 0) + 1}`;
    const canalCat = openItem.peca ? ctdGetCategoria(openItem.peca.content_type) : null;
    return (
      <div style={{ width: '100%' }}>
        <div style={{ position: 'sticky', top: 0, zIndex: 10, background: 'var(--bg-page, #F4F6FA)', padding: '12px 32px 0' }}>
          <CtdBreadcrumb
            trail={[
              { label: campanha?.titulo || 'Campanha', onClick: onBackToKanban },
              { label: (openItem.peca?.waGroup === 'qualificacao' || openItem.peca?.waGroup === 'abertura') ? 'Aprovação Meta' : openItem.wa ? 'Template' : 'Prompts', onClick: () => { setOpenItem(null); if (livePieceRef) livePieceRef.current = null; onPiece?.(null); } },
              { label: pecaTitle, badge: canalCat ? { text: ctdCanalLabel(openItem.peca), color: canalCat.color } : null },
            ]}
            onBack={() => setOpenItem(null)}
            currentPos={rowIndex + 1}
            totalItems={total}
            onPrev={prevRow ? () => setOpenItem(prevRow) : null}
            onNext={nextRow ? () => setOpenItem(nextRow) : null}
          />
        </div>
        <div style={{ padding: '12px 32px 32px' }}>
        {openItem.wa ? (
          <CtdWaTemplateScreen
            campanhaId={campanha.id}
            row={openItem}
            userEmail={userEmail}
          lockCanal={lockCanal}
            onNavigate={onNavigate}
            reloadCopies={reloadCopies}
            brandName={campanha?.brand_name}
          />
        ) : (
        <CtdPromptDetailModal
          campanhaId={campanha.id}
          row={openItem}
          userEmail={userEmail}
          lockCanal={lockCanal}
          campFull={campFull}
          brandSlug={campanha?.brand_slug}
          brandName={campanha?.brand_name}
          onNavigate={onNavigate}
          onClose={() => setOpenItem(null)}
          onDone={() => { reloadPrompts?.(); reloadImagens?.(); }}
          onPrev={prevRow ? () => setOpenItem(prevRow) : null}
          onNext={nextRow ? () => setOpenItem(nextRow) : null}
          currentPos={rowIndex + 1}
          totalItems={total}
          inline={true}
        />
        )}
        </div>
      </div>
    );
  }

  const paisesFase = [...new Set(rows.filter(r => !lockCanal || ctdCanalLabel(r.peca) === lockCanal).map(r => r.country).filter(Boolean))];
  return (
    <div style={{ padding: '20px 32px 32px', width: '100%' }}>
      {/* Controls — filtros + modelo + bulk (= CtdCopiesPlanTable controls row) */}
      {(!lockCanal || paisesFase.length > 1) && (
      <div style={{ display: 'flex', gap: 8, alignItems: 'center', padding: '10px 0', fontSize: 11, flexWrap: 'wrap', marginBottom: 4 }}>
        {!lockCanal && (
          <CtdDropdown
            label="Canal"
            value={filterCanal}
            onChange={setFilterCanal}
            options={[{ value: 'all', label: 'Todos os canais' }, ...canaisPresent.map(c => ({ value: c, label: c }))]}
          />
        )}
        {paisesFase.length > 1 && (
          <CtdDropdown
            label="Mercado"
            value={filterCountry}
            onChange={setFilterCountry}
            options={[{ value: 'all', label: 'Todos mercados' }, ...paisesFase.map(c => ({ value: c, label: c }))]}
          />
        )}
        {(!lockCanal || paisesFase.length > 1) && (
          <span style={{ color: 'var(--text-dim, #7a8fa8)', fontSize: 11 }}>{filtered.length} de {rows.length}</span>
        )}
        {!lockCanal && (
        <div style={{ marginLeft: 'auto', display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
          <span style={{ fontSize: 10, fontWeight: 700, padding: '4px 10px', borderRadius: 4, background: '#ECF1FB', color: '#3859D0', fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.04em' }}>OPUS 4.7</span>
          <button onClick={runSelected} disabled={bulkBusy || selected.size === 0} className="btn btn-sm" style={{ fontSize: 11, opacity: (bulkBusy || selected.size === 0) ? 0.5 : 1 }}>
            <Icon name="sparkle" size={11} /> Gerar seleccionados ({selected.size})
          </button>
          <button onClick={runAllPending} disabled={bulkBusy || counters.pendente === 0} style={{ background: 'var(--color-primary, #3859D0)', color: '#fff', border: 0, padding: '6px 12px', borderRadius: 'var(--radius-sm, 6px)', fontSize: 11, fontWeight: 700, cursor: bulkBusy ? 'not-allowed' : 'pointer', display: 'inline-flex', alignItems: 'center', gap: 5, opacity: (bulkBusy || counters.pendente === 0) ? 0.5 : 1 }}>
            <Icon name="sparkle" size={11} /> Gerar todos pendentes ({counters.pendente})
          </button>
          {counters.semImagem > 0 && (
            <button onClick={generateAllPendingImages} disabled={bulkBusy} style={{ background: 'transparent', color: '#1F8A52', border: '1px solid #1F8A52', padding: '6px 12px', borderRadius: 'var(--radius-sm, 6px)', fontSize: 11, fontWeight: 700, cursor: bulkBusy ? 'not-allowed' : 'pointer', display: 'inline-flex', alignItems: 'center', gap: 5, opacity: bulkBusy ? 0.5 : 1 }}>
              <Icon name="sparkle" size={11} /> Gerar imagens ({counters.semImagem})
            </button>
          )}
        </div>
        )}
      </div>
      )}

      {!lockCanal && (
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 12, marginBottom: 20 }}>
        {[
          { key: 'all',         label: 'PLANO',      value: counters.total,     accent: '#3859D0', activeBg: '#EFF3FF' },
          { key: 'pendente',    label: 'PENDENTES',  value: counters.pendente,  accent: '#64748B', activeBg: '#F1F5F9' },
          { key: 'gerado',      label: 'GERADO',     value: counters.gerado,    accent: '#92400E', activeBg: '#FEF3C7' },
          { key: 'aprovado',    label: 'APROVADOS',  value: counters.aprovado,  accent: '#166534', activeBg: '#DCFCE7' },
          { key: 'sem_imagem',  label: 'SEM IMAGEM', value: counters.semImagem, accent: '#B45309', activeBg: '#FEF3C7' },
        ].map(card => {
          const isActive = filterStatus === card.key;
          return (
            <div key={card.key} onClick={() => setFilterStatus(isActive && card.key !== 'all' ? 'all' : card.key)}
              style={{ background: isActive ? card.activeBg : 'var(--bg-surface, #fff)', border: '1px solid ' + (isActive ? card.accent + '50' : 'var(--border, #E3E6EB)'), borderRadius: 8, padding: '14px 16px', cursor: 'pointer' }}>
              <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: isActive ? card.accent : 'var(--text-muted)', fontFamily: 'var(--font-mono, monospace)', marginBottom: 6 }}>{card.label}</div>
              <div style={{ fontSize: 28, fontWeight: 700, color: isActive ? card.accent : 'var(--text, #112954)', lineHeight: 1 }}>{card.value}</div>
              <div style={{ marginTop: 10, height: 3, background: isActive ? card.accent : 'var(--border, #E3E6EB)', borderRadius: 2, opacity: isActive ? 1 : 0.35 }} />
            </div>
          );
        })}
      </div>
      )}

      {/* Bulk progress */}
      {bulkBusy && (
        <div style={{ padding: '10px 16px', background: '#EFF6FF', borderBottom: '1px solid #DBEAFE', fontSize: 11, color: '#1E3A8A', display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
          <div style={{ flex: 1 }}>
            <div style={{ fontWeight: 700, marginBottom: 3 }}>A gerar {bulkProg.done + 1}/{bulkProg.total} · {bulkProg.current}</div>
            <div style={{ height: 4, background: '#DBEAFE', borderRadius: 2, overflow: 'hidden' }}>
              <div style={{ height: '100%', background: '#3859D0', width: `${(bulkProg.done / bulkProg.total) * 100}%`, transition: 'width 300ms' }} />
            </div>
          </div>
        </div>
      )}

      {error && <div style={{ padding: '10px 14px', marginBottom: 14, background: '#FEE2E2', color: '#991B1B', borderRadius: 'var(--radius-sm, 6px)', fontSize: 12 }}>{error}</div>}

      {/* Tabela — estrutura idêntica ao CtdCopiesPlanTable */}
      {rows.length === 0 ? (
        <div style={{ padding: 40, textAlign: 'center', color: 'var(--text-muted)', fontSize: 13, fontStyle: 'italic' }}>
          Sem peças visuais no comm_plan. Confirma o Planeamento no módulo Campanhas.
        </div>
      ) : (
        <div className="scrollbar" style={{ overflowX: 'auto', width: '100%' }}>
          <table style={{ width: '100%', minWidth: 1480, borderCollapse: 'collapse', fontSize: 12.5, fontFamily: 'var(--font-sans)' }}>
            <thead>
              <tr>
                <th style={{ ...thSt, width: 36, textAlign: 'center', padding: '4px 4px' }}>
                  <input type="checkbox" checked={allFilteredSel} onChange={toggleAll} disabled={bulkBusy} style={{ width: 14, height: 14, cursor: 'pointer', accentColor: 'var(--ai-500, #3859D0)' }} />
                </th>
                <th style={{ ...thSt, width: 40 }}>#</th>
                <th style={thSt}>Peça</th>
                {filterCanal === 'WhatsApp' ? (
                  <>
                    <th style={thSt}>Imagem</th>
                    <th style={thSt}>Copy</th>
                  </>
                ) : (
                  <>
                    <th style={thSt}>Aplicação</th>
                    <th style={thSt}>Canal</th>
                    <th style={thSt}>Data</th>
                  </>
                )}
                <th style={thSt}>País</th>
                <th style={thSt}>Autor</th>
                <th style={thSt}>{filterCanal === 'WhatsApp' ? 'Meta' : 'Estado'}</th>
                <th style={{ ...thSt, textAlign: 'right' }}>Acções</th>
              </tr>
            </thead>
            <tbody>
              {filtered.map((r, rowIndex) => {
                const busy       = rowBusy.has(r.key) || bulkBusy;
                const isAppr     = r.prompt && approvingIds.has(r.prompt.id);
                const isImg      = r.prompt && imagingIds.has(r.prompt.id);
                const isBusy     = busy || isAppr || isImg;
                const st         = STATUS_STYLE[r.status];
                const waPack     = r.library && !r.waPending ? ctdWaPackFromCopy(r.existing || r.copy) : null;
                const metaPill   = waPack ? ctdWaMetaPill(waPack.metaStatus, waPack.rejectedReason) : null;
                const imagePill  = r.waPending ? { fg: '#475569', bg: '#F1F5F9', label: '—', title: 'Template ainda por gerar' } : (waPack ? ctdWaImagePill(r.peca.waGroup, waPack) : null);
                const copyPill   = r.waPending ? ctdWaCopyPill('pendente', false, '') : (waPack ? ctdWaCopyPill(r.copyStatus, r.copyStale, r.adRef) : null);
                const hasImage   = r.hasImage;
                const prev       = rowIndex > 0 ? filtered[rowIndex - 1] : null;
                const isNewGrp   = !prev || ctdGroupTitle(prev) !== ctdGroupTitle(r);
                const title      = r.peca.titulo || `Peça #${r.library ? r.groupPos : r.idx + 1}`;
                const subtitulo  = r.peca.subtitulo || '';
                const author     = r.prompt?.approved_by || r.prompt?.updated_by || r.prompt?.actor_email || r.existing?.approved_by || r.existing?.updated_by || r.copy?.approved_by || r.copy?.updated_by || null;
                return (
                  <React.Fragment key={r.key}>
                    {(rowIndex === 0 || isNewGrp) && (
                      <tr aria-hidden>
                        <td colSpan={filterCanal === 'WhatsApp' ? 9 : 10} style={{ padding: rowIndex === 0 ? '4px 12px' : '18px 12px 4px', borderBottom: '2px solid var(--ai-500, #3859D0)', background: 'transparent' }}>
                          <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--ai-500, #3859D0)', fontFamily: 'var(--font-mono, monospace)' }}>
                            {ctdGroupTitle(r)}
                          </div>
                        </td>
                      </tr>
                    )}
                    <CtdPlanRow busy={isBusy} isSelected={selected.has(r.key)} rowIndex={rowIndex}
                      onClick={(e) => { if (isBusy || r.waPending) return; const tag = e.target.tagName; if (tag === 'INPUT' || tag === 'BUTTON' || e.target.closest('button')) return; setOpenItem(r); }}>
                      <td style={{ ...tdSt, padding: '4px 4px', textAlign: 'center' }} onClick={e => e.stopPropagation()}>
                        <input type="checkbox" checked={selected.has(r.key)} onChange={() => toggleOne(r.key)} disabled={bulkBusy} style={{ width: 14, height: 14, cursor: 'pointer', accentColor: 'var(--ai-500, #3859D0)' }} />
                      </td>
                      <td style={{ ...tdSt, fontFamily: 'var(--font-mono)', color: 'var(--text-dim)', fontSize: 11 }}>{r.library ? r.groupPos : r.idx + 1}</td>
                      <td style={{ ...tdSt, overflow: 'hidden' }}>
                        <div style={{ fontWeight: 500, color: 'var(--text)', fontSize: 12, fontFamily: 'var(--font-mono)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={subtitulo || ''}>{title}</div>
                        {subtitulo && <div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 480 }} title={subtitulo}>{subtitulo}</div>}
                        {r.stale && <div style={{ fontSize: 10, color: '#92400E', fontWeight: 600, marginTop: 2 }}>{r.wa ? 'Copy mudou · template desactualizado' : 'Copy mudou · prompt desactualizado'}</div>}
                        {r.prompt?.prompt_text_pt && <div style={{ fontSize: 10, color: 'var(--text-dim)', marginTop: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontStyle: 'italic' }}>{r.prompt.prompt_text_pt.slice(0, 90)}…</div>}
                      </td>
                      {filterCanal === 'WhatsApp' && r.library ? (
                        <>
                          <td style={tdSt}>
                            <span title={imagePill.title} style={{ fontSize: 11, fontFamily: 'var(--font-mono)', fontWeight: 600, padding: '2px 8px', borderRadius: 4, color: imagePill.fg, background: imagePill.bg }}>{imagePill.label}</span>
                          </td>
                          <td style={tdSt}>
                            <span title={copyPill.title} style={{ fontSize: 11, fontFamily: 'var(--font-mono)', fontWeight: 600, padding: '2px 8px', borderRadius: 4, color: copyPill.fg, background: copyPill.bg }}>{copyPill.label}</span>
                          </td>
                        </>
                      ) : (
                        <>
                          <td style={tdSt}>
                            {r.app ? (
                              <div>
                                <span title={r.app} style={{ fontSize: 10, fontWeight: 700, padding: '3px 7px', borderRadius: 4, background: '#EFF3FF', color: '#3859D0', fontFamily: 'var(--font-mono, monospace)', whiteSpace: 'nowrap' }}>{ctdAppLabel(r.app)}</span>
                                {r.sceneLabel && <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 3 }}>{r.sceneLabel}</div>}
                              </div>
                            ) : <span style={{ color: 'var(--text-dim)' }}>—</span>}
                          </td>
                          <td style={tdSt}>{canalBadge(r.peca)}</td>
                          <td style={{ ...tdSt, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>{r.peca.planned_date ? ctdFmtDate(r.peca.planned_date) : '—'}</td>
                        </>
                      )}
                      <td style={{ ...tdSt, fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600 }}>{r.country}</td>
                      <td style={{ ...tdSt, whiteSpace: 'nowrap' }}>{ctdAuthorChip(author)}</td>
                      <td style={tdSt}>
                        <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
                          <span title={metaPill ? metaPill.title : undefined} style={{ fontSize: 11, fontFamily: 'var(--font-mono)', fontWeight: 600, padding: '2px 8px', borderRadius: 4, display: 'inline-block', color: (metaPill || st).fg, background: (metaPill || st).bg }}>{metaPill ? metaPill.label : st.label.toUpperCase()}</span>
                          {hasImage && !r.stale && <Icon name="check" size={11} color="#166534" />}
                        </div>
                      </td>
                      <td style={{ ...tdSt, textAlign: 'right', whiteSpace: 'nowrap' }} onClick={e => e.stopPropagation()}>
                        {/* REGERAR/GERAR — mesmo estilo que Copies */}
                        <button onClick={() => runOne(r)} disabled={isBusy}
                          title={r.waPending ? 'Gerar os templates WhatsApp' : r.wa ? 'Abrir template' : (r.status === 'pendente' ? 'Gerar prompt' : 'Regerar prompt (substitui)')}
                          style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.04em', padding: '4px 9px', background: r.status === 'pendente' ? 'var(--ai-500, #3859D0)' : 'transparent', color: r.status === 'pendente' ? '#fff' : 'var(--ai-500, #3859D0)', border: '1px solid var(--ai-500, #3859D0)', borderRadius: 4, cursor: isBusy ? 'not-allowed' : 'pointer', opacity: isBusy ? 0.5 : 1, marginRight: r.status === 'gerado' && !r.wa ? 4 : 0, fontFamily: 'var(--font-mono)', textTransform: 'uppercase' }}>
                          {rowBusy.has(r.key) ? '…' : r.waPending ? 'Gerar' : r.wa ? 'Abrir' : (r.status === 'pendente' || r.status === 'desactualizado') ? 'Gerar' : 'Regerar'}
                        </button>
                        {/* APROVAR — só aprova o prompt */}
                        {r.status === 'gerado' && !r.wa && (
                          <button onClick={() => runApprove(r)} disabled={isBusy}
                            style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.04em', padding: '4px 9px', background: '#fff', color: '#283252', border: '1px solid #E3E6EB', borderRadius: 4, cursor: isBusy ? 'not-allowed' : 'pointer', opacity: isBusy ? 0.5 : 1, fontFamily: 'var(--font-mono)', textTransform: 'uppercase' }}>
                            {isAppr ? '…' : 'Aprovar'}
                          </button>
                        )}
                        {r.status === 'aprovado' && !hasImage && !r.wa && (
                          <button onClick={() => runGenerateImage(r)} disabled={isBusy}
                            style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.04em', padding: '4px 9px', background: 'transparent', color: '#1F8A52', border: '1px solid #1F8A52', borderRadius: 4, cursor: isBusy ? 'not-allowed' : 'pointer', opacity: isBusy ? 0.5 : 1, fontFamily: 'var(--font-mono)', textTransform: 'uppercase', marginLeft: 4 }}>
                            {isImg ? '…' : 'Imagem'}
                          </button>
                        )}
                      </td>
                    </CtdPlanRow>
                  </React.Fragment>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
};
// ─── CtdVariationsModal — escolher perspectives + progress ──────────────────────
const CtdVariationsModal = ({ campanha, image, onClose, onDone }) => {
  const [selected, setSelected] = React.useState(CTD_PERSPECTIVES_DEFAULT);
  const [busy, setBusy]         = React.useState(false);
  const [step, setStep]         = React.useState({ step: 0, total: 0, message: '' });
  const [error, setError]       = React.useState(null);

  const toggle = (k) => setSelected(s => s.includes(k) ? s.filter(x => x !== k) : [...s, k]);
  const cost = (selected.length * 0.40).toFixed(2);

  const handleGenerate = async () => {
    if (!selected.length) return;
    setBusy(true); setError(null);
    setStep({ step: 0, total: selected.length, message: 'A iniciar...' });
    try {
      const result = await CtdAPI.generateVariations(
        campanha.id, image.id, { perspectives: selected },
        (s, t, msg) => setStep({ step: s, total: t, message: msg }),
      );
      onDone?.(result);
    } catch (e) { setError(e.message || 'Erro'); }
    setBusy(false);
  };

  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.75)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1100, padding: 40,
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        background: '#fff', borderRadius: 12, padding: 24, width: 480, maxWidth: '90vw', maxHeight: '85vh', overflow: 'auto',
      }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 6 }}>
          <div>
            <div style={{ fontSize: 10, letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', marginBottom: 4 }}>
              VARIATIONS · REFRAME MODE
            </div>
            <h3 style={{ margin: 0, fontSize: 18, fontWeight: 700, color: '#283252', fontFamily: 'var(--font-display, Montserrat, sans-serif)' }}>
              Gerar variações desta imagem
            </h3>
          </div>
          <button onClick={onClose} className="btn btn-ghost btn-sm" style={{ width: 28, height: 28, padding: 0, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }} title="Fechar">
            <window.Icon name="x" size={16} />
          </button>
        </div>
        <div style={{ fontSize: 12, color: '#64748b', marginBottom: 16, lineHeight: 1.5 }}>
          Cada ângulo gera primeiro a máquina, sem texto. O claim, a linha de apoio e o botão entram depois. O close-up da impressão e o do painel enchem o enquadramento.
        </div>

        <div style={{
          display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6,
          marginBottom: 16, maxHeight: 320, overflowY: 'auto',
        }}>
          {CTD_PERSPECTIVES.map(p => {
            const isOn = selected.includes(p.key);
            return (
              <label key={p.key} style={{
                display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px',
                background: isOn ? '#eef2ff' : '#f8fafc', borderRadius: 6, cursor: 'pointer',
                border: `1px solid ${isOn ? '#c7d2fe' : '#e2e8f0'}`, fontSize: 12,
              }}>
                <input type="checkbox" checked={isOn} onChange={() => toggle(p.key)} />
                <span style={{ color: isOn ? '#3730a3' : '#334155', fontWeight: isOn ? 600 : 500 }}>{p.label}</span>
              </label>
            );
          })}
        </div>

        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 14, fontSize: 11, color: '#64748b', padding: '8px 10px', background: '#f8fafc', borderRadius: 6 }}>
          <span>{selected.length} perspective{selected.length !== 1 ? 's' : ''} seleccionada{selected.length !== 1 ? 's' : ''}</span>
          <span style={{ fontFamily: 'var(--font-mono, monospace)' }}>Custo estimado: ${cost}</span>
        </div>

        {error && <div style={{ marginBottom: 12, padding: 10, background: '#fef2f2', borderRadius: 6, color: '#dc2626', fontSize: 12 }}>{error}</div>}
        {busy && <div style={{ marginBottom: 12 }}><CtdGenProgress step={step.step} total={step.total} message={step.message} /></div>}

        <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
          <button onClick={onClose} disabled={busy} className="btn btn-sm">Cancelar</button>
          <button onClick={handleGenerate} disabled={busy || !selected.length} className="btn btn-sm btn-ai">
            <window.Icon name="sparkle" size={14} />
            {busy ? 'A gerar…' : `Gerar ${selected.length} Variaç${selected.length !== 1 ? 'ões' : 'ão'}`}
          </button>
        </div>
      </div>
    </div>
  );
};

// ─── CtdImageModal — padrão portal (card centrado, backdrop .45) ──────────────
const CtdImageModal = ({ items, index, onNavigate, onClose, campanha, userEmail, reloadImagens, reloadPrompts, onOpenVariations, onSelectVariation, inline = false, copies = [], campFull = null, onOpenTab }) => {
  const Icon        = window.Icon;
  const item        = items[index];
  const [pt, setPt] = React.useState(true);
  const [copied, setCopied] = React.useState(false);
  const [approving, setApproving] = React.useState(false);
  const [regenerating, setRegen]  = React.useState(false);
  const [restoring, setRestoring] = React.useState(false);

  const url        = item?.currentUrl || item?.image?.url || item?.image?.image_url;
  const image      = item?.image;
  const prompt     = item?.prompt;
  const peca       = item?.peca;
  const canalLabel = ctdCanalLabel(peca, image?.canal || prompt?.canal);
  const title      = peca?.titulo || image?.titulo || prompt?.titulo || 'Sem título';
  const subtitulo  = peca?.subtitulo || '';
  const format     = image?.formato || image?.format || prompt?.formato || '';
  const promptText = pt
    ? (prompt?.prompt_text_pt || prompt?.prompt_text || prompt?.prompt || image?.prompt_text_pt || image?.prompt_text || '—')
    : (prompt?.prompt_text || prompt?.prompt || image?.prompt_text || '—');
  const variations = Array.isArray(image?.magnific_variations) ? image.magnific_variations : [];
  const variationCount = variations.length;
  const activeVarIdx = item?.variationIdx ?? -1;
  const imageApproved = !!image?.image_approved_at;
  const filename = `${(peca?.titulo || image?.titulo || 'imagem').replace(/[^a-z0-9]+/gi, '_').toLowerCase()}${activeVarIdx >= 0 ? `_${variations[activeVarIdx]?.perspective || activeVarIdx}` : ''}.png`;

  const handleDownload = () => {
    if (!url) return;
    window.location.href = CtdAPI.downloadUrl(url, filename);
  };
  const handleCopyUrl = async () => {
    if (!url) return;
    try { await navigator.clipboard.writeText(url); setCopied(true); setTimeout(() => setCopied(false), 1500); } catch {}
  };
  const handleApproveImage = async () => {
    if (!prompt?.id) return;
    setApproving(true);
    try { await CtdAPI.approveImage(campanha.id, prompt.id, { approver_email: userEmail }); reloadImagens?.(); }
    catch (e) { alert('Erro a aprovar: ' + (e.message || 'desconhecido')); }
    setApproving(false);
  };
  const handleRegenerate = async () => {
    if (!prompt?.id) return;
    setRegen(true);
    try { await CtdAPI.generateImage(campanha.id, prompt.id, { actor_email: userEmail }); reloadImagens?.(); reloadPrompts?.(); }
    catch (e) { alert('Erro a regerar: ' + (e.message || 'desconhecido')); }
    setRegen(false);
  };
  const handleRestoreImage = async (restoreUrl) => {
    if (!prompt?.id || !restoreUrl) return;
    setRestoring(true);
    try {
      await CtdAPI.restoreImage(campanha.id, prompt.id, restoreUrl);
      reloadImagens?.();
      reloadPrompts?.();
    } catch (e) { alert('Erro ao restaurar: ' + (e.message || 'desconhecido')); }
    setRestoring(false);
  };
  const archivePrompt = prompt ? {
    ...prompt,
    magnific_url: prompt.magnific_url || image?.url || null,
    image_url: prompt.image_url || image?.url || null,
    magnific_history: prompt.magnific_history || image?.magnific_history || [],
  } : null;

  React.useEffect(() => {
    const onKey = (e) => {
      if (e.key === 'Escape')          onClose();
      else if (e.key === 'ArrowLeft')  onNavigate(-1);
      else if (e.key === 'ArrowRight') onNavigate( 1);
      else if (e.key === 'd' || e.key === 'D') handleDownload();
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [index, url]);

  if (!item) return null;

  const pieceIdx = Number.isFinite(Number(prompt?.plano_item_index)) ? Number(prompt.plano_item_index) : null;
  const pieceCopy = (copies || []).find(c => pieceIdx != null && Number(c.comm_plan_item_index) === pieceIdx) || null;
  const isMetaPiece = peca?.content_type === 'social_post' || pieceCopy?.content_type === 'social_post' || peca?.canal === 'meta_ads' || prompt?.canal === 'meta_ads' || pieceCopy?.canal === 'meta_ads' || peca?.content_type === 'meta_ads';
  const needsVideo = false;
  const hasVideo = !!prompt?.video_url;
  const videoApproved = !!prompt?.video_approved_at;
  const openStep = (tab) => onOpenTab?.({ tab, idx: pieceIdx, country: pieceCopy?.country || peca?.country || null, contentType: peca?.content_type || null });
  const nextAfterImage = !needsVideo ? 'peca' : (!hasVideo || !videoApproved) ? 'video' : 'peca';
  const nextAfterImageLabel = !needsVideo ? 'Peça final' : !hasVideo ? 'Gerar vídeo' : !videoApproved ? 'Rever e aprovar vídeo' : 'Peça final';

  if (inline && isMetaPiece) {
    return (
      <div style={{ display: 'grid', gridTemplateColumns: isMetaPiece ? 'minmax(292px, 320px) minmax(0, 1fr) 300px' : 'minmax(292px, 320px) minmax(0, 1fr)', gap: 20, alignItems: 'start' }}>
        <div style={{ minWidth: 0, display: 'flex', flexDirection: 'column', gap: 12 }}>
          <div style={{ background: '#fff', border: '1px solid #BBF7D0', borderRadius: 8, overflow: 'hidden' }}>
            <div style={{ padding: '10px 14px', borderBottom: '1px solid #DCFCE7', background: '#F0FDF4', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#166534', fontFamily: 'var(--font-mono, monospace)' }}>{imageApproved ? 'IMAGEM APROVADA' : 'IMAGEM GERADA'}</span>
              <span style={{ fontSize: 10, fontWeight: 700, color: '#166534' }}>{imageApproved ? (needsVideo ? 'Pronta para vídeo' : 'Pronta para peça final') : 'Por aprovar'}</span>
            </div>
            <div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 12 }}>
              <CtdPipelineStep n="1" label="Copy" detail={pieceCopy?.status === 'aprovado' ? 'Aprovada nesta peça' : 'Desta peça'} state={pieceCopy?.status === 'aprovado' ? 'done' : 'wait'} />
              <CtdPipelineStep n="2" label="Prompt" detail="Aprovado" state="done" onClick={() => openStep('prompts')} />
              <CtdPipelineStep n="3" label="Imagem" detail={imageApproved ? 'Aprovada' : 'Gerada · por aprovar'} state={imageApproved ? 'done' : 'current'} />
              {needsVideo && (
                <CtdPipelineStep n="4" label="Vídeo"
                  detail={!imageApproved ? 'Depois da imagem' : videoApproved ? 'Aprovado' : hasVideo ? 'Gerado · por aprovar' : 'Aguarda geração'}
                  state={!imageApproved ? 'wait' : (hasVideo && videoApproved) ? 'done' : 'current'}
                  onClick={imageApproved ? () => openStep('video') : null} />
              )}
              {!imageApproved && (
                <button type="button" onClick={handleApproveImage} disabled={approving || !prompt?.id} style={{ background: '#1F8A52', color: '#fff', border: 0, width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: approving ? 'not-allowed' : 'pointer' }}>
                  <Icon name="check" size={13} /> {approving ? 'A aprovar…' : 'Aprovar imagem'}
                </button>
              )}
              {imageApproved && (
                <button type="button" onClick={() => openStep(nextAfterImage)} style={{ background: '#3859D0', color: '#fff', border: 0, width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: 'pointer' }}>
                  {nextAfterImageLabel}
                </button>
              )}
              <button type="button" onClick={handleRegenerate} disabled={regenerating || !prompt?.id} style={{ background: 'transparent', color: '#112954', border: '1px solid var(--border-2, #E3E6EB)', width: '100%', padding: '8px 10px', borderRadius: 6, fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>
                {regenerating ? 'A gerar…' : 'Regerar imagem'}
              </button>
              <button type="button" onClick={() => onOpenVariations?.(image)} disabled={!image} style={{ background: 'transparent', color: '#112954', border: '1px solid var(--border-2, #E3E6EB)', width: '100%', padding: '8px 10px', borderRadius: 6, fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>Variações</button>
              <button type="button" onClick={handleDownload} disabled={!url} style={{ background: 'transparent', color: '#112954', border: '1px solid var(--border-2, #E3E6EB)', width: '100%', padding: '8px 10px', borderRadius: 6, fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>Download</button>
            </div>
          </div>
        </div>
        <div style={{ minWidth: 0 }}>
          <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
            <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'space-between' }}>
              <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>IMAGEM</span>
              <span style={{ fontSize: 10, color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>{format || '4:5'}</span>
            </div>
            <div style={{ padding: 16, background: '#f8fafc', display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: 420 }}>
              {url ? <img src={url} alt="" style={{ maxWidth: '100%', maxHeight: 640, objectFit: 'contain', display: 'block' }} /> : <div style={{ color: '#94a3b8', fontSize: 12 }}>Sem imagem</div>}
            </div>
          </div>
          {(image?.url || variations.length > 0) && (
            <div style={{ display: 'flex', gap: 8, marginTop: 12, overflowX: 'auto' }}>
              {image?.url && (
                <button type="button" onClick={() => onSelectVariation?.(-1)} style={{ flexShrink: 0, width: 72, padding: 0, border: `2px solid ${activeVarIdx < 0 ? '#3859D0' : 'transparent'}`, borderRadius: 6, overflow: 'hidden', cursor: 'pointer', background: 'transparent' }}>
                  <img src={image.url} alt="" style={{ width: '100%', height: 72, objectFit: 'cover', display: 'block' }} />
                </button>
              )}
              {variations.map((v, i) => v.url ? (
                <button type="button" key={i} onClick={() => onSelectVariation?.(i)} title={v.perspective_label || v.perspective} style={{ flexShrink: 0, width: 72, padding: 0, border: `2px solid ${activeVarIdx === i ? '#3859D0' : 'transparent'}`, borderRadius: 6, overflow: 'hidden', cursor: 'pointer', background: 'transparent' }}>
                  <img src={v.url} alt="" style={{ width: '100%', height: 72, objectFit: 'cover', display: 'block' }} />
                </button>
              ) : null)}
            </div>
          )}
          <div style={{ marginTop: 12 }}>
            <CtdImageHistoryStrip
              prompt={archivePrompt}
              currentUrl={url}
              disabled={regenerating || restoring}
              onRestore={handleRestoreImage}
              title={isMetaPiece ? 'Imagens deste anúncio' : 'Imagens desta peça'}
            />
          </div>
        </div>
        {isMetaPiece && (
          <div style={{ position: 'sticky', top: 76, minWidth: 0 }}>
            <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
              <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)' }}>
                <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>PRÉ-VISUALIZAÇÃO</span>
              </div>
              <CtdMetaAdsFeedMockup form={ctdCopyToPreviewForm(pieceCopy)} flush brandName={campFull?.brand_name || campanha?.brand_name} brandSlug={campFull?.brand_slug || campanha?.brand_slug} imageUrl={url} aspectRatio="4 / 5" />
            </div>
          </div>
        )}
      </div>
    );
  }

  const _cardEl = (
      <div style={inline ? {
        background: '#fff', borderRadius: 'var(--radius-lg, 10px)', width: '100%',
        height: 'calc(100vh - 220px)', minHeight: 520,
        border: '1px solid var(--border-1, #ECEFF5)',
        display: 'flex', flexDirection: 'column', overflow: 'hidden',
      } : {
        background: '#fff', borderRadius: 12, width: 'min(1400px, 95vw)', height: 'min(920px, 92vh)',
        display: 'flex', flexDirection: 'column', boxShadow: '0 20px 60px rgba(0,0,0,.25)', overflow: 'hidden',
      }}>
        {/* Header — só em modo modal (inline usa CtdBreadcrumb externo) */}
        {!inline && (
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '14px 20px', borderBottom: '1px solid #ECEFF5', flexShrink: 0 }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: '#3859D0', fontFamily: 'var(--font-mono, monospace)', marginBottom: 2 }}>
              {canalLabel} {format && <span style={{ opacity: 0.6, color: '#64748b' }}>· {format}</span>} {activeVarIdx >= 0 && <span style={{ color: '#3859D0' }}>· {variations[activeVarIdx]?.perspective_label}</span>}
            </div>
            <div style={{ fontSize: 13, fontWeight: 700, color: '#283252', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', fontFamily: 'var(--font-mono, monospace)' }} title={subtitulo || title}>{title}</div>
            {subtitulo && <div style={{ fontSize: 13, fontWeight: 600, color: '#334155', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', marginTop: 2 }}>{subtitulo}</div>}
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#64748b', fontFamily: 'var(--font-mono, monospace)' }}>
            <button onClick={() => onNavigate(-1)} disabled={items.length <= 1} className="btn btn-sm" style={{ width: 32, padding: 0, height: 28, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
              <Icon name="chevron" size={16} style={{ transform: 'rotate(90deg)' }} />
            </button>
            <span style={{ minWidth: 44, textAlign: 'center' }}>{index + 1} / {items.length}</span>
            <button onClick={() => onNavigate(1)} disabled={items.length <= 1} className="btn btn-sm" style={{ width: 32, padding: 0, height: 28, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
              <Icon name="chevron" size={16} style={{ transform: 'rotate(-90deg)' }} />
            </button>
          </div>
          <button onClick={onClose} className="btn btn-ghost btn-sm" style={{ width: 32, padding: 0, height: 28, display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }} title="Fechar (Esc)">
            <Icon name="x" size={16} />
          </button>
        </div>
        )}

        {/* Body */}
        <div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
          <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, background: '#f8fafc' }}>
            <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24, minHeight: 0 }}>
              {url ? (
                <img src={url} alt="" style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', display: 'block' }} />
              ) : (
                <div style={{ color: '#94a3b8', fontSize: 12 }}>Sem imagem</div>
              )}
            </div>
            {/* Thumbnail strip — Original + variations */}
            {(image?.url || variations.length > 0) && (
              <div style={{
                display: 'flex', gap: 8, padding: '10px 14px', overflowX: 'auto',
                background: '#fff', borderTop: '1px solid #ECEFF5', flexShrink: 0,
              }}>
                {image?.url && (() => {
                  const isActive = activeVarIdx < 0;
                  return (
                    <button onClick={() => onSelectVariation?.(-1)} title="Original"
                      style={{
                        flexShrink: 0, width: 72, padding: 0, border: `2px solid ${isActive ? 'var(--color-primary, #3859D0)' : 'transparent'}`,
                        borderRadius: 6, background: 'transparent', cursor: 'pointer', overflow: 'hidden',
                        position: 'relative',
                      }}>
                      <img src={image.url} alt="original" style={{ width: '100%', height: 72, objectFit: 'cover', display: 'block' }} />
                      <div style={{
                        position: 'absolute', bottom: 0, left: 0, right: 0,
                        background: 'linear-gradient(to top, rgba(0,0,0,0.72), transparent)',
                        color: '#fff', fontSize: 8, fontFamily: 'var(--font-mono, monospace)',
                        letterSpacing: '0.06em', padding: '2px 4px', textAlign: 'center',
                      }}>ORIGINAL</div>
                    </button>
                  );
                })()}
                {variations.map((v, i) => {
                  if (!v.url) return null;
                  const isActive = activeVarIdx === i;
                  return (
                    <button key={i} onClick={() => onSelectVariation?.(i)} title={v.perspective_label || v.perspective}
                      style={{
                        flexShrink: 0, width: 72, padding: 0, border: `2px solid ${isActive ? 'var(--color-primary, #3859D0)' : 'transparent'}`,
                        borderRadius: 6, background: 'transparent', cursor: 'pointer', overflow: 'hidden',
                        position: 'relative',
                      }}>
                      <img src={v.url} alt={v.perspective_label} style={{ width: '100%', height: 72, objectFit: 'cover', display: 'block' }} />
                      <div style={{
                        position: 'absolute', bottom: 0, left: 0, right: 0,
                        background: 'linear-gradient(to top, rgba(0,0,0,0.72), transparent)',
                        color: '#fff', fontSize: 8, fontFamily: 'var(--font-mono, monospace)',
                        letterSpacing: '0.06em', padding: '2px 4px', textAlign: 'center',
                        overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
                      }}>{(v.perspective_label || v.perspective || '').toUpperCase()}</div>
                    </button>
                  );
                })}
              </div>
            )}
          </div>
          <div style={{ width: 340, flexShrink: 0, background: '#fff', borderLeft: '1px solid #ECEFF5', overflowY: 'auto', padding: 16 }}>
            {/* Status */}
            <div style={{ fontSize: 10, letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', marginBottom: 6 }}>ESTADO</div>
            <div style={{ marginBottom: 14, display: 'flex', gap: 6, flexWrap: 'wrap' }}>
              {imageApproved ? (
                <span style={{ padding: '3px 8px', borderRadius: 4, fontSize: 10, fontWeight: 700, fontFamily: 'var(--font-mono, monospace)', background: '#E1F7E6', color: '#1F8A52', display: 'inline-flex', alignItems: 'center', gap: 4 }}>
                  <Icon name="check" size={12} /> APROVADA
                </span>
              ) : (
                <span style={{ padding: '3px 8px', borderRadius: 4, fontSize: 10, fontWeight: 700, fontFamily: 'var(--font-mono, monospace)', background: '#F1F5F9', color: '#475569' }}>POR APROVAR</span>
              )}
              {imageApproved && image?.image_approved_by && (
                <span style={{ fontSize: 10, color: '#64748b', fontFamily: 'var(--font-mono, monospace)', alignSelf: 'center' }}>
                  {String(image.image_approved_by).split('@')[0]} · {new Date(image.image_approved_at).toLocaleString('pt-PT', { day:'2-digit', month:'2-digit', hour:'2-digit', minute:'2-digit' })}
                </span>
              )}
            </div>

            {/* Acções */}
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, marginBottom: 16 }}>
              <button onClick={handleDownload} disabled={!url} className="btn btn-primary btn-sm" style={{ justifyContent: 'center' }}>
                <Icon name="download" size={14} /> Download
              </button>
              <button onClick={handleCopyUrl} disabled={!url} className="btn btn-sm" style={{ justifyContent: 'center' }}>
                <Icon name={copied ? 'check' : 'copy'} size={14} /> {copied ? 'Copiado' : 'Copiar URL'}
              </button>
              <button onClick={handleRegenerate} disabled={regenerating || !prompt?.id} className="btn btn-sm" style={{ justifyContent: 'center' }}>
                <Icon name="refresh" size={14} /> {regenerating ? 'A gerar…' : 'Regerar'}
              </button>
              <button onClick={() => onOpenVariations?.(image)} disabled={!image} className="btn btn-sm" style={{ justifyContent: 'center' }}>
                <Icon name="plus" size={14} /> Variações
              </button>
              {!imageApproved && prompt?.id && (
                <button onClick={handleApproveImage} disabled={approving} className="btn btn-ai btn-sm" style={{ gridColumn: 'span 2', justifyContent: 'center' }}>
                  <Icon name="check" size={14} /> {approving ? 'A aprovar…' : 'Aprovar Imagem'}
                </button>
              )}
            </div>

            {/* Prompt */}
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
              <div style={{ fontSize: 10, letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>PROMPT</div>
              <div style={{ display: 'flex', gap: 2 }}>
                <button onClick={() => setPt(true)}  style={{ background: pt ? '#3859D0' : 'transparent', color: pt ? '#fff' : '#64748b', border: '1px solid #e2e8f0', borderRadius: 3, padding: '2px 6px', fontSize: 9, cursor: 'pointer', fontFamily: 'var(--font-mono, monospace)' }}>PT</button>
                <button onClick={() => setPt(false)} style={{ background: !pt ? '#3859D0' : 'transparent', color: !pt ? '#fff' : '#64748b', border: '1px solid #e2e8f0', borderRadius: 3, padding: '2px 6px', fontSize: 9, cursor: 'pointer', fontFamily: 'var(--font-mono, monospace)' }}>EN</button>
              </div>
            </div>
            <div style={{ background: '#f8fafc', border: '1px solid #ECEFF5', borderRadius: 4, padding: 10, fontSize: 11, lineHeight: 1.5, color: '#334155', maxHeight: 220, overflowY: 'auto', marginBottom: 16, whiteSpace: 'pre-wrap' }}>
              {promptText}
            </div>

            {/* Variations grid */}
            {variationCount > 0 && (
              <>
                <div style={{ fontSize: 10, letterSpacing: '0.08em', textTransform: 'uppercase', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', marginBottom: 6 }}>
                  VARIAÇÕES ({variationCount}/{CTD_PERSPECTIVES.length})
                </div>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4, marginBottom: 12 }}>
                  <div onClick={() => onNavigate(0, -1)} style={{ aspectRatio: '1', borderRadius: 4, overflow: 'hidden', cursor: 'pointer', position: 'relative', border: activeVarIdx === -1 ? '2px solid #3859D0' : '2px solid transparent' }}>
                    <img src={image?.url} alt="base" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
                    <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, background: 'linear-gradient(transparent, rgba(0,0,0,0.85))', padding: '10px 3px 3px', fontSize: 7, color: '#fff', fontFamily: 'var(--font-mono, monospace)', textAlign: 'center' }}>BASE</div>
                  </div>
                  {variations.map((v, i) => (
                    <div key={i} onClick={() => onNavigate(0, i)} style={{ aspectRatio: '1', borderRadius: 4, overflow: 'hidden', cursor: 'pointer', position: 'relative', border: activeVarIdx === i ? '2px solid #3859D0' : '2px solid transparent' }}>
                      <img src={v.url} alt={v.perspective_label} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
                      <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, background: 'linear-gradient(transparent, rgba(0,0,0,0.85))', padding: '10px 3px 3px', fontSize: 7, color: '#fff', fontFamily: 'var(--font-mono, monospace)', textAlign: 'center' }}>{v.perspective_label}</div>
                    </div>
                  ))}
                </div>
              </>
            )}

            <div style={{ marginBottom: 16 }}>
              <CtdImageHistoryStrip
                prompt={archivePrompt}
                currentUrl={url}
                disabled={regenerating || restoring}
                onRestore={handleRestoreImage}
                title="Imagens desta peça"
              />
            </div>

            {/* Keyboard hints */}
            <div style={{ marginTop: 20, paddingTop: 12, borderTop: '1px solid #ECEFF5', fontSize: 9, color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', lineHeight: 1.7 }}>
              <div>ESC — fechar</div>
              <div>← / → — navegar</div>
              <div>D — download</div>
            </div>
          </div>
        </div>
      </div>
  );

  if (inline) return _cardEl;
  return (
    <div
      onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
      style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,.45)', zIndex: 1000,
        display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24,
      }}
    >{_cardEl}</div>
  );
};

// ─── CtdImageCard — 1 imagem gerada + variations sempre visíveis + toolbar ────
const CtdImageCard = ({ image, prompt, peca, campanha, userEmail, reloadImagens, reloadPrompts, onOpenModal, onOpenVariations }) => {
  const Icon = window.Icon;
  const [copied, setCopied]         = React.useState(false);
  const [approving, setApproving]   = React.useState(false);
  const [regenerating, setRegen]    = React.useState(false);
  const [editPrompt, setEditPrompt] = React.useState('');
  const [editing, setEditing]       = React.useState(false);
  const [editMessage, setEditMsg]   = React.useState('');
  const [hover, setHover]           = React.useState(false);

  const status       = image.status || (image.url ? 'ready' : 'pending');
  const isGenerating = ['generating', 'in_progress', 'pending', 'processing', 'queued'].includes(status);
  const isFailed     = ['failed', 'error', 'failed_generate'].includes(status);
  const url          = image.url || image.image_url || image.composed_url;
  const format       = image.formato || image.format || image.aspect_ratio || prompt?.formato || '';
  const variations   = Array.isArray(image.magnific_variations) ? image.magnific_variations : [];
  const history      = Array.isArray(image.magnific_history) ? image.magnific_history : [];

  const canalLabel   = ctdCanalLabel(peca, image.canal || prompt?.canal);
  const title        = peca?.titulo || image.titulo || prompt?.titulo || '—';
  const cardSubtitulo = peca?.subtitulo || '';
  const imageApproved = !!image.image_approved_at;

  const aspectStyle = format === '9:16' ? '9 / 16' : format === '4:5' ? '4 / 5' : format === '16:9' ? '16 / 9' : '1 / 1';
  const filename    = `${title.replace(/[^a-z0-9]+/gi, '_').toLowerCase()}.png`;

  const handleDownload = (e) => { e.stopPropagation(); if (!url) return; window.location.href = CtdAPI.downloadUrl(url, filename); };
  const handleCopyUrl = async (e) => {
    e.stopPropagation();
    if (!url) return;
    try { await navigator.clipboard.writeText(url); setCopied(true); setTimeout(() => setCopied(false), 1500); } catch {}
  };
  const handleApproveImage = async (e) => {
    e.stopPropagation();
    if (!prompt?.id) return;
    setApproving(true);
    try { await CtdAPI.approveImage(campanha.id, prompt.id, { approver_email: userEmail }); reloadImagens?.(); }
    catch (err) { alert('Erro a aprovar: ' + (err.message || 'desconhecido')); }
    setApproving(false);
  };
  const handleRegenerate = async (e) => {
    e.stopPropagation();
    if (!prompt?.id) return;
    setRegen(true);
    try { await CtdAPI.generateImage(campanha.id, prompt.id, { actor_email: userEmail }); reloadImagens?.(); }
    catch (err) { alert('Erro a regerar: ' + (err.message || 'desconhecido')); }
    setRegen(false);
  };
  const handleEditImage = async (e) => {
    e.stopPropagation();
    const p = editPrompt.trim();
    if (!p || !prompt?.id) return;
    setEditing(true);
    setEditMsg('A aplicar edição…');
    try {
      await CtdAPI.editImage(campanha.id, prompt.id, p, (msg) => setEditMsg(msg));
      setEditPrompt('');
      reloadImagens?.();
    } catch (err) {
      alert('Erro a editar: ' + (err.message || 'desconhecido'));
    }
    setEditing(false);
    setEditMsg('');
  };

  const outlineColor = imageApproved ? '#1F8A52' : 'var(--border-1)';
  const outlineWidth = imageApproved ? 2 : 1;

  const iconBtn = {
    background: 'transparent', border: 0, padding: 6, cursor: 'pointer',
    color: 'var(--text-muted, #4a5e7a)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
    borderRadius: 'var(--radius-sm, 6px)', transition: 'background 120ms',
  };

  return (
    <div
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      style={{
        background: 'var(--bg-surface, #ffffff)',
        border: `${outlineWidth}px solid ${outlineColor}`,
        borderRadius: 'var(--radius-lg, 10px)',
        overflow: 'hidden',
        boxShadow: hover ? 'var(--shadow-regular, 0 4px 8px rgba(40,41,61,0.04), 0 8px 16px rgba(96,97,112,0.10))' : 'var(--shadow-card, 0 1px 4px rgba(40,41,61,0.04), 0 4px 16px rgba(96,97,112,0.08))',
        transition: 'box-shadow 180ms ease, border-color 180ms ease',
        display: 'flex', flexDirection: 'column',
      }}
    >
      {/* Thumbnail — image hero */}
      <div
        onClick={() => url && !editing && onOpenModal(image, -1)}
        style={{
          width: '100%', aspectRatio: aspectStyle, background: 'var(--bg-subtle, #FAFAFA)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          overflow: 'hidden', position: 'relative', cursor: url && !editing ? 'zoom-in' : 'default',
        }}
      >
        {url ? (
          <img src={url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
        ) : isGenerating ? (
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8, color: 'var(--text-muted, #4a5e7a)' }}>
            <div style={{ width: 22, height: 22, border: '2px solid var(--border-2, #E3E6EB)', borderTopColor: 'var(--color-primary, #3859D0)', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
            <span style={{ fontSize: 10, fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>A gerar…</span>
          </div>
        ) : isFailed ? (
          <div style={{ color: '#dc2626', fontSize: 11, textAlign: 'center', padding: 12, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
            <Icon name="alert" size={16} />Falhou<span style={{ fontSize: 10, color: '#991b1b' }}>{image.error || ''}</span>
          </div>
        ) : (
          <div style={{ color: 'var(--text-dim, #7a8fa8)', fontSize: 11 }}>—</div>
        )}

        {/* Autor chip (bottom-left) */}
        {(() => {
          const author = image?.image_approved_by || image?.updated_by || prompt?.approved_by || prompt?.updated_by || null;
          if (!author) return null;
          const nick = String(author).split('@')[0] || '';
          const parts = nick.split('.').filter(Boolean);
          const initials = ((parts[0]?.[0] || '') + (parts[1]?.[0] || '')).toUpperCase() || nick.slice(0,2).toUpperCase();
          return (
            <div title={author} style={{
              position: 'absolute', bottom: 8, left: 8,
              width: 22, height: 22, borderRadius: '50%',
              background: 'var(--ai-500, #3859D0)', color: '#fff',
              fontSize: 9, fontWeight: 700, letterSpacing: '0.02em',
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
              fontFamily: 'var(--font-mono, monospace)',
              boxShadow: '0 2px 4px rgba(0,0,0,0.24)',
            }}>{initials}</div>
          );
        })()}

        {/* Approved badge (top-left) */}
        {imageApproved && (
          <div style={{
            position: 'absolute', top: 8, left: 8, background: '#1F8A52', color: '#fff',
            padding: '3px 8px', borderRadius: 'var(--radius-xs, 4px)', fontSize: 9, fontWeight: 700,
            fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.06em',
            display: 'inline-flex', alignItems: 'center', gap: 4,
            boxShadow: '0 2px 4px rgba(31,138,82,0.28)',
          }}>
            <Icon name="check" size={11} />APROVADA
          </div>
        )}

        {/* History badge (top-right) — mostra número de edições */}
        {history.length > 0 && (
          <div title={`${history.length} edição(ões) anterior(es)`} style={{
            position: 'absolute', top: 8, right: 8, background: 'rgba(17,41,84,0.85)', color: '#fff',
            padding: '3px 7px', borderRadius: 'var(--radius-xs, 4px)', fontSize: 9, fontWeight: 700,
            fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.06em',
          }}>v{history.length + 1}</div>
        )}

        {/* Editing overlay */}
        {editing && (
          <div style={{
            position: 'absolute', inset: 0, background: 'rgba(17,41,84,0.55)',
            display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
            gap: 12, color: '#fff', backdropFilter: 'blur(2px)',
          }}>
            <div style={{ width: 28, height: 28, border: '2px solid rgba(255,255,255,0.35)', borderTopColor: '#fff', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
            <span style={{ fontSize: 11, fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase', letterSpacing: '0.08em', textAlign: 'center', padding: '0 16px' }}>{editMessage || 'A editar…'}</span>
          </div>
        )}
      </div>

      {/* Meta line */}
      <div style={{ padding: '10px 12px 4px' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
          <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--color-primary, #3859D0)', fontFamily: 'var(--font-mono, monospace)' }}>{canalLabel}</span>
          {format && <span style={{ fontSize: 9, padding: '2px 6px', background: 'var(--bg-selected, #ECF1FB)', borderRadius: 'var(--radius-xs, 4px)', color: 'var(--text-muted, #4a5e7a)', fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.04em' }}>{format}</span>}
        </div>
        <div style={{
          fontSize: 11, color: 'var(--text, #112954)', lineHeight: 1.35, fontWeight: 700,
          fontFamily: 'var(--font-mono, monospace)',
          display: '-webkit-box', WebkitBoxOrient: 'vertical', WebkitLineClamp: 1, overflow: 'hidden',
        }} title={cardSubtitulo || title}>{title}</div>
        {cardSubtitulo && (
          <div style={{ fontSize: 11, color: '#334155', fontWeight: 600, marginTop: 2, display: '-webkit-box', WebkitBoxOrient: 'vertical', WebkitLineClamp: 2, overflow: 'hidden' }}>
            {cardSubtitulo}
          </div>
        )}
      </div>

      {/* Edit prompt input — inline below meta */}
      {url && prompt?.id && (
        <div style={{ padding: '6px 12px 8px', borderTop: '1px solid var(--border-1, #ECEFF5)', marginTop: 6 }}>
          <div style={{ display: 'flex', gap: 6, alignItems: 'stretch' }}>
            <input
              value={editPrompt}
              onChange={(e) => setEditPrompt(e.target.value)}
              onKeyDown={(e) => { if (e.key === 'Enter' && editPrompt.trim() && !editing) handleEditImage(e); }}
              placeholder="O que queres alterar? (ex: mais bruma, centrar máquina)"
              disabled={editing}
              style={{
                flex: 1, padding: '7px 10px', fontSize: 11.5,
                border: '1px solid var(--border-2, #E3E6EB)',
                borderRadius: 'var(--radius-sm, 6px)',
                background: editing ? 'var(--bg-subtle, #FAFAFA)' : 'var(--bg-surface, #ffffff)',
                color: 'var(--text, #112954)',
                fontFamily: 'var(--font-body, inherit)',
                outline: 'none',
              }}
              onFocus={(e) => { e.currentTarget.style.borderColor = 'var(--border-focus, #61A9FF)'; }}
              onBlur={(e)  => { e.currentTarget.style.borderColor = 'var(--border-2, #E3E6EB)'; }}
              onClick={(e) => e.stopPropagation()}
            />
            <button
              onClick={handleEditImage}
              disabled={!editPrompt.trim() || editing}
              title="Editar imagem"
              style={{
                background: editPrompt.trim() && !editing ? 'var(--color-primary, #3859D0)' : 'var(--bg-subtle, #FAFAFA)',
                color: editPrompt.trim() && !editing ? '#fff' : 'var(--text-dim, #7a8fa8)',
                border: '1px solid ' + (editPrompt.trim() && !editing ? 'var(--color-primary, #3859D0)' : 'var(--border-2, #E3E6EB)'),
                borderRadius: 'var(--radius-sm, 6px)',
                padding: '0 10px', cursor: editPrompt.trim() && !editing ? 'pointer' : 'not-allowed',
                display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                transition: 'background 120ms',
              }}
            >
              <Icon name="sparkle" size={14} />
            </button>
          </div>
        </div>
      )}

      {/* Toolbar — icons secondary + primary approve button */}
      {url && (
        <div style={{
          display: 'flex', gap: 4, padding: '8px 10px 10px',
          borderTop: '1px solid var(--border-1, #ECEFF5)',
          alignItems: 'center',
        }}>
          <button onClick={handleDownload} title="Download" style={iconBtn}
            onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bg-hover, #eef1f6)'}
            onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
            <Icon name="download" size={15} />
          </button>
          <button onClick={handleRegenerate} disabled={regenerating || !prompt?.id} title="Regerar do zero" style={{ ...iconBtn, opacity: regenerating ? 0.4 : 1 }}
            onMouseEnter={(e) => !regenerating && (e.currentTarget.style.background = 'var(--bg-hover, #eef1f6)')}
            onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
            <Icon name="refresh" size={15} />
          </button>
          <button onClick={handleCopyUrl} title={copied ? 'Copiado' : 'Copiar URL'} style={{ ...iconBtn, color: copied ? '#1F8A52' : 'var(--text-muted, #4a5e7a)' }}
            onMouseEnter={(e) => !copied && (e.currentTarget.style.background = 'var(--bg-hover, #eef1f6)')}
            onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
            <Icon name={copied ? 'check' : 'copy'} size={15} />
          </button>
          <button onClick={(e) => { e.stopPropagation(); onOpenVariations(image); }} title="Gerar variações (perspectivas)" style={iconBtn}
            onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bg-hover, #eef1f6)'}
            onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
            <Icon name="plus" size={15} />
          </button>

          {/* Approve primary button — takes remaining space */}
          {!imageApproved && prompt?.id ? (
            <button
              onClick={handleApproveImage}
              disabled={approving}
              style={{
                marginLeft: 'auto', background: '#1F8A52', color: '#fff', border: 0,
                padding: '7px 14px', cursor: 'pointer',
                borderRadius: 'var(--radius-sm, 6px)',
                fontSize: 11, fontWeight: 700, letterSpacing: '0.04em',
                display: 'inline-flex', alignItems: 'center', gap: 5,
                opacity: approving ? 0.5 : 1,
                boxShadow: '0 1px 2px rgba(31,138,82,0.24)',
              }}
            >
              <Icon name="check" size={13} /> APROVAR
            </button>
          ) : imageApproved ? (
            <span style={{
              marginLeft: 'auto', fontSize: 10, fontWeight: 700, letterSpacing: '0.06em',
              color: '#1F8A52', fontFamily: 'var(--font-mono, monospace)',
              display: 'inline-flex', alignItems: 'center', gap: 4,
            }}>
              <Icon name="check" size={12} /> APROVADA
            </span>
          ) : null}
        </div>
      )}

      {/* Variations always visible */}
      {variations.length > 0 && (
        <div style={{ padding: '0 12px 12px', borderTop: '1px solid var(--border-1, #ECEFF5)' }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', margin: '10px 0 6px' }}>
            <span style={{ fontSize: 9, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-dim, #7a8fa8)', fontFamily: 'var(--font-mono, monospace)' }}>
              VARIAÇÕES · {variations.length}/{CTD_PERSPECTIVES.length}
            </span>
            <button onClick={(e) => { e.stopPropagation(); onOpenVariations(image); }} className="btn btn-ghost btn-xs" style={{ height: 22, padding: '0 6px', color: 'var(--color-primary, #3859D0)' }}>
              <Icon name="plus" size={12} /> Mais
            </button>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }}>
            {variations.map((v, i) => (
              <div key={i} onClick={() => onOpenModal(image, i)} title={v.perspective_label} style={{
                aspectRatio: '1', background: 'var(--bg-subtle, #FAFAFA)', borderRadius: 'var(--radius-xs, 4px)', overflow: 'hidden', cursor: 'pointer', position: 'relative',
              }}>
                <img src={v.url} alt={v.perspective_label} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
                <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, background: 'linear-gradient(transparent, rgba(0,0,0,0.75))', padding: '10px 3px 3px', fontSize: 7, color: '#fff', fontFamily: 'var(--font-mono, monospace)', textAlign: 'center', letterSpacing: '0.03em' }}>
                  {v.perspective_label}
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

// ─── CtdImageApprovalStrip — status bar aprovação imagens (mirror de prompts) ─
const CtdImageApprovalStrip = ({ campanhaId, imagens, prompts, userEmail, reloadImagens, onGenerate, generating, pendingImage }) => {
  const Icon = window.Icon;
  const [busy, setBusy] = React.useState(null);

  const withImage       = (imagens || []).filter(i => i.url).length;
  const approvedImgs    = (imagens || []).filter(i => i.image_approved_at).length;
  const approvedPrompts = (prompts  || []).filter(p => p.status === 'approved').length;
  const lastApprovedAt  = (imagens  || [])
    .map(i => i.image_approved_at).filter(Boolean).sort().slice(-1)[0];
  const lastApprovedBy  = (imagens  || []).find(i => i.image_approved_at === lastApprovedAt)?.image_approved_by;

  const allApproved   = withImage > 0 && approvedImgs === withImage;
  const canApproveAll = withImage > 0 && approvedImgs < withImage;

  const label = allApproved ? 'IMAGENS APROVADAS' : approvedImgs > 0 ? 'IMAGENS EM APROVAÇÃO' : 'IMAGENS';
  const bg    = allApproved ? '#E1F7E6' : approvedImgs > 0 ? '#FEF3C7' : '#F1F5F9';
  const fg    = allApproved ? '#1F8A52' : approvedImgs > 0 ? '#92400E' : '#475569';

  const handleApproveAll = async () => {
    setBusy('approve');
    try { await CtdAPI.approveAllImages(campanhaId, { approver_email: userEmail }); reloadImagens?.(); }
    catch (e) { alert(e.message || 'Erro ao aprovar'); }
    setBusy(null);
  };

  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',
      background: 'var(--bg-surface, #fff)', borderRadius: 'var(--radius-md, 8px)',
      boxShadow: 'var(--shadow-card, 0 1px 4px rgba(40,41,61,0.04), 0 4px 16px rgba(96,97,112,0.08))',
      border: '1px solid var(--border-1, #ECEFF5)', marginBottom: 14, flexWrap: 'wrap',
    }}>
      <span style={{
        fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em',
        padding: '3px 8px', borderRadius: 'var(--radius-xs, 4px)', background: bg, color: fg, fontFamily: 'var(--font-mono, monospace)',
      }}>{label}</span>
      <span style={{ fontSize: 11, color: 'var(--text-muted, #4a5e7a)' }}>
        {withImage}/{approvedPrompts} geradas
        {approvedImgs > 0 && ` · ${approvedImgs} aprovadas`}
        {pendingImage > 0 && ` · ${pendingImage} por gerar`}
        {lastApprovedAt && ` · última aprovação ${new Date(lastApprovedAt).toLocaleString('pt-PT')}${lastApprovedBy ? ` por ${String(lastApprovedBy).split('@')[0]}` : ''}`}
      </span>
      <div style={{ marginLeft: 'auto', display: 'flex', gap: 6, alignItems: 'center' }}>
        {pendingImage > 0 && (
          <button className="btn btn-ai btn-sm" onClick={onGenerate} disabled={generating}>
            <Icon name="sparkle" size={14} />
            {generating ? 'A gerar…' : `Gerar ${pendingImage} em falta`}
          </button>
        )}
        {canApproveAll && (
          <button className="btn btn-primary btn-sm" onClick={handleApproveAll} disabled={!!busy}>
            <Icon name="check" size={14} />
            {busy === 'approve' ? 'A aprovar…' : 'Aprovar Todas'}
          </button>
        )}
        {allApproved && (
          <span style={{ fontSize: 10, fontWeight: 700, padding: '3px 10px', borderRadius: 99, background: '#E1F7E6', color: '#1F8A52', fontFamily: 'var(--font-mono, monospace)', display: 'inline-flex', alignItems: 'center', gap: 4 }}>
            <Icon name="check" size={11} />
            Todas as {withImage} imagens aprovadas
          </span>
        )}
      </div>
    </div>
  );
};

// ─── CtdTabImagens — grid de imagens agrupadas por canal ───────────────────────
const CtdTabImagens = ({ campanha, imagens, prompts, copies, campFull, userEmail, reloadImagens, reloadPrompts, onBackToKanban, focusPiece, onNavigate: onOpenTab, lockCanal }) => {
  const Icon = window.Icon;
  const [generating, setGenerating] = React.useState(false);
  const [error, setError]           = React.useState(null);
  const [genStep, setGenStep]       = React.useState({ step: 0, total: 0, message: '' });
  const [modalState, setModalState] = React.useState(null);
  const [varModalImg, setVarModalImg] = React.useState(null);
  const [selectedIds, setSelectedIds] = React.useState(new Set());
  const [filterCanal, setFilterCanal] = React.useState(lockCanal || 'all');
  React.useEffect(() => { if (lockCanal) setFilterCanal(lockCanal); }, [lockCanal]);
  const [filterCountry, setFilterCountry] = React.useState('all');
  const [filterStatus, setFilterStatus] = React.useState('all');
  const [openKey, setOpenKey] = React.useState(null);
  const [rowBusy, setRowBusy] = React.useState(new Set());
  const [pieceVarIdx, setPieceVarIdx] = React.useState(-1);

  const commPlan = (campFull?.proposta_json?.comm_plan) || [];
  const markets = (campFull?.estrategia_json?.markets) || [];
  const primaryCountry = markets[0]?.country || markets[0]?.pais || 'PT';
  const promptById = React.useMemo(() => {
    const m = new Map();
    (prompts || []).forEach(p => m.set(p.id, p));
    return m;
  }, [prompts]);

  // Enriched images: image + matching prompt + matching peça
  const enrichedImgs = React.useMemo(() => (imagens || []).map(img => {
    const prompt = promptById.get(img.id) || (prompts || []).find(p => p.magnific_url === img.url) || null;
    const peca   = prompt ? ctdMatchPeca(prompt, commPlan) : null;
    return { image: img, prompt, peca };
  }), [imagens, promptById, commPlan, prompts]);

  const byCanal = React.useMemo(() => {
    const map = new Map();
    enrichedImgs.forEach(item => {
      const canal = ctdCanalLabel(item.peca, item.image.canal || item.prompt?.canal);
      if (!map.has(canal)) map.set(canal, []);
      map.get(canal).push(item);
    });
    return [...map.entries()];
  }, [enrichedImgs]);

  const flatItems = React.useMemo(() => byCanal.flatMap(([, arr]) => arr), [byCanal]);

  const detectCountry = (peca) => {
    if (peca?.country) return String(peca.country).toUpperCase();
    const m = String(peca?.titulo || '').match(/^\[([A-Z]{2})\]/);
    if (m) return m[1];
    return primaryCountry;
  };
  const planRows = React.useMemo(() => {
    const out = [];
    commPlan.forEach((peca, idx) => {
      if (['campanha_paid', 'paid_campaign'].includes(peca.content_type)) return;
      if (ctdPecaArchived(peca)) return;
      if (ctdEmailBrevo(peca)) return;
      if (ctdHideWaFromPlan(peca)) return;
      const country = detectCountry(peca);
      const copy = (copies || []).find(c => Number(c.comm_plan_item_index) === idx && (!c.country || String(c.country).toUpperCase() === country)) || null;
      const prompt = ctdPromptForPeca(prompts, idx, peca, copy);
      const stale = ctdPromptIsStale(prompt, copy);
      const rawUrl = prompt?.magnific_url || prompt?.image_url || null;
      const url = stale ? null : rawUrl;
      const imageRec = (imagens || []).find(i => prompt && (i.id === prompt.id || i.prompt_id === prompt.id || (rawUrl && (i.url === rawUrl || i.magnific_url === rawUrl)))) || null;
      const image = imageRec || (url ? {
        id: prompt.id,
        url,
        image_approved_at: prompt.image_approved_at || null,
        image_approved_by: prompt.image_approved_by || null,
        magnific_variations: prompt.magnific_variations || [],
        magnific_history: prompt.magnific_history || [],
        formato: prompt.formato || (peca.content_type === 'social_post' ? '4:5' : ''),
      } : null);
      const approved = !stale && !!(prompt?.image_approved_at || imageRec?.image_approved_at);
      const status = stale ? 'desactualizado' : !url ? 'pendente' : approved ? 'aprovado' : 'gerado';
      out.push({
        key: `${idx}::${country}`, idx, country, peca, copy, prompt, image, url, status, stale,
        app: ctdPromptApp(prompt), sceneLabel: ctdPromptSceneLabel(prompt),
        canGenerate: !!(prompt?.id && prompt.status === 'approved' && !stale),
      });
    });
    return out;
  }, [commPlan, copies, prompts, imagens, primaryCountry]);

  React.useEffect(() => {
    setOpenKey(prev => {
      if (!prev) return prev;
      return planRows.some(r => r.key === prev) ? prev : null;
    });
  }, [planRows]);

  const focusToken = focusPiece ? `${focusPiece.idx}:${focusPiece.country || ''}:${focusPiece.titulo || ''}:${ctdPieceCode(focusPiece.titulo)}` : '';
  const openedFocus = React.useRef('');
  React.useEffect(() => {
    if (!focusToken || openedFocus.current === focusToken || !planRows.length) return;
    const idx = Number(focusPiece.idx);
    const wantTitle = ctdNormTitle(focusPiece.titulo);
    const wantCode = ctdPieceCode(focusPiece.titulo);
    const row = (wantCode && planRows.find(r => ctdPieceCode(r.peca?.titulo) === wantCode))
      || (wantTitle && planRows.find(r => ctdNormTitle(r.peca?.titulo) === wantTitle))
      || (Number.isFinite(idx) && planRows.find(r => r.idx === idx && (!focusPiece.country || r.country === focusPiece.country)))
      || (Number.isFinite(idx) && planRows.find(r => r.idx === idx))
      || null;
    if (!row) return;
    openedFocus.current = focusToken;
    setOpenKey(row.key);
    setPieceVarIdx(-1);
    setModalState(null);
  }, [focusToken, planRows, focusPiece]);

  const genStarted = React.useRef('');
  React.useEffect(() => {
    if (!focusPiece?.generate || !focusToken || genStarted.current === focusToken || !campanha?.id) return;
    const idx = Number(focusPiece.idx);
    if (!Number.isFinite(idx)) return;
    const peca = commPlan[idx];
    const prompt = ctdPromptForPeca(prompts, idx, peca, null);
    if (!prompt?.id || prompt.status !== 'approved') return;
    if (prompt.magnific_url || prompt.image_url) return;
    genStarted.current = focusToken;
    let cancelled = false;
    (async () => {
      setGenerating(true); setError(null);
      setGenStep({ step: 1, total: 1, message: 'A gerar imagem…' });
      try {
        await CtdAPI.generateImage(campanha.id, prompt.id, { actor_email: userEmail }, (s, t, m) => {
          if (!cancelled) setGenStep({ step: s || 1, total: t || 1, message: m || 'A gerar imagem…' });
        });
        reloadImagens?.();
        reloadPrompts?.();
      } catch (e) { if (!cancelled) setError(e.message || 'Erro ao gerar imagem'); }
      if (!cancelled) setGenerating(false);
    })();
    return () => { cancelled = true; };
  }, [focusToken, focusPiece, prompts, commPlan, campanha?.id, userEmail]);

  const approvedPrompts = (prompts || []).filter(p => p.status === 'approved');
  const withImage       = approvedPrompts.filter(p => p.magnific_url).length;
  const pendingImage    = approvedPrompts.length - withImage;
  const totalImgs       = (imagens || []).length;

  const handleOpenModal = (image, variationIdx = -1) => {
    const flatIdx = flatItems.findIndex(it => it.image.id === image.id);
    if (flatIdx < 0) return;
    setModalState({ index: flatIdx, variationIdx });
  };
  const handleNavigate = (delta, forceVarIdx) => {
    setModalState(prev => {
      if (!prev) return prev;
      if (delta === 0 && typeof forceVarIdx === 'number') {
        return { ...prev, variationIdx: forceVarIdx };
      }
      const canal = ctdCanalLabel(flatItems[prev.index]?.peca || {});
      const indexes = flatItems.map((it, i) => ctdCanalLabel(it?.peca || {}) === canal ? i : -1).filter(i => i >= 0);
      const pos = indexes.indexOf(prev.index);
      const nextPos = pos + delta;
      if (nextPos < 0 || nextPos >= indexes.length) return prev;
      return { index: indexes[nextPos], variationIdx: -1 };
    });
  };
  const modalItem = React.useMemo(() => {
    if (!modalState) return null;
    const it = flatItems[modalState.index];
    if (!it) return null;
    const varIdx = modalState.variationIdx;
    const vars   = Array.isArray(it.image.magnific_variations) ? it.image.magnific_variations : [];
    const currentUrl = varIdx >= 0 && vars[varIdx] ? vars[varIdx].url : it.image.url;
    return { ...it, variationIdx: varIdx, currentUrl };
  }, [modalState, flatItems]);

  const handleGenerateAll = async () => {
    if (!campanha?.id || pendingImage === 0) return;
    setGenerating(true); setError(null);
    setGenStep({ step: 0, total: pendingImage, message: 'A iniciar...' });
    try {
      const result = await CtdAPI.generateAllImages(
        campanha.id, { actor_email: userEmail },
        (step, total, message) => setGenStep({ step, total, message }),
      );
      reloadImagens?.(); reloadPrompts?.();
      if (result?.failed > 0) setError(`Geradas ${result.generated}. Falharam ${result.failed} — vê consola/logs.`);
    } catch (e) { setError(e.message || 'Erro ao gerar imagens'); }
    setGenerating(false);
    setGenStep({ step: 0, total: 0, message: '' });
  };

  const approvedCount = (imagens || []).filter(i => i.image_approved_at).length;
  const allCanais = ['all', ...new Set(byCanal.map(([c]) => c))];
  const canalImagens = lockCanal || filterCanal;
  const filteredByCanal = canalImagens === 'all' ? byCanal : byCanal.filter(([c]) => c === canalImagens);
  const totalFiltered = filteredByCanal.reduce((s, [, arr]) => s + arr.length, 0);

  const handleToggleSelect = (id) => setSelectedIds(prev => {
    const n = new Set(prev);
    n.has(id) ? n.delete(id) : n.add(id);
    return n;
  });

  const handleGenerateSelected = async () => {
    if (!selectedIds.size) return;
    setGenerating(true); setError(null);
    const selected = flatItems.filter(it => selectedIds.has(it.image.id));
    setGenStep({ step: 0, total: selected.length, message: 'A iniciar...' });
    let done = 0;
    for (const it of selected) {
      if (it.prompt?.id) {
        try {
          setGenStep({ step: ++done, total: selected.length, message: `A gerar ${it.peca?.titulo || it.image.canal}…` });
          await CtdAPI.generateImage(campanha.id, it.prompt.id, { actor_email: userEmail }, () => {});
        } catch {}
      }
    }
    reloadImagens?.(); reloadPrompts?.();
    setSelectedIds(new Set());
    setGenerating(false);
    setGenStep({ step: 0, total: 0, message: '' });
  };

  const openRow = openKey ? (planRows.find(r => r.key === openKey) || null) : null;
  const runGenerateRow = async (row) => {
    if (!row?.prompt?.id || !row.canGenerate) return;
    setRowBusy(prev => { const n = new Set(prev); n.add(row.key); return n; });
    setError(null);
    setGenerating(true);
    setGenStep({ step: 1, total: 1, message: 'A gerar imagem…' });
    try {
      await CtdAPI.generateImage(campanha.id, row.prompt.id, { actor_email: userEmail }, (s, t, m) => {
        setGenStep({ step: s || 1, total: t || 1, message: m || 'A gerar imagem…' });
      });
      reloadImagens?.(); reloadPrompts?.();
    } catch (e) { setError(e.message || 'Erro ao gerar imagem'); }
    setGenerating(false);
    setRowBusy(prev => { const n = new Set(prev); n.delete(row.key); return n; });
  };
  const runApproveRow = async (row) => {
    if (!row?.prompt?.id) return;
    setRowBusy(prev => { const n = new Set(prev); n.add(row.key); return n; });
    setError(null);
    try { await CtdAPI.approveImage(campanha.id, row.prompt.id, { approver_email: userEmail }); reloadImagens?.(); }
    catch (e) { setError(e.message || 'Erro ao aprovar'); }
    setRowBusy(prev => { const n = new Set(prev); n.delete(row.key); return n; });
  };

  if (openRow) {
    const navRows = ctdNavRowsSameCanal(planRows, openRow.peca);
    const rowIndex = Math.max(0, navRows.findIndex(r => r.key === openRow.key));
    const prevRow = rowIndex > 0 ? navRows[rowIndex - 1] : null;
    const nextRow = rowIndex < navRows.length - 1 ? navRows[rowIndex + 1] : null;
    const pecaTitle = openRow.peca?.titulo || `Peça #${openRow.idx + 1}`;
    const canalCat = openRow.peca ? ctdGetCategoria(openRow.peca.content_type) : null;
    const goRow = (r) => { setOpenKey(r.key); setPieceVarIdx(-1); };
    const isMeta = ctdCanalLabel(openRow.peca) === 'Meta Ads';
    const busyRow = rowBusy.has(openRow.key) || (generating && !openRow.url);
    const items = navRows.map(r => {
      const vars = Array.isArray(r.image?.magnific_variations) ? r.image.magnific_variations : [];
      const active = r.key === openRow.key;
      const vIdx = active ? pieceVarIdx : -1;
      const currentUrl = vIdx >= 0 && vars[vIdx]?.url ? vars[vIdx].url : r.url;
      return { image: r.image || { url: r.url || null, magnific_variations: [] }, prompt: r.prompt, peca: r.peca, currentUrl, variationIdx: vIdx };
    });
    return (
      <div style={{ width: '100%' }}>
        <div style={{ position: 'sticky', top: 0, zIndex: 10, background: 'var(--bg-page, #F4F6FA)', padding: '12px 32px 0' }}>
          <CtdBreadcrumb
            trail={[
              { label: campanha?.titulo || 'Campanha', onClick: onBackToKanban },
              { label: 'Imagens', onClick: () => setOpenKey(null) },
              { label: pecaTitle, badge: canalCat ? { text: ctdCanalLabel(openRow.peca), color: canalCat.color } : null },
            ]}
            onBack={() => setOpenKey(null)}
            currentPos={rowIndex + 1}
            totalItems={navRows.length || 1}
            onPrev={prevRow ? () => goRow(prevRow) : null}
            onNext={nextRow ? () => goRow(nextRow) : null}
          />
        </div>
        <div style={{ padding: '12px 32px 32px' }}>
          {openRow.url ? (
            <CtdImageModal
              items={items}
              index={rowIndex}
              onNavigate={(delta, forceVar) => {
                if (delta === 0 && typeof forceVar === 'number') { setPieceVarIdx(forceVar); return; }
                const next = navRows[rowIndex + delta];
                if (next) goRow(next);
              }}
              onClose={() => setOpenKey(null)}
              campanha={campanha}
              userEmail={userEmail}
          lockCanal={lockCanal}
              reloadImagens={reloadImagens}
              reloadPrompts={reloadPrompts}
              copies={copies}
              campFull={campFull}
              onOpenTab={onOpenTab}
              onOpenVariations={(img) => setVarModalImg(img)}
              onSelectVariation={(vIdx) => setPieceVarIdx(vIdx)}
              inline={true}
            />
          ) : (
            <div style={{ display: 'grid', gridTemplateColumns: isMeta ? 'minmax(292px, 320px) minmax(0, 1fr) 300px' : 'minmax(292px, 320px) minmax(0, 1fr)', gap: 20, alignItems: 'start' }}>
              <div style={{ background: '#fff', border: '1px solid #BBF7D0', borderRadius: 8, overflow: 'hidden' }}>
                <div style={{ padding: '10px 14px', background: '#F0FDF4', borderBottom: '1px solid #DCFCE7' }}>
                  <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#166534', fontFamily: 'var(--font-mono, monospace)' }}>{busyRow ? 'A GERAR IMAGEM' : 'IMAGEM'}</span>
                </div>
                <div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 10 }}>
                  <CtdPipelineStep n="1" label="Copy" detail={openRow.copy?.status === 'aprovado' ? 'Aprovada nesta peça' : 'Desta peça'} state={openRow.copy?.status === 'aprovado' ? 'done' : 'wait'} />
                  <CtdPipelineStep n="2" label="Prompt" detail={openRow.canGenerate ? 'Aprovado' : openRow.stale ? 'Desactualizado' : 'Por aprovar'} state={openRow.canGenerate ? 'done' : 'wait'} onClick={() => onOpenTab?.({ tab: 'prompts', idx: openRow.idx, country: openRow.country, titulo: openRow.peca?.titulo, contentType: openRow.peca?.content_type })} />
                  <CtdPipelineStep n="3" label="Imagem" detail={busyRow ? 'A gerar' : 'Aguarda geração'} state="current" />
                  {openRow.canGenerate && (
                    <button type="button" onClick={() => runGenerateRow(openRow)} disabled={busyRow} style={{ background: '#3859D0', color: '#fff', border: 0, width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: busyRow ? 'not-allowed' : 'pointer' }}>
                      {busyRow ? 'A gerar…' : 'Gerar imagem'}
                    </button>
                  )}
                  {!openRow.canGenerate && (
                    <button type="button" onClick={() => onOpenTab?.({ tab: 'prompts', idx: openRow.idx, country: openRow.country, titulo: openRow.peca?.titulo, contentType: openRow.peca?.content_type })} style={{ background: 'transparent', color: '#112954', border: '1px solid var(--border-2, #E3E6EB)', width: '100%', padding: '8px 10px', borderRadius: 6, fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>
                      {openRow.stale ? 'Gerar prompt desta peça' : 'Rever prompt'}
                    </button>
                  )}
                </div>
              </div>
              <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, minHeight: 420, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 32 }}>
                <div style={{ textAlign: 'center', width: '100%' }}>
                  {busyRow ? <CtdGenProgress step={genStep.step} total={genStep.total} message={genStep.message || 'A gerar imagem…'} /> : <div style={{ color: '#94a3b8', fontSize: 13 }}>Ainda sem imagem</div>}
                  {error && <div style={{ marginTop: 12, color: '#dc2626', fontSize: 12 }}>{error}</div>}
                </div>
              </div>
              {isMeta && (
                <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
                  <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)' }}>
                    <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>PRÉ-VISUALIZAÇÃO</span>
                  </div>
                  <CtdMetaAdsFeedMockup form={ctdCopyToPreviewForm(openRow.copy)} flush brandName={campanha?.brand_name || campFull?.briefing?.brand_name || campFull?.brand_name} brandSlug={campanha?.brand_slug || campFull?.briefing?.brand_slug || campFull?.brand_slug} aspectRatio="4 / 5" />
                </div>
              )}
            </div>
          )}
          {varModalImg && (
            <CtdVariationsModal
              campanha={campanha}
              image={varModalImg}
              onClose={() => setVarModalImg(null)}
              onDone={() => { setVarModalImg(null); reloadImagens?.(); }}
            />
          )}
        </div>
      </div>
    );
  }

  if (modalState && modalItem) {
    const it = flatItems[modalState.index];
    const canalCat = it?.peca ? ctdGetCategoria(it.peca.content_type) : null;
    const pecaTitle = it?.peca?.titulo || it?.image?.titulo || it?.prompt?.titulo || 'Imagem';
    const canalNow = ctdCanalLabel(it?.peca || {});
    const canalIndexes = flatItems.map((x, i) => ctdCanalLabel(x?.peca || {}) === canalNow ? i : -1).filter(i => i >= 0);
    const canalPos = Math.max(0, canalIndexes.indexOf(modalState.index));
    return (
      <div style={{ padding: '20px 32px 32px', width: '100%' }}>
        <CtdBreadcrumb
          trail={[
            { label: campanha?.titulo || 'Campanha', onClick: onBackToKanban },
            { label: 'Imagens', onClick: () => setModalState(null) },
            { label: pecaTitle, badge: canalCat ? { text: ctdCanalLabel(it.peca), color: canalCat.color } : null },
          ]}
          onBack={() => setModalState(null)}
          currentPos={canalPos + 1}
          totalItems={canalIndexes.length}
          onPrev={canalPos > 0 ? () => handleNavigate(-1) : null}
          onNext={canalPos < canalIndexes.length - 1 ? () => handleNavigate(1) : null}
        />
        <CtdImageModal
          items={flatItems.map((x, idx) => idx === modalState.index ? modalItem : x)}
          index={modalState.index}
          onNavigate={handleNavigate}
          onClose={() => setModalState(null)}
          campanha={campanha}
          userEmail={userEmail}
          lockCanal={lockCanal}
          reloadImagens={reloadImagens}
          reloadPrompts={reloadPrompts}
          copies={copies}
          campFull={campFull}
          onOpenTab={onOpenTab}
          onOpenVariations={(img) => setVarModalImg(img)}
          onSelectVariation={(vIdx) => setModalState(prev => prev ? { ...prev, variationIdx: vIdx } : prev)}
          inline={true}
        />
        {varModalImg && (
          <CtdVariationsModal
            campanha={campanha}
            image={varModalImg}
            onClose={() => setVarModalImg(null)}
            onDone={() => { setVarModalImg(null); reloadImagens?.(); }}
          />
        )}
      </div>
    );
  }

  const CANAL_ORDER_TBL = ['Meta Ads', 'Email', 'WhatsApp', 'Blog', 'Landing', 'LED', 'Site', 'Comunicação Interna'];
  const COUNTRY_ORDER_TBL = ['PT', 'ES', 'FR', 'IT', 'DE', 'EN'];
  const canaisPresent = [...new Set(planRows.map(r => ctdCanalLabel(r.peca)).filter(Boolean))].sort((a, b) => {
    const ia = CANAL_ORDER_TBL.indexOf(a), ib = CANAL_ORDER_TBL.indexOf(b);
    if (ia !== -1 && ib !== -1) return ia - ib;
    if (ia !== -1) return -1;
    if (ib !== -1) return 1;
    return a.localeCompare(b);
  });
  const countriesPresent = [...new Set(planRows.map(r => r.country))];
  const canalPlano = lockCanal || filterCanal;
  const filtered = planRows.filter(r => {
    if (canalPlano !== 'all' && ctdCanalLabel(r.peca) !== canalPlano) return false;
    if (filterCountry !== 'all' && r.country !== filterCountry) return false;
    if (filterStatus === 'pendente') return r.status === 'pendente' || r.status === 'desactualizado';
    if (filterStatus !== 'all' && r.status !== filterStatus) return false;
    return true;
  }).sort((a, b) => {
    const ia = COUNTRY_ORDER_TBL.indexOf(a.country), ib = COUNTRY_ORDER_TBL.indexOf(b.country);
    const pa = ia === -1 ? 99 : ia, pb = ib === -1 ? 99 : ib;
    if (pa !== pb) return pa - pb;
    return a.idx - b.idx;
  });
  const counters = {
    total: planRows.length,
    pendente: planRows.filter(r => r.status === 'pendente' || r.status === 'desactualizado').length,
    gerado: planRows.filter(r => r.status === 'gerado').length,
    aprovado: planRows.filter(r => r.status === 'aprovado').length,
    porGerar: planRows.filter(r => r.status === 'pendente' && r.canGenerate).length,
  };
  const allFilteredSel = filtered.length > 0 && filtered.every(r => selectedIds.has(r.key));
  const toggleAll = () => setSelectedIds(prev => {
    const next = new Set(prev);
    if (allFilteredSel) filtered.forEach(r => next.delete(r.key));
    else filtered.forEach(r => next.add(r.key));
    return next;
  });
  const toggleOne = (key) => setSelectedIds(prev => {
    const next = new Set(prev);
    if (next.has(key)) next.delete(key); else next.add(key);
    return next;
  });
  const runSelectedImages = async () => {
    const targets = filtered.filter(r => selectedIds.has(r.key) && r.canGenerate && r.status !== 'aprovado');
    for (const r of targets) await runGenerateRow(r);
    setSelectedIds(new Set());
  };
  const runAllPendingImages = async () => {
    const targets = planRows.filter(r => r.status === 'pendente' && r.canGenerate && (!lockCanal || ctdCanalLabel(r.peca) === lockCanal));
    for (const r of targets) await runGenerateRow(r);
  };
  const approveAllGeradas = async () => {
    const targets = planRows.filter(r => r.status === 'gerado' && r.prompt?.id);
    for (const r of targets) await runApproveRow(r);
  };
  const STATUS_STYLE = {
    pendente: { bg: '#F1F5F9', fg: '#475569', label: 'Pendente' },
    desactualizado: { bg: '#FEF3C7', fg: '#92400E', label: 'Copy mudou' },
    gerado: { bg: '#FEF3C7', fg: '#92400E', label: 'Gerada' },
    aprovado: { bg: '#DCFCE7', fg: '#166534', label: 'Aprovada' },
  };
  const thSt = { textAlign: 'left', padding: '8px 12px', fontSize: 10, fontFamily: 'var(--font-display)', fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--text-muted)', whiteSpace: 'nowrap', borderBottom: '1px solid var(--border)' };
  const tdSt = { padding: '10px 12px', verticalAlign: 'middle', borderBottom: '1px solid var(--border)', fontSize: 13, color: 'var(--text)' };
  const canalBadge = (peca) => {
    const label = ctdCanalLabel(peca);
    const cat = ctdGetCategoria(peca.content_type);
    return <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.06em', padding: '3px 7px', borderRadius: 4, background: `${cat.color}18`, color: cat.color, fontFamily: 'var(--font-mono, monospace)' }}>{label.toUpperCase()}</span>;
  };

  const paisesImagem = [...new Set(planRows.filter(r => !lockCanal || ctdCanalLabel(r.peca) === lockCanal).map(r => r.country).filter(Boolean))];
  const porGerarVista = planRows.filter(r => r.status === 'pendente' && r.canGenerate && (!lockCanal || ctdCanalLabel(r.peca) === lockCanal)).length;
  return (
    <div style={{ padding: '20px 32px 32px', width: '100%' }}>
      {(!lockCanal || paisesImagem.length > 1) && (
      <div style={{ display: 'flex', gap: 8, alignItems: 'center', padding: '10px 0', fontSize: 11, flexWrap: 'wrap', marginBottom: 4 }}>
        {!lockCanal && <CtdDropdown label="Canal" value={filterCanal} onChange={setFilterCanal} options={[{ value: 'all', label: 'Todos os canais' }, ...canaisPresent.map(c => ({ value: c, label: c }))]} />}
        {paisesImagem.length > 1 && <CtdDropdown label="Mercado" value={filterCountry} onChange={setFilterCountry} options={[{ value: 'all', label: 'Todos mercados' }, ...paisesImagem.map(c => ({ value: c, label: c }))]} />}
        {(!lockCanal || paisesImagem.length > 1) && <span style={{ color: 'var(--text-dim, #7a8fa8)', fontSize: 11 }}>{filtered.length} de {planRows.length}</span>}
        {!lockCanal && (
        <div style={{ marginLeft: 'auto', display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
          <span style={{ fontSize: 10, fontWeight: 700, padding: '4px 10px', borderRadius: 4, background: '#ECF1FB', color: '#3859D0', fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.04em' }}>NANO BANANA</span>
          <button onClick={runSelectedImages} disabled={generating || selectedIds.size === 0} className="btn btn-sm" style={{ fontSize: 11, opacity: (generating || selectedIds.size === 0) ? 0.5 : 1 }}>
            <Icon name="sparkle" size={11} /> Gerar seleccionados ({selectedIds.size})
          </button>
          <button onClick={runAllPendingImages} disabled={generating || porGerarVista === 0} style={{ background: 'var(--color-primary, #3859D0)', color: '#fff', border: 0, padding: '6px 12px', borderRadius: 'var(--radius-sm, 6px)', fontSize: 11, fontWeight: 700, cursor: generating ? 'not-allowed' : 'pointer', display: 'inline-flex', alignItems: 'center', gap: 5, opacity: (generating || porGerarVista === 0) ? 0.5 : 1 }}>
            <Icon name="sparkle" size={11} /> Gerar todos pendentes ({porGerarVista})
          </button>
        </div>
        )}
      </div>
      )}
      {!lockCanal && (
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12, marginBottom: 20 }}>
        {[
          { key: 'all', label: 'PLANO', value: counters.total, accent: '#3859D0', activeBg: '#EFF3FF' },
          { key: 'pendente', label: 'PENDENTES', value: counters.pendente, accent: '#64748B', activeBg: '#F1F5F9' },
          { key: 'gerado', label: 'GERADAS', value: counters.gerado, accent: '#92400E', activeBg: '#FEF3C7' },
          { key: 'aprovado', label: 'APROVADAS', value: counters.aprovado, accent: '#166534', activeBg: '#DCFCE7' },
        ].map(card => {
          const isActive = filterStatus === card.key;
          return (
            <div key={card.key} onClick={() => setFilterStatus(isActive && card.key !== 'all' ? 'all' : card.key)}
              style={{ background: isActive ? card.activeBg : 'var(--bg-surface, #fff)', border: '1px solid ' + (isActive ? card.accent + '50' : 'var(--border, #E3E6EB)'), borderRadius: 8, padding: '14px 16px', cursor: 'pointer' }}>
              <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: isActive ? card.accent : 'var(--text-muted)', fontFamily: 'var(--font-mono, monospace)', marginBottom: 6 }}>{card.label}</div>
              <div style={{ fontSize: 28, fontWeight: 700, color: isActive ? card.accent : 'var(--text, #112954)', lineHeight: 1 }}>{card.value}</div>
              <div style={{ marginTop: 10, height: 3, background: isActive ? card.accent : 'var(--border, #E3E6EB)', borderRadius: 2, opacity: isActive ? 1 : 0.35 }} />
            </div>
          );
        })}
      </div>
      )}

      {error && <div style={{ padding: '10px 14px', marginBottom: 14, background: '#FEE2E2', color: '#991B1B', borderRadius: 'var(--radius-sm, 6px)', fontSize: 12 }}>{error}</div>}
      {generating && <div style={{ marginBottom: 16 }}><CtdGenProgress step={genStep.step} total={genStep.total} message={genStep.message} /></div>}

      {filtered.length === 0 ? (
        <div style={{ padding: 40, textAlign: 'center', color: 'var(--text-muted)', fontSize: 13 }}>Sem peças neste filtro.</div>
      ) : (
        <div style={{ overflowX: 'auto' }}>
          <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12.5, fontFamily: 'var(--font-sans)' }}>
            <thead>
              <tr>
                <th style={{ ...thSt, width: 36, textAlign: 'center', padding: '4px 4px' }}>
                  <input type="checkbox" checked={allFilteredSel} onChange={toggleAll} disabled={generating} style={{ width: 14, height: 14, cursor: 'pointer', accentColor: 'var(--ai-500, #3859D0)' }} />
                </th>
                <th style={{ ...thSt, width: 40 }}>#</th>
                <th style={thSt}>Peça</th>
                <th style={thSt}>Aplicação</th>
                <th style={thSt}>Canal</th>
                <th style={thSt}>Data</th>
                <th style={thSt}>País</th>
                <th style={thSt}>Autor</th>
                <th style={thSt}>Estado</th>
                <th style={{ ...thSt, textAlign: 'right' }}>Acções</th>
              </tr>
            </thead>
            <tbody>
              {filtered.map((r, rowIndex) => {
                const st = STATUS_STYLE[r.status] || STATUS_STYLE.pendente;
                const prevCnt = rowIndex > 0 ? filtered[rowIndex - 1].country : null;
                const title = r.peca?.titulo || `Peça #${r.library ? r.groupPos : r.idx + 1}`;
                const subtitulo = r.peca?.subtitulo || '';
                const author = r.image?.image_approved_by || r.prompt?.approved_by || r.prompt?.updated_by || null;
                const busy = rowBusy.has(r.key) || generating;
                return (
                  <React.Fragment key={r.key}>
                    {(rowIndex === 0 || prevCnt !== r.country) && (
                      <tr aria-hidden>
                        <td colSpan={10} style={{ padding: rowIndex === 0 ? '4px 12px' : '18px 12px 4px', borderBottom: '2px solid var(--ai-500, #3859D0)', background: 'transparent' }}>
                          <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--ai-500, #3859D0)', fontFamily: 'var(--font-mono, monospace)' }}>Mercado · {r.country}</div>
                        </td>
                      </tr>
                    )}
                    <CtdPlanRow busy={busy} isSelected={selectedIds.has(r.key)} rowIndex={rowIndex}
                      onClick={(e) => { if (busy) return; const tag = e.target.tagName; if (tag === 'INPUT' || tag === 'BUTTON' || e.target.closest('button')) return; setOpenKey(r.key); setPieceVarIdx(-1); }}>
                      <td style={{ ...tdSt, padding: '4px 4px', textAlign: 'center' }} onClick={e => e.stopPropagation()}>
                        <input type="checkbox" checked={selectedIds.has(r.key)} onChange={() => toggleOne(r.key)} disabled={generating} style={{ width: 14, height: 14, cursor: 'pointer', accentColor: 'var(--ai-500, #3859D0)' }} />
                      </td>
                      <td style={{ ...tdSt, fontFamily: 'var(--font-mono)', color: 'var(--text-dim)', fontSize: 11 }}>{r.library ? r.groupPos : r.idx + 1}</td>
                      <td style={{ ...tdSt, overflow: 'hidden' }}>
                        <div style={{ display: 'flex', gap: 10, alignItems: 'center', minWidth: 0 }}>
                          {r.url ? <img src={r.url} alt="" style={{ width: 36, height: 36, objectFit: 'cover', borderRadius: 4, flexShrink: 0 }} /> : <div style={{ width: 36, height: 36, borderRadius: 4, background: '#F1F5F9', flexShrink: 0 }} />}
                          <div style={{ minWidth: 0 }}>
                            <div style={{ fontWeight: 500, color: 'var(--text)', fontSize: 12, fontFamily: 'var(--font-mono)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{title}</div>
                            {subtitulo && <div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 420 }}>{subtitulo}</div>}
                            {r.stale && <div style={{ fontSize: 10, color: '#92400E', fontWeight: 600, marginTop: 2 }}>Copy mudou · gerar prompt e imagem</div>}
                          </div>
                        </div>
                      </td>
                      <td style={tdSt}>
                        {r.app ? (
                          <div>
                            <span style={{ fontSize: 10, fontWeight: 700, padding: '3px 7px', borderRadius: 4, background: '#EFF3FF', color: '#3859D0', fontFamily: 'var(--font-mono, monospace)', whiteSpace: 'nowrap' }}>{ctdAppLabel(r.app)}</span>
                            {r.sceneLabel && <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 3 }}>{r.sceneLabel}</div>}
                          </div>
                        ) : <span style={{ color: 'var(--text-dim)' }}>—</span>}
                      </td>
                      <td style={tdSt}>{canalBadge(r.peca)}</td>
                      <td style={{ ...tdSt, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>{r.peca?.planned_date ? ctdFmtDate(r.peca.planned_date) : '—'}</td>
                      <td style={{ ...tdSt, fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600 }}>{r.country}</td>
                      <td style={tdSt}>{ctdAuthorChip(author)}</td>
                      <td style={tdSt}>
                        <span style={{ fontSize: 11, fontFamily: 'var(--font-mono)', fontWeight: 600, padding: '2px 8px', borderRadius: 4, display: 'inline-block', color: st.fg, background: st.bg }}>{st.label.toUpperCase()}</span>
                      </td>
                      <td style={{ ...tdSt, textAlign: 'right', whiteSpace: 'nowrap' }} onClick={e => e.stopPropagation()}>
                        {(r.status === 'pendente' && r.canGenerate) && (
                          <button onClick={() => runGenerateRow(r)} disabled={busy} style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.04em', padding: '4px 9px', background: 'var(--ai-500, #3859D0)', color: '#fff', border: '1px solid var(--ai-500, #3859D0)', borderRadius: 4, cursor: busy ? 'not-allowed' : 'pointer', fontFamily: 'var(--font-mono)', textTransform: 'uppercase' }}>Gerar</button>
                        )}
                        {(r.status === 'desactualizado' || (r.status === 'pendente' && !r.canGenerate)) && (
                          <button onClick={() => onOpenTab?.({ tab: 'prompts', idx: r.idx, country: r.country, titulo: r.peca?.titulo, contentType: r.peca?.content_type })} style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.04em', padding: '4px 9px', background: 'transparent', color: '#92400E', border: '1px solid #FDE68A', borderRadius: 4, cursor: 'pointer', fontFamily: 'var(--font-mono)', textTransform: 'uppercase' }}>Prompt</button>
                        )}
                        {r.status === 'gerado' && (
                          <button onClick={() => runApproveRow(r)} disabled={busy} style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.04em', padding: '4px 9px', background: '#fff', color: '#283252', border: '1px solid #E3E6EB', borderRadius: 4, cursor: busy ? 'not-allowed' : 'pointer', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', marginRight: 4 }}>Aprovar</button>
                        )}
                        {(r.status === 'gerado' || r.status === 'aprovado') && r.canGenerate && (
                          <button onClick={() => runGenerateRow(r)} disabled={busy} style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.04em', padding: '4px 9px', background: 'transparent', color: 'var(--ai-500, #3859D0)', border: '1px solid var(--ai-500, #3859D0)', borderRadius: 4, cursor: busy ? 'not-allowed' : 'pointer', fontFamily: 'var(--font-mono)', textTransform: 'uppercase' }}>Regerar</button>
                        )}
                      </td>
                    </CtdPlanRow>
                  </React.Fragment>
                );
              })}
            </tbody>
          </table>
        </div>
      )}

      {varModalImg && (
        <CtdVariationsModal
          campanha={campanha}
          image={varModalImg}
          onClose={() => setVarModalImg(null)}
          onDone={() => { setVarModalImg(null); reloadImagens?.(); }}
        />
      )}
    </div>
  );
};
// ─── CtdCopyCard — 1 canal com copy editável ───────────────────────────────────
const CtdCopyCard = ({ canalData, campanha, userEmail, onRefresh }) => {
  const canal = canalData.canal || canalData.canal_pai || canalData.id || 'outro';
  const copyObj = canalData.copy || canalData.copy_data || canalData;
  const status = canalData.status || copyObj.status || (copyObj.headline || copyObj.body ? 'draft' : 'pending');
  const isApproved = ['approved', 'ready', 'ready_review'].includes(status);
  const canalColor = ctdBrandColor(campanha?.brand_slug);

  const [editing, setEditing] = React.useState(false);
  const [generating, setGenerating] = React.useState(false);
  const [saving, setSaving] = React.useState(false);
  const [approving, setApproving] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [fields, setFields] = React.useState({
    headline: copyObj.headline || '',
    body: copyObj.body || '',
    cta: copyObj.cta || '',
    caption: copyObj.caption || copyObj.caption_organica || '',
    hashtags: copyObj.hashtags || '',
    subject: copyObj.subject || '',
  });

  React.useEffect(() => {
    setFields({
      headline: copyObj.headline || '',
      body: copyObj.body || '',
      cta: copyObj.cta || '',
      caption: copyObj.caption || copyObj.caption_organica || '',
      hashtags: copyObj.hashtags || '',
      subject: copyObj.subject || '',
    });
  }, [canalData?.id, copyObj?.headline, copyObj?.body]);

  const hasCopy = fields.headline || fields.body || fields.cta || fields.caption;

  const handleGenerate = async () => {
    setGenerating(true); setError(null);
    try {
      await CtdAPI.generateCanal(campanha.id, canal, { actor_email: userEmail });
      onRefresh?.();
    } catch (e) { setError('Erro a gerar copy: ' + (e.message || 'desconhecido')); }
    setGenerating(false);
  };

  const handleApprove = async () => {
    setApproving(true); setError(null);
    try {
      await CtdAPI.approveCanal(campanha.id, canal, { actor_email: userEmail });
      onRefresh?.();
    } catch (e) { setError('Erro a aprovar: ' + (e.message || 'desconhecido')); }
    setApproving(false);
  };

  const handleFieldChange = (key, value) => setFields(f => ({ ...f, [key]: value }));

  // Fields visíveis por canal
  const isEmail = /email/i.test(canal);
  const isSocial = /instagram|facebook|linkedin|social|tiktok/i.test(canal);
  const isWA = /whatsapp|wa/i.test(canal);
  const isBlog = /blog/i.test(canal);
  const isAd = /ads|meta_ads|google/i.test(canal);

  const showSubject = isEmail;
  const showHeadline = true;
  const showBody = true;
  const showCTA = !isSocial;
  const showCaption = isSocial;
  const showHashtags = isSocial;

  const Field = ({ label, value, onChange, multiline, maxChars }) => {
    const chars = value?.length || 0;
    return (
      <div style={{ marginBottom: 10 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
          <span style={{ fontSize: 10, fontWeight: 600, letterSpacing: '0.08em', color: '#64748b', textTransform: 'uppercase', fontFamily: 'var(--font-mono, monospace)' }}>{label}</span>
          {maxChars && (
            <span style={{ fontSize: 9, color: chars > maxChars ? '#dc2626' : '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>
              {chars}/{maxChars}
            </span>
          )}
        </div>
        {editing ? (
          multiline ? (
            <textarea value={value} onChange={e => onChange(e.target.value)} rows={4}
              style={{ width: '100%', boxSizing: 'border-box', padding: 8, border: '1px solid #cbd5e1', borderRadius: 4, fontSize: 12, fontFamily: 'var(--font-body, Inter, sans-serif)', resize: 'vertical', outline: 'none', background: '#fafbfc' }} />
          ) : (
            <input value={value} onChange={e => onChange(e.target.value)}
              style={{ width: '100%', boxSizing: 'border-box', padding: 8, border: '1px solid #cbd5e1', borderRadius: 4, fontSize: 12, fontFamily: 'var(--font-body, Inter, sans-serif)', outline: 'none', background: '#fafbfc' }} />
          )
        ) : (
          <div style={{ padding: 8, background: '#f8fafc', borderRadius: 4, fontSize: 12, color: value ? '#334155' : '#94a3b8', minHeight: 20, whiteSpace: 'pre-wrap', lineHeight: 1.5 }}>
            {value || '—'}
          </div>
        )}
      </div>
    );
  };

  return (
    <div style={{ background: '#ffffff', border: '1px solid #e2e8f0', borderLeft: `3px solid ${canalColor}`, borderRadius: 8, padding: 14, marginBottom: 10 }}>
      {/* Header */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12, gap: 8 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
          <span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.08em', color: canalColor, textTransform: 'uppercase', fontFamily: 'var(--font-mono, monospace)' }}>{canal}</span>
          {canalData.content_type && <span style={{ fontSize: 10, color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>· {canalData.content_type}</span>}
        </div>
        <span style={{
          fontSize: 9, fontWeight: 600, letterSpacing: '0.06em', textTransform: 'uppercase', padding: '2px 8px', borderRadius: 4,
          background: isApproved ? '#dcfce7' : (hasCopy ? '#fef3c7' : '#f1f5f9'),
          color: isApproved ? '#166534' : (hasCopy ? '#92400e' : '#64748b'),
          fontFamily: 'var(--font-mono, monospace)',
        }}>
          {isApproved ? 'APROVADO' : (hasCopy ? 'DRAFT' : 'SEM COPY')}
        </span>
      </div>

      {/* Fields */}
      {hasCopy ? (
        <>
          {showSubject && <Field label="Subject" value={fields.subject} onChange={v => handleFieldChange('subject', v)} maxChars={80} />}
          {showHeadline && <Field label="Headline" value={fields.headline} onChange={v => handleFieldChange('headline', v)} maxChars={isAd ? 40 : (isBlog ? 65 : 100)} />}
          {showBody && <Field label={isBlog ? 'Body (long)' : 'Body'} value={fields.body} onChange={v => handleFieldChange('body', v)} multiline />}
          {showCTA && <Field label="CTA" value={fields.cta} onChange={v => handleFieldChange('cta', v)} maxChars={30} />}
          {showCaption && <Field label="Caption" value={fields.caption} onChange={v => handleFieldChange('caption', v)} multiline />}
          {showHashtags && <Field label="Hashtags" value={fields.hashtags} onChange={v => handleFieldChange('hashtags', v)} />}
        </>
      ) : (
        <div style={{ padding: 20, textAlign: 'center', background: '#f8fafc', borderRadius: 6, color: '#64748b', fontSize: 12, marginBottom: 12 }}>
          Ainda sem copy. Clica "✨ Gerar Copy" para criar automaticamente para este canal.
        </div>
      )}

      {error && <div style={{ marginBottom: 8, padding: 8, background: '#fef2f2', borderRadius: 4, color: '#dc2626', fontSize: 11 }}>{error}</div>}

      {/* Actions */}
      <div style={{ display: 'flex', gap: 6, justifyContent: 'flex-end', flexWrap: 'wrap' }}>
        {editing ? (
          <>
            <button onClick={() => setEditing(false)} className="btn btn-sm">Cancelar</button>
            <button onClick={() => { setEditing(false); /* TODO: PATCH save copy fields */ }} disabled={saving} className="btn btn-sm btn-primary">
              {saving ? 'A guardar...' : 'Guardar'}
            </button>
          </>
        ) : (
          <>
            {!hasCopy && (
              <button onClick={handleGenerate} disabled={generating} className="btn btn-sm btn-ai">
                {generating ? 'A gerar...' : '✨ Gerar Copy'}
              </button>
            )}
            {hasCopy && !isApproved && (
              <>
                <button onClick={() => setEditing(true)} className="btn btn-sm">Editar</button>
                <button onClick={handleGenerate} disabled={generating} className="btn btn-sm">
                  {generating ? 'A regenerar...' : '↻ Regenerar'}
                </button>
                <button onClick={handleApprove} disabled={approving} className="btn btn-sm btn-ai">
                  {approving ? 'A aprovar...' : '✓ Aprovar'}
                </button>
              </>
            )}
            {isApproved && <span style={{ fontSize: 11, color: '#166534', padding: '4px 8px' }}>✓ Copy aprovado</span>}
          </>
        )}
      </div>
    </div>
  );
};

// ─── CtdCopiesApprovalStrip — status bar copies (mirror do prompts/imagens) ───
const CtdCopiesApprovalStrip = ({ campanhaId, copies, userEmail, reloadCopies, onGenerate, onGenerateIdiomas, generating, generatingIdiomas, genStep, currentLingua }) => {
  const Icon = window.Icon;
  const [busy, setBusy] = React.useState(null);

  // Filtra por lingua actual se definida (senão conta todas)
  const scoped     = currentLingua ? (copies || []).filter(c => (c.lingua || 'pt') === currentLingua) : (copies || []);
  const total      = scoped.length;
  const approved   = scoped.filter(c => c.status === 'aprovado').length;
  const pending    = total - approved;
  const lastAt     = null;   // conteudo_copy não tem approved_at column; poderia usar updated_at
  const lastBy     = null;

  const allApproved   = total > 0 && approved === total;
  const canApproveAll = total > 0 && approved < total;

  const label = allApproved ? 'COPIES APROVADAS' : approved > 0 ? 'COPIES EM APROVAÇÃO' : total > 0 ? 'COPIES GERADAS' : 'COPIES';
  const bg    = allApproved ? '#E1F7E6' : approved > 0 ? '#FEF3C7' : total > 0 ? '#ECF1FB' : '#F1F5F9';
  const fg    = allApproved ? '#1F8A52' : approved > 0 ? '#92400E' : total > 0 ? '#3859D0' : '#475569';

  const handleApproveAll = async () => {
    setBusy('approve');
    try { await CtdAPI.approveAllCopies(campanhaId, { approver_email: userEmail }); reloadCopies?.(); }
    catch (e) { alert(e.message || 'Erro ao aprovar'); }
    setBusy(null);
  };

  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',
      background: 'var(--bg-surface, #fff)', borderRadius: 'var(--radius-md, 8px)',
      boxShadow: 'var(--shadow-card, 0 1px 4px rgba(40,41,61,0.04), 0 4px 16px rgba(96,97,112,0.08))',
      border: '1px solid var(--border-1, #ECEFF5)', marginBottom: 14, flexWrap: 'wrap',
    }}>
      <span style={{
        fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.1em',
        padding: '3px 8px', borderRadius: 'var(--radius-xs, 4px)', background: bg, color: fg, fontFamily: 'var(--font-mono, monospace)',
      }}>{label}</span>

      <span style={{ fontSize: 11, color: 'var(--text-muted, #4a5e7a)' }}>
        {total > 0 ? `${approved}/${total} aprovadas` : 'Sem copies geradas ainda'}
        {pending > 0 && ` · ${pending} pendentes`}
        {lastAt && ` · última aprovação ${new Date(lastAt).toLocaleString('pt-PT')}${lastBy ? ` por ${String(lastBy).split('@')[0]}` : ''}`}
      </span>

      <div style={{ marginLeft: 'auto', display: 'flex', gap: 6, alignItems: 'center' }}>
        {allApproved && (
          <span style={{
            fontSize: 10, fontWeight: 700, padding: '4px 10px', borderRadius: 4,
            background: '#DCFCE7', color: '#166534', fontFamily: 'var(--font-mono, monospace)',
            letterSpacing: '0.06em', display: 'inline-flex', alignItems: 'center', gap: 4,
          }}>
            <Icon name="check" size={11} /> TODAS APROVADAS
          </span>
        )}
        {canApproveAll && (
          <button
            onClick={handleApproveAll}
            disabled={busy === 'approve'}
            style={{
              background: '#1F8A52', color: '#fff', border: 0,
              padding: '6px 12px', borderRadius: 'var(--radius-sm, 6px)',
              fontSize: 11, fontWeight: 700, cursor: 'pointer',
              display: 'inline-flex', alignItems: 'center', gap: 5,
              opacity: busy === 'approve' ? 0.5 : 1,
              boxShadow: '0 1px 2px rgba(31,138,82,0.24)',
            }}
            title={`Aprovar ${pending} copies em draft`}
          >
            <Icon name="check" size={12} />
            APROVAR TODAS ({pending})
          </button>
        )}
      </div>
    </div>
  );
};

// ─── WhatsApp template — o mesmo esqueleto de Meta Ads, sem imagem ───────────
function ctdIsWaPiece(peca, copy) {
  const ct = peca?.content_type || copy?.content_type || '';
  const canal = peca?.canal || copy?.canal || '';
  return ct === 'wa_template' || ct === 'wa_message' || canal === 'whatsapp';
}
function ctdHideWaFromPlan(peca) {
  return peca?.item_type !== 'one_shot' && ctdIsWaPiece(peca, null);
}
function ctdPecaArchived(peca) {
  return peca?.archived === true || String(peca?.archived) === 'true';
}
function ctdIsOneShotEmail(peca) {
  if (peca?.item_type !== 'one_shot') return false;
  const canal = String(peca.canal || '');
  if (canal.includes('interno') || canal.includes('portal_notificacao')) return false;
  const ct = peca.content_type || '';
  return ct === 'email' || ct === 'email_html' || canal === 'email';
}
function ctdEmailBrevo(peca) {
  if (!peca || peca.email_modo === 'criar') return false;
  if (peca.email_modo === 'brevo') return true;
  return ctdIsOneShotEmail(peca);
}
function ctdFillBrevoHtml(html, params) {
  return String(html || '').replace(/\{\{\s*params\.([A-Za-z0-9_]+)\s*\}\}/g, (_, key) => {
    const value = params?.[key];
    if (value == null || value === '') return '';
    return String(value).replace(/[&<>"]/g, ch => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[ch]));
  });
}
function ctdParseAdSpecs(copy) {
  const raw = copy?.ad_specs;
  if (raw && typeof raw === 'object') return raw;
  try { return JSON.parse(raw || '{}'); } catch { return {}; }
}
function ctdWaPackFromCopy(copy) {
  const specs = ctdParseAdSpecs(copy);
  const exemplos = {};
  const src = specs.wa_exemplo_vars && typeof specs.wa_exemplo_vars === 'object' ? specs.wa_exemplo_vars : {};
  Object.keys(src).sort().forEach(k => { exemplos[k] = String(src[k] || ''); });
  return {
    name: String(specs.wa_template_name || copy?.headline || ''),
    categoria: String(specs.wa_categoria || 'Marketing'),
    headerType: String(specs.wa_header_type || 'none'),
    header: String(specs.wa_header_content || ''),
    body: String(specs.wa_body || copy?.body || '').trim(),
    footer: String(specs.wa_footer || ''),
    buttons: [],
    exemplos,
    headerImage: String(specs.wa_header_image_url || ''),
    headerClaim: String(specs.wa_header_claim || ''),
    ctaOferta: String(specs.wa_cta_oferta || ''),
    nota: String(specs.nota_crm_texto || ''),
    proxima: specs.proxima_accao || null,
    rejectedReason: String(specs.wa_rejected_reason || ''),
    approvedAt: specs.wa_template_approved_at || null,
    fp: specs.wa_template_fp || '',
    metaStatus: specs.wa_meta_status || '',
    requiresImage: ctdWaCopyRequiresImage(copy),
  };
}
function ctdWaCopyRequiresImage(copy) {
  const payload = ctdCopyPayload(copy);
  const key = String(payload.piece_key || '');
  const papel = String(payload.papel || '');
  if (key.startsWith('wa_qual_') || papel === 'qualificacao') return false;
  return true;
}
function ctdWaFormFields(copy) {
  const p = ctdWaPackFromCopy(copy);
  return {
    wa_name: p.name, wa_categoria: p.categoria, wa_header_type: p.headerType, wa_header: p.header,
    wa_body: p.body, wa_footer: p.footer, wa_buttons: [], wa_exemplos: p.exemplos, wa_header_image: p.headerImage,
    wa_cta_oferta: p.ctaOferta, wa_nota: p.nota,
    wa_approved_at: p.approvedAt, wa_fp: p.fp, wa_meta_status: p.metaStatus,
    wa_requires_image: p.requiresImage !== false,
  };
}
function ctdWaPackFromForm(form) {
  if (!form || form.wa_name == null) return ctdWaPackFromCopy(form);
  const exemplos = {};
  Object.keys(form.wa_exemplos || {}).sort().forEach(k => { exemplos[k] = String(form.wa_exemplos[k] || ''); });
  return {
    name: String(form.wa_name || ''),
    categoria: String(form.wa_categoria || 'Marketing'),
    headerType: String(form.wa_header_type || 'none'),
    header: String(form.wa_header || ''),
    body: String(form.wa_body || '').trim(),
    footer: String(form.wa_footer || ''),
    buttons: [],
    exemplos,
    headerImage: String(form.wa_header_image || ''),
    ctaOferta: String(form.wa_cta_oferta || ''),
    nota: String(form.wa_nota || ''),
    approvedAt: form.wa_approved_at || null,
    fp: form.wa_fp || '',
    metaStatus: form.wa_meta_status || '',
    requiresImage: form.wa_requires_image !== false,
  };
}
function ctdWaFingerprint(pack) {
  const exemplos = {};
  Object.keys(pack.exemplos || pack.exemplo_vars || {}).sort().forEach(k => {
    exemplos[k] = String((pack.exemplos || pack.exemplo_vars)[k] || '');
  });
  const base = {
    name: String(pack.name || ''),
    categoria: String(pack.categoria || 'Marketing'),
    header_type: String(pack.headerType || pack.header_type || 'none'),
    header_content: String(pack.header || pack.header_content || ''),
    body: String(pack.body || '').trim(),
    footer: String(pack.footer || ''),
    buttons: (pack.buttons || []).map(b => ({
      type: b.type || 'QUICK_REPLY', text: String(b.text || ''), url: b.url || '', phone_number: b.phone_number || '',
    })),
    exemplo_vars: exemplos,
  };
  const img = String(pack.headerImage || pack.header_image_url || '');
  if (img) base.header_image_url = img;
  return JSON.stringify(base);
}
function ctdWaTemplateStale(pack) {
  if (!pack?.approvedAt) return false;
  return pack.fp !== ctdWaFingerprint(pack);
}
function ctdWaVars(body) {
  return [...new Set([...String(body || '').matchAll(/\{\{\s*([a-z0-9_]+)\s*\}\}/gi)].map(m => m[1]))];
}
function ctdWaFill(body, exemplos) {
  return String(body || '').replace(/\{\{\s*([a-z0-9_]+)\s*\}\}/gi, (all, name) => {
    const v = exemplos?.[name];
    return v ? v : all;
  });
}
function ctdWaChecklist(pack) {
  const items = [];
  const body = String(pack?.body || '').trim();
  const len = body.length;
  if (!body) items.push({ ok: false, label: 'O corpo está vazio' });
  else if (len > 400) items.push({ ok: false, label: `Corpo ${len} — acima de 400 a Meta rejeita` });
  else if (len > 280) items.push({ ok: true, warn: true, label: `Corpo ${len} — até 280 é o ideal` });
  else items.push({ ok: true, label: `Corpo ${len} caracteres` });
  const startVar = /^\s*\{\{/.test(body);
  items.push({ ok: !startVar, label: startVar ? 'O corpo começa com uma variável' : 'Começa com texto fixo' });
  const endVar = /\{\{[^}]+\}\}[\s.!?…,;:]*$/.test(body);
  items.push({ ok: !endVar, label: endVar ? 'O corpo acaba com uma variável' : 'Não acaba com variável' });
  const buttons = (pack?.buttons || []).filter(b => String(b.text || '').trim());
  const endsQ = buttons.length > 0 && /\?\s*$/.test(body);
  items.push({ ok: !endsQ, label: endsQ ? 'Com botões, o corpo não pode acabar em pergunta' : 'O corpo não duplica o botão' });
  const urgency = ['última oportunidade', 'só esta semana', 'esta semana ainda', 'não percas', 'tempo limitado', 'agora ou nunca', 'último lugar', 'oferta expira'];
  const hit = urgency.find(p => body.toLowerCase().includes(p));
  items.push({ ok: !hit, label: hit ? `Tem linguagem de urgência: ${hit}` : 'Sem urgência' });
  const price = /desconto|entrada reduzida|preço especial|\d+\s*%|€|\bbarato\b/i.test(body);
  items.push({ ok: !price, label: price ? 'O corpo tem preço, percentagem ou «barato»' : 'Sem preço' });
  const blocked = /PIEP|TÜV\s*Rheinland|87\s*%/i.test(body);
  items.push({ ok: !blocked, label: blocked ? 'O corpo tem um termo bloqueado da marca' : 'Sem termos bloqueados' });
  const siglas = /\b[A-ZÁÉÍÓÚ]{2,4}(?:\s+[A-ZÁÉÍÓÚ]{2,4}){2,}/.test(body);
  items.push({ ok: !siglas, label: siglas ? 'O corpo tem três ou mais siglas seguidas' : 'Sem siglas a mais' });
  const vars = ctdWaVars(body);
  const estranhas = vars.filter(v => v.toLowerCase() !== 'nome' && v.toLowerCase() !== 'empresa');
  items.push({ ok: estranhas.length === 0, label: estranhas.length ? `Variável não permitida: ${estranhas.join(', ')}` : 'Só nome e empresa' });
  const nameOk = /^[a-z0-9_]{1,512}$/.test(String(pack?.name || ''));
  items.push({ ok: nameOk, label: nameOk ? 'Nome em minúsculas e underscores' : 'O nome tem de ser minúsculas e underscores' });
  if (pack?.headerType === 'text' || pack?.header_type === 'text') {
    const h = String(pack.header || pack.header_content || '');
    const hOk = h.length > 0 && h.length <= 60 && !/\{\{/.test(h);
    items.push({ ok: hOk, label: hOk ? 'Cabeçalho até 60' : 'O cabeçalho é texto até 60 caracteres, sem variável' });
  }
  const footer = String(pack?.footer || '');
  const fOk = footer.length <= 60 && !/\{\{/.test(footer);
  items.push({ ok: fOk, label: fOk ? 'Rodapé até 60' : 'O rodapé passa de 60 caracteres ou tem variável' });
  if (buttons.length) items.push({ ok: false, label: 'Os templates de WhatsApp não têm botões' });
  if (pack?.requiresImage !== false || (pack?.headerType || pack?.header_type) === 'image') {
    const imgOk = !!(String(pack.headerImage || pack.header_image_url || '').trim());
    items.push({ ok: imgOk, label: imgOk ? 'Cabeçalho com imagem' : 'Falta a imagem do cabeçalho' });
  }
  const missing = ctdWaVars(body).filter(v => !String(pack?.exemplos?.[v] || pack?.exemplo_vars?.[v] || '').trim());
  items.push({ ok: missing.length === 0, label: missing.length ? `Falta exemplo de ${missing.join(', ')}` : 'Cada variável tem exemplo' });
  return items;
}
function ctdWaBlockers(pack) {
  return ctdWaChecklist(pack).filter(i => !i.ok).map(i => i.label);
}
function ctdWaAdSpecs(form) {
  const p = ctdWaPackFromForm(form);
  return {
    wa_template_name: p.name,
    wa_categoria: p.categoria,
    wa_header_type: p.headerImage ? 'image' : p.headerType,
    wa_header_content: p.header,
    wa_body: p.body,
    wa_footer: p.footer,
    wa_buttons: [],
    wa_exemplo_vars: p.exemplos,
    wa_parameter_format: ctdWaVars(p.body).some(v => !/^[0-9]+$/.test(v)) ? 'NAMED' : 'POSITIONAL',
    wa_header_image_url: p.headerImage || '',
    wa_cta_oferta: p.ctaOferta || '',
    nota_crm_texto: p.nota || '',
  };
}

function CtdWaChecklistList({ pack }) {
  const items = ctdWaChecklist(pack).filter(i => !i.ok || i.warn);
  if (!items.length) return null;
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
      {items.map(i => (
        <div key={i.label} style={{ fontSize: 11, lineHeight: 1.35, color: i.ok ? '#92400E' : '#991B1B' }}>{i.label}</div>
      ))}
    </div>
  );
}

function CtdWaPhone({ pack, brandName, generating, generatingMessage }) {
  const body = ctdWaFill(pack?.body, pack?.exemplos);
  const header = pack?.headerType === 'text' ? pack.header : '';
  const showImage = pack?.headerType === 'image';
  const name = brandName || 'Digidelta';
  const stroke = '#C5CDD8';
  const ink = '#283252';
  const mute = '#94A4C4';
  return (
    <div style={{ background: '#F4F6F9', borderRadius: 8, padding: '16px 12px 18px', display: 'flex', justifyContent: 'center' }}>
      <div style={{ width: '100%', maxWidth: 260, height: 520, boxSizing: 'border-box', background: '#fff', border: '1.5px solid #D5DCE6', borderRadius: 32, padding: '10px 10px 8px', boxShadow: '0 10px 28px rgba(17,41,84,0.08)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 8px 8px', flexShrink: 0 }}>
          <span style={{ fontSize: 10, fontWeight: 600, color: ink, fontFamily: 'var(--font-mono, monospace)' }}>9:41</span>
          <div style={{ width: 54, height: 8, borderRadius: 8, border: `1px solid ${stroke}` }} />
          <span style={{ display: 'inline-flex', gap: 3, alignItems: 'center' }}>
            <span style={{ width: 11, height: 8, border: `1.5px solid ${stroke}`, borderRadius: 2 }} />
            <span style={{ width: 16, height: 8, border: `1.5px solid ${stroke}`, borderRadius: 2, position: 'relative' }}>
              <span style={{ position: 'absolute', right: -3, top: 2, width: 2, height: 3, borderRadius: 1, background: stroke }} />
            </span>
          </span>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '4px 4px 8px', borderBottom: `1px solid ${stroke}`, flexShrink: 0 }}>
          <span style={{ width: 7, height: 7, borderLeft: `1.5px solid ${ink}`, borderBottom: `1.5px solid ${ink}`, transform: 'rotate(45deg)', marginLeft: 4, flexShrink: 0 }} />
          <span style={{ width: 22, height: 22, borderRadius: '50%', border: `1.5px solid ${stroke}`, flexShrink: 0 }} />
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 11, fontWeight: 700, color: ink, lineHeight: 1.2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{name}</div>
            <div style={{ fontSize: 9, color: mute, lineHeight: 1.2 }}>online</div>
          </div>
          <span style={{ width: 12, height: 12, borderRadius: '50%', border: `1.5px solid ${stroke}`, flexShrink: 0 }} />
          <span style={{ width: 13, height: 10, border: `1.5px solid ${stroke}`, borderRadius: 2, flexShrink: 0 }} />
        </div>
        <div className="scrollbar" style={{ background: '#F7F8FA', flex: 1, minHeight: 0, overflowY: 'auto', padding: '12px 8px' }}>
          <div style={{ background: '#fff', border: `1px solid ${stroke}`, borderRadius: '4px 12px 12px 12px', padding: '6px 8px', maxWidth: '94%' }}>
            {(generating || (showImage && pack.headerImage)) && generating && (
              <div style={{ aspectRatio: '1.91 / 1', borderRadius: 4, marginBottom: 6, background: 'linear-gradient(135deg, #eef2ff 0%, #f5f3ff 100%)', border: '1px solid #c7d2fe', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
                <div style={{ width: 18, height: 18, border: '2px solid #c7d2fe', borderTopColor: '#6366f1', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
                <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.04em', color: '#3730a3', fontFamily: 'var(--font-mono, monospace)', textAlign: 'center', padding: '0 8px' }}>{generatingMessage || 'A gerar…'}</span>
                <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
              </div>
            )}
            {!generating && showImage && pack.headerImage && (
              <img src={pack.headerImage} alt="" style={{ width: '100%', aspectRatio: '1.91 / 1', objectFit: 'contain', background: '#fff', borderRadius: 4, marginBottom: 6, display: 'block' }} />
            )}
            {!generating && showImage && !pack.headerImage && (
              <div style={{ aspectRatio: '1.91 / 1', border: `1px dashed ${stroke}`, borderRadius: 4, marginBottom: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 9, fontWeight: 700, letterSpacing: '0.06em', color: mute, fontFamily: 'var(--font-mono, monospace)' }}>1125×600</div>
            )}
            {header && <div style={{ fontSize: 12, fontWeight: 700, color: ink, marginBottom: 4 }}>{header}</div>}
            <div style={{ fontSize: 12, lineHeight: 1.45, color: ink, whiteSpace: 'pre-wrap' }}>{body || 'Corpo da mensagem'}</div>
            {pack?.footer && <div style={{ fontSize: 10, color: mute, marginTop: 6 }}>{pack.footer}</div>}
            <div style={{ fontSize: 9, color: mute, textAlign: 'right', marginTop: 4 }}>12:34</div>
          </div>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 4px 2px', flexShrink: 0 }}>
          <span style={{ width: 18, height: 18, borderRadius: '50%', border: `1.5px solid ${stroke}`, flexShrink: 0 }} />
          <div style={{ flex: 1, height: 22, borderRadius: 12, border: `1px solid ${stroke}` }} />
          <span style={{ width: 18, height: 18, borderRadius: '50%', border: `1.5px solid ${stroke}`, flexShrink: 0 }} />
        </div>
        <div style={{ display: 'flex', justifyContent: 'center', paddingTop: 8, flexShrink: 0 }}>
          <div style={{ width: 72, height: 4, borderRadius: 4, background: '#D5DCE6' }} />
        </div>
      </div>
    </div>
  );
}

const ctdWaInput = {
  width: '100%', border: '1px solid #E5E7EB', borderRadius: 4, background: '#fff',
  fontSize: 12, color: '#111827', outline: 'none', padding: '8px 10px', boxSizing: 'border-box', fontFamily: 'Inter,sans-serif',
};
function CtdWaFields({ form, editing, onChange }) {
  const pack = ctdWaPackFromForm(form);
  const len = (form.wa_body || '').length;
  const lenColor = len > 400 ? '#EF4444' : len > 280 ? '#B45309' : '#9CA3AF';
  const vars = ctdWaVars(form.wa_body);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      <div>
        <div style={{ fontSize: 10, fontWeight: 600, color: '#374151', marginBottom: 4 }}>Nome do template</div>
        <input disabled={!editing} value={form.wa_name || ''} onChange={e => onChange('wa_name', e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, '_'))} placeholder="marca_produto_angulo_mes26" style={ctdWaInput} />
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
        <div>
          <div style={{ fontSize: 10, fontWeight: 600, color: '#374151', marginBottom: 4 }}>Categoria</div>
          <select disabled={!editing} value={form.wa_categoria || 'Marketing'} onChange={e => onChange('wa_categoria', e.target.value)} style={ctdWaInput}>
            <option value="Marketing">Marketing</option>
            <option value="Serviços">Serviços</option>
          </select>
        </div>
        <div>
          <div style={{ fontSize: 10, fontWeight: 600, color: '#374151', marginBottom: 4 }}>Cabeçalho</div>
          {form.wa_header_image ? (
            <div style={{ ...ctdWaInput, background: '#F9FAFB', color: '#374151' }}>Imagem · 1125×600</div>
          ) : (
            <select disabled={!editing} value={form.wa_header_type || 'none'} onChange={e => onChange('wa_header_type', e.target.value)} style={ctdWaInput}>
              <option value="none">Nenhum</option>
              <option value="text">Texto</option>
            </select>
          )}
        </div>
      </div>
      {form.wa_header_type === 'text' && (
        <div>
          <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
            <div style={{ fontSize: 10, fontWeight: 600, color: '#374151' }}>Texto do cabeçalho</div>
            <span style={{ fontSize: 9, color: (form.wa_header || '').length > 60 ? '#EF4444' : '#9CA3AF', fontFamily: 'monospace' }}>{(form.wa_header || '').length} / 60</span>
          </div>
          <input disabled={!editing} value={form.wa_header || ''} maxLength={60} onChange={e => onChange('wa_header', e.target.value)} style={ctdWaInput} />
        </div>
      )}
      <div>
        <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
          <div style={{ fontSize: 10, fontWeight: 600, color: '#374151' }}>Corpo</div>
          <span style={{ fontSize: 9, color: lenColor, fontFamily: 'monospace' }}>{len} / 280</span>
        </div>
        <textarea disabled={!editing} value={form.wa_body || ''} rows={6} onChange={e => onChange('wa_body', e.target.value)}
          style={{ ...ctdWaInput, resize: 'vertical', lineHeight: 1.5, background: editing ? '#fff' : '#F9FAFB' }} />
      </div>
      <div>
        <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
          <div style={{ fontSize: 10, fontWeight: 600, color: '#374151' }}>Rodapé</div>
          <span style={{ fontSize: 9, color: (form.wa_footer || '').length > 60 ? '#EF4444' : '#9CA3AF', fontFamily: 'monospace' }}>{(form.wa_footer || '').length} / 60</span>
        </div>
        <input disabled={!editing} value={form.wa_footer || ''} maxLength={60} onChange={e => onChange('wa_footer', e.target.value)} placeholder="Digidelta · Responde PARAR para cancelar" style={ctdWaInput} />
      </div>
      {vars.length > 0 && (
        <div>
          <div style={{ fontSize: 10, fontWeight: 600, color: '#374151', marginBottom: 6 }}>Exemplos</div>
          {vars.map(name => (
            <div key={name} style={{ display: 'grid', gridTemplateColumns: '120px minmax(0,1fr)', gap: 8, marginBottom: 6, alignItems: 'center' }}>
              <div style={{ fontSize: 11, fontFamily: 'monospace', color: '#128C7E' }}>{`{{${name}}}`}</div>
              <input disabled={!editing} value={(form.wa_exemplos || {})[name] || ''} onChange={e => onChange('wa_exemplos', { ...(form.wa_exemplos || {}), [name]: e.target.value })} placeholder="Exemplo" style={ctdWaInput} />
            </div>
          ))}
        </div>
      )}
      <CtdWaChecklistList pack={pack} />
    </div>
  );
}

function CtdWaTemplateScreen({ campanhaId, row, userEmail, onNavigate, reloadCopies, brandName, place }) {
  const copy = row?.copy;
  const [form, setForm] = React.useState(() => ({ ...ctdWaFormFields(copy), ...(copy ? {} : {}) }));
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);
  React.useEffect(() => { setForm(ctdWaFormFields(copy)); setError(null); }, [copy?.id, copy?.ad_specs, copy?.body]);
  const pack = ctdWaPackFromCopy(copy);
  const copyOk = copy?.status === 'aprovado';
  const blockers = ctdWaBlockers(ctdWaPackFromForm(form));
  const onChange = (field, value) => setForm(prev => ({ ...prev, [field]: value }));
  const metaStatus = pack.metaStatus || '';
  const metaSent = metaStatus === 'submitted' || metaStatus === 'pending';
  const metaOk = metaStatus === 'approved';
  const metaRejected = metaStatus === 'rejected';
  const samePiece = (tab) => onNavigate?.({ tab, idx: row.idx, country: row.country, titulo: row.peca?.titulo, contentType: row.peca?.content_type, waGroup: row.peca?.waGroup || null });
  const metaDetail = metaOk ? 'Aprovado na Meta' : metaSent ? 'Em revisão na Meta' : metaRejected ? 'Rejeitado na Meta' : 'Por enviar';
  const qual = row?.peca?.waGroup === 'qualificacao';
  const metaPill = ctdWaMetaPill(metaStatus, pack.rejectedReason);
  const digiLine = qual
    ? (metaOk ? 'Aprovado na Meta. Disponível para a Digi arrancar a qualificação.'
      : metaRejected ? 'Rejeitado na Meta. A Digi não arranca com este template.'
      : metaSent ? 'Em revisão na Meta. Quando aprovar, a Digi pode arrancar a qualificação.'
      : 'Ainda por enviar. A Digi só arranca depois da aprovação da Meta.')
    : metaDetail;
  const [metaChecking, setMetaChecking] = React.useState(false);
  React.useEffect(() => {
    if (!copy?.id || !metaSent) return undefined;
    let stop = false;
    const tick = async () => {
      setMetaChecking(true);
      try {
        const out = await CtdAPI.refreshWaMeta(copy.id);
        if (!stop && out?.status && out.status !== metaStatus) reloadCopies?.();
      } catch { /* a próxima leitura tenta outra vez */ }
      if (!stop) setMetaChecking(false);
    };
    tick();
    const timer = setInterval(tick, 15000);
    return () => { stop = true; clearInterval(timer); };
  }, [copy?.id, metaSent]);
  const enviar = async () => {
    if (!copy?.id) return;
    if (blockers.length) { setError(blockers[0]); return; }
    setBusy(true); setError(null);
    try {
      const specs = ctdWaAdSpecs(form);
      await CtdAPI.editCopy(campanhaId, copy.id, {
        ad_specs: specs, body: form.wa_body, headline: form.wa_name, status: 'aprovado', actor_email: userEmail,
      });
      await CtdAPI.approveWaTemplate(copy.id, { actor_email: userEmail });
      await CtdAPI.submitWaTemplate(campanhaId, copy.id, { actor_email: userEmail });
      reloadCopies?.();
    } catch (e) { setError(e.message || 'Erro ao enviar para a Meta'); }
    setBusy(false);
  };
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'minmax(292px, 320px) minmax(0, 1fr) 300px', gap: 20, alignItems: 'start' }}>
      <div style={{ background: '#fff', border: '1px solid #BBF7D0', borderRadius: 8, overflow: 'hidden' }}>
        <div style={{ padding: '10px 14px', background: '#F0FDF4', borderBottom: '1px solid #DCFCE7' }}>
          <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#166534', fontFamily: 'var(--font-mono, monospace)' }}>APROVAÇÃO META</span>
        </div>
        <div style={{ padding: '12px 14px', borderBottom: '1px solid #DCFCE7', display: 'flex', flexDirection: 'column', gap: 8 }}>
          <span title={metaPill.title} style={{ alignSelf: 'flex-start', fontSize: 11, fontFamily: 'var(--font-mono)', fontWeight: 700, padding: '3px 8px', borderRadius: 4, color: metaPill.fg, background: metaPill.bg }}>{metaPill.label}</span>
          <div style={{ fontSize: 12, color: '#112954', lineHeight: 1.45 }}>{digiLine}</div>
          {pack.rejectedReason ? <div style={{ fontSize: 12, color: '#991B1B' }}>{pack.rejectedReason}</div> : null}
          {metaChecking && metaSent ? <div style={{ fontSize: 11, color: '#64748B' }}>A verificar na Meta…</div> : null}
        </div>
        <div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 10 }}>
          <CtdPipelineStep n="1" label="Copy" detail={copyOk ? 'Aprovada nesta peça' : 'Segue neste envio'} state="done" onClick={() => samePiece('copies')} />
          <CtdPipelineStep n="2" label="Meta" detail={metaDetail} state={metaOk ? 'done' : 'current'} />
          <CtdPipelineStep n="3" label="Peça final" detail={metaSent || metaOk ? 'Disponível' : 'Depois do envio'} state={metaSent || metaOk ? 'current' : 'wait'} onClick={metaSent || metaOk ? () => samePiece('peca') : null} />
          {!metaSent && !metaOk && <CtdWaChecklistList pack={ctdWaPackFromForm(form)} />}
          {!metaSent && !metaOk && (
            <button type="button" onClick={enviar} disabled={busy || blockers.length > 0 || !copy?.id} style={{ background: '#3859D0', color: '#fff', border: 0, width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: busy || blockers.length || !copy?.id ? 'not-allowed' : 'pointer', opacity: busy || blockers.length || !copy?.id ? 0.55 : 1 }}>
              {busy ? 'A enviar…' : !copy?.id ? 'Esta peça ainda não tem copy' : blockers.length ? blockers[0] : metaRejected ? 'Enviar de novo para aprovação' : 'Enviar para aprovação'}
            </button>
          )}
          {place !== 'peca' && (metaSent || metaOk) && (
            <button type="button" onClick={() => samePiece('peca')} style={{ background: '#3859D0', color: '#fff', border: 0, width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: 'pointer' }}>
              Peça final
            </button>
          )}
          {error && <div style={{ fontSize: 12, color: '#991B1B' }}>{error}</div>}
        </div>
      </div>
      <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, padding: 14, minHeight: 280 }}>
        {busy ? (
          <div style={{ padding: '48px 16px', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 20 }}>
            <div style={{ display: 'flex', alignItems: 'flex-end', gap: 4, height: 48 }}>
              {[0,1,2,3,4,5,6].map(function(i) {
                return React.createElement('div', { key: i, style: {
                  width: 4, height: 32, background: 'var(--color-primary, #3859D0)',
                  borderRadius: 4, animation: 'ctdWave 1s ease-in-out infinite',
                  animationDelay: (i * 0.1) + 's', transformOrigin: 'bottom',
                }});
              })}
            </div>
            <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.10em', color: 'var(--color-primary, #3859D0)', fontFamily: 'var(--font-mono, monospace)' }}>A ENVIAR PARA APROVAÇÃO</div>
            <style>{`@keyframes ctdWave { 0%,100%{transform:scaleY(0.25)} 50%{transform:scaleY(1)} }`}</style>
          </div>
        ) : (
          <>
            <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', textTransform: 'uppercase', color: '#128C7E', fontFamily: 'var(--font-mono, monospace)', marginBottom: 12 }}>Template · WhatsApp</div>
            {copy ? <CtdWaFields form={form} editing={!metaSent && !metaOk} onChange={onChange} /> : <div style={{ fontSize: 13, color: '#64748B' }}>Esta peça ainda não tem copy.</div>}
          </>
        )}
      </div>
      <div style={{ position: 'sticky', top: 76 }}>
        <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
          <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)' }}>
            <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>PRÉ-VISUALIZAÇÃO</span>
          </div>
          <div style={{ padding: 12 }}>
            <CtdWaPhone pack={ctdWaPackFromForm(form)} brandName={brandName} />
          </div>
        </div>
      </div>
    </div>
  );
}

function ctdCopyPayload(copy) {
  const raw = copy?.payload;
  if (raw && typeof raw === 'object') return raw;
  try { return JSON.parse(raw || '{}'); } catch { return {}; }
}
function ctdWaLibraryKey(copy) {
  const key = String(ctdCopyPayload(copy).piece_key || '');
  return /^wa_(ab_|usp|qual_)/.test(key) ? key : '';
}
const CTD_WA_ABERTURA = {
  wa_ab_reac: 'Reactivação',
  wa_ab_acel: 'Aceleração',
  wa_ab_cart: 'Carteira',
  wa_ab_prosp: 'Prospecção',
};
const CTD_WA_QUAL = {
  wa_qual_t0: 'Primeiro contacto',
  wa_qual_fup2: 'Follow-up 2',
  wa_qual_fup3: 'Follow-up 3',
  wa_qual_fup4: 'Follow-up 4',
};
const CTD_WA_ORDEM = { wa_ab_reac: 1, wa_ab_acel: 2, wa_ab_cart: 3, wa_ab_prosp: 4, wa_qual_t0: 1, wa_qual_fup2: 2, wa_qual_fup3: 3, wa_qual_fup4: 4 };
const CTD_WA_GRUPO = { abertura: 0, qualificacao: 1, usp: 2 };
function ctdWaLibraryEntries(copies) {
  const list = (copies || []).map(copy => {
    const key = ctdWaLibraryKey(copy);
    if (!key) return null;
    const pack = ctdWaPackFromCopy(copy);
    const grupo = key.startsWith('wa_ab_') ? 'abertura' : key.startsWith('wa_qual_') ? 'qualificacao' : 'usp';
    const payload = ctdCopyPayload(copy);
    const adRef = payload.ad_ref || '';
    const title = grupo === 'abertura' ? (CTD_WA_ABERTURA[key] || key) : grupo === 'qualificacao' ? (CTD_WA_QUAL[key] || key) : (adRef || key);
    const ord = (grupo === 'abertura' || grupo === 'qualificacao') ? (CTD_WA_ORDEM[key] || 9) : (parseInt(String(key).replace(/\D/g, ''), 10) || 0);
    return { key, copy, pack, grupo, adRef, title, ord, lingua: copy.lingua || 'pt', country: String(copy.country || 'PT').toUpperCase() };
  }).filter(Boolean);
  list.sort((a, b) => {
    if (a.country !== b.country) return a.country === 'PT' ? -1 : 1;
    if (a.grupo !== b.grupo) return (CTD_WA_GRUPO[a.grupo] ?? 9) - (CTD_WA_GRUPO[b.grupo] ?? 9);
    return a.ord - b.ord;
  });
  const counts = {};
  list.forEach(e => {
    const bucket = `${e.country}:${e.grupo}`;
    counts[bucket] = (counts[bucket] || 0) + 1;
    e.pos = counts[bucket];
  });
  list.forEach(e => { e.total = counts[`${e.country}:${e.grupo}`]; });
  return list;
}
function ctdWaLibraryRow(e, status) {
  return {
    key: `wa::${e.key}::${e.lingua}`,
    idx: 1000 + (e.grupo === 'usp' ? 200 : e.grupo === 'qualificacao' ? 100 : 0) + e.pos,
    groupPos: e.pos,
    country: e.country,
    lingua: e.lingua,
    peca: {
      titulo: e.title,
      subtitulo: e.pack.body,
      content_type: 'wa_message',
      canal: 'whatsapp',
      country: e.country,
      waGroup: e.grupo,
      waTotal: e.total,
    },
    existing: e.copy,
    copy: e.copy,
    prompt: null,
    status,
    copyStatus: e.copy?.status === 'aprovado' ? 'aprovado' : (e.copy?.body ? 'gerado' : 'pendente'),
    copyStale: !!e.copyStale,
    adRef: e.adRef || '',
    stale: ctdWaTemplateStale(e.pack),
    hasImage: false,
    app: null,
    sceneLabel: null,
    wa: true,
    library: true,
  };
}
function ctdCampaignHasWhatsapp(campFull) {
  const raw = campFull?.briefing?.channels;
  const channels = Array.isArray(raw) ? raw : [];
  if (channels.map(c => String(c || '')).includes('whatsapp')) return true;
  const plan = campFull?.proposta_json?.comm_plan || [];
  return plan.some(p => p?.canal === 'whatsapp' || p?.content_type === 'wa_message' || p?.content_type === 'wa_template');
}
function ctdCampaignUspRefs(campFull) {
  let raw = campFull?.briefing?.usps;
  if (typeof raw === 'string') { try { raw = JSON.parse(raw); } catch { raw = []; } }
  const list = Array.isArray(raw) ? raw.filter(s => String(s || '').trim()) : [];
  if (list.length) return list.map((_, i) => `AD-${String(i + 1).padStart(2, '0')}`);
  const plan = (campFull?.proposta_json?.comm_plan || []).filter(p => {
    if (p?.canal !== 'meta_ads') return false;
    const ct = String(p.content_type || '');
    return ct !== 'campanha_paid' && ct !== 'paid_campaign';
  });
  return plan.map((p, i) => {
    const m = String(p.titulo || '').toUpperCase().match(/\b(AD-\d+)\b/);
    return m ? m[1] : `AD-${String(i + 1).padStart(2, '0')}`;
  });
}
function ctdWaPendingAberturas(copies, campFull) {
  if (!ctdCampaignHasWhatsapp(campFull)) return [];
  const have = new Set(ctdWaLibraryEntries(copies).map(e => e.key));
  const haveAd = new Set(ctdWaLibraryEntries(copies).map(e => e.adRef).filter(Boolean));
  const mimaki = String(campFull?.brand_slug || campFull?.briefing?.brand_slug || '').toLowerCase() === 'mimaki';
  const slots = [];
  const push = (key, grupo, title, adRef, pos, total) => {
    if (have.has(key) || (adRef && haveAd.has(adRef))) return;
    const row = ctdWaLibraryRow({
      key, copy: null, pack: ctdWaPackFromCopy(null), grupo, adRef, title,
      ord: pos, lingua: 'pt', country: 'PT', pos, total,
    }, 'pendente');
    row.existing = null;
    row.copy = null;
    row.waPending = true;
    row.copyStatus = 'pendente';
    slots.push(row);
  };
  Object.keys(CTD_WA_ABERTURA).forEach(key => push(key, 'abertura', CTD_WA_ABERTURA[key], '', CTD_WA_ORDEM[key] || 1, 4));
  if (mimaki) {
    const ads = ctdCampaignUspRefs(campFull);
    ads.forEach((ad, i) => {
      const n = parseInt(ad.replace(/\D/g, ''), 10);
      push(`wa_usp${String(n).padStart(2, '0')}`, 'usp', ad, ad, i + 1, ads.length);
    });
    Object.keys(CTD_WA_QUAL).forEach(key => push(key, 'qualificacao', CTD_WA_QUAL[key], '', CTD_WA_ORDEM[key] || 1, 4));
  }
  return slots;
}
function ctdWaMetaLabel(status, reason) {
  if (status === 'approved') return 'Aprovado';
  if (status === 'submitted' || status === 'pending') return 'Em revisão';
  if (status === 'rejected') return reason ? `Rejeitado · ${reason}` : 'Rejeitado';
  if (status === 'paused') return 'Pausado';
  return 'Por submeter';
}
function ctdWaMetaPill(status, reason) {
  if (status === 'approved') return { fg: '#166534', bg: '#DCFCE7', label: 'APROVADO', title: 'Aprovado na Meta' };
  if (status === 'rejected') return { fg: '#991B1B', bg: '#FEE2E2', label: 'REJEITADO', title: reason ? `Rejeitado na Meta · ${reason}` : 'Rejeitado na Meta' };
  if (status === 'submitted' || status === 'pending') return { fg: '#92400E', bg: '#FEF3C7', label: 'EM REVISÃO', title: 'Em revisão na Meta' };
  if (status === 'paused') return { fg: '#92400E', bg: '#FEF3C7', label: 'PAUSADO', title: 'Pausado na Meta' };
  return { fg: '#475569', bg: '#F1F5F9', label: 'POR SUBMETER', title: 'Ainda não submetido à Meta' };
}
function ctdAdCopyFp(copy) {
  const p = ctdCopyPayload(copy);
  return [copy?.copy_imagem, copy?.headline, p.image_headline, p.primary_text_hook, p.primary_text_full]
    .map(s => String(s || '').replace(/\s+/g, ' ').trim())
    .join('\n');
}
function ctdWaUspStale(entry, copies) {
  if (entry?.grupo !== 'usp' || !entry.adRef) return false;
  const stored = String(ctdCopyPayload(entry.copy).source_fp || '');
  if (!stored) return false;
  const ad = (copies || []).find(c => c?.canal === 'meta_ads' && ctdPieceCode(c.item_title) === entry.adRef);
  if (!ad) return false;
  return ctdAdCopyFp(ad) !== stored;
}
function ctdWaImagePill(grupo, pack) {
  const type = String(pack?.headerType || 'none');
  if (grupo === 'qualificacao' || type === 'none') {
    return { fg: '#475569', bg: '#F1F5F9', label: 'SEM IMAGEM', title: 'Template de texto, sem cabeçalho' };
  }
  if (String(pack?.headerImage || '').trim()) {
    return { fg: '#166534', bg: '#DCFCE7', label: 'COM IMAGEM', title: 'Cabeçalho com imagem' };
  }
  return { fg: '#991B1B', bg: '#FEE2E2', label: 'FALTA', title: 'Falta a imagem do cabeçalho' };
}
function ctdWaCopyPill(status, stale, adRef) {
  if (stale) return { fg: '#92400E', bg: '#FEF3C7', label: 'DESACTUALIZADO', title: adRef ? `O anúncio ${adRef} foi regenerado` : 'O anúncio de origem foi regenerado' };
  if (status === 'aprovado') return { fg: '#166534', bg: '#DCFCE7', label: 'APROVADO', title: 'Copy aprovada' };
  if (status === 'gerado') return { fg: '#92400E', bg: '#FEF3C7', label: 'GERADO', title: 'Copy gerada, ainda por aprovar' };
  return { fg: '#475569', bg: '#F1F5F9', label: 'PENDENTE', title: 'Ainda sem copy' };
}

function CtdWaLibraryPanel({ campanhaId, copies, prompts = [], userEmail, brandName, brandSlug = '', reloadCopies, stage = 'copy', channelOn = false }) {
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [openKey, setOpenKey] = React.useState(null);
  const [body, setBody] = React.useState('');
  const [rever, setRever] = React.useState([]);
  React.useEffect(() => {
    if (!campanhaId) return;
    fetch(`/api/marketing/biblioteca?campanha_id=${campanhaId}&canal=whatsapp`)
      .then(r => r.ok ? r.json() : { conteudos: [] })
      .then(d => setRever((d.conteudos || []).filter(c => c.estado === 'ad_a_rever')))
      .catch(() => setRever([]));
  }, [campanhaId, copies]);

  const rows = React.useMemo(() => ctdWaLibraryEntries(copies).filter(e => (e.lingua || 'pt') === 'pt').map(e => ({
    key: e.key, copy: e.copy, pack: e.pack, grupo: e.grupo, adRef: e.adRef, title: e.title, pos: e.pos, total: e.total,
    copyOk: e.copy.status === 'aprovado',
    templateOk: !!e.pack.approvedAt && !ctdWaTemplateStale(e.pack),
  })), [copies]);

  const group = (name) => rows.filter(r => r.grupo === name);
  const aberturas = group('abertura');
  const quals = group('qualificacao');
  const usps = group('usp');
  const open = rows.find(r => r.key === openKey) || null;
  const siblings = open ? (open.grupo === 'abertura' ? aberturas : open.grupo === 'qualificacao' ? quals : usps) : [];
  const pos = open ? Math.max(0, siblings.findIndex(r => r.key === open.key)) : 0;
  const images = (prompts || []).filter(p => p.image_approved_at && (p.magnific_url || p.image_url)).map(p => ({
    url: p.magnific_url || p.image_url,
    label: p.titulo || 'Imagem aprovada',
  }));
  const metaApproved = (list) => list.filter(r => r.pack.metaStatus === 'approved').length;
  const canalPronto = metaApproved(aberturas) >= 4 && metaApproved(usps) >= 3;

  React.useEffect(() => { setBody(open?.pack.body || ''); setError(null); }, [open?.copy?.id, open?.pack.body]);

  const gerar = async () => {
    setBusy(true); setError(null);
    try {
      await CtdAPI.generateWaLibrary(campanhaId, 'pt', { actor_email: userEmail });
      reloadCopies?.();
    } catch (e) { setError(e.message || 'Erro ao gerar a biblioteca'); }
    setBusy(false);
  };
  const guardar = async (extra = {}) => {
    const specs = {
      wa_body: body,
      wa_header_type: extra.headerType || open.pack.headerType,
      wa_header_image_url: extra.headerImage != null ? extra.headerImage : open.pack.headerImage,
    };
    await CtdAPI.editCopy(campanhaId, open.copy.id, { body, ad_specs: specs, actor_email: userEmail });
    reloadCopies?.();
  };
  const aprovarCopy = async () => {
    setBusy(true); setError(null);
    try {
      await CtdAPI.editCopy(campanhaId, open.copy.id, {
        body, status: 'aprovado', actor_email: userEmail,
        ad_specs: { wa_body: body, wa_header_type: open.pack.headerType, wa_header_image_url: open.pack.headerImage },
      });
      reloadCopies?.();
    } catch (e) { setError(e.message || 'Erro ao aprovar a copy'); }
    setBusy(false);
  };
  const aprovarTemplate = async () => {
    setBusy(true); setError(null);
    try {
      await CtdAPI.approveWaTemplate(open.copy.id, { actor_email: userEmail });
      reloadCopies?.();
    } catch (e) { setError(e.message || 'Erro ao aprovar o template'); }
    setBusy(false);
  };
  const submeter = async (ids) => {
    setBusy(true); setError(null);
    try {
      const out = await CtdAPI.submitWaBatch(campanhaId, ids, { actor_email: userEmail });
      const falha = (out.resultados || []).find(r => r.error);
      if (falha) setError(falha.error);
      reloadCopies?.();
    } catch (e) { setError(e.message || 'Erro ao submeter'); }
    setBusy(false);
  };

  const pillFor = (r) => ctdWaMetaPill(r.pack.metaStatus, r.pack.rejectedReason);

  if (open && stage === 'peca') {
    const pill = pillFor(open);
    const finalText = ctdWaFill(open.pack.body, open.pack.exemplos);
    return (
      <div style={{ marginBottom: 22 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, gap: 8 }}>
          <button type="button" onClick={() => setOpenKey(null)} style={{ background: 'transparent', border: 0, color: '#3859D0', fontWeight: 700, cursor: 'pointer' }}>← Peça final</button>
          <span style={{ fontSize: 12, fontWeight: 700, color: '#112954' }}>{ctdWaGrupoTitulo(open.grupo, 'PT')} · {pos + 1}/{siblings.length}</span>
          <span style={{ display: 'flex', gap: 6 }}>
            <button type="button" disabled={pos <= 0} onClick={() => setOpenKey(siblings[pos - 1].key)} style={{ border: '1px solid #E5E7EB', background: '#fff', borderRadius: 4, padding: '4px 8px', cursor: 'pointer' }}>←</button>
            <button type="button" disabled={pos >= siblings.length - 1} onClick={() => setOpenKey(siblings[pos + 1].key)} style={{ border: '1px solid #E5E7EB', background: '#fff', borderRadius: 4, padding: '4px 8px', cursor: 'pointer' }}>→</button>
          </span>
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: 'minmax(292px, 320px) minmax(0, 1fr) 300px', gap: 20, alignItems: 'start' }}>
          <div style={{ background: '#fff', border: '1px solid #BBF7D0', borderRadius: 8, overflow: 'hidden' }}>
            <div style={{ padding: '10px 14px', background: '#F0FDF4', borderBottom: '1px solid #DCFCE7' }}>
              <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#166534', fontFamily: 'var(--font-mono, monospace)' }}>APROVAÇÃO META</span>
            </div>
            <div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 10 }}>
              <span title={pill.title} style={{ alignSelf: 'flex-start', fontSize: 11, fontFamily: 'var(--font-mono)', fontWeight: 700, padding: '4px 10px', borderRadius: 4, color: pill.fg, background: pill.bg }}>{pill.label}</span>
              <div style={{ fontSize: 13, color: '#112954', lineHeight: 1.45 }}>{pill.title}</div>
              <div style={{ fontSize: 12, color: '#64748B' }}><b style={{ color: '#112954' }}>Template</b><div style={{ fontFamily: 'monospace', marginTop: 2 }}>{open.pack.name || '—'}</div></div>
              {open.pack.rejectedReason ? <div style={{ fontSize: 12, color: '#991B1B' }}>{open.pack.rejectedReason}</div> : null}
            </div>
          </div>
          <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, padding: 14 }}>
            <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', textTransform: 'uppercase', color: '#128C7E', fontFamily: 'var(--font-mono, monospace)', marginBottom: 8 }}>Conteúdo final</div>
            <div style={{ fontSize: 14, fontWeight: 700, color: '#112954', marginBottom: 10 }}>{open.title}</div>
            <div style={{ fontSize: 14, color: '#112954', lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>{finalText || '—'}</div>
            {open.pack.footer ? <div style={{ marginTop: 12, fontSize: 12, color: '#64748B' }}>{open.pack.footer}</div> : null}
          </div>
          <div style={{ position: 'sticky', top: 76 }}>
            <CtdWaPhone pack={open.pack} brandName={brandName} />
          </div>
        </div>
      </div>
    );
  }

  if (open) {
    const pack = { ...open.pack, body };
    const blockers = ctdWaBlockers(pack);
    const readOnlyUsp = open.grupo === 'usp';
    return (
      <div style={{ marginBottom: 22, background: '#F8FAFC', border: '1px solid #E2E8F0', borderRadius: 8, padding: 14 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, gap: 8 }}>
          <button type="button" onClick={() => setOpenKey(null)} style={{ background: 'transparent', border: 0, color: '#3859D0', fontWeight: 700, cursor: 'pointer' }}>← Biblioteca</button>
          <span style={{ fontSize: 12, fontWeight: 700, color: '#112954' }}>{ctdWaGrupoTitulo(open.grupo, 'PT')} · {pos + 1}/{siblings.length}</span>
          <span style={{ display: 'flex', gap: 6 }}>
            <button type="button" disabled={pos <= 0} onClick={() => setOpenKey(siblings[pos - 1].key)} style={{ border: '1px solid #E5E7EB', background: '#fff', borderRadius: 4, padding: '4px 8px', cursor: 'pointer' }}>←</button>
            <button type="button" disabled={pos >= siblings.length - 1} onClick={() => setOpenKey(siblings[pos + 1].key)} style={{ border: '1px solid #E5E7EB', background: '#fff', borderRadius: 4, padding: '4px 8px', cursor: 'pointer' }}>→</button>
          </span>
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1fr) 280px', gap: 16 }}>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            <div style={{ fontSize: 13, fontWeight: 700, color: '#112954' }}>{open.title}</div>
            <div style={{ fontSize: 11, fontFamily: 'monospace', color: '#64748B' }}>{open.pack.name}</div>
            {open.grupo === 'usp' ? (
              <div style={{ fontSize: 12, color: '#64748B' }}>{open.pack.headerImage ? `Imagem de ${open.adRef || 'o anúncio'}` : `${open.adRef || 'O anúncio'} ainda não tem imagem aprovada`}</div>
            ) : open.grupo === 'qualificacao' ? (
              <div style={{ fontSize: 12, color: '#64748B' }}>Template de texto. A Digi só o envia quando o arranque da qualificação estiver ligado e a Meta o tiver aprovado.</div>
            ) : (
              <select value={open.pack.headerImage || ''} onChange={async (e) => { setBusy(true); try { await guardar({ headerType: e.target.value ? 'image' : 'none', headerImage: e.target.value }); } catch (err) { setError(err.message); } setBusy(false); }} style={ctdWaInput}>
                <option value="">Escolher imagem aprovada</option>
                {images.map(img => <option key={img.url} value={img.url}>{img.label}</option>)}
              </select>
            )}
            {open.pack.headerImage && <img src={open.pack.headerImage} alt="" style={{ width: 120, aspectRatio: '4 / 5', objectFit: 'cover', borderRadius: 6 }} />}
            <textarea value={body} onChange={e => setBody(e.target.value)} rows={5} style={{ ...ctdWaInput, lineHeight: 1.5 }} />
            <div style={{ fontSize: 11, color: body.length > 400 ? '#991B1B' : body.length > 280 ? '#92400E' : '#64748B' }}>{body.length} caracteres · ideal até 280</div>
            {open.pack.nota && <div style={{ fontSize: 12, color: '#374151' }}><b>Nota CRM.</b> {open.pack.nota}</div>}
            <div style={{ fontSize: 12, color: '#374151' }}><b>Próxima acção.</b> {open.grupo === 'qualificacao' ? 'Toques 0, 2, 3 e 4. A retoma a meio da conversa é da SP, com o histórico.' : '3, 7 e 14 dias depois da abertura.'}</div>
            <CtdWaChecklistList pack={pack} />
            <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
              {open.pack.metaStatus !== 'submitted' && open.pack.metaStatus !== 'pending' && open.pack.metaStatus !== 'approved' && (
                <button type="button" onClick={async () => {
                  if (blockers.length) { setError(blockers[0]); return; }
                  setBusy(true); setError(null);
                  try {
                    await guardar({ body });
                    await CtdAPI.editCopy(campanhaId, open.copy.id, { body, status: 'aprovado', actor_email: userEmail });
                    await CtdAPI.approveWaTemplate(open.copy.id, { actor_email: userEmail });
                    await CtdAPI.submitWaTemplate(campanhaId, open.copy.id, { actor_email: userEmail });
                    reloadCopies?.();
                  } catch (e) { setError(e.message || 'Erro ao enviar para a Meta'); }
                  setBusy(false);
                }} disabled={busy || blockers.length > 0} style={{ background: '#3859D0', color: '#fff', border: 0, borderRadius: 6, padding: '8px 12px', fontWeight: 700, cursor: (busy || blockers.length) ? 'not-allowed' : 'pointer' }}>
                  {busy ? 'A enviar…' : open.pack.metaStatus === 'rejected' ? 'Enviar de novo para aprovação' : 'Enviar para aprovação'}
                </button>
              )}
              <button type="button" onClick={() => guardar({})} disabled={busy} style={{ background: '#fff', border: '1px solid #E5E7EB', borderRadius: 6, padding: '8px 12px', fontWeight: 700, cursor: 'pointer' }}>Guardar</button>
            </div>
            {readOnlyUsp && open.adRef && <div style={{ fontSize: 11, color: '#64748B' }}>A USP, a dor e o ângulo vêm de {open.adRef}. Se esse anúncio for regenerado, este template fica desactualizado.</div>}
            {error && <div style={{ fontSize: 12, color: '#991B1B' }}>{error}</div>}
          </div>
          <CtdWaPhone pack={pack} brandName={brandName} />
        </div>
      </div>
    );
  }

  const porSubmeter = rows.filter(r => {
    if (!r.copyOk || !r.templateOk) return false;
    if (['submitted', 'pending', 'approved'].includes(r.pack.metaStatus)) return false;
    const body = String(r.pack.body || '').trim();
    const image = String(r.pack.headerImage || '').trim();
    if (!body) return false;
    if (r.grupo !== 'qualificacao' && !image) return false;
    return true;
  }).map(r => r.copy.id);
  const pecaView = stage === 'peca';
  const colCount = pecaView ? 6 : 8;
  const th = { textAlign: 'left', padding: '8px 12px', fontSize: 10, fontFamily: 'var(--font-display)', fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--text-muted)', whiteSpace: 'nowrap', borderBottom: '1px solid var(--border)' };
  const td = { padding: '10px 12px', verticalAlign: 'middle', borderBottom: '1px solid var(--border)', fontSize: 13, color: 'var(--text)' };
  const ordered = [...quals, ...aberturas, ...usps];
  return (
    <div style={{ marginBottom: 22 }}>
      <div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 8, marginBottom: 8 }}>
        {channelOn && !rows.length && (
          <button type="button" onClick={gerar} disabled={busy} style={{ background: '#3859D0', color: '#fff', border: 0, borderRadius: 6, padding: '6px 12px', fontSize: 11, fontWeight: 700, cursor: busy ? 'not-allowed' : 'pointer', opacity: busy ? 0.5 : 1 }}>{busy ? 'A gerar…' : 'Gerar templates'}</button>
        )}
        {porSubmeter.length > 0 && (
          <button type="button" onClick={() => submeter(porSubmeter)} disabled={busy} style={{ background: '#3859D0', color: '#fff', border: 0, borderRadius: 6, padding: '6px 12px', fontSize: 11, fontWeight: 700, cursor: busy ? 'not-allowed' : 'pointer', opacity: busy ? 0.5 : 1 }}>Submeter aprovados ({porSubmeter.length})</button>
        )}
      </div>
      <div style={{ fontSize: 12, color: canalPronto ? '#166534' : 'var(--text-muted, #4a5e7a)', marginBottom: 8 }}>O canal segue para Activação quando os 4 de Daily Prospect e pelo menos 3 de SDR AI estão aprovados na Meta. Agora: {metaApproved(aberturas)}/4 Daily Prospect, {metaApproved(usps)} SDR AI. Qualificação LEADS: {quals.length} templates gerados, {metaApproved(quals)} aprovados na Meta. O arranque automático da Digi está desligado.</div>
      {error && <div style={{ fontSize: 12, color: '#991B1B', marginBottom: 8 }}>{error}</div>}
      <div className="scrollbar" style={{ overflowX: 'auto', width: '100%' }}>
      <table style={{ width: '100%', minWidth: pecaView ? 980 : 1280, borderCollapse: 'collapse', fontSize: 12.5 }}>
        <thead>
          <tr>
            <th style={{ ...th, width: 40 }}>#</th>
            <th style={th}>Peça</th>
            {pecaView ? <th style={th}>Conteúdo final</th> : (
              <>
                <th style={th}>Imagem</th>
                <th style={th}>Copy</th>
                <th style={th}>País</th>
              </>
            )}
            <th style={th}>Autor</th>
            <th style={th}>{pecaView ? 'Aprovação Meta' : 'Meta'}</th>
            <th style={{ ...th, textAlign: 'right' }}>Acções</th>
          </tr>
        </thead>
        <tbody>
          {ordered.map((r, rowIndex) => {
            const prev = rowIndex > 0 ? ordered[rowIndex - 1] : null;
            const header = ctdWaGrupoTitulo(r.grupo, 'PT');
            const prevHeader = prev ? ctdWaGrupoTitulo(prev.grupo, 'PT') : null;
            const pill = pillFor(r);
            const imagePill = ctdWaImagePill(r.grupo, r.pack);
            const copyPill = ctdWaCopyPill(r.copy?.status === 'aprovado' ? 'aprovado' : (r.copy?.body ? 'gerado' : 'pendente'), ctdWaUspStale(r, copies), r.adRef);
            return (
              <React.Fragment key={r.key}>
                {header !== prevHeader && (
                  <tr>
                    <td colSpan={colCount} style={{ padding: rowIndex === 0 ? '4px 12px' : '18px 12px 4px', borderBottom: '2px solid var(--ai-500, #3859D0)' }}>
                      <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--ai-500, #3859D0)', fontFamily: 'var(--font-mono, monospace)' }}>{header}</div>
                    </td>
                  </tr>
                )}
                <tr onClick={() => setOpenKey(r.key)} style={{ cursor: 'pointer' }}>
                  <td style={{ ...td, fontFamily: 'var(--font-mono)', color: 'var(--text-dim)', fontSize: 11 }}>{r.pos}</td>
                  <td style={td}>
                    <div style={{ fontWeight: 500, fontSize: 12, fontFamily: 'var(--font-mono)', color: 'var(--text)' }}>{r.title}</div>
                    {!pecaView && <div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 360 }}>{r.pack.body}</div>}
                  </td>
                  {pecaView ? (
                    <td style={{ ...td, maxWidth: 560 }}>
                      <div style={{ whiteSpace: 'pre-wrap', lineHeight: 1.45 }}>{ctdWaFill(r.pack.body, r.pack.exemplos) || '—'}</div>
                      {r.pack.footer ? <div style={{ marginTop: 4, fontSize: 11, color: '#64748B' }}>{r.pack.footer}</div> : null}
                    </td>
                  ) : (
                    <>
                      <td style={td}><span title={imagePill.title} style={{ fontSize: 11, fontFamily: 'var(--font-mono)', fontWeight: 600, padding: '2px 8px', borderRadius: 4, color: imagePill.fg, background: imagePill.bg }}>{imagePill.label}</span></td>
                      <td style={td}><span title={copyPill.title} style={{ fontSize: 11, fontFamily: 'var(--font-mono)', fontWeight: 600, padding: '2px 8px', borderRadius: 4, color: copyPill.fg, background: copyPill.bg }}>{copyPill.label}</span></td>
                      <td style={{ ...td, fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600 }}>PT</td>
                    </>
                  )}
                  <td style={{ ...td, whiteSpace: 'nowrap' }}>{ctdAuthorChip(r.copy?.approved_by || r.copy?.updated_by)}</td>
                  <td style={td}><span title={pill.title} style={{ fontSize: 11, fontFamily: 'var(--font-mono)', fontWeight: 600, padding: '2px 8px', borderRadius: 4, color: pill.fg, background: pill.bg }}>{pill.label}</span></td>
                  <td style={{ ...td, textAlign: 'right' }}><button type="button" onClick={() => setOpenKey(r.key)} style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.04em', padding: '4px 9px', background: 'transparent', color: '#3859D0', border: '1px solid #3859D0', borderRadius: 4, fontFamily: 'var(--font-mono)', textTransform: 'uppercase', cursor: 'pointer' }}>Abrir</button></td>
                </tr>
              </React.Fragment>
            );
          })}
        </tbody>
      </table>
      </div>
      {!rows.length && (
        <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-muted)', fontSize: 13 }}>
          {channelOn ? (String(brandSlug || '').toLowerCase() === 'mimaki' ? 'WhatsApp está no briefing. Faltam os 4 de Daily Prospect, um de SDR AI por cada USP da campanha e os 4 de qualificação.' : 'WhatsApp está no briefing. Os quatro templates de Daily Prospect ainda estão por gerar.') : 'Ainda não há templates nesta campanha.'}
        </div>
      )}
    </div>
  );
}

// ─── CtdMetaAdsFeedMockup — cartão IG/FB (feed 4:5) para coluna de preview ───
function ctdCopyToPreviewForm(copy) {
  if (!copy) return {};
  let payload = {};
  if (copy.payload && typeof copy.payload === 'object') payload = copy.payload;
  else try { payload = JSON.parse(copy.payload || '{}'); } catch { payload = {}; }
  const base = {
    copy_imagem: copy.copy_imagem || '',
    copy_imagem_sub: copy.copy_imagem_sub || '',
    headline: copy.headline || '',
    caption_organica: copy.caption_organica || '',
    cta: copy.cta || '',
    payload,
  };
  if (ctdIsWaPiece(null, copy)) Object.assign(base, ctdWaFormFields(copy));
  return base;
}

function CtdMetaAdsFeedMockup({ form = {}, flush = false, brandName, brandSlug, imageUrl, pageDomain, staleLabel, aspectRatio = '4 / 5', videoUrl, onOpenFullscreen }) {
  const [expanded, setExpanded] = React.useState(false);
  const [imgFailed, setImgFailed] = React.useState(false);
  const [playing, setPlaying] = React.useState(false);
  const videoRef = React.useRef(null);
  React.useEffect(() => { setImgFailed(false); }, [imageUrl]);
  React.useEffect(() => { setPlaying(false); }, [videoUrl]);
  const page = ctdBrandPage(brandSlug, brandName);
  const name = brandName || page.name;
  const domain = pageDomain || page.domain;
  const color = ctdBrandColor(brandSlug);
  const pt = (form.payload?.primary_text_full || form.caption_organica || '').trim();
  const blocks = pt.split(/\n\n+/).map(b => b.trim()).filter(Boolean);
  const hook = (blocks[0] || pt).split(/\n+/)[0] || '';
  const restBlocks = blocks.length > 1 ? blocks.slice(1) : pt.split(/\n+/).slice(1);
  const hookNeedsCut = hook.length > 125 || restBlocks.length > 0 || pt.length > 160;
  const hookShown = (!expanded && hook.length > 125) ? (hook.slice(0, 125).replace(/\s+\S*$/, '') + '…') : hook;
  const hasMore = hookNeedsCut;
  const isVideo = !!videoUrl;
  const isStory = String(aspectRatio).replace(/\s/g, '') === '9/16';
  const showImage = imageUrl && !imgFailed;
  const showMedia = isVideo || showImage;
  const emptyLabel = isStory || isVideo ? 'Vídeo 9:16' : 'Imagem 4:5';
  const togglePlay = (e) => {
    e?.stopPropagation();
    const el = videoRef.current;
    if (!el) return;
    if (el.paused) el.play().catch(() => {}); else el.pause();
  };
  const FbIco = ({ d }) => (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#050505" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d={d} /></svg>
  );
  return (
    <div style={{
      width: '100%', border: flush ? 0 : '1px solid #DADDE1', borderRadius: flush ? 0 : 8,
      overflow: 'hidden', background: '#fff',
      fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
      boxShadow: flush ? 'none' : '0 1px 2px rgba(0,0,0,0.06)',
    }}>
      <div style={{ padding: '10px 12px 8px', display: 'flex', alignItems: 'center', gap: 8 }}>
        <div style={{
          width: 36, height: 36, borderRadius: '50%', flexShrink: 0,
          background: color, color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center',
          fontSize: 14, fontWeight: 700, letterSpacing: '-0.02em',
        }}>{String(name).charAt(0).toUpperCase()}</div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 13, fontWeight: 600, color: '#050505', lineHeight: 1.2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{name}</div>
          <div style={{ fontSize: 12, color: '#65676B', lineHeight: 1.3, display: 'flex', alignItems: 'center', gap: 4 }}>
            Sponsored
            <span style={{ opacity: 0.7 }}>·</span>
            <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#65676B" strokeWidth="2"><circle cx="12" cy="12" r="9" /><path d="M2 12h20M12 3a15 15 0 010 18M12 3a15 15 0 000 18" /></svg>
          </div>
        </div>
        <span style={{ fontSize: 18, color: '#65676B', lineHeight: 1, padding: '0 2px' }}>···</span>
      </div>
      {pt ? (
        <div style={{ padding: '0 12px 10px', fontSize: 14, color: '#050505', lineHeight: 1.38 }}>
          <div style={expanded ? { whiteSpace: 'pre-wrap' } : {
            display: '-webkit-box', WebkitLineClamp: 3, WebkitBoxOrient: 'vertical',
            overflow: 'hidden', whiteSpace: 'pre-wrap',
          }}>
            {expanded ? blocks.join('\n\n') : hookShown}
          </div>
          {hasMore && (
            <button type="button" onClick={() => setExpanded(v => !v)}
              style={{ border: 0, background: 'none', padding: 0, marginTop: 4, color: '#65676B', fontWeight: 600, fontSize: 14, cursor: 'pointer' }}>
              {expanded ? 'Ver menos' : 'Ver mais'}
            </button>
          )}
        </div>
      ) : (
        <div style={{ padding: '0 12px 10px', fontSize: 13, color: '#8A8D91' }}>O primary text aparece aqui.</div>
      )}
      <div style={{ position: 'relative', background: showMedia ? '#111318' : '#EEF1F6', aspectRatio: aspectRatio, overflow: 'hidden' }}>
        {isVideo ? (
          <>
            <video ref={videoRef} src={videoUrl} poster={imageUrl || undefined} playsInline muted loop
              onPlay={() => setPlaying(true)} onPause={() => setPlaying(false)}
              onClick={togglePlay}
              onDoubleClick={(e) => { e.stopPropagation(); onOpenFullscreen?.(); }}
              style={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block', background: '#111318', cursor: 'pointer' }} />
            {!playing && (
              <button type="button" onClick={togglePlay} title="Reproduzir"
                style={{ position: 'absolute', inset: 0, zIndex: 2, background: 'rgba(0,0,0,0.12)', border: 0, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                <span style={{ width: 48, height: 48, borderRadius: '50%', background: 'rgba(255,255,255,0.94)', display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 6px 18px rgba(0,0,0,0.28)' }}>
                  <svg width="16" height="16" viewBox="0 0 24 24" fill="#111"><path d="M8 5v14l11-7z"/></svg>
                </span>
              </button>
            )}
          </>
        ) : showImage ? (
          <>
            <img src={imageUrl} alt="" onError={() => setImgFailed(true)}
              style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
            {staleLabel && (
              <div style={{ position: 'absolute', inset: 0, background: 'rgba(17,24,39,0.42)', display: 'flex', alignItems: 'flex-end', justifyContent: 'center', padding: 12 }}>
                <div style={{ background: '#FEF3C7', color: '#92400E', fontSize: 11, fontWeight: 700, padding: '8px 10px', borderRadius: 6, textAlign: 'center', lineHeight: 1.35 }}>
                  {staleLabel}
                </div>
              </div>
            )}
          </>
        ) : (
          <div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column' }}>
            <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 8, padding: 16 }}>
              <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#9AA3B2" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <rect x="3" y="3" width="18" height="18" rx="2" />
                <circle cx="8.5" cy="8.5" r="1.5" />
                <path d="M21 15l-5-5-4 4-2-2-7 7" />
              </svg>
              <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.06em', color: '#7A8494', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>{emptyLabel}</div>
              <div style={{ fontSize: 12, color: '#8B93A1', textAlign: 'center', lineHeight: 1.35 }}>{isVideo || isStory ? 'Ainda não gerado' : 'Ainda não gerada'}</div>
            </div>
            {(form.copy_imagem || form.copy_imagem_sub) && (
              <div style={{ background: color, padding: '10px 12px' }}>
                {form.copy_imagem && (
                  <div style={{ fontSize: 13, fontWeight: 800, color: '#fff', textTransform: 'uppercase', letterSpacing: '0.02em', lineHeight: 1.2 }}>
                    {form.copy_imagem}
                  </div>
                )}
                {form.copy_imagem_sub && (
                  <div style={{ fontSize: 11, color: 'rgba(255,255,255,0.88)', lineHeight: 1.3, marginTop: 4 }}>
                    {form.copy_imagem_sub}
                  </div>
                )}
                {form.copy_imagem_cta && (
                  <div style={{ fontSize: 11, fontWeight: 800, color: ctdInkOn(color), marginTop: 6 }}>{String(form.copy_imagem_cta).replace(/\s*→+\s*/g, ' ').trim()}</div>
                )}
              </div>
            )}
          </div>
        )}
      </div>
      <div style={{ padding: '10px 12px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10, background: '#F0F2F5', borderTop: '1px solid #E4E6EB' }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 11, color: '#65676B', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: 2 }}>{domain}</div>
          <div style={{ fontSize: 15, fontWeight: 600, color: '#050505', lineHeight: 1.25, overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }}>
            {form.headline || 'Título do anúncio'}
          </div>
          {form.payload?.description && (
            <div style={{ fontSize: 13, color: '#65676B', marginTop: 2, overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
              {form.payload.description}
            </div>
          )}
        </div>
        <div style={{
          background: '#E4E6EB', borderRadius: 6, padding: '8px 12px', flexShrink: 0,
          fontSize: 13, fontWeight: 700, color: '#050505', whiteSpace: 'nowrap',
        }}>
          {form.cta || 'Saber mais'}
        </div>
      </div>
      <div style={{ padding: '8px 14px 10px', display: 'flex', alignItems: 'center', gap: 16, borderTop: '1px solid #E4E6EB' }}>
        <FbIco d="M20.8 4.6a5.5 5.5 0 00-7.8 0L12 5.6l-1-1a5.5 5.5 0 00-7.8 7.8l1 1L12 21l7.8-7.6 1-1a5.5 5.5 0 000-7.8z" />
        <FbIco d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
        <FbIco d="M4 12v8a2 2 0 002 2h12a2 2 0 002-2v-8M16 6l-4-4-4 4M12 2v13" />
        <span style={{ marginLeft: 'auto', display: 'flex' }}>
          <FbIco d="M19 21l-7-4-7 4V5a2 2 0 012-2h10a2 2 0 012 2z" />
        </span>
      </div>
    </div>
  );
}

function ctdHtmlToLines(html) {
  const src = String(html || '');
  const clean = (s) => s.replace(/<[^>]+>/g, '').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&nbsp;/g, ' ').trim();
  const parts = [...src.matchAll(/<(p|li)[^>]*>([\s\S]*?)<\/\1>/gi)].map(m => clean(m[2])).filter(Boolean);
  if (parts.length) return parts.join('\n');
  return clean(src.replace(/<br\s*\/?>/gi, '\n').replace(/<\/p>/gi, '\n'));
}
function ctdLinesToHtml(text) {
  const lines = String(text || '').split('\n').map(s => s.trim()).filter(Boolean);
  if (!lines.length) return '';
  const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;');
  return '<ul>' + lines.map(l => '<li>' + esc(l) + '</li>').join('') + '</ul>';
}
function ctdSplitPgArray(s) {
  if (!s.startsWith('{') || !s.endsWith('}')) return null;
  const inner = s.slice(1, -1);
  const out = [];
  let cur = '';
  let quoted = false;
  for (let i = 0; i < inner.length; i++) {
    const c = inner[i];
    if (c === '"') {
      if (quoted && inner[i + 1] === '"') { cur += '"'; i++; continue; }
      quoted = !quoted;
      continue;
    }
    if (c === '\\' && quoted) { cur += inner[++i] || ''; continue; }
    if (c === ',' && !quoted) { out.push(cur.trim()); cur = ''; continue; }
    cur += c;
  }
  if (cur.trim() || out.length) out.push(cur.trim());
  return out.filter(Boolean);
}

function ctdPlainItems(value) {
  if (value == null || value === '') return [];
  if (Array.isArray(value)) return value.flatMap(ctdPlainItems);
  if (typeof value === 'object') {
    const t = value.kpi || value.metric || value.label || value.text || value.name || '';
    return t ? ctdPlainItems(t) : [];
  }
  const s = String(value).trim();
  if (!s) return [];
  if (s.startsWith('[')) {
    try {
      const parsed = JSON.parse(s);
      if (Array.isArray(parsed)) return parsed.flatMap(ctdPlainItems);
    } catch { /* texto simples */ }
  }
  if (s.startsWith('{') && s.endsWith('}')) {
    try {
      const parsed = JSON.parse(s);
      if (Array.isArray(parsed)) return parsed.flatMap(ctdPlainItems);
      if (parsed && typeof parsed === 'object') return ctdPlainItems(parsed);
    } catch { /* formato de array do Postgres */ }
    const pg = ctdSplitPgArray(s);
    if (pg && pg.length) return pg.flatMap(ctdPlainItems);
  }
  return [s];
}

function ctdPlainText(value) {
  return ctdPlainItems(value).join('\n');
}
function ctdLineStats(text, maxLines, maxWords, pitchWords) {
  const lines = String(text || '').split('\n').map(s => s.trim()).filter(Boolean);
  const long = lines.filter(l => {
    const n = l.split(/\s+/).filter(Boolean).length;
    const cap = (pitchWords && /^pitch\b/i.test(l)) ? pitchWords : (maxWords || 99);
    return n > cap;
  }).length;
  return { n: lines.length, overLines: maxLines ? lines.length > maxLines : false, overWords: long > 0 };
}

const CTD_INTERNAL_ROLES = [
  { id: 'comercial', label: 'Comercial' },
  { id: 'sat', label: 'SAT' },
  { id: 'board', label: 'Board' },
  { id: 'operacoes', label: 'Operações' },
  { id: 'marketing', label: 'Marketing' },
  { id: 'rh_finance', label: 'RH & Fin' },
  { id: 'default', label: 'Default' },
];

const ctdMailInput = {
  width: '100%', border: 'none', background: 'transparent', outline: 'none',
  fontFamily: 'Inter, sans-serif', padding: 0, margin: 0, boxSizing: 'border-box', resize: 'none',
};

const CtdMailCount = ({ n, max, bad }) => (
  <span style={{ fontSize: 9, color: bad ? '#EF4444' : '#B4B4B4', fontFamily: 'monospace', whiteSpace: 'nowrap' }}>{n}{max != null ? ` / ${max}` : ''}</span>
);

const CtdMailLines = ({ label, html, onHtml, editing, maxLines, maxWords, pitchWords, placeholder, color, sentence }) => {
  const text = ctdHtmlToLines(html);
  const stats = ctdLineStats(text, maxLines, maxWords, pitchWords);
  const bad = stats.overLines || stats.overWords;
  return (
    <div style={{ marginBottom: 18 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
        <div style={sentence
          ? { fontSize: 16, fontWeight: 700, color: '#112954', lineHeight: 1.3, marginBottom: 2 }
          : { fontSize: editing ? 9 : 11, fontWeight: 700, color: color || '#EF644C', letterSpacing: '0.08em' }}>{label}</div>
        {editing && <CtdMailCount n={stats.n} max={maxLines} bad={bad} />}
      </div>
      {editing ? (
        <textarea
          value={text}
          onChange={e => onHtml(ctdLinesToHtml(e.target.value))}
          placeholder={placeholder}
          rows={Math.min(12, Math.max(2, stats.n || 2))}
          style={{ ...ctdMailInput, fontSize: 14, lineHeight: 1.65, color: text ? '#141618' : '#B4B4B4', minHeight: 48 }}
        />
      ) : (
        <div className="ctd-mail-copy" style={{ fontSize: 14, lineHeight: 1.65, color: '#141618', overflowWrap: 'anywhere', wordBreak: 'break-word' }} dangerouslySetInnerHTML={{ __html: html || '<span style="color:#B4B4B4">—</span>' }} />
      )}
    </div>
  );
};

function ctdEsc(s) {
  return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function ctdSaudacao() {
  const hour = Number(new Intl.DateTimeFormat('en-GB', { timeZone: 'Europe/Lisbon', hour: 'numeric', hour12: false }).format(new Date()));
  return hour < 12 ? 'Bom dia' : hour < 19 ? 'Boa tarde' : 'Boa noite';
}

function ctdComposeInternalLead(html, { brand, headline, title, preheader, tagline } = {}) {
  const src = String(html || '');
  const clean = (s) => s.replace(/<[^>]+>/g, ' ').replace(/&amp;/g, '&').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim();
  const paras = [...src.matchAll(/<(p|li)[^>]*>([\s\S]*?)<\/\1>/gi)].map(m => clean(m[2])).filter(Boolean);
  if (!paras.length) {
    const t = clean(src);
    if (t) paras.push(t);
  }
  const greetingOnly = /^(bom dia|boa tarde|boa noite)\b/i;
  const body = paras.filter((p, i) => !(i === 0 && greetingOnly.test(p) && p.length < 220 && !/\b(filme|rende|sustent|oferta|testing|preço|preco)\b/i.test(p)));
  let pitch = body.join(' ').replace(/^(bom dia|boa tarde|boa noite)\b[.!]?\s*/i, '').trim();
  pitch = pitch.replace(/^(?:vai sair a campanha|a campanha)\b[\s\S]*?\.\s+/i, '').trim();
  const name = String(headline || '').replace(/^campanha\s+/i, '').trim()
    || String(title || '').replace(/\s*·\s*briefing\s*$/i, '').trim();
  const when = `${preheader || ''} ${tagline || ''}`.match(/(no ar desde|arranca a)\s+(\d{1,2}\s+[A-Za-zÀ-ÿ]+)/i);
  let launch = '';
  if (name && when) {
    const day = when[2];
    const future = /arranca/i.test(when[1]);
    const brandName = String(brand || '').trim();
    const who = brandName && !name.toLowerCase().includes(brandName.toLowerCase()) ? `${brandName} ${name}` : name;
    launch = future
      ? `Vai sair a campanha ${who} no dia ${day}.`
      : `A campanha ${who} está no ar desde ${day}.`;
  }
  if (!launch && !pitch) return src;
  const text = [ctdSaudacao() + '.', launch, pitch].filter(Boolean).join(' ');
  return '<p>' + ctdEsc(text) + '</p>';
}

function ctdInternalEmailHtml({ wide, accent, brandName, title, tagline, imageUrl, lead, tail, ctaLabel, ctaUrl, signature }) {
  const pad = wide ? '28px 32px 8px' : '20px 16px 8px';
  const titleSize = wide ? 22 : 18;
  const block = (label, html) => `
    <tr><td style="padding:0 0 8px;">
      <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
        <tr>
          <td width="3" style="width:3px;background:${accent};font-size:0;line-height:0;">&nbsp;</td>
          <td style="padding:2px 0 2px 14px;font-family:Inter,Arial,sans-serif;">
            <div style="font-size:16px;font-weight:700;color:#112954;line-height:1.3;margin:0 0 8px;">${ctdEsc(label)}</div>
            <div class="copy" style="font-size:14px;line-height:1.65;color:#141618;">${html || '<span style="color:#B4B4B4">—</span>'}</div>
          </td>
        </tr>
      </table>
    </td></tr>`;
  const hero = imageUrl ? `
    <tr><td style="padding:12px 0 8px;">
      <img src="${ctdEsc(imageUrl)}" alt="" width="536" style="display:block;width:100%;max-width:536px;height:auto;border:0;border-radius:4px;" />
    </td></tr>` : '';
  const brand = brandName && brandName !== 'Digidelta Internacional'
    ? `<td align="right" style="font-family:Inter,Arial,sans-serif;font-size:11px;font-weight:700;letter-spacing:0.08em;color:#ffffff;text-transform:uppercase;">${ctdEsc(brandName)}</td>`
    : '';
  return `<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>${ctdEsc(title || 'Campanha')}</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet"/>
<style>
  body{margin:0;padding:0;background:#ffffff;}
  .copy p{margin:0 0 10px;}
  .copy p:last-child{margin-bottom:0;}
  .copy ul{margin:8px 0 0;padding-left:1.15em;}
  .copy li{margin:0 0 8px;}
  .copy li:last-child{margin-bottom:0;}
</style>
</head>
<body>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#ffffff;">
  <tr><td style="background:#112954;padding:${wide ? '16px 28px' : '14px 16px'};">
    <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
      <tr>
        <td><img src="https://www.digidelta.pt/wp-content/uploads/2026/01/digidelta-logo-branco.svg" alt="Digidelta" height="22" style="display:block;border:0;height:22px;" /></td>
        ${brand}
      </tr>
    </table>
  </td></tr>
  <tr><td style="height:3px;background:${accent};font-size:0;line-height:0;">&nbsp;</td></tr>
  <tr><td style="padding:${pad};font-family:Inter,Arial,sans-serif;">
    <div style="font-size:${titleSize}px;font-weight:800;color:#112954;line-height:1.25;margin:0 0 6px;">${ctdEsc(title || 'Campanha')}</div>
    <div style="font-size:13px;color:#6B7280;line-height:1.45;margin:0 0 8px;">${ctdEsc(tagline)}</div>
    <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
      ${hero}
      ${(lead || []).map(b => block(b.label, b.html)).join('')}
      <tr><td align="center" style="padding:8px 0 22px;">
        <a href="${ctdEsc(ctaUrl)}" style="display:inline-block;background:${accent};color:#ffffff;font-family:Inter,Arial,sans-serif;font-size:15px;font-weight:700;text-decoration:none;border-radius:4px;padding:12px 28px;">${ctdEsc(ctaLabel || 'Abrir Daily Prospect')}</a>
      </td></tr>
      ${(tail || []).map(b => block(b.label, b.html)).join('')}
    </table>
  </td></tr>
  <tr><td align="center" style="padding:4px 24px 4px;font-family:Inter,Arial,sans-serif;font-size:14px;color:#374151;">${ctdEsc(signature || 'Boa campanha')}</td></tr>
  <tr><td align="center" style="padding:8px 24px 28px;font-family:Inter,Arial,sans-serif;font-size:11px;line-height:1.6;color:#B4B4B4;">Digidelta Internacional · Rua da Igreja, 251 · 4485-849 Vairão<br/>Privacidade</td></tr>
</table>
</body>
</html>`;
}

const CtdInternalMailPreview = ({ form, copy, imageUrl, editing, onField, onPayload, mode: modeProp, role: roleProp, embed, brandSlug }) => {
  const [modeState, setModeState] = React.useState('desktop');
  const [roleState, setRoleState] = React.useState('comercial');
  const mode = embed ? (modeProp || 'desktop') : modeState;
  const role = embed ? (roleProp || 'comercial') : roleState;
  const setMode = setModeState;
  const setRole = setRoleState;
  const p = form?.payload || {};
  const wide = mode === 'desktop';
  const accent = ctdBrandColor(brandSlug);
  const brandName = (CTD_BRAND_PAGE[String(brandSlug || '').toLowerCase()] || {}).name || '';
  const [frameH, setFrameH] = React.useState(720);
  const roleHeadline = ({
    comercial: 'Quem contactas esta semana',
    sat: 'Se a dúvida for técnica',
    board: 'Em duas linhas para a direcção',
    operacoes: 'O que muda na operação',
    marketing: 'O que o marketing acompanha',
    rh_finance: 'Se houver uma condição financeira',
    default: 'Para quem receber',
  })[role];
  const subject = form?.headline || '';
  const preheader = p.preheader || '';
  const title = p.campaign_title || '';
  const tagline = p.campaign_tagline || '';
  const flags = Array.isArray(p.qa_flags) ? p.qa_flags : [];
  const setHeadline = (v) => { onField?.('headline', v); onPayload?.('email_subject', v); };
  const mailHtml = ctdInternalEmailHtml({
    wide,
    accent,
    brandName,
    title,
    tagline,
    imageUrl,
    lead: [
      { label: 'O que dizes ao cliente', html: ctdComposeInternalLead(p.resumo_html, { brand: brandName === 'Digidelta Internacional' ? '' : brandName, headline: subject, title, preheader, tagline }) },
      { label: 'O que levas para a reunião', html: p.objectivos_html },
      { label: 'Se te disserem isto', html: p.canais_html },
    ],
    tail: (p['role_' + role + '_html'] || role === 'comercial')
      ? [{ label: roleHeadline, html: p['role_' + role + '_html'] }]
      : [],
    ctaLabel: p.cta_label || 'Abrir Daily Prospect',
    ctaUrl: p.cta_url || 'https://marketing.farben-holding.ai/#screen=marketing&sub=sdr',
    signature: p.signature_close || 'Boa campanha',
  });
  const fitFrame = (frame) => {
    const doc = frame && frame.contentDocument;
    if (!doc) return;
    const h = Math.max(doc.documentElement ? doc.documentElement.scrollHeight : 0, doc.body ? doc.body.scrollHeight : 0);
    if (h > 80) setFrameH(h);
    Array.from(doc.images || []).forEach(img => {
      if (!img.complete) img.addEventListener('load', () => fitFrame(frame), { once: true });
    });
  };
  const emailBody = (
        <div style={{ background: '#f9faff', borderRadius: wide ? 0 : 26, overflow: 'hidden', overflowWrap: 'anywhere', wordBreak: 'break-word' }}>
          <div style={{ padding: '14px 20px 12px', background: '#fff', borderBottom: '1px solid #E5E7EB' }}>
            <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
              <span style={{ fontSize: 10, color: '#9CA3AF', width: 52, flexShrink: 0 }}>Assunto</span>
              {editing ? (
                <input value={subject} onChange={e => setHeadline(e.target.value)} placeholder="Assunto do email" style={{ ...ctdMailInput, width: 'auto', minWidth: 0, flex: 1, fontSize: 13, fontWeight: 700, color: '#111827' }} />
              ) : (
                <span style={{ fontSize: 13, fontWeight: 700, color: '#111827' }}>{subject || '—'}</span>
              )}
              {editing && <CtdMailCount n={subject.length} max={80} bad={subject.length > 80} />}
            </div>
            <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
              <span style={{ fontSize: 10, color: '#9CA3AF', width: 52, flexShrink: 0 }}>Preview</span>
              {editing ? (
                <input value={preheader} onChange={e => onPayload?.('preheader', e.target.value)} placeholder="Preheader" style={{ ...ctdMailInput, width: 'auto', minWidth: 0, flex: 1, fontSize: 12, color: '#6B7280' }} />
              ) : (
                <span style={{ fontSize: 12, color: '#6B7280' }}>{preheader || '—'}</span>
              )}
              {editing && <CtdMailCount n={preheader.length} max={85} bad={preheader.length > 85} />}
            </div>
          </div>
          <iframe
            key={role + (wide ? '-d' : '-m')}
            title="Template do email"
            sandbox="allow-same-origin"
            srcDoc={mailHtml}
            onLoad={e => fitFrame(e.currentTarget)}
            style={{ width: '100%', height: frameH, border: 0, display: 'block', background: '#fff' }}
          />
        </div>
  );
  return (
    <div style={{ fontFamily: 'Inter,sans-serif', background: embed ? 'transparent' : '#fff', border: embed ? 'none' : '1px solid #ECEFF5', borderRadius: embed ? 0 : 8, overflow: 'hidden' }}>
      <style>{'.ctd-mail-copy ul{margin:8px 0 0;padding-left:1.15em}.ctd-mail-copy li{margin:0 0 8px;padding:0}.ctd-mail-copy li:last-child{margin-bottom:0}.ctd-mail-copy p{margin:0 0 10px}.ctd-mail-copy p:last-child{margin-bottom:0}@keyframes ctdRoleIn{from{background:#EEF3FF}to{background:transparent}}.ctd-role-block{animation:ctdRoleIn .7s ease;border-radius:4px}'}</style>
      {!embed && (
        <React.Fragment>
          <div style={{ padding: '10px 14px', borderBottom: '1px solid #ECEFF5', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
            <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>Pré-visualização</span>
            <span style={{ fontSize: 9, color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>Email · 16:9</span>
          </div>
          <div style={{ display: 'flex', gap: 6, padding: '10px 14px', borderBottom: '1px solid #E5E7EB' }}>
            {['desktop', 'mobile'].map(id => (
              <button key={id} type="button" onClick={() => setMode(id)} style={{ border: '1px solid ' + (mode === id ? '#0F4FF9' : '#E5E7EB'), background: mode === id ? '#0F4FF9' : '#fff', borderRadius: 6, padding: '6px 12px', fontSize: 12, fontWeight: 700, cursor: 'pointer', color: mode === id ? '#fff' : '#111827' }}>
                {id === 'desktop' ? 'Desktop' : 'Telemóvel'}
              </button>
            ))}
          </div>
          <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', padding: '10px 14px', borderBottom: '1px solid #E5E7EB' }}>
            {CTD_INTERNAL_ROLES.map(r => (
              <button key={r.id} type="button" onClick={() => setRole(r.id)} style={{ border: '1px solid ' + (role === r.id ? '#0F4FF9' : '#E5E7EB'), background: role === r.id ? 'rgba(15,79,249,0.08)' : '#fff', borderRadius: 6, padding: '5px 10px', fontSize: 11, fontWeight: 700, cursor: 'pointer', color: role === r.id ? '#0F4FF9' : '#374151' }}>
                {r.label}
              </button>
            ))}
          </div>
        </React.Fragment>
      )}
      {!embed && flags.length > 0 && (
        <div style={{ padding: '8px 14px', background: '#FEF3C7', color: '#92400E', fontSize: 11, lineHeight: 1.45 }}>{flags.join(' · ')}</div>
      )}
      <div style={{ background: embed ? 'transparent' : '#F5F5F7', padding: embed ? 0 : (wide ? 16 : '16px 12px 24px'), display: 'flex', justifyContent: 'center' }}>
        {wide ? (
          <div style={{ width: 600, maxWidth: '100%', borderRadius: 12, overflow: 'hidden', boxShadow: '0 16px 40px rgba(17,41,84,0.16)' }}>{emailBody}</div>
        ) : (
          <div style={{ width: 392, maxWidth: '100%', background: '#0B1220', borderRadius: 36, padding: '12px 10px 14px', boxShadow: '0 20px 48px rgba(17,41,84,0.28)' }}>
            <div style={{ display: 'flex', justifyContent: 'center', marginBottom: 8 }}><div style={{ width: 84, height: 10, borderRadius: 8, background: '#000' }} /></div>
            {emailBody}
          </div>
        )}
      </div>
    </div>
  );
};

const CtdInternalPreviewModal = ({ form, copy, imageUrl, onField, onPayload, onClose, brandSlug }) => {
  const [mode, setMode] = React.useState('desktop');
  const [role, setRole] = React.useState('comercial');
  const subject = form?.headline || '';
  const flags = Array.isArray(form?.payload?.qa_flags) ? form.payload.qa_flags : [];
  React.useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => {
      document.body.style.overflow = prev;
      window.removeEventListener('keydown', onKey);
    };
  }, [onClose]);
  return ReactDOM.createPortal(
    <div onClick={onClose} role="presentation" style={{ position: 'fixed', inset: 0, background: 'rgba(17,41,84,0.55)', zIndex: 1100, display: 'flex', justifyContent: 'center', alignItems: 'center', padding: 20 }}>
      <div role="dialog" aria-modal="true" aria-label="Pré-visualização do email" onClick={e => e.stopPropagation()} style={{ width: 'min(980px, 100%)', height: 'min(860px, calc(100vh - 40px))', background: '#fff', borderRadius: 14, boxShadow: '0 24px 64px rgba(17,41,84,0.28)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16, padding: '14px 18px', borderBottom: '1px solid #ECEFF5', flexShrink: 0 }}>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.08em', color: '#3859D0' }}>COMUNICAÇÃO INTERNA</div>
            <div style={{ fontSize: 15, fontWeight: 700, color: '#112954', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{subject || 'Pré-visualização do email'}</div>
          </div>
          <button type="button" onClick={onClose} style={{ border: '1px solid #E3E6EB', background: '#fff', borderRadius: 8, padding: '7px 12px', cursor: 'pointer', color: '#112954', fontSize: 13, fontWeight: 700, flexShrink: 0 }}>Fechar</button>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 18px', borderBottom: '1px solid #ECEFF5', flexShrink: 0, flexWrap: 'wrap' }}>
          <div style={{ display: 'inline-flex', background: '#F1F4F9', borderRadius: 8, padding: 3, flexShrink: 0 }}>
            {[['desktop', 'Desktop'], ['mobile', 'Telemóvel']].map(([id, label]) => (
              <button key={id} type="button" onClick={() => setMode(id)} style={{ border: 'none', borderRadius: 6, padding: '6px 12px', background: mode === id ? '#fff' : 'transparent', boxShadow: mode === id ? '0 1px 2px rgba(17,41,84,0.12)' : 'none', fontWeight: 700, fontSize: 12, color: mode === id ? '#112954' : '#64748b', cursor: 'pointer' }}>
                {label}
              </button>
            ))}
          </div>
          <div style={{ width: 1, alignSelf: 'stretch', background: '#E3E6EB', flexShrink: 0 }} />
          <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', flex: 1, minWidth: 0 }}>
            {CTD_INTERNAL_ROLES.map(r => (
              <button key={r.id} type="button" onClick={() => setRole(r.id)} style={{ border: '1px solid ' + (role === r.id ? '#3859D0' : '#E3E6EB'), background: role === r.id ? '#EFF3FF' : '#fff', color: role === r.id ? '#3859D0' : '#475569', borderRadius: 999, padding: '5px 10px', fontSize: 11, fontWeight: 700, cursor: 'pointer' }}>
                {r.label}
              </button>
            ))}
          </div>
        </div>
        {flags.length > 0 && (
          <div style={{ padding: '8px 18px', background: '#FEF3C7', color: '#92400E', fontSize: 12, lineHeight: 1.45, flexShrink: 0 }}>{flags.join(' · ')}</div>
        )}
        <div style={{ flex: 1, overflow: 'auto', background: '#E7EBF3', padding: '28px 16px 40px' }}>
          <CtdInternalMailPreview form={form} copy={copy} imageUrl={imageUrl} editing={false} onField={onField} onPayload={onPayload} embed mode={mode} role={role} brandSlug={brandSlug} />
        </div>
      </div>
    </div>,
    document.body
  );
};

const CtdInternalPreviewRail = ({ form, copy, imageUrl, brandSlug }) => {
  const [mode, setMode] = React.useState('desktop');
  const [role, setRole] = React.useState('comercial');
  return (
    <div style={{ position: 'sticky', top: 76, minWidth: 0 }}>
      <div style={{ background: '#fff', border: '1px solid #ECEFF5', borderRadius: 8, overflow: 'hidden' }}>
        <div style={{ padding: '10px 12px', borderBottom: '1px solid #ECEFF5', display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
          <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>Pré-visualização</span>
          <span style={{ fontSize: 9, color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>Email · 16:9</span>
        </div>
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', padding: '8px 12px', borderBottom: '1px solid #ECEFF5' }}>
          {[['desktop', 'Desktop'], ['mobile', 'Telemóvel']].map(([id, label]) => (
            <button key={id} type="button" onClick={() => setMode(id)} style={{ border: '1px solid ' + (mode === id ? '#3859D0' : '#E3E6EB'), background: mode === id ? '#EFF3FF' : '#fff', color: mode === id ? '#3859D0' : '#475569', borderRadius: 999, padding: '4px 8px', fontSize: 11, fontWeight: 700, cursor: 'pointer' }}>
              {label}
            </button>
          ))}
          {CTD_INTERNAL_ROLES.map(r => (
            <button key={r.id} type="button" onClick={() => setRole(r.id)} style={{ border: '1px solid ' + (role === r.id ? '#3859D0' : '#E3E6EB'), background: role === r.id ? '#EFF3FF' : '#fff', color: role === r.id ? '#3859D0' : '#475569', borderRadius: 999, padding: '4px 8px', fontSize: 11, fontWeight: 700, cursor: 'pointer' }}>
              {r.label}
            </button>
          ))}
        </div>
        <div style={{ background: '#E7EBF3', padding: 10, maxHeight: 'calc(100vh - 220px)', overflow: 'auto' }}>
          <CtdInternalMailPreview form={form} copy={copy} imageUrl={imageUrl} editing={false} embed mode={mode} role={role} brandSlug={brandSlug} />
        </div>
      </div>
    </div>
  );
};

function ctdFitArea(el) {
  if (!el) return;
  el.style.height = 'auto';
  const border = el.offsetHeight - el.clientHeight;
  el.style.height = (el.scrollHeight + border) + 'px';
}

const CtdInternalLineField = ({ label, hint, html, onHtml, maxLines, maxWords, pitchWords, editing }) => {
  const text = ctdHtmlToLines(html);
  const stats = ctdLineStats(text, maxLines, maxWords, pitchWords);
  const bad = stats.overLines || stats.overWords;
  const areaRef = React.useRef(null);
  React.useLayoutEffect(() => { ctdFitArea(areaRef.current); }, [text]);
  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
        <label style={{ fontSize: 13, fontWeight: 600, color: '#112954' }}>{label}</label>
        <span style={{ fontSize: 11, color: bad ? '#EF4444' : '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>{stats.n} / {maxLines} linhas · ≤{maxWords} pal.</span>
      </div>
      {hint && <div style={{ fontSize: 11, color: '#94a3b8', margin: '2px 0 6px' }}>{hint}</div>}
      <textarea ref={areaRef} value={text} disabled={!editing} rows={1}
        onChange={e => { onHtml(ctdLinesToHtml(e.target.value)); ctdFitArea(e.target); }}
        style={{ width: '100%', boxSizing: 'border-box', border: '1px solid ' + (bad ? '#FECACA' : '#E3E6EB'), borderRadius: 8, padding: '10px 12px', fontSize: 13, lineHeight: 1.5, color: '#112954', fontFamily: 'Inter, sans-serif', resize: 'none', overflow: 'hidden', background: '#fff', outline: 'none' }} />
    </div>
  );
};

const CtdInternalCopyForm = ({ form, copy, imageUrl, editing, onField, onPayload, brandSlug }) => {
  const [openPreview, setOpenPreview] = React.useState(false);
  const [role, setRole] = React.useState('comercial');
  const p = form?.payload || {};
  const setHeadline = (v) => { onField?.('headline', v); onPayload?.('email_subject', v); };
  const roleTitle = ({
    comercial: 'No Daily Prospect', sat: 'Suporte técnico', board: 'Resumo executivo',
    operacoes: 'Operações', marketing: 'Marketing', rh_finance: 'RH & Financeiro', default: 'Informação',
  })[role];
  const roleCap = role === 'comercial' ? { maxLines: 6, maxWords: 40 } : { maxLines: 2, maxWords: 40 };
  const Count = ({ n, max, bad }) => (
    <span style={{ fontSize: 11, color: bad ? '#EF4444' : '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>{n} / {max}</span>
  );
  const inputStyle = { width: '100%', boxSizing: 'border-box', border: '1px solid #E3E6EB', borderRadius: 8, padding: '10px 12px', fontSize: 13, color: '#112954', fontFamily: 'Inter, sans-serif', background: editing ? '#fff' : '#FAFAFA', outline: 'none' };
  return (
    <div style={{ background: '#fff', border: '1px solid #ECEFF5', borderRadius: 10, padding: '16px 18px 8px' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, marginBottom: 16 }}>
        <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.08em', color: '#3859D0', fontFamily: 'var(--font-mono, monospace)' }}>COPY · COMUNICAÇÃO INTERNA</div>
        <button type="button" onClick={() => setOpenPreview(true)}
          style={{ border: '1px solid #3859D0', background: '#fff', color: '#3859D0', borderRadius: 6, padding: '6px 12px', fontSize: 12, fontWeight: 700, cursor: 'pointer' }}>
          Pré-visualizar
        </button>
      </div>
      <div style={{ marginBottom: 14 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}><label style={{ fontSize: 13, fontWeight: 600, color: '#112954' }}>Assunto</label><Count n={(form?.headline || '').length} max={80} bad={(form?.headline || '').length > 80} /></div>
        <input value={form?.headline || ''} disabled={!editing} onChange={e => setHeadline(e.target.value)} style={inputStyle} />
      </div>
      <div style={{ marginBottom: 14 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}><label style={{ fontSize: 13, fontWeight: 600, color: '#112954' }}>Preheader</label><Count n={(p.preheader || '').length} max={85} bad={(p.preheader || '').length > 85} /></div>
        <input value={p.preheader || ''} disabled={!editing} onChange={e => onPayload?.('preheader', e.target.value)} style={inputStyle} />
      </div>
      <div style={{ marginBottom: 14 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}><label style={{ fontSize: 13, fontWeight: 600, color: '#112954' }}>Título</label><Count n={(p.campaign_title || '').length} max={50} bad={(p.campaign_title || '').length > 50} /></div>
        <input value={p.campaign_title || ''} disabled={!editing} onChange={e => onPayload?.('campaign_title', e.target.value)} style={inputStyle} />
      </div>
      <div style={{ marginBottom: 14 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}><label style={{ fontSize: 13, fontWeight: 600, color: '#112954' }}>Tagline</label><Count n={(p.campaign_tagline || '').length} max={80} bad={(p.campaign_tagline || '').length > 80} /></div>
        <input value={p.campaign_tagline || ''} disabled={!editing} onChange={e => onPayload?.('campaign_tagline', e.target.value)} style={inputStyle} />
      </div>
      <CtdInternalLineField label="Elevator pitch" hint="Cumprimento conforme a hora, a campanha, o dia e o pitch" html={editing ? p.resumo_html : ctdComposeInternalLead(p.resumo_html, { brand: (ctdBrandPage(brandSlug).name === 'Digidelta Internacional' ? '' : ctdBrandPage(brandSlug).name), headline: form?.headline, title: p.campaign_title, preheader: p.preheader, tagline: p.campaign_tagline })} onHtml={v => onPayload?.('resumo_html', v)} maxLines={6} maxWords={110} editing={editing} />
      <CtdInternalLineField label="Oferta" hint="O que propões na reunião" html={p.objectivos_html} onHtml={v => onPayload?.('objectivos_html', v)} maxLines={4} maxWords={50} editing={editing} />
      <CtdInternalLineField label="Objeções" hint="O que te dizem e o que respondes" html={p.canais_html} onHtml={v => onPayload?.('canais_html', v)} maxLines={6} maxWords={40} editing={editing} />
      <div style={{ marginBottom: 8 }}>
        <div style={{ fontSize: 13, fontWeight: 600, color: '#112954', marginBottom: 8 }}>Bloco por função</div>
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 8 }}>
          {CTD_INTERNAL_ROLES.map(r => (
            <button key={r.id} type="button" onClick={() => setRole(r.id)}
              style={{ border: '1px solid ' + (role === r.id ? '#3859D0' : '#E3E6EB'), background: role === r.id ? '#EFF3FF' : '#fff', color: role === r.id ? '#3859D0' : '#475569', borderRadius: 6, padding: '4px 10px', fontSize: 11, fontWeight: 700, cursor: 'pointer' }}>
              {r.label}
            </button>
          ))}
        </div>
        <CtdInternalLineField key={role} label={roleTitle} html={p['role_' + role + '_html']} onHtml={v => onPayload?.('role_' + role + '_html', v)} maxLines={roleCap.maxLines} maxWords={roleCap.maxWords} editing={editing} />
      </div>
      {openPreview && (
        <CtdInternalPreviewModal form={form} copy={copy} imageUrl={imageUrl} onField={onField} onPayload={onPayload} brandSlug={brandSlug} onClose={() => setOpenPreview(false)} />
      )}
    </div>
  );
};

const CtdInternalContextCard = ({ campFull }) => {
  const brief = campFull?.briefing || {};
  let offer = campFull?.commercial_offer || brief.commercial_offer || null;
  if (typeof offer === 'string') { try { offer = JSON.parse(offer); } catch { offer = null; } }
  const mech = ({ digirent: 'Digirent', printplan: 'PrintPlan', voucher: 'Voucher on Demand', direct_discount: 'Desconto Directo', trade_in: 'Trade-In', testing_roll: 'Testing Roll', sample_request: 'Amostra' })[offer?.type] || offer?.type || '';
  const tierLabel = ({ with_deposit: 'Com caução', free: 'Gratuito', standard: 'Standard', premium: 'Premium', large: 'Large', enterprise: 'Enterprise', tailor_made: 'Tailor-made' })[offer?.tier] || (offer?.tier ? String(offer.tier).replace(/_/g, ' ') : '');
  const cta = ({ testing_roll: 'Pedir Testing Roll', sample_request: 'Pedir amostra', quote_request: 'Pedido de proposta', demo_request: 'Pedido de demonstração', call_back: 'Callback comercial', visit_showroom: 'Visita ao showroom', whatsapp: 'WhatsApp', form: 'Formulário', call: 'Chamada directa', landing: 'Landing page' })[offer?.primary_cta] || (offer?.primary_cta ? String(offer.primary_cta).replace(/_/g, ' ') : '');
  const triggers = ctdPlainItems(brief.purchase_trigger);
  const objections = ctdPlainItems(brief.objections);
  const kpis = ctdPlainItems(brief.kpis);
  const Row = ({ k, v }) => v ? (
    <div>
      <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.08em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', marginBottom: 3 }}>{k}</div>
      <CtdClampText text={v} lines={4} />
    </div>
  ) : null;
  return (
    <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
      <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)', fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>
        Ficha para o comercial
      </div>
      <div style={{ padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 12 }}>
        <div style={{ borderLeft: '3px solid #3859D0', paddingLeft: 10 }}>
          <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.08em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', marginBottom: 4 }}>Elevator pitch</div>
          {brief.elevator_pitch
            ? <CtdClampText text={ctdPlainText(brief.elevator_pitch)} lines={4} />
            : <div style={{ fontSize: 12, color: '#94a3b8' }}>Sem elevator pitch no briefing.</div>}
        </div>
        <div style={{ borderLeft: '3px solid #94a3b8', paddingLeft: 10 }}>
          <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.08em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', marginBottom: 4 }}>Oferta</div>
          {mech ? (
            <div style={{ fontSize: 12, lineHeight: 1.45, color: '#112954' }}>
              <b>{mech}</b>{tierLabel ? ` · ${tierLabel}` : ''}{offer.negotiable ? ' · negociável' : ''}
              {offer.details ? <div style={{ marginTop: 4 }}>{ctdPlainText(offer.details)}</div> : null}
              {cta ? <div style={{ marginTop: 4, fontWeight: 600 }}>CTA · {cta}</div> : null}
            </div>
          ) : <div style={{ fontSize: 12, color: '#94a3b8' }}>Sem oferta específica nesta campanha.</div>}
        </div>
        <Row k="Decisor" v={ctdPlainText(brief.decision_maker)} />
        <Row k="Gatilho" v={triggers.join('\n')} />
        <Row k="Objecção" v={objections.join('\n')} />
        <Row k="KPI" v={kpis.join('\n')} />
      </div>
    </div>
  );
};

// ─── CtdCopyPecaCard — card editável por peça (conteudo_copy schema) ──────────
function ctdPayloadOf(row) {
  const p = row?.payload;
  if (p && typeof p === 'object') return p;
  if (typeof p === 'string') { try { return JSON.parse(p); } catch { return {}; } }
  return {};
}
function ctdCopyEditSig(row) {
  const p = ctdPayloadOf(row);
  return JSON.stringify({
    h: row?.headline || '',
    cta: row?.cta || '',
    img: row?.copy_imagem || '',
    sub: row?.copy_imagem_sub || '',
    icta: row?.copy_imagem_cta || '',
    cap: row?.caption_organica || '',
    primary: p.primary_text_full || '',
    desc: p.description || '',
    cl: p.cta_label || '',
  });
}
const CtdCopyPecaCard = React.forwardRef(({ campanhaId, copy, userEmail, reloadCopies, staleIds, masterCopy, onOpenDiff, onOpenHistory, onOpenGenerate, hideActions, hideMockup, onDirtyChange, onFormChange, imageUrl, brandSlug, fieldsLocked, onBeforeSave, onCopySaved }, ref) => {
  const Icon = window.Icon;
  const [editing, setEditing]     = React.useState(!!hideActions && !fieldsLocked);
  const formEditing = fieldsLocked ? false : editing;
  React.useEffect(() => {
    if (hideActions) setEditing(!fieldsLocked);
  }, [fieldsLocked, hideActions, copy?.id]);
  const [saving, setSaving]       = React.useState(false);
  const [savedFlash, setSavedFlash] = React.useState(false);
  const [approving, setApprove]   = React.useState(false);
  const [regenerating, setRegen]  = React.useState(false);
  const [hover, setHover]         = React.useState(false);
  const isStale = staleIds && staleIds.includes(copy.id);
  const isMaster = masterCopy && masterCopy.id === copy.id;

  // conteudo_copy schema: content_type + canal determinam campos visíveis
  const contentType = copy.content_type || '';
  const canal       = copy.canal || '';
  const isInternalCopy = contentType === 'briefing_interno' || contentType === 'comunicacao_interna'
    || String(canal).includes('interno') || String(canal).includes('portal_notificacao') || canal === 'comunicacao_interna';
  const isExternalEmail = !isInternalCopy && (contentType === 'email_html' || canal === 'email');
  const isSocial   = !isInternalCopy && !isExternalEmail && (contentType === 'social_post' || ['meta_ads','instagram','facebook','linkedin','linkedin_ads'].includes(canal));
  const isEmail    = isExternalEmail;
  const isWa       = !isInternalCopy && (contentType === 'wa_template' || contentType === 'wa_message' || canal === 'whatsapp');
  const isBlog     = !isInternalCopy && (contentType === 'blog_post' || canal === 'site' || canal === 'website');
  const isLanding  = !isInternalCopy && contentType === 'landing_page';

  // Local editable state — mapeado ao schema conteudo_copy
  const [previewMode, setPreviewMode] = React.useState('desktop'); // desktop | mobile
  const [previewRole, setPreviewRole] = React.useState('comercial');
  const [adFormat, setAdFormat] = React.useState('feed'); // feed | stories // comercial | sat | board | operacoes | marketing | rh_finance | default
  const _isMetaAds = canal === 'meta_ads' || contentType === 'meta_ads';
  const _payloadInit = (() => {
    if (!isInternalCopy && !isExternalEmail && !_isMetaAds) return {};
    let parsed = {};
    if (copy.payload && typeof copy.payload === 'object') parsed = copy.payload;
    else try { parsed = JSON.parse(copy.payload || '{}'); } catch { parsed = {}; }

    // Auto-parsear body → payload se resumo_html vazio e body tem conteúdo
    if (isInternalCopy && !parsed.resumo_html && copy.body) {
      const body = copy.body;
      const LABEL_RE = /\b(RESUMO|OBJECTIVOS|CANAIS ACTIVOS)\b/g;
      const matches = [...body.matchAll(LABEL_RE)];
      if (matches.length > 0) {
        for (let i = 0; i < matches.length; i++) {
          const label = matches[i][1];
          const start = matches[i].index + matches[i][0].length;
          const end = i + 1 < matches.length ? matches[i + 1].index : body.length;
          const content = body.slice(start, end).trim();
          if (label === 'RESUMO') parsed.resumo_html = content;
          else if (label === 'OBJECTIVOS') parsed.objectivos_html = content;
          else if (label === 'CANAIS ACTIVOS') {
            const roleIdx = content.search(/<p[^>]*>\s*(No teu|Para o|Âncora:|Boas\s)/i);
            if (roleIdx !== -1) {
              parsed.canais_html = content.slice(0, roleIdx).trim();
              parsed.role_comercial_html = content.slice(roleIdx).trim();
            } else { parsed.canais_html = content; }
          }
        }
      } else {
        // Sem labels → body inteiro no resumo
        parsed.resumo_html = body;
      }
    }
    return parsed;
  })();
  const initialForm = {
    copy_imagem:     copy.copy_imagem     || '',
    copy_imagem_sub: copy.copy_imagem_sub || '',
    copy_imagem_cta: copy.copy_imagem_cta || '',
    headline:        copy.headline        || '',
    body:            copy.body            || '',
    caption_organica: copy.caption_organica || '',
    cta:             copy.cta             || '',
    hashtags:        copy.hashtags        || '',
    payload: _payloadInit,
    ...(isWa ? ctdWaFormFields(copy) : {}),
  };
  const [form, setForm] = React.useState(initialForm);
  const metaPrimary = (form.payload && Object.prototype.hasOwnProperty.call(form.payload, 'primary_text_full'))
    ? String(form.payload.primary_text_full ?? '')
    : String(form.caption_organica || '');
  const unsavedFor = React.useRef(null);
  const pendingSnap = React.useRef(null);
  const flashTimer = React.useRef(null);
  const copySig = ctdCopyEditSig(copy) + '|' + (copy?.id || '');
  // Não repor o formulário enquanto a edição ainda não chegou às props.
  // Um GET antigo (ou um payload com nova referência e o texto velho) apagava o que o utilizador tinha escrito.
  React.useEffect(() => {
    if (unsavedFor.current && unsavedFor.current === copy?.id) {
      if (!pendingSnap.current || pendingSnap.current !== ctdCopyEditSig(copy)) return;
    }
    unsavedFor.current = null;
    pendingSnap.current = null;
    setForm(initialForm);
  }, [copySig]); // eslint-disable-line react-hooks/exhaustive-deps
  React.useEffect(() => { onFormChange?.(form); }, [form]);

  const editRev = React.useRef(0);
  const markDirty = () => { unsavedFor.current = copy?.id || null; pendingSnap.current = null; editRev.current += 1; };
  const handlePayloadChange = (field, value) => { markDirty(); setForm(prev => ({ ...prev, payload: { ...prev.payload, [field]: value } })); };

  const isApproved = copy.status === 'aprovado';
  const outlineColor = isApproved ? '#1F8A52' : 'var(--border-1, #ECEFF5)';
  const outlineWidth = isApproved ? 2 : 1;

  const handleFieldChange = (field, value) => { markDirty(); setForm(prev => ({ ...prev, [field]: value })); };

  const handleSave = async () => {
    const revAtSave = editRev.current;
    setSaving(true);
    try {
      if (onBeforeSave) {
        try { await onBeforeSave(); }
        catch (e) { alert('A ficha não gravou: ' + (e.message || 'erro')); }
      }
      const patchBody = {};
      const fields = ['copy_imagem','copy_imagem_sub','copy_imagem_cta','headline','body','caption_organica','cta','hashtags'];
      for (const f of fields) {
        if (form[f] !== (copy[f] || '')) patchBody[f] = form[f];
      }
      if (_isMetaAds) {
        const full = String(form.payload?.primary_text_full ?? form.caption_organica ?? '');
        const hook = full.split(/\n+/)[0].trim().slice(0, 125);
        patchBody.headline = form.headline || '';
        patchBody.cta = form.cta || 'Saber mais';
        patchBody.copy_imagem = form.copy_imagem || '';
        patchBody.copy_imagem_sub = form.copy_imagem_sub || '';
        patchBody.copy_imagem_cta = form.copy_imagem_cta || '';
        patchBody.caption_organica = hook;
        patchBody.payload = {
          ...(form.payload || {}),
          primary_text_full: full,
          primary_text_hook: hook,
          description: form.payload?.description || '',
          cta_label: form.cta || form.payload?.cta_label || 'Saber mais',
        };
      } else if ((isInternalCopy || isExternalEmail) && form.payload) {
        patchBody.payload = isInternalCopy ? { ...form.payload, email_subject: form.headline || form.payload.email_subject || '' } : form.payload;
      }
      if (isWa) {
        patchBody.ad_specs = { ...ctdParseAdSpecs(copy), ...ctdWaAdSpecs(form) };
        patchBody.body = form.wa_body || '';
        patchBody.headline = form.wa_name || '';
        patchBody.cta = (form.wa_buttons || [])[0]?.text || '';
      }
      if (Object.keys(patchBody).length > 0) {
        const saved = await CtdAPI.editCopy(campanhaId, copy.id, patchBody);
        if (editRev.current !== revAtSave) {
          reloadCopies?.();
        } else {
          pendingSnap.current = ctdCopyEditSig(saved);
          onCopySaved?.(saved);
          reloadCopies?.();
          setSavedFlash(true);
          clearTimeout(flashTimer.current);
          flashTimer.current = setTimeout(() => setSavedFlash(false), 1800);
        }
      }
      if (!hideActions) setEditing(false);
    } finally {
      setSaving(false);
    }
  };

  const handleApprove = async () => {
    if (isWa) {
      const blockers = ctdWaBlockers(ctdWaPackFromForm(form));
      if (blockers.length) throw new Error(blockers[0]);
    }
    setApprove(true);
    try {
      await CtdAPI.approveCopy(campanhaId, copy.id, { approver_email: userEmail });
      reloadCopies?.();
    } finally {
      setApprove(false);
    }
  };

  const handleRegenerate = async () => {
    if (!confirm('Regerar apenas esta peça? Sonnet vai gerar um novo ângulo mantendo invariantes.')) return;
    setRegen(true);
    try {
      await CtdAPI.regenerateCopy(copy.id);
      reloadCopies?.();
    } catch (e) { alert('Erro ao regerar: ' + e.message); }
    setRegen(false);
  };

  // Expor métodos ao pai (usado pelo CtdGenerateCopyItemModal quando hideActions=true)
  React.useImperativeHandle(ref, () => ({
    save: handleSave,
    approve: handleApprove,
    isSaving: () => saving,
    isApproving: () => approving,
    isApproved: () => copy.status === 'aprovado',
  }), [form, saving, approving, copy.status, onBeforeSave, onCopySaved]);

  // Keyboard shortcuts em edit mode: Enter (Cmd/Ctrl) → save, Esc → cancel
  const handleKeyDown = (e) => {
    if (!editing) return;
    if (e.key === 'Escape') { unsavedFor.current = null; pendingSnap.current = null; setEditing(false); setForm(initialForm); e.preventDefault(); }
    else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { handleSave().catch(err => alert('Erro ao guardar: ' + err.message)); e.preventDefault(); }
  };

  const isInternal = canal && (canal.includes('interno') || canal.includes('portal_notificacao'));
  const canalLabel = isInternal ? 'COMUNICAÇÃO INTERNA' : ({
    meta_ads: 'META ADS', instagram: 'INSTAGRAM', facebook: 'FACEBOOK',
    linkedin_ads: 'LINKEDIN', linkedin: 'LINKEDIN',
    email: 'EMAIL', whatsapp: 'WHATSAPP',
    google_ads_search: 'GOOGLE SEARCH', google_ads_display: 'GOOGLE DISPLAY',
    muppi_led: 'LED · MUPPI', site: 'SITE', website: 'SITE', led: 'LED',
  }[canal] || (canal || '').toUpperCase());
  const contentTypeLabel = {
    social_post: 'SOCIAL POST',
    email_html: 'EMAIL',
    wa_template: 'WA TEMPLATE',
    wa_message: 'WA MESSAGE',
    blog_post: 'BLOG',
    landing_page: 'LANDING',
  }[contentType] || contentType.toUpperCase();

  const inputStyle = {
    width: '100%', padding: '8px 10px', fontSize: 12.5,
    border: '1px solid var(--border-2, #E3E6EB)',
    borderRadius: 'var(--radius-sm, 6px)',
    background: editing ? 'var(--bg-surface, #fff)' : 'var(--bg-subtle, #FAFAFA)',
    color: 'var(--text, #112954)',
    fontFamily: 'var(--font-body, inherit)',
    outline: 'none',
    transition: 'border-color 120ms',
    resize: 'vertical',
  };
  const inputFocus = (e) => { e.currentTarget.style.borderColor = 'var(--border-focus, #61A9FF)'; };
  const inputBlur  = (e) => { e.currentTarget.style.borderColor = 'var(--border-2, #E3E6EB)'; };

  const FieldGroup = ({ title, children }) => (
    <div style={{ marginBottom: 14 }}>
      <div style={{
        fontSize: 9, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase',
        color: 'var(--text-dim, #7a8fa8)', fontFamily: 'var(--font-mono, monospace)',
        marginBottom: 6,
      }}>{title}</div>
      {children}
    </div>
  );
  const Field = ({ label, field, multi = false, rows = 2, maxLen, value: extValue, onChange: extOnChange }) => {
    const val = extValue !== undefined ? extValue : (form[field] || '');
    const handleChange = extOnChange ? extOnChange : (v) => handleFieldChange(field, v);
    return (
    <div style={{ marginBottom: 8 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 3 }}>
        <label style={{ fontSize: 10.5, color: 'var(--text-muted, #4a5e7a)', fontWeight: 600 }}>{label}</label>
        {maxLen && <span style={{ fontSize: 9, color: 'var(--text-dim, #7a8fa8)', fontFamily: 'var(--font-mono, monospace)' }}>{val.length}/{maxLen}</span>}
      </div>
      {multi ? (
        <textarea
          ref={(el) => {
            if (!el) return;
            el.style.height = 'auto';
            el.style.height = el.scrollHeight + 'px';
          }}
          value={val} onChange={(e) => {
            handleChange(e.target.value);
            e.target.style.height = 'auto';
            e.target.style.height = e.target.scrollHeight + 'px';
          }}
          disabled={!editing} rows={rows} maxLength={maxLen}
          style={{ ...inputStyle, overflow: 'hidden', resize: 'none', minHeight: `${rows * 1.5}em` }}
          onFocus={inputFocus} onBlur={inputBlur}
        />
      ) : (
        <input
          value={val} onChange={(e) => handleChange(e.target.value)}
          disabled={!editing} maxLength={maxLen}
          style={inputStyle} onFocus={inputFocus} onBlur={inputBlur}
        />
      )}
    </div>
    );
  };

  return (
    <div
      id={`copy-card-${copy.id}`}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      style={{
        background: (hideMockup && isInternalCopy) ? 'transparent' : 'var(--bg-surface, #fff)',
        border: (hideMockup && isInternalCopy) ? 'none' : `${outlineWidth}px solid ${outlineColor}`,
        borderRadius: 'var(--radius-lg, 10px)',
        padding: (hideMockup && isInternalCopy) ? 0 : (hideMockup ? 16 : 18), marginBottom: hideMockup ? 0 : 14,
        boxShadow: hideActions ? 'none' : (hover ? 'var(--shadow-regular, 0 4px 8px rgba(40,41,61,0.04), 0 8px 16px rgba(96,97,112,0.10))' : 'var(--shadow-card, 0 1px 4px rgba(40,41,61,0.04), 0 4px 16px rgba(96,97,112,0.08))'),
        transition: 'box-shadow 180ms, border-color 180ms',
      }}
    >
      {/* Header — no email interno inline o mockup é o editor */}
      {!(hideMockup && isInternalCopy) && <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: hideMockup ? 8 : 12, gap: 12 }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4, flexWrap: 'wrap' }}>
            <span style={{
              fontSize: 9, fontWeight: 700, letterSpacing: '0.08em', whiteSpace: 'nowrap',
              color: 'var(--color-primary, #3859D0)', fontFamily: 'var(--font-mono, monospace)',
            }}>{contentTypeLabel || canalLabel}</span>
            {canalLabel && canalLabel !== contentTypeLabel && (
              <span style={{
                fontSize: 9, padding: '2px 6px', background: 'var(--bg-selected, #ECF1FB)',
                borderRadius: 'var(--radius-xs, 4px)', color: 'var(--text-muted, #4a5e7a)',
                fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.04em', whiteSpace: 'nowrap',
              }}>{canalLabel}</span>
            )}
            {copy.formato && (
              <span style={{
                fontSize: 9, padding: '2px 6px', background: 'var(--bg-selected, #ECF1FB)',
                borderRadius: 'var(--radius-xs, 4px)', color: 'var(--text-muted, #4a5e7a)',
                fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.04em', whiteSpace: 'nowrap',
              }}>{copy.formato}</span>
            )}
            <span style={{
              fontSize: 9, padding: '2px 6px', background: 'var(--bg-subtle, #FAFAFA)',
              borderRadius: 'var(--radius-xs, 4px)', color: 'var(--text-dim, #7a8fa8)',
              fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.04em', whiteSpace: 'nowrap',
            }}>{(copy.lingua || 'pt').toUpperCase()}{copy.country ? ` · ${copy.country}` : ''}</span>
            {copy.copy_type && copy.copy_type !== 'organico' && (
              <span style={{
                fontSize: 9, padding: '2px 6px', background: '#FEF3C7',
                borderRadius: 'var(--radius-xs, 4px)', color: '#92400E',
                fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.04em',
              }}>{copy.copy_type.toUpperCase()}</span>
            )}
          </div>
          {!hideMockup && (
          <div style={{
            fontSize: 14, fontWeight: 700, color: 'var(--text, #112954)',
            fontFamily: 'var(--font-display, Montserrat, sans-serif)',
            lineHeight: 1.35, marginBottom: 6,
          }}>{copy.item_title || `Peça #${(copy.comm_plan_item_index != null ? copy.comm_plan_item_index + 1 : '?')}`}</div>
          )}
        </div>

        {/* Status badges — omitido dentro do detail view (barra de acções já mostra) */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 4, alignItems: 'flex-end', flexShrink: 0 }}>
          {!hideActions && (isApproved ? (
            <span style={{
              fontSize: 10, fontWeight: 700, letterSpacing: '0.06em',
              padding: '4px 9px', borderRadius: 'var(--radius-xs, 4px)',
              background: '#1F8A52', color: '#fff',
              fontFamily: 'var(--font-mono, monospace)',
              display: 'inline-flex', alignItems: 'center', gap: 4,
              boxShadow: '0 1px 2px rgba(31,138,82,0.24)',
            }}>
              <Icon name="check" size={11} /> APROVADA
            </span>
          ) : (
            <span style={{
              fontSize: 10, fontWeight: 700, letterSpacing: '0.06em',
              padding: '4px 9px', borderRadius: 'var(--radius-xs, 4px)',
              background: '#FEF3C7', color: '#92400E',
              fontFamily: 'var(--font-mono, monospace)',
            }}>DRAFT</span>
          ))}
          {isStale && (
            <span title="Master PT foi actualizado depois desta versão — considera regerar" style={{
              fontSize: 10, fontWeight: 700, letterSpacing: '0.06em',
              padding: '4px 9px', borderRadius: 'var(--radius-xs, 4px)',
              background: '#FDE68A', color: '#78350F',
              fontFamily: 'var(--font-mono, monospace)',
              display: 'inline-flex', alignItems: 'center', gap: 4,
            }}>
              <Icon name="refresh" size={10} /> STALE
            </span>
          )}
          {isMaster && (
            <span style={{
              fontSize: 9, fontWeight: 700, letterSpacing: '0.06em',
              padding: '3px 7px', borderRadius: 'var(--radius-xs, 4px)',
              background: 'var(--bg-selected, #ECF1FB)', color: 'var(--color-primary, #3859D0)',
              fontFamily: 'var(--font-mono, monospace)',
            }}>MASTER</span>
          )}
        </div>
      </div>}

      {/* Image typography (só para tipos visuais — não aplicável a briefing interno/email/meta_ads/WhatsApp) */}
      {!isInternalCopy && !isEmail && !_isMetaAds && !isWa && (
        <FieldGroup title="Tipografia da Imagem (pixel-baked)">
          <Field label="Headline — 3-5 palavras UPPERCASE, ≤12 chars/linha" field="copy_imagem" maxLen={40} />
          <Field label="Sub-descriptor — 2 linhas, ≤10 palavras" field="copy_imagem_sub" multi rows={2} maxLen={120} />
          <Field label="Image CTA — 2-4 palavras + →" field="copy_imagem_cta" maxLen={40} />
        </FieldGroup>
      )}

      {/* ── EMAIL EXTERNO (email_html) — mockup Apple Mail outline + template real 600px ── */}
      {isExternalEmail ? (
        <div style={{ fontFamily:'Inter,sans-serif', border:'1px solid #D1D5DB', borderRadius:8, overflow:'hidden' }}>
          {/* Title bar outline — Apple Mail */}
          <div style={{ borderBottom:'1px solid #D1D5DB', padding:'6px 10px', display:'flex', alignItems:'center', gap:8, background:'transparent' }}>
            <div style={{ display:'flex', gap:5 }}>
              <div style={{ width:10, height:10, borderRadius:'50%', border:'1px solid #D1D5DB' }} />
              <div style={{ width:10, height:10, borderRadius:'50%', border:'1px solid #D1D5DB' }} />
              <div style={{ width:10, height:10, borderRadius:'50%', border:'1px solid #D1D5DB' }} />
            </div>
            <span style={{ fontSize:10, color:'#9CA3AF', fontWeight:500, margin:'0 auto' }}>Mail — Caixa de Entrada</span>
          </div>

          {/* Toolbar outline */}
          <div style={{ borderBottom:'1px solid #E5E7EB', padding:'8px 12px', display:'flex', gap:16, fontSize:10, color:'#9CA3AF' }}>
            <span>Responder</span>
            <span>Reencaminhar</span>
            <span>Apagar</span>
            <span style={{ marginLeft:'auto' }}>⋯</span>
          </div>

          {/* Body layout: sidebar outline + list outline + message pane */}
          <div style={{ display:'flex', minHeight:400 }}>
            {/* Sidebar (mailboxes) — outline */}
            <div style={{ width:90, borderRight:'1px solid #E5E7EB', padding:'10px 8px', display:'flex', flexDirection:'column', gap:6, fontSize:9, color:'#9CA3AF' }}>
              <div style={{ fontWeight:600, color:'#6B7280' }}>Entrada</div>
              <div>Enviados</div>
              <div>Rascunhos</div>
              <div>Assinalados</div>
              <div style={{ borderTop:'1px dashed #E5E7EB', margin:'4px 0' }} />
              <div style={{ fontSize:8, textTransform:'uppercase', letterSpacing:'0.06em' }}>Marcas</div>
              <div>· Mimaki</div>
              <div>· BIOND</div>
              <div>· Decal</div>
            </div>

            {/* Toggle Desktop/Mobile — no lugar do message list */}
            <div style={{ width:160, borderRight:'1px solid #E5E7EB', padding:'8px 8px', display:'flex', flexDirection:'column', gap:8 }}>
              <div style={{ fontSize:8, textTransform:'uppercase', letterSpacing:'0.08em', color:'#9CA3AF', fontWeight:700, padding:'2px 4px' }}>Pré-visualizar</div>
              {[
                { id:'desktop', label:'Desktop' },
                { id:'mobile',  label:'Mobile' },
              ].map(opt => {
                const active = previewMode === opt.id;
                return (
                  <button key={opt.id} onClick={()=>setPreviewMode(opt.id)}
                    style={{ border:'1px solid ' + (active?'#0F4FF9':'#E5E7EB'), borderRadius:4, padding:'8px 10px', display:'flex', flexDirection:'column', gap:4, alignItems:'flex-start', background: active?'rgba(15,79,249,0.04)':'transparent', cursor:'pointer', textAlign:'left' }}>
                    <div style={{ fontSize:11, fontWeight: active?700:600, color: active?'#111827':'#6B7280' }}>{opt.label}</div>
                    <div style={{ height:6, background: active?'#0F4FF9':'#E5E7EB', borderRadius:2, width:'90%', opacity: active?0.7:1 }} />
                    <div style={{ height:4, background: active?'#3859d0':'#F3F4F6', borderRadius:2, width:'60%', opacity: active?0.5:1 }} />
                  </button>
                );
              })}
            </div>

            {/* Message pane */}
            <div style={{ flex:1, background:'#E8ECF5', padding: previewMode==='mobile'?'12px 8px':'20px 16px', overflow:'auto', display:'flex', flexDirection:'column', alignItems:'center' }}>

              {previewMode==='mobile' ? (
                // ── MOBILE: iPhone frame com template completo ──
                <div style={{ width:335, border:'2px solid #C8CEDD', borderRadius:28, overflow:'hidden', background:'#F0F3FA', boxShadow:'0 6px 24px rgba(0,0,0,0.10)' }}>
                  {/* Status bar */}
                  <div style={{ background:'#fff', padding:'6px 18px', display:'flex', justifyContent:'space-between', alignItems:'center', fontSize:9, color:'#222', borderBottom:'1px solid #F0F0F0' }}>
                    <span style={{ fontWeight:700 }}>9:41</span>
                    <span style={{ letterSpacing:'0.25em', fontSize:8 }}>● ● ●</span>
                  </div>
                  {/* Mail nav */}
                  <div style={{ background:'#F7F7F7', padding:'5px 14px', borderBottom:'1px solid #E5E7EB', display:'flex', justifyContent:'space-between', fontSize:10, color:'#6B7280' }}>
                    <span>&lt; Entrada</span><span>{copy?.planned_date||'—'}</span>
                  </div>
                  {/* From / Subject */}
                  <div style={{ background:'#fff', padding:'8px 14px', borderBottom:'1px solid #F3F4F6' }}>
                    <div style={{ fontSize:11, fontWeight:700, color:'#111' }}>Marketing da Digidelta</div>
                    <input disabled={!editing} value={form.payload?.subject||''} onChange={e=>handlePayloadChange('subject',e.target.value)} placeholder="Assunto…"
                      style={{ border:'none', borderBottom:editing?'1px dashed #9CA3AF':'none', background:'transparent', fontSize:11, fontWeight:600, color:'#374151', outline:'none', cursor:editing?'text':'default', padding:'2px 0', width:'100%' }} />
                    <div style={{ fontSize:9, color:'#9CA3AF', marginTop:1 }}>{form.payload?.preheader||'…'}</div>
                  </div>
                  {/* Email body — Mimaki template mobile */}
                  <div style={{ overflowY:'auto', maxHeight:500, background:'#F0F3FA' }}>
                    {/* Header Mimaki vermelho */}
                    <div style={{ background:'radial-gradient(ellipse at 55% 35%, #C41230 0%, #7B0A1F 45%, #2D0008 100%)', height:130, display:'flex', alignItems:'center', justifyContent:'center' }}>
                      <img src="https://pub-ba73e5f75b434dcb9e0cc769a1082ab2.r2.dev/brand-logos/mimaki/mimaki-light.svg" alt="Mimaki" style={{ height:30, width:'auto', display:'block' }} />
                    </div>
                    {/* Card branco com overlap */}
                    <div style={{ background:'#fff', borderRadius:12, marginTop:-40, marginLeft:12, marginRight:12, position:'relative', zIndex:1, boxShadow:'0 4px 16px rgba(17,41,84,0.10)', padding:'24px 20px 20px' }}>
                      <div style={{ background:'#EEF2F9', borderRadius:4, height:80, display:'flex', alignItems:'center', justifyContent:'center', marginBottom:14 }}>
                        <span style={{ fontSize:9, color:'#9DAFC8', letterSpacing:'0.06em', textTransform:'uppercase' }}>HERO · 1.91:1</span>
                      </div>
                      <p style={{ margin:'0 0 10px', fontSize:13, fontWeight:600, color:'#112954', fontFamily:'Inter,sans-serif' }}>Cliente,</p>
                      <div style={{ fontSize:13, lineHeight:1.7, color:'#1C1C2E', fontFamily:'Inter,sans-serif', whiteSpace:'pre-wrap', marginBottom:16 }}>
                        {form.payload?.body || React.createElement('span', { style:{ color:'#9CA3AF', fontStyle:'italic' } }, 'Corpo do email…')}
                      </div>
                      <div style={{ textAlign:'center', marginBottom:14 }}>
                        <div style={{ display:'inline-block', background:'#0F4FF9', borderRadius:5, padding:'10px 22px', fontSize:13, color:'#fff', fontWeight:700, fontFamily:'Inter,sans-serif' }}>
                          {form.payload?.cta_label || 'CTA'}
                        </div>
                      </div>
                      {form.payload?.ps_line && (
                        <div style={{ borderTop:'1px solid #EDF0F7', paddingTop:10, fontSize:12, color:'#5A6A8A', fontStyle:'italic', fontFamily:'Inter,sans-serif' }}>{form.payload.ps_line}</div>
                      )}
                    </div>
                    <div style={{ padding:'16px 20px', textAlign:'center' }}>
                      <div style={{ fontSize:10, fontWeight:600, color:'#112954', fontFamily:'Inter,sans-serif' }}>{form.payload?.sender_name||'Equipa Mimaki · Digidelta'}</div>
                      <div style={{ fontSize:10, color:'#1C1C2E', marginTop:6, fontFamily:'Inter,sans-serif' }}>Cancelar subscrição · Privacidade</div>
                    </div>
                  </div>
                </div>
              ) : (
              // ── DESKTOP: from/subject + template real 600px ──
              <>
              {/* From / Assunto / Preview */}
              <div style={{ width:'100%', maxWidth:600, marginBottom:12, padding:'8px 12px', borderBottom:'1px solid #E5E7EB', background:'transparent' }}>
                <div style={{ display:'flex', alignItems:'center', gap:6, marginBottom:3 }}>
                  <span style={{ fontSize:9, color:'#9CA3AF', width:56, flexShrink:0, textAlign:'right' }}>De</span>
                  <span style={{ fontSize:11, fontWeight:600, color:'#374151' }}>Marketing da Digidelta &lt;marketing@digidelta.pt&gt;</span>
                </div>
                <div style={{ display:'flex', alignItems:'center', gap:6, marginBottom:3 }}>
                  <span style={{ fontSize:9, color:'#9CA3AF', width:56, flexShrink:0, textAlign:'right' }}>Assunto</span>
                  <input disabled={!editing} value={form.payload?.subject||''} onChange={e=>handlePayloadChange('subject',e.target.value)} placeholder="Subject line…" maxLength={60}
                    style={{ flex:1, border:'none', borderBottom:editing?'1px dashed #6B7280':'none', background:'transparent', fontSize:12, fontWeight:700, color:'#111827', outline:'none', cursor:editing?'text':'default', padding:'1px 0' }} />
                  <span style={{ fontSize:9, color:(form.payload?.subject||'').length>60?'#EF4444':'#9CA3AF', fontFamily:'monospace', flexShrink:0 }}>{(form.payload?.subject||'').length}/60</span>
                </div>
                <div style={{ display:'flex', alignItems:'center', gap:6 }}>
                  <span style={{ fontSize:9, color:'#9CA3AF', width:56, flexShrink:0, textAlign:'right' }}>Preview</span>
                  <input disabled={!editing} value={form.payload?.preheader||''} onChange={e=>handlePayloadChange('preheader',e.target.value)} placeholder="Preheader inbox…" maxLength={85}
                    style={{ flex:1, border:'none', borderBottom:editing?'1px dashed #D1D5DB':'none', background:'transparent', fontSize:10, color:'#6B7280', outline:'none', cursor:editing?'text':'default', padding:'1px 0' }} />
                  <span style={{ fontSize:9, color:(form.payload?.preheader||'').length>85?'#EF4444':'#9CA3AF', fontFamily:'monospace', flexShrink:0 }}>{(form.payload?.preheader||'').length}/85</span>
                </div>
              </div>

              {/* ── TEMPLATE REAL 600px (mimaki-clientes-v1) ── */}
              <div style={{ width:'100%', maxWidth:600, background:'#F0F3FA' }}>
                {/* Header — fundo vermelho Mimaki */}
                <div style={{ background:'radial-gradient(ellipse at 55% 35%, #C41230 0%, #7B0A1F 45%, #2D0008 100%)', height:200, display:'flex', alignItems:'center', justifyContent:'center' }}>
                  <img src="https://pub-ba73e5f75b434dcb9e0cc769a1082ab2.r2.dev/brand-logos/mimaki/mimaki-light.svg" alt="Mimaki" style={{ height:44, width:'auto', display:'block' }} />
                </div>
                {/* Main card — overlap + border-radius 16px */}
                <div style={{ background:'#fff', borderRadius:16, marginTop:-60, marginLeft:20, marginRight:20, position:'relative', zIndex:1, boxShadow:'0 4px 24px rgba(17,41,84,0.08)', padding:'40px 40px 0 40px' }}>
                  {/* Hero */}
                  <div style={{ background:'#EEF2F9', borderRadius:4, height:210, display:'flex', alignItems:'center', justifyContent:'center', marginBottom:28 }}>
                    <span style={{ fontSize:11, color:'#9DAFC8', letterSpacing:'0.08em', textTransform:'uppercase', fontFamily:'Inter,sans-serif' }}>HERO IMAGE · 1.91:1</span>
                  </div>
                  {/* Greeting */}
                  <p style={{ margin:'0 0 14px', fontSize:16, fontWeight:600, color:'#112954', fontFamily:'Inter,sans-serif' }}>Cliente,</p>
                  {/* Body */}
                  <textarea disabled={!editing} value={form.payload?.body||''} rows={6}
                    onChange={e=>{handlePayloadChange('body',e.target.value); e.target.style.height='auto'; e.target.style.height=e.target.scrollHeight+'px';}}
                    ref={el=>{if(el&&!el._inited){el._inited=true; el.style.height='auto'; el.style.height=el.scrollHeight+'px';}}}
                    placeholder="Corpo do email — 90-105 palavras…"
                    style={{ width:'100%', border:editing?'1px dashed #D1D5DB':'none', borderRadius:4, background:'transparent', fontSize:16, lineHeight:1.75, color:'#1C1C2E', resize:'none', outline:'none', padding:editing?'6px':'0', cursor:editing?'text':'default', boxSizing:'border-box', overflow:'hidden', fontFamily:'Inter,sans-serif' }} />
                  <div style={{ fontSize:9, textAlign:'right', fontFamily:'monospace', marginTop:4, marginBottom:28, color: (form.payload?.body||'').trim().split(/\s+/).filter(Boolean).length < 85 ? '#f59e0b' : (form.payload?.body||'').trim().split(/\s+/).filter(Boolean).length > 115 ? '#dc2626' : '#22c55e' }}>
                    {(form.payload?.body||'').trim().split(/\s+/).filter(Boolean).length} palavras · alvo 90-105
                  </div>
                  {/* CTA */}
                  <div style={{ textAlign:'center', marginBottom:44 }}>
                    <div style={{ display:'inline-flex', flexDirection:'column', alignItems:'center', gap:8 }}>
                      <div style={{ background:'#0F4FF9', borderRadius:5, padding:'15px 36px' }}>
                        <input disabled={!editing} value={form.payload?.cta_label||''} onChange={e=>handlePayloadChange('cta_label',e.target.value)} placeholder="Texto do botão"
                          style={{ border:'none', background:'transparent', color:'#fff', fontWeight:700, fontSize:15, textAlign:'center', outline:'none', cursor:editing?'text':'default', minWidth:120, fontFamily:'Inter,sans-serif' }} />
                      </div>
                      <input disabled={!editing} value={form.payload?.cta_url||''} onChange={e=>handlePayloadChange('cta_url',e.target.value)} placeholder="https://…"
                        style={{ border:editing?'1px dashed #C4C8D0':'none', borderRadius:3, background:'transparent', fontSize:10, color:'#7A8AA8', padding:editing?'2px 6px':'1px 0', outline:'none', width:280, textAlign:'center', cursor:editing?'text':'default', fontFamily:'Inter,sans-serif' }} />
                    </div>
                  </div>
                  {/* P.S. */}
                  {(editing || form.payload?.ps_line) && (
                    <div style={{ borderTop:'1px solid #EDF0F7', padding:'20px 0 40px' }}>
                      <input disabled={!editing} value={form.payload?.ps_line||''} onChange={e=>handlePayloadChange('ps_line',e.target.value)} placeholder="P.S. (opcional)"
                        style={{ width:'100%', border:editing?'1px dashed #D1D5DB':'none', borderRadius:3, background:'transparent', fontSize:13, color:'#5A6A8A', fontStyle:'italic', padding:editing?'3px 6px':'2px 0', outline:'none', cursor:editing?'text':'default', boxSizing:'border-box', fontFamily:'Inter,sans-serif' }} />
                    </div>
                  )}
                </div>
                {/* Footer */}
                <div style={{ padding:'28px 48px 36px', textAlign:'center' }}>
                  <p style={{ margin:'0 0 2px', fontSize:13, fontWeight:600, color:'#112954', fontFamily:'Inter,sans-serif' }}>{form.payload?.sender_name||'Equipa Mimaki · Digidelta'}</p>
                  <p style={{ margin:'0 0 18px', fontSize:12, color:'#1C1C2E', fontFamily:'Inter,sans-serif' }}>{form.payload?.sender_title||'Especialistas em Impressão Digital'}</p>
                  <div style={{ borderTop:'1px solid #DDE3EF', paddingTop:14 }}>
                    <p style={{ margin:'0 0 3px', fontSize:11, color:'#1C1C2E', lineHeight:1.6, fontFamily:'Inter,sans-serif' }}>Digidelta Internacional · Rua da Igreja, 251 · 4485-849 Vairão · Portugal</p>
                    <p style={{ margin:0, fontSize:11, color:'#1C1C2E', fontFamily:'Inter,sans-serif' }}>Cancelar subscrição · Privacidade</p>
                  </div>
                </div>
              </div>
              </>)}

            </div>
          </div>
        </div>
      ) : isInternalCopy && hideMockup ? (
        <CtdInternalCopyForm
          form={form}
          copy={copy}
          imageUrl={imageUrl}
          editing={formEditing}
          onField={handleFieldChange}
          onPayload={handlePayloadChange}
          brandSlug={brandSlug}
        />
      ) : isInternalCopy ? (
        <div style={{ fontFamily:'Inter,sans-serif', border:'1px solid #D1D5DB', borderRadius:8, overflow:'hidden' }}>
          {/* Title bar Apple Mail */}
          <div style={{ borderBottom:'1px solid #D1D5DB', padding:'6px 10px', display:'flex', alignItems:'center', gap:8 }}>
            <div style={{ display:'flex', gap:5 }}>
              <div style={{ width:10, height:10, borderRadius:'50%', border:'1px solid #D1D5DB' }} />
              <div style={{ width:10, height:10, borderRadius:'50%', border:'1px solid #D1D5DB' }} />
              <div style={{ width:10, height:10, borderRadius:'50%', border:'1px solid #D1D5DB' }} />
            </div>
            <span style={{ fontSize:10, color:'#9CA3AF', fontWeight:500, margin:'0 auto' }}>Mail — Briefing Interno</span>
          </div>
          {/* Toolbar */}
          <div style={{ borderBottom:'1px solid #E5E7EB', padding:'8px 12px', display:'flex', gap:16, fontSize:10, color:'#9CA3AF' }}>
            <span>Responder</span><span>Reencaminhar</span><span>Apagar</span><span style={{ marginLeft:'auto' }}>⋯</span>
          </div>
          {/* Layout: sidebar (roles) + list (Desktop/Mobile toggle) + pane */}
          <div style={{ display:'flex', minHeight:400 }}>
            {/* Sidebar — role selector */}
            <div style={{ width:120, borderRight:'1px solid #E5E7EB', padding:'10px 8px', display:'flex', flexDirection:'column', gap:4, fontSize:10 }}>
              <div style={{ fontSize:8, textTransform:'uppercase', letterSpacing:'0.08em', color:'#9CA3AF', fontWeight:700, padding:'2px 4px', marginBottom:2 }}>Role</div>
              {[
                { id:'comercial', label:'Comercial' },
                { id:'sat', label:'SAT' },
                { id:'board', label:'Board' },
                { id:'operacoes', label:'Operações' },
                { id:'marketing', label:'Marketing' },
                { id:'rh_finance', label:'RH & Fin' },
                { id:'default', label:'Default' },
              ].map(r => {
                const on = previewRole === r.id;
                return (
                  <button key={r.id} onClick={()=>setPreviewRole(r.id)}
                    style={{ border:'1px solid ' + (on?'#0F4FF9':'transparent'), borderLeft:'3px solid ' + (on?'#0F4FF9':'transparent'), background: on?'rgba(15,79,249,0.04)':'transparent', padding:'5px 8px', fontSize:10, color: on?'#111827':'#6B7280', fontWeight: on?700:500, cursor:'pointer', textAlign:'left', borderRadius:3 }}>
                    {r.label}
                  </button>
                );
              })}
            </div>
            {/* Toggle Desktop/Mobile */}
            <div style={{ width:140, borderRight:'1px solid #E5E7EB', padding:'8px 8px', display:'flex', flexDirection:'column', gap:8 }}>
              <div style={{ fontSize:8, textTransform:'uppercase', letterSpacing:'0.08em', color:'#9CA3AF', fontWeight:700, padding:'2px 4px' }}>Pré-visualizar</div>
              {[{id:'desktop',label:'Desktop'},{id:'mobile',label:'Mobile'}].map(opt => {
                const active = previewMode === opt.id;
                return (
                  <button key={opt.id} onClick={()=>setPreviewMode(opt.id)}
                    style={{ border:'1px solid ' + (active?'#0F4FF9':'#E5E7EB'), borderRadius:4, padding:'8px 10px', display:'flex', flexDirection:'column', gap:4, alignItems:'flex-start', background: active?'rgba(15,79,249,0.04)':'transparent', cursor:'pointer', textAlign:'left' }}>
                    <div style={{ fontSize:11, fontWeight: active?700:600, color: active?'#111827':'#6B7280' }}>{opt.label}</div>
                    <div style={{ height:6, background: active?'#0F4FF9':'#E5E7EB', borderRadius:2, width:'90%', opacity: active?0.7:1 }} />
                    <div style={{ height:4, background: active?'#3859d0':'#F3F4F6', borderRadius:2, width:'60%', opacity: active?0.5:1 }} />
                  </button>
                );
              })}
            </div>
            {/* Message pane */}
            <div style={{ flex:1, background:'#F5F5F7', padding: previewMode==='mobile'?'16px 12px':'20px 16px', overflow:'auto', display:'flex', flexDirection:'column', alignItems:'center' }}>

              {previewMode==='mobile' ? (
                <div style={{ width:340, border:'2px solid #C8CEDD', borderRadius:28, overflow:'hidden', background:'#f9faff', boxShadow:'0 6px 24px rgba(0,0,0,0.10)' }}>
                  <div style={{ background:'#fff', padding:'6px 18px', display:'flex', justifyContent:'space-between', fontSize:9, color:'#222', borderBottom:'1px solid #F0F0F0' }}>
                    <span style={{ fontWeight:700 }}>9:41</span><span style={{ fontSize:8, letterSpacing:'0.25em' }}>● ● ●</span>
                  </div>
                  <div style={{ background:'#F7F7F7', padding:'5px 14px', borderBottom:'1px solid #E5E7EB', display:'flex', justifyContent:'space-between', fontSize:10, color:'#6B7280' }}>
                    <span>&lt; Entrada</span><span>{copy?.planned_date||'—'}</span>
                  </div>
                  <div style={{ background:'#fff', padding:'8px 14px', borderBottom:'1px solid #F3F4F6' }}>
                    <div style={{ fontSize:11, fontWeight:700, color:'#111' }}>Marketing da Digidelta</div>
                    <input disabled={!editing} value={form.headline||''} onChange={e=>handleFieldChange('headline',e.target.value)} placeholder="Subject…"
                      style={{ border:'none', borderBottom:editing?'1px dashed #9CA3AF':'none', background:'transparent', fontSize:11, fontWeight:600, color:'#374151', outline:'none', cursor:editing?'text':'default', padding:'2px 0', width:'100%' }} />
                    <div style={{ fontSize:9, color:'#9CA3AF', marginTop:1 }}>{form.payload?.preheader||'…'}</div>
                  </div>
                  {/* Template briefing mobile */}
                  <div style={{ overflowY:'auto', maxHeight:480 }}>
                    <div style={{ background:'linear-gradient(120deg,#0A1B3D 0%,#112954 35%,#3859d0 70%,#0F4FF9 100%)', height:80, display:'flex', alignItems:'center', padding:'0 20px' }}>
                      <img src="https://www.digidelta.pt/wp-content/uploads/2026/01/digidelta-logo-branco.svg" alt="Digidelta" height="20" style={{ display:'block' }} onError={function(e){e.target.style.display='none';}} />
                    </div>
                    <div style={{ background:'#fff', borderRadius:8, marginTop:-30, marginLeft:10, marginRight:10, position:'relative', zIndex:1, boxShadow:'0 4px 16px rgba(17,41,84,0.08)', padding:'20px 16px 16px' }}>
                      <p style={{ margin:'0 0 8px', fontSize:13, fontWeight:700, color:'#112954', fontFamily:'Inter,sans-serif' }}>{form.payload?.campaign_title||'Campanha'}</p>
                      <p style={{ margin:'0 0 10px', fontSize:11, color:'#B4B4B4' }}>{form.payload?.campaign_tagline||''}</p>
                      <div style={{ fontSize:12, lineHeight:1.65, color:'#141618', whiteSpace:'pre-wrap' }}>{form.payload?.body_resumo||form.payload?.resumo_html||<span style={{ color:'#9CA3AF', fontStyle:'italic' }}>Resumo…</span>}</div>
                    </div>
                    <div style={{ padding:'12px 16px', textAlign:'center' }}>
                      <div style={{ fontSize:10, color:'#B4B4B4', fontFamily:'Inter,sans-serif' }}>Digidelta Internacional · Privacidade</div>
                    </div>
                  </div>
                </div>
              ) : (
                <>
                {/* Header outline for desktop */}
                <div style={{ width:'100%', maxWidth:600, marginBottom:12, padding:'8px 12px', borderBottom:'1px solid #E5E7EB' }}>
                  <div style={{ display:'flex', alignItems:'center', gap:6, marginBottom:3 }}>
                    <span style={{ fontSize:9, color:'#9CA3AF', width:56, flexShrink:0, textAlign:'right' }}>De</span>
                    <span style={{ fontSize:11, fontWeight:600, color:'#374151' }}>Marketing da Digidelta &lt;marketing@digidelta.pt&gt;</span>
                  </div>
                  <div style={{ display:'flex', alignItems:'center', gap:6, marginBottom:3 }}>
                    <span style={{ fontSize:9, color:'#9CA3AF', width:56, flexShrink:0, textAlign:'right' }}>Assunto</span>
                    <input disabled={!editing} value={form.headline||''} onChange={e=>handleFieldChange('headline',e.target.value)} placeholder="Subject…" maxLength={80}
                      style={{ flex:1, border:'none', borderBottom:editing?'1px dashed #6B7280':'none', background:'transparent', fontSize:12, fontWeight:700, color:'#111827', outline:'none', cursor:editing?'text':'default', padding:'1px 0' }} />
                    <span style={{ fontSize:9, color:(form.headline||'').length>80?'#EF4444':'#9CA3AF', fontFamily:'monospace' }}>{(form.headline||'').length}/80</span>
                  </div>
                  <div style={{ display:'flex', alignItems:'center', gap:6 }}>
                    <span style={{ fontSize:9, color:'#9CA3AF', width:56, flexShrink:0, textAlign:'right' }}>Preview</span>
                    <input disabled={!editing} value={form.payload?.preheader||''} onChange={e=>handlePayloadChange('preheader',e.target.value)} placeholder="Preheader…" maxLength={90}
                      style={{ flex:1, border:'none', borderBottom:editing?'1px dashed #D1D5DB':'none', background:'transparent', fontSize:10, color:'#6B7280', outline:'none', cursor:editing?'text':'default', padding:'1px 0' }} />
                    <span style={{ fontSize:9, color:(form.payload?.preheader||'').length>90?'#EF4444':'#9CA3AF', fontFamily:'monospace' }}>{(form.payload?.preheader||'').length}/90</span>
                  </div>
                </div>
                </>
              )}

              {/* ── TEMPLATE 28 renderizado (desktop 600px / mobile 340px) ── */}
              <div style={{ width:'100%', maxWidth: previewMode==='mobile'?340:600, background:'#f9faff', boxShadow: previewMode==='mobile'?'none':'0 4px 16px rgba(0,0,0,0.08)' }}>
                {/* Header gradient */}
                <div style={{ background:'linear-gradient(120deg,#0A1B3D 0%,#112954 35%,#3859d0 70%,#0F4FF9 100%)', padding:'20px 24px 60px', display:'flex', justifyContent:'space-between', alignItems:'flex-start' }}>
                  <img src="https://www.digidelta.pt/wp-content/uploads/2026/01/digidelta-logo-branco.svg" alt="Digidelta" height="26" style={{ display:'block' }} onError={e=>{e.target.style.display='none';}} />
                  <span style={{ fontSize:11, color:'rgba(255,255,255,0.85)', fontWeight:600 }}>{copy?.planned_date||'—'}</span>
                </div>
                {/* Card */}
                <div style={{ margin:'-40px 16px 0', background:'#fff', borderRadius:4, boxShadow:'0 4px 20px rgba(17,41,84,0.1)', padding:'20px 24px 24px' }}>
                  {/* Campaign title */}
                  <input disabled={!editing} value={form.payload?.campaign_title||''} onChange={e=>handlePayloadChange('campaign_title',e.target.value)} placeholder="Campaign title (H1)…" maxLength={50}
                    style={{ width:'100%', border:editing?'1px dashed #E3E6EB':'none', borderRadius:3, background:'transparent', fontSize:20, fontWeight:800, color:'#112954', padding:editing?'4px 6px':'2px 0', outline:'none', cursor:editing?'text':'default', boxSizing:'border-box', marginBottom:6 }} />
                  <input disabled={!editing} value={form.payload?.campaign_tagline||''} onChange={e=>handlePayloadChange('campaign_tagline',e.target.value)} placeholder="Campaign tagline…" maxLength={80}
                    style={{ width:'100%', border:editing?'1px dashed #E3E6EB':'none', borderRadius:3, background:'transparent', fontSize:12, color:'#6B7280', padding:editing?'3px 6px':'2px 0', outline:'none', cursor:editing?'text':'default', boxSizing:'border-box', marginBottom:16 }} />

                  {/* RESUMO — elevator pitch na primeira linha */}
                  <div style={{ marginBottom:14 }}>
                    <div style={{ fontSize:9, fontWeight:700, color:'#EF644C', letterSpacing:'0.1em', textTransform:'uppercase', marginBottom:6 }}>ELEVATOR PITCH</div>
                    {editing ? (
                      <textarea value={form.payload?.resumo_html||''} onChange={e=>{handlePayloadChange('resumo_html',e.target.value);e.target.style.height='auto';e.target.style.height=e.target.scrollHeight+'px';}}
                        ref={el=>{if(el&&!el._r){el._r=true;el.style.height='auto';el.style.height=el.scrollHeight+'px';}}}
                        placeholder="<p>Resumo HTML…</p>"
                        style={{ width:'100%', border:'1px dashed #E3E6EB', borderRadius:3, background:'transparent', fontSize:11, fontFamily:'monospace', color:'#374151', padding:'6px', outline:'none', resize:'none', boxSizing:'border-box', overflow:'hidden' }} />
                    ) : (
                      <div dangerouslySetInnerHTML={{ __html: form.payload?.resumo_html||'<p style="color:#B4B4B4;font-style:italic">RESUMO vazio</p>' }} style={{ fontSize:14, lineHeight:1.7, color:'#141618' }} />
                    )}
                  </div>

                  <div style={{ background:'#ebeff9', borderRadius:4, height:previewMode==='mobile'?100:160, display:'flex', alignItems:'center', justifyContent:'center', marginBottom:20, overflow:'hidden' }}>
                    {imageUrl
                      ? <img src={imageUrl} alt="" style={{ width:'100%', height:'100%', objectFit:'cover', display:'block' }} />
                      : <span style={{ fontSize:10, color:'#B4B4B4', letterSpacing:'0.08em', textTransform:'uppercase' }}>HERO · 16:9</span>}
                  </div>

                  {/* OBJECTIVOS */}
                  <div style={{ marginBottom:14 }}>
                    <div style={{ fontSize:9, fontWeight:700, color:'#EF644C', letterSpacing:'0.1em', textTransform:'uppercase', marginBottom:6 }}>OFERTA</div>
                    {editing ? (
                      <textarea value={form.payload?.objectivos_html||''} onChange={e=>{handlePayloadChange('objectivos_html',e.target.value);e.target.style.height='auto';e.target.style.height=e.target.scrollHeight+'px';}}
                        ref={el=>{if(el&&!el._o){el._o=true;el.style.height='auto';el.style.height=el.scrollHeight+'px';}}}
                        placeholder="<ol><li>Objectivo…</li></ol>"
                        style={{ width:'100%', border:'1px dashed #E3E6EB', borderRadius:3, background:'transparent', fontSize:11, fontFamily:'monospace', color:'#374151', padding:'6px', outline:'none', resize:'none', boxSizing:'border-box', overflow:'hidden' }} />
                    ) : (
                      <div dangerouslySetInnerHTML={{ __html: form.payload?.objectivos_html||'<p style="color:#B4B4B4;font-style:italic">OBJECTIVOS vazios</p>' }} style={{ fontSize:14, lineHeight:1.7, color:'#141618' }} />
                    )}
                  </div>

                  {/* CANAIS */}
                  <div style={{ marginBottom:16 }}>
                    <div style={{ fontSize:9, fontWeight:700, color:'#EF644C', letterSpacing:'0.1em', textTransform:'uppercase', marginBottom:6 }}>OBJEÇÕES</div>
                    {editing ? (
                      <textarea value={form.payload?.canais_html||''} onChange={e=>{handlePayloadChange('canais_html',e.target.value);e.target.style.height='auto';e.target.style.height=e.target.scrollHeight+'px';}}
                        ref={el=>{if(el&&!el._c){el._c=true;el.style.height='auto';el.style.height=el.scrollHeight+'px';}}}
                        placeholder="<ol><li>Canal…</li></ol>"
                        style={{ width:'100%', border:'1px dashed #E3E6EB', borderRadius:3, background:'transparent', fontSize:11, fontFamily:'monospace', color:'#374151', padding:'6px', outline:'none', resize:'none', boxSizing:'border-box', overflow:'hidden' }} />
                    ) : (
                      <div dangerouslySetInnerHTML={{ __html: form.payload?.canais_html||'<p style="color:#B4B4B4;font-style:italic">CANAIS vazios</p>' }} style={{ fontSize:14, lineHeight:1.7, color:'#141618' }} />
                    )}
                  </div>

                  {/* CTA */}
                  <div style={{ textAlign:'center', margin:'16px 0' }}>
                    <div style={{ display:'inline-flex', flexDirection:'column', alignItems:'center', gap:6 }}>
                      <div style={{ background:'#0F4FF9', borderRadius:4, padding:'12px 32px' }}>
                        <input disabled={!editing} value={form.payload?.cta_label||''} onChange={e=>handlePayloadChange('cta_label',e.target.value)} placeholder="CTA"
                          style={{ border:'none', background:'transparent', color:'#fff', fontWeight:700, fontSize:15, textAlign:'center', outline:'none', cursor:editing?'text':'default', minWidth:100 }} />
                      </div>
                      <input disabled={!editing} value={form.payload?.cta_url||''} onChange={e=>handlePayloadChange('cta_url',e.target.value)} placeholder="https://…"
                        style={{ border:editing?'1px dashed #B4B4B4':'none', borderRadius:3, background:'transparent', fontSize:10, color:'#888', padding:editing?'2px 6px':'1px 0', outline:'none', width:280, textAlign:'center', cursor:editing?'text':'default' }} />
                    </div>
                  </div>

                  {/* ROLE BLOCK — mostra o role seleccionado no sidebar */}
                  <div style={{ borderTop:'1px solid #ebeff9', paddingTop:16, marginTop:8 }}>
                    <div style={{ fontSize:9, fontWeight:700, color:'#112954', letterSpacing:'0.1em', textTransform:'uppercase', marginBottom:6 }}>
                      {({comercial:'NO DAILY PROSPECT',sat:'SUPORTE TÉCNICO — O QUE ESPERAR',board:'RESUMO EXECUTIVO',operacoes:'OPERAÇÕES — LOGÍSTICA',marketing:'MARKETING — ESTADO DA CAMPANHA',rh_finance:'CONTEXTO FINANCEIRO',default:'INFORMAÇÃO ADICIONAL'})[previewRole]}
                    </div>
                    {editing ? (
                      <textarea value={form.payload?.['role_'+previewRole+'_html']||''} onChange={e=>{handlePayloadChange('role_'+previewRole+'_html',e.target.value);e.target.style.height='auto';e.target.style.height=e.target.scrollHeight+'px';}}
                        ref={el=>{if(el){el.style.height='auto';el.style.height=el.scrollHeight+'px';}}}
                        placeholder={"HTML para role: "+previewRole}
                        style={{ width:'100%', border:'1px dashed #E3E6EB', borderRadius:3, background:'transparent', fontSize:11, fontFamily:'monospace', color:'#374151', padding:'6px', outline:'none', resize:'none', boxSizing:'border-box', overflow:'hidden', minHeight:80 }} />
                    ) : (
                      <div dangerouslySetInnerHTML={{ __html: form.payload?.['role_'+previewRole+'_html']||'<p style="color:#B4B4B4;font-style:italic">Sem conteúdo para este role</p>' }} style={{ fontSize:14, lineHeight:1.65, color:'#141618' }} />
                    )}
                  </div>
                </div>

                {/* Signature + Footer */}
                <div style={{ padding:'16px 24px 8px', textAlign:'center' }}>
                  <input disabled={!editing} value={form.payload?.signature_close||''} onChange={e=>handlePayloadChange('signature_close',e.target.value)} placeholder="Boa campanha" maxLength={40}
                    style={{ border:editing?'1px dashed #E3E6EB':'none', borderRadius:3, background:'transparent', fontSize:13, color:'#374151', padding:editing?'3px 8px':'0', outline:'none', textAlign:'center', cursor:editing?'text':'default' }} />
                  {userEmail && <div style={{ fontSize:12, color:'#0F4FF9', marginTop:4 }}>{userEmail}</div>}
                </div>
                <div style={{ padding:'12px 24px 20px', textAlign:'center' }}>
                  <div style={{ fontSize:11, color:'#B4B4B4' }}>Digidelta Internacional · Rua da Igreja, 251 · 4485-849 Vairão</div>
                  <div style={{ fontSize:11, color:'#B4B4B4', marginTop:3 }}>Cancelar subscrição · Privacidade</div>
                </div>
              </div>
              {/* end template */}

            </div>
          </div>
        </div>
      ) : isSocial ? (
      /* ── META ADS / SOCIAL — mockup feed Instagram/Facebook outline ── */
      <div style={{ fontFamily:'Inter,sans-serif', border: hideMockup ? 0 : '1px solid #D1D5DB', borderRadius: hideMockup ? 0 : 8, overflow: hideMockup ? 'visible' : 'hidden' }}>
        {/* Title bar — omitida no detail 3-col (preview vive na coluna direita) */}
        {!hideMockup && (
        <div style={{ borderBottom:'1px solid #D1D5DB', padding:'6px 10px', display:'flex', alignItems:'center', gap:8 }}>
          <div style={{ display:'flex', gap:5 }}>
            <div style={{ width:10,height:10,borderRadius:'50%',border:'1px solid #D1D5DB' }} />
            <div style={{ width:10,height:10,borderRadius:'50%',border:'1px solid #D1D5DB' }} />
            <div style={{ width:10,height:10,borderRadius:'50%',border:'1px solid #D1D5DB' }} />
          </div>
            <span style={{ fontSize:10, color:'#9CA3AF', fontWeight:500, margin:'0 auto' }}>{_isMetaAds ? 'Meta Ads — Pré-visualização' : 'Instagram / Facebook — Pré-visualização'}</span>
        </div>
        )}

        {/* ── META ADS: wireframe telemóvel (estilo Apple Mail outline) ── */}
        {_isMetaAds && (
          <React.Fragment>
          <div style={hideMockup ? { display:'flex', flexDirection:'column', gap:14 } : { display:'flex', gap:16, alignItems:'flex-start', background:'#F5F5F7', padding:'16px' }}>
            {!hideMockup && <div style={{ width: 300, flexShrink: 0 }}><CtdMetaAdsFeedMockup form={form} /></div>}
          {/* ── COPY FIELDS — ao lado do phone (ou em pleno se hideMockup) ── */}
            <div style={hideMockup ? { display:'flex', flexDirection:'column', gap:12 } : { flex:1, background:'#fff', border:'1px solid #E3E6EB', borderRadius:8, padding:'14px', alignSelf:'flex-start', display:'flex', flexDirection:'column', gap:12 }}>
              <div style={{ fontSize:9, fontWeight:700, letterSpacing:'0.10em', textTransform:'uppercase', color:'#3859D0', fontFamily:'var(--font-mono, monospace)' }}>Copy · Meta Ads</div>

              {Array.isArray(form.payload?.claim_sugestoes) && form.payload.claim_sugestoes.length > 0 && (
                <div>
                  <div style={{ fontSize:10, fontWeight:600, color:'#374151', marginBottom:6 }}>Sugestões</div>
                  <select disabled={!editing}
                    value={(() => {
                      const i = form.payload.claim_sugestoes.findIndex(s => String(s?.claim || '') === String(form.copy_imagem || '') && String(s?.sub || '') === String(form.copy_imagem_sub || ''));
                      return i >= 0 ? String(i) : '';
                    })()}
                    onChange={e => {
                      const s = form.payload.claim_sugestoes[Number(e.target.value)];
                      if (!s) return;
                      const claim = String(s.claim || '');
                      const sub = String(s.sub || '');
                      markDirty();
                      setForm(prev => ({
                        ...prev,
                        copy_imagem: claim,
                        copy_imagem_sub: sub,
                        payload: { ...(prev.payload || {}), image_headline: claim, image_sub: sub },
                      }));
                    }}
                    style={{ width:'100%', border: editing ? '1px solid #D1D5DB' : '1px solid #E5E7EB', borderRadius:4, background: editing ? '#fff' : '#F9FAFB', fontSize:12, color:'#111827', padding:'8px 10px', boxSizing:'border-box', fontFamily:'Inter,sans-serif', cursor: editing ? 'pointer' : 'default' }}>
                    {(() => {
                      const matched = form.payload.claim_sugestoes.some(s => String(s?.claim || '') === String(form.copy_imagem || '') && String(s?.sub || '') === String(form.copy_imagem_sub || ''));
                      return !matched ? <option value="">Par editado nas caixas</option> : null;
                    })()}
                    {form.payload.claim_sugestoes.map((s, i) => (
                      <option key={i} value={String(i)}>{String(s?.claim || '')} — {String(s?.sub || '')}</option>
                    ))}
                  </select>
                </div>
              )}

              {/* Claim + Sub-descriptor */}
              <div style={{ display:'grid', gridTemplateColumns: hideMockup ? '1fr' : '1fr 1fr', gap:12 }}>
                <div>
                  <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', gap:8, marginBottom:4 }}>
                    <div style={{ fontSize:10, fontWeight:600, color:'#374151', whiteSpace:'nowrap' }}>Claim da imagem</div>
                    <span style={{ fontSize:9, color:(form.copy_imagem||'').split(/\s+/).filter(Boolean).length > 6 ? '#EF4444' : '#9CA3AF', fontFamily:'monospace', whiteSpace:'nowrap', flexShrink:0 }}>{(form.copy_imagem||'').split(/\s+/).filter(Boolean).length} / 6 palavras</span>
                  </div>
                  <textarea disabled={!editing} value={form.copy_imagem||''} onChange={e=>handleFieldChange('copy_imagem',e.target.value)} rows={2} placeholder="CLAIM UPPERCASE…"
                    style={{ width:'100%', border:editing?'1px solid #D1D5DB':'1px solid #E5E7EB', borderRadius:4, background:editing?'#fff':'#F9FAFB', fontSize:12, fontWeight:700, color:'#111827', textTransform:'uppercase', outline:'none', cursor:editing?'text':'default', resize:'none', padding:'8px 10px', boxSizing:'border-box', fontFamily:'Inter,sans-serif', letterSpacing:'0.03em', lineHeight:1.4 }} />
                </div>
                <div>
                  <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', gap:8, marginBottom:4 }}>
                    <div style={{ fontSize:10, fontWeight:600, color:'#374151', whiteSpace:'nowrap' }}>Sub-descriptor</div>
                    <span style={{ fontSize:9, color:(form.copy_imagem_sub||'').split(/\s+/).filter(Boolean).length > 14 ? '#EF4444' : '#9CA3AF', fontFamily:'monospace', whiteSpace:'nowrap', flexShrink:0 }}>{(form.copy_imagem_sub||'').split(/\s+/).filter(Boolean).length} / 14 palavras</span>
                  </div>
                  <textarea disabled={!editing} value={form.copy_imagem_sub||''} onChange={e=>handleFieldChange('copy_imagem_sub',e.target.value)} rows={2} placeholder="Sub-descriptor…"
                    style={{ width:'100%', border:editing?'1px solid #D1D5DB':'1px solid #E5E7EB', borderRadius:4, background:editing?'#fff':'#F9FAFB', fontSize:12, color:'#374151', outline:'none', cursor:editing?'text':'default', resize:'none', padding:'8px 10px', boxSizing:'border-box', fontFamily:'Inter,sans-serif', lineHeight:1.4 }} />
                </div>
              </div>

              <div>
                <div style={{ fontSize:10, fontWeight:600, color:'#374151', marginBottom:4 }}>Apelo da imagem</div>
                <input disabled={!editing} value={form.copy_imagem_cta||''} onChange={e=>handleFieldChange('copy_imagem_cta', e.target.value)} placeholder="SEIS MESES SEM RENDA"
                  style={{ width:'100%', border:editing?'1px solid #D1D5DB':'1px solid #E5E7EB', borderRadius:4, background:editing?'#fff':'#F9FAFB', fontSize:12, fontWeight:700, color:'#111827', outline:'none', cursor:editing?'text':'default', padding:'8px 10px', boxSizing:'border-box', fontFamily:'Inter,sans-serif' }} />
              </div>

              {/* Primary text — 5 blocos */}
              <div>
                <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', gap:8, marginBottom:4 }}>
                  <div style={{ fontSize:10, fontWeight:600, color:'#374151', whiteSpace:'nowrap' }}>Primary text · 5 blocos</div>
                  <span style={{ fontSize:9, color:metaPrimary.length > 1100 ? '#EF4444' : '#9CA3AF', fontFamily:'monospace', whiteSpace:'nowrap', flexShrink:0 }}>
                    {metaPrimary.length} / 1100
                  </span>
                </div>
                <textarea disabled={!editing}
                  value={metaPrimary}
                  onChange={e => { handlePayloadChange('primary_text_full', e.target.value); e.target.style.height='auto'; e.target.style.height=Math.max(e.target.scrollHeight, 340)+'px'; }}
                  ref={el => { if (!el) return; el.style.height='auto'; el.style.height=Math.max(el.scrollHeight, 340)+'px'; }}
                  rows={18}
                  placeholder={'L1 Problema/Dor (≤70)\n\nL2 Necessidade agitada (≤70)\n\nL3 Solução com USP (≤100)\n\nL4 Oferta + Motivador (≤90)\n\nL5 CTA demo (≤40)'}
                  style={{ width:'100%', minHeight:340, border:editing?'1px solid #D1D5DB':'1px solid #E5E7EB', borderRadius:4, background:editing?'#fff':'#F9FAFB', fontSize:12, lineHeight:1.6, color:'#111827', outline:'none', cursor:editing?'text':'default', resize:'vertical', padding:'8px 10px', boxSizing:'border-box', fontFamily:'Inter,sans-serif', whiteSpace:'pre-wrap', overflow:'auto' }} />
                <div style={{ display:'flex', justifyContent:'flex-end', marginTop:4 }}>
                  <span style={{ fontSize:9, color:'#9CA3AF', fontFamily:'monospace', whiteSpace:'nowrap' }}>
                    hook feed: {(metaPrimary.split(/\n+/)[0]||'').length} / 125
                  </span>
                </div>
              </div>

              {/* Headline + Descrição + CTA Meta */}
              <div style={{ display:'flex', flexDirection:'column', gap:12 }}>
                <div>
                  <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', gap:8, marginBottom:4 }}>
                    <div style={{ fontSize:10, fontWeight:600, color:'#374151', whiteSpace:'nowrap' }}>Título</div>
                    <span style={{ fontSize:9, color:(form.headline||'').length > 40 ? '#EF4444' : '#9CA3AF', fontFamily:'monospace', whiteSpace:'nowrap' }}>{(form.headline||'').length} / 40</span>
                  </div>
                  <input disabled={!editing} value={form.headline||''} onChange={e=>handleFieldChange('headline',e.target.value)} maxLength={80} placeholder="Headline ≤40 chars…"
                    style={{ width:'100%', border:editing?'1px solid #D1D5DB':'1px solid #E5E7EB', borderRadius:4, background:editing?'#fff':'#F9FAFB', fontSize:12, fontWeight:600, color:'#111827', outline:'none', cursor:editing?'text':'default', padding:'8px 10px', boxSizing:'border-box', fontFamily:'Inter,sans-serif' }} />
                </div>
                <div style={{ display:'grid', gridTemplateColumns:'minmax(0, 1fr) minmax(160px, 200px)', gap:12, alignItems:'end' }}>
                  <div>
                    <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', gap:8, marginBottom:4 }}>
                      <div style={{ fontSize:10, fontWeight:600, color:'#374151', whiteSpace:'nowrap' }}>Descrição</div>
                      <span style={{ fontSize:9, color:String(form.payload?.description||'').length > 30 ? '#EF4444' : '#9CA3AF', fontFamily:'monospace' }}>{String(form.payload?.description||'').length} / 30</span>
                    </div>
                    <input disabled={!editing} value={form.payload?.description||''} onChange={e=>handlePayloadChange('description',e.target.value)} maxLength={30} placeholder="Prova debaixo do título…"
                      style={{ width:'100%', border:editing?'1px solid #D1D5DB':'1px solid #E5E7EB', borderRadius:4, background:editing?'#fff':'#F9FAFB', fontSize:12, color:'#374151', outline:'none', cursor:editing?'text':'default', padding:'8px 10px', boxSizing:'border-box', fontFamily:'Inter,sans-serif' }} />
                  </div>
                  <div>
                    <div style={{ fontSize:10, fontWeight:600, color:'#374151', marginBottom:4, whiteSpace:'nowrap' }}>Apelo à acção</div>
                    <select disabled={!editing} value={form.cta || 'Saber mais'} onChange={e => { handleFieldChange('cta', e.target.value); handlePayloadChange('cta_label', e.target.value); }}
                      style={{ width:'100%', border:editing?'1px solid #D1D5DB':'1px solid #E5E7EB', borderRadius:4, background:editing?'#fff':'#F9FAFB', fontSize:12, fontWeight:700, color:'#111827', outline:'none', cursor:editing?'pointer':'default', padding:'8px 10px', boxSizing:'border-box', fontFamily:'Inter,sans-serif' }}>
                      {['Registar','Candidatar-me agora','Descarregar','Obter oferta','Pedir orçamento','Ver detalhes','Saber mais','Subscrever','Reservar agora'].map(opt => (
                        <option key={opt} value={opt}>{opt}</option>
                      ))}
                      {form.cta && !['Registar','Candidatar-me agora','Descarregar','Obter oferta','Pedir orçamento','Ver detalhes','Saber mais','Subscrever','Reservar agora'].includes(form.cta) && (
                        <option value={form.cta}>{form.cta}</option>
                      )}
                    </select>
                  </div>
                </div>
              </div>
              {hideActions && editing && (
                <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
                  <button type="button" onClick={() => handleSave().catch(err => alert('Erro ao guardar: ' + err.message))} disabled={saving}
                    style={{ background: '#3859D0', color: '#fff', border: 0, padding: '8px 14px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: saving ? 'not-allowed' : 'pointer', opacity: saving ? 0.6 : 1 }}>
                    {saving ? 'A guardar…' : savedFlash ? 'Guardado' : 'Guardar alterações'}
                  </button>
                </div>
              )}
            </div>
          </div>

          </React.Fragment>
        )}

        {/* Layout: format toggle + preview (só para Instagram/Facebook/LinkedIn) */}
        {!_isMetaAds && !isWa && <div style={{ display:'flex' }}>
          {/* Format toggle */}
          <div style={{ width:140, borderRight:'1px solid #E5E7EB', padding:'10px 8px', display:'flex', flexDirection:'column', gap:8 }}>
            <div style={{ fontSize:8, textTransform:'uppercase', letterSpacing:'0.08em', color:'#9CA3AF', fontWeight:700, padding:'2px 4px' }}>Formato</div>
            {[{id:'feed',label:'Feed 1:1'},{id:'portrait',label:'Feed 4:5'},{id:'stories',label:'Stories 9:16'}].map(opt => {
              const active = adFormat === opt.id;
              return (
                <button key={opt.id} onClick={()=>setAdFormat(opt.id)}
                  style={{ border:'1px solid '+(active?'#0F4FF9':'#E5E7EB'), borderRadius:4, padding:'8px 10px', display:'flex', flexDirection:'column', gap:4, alignItems:'flex-start', background:active?'rgba(15,79,249,0.04)':'transparent', cursor:'pointer', textAlign:'left' }}>
                  <div style={{ fontSize:11, fontWeight:active?700:600, color:active?'#111827':'#6B7280' }}>{opt.label}</div>
                  <div style={{ height:6, background:active?'#0F4FF9':'#E5E7EB', borderRadius:2, width:'90%', opacity:active?0.7:1 }} />
                  <div style={{ height:4, background:active?'#3859d0':'#F3F4F6', borderRadius:2, width:'60%', opacity:active?0.5:1 }} />
                </button>
              );
            })}
          </div>
          {/* Ad preview */}
          <div style={{ flex:1, background:'#F5F5F7', padding:'16px', display:'flex', justifyContent:'center' }}>
            <div style={{ width:320, border:'1px solid #E5E7EB', borderRadius:8, overflow:'hidden', background:'#fff', boxShadow:'0 2px 12px rgba(0,0,0,0.06)' }}>
              {/* Profile header */}
              <div style={{ padding:'10px 12px', display:'flex', alignItems:'center', gap:8, borderBottom:'1px solid #F3F4F6' }}>
                <div style={{ width:32, height:32, borderRadius:'50%', border:'1px solid #E5E7EB', flexShrink:0 }} />
                <div style={{ flex:1 }}>
                  <div style={{ fontSize:11, fontWeight:700, color:'#111827' }}>Digidelta Internacional</div>
                  <div style={{ fontSize:9, color:'#9CA3AF' }}>Sponsorisado</div>
                </div>
                <span style={{ fontSize:16, color:'#9CA3AF' }}>⋯</span>
              </div>
              {/* Image area */}
              <div style={{ position:'relative', background:'#1a1a2e', aspectRatio: adFormat==='stories'?'9/16':adFormat==='portrait'?'4/5':'1/1', display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', overflow:'hidden' }}>
                <div style={{ position:'absolute', inset:0, background:'linear-gradient(135deg,#1a1a2e 0%,#112954 100%)', opacity:0.9 }} />
                <div style={{ position:'relative', zIndex:1, padding:'16px', textAlign:'center', width:'100%' }}>
                  <textarea disabled={!editing} value={form.copy_imagem||''} onChange={e=>handleFieldChange('copy_imagem',e.target.value)} placeholder="HEADLINE IMAGEM" rows={2}
                    style={{ width:'100%', border:editing?'1px dashed rgba(255,255,255,0.4)':'none', borderRadius:3, background:'transparent', fontSize:18, fontWeight:800, color:'#FFFFFF', textTransform:'uppercase', textAlign:'center', outline:'none', cursor:editing?'text':'default', resize:'none', letterSpacing:'0.02em', lineHeight:1.2, boxSizing:'border-box', padding:editing?'4px':'0' }} />
                  <textarea disabled={!editing} value={form.copy_imagem_sub||''} onChange={e=>handleFieldChange('copy_imagem_sub',e.target.value)} placeholder="Sub-descriptor…" rows={2}
                    style={{ width:'100%', border:editing?'1px dashed rgba(255,255,255,0.3)':'none', borderRadius:3, background:'transparent', fontSize:12, color:'rgba(255,255,255,0.8)', textAlign:'center', outline:'none', cursor:editing?'text':'default', resize:'none', marginTop:6, boxSizing:'border-box', padding:editing?'3px':'0' }} />
                  <div style={{ marginTop:12 }}>
                    <input disabled={!editing} value={form.copy_imagem_cta||''} onChange={e=>handleFieldChange('copy_imagem_cta',e.target.value)} placeholder="CTA →"
                      style={{ border:editing?'1px dashed rgba(255,255,255,0.4)':'none', borderRadius:3, background:'rgba(255,255,255,0.15)', color:'#fff', fontWeight:700, fontSize:12, textAlign:'center', outline:'none', cursor:editing?'text':'default', padding:'4px 12px' }} />
                  </div>
                </div>
              </div>
              {/* Primary text */}
              <div style={{ padding:'10px 12px', borderTop:'1px solid #F3F4F6' }}>
                <textarea disabled={!editing} value={form.caption_organica||''} onChange={e=>handleFieldChange('caption_organica',e.target.value)} placeholder="Primary text / caption…" rows={3}
                  style={{ width:'100%', border:editing?'1px dashed #E3E6EB':'none', borderRadius:3, background:'transparent', fontSize:12, lineHeight:1.6, color:'#141618', outline:'none', cursor:editing?'text':'default', resize:'none', boxSizing:'border-box', padding:editing?'4px':'0' }} />
              </div>
              {/* Link preview + CTA */}
              <div style={{ borderTop:'1px solid #E5E7EB', padding:'8px 12px', display:'flex', justifyContent:'space-between', alignItems:'center', background:'#F9FAFB' }}>
                <div style={{ flex:1, minWidth:0 }}>
                  <div style={{ fontSize:9, color:'#9CA3AF', textTransform:'uppercase', letterSpacing:'0.04em' }}>digidelta.pt</div>
                  <input disabled={!editing} value={form.headline||''} onChange={e=>handleFieldChange('headline',e.target.value)} placeholder="Ad headline…" maxLength={40}
                    style={{ width:'100%', border:'none', background:'transparent', fontSize:11, fontWeight:700, color:'#111827', outline:'none', cursor:editing?'text':'default', padding:'1px 0' }} />
                  {/* Description ≤30 */}
                  <input disabled={!editing} value={form.payload?.description||''} onChange={e=>handlePayloadChange('description',e.target.value)} placeholder="Description ≤30 chars…" maxLength={30}
                    style={{ width:'100%', border:editing?'1px dashed #D1D5DB':'none', borderRadius:2, background:'transparent', fontSize:10, color:'#6B7280', outline:'none', cursor:editing?'text':'default', padding:editing?'1px 3px':'1px 0', marginTop:1 }} />
                </div>
                <div style={{ background:'#1877F2', borderRadius:4, padding:'5px 10px', flexShrink:0, marginLeft:8 }}>
                  <input disabled={!editing} value={form.cta||''} onChange={e=>handleFieldChange('cta',e.target.value)} placeholder="CTA" maxLength={20}
                    style={{ border:'none', background:'transparent', color:'#fff', fontWeight:700, fontSize:10, outline:'none', cursor:editing?'text':'default', width:60 }} />
                </div>
              </div>
            </div>
          </div>
        </div>}


      </div>
      ) : isWa ? (
      <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
        <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', textTransform: 'uppercase', color: '#128C7E', fontFamily: 'var(--font-mono, monospace)' }}>Copy · WhatsApp</div>
        <CtdWaFields form={form} editing={formEditing} onChange={handleFieldChange} />
        {!hideMockup && <CtdWaPhone pack={ctdWaPackFromForm(form)} brandName={brandSlug} />}
      </div>
      ) : (
      /* ── OUTROS CANAIS (blog, landing, etc) — renderer genérico ── */
      <FieldGroup title={
        isBlog ? 'Blog / Site' : isLanding ? 'Landing' : 'Copy do Anúncio'
      }>
        <Field
          label={
            isBlog ? 'Título / H1 (≤60 chars)'
            : isLanding ? 'Título (≤50 chars)'
            : 'Headline (≤40 chars Meta / ≤70 LinkedIn)'
          }
          field="headline" maxLen={isBlog ? 60 : isLanding ? 50 : 70}
        />
        <Field
          label={
            isBlog ? 'Corpo (200-400 palavras)'
            : isLanding ? 'Corpo (3-5 parágrafos)'
            : 'Primary text / Body (300-600 chars Meta)'
          }
          field="body" multi rows={isBlog ? 8 : 5}
        />
        <Field label="CTA" field="cta" maxLen={40} />
      </FieldGroup>
      )}

      {/* Preview mockup (só para tipos visuais — não meta_ads que tem o seu próprio mockup acima) */}
      {!_isMetaAds && !isWa && (form.copy_imagem || form.copy_imagem_sub || form.copy_imagem_cta) && (
        <CtdCopyPreviewMockup
          headline={form.copy_imagem}
          sub={form.copy_imagem_sub}
          cta={form.copy_imagem_cta}
          aspectRatio={copy.formato === '16:9' ? '16/9' : copy.formato === '1:1' ? '1/1' : '4/5'}
        />
      )}

      {/* Actions — omitido quando renderizado dentro do modal (hideActions=true) */}
      {!hideActions && (
      <div onKeyDown={handleKeyDown} style={{
        display: 'flex', gap: 6, alignItems: 'center', paddingTop: 12,
        borderTop: '1px solid var(--border-1, #ECEFF5)', flexWrap: 'wrap',
      }}>
        {!editing ? (
          <>
            <button
              onClick={() => setEditing(true)}
              className="btn btn-sm"
              style={{ fontSize: 11, display: 'inline-flex', alignItems: 'center', gap: 5 }}
            >
              <Icon name="wrench" size={12} /> Editar
            </button>
            <button
              onClick={() => onOpenGenerate ? onOpenGenerate(copy) : handleRegenerate()}
              disabled={regenerating}
              title="Abrir modal para gerar esta peça (escolha de modelo: Sonnet / Opus)"
              className="btn btn-sm"
              style={{ fontSize: 11, display: 'inline-flex', alignItems: 'center', gap: 5, opacity: regenerating ? 0.5 : 1 }}
            >
              <Icon name="sparkle" size={12} /> {regenerating ? 'A regerar…' : 'Regerar…'}
            </button>
            {!isMaster && masterCopy && onOpenDiff && (
              <button
                onClick={() => onOpenDiff(copy, masterCopy)}
                title="Comparar com master PT"
                className="btn btn-sm"
                style={{ fontSize: 11, display: 'inline-flex', alignItems: 'center', gap: 5 }}
              >
                <Icon name="layers" size={12} /> Comparar
              </button>
            )}
            {onOpenHistory && (
              <button
                onClick={() => onOpenHistory(copy)}
                title="Histórico de edições"
                className="btn btn-sm"
                style={{ fontSize: 11, display: 'inline-flex', alignItems: 'center', gap: 5 }}
              >
                <Icon name="history" size={12} /> Histórico
              </button>
            )}
          </>
        ) : (
          <>
            <button
              onClick={() => handleSave().catch(err => alert('Erro ao guardar: ' + err.message))} disabled={saving}
              title="⌘/Ctrl + Enter"
              style={{
                background: 'var(--color-primary, #3859D0)', color: '#fff', border: 0,
                padding: '6px 12px', borderRadius: 'var(--radius-sm, 6px)',
                fontSize: 11, fontWeight: 600, cursor: 'pointer',
                display: 'inline-flex', alignItems: 'center', gap: 5,
                opacity: saving ? 0.5 : 1,
              }}
            >
              <Icon name="check" size={12} /> {saving ? 'A guardar…' : 'Guardar'}
            </button>
            <button
              onClick={() => { setEditing(false); setForm(initialForm); }}
              title="Esc"
              className="btn btn-sm btn-ghost"
              style={{ fontSize: 11 }}
            >
              Cancelar
            </button>
            <span style={{ fontSize: 9, color: 'var(--text-dim, #7a8fa8)', fontFamily: 'var(--font-mono, monospace)', marginLeft: 6 }}>
              ⌘/Ctrl+Enter guardar · Esc cancelar
            </span>
          </>
        )}

        {copy.format_notes && !editing && (
          <span title={copy.format_notes} style={{
            fontSize: 10, color: 'var(--text-dim, #7a8fa8)',
            fontStyle: 'italic', flex: 1, minWidth: 0,
            overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
          }}>
            {copy.format_notes}
          </span>
        )}

        {!isApproved && !editing && (
          <button
            onClick={handleApprove}
            disabled={approving}
            style={{
              marginLeft: 'auto', background: '#1F8A52', color: '#fff', border: 0,
              padding: '7px 14px', cursor: 'pointer',
              borderRadius: 'var(--radius-sm, 6px)',
              fontSize: 11, fontWeight: 700, letterSpacing: '0.04em',
              display: 'inline-flex', alignItems: 'center', gap: 5,
              opacity: approving ? 0.5 : 1,
              boxShadow: '0 1px 2px rgba(31,138,82,0.24)',
            }}
          >
            <Icon name="check" size={13} /> APROVAR
          </button>
        )}
      </div>
      )}
    </div>
  );
});

// ─── CtdCopyPreviewMockup — SVG mockup image typography preview ────────────────
const CtdCopyPreviewMockup = ({ headline, sub, cta, aspectRatio = '4/5' }) => {
  if (!headline && !sub && !cta) return null;
  const [w, h] = aspectRatio === '16/9' ? [640, 360] : aspectRatio === '1/1' ? [400, 400] : [320, 400];
  const lines = String(headline || '').split(/\s+/).reduce((acc, w) => {
    const last = acc[acc.length - 1] || '';
    if ((last + ' ' + w).trim().length > 12) acc.push(w);
    else acc[acc.length - 1] = (last + ' ' + w).trim();
    return acc;
  }, ['']);
  const headlineFontSize = h * 0.10;
  const subFontSize      = h * 0.035;
  const ctaFontSize      = h * 0.03;
  return (
    <div style={{
      marginTop: 12,
      display: 'inline-block',
      background: 'linear-gradient(135deg, #0f1a3e 0%, #14b8a6 100%)',
      borderRadius: 'var(--radius-md, 8px)',
      padding: 8,
      maxWidth: '100%',
    }}>
      <div style={{ fontSize: 9, color: 'rgba(255,255,255,0.6)', fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.08em', textTransform: 'uppercase', marginBottom: 6 }}>
        Preview {aspectRatio}
      </div>
      <svg viewBox={`0 0 ${w} ${h}`} width={Math.min(w, 280)} height="auto" style={{ display: 'block', borderRadius: 'var(--radius-sm, 6px)' }}>
        <defs>
          <linearGradient id="bg-grad" x1="0" y1="0" x2="1" y2="1">
            <stop offset="0%"  stopColor="#0f1a3e" />
            <stop offset="60%" stopColor="#0d9488" />
            <stop offset="100%" stopColor="#14b8a6" />
          </linearGradient>
        </defs>
        <rect width={w} height={h} fill="url(#bg-grad)" />
        {/* Headline */}
        {lines.map((ln, i) => (
          <text key={i} x={w * 0.06} y={h * 0.18 + i * headlineFontSize * 1.02}
                fill="#fff" fontFamily="Arial, sans-serif" fontWeight="900"
                fontSize={headlineFontSize} letterSpacing="-1">
            {ln}
          </text>
        ))}
        {/* Sub */}
        {sub && String(sub).split(/\n|\. /).slice(0, 2).map((ln, i) => (
          <text key={i} x={w * 0.06} y={h * 0.58 + i * subFontSize * 1.35}
                fill="rgba(255,255,255,0.9)" fontFamily="Arial, sans-serif"
                fontSize={subFontSize}>
            {ln.slice(0, 60)}
          </text>
        ))}
        {/* CTA */}
        {cta && (
          <text x={w * 0.06} y={h * 0.88}
                fill="#E60012" fontFamily="Arial, sans-serif" fontWeight="700"
                fontSize={ctaFontSize} letterSpacing="0.5">
            {String(cta).toUpperCase().slice(0, 30)}
          </text>
        )}
        {/* Mimaki wordmark top-right */}
        <text x={w * 0.94} y={h * 0.08} fill="#E60012" fontFamily="Arial, sans-serif" fontWeight="900"
              fontSize={h * 0.05} textAnchor="end">Mimaki</text>
      </svg>
    </div>
  );
};

// ─── CtdCopyDiffModal — comparar copy atual vs master ───────────────────────
const CtdCopyDiffModal = ({ copy, master, onClose }) => {
  const Icon = window.Icon;
  const fields = [
    { k: 'copy_imagem',       label: 'Image Headline' },
    { k: 'copy_imagem_sub',   label: 'Image Sub' },
    { k: 'copy_imagem_cta',   label: 'Image CTA' },
    { k: 'headline',          label: 'Headline' },
    { k: 'body',              label: 'Body' },
    { k: 'caption_organica',  label: 'Caption' },
    { k: 'cta',               label: 'CTA' },
    { k: 'hashtags',          label: 'Hashtags' },
  ];
  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, background: 'rgba(17,41,84,0.45)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        background: 'var(--bg-surface, #fff)', borderRadius: 'var(--radius-lg, 10px)',
        maxWidth: 1200, maxHeight: '90vh', width: '90vw', overflow: 'hidden',
        display: 'flex', flexDirection: 'column',
        boxShadow: 'var(--shadow-huge)',
      }}>
        <div style={{ padding: '16px 20px', borderBottom: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <div>
            <div className="font-display" style={{ fontSize: 16, fontWeight: 700, color: 'var(--text, #112954)' }}>Comparar com master</div>
            <div style={{ fontSize: 11, color: 'var(--text-muted, #4a5e7a)', marginTop: 2 }}>
              {copy.canal} · {(copy.lingua || 'pt').toUpperCase()}{copy.country ? ' · ' + copy.country : ''} vs master {(master.lingua || 'pt').toUpperCase()}
            </div>
          </div>
          <button onClick={onClose} style={{ background: 'none', border: 0, cursor: 'pointer' }}>
            <Icon name="x" size={20} />
          </button>
        </div>
        <div style={{ flex: 1, overflowY: 'auto', padding: 20 }}>
          <div style={{ display: 'grid', gridTemplateColumns: '140px 1fr 1fr', gap: 8, alignItems: 'start' }}>
            <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.08em', color: 'var(--text-dim, #7a8fa8)', fontFamily: 'var(--font-mono, monospace)' }}>CAMPO</div>
            <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.08em', color: 'var(--color-primary, #3859D0)', fontFamily: 'var(--font-mono, monospace)' }}>MASTER (PT)</div>
            <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.08em', color: 'var(--text, #112954)', fontFamily: 'var(--font-mono, monospace)' }}>{(copy.lingua || 'pt').toUpperCase()}{copy.country ? ' · ' + copy.country : ''}</div>
            {fields.map(f => {
              const mv = master[f.k] || '';
              const cv = copy[f.k] || '';
              const diff = mv !== cv;
              return (
                <React.Fragment key={f.k}>
                  <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-muted, #4a5e7a)', paddingTop: 10 }}>{f.label}</div>
                  <div style={{ padding: 10, background: 'var(--bg-subtle, #FAFAFA)', borderRadius: 'var(--radius-sm, 6px)', fontSize: 12, color: 'var(--text, #112954)', lineHeight: 1.4, whiteSpace: 'pre-wrap' }}>
                    {mv || <span style={{ color: 'var(--text-dim, #7a8fa8)', fontStyle: 'italic' }}>—</span>}
                  </div>
                  <div style={{ padding: 10, background: diff ? '#FFFBEB' : 'var(--bg-subtle, #FAFAFA)', borderRadius: 'var(--radius-sm, 6px)', fontSize: 12, color: 'var(--text, #112954)', lineHeight: 1.4, whiteSpace: 'pre-wrap', border: diff ? '1px solid #FCD34D' : 'none' }}>
                    {cv || <span style={{ color: 'var(--text-dim, #7a8fa8)', fontStyle: 'italic' }}>—</span>}
                  </div>
                </React.Fragment>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
};

// ─── CtdCopyHistoryModal — histórico de edições ─────────────────────────────
const CtdCopyHistoryModal = ({ copy, onClose }) => {
  const Icon = window.Icon;
  const [versions, setVersions] = React.useState([]);
  const [loading, setLoading]   = React.useState(true);
  React.useEffect(() => {
    CtdAPI.copyVersions(copy.id).then(d => { setVersions(d?.versions || []); setLoading(false); }).catch(() => setLoading(false));
  }, [copy.id]);
  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, background: 'rgba(17,41,84,0.45)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        background: 'var(--bg-surface, #fff)', borderRadius: 'var(--radius-lg, 10px)',
        maxWidth: 900, maxHeight: '90vh', width: '85vw', overflow: 'hidden',
        display: 'flex', flexDirection: 'column',
        boxShadow: 'var(--shadow-huge)',
      }}>
        <div style={{ padding: '16px 20px', borderBottom: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <div>
            <div className="font-display" style={{ fontSize: 16, fontWeight: 700, color: 'var(--text, #112954)' }}>Histórico de edições</div>
            <div style={{ fontSize: 11, color: 'var(--text-muted, #4a5e7a)', marginTop: 2 }}>
              {copy.item_title || `Peça #${copy.comm_plan_item_index || 0}`} · {copy.canal} · {(copy.lingua || 'pt').toUpperCase()}
            </div>
          </div>
          <button onClick={onClose} style={{ background: 'none', border: 0, cursor: 'pointer' }}>
            <Icon name="x" size={20} />
          </button>
        </div>
        <div style={{ flex: 1, overflowY: 'auto', padding: 20 }}>
          {loading ? (
            <div style={{ padding: 40, textAlign: 'center', color: 'var(--text-muted, #4a5e7a)' }}>A carregar…</div>
          ) : versions.length === 0 ? (
            <div style={{ padding: 40, textAlign: 'center', color: 'var(--text-muted, #4a5e7a)', fontSize: 13 }}>
              Sem edições anteriores. As versões são guardadas automaticamente a cada edição de conteúdo.
            </div>
          ) : versions.map((v, i) => {
            const s = v.snapshot || {};
            return (
              <div key={v.id} style={{
                marginBottom: 12, padding: 12,
                background: 'var(--bg-subtle, #FAFAFA)',
                borderRadius: 'var(--radius-sm, 6px)',
                border: '1px solid var(--border-1, #ECEFF5)',
              }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
                  <span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.06em', color: 'var(--color-primary, #3859D0)', fontFamily: 'var(--font-mono, monospace)' }}>V{versions.length - i}</span>
                  <span style={{ fontSize: 10, color: 'var(--text-dim, #7a8fa8)', fontFamily: 'var(--font-mono, monospace)' }}>
                    {new Date(v.created_at).toLocaleString('pt-PT')}{v.changed_by ? ` · ${v.changed_by}` : ''}
                  </span>
                </div>
                {s.copy_imagem && (
                  <div style={{ fontSize: 11, marginBottom: 4 }}><b>Image Headline:</b> {s.copy_imagem}</div>
                )}
                {s.headline && (
                  <div style={{ fontSize: 11, marginBottom: 4 }}><b>Headline:</b> {s.headline}</div>
                )}
                {s.body && (
                  <div style={{ fontSize: 11, marginBottom: 4, whiteSpace: 'pre-wrap' }}><b>Body:</b> {String(s.body).slice(0, 200)}{String(s.body).length > 200 ? '…' : ''}</div>
                )}
                {s.cta && (
                  <div style={{ fontSize: 11, marginBottom: 4 }}><b>CTA:</b> {s.cta}</div>
                )}
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
};

const CtdClampText = ({ text, lines = 3, empty = '' }) => {
  const [open, setOpen] = React.useState(false);
  const t = String(text || '').trim();
  if (!t) return empty ? <span style={{ color: 'var(--text-dim, #94a3b8)' }}>{empty}</span> : null;
  const long = t.length > (lines === 1 ? 90 : lines * 70);
  return (
    <div>
      <div style={open || !long ? { fontSize: 12, lineHeight: 1.5, color: 'var(--text, #112954)', whiteSpace: 'pre-line' } : {
        fontSize: 12, lineHeight: 1.5, color: 'var(--text, #112954)', whiteSpace: 'pre-line',
        display: '-webkit-box', WebkitLineClamp: lines, WebkitBoxOrient: 'vertical', overflow: 'hidden',
      }}>{t}</div>
      {long && (
        <button type="button" onClick={() => setOpen(v => !v)}
          style={{ border: 0, background: 'none', padding: 0, marginTop: 4, fontSize: 11, fontWeight: 600, color: 'var(--color-primary, #3859D0)', cursor: 'pointer' }}>
          {open ? 'mostrar menos' : 'mostrar tudo'}
        </button>
      )}
    </div>
  );
};

const CtdAdContextCard = ({ peca, commPlanIndex, campFull, country, title = 'Ficha desta peça', campanhaId, saveRef, onCopyBase }) => {
  const ficha = React.useMemo(
    () => ctdAdFicha({ peca, commPlanIndex, campFull, country }),
    [peca, commPlanIndex, campFull, country],
  );
  const fromFicha = (f) => ({
    usp: f.usp || '',
    pain: f.pain || '',
    angulo: f.angulo || '',
    persona: f.persona || '',
    cert: f.cert || '',
    core_promise: f.corePromise || '',
    baseline: f.baseline || '',
  });
  const [draft, setDraft] = React.useState(() => fromFicha(ficha));
  const [baseMsg, setBaseMsg] = React.useState('');
  const draftRef = React.useRef(draft);
  draftRef.current = draft;
  const timer = React.useRef(null);
  React.useEffect(() => { setDraft(fromFicha(ficha)); }, [commPlanIndex, campFull?.id]);
  const persist = React.useCallback(async (next) => {
    if (campanhaId == null || commPlanIndex == null) return;
    const b = ficha.brief || {};
    const map = { usp: 'usp', pain: 'pain', angulo: 'angulo', persona: 'persona', cert: 'cert', core_promise: 'corePromise', baseline: 'baseline' };
    const payload = { comm_plan_index: commPlanIndex };
    for (const [k, bk] of Object.entries(map)) {
      const changed = String(next[k] || '').trim() !== String(b[bk] || '').trim();
      payload[k] = changed ? next[k] : null;
    }
    setBaseMsg('A guardar…');
    const res = await fetch(`/api/marketing/campanhas/${campanhaId}/copy-base`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
    const j = await res.json().catch(() => ({}));
    if (!res.ok) {
      setBaseMsg(j.error || 'Não gravou');
      throw new Error(j.error || 'Não gravou a base');
    }
    const changedAny = Object.entries(payload).some(([k, v]) => k !== 'comm_plan_index' && v != null && String(v).trim());
    setBaseMsg(changedAny ? 'Base guardada' : '');
    if (j.copy_base) onCopyBase?.(j.copy_base);
  }, [campanhaId, commPlanIndex, onCopyBase, ficha]);
  const schedule = (next) => {
    clearTimeout(timer.current);
    timer.current = setTimeout(() => { persist(next).catch(() => {}); }, 500);
  };
  React.useEffect(() => {
    if (!saveRef) return undefined;
    saveRef.current = async () => {
      clearTimeout(timer.current);
      await persist(draftRef.current);
    };
    return () => { saveRef.current = null; };
  }, [saveRef, persist]);
  const setField = (k, v) => setDraft(d => {
    const next = { ...d, [k]: v };
    schedule(next);
    return next;
  });
  const offerSlug = String(ficha.offer?.primary_cta || '');
  const offerCta = ({
    testing_roll: 'Pedir Testing Roll',
    sample_request: 'Pedir amostra',
    quote_request: 'Pedido de proposta',
    demo_request: 'Pedido de demonstração',
    call_back: 'Callback comercial',
    visit_showroom: 'Visita ao showroom',
    whatsapp: 'WhatsApp',
    form: 'Formulário',
    call: 'Chamada directa',
    landing: 'Landing page',
  })[offerSlug] || (offerSlug ? offerSlug.replace(/_/g, ' ') : '');
  const offerDetails = String(ficha.offer?.details || '').trim();
  const brief = ficha.brief || {};
  const Grow = ({ value, onChange, changed, min = 72, max = 160, placeholder }) => {
    const ref = React.useRef(null);
    React.useLayoutEffect(() => {
      const el = ref.current;
      if (!el) return;
      el.style.height = 'auto';
      el.style.height = `${Math.min(max, Math.max(min, el.scrollHeight))}px`;
    }, [value, min, max]);
    return (
      <textarea ref={ref} value={value} placeholder={placeholder} onChange={e => onChange(e.target.value)}
        style={{ width: '100%', boxSizing: 'border-box', resize: 'vertical', overflow: 'auto', padding: '8px 10px', border: `1px solid ${changed ? '#FCD34D' : 'var(--border-2, #E3E6EB)'}`, borderRadius: 6, fontSize: 12.5, lineHeight: 1.5, color: 'var(--text, #112954)', fontFamily: 'inherit', background: changed ? '#FFFBEB' : '#fff' }} />
    );
  };
  const Field = ({ k, label, briefKey, min, max, placeholder }) => {
    const changed = String(draft[k] || '').trim() !== String(brief[briefKey] || '').trim();
    return (
      <label style={{ display: 'block' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, marginBottom: 4 }}>
          <span style={{ fontSize: 12, fontWeight: 650, color: '#334155' }}>{label}</span>
          {changed && <span style={{ fontSize: 10, fontWeight: 700, color: '#B45309' }}>alterado</span>}
        </div>
        <Grow value={draft[k] || ''} onChange={v => setField(k, v)} changed={changed} min={min} max={max} placeholder={placeholder} />
      </label>
    );
  };
  const adName = ficha.adTitle ? ficha.adTitle.replace(/^\[..\]\s*/, '') : '';
  return (
    <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
      <div style={{ padding: '12px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)' }}>
        <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.08em', color: 'var(--text-dim, #94a3b8)', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>
          {title}
        </div>
        {adName && (
          <div style={{ marginTop: 4, fontSize: 13, fontWeight: 650, lineHeight: 1.35, color: 'var(--text, #112954)' }}>{adName}</div>
        )}
      </div>
      <div style={{ padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 12 }}>
        <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.06em', color: '#64748b', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>Deste anúncio</div>
        <Field k="usp" briefKey="usp" label="USP" min={88} max={180} />
        <Field k="pain" briefKey="pain" label="Dor" min={72} max={150} />
        <Field k="angulo" briefKey="angulo" label="Ângulo" min={56} max={110} />
        <Field k="persona" briefKey="persona" label="Persona" min={88} max={220} />
        <Field k="cert" briefKey="cert" label="Certificação" min={52} max={96} placeholder="Nenhuma neste anúncio" />
        <div style={{ paddingTop: 4, borderTop: '1px solid var(--border-1, #ECEFF5)', display: 'flex', flexDirection: 'column', gap: 12 }}>
          <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.06em', color: '#64748b', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>Toda a campanha</div>
          <Field k="core_promise" briefKey="corePromise" label="Core promise" min={88} max={180} />
          <Field k="baseline" briefKey="baseline" label="Baseline" min={52} max={110} placeholder="Nenhuma nesta campanha" />
        </div>
        {(offerDetails || offerCta || ficha.allowPrice) && (
          <div style={{ padding: '10px 12px', borderRadius: 6, background: '#F8FAFC', border: '1px solid #E2E8F0', display: 'flex', flexDirection: 'column', gap: 6 }}>
            <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.06em', color: '#64748b', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>Oferta</div>
            {offerDetails && <div style={{ fontSize: 12.5, lineHeight: 1.5, color: '#112954' }}>{offerDetails}</div>}
            {offerCta && <div style={{ fontSize: 12, fontWeight: 700, color: '#3859D0' }}>{offerCta}</div>}
            {ficha.allowPrice && <div style={{ fontSize: 11, fontWeight: 650, color: '#92400E' }}>Preço por m² autorizado neste anúncio</div>}
          </div>
        )}
        {baseMsg && (
          <div style={{ fontSize: 11, color: baseMsg === 'Base guardada' ? '#166534' : '#991B1B' }}>{baseMsg}</div>
        )}
      </div>
    </div>
  );
};

const CtdPipelineStep = ({ n, label, detail, state, onClick }) => {
  const Icon = window.Icon;
  const pal = state === 'done'
    ? { bg: '#DCFCE7', fg: '#166534', ring: '#86EFAC' }
    : state === 'current'
      ? { bg: '#EFF3FF', fg: '#3859D0', ring: '#3859D0' }
      : { bg: '#F8FAFC', fg: '#94A3B8', ring: '#E2E8F0' };
  const body = (
    <div style={{ display: 'flex', alignItems: 'flex-start', gap: 10, textAlign: 'left', width: '100%' }}>
      <div style={{
        width: 22, height: 22, borderRadius: '50%', flexShrink: 0,
        background: pal.bg, color: pal.fg, border: `1.5px solid ${pal.ring}`,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }}>
        {state === 'done'
          ? <Icon name="check" size={11} />
          : <span style={{ fontSize: 10, fontWeight: 700, fontFamily: 'var(--font-mono, monospace)' }}>{n}</span>}
      </div>
      <div style={{ minWidth: 0, paddingTop: 1 }}>
        <div style={{ fontSize: 12, fontWeight: 600, color: pal.fg, lineHeight: 1.2 }}>{label}</div>
        {detail && <div style={{ fontSize: 11, color: 'var(--text-muted, #4a5e7a)', marginTop: 2, lineHeight: 1.35 }}>{detail}</div>}
      </div>
    </div>
  );
  if (!onClick) return body;
  return (
    <button type="button" onClick={onClick} style={{
      display: 'block', width: '100%', border: 0, background: 'none', padding: 0, cursor: 'pointer',
    }}>{body}</button>
  );
};

const CtdCopyPipelineCard = ({
  prompt, imageUrl, imageApproved, needsVideo, imageOnTab, stale, busy, busyPrompt, stepMessage,
  onOpenStep, onNext, hasNext, onReopenToggle, reopen,
  nextLabel,
  children,
  wa, templateApproved, templateStale, waGroup, waImageReady, waHeaderFromAd, onGenerateImage, genStep, genTotal,
}) => {
  const Icon = window.Icon;
  if (wa) {
    const qual = waGroup === 'qualificacao';
    const needsHeader = !!waGroup && !qual;
    const headerReady = !needsHeader || !!waImageReady;
    const tplOk = !!templateApproved && !templateStale && headerReady;
    const go = (tab, extra) => { if (!busy && !busyPrompt) onOpenStep?.(tab, extra); };
    const nextTab = tplOk ? 'peca' : 'prompts';
    const approvalLabel = 'Aprovação Meta';
    const gerarImagem = waGroup === 'abertura' && !headerReady && !waHeaderFromAd;
    const adaptarImagem = !!waHeaderFromAd && !qual && !headerReady;
    const nextLabelAction = !headerReady
      ? (gerarImagem ? 'Gerar Imagem' : 'Adaptar imagem')
      : templateStale ? 'Actualizar template desta peça' : tplOk ? 'Peça final' : approvalLabel;
    return (
      <div style={{ background: '#fff', border: '1px solid #BBF7D0', borderRadius: 8, overflow: 'hidden' }}>
        <div style={{ padding: '10px 14px', borderBottom: '1px solid #DCFCE7', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, background: '#F0FDF4' }}>
          <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#166534', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>{busyPrompt && gerarImagem ? 'A gerar imagem' : 'Copy aprovada'}</div>
          <span style={{ fontSize: 10, fontWeight: 700, color: '#166534', display: 'inline-flex', alignItems: 'center', gap: 4 }}>
            <Icon name="check" size={12} /> {busyPrompt && gerarImagem ? 'Em geração' : tplOk ? 'Pronta para a peça final' : 'Em produção'}
          </span>
        </div>
        <div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 12 }}>
          <CtdPipelineStep n="1" label="Copy" detail="Aprovada nesta peça" state="done" />
          {needsHeader && (
            <CtdPipelineStep n="2" label="Cabeçalho"
              detail={busyPrompt && (gerarImagem || adaptarImagem) ? 'A gerar' : waImageReady ? '1125×600' : (waGroup === 'abertura' && !waHeaderFromAd ? 'Gerar a partir do pitch' : 'Falta adaptar a imagem')}
              state={waImageReady ? 'done' : 'current'} />
          )}
          <CtdPipelineStep n={needsHeader ? '3' : '2'} label={approvalLabel}
            detail={!headerReady ? 'Depois da imagem' : templateStale ? 'Desactualizado' : templateApproved ? 'Aprovado' : 'Por aprovar'}
            state={!headerReady ? 'wait' : tplOk ? 'done' : 'current'}
            onClick={headerReady ? () => go('prompts') : null} />
          <CtdPipelineStep n={needsHeader ? '4' : '3'} label="Peça final"
            detail={tplOk ? 'Pronta para submeter' : 'Depois da aprovação'}
            state={tplOk ? 'current' : 'wait'}
            onClick={tplOk ? () => go('peca') : null} />
          <button type="button" onClick={() => { if (gerarImagem || adaptarImagem) { onGenerateImage?.(); return; } if (headerReady) go(nextTab); }} disabled={busy || busyPrompt || (!headerReady && !gerarImagem && !adaptarImagem)}
            style={{ background: '#3859D0', color: '#fff', border: 0, width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: (busy || busyPrompt) ? 'not-allowed' : 'pointer', opacity: (busy || busyPrompt) ? 0.55 : 1 }}>
            {busyPrompt ? 'A gerar…' : nextLabelAction}
          </button>
          {busyPrompt && gerarImagem && (
            <CtdGenProgress step={genStep || 1} total={genTotal || 5} message={stepMessage || 'A gerar imagem…'} />
          )}
          {hasNext && (
            <button type="button" onClick={onNext} disabled={busy || busyPrompt}
              style={{ background: 'transparent', color: 'var(--text, #112954)', border: '1px solid var(--border-2, #E3E6EB)', padding: '8px 10px', borderRadius: 6, fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>
              {nextLabel || 'Próxima peça →'}
            </button>
          )}
          <button type="button" onClick={onReopenToggle}
            style={{ background: 'transparent', color: 'var(--text, #112954)', border: '1px solid var(--border-2, #E3E6EB)', width: '100%', padding: '8px 10px', borderRadius: 6, fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>
            {reopen ? 'Ocultar alteração de copy' : 'Alterar copy aprovada'}
          </button>
          {reopen && children}
        </div>
      </div>
    );
  }
  const promptApproved = !stale && prompt?.status === 'approved';
  const hasPrompt = !!prompt && !stale;
  const hasImageFile = !stale && !!imageUrl;
  const imageOk = hasImageFile && !!imageApproved;
  const hasVideo = !stale && !!prompt?.video_url;
  const videoApproved = !stale && !!prompt?.video_approved_at;
  const promptState = promptApproved ? 'done' : 'current';
  const imageState = imageOk ? 'done' : promptApproved ? 'current' : 'wait';
  const videoState = !needsVideo ? 'wait'
    : (hasVideo && videoApproved) ? 'done'
    : imageOk ? 'current'
    : 'wait';
  const chainDone = promptApproved && imageOk && (!needsVideo || (hasVideo && videoApproved));
  let nextTab = 'prompts';
  let nextLabelAction = 'Gerar prompt desta peça';
  let nextExtra = null;
  if (!hasPrompt) {
    nextTab = 'prompts';
    nextLabelAction = 'Gerar prompt desta peça';
  } else if (!promptApproved) {
    nextTab = 'prompts';
    nextLabelAction = 'Rever e aprovar prompt';
  } else if (!hasImageFile) {
    nextTab = imageOnTab ? 'imagens' : 'prompts';
    nextLabelAction = 'Gerar imagem';
    nextExtra = imageOnTab ? { generate: true } : null;
  } else if (!imageOk) {
    nextTab = 'imagens';
    nextLabelAction = 'Aprovar imagem';
  } else if (needsVideo && !hasVideo) {
    nextTab = 'video';
    nextLabelAction = 'Gerar vídeo';
  } else if (needsVideo && !videoApproved) {
    nextTab = 'video';
    nextLabelAction = 'Rever e aprovar vídeo';
  } else {
    nextTab = 'peca';
    nextLabelAction = 'Peça final';
  }
  const primaryLabel = busyPrompt ? (stepMessage || 'A gerar prompt…') : nextLabelAction;
  const go = (tab, extra) => { if (!busy && !busyPrompt) onOpenStep?.(tab, extra); };
  return (
    <div style={{ background: '#fff', border: '1px solid #BBF7D0', borderRadius: 8, overflow: 'hidden' }}>
      <div style={{ padding: '10px 14px', borderBottom: '1px solid #DCFCE7', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, background: '#F0FDF4' }}>
        <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#166534', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>
          Copy aprovada
        </div>
        <span style={{ fontSize: 10, fontWeight: 700, color: '#166534', display: 'inline-flex', alignItems: 'center', gap: 4 }}>
          <Icon name="check" size={12} /> {chainDone ? 'Pronta para a peça final' : 'Em produção'}
        </span>
      </div>
      <div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 12 }}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          <CtdPipelineStep n="1" label="Copy" detail="Aprovada nesta peça" state="done" />
          <CtdPipelineStep n="2" label="Prompt"
            detail={stale ? 'Desactualizado' : !prompt ? 'Ainda por gerar' : promptApproved ? 'Aprovado' : 'Gerado · por aprovar'}
            state={promptState}
            onClick={() => go('prompts')} />
          <CtdPipelineStep n="3" label="Imagem"
            detail={imageOk ? 'Aprovada' : hasImageFile ? 'Gerada · por aprovar' : promptApproved ? 'Aguarda geração' : 'Depois do prompt aprovado'}
            state={imageState}
            onClick={imageState !== 'wait' ? () => go(imageOnTab || hasImageFile ? 'imagens' : 'prompts', !hasImageFile && imageOnTab ? { generate: true } : null) : null} />
          {needsVideo && (
            <CtdPipelineStep n="4" label="Vídeo"
              detail={videoApproved ? 'Aprovado' : hasVideo ? 'Gerado · por aprovar' : imageOk ? 'Aguarda geração' : 'Depois da imagem aprovada'}
              state={videoState}
              onClick={videoState !== 'wait' ? () => go('video') : null} />
          )}
        </div>
        <button type="button" onClick={() => go(nextTab, nextExtra)} disabled={busy || busyPrompt}
          style={{
            background: '#3859D0', color: '#fff', border: 0, width: '100%',
            padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700,
            cursor: (busy || busyPrompt) ? 'not-allowed' : 'pointer',
            opacity: (busy || busyPrompt) ? 0.55 : 1,
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 7,
          }}>
          <Icon name="sparkle" size={13} /> {primaryLabel}
        </button>
        {hasNext && (
          <button type="button" onClick={onNext} disabled={busy || busyPrompt}
            style={{
              background: 'transparent', color: 'var(--text, #112954)',
              border: '1px solid var(--border-2, #E3E6EB)', padding: '8px 10px', borderRadius: 6,
              fontSize: 12, fontWeight: 600, cursor: 'pointer',
            }}>
            {nextLabel || 'Próxima peça →'}
          </button>
        )}
        <button type="button" onClick={onReopenToggle}
          style={{
            background: 'transparent', color: 'var(--text, #112954)',
            border: '1px solid var(--border-2, #E3E6EB)', width: '100%',
            padding: '8px 10px', borderRadius: 6, fontSize: 12, fontWeight: 600, cursor: 'pointer',
          }}>
          {reopen ? 'Ocultar alteração de copy' : 'Alterar copy aprovada'}
        </button>
        {reopen && children}
      </div>
    </div>
  );
};

function CtdWaCopyContext({ peca, copy, pack, busy, error, onAdapt, hideAction, brandSlug }) {
  const grupo = peca?.waGroup || '';
  const qual = grupo === 'qualificacao';
  const mimaki = String(brandSlug || '').toLowerCase() === 'mimaki';
  const payload = ctdCopyPayload(copy);
  const adRef = qual ? '' : (payload.ad_ref || (grupo === 'abertura' && !mimaki ? 'AD-01' : ''));
  const titulo = ctdWaGrupoTitulo(grupo, copy?.country || peca?.country || 'PT');
  const hasHeader = !qual && !!pack?.headerImage;
  return (
    <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
      <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)' }}>
        <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>{titulo || 'WhatsApp'}</div>
      </div>
      <div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 10 }}>
        <div style={{ fontSize: 12, lineHeight: 1.45, color: 'var(--text, #112954)' }}>
          {qual
            ? 'Este template vai só com texto. Não leva imagem no cabeçalho.'
            : grupo === 'abertura' && !mimaki
              ? 'O cabeçalho é o filme aplicado, como nos anúncios, sem frase no vinil. O logotipo fica ao centro. Nasce em 1125×600.'
              : `O cabeçalho é a imagem de ${adRef || 'este anúncio'}. O ficheiro fica 1125×600 para o WhatsApp. A imagem do anúncio não muda.`}
        </div>
        {!qual && <div style={{ fontSize: 11, color: 'var(--text-muted, #4a5e7a)' }}>1125×600 · JPEG · até 5 MB</div>}
        {!qual && hasHeader && (
          <img src={pack.headerImage} alt="" style={{ width: '100%', aspectRatio: '1.91 / 1', objectFit: 'contain', background: '#fff', borderRadius: 6, display: 'block' }} />
        )}
        {!qual && !hideAction && (
          <button type="button" onClick={onAdapt} disabled={busy}
            style={{ background: '#3859D0', color: '#fff', border: 0, padding: '8px 10px', borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: busy ? 'not-allowed' : 'pointer', opacity: busy ? 0.6 : 1 }}>
            {grupo === 'abertura' && !mimaki
              ? (busy ? 'A gerar imagem…' : hasHeader ? 'Gerar de novo' : 'Gerar Imagem')
              : (busy ? 'A adaptar…' : hasHeader ? 'Adaptar de novo' : 'Adaptar imagem')}
          </button>
        )}
        {error && <div style={{ fontSize: 11, color: '#991B1B', lineHeight: 1.4 }}>{error}</div>}
      </div>
    </div>
  );
}

// ─── CtdGenerateCopyItemModal — modal per-item para gerar/editar copy de 1 peça ─
// Abre por click na row da tabela OU no botão "Regerar…" dum card.
// - Copy inexistente → mostra contexto da peça + escolha de modelo + botão Gerar
// - Copy existente → mostra CtdCopyPecaCard inline (edit rico) + Regerar com modelo
// Backend usa `include_tone_refs=true` para o Sonnet manter alinhamento com copies já aprovadas.
function CtdOneShotEmailChoice({ busy, erro, onCriar, onBrevo }) {
  const card = {
    flex: 1, minWidth: 220, textAlign: 'left', background: '#fff', border: '1px solid #E3E6EB',
    borderRadius: 12, padding: '16px 16px 14px', cursor: busy ? 'not-allowed' : 'pointer', fontFamily: 'inherit',
  };
  return (
    <div style={{ background: '#fff', border: '1px solid #ECEFF5', borderRadius: 12, padding: 18 }}>
      <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.08em', color: '#3859D0' }}>EMAIL · ONE SHOT</div>
      <div style={{ fontSize: 18, fontWeight: 700, color: '#112954', marginTop: 4 }}>Como queres fazer este email?</div>
      <div style={{ display: 'flex', gap: 12, marginTop: 16, flexWrap: 'wrap' }}>
        <button type="button" disabled={busy} onClick={onCriar} style={card}>
          <div style={{ fontSize: 14, fontWeight: 700, color: '#112954' }}>Criar copy e imagem</div>
          <div style={{ fontSize: 12, color: '#64748b', marginTop: 6, lineHeight: 1.45 }}>Escreves um breve. O Gerar faz o assunto, o texto e o botão. A imagem é o hero 16:9.</div>
        </button>
        <button type="button" disabled={busy} onClick={onBrevo} style={card}>
          <div style={{ fontSize: 14, fontWeight: 700, color: '#112954' }}>Usar um template da Brevo</div>
          <div style={{ fontSize: 12, color: '#64748b', marginTop: 6, lineHeight: 1.45 }}>Escolhes um template que já está na conta e preenches o texto. A imagem nasce depois, a partir desse copy.</div>
        </button>
      </div>
      {erro && <div style={{ color: '#dc2626', fontSize: 12, marginTop: 10 }}>{erro}</div>}
    </div>
  );
}

function ctdBrevoLockedText(text) {
  const t = String(text || '').replace(/\u00ad/g, '').replace(/\s+/g, ' ').trim();
  if (t.length < 2) return true;
  if (/view in browser/i.test(t)) return true;
  if (/unsubscribe/i.test(t)) return true;
  if (/\{\{\s*contact\./i.test(t)) return true;
  if (/this email was sent to/i.test(t)) return true;
  if (/you'?ve received this email/i.test(t)) return true;
  if (/zona industrial|torres novas/i.test(t)) return true;
  return false;
}
function ctdBrevoAssuntoLimpo(subject) {
  let subj = String(subject || '').replace(/[\u200b\u00ad]/g, '');
  subj = subj.replace(/\s+(?:da|de|do|das|dos)\s+\{\{\s*empresa\s*\}\}/gi, '');
  subj = subj.replace(/\{\{[^{}]*\}\}/g, ' ');
  subj = subj.replace(/\s+/g, ' ').trim().replace(/^re:\s*/i, '');
  subj = subj.replace(/\s+([?.!,;:])/g, '$1');
  subj = subj.replace(/\s+(da|de|do|das|dos|para|a|o|na|no|em)\s*([?.!])?$/i, '$2');
  return subj.replace(/\s+/g, ' ').replace(/\s+([?.!])$/g, '$1').trim();
}
function ctdBrevoAlvoRealWorks(plain) {
  if (!/biond real works/i.test(plain)) return '';
  if (/teus clientes|escolher um cliente|um cliente da|portf[oó]lio e um cliente/i.test(plain)) return 'agencias';
  return 'marcas';
}
function ctdBrevoCtaResposta(text) {
  const t = String(text || '').replace(/\s+/g, ' ').trim();
  if (/pedir\s+amostras/i.test(t)) return 'amostras';
  if (/quero\s+saber\s+mais/i.test(t)) return 'saber';
  return '';
}
function ctdBrevoRespostaAmostras(subject, html) {
  const plain = String(html || '').replace(/<style[\s\S]*?<\/style>/gi, ' ').replace(/<[^>]+>/g, ' ').replace(/&nbsp;/gi, ' ').replace(/&amp;/g, '&').replace(/\s+/g, ' ').trim();
  const subj = ctdBrevoAssuntoLimpo(subject);
  const replySubject = (subj ? `Re: ${subj}` : 'Pedido de amostras grátis').slice(0, 180);
  const alvo = ctdBrevoAlvoRealWorks(plain);
  const co2 = /co₂|co2/i.test(plain);
  const lines = ['Olá,', ''];
  if (alvo === 'marcas') lines.push('Quero pedir amostras grátis e avançar com uma campanha real da nossa marca no programa BIOND Real Works.');
  else if (alvo === 'agencias') lines.push('Quero pedir amostras grátis e escolher um cliente e uma campanha real para o programa BIOND Real Works.');
  else if (subj) lines.push(`Quero pedir amostras grátis, em resposta ao email «${subj}».`);
  else lines.push('Quero pedir amostras grátis.');
  if (alvo === 'marcas' && co2) lines.push('', 'Quero ver o filme bio-based na campanha, a redução de CO₂ face ao vinil polimérico e o relatório de impacto.');
  else if (alvo === 'agencias' && co2) lines.push('', 'Quero ver o filme bio-based na campanha, a redução de CO₂ face ao vinil polimérico e o relatório de impacto para o cliente.');
  lines.push('', 'Obrigado.');
  return { subject: replySubject, body: lines.join('\n') };
}
function ctdBrevoRespostaSaber(subject, html) {
  const plain = String(html || '').replace(/<style[\s\S]*?<\/style>/gi, ' ').replace(/<[^>]+>/g, ' ').replace(/&nbsp;/gi, ' ').replace(/&amp;/g, '&').replace(/\s+/g, ' ').trim();
  const subj = ctdBrevoAssuntoLimpo(subject);
  const replySubject = (subj ? `Re: ${subj}` : 'Quero saber mais').slice(0, 180);
  const alvo = ctdBrevoAlvoRealWorks(plain);
  const lines = ['Olá,', ''];
  if (alvo === 'marcas') lines.push('Quero saber mais e avançar com uma campanha real da nossa marca no programa BIOND Real Works.');
  else if (alvo === 'agencias') lines.push('Quero saber mais e escolher um cliente e uma campanha real para o programa BIOND Real Works.');
  else if (subj) lines.push(`Quero saber mais, em resposta ao email «${subj}».`);
  else lines.push('Quero saber mais.');
  lines.push('', 'Obrigado.');
  return { subject: replySubject, body: lines.join('\n') };
}
function ctdBrevoMailtoCta(kind, subject, html, { htmlAttr } = {}) {
  const reply = kind === 'saber' ? ctdBrevoRespostaSaber(subject, html) : ctdBrevoRespostaAmostras(subject, html);
  const query = `subject=${encodeURIComponent(reply.subject)}&body=${encodeURIComponent(reply.body)}`;
  return `mailto:{{remetente_email}}?${htmlAttr ? query.replace(/&/g, '&amp;') : query}`;
}
function ctdBrevoMailtoAmostras(subject, html, opts) {
  return ctdBrevoMailtoCta('amostras', subject, html, opts);
}
function ctdBrevoAmostrasMailto(html, subject) {
  return String(html || '').replace(/<a\b([^>]*?)>([\s\S]*?)<\/a>/gi, (full, attrs, inner) => {
    const text = inner.replace(/<[^>]+>/g, ' ').replace(/&nbsp;/gi, ' ').replace(/\s+/g, ' ').trim();
    const kind = ctdBrevoCtaResposta(text);
    if (!kind) return full;
    const mailto = ctdBrevoMailtoCta(kind, subject, html, { htmlAttr: true });
    const next = /\bhref\s*=/i.test(attrs)
      ? attrs.replace(/\bhref\s*=\s*(["'])[\s\S]*?\1/i, `href="${mailto}"`)
      : ` href="${mailto}"${attrs}`;
    return `<a${next}>${inner}</a>`;
  });
}
function ctdBrevoFixAmostras(doc, subject) {
  if (!doc?.body) return;
  const html = doc.body.innerHTML;
  doc.querySelectorAll('a').forEach(el => {
    const kind = ctdBrevoCtaResposta(el.textContent || '');
    if (!kind) return;
    el.setAttribute('href', ctdBrevoMailtoCta(kind, subject, html));
  });
}
function ctdBrevoCleanHtml(doc) {
  const clone = doc.documentElement.cloneNode(true);
  clone.querySelectorAll('[data-ctd-edit]').forEach(el => {
    el.removeAttribute('contenteditable');
    el.removeAttribute('data-ctd-edit');
    el.removeAttribute('data-ctd-bound');
    el.removeAttribute('data-ctd-fresh');
  });
  clone.querySelectorAll('[data-ctd-img]').forEach(el => {
    el.removeAttribute('data-ctd-img');
    el.removeAttribute('data-ctd-bound');
  });
  clone.querySelectorAll('[data-ctd-footer]').forEach(el => el.removeAttribute('data-ctd-footer'));
  clone.querySelectorAll('[data-ctd-block]').forEach(el => el.removeAttribute('data-ctd-block'));
  clone.querySelectorAll('[data-ctd-logo]').forEach(el => el.removeAttribute('data-ctd-logo'));
  clone.querySelectorAll('[data-ctd-layer]').forEach(el => el.remove());
  const body = clone.querySelector('body');
  if (body) {
    body.removeAttribute('data-ctd-armed');
    if (body.getAttribute('data-ctd-pos') === '1') {
      body.style.position = '';
      body.removeAttribute('data-ctd-pos');
    }
  }
  clone.querySelectorAll('[data-ctd-font]').forEach(el => el.remove());
  clone.querySelectorAll('style').forEach(el => {
    if (String(el.textContent || '').includes('data-ctd-edit')) el.remove();
  });
  return '<!DOCTYPE html>\n' + clone.outerHTML;
}
function ctdBrevoReadPreheader(doc) {
  return String(doc?.querySelector('.nl2go_preheader')?.textContent || '').replace(/\s+/g, ' ').trim();
}
function ctdBrevoWritePreheader(doc, text) {
  if (!doc?.body) return;
  const value = String(text || '').replace(/\s+/g, ' ').trim();
  let el = doc.querySelector('.nl2go_preheader');
  if (!value) {
    if (el) el.textContent = '';
    return;
  }
  if (!el) {
    el = doc.createElement('div');
    el.className = 'nl2go_preheader';
    el.setAttribute('style', 'display:none!important;visibility:hidden;mso-hide:all;font-size:1px;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;');
    doc.body.insertBefore(el, doc.body.firstChild);
  }
  el.textContent = value;
}
function ctdBrevoContentImgs(root) {
  return [...root.querySelectorAll('img')].filter(img => {
    if (img.closest('[data-ctd-footer]') || img.getAttribute('data-ctd-logo') === '1') return false;
    const w = Number(img.getAttribute('width')) || 0;
    return !(w > 0 && w <= 24);
  });
}
function ctdBrevoBlocks(doc) {
  const blocks = new Set();
  const containsFooter = (node) => !!node.querySelector?.('[data-ctd-footer]');
  const climb = (start, images) => {
    let best = null;
    let node = start.parentElement;
    while (node && node !== doc.body) {
      if (node.closest('[data-ctd-footer]') || containsFooter(node) || node.querySelector?.('[data-ctd-logo]')) break;
      const text = (node.innerText || '').replace(/\s+/g, ' ').trim();
      if (text.length > 900) break;
      if (images) {
        if (ctdBrevoContentImgs(node).length !== 1) break;
      } else if (ctdBrevoContentImgs(node).length) break;
      if (node.tagName === 'TABLE' || node.tagName === 'TD') best = node;
      node = node.parentElement;
    }
    return best;
  };
  ctdBrevoContentImgs(doc).forEach(img => {
    const block = climb(img, true);
    if (block && !block.closest('[data-ctd-footer]')) blocks.add(block);
  });
  [...doc.querySelectorAll('[data-ctd-edit], a')].forEach(el => {
    if (el.closest('[data-ctd-footer]') || el.querySelector?.('img')) return;
    const text = (el.textContent || '').replace(/\s+/g, ' ').trim();
    if (text.length < 2 || ctdBrevoLockedText(text)) return;
    const block = climb(el, false);
    if (block && !block.closest('[data-ctd-footer]')) blocks.add(block);
  });
  return [...blocks].filter(block => ![...blocks].some(other => other !== block && block.contains(other)));
}
function ctdBrevoMarkFooter(doc) {
  const marker = /unsubscribe|contact\s*\.\s*email|this email was sent|you'?ve received this email|zona industrial|torres novas/i;
  doc.querySelectorAll('td, p, div, span, a, th').forEach(el => {
    if (el.closest('[data-ctd-footer]')) return;
    const text = el.textContent || '';
    if (!marker.test(text) || text.length > 420) return;
    let node = el;
    while (node && node !== doc.body && (node.textContent || '').length <= 420) {
      node.setAttribute('data-ctd-footer', '1');
      node = node.parentElement;
    }
  });
}
function ctdBrevoMarkLogo(doc) {
  const img = [...doc.querySelectorAll('img')].find(el => {
    if (el.closest('[data-ctd-footer]')) return false;
    return (Number(el.getAttribute('width')) || 0) >= 480;
  });
  if (img) img.setAttribute('data-ctd-logo', '1');
}
function ctdBrevoProtected(el) {
  if (!el) return true;
  if (el.querySelector('[data-ctd-logo], [data-ctd-footer]')) return true;
  const text = (el.textContent || '').replace(/\s+/g, ' ').trim();
  return /view in browser/i.test(text) && text.length < 80;
}
function ctdBrevoHasPayload(table) {
  const text = (table.innerText || '').replace(/\s+/g, ' ').trim();
  return text.length >= 2 || table.querySelectorAll('img').length > 0;
}
function ctdBrevoDirectBands(table) {
  return [...table.querySelectorAll('table')].filter(t => {
    if (t.closest('[data-ctd-footer]')) return false;
    if (!ctdBrevoHasPayload(t)) return false;
    let parent = t.parentElement;
    while (parent && parent !== table) {
      if (parent.tagName === 'TABLE' && ctdBrevoHasPayload(parent)) return false;
      parent = parent.parentElement;
    }
    return true;
  });
}
function ctdBrevoSectionPapel(table) {
  const text = (table.innerText || '').replace(/\s+/g, ' ').trim();
  const imgs = [...table.querySelectorAll('img')].filter(img => !img.closest('[data-ctd-footer]'));
  if (/view in browser/i.test(text) && text.length < 80) return 'fixo';
  if (imgs.some(img => img.getAttribute('data-ctd-logo') === '1') && text.length < 40) return 'logo';
  if (/step\s*\d/i.test(text)) return 'passos';
  if (/€|\bfrom\s+\d/i.test(text)) return 'preco';
  if (!text && imgs.length) return 'grelha';
  if (imgs.length >= 4 && text.length < 40) return 'grelha';
  if (imgs.length && imgs.every(img => (Number(img.getAttribute('width')) || 99) <= 80)) return 'icones';
  if (imgs.length === 1 && text.length >= 8) {
    const w = Number(imgs[0].getAttribute('width')) || 0;
    if (w >= 80 && w < 400) return 'cartao';
  }
  if (imgs.length >= 2 && text.length >= 8) return 'cartoes';
  return 'bloco';
}
function ctdBrevoTextPapel(el) {
  const text = (el.textContent || '').replace(/\s+/g, ' ').trim();
  if (/€|\bfrom\s+\d/i.test(text) && text.length < 24) return 'preco';
  if ((el.tagName === 'A' || el.querySelector?.('a') || el.closest?.('a')) && text.length <= 40 && !/[.!?]$/.test(text)) return 'botao';
  if (/^H[1-4]$/.test(el.tagName) || (text.length <= 42 && !/[.!?]$/.test(text))) return 'titulo';
  return 'texto';
}
function ctdBrevoWriteText(el, value) {
  const plain = (el.textContent || '').replace(/\s+/g, ' ').trim();
  const link = el.tagName === 'A' ? el : el.querySelector?.('a');
  if (link && plain.length <= 40) {
    const leaf = link.querySelector('span') || link;
    const old = (leaf.textContent || '').replace(/\s+/g, ' ').trim();
    leaf.textContent = value;
    [...el.childNodes].forEach(node => {
      if (node.nodeType === 8 && old && node.textContent.includes(old)) node.textContent = node.textContent.split(old).join(value);
    });
    return;
  }
  el.textContent = value;
}
function ctdBrevoImageOnly(table) {
  const text = (table.innerText || '').replace(/\s+/g, ' ').trim();
  return table.querySelectorAll('img').length > 0 && text.length < 2 && !table.querySelector('[data-ctd-logo]');
}
function ctdBrevoSectionGroups(doc) {
  ctdBrevoMarkFooter(doc);
  ctdBrevoMarkLogo(doc);
  const roots = [...doc.querySelectorAll('table')].filter(t => {
    if (t.closest('[data-ctd-footer]') || t.querySelector('[data-ctd-footer]')) return false;
    const parent = t.parentElement?.closest('table');
    return !parent || !!parent.querySelector('[data-ctd-footer]');
  });
  const bands = [];
  const walk = (table) => {
    if (!table || table.closest('[data-ctd-footer]') || table.querySelector('[data-ctd-footer]')) return;
    const kids = ctdBrevoDirectBands(table);
    const rich = kids.filter(k => (k.innerText || '').replace(/\s+/g, ' ').trim().length >= 8);
    if (rich.length >= 2) { kids.forEach(walk); return; }
    if (ctdBrevoHasPayload(table)) bands.push(table);
  };
  roots.forEach(walk);
  const grouped = [];
  bands.forEach(el => {
    const papel = ctdBrevoSectionPapel(el);
    const prev = grouped[grouped.length - 1];
    if (papel === 'grelha' && ctdBrevoImageOnly(el) && prev?.papel === 'grelha' && prev.els.every(ctdBrevoImageOnly)) {
      prev.els.push(el);
      return;
    }
    grouped.push({ papel, els: [el] });
  });
  return grouped.map((g, id) => {
    const textos = [];
    g.els.forEach(el => {
      [...el.querySelectorAll('[data-ctd-edit]')].forEach(node => {
        if (node.closest('[data-ctd-footer]') || node.querySelector('[data-ctd-edit]')) return;
        textos.push({ id: textos.length, papel: ctdBrevoTextPapel(node), el: node });
      });
    });
    const imagens = g.els.reduce((n, el) => n + [...el.querySelectorAll('img')].filter(img => !img.closest('[data-ctd-footer]') && img.getAttribute('data-ctd-logo') !== '1').length, 0);
    return { id, papel: g.papel, els: g.els, textos, imagens };
  });
}
function ctdBrevoDropSlot(node, section) {
  const row = node.closest('tr');
  if (!row || !section || !section.contains(row)) { node.textContent = ''; return; }
  const others = [...row.querySelectorAll('[data-ctd-edit]')].filter(el => el !== node);
  if (others.length || row.querySelector('[data-ctd-logo]')) node.textContent = '';
  else row.remove();
}
function ctdBrevoImageCopy(img) {
  let node = img.parentElement;
  let best = '';
  while (node && node.tagName !== 'BODY') {
    const text = (node.innerText || '').replace(/\s+/g, ' ').trim();
    const imgs = [...node.querySelectorAll('img')].filter(el => !el.closest('[data-ctd-footer]') && el.getAttribute('data-ctd-logo') !== '1');
    if (text.length >= 20 && imgs.length <= 1 && text.length <= 500) best = text;
    if (imgs.length > 1 && best) break;
    node = node.parentElement;
  }
  return best.slice(0, 800);
}
const CTD_SECCAO_LABEL = { bloco: 'Bloco', icones: 'Ícones', passos: 'Passos', cartao: 'Cartão', grelha: 'Grelha', preco: 'Preço' };
const CTD_EDIT_SEL = 'p, h1, h2, h3, h4, a, span, td, th, li, div, font, strong, b, em';
const CTD_INLINE = new Set(['BR', 'SPAN', 'STRONG', 'EM', 'B', 'I', 'A', 'FONT']);
function ctdBrevoFolhaTexto(el) {
  if (!el || el.nodeType !== 1) return false;
  if (el.closest('[data-ctd-footer], [data-ctd-layer]')) return false;
  if (el.getAttribute('data-ctd-edit') === '1') return false;
  if (el.parentElement?.closest('[data-ctd-edit]')) return false;
  if (el.querySelector('img, table')) return false;
  if ([...el.children].some(c => !CTD_INLINE.has(c.tagName))) return false;
  if (ctdBrevoLockedText(el.textContent)) return false;
  return true;
}
function ctdBrevoLimparExemplo(root) {
  if (!root?.querySelectorAll) return;
  const slots = [...root.querySelectorAll('p, h1, h2, h3, h4, li, a, td, th, div')].filter(el => {
    if (el.closest('[data-ctd-footer], [data-ctd-layer]')) return false;
    if (el.querySelector('img, table, p, h1, h2, h3, h4, li')) return false;
    const text = (el.textContent || '').replace(/\s+/g, ' ').trim();
    if (text.length < 2) return false;
    if (el.tagName !== 'A' && el.querySelector('a') && text.length <= 40) return false;
    if (ctdBrevoLockedText(text) && text.length < 80) return false;
    return true;
  });
  const leaves = slots.filter(el => !slots.some(other => other !== el && el.contains(other)));
  leaves.forEach(el => {
    const block = el.closest('p, h1, h2, h3, h4, li');
    const target = block && !block.querySelector('img, table') ? block : el;
    const plain = (target.textContent || '').replace(/\s+/g, ' ').trim();
    const link = target.tagName === 'A' ? target : target.querySelector?.('a');
    const asButton = !!(link && plain.length <= 40 && !target.querySelector('img, table, p'));
    if (asButton) ctdBrevoWriteText(target, '');
    else target.textContent = '';
    const leaf = asButton ? (link.querySelector('span') || link) : target;
    leaf.setAttribute('data-ctd-edit', '1');
    leaf.setAttribute('data-ctd-fresh', '1');
    leaf.removeAttribute('data-ctd-bound');
  });
}
function ctdBrevoCatalogo(html) {
  try {
    const raw = String(html || '');
    if (!raw.trim()) return [];
    const doc = new DOMParser().parseFromString(raw, 'text/html');
    ctdBrevoMarkFooter(doc);
    ctdBrevoMarkLogo(doc);
    const seen = new Set();
    const out = [];
    ctdBrevoSectionGroups(doc).forEach(g => {
      if (g.papel === 'fixo' || g.papel === 'logo') return;
      const papel = g.papel === 'cartoes' ? 'cartao' : g.papel;
      if (seen.has(papel) || !CTD_SECCAO_LABEL[papel]) return;
      seen.add(papel);
      out.push({ papel, html: g.els.map(el => el.outerHTML).join('') });
    });
    return out;
  } catch (e) { return []; }
}
function ctdBrevoAncoraInsercao(block) {
  if (!block?.isConnected) return null;
  if (block.tagName === 'TABLE') return block;
  return block.closest('table') || block;
}
function ctdBrevoAntesDoRodape(doc) {
  const footer = doc.querySelector('[data-ctd-footer]');
  if (!footer) return null;
  let node = footer;
  while (node.parentElement && node.parentElement !== doc.body) {
    const parent = node.parentElement;
    const parentText = (parent.textContent || '').replace(/\s+/g, ' ').trim();
    const nodeText = (node.textContent || '').replace(/\s+/g, ' ').trim();
    if (parentText.length > nodeText.length + 80) break;
    node = parent;
  }
  return node;
}

function CtdOneShotEmailTeste({ userEmail, disabled, onSend }) {
  const [open, setOpen] = React.useState(false);
  const [pessoas, setPessoas] = React.useState([]);
  const [q, setQ] = React.useState('');
  const [sel, setSel] = React.useState(() => new Set(userEmail ? [String(userEmail).toLowerCase()] : []));
  const [loading, setLoading] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const [msg, setMsg] = React.useState('');
  const box = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    let live = true;
    setLoading(true);
    CtdAPI.destinatariosTeste()
      .then(d => { if (live) setPessoas(Array.isArray(d?.pessoas) ? d.pessoas : []); })
      .catch(e => { if (live) setMsg(e.message || 'Não consegui ler as contas do portal.'); })
      .finally(() => { if (live) setLoading(false); });
    return () => { live = false; };
  }, [open]);
  React.useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (box.current && !box.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [open]);
  const filtered = pessoas.filter(p => {
    if (!q) return true;
    return `${p.nome} ${p.email}`.toLowerCase().includes(q.toLowerCase());
  });
  const toggle = (email) => {
    setSel(prev => {
      const next = new Set(prev);
      if (next.has(email)) next.delete(email);
      else if (next.size >= 10) return prev;
      else next.add(email);
      return next;
    });
  };
  const enviar = async () => {
    if (!sel.size || busy) return;
    setBusy(true);
    setMsg('');
    try {
      const res = await onSend([...sel]);
      const n = Number(res?.sent || sel.size);
      setMsg(`Enviado para ${n} destinatário${n === 1 ? '' : 's'}.`);
    } catch (e) {
      setMsg(e.message || 'Não consegui enviar o teste.');
    }
    setBusy(false);
  };
  return (
    <div ref={box} style={{ position: 'relative' }}>
      <button type="button" disabled={disabled || busy} onClick={() => { setMsg(''); setOpen(v => !v); }} style={{ width: '100%', background: '#fff', color: '#112954', border: '1px solid #E3E6EB', borderRadius: 8, padding: '9px 12px', fontSize: 13, fontWeight: 700, cursor: disabled || busy ? 'not-allowed' : 'pointer', opacity: disabled ? 0.55 : 1 }}>
        {busy ? 'A enviar…' : 'Enviar teste'}
      </button>
      {open && (
        <div style={{ position: 'absolute', left: 0, bottom: 'calc(100% + 8px)', zIndex: 30, width: '100%', boxSizing: 'border-box', background: '#fff', border: '1px solid #E3E6EB', borderRadius: 10, boxShadow: '0 12px 32px rgba(17,41,84,.14)', padding: 12 }}>
          <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: '#64748b', marginBottom: 8 }}>Email de teste</div>
          <input value={q} onChange={e => setQ(e.target.value)} placeholder="Procurar nome ou email…" style={{ display: 'block', width: '100%', boxSizing: 'border-box', border: '1px solid #E3E6EB', borderRadius: 8, padding: '8px 10px', fontSize: 12, marginBottom: 8 }} />
          <div style={{ maxHeight: 200, overflowY: 'auto', border: '1px solid #E3E6EB', borderRadius: 8 }}>
            {loading && <div style={{ padding: 10, fontSize: 12, color: '#64748b' }}>A carregar contas do portal…</div>}
            {!loading && filtered.length === 0 && <div style={{ padding: 10, fontSize: 12, color: '#64748b' }}>Sem resultados.</div>}
            {filtered.map(p => {
              const on = sel.has(p.email);
              return (
                <label key={p.email} style={{ display: 'flex', gap: 8, alignItems: 'flex-start', padding: '7px 8px', borderBottom: '1px solid #F1F5F9', cursor: 'pointer', background: on ? 'rgba(56,89,208,0.06)' : 'transparent' }}>
                  <input type="checkbox" checked={on} onChange={() => toggle(p.email)} style={{ marginTop: 2 }} />
                  <span style={{ minWidth: 0 }}>
                    <span style={{ display: 'block', fontSize: 12, fontWeight: 600, color: '#112954' }}>{p.nome}</span>
                    <span style={{ display: 'block', fontSize: 10, color: '#64748b' }}>{p.email}</span>
                  </span>
                </label>
              );
            })}
          </div>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 10, gap: 8 }}>
            <span style={{ fontSize: 11, color: '#64748b' }}>{sel.size}/10</span>
            <button type="button" disabled={!sel.size || busy} onClick={enviar} style={{ background: '#3859D0', color: '#fff', border: 0, borderRadius: 8, padding: '6px 12px', fontSize: 12, fontWeight: 700, cursor: !sel.size || busy ? 'not-allowed' : 'pointer', opacity: sel.size && !busy ? 1 : 0.45 }}>
              {busy ? 'A enviar…' : `Enviar (${sel.size})`}
            </button>
          </div>
          <div style={{ fontSize: 11, color: '#64748b', marginTop: 8, lineHeight: 1.4 }}>Sai com o assunto, o preview text e o email que estás a ver.</div>
          {msg && <div style={{ marginTop: 8, fontSize: 12, color: /^Enviado/.test(msg) ? '#166534' : '#b91c1c' }}>{msg}</div>}
        </div>
      )}
    </div>
  );
}

function CtdBrevoPreview({ html, frameRef: frameRefProp, onLoad, extraindo = false, faseExtracao = '', empty = 'Escolhe um template para ver o email.' }) {
  const ownRef = React.useRef(null);
  const frameRef = frameRefProp || ownRef;
  const previewBoxRef = React.useRef(null);
  const [emailMax, setEmailMax] = React.useState(false);
  const [previewH, setPreviewH] = React.useState(0);
  const [boxW, setBoxW] = React.useState(0);
  React.useEffect(() => { setPreviewH(0); }, [html]);
  React.useEffect(() => {
    if (!emailMax) return undefined;
    const onKey = (e) => { if (e.key === 'Escape') setEmailMax(false); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [emailMax]);
  React.useEffect(() => {
    if (!emailMax) return undefined;
    const frame = frameRef.current;
    const doc = frame?.contentDocument;
    if (!doc?.head || !doc.body) return undefined;
    const style = doc.createElement('style');
    style.id = 'ctd-max-scroll';
    style.textContent = 'html,body{height:auto!important;min-height:100%!important;overflow:auto!important;}';
    doc.head.appendChild(style);
    frame.setAttribute('scrolling', 'auto');
    return () => { style.remove(); frame.setAttribute('scrolling', 'no'); };
  }, [emailMax, html]);
  React.useEffect(() => {
    const frame = frameRef.current;
    if (!frame || emailMax || extraindo) return undefined;
    let stop = false;
    let obs = null;
    let imgs = [];
    let timer = 0;
    const fit = () => {
      const doc = frame.contentDocument;
      if (stop || !doc?.body) return;
      frame.style.height = '1px';
      doc.documentElement.style.height = 'auto';
      doc.body.style.height = 'auto';
      doc.documentElement.style.minHeight = '0';
      doc.body.style.minHeight = '0';
      doc.documentElement.style.overflow = 'visible';
      doc.body.style.overflow = 'visible';
      ctdBrevoMarkFooter(doc);
      const feet = [...doc.querySelectorAll('[data-ctd-footer]')];
      let h = 0;
      if (feet.length) {
        const base = doc.body.getBoundingClientRect().top;
        h = Math.ceil(Math.max(...feet.map(el => el.getBoundingClientRect().bottom - base)));
      }
      if (h < 80) h = Math.ceil(Math.max(doc.body.scrollHeight, doc.body.offsetHeight));
      h += 2;
      doc.documentElement.style.overflow = 'hidden';
      doc.body.style.overflow = 'hidden';
      if (h < 80) {
        frame.style.height = '';
        return;
      }
      frame.style.height = h + 'px';
      setPreviewH(prev => (Math.abs((prev || 0) - h) < 2 ? prev : h));
    };
    const watch = () => {
      const doc = frame.contentDocument;
      if (!doc?.body) return;
      obs?.disconnect();
      imgs.forEach(img => img.removeEventListener('load', fit));
      fit();
      obs = new MutationObserver(fit);
      obs.observe(doc.body, { subtree: true, childList: true, characterData: true });
      imgs = [...doc.querySelectorAll('img')];
      imgs.forEach(img => { if (!img.complete) img.addEventListener('load', fit); });
      window.clearTimeout(timer);
      timer = window.setTimeout(fit, 400);
    };
    watch();
    frame.addEventListener('load', watch);
    return () => {
      stop = true;
      frame.removeEventListener('load', watch);
      obs?.disconnect();
      imgs.forEach(img => img.removeEventListener('load', fit));
      window.clearTimeout(timer);
    };
  }, [html, extraindo, emailMax, boxW]);
  React.useEffect(() => {
    const el = previewBoxRef.current;
    if (!el) return undefined;
    const read = () => setBoxW(el.clientWidth || 0);
    read();
    const ro = new ResizeObserver(read);
    ro.observe(el);
    return () => ro.disconnect();
  }, [emailMax, html]);
  const narrow = !emailMax && boxW > 0 && boxW < 600;
  return (
    <div style={emailMax
      ? { position: 'fixed', inset: 16, zIndex: 1100, background: '#fff', border: '1px solid #ECEFF5', borderRadius: 10, minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column', boxShadow: '0 24px 80px rgba(15,23,42,.28)' }
      : { background: '#fff', border: '1px solid #ECEFF5', borderRadius: 10, minHeight: 420, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
      <div style={{ padding: '8px 8px 8px 14px', borderBottom: '1px solid #ECEFF5', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexShrink: 0 }}>
        <span style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.08em', color: '#94a3b8' }}>PRÉ-VISUALIZAÇÃO</span>
        <button type="button" onClick={() => setEmailMax(v => !v)} aria-label={emailMax ? 'Reduzir o email' : 'Maximizar o email'} title={emailMax ? 'Reduzir' : 'Maximizar'} style={{ width: 28, height: 28, borderRadius: 6, border: '1px solid #E3E6EB', background: '#fff', color: '#475569', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: 0 }}>
          {emailMax
            ? <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"><path d="M5 1v4H1M9 5h4V1M13 9h-4v4M5 13V9H1" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg>
            : <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"><path d="M1 5V1h4M9 1h4v4M13 9v4H9M5 13H1V9" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg>}
        </button>
      </div>
      <div ref={previewBoxRef} style={{ position: 'relative', height: emailMax ? '100%' : (extraindo ? 640 : Math.max(80, Math.round((previewH || 640) * (narrow ? boxW / 600 : 1)))), flex: emailMax ? '1 1 auto' : 'none', minHeight: 0, overflow: emailMax ? 'auto' : 'hidden' }}>
        {html
          ? <iframe ref={frameRef} title="Template Brevo" sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox" scrolling={emailMax ? 'yes' : 'no'} srcDoc={html} onLoad={onLoad} style={{ width: narrow ? 600 : '100%', height: narrow ? (previewH || 640) : '100%', border: 0, background: '#f4f4f4', overflow: emailMax ? 'auto' : 'hidden', transform: narrow ? `scale(${boxW / 600})` : undefined, transformOrigin: 'top left' }} />
          : <div style={{ padding: 24, fontSize: 13, color: '#64748b' }}>{empty}</div>}
        {extraindo && (
          <div style={{ position: 'absolute', inset: 0, background: '#fff', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 20, zIndex: 2, padding: 24 }}>
            <div style={{ display: 'flex', alignItems: 'flex-end', gap: 4, height: 48 }}>
              {[0, 1, 2, 3, 4, 5, 6].map(i => <div key={i} style={{ width: 4, height: 32, background: 'var(--color-primary, #3859D0)', borderRadius: 4, animation: 'ctdWave 1s ease-in-out infinite', animationDelay: (i * 0.1) + 's', transformOrigin: 'bottom' }} />)}
            </div>
            <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.10em', color: 'var(--color-primary, #3859D0)', fontFamily: 'var(--font-mono, monospace)', textAlign: 'center' }}>
              {(faseExtracao || 'A ler o documento…').replace('…', '').toUpperCase()}
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 8, width: '100%', maxWidth: 280 }}>
              {['A ler o documento…', 'A escolher as secções…', 'A confirmar os factos…', 'A corrigir os factos…'].map((label, i, all) => {
                const actual = all.indexOf(faseExtracao);
                const at = actual < 0 ? 0 : actual;
                const done = i < at;
                const active = i === at;
                return (
                  <div key={label} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, fontWeight: active ? 700 : 500, color: done ? '#15803d' : active ? '#3859D0' : '#94a3b8' }}>
                    <span style={{ width: 14, textAlign: 'center', flexShrink: 0 }}>{done ? '✓' : active ? '●' : '○'}</span>
                    <span>{label.replace('…', '')}</span>
                  </div>
                );
              })}
            </div>
            <style>{`@keyframes ctdWave { 0%,100%{transform:scaleY(0.25)} 50%{transform:scaleY(1)} }`}</style>
          </div>
        )}
      </div>
    </div>
  );
}
window.CtdBrevoPreview = CtdBrevoPreview;
window.CtdBreadcrumb = CtdBreadcrumb;

function CtdOneShotBrevoEditor({ campanhaId, commPlanIndex, peca, copy, userEmail, locked, onSaved, finalAction = null }) {
  const [list, setList] = React.useState([]);
  const [pick, setPick] = React.useState(null);
  const [params, setParams] = React.useState({});
  const [subject, setSubject] = React.useState(peca?.brevo_subject || '');
  const [preheader, setPreheader] = React.useState(peca?.brevo_preheader || '');
  const [frameHtml, setFrameHtml] = React.useState('');
  const [catalogo, setCatalogo] = React.useState([]);
  const [extraido, setExtraido] = React.useState(false);
  const [docFile, setDocFile] = React.useState(null);
  const [extraindo, setExtraindo] = React.useState(false);
  const [faseExtracao, setFaseExtracao] = React.useState('');
  const [kbCheck, setKbCheck] = React.useState(null);
  const [seccaoNota, setSeccaoNota] = React.useState('');
  const [publico, setPublico] = React.useState(peca?.brevo_publico || '');
  const [imgMenu, setImgMenu] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [busy, setBusy] = React.useState(false);
  const [erro, setErro] = React.useState('');
  const [saved, setSaved] = React.useState(!!copy?.id);
  const [reeditar, setReeditar] = React.useState(false);
  const frameRef = React.useRef(null);
  const subjectRef = React.useRef(subject);
  subjectRef.current = subject;
  React.useEffect(() => {
    const doc = frameRef.current?.contentDocument;
    if (doc?.body) ctdBrevoFixAmostras(doc, subject);
  }, [subject, frameHtml]);
  const fileRef = React.useRef(null);
  const docRef = React.useRef(null);
  const imgIndexRef = React.useRef(null);
  const preheaderSeeded = React.useRef(false);
  const hoverBlockRef = React.useRef(null);
  const editorUi = React.useRef({ layer: null, place: () => {}, seq: 0 });
  const travado = !!finalAction || (locked && !reeditar);
  const lockedRef = React.useRef(travado);
  lockedRef.current = travado;
  React.useEffect(() => { setReeditar(false); }, [copy?.id, commPlanIndex]);
  const senderRef = React.useRef('');
  const loadGenRef = React.useRef(0);
  const appliedChoiceRef = React.useRef('');
  const pieceKeyRef = React.useRef('');
  React.useEffect(() => {
    if (!imgMenu) return;
    const close = () => { imgIndexRef.current = null; setImgMenu(null); };
    const onKey = (e) => { if (e.key === 'Escape') close(); };
    const onDown = (e) => { if (e.target?.closest?.('[data-ctd-img-menu]')) return; close(); };
    const onFrame = (e) => { if (e.target?.closest?.('img[data-ctd-img]')) return; close(); };
    document.addEventListener('keydown', onKey);
    document.addEventListener('mousedown', onDown);
    const frameDoc = frameRef.current?.contentDocument;
    frameDoc?.addEventListener('mousedown', onFrame);
    return () => {
      document.removeEventListener('keydown', onKey);
      document.removeEventListener('mousedown', onDown);
      frameDoc?.removeEventListener('mousedown', onFrame);
    };
  }, [imgMenu]);
  React.useEffect(() => {
    let live = true;
    setLoading(true);
    CtdAPI.brevoTemplates()
      .then(d => { if (live) setList(Array.isArray(d?.templates) ? d.templates : []); })
      .catch(e => { if (live) setErro(e.message || 'Não consegui ler os templates.'); })
      .finally(() => { if (live) setLoading(false); });
    return () => { live = false; };
  }, []);
  React.useEffect(() => {
    const pieceKey = String(commPlanIndex);
    if (pieceKeyRef.current !== pieceKey) {
      pieceKeyRef.current = pieceKey;
      appliedChoiceRef.current = '';
    }
    const id = peca?.brevo_template_id;
    if (!id) return;
    if (appliedChoiceRef.current === String(id)) return;
    const gen = ++loadGenRef.current;
    let live = true;
    CtdAPI.brevoTemplate(id).then(d => {
      if (!live || gen !== loadGenRef.current || !d?.id) return;
      setPick(d);
      const stored = peca?.brevo_params || {};
      const next = {};
      (d.params || []).forEach(name => { next[name] = stored[name] || ''; });
      setParams(next);
      setSubject(peca?.brevo_subject || d.subject || '');
      setPreheader(peca?.brevo_preheader || d.previewText || '');
      setPublico(peca?.brevo_publico || '');
      preheaderSeeded.current = false;
      hoverBlockRef.current = null;
      setExtraido(false);
      setCatalogo(ctdBrevoCatalogo(d.html || ''));
      senderRef.current = String(d.senderEmail || '').trim();
      const payload = (copy?.payload && typeof copy.payload === 'object') ? copy.payload : {};
      const copyMatches = payload.brevo_template_id == null || payload.brevo_template_id === '' || String(payload.brevo_template_id) === String(d.id);
      const savedHtml = String(peca?.brevo_html || (copyMatches ? payload.brevo_html : '') || '');
      const subject0 = (peca?.brevo_html ? peca?.brevo_subject : '')
        || (copyMatches ? payload.brevo_subject : '')
        || peca?.brevo_subject
        || d.subject
        || '';
      if (!peca?.brevo_html && copyMatches && payload.brevo_subject) setSubject(payload.brevo_subject);
      if (!peca?.brevo_preheader && copyMatches && payload.brevo_preheader) setPreheader(payload.brevo_preheader);
      setFrameHtml(ctdBrevoAmostrasMailto(savedHtml || d.html || '', subject0));
    }).catch(e => { if (live && gen === loadGenRef.current) setErro(e.message || 'Não consegui abrir o template.'); });
    return () => { live = false; };
  }, [peca?.brevo_template_id, commPlanIndex]);
  const docOf = () => frameRef.current?.contentDocument || null;
  const ligarTexto = (doc) => {
    if (!doc?.body) return;
    [...doc.body.querySelectorAll(CTD_EDIT_SEL)].forEach(el => {
      if (el.getAttribute('data-ctd-edit') === '1') return;
      if (!ctdBrevoFolhaTexto(el)) return;
      if ([...el.querySelectorAll(CTD_EDIT_SEL)].some(ctdBrevoFolhaTexto)) return;
      el.setAttribute('data-ctd-edit', '1');
    });
    [...doc.body.querySelectorAll('[data-ctd-edit]')].forEach(el => {
      if (el.getAttribute('data-ctd-bound') === '1' || el.closest('[data-ctd-footer], [data-ctd-layer]')) return;
      el.setAttribute('data-ctd-bound', '1');
      const amostras = el.tagName === 'A' && !!ctdBrevoCtaResposta(el.textContent || '');
      el.setAttribute('contenteditable', (lockedRef.current || amostras) ? 'false' : 'true');
      el.addEventListener('input', () => setSaved(false));
      if (el.tagName === 'A' && amostras) {
        el.addEventListener('click', (e) => {
          const href = el.getAttribute('href') || '';
          const sender = String(senderRef.current || '').trim();
          if (!/^mailto:/i.test(href) || !sender || !/\{\{\s*remetente_email\s*\}\}/i.test(href)) return;
          e.preventDefault();
          window.open(href.replace(/\{\{\s*remetente_email\s*\}\}/gi, sender));
        });
      } else if (el.tagName === 'A') el.addEventListener('click', e => e.preventDefault());
    });
  };
  const ligarImagens = (doc) => {
    if (!doc?.body) return;
    let next = 0;
    doc.body.querySelectorAll('img').forEach(img => {
      const raw = img.getAttribute('data-ctd-img');
      const n = raw == null || raw === '' ? NaN : Number(raw);
      if (!Number.isNaN(n)) next = Math.max(next, n + 1);
    });
    [...doc.body.querySelectorAll('img')].forEach(img => {
      if (img.getAttribute('data-ctd-bound') === '1') return;
      if (!img.getAttribute('data-ctd-img')) {
        img.setAttribute('data-ctd-img', String(next));
        next += 1;
      }
      img.setAttribute('data-ctd-bound', '1');
      img.addEventListener('load', () => editorUi.current.place());
      img.addEventListener('click', (e) => {
        if (lockedRef.current || img.closest('[data-ctd-footer]')) return;
        e.preventDefault();
        e.stopPropagation();
        const index = img.getAttribute('data-ctd-img');
        if (imgIndexRef.current === index) {
          imgIndexRef.current = null;
          setImgMenu(null);
          return;
        }
        const frameBox = frameRef.current.getBoundingClientRect();
        const box = img.getBoundingClientRect();
        imgIndexRef.current = index;
        setImgMenu({
          index,
          logo: img.getAttribute('data-ctd-logo') === '1',
          x: Math.max(12, frameBox.left + box.left),
          y: Math.min(window.innerHeight - 160, frameBox.top + box.bottom + 8),
        });
      });
    });
  };
  const ligarBlocos = (doc) => {
    const layer = editorUi.current.layer;
    if (!doc?.body || !layer || lockedRef.current) return;
    const place = editorUi.current.place;
    ctdBrevoBlocks(doc).forEach(block => {
      if (block.getAttribute('data-ctd-block') || ctdBrevoProtected(block)) return;
      const id = String(editorUi.current.seq);
      editorUi.current.seq += 1;
      block.setAttribute('data-ctd-block', id);
      const btn = doc.createElement('button');
      btn.type = 'button';
      btn.setAttribute('data-ctd-del', '1');
      btn.setAttribute('data-ctd-del-for', id);
      btn.textContent = 'Apagar';
      btn.style.cssText = 'display:none;position:absolute;pointer-events:auto;z-index:21;background:#fff;color:#b91c1c;border:1px solid #fecaca;border-radius:6px;padding:3px 8px;font:700 11px sans-serif;cursor:pointer;box-shadow:0 2px 8px rgba(15,23,42,.12)';
      const show = () => {
        hoverBlockRef.current = block;
        [...layer.querySelectorAll('[data-ctd-del]')].forEach(other => { if (other !== btn) other.style.display = 'none'; });
        btn.style.display = 'block';
        place();
      };
      const hide = (e) => {
        const next = e.relatedTarget;
        if (next === btn || block.contains(next)) return;
        btn.style.display = 'none';
      };
      block.addEventListener('mouseenter', show);
      btn.addEventListener('mouseenter', show);
      block.addEventListener('mouseleave', hide);
      btn.addEventListener('mouseleave', hide);
      btn.addEventListener('click', (e) => {
        e.preventDefault();
        e.stopPropagation();
        if (hoverBlockRef.current === block) hoverBlockRef.current = null;
        block.remove();
        btn.remove();
        imgIndexRef.current = null;
        setImgMenu(null);
        setSaved(false);
      });
      layer.appendChild(btn);
    });
    place();
  };
  const armPreview = () => {
    const doc = docOf();
    if (!doc?.body) return;
    ctdBrevoFixAmostras(doc, subjectRef.current);
    if (doc.body.dataset.ctdArmed) return;
    doc.body.dataset.ctdArmed = '1';
    ctdBrevoMarkFooter(doc);
    ctdBrevoMarkLogo(doc);
    if (!preheaderSeeded.current) {
      const found = ctdBrevoReadPreheader(doc);
      if (found) setPreheader(prev => prev || found);
      preheaderSeeded.current = true;
    }
    if (doc.head && !doc.querySelector('[data-ctd-font]')) {
      const imports = [...doc.querySelectorAll('style')].flatMap(el => [...String(el.textContent || '').matchAll(/@import\s+url\((['"]?)(https?:[^'")]+)\1\)/gi)].map(m => m[2]));
      imports.forEach(href => {
        const font = doc.createElement('link');
        font.rel = 'stylesheet';
        font.href = href;
        font.setAttribute('data-ctd-font', '1');
        doc.head.appendChild(font);
      });
    }
    const style = doc.createElement('style');
    style.textContent = '[data-ctd-edit="1"]{cursor:text}[data-ctd-edit="1"]:hover,[data-ctd-edit="1"]:focus{outline:1px dashed #3859D0;outline-offset:2px}[data-ctd-fresh="1"]:empty:before{content:"Escreve aqui";color:#94a3b8}img[data-ctd-img]{cursor:pointer}img[data-ctd-img]:hover{outline:2px solid #3859D0;outline-offset:2px}[data-ctd-block]:hover{outline:1px dashed #b91c1c;outline-offset:2px}';
    (doc.head || doc.body).appendChild(style);
    ligarTexto(doc);
    ligarImagens(doc);
    if (lockedRef.current) return;
    doc.body.style.position = 'relative';
    doc.body.setAttribute('data-ctd-pos', '1');
    const layer = doc.createElement('div');
    layer.setAttribute('data-ctd-layer', '1');
    layer.style.cssText = 'position:absolute;left:0;top:0;width:0;height:0;overflow:visible;z-index:20;pointer-events:none;';
    doc.body.appendChild(layer);
    const place = () => {
      if (!doc.body) return;
      const origin = doc.body.getBoundingClientRect();
      [...layer.querySelectorAll('[data-ctd-del]')].forEach(btn => {
        const block = doc.querySelector(`[data-ctd-block="${btn.getAttribute('data-ctd-del-for')}"]`);
        if (!block) { btn.remove(); return; }
        const box = block.getBoundingClientRect();
        btn.style.top = `${box.top - origin.top + 6}px`;
        btn.style.left = `${Math.max(0, box.right - origin.left - 74)}px`;
      });
    };
    editorUi.current = { layer, place, seq: 0 };
    ligarBlocos(doc);
    doc.defaultView?.addEventListener('scroll', place);
  };
  const adicionarSeccao = (item) => {
    const doc = docOf();
    if (!doc?.body || lockedRef.current || extraindo || !item?.html) return;
    if (!doc.body.dataset.ctdArmed) armPreview();
    const holder = doc.createElement('div');
    holder.innerHTML = item.html;
    ctdBrevoLimparExemplo(holder);
    const nodes = [...holder.childNodes];
    const anchor = ctdBrevoAncoraInsercao(hoverBlockRef.current && doc.contains(hoverBlockRef.current) ? hoverBlockRef.current : null);
    if (anchor?.parentNode) {
      let after = anchor;
      nodes.forEach(node => {
        after.parentNode.insertBefore(node, after.nextSibling);
        if (node.nodeType === 1) after = node;
      });
    } else {
      const before = ctdBrevoAntesDoRodape(doc);
      nodes.forEach(node => {
        if (before?.parentNode) before.parentNode.insertBefore(node, before);
        else doc.body.appendChild(node);
      });
    }
    ligarTexto(doc);
    ligarImagens(doc);
    ligarBlocos(doc);
    setImgMenu(null);
    imgIndexRef.current = null;
    setSaved(false);
    setErro('');
  };
  const choose = async (t) => {
    const gen = ++loadGenRef.current;
    const chosenId = String(t.id);
    appliedChoiceRef.current = chosenId;
    setErro('');
    setBusy(true);
    try {
      const detail = await CtdAPI.brevoTemplate(t.id);
      if (gen !== loadGenRef.current) return;
      const savedItem = await CtdAPI.oneShotEmail(campanhaId, {
        comm_plan_index: commPlanIndex,
        modo: 'brevo',
        brevo_template_id: detail.id,
        brevo_template_name: detail.name,
        brevo_subject: detail.subject || '',
        brevo_params: {},
      });
      if (gen !== loadGenRef.current) return;
      appliedChoiceRef.current = String(detail.id);
      senderRef.current = String(detail.senderEmail || '').trim();
      setPick(detail);
      const next = {};
      (detail.params || []).forEach(name => { next[name] = ''; });
      setParams(next);
      setSubject(detail.subject || '');
      setPreheader(detail.previewText || '');
      setPublico('');
      setSeccaoNota('');
      setKbCheck(null);
      preheaderSeeded.current = false;
      hoverBlockRef.current = null;
      setExtraido(false);
      setCatalogo(ctdBrevoCatalogo(detail.html || ''));
      setDocFile(null);
      setFrameHtml(ctdBrevoAmostrasMailto(detail.html || '', detail.subject || ''));
      setSaved(false);
      onSaved?.(savedItem);
    } catch (e) {
      if (gen !== loadGenRef.current) return;
      appliedChoiceRef.current = '';
      setErro(e.message || 'Não consegui escolher o template.');
    } finally {
      if (gen === loadGenRef.current) setBusy(false);
    }
  };
  const currentHtml = () => {
    const doc = docOf();
    if (!doc?.documentElement) throw new Error('O email ainda não está pronto.');
    return ctdBrevoCleanHtml(doc);
  };
  const persistir = async () => {
    if (!pick?.id) throw new Error('Escolhe um template.');
    const doc = docOf();
    ctdBrevoWritePreheader(doc, preheader);
    const title = doc?.querySelector('title');
    if (title) title.textContent = subject;
    const savedItem = await CtdAPI.oneShotEmail(campanhaId, {
      comm_plan_index: commPlanIndex,
      modo: 'brevo',
      brevo_template_id: pick.id,
      brevo_template_name: pick.name,
      brevo_subject: subject,
      brevo_preheader: preheader,
      brevo_publico: publico,
      brevo_params: params,
      brevo_html: currentHtml(),
      save_copy: true,
    });
    setSaved(true);
    onSaved?.(savedItem);
    return savedItem;
  };
  const guardar = async () => {
    setErro('');
    setBusy(true);
    try { await persistir(); }
    catch (e) { setErro(e.message || 'Não gravei o texto.'); }
    setBusy(false);
  };
  const aprovar = async () => {
    setBusy(true);
    setErro('');
    try {
      let id = copy?.id;
      let base = copy;
      if (!saved || !id) {
        const savedItem = await persistir();
        id = savedItem?.copy?.id || id;
        base = savedItem?.copy || base;
      }
      if (!id) throw new Error('Guarda o texto antes de aprovar.');
      await CtdAPI.approveCopy(campanhaId, id, { approver_email: userEmail });
      setReeditar(false);
      onSaved?.({ copy: { ...(base || {}), id, status: 'aprovado' } });
    } catch (e) { setErro(e.message || 'Não aprovei.'); }
    setBusy(false);
  };
  const escreverSeccoes = (groups, sections, { apagar } = { apagar: false }) => {
    const doc = docOf();
    const byId = new Map((sections || []).map(s => [Number(s.id), s]));
    groups.forEach(s => {
      const out = byId.get(s.id);
      if (apagar && !out?.keep) {
        s.els.forEach(el => { if (!ctdBrevoProtected(el)) el.remove(); });
        return;
      }
      if (!out?.keep && apagar) return;
      const texts = new Map((out?.textos || []).map(t => [Number(t.id), String(t.text || '').trim()]));
      s.textos.forEach(t => {
        const value = texts.get(t.id);
        if (!t.el?.isConnected) return;
        if (!value && apagar) ctdBrevoDropSlot(t.el, s.els.find(el => el.contains(t.el)) || s.els[0]);
        else if (value) ctdBrevoWriteText(t.el, value);
      });
    });
    if (apagar && doc) {
      [...doc.querySelectorAll('[data-ctd-del]')].forEach(btn => {
        if (!doc.querySelector(`[data-ctd-block="${btn.getAttribute('data-ctd-del-for')}"]`)) btn.remove();
      });
    }
  };
  const varrerCorrecoes = (doc, correcoes) => {
    (correcoes || []).forEach(c => {
      const antes = String(c.antes || '').replace(/\s+/g, ' ').trim();
      const depois = String(c.depois || '').replace(/\s+/g, ' ').trim();
      if (antes.length < 2 || !depois || antes === depois) return;
      doc.querySelectorAll('[data-ctd-edit]').forEach(el => {
        if (!el.isConnected || el.querySelector('[data-ctd-edit]')) return;
        const plain = (el.textContent || '').replace(/\s+/g, ' ').trim();
        if (!plain.includes(antes)) return;
        ctdBrevoWriteText(el, plain.split(antes).join(depois));
      });
    });
  };
  const extrairDocumento = async () => {
    const doc = docOf();
    if (!docFile) { setErro('Escolhe um docx ou um PDF.'); return; }
    if (!doc?.body) { setErro('O email ainda não está pronto.'); return; }
    if (!doc.body.dataset.ctdArmed) armPreview();
    const groups = ctdBrevoSectionGroups(doc).filter(s => s.papel !== 'fixo' && s.papel !== 'logo');
    if (!groups.length) { setErro('Este template não tem secções para preencher.'); return; }
    const inventario = groups.map(s => ({
      id: s.id,
      papel: s.papel === 'cartoes' ? 'cartao' : s.papel,
      imagens: s.imagens,
      textos: s.textos.map(t => ({ id: t.id, papel: t.papel })),
    }));
    setExtraindo(true);
    setFaseExtracao('A ler o documento…');
    setKbCheck(null);
    setSeccaoNota('');
    setErro('');
    try {
      const lido = await CtdAPI.oneShotEmailDocumento(campanhaId, docFile, { passo: 'texto' });
      setFaseExtracao('A escolher as secções…');
      const sec = await CtdAPI.oneShotEmailDocumentoSeccoes(campanhaId, {
        texto: lido.texto,
        assunto: lido.assunto,
        subject,
        preheader,
        sections: inventario,
      });
      escreverSeccoes(groups, sec.sections, { apagar: true });
      varrerCorrecoes(doc, sec.correcoes);
      if (sec.subject) setSubject(sec.subject);
      if (sec.preheader != null) setPreheader(sec.preheader);
      if (sec.publico) setPublico(sec.publico);
      setSeccaoNota(sec.nota || '');
      const kept = (sec.sections || []).filter(s => s.keep);
      setFaseExtracao('A confirmar os factos…');
      const rev = await CtdAPI.oneShotEmailDocumentoRever(campanhaId, {
        subject: sec.subject,
        preheader: sec.preheader,
        sections: kept,
      });
      let kb = rev.kb || { estado: 'sem_kb', notas: [] };
      let correcoes = Array.isArray(sec.correcoes) ? sec.correcoes.slice() : [];
      if (kb.estado === 'atencao' && (kb.notas || []).length) {
        setFaseExtracao('A corrigir os factos…');
        const fix = await CtdAPI.oneShotEmailDocumentoCorrigir(campanhaId, {
          subject: sec.subject,
          preheader: sec.preheader,
          sections: kept,
          notas: kb.notas,
        });
        escreverSeccoes(groups, fix.sections, { apagar: false });
        varrerCorrecoes(doc, fix.kb?.correcoes);
        if (fix.subject) setSubject(fix.subject);
        if (fix.preheader != null) setPreheader(fix.preheader);
        kb = fix.kb || kb;
        correcoes = correcoes.concat(fix.kb?.correcoes || []);
      }
      if (correcoes.length) {
        const seen = new Set();
        kb = {
          estado: 'corrigido',
          notas: kb.notas || [],
          correcoes: correcoes.filter(c => {
            const key = `${c.motivo}|${c.antes}`;
            if (seen.has(key)) return false;
            seen.add(key);
            return true;
          }).slice(0, 6),
        };
      }
      setKbCheck(kb);
      setImgMenu(null);
      imgIndexRef.current = null;
      if (doc?.body) {
        ligarTexto(doc);
        editorUi.current.place();
      }
      setExtraido(true);
      setSaved(false);
    } catch (e) { setErro(e.message || 'Não consegui extrair o documento.'); }
    setExtraindo(false);
    setFaseExtracao('');
  };
  const trocarImagem = (url) => {
    const doc = docOf();
    const img = doc?.querySelector(`img[data-ctd-img="${imgIndexRef.current}"]`);
    if (!img || !url) return;
    img.src = url;
    setSaved(false);
    setImgMenu(null);
  };
  const carregarImagem = async (file) => {
    if (!file) return;
    setBusy(true);
    setErro('');
    try {
      const res = await CtdAPI.oneShotEmailImagem(campanhaId, file);
      trocarImagem(res.url);
    } catch (e) { setErro(e.message || 'Não carreguei a imagem.'); }
    setBusy(false);
  };
  const apagarImagem = () => {
    const doc = docOf();
    const img = doc?.querySelector(`img[data-ctd-img="${imgIndexRef.current}"]`);
    const block = img?.closest('[data-ctd-block]');
    if (!block || block.closest('[data-ctd-footer]')) { setErro('Este elemento fica no email.'); return; }
    doc.querySelector(`[data-ctd-del-for="${block.getAttribute('data-ctd-block')}"]`)?.remove();
    block.remove();
    imgIndexRef.current = null;
    setImgMenu(null);
    setSaved(false);
  };
  const gerarImagem = async () => {
    const doc = docOf();
    const img = doc?.querySelector(`img[data-ctd-img="${imgIndexRef.current}"]`);
    if (!saved || !copy?.id) { setErro('Primeiro guarda o texto. A Digi usa esse copy para a aplicação da imagem.'); return; }
    if (!img?.src || imgMenu?.logo) return;
    setBusy(true);
    setErro('');
    try {
      const wide = (img.naturalWidth || Number(img.getAttribute('width')) || 600) >= 280;
      const res = await CtdAPI.oneShotEmailGerarImagem(campanhaId, {
        comm_plan_index: commPlanIndex,
        image_url: img.src,
        aspect: wide ? '16:9' : '1:1',
        section_text: ctdBrevoImageCopy(img),
        publico,
      });
      trocarImagem(res.url);
    } catch (e) { setErro(e.message || 'Não consegui gerar a imagem.'); }
    setBusy(false);
  };
  const reabrirEdicao = () => {
    lockedRef.current = false;
    setReeditar(true);
    const doc = docOf();
    if (!doc?.body) return;
    doc.querySelectorAll('[data-ctd-edit]').forEach(el => {
      const resposta = el.tagName === 'A' && !!ctdBrevoCtaResposta(el.textContent || '');
      el.setAttribute('contenteditable', resposta ? 'false' : 'true');
    });
    if (!doc.querySelector('[data-ctd-layer]')) {
      delete doc.body.dataset.ctdArmed;
      armPreview();
    }
  };
  return (
    <>
    <div style={{ display: 'grid', gridTemplateColumns: 'minmax(280px, 360px) minmax(0, 1fr)', gap: 16, alignItems: 'start' }}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        {finalAction ? (
          <div style={{ background: '#fff', border: '1px solid #ECEFF5', borderRadius: 10, padding: 12, display: 'flex', flexDirection: 'column', gap: 12 }}>
            <div>
              <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.06em', color: '#64748b' }}>ASSUNTO</div>
              <div style={{ marginTop: 6, fontSize: 13, fontWeight: 600, color: '#112954', lineHeight: 1.4 }}>{subject || peca?.brevo_subject || '—'}</div>
            </div>
            <div>
              <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.06em', color: '#64748b' }}>PREVIEW TEXT</div>
              <div style={{ marginTop: 6, fontSize: 13, color: '#475569', lineHeight: 1.4 }}>{preheader || peca?.brevo_preheader || '—'}</div>
            </div>
            {finalAction}
          </div>
        ) : (<>
        <div style={{ background: '#fff', border: '1px solid #ECEFF5', borderRadius: 10, padding: 12 }}>
          <label style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.06em', color: '#64748b' }}>
            TEMPLATE BREVO
            <select
              value={pick ? String(pick.id) : ''}
              disabled={loading || busy || travado}
              onChange={e => {
                const t = list.find(x => String(x.id) === e.target.value);
                if (t && String(t.id) !== String(pick?.id)) choose(t);
              }}
              style={{ display: 'block', width: '100%', boxSizing: 'border-box', marginTop: 8, border: '1px solid #E3E6EB', borderRadius: 8, padding: '8px 10px', fontSize: 13, fontWeight: 600, color: '#112954', background: '#fff' }}
            >
              <option value="">{loading ? 'A ler a Brevo…' : 'Escolhe um template'}</option>
              {list.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
            </select>
          </label>
        </div>
        {pick && (
          <div style={{ background: '#fff', border: '1px solid #ECEFF5', borderRadius: 10, padding: 12, display: 'flex', flexDirection: 'column', gap: 8 }}>
            <label style={{ fontSize: 12, fontWeight: 700, color: '#475569' }}>
              Assunto
              <input value={subject} disabled={travado} onChange={e => { setSubject(e.target.value); setSaved(false); }} style={{ display: 'block', width: '100%', boxSizing: 'border-box', marginTop: 4, border: '1px solid #E3E6EB', borderRadius: 8, padding: '8px 10px', fontSize: 13, fontWeight: 500 }} />
            </label>
            <label style={{ fontSize: 12, fontWeight: 700, color: '#475569' }}>
              Preview text
              <input value={preheader} disabled={travado} onChange={e => { setPreheader(e.target.value); setSaved(false); }} placeholder="A frase na caixa de entrada, a seguir ao assunto." style={{ display: 'block', width: '100%', boxSizing: 'border-box', marginTop: 4, border: '1px solid #E3E6EB', borderRadius: 8, padding: '8px 10px', fontSize: 13, fontWeight: 500 }} />
            </label>
            {!travado && (
              <div>
                <div style={{ fontSize: 12, fontWeight: 700, color: '#475569' }}>Documento</div>
                <button type="button" onClick={() => docRef.current?.click()} disabled={busy || extraindo} style={{ marginTop: 4, width: '100%', background: '#fff', color: '#112954', border: '1px solid #E3E6EB', borderRadius: 8, padding: '8px 12px', fontSize: 13, fontWeight: 700, cursor: busy || extraindo ? 'not-allowed' : 'pointer' }}>{docFile ? docFile.name : 'Carregar docx ou PDF'}</button>
                <button type="button" onClick={extrairDocumento} disabled={busy || extraindo || !docFile} style={{ marginTop: 8, width: '100%', background: '#fff', color: '#3859D0', border: '1px solid #3859D0', borderRadius: 8, padding: '8px 12px', fontSize: 13, fontWeight: 700, cursor: busy || extraindo || !docFile ? 'not-allowed' : 'pointer', opacity: docFile && !extraindo ? 1 : 0.55 }}>{extraindo ? (faseExtracao || 'A ler o documento…') : 'Extrair e colocar'}</button>
                {seccaoNota && <div style={{ fontSize: 12, color: '#475569', marginTop: 8, lineHeight: 1.4 }}>{seccaoNota}</div>}
                {kbCheck?.estado === 'ok' && <div style={{ fontSize: 12, color: '#166534', marginTop: 8, lineHeight: 1.4 }}>Confirmado com a base de conhecimento. Sem correcções.</div>}
                {kbCheck?.estado === 'corrigido' && (
                  <div style={{ marginTop: 8, background: '#F0FDF4', border: '1px solid #BBF7D0', borderRadius: 8, padding: '8px 10px' }}>
                    <div style={{ fontSize: 12, fontWeight: 700, color: '#166534' }}>A base de conhecimento corrigiu este texto.</div>
                    {(kbCheck.correcoes || []).map((c, i) => (
                      <div key={i} style={{ fontSize: 12, color: '#14532D', marginTop: 6, lineHeight: 1.4 }}>
                        <span style={{ textDecoration: 'line-through', color: '#64748b' }}>{c.antes}</span>
                        <span> {c.depois}</span>
                        {c.motivo ? <span style={{ display: 'block', color: '#166534' }}>{c.motivo}</span> : null}
                      </div>
                    ))}
                  </div>
                )}
                {kbCheck?.estado === 'sem_kb' && <div style={{ fontSize: 12, color: '#92400E', marginTop: 8, lineHeight: 1.4 }}>A base de conhecimento não devolveu excertos desta marca. O texto ficou como no documento.</div>}
                {kbCheck?.estado === 'atencao' && (
                  <div style={{ marginTop: 8, background: '#FFFBEB', border: '1px solid #FDE68A', borderRadius: 8, padding: '8px 10px' }}>
                    <div style={{ fontSize: 12, fontWeight: 700, color: '#92400E' }}>A base de conhecimento contradiz este texto.</div>
                    {(kbCheck.notas || []).map((n, i) => (
                      <div key={i} style={{ fontSize: 12, color: '#78350F', marginTop: 6, lineHeight: 1.4 }}>
                        {n.trecho ? <span style={{ fontWeight: 700 }}>{n.trecho}. </span> : null}{n.problema}
                      </div>
                    ))}
                  </div>
                )}
              </div>
            )}
            {!travado && (extraido || peca?.brevo_html || copy?.payload?.brevo_html) && catalogo.length > 0 && (
              <div>
                <div style={{ fontSize: 12, fontWeight: 700, color: '#475569' }}>Adicionar secção</div>
                <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 6 }}>
                  {catalogo.map(s => (
                    <button key={s.papel} type="button" disabled={busy || extraindo} onClick={() => adicionarSeccao(s)} style={{ background: '#fff', color: '#112954', border: '1px solid #E3E6EB', borderRadius: 8, padding: '6px 10px', fontSize: 12, fontWeight: 700, cursor: busy || extraindo ? 'not-allowed' : 'pointer' }}>{CTD_SECCAO_LABEL[s.papel]}</button>
                  ))}
                </div>
                <div style={{ fontSize: 11, color: '#64748b', marginTop: 6, lineHeight: 1.4 }}>Entra abaixo da secção onde passaste o rato. Se não estiveres em nenhuma, entra antes do rodapé. O texto de exemplo sai e as imagens do template ficam.</div>
              </div>
            )}
            {!travado && (
              <button type="button" onClick={guardar} disabled={busy} style={{ background: '#3859D0', color: '#fff', border: 0, borderRadius: 8, padding: '9px 12px', fontSize: 13, fontWeight: 700, cursor: busy ? 'not-allowed' : 'pointer' }}>{busy ? 'A guardar…' : 'Guardar'}</button>
            )}
            <CtdOneShotEmailTeste
              userEmail={userEmail}
              disabled={busy || !pick}
              onSend={async (emails) => {
                if (!subject.trim()) throw new Error('Escreve o assunto.');
                const doc = docOf();
                ctdBrevoWritePreheader(doc, preheader);
                const title = doc?.querySelector('title');
                if (title) title.textContent = subject;
                return CtdAPI.oneShotEmailTeste(campanhaId, {
                  comm_plan_index: commPlanIndex,
                  emails,
                  subject,
                  preheader,
                  html: currentHtml(),
                  actor_email: userEmail,
                });
              }}
            />
            {!travado && (
              <button type="button" onClick={aprovar} disabled={busy || !pick} style={{ background: '#1F8A52', color: '#fff', border: 0, borderRadius: 8, padding: '9px 12px', fontSize: 13, fontWeight: 700, cursor: busy || !pick ? 'not-allowed' : 'pointer', opacity: busy || !pick ? 0.55 : 1 }}>{busy ? 'A aprovar…' : 'Aprovar e continuar'}</button>
            )}
            {locked && !reeditar && (
              <>
                <div style={{ fontSize: 12, color: '#166534', fontWeight: 700 }}>Texto aprovado neste template.</div>
                <button type="button" onClick={reabrirEdicao} style={{ background: '#fff', color: '#112954', border: '1px solid #E3E6EB', borderRadius: 8, padding: '9px 12px', fontSize: 13, fontWeight: 700, cursor: 'pointer' }}>Editar novamente</button>
              </>
            )}
          </div>
        )}
        </>)}
        {erro && <div style={{ color: '#dc2626', fontSize: 12 }}>{erro}</div>}
      </div>
      <CtdBrevoPreview html={frameHtml} frameRef={frameRef} onLoad={armPreview} extraindo={extraindo} faseExtracao={faseExtracao} />
    </div>
      <input ref={fileRef} type="file" accept="image/png,image/jpeg,image/webp" hidden onChange={e => { const file = e.target.files?.[0]; e.target.value = ''; carregarImagem(file); }} />
      <input ref={docRef} type="file" accept=".docx,.pdf,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document" hidden onChange={e => { setDocFile(e.target.files?.[0] || null); setKbCheck(null); setSeccaoNota(''); e.target.value = ''; }} />
      {imgMenu && ReactDOM.createPortal(
        <div data-ctd-img-menu="1" style={{ position: 'fixed', left: imgMenu.x, top: imgMenu.y, zIndex: 1200, width: 280, background: '#fff', border: '1px solid #E3E6EB', borderRadius: 10, boxShadow: '0 12px 40px rgba(15,23,42,.16)', padding: 12 }}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
            <div style={{ fontSize: 12, fontWeight: 700, color: '#112954' }}>{imgMenu.logo ? 'Logótipo' : 'Imagem'}</div>
            <button type="button" onClick={() => { imgIndexRef.current = null; setImgMenu(null); }} aria-label="Fechar" style={{ border: 0, background: 'transparent', color: '#64748b', fontSize: 16, lineHeight: 1, cursor: 'pointer', padding: 0 }}>×</button>
          </div>
          <button type="button" disabled={busy} onClick={() => fileRef.current?.click()} style={{ width: '100%', background: '#fff', color: '#112954', border: '1px solid #E3E6EB', borderRadius: 8, padding: '8px 10px', fontSize: 13, fontWeight: 700, cursor: 'pointer' }}>Carregar ficheiro</button>
          {!imgMenu.logo && (
            <button type="button" disabled={busy} onClick={apagarImagem} style={{ width: '100%', marginTop: 8, background: '#fff', color: '#b91c1c', border: '1px solid #fecaca', borderRadius: 8, padding: '8px 10px', fontSize: 13, fontWeight: 700, cursor: 'pointer' }}>Apagar</button>
          )}
          {imgMenu.logo ? (
            <div style={{ fontSize: 11, color: '#64748b', marginTop: 8, lineHeight: 1.4 }}>O logótipo carrega-se por ficheiro.</div>
          ) : (
            <>
              <button type="button" disabled={busy || !saved} onClick={gerarImagem} style={{ width: '100%', marginTop: 8, background: '#3859D0', color: '#fff', border: 0, borderRadius: 8, padding: '8px 10px', fontSize: 13, fontWeight: 700, cursor: busy || !saved ? 'not-allowed' : 'pointer', opacity: saved ? 1 : 0.55 }}>{busy ? 'A gerar…' : 'Gerar imagem'}</button>
              <div style={{ fontSize: 11, color: saved ? '#92400E' : '#64748b', marginTop: 6, lineHeight: 1.4 }}>{saved ? 'Gerar imagem tem custo. A Digi usa o texto desta secção e mostra a aplicação no trabalho de quem recebe o email, sem texto na imagem.' : 'Primeiro guarda o texto. A Digi usa esse copy para a aplicação da imagem.'}</div>
            </>
          )}
        </div>,
        document.body
      )}
    </>
  );
}

const CtdGenerateCopyItemModal = ({ campanhaId, copy, peca, commPlanIndex, userEmail, brandSlug, brandName, reloadCopies, reloadPrompts, reloadImagens, onNavigate, proposta = {}, campFull = null, prompts = [], imagens = [], onClose, onDone, onPrev, onNext, currentPos, totalItems, inline = false, onCopyBase, onCopySaved, onPlanItem }) => {
  const Icon = window.Icon;
  const [modelo, setModelo]     = React.useState('opus-4-7'); // sempre Opus
  const [busy, setBusy]         = React.useState(false);
  const [step, setStep]         = React.useState({ step: 0, total: 0, message: '' });
  const [error, setError]       = React.useState(null);
  const [donePayload, setDone]  = React.useState(null);
  const [localCopy, setLocalCopy] = React.useState(null);
  const [saving, setSaving]     = React.useState(false);
  const [approving, setApproving] = React.useState(false);
  const [userNote, setUserNote] = React.useState('');
  const [reopenCopy, setReopenCopy] = React.useState(false);
  const [busyPrompt, setBusyPrompt] = React.useState(false);
  const [waHeaderBusy, setWaHeaderBusy] = React.useState(false);
  const [waHeaderStep, setWaHeaderStep] = React.useState({ step: 1, total: 5, message: 'A gerar imagem…' });
  const [waHeaderError, setWaHeaderError] = React.useState(null);
  const [feedbackHistory, setFeedbackHistory] = React.useState(null); // null=a carregar, []=vazio, [...]=dados
  const [previewForm, setPreviewForm] = React.useState(() => ctdCopyToPreviewForm(copy));
  const [emailModo, setEmailModo] = React.useState(peca?.email_modo || '');
  const [brevoOpen, setBrevoOpen] = React.useState(false);
  const [modoBusy, setModoBusy] = React.useState(false);
  const [modoErro, setModoErro] = React.useState('');
  const cardRef                 = React.useRef(null);

  // Reset state interno quando navega para outro item (prev/next via breadcrumb externo)
  React.useEffect(() => {
    setLocalCopy(null);
    setDone(null);
    setError(null);
    setStep({ step: 0, total: 0, message: '' });
    setUserNote('');
    setReopenCopy(false);
    setWaHeaderBusy(false);
    setWaHeaderStep({ step: 1, total: 5, message: 'A gerar imagem…' });
    setWaHeaderError(null);
    setPreviewForm(ctdCopyToPreviewForm(copy));
    setEmailModo(peca?.email_modo || '');
    setBrevoOpen(false);
    setModoErro('');
  }, [copy?.id, commPlanIndex, peca?.email_modo]);

  // localCopy (imediato após geração) tem prioridade sobre copy prop (pode ser stale)
  const activeCopy = localCopy || copy;
  React.useEffect(() => {
    setPreviewForm(ctdCopyToPreviewForm(activeCopy));
  }, [activeCopy?.id]);
  const isApproved              = activeCopy?.status === 'aprovado';
  const CTD_VIS = ['social_post', 'email_html', 'email', 'wa_template', 'wa_message', 'blog_post', 'landing_page', 'meta_ads', 'briefing_interno'];
  const _isInternalPiece = (peca?.content_type === 'briefing_interno' || activeCopy?.content_type === 'briefing_interno'
    || String(peca?.canal || activeCopy?.canal || '').includes('interno')
    || String(peca?.canal || activeCopy?.canal || '').includes('portal_notificacao'));
  const _isMetaPiece = peca?.canal === 'meta_ads' || peca?.content_type === 'meta_ads' || copy?.canal === 'meta_ads' || activeCopy?.canal === 'meta_ads';
  const _isWaPiece = ctdIsWaPiece(peca, activeCopy);
  const libraryPiece = !!peca?.waGroup;
  const waLivePack = _isWaPiece ? ctdWaPackFromForm(previewForm && previewForm.wa_name != null ? previewForm : ctdWaFormFields(activeCopy)) : null;
  const waStored = _isWaPiece ? ctdWaPackFromCopy(activeCopy) : null;
  const waBlockers = waLivePack ? ctdWaBlockers(waLivePack) : [];
  const canGenerateImage = isApproved && peca?.email_modo !== 'brevo' && (CTD_VIS.includes(activeCopy?.content_type || peca?.content_type || '') || _isMetaPiece || _isInternalPiece);
  const thisPrompt = React.useMemo(
    () => ctdPromptForPeca(prompts, commPlanIndex, peca, activeCopy),
    [prompts, commPlanIndex, peca, activeCopy],
  );
  const thisImageUrl = React.useMemo(
    () => ctdImageForPeca(prompts, imagens, commPlanIndex, peca, activeCopy),
    [prompts, imagens, commPlanIndex, peca, activeCopy],
  );

  // P3 — Histórico de notas de feedback para esta marca/canal
  React.useEffect(() => {
    if (!brandSlug) return;
    const canal = activeCopy?.canal || peca?.canal || '';
    fetch(`/api/marketing/skill-feedback/list?brand_slug=${encodeURIComponent(brandSlug)}&status=pending_review`)
      .then(r => r.ok ? r.json() : [])
      .then(items => {
        const filtered = (Array.isArray(items) ? items : [])
          .filter(i => !canal || i.canal === canal)
          .slice(0, 4);
        setFeedbackHistory(filtered);
      })
      .catch(() => setFeedbackHistory([]));
  }, [brandSlug, activeCopy?.canal]);

  const handleSave = async () => {
    if (!cardRef.current) return;
    setSaving(true);
    try { await cardRef.current.save(); }
    catch (e) { alert('Erro ao guardar: ' + e.message); }
    setSaving(false);
  };
  const handleApproveCopy = async () => {
    if (!cardRef.current) return;
    setApproving(true);
    try {
      if (cardRef.current.save) await cardRef.current.save();
    } catch (e) {
      alert('Erro ao guardar: ' + e.message);
      setApproving(false);
      return;
    }
    try {
      await cardRef.current.approve();
      setLocalCopy(prev => ({ ...(prev || activeCopy || {}), status: 'aprovado' }));
      reloadCopies?.();
      const waFromAd = String(brandSlug || campFull?.brand_slug || '').toLowerCase() === 'mimaki';
      if (peca?.waGroup === 'abertura' && !waFromAd) {
        if (waStored?.headerImage && waStored?.headerClaim) openProductionStep('prompts');
        else setWaHeaderError('Gera o cabeçalho a partir do pitch antes de continuar.');
      } else if (_isWaPiece) {
        const needsHeader = peca?.waGroup && peca.waGroup !== 'qualificacao';
        if (!needsHeader || waStored?.headerImage) openProductionStep('prompts');
      }
    } catch (e) { alert('Erro ao aprovar: ' + e.message); }
    setApproving(false);
  };

  const openProductionStep = (tab, extra) => {
    const pieceIdx = commPlanIndex ?? copy?.comm_plan_item_index;
    const country = copy?.country || peca?.country || null;
    const titulo = peca?.titulo || peca?.title || copy?.item_title || null;
    onNavigate?.({ tab, idx: pieceIdx, country, titulo, contentType: peca?.content_type || copy?.content_type || null, waGroup: peca?.waGroup || null, ...(extra || {}) });
  };


  const idx = commPlanIndex != null ? commPlanIndex : copy?.comm_plan_item_index;
  const country = copy?.country || null;
  const pecaTitle = peca?.titulo || copy?.item_title || `Peça #${(idx != null ? idx + 1 : '?')}`;

  // Reset estado local quando navega para outro item
  const handleNav = (navFn) => {
    if (busy) return;
    setDone(null); setError(null); setLocalCopy(null);
    setStep({ step: 0, total: 0, message: '' });
    navFn?.();
  };
  const pecaSubtitulo = peca?.subtitulo || '';
  const canalLabel = peca ? ctdCanalLabel(peca) : (copy?.canal || copy?.content_type || '—');
  const usp = peca?.body || copy?.body || '';
  const plannedDate = peca?.planned_date || null;

  const saveFeedback = async (nota) => {
    if (!nota?.trim() || !brandSlug) return;
    try {
      await fetch(`/api/marketing/copy/${activeCopy?.id || 'null'}/feedback`, {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          nota: nota.trim(), brand_slug: brandSlug,
          campanha_id: campanhaId,
          content_type: peca?.content_type || copy?.content_type,
          canal: copy?.canal || peca?.canal,
          lingua: copy?.lingua || 'pt',
          country: copy?.country || country || null,
          user_email: userEmail,
        }),
      });
    } catch {}
  };

  const baseSaveRef = React.useRef(null);
  const handleGenerate = async () => {
    if (idx == null) { setError('Peça sem comm_plan_item_index'); return; }
    setBusy(true); setError(null); setDone(null);
    setStep({ step: 0, total: 1, message: `A iniciar (${modelo})…` });
    try {
      if (baseSaveRef.current) await baseSaveRef.current();
      const data = await CtdAPI.generateCopyItem(
        campanhaId,
        { commPlanIndex: idx, country, modelo, include_tone_refs: true, user_note: userNote || undefined, actor_email: userEmail || undefined },
        (s, t, m) => setStep({ step: s, total: t, message: m }),
      );
      setDone(data || { ok: true });
      const firstInserted = Array.isArray(data) ? data[0] : (data?.id ? data : null);
      // Forçar status 'rascunho' — o utilizador ainda não aprovou a nova versão
      if (firstInserted) setLocalCopy({ ...firstInserted, status: 'rascunho' });
      // Feedback guardado no backend antes do DELETE (copy_id válido) — não repetir aqui
      onDone?.();
      reloadCopies?.();
    } catch (e) { setError(e.message || 'Erro ao gerar'); }
    setBusy(false);
  };

  const oneShotEmail = ctdIsOneShotEmail(peca);
  const modoEff = emailModo || (activeCopy && !brevoOpen ? (peca?.email_modo || 'criar') : '');
  const showChoice = false;
  const showBrevo = oneShotEmail && emailModo !== 'criar';
  const applyPlan = (res) => {
    if (res?.item) {
      setEmailModo(res.item.email_modo || '');
      onPlanItem?.(commPlanIndex, res.item);
    }
    if (res?.copy) setLocalCopy(res.copy);
    reloadCopies?.();
  };
  const escolherCriar = async () => {
    setModoBusy(true); setModoErro('');
    try {
      const res = await CtdAPI.oneShotEmail(campanhaId, { comm_plan_index: idx, modo: 'criar' });
      setBrevoOpen(false);
      applyPlan(res);
    } catch (e) { setModoErro(e.message || 'Não gravei o caminho.'); }
    setModoBusy(false);
  };
  const _cardEl = (
    <>
      <div onClick={inline ? undefined : e => e.stopPropagation()} style={inline ? {
        background: 'transparent',
        display: 'flex', flexDirection: 'column', width: '100%',
      } : {
        background: 'var(--bg-surface, #fff)', borderRadius: 'var(--radius-lg, 10px)',
        maxWidth: 1240, width: '94vw', maxHeight: '92vh', overflowY: 'auto',
        display: 'flex', flexDirection: 'column',
        boxShadow: 'var(--shadow-huge, 0 20px 60px rgba(0,0,0,0.3))',
      }}>
        {/* Header — só em modo modal (inline usa CtdBreadcrumb externo) */}
        {!inline && (
          <div style={{ padding: '16px 20px', borderBottom: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12, background: 'var(--bg-subtle, #FAFAFA)', position: 'sticky', top: 0, zIndex: 2 }}>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.08em', color: 'var(--color-primary, #3859D0)', fontFamily: 'var(--font-mono, monospace)', marginBottom: 4 }}>
                {copy ? 'EDITAR / REGERAR COPY' : 'GERAR COPY'} · {canalLabel.toUpperCase()}{country ? ` · ${country}` : ''}
                {plannedDate && ` · ${ctdFmtDate(plannedDate)}`}
              </div>
              <div style={{ fontSize: 15, fontWeight: 700, color: 'var(--text, #112954)', lineHeight: 1.3, fontFamily: 'var(--font-mono, monospace)' }}>
                {pecaTitle}
              </div>
              {pecaSubtitulo && (
                <div className="font-display" style={{ fontSize: 14, color: 'var(--text-muted, #4a5e7a)', marginTop: 4, fontWeight: 600 }}>
                  {pecaSubtitulo}
                </div>
              )}
            </div>
            {totalItems > 1 && (
              <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginRight: 8 }}>
                <button onClick={() => handleNav(onPrev)} disabled={!onPrev || busy}
                  style={{ background: 'none', border: '1px solid var(--border-2,#E3E6EB)', borderRadius: 4, padding: '4px 10px', cursor: (!onPrev||busy)?'not-allowed':'pointer', opacity: (!onPrev||busy)?0.4:1, fontSize: 14 }}>←</button>
                <span style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted,#4a5e7a)', fontFamily: 'var(--font-mono,monospace)', minWidth: 44, textAlign: 'center' }}>{currentPos} / {totalItems}</span>
                <button onClick={() => handleNav(onNext)} disabled={!onNext || busy}
                  style={{ background: 'none', border: '1px solid var(--border-2,#E3E6EB)', borderRadius: 4, padding: '4px 10px', cursor: (!onNext||busy)?'not-allowed':'pointer', opacity: (!onNext||busy)?0.4:1, fontSize: 14 }}>→</button>
              </div>
            )}
            <button onClick={onClose} disabled={busy} style={{ background: 'none', border: 0, cursor: busy ? 'not-allowed' : 'pointer', opacity: busy ? 0.4 : 1 }}>
              <Icon name="x" size={20} />
            </button>
          </div>
        )}

        {/* Body */}
        <div style={{ padding: inline ? 0 : 20 }}>
          {showChoice && (
            <CtdOneShotEmailChoice busy={modoBusy} erro={modoErro} onCriar={escolherCriar} onBrevo={() => { setBrevoOpen(true); setModoErro(''); }} />
          )}
          {showBrevo && (
            <CtdOneShotBrevoEditor
              campanhaId={campanhaId}
              commPlanIndex={idx}
              peca={peca}
              copy={activeCopy}
              userEmail={userEmail}
              locked={isApproved}
              onSaved={(res) => { applyPlan(res); setBrevoOpen(false); }}
            />
          )}
          {!(showChoice || showBrevo) && (<>

          {/* Grid 3-col em modo inline: esquerda = notas+gerar+actions+contexto; centro = copy; direita = mockup */}
          <div style={inline ? { display: 'grid', gridTemplateColumns: (_isMetaPiece || _isWaPiece) ? 'minmax(292px, 320px) minmax(0, 1fr) 300px' : 'minmax(292px, 320px) minmax(0, 1fr)', gap: 20, alignItems: 'start' } : {}}>
          <div style={inline ? { minWidth: 0, display: 'flex', flexDirection: 'column', gap: 12 } : {}}>

          {/* P3 — Histórico de notas de feedback (só em rascunho) */}
          {!isApproved && activeCopy && feedbackHistory !== null && (
            feedbackHistory.length > 0 ? (
              <div style={{ padding: '10px 14px', background: '#FFFBF0', border: '1px solid #FDE68A', borderRadius: 8 }}>
                <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.08em', color: '#92400E', fontFamily: 'var(--font-mono, monospace)', marginBottom: 6 }}>
                  NOTAS ANTERIORES — {feedbackHistory.length} correcç{feedbackHistory.length > 1 ? 'ões' : 'ão'} pendente{feedbackHistory.length > 1 ? 's' : ''} de revisão
                </div>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
                  {feedbackHistory.map((f, i) => (
                    <div key={f.id || i} style={{ fontSize: 11, color: '#78350F', background: '#FEF3C7', borderRadius: 4, padding: '5px 8px', lineHeight: 1.45 }}>
                      <span style={{ fontSize: 9, color: '#B45309', fontFamily: 'var(--font-mono, monospace)', marginRight: 6 }}>
                        {new Date(f.created_at).toLocaleDateString('pt-PT', { day: '2-digit', month: '2-digit' })}
                        {f.user_email ? ` · ${String(f.user_email).split('@')[0]}` : ''}
                      </span>
                      {f.nota}
                    </div>
                  ))}
                </div>
              </div>
            ) : null
          )}

          {isApproved ? (
            <CtdCopyPipelineCard
              prompt={thisPrompt}
              imageUrl={thisImageUrl}
              imageApproved={!!thisPrompt?.image_approved_at}
              stale={ctdPromptIsStale(thisPrompt, activeCopy)}
              needsVideo={false}
              imageOnTab={_isMetaPiece}
              wa={_isWaPiece}
              waGroup={peca?.waGroup || null}
              waImageReady={!!waStored?.headerImage}
              waHeaderFromAd={String(brandSlug || campFull?.brand_slug || '').toLowerCase() === 'mimaki'}
              templateApproved={!!waStored?.approvedAt}
              templateStale={waStored ? ctdWaTemplateStale(waStored) : false}
              busy={busy || saving || approving}
              busyPrompt={busyPrompt || waHeaderBusy}
              stepMessage={waHeaderBusy ? (waHeaderStep.message || 'A gerar imagem…') : step.message}
              genStep={waHeaderStep.step}
              genTotal={waHeaderStep.total}
              onGenerateImage={() => {
                if (!activeCopy?.id || waHeaderBusy) return;
                const fromAd = String(brandSlug || campFull?.brand_slug || '').toLowerCase() === 'mimaki';
                setWaHeaderBusy(true);
                setWaHeaderError(null);
                setWaHeaderStep({ step: 1, total: fromAd ? 1 : 5, message: fromAd ? 'A adaptar a imagem do anúncio…' : 'A seleccionar referências de produto…' });
                const run = fromAd
                  ? CtdAPI.adaptWaHeader(activeCopy.id, { actor_email: userEmail })
                  : CtdAPI.generateWaPitchHeader(activeCopy.id, { actor_email: userEmail }, (s, t, m) => {
                    setWaHeaderStep({ step: s || 1, total: t || 5, message: m || 'A gerar imagem…' });
                  });
                run
                  .then(saved => { setLocalCopy(saved); setPreviewForm(ctdCopyToPreviewForm(saved)); reloadCopies?.(); })
                  .catch(e => setWaHeaderError(e.message || 'Não consegui gerar a imagem'))
                  .finally(() => setWaHeaderBusy(false));
              }}
              onOpenStep={openProductionStep}
              onNext={onNext}
              hasNext={!!onNext}
              onReopenToggle={() => setReopenCopy(v => !v)}
              reopen={reopenCopy}
              nextLabel="Próxima peça →"
            >
              <div>
                <div style={{ fontSize: 11, color: '#92400E', background: '#FFFBEB', border: '1px solid #FDE68A', borderRadius: 6, padding: '8px 10px', lineHeight: 1.4, marginBottom: 8 }}>
                  {libraryPiece ? 'Guardar substitui o texto aprovado. O template volta a ficar por aprovar.' : 'Regerar substitui esta copy. A nova versão fica por aprovar.'}
                </div>
                <textarea
                  value={userNote} onChange={e => setUserNote(e.target.value)}
                  disabled={busy}
                  placeholder="Nota para a nova versão, ex: Tom mais directo."
                  style={{
                    width: '100%', resize: 'vertical', boxSizing: 'border-box',
                    border: '1px solid var(--border-2, #E3E6EB)', borderRadius: 6,
                    padding: '8px 10px', fontSize: 12, lineHeight: 1.55,
                    color: 'var(--text, #112954)', background: '#fff',
                    outline: 'none', fontFamily: 'var(--font-body, Inter, sans-serif)',
                    minHeight: 48, marginBottom: 8,
                  }}
                />
                <div style={{ display: 'grid', gridTemplateColumns: libraryPiece ? '1fr' : '1fr 1fr', gap: 6 }}>
                  {!libraryPiece && (
                  <button onClick={handleGenerate} disabled={busy || saving}
                    style={{ background: 'transparent', color: 'var(--color-primary, #3859D0)', border: '1px solid var(--color-primary, #3859D0)', padding: '8px', borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: 'pointer', opacity: busy ? 0.5 : 1 }}>
                    {busy ? 'A gerar…' : 'Regerar'}
                  </button>
                  )}
                  <button onClick={handleSave} disabled={busy || saving}
                    style={{ background: 'transparent', color: 'var(--text, #112954)', border: '1px solid var(--border-2, #E3E6EB)', padding: '8px', borderRadius: 6, fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>
                    {saving ? 'A guardar…' : 'Guardar'}
                  </button>
                </div>
              </div>
            </CtdCopyPipelineCard>
          ) : (
          <div style={{ background: 'var(--bg-surface, #fff)', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
            <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)', fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--text-dim, #94a3b8)', fontFamily: 'var(--font-mono, monospace)' }}>
              {oneShotEmail ? 'Breve' : 'Correcções'} <span style={{ fontWeight: 400, letterSpacing: '0.02em', textTransform: 'none' }}>{oneShotEmail ? '' : '(opcional)'}</span>
            </div>
            <div style={{ padding: 14 }}>
            <textarea
              value={userNote} onChange={e => setUserNote(e.target.value)}
              disabled={busy}
              placeholder={copy
                ? 'Ex: "Tom mais técnico. Remover referência ao preço."'
                : (oneShotEmail ? 'O que este email tem de dizer. Ex: lançamento do filme, tom directo, pedido de amostra.' : 'Ex: "Foca na velocidade de entrega. Tom mais directo."')}
              style={{
                width: '100%', resize: 'vertical', boxSizing: 'border-box',
                border: '1px solid var(--border-2, #E3E6EB)', borderRadius: 6,
                padding: '8px 10px', fontSize: 12, lineHeight: 1.55,
                color: 'var(--text, #112954)', background: busy ? 'var(--bg-subtle, #FAFAFA)' : '#fff',
                outline: 'none', cursor: busy ? 'not-allowed' : 'text',
                fontFamily: 'var(--font-body, Inter, sans-serif)',
                minHeight: 56, marginBottom: 10,
              }}
            />
            <div style={{ display: 'grid', gridTemplateColumns: libraryPiece ? '1fr' : '1fr 1fr', gap: 6 }}>
              {!libraryPiece && (
              <button
                onClick={handleGenerate} disabled={busy || saving || approving}
                style={{
                  background: 'transparent', color: 'var(--color-primary, #3859D0)', border: '1px solid var(--color-primary, #3859D0)',
                  padding: '8px 8px', borderRadius: 6, width: '100%',
                  fontSize: 12, fontWeight: 700, cursor: busy ? 'not-allowed' : 'pointer',
                  opacity: (busy || saving || approving) ? 0.5 : 1, whiteSpace: 'nowrap',
                  gridColumn: activeCopy ? 'auto' : '1 / -1',
                }}>
                {busy ? 'A gerar…' : (activeCopy ? 'Regerar' : 'Gerar')}
              </button>
              )}
              {activeCopy && (
                <button
                  onClick={handleSave} disabled={busy || saving || approving}
                  style={{
                    background: 'transparent', color: 'var(--text, #112954)', border: '1px solid var(--border-2, #E3E6EB)',
                    padding: '8px 8px', borderRadius: 6, width: '100%',
                    fontSize: 12, fontWeight: 600, cursor: 'pointer',
                    opacity: (busy || saving || approving) ? 0.5 : 1, whiteSpace: 'nowrap',
                  }}>
                  {saving ? 'A guardar…' : 'Guardar'}
                </button>
              )}
              {activeCopy && (
                <button
                  onClick={handleApproveCopy} disabled={busy || saving || approving || waBlockers.length > 0}
                  style={{
                    gridColumn: '1 / -1',
                    background: '#1F8A52', color: '#fff', border: 0,
                    padding: '9px 8px', borderRadius: 6, width: '100%',
                    fontSize: 12, fontWeight: 700, cursor: 'pointer',
                    opacity: (busy || saving || approving) ? 0.5 : 1, whiteSpace: 'nowrap',
                  }}>
                  {approving ? 'A aprovar…' : 'Aprovar e continuar'}
                </button>
              )}
              {_isWaPiece && waLivePack && <CtdWaChecklistList pack={waLivePack} />}
            </div>
            </div>
          </div>
          )}

          {donePayload && !busy && !isApproved && (
            <div style={{ padding: 10, background: '#DCFCE7', borderRadius: 'var(--radius-sm, 6px)', color: '#166534', fontSize: 12, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
              <Icon name="check" size={14} /> Copy gerada. Podes editar/aprovar abaixo.
            </div>
          )}
          {error && (
            <div style={{ padding: 12, background: '#FEE2E2', borderRadius: 'var(--radius-sm, 6px)', color: '#991B1B', fontSize: 12, marginBottom: 12 }}>
              {error}
            </div>
          )}

          {/* Ficha deste anúncio — coluna esquerda (só inline). WhatsApp da biblioteca não usa a ficha do anúncio. */}
          {inline && (_isInternalPiece ? (
            <CtdInternalContextCard campFull={campFull || { proposta_json: proposta }} />
          ) : libraryPiece ? (
            <CtdWaCopyContext
              peca={peca}
              copy={activeCopy}
              pack={waStored}
              brandSlug={brandSlug || campFull?.brand_slug}
              busy={waHeaderBusy}
              error={waHeaderError}
              hideAction={isApproved && peca?.waGroup === 'abertura' && !waStored?.headerImage && String(brandSlug || campFull?.brand_slug || '').toLowerCase() !== 'mimaki'}
              onAdapt={async () => {
                if (!activeCopy?.id) return;
                const pitch = peca?.waGroup === 'abertura' && String(brandSlug || campFull?.brand_slug || '').toLowerCase() !== 'mimaki';
                setWaHeaderBusy(true);
                setWaHeaderError(null);
                try {
                  const saved = pitch
                    ? await CtdAPI.generateWaPitchHeader(activeCopy.id, { actor_email: userEmail })
                    : await CtdAPI.adaptWaHeader(activeCopy.id, { actor_email: userEmail });
                  setLocalCopy(saved);
                  setPreviewForm(ctdCopyToPreviewForm(saved));
                  reloadCopies?.();
                } catch (e) {
                  setWaHeaderError(e.message || (pitch ? 'Não consegui gerar o cabeçalho' : 'Não consegui adaptar a imagem'));
                }
                setWaHeaderBusy(false);
              }}
            />
          ) : oneShotEmail ? null : (
            <CtdAdContextCard
              peca={peca}
              commPlanIndex={commPlanIndex}
              campFull={campFull || { proposta_json: proposta }}
              country={copy?.country || peca?.country || 'PT'}
              title={_isMetaPiece ? 'Ficha deste anúncio' : 'Ficha desta peça'}
              campanhaId={campanhaId}
              saveRef={baseSaveRef}
              onCopyBase={onCopyBase}
            />
          ))}

          </div>{/* /col-esq */}

          {/* col-centro: Copy Meta Ads + estados (só em inline) */}
          {inline && (
            <div style={{ minWidth: 0 }}>
              {activeCopy && !busy && waHeaderBusy && (
                <div style={{ background: '#fff', border: '1px solid #c7d2fe', borderRadius: 8, minHeight: 420, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 32 }}>
                  <div style={{ width: '100%', maxWidth: 480 }}>
                    <CtdGenProgress step={waHeaderStep.step} total={waHeaderStep.total} message={waHeaderStep.message || 'A gerar imagem…'} />
                    {waHeaderError && <div style={{ marginTop: 12, color: '#991B1B', fontSize: 12 }}>{waHeaderError}</div>}
                  </div>
                </div>
              )}
              {activeCopy && !busy && !waHeaderBusy && (
                <CtdCopyPecaCard
                  ref={cardRef}
                  hideActions
                  hideMockup
                  campanhaId={campanhaId}
                  copy={activeCopy}
                  userEmail={userEmail}
                  reloadCopies={reloadCopies}
                  staleIds={[]}
                  masterCopy={null}
                  imageUrl={_isInternalPiece ? thisImageUrl : undefined}
                  brandSlug={brandSlug || campFull?.brand_slug || campFull?.briefing?.brand_slug}
                  fieldsLocked={isApproved && !reopenCopy}
                  onFormChange={setPreviewForm}
                  onBeforeSave={() => baseSaveRef.current?.()}
                  onCopySaved={onCopySaved}
                />
              )}
              {busy && (
                <div style={{ padding: '48px 16px', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 20 }}>
                  <div style={{ display: 'flex', alignItems: 'flex-end', gap: 4, height: 48 }}>
                    {[0,1,2,3,4,5,6].map(function(i) {
                      return React.createElement('div', { key: i, style: {
                        width: 4, height: 32, background: 'var(--color-primary, #3859D0)',
                        borderRadius: 4, animation: 'ctdWave 1s ease-in-out infinite',
                        animationDelay: (i * 0.1) + 's', transformOrigin: 'bottom',
                      }});
                    })}
                  </div>
                  <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.10em', color: 'var(--color-primary, #3859D0)', fontFamily: 'var(--font-mono, monospace)' }}>A GERAR CONTEÚDO</div>
                  <style>{`@keyframes ctdWave { 0%,100%{transform:scaleY(0.25)} 50%{transform:scaleY(1)} }`}</style>
                </div>
              )}
              {!copy && !busy && !donePayload && (
                <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-muted, #4a5e7a)', background: 'var(--bg-subtle, #FAFAFA)', borderRadius: 'var(--radius-sm, 6px)', border: '1px dashed var(--border-2, #E3E6EB)', fontSize: 12 }}>
                  {oneShotEmail
                    ? <>Sem copy ainda. Escreve o breve e clica <b>Gerar</b>. O assunto, o texto e o botão ficam aqui.</>
                    : <>Sem copy ainda para esta peça. Escolhe modelo e clica <b>Gerar</b>.</>}
                </div>
              )}
            </div>
          )}

          {/* col-direita: pré-visualização Meta Ads. O email interno abre em Pré-visualizar. */}
          {inline && _isMetaPiece && (
            <div style={{ position: 'sticky', top: 76, minWidth: 0 }}>
              <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
                <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
                  <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>Pré-visualização</span>
                  <span style={{ fontSize: 9, color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>Feed · 4:5</span>
                </div>
                <CtdMetaAdsFeedMockup
                  form={previewForm || ctdCopyToPreviewForm(activeCopy)}
                  flush
                  brandName={brandName || campFull?.brand_name || campFull?.briefing?.brand_name}
                  brandSlug={brandSlug || campFull?.brand_slug || campFull?.briefing?.brand_slug}
                  imageUrl={thisImageUrl}
                />
              </div>
            </div>
          )}
          {inline && _isWaPiece && (
            <div style={{ position: 'sticky', top: 76, minWidth: 0 }}>
              <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
                <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
                  <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>Pré-visualização</span>
                  <span style={{ fontSize: 9, color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>{peca?.waGroup === 'qualificacao' ? 'Sem imagem' : 'Cabeçalho · 1125×600'}</span>
                </div>
                <div style={{ padding: 12 }}>
                  <CtdWaPhone generating={waHeaderBusy} generatingMessage={waHeaderStep.message} pack={waLivePack || ctdWaPackFromCopy(activeCopy)} brandName={brandName || campFull?.brand_name || campFull?.briefing?.brand_name} />
                </div>
              </div>
            </div>
          )}

          </div>{/* /grid 3-col */}
          </>)}

          {/* Copy fields — modo modal (não inline): layout vertical */}
          {!inline && activeCopy && !busy && (
            <div>
              <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.08em', color: 'var(--text-dim, #7a8fa8)', fontFamily: 'var(--font-mono, monospace)', marginBottom: 8 }}>
                COPY · CAMPOS EDITÁVEIS
              </div>
              <CtdCopyPecaCard
                ref={cardRef}
                hideActions
                campanhaId={campanhaId}
                copy={activeCopy}
                userEmail={userEmail}
                reloadCopies={reloadCopies}
                staleIds={[]}
                masterCopy={null}
                brandSlug={brandSlug || campFull?.brand_slug || campFull?.briefing?.brand_slug}
                fieldsLocked={isApproved && !reopenCopy}
                onBeforeSave={() => baseSaveRef.current?.()}
                onCopySaved={onCopySaved}
              />
            </div>
          )}
          {!inline && busy && (
            <div style={{ padding: '32px 16px', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 20 }}>
              <div style={{ display: 'flex', alignItems: 'flex-end', gap: 4, height: 48 }}>
                {[0,1,2,3,4,5,6].map(function(i) {
                  return React.createElement('div', { key: i, style: {
                    width: 4, height: 32, background: 'var(--color-primary, #3859D0)',
                    borderRadius: 4, animation: 'ctdWave 1s ease-in-out infinite',
                    animationDelay: (i * 0.1) + 's', transformOrigin: 'bottom',
                  }});
                })}
              </div>
              <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.10em', color: 'var(--color-primary, #3859D0)', fontFamily: 'var(--font-mono, monospace)', textAlign: 'center' }}>A GERAR CONTEÚDO</div>
              <style>{`@keyframes ctdWave { 0%,100%{transform:scaleY(0.25)} 50%{transform:scaleY(1)} }`}</style>
            </div>
          )}
          {!inline && !copy && !busy && !donePayload && (
            <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-muted, #4a5e7a)', background: 'var(--bg-subtle, #FAFAFA)', borderRadius: 'var(--radius-sm, 6px)', border: '1px dashed var(--border-2, #E3E6EB)', fontSize: 12 }}>
              Sem copy ainda para esta peça. Escolhe modelo e clica <b>Gerar</b>.<br />
              O Sonnet lê a estratégia, personas, guardrails e copies já aprovadas para manter tom coerente.
            </div>
          )}
        </div>

        {/* Footer — só em modo modal */}
        {!inline && (
          <div style={{ padding: '12px 20px', borderTop: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'flex-end', gap: 8, background: 'var(--bg-subtle, #FAFAFA)' }}>
            <button onClick={onClose} disabled={busy} className="btn btn-sm" style={{ fontSize: 12 }}>
              Fechar
            </button>
          </div>
        )}
      </div>

      {/* O prompt e a imagem abrem na tab respectiva, não numa modal por cima da copy. */}
    </>
  );

  if (inline) return _cardEl;
  return ReactDOM.createPortal(
    <div onClick={busy ? undefined : onClose} style={{
      position: 'fixed', inset: 0, background: 'rgba(17,41,84,0.55)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
    }}>{_cardEl}</div>,
    document.body
  );
};

// ─── CtdGeneratePromptItemModal — modal per-item para gerar prompt de 1 peça ────
const CtdGeneratePromptItemModal = ({ campanhaId, peca, commPlanIndex, prompt, onClose, onDone }) => {
  const Icon = window.Icon;
  const [busy, setBusy]         = React.useState(false);
  const [step, setStep]         = React.useState({ step: 0, total: 0, message: '' });
  const [error, setError]       = React.useState(null);
  const [donePayload, setDone]  = React.useState(null);

  const idx = commPlanIndex;
  const canalLabel = (peca?.canal || peca?.content_type || prompt?.canal || '').toUpperCase();
  const itemTitle  = peca?.titulo || prompt?.titulo || `Peça #${(idx != null ? idx + 1 : '?')}`;

  const handleGenerate = async () => {
    if (idx == null) { setError('Peça sem índice no comm_plan'); return; }
    setBusy(true); setError(null); setDone(null);
    setStep({ step: 0, total: 1, message: 'A iniciar…' });
    try {
      const data = await CtdAPI.generatePromptItem(
        campanhaId, idx, { modelo: 'opus-4-7' },
        (s, t, m) => setStep({ step: s, total: t, message: m }),
      );
      setDone(data || { ok: true });
      onDone?.();
    } catch (e) { setError(e.message || 'Erro ao gerar prompt'); }
    setBusy(false);
  };

  return ReactDOM.createPortal(
    <div onClick={busy ? undefined : onClose} style={{
      position: 'fixed', inset: 0, background: 'rgba(17,41,84,0.45)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        background: 'var(--bg-surface, #fff)', borderRadius: 'var(--radius-lg, 10px)',
        maxWidth: 820, width: '90vw', maxHeight: '90vh', overflowY: 'auto',
        display: 'flex', flexDirection: 'column',
        boxShadow: 'var(--shadow-huge, 0 20px 60px rgba(0,0,0,0.25))',
      }}>
        <div style={{ padding: '16px 20px', borderBottom: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.08em', color: 'var(--color-primary, #3859D0)', fontFamily: 'var(--font-mono, monospace)', marginBottom: 4 }}>
              GERAR PROMPT · {canalLabel}
            </div>
            <div className="font-display" style={{ fontSize: 16, fontWeight: 700, color: 'var(--text, #112954)', lineHeight: 1.3 }}>
              {itemTitle}
            </div>
          </div>
          <button onClick={onClose} disabled={busy} style={{ background: 'none', border: 0, cursor: busy ? 'not-allowed' : 'pointer', opacity: busy ? 0.4 : 1 }}>
            <Icon name="x" size={20} />
          </button>
        </div>

        <div style={{ padding: 20 }}>
          {prompt?.prompt_text_pt && (
            <div style={{ marginBottom: 16, padding: 12, background: 'var(--bg-subtle, #FAFAFA)', borderRadius: 'var(--radius-sm, 6px)', border: '1px solid var(--border-1, #ECEFF5)' }}>
              <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.08em', color: 'var(--text-dim, #7a8fa8)', fontFamily: 'var(--font-mono, monospace)', marginBottom: 6 }}>PROMPT ACTUAL (será substituído)</div>
              <div style={{ fontSize: 12, color: 'var(--text-muted, #4a5e7a)', lineHeight: 1.5, whiteSpace: 'pre-wrap', maxHeight: 160, overflow: 'auto' }}>
                {String(prompt.prompt_text_pt).slice(0, 500)}{String(prompt.prompt_text_pt).length > 500 ? '…' : ''}
              </div>
            </div>
          )}

          {busy && (
            <div style={{ marginBottom: 12 }}>
              <CtdGenProgress step={step.step} total={step.total} message={step.message} />
            </div>
          )}

          {donePayload && !busy && (
            <div style={{ padding: 12, background: '#DCFCE7', borderRadius: 'var(--radius-sm, 6px)', color: '#166534', fontSize: 13, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
              <Icon name="check" size={14} /> Prompt gerado. Fecha para revisitar o card.
            </div>
          )}

          {error && (
            <div style={{ padding: 12, background: '#FEE2E2', borderRadius: 'var(--radius-sm, 6px)', color: '#991B1B', fontSize: 12 }}>
              {error}
            </div>
          )}
        </div>

        <div style={{ padding: '12px 20px', borderTop: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
          <button onClick={onClose} disabled={busy} className="btn btn-sm btn-ghost" style={{ fontSize: 12 }}>
            {donePayload ? 'Fechar' : 'Cancelar'}
          </button>
          {!donePayload && (
            <button
              onClick={handleGenerate} disabled={busy}
              style={{
                background: 'var(--color-primary, #3859D0)', color: '#fff', border: 0,
                padding: '8px 16px', borderRadius: 'var(--radius-sm, 6px)',
                fontSize: 12, fontWeight: 700, cursor: busy ? 'not-allowed' : 'pointer',
                display: 'inline-flex', alignItems: 'center', gap: 6,
                opacity: busy ? 0.5 : 1,
              }}
            >
              <Icon name="sparkle" size={12} /> {busy ? 'A gerar…' : 'Gerar prompt'}
            </button>
          )}
        </div>
      </div>
    </div>,
    document.body
  );
};

function CtdPieceRenameModal({ titulo, busy, erro, onClose, onSave }) {
  const [nome, setNome] = React.useState(titulo || '');
  const inputRef = React.useRef(null);
  React.useEffect(() => { inputRef.current?.focus(); inputRef.current?.select(); }, []);
  return ReactDOM.createPortal(
    <div onClick={busy ? undefined : onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(15,23,42,.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
      <div role="dialog" aria-modal="true" aria-label="Editar nome" onClick={e => e.stopPropagation()} style={{ background: '#fff', borderRadius: 14, width: '100%', maxWidth: 480, boxShadow: '0 24px 80px rgba(0,0,0,.2)' }}>
        <div style={{ padding: '18px 22px 12px', borderBottom: '1px solid #f1f5f9' }}>
          <div style={{ fontSize: 15, fontWeight: 700, color: '#1d2e38' }}>Editar nome</div>
        </div>
        <div style={{ padding: '16px 22px' }}>
          <input
            ref={inputRef}
            value={nome}
            disabled={busy}
            maxLength={180}
            onChange={e => setNome(e.target.value)}
            onKeyDown={e => {
              if (e.key === 'Enter') { e.preventDefault(); onSave(nome.trim().replace(/\s+/g, ' ')); }
              if (e.key === 'Escape') onClose();
            }}
            style={{ display: 'block', width: '100%', boxSizing: 'border-box', border: '1px solid #E3E6EB', borderRadius: 8, padding: '9px 10px', fontSize: 14, fontWeight: 600, color: '#112954' }}
          />
          {erro && <div style={{ color: '#991B1B', fontSize: 12, marginTop: 8 }}>{erro}</div>}
        </div>
        <div style={{ padding: '12px 22px 16px', borderTop: '1px solid #f1f5f9', display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
          <button type="button" onClick={onClose} className="btn" style={{ fontSize: 12 }} disabled={busy}>Cancelar</button>
          <button type="button" onClick={() => onSave(nome.trim().replace(/\s+/g, ' '))} disabled={busy} className="btn" style={{ fontSize: 12, background: '#3859D0', color: '#fff', borderColor: '#3859D0' }}>{busy ? 'A guardar…' : 'Guardar'}</button>
        </div>
      </div>
    </div>,
    document.body
  );
}

// ─── CtdCopiesPlanTable — plano completo comm_plan × mercados, gerar em lote/1-a-1 ─
const CtdCopiesPlanTable = ({ campanhaId, campFull, copies, prompts = [], onOpenGenerate, reloadCopies, reloadPrompts, userEmail, onPlan, onRefresh, lockCanal }) => {
  const Icon = window.Icon;
  const commPlan  = (campFull?.proposta_json?.comm_plan) || [];
  const markets   = (campFull?.estrategia_json?.markets) || [];
  const marketList = markets.length
    ? markets.map(m => ({ country: m.country || m.pais || 'PT', lingua: m.lingua || (m.country === 'ES' ? 'es' : 'pt') }))
    : [{ country: 'PT', lingua: 'pt' }];

  const [selected, setSelected]     = React.useState(new Set());
  const [modelo, setModelo]         = React.useState('sonnet-4-6');
  const [bulkBusy, setBulkBusy]     = React.useState(false);
  const [bulkProg, setBulkProg]     = React.useState({ done: 0, total: 0, current: '' });
  const [rowBusy, setRowBusy]       = React.useState(new Set());
  const [filterCanal, setFilterCanal] = React.useState(lockCanal || 'all');
  React.useEffect(() => { if (lockCanal) setFilterCanal(lockCanal); }, [lockCanal]);
  const [filterCountry, setFCtry]   = React.useState('all');
  const [filterStatus, setFStatus]  = React.useState('all');
  const [verArquivadas, setVerArquivadas] = React.useState(false);
  const [pieceBusy, setPieceBusy]   = React.useState(false);
  const [pieceErro, setPieceErro]   = React.useState('');
  const [renameRow, setRenameRow]   = React.useState(null);

  // Build planned rows: 1 per comm_plan item (SEM cross-product — items já vêm por país
  // quando aplicável, ex: [PT] Uma máquina..., [ES] Una sola máquina...).
  // Envelope items (campanha_paid/paid_campaign) são metadata dos 5 Meta Ads e NÃO se
  // geram como copy autónoma — filtrados.
  const ENVELOPE_TYPES = ['campanha_paid', 'paid_campaign'];
  const primaryCountry = marketList[0]?.country || 'PT';

  const detectCountry = (peca) => {
    // Prioridade: peca.country > prefixo [XX] no título > primeiro market
    if (peca.country) return String(peca.country).toUpperCase();
    const m = String(peca.titulo || '').match(/^\[([A-Z]{2})\]/);
    if (m) return m[1];
    return primaryCountry;
  };
  const countryToLingua = (cc) => {
    const found = marketList.find(m => m.country === cc);
    if (found) return found.lingua;
    return cc === 'ES' ? 'es' : cc === 'FR' ? 'fr' : cc === 'IT' ? 'it' : cc === 'DE' ? 'de' : 'pt';
  };

  const rows = React.useMemo(() => {
    const out = [];
    commPlan.forEach((peca, idx) => {
      if (ENVELOPE_TYPES.includes(peca.content_type)) return;
      if (ctdPecaArchived(peca) !== verArquivadas) return;
      if (ctdHideWaFromPlan(peca)) return;
      // Item com prefixo [PT]/[ES] → 1 row para esse país
      // Item sem prefixo (Email/WA/Interna genéricos) → N rows (1 por market)
      const prefixMatch = String(peca.titulo || '').match(/^\[([A-Z]{2})\]/);
      const countries = peca.country
        ? [String(peca.country).toUpperCase()]
        : prefixMatch
          ? [prefixMatch[1]]
          : marketList.map(m => m.country);
      countries.forEach(country => {
        const lingua = countryToLingua(country);
        // Filtro estrito: item_title + content_type + canal devem ser consistentes com peca.
        // Evita que copies com comm_plan_item_index errado (bug antigo) apareçam na linha errada.
        const matches = (copies || []).filter(c => {
          if (Number(c.comm_plan_item_index) !== idx) return false;
          if (!(String(c.country || '').toUpperCase() === country || (!c.country && country === primaryCountry))) return false;
          // item_title: se ambos presentes, devem ser iguais
          if (c.item_title && peca.titulo && c.item_title !== peca.titulo) return false;
          // content_type: se ambos presentes, devem ser iguais
          if (c.content_type && peca.content_type && c.content_type !== peca.content_type) return false;
          return true;
        }).sort((a, b) => {
          // Preferred order: aprovado first, then by content length
          if (a.status === 'aprovado' && b.status !== 'aprovado') return -1;
          if (b.status === 'aprovado' && a.status !== 'aprovado') return 1;
          return (String(b.body || '').length + String(b.headline || '').length) - (String(a.body || '').length + String(a.headline || '').length);
        });
        const existing = matches[0];
        const status = existing?.status === 'aprovado' ? 'aprovado'
                     : existing ? 'gerado'
                     : 'pendente';
        const prompt = ctdPromptForPeca(prompts, idx, peca, existing);
        const stale = ctdIsWaPiece(peca, existing) ? ctdWaTemplateStale(ctdWaPackFromCopy(existing)) : ctdPromptIsStale(prompt, existing);
        out.push({
          key: `${idx}::${country}`,
          idx, country, lingua, peca, existing, status, stale,
        });
      });
    });
    ctdWaLibraryEntries(copies).forEach(e => {
      e.copyStale = ctdWaUspStale(e, copies);
      const status = e.copy.status === 'aprovado' ? 'aprovado' : (e.copy.body ? 'gerado' : 'pendente');
      out.push(ctdWaLibraryRow(e, status));
    });
    ctdWaPendingAberturas(copies, campFull).forEach(row => out.push(row));
    return out;
  }, [commPlan, marketList, copies, prompts, campFull, verArquivadas]);

  // Sort deterministica: PT primeiro (mercado primário), depois ES, FR..., depois idx
  const COUNTRY_ORDER_TBL = ['PT', 'ES', 'FR', 'IT', 'DE', 'EN'];
  const canalEff = lockCanal || filterCanal;
  const filtered = React.useMemo(() => {
    const list = rows.filter(r => {
      if (canalEff !== 'all' && ctdCanalLabel(r.peca) !== canalEff) return false;
      if (filterCountry !== 'all' && r.country !== filterCountry) return false;
      if (filterStatus !== 'all' && r.status !== filterStatus) return false;
      return true;
    });
    return list.sort((a, b) => {
      const ra = ctdListRank(a), rb = ctdListRank(b);
      if (ra !== rb) return ra - rb;
      const ia = COUNTRY_ORDER_TBL.indexOf(a.country); const ib = COUNTRY_ORDER_TBL.indexOf(b.country);
      const pa = ia === -1 ? 99 : ia; const pb = ib === -1 ? 99 : ib;
      if (pa !== pb) return pa - pb;
      return (a.groupPos || a.idx) - (b.groupPos || b.idx);
    });
  }, [rows, canalEff, filterCountry, filterStatus]);

  const CANAL_ORDER_TBL = ['Meta Ads', 'Email', 'WhatsApp', 'Blog', 'Landing', 'LED', 'Site', 'Comunicação Interna'];
  const canaisPresent = React.useMemo(() => {
    const set = new Set(rows.map(r => ctdCanalLabel(r.peca)).filter(Boolean));
    return [...set].sort((a, b) => {
      const ia = CANAL_ORDER_TBL.indexOf(a), ib = CANAL_ORDER_TBL.indexOf(b);
      if (ia !== -1 && ib !== -1) return ia - ib;
      if (ia !== -1) return -1;
      if (ib !== -1) return 1;
      return a.localeCompare(b);
    });
  }, [rows]);
  const countriesPresent = React.useMemo(() => [...new Set(rows.map(r => r.country))], [rows]);

  const counters = React.useMemo(() => {
    const plan = rows.filter(r => !r.library);
    return {
      total: plan.length,
      pendente: plan.filter(r => r.status === 'pendente').length,
      gerado: plan.filter(r => r.status === 'gerado').length,
      aprovado: plan.filter(r => r.status === 'aprovado').length,
    };
  }, [rows]);

  const allFilteredSelected = filtered.length > 0 && filtered.every(r => selected.has(r.key));
  const toggleAll = () => {
    setSelected(prev => {
      const next = new Set(prev);
      if (allFilteredSelected) filtered.forEach(r => next.delete(r.key));
      else filtered.forEach(r => next.add(r.key));
      return next;
    });
  };
  const toggleOne = (key) => setSelected(prev => {
    const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key);
    return next;
  });

  const runOne = async (row) => {
    if (row.waPending) {
      setRowBusy(prev => { const n = new Set(prev); n.add(row.key); return n; });
      try {
        await CtdAPI.generateWaLibrary(campanhaId, 'pt', { actor_email: userEmail });
        reloadCopies?.();
      } catch (e) { alert(e.message || 'Erro ao gerar os templates WhatsApp'); }
      setRowBusy(prev => { const n = new Set(prev); n.delete(row.key); return n; });
      return;
    }
    if (row.library || ctdEmailBrevo(row.peca)) {
      const canalRows = ctdNavRowsSameCanal(rows, row.peca);
      const canalPos = Math.max(0, canalRows.findIndex(x => x.idx === row.idx && x.country === row.country));
      onOpenGenerate?.({ copy: row.existing, peca: row.peca, commPlanIndex: row.idx, country: row.country, allRows: rows, rowIndex: canalPos });
      return;
    }
    setRowBusy(prev => { const n = new Set(prev); n.add(row.key); return n; });
    try {
      await CtdAPI.generateCopyItem(campanhaId, { commPlanIndex: row.idx, country: row.country, modelo }, () => {});
      reloadCopies?.();
    } catch (e) { alert(`Erro na peça #${row.idx+1} (${row.country}): ${e.message}`); }
    setRowBusy(prev => { const n = new Set(prev); n.delete(row.key); return n; });
  };

  const runBulk = async (targetRows) => {
    const pending = (targetRows || []).filter(r => !r.library);
    if (!pending.length) return;
    // Master-Mirror: ordenar PT primeiro, depois secundários — permite backend usar
    // PT master como referência para gerar ES/FR/... como espelho semântico.
    const sorted = [...pending].sort((a, b) => {
      const ap = a.country === 'PT' ? 0 : 1;
      const bp = b.country === 'PT' ? 0 : 1;
      if (ap !== bp) return ap - bp;
      // Dentro do mesmo tier, manter ordem por usp_index/idx
      return a.idx - b.idx;
    });
    setBulkBusy(true);
    setBulkProg({ done: 0, total: sorted.length, current: '' });
    for (let i = 0; i < sorted.length; i++) {
      const r = sorted[i];
      const label = `${r.peca.titulo || `Peça #${r.idx+1}`} · ${r.country}`;
      setBulkProg({ done: i, total: sorted.length, current: label });
      try {
        await CtdAPI.generateCopyItem(campanhaId, { commPlanIndex: r.idx, country: r.country, modelo, include_tone_refs: true }, () => {});
      } catch (e) { console.warn(`Erro em ${label}:`, e.message); }
      reloadCopies?.();
    }
    setBulkProg({ done: sorted.length, total: sorted.length, current: 'Concluído' });
    setBulkBusy(false);
    setSelected(new Set());
    setTimeout(() => setBulkProg({ done: 0, total: 0, current: '' }), 1200);
  };

  const runSelected = () => {
    const rows2 = filtered.filter(r => selected.has(r.key));
    runBulk(rows2);
  };
  const runAllPending = () => {
    const pool = lockCanal ? rows.filter(r => ctdCanalLabel(r.peca) === lockCanal) : rows;
    const rows2 = pool.filter(r => r.status === 'pendente' && !r.library);
    if (rows2.length && !confirm(`Gerar ${rows2.length} peças pendentes com ${modelo === 'opus-4-7' ? 'Opus 4.7' : 'Sonnet 4.6'}?`)) return;
    runBulk(rows2);
  };

  const STATUS_STYLE = {
    pendente: { bg: '#F1F5F9', fg: '#475569', label: 'Pendente' },
    gerado:   { bg: '#FEF3C7', fg: '#92400E', label: 'Gerado' },
    aprovado: { bg: '#DCFCE7', fg: '#166534', label: 'Aprovado' },
  };

  const canalBadge = (peca) => {
    const label = ctdCanalLabel(peca);
    const cat = ctdGetCategoria(peca.content_type);
    return (
      <span style={{
        fontSize: 9, fontWeight: 700, letterSpacing: '0.06em',
        padding: '3px 7px', borderRadius: 4,
        background: `${cat.color}18`, color: cat.color,
        fontFamily: 'var(--font-mono, monospace)',
      }}>{label.toUpperCase()}</span>
    );
  };

  const oneShot = String(campFull?.proposta_json?.one_shot) === 'true';
  const archivedCount = commPlan.filter(ctdPecaArchived).length;
  React.useEffect(() => {
    if (verArquivadas && archivedCount === 0) setVerArquivadas(false);
  }, [verArquivadas, archivedCount]);
  const picked = filtered.filter(r => selected.has(r.key) && !r.library && r.idx != null && r.peca?.item_type === 'one_shot');
  const aplicarAccao = async (accao, indexes, titulo) => {
    setPieceBusy(true);
    setPieceErro('');
    try {
      const res = await CtdAPI.oneShotPecasAccao(campanhaId, { accao, indexes, titulo });
      onPlan?.(res?.plan);
      setSelected(new Set());
      if (accao === 'restaurar' && !(res?.plan || []).some(ctdPecaArchived)) setVerArquivadas(false);
      reloadCopies?.();
      reloadPrompts?.();
      onRefresh?.();
    } catch (e) {
      const message = e.message || 'Não consegui actualizar as peças.';
      setPieceErro(message);
      setPieceBusy(false);
      throw e;
    }
    setPieceBusy(false);
  };
  const pedirApagar = () => {
    if (!picked.length || pieceBusy) return;
    const frase = picked.length === 1
      ? `Apagar «${picked[0].peca?.titulo || 'esta peça'}»? A copy sai com ela. Uma peça aprovada não se apaga.`
      : `Apagar ${picked.length} peças? A copy sai com elas. Uma peça aprovada não se apaga.`;
    if (!window.confirm(frase)) return;
    aplicarAccao('apagar', picked.map(r => r.idx));
  };

  // Estilos alinhados com screen_marketing_briefings.jsx (thSt)
  const thSt = { textAlign: 'left', padding: '8px 12px', fontSize: 10, fontFamily: 'var(--font-display)', fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--text-muted)', whiteSpace: 'nowrap', borderBottom: '1px solid var(--border)' };
  const tdSt = { padding: '10px 12px', verticalAlign: 'middle', borderBottom: '1px solid var(--border)', fontSize: 13, color: 'var(--text)' };
  const mesaCanal = !!lockCanal;
  const paisesVista = mesaCanal
    ? [...new Set(rows.filter(r => ctdCanalLabel(r.peca) === lockCanal).map(r => r.country).filter(Boolean))]
    : countriesPresent;
  const variosMercados = paisesVista.length > 1;
  const waCols = canalEff === 'WhatsApp';
  const showCanalCol = !mesaCanal && !waCols;
  const showData = !mesaCanal && !waCols;
  const showPais = variosMercados;
  const showAutor = !mesaCanal;
  const showGrupo = variosMercados;
  const colN = 3 + (waCols ? 2 : ((showCanalCol ? 1 : 0) + (showData ? 1 : 0))) + (showPais ? 1 : 0) + (showAutor ? 1 : 0) + 2;
  const pendentesVista = (mesaCanal ? rows.filter(r => ctdCanalLabel(r.peca) === lockCanal) : rows).filter(r => r.status === 'pendente' && !r.library).length;

  return (
    <div style={{ marginBottom: 20 }}>
      {/* Filtros do plano. Na fase do canal, gerar fica na linha da peça. */}
      {(!mesaCanal || variosMercados || (oneShot && archivedCount > 0)) && (
      <div style={{ display: 'flex', gap: 8, alignItems: 'center', padding: '10px 0', fontSize: 11, flexWrap: 'wrap', marginBottom: 4 }}>
        {!mesaCanal && (
          <CtdDropdown
            label="Canal"
            value={canalEff}
            onChange={setFilterCanal}
            options={[{ value: 'all', label: 'Todos os canais' }, ...canaisPresent.map(c => ({ value: c, label: c }))]}
          />
        )}
        {variosMercados && (
          <CtdDropdown
            label="Mercado"
            value={filterCountry}
            onChange={setFCtry}
            options={[{ value: 'all', label: 'Todos mercados' }, ...paisesVista.map(c => ({ value: c, label: c }))]}
          />
        )}
        {(!mesaCanal || variosMercados || filterStatus !== 'all') && (
          <span style={{ color: 'var(--text-dim, #7a8fa8)', fontSize: 11 }}>{filtered.length} de {rows.length}</span>
        )}
        {oneShot && archivedCount > 0 && (
          <button type="button" onClick={() => { setVerArquivadas(v => !v); setSelected(new Set()); setPieceErro(''); }} style={{ border: 0, background: 'transparent', color: '#3859D0', fontWeight: 700, fontSize: 12, cursor: 'pointer' }}>
            {verArquivadas ? 'Ver activas' : `Arquivadas (${archivedCount})`}
          </button>
        )}
        {!mesaCanal && (
        <div style={{ marginLeft: 'auto', display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
          <div style={{ display: 'flex', gap: 2, alignItems: 'center', padding: '3px', background: 'var(--bg-surface, #fff)', borderRadius: 6, border: '1px solid var(--border-2, #E3E6EB)' }}>
            {[{ id: 'sonnet-4-6', label: 'Sonnet' }, { id: 'opus-4-7', label: 'Opus' }].map(m => {
              const active = modelo === m.id;
              return (
                <button key={m.id} onClick={() => !bulkBusy && setModelo(m.id)} disabled={bulkBusy}
                  style={{
                    padding: '4px 10px', fontSize: 10, fontWeight: 700, letterSpacing: '0.04em',
                    background: active ? 'var(--color-primary, #3859D0)' : 'transparent',
                    color: active ? '#fff' : 'var(--text-muted, #4a5e7a)',
                    border: 0, borderRadius: 4, cursor: bulkBusy ? 'not-allowed' : 'pointer',
                    fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase',
                  }}>{m.label}</button>
              );
            })}
          </div>
          <button onClick={runSelected} disabled={bulkBusy || selected.size === 0}
            className="btn btn-sm" style={{ fontSize: 11, opacity: (bulkBusy || selected.size === 0) ? 0.5 : 1 }}>
            <Icon name="sparkle" size={11} /> Gerar seleccionados ({selected.size})
          </button>
          <button onClick={runAllPending} disabled={bulkBusy || pendentesVista === 0}
            style={{
              background: 'var(--color-primary, #3859D0)', color: '#fff', border: 0,
              padding: '6px 12px', borderRadius: 'var(--radius-sm, 6px)',
              fontSize: 11, fontWeight: 700, cursor: bulkBusy ? 'not-allowed' : 'pointer',
              display: 'inline-flex', alignItems: 'center', gap: 5,
              opacity: (bulkBusy || pendentesVista === 0) ? 0.5 : 1,
            }}>
            <Icon name="sparkle" size={11} /> Gerar todos pendentes ({pendentesVista})
          </button>
        </div>
        )}
      </div>
      )}
      {oneShot && picked.length > 0 && (
        <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', padding: '8px 10px', marginBottom: 8, background: '#EFF3FF', borderRadius: 8 }}>
          <span style={{ fontSize: 12, fontWeight: 700, color: '#112954' }}>{picked.length === 1 ? '1 seleccionada' : `${picked.length} seleccionadas`}</span>
          <button type="button" disabled={pieceBusy || picked.length !== 1} onClick={() => setRenameRow(picked[0])} style={{ border: '1px solid #3859D0', background: '#fff', color: '#3859D0', borderRadius: 6, padding: '5px 10px', fontSize: 12, fontWeight: 700, cursor: pieceBusy || picked.length !== 1 ? 'not-allowed' : 'pointer', opacity: picked.length === 1 ? 1 : 0.45 }}>Editar nome</button>
          <button type="button" disabled={pieceBusy} onClick={() => aplicarAccao(verArquivadas ? 'restaurar' : 'arquivar', picked.map(r => r.idx))} style={{ border: '1px solid #E3E6EB', background: '#fff', color: '#112954', borderRadius: 6, padding: '5px 10px', fontSize: 12, fontWeight: 700, cursor: pieceBusy ? 'not-allowed' : 'pointer' }}>{verArquivadas ? 'Restaurar' : 'Arquivar'}</button>
          <button type="button" disabled={pieceBusy} onClick={pedirApagar} style={{ border: 0, background: 'transparent', color: '#991B1B', borderRadius: 6, padding: '5px 10px', fontSize: 12, fontWeight: 700, cursor: pieceBusy ? 'not-allowed' : 'pointer' }}>Apagar</button>
          {pieceErro && <span style={{ fontSize: 12, color: '#991B1B' }}>{pieceErro}</span>}
        </div>
      )}
      {renameRow && (
        <CtdPieceRenameModal
          titulo={renameRow.peca?.titulo || ''}
          busy={pieceBusy}
          onClose={() => setRenameRow(null)}
          erro={pieceErro}
          onSave={async (titulo) => {
            try {
              await aplicarAccao('renomear', [renameRow.idx], titulo);
              setRenameRow(null);
            } catch {}
          }}
        />
      )}

      {!mesaCanal && (
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12, marginBottom: 20 }}>
        {[
          { key: 'all',      label: 'PLANO',     value: counters.total,    accent: '#3859D0', activeBg: '#EFF3FF' },
          { key: 'pendente', label: 'PENDENTES',  value: counters.pendente, accent: '#64748B', activeBg: '#F1F5F9' },
          { key: 'gerado',   label: 'DRAFT',      value: counters.gerado,   accent: '#92400E', activeBg: '#FEF3C7' },
          { key: 'aprovado', label: 'APROVADAS',  value: counters.aprovado, accent: '#166534', activeBg: '#DCFCE7' },
        ].map(function(card) {
          var isActive = filterStatus === card.key;
          return (
            <div key={card.key}
              onClick={function() { setFStatus(isActive && card.key !== 'all' ? 'all' : card.key); }}
              style={{
                background: isActive ? card.activeBg : 'var(--bg-surface, #fff)',
                border: '1px solid ' + (isActive ? card.accent + '50' : 'var(--border, #E3E6EB)'),
                borderRadius: 8, padding: '14px 16px', cursor: 'pointer',
              }}>
              <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: isActive ? card.accent : 'var(--text-muted)', fontFamily: 'var(--font-mono, monospace)', marginBottom: 6 }}>{card.label}</div>
              <div style={{ fontSize: 28, fontWeight: 700, color: isActive ? card.accent : 'var(--text, #112954)', lineHeight: 1 }}>{card.value}</div>
              <div style={{ marginTop: 10, height: 3, background: isActive ? card.accent : 'var(--border, #E3E6EB)', borderRadius: 2, opacity: isActive ? 1 : 0.35 }} />
            </div>
          );
        })}
      </div>
      )}

      {/* Bulk progress */}
      {bulkBusy && (
        <div style={{ padding: '10px 16px', background: '#EFF6FF', borderBottom: '1px solid #DBEAFE', fontSize: 11, color: '#1E3A8A', display: 'flex', alignItems: 'center', gap: 10 }}>
          <div style={{ flex: 1 }}>
            <div style={{ fontWeight: 700, marginBottom: 3 }}>A gerar {bulkProg.done + 1}/{bulkProg.total} · {bulkProg.current}</div>
            <div style={{ height: 4, background: '#DBEAFE', borderRadius: 2, overflow: 'hidden' }}>
              <div style={{ height: '100%', background: '#3859D0', width: `${(bulkProg.done / bulkProg.total) * 100}%`, transition: 'width 300ms' }} />
            </div>
          </div>
        </div>
      )}

      {/* Table — UI alinhado com screen_marketing_briefings */}
      {commPlan.length === 0 ? (
        <div style={{ padding: 40, textAlign: 'center', color: 'var(--text-muted)', fontSize: 13, fontStyle: 'italic' }}>
          Sem peças no comm_plan da campanha. Confirma o Planeamento no módulo Campanhas.
        </div>
      ) : (
        <div className="scrollbar" style={{ overflowX: 'auto', width: '100%' }}>
          <table style={{ width: '100%', ...(mesaCanal ? {} : { minWidth: 1480 }), borderCollapse: 'collapse', fontSize: 12.5, fontFamily: 'var(--font-sans)' }}>
            <thead>
              <tr>
                <th style={{ ...thSt, width: 36, textAlign: 'center', padding: '4px 4px' }}>
                  <input type="checkbox" checked={allFilteredSelected} onChange={toggleAll} disabled={bulkBusy}
                    style={{ width: 14, height: 14, cursor: 'pointer', accentColor: 'var(--ai-500, #3859D0)' }} />
                </th>
                <th style={{ ...thSt, width: 40 }}>#</th>
                <th style={thSt}>Peça</th>
                {waCols && (
                  <>
                    <th style={thSt}>Imagem</th>
                    <th style={thSt}>Copy</th>
                  </>
                )}
                {showCanalCol && <th style={thSt}>Canal</th>}
                {showData && <th style={thSt}>Data</th>}
                {showPais && <th style={thSt}>País</th>}
                {showAutor && <th style={thSt}>Autor</th>}
                <th style={thSt}>{waCols ? 'Meta' : 'Estado'}</th>
                <th style={{ ...thSt, textAlign: 'right' }}>Acções</th>
              </tr>
            </thead>
            <tbody>
              {filtered.map((r, rowIndex) => {
                const busy = rowBusy.has(r.key) || bulkBusy;
                const st = STATUS_STYLE[r.status];
                const waPack = r.library && !r.waPending ? ctdWaPackFromCopy(r.existing) : null;
                const metaPill = waPack ? ctdWaMetaPill(waPack.metaStatus, waPack.rejectedReason) : null;
                const imagePill = r.waPending ? { fg: '#475569', bg: '#F1F5F9', label: '—', title: 'Template ainda por gerar' } : (waPack ? ctdWaImagePill(r.peca.waGroup, waPack) : null);
                const copyPill = r.waPending ? ctdWaCopyPill('pendente', false, '') : (waPack ? ctdWaCopyPill(r.copyStatus, r.copyStale, r.adRef) : null);
                const usp = r.peca.body || r.peca.usp || '';
                const title = r.peca.titulo || `Peça #${r.library ? r.groupPos : r.idx + 1}`;
                const subtitulo = r.peca.subtitulo || '';
                const author = r.existing?.approved_by || r.existing?.updated_by || r.existing?.actor_email || null;
                const canalRows = ctdNavRowsSameCanal(rows, r.peca);
                const canalPos = Math.max(0, canalRows.findIndex(x => x.idx === r.idx && x.country === r.country));
                const openModal = () => {
                  if (r.waPending) return;
                  onOpenGenerate && onOpenGenerate({ copy: r.existing, peca: r.peca, commPlanIndex: r.idx, country: r.country, allRows: rows, rowIndex: canalPos });
                };
                const isSelected = selected.has(r.key);
                // Separador visual quando muda country vs row anterior
                const prevRow = rowIndex > 0 ? filtered[rowIndex - 1] : null;
                const isNewCountryGroup = !prevRow || ctdGroupTitle(prevRow) !== ctdGroupTitle(r);
                return (
                  <React.Fragment key={r.key}>
                    {showGrupo && (rowIndex === 0 || isNewCountryGroup) && (
                      <tr aria-hidden>
                        <td colSpan={colN} style={{ padding: rowIndex === 0 ? '4px 12px' : '18px 12px 4px', borderBottom: '2px solid var(--ai-500, #3859D0)', background: 'transparent' }}>
                          <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--ai-500, #3859D0)', fontFamily: 'var(--font-mono, monospace)' }}>
                            {ctdGroupTitle(r)}
                          </div>
                        </td>
                      </tr>
                    )}
                  <CtdPlanRow
                    busy={busy}
                    isSelected={isSelected}
                    onClick={(e) => {
                      if (busy) return;
                      const tag = e.target.tagName;
                      if (tag === 'INPUT' || tag === 'BUTTON' || e.target.closest('button')) return;
                      openModal();
                    }}
                    rowIndex={rowIndex}
                  >
                    <td style={{ ...tdSt, padding: '4px 4px', textAlign: 'center' }} onClick={e => e.stopPropagation()}>
                      <input type="checkbox" checked={isSelected} onChange={() => toggleOne(r.key)} disabled={busy}
                        style={{ width: 14, height: 14, cursor: 'pointer', accentColor: 'var(--ai-500, #3859D0)' }} />
                    </td>
                    <td style={{ ...tdSt, fontFamily: 'var(--font-mono)', color: 'var(--text-dim)', fontSize: 11 }}>{r.library ? r.groupPos : r.idx + 1}</td>
                    <td style={{ ...tdSt, overflow: 'hidden' }}>
                      <div style={{ fontWeight: 500, color: 'var(--text)', fontSize: 12, fontFamily: 'var(--font-mono)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={usp || ''}>{title}</div>
                      {subtitulo && (
                        <div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 480 }} title={subtitulo}>
                          {subtitulo}
                        </div>
                      )}
                      {r.stale && <div style={{ fontSize: 10, color: '#92400E', fontWeight: 600, marginTop: 2 }}>{ctdIsWaPiece(r.peca, r.existing) ? 'Copy mudou · template desactualizado' : 'Copy mudou · gerar prompt e imagem'}</div>}
                    </td>
                    {waCols && imagePill && copyPill && (
                      <>
                        <td style={tdSt}>
                          <span title={imagePill.title} style={{ fontSize: 11, fontFamily: 'var(--font-mono)', fontWeight: 600, padding: '2px 8px', borderRadius: 4, color: imagePill.fg, background: imagePill.bg }}>{imagePill.label}</span>
                        </td>
                        <td style={tdSt}>
                          <span title={copyPill.title} style={{ fontSize: 11, fontFamily: 'var(--font-mono)', fontWeight: 600, padding: '2px 8px', borderRadius: 4, color: copyPill.fg, background: copyPill.bg }}>{copyPill.label}</span>
                        </td>
                      </>
                    )}
                    {showCanalCol && <td style={tdSt}>{canalBadge(r.peca)}</td>}
                    {showData && <td style={{ ...tdSt, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>{r.peca.planned_date ? ctdFmtDate(r.peca.planned_date) : '—'}</td>}
                    {showPais && <td style={{ ...tdSt, fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600 }}>{r.country}</td>}
                    {showAutor && <td style={{ ...tdSt, whiteSpace: 'nowrap' }}>{ctdAuthorChip(author)}</td>}
                    <td style={tdSt}>
                      <span title={metaPill ? metaPill.title : undefined} style={{ fontSize: 11, fontFamily: 'var(--font-mono)', fontWeight: 600, padding: '2px 8px', borderRadius: 4, display: 'inline-block', color: (metaPill || st).fg, background: (metaPill || st).bg }}>
                        {metaPill ? metaPill.label : st.label.toUpperCase()}
                      </span>
                    </td>
                    <td style={{ ...tdSt, textAlign: 'right', whiteSpace: 'nowrap' }} onClick={e => e.stopPropagation()}>
                      <button onClick={() => runOne(r)} disabled={busy}
                        title={r.waPending ? 'Gerar os templates WhatsApp' : r.library ? 'Abrir' : r.status === 'pendente' ? 'Gerar copy' : 'Regerar copy (substitui)'}
                        style={{
                          fontSize: 10, fontWeight: 700, letterSpacing: '0.04em', padding: '4px 9px',
                          background: r.status === 'pendente' ? 'var(--ai-500, #3859D0)' : 'transparent',
                          color: r.status === 'pendente' ? '#fff' : 'var(--ai-500, #3859D0)',
                          border: '1px solid var(--ai-500, #3859D0)',
                          borderRadius: 4, cursor: busy ? 'not-allowed' : 'pointer',
                          opacity: busy ? 0.5 : 1, marginRight: 4, fontFamily: 'var(--font-mono)', textTransform: 'uppercase',
                        }}>
                        {rowBusy.has(r.key) ? '…' : r.waPending ? 'Gerar' : r.library ? 'Abrir' : r.status === 'pendente' ? 'Gerar' : 'Regerar'}
                      </button>
                    </td>
                  </CtdPlanRow>
                  </React.Fragment>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
};

// Row com hover suave (padrão screen_marketing_briefings)
const CtdPlanRow = ({ children, busy, isSelected, onClick, rowIndex }) => {
  const [hovered, setHovered] = React.useState(false);
  const bg = isSelected ? 'var(--bg-selected, color-mix(in oklch, var(--ai-500) 8%, transparent))'
           : hovered ? 'var(--bg-hover)'
           : 'transparent';
  return (
    <tr
      onClick={onClick}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      style={{
        background: bg,
        cursor: busy ? 'default' : 'pointer',
        transition: 'background 150ms ease',
        animation: 'slide-up 0.2s ease-out both',
        animationDelay: `${Math.min(rowIndex || 0, 9) * 25}ms`,
      }}
    >
      {children}
    </tr>
  );
};

// ─── CtdTabCopies — conteudo_copy schema (multi-mercado + language tabs + filter) ─
const CtdTabCopies = ({ campanha, copies, campFull, userEmail, reloadCopies, onBackToKanban, prompts, imagens, onNavigate, reloadPrompts, reloadImagens, focusPiece, onPiece, dismissPiece, livePieceRef, onCopyBase, onCopySaved, onPlanItem, onPlan, onRefresh, lockCanal }) => {
  const Icon = window.Icon;
  const [generating, setGenerating]           = React.useState(false);
  const [generatingIdiomas, setGenIdiomas]    = React.useState(false);
  const [genStep, setGenStep]                 = React.useState({ step: 0, total: 0, message: '' });
  const [error, setError]                     = React.useState(null);
  const [filterCanalLabel, setFilterCanalLabel] = React.useState(lockCanal || 'all');
  React.useEffect(() => { if (lockCanal) setFilterCanalLabel(lockCanal); }, [lockCanal]);
  const [filterCountry, setFilterCountry]     = React.useState('all');
  const [filterStatus, setFilterStatus]       = React.useState('all');
  const [filterSearch, setFilterSearch]       = React.useState('');
  const [filterCopyType, setFilterCopyType]   = React.useState('organico');  // organico | performance | all
  const [staleIds, setStaleIds]               = React.useState([]);
  const [diffCopy, setDiffCopy]               = React.useState(null);   // { copy, master }
  const [historyCopy, setHistoryCopy]         = React.useState(null);
  const [genItemCopy, setGenItemCopy]         = React.useState(null);   // peça aberta nesta tab
  if (livePieceRef) {
    livePieceRef.current = genItemCopy ? {
      idx: genItemCopy.commPlanIndex,
      country: genItemCopy.country || null,
      titulo: genItemCopy.peca?.titulo || genItemCopy.copy?.item_title || null,
      contentType: genItemCopy.peca?.content_type || genItemCopy.copy?.content_type || null,
      waGroup: genItemCopy.peca?.waGroup || null,
    } : null;
  }
  React.useEffect(() => {
    onPiece?.(genItemCopy ? {
      idx: genItemCopy.commPlanIndex,
      country: genItemCopy.country || null,
      titulo: genItemCopy.peca?.titulo || genItemCopy.copy?.item_title || null,
      contentType: genItemCopy.peca?.content_type || null,
      waGroup: genItemCopy.peca?.waGroup || null,
    } : null);
  }, [genItemCopy?.commPlanIndex, genItemCopy?.country, genItemCopy?.peca?.titulo, genItemCopy?.peca?.content_type, genItemCopy?.peca?.waGroup]);
  React.useEffect(() => {
    if (!dismissPiece) return;
    setGenItemCopy(null);
  }, [dismissPiece]);
  const focusToken = focusPiece ? `${focusPiece.idx}:${focusPiece.country || ''}:${focusPiece.titulo || ''}` : '';
  const openedFocus = React.useRef('');
  React.useEffect(() => {
    if (!focusToken || openedFocus.current === focusToken) return;
    const idx = Number(focusPiece.idx);
    const plan = campFull?.proposta_json?.comm_plan || [];
    const wantTitle = ctdNormTitle(focusPiece.titulo);
    let peca = null;
    let pieceIdx = idx;
    if (wantTitle) {
      const found = plan.findIndex(p => ctdNormTitle(p?.titulo) === wantTitle);
      if (found >= 0) { peca = plan[found]; pieceIdx = found; }
    }
    if (!peca && Number.isFinite(idx)) { peca = plan[idx]; pieceIdx = idx; }
    if (!peca) return;
    openedFocus.current = focusToken;
    const country = focusPiece.country || null;
    const copy = (copies || []).find(c => Number(c.comm_plan_item_index) === pieceIdx && (!country || !c.country || String(c.country).toUpperCase() === String(country).toUpperCase()));
    setGenItemCopy({ copy: copy || null, peca, commPlanIndex: pieceIdx, country: country || 'PT' });
  }, [focusToken, campFull, copies, focusPiece]);

  const matchCopyForRow = (c, idx, country, peca) => {
    if (Number(c.comm_plan_item_index) !== idx) return false;
    if (!(String(c.country || '').toUpperCase() === country || (!c.country && country === 'PT'))) return false;
    if (c.item_title && peca?.titulo && c.item_title !== peca.titulo) return false;
    if (c.content_type && peca?.content_type && c.content_type !== peca.content_type) return false;
    return true;
  };

  const handleNavTo = (newRowIndex) => {
    const navRows = ctdNavRowsSameCanal(genItemCopy?.allRows || [], genItemCopy?.peca);
    if (!navRows.length || newRowIndex < 0 || newRowIndex >= navRows.length) return;
    const r = navRows[newRowIndex];
    const freshCopy = list.find(c => matchCopyForRow(c, r.idx, r.country, r.peca));
    setGenItemCopy({ copy: freshCopy || r.existing, peca: r.peca, commPlanIndex: r.idx, country: r.country, allRows: genItemCopy.allRows, rowIndex: newRowIndex });
  };

  const list = Array.isArray(copies) ? copies : [];

  // Fetch stale ids on copies change
  React.useEffect(() => {
    if (!campanha?.id) return;
    CtdAPI.copiesStale(campanha.id).then(d => setStaleIds(d?.stale_ids || [])).catch(() => setStaleIds([]));
  }, [campanha?.id, list.length]);

  // Distinct languages present (with PT default)
  const langsPresent = React.useMemo(() => {
    const s = new Set();
    list.forEach(c => s.add(c.lingua || 'pt'));
    if (!s.size) s.add('pt');
    return Array.from(s).sort((a, b) => a === 'pt' ? -1 : b === 'pt' ? 1 : a.localeCompare(b));
  }, [list]);
  const [currentLingua, setCurrentLingua] = React.useState(langsPresent[0] || 'pt');
  React.useEffect(() => {
    if (!langsPresent.includes(currentLingua)) setCurrentLingua(langsPresent[0] || 'pt');
  }, [langsPresent, currentLingua]);

  // Label canónico por copy — usa ctdCanalLabel para alinhar com Tab Prompts/Imagens
  const copyLabel = (c) => ctdCanalLabel({ content_type: c.content_type, canal: c.canal }, c.canal);

  // Distinct canonical canal labels present (ordem canónica)
  const CANAL_ORDER = ['Meta Ads', 'Email', 'WhatsApp', 'LinkedIn', 'Google', 'Blog', 'Landing', 'LED', 'Site', 'Comunicação Interna'];
  const canaisPresent = React.useMemo(() => {
    const s = new Set();
    list.forEach(c => s.add(copyLabel(c)));
    // Ordena por CANAL_ORDER; resto alfabético
    return Array.from(s).sort((a, b) => {
      const ia = CANAL_ORDER.indexOf(a), ib = CANAL_ORDER.indexOf(b);
      if (ia !== -1 && ib !== -1) return ia - ib;
      if (ia !== -1) return -1;
      if (ib !== -1) return 1;
      return a.localeCompare(b);
    });
  }, [list]);

  // Distinct countries (ex: PT, ES, BR, MX, FR, ...)
  const countriesPresent = React.useMemo(() => {
    const s = new Set();
    list.forEach(c => c.country && s.add(c.country));
    return Array.from(s).sort();
  }, [list]);
  const COUNTRY_LABEL = {
    PT: 'Portugal (PT)', ES: 'Espanha (ES)', BR: 'Brasil (BR)', FR: 'França (FR)',
    IT: 'Itália (IT)', DE: 'Alemanha (DE)', UK: 'Reino Unido (UK)', US: 'EUA (US)',
    NL: 'Holanda (NL)', BE: 'Bélgica (BE)', CH: 'Suíça (CH)', AT: 'Áustria (AT)',
    IE: 'Irlanda (IE)', CA: 'Canadá (CA)', MX: 'México (MX)', AR: 'Argentina (AR)',
  };
  const countryLabel = (cc) => COUNTRY_LABEL[cc] || cc;

  const LANG_LABEL = { pt: 'PT-PT', es: 'ES-ES', en: 'EN', de: 'DE', fr: 'FR', it: 'IT', nl: 'NL' };
  const langLabel  = (l) => LANG_LABEL[l] || (l || '').toUpperCase();

  // Filter pipeline
  const filtered = React.useMemo(() => {
    const pass = list.filter(c => {
      const lng = c.lingua || 'pt';
      if (lng !== currentLingua) return false;
      if (filterCanalLabel !== 'all' && copyLabel(c) !== filterCanalLabel) return false;
      if (filterCountry !== 'all' && (c.country || '') !== filterCountry) return false;
      if (filterStatus !== 'all' && (c.status || 'gerado') !== filterStatus) return false;
      if (filterCopyType !== 'all' && (c.copy_type || 'organico') !== filterCopyType) return false;
      if (filterSearch.trim()) {
        const q = filterSearch.trim().toLowerCase();
        const hay = [c.item_title, c.headline, c.body, c.copy_imagem, c.cta].filter(Boolean).join(' ').toLowerCase();
        if (!hay.includes(q)) return false;
      }
      return true;
    });
    // Ordem canónica: canal (CANAL_ORDER — Meta Ads primeiro, Comunicação Interna no fim)
    // → depois comm_plan_item_index (ordem natural do plano) → depois canal raw como tiebreaker
    return pass.sort((a, b) => {
      const la = copyLabel(a), lb = copyLabel(b);
      const ia = CANAL_ORDER.indexOf(la), ib = CANAL_ORDER.indexOf(lb);
      const pa = ia === -1 ? 999 : ia;
      const pb = ib === -1 ? 999 : ib;
      if (pa !== pb) return pa - pb;
      const ida = a.comm_plan_item_index ?? 999;
      const idb = b.comm_plan_item_index ?? 999;
      if (ida !== idb) return ida - idb;
      return (a.canal || '').localeCompare(b.canal || '');
    });
  }, [list, currentLingua, filterCanalLabel, filterCountry, filterStatus, filterCopyType, filterSearch]);

  const handleGenerate = async () => {
    if (!campanha?.id) return;
    if (list.length > 0 && !confirm('Regerar copies? As copies existentes serão substituídas (todos os mercados).')) return;
    setGenerating(true); setError(null);
    setGenStep({ step: 0, total: 0, message: 'A iniciar geração de copies…' });
    try {
      await CtdAPI.generateCopies(
        campanha.id, {},
        (step, total, message) => setGenStep({ step, total, message }),
      );
      reloadCopies?.();
    } catch (e) { setError(e.message || 'Erro ao gerar copies'); }
    setGenerating(false);
    setGenStep({ step: 0, total: 0, message: '' });
  };
  const handleGenerateIdiomas = async () => {
    if (!campanha?.id) return;
    if (!confirm('Adaptar copies PT para outras línguas? As traduções existentes serão substituídas.')) return;
    setGenIdiomas(true); setError(null);
    setGenStep({ step: 0, total: 0, message: 'A adaptar…' });
    try {
      await CtdAPI.generateIdiomas(
        campanha.id, [],
        (step, total, message) => setGenStep({ step, total, message }),
      );
      reloadCopies?.();
    } catch (e) { setError(e.message || 'Erro ao adaptar idiomas'); }
    setGenIdiomas(false);
    setGenStep({ step: 0, total: 0, message: '' });
  };

  // Empty state — mostra apenas a tabela do plano (sempre visível, mesmo sem copies)
  if (list.length === 0 && !generating && !genItemCopy) {
    return (
      <div style={{ padding: '20px 32px 32px', width: '100%' }}>
        <CtdCopiesPlanTable
          campanhaId={campanha?.id}
          campFull={campFull}
          copies={list}
          prompts={prompts}
          reloadCopies={reloadCopies}
          reloadPrompts={reloadPrompts}
          userEmail={userEmail}
          lockCanal={lockCanal}
          onPlan={onPlan}
          onRefresh={onRefresh}
          onOpenGenerate={(c) => setGenItemCopy(c)}
        />
      </div>
    );
  }

  const selectStyle = {
    padding: '5px 8px', fontSize: 11.5,
    border: '1px solid var(--border-2, #E3E6EB)',
    borderRadius: 'var(--radius-sm, 6px)',
    background: 'var(--bg-surface, #fff)',
    color: 'var(--text, #112954)',
    cursor: 'pointer',
  };

  // Detail view inline (substitui a tabela quando um item está aberto)
  if (genItemCopy) {
    const cc = genItemCopy.country || 'PT';
    const fresh = list
      .filter(c => matchCopyForRow(c, genItemCopy.commPlanIndex, cc, genItemCopy.peca))
      .sort((a, b) => (String(b.body||'').length + String(b.headline||'').length) - (String(a.body||'').length + String(a.headline||'').length))[0];
    const navRows = ctdNavRowsSameCanal(genItemCopy.allRows || [], genItemCopy.peca);
    const rowIdx = Math.max(0, navRows.findIndex(r => r.idx === genItemCopy.commPlanIndex && r.country === (genItemCopy.country || r.country)));
    const total  = navRows.length || 1;
    const livePeca = (campFull?.proposta_json?.comm_plan || [])[genItemCopy.commPlanIndex] || genItemCopy.peca;
    const pecaTitle = livePeca?.titulo || genItemCopy.peca?.titulo || `Peça #${(genItemCopy.commPlanIndex ?? 0) + 1}`;
    const canalCat = genItemCopy.peca ? ctdGetCategoria(genItemCopy.peca.content_type) : null;
    return (
      <div style={{ width: '100%' }}>
        <div style={{ position: 'sticky', top: 0, zIndex: 10, background: 'var(--bg-page, #F4F6FA)', padding: '12px 32px 0', marginBottom: 0 }}>
          <CtdBreadcrumb
            trail={[
              { label: campanha?.titulo || 'Campanha', onClick: onBackToKanban },
              { label: 'Copies', onClick: () => setGenItemCopy(null) },
              { label: pecaTitle, badge: canalCat ? { text: ctdCanalLabel(genItemCopy.peca), color: canalCat.color } : null },
            ]}
            onBack={() => setGenItemCopy(null)}
            currentPos={rowIdx + 1}
            totalItems={total}
            onPrev={rowIdx > 0 ? () => handleNavTo(rowIdx - 1) : null}
            onNext={rowIdx < total - 1 ? () => handleNavTo(rowIdx + 1) : null}
          />
        </div>
        <div style={{ padding: '12px 32px 32px' }}>
        <CtdGenerateCopyItemModal
          campanhaId={campanha?.id}
          copy={fresh || genItemCopy.copy}
          peca={livePeca}
          commPlanIndex={genItemCopy.commPlanIndex}
          userEmail={userEmail}
          lockCanal={lockCanal}
          brandSlug={campanha?.brand_slug}
          brandName={campanha?.brand_name}
          reloadCopies={reloadCopies}
          reloadPrompts={reloadPrompts}
          reloadImagens={reloadImagens}
          onNavigate={onNavigate}
          proposta={campFull?.proposta_json || {}}
          campFull={campFull}
          prompts={prompts}
          imagens={imagens}
          onClose={() => setGenItemCopy(null)}
          onDone={() => reloadCopies?.()}
          currentPos={rowIdx + 1}
          totalItems={total}
          onPrev={rowIdx > 0 ? () => handleNavTo(rowIdx - 1) : null}
          onNext={rowIdx < total - 1 ? () => handleNavTo(rowIdx + 1) : null}
          inline={true}
          onCopyBase={onCopyBase}
          onCopySaved={onCopySaved}
          onPlanItem={onPlanItem}
        />
        {diffCopy && (
          <CtdCopyDiffModal copy={diffCopy.copy} master={diffCopy.master} onClose={() => setDiffCopy(null)} />
        )}
        {historyCopy && (
          <CtdCopyHistoryModal copy={historyCopy} onClose={() => setHistoryCopy(null)} />
        )}
        </div>{/* /padding inner */}
      </div>
    );
  }

  return (
    <div style={{ padding: '20px 32px 32px', width: '100%' }}>
      <CtdCopiesPlanTable
        campanhaId={campanha?.id}
        campFull={campFull}
        copies={list}
        prompts={prompts}
        reloadCopies={reloadCopies}
        reloadPrompts={reloadPrompts}
        userEmail={userEmail}
        lockCanal={lockCanal}
        onPlan={onPlan}
        onRefresh={onRefresh}
        onOpenGenerate={(c) => setGenItemCopy(c)}
      />

      {(generating || generatingIdiomas) && (
        <div style={{ marginBottom: 14 }}>
          <CtdGenProgress step={genStep.step} total={genStep.total} message={genStep.message} />
        </div>
      )}

      {error && (
        <div style={{
          padding: '10px 14px', marginBottom: 14,
          background: '#FEE2E2', color: '#991B1B',
          borderRadius: 'var(--radius-sm, 6px)', fontSize: 12,
        }}>{error}</div>
      )}

      {/* Modais secundárias (Diff/History) — modo modal mesmo quando lista visível */}
      {diffCopy && (
        <CtdCopyDiffModal copy={diffCopy.copy} master={diffCopy.master} onClose={() => setDiffCopy(null)} />
      )}
      {historyCopy && (
        <CtdCopyHistoryModal copy={historyCopy} onClose={() => setHistoryCopy(null)} />
      )}
    </div>
  );
};
// ─── CtdMetaAdFullMockup — feed IG completo (headline + primary text + CTA) ──
const CtdMetaAdFullMockup = ({ url, copy, peca, brandSlug }) => {
  const brandName = (brandSlug || 'digidelta').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
  const primaryText = copy?.body || copy?.caption_organica || '';
  const headline    = copy?.copy_imagem || copy?.headline || peca?.titulo || 'Headline';
  const description = copy?.copy_imagem_sub || copy?.cta || '';
  const ctaText     = copy?.copy_imagem_cta || copy?.cta || 'Saiba mais';
  return (
    <div style={{ background: '#fff', border: '1px solid #dbdbdb', borderRadius: 8, maxWidth: 470, fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif' }}>
      {/* Header: avatar + brand + sponsored */}
      <div style={{ padding: '10px 14px', display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid #efefef' }}>
        <div style={{ width: 32, height: 32, borderRadius: '50%', background: 'linear-gradient(45deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 12, fontWeight: 700 }}>
          {brandName.charAt(0)}
        </div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 13, fontWeight: 700, color: '#262626', lineHeight: 1.2 }}>{brandName.toLowerCase()}</div>
          <div style={{ fontSize: 11, color: '#8e8e8e', lineHeight: 1.2 }}>Patrocinado</div>
        </div>
        <span style={{ fontSize: 18, color: '#262626' }}>⋯</span>
      </div>
      {/* Image */}
      <div style={{ background: '#000', aspectRatio: ctdAspectForPeca(peca), display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
        {url ? (
          <img src={url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
        ) : (
          <div style={{ color: '#666', fontSize: 12 }}>Sem imagem</div>
        )}
      </div>
      {/* CTA banner */}
      <div style={{ background: '#fafafa', padding: '10px 14px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #efefef' }}>
        <div style={{ minWidth: 0 }}>
          <div style={{ fontSize: 12, fontWeight: 600, color: '#262626', textTransform: 'uppercase', letterSpacing: '0.02em', lineHeight: 1.2 }}>{headline}</div>
          {description && <div style={{ fontSize: 11, color: '#8e8e8e', lineHeight: 1.2, marginTop: 2 }}>{description}</div>}
        </div>
        <button style={{ background: '#efefef', border: 0, padding: '6px 12px', borderRadius: 4, fontSize: 12, fontWeight: 600, color: '#262626', cursor: 'default', whiteSpace: 'nowrap', flexShrink: 0, marginLeft: 8 }}>
          {ctaText}
        </button>
      </div>
      {/* Actions */}
      <div style={{ padding: '8px 14px', display: 'flex', gap: 14, fontSize: 22, color: '#262626' }}>
        <span>♡</span><span>💬</span><span>↗</span><span style={{ marginLeft: 'auto' }}>🔖</span>
      </div>
      {/* Primary text */}
      {primaryText && (
        <div style={{ padding: '0 14px 14px', fontSize: 13, color: '#262626', lineHeight: 1.4 }}>
          <span style={{ fontWeight: 700 }}>{brandName.toLowerCase()} </span>
          {primaryText.length > 220 ? primaryText.slice(0, 220) + '…' : primaryText}
        </div>
      )}
    </div>
  );
};

// ─── Aspect ratio por content_type — dimensões correctas por canal ────────────
const ctdAspectForPeca = (peca) => {
  const ct = peca?.content_type;
  if (ct === 'social_post') return '4/5';           // Meta feed 1080×1350
  if (ct === 'email_html' || ct === 'email' || ct === 'briefing_interno') return '191/100';  // Email / briefing hero
  if (ct === 'wa_template') return '191/100';       // WA media header
  if (ct === 'wa_message') return '1/1';            // WA sticker/image
  if (ct === 'blog_post') return '16/9';            // Blog hero
  if (ct === 'landing_page') return '16/9';         // Landing hero
  return '4/5';
};

// ─── Ordem canónica dos canais (paid → owned → earned) ────────────────────────
const CTD_CANAL_ORDER = ['Meta Ads', 'Email', 'WhatsApp', 'Blog', 'Landing', 'Comunicação Interna', 'LED', 'Site'];
const CTD_CANAL_API = {
  'Meta Ads': 'meta_ads',
  'Email': 'email',
  'WhatsApp': 'whatsapp',
  'Blog': 'website',
  'Landing': 'website',
  'Site': 'website',
  'LED': 'muppi_led',
};

const CTD_VIDEO_TYPES = ['social_post'];

function CtdVideoFullscreen({ url, stillUrl, onClose, brandName }) {
  React.useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    const onKey = (e) => { if (e.key === 'Escape') onClose?.(); };
    window.addEventListener('keydown', onKey);
    return () => { document.body.style.overflow = prev; window.removeEventListener('keydown', onKey); };
  }, [onClose]);
  if (!url) return null;
  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 9800, background: 'rgba(6,8,14,0.96)', display: 'flex', flexDirection: 'column' }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 20px', flexShrink: 0 }}>
        <span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.55)', fontFamily: 'var(--font-mono, monospace)' }}>{brandName || 'Story'} · 9:16 · 15s · 1080p</span>
        <button type="button" onClick={onClose} style={{ background: 'rgba(255,255,255,0.1)', border: 0, color: '#fff', width: 36, height: 36, borderRadius: 8, cursor: 'pointer', fontSize: 18, lineHeight: 1 }}>×</button>
      </div>
      <div style={{ flex: 1, minHeight: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 24px 24px' }} onClick={e => e.stopPropagation()}>
        <video src={url} poster={stillUrl || undefined} autoPlay controls playsInline
          style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', display: 'block', background: '#000' }} />
      </div>
    </div>
  );
}

function CtdStoriesVideoMockup({ videoUrl, stillUrl, brandName, brandSlug, copyForm, onOpenFullscreen }) {
  return (
    <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
      <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
        <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>Pré-visualização</span>
        <button type="button" disabled={!videoUrl} onClick={() => onOpenFullscreen?.()}
          style={{ border: 0, background: 'none', padding: 0, fontSize: 9, color: videoUrl ? '#3859D0' : '#94a3b8', fontFamily: 'var(--font-mono, monospace)', cursor: videoUrl ? 'pointer' : 'default', fontWeight: 600 }}>
          Stories · 9:16
        </button>
      </div>
      <CtdMetaAdsFeedMockup
        form={copyForm || {}}
        flush
        brandName={brandName}
        brandSlug={brandSlug}
        imageUrl={stillUrl}
        videoUrl={videoUrl}
        aspectRatio="9 / 16"
        onOpenFullscreen={onOpenFullscreen}
      />
    </div>
  );
}

function CtdVideoHistoryStrip({ prompt, currentUrl, disabled, onRestore }) {
  const items = ctdVideoArchive(prompt);
  if (!items.length) return null;
  return (
    <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
      <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
        <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>Vídeos deste anúncio</span>
        <span style={{ fontSize: 9, color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)' }}>{items.length}</span>
      </div>
      <div style={{ padding: 10, display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6, maxHeight: 320, overflowY: 'auto' }}>
        {items.map((h, i) => {
          const isCurrent = !!(currentUrl && h.url === currentUrl);
          const when = h.created_at ? String(h.created_at).replace('T', ' ').slice(0, 16) : '';
          return (
            <button key={`${h.url}-${i}`} type="button" disabled={disabled || isCurrent}
              onClick={() => { if (!isCurrent) onRestore?.(h.url); }}
              title={isCurrent ? 'Vídeo actual no mockup' : 'Usar no mockup'}
              style={{ padding: 0, border: isCurrent ? '2px solid #3859D0' : '1px solid var(--border-2, #E3E6EB)', borderRadius: 6, overflow: 'hidden', background: '#111318', cursor: (disabled || isCurrent) ? 'default' : 'pointer', opacity: disabled ? 0.55 : 1, textAlign: 'left' }}>
              <div style={{ aspectRatio: '9 / 16', overflow: 'hidden', position: 'relative', background: '#111' }}>
                <video src={h.url} muted playsInline preload="metadata" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
                {isCurrent && <span style={{ position: 'absolute', top: 6, left: 6, fontSize: 8, fontWeight: 800, letterSpacing: '0.08em', textTransform: 'uppercase', background: '#3859D0', color: '#fff', borderRadius: 4, padding: '2px 6px', fontFamily: 'var(--font-mono, monospace)' }}>Actual</span>}
              </div>
              <div style={{ padding: '5px 6px', background: '#fff' }}>
                <div style={{ fontSize: 9, fontWeight: 700, color: isCurrent ? '#3859D0' : '#112954', fontFamily: 'var(--font-mono, monospace)' }}>{isCurrent ? 'Actual' : 'Geração'}</div>
                {when && <div style={{ fontSize: 8, color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', marginTop: 1 }}>{when}</div>}
              </div>
            </button>
          );
        })}
      </div>
    </div>
  );
}

const CtdVideoDetail = ({ campanhaId, row, userEmail, onDone, onNext, campFull, brandSlug, brandName, onNavigate }) => {
  const Icon = window.Icon;
  const { peca, prompt: initialPrompt, idx, copy: rowCopy, stale: copyStale } = row;
  const [prompt, setPrompt] = React.useState(initialPrompt);
  const [motion, setMotion] = React.useState(initialPrompt?.video_prompt_text || '');
  const [busyPrompt, setBusyPrompt] = React.useState(false);
  const [busyVideo, setBusyVideo] = React.useState(false);
  const [busyRestore, setBusyRestore] = React.useState(false);
  const [step, setStep] = React.useState({ step: 0, total: 0, message: '' });
  const [error, setError] = React.useState(null);
  const [fullscreen, setFullscreen] = React.useState(false);
  React.useEffect(() => {
    setPrompt(initialPrompt);
    setMotion(initialPrompt?.video_prompt_text || '');
    setError(null); setStep({ step: 0, total: 0, message: '' }); setFullscreen(false);
  }, [initialPrompt?.id, idx]);
  const hasImage = !!(prompt?.magnific_url || prompt?.image_url);
  const imageApproved = !!prompt?.image_approved_at;
  const hasVideo = !!prompt?.video_url;
  const videoApproved = !!prompt?.video_approved_at;
  const openStep = (tab) => onNavigate?.({
    tab,
    idx,
    country: rowCopy?.country || peca?.country || null,
    titulo: peca?.titulo || null,
    contentType: peca?.content_type || null,
  });
  const canalLabel = ctdCanalLabel(peca);
  const refreshPrompt = async () => {
    try {
      const fresh = await CtdAPI.prompts(campanhaId);
      const list = Array.isArray(fresh) ? fresh : (fresh?.prompts || []);
      const found = list.find(p => p.id === prompt?.id) || ctdPromptForPeca(list, idx, peca, rowCopy);
      if (found) { setPrompt(found); setMotion(found.video_prompt_text || motion); }
    } catch {}
  };
  const busy = busyPrompt || busyVideo || busyRestore;
  const onGenStep = (s, t, m) => setStep({ step: s || 0, total: t || 0, message: m || '' });
  const handleMotion = async () => {
    if (!prompt?.id) return;
    setBusyPrompt(true); setError(null);
    setStep({ step: 0, total: 1, message: 'A escrever o movimento…' });
    try {
      const result = await CtdAPI.generateVideoPrompt(campanhaId, prompt.id, { actor_email: userEmail }, onGenStep);
      const text = result?.prompt || '';
      if (text) setMotion(text);
      setPrompt(pp => ({ ...pp, video_prompt_text: text || pp.video_prompt_text }));
      await refreshPrompt();
    } catch (e) { setError(e.message || 'Erro ao gerar prompt de movimento'); }
    setBusyPrompt(false); setStep({ step: 0, total: 0, message: '' });
  };
  const handleVideo = async () => {
    if (!prompt?.id) return;
    setBusyVideo(true); setError(null);
    setStep({ step: 0, total: 4, message: 'A iniciar Seedance 1080p 15s…' });
    try {
      const result = await CtdAPI.generateVideo(campanhaId, prompt.id, { actor_email: userEmail, video_prompt: motion }, onGenStep);
      setPrompt(pp => ({ ...pp, video_url: result?.url || pp.video_url, video_source_url: result?.source_url || pp.video_source_url, video_history: result?.history || pp.video_history, video_prompt_text: motion || pp.video_prompt_text, video_approved_at: null }));
      await refreshPrompt();
      onDone?.();
    } catch (e) { setError(e.message || 'Erro ao gerar vídeo'); }
    setBusyVideo(false); setStep({ step: 0, total: 0, message: '' });
  };
  const handleApprove = async () => {
    if (!prompt?.id) return;
    setBusyVideo(true); setError(null);
    try { await CtdAPI.approveVideo(campanhaId, prompt.id, { approver_email: userEmail }); await refreshPrompt(); onDone?.(); }
    catch (e) { setError(e.message || 'Erro a aprovar'); }
    setBusyVideo(false);
  };
  const handleRestore = async (url) => {
    if (!prompt?.id || !url) return;
    setBusyRestore(true); setError(null);
    try {
      const data = await CtdAPI.restoreVideo(campanhaId, prompt.id, url);
      setPrompt(pp => ({ ...pp, video_url: data.url, video_history: data.history || pp.video_history, video_approved_at: null }));
      await refreshPrompt(); onDone?.();
    } catch (e) { setError(e.message || 'Erro ao restaurar'); }
    setBusyRestore(false);
  };
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'minmax(292px, 320px) minmax(0, 1fr) 300px', gap: 20, alignItems: 'start' }}>
      <div style={{ minWidth: 0, display: 'flex', flexDirection: 'column', gap: 12 }}>
        <div style={{ background: '#fff', border: '1px solid #BBF7D0', borderRadius: 8, overflow: 'hidden' }}>
          <div style={{ padding: '10px 14px', borderBottom: '1px solid #DCFCE7', background: '#F0FDF4', display: 'flex', justifyContent: 'space-between' }}>
            <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#166534', fontFamily: 'var(--font-mono, monospace)' }}>PIPELINE</span>
            <span style={{ fontSize: 10, fontWeight: 700, color: '#166534' }}>{canalLabel}</span>
          </div>
          <div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 10 }}>
            <CtdPipelineStep n="1" label="Copy" detail="Desta peça" state={rowCopy?.status === 'aprovado' ? 'done' : 'wait'} />
            <CtdPipelineStep n="2" label="Prompt" detail={prompt?.status === 'approved' ? 'Aprovado' : 'Pendente'} state={prompt?.status === 'approved' ? 'done' : 'wait'} />
            <CtdPipelineStep n="3" label="Imagem" detail={imageApproved ? 'Aprovada' : hasImage ? 'Gerada · por aprovar' : 'Gera na tab Imagens'} state={imageApproved ? 'done' : hasImage ? 'current' : 'wait'} onClick={hasImage ? () => openStep('imagens') : null} />
            <CtdPipelineStep n="4" label="Video" detail={videoApproved ? 'Aprovado · Story 15s editado' : hasVideo ? 'Gerado · por aprovar' : imageApproved ? 'Seedance + motion graphics' : 'Depois da imagem aprovada'} state={hasVideo && videoApproved ? 'done' : imageApproved ? 'current' : 'wait'} />
            {copyStale && <button type="button" onClick={() => openStep('prompts')} style={{ background: '#3859D0', color: '#fff', border: 0, width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: 'pointer' }}>Gerar prompt desta peça</button>}
            {!copyStale && !imageApproved && <button type="button" onClick={() => openStep('imagens')} style={{ background: '#3859D0', color: '#fff', border: 0, width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: 'pointer' }}>{hasImage ? 'Aprovar imagem' : 'Ir à tab Imagens'}</button>}
            {!copyStale && imageApproved && hasVideo && !videoApproved && (
              <button type="button" onClick={handleApprove} disabled={busy} style={{ background: '#1F8A52', color: '#fff', border: 0, width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: busy ? 'not-allowed' : 'pointer' }}><Icon name="check" size={13} /> Aprovar vídeo</button>
            )}
            {!copyStale && imageApproved && videoApproved && (
              <button type="button" onClick={() => openStep('peca')} style={{ background: '#3859D0', color: '#fff', border: 0, width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: 'pointer' }}>Peça final</button>
            )}
            {!copyStale && imageApproved && (
              <button type="button" onClick={handleVideo} disabled={busy} style={{ background: hasVideo ? 'transparent' : '#3859D0', color: hasVideo ? '#1F8A52' : '#fff', border: hasVideo ? '1px solid #1F8A52' : 0, width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: busy ? 'not-allowed' : 'pointer', opacity: busy ? 0.55 : 1 }}>{busyVideo ? (step.message || 'A gerar…') : hasVideo ? 'Regerar vídeo' : 'Gerar vídeo 15s 1080p'}</button>
            )}
            {onNext && <button type="button" onClick={onNext} disabled={busy} className="btn btn-sm" style={{ width: '100%' }}>Próximo anúncio →</button>}
          </div>
        </div>
      </div>
      <div style={{ minWidth: 0, overflow: 'hidden' }}>
        <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, overflow: 'hidden' }}>
          <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--border-1, #ECEFF5)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
            <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#94a3b8', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase' }}>Prompt de movimento</span>
            <span style={{ fontSize: 10, fontWeight: 700, padding: '3px 8px', borderRadius: 4, background: '#ECF1FB', color: '#3859D0', fontFamily: 'var(--font-mono, monospace)', whiteSpace: 'nowrap' }}>SEEDANCE 1080P</span>
          </div>
          <div style={{ padding: 14 }}>
            {(busyPrompt || (busyVideo && step.message)) ? (
              <div style={{ padding: '48px 16px', background: '#fff', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 20 }}>
                <div style={{ display: 'flex', alignItems: 'flex-end', gap: 4, height: 48 }}>
                  {[0,1,2,3,4,5,6].map(i => <div key={i} style={{ width: 4, height: 32, background: 'var(--color-primary, #3859D0)', borderRadius: 4, animation: 'ctdWave 1s ease-in-out infinite', animationDelay: (i * 0.1) + 's', transformOrigin: 'bottom' }} />)}
                </div>
                <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.10em', color: 'var(--color-primary, #3859D0)', fontFamily: 'var(--font-mono, monospace)', textAlign: 'center' }}>
                  {step.message || (busyVideo ? 'A GERAR VÍDEO 15S 1080P' : 'A GERAR PROMPT')}
                </div>
                {step.total > 0 && (
                  <div style={{ fontSize: 11, color: '#6366f1', fontFamily: 'var(--font-mono, monospace)', fontWeight: 600 }}>{step.step}/{step.total}</div>
                )}
                <style>{`@keyframes ctdWave { 0%,100%{transform:scaleY(0.25)} 50%{transform:scaleY(1)} }`}</style>
              </div>
            ) : (
              <>
                <textarea value={motion} onChange={e => setMotion(e.target.value)} disabled={busy} rows={16} placeholder="Dolly-in + travelling, pessoas com movimento, tipografia do filme fixa no suporte. Claim e descrição entram depois como motion graphics…"
                  style={{ width: '100%', minHeight: 280, border: '1px solid var(--border-2, #E3E6EB)', borderRadius: 6, padding: '10px 12px', fontSize: 12, lineHeight: 1.55, color: 'var(--text)', background: '#fff', resize: 'vertical', fontFamily: 'var(--font-body)', boxSizing: 'border-box' }} />
                <button type="button" onClick={handleMotion} disabled={busy || !hasImage} className="btn btn-sm" style={{ marginTop: 10, opacity: (busy || !hasImage) ? 0.5 : 1 }}>{busyPrompt ? 'A escrever…' : 'Gerar prompt Opus'}</button>
              </>
            )}
            {error && <div style={{ marginTop: 10, fontSize: 11, color: '#991B1B', background: '#FEE2E2', borderRadius: 6, padding: '8px 10px' }}>{error}</div>}
          </div>
        </div>
      </div>
      <div style={{ position: 'sticky', top: 76, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 12, opacity: (busyPrompt || (busyVideo && step.message)) ? 0.45 : 1, transition: 'opacity 200ms' }}>
        <CtdStoriesVideoMockup videoUrl={prompt?.video_url} stillUrl={prompt?.video_source_url || prompt?.magnific_url} brandName={brandName || campFull?.brand_name} brandSlug={brandSlug || campFull?.brand_slug} copyForm={ctdCopyToPreviewForm(rowCopy)} onOpenFullscreen={() => prompt?.video_url && setFullscreen(true)} />
        <CtdVideoHistoryStrip prompt={prompt} currentUrl={prompt?.video_url} disabled={busy} onRestore={handleRestore} />
      </div>
      {fullscreen && prompt?.video_url && (
        <CtdVideoFullscreen url={prompt.video_url} stillUrl={prompt?.video_source_url} brandName={brandName || campFull?.brand_name} onClose={() => setFullscreen(false)} />
      )}
    </div>
  );
};

const CtdTabVideo = ({ campanha, prompts, copies, campFull, userEmail, reloadPrompts, onBackToKanban, onNavigate, focusPiece, lockCanal }) => {
  const Icon = window.Icon;
  const commPlan = (campFull?.proposta_json?.comm_plan) || [];
  const [filterCanal, setFilterCanal] = React.useState(lockCanal || 'all');
  React.useEffect(() => { if (lockCanal) setFilterCanal(lockCanal); }, [lockCanal]);
  const [filterCountry, setFilterCountry] = React.useState('all');
  const [filterStatus, setFilterStatus] = React.useState('all');
  const [selected, setSelected] = React.useState(new Set());
  const [rowBusy, setRowBusy] = React.useState(new Set());
  const [bulkBusy, setBulkBusy] = React.useState(false);
  const [bulkProg, setBulkProg] = React.useState({ done: 0, total: 0, current: '', step: 0, totalSteps: 0, message: '' });
  const [error, setError] = React.useState(null);
  const [openItem, setOpenItem] = React.useState(null);
  const detectCountry = (peca) => {
    if (peca.country) return String(peca.country).toUpperCase();
    const m = String(peca.titulo || '').match(/^\[([A-Z]{2})\]/);
    return m ? m[1] : 'PT';
  };
  const rows = React.useMemo(() => {
    const out = [];
    commPlan.forEach((peca, idx) => {
      if (!CTD_VIDEO_TYPES.includes(peca.content_type)) return;
      const country = detectCountry(peca);
      const copyForPeca = (copies || []).find(c => Number(c.comm_plan_item_index) === idx);
      const prompt = ctdPromptForPeca(prompts, idx, peca, copyForPeca);
      const hasImage = !!(prompt && (prompt.magnific_url || prompt.image_url));
      const imageApproved = !!(prompt && prompt.image_approved_at);
      const hasVideo = !!(prompt && prompt.video_url);
      const stale = ctdPromptIsStale(prompt, copyForPeca);
      const status = stale ? 'desactualizado' : !hasImage ? 'sem_imagem' : !imageApproved ? 'imagem_por_aprovar' : !hasVideo ? 'pendente' : (prompt.video_approved_at ? 'aprovado' : 'gerado');
      out.push({ key: `${idx}::${country}`, idx, country, peca, prompt, copy: copyForPeca, status, hasImage, imageApproved, hasVideo, stale });
    });
    return out;
  }, [commPlan, prompts, copies]);
  const focusToken = focusPiece ? `${focusPiece.idx}:${focusPiece.country || ''}` : '';
  const openedFocus = React.useRef('');
  React.useEffect(() => {
    if (!focusToken || openedFocus.current === focusToken) return;
    const idx = Number(focusPiece.idx);
    if (!Number.isFinite(idx)) return;
    const row = rows.find(r => r.idx === idx && (!focusPiece.country || r.country === focusPiece.country))
      || rows.find(r => r.idx === idx);
    if (!row) return;
    openedFocus.current = focusToken;
    setOpenItem(row);
  }, [focusToken, rows, focusPiece]);
  const COUNTRY_ORDER_TBL = ['PT', 'ES', 'FR', 'IT', 'DE', 'EN'];
  const canaisPresent = React.useMemo(() => [...new Set(rows.map(r => ctdCanalLabel(r.peca)).filter(Boolean))], [rows]);
  const countriesPresent = React.useMemo(() => [...new Set(rows.map(r => r.country))], [rows]);
  const canalVideo = lockCanal || filterCanal;
  const filtered = React.useMemo(() => {
    const list = rows.filter(r => {
      if (canalVideo !== 'all' && ctdCanalLabel(r.peca) !== canalVideo) return false;
      if (filterCountry !== 'all' && r.country !== filterCountry) return false;
      if (filterStatus !== 'all' && r.status !== filterStatus) return false;
      return true;
    });
    return list.sort((a, b) => {
      const ia = COUNTRY_ORDER_TBL.indexOf(a.country), ib = COUNTRY_ORDER_TBL.indexOf(b.country);
      if (ia !== ib) return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib);
      return a.idx - b.idx;
    });
  }, [rows, canalVideo, filterCountry, filterStatus]);
  const counters = React.useMemo(() => ({
    total: rows.length,
    pendente: rows.filter(r => r.status === 'pendente').length,
    gerado: rows.filter(r => r.status === 'gerado').length,
    aprovado: rows.filter(r => r.status === 'aprovado').length,
    sem_imagem: rows.filter(r => r.status === 'sem_imagem').length,
  }), [rows]);
  const allFilteredSel = filtered.length > 0 && filtered.every(r => selected.has(r.key));
  const toggleAll = () => setSelected(prev => {
    const next = new Set(prev);
    if (allFilteredSel) filtered.forEach(r => next.delete(r.key)); else filtered.forEach(r => next.add(r.key));
    return next;
  });
  const toggleOne = (key) => setSelected(prev => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; });
  const runOne = async (row) => {
    if (!row.prompt?.id || !row.imageApproved) return;
    setRowBusy(prev => { const n = new Set(prev); n.add(row.key); return n; });
    setError(null);
    try {
      await CtdAPI.generateVideo(campanha.id, row.prompt.id, { actor_email: userEmail }, (s, t, m) => {
        setBulkProg(p => ({ ...p, current: row.peca?.titulo || p.current, step: s, totalSteps: t, message: m }));
      });
      reloadPrompts?.();
    }
    catch (e) { setError(`Erro na peça #${row.idx + 1}: ${e.message}`); }
    setRowBusy(prev => { const n = new Set(prev); n.delete(row.key); return n; });
  };
  const runApprove = async (row) => {
    if (!row.prompt?.id) return;
    try { await CtdAPI.approveVideo(campanha.id, row.prompt.id, { approver_email: userEmail }); reloadPrompts?.(); }
    catch (e) { setError(e.message); }
  };
  const runList = async (list) => {
    if (!list.length) return;
    setBulkBusy(true);
    for (let i = 0; i < list.length; i++) {
      setBulkProg({ done: i, total: list.length, current: list[i].peca?.titulo || '', step: 0, totalSteps: 4, message: 'A iniciar…' });
      await runOne(list[i]);
    }
    setBulkBusy(false); setBulkProg({ done: 0, total: 0, current: '', step: 0, totalSteps: 0, message: '' });
  };
  const STATUS_STYLE = {
    sem_imagem: { bg: '#FEF3C7', fg: '#92400E', label: 'Sem imagem' },
    imagem_por_aprovar: { bg: '#FEF3C7', fg: '#92400E', label: 'Imagem por aprovar' },
    desactualizado: { bg: '#FEF3C7', fg: '#92400E', label: 'Copy mudou' },
    pendente: { bg: '#F1F5F9', fg: '#475569', label: 'Pendente' },
    gerado: { bg: '#FEF3C7', fg: '#92400E', label: 'Gerado' },
    aprovado: { bg: '#DCFCE7', fg: '#166534', label: 'Aprovado' },
  };
  const thSt = { textAlign: 'left', padding: '8px 12px', fontSize: 10, fontFamily: 'var(--font-display)', fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--text-muted)', whiteSpace: 'nowrap', borderBottom: '1px solid var(--border)' };
  const tdSt = { padding: '10px 12px', verticalAlign: 'middle', borderBottom: '1px solid var(--border)', fontSize: 13, color: 'var(--text)' };
  if (openItem) {
    const navRows = ctdNavRowsSameCanal(rows, openItem.peca);
    const rowIndex = Math.max(0, navRows.findIndex(r => r.key === openItem.key));
    const prevRow = rowIndex > 0 ? navRows[rowIndex - 1] : null;
    const nextRow = rowIndex < navRows.length - 1 ? navRows[rowIndex + 1] : null;
    const pecaTitle = openItem.peca?.titulo || `Peça #${(openItem.idx ?? 0) + 1}`;
    const canalCat = openItem.peca ? ctdGetCategoria(openItem.peca.content_type) : null;
    const live = rows.find(r => r.key === openItem.key) || openItem;
    return (
      <div style={{ width: '100%' }}>
        <div style={{ position: 'sticky', top: 0, zIndex: 10, background: 'var(--bg-page, #F4F6FA)', padding: '12px 32px 0' }}>
          <CtdBreadcrumb
            trail={[
              { label: campanha?.titulo || 'Campanha', onClick: onBackToKanban },
              { label: 'Video', onClick: () => setOpenItem(null) },
              { label: pecaTitle, badge: canalCat ? { text: ctdCanalLabel(openItem.peca), color: canalCat.color } : null },
            ]}
            onBack={() => setOpenItem(null)}
            currentPos={rowIndex + 1}
            totalItems={navRows.length}
            onPrev={prevRow ? () => setOpenItem(prevRow) : null}
            onNext={nextRow ? () => setOpenItem(nextRow) : null}
          />
        </div>
        <div style={{ padding: '12px 32px 32px' }}>
          <CtdVideoDetail campanhaId={campanha.id} row={live} userEmail={userEmail} campFull={campFull} brandSlug={campanha?.brand_slug} brandName={campanha?.brand_name} onNavigate={onNavigate} onDone={() => reloadPrompts?.()} onNext={nextRow ? () => setOpenItem(nextRow) : null} />
        </div>
      </div>
    );
  }
  const paisesVideo = [...new Set(rows.filter(r => !lockCanal || ctdCanalLabel(r.peca) === lockCanal).map(r => r.country).filter(Boolean))];
  const pendentesVideo = rows.filter(r => r.status === 'pendente' && (!lockCanal || ctdCanalLabel(r.peca) === lockCanal)).length;
  return (
    <div style={{ padding: '12px 32px 32px' }}>
      {(!lockCanal || paisesVideo.length > 1) && (
      <div style={{ display: 'flex', gap: 8, alignItems: 'center', padding: '10px 0', fontSize: 11, flexWrap: 'wrap', marginBottom: 4 }}>
        {!lockCanal && <CtdDropdown label="Canal" value={filterCanal} onChange={setFilterCanal} options={[{ value: 'all', label: 'Todos os canais' }, ...canaisPresent.map(c => ({ value: c, label: c }))]} />}
        {paisesVideo.length > 1 && <CtdDropdown label="Mercado" value={filterCountry} onChange={setFilterCountry} options={[{ value: 'all', label: 'Todos mercados' }, ...paisesVideo.map(c => ({ value: c, label: c }))]} />}
        {(!lockCanal || paisesVideo.length > 1) && <span style={{ color: 'var(--text-dim, #7a8fa8)', fontSize: 11 }}>{filtered.length} de {rows.length}</span>}
        {!lockCanal && (
        <div style={{ marginLeft: 'auto', display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
          <span style={{ fontSize: 10, fontWeight: 700, padding: '4px 10px', borderRadius: 4, background: '#ECF1FB', color: '#3859D0', fontFamily: 'var(--font-mono, monospace)' }}>SEEDANCE 1080P · 15s</span>
          <button onClick={() => runList(filtered.filter(r => selected.has(r.key) && r.imageApproved && r.status === 'pendente'))} disabled={bulkBusy || selected.size === 0} className="btn btn-sm" style={{ fontSize: 11, opacity: (bulkBusy || selected.size === 0) ? 0.5 : 1 }}><Icon name="sparkle" size={11} /> Gerar seleccionados ({selected.size})</button>
          <button onClick={() => runList(rows.filter(r => r.status === 'pendente' && (!lockCanal || ctdCanalLabel(r.peca) === lockCanal)))} disabled={bulkBusy || pendentesVideo === 0} style={{ background: '#3859D0', color: '#fff', border: 0, padding: '6px 12px', borderRadius: 6, fontSize: 11, fontWeight: 700, cursor: bulkBusy ? 'not-allowed' : 'pointer', opacity: (bulkBusy || pendentesVideo === 0) ? 0.5 : 1 }}><Icon name="sparkle" size={11} /> Gerar todos pendentes ({pendentesVideo})</button>
        </div>
        )}
      </div>
      )}
      {!lockCanal && (
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 12, marginBottom: 20 }}>
        {[
          { key: 'all', label: 'PLANO', value: counters.total, accent: '#3859D0', activeBg: '#EFF3FF' },
          { key: 'pendente', label: 'PENDENTES', value: counters.pendente, accent: '#64748B', activeBg: '#F1F5F9' },
          { key: 'gerado', label: 'GERADO', value: counters.gerado, accent: '#92400E', activeBg: '#FEF3C7' },
          { key: 'aprovado', label: 'APROVADOS', value: counters.aprovado, accent: '#166534', activeBg: '#DCFCE7' },
          { key: 'sem_imagem', label: 'SEM IMAGEM', value: counters.sem_imagem, accent: '#B45309', activeBg: '#FEF3C7' },
        ].map(card => {
          const isActive = filterStatus === card.key;
          return (
            <div key={card.key} onClick={() => setFilterStatus(isActive && card.key !== 'all' ? 'all' : card.key)} style={{ background: isActive ? card.activeBg : '#fff', border: '1px solid ' + (isActive ? card.accent + '50' : '#E3E6EB'), borderRadius: 8, padding: '14px 16px', cursor: 'pointer' }}>
              <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: isActive ? card.accent : 'var(--text-muted)', fontFamily: 'var(--font-mono, monospace)', marginBottom: 6 }}>{card.label}</div>
              <div style={{ fontSize: 28, fontWeight: 700, color: isActive ? card.accent : '#112954', lineHeight: 1 }}>{card.value}</div>
            </div>
          );
        })}
      </div>
      )}
      {bulkBusy && (
        <div style={{ marginBottom: 14 }}>
          <CtdGenProgress
            step={bulkProg.step || bulkProg.done}
            total={bulkProg.totalSteps || bulkProg.total}
            message={bulkProg.message || `A gerar ${bulkProg.done + 1}/${bulkProg.total} · ${bulkProg.current}`}
          />
        </div>
      )}
      {error && <div style={{ padding: '10px 14px', marginBottom: 14, background: '#FEE2E2', color: '#991B1B', borderRadius: 6, fontSize: 12 }}>{error}</div>}
      {rows.length === 0 ? (
        <div style={{ padding: 40, textAlign: 'center', color: 'var(--text-muted)', fontSize: 13, fontStyle: 'italic' }}>Sem anúncios Meta neste plano. O vídeo 9:16 15s é para Stories a partir da imagem de cada social post.</div>
      ) : (
        <div style={{ overflowX: 'auto' }}>
          <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12.5 }}>
            <thead>
              <tr>
                <th style={{ ...thSt, width: 36, textAlign: 'center' }}><input type="checkbox" checked={allFilteredSel} onChange={toggleAll} disabled={bulkBusy} style={{ width: 14, height: 14 }} /></th>
                <th style={{ ...thSt, width: 40 }}>#</th>
                <th style={thSt}>Peça</th>
                <th style={thSt}>Canal</th>
                <th style={thSt}>País</th>
                <th style={thSt}>Estado</th>
                <th style={{ ...thSt, textAlign: 'right' }}>Acções</th>
              </tr>
            </thead>
            <tbody>
              {filtered.map((r, rowIndex) => {
                const isBusy = rowBusy.has(r.key) || bulkBusy;
                const st = STATUS_STYLE[r.status] || STATUS_STYLE.pendente;
                return (
                  <CtdPlanRow key={r.key} busy={isBusy} isSelected={selected.has(r.key)} rowIndex={rowIndex}
                    onClick={(e) => { if (isBusy) return; const tag = e.target.tagName; if (tag === 'INPUT' || tag === 'BUTTON' || e.target.closest('button')) return; setOpenItem(r); }}>
                    <td style={{ ...tdSt, textAlign: 'center' }} onClick={e => e.stopPropagation()}><input type="checkbox" checked={selected.has(r.key)} onChange={() => toggleOne(r.key)} disabled={bulkBusy} style={{ width: 14, height: 14 }} /></td>
                    <td style={{ ...tdSt, fontFamily: 'var(--font-mono)', fontSize: 11 }}>{r.library ? r.groupPos : r.idx + 1}</td>
                    <td style={tdSt}><div style={{ fontWeight: 500, fontFamily: 'var(--font-mono)', fontSize: 12 }}>{r.peca.titulo || `Peça #${r.library ? r.groupPos : r.idx + 1}`}</div>{r.hasVideo && <div style={{ fontSize: 10, color: '#64748b' }}>15s · 1080p · 9:16</div>}</td>
                    <td style={tdSt}><span style={{ fontSize: 9, fontWeight: 700, padding: '3px 7px', borderRadius: 4, background: '#ECF1FB', color: '#3859D0', fontFamily: 'var(--font-mono, monospace)' }}>{ctdCanalLabel(r.peca).toUpperCase()}</span></td>
                    <td style={{ ...tdSt, fontFamily: 'var(--font-mono)', fontWeight: 600 }}>{r.country}</td>
                    <td style={tdSt}><span style={{ fontSize: 11, fontFamily: 'var(--font-mono)', fontWeight: 600, padding: '2px 8px', borderRadius: 4, color: st.fg, background: st.bg }}>{st.label.toUpperCase()}</span></td>
                    <td style={{ ...tdSt, textAlign: 'right', whiteSpace: 'nowrap' }} onClick={e => e.stopPropagation()}>
                      {(r.status === 'pendente' || r.status === 'gerado' || r.status === 'aprovado') ? (
                        <button onClick={() => runOne(r)} disabled={isBusy} style={{ fontSize: 10, fontWeight: 700, padding: '4px 9px', background: r.status === 'pendente' ? '#3859D0' : 'transparent', color: r.status === 'pendente' ? '#fff' : '#3859D0', border: '1px solid #3859D0', borderRadius: 4, fontFamily: 'var(--font-mono)', textTransform: 'uppercase' }}>{isBusy ? 'A gerar…' : r.status === 'pendente' ? 'Gerar' : 'Regerar'}</button>
                      ) : (
                        <button onClick={() => onNavigate?.({ tab: r.status === 'desactualizado' ? 'prompts' : 'imagens', idx: r.idx, country: r.country, titulo: r.peca?.titulo, contentType: r.peca?.content_type })} style={{ fontSize: 10, fontWeight: 700, padding: '4px 9px', background: 'transparent', color: '#92400E', border: '1px solid #FDE68A', borderRadius: 4, fontFamily: 'var(--font-mono)', textTransform: 'uppercase' }}>{r.status === 'desactualizado' ? 'Prompt' : 'Imagem'}</button>
                      )}
                      {r.status === 'gerado' && <button onClick={() => runApprove(r)} disabled={isBusy} style={{ fontSize: 10, fontWeight: 700, padding: '4px 9px', background: '#1F8A52', color: '#fff', border: 0, borderRadius: 4, marginLeft: 4, fontFamily: 'var(--font-mono)', textTransform: 'uppercase' }}>Aprovar</button>}
                    </td>
                  </CtdPlanRow>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
};

// ─── CtdTabPecaFinal — imagens + copies aprovadas lado-a-lado + envio para Activação ─
const CtdTabPecaFinal = ({ campanha, prompts, imagens, copies, campFull, userEmail, onNavigate, onBackToKanban, focusPiece, reloadCopies, lockCanal, podeActivar }) => {
  const Icon = window.Icon;
  const [busy, setBusy]           = React.useState(false);
  const [enviado, setEnviado]     = React.useState(false);
  const [error, setError]         = React.useState(null);
  const [currentLingua, setLingua] = React.useState('pt');
  const [selectedPecaIdx, setSelectedPecaIdx] = React.useState(null);

  const commPlan = (campFull?.proposta_json?.comm_plan) || [];
  const list     = Array.isArray(copies) ? copies : [];

  // Distinct languages
  const langs = React.useMemo(() => {
    const s = new Set(); list.forEach(c => s.add(c.lingua || 'pt'));
    if (!s.size) s.add('pt');
    return Array.from(s).sort((a, b) => a === 'pt' ? -1 : b === 'pt' ? 1 : a.localeCompare(b));
  }, [list]);
  React.useEffect(() => { if (!langs.includes(currentLingua)) setLingua(langs[0] || 'pt'); }, [langs, currentLingua]);

  // Build per-peça view: copy (lingua actual) + prompt + imagem
  const pecas = React.useMemo(() => {
    return commPlan.map((peca, idx) => {
      if (ctdPecaArchived(peca) || ctdHideWaFromPlan(peca)) return null;
      if (lockCanal && ctdCanalLabel(peca) !== lockCanal) return null;
      const copy = list.find(c => c.comm_plan_item_index === idx && (c.lingua || 'pt') === currentLingua && (c.copy_type || 'organico') === 'organico');
      const canal = peca?.canal || copy?.canal;
      const ct = peca?.content_type || copy?.content_type;
      const plist = prompts || [];
      const prompt = ctdPromptForPeca(plist, idx, peca, copy)
        || plist.find(p => p.canal === canal && p.content_type === ct && p.image_approved_at)
        || null;
      const imagem = (imagens || []).find(i => (i.id && prompt && i.id === prompt.id) || (i.url && prompt?.magnific_url === i.url));
      const isWa = ctdIsWaPiece(peca, copy);
      const isBrevo = ctdEmailBrevo(peca);
      const waPack = isWa ? ctdWaPackFromCopy(copy) : null;
      const waStale = isWa && ctdWaTemplateStale(waPack);
      const templateApproved = isWa && !!waPack?.approvedAt && !waStale;
      const stale = isWa ? waStale : ctdPromptIsStale(prompt, copy);
      const copyApproved   = copy?.status === 'aprovado';
      const promptApproved = isWa ? templateApproved : (!stale && prompt?.status === 'approved');
      const imageApproved  = isWa ? false : (!stale && !!(imagem?.image_approved_at || prompt?.image_approved_at));
      const videoApproved  = !stale && !!prompt?.video_approved_at;
      const ready          = isBrevo ? !!copyApproved : (isWa ? (copyApproved && templateApproved) : (copyApproved && imageApproved && !stale));
      return { idx, peca, copy, prompt, imagem, copyApproved, promptApproved, imageApproved, videoApproved, ready, stale, isWa, isBrevo, waPack, templateApproved };
    }).filter(Boolean);
  }, [commPlan, list, prompts, imagens, currentLingua, lockCanal]);

  const waOpenRow = React.useMemo(() => {
    if (!focusPiece?.titulo && !focusPiece?.waGroup) return null;
    const entries = ctdWaLibraryEntries(list);
    const want = ctdNormTitle(focusPiece.titulo);
    const e = entries.find(x => ctdNormTitle(x.title) === want && (!focusPiece.waGroup || x.grupo === focusPiece.waGroup))
      || entries.find(x => want && ctdNormTitle(x.title) === want);
    if (!e) return null;
    e.copyStale = ctdWaUspStale(e, list);
    const stale = ctdWaTemplateStale(e.pack);
    const status = e.copy.status !== 'aprovado' ? 'pendente' : stale ? 'desactualizado' : e.pack.approvedAt ? 'aprovado' : 'gerado';
    return ctdWaLibraryRow(e, status);
  }, [focusPiece, list]);

  const focusToken = focusPiece ? String(focusPiece.idx) : '';
  const openedFocus = React.useRef('');
  React.useEffect(() => {
    if (!focusToken || openedFocus.current === focusToken) return;
    const idx = Number(focusPiece.idx);
    if (!Number.isFinite(idx) || !pecas.some(p => p.idx === idx)) return;
    openedFocus.current = focusToken;
    setSelectedPecaIdx(idx);
  }, [focusToken, pecas, focusPiece]);
  React.useEffect(() => {
    if (selectedPecaIdx != null) return;
    if (!pecas.length || !pecas.every(p => p.isBrevo)) return;
    setSelectedPecaIdx(pecas[0].idx);
  }, [pecas, selectedPecaIdx]);

  const totalPecas    = pecas.length;
  const readyCount    = pecas.filter(p => p.ready).length;
  const canalGroups   = React.useMemo(() => {
    const map = new Map();
    pecas.forEach(p => {
      const label = ctdCanalLabel(p.peca) || '—';
      if (!map.has(label)) map.set(label, []);
      map.get(label).push(p);
    });
    return map;
  }, [pecas]);
  const completeCanals = React.useMemo(
    () => [...canalGroups.entries()].filter(([, items]) => items.length > 0 && items.every(p => p.ready)).map(([label]) => label),
    [canalGroups]
  );
  const completeReadyCount = completeCanals.reduce((n, label) => n + (canalGroups.get(label) || []).length, 0);
  const canSend = false;

  const handleEnviarActivacao = async ({ canal, indexes, label, aprovarAntes } = {}) => {
    if (!campanha?.id) return;
    const canalItems = canal ? (canalGroups.get(canal) || []) : null;
    if (canal && (!canalItems.length || canalItems.some(p => !p.ready))) {
      setError(canalItems.some(p => p.isWa) ? 'Canal incompleto: aprova a copy e o template de todas as peças.' : 'Canal incompleto: aprova a copy e a imagem de todas as peças.');
      return;
    }
    setBusy(true); setError(null);
    try {
      const postJson = async (url, body) => {
        const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
        const j = await res.json().catch(() => ({}));
        if (!res.ok) throw new Error(j.error || ('HTTP ' + res.status));
        return j;
      };
      if (aprovarAntes && !podeActivar) {
        try {
          await postJson(`/api/marketing/producao/campanha/${campanha.id}/validacao`, {
            action: 'aprovar', actor_email: userEmail, actor_name: userEmail?.split('@')[0],
          });
        } catch (e) {
          if (!/não está em validação/i.test(e.message || '')) throw e;
        }
      }
      await postJson(`/api/marketing/producao/campanha/${campanha.id}/enviar-producao`, {
        approver_email: userEmail, approver_name: userEmail?.split('@')[0],
        canal: canal ? CTD_CANAL_API[canal] : undefined,
        indexes: canal ? canalItems.map(p => p.idx) : undefined,
      });
      setEnviado(true);
      reloadCopies?.();
    } catch (e) { setError(e.message); }
    setBusy(false);
  };

  const handleSubmitWa = async (sel) => {
    if (!campanha?.id || !sel?.copy?.id) return;
    setBusy(true); setError(null);
    try {
      await CtdAPI.submitWaTemplate(campanha.id, sel.copy.id, { actor_email: userEmail });
      reloadCopies?.();
    } catch (e) { setError(e.message || 'Erro ao submeter o template'); }
    setBusy(false);
  };

  const LANG_LABEL = { pt: 'PT-PT', es: 'ES-ES', en: 'EN', de: 'DE', fr: 'FR', it: 'IT' };
  const canalLabelOf = (c, p) => ctdCanalLabel({ content_type: c?.content_type || p?.content_type, canal: c?.canal || p?.canal }, c?.canal || p?.canal);

  if (waOpenRow) {
    const waRows = ctdWaLibraryEntries(list).map(e => ctdWaLibraryRow(e, e.copy?.status === 'aprovado' ? 'aprovado' : 'gerado'));
    const navRows = ctdNavRowsSameCanal(waRows, waOpenRow.peca);
    const rowIndex = Math.max(0, navRows.findIndex(r => r.key === waOpenRow.key));
    const prevRow = rowIndex > 0 ? navRows[rowIndex - 1] : null;
    const nextRow = rowIndex < navRows.length - 1 ? navRows[rowIndex + 1] : null;
    const openSibling = (r) => onNavigate?.({ tab: 'peca', idx: r.idx, country: r.country, titulo: r.peca?.titulo, contentType: r.peca?.content_type, waGroup: r.peca?.waGroup || null });
    const canalCat = ctdGetCategoria(waOpenRow.peca?.content_type);
    return (
      <div style={{ width: '100%' }}>
        <div style={{ position: 'sticky', top: 0, zIndex: 10, background: 'var(--bg-page, #F4F6FA)', padding: '12px 32px 0' }}>
          <CtdBreadcrumb
            trail={[
              { label: campanha?.titulo || 'Campanha', onClick: onBackToKanban },
              { label: 'Peça Final', onClick: () => onNavigate?.('peca') },
              { label: waOpenRow.peca?.titulo || 'Peça', badge: canalCat ? { text: ctdCanalLabel(waOpenRow.peca), color: canalCat.color } : null },
            ]}
            onBack={() => onNavigate?.('peca')}
            currentPos={rowIndex + 1}
            totalItems={navRows.length}
            onPrev={prevRow ? () => openSibling(prevRow) : null}
            onNext={nextRow ? () => openSibling(nextRow) : null}
          />
        </div>
        <div style={{ padding: '12px 32px 32px' }}>
          <CtdWaTemplateScreen
            campanhaId={campanha.id}
            row={waOpenRow}
            userEmail={userEmail}
          lockCanal={lockCanal}
            onNavigate={onNavigate}
            reloadCopies={reloadCopies}
            brandName={campanha?.brand_name}
            place="peca"
          />
        </div>
      </div>
    );
  }

  if (totalPecas === 0) {
    return (
      <div style={{ padding: '48px 32px', width: '100%', textAlign: 'center' }}>
        <div className="font-display" style={{ fontSize: 18, color: 'var(--text, #112954)', marginBottom: 8, fontWeight: 600 }}>Sem peças no comm_plan</div>
        <div style={{ fontSize: 13, color: 'var(--text-muted, #4a5e7a)' }}>Confirma o Planeamento em Campanhas.</div>
      </div>
    );
  }

  const allBrevo = pecas.length > 0 && pecas.every(p => p.isBrevo);
  const detailIdx = selectedPecaIdx != null ? selectedPecaIdx : (allBrevo ? pecas[0].idx : null);

  // Detail view inline — preview completo por canal
  if (detailIdx !== null) {
    const sel = pecas.find(p => p.idx === detailIdx);
    if (!sel) { setSelectedPecaIdx(null); return null; }
    const url = sel.imagem?.url || sel.prompt?.magnific_url;
    const canalCat = ctdGetCategoria(sel.peca?.content_type);
    const canalLabel = ctdCanalLabel(sel.peca) || canalLabelOf(sel.copy, sel.prompt) || sel.peca.canal || '—';
    const ct = sel.peca?.content_type;
    // Peça index dentro do array filtrado (para prev/next)
    const navRows = ctdNavRowsSameCanal(pecas, sel.peca);
    const currIdx = Math.max(0, navRows.findIndex(p => p.idx === detailIdx));
    const prevPeca = currIdx > 0 ? navRows[currIdx - 1] : null;
    const nextPeca = currIdx < navRows.length - 1 ? navRows[currIdx + 1] : null;
    if (sel.isBrevo) {
      const payload = (sel.copy?.payload && typeof sel.copy.payload === 'object') ? sel.copy.payload : {};
      const html = payload.brevo_html || sel.peca?.brevo_html || '';
      const assunto = payload.brevo_subject || payload.subject || sel.copy?.headline || sel.peca?.titulo || 'Email';
      const canalItems = canalGroups.get(canalLabel) || [];
      const prontas = canalItems.length > 0 && canalItems.every(p => p.ready);
      const podeAprovarCampanha = !!podeActivar || ctdPodeValidar(userEmail);
      const pode = podeAprovarCampanha && prontas && !!html && !enviado;
      const motivo = !html
        ? 'Este email ainda não tem HTML.'
        : !prontas
          ? 'Aprova a copy dos emails antes de enviar para activação.'
          : !podeAprovarCampanha
            ? 'A campanha segue para activação depois de aprovada.'
            : '';
      const pecaVista = {
        ...(sel.peca || {}),
        brevo_html: html || sel.peca?.brevo_html || '',
        brevo_subject: assunto,
        brevo_preheader: payload.brevo_preheader || sel.peca?.brevo_preheader || '',
        brevo_template_id: payload.brevo_template_id || sel.peca?.brevo_template_id,
      };
      return (
        <div style={{ width: '100%' }}>
          <div style={{ position: 'sticky', top: 0, zIndex: 10, background: 'var(--bg-page, #F4F6FA)', padding: '12px 32px 0' }}>
            <CtdBreadcrumb
              trail={[
                { label: campanha?.titulo || 'Campanha', onClick: onBackToKanban },
                { label: 'Peça final', onClick: () => onNavigate?.('copies') },
                { label: sel.peca.titulo || `Email ${currIdx + 1}`, badge: { text: canalLabel, color: canalCat.color } },
              ]}
              onBack={() => onNavigate?.('copies')}
              currentPos={currIdx + 1}
              totalItems={navRows.length}
              onPrev={prevPeca ? () => setSelectedPecaIdx(prevPeca.idx) : null}
              onNext={nextPeca ? () => setSelectedPecaIdx(nextPeca.idx) : null}
            />
          </div>
          <div style={{ padding: '12px 32px 32px' }}>
            {error && <div style={{ marginBottom: 12, fontSize: 12, color: '#991B1B' }}>{error}</div>}
            <CtdOneShotBrevoEditor
              campanhaId={campanha?.id}
              commPlanIndex={sel.idx}
              peca={pecaVista}
              copy={sel.copy}
              userEmail={userEmail}
              locked
              finalAction={(
                <>
                  {motivo && !enviado && <div style={{ fontSize: 12, color: '#64748B', lineHeight: 1.4 }}>{motivo}</div>}
                  {enviado ? (
                    <span style={{ fontSize: 10, fontWeight: 700, padding: '8px 10px', borderRadius: 8, background: '#DCFCE7', color: '#166534', fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.06em', display: 'inline-flex', alignItems: 'center', gap: 4, alignSelf: 'flex-start' }}>
                      <Icon name="check" size={11} /> ENVIADO PARA ACTIVAÇÃO
                    </span>
                  ) : (
                    <button type="button" onClick={() => handleEnviarActivacao({ canal: canalLabel, aprovarAntes: !podeActivar })} disabled={!pode || busy}
                      style={{ background: '#1F8A52', color: '#fff', border: 0, borderRadius: 8, padding: '9px 12px', fontSize: 13, fontWeight: 700, cursor: (!pode || busy) ? 'not-allowed' : 'pointer', opacity: (!pode || busy) ? 0.45 : 1 }}>
                      {busy ? 'A enviar…' : 'Enviar para activação'}
                    </button>
                  )}
                </>
              )}
            />
          </div>
        </div>
      );
    }
    if (sel.isWa) {
      const pack = sel.waPack || ctdWaPackFromCopy(sel.copy);
      const canalItems = canalGroups.get(canalLabel) || [];
      const readyN = canalItems.filter(p => p.ready).length;
      const totalN = canalItems.length;
      const allReady = totalN > 0 && readyN === totalN;
      const metaLabel = pack.metaStatus === 'submitted' ? 'Em revisão na Meta' : pack.metaStatus === 'approved' ? 'Aprovado na Meta' : pack.metaStatus === 'rejected' ? 'Rejeitado na Meta' : 'Por submeter';
      return (
        <div style={{ padding: '20px 32px 32px', width: '100%' }}>
          <CtdBreadcrumb
            trail={[
              { label: campanha?.titulo || 'Campanha', onClick: onBackToKanban },
              { label: 'Peça Final', onClick: () => setSelectedPecaIdx(null) },
              { label: sel.peca.titulo || `Peça #${sel.idx + 1}`, badge: { text: canalLabel, color: canalCat.color } },
            ]}
            onBack={() => setSelectedPecaIdx(null)}
            currentPos={currIdx + 1}
            totalItems={navRows.length}
            onPrev={prevPeca ? () => setSelectedPecaIdx(prevPeca.idx) : null}
            onNext={nextPeca ? () => setSelectedPecaIdx(nextPeca.idx) : null}
          />
          <div style={{ display: 'grid', gridTemplateColumns: 'minmax(292px, 320px) minmax(0, 1fr) 300px', gap: 20, alignItems: 'start' }}>
            <div style={{ background: '#fff', border: '1px solid #BBF7D0', borderRadius: 8, overflow: 'hidden' }}>
              <div style={{ padding: '10px 14px', background: '#F0FDF4', borderBottom: '1px solid #DCFCE7' }}>
                <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#166534', fontFamily: 'var(--font-mono, monospace)' }}>PEÇA FINAL</span>
              </div>
              <div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 10 }}>
                <CtdPipelineStep n="1" label="Copy" detail={sel.copyApproved ? 'Aprovada' : 'Por aprovar'} state={sel.copyApproved ? 'done' : 'wait'} onClick={() => onNavigate?.({ tab: 'copies', idx: sel.idx, titulo: sel.peca?.titulo, contentType: sel.peca?.content_type })} />
                <CtdPipelineStep n="2" label="Meta" detail={metaLabel} state={pack.metaStatus === 'approved' ? 'done' : (pack.metaStatus === 'submitted' || pack.metaStatus === 'pending') ? 'done' : 'wait'} onClick={() => onNavigate?.({ tab: 'prompts', idx: sel.idx, country: sel.country, titulo: sel.peca?.titulo, contentType: sel.peca?.content_type, waGroup: sel.peca?.waGroup || null })} />
                <CtdPipelineStep n="3" label="Peça final" detail={sel.ready ? 'Pronta' : 'Depois da Meta'} state={sel.ready ? 'current' : 'wait'} />
                <button type="button" onClick={() => onNavigate?.({ tab: 'prompts', idx: sel.idx, country: sel.country, titulo: sel.peca?.titulo, contentType: sel.peca?.content_type, waGroup: sel.peca?.waGroup || null })} disabled={pack.metaStatus === 'submitted' || pack.metaStatus === 'pending' || pack.metaStatus === 'approved'}
                  style={{ background: '#3859D0', color: '#fff', border: 0, width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12.5, fontWeight: 700, cursor: (pack.metaStatus === 'submitted' || pack.metaStatus === 'pending' || pack.metaStatus === 'approved') ? 'not-allowed' : 'pointer', opacity: (pack.metaStatus === 'submitted' || pack.metaStatus === 'pending' || pack.metaStatus === 'approved') ? 0.55 : 1 }}>
                  {pack.metaStatus === 'approved' ? 'Aprovado na Meta' : pack.metaStatus === 'submitted' || pack.metaStatus === 'pending' ? 'Em revisão na Meta' : pack.metaStatus === 'rejected' ? 'Enviar de novo para aprovação' : 'Enviar para aprovação'}
                </button>
                <div style={{ fontSize: 11, color: '#64748B', lineHeight: 1.4 }}>WhatsApp · {readyN}/{totalN} prontas. A campanha segue para activação depois de aprovada.</div>
                {error && <div style={{ fontSize: 12, color: '#991B1B' }}>{error}</div>}
              </div>
            </div>
            <div style={{ background: '#fff', border: '1px solid var(--border-1, #ECEFF5)', borderRadius: 8, padding: 16 }}>
              <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.10em', color: '#128C7E', fontFamily: 'var(--font-mono, monospace)', marginBottom: 10 }}>TEMPLATE APROVADO</div>
              <div style={{ fontSize: 12, color: '#112954', display: 'flex', flexDirection: 'column', gap: 8, lineHeight: 1.45 }}>
                <div><b>Nome</b><div style={{ fontFamily: 'monospace' }}>{pack.name || '—'}</div></div>
                <div><b>Categoria</b><div>{pack.categoria}</div></div>
                <div><b>Corpo</b><div style={{ whiteSpace: 'pre-wrap' }}>{pack.body || '—'}</div></div>
                {pack.footer && <div><b>Rodapé</b><div>{pack.footer}</div></div>}
              </div>
            </div>
            <div>
              <CtdWaPhone pack={pack} brandName={campanha?.brand_name} />
            </div>
          </div>
        </div>
      );
    }
    return (
      <div style={{ padding: '20px 32px 32px', width: '100%' }}>
        <CtdBreadcrumb
          trail={[
            { label: campanha?.titulo || 'Campanha', onClick: onBackToKanban },
            { label: 'Peça Final', onClick: () => setSelectedPecaIdx(null) },
            { label: sel.peca.titulo || `Peça #${sel.idx + 1}`, badge: { text: canalLabel, color: canalCat.color } },
          ]}
          onBack={() => setSelectedPecaIdx(null)}
          currentPos={currIdx + 1}
          totalItems={navRows.length}
          onPrev={prevPeca ? () => setSelectedPecaIdx(prevPeca.idx) : null}
          onNext={nextPeca ? () => setSelectedPecaIdx(nextPeca.idx) : null}
        />
        <div style={{
          display: 'grid', gridTemplateColumns: '1fr 380px', gap: 20,
          background: 'var(--bg-surface, #fff)', borderRadius: 'var(--radius-lg, 10px)',
          border: '1px solid var(--border-1, #ECEFF5)',
          padding: 24, alignItems: 'start',
        }}>
          {/* LEFT: mockup contextual por canal */}
          <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'flex-start' }}>
            {ct === 'social_post' ? (
              <CtdMetaAdFullMockup url={url} copy={sel.copy} peca={sel.peca} brandSlug={campanha?.brand_slug} />
            ) : (
              /* Fallback simples para outros canais (mockups específicos em Fase 2B+) */
              <div style={{ maxWidth: 520 }}>
                {url && <img src={url} alt="" style={{ width: '100%', aspectRatio: ctdAspectForPeca(sel.peca), objectFit: 'cover', borderRadius: 8, display: 'block' }} />}
                <div style={{ marginTop: 12 }}>
                  {sel.copy?.copy_imagem && <div style={{ fontSize: 15, fontWeight: 700, color: 'var(--text, #112954)', marginBottom: 6 }}>{sel.copy.copy_imagem}</div>}
                  {sel.copy?.copy_imagem_sub && <div style={{ fontSize: 13, color: 'var(--text-muted, #4a5e7a)', marginBottom: 6 }}>{sel.copy.copy_imagem_sub}</div>}
                  {sel.copy?.body && <div style={{ fontSize: 12, color: 'var(--text, #112954)', lineHeight: 1.5, marginTop: 8 }}>{sel.copy.body}</div>}
                  {sel.copy?.copy_imagem_cta && <div style={{ marginTop: 10, display: 'inline-block', padding: '8px 16px', background: '#3859D0', color: '#fff', borderRadius: 4, fontSize: 12, fontWeight: 700 }}>{sel.copy.copy_imagem_cta}</div>}
                </div>
              </div>
            )}
          </div>
          {/* RIGHT: metadata + acções */}
          <div>
            <div style={{ fontSize: 10, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-dim, #7a8fa8)', fontFamily: 'var(--font-mono, monospace)', marginBottom: 8 }}>ESTADO</div>
            <div style={{ marginBottom: 16, display: 'flex', gap: 6, flexWrap: 'wrap' }}>
              <StatusChip ok={sel.copyApproved} label="Copy" onClick={() => onNavigate?.('copies')} />
              <StatusChip ok={sel.promptApproved} label="Prompt" onClick={() => onNavigate?.('prompts')} />
              <StatusChip ok={sel.imageApproved} label="Imagem" onClick={() => onNavigate?.('imagens')} />
              {sel.ready && (
                <span style={{ fontSize: 9, fontWeight: 700, padding: '3px 8px', borderRadius: 4, background: '#DCFCE7', color: '#166534', fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.06em' }}>PRONTA</span>
              )}
            </div>
            <div style={{ fontSize: 10, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-dim, #7a8fa8)', fontFamily: 'var(--font-mono, monospace)', marginBottom: 8 }}>METADATA</div>
            <div style={{ fontSize: 11, color: 'var(--text-muted, #4a5e7a)', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, marginBottom: 16, fontFamily: 'var(--font-mono, monospace)' }}>
              <div>Canal:</div><div style={{ color: 'var(--text, #112954)' }}>{canalLabel}</div>
              <div>Formato:</div><div style={{ color: 'var(--text, #112954)' }}>{ctdAspectForPeca(sel.peca).replace('/', ':')}</div>
              <div>Content type:</div><div style={{ color: 'var(--text, #112954)' }}>{ct || '—'}</div>
              <div>Idioma:</div><div style={{ color: 'var(--text, #112954)' }}>{LANG_LABEL[currentLingua] || currentLingua}</div>
              {sel.peca.planned_date && <><div>Data:</div><div style={{ color: 'var(--text, #112954)' }}>{ctdFmtDate(sel.peca.planned_date)}</div></>}
            </div>
            {(() => {
              const canalItems = canalGroups.get(canalLabel) || [];
              const readyN = canalItems.filter(p => p.ready).length;
              const totalN = canalItems.length;
              const allReady = totalN > 0 && readyN === totalN;
              return (
                <div style={{ marginBottom: 16 }}>
                  <div style={{ fontSize: 11, color: '#64748B', marginBottom: 8, lineHeight: 1.4 }}>
                    {canalLabel} · {readyN}/{totalN} prontas. A campanha segue para activação depois de aprovada.
                  </div>
                  {error && <div style={{ marginTop: 8, fontSize: 12, color: '#991B1B' }}>{error}</div>}
                </div>
              );
            })()}
            {sel.copy && (
              <>
                <div style={{ fontSize: 10, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-dim, #7a8fa8)', fontFamily: 'var(--font-mono, monospace)', marginBottom: 8 }}>COPIES</div>
                <div style={{ fontSize: 11, color: 'var(--text, #112954)', marginBottom: 16, lineHeight: 1.5 }}>
                  {sel.copy.copy_imagem && <div style={{ marginBottom: 6 }}><b>Headline:</b> {sel.copy.copy_imagem}</div>}
                  {sel.copy.copy_imagem_sub && <div style={{ marginBottom: 6 }}><b>Sub:</b> {sel.copy.copy_imagem_sub}</div>}
                  {sel.copy.copy_imagem_cta && <div style={{ marginBottom: 6 }}><b>CTA:</b> {sel.copy.copy_imagem_cta}</div>}
                  {(() => {
                    const pt = sel.copy.payload?.primary_text_full || sel.copy.caption_organica || sel.copy.body || '';
                    if (!pt) return null;
                    return (
                      <div style={{ marginTop: 10, paddingTop: 10, borderTop: '1px solid var(--border-1, #ECEFF5)' }}>
                        <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-dim, #7a8fa8)', fontFamily: 'var(--font-mono)', marginBottom: 6 }}>Primary text · 5 blocos</div>
                        <div style={{ whiteSpace: 'pre-wrap', lineHeight: 1.65, fontSize: 11.5, color: 'var(--text, #112954)' }}>{pt}</div>
                      </div>
                    );
                  })()}
                </div>
              </>
            )}
          </div>
        </div>
      </div>
    );
  }

  return (
    <div style={{ padding: '20px 32px 32px', width: '100%' }}>
      {/* Language tabs */}
      {langs.length > 1 && (
        <div style={{ display: 'flex', gap: 4, marginBottom: 14, borderBottom: '1px solid var(--border-1, #ECEFF5)' }}>
          {langs.map(lng => {
            const active = lng === currentLingua;
            return (
              <button key={lng} onClick={() => setLingua(lng)}
                style={{
                  background: 'none', border: 0, outline: 'none',
                  borderBottom: `2px solid ${active ? 'var(--color-primary, #3859D0)' : 'transparent'}`,
                  color: active ? 'var(--text, #112954)' : 'var(--text-dim, #7a8fa8)',
                  fontSize: 12, fontWeight: active ? 700 : 500,
                  padding: '8px 14px', cursor: 'pointer',
                  fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.06em',
                }}>{LANG_LABEL[lng] || lng.toUpperCase()}</button>
            );
          })}
        </div>
      )}

      {/* Toolbar → KpiCards (mesma ordem que Copies) */}
      {(() => {
        const copyOk   = pecas.filter(p => p.copyApproved).length;
        const promptOk = pecas.filter(p => p.promptApproved).length;
        const _pecaCanais    = [...new Set(pecas.map(p => canalLabelOf(p.copy, p.prompt)).filter(Boolean))];
        const _pecaCountries = [...new Set(langs)];
        return (
          <>
            <CtdToolbar
              canais={_pecaCanais} countries={_pecaCountries.length > 1 ? _pecaCountries : []}
              total={totalPecas} filtered={totalPecas}
              hideCanal={!!lockCanal}
              filterCanal={lockCanal || 'all'} onFilterCanal={() => {}}
              filterCountry={currentLingua} onFilterCountry={setLingua}
              rightExtra={
                enviado ? (
                  <span style={{ fontSize: 10, fontWeight: 700, padding: '4px 10px', borderRadius: 4, background: '#DCFCE7', color: '#166534', fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.06em', display: 'inline-flex', alignItems: 'center', gap: 4 }}>
                    <Icon name="check" size={11} /> ENVIADO PARA ACTIVAÇÃO
                  </span>
                ) : canSend ? (
                  <button onClick={handleEnviarActivacao} disabled={busy}
                    style={{ background: '#1F8A52', color: '#fff', border: 0, padding: '6px 12px', borderRadius: 'var(--radius-sm, 6px)', fontSize: 11, fontWeight: 700, cursor: busy?'not-allowed':'pointer', display: 'inline-flex', alignItems: 'center', gap: 5, opacity: busy ? 0.5 : 1, boxShadow: '0 1px 2px rgba(31,138,82,0.24)' }}>
                    <Icon name="send" size={12} /> Enviar para activação ({completeReadyCount}/{totalPecas})
                  </button>
                ) : null
              }
            />

            <CtdKpiCards
              filterStatus="all"
              onFilter={null}
              cards={[
                { key: 'all',      label: 'PLANO',    value: totalPecas,  accent: '#3859D0', activeBg: '#EFF3FF' },
                { key: 'copy',     label: 'COPY ✓',   value: copyOk,      accent: '#0891B2', activeBg: '#E0F7FB' },
                { key: 'prompt',   label: 'PROMPT ✓', value: promptOk,    accent: '#7C3AED', activeBg: '#F3EEF8' },
                { key: 'aprovado', label: 'PRONTAS',  value: readyCount,  accent: '#166534', activeBg: '#DCFCE7' },
              ]}
            />
          </>
        );
      })()}

      {(!lockCanal || lockCanal === 'WhatsApp') && <CtdWaLibraryPanel campanhaId={campanha?.id} copies={list} prompts={prompts} userEmail={userEmail} brandName={campanha?.brand_name} brandSlug={campanha?.brand_slug || campFull?.brand_slug || campFull?.briefing?.brand_slug} reloadCopies={reloadCopies} stage="peca" channelOn={ctdCampaignHasWhatsapp(campFull)} />}
      {error && (
        <div style={{ padding: '10px 14px', marginBottom: 14, background: '#FEE2E2', color: '#991B1B', borderRadius: 'var(--radius-sm, 6px)', fontSize: 12 }}>{error}</div>
      )}

      {/* Grid AGRUPADO POR CANAL — cada canal com header + cards com aspect ratio correcto.
          Deriva canal da PEÇA (sempre disponível) — garante que peças pendentes (sem copy/prompt)
          aparecem no grupo correcto e não em '—'. */}
      {(() => {
        // Group pecas by canal label — derivado da peça, não da copy/prompt
        const byCanal = new Map();
        pecas.forEach(item => {
          const label = ctdCanalLabel(item.peca) || '—';
          if (!byCanal.has(label)) byCanal.set(label, []);
          byCanal.get(label).push(item);
        });
        // Sort canais by canonical order
        const canaisOrdered = [...byCanal.keys()].sort((a, b) => {
          const ia = CTD_CANAL_ORDER.indexOf(a);
          const ib = CTD_CANAL_ORDER.indexOf(b);
          const pa = ia === -1 ? 99 : ia;
          const pb = ib === -1 ? 99 : ib;
          return pa - pb;
        });
        return canaisOrdered.map(canal => {
          const items = byCanal.get(canal);
          const catColor = ctdGetCategoria(items[0]?.peca?.content_type).color;
          const readyN = items.filter(x => x.ready).length;
          const totalN = items.length;
          const allReady = readyN === totalN;
          return (
            <div key={canal} style={{ marginBottom: 28 }}>
              {/* Canal header — cor da categoria + contador */}
              <div style={{
                display: 'flex', alignItems: 'center', gap: 12,
                padding: '10px 14px', marginBottom: 12,
                background: `${catColor}0F`, borderLeft: `4px solid ${catColor}`,
                borderRadius: 'var(--radius-sm, 6px)',
              }}>
                <span style={{
                  fontSize: 11, fontWeight: 700, letterSpacing: '0.08em',
                  color: catColor, fontFamily: 'var(--font-mono, monospace)',
                  textTransform: 'uppercase',
                }}>{canal}</span>
                <span style={{ fontSize: 11, color: 'var(--text-muted, #4a5e7a)' }}>
                  {readyN}/{totalN} pronta{totalN !== 1 ? 's' : ''}
                </span>
                {allReady && (
                  <span style={{
                    fontSize: 9, fontWeight: 700, padding: '3px 8px', borderRadius: 4,
                    background: '#DCFCE7', color: '#166534',
                    fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.06em',
                    display: 'inline-flex', alignItems: 'center', gap: 4,
                  }}>
                    <Icon name="check" size={11} /> TUDO PRONTO
                  </span>
                )}
                {false && allReady && !enviado && (
                  <button
                    onClick={(e) => { e.stopPropagation(); handleEnviarActivacao({ canal, label: canal }); }}
                    disabled={busy}
                    style={{ marginLeft: 'auto', background: '#1F8A52', color: '#fff', border: 0, padding: '5px 10px', borderRadius: 6, fontSize: 11, fontWeight: 700, cursor: busy ? 'not-allowed' : 'pointer' }}
                  >
                    Enviar {canal}
                  </button>
                )}
              </div>
              {/* Cards do canal — aspect ratio dinâmico */}
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))', gap: 14 }}>
                {items.map(({ idx, peca, copy, prompt, imagem, copyApproved, promptApproved, imageApproved, videoApproved, ready, stale, isBrevo }) => {
                  const url = stale ? null : (imagem?.url || prompt?.magnific_url);
                  const aspect = ctdAspectForPeca(peca);
                  return (
                    <div key={idx}
                      onClick={() => setSelectedPecaIdx(idx)}
                      style={{
                        background: 'var(--bg-surface, #fff)',
                        border: `${ready ? 2 : 1}px solid ${ready ? '#1F8A52' : 'var(--border-1, #ECEFF5)'}`,
                        borderRadius: 'var(--radius-lg, 10px)',
                        overflow: 'hidden', boxShadow: 'var(--shadow-card)',
                        display: 'flex', flexDirection: 'column', cursor: 'pointer',
                        transition: 'transform 150ms, box-shadow 150ms',
                      }}
                      onMouseEnter={(e) => { e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = '0 8px 24px rgba(0,0,0,0.12)'; }}
                      onMouseLeave={(e) => { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = 'var(--shadow-card)'; }}
                    >
                      {/* Image — aspect ratio correcto por canal */}
                      <div style={{ aspectRatio: aspect, background: 'var(--bg-subtle, #FAFAFA)', position: 'relative', overflow: 'hidden' }}>
                        {url ? (
                          <img src={url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
                        ) : (
                          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: 'var(--text-dim, #7a8fa8)', fontSize: 11, textAlign: 'center', padding: 12 }}>
                            {isBrevo ? 'Email' : (stale ? 'Copy mudou · gerar prompt e imagem' : 'Sem imagem')}
                          </div>
                        )}
                        {ready && (
                          <div style={{
                            position: 'absolute', top: 8, right: 8,
                            background: '#1F8A52', color: '#fff',
                            padding: '3px 8px', borderRadius: 'var(--radius-xs, 4px)',
                            fontSize: 9, fontWeight: 700, letterSpacing: '0.06em',
                            fontFamily: 'var(--font-mono, monospace)',
                            display: 'inline-flex', alignItems: 'center', gap: 4,
                          }}>
                            <Icon name="check" size={11} /> PRONTA
                          </div>
                        )}
                      </div>
                      {/* Meta + status */}
                      <div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 8, flex: 1 }}>
                        <div style={{ fontSize: 11.5, fontWeight: 700, color: 'var(--text, #112954)', lineHeight: 1.3, fontFamily: 'var(--font-mono, monospace)' }} title={peca.subtitulo || peca.titulo}>
                          {peca.titulo || `Peça #${idx + 1}`}
                        </div>
                        {copy?.copy_imagem && (
                          <div style={{ fontSize: 11, color: 'var(--text, #112954)', fontWeight: 700, lineHeight: 1.35, overflow: 'hidden', textOverflow: 'ellipsis', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }}>
                            {copy.copy_imagem}
                          </div>
                        )}
                        {(() => {
                          const pt = (copy?.payload?.primary_text_full) || (copy?.caption_organica) || '';
                          if (!pt) return null;
                          return (
                            <div style={{ fontSize: 10.5, color: 'var(--text-muted, #4a5e7a)', lineHeight: 1.55, whiteSpace: 'pre-wrap', marginTop: 4, paddingTop: 6, borderTop: '1px solid var(--border-1, #ECEFF5)' }}>
                              {pt}
                            </div>
                          );
                        })()}
                        <div style={{
                          marginTop: 'auto', paddingTop: 10, borderTop: '1px solid var(--border-1, #ECEFF5)',
                          display: 'flex', gap: 5, flexWrap: 'wrap', alignItems: 'center',
                        }}>
                          <StatusChip ok={copyApproved} label="Copy" onClick={(e) => { e.stopPropagation(); onNavigate?.('copies'); }} />
                          {!isBrevo && <StatusChip ok={promptApproved} label="Prompt" onClick={(e) => { e.stopPropagation(); onNavigate?.('prompts'); }} />}
                          {!isBrevo && <StatusChip ok={imageApproved} label="Imagem" onClick={(e) => { e.stopPropagation(); onNavigate?.('imagens'); }} />}
                          <span style={{ marginLeft: 'auto', fontSize: 10, color: 'var(--color-primary, #3859D0)', fontWeight: 700, fontFamily: 'var(--font-mono, monospace)' }}>Ver completo →</span>
                        </div>
                      </div>
                    </div>
                  );
                })}
              </div>
            </div>
          );
        });
      })()}
    </div>
  );
};

const StatusChip = ({ ok, label, onClick }) => {
  const Icon = window.Icon;
  return (
    <button onClick={onClick} title={ok ? `${label} aprovada` : `${label} pendente — clica para ir à tab`} style={{
      background: ok ? '#E1F7E6' : '#FEF3C7',
      color: ok ? '#1F8A52' : '#92400E',
      border: `1px solid ${ok ? '#86EFAC' : '#FCD34D'}`,
      padding: '3px 8px', fontSize: 9, fontWeight: 700, letterSpacing: '0.06em',
      borderRadius: 'var(--radius-xs, 4px)', cursor: 'pointer',
      fontFamily: 'var(--font-mono, monospace)',
      display: 'inline-flex', alignItems: 'center', gap: 4,
    }}>
      <Icon name={ok ? 'check' : 'circle'} size={9} /> {label}
    </button>
  );
};

function ctdOneShotCanalId(peca) {
  const ct = peca?.content_type || '';
  const canal = String(peca?.canal || '');
  if (ct === 'briefing_interno' || canal.includes('interno') || canal.includes('portal_notificacao')) return 'interno';
  if (ct === 'social_post' || canal === 'meta_ads') return 'meta_ads';
  if (ct === 'email' || ct === 'email_html' || canal === 'email') return 'email';
  if (ct === 'wa_message' || ct === 'wa_template' || canal === 'whatsapp') return 'whatsapp';
  if (ct === 'blog_post') return 'blog';
  if (ct === 'landing_page') return 'landing';
  if (ct === 'site') return 'site';
  if (ct === 'led' || canal === 'muppi_led') return 'led';
  return '';
}

function CtdAddPecaModal({ campanhaId, plan, onClose, onAdded }) {
  const [canal, setCanal] = React.useState('');
  const [nome, setNome] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [erro, setErro] = React.useState('');
  const counts = {};
  (plan || []).forEach(p => {
    const id = ctdOneShotCanalId(p);
    if (id) counts[id] = (counts[id] || 0) + 1;
  });
  React.useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    const onKey = (e) => { if (e.key === 'Escape' && !busy) onClose(); };
    window.addEventListener('keydown', onKey);
    return () => {
      document.body.style.overflow = prev;
      window.removeEventListener('keydown', onKey);
    };
  }, [onClose, busy]);
  const adicionar = async () => {
    if (!canal) { setErro('Escolhe um canal.'); return; }
    setBusy(true);
    setErro('');
    try {
      const res = await CtdAPI.oneShotPeca(campanhaId, { canal, nome: nome.trim() });
      onAdded?.(res?.item);
      onClose();
    } catch (e) {
      setErro(e.message || 'Não adicionei a peça.');
      setBusy(false);
    }
  };
  return ReactDOM.createPortal(
    <div onClick={busy ? undefined : onClose} role="presentation" style={{ position: 'fixed', inset: 0, background: 'rgba(17,41,84,0.55)', zIndex: 1200, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }}>
      <div role="dialog" aria-modal="true" aria-label="Adicionar conteúdo" onClick={e => e.stopPropagation()} style={{ width: 'min(520px, 100%)', background: '#fff', borderRadius: 14, boxShadow: '0 24px 64px rgba(17,41,84,0.28)', overflow: 'hidden' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16, padding: '16px 18px 14px', borderBottom: '1px solid #ECEFF5' }}>
          <div>
            <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.08em', color: '#3859D0' }}>ONE SHOT</div>
            <div style={{ fontSize: 18, fontWeight: 700, color: '#112954', marginTop: 2 }}>Adicionar conteúdo</div>
            <div style={{ fontSize: 13, color: '#64748b', marginTop: 2 }}>Mais um email, ou uma peça noutro canal.</div>
          </div>
          <button type="button" onClick={onClose} disabled={busy} aria-label="Fechar" style={{ border: '1px solid #E3E6EB', background: '#fff', borderRadius: 8, width: 32, height: 32, cursor: 'pointer', color: '#112954', fontSize: 16, flexShrink: 0 }}>×</button>
        </div>
        <div style={{ padding: 16 }}>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
            {CTD_ONE_SHOT_CANAIS.map(c => {
              const on = canal === c.id;
              const n = counts[c.id] || 0;
              return (
                <button key={c.id} type="button" onClick={() => { setCanal(c.id); setErro(''); }} style={{
                  display: 'flex', alignItems: 'flex-start', gap: 8, textAlign: 'left',
                  border: `1px solid ${on ? '#3859D0' : '#E3E6EB'}`, background: on ? '#EFF3FF' : '#fff',
                  borderRadius: 10, padding: '10px 12px', cursor: 'pointer', fontFamily: 'inherit', minHeight: 64,
                }}>
                  <span style={{ flex: 1, minWidth: 0 }}>
                    <span style={{ display: 'block', fontSize: 13, fontWeight: 700, color: on ? '#3859D0' : '#112954' }}>{c.label}</span>
                    <span style={{ display: 'block', fontSize: 11, color: '#64748b', marginTop: 2 }}>{n ? `${n} no plano` : c.sub}</span>
                  </span>
                </button>
              );
            })}
          </div>
          <label style={{ display: 'block', fontSize: 12, fontWeight: 700, color: '#475569', marginTop: 14 }}>
            Nome da peça
            <input value={nome} disabled={busy} maxLength={60} onChange={e => setNome(e.target.value)} placeholder="Opcional. Ex: follow-up" style={{ display: 'block', width: '100%', boxSizing: 'border-box', marginTop: 6, border: '1px solid #E3E6EB', borderRadius: 8, padding: '9px 10px', fontSize: 14, color: '#112954', fontWeight: 500 }} />
          </label>
          <div style={{ fontSize: 12, color: '#64748b', marginTop: 6 }}>Sem nome, usa o canal. O segundo email fica «Email 2».</div>
          {erro && <div style={{ color: '#991B1B', fontSize: 12, marginTop: 8 }}>{erro}</div>}
        </div>
        <div style={{ padding: '12px 16px 16px', borderTop: '1px solid #ECEFF5', display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
          <button type="button" onClick={onClose} disabled={busy} className="btn" style={{ fontSize: 12 }}>Cancelar</button>
          <button type="button" onClick={adicionar} disabled={busy || !canal} className="btn" style={{ fontSize: 12, background: '#3859D0', color: '#fff', borderColor: '#3859D0', opacity: busy || !canal ? 0.5 : 1 }}>
            {busy ? 'A adicionar…' : 'Adicionar'}
          </button>
        </div>
      </div>
    </div>,
    document.body
  );
}

// Canal na linha de estado. Com vários canais, é um selector — não uma segunda tab.
const CtdCanalLinha = ({ canais, actual, onChange }) => {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [open]);
  if (!actual) return null;
  if (!canais || canais.length < 2) {
    return <span style={{ color: '#283252', fontWeight: 650 }}>· {actual.canal}</span>;
  }
  return (
    <span ref={ref} style={{ position: 'relative', display: 'inline-flex', alignItems: 'center', gap: 6 }}>
      <span>·</span>
      <button type="button" onClick={() => setOpen(v => !v)} aria-haspopup="listbox" aria-expanded={open} style={{
        background: 'none', border: 0, padding: 0, cursor: 'pointer',
        fontSize: 12, fontWeight: 650, color: '#283252',
        display: 'inline-flex', alignItems: 'center', gap: 4, fontFamily: 'inherit',
      }}>
        {actual.canal}
        <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="6 9 12 15 18 9"/></svg>
      </button>
      {open && (
        <div role="listbox" style={{
          position: 'absolute', top: '100%', left: 8, marginTop: 6, minWidth: 168,
          background: '#fff', border: '1px solid #e2e8f0', borderRadius: 6,
          boxShadow: '0 4px 12px rgba(0,0,0,0.08)', zIndex: 30, padding: 4,
        }}>
          {canais.map(ch => (
            <button key={ch.canal} type="button" role="option" aria-selected={ch.canal === actual.canal}
              onClick={() => { onChange(ch.canal); setOpen(false); }}
              style={{
                display: 'block', width: '100%', textAlign: 'left', border: 0, borderRadius: 4,
                background: ch.canal === actual.canal ? '#f1f5f9' : 'transparent',
                padding: '7px 10px', fontSize: 12, fontWeight: ch.canal === actual.canal ? 650 : 500,
                color: '#283252', cursor: 'pointer', fontFamily: 'inherit',
              }}>
              {ch.canal}
            </button>
          ))}
        </div>
      )}
    </span>
  );
};

// ═══════════════════════════════════════════════════════════════════════════════
// CtdCampanhaDetail — vista de detalhe com tabs
// ═══════════════════════════════════════════════════════════════════════════════
const CtdCampanhaDetail = ({ campanha, onBack, onRefresh, onRestaurada, userEmail, initialTab = 'contexto', initialCanal = null }) => {
  const [activeTab, setActiveTab]         = React.useState(initialTab);
  const [activeCanal, setActiveCanal]     = React.useState(initialCanal);
  const [quadroLocal, setQuadroLocal]     = React.useState(null);
  const [valBusy, setValBusy]             = React.useState(false);
  const [ligarOpen, setLigarOpen]         = React.useState(false);
  const [restaurarOpen, setRestaurarOpen] = React.useState(false);
  const [valErro, setValErro]             = React.useState(null);
  const [focusPiece, setFocusPiece]       = React.useState(null);
  const [openPiece, setOpenPiece]         = React.useState(null);
  const [dismissPiece, setDismissPiece]   = React.useState(0);
  const [prompts, setPrompts]             = React.useState([]);
  const [imagens, setImagens]             = React.useState([]);
  const [copies, setCopies]               = React.useState([]);
  const [canais, setCanais]               = React.useState([]);
  const [campFull, setCampFull]           = React.useState(null);
  const [addOpen, setAddOpen]             = React.useState(false);
  const [addNote, setAddNote]             = React.useState('');
  const esRef                             = React.useRef(null);
  const copiesGen                         = React.useRef(0);

  const reloadPrompts = React.useCallback(() => {
    if (!campanha?.id) return Promise.resolve();
    return CtdAPI.prompts(campanha.id).then(d => setPrompts(d?.prompts || d?.rows || [])).catch(() => {});
  }, [campanha?.id]);
  const reloadImagens = React.useCallback(() => {
    if (!campanha?.id) return Promise.resolve();
    return CtdAPI.imagens(campanha.id).then(d => setImagens(d?.imagens || d?.rows || [])).catch(() => {});
  }, [campanha?.id]);
  const reloadCopies = React.useCallback(() => {
    if (!campanha?.id) return Promise.resolve();
    const gen = ++copiesGen.current;
    return CtdAPI.copies(campanha.id).then(d => {
      if (gen !== copiesGen.current) return;
      setCopies(d?.copies || d?.rows || []);
      return CtdAPI.campanhaFull(campanha.id).then(full => {
        if (gen !== copiesGen.current || !full || full.error) return;
        setCampFull(full);
      });
    }).catch(() => {});
  }, [campanha?.id]);
  const onCopySaved = React.useCallback((row) => {
    if (!row?.id) return;
    setCopies(prev => {
      const i = prev.findIndex(c => c.id === row.id);
      if (i < 0) return prev;
      const next = prev.slice();
      next[i] = { ...prev[i], ...row };
      return next;
    });
  }, []);

  React.useEffect(() => {
    if (!campanha?.id) return;
    CtdAPI.prompts(campanha.id).then(d => setPrompts(d?.prompts || d?.rows || [])).catch(() => {});
    CtdAPI.imagens(campanha.id).then(d => setImagens(d?.imagens || d?.rows || [])).catch(() => {});
    reloadCopies();
    CtdAPI.tabs(campanha.id).then(d => setCanais(d?.canais || d?.tabs || d?.rows || [])).catch(() => {});
    CtdAPI.campanhaFull(campanha.id).then(d => setCampFull(d)).catch(() => {});
  }, [campanha?.id]);

  React.useEffect(() => {
    if (!campanha?.id) return;
    try {
      const es = CtdAPI.stream(campanha.id, (ev) => {
        if (['peca_updated', 'prompt_updated', 'image_generated', 'copy_generated', 'refresh'].includes(ev?.type)) {
          CtdAPI.prompts(campanha.id).then(d => setPrompts(d?.prompts || d?.rows || []));
          CtdAPI.imagens(campanha.id).then(d => setImagens(d?.imagens || d?.rows || []));
          reloadCopies();
        }
      });
      esRef.current = es;
      return () => { try { es.close(); } catch {} };
    } catch {}
  }, [campanha?.id]);

  const brandColor = ctdBrandColor(campanha?.brand_slug);
  const isOneShot = campanha?.one_shot === true || String(campanha?.one_shot) === 'true' || String(campFull?.proposta_json?.one_shot) === 'true';
  const addPiece = (item) => {
    if (!item) return;
    setCampFull(prev => {
      if (!prev) return prev;
      const proposta = { ...(prev.proposta_json || {}) };
      proposta.comm_plan = [...(proposta.comm_plan || []), item];
      return { ...prev, proposta_json: proposta };
    });
    const canalNovo = ctdCanalLabel(item);
    if (canalNovo && canalNovo !== '—') setActiveCanal(canalNovo);
    setActiveTab('copies');
    setFocusPiece(null);
    setOpenPiece(null);
    setDismissPiece(n => n + 1);
    setAddNote(`«${item.titulo}» entrou no plano.`);
    setTimeout(() => setAddNote(''), 4200);
    CtdAPI.campanhaFull(campanha.id).then(d => { if (d && !d.error) setCampFull(d); }).catch(() => {});
    onRefresh?.();
  };
  const livePieceRef = React.useRef(null);
  const currentPiece = (livePieceRef.current && livePieceRef.current.idx != null)
    ? livePieceRef.current
    : ((focusPiece && focusPiece.tab === activeTab) ? focusPiece : (openPiece || focusPiece));
  const goTab = (target) => {
    if (target && typeof target === 'object') {
      const piece = {
        tab: target.tab,
        idx: target.idx,
        country: target.country || null,
        titulo: target.titulo || null,
        contentType: target.contentType || null,
        waGroup: target.waGroup || livePieceRef.current?.waGroup || null,
        generate: !!target.generate,
      };
      if (piece.idx == null && piece.titulo == null && livePieceRef.current) {
        piece.idx = livePieceRef.current.idx;
        piece.titulo = livePieceRef.current.titulo;
        piece.country = piece.country || livePieceRef.current.country;
        piece.contentType = piece.contentType || livePieceRef.current.contentType;
        piece.waGroup = piece.waGroup || livePieceRef.current.waGroup || null;
      }
      setActiveTab(target.tab);
      const label = target.canal || ctdCanalLabel({ content_type: piece.contentType });
      if (label && label !== '—') setActiveCanal(label);
      setFocusPiece(piece);
      return;
    }
    if (target === 'contexto') {
      setActiveTab('contexto');
      setFocusPiece(null);
      return;
    }
    setActiveTab(target);
    setFocusPiece(null);
  };
  const goTabSamePiece = (tabId) => {
    const src = (livePieceRef.current && livePieceRef.current.idx != null) ? livePieceRef.current : currentPiece;
    const pieceTabs = ['copies', 'prompts', 'imagens', 'peca', 'video'];
    if (src && src.idx != null && pieceTabs.includes(tabId)) {
      if (tabId === 'video' && src.contentType && src.contentType !== 'social_post') {
        goTab('video');
        return;
      }
      goTab({ tab: tabId, idx: src.idx, country: src.country, titulo: src.titulo, contentType: src.contentType, waGroup: src.waGroup || null });
      return;
    }
    goTab(tabId);
  };
  const closeTop = () => {
    if (currentPiece && currentPiece.idx != null) {
      livePieceRef.current = null;
      setFocusPiece(null);
      setOpenPiece(null);
      setActiveTab('copies');
      setDismissPiece(n => n + 1);
      return;
    }
    onBack?.();
  };
  const onCopyBase = (copyBase) => setCampFull(prev => prev ? ({ ...prev, proposta_json: { ...(prev.proposta_json || {}), copy_base: copyBase } }) : prev);
  const onPlan = (plan) => setCampFull(prev => {
    if (!prev || !Array.isArray(plan)) return prev;
    return { ...prev, proposta_json: { ...(prev.proposta_json || {}), comm_plan: plan } };
  });
  const onPlanItem = (index, item) => setCampFull(prev => {
    if (!prev || index == null || !item) return prev;
    const plan = [...((prev.proposta_json || {}).comm_plan || [])];
    plan[index] = item;
    return { ...prev, proposta_json: { ...(prev.proposta_json || {}), comm_plan: plan } };
  });
  const quadro = quadroLocal || campFull?.quadro || campanha?.quadro || { coluna: campanha?.coluna || 'por_fazer', validacao: campanha?.validacao || null, canais: campanha?.canais || [] };
  const quadroCanais = quadro.canais || [];
  const canalActual = quadroCanais.find(c => c.canal === activeCanal) || (quadroCanais.length === 1 ? quadroCanais[0] : null) || ctdCanalAtrasado(quadroCanais);
  const fases = canalActual?.fases || [];
  const escolherCanal = (nome) => {
    const ch = quadroCanais.find(c => c.canal === nome);
    if (!ch) return;
    setActiveCanal(ch.canal);
    setFocusPiece(null);
    if (activeTab === 'contexto') return;
    const ids = (ch.fases || []).map(f => f.id);
    if (!ids.includes(activeTab)) setActiveTab(ch.tab || ch.fases?.[0]?.id || 'copies');
  };
  const podeValidar = ctdPodeValidar(userEmail);
  const estadoCampanha = String(campFull?.status || campanha?.status || campanha?.camp_status || '');
  const somenteLeitura = estadoCampanha === 'arquivada' || estadoCampanha === 'cancelada';
  const estadoRotulo = somenteLeitura
    ? (estadoCampanha === 'cancelada' ? 'Cancelada' : 'Arquivada')
    : ((CTD_KANBAN_COLS.find(k => k.id === quadro.coluna) || {}).label || 'Em produção');
  const jaEmActivacao = estadoCampanha === 'em_activacao' || estadoCampanha === 'publicado';
  const pedirValidacao = async (action) => {
    if (!campanha?.id) return;
    setValBusy(true); setValErro(null);
    try {
      const res = await CtdAPI.validacao(campanha.id, { action, actor_email: userEmail, actor_name: userEmail?.split('@')[0] });
      if (res?.quadro) setQuadroLocal(res.quadro);
      onRefresh?.();
    } catch (e) { setValErro(e.message); }
    setValBusy(false);
  };
  const enviarActivacao = async () => {
    if (!campanha?.id || quadro.coluna !== 'aprovado') return;
    setValBusy(true); setValErro(null);
    try {
      const res = await fetch(`/api/marketing/producao/campanha/${campanha.id}/enviar-producao`, {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ approver_email: userEmail, approver_name: userEmail?.split('@')[0] }),
      });
      const j = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(j.error || ('HTTP ' + res.status));
      onRefresh?.();
      onBack?.();
    } catch (e) { setValErro(e.message); }
    setValBusy(false);
  };
  const ligarBriefing = async (briefingId) => {
    const res = await CtdAPI.oneShotBriefing(campanha.id, briefingId);
    if (!res?.briefing_id) throw new Error('Não foi possível ligar o briefing.');
    setLigarOpen(false);
    const full = await CtdAPI.campanhaFull(campanha.id);
    if (full && !full.error) setCampFull(full);
  };
  const abrirBriefing = () => {
    const id = campFull?.briefing?.briefing_id;
    if (!id || !campanha?.id) return;
    const canal = activeCanal || canalActual?.canal || null;
    const seq = Date.now();
    const pedido = {
      id: String(id),
      seq,
      origem: {
        sub: 'conteudos',
        campanhaId: String(campanha.id),
        tab: activeTab || 'contexto',
        canal: canal && canal !== '—' ? canal : null,
      },
    };
    window.__mktAbrirPedido = pedido;
    try {
      localStorage.setItem('mkt-open-briefing-id', pedido.id);
      sessionStorage.setItem('mkt-briefing-origem', JSON.stringify(pedido.origem));
    } catch {}
    try {
      const prev = new URLSearchParams(String(window.location.hash || '').replace(/^#/, ''));
      const next = new URLSearchParams();
      next.set('screen', 'marketing');
      next.set('sub', 'briefings');
      next.set('id', pedido.id);
      next.set('abrir', String(seq));
      const marca = prev.get('marca');
      if (marca) next.set('marca', marca);
      window.location.hash = next.toString();
    } catch {}
    if (window.mktNavToSub) window.mktNavToSub('briefings');
    window.dispatchEvent(new CustomEvent('mkt-open-briefing', { detail: pedido }));
  };
  const tabProps = { campanha, prompts, imagens, copies, canais, campFull, onRefresh, userEmail, reloadPrompts, reloadImagens, reloadCopies, onNavigate: goTab, onBackToKanban: onBack, onPiece: setOpenPiece, dismissPiece, livePieceRef, focusPiece: focusPiece && focusPiece.tab === activeTab ? focusPiece : null, onCopyBase, onCopySaved, onPlanItem, onPlan, lockCanal: activeTab === 'contexto' ? null : activeCanal, podeActivar: quadro.coluna === 'aprovado' };

  return (
    <div data-theme="light" style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#f8fafc' }}>
      {/* Header */}
      <div style={{ background: '#ffffff', borderBottom: '1px solid #ECEFF5', padding: '16px 40px 0', flexShrink: 0 }}>
        <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.07em', color: '#94A4C4', textTransform: 'uppercase', marginBottom: 6, fontFamily: 'var(--font-mono, monospace)' }}>
          MARKETING · PRODUÇÃO DE CONTEÚDOS
        </div>
        <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16 }}>
          <div>
            <h1 style={{ margin: 0, fontSize: 21, fontWeight: 700, color: '#283252', fontFamily: 'var(--font-display, Montserrat, sans-serif)', letterSpacing: '-0.01em' }}>
              {campanha?.titulo || 'Sem título'}
            </h1>
            <div style={{ fontSize: 12, color: '#64748b', marginTop: 6, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
              <span style={{ color: brandColor, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em', fontSize: 10, fontFamily: 'var(--font-mono, monospace)' }}>{(campanha?.brand_name || '—').toUpperCase()}</span>
              <span>· {estadoRotulo}</span>
              <CtdCanalLinha canais={quadroCanais} actual={canalActual} onChange={escolherCanal} />
              {addNote && <span style={{ color: '#166534', fontWeight: 600 }}>{addNote}</span>}
            </div>
            {somenteLeitura && (
              <div style={{ marginTop: 10, padding: '8px 10px', borderRadius: 8, background: '#F8FAFC', border: '1px solid #E2E8F0', fontSize: 12, color: '#334155', lineHeight: 1.45 }}>
                Campanha arquivada. O plano, as copies, as imagens e a peça final ficam para consulta. Restaura para voltar a alterar.
              </div>
            )}
          </div>
          <div style={{ display: 'flex', gap: 8, flexShrink: 0, alignItems: 'center', flexWrap: 'wrap', justifyContent: 'flex-end' }}>
            {!somenteLeitura && quadro.coluna === 'em_validacao' && podeValidar && (
              <button type="button" onClick={() => pedirValidacao('aprovar')} disabled={valBusy} className="btn btn-sm" style={{ fontSize: 12 }}>Aprovar</button>
            )}
            {!somenteLeitura && quadro.coluna === 'em_validacao' && podeValidar && (
              <button type="button" onClick={() => pedirValidacao('devolver')} disabled={valBusy} className="btn btn-sm" style={{ fontSize: 12 }}>Pedir alterações</button>
            )}
            {!somenteLeitura && quadro.validacao === 'devolvida' && quadroCanais.length > 0 && quadroCanais.every(c => c.fechado) && (
              <button type="button" onClick={() => pedirValidacao('partilhar')} disabled={valBusy} className="btn btn-sm" style={{ fontSize: 12 }}>Partilhar novamente</button>
            )}
            {!somenteLeitura && (jaEmActivacao ? (
              <span style={{ fontSize: 10, fontWeight: 700, padding: '8px 10px', borderRadius: 8, background: '#DCFCE7', color: '#166534', fontFamily: 'var(--font-mono, monospace)', letterSpacing: '0.06em' }}>ENVIADO PARA ACTIVAÇÃO</span>
            ) : quadro.coluna === 'aprovado' && (
              <button type="button" onClick={enviarActivacao} disabled={valBusy} style={{ background: '#1F8A52', color: '#fff', border: 0, padding: '6px 12px', borderRadius: 6, fontSize: 12, fontWeight: 700, cursor: valBusy ? 'not-allowed' : 'pointer' }}>
                Enviar para activação
              </button>
            ))}
            {valErro && <span style={{ fontSize: 11, color: '#991B1B', maxWidth: 220 }}>{valErro}</span>}
            {somenteLeitura && ctdCanArchive(userEmail) && (
              <button type="button" onClick={() => setRestaurarOpen(true)} className="btn btn-sm" style={{ fontSize: 12 }}>Restaurar</button>
            )}
            {isOneShot && campFull?.briefing?.briefing_id && campFull.briefing.briefing_indexado && (
              <button type="button" onClick={abrirBriefing} className="btn btn-sm" style={{ fontSize: 12 }}>Abrir briefing</button>
            )}
            {!somenteLeitura && isOneShot && campFull && !campFull?.briefing?.briefing_indexado && (
              <button type="button" onClick={() => setLigarOpen(true)} className="btn btn-sm" style={{ fontSize: 12 }}>Ligar briefing</button>
            )}
            {!somenteLeitura && isOneShot && activeTab === 'contexto' && (
              <button type="button" onClick={() => setAddOpen(true)} className="btn btn-sm" style={{ fontSize: 12 }}>
                Adicionar conteúdo
              </button>
            )}
            <button onClick={closeTop} title="Fechar" style={{
              background: 'none', border: '1px solid #e2e8f0', cursor: 'pointer',
              width: 30, height: 30, borderRadius: 6, color: '#64748b',
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 16,
            }}>×</button>
          </div>
        </div>
        <div style={{ display: 'flex', gap: 0, marginTop: 14, marginLeft: -4, flexWrap: 'wrap' }}>
          {[{ id: 'contexto', label: 'Plano' }, ...fases.map(f => ({ id: f.id, label: f.label }))].map(t => {
            const active = t.id === 'contexto' ? activeTab === 'contexto' : t.id === activeTab;
            return (
              <button key={t.id}
                onClick={() => {
                  if (t.id === 'contexto') { setActiveTab('contexto'); setFocusPiece(null); return; }
                  if (canalActual?.canal) setActiveCanal(canalActual.canal);
                  goTabSamePiece(t.id);
                }}
                style={{
                  background: 'none', border: 0, outline: 'none',
                  borderBottom: `2px solid ${active ? brandColor : 'transparent'}`,
                  color: active ? '#283252' : '#94A4C4',
                  fontSize: 13, fontWeight: active ? 600 : 500,
                  fontFamily: 'var(--font-display, Montserrat, sans-serif)', letterSpacing: '.01em',
                  padding: '10px 16px 12px', cursor: 'pointer', whiteSpace: 'nowrap',
                }}>
                {t.label}
              </button>
            );
          })}
        </div>
      </div>

      {/* Content */}
      {addOpen && (
        <CtdAddPecaModal
          campanhaId={campanha?.id}
          plan={(campFull?.proposta_json?.comm_plan) || []}
          onClose={() => setAddOpen(false)}
          onAdded={addPiece}
        />
      )}
      {ligarOpen && (
        <CtdLigarBriefingModal
          brandSlug={campanha?.brand_slug || campFull?.brand_slug || campFull?.briefing?.brand_slug}
          onClose={() => setLigarOpen(false)}
          onLigar={ligarBriefing}
        />
      )}
      {restaurarOpen && (
        <CtdRestaurarModal
          campanha={{ ...campanha, status_antes_arquivo: campFull?.status_antes_arquivo || campanha?.status_antes_arquivo }}
          onClose={() => setRestaurarOpen(false)}
          onDone={(info) => { setRestaurarOpen(false); onRestaurada?.(info); }}
        />
      )}
      <div style={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>
        {activeTab === 'contexto' && <CtdTabContexto {...tabProps} />}
        {activeTab === 'prompts'  && <CtdTabPrompts  {...tabProps} />}
        {activeTab === 'imagens'  && <CtdTabImagens  {...tabProps} />}
        {activeTab === 'video'    && <CtdTabVideo    {...tabProps} />}
        {activeTab === 'copies'   && <CtdTabCopies   {...tabProps} />}
        {activeTab === 'peca'     && <CtdTabPecaFinal {...tabProps} />}
      </div>
    </div>
  );
};

const CTD_BRIEFING_ESTADO = {
  rascunho: 'Rascunho',
  submetido: 'Submetido',
  validado_fc: 'Validado',
  validado_ceo: 'Validado',
  aprovado: 'Aprovado',
  em_campanha: 'Aprovado',
  alteracoes_pedidas: 'Alterações pedidas',
};
const ctdBriefingOpcao = (b) => {
  const estado = CTD_BRIEFING_ESTADO[b.status] ? ` · ${CTD_BRIEFING_ESTADO[b.status]}` : '';
  const n = Number(b.n_campanhas || 0);
  const irmas = n > 0 ? ` · ${n} campanha${n === 1 ? '' : 's'}` : '';
  return `${b.nome}${estado}${irmas}`;
};

function CtdLigarBriefingModal({ brandSlug, onClose, onLigar }) {
  const [opcoes, setOpcoes] = React.useState(null);
  const [escolha, setEscolha] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [erro, setErro] = React.useState('');
  React.useEffect(() => {
    let vivo = true;
    CtdAPI.oneShotBriefings(brandSlug)
      .then(d => { if (vivo) setOpcoes(d?.rows || []); })
      .catch(e => { if (vivo) { setOpcoes([]); setErro(e?.message || 'Não foi possível ler os briefings.'); } });
    return () => { vivo = false; };
  }, [brandSlug]);
  const confirmar = async () => {
    if (!escolha) { setErro('Escolhe um briefing.'); return; }
    setBusy(true);
    setErro('');
    try {
      await onLigar(Number(escolha));
    } catch (e) {
      setErro(e?.message || 'Não foi possível ligar.');
      setBusy(false);
    }
  };
  return ReactDOM.createPortal(
    <div onClick={onClose} role="presentation" style={{ position: 'fixed', inset: 0, background: 'rgba(17,41,84,0.55)', zIndex: 1200, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }}>
      <div role="dialog" aria-modal="true" aria-label="Ligar briefing" onClick={e => e.stopPropagation()} style={{ width: 'min(480px, 100%)', background: '#fff', borderRadius: 14, boxShadow: '0 24px 64px rgba(17,41,84,0.28)', padding: 18 }}>
        <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.08em', color: '#3859D0' }}>ONE SHOT</div>
        <div style={{ fontSize: 18, fontWeight: 700, color: '#112954', marginTop: 2 }}>Ligar a um briefing</div>
        <div style={{ fontSize: 13, color: '#64748b', marginTop: 4, marginBottom: 14 }}>Escolhe um briefing aprovado desta marca. A ficha desta execução fica independente das outras campanhas.</div>
        <select value={escolha} onChange={e => { setEscolha(e.target.value); setErro(''); }} disabled={opcoes == null || busy} style={{ width: '100%', boxSizing: 'border-box', border: '1px solid #E3E6EB', borderRadius: 8, padding: '10px 12px', fontSize: 14, fontFamily: 'inherit', color: '#112954', background: '#fff' }}>
          <option value="">{opcoes == null ? 'A carregar…' : 'Escolhe um briefing'}</option>
          {(opcoes || []).map(b => (
            <option key={b.id} value={b.id}>{ctdBriefingOpcao(b)}</option>
          ))}
        </select>
        {opcoes && opcoes.length === 0 && <div style={{ fontSize: 12, color: '#64748b', marginTop: 8 }}>Não há briefings aprovados nesta marca.</div>}
        {erro && <div style={{ color: '#dc2626', fontSize: 12, marginTop: 8 }}>{erro}</div>}
        <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 }}>
          <button type="button" onClick={onClose} disabled={busy} className="btn btn-sm" style={{ fontSize: 12 }}>Cancelar</button>
          <button type="button" onClick={confirmar} disabled={busy || !escolha} style={{ background: '#3859D0', color: '#fff', border: 0, borderRadius: 8, padding: '8px 14px', fontSize: 13, fontWeight: 700, cursor: busy || !escolha ? 'not-allowed' : 'pointer' }}>{busy ? 'A ligar…' : 'Ligar'}</button>
        </div>
      </div>
    </div>,
    document.body
  );
}

const CTD_MERCADOS = [
  { code: 'PT', label: 'Portugal' },
  { code: 'ES', label: 'Espanha' },
  { code: 'FR', label: 'França' },
  { code: 'IT', label: 'Itália' },
  { code: 'UK', label: 'UK' },
  { code: 'DE', label: 'Alemanha' },
  { code: 'TR', label: 'Turquia' },
];
const CTD_ONE_SHOT_CANAIS = [
  { id: 'meta_ads', label: 'Meta Ads', sub: 'Facebook e Instagram' },
  { id: 'email', label: 'Email', sub: 'Um envio' },
  { id: 'whatsapp', label: 'WhatsApp', sub: 'Uma mensagem' },
  { id: 'blog', label: 'Blog', sub: 'Um artigo' },
  { id: 'landing', label: 'Landing', sub: 'Uma página de conversão' },
  { id: 'site', label: 'Site', sub: 'Uma página do site' },
  { id: 'interno', label: 'Comunicação Interna', sub: 'Briefing para a equipa' },
  { id: 'led', label: 'LED', sub: 'Um painel' },
];

function CtdNovoConteudoModal({ brands, userEmail, initialBrand, onClose, onCreated }) {
  const marcas = (brands || []).filter(b => b.slug && b.slug !== 'all');
  const [marca, setMarca] = React.useState(initialBrand && initialBrand !== 'all' ? initialBrand : '');
  const [titulo, setTitulo] = React.useState('');
  const [pais, setPais] = React.useState('');
  const [canais, setCanais] = React.useState([]);
  const [busy, setBusy] = React.useState(false);
  const [erro, setErro] = React.useState('');
  const pronto = !!(marca && titulo.trim() && pais && canais.length);
  React.useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => {
      document.body.style.overflow = prev;
      window.removeEventListener('keydown', onKey);
    };
  }, [onClose]);
  const toggleCanal = (id) => setCanais(prev => prev.includes(id) ? prev.filter(c => c !== id) : [...prev, id]);
  const criar = async () => {
    setErro('');
    if (!marca) { setErro('Escolhe a marca.'); return; }
    if (!titulo.trim()) { setErro('Escreve o nome.'); return; }
    if (!pais) { setErro('Escolhe o mercado.'); return; }
    if (!canais.length) { setErro('Escolhe pelo menos um canal.'); return; }
    setBusy(true);
    try {
      const row = await CtdAPI.oneShot({
        titulo: titulo.trim(),
        brand_slug: marca,
        canais,
        pais,
        created_by_email: userEmail || '',
      });
      onCreated?.(row);
      onClose();
    } catch (e) {
      setErro(e?.message || 'Não foi possível criar.');
    } finally {
      setBusy(false);
    }
  };
  return ReactDOM.createPortal(
    <div onClick={onClose} role="presentation" style={{ position: 'fixed', inset: 0, background: 'rgba(17,41,84,0.55)', zIndex: 1200, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }}>
      <div role="dialog" aria-modal="true" aria-label="Novo conteúdo" onClick={e => e.stopPropagation()} style={{ width: 'min(560px, 100%)', maxHeight: 'min(820px, calc(100vh - 40px))', background: '#fff', borderRadius: 14, boxShadow: '0 24px 64px rgba(17,41,84,0.28)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16, padding: '16px 18px 14px', borderBottom: '1px solid #ECEFF5' }}>
          <div>
            <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.08em', color: '#3859D0' }}>NOVO CONTEÚDO</div>
            <div style={{ fontSize: 18, fontWeight: 700, color: '#112954', marginTop: 2 }}>Sem campanha</div>
            <div style={{ fontSize: 13, color: '#64748b', marginTop: 2 }}>Email, anúncios e os outros canais. O briefing liga-se depois, se existir.</div>
          </div>
          <button type="button" onClick={onClose} aria-label="Fechar" style={{ border: '1px solid #E3E6EB', background: '#fff', borderRadius: 8, width: 32, height: 32, cursor: 'pointer', color: '#112954', fontSize: 16, flexShrink: 0 }}>×</button>
        </div>
        <div style={{ flex: 1, overflow: 'auto', padding: 16 }}>
          <div style={{ fontSize: 12, fontWeight: 700, color: '#475569', marginBottom: 6 }}>Marca</div>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 14 }}>
            {marcas.map(b => {
              const on = marca === b.slug;
              return (
                <button key={b.slug} type="button" onClick={() => setMarca(b.slug)} style={{
                  border: `1px solid ${on ? '#3859D0' : '#E3E6EB'}`, background: on ? '#EFF3FF' : '#fff',
                  color: on ? '#3859D0' : '#112954', borderRadius: 99, padding: '4px 10px', fontSize: 12, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit',
                }}>{b.name}</button>
              );
            })}
          </div>
          <label style={{ display: 'block', fontSize: 12, fontWeight: 700, color: '#475569', marginBottom: 6 }}>Nome</label>
          <input value={titulo} onChange={e => setTitulo(e.target.value)} placeholder="Nome da peça" style={{
            width: '100%', boxSizing: 'border-box', border: '1px solid #E3E6EB', borderRadius: 8,
            padding: '10px 12px', fontSize: 14, fontFamily: 'inherit', marginBottom: 14, color: '#112954',
          }} />
          <div style={{ fontSize: 12, fontWeight: 700, color: '#475569', marginBottom: 6 }}>Mercado</div>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 14 }}>
            {CTD_MERCADOS.map(m => {
              const on = pais === m.code;
              return (
                <button key={m.code} type="button" onClick={() => setPais(m.code)} style={{
                  border: `1px solid ${on ? '#3859D0' : '#E3E6EB'}`, background: on ? '#EFF3FF' : '#fff',
                  color: on ? '#3859D0' : '#112954', borderRadius: 99, padding: '4px 10px', fontSize: 12, fontWeight: 700, cursor: 'pointer', fontFamily: 'inherit',
                }}>{m.label}</button>
              );
            })}
          </div>
          <div style={{ fontSize: 12, fontWeight: 700, color: '#475569', marginBottom: 6 }}>Canais</div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
            {CTD_ONE_SHOT_CANAIS.map(c => {
              const on = canais.includes(c.id);
              return (
                <button key={c.id} type="button" onClick={() => toggleCanal(c.id)} style={{
                  display: 'flex', alignItems: 'flex-start', gap: 8, textAlign: 'left',
                  border: `1px solid ${on ? '#3859D0' : '#E3E6EB'}`, background: on ? '#EFF3FF' : '#fff',
                  borderRadius: 10, padding: '10px 12px', cursor: 'pointer', fontFamily: 'inherit', minHeight: 58,
                }}>
                  <span style={{ flex: 1, minWidth: 0 }}>
                    <span style={{ display: 'block', fontSize: 13, fontWeight: 700, color: on ? '#3859D0' : '#112954' }}>{c.label}</span>
                    <span style={{ display: 'block', fontSize: 11, color: '#64748b', marginTop: 2 }}>{c.sub}</span>
                  </span>
                  <span style={{ width: 16, height: 16, borderRadius: 4, border: `1.5px solid ${on ? '#3859D0' : '#CBD5E1'}`, background: on ? '#3859D0' : '#fff', color: '#fff', fontSize: 11, lineHeight: '14px', textAlign: 'center', flexShrink: 0 }}>{on ? '✓' : ''}</span>
                </button>
              );
            })}
          </div>
          {erro && <div style={{ color: '#dc2626', fontSize: 12, marginTop: 10 }}>{erro}</div>}
        </div>
        <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, padding: '12px 16px', borderTop: '1px solid #ECEFF5' }}>
          <button type="button" onClick={onClose} className="btn" style={{ fontSize: 13 }}>Cancelar</button>
          <button type="button" onClick={criar} disabled={!pronto || busy} style={{
            background: '#3859D0', color: '#fff', border: 0, borderRadius: 8, padding: '9px 16px',
            fontSize: 13, fontWeight: 700, cursor: !pronto || busy ? 'not-allowed' : 'pointer', opacity: !pronto || busy ? 0.5 : 1,
          }}>{busy ? 'A criar…' : 'Criar e abrir'}</button>
        </div>
      </div>
    </div>,
    document.body
  );
}

function CtdVistaMenu({ brands, userEmail, initialBrand, onCreated, vista, onVista }) {
  const [open, setOpen] = React.useState(false);
  const [modal, setModal] = React.useState(false);
  const [foco, setFoco] = React.useState(-1);
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const onClick = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onClick);
    document.addEventListener('keydown', onKey);
    return () => { document.removeEventListener('mousedown', onClick); document.removeEventListener('keydown', onKey); };
  }, [open]);
  const linha = (id, label, sub, on, marked) => (
    <button key={id} type="button" role="menuitem" onClick={on} onMouseEnter={() => setFoco(id)} onMouseLeave={() => setFoco(-1)} style={{
      display: 'flex', alignItems: 'center', gap: 8, width: '100%', textAlign: 'left',
      background: foco === id || marked ? 'color-mix(in oklch, var(--ai-500) 10%, #fff)' : 'transparent',
      color: foco === id || marked ? 'var(--ai-500)' : 'var(--text)',
      border: 'none', padding: '8px 10px', borderRadius: 6, cursor: 'pointer', fontFamily: 'inherit',
    }}>
      <span style={{ flex: 1, minWidth: 0 }}>
        <span style={{ display: 'block', fontSize: 13, fontWeight: 650 }}>{label}</span>
        {sub && <span style={{ display: 'block', fontSize: 11, color: 'var(--text-muted)', fontWeight: 400, marginTop: 1 }}>{sub}</span>}
      </span>
      {marked && <span style={{ color: 'var(--ai-500)' }}>✓</span>}
    </button>
  );
  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <button type="button" aria-label="Menu" onMouseDown={e => e.stopPropagation()} onClick={() => { setOpen(v => !v); setFoco(-1); }} style={{
        width: 30, height: 30, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
        border: `1px solid ${open ? 'var(--ai-500)' : 'var(--border, #E3E6EB)'}`, borderRadius: 6,
        background: open ? 'color-mix(in oklch, var(--ai-500) 10%, #fff)' : '#fff',
        color: open ? 'var(--ai-500)' : 'var(--text, #283252)', cursor: 'pointer', padding: 0,
      }}>
        <svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"><path d="M2.5 4h11M2.5 8h11M2.5 12h11" /></svg>
      </button>
      {open && (
        <div role="menu" style={{
          position: 'absolute', top: 'calc(100% + 6px)', right: 0, zIndex: 70,
          background: '#fff', border: '1px solid var(--border, #E3E6EB)', borderRadius: 8,
          boxShadow: '0 12px 32px rgba(17,41,84,0.14)', minWidth: 280, padding: 4,
        }}>
          {linha('campanhas', 'Campanhas', 'As que estão no quadro', () => { setOpen(false); onVista?.('quadro'); }, vista !== 'arquivo')}
          {linha('arquivo', 'Campanhas arquivadas', 'Consultar as que saíram do quadro', () => { setOpen(false); onVista?.('arquivo'); }, vista === 'arquivo')}
          {linha('novo', 'Novo conteúdo', 'Email, anúncios e os outros canais, sem campanha', () => { setOpen(false); setModal(true); }, false)}
        </div>
      )}
      {modal && (
        <CtdNovoConteudoModal
          brands={brands}
          userEmail={userEmail}
          initialBrand={initialBrand}
          onClose={() => setModal(false)}
          onCreated={onCreated}
        />
      )}
    </div>
  );
}

// ═══════════════════════════════════════════════════════════════════════════════
// Root component — MktConteudosScreen
// ═══════════════════════════════════════════════════════════════════════════════
const ctdPeriodoTodo = () => {
  const today = new Date();
  return { preset: 'all', start: new Date(today.getFullYear(), 0, 1), end: today };
};
const ctdNoPeriodo = (c, periodo) => {
  if (!periodo || periodo.preset === 'all') return true;
  const raw = c.data_kickoff || c.ultima_actividade || c.created_at;
  const d = raw ? new Date(raw) : null;
  if (!d || isNaN(d)) return false;
  const start = new Date(periodo.start); start.setHours(0, 0, 0, 0);
  const end = new Date(periodo.end); end.setHours(23, 59, 59, 999);
  return d >= start && d <= end;
};
const CTD_PASSOS_FILTRO = ['Por começar', 'Copy', 'Prompt', 'Imagem', 'Vídeo', 'Aprovação Meta'];
const ctdNormBusca = (s) => String(s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();

function CtdBusca({ campanhas, onOpen, onClose }) {
  const [q, setQ] = React.useState('');
  const [rows, setRows] = React.useState([]);
  const [foco, setFoco] = React.useState(0);
  const [loading, setLoading] = React.useState(false);
  const inputRef = React.useRef(null);
  React.useEffect(() => { inputRef.current?.focus(); }, []);
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [onClose]);
  React.useEffect(() => {
    const termo = ctdNormBusca(q.trim());
    if (termo.length < 2) { setRows([]); setLoading(false); return; }
    let stop = false;
    setLoading(true);
    const t = setTimeout(() => {
      if (stop) return;
      const found = (campanhas || []).filter(c => {
        const hay = ctdNormBusca([c.titulo, c.commercial_name, c.product_name, c.brand_name, ...(c.canais || []).map(ch => `${ch.canal} ${ch.rotulo || ch.passo}`)].filter(Boolean).join(' '));
        return hay.includes(termo);
      }).slice(0, 12);
      setRows(found);
      setFoco(0);
      setLoading(false);
    }, 180);
    return () => { stop = true; clearTimeout(t); };
  }, [q, campanhas]);
  const abrir = (r) => { if (r) onOpen(r); };
  const onKeyDown = (e) => {
    if (e.key === 'ArrowDown') { e.preventDefault(); setFoco(i => Math.min(rows.length - 1, i + 1)); }
    if (e.key === 'ArrowUp') { e.preventDefault(); setFoco(i => Math.max(0, i - 1)); }
    if (e.key === 'Enter') { e.preventDefault(); abrir(rows[foco]); }
  };
  const termo = q.trim();
  const sub = (c) => [c.brand_name, c.commercial_name || c.product_name, (CTD_KANBAN_COLS.find(k => k.match(c)) || {}).label].filter(Boolean).join(' · ');
  return (
    <div role="dialog" aria-label="Procurar campanhas" onMouseDown={onClose} style={{
      position: 'fixed', inset: 0, zIndex: 9999, background: 'rgba(0,0,0,.78)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24,
    }}>
      <button type="button" aria-label="Fechar" onMouseDown={e => e.stopPropagation()} onClick={onClose} style={{
        position: 'absolute', top: 18, right: 22, width: 36, height: 36, border: 'none', background: 'transparent',
        color: '#fff', fontSize: 26, lineHeight: 1, cursor: 'pointer', fontFamily: 'inherit',
      }}>×</button>
      <div onMouseDown={e => e.stopPropagation()} style={{ width: 'min(920px, 100%)' }}>
        <style>{'.ctd-procurar::placeholder{color:rgba(255,255,255,.65)}'}</style>
        <div style={{ display: 'flex', alignItems: 'center', gap: 16, borderBottom: '1px solid rgba(255,255,255,.85)', paddingBottom: 8 }}>
          <input ref={inputRef} className="ctd-procurar" value={q} onChange={e => setQ(e.target.value)} onKeyDown={onKeyDown}
            placeholder="Procurar" aria-label="Procurar"
            style={{ flex: 1, height: 48, border: 'none', outline: 'none', background: 'transparent', color: '#fff', fontSize: 22, fontFamily: 'inherit', fontWeight: 400 }} />
          <svg width="22" height="22" viewBox="0 0 16 16" fill="none" stroke="#fff" strokeWidth="1.6" strokeLinecap="round" aria-hidden="true"><circle cx="7" cy="7" r="4.25" /><path d="M10.5 10.5L13.5 13.5" /></svg>
        </div>
        {termo.length >= 2 && (
          <div style={{ marginTop: 8 }}>
            {loading && !rows.length && <div style={{ padding: '12px 4px', fontSize: 14, color: 'rgba(255,255,255,.75)' }}>A procurar…</div>}
            {!loading && !rows.length && <div style={{ padding: '12px 4px', fontSize: 14, color: 'rgba(255,255,255,.75)' }}>Nenhuma campanha.</div>}
            {rows.map((r, i) => (
              <button key={r.id} type="button" onMouseEnter={() => setFoco(i)} onClick={() => abrir(r)} style={{
                display: 'flex', width: '100%', textAlign: 'left', gap: 12, alignItems: 'baseline',
                background: i === foco ? 'rgba(255,255,255,.14)' : 'transparent',
                border: 'none', borderRadius: 6, padding: '10px 8px', cursor: 'pointer', fontFamily: 'inherit', color: '#fff',
              }}>
                <span style={{ fontSize: 11, fontWeight: 650, letterSpacing: '.04em', textTransform: 'uppercase', color: 'rgba(255,255,255,.6)', minWidth: 72 }}>Campanha</span>
                <span style={{ fontSize: 16, fontWeight: 600 }}>{r.titulo || 'Sem título'}</span>
                <span style={{ fontSize: 13, color: 'rgba(255,255,255,.72)' }}>{sub(r)}</span>
              </button>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

const MktConteudosScreen = ({ userEmail }) => {
  const [campanhas, setCampanhas]       = React.useState([]);
  const [allBrands, setAllBrands]       = React.useState([]);
  const [selected, setSelected]         = React.useState(null);
  const [selectedTab, setSelectedTab]   = React.useState(null);
  const [selectedCanal, setSelectedCanal] = React.useState(null);
  const [loading, setLoading]           = React.useState(true);
  const [error, setError]               = React.useState(null);
  const [archiveCamp, setArchiveCamp]   = React.useState(null);
  const [restaurarCamp, setRestaurarCamp] = React.useState(null);
  const [renameCamp, setRenameCamp]     = React.useState(null);
  const [archiveNote, setArchiveNote]   = React.useState('');
  const [vista, setVista]               = React.useState('quadro');
  window.useMktZoneLoading(loading && !campanhas.length && !error);
  const [filterBrand, setFilterBrand]   = React.useState('all');
  const [filterCol, setFilterCol]       = React.useState(() => {
    try {
      const col = localStorage.getItem('mkt-inicio-filter-col');
      if (col) { localStorage.removeItem('mkt-inicio-filter-col'); return col; }
    } catch {}
    return 'all';
  });
  const [filterProduto, setFilterProduto] = React.useState('all');
  const [filterCanal, setFilterCanal] = React.useState('all');
  const [filterPasso, setFilterPasso] = React.useState('all');
  const [filterOrdem, setFilterOrdem] = React.useState('recentes');
  const [periodo, setPeriodo] = React.useState(ctdPeriodoTodo);
  const [busca, setBusca] = React.useState(false);
  const [freshId, setFreshId]           = React.useState(null);
  const [manualExpanded, setManualExpanded] = React.useState(new Set());

  const toggleCol = React.useCallback((id) => {
    setManualExpanded(prev => {
      const next = new Set(prev);
      next.has(id) ? next.delete(id) : next.add(id);
      return next;
    });
  }, []);

  const load = React.useCallback(() => {
    setLoading(true);
    setError(null);
    CtdAPI.list(vista === 'arquivo' ? { arquivo: '1' } : {})
      .then(d => {
        if (d && !Array.isArray(d) && d.error) throw new Error(d.error);
        setCampanhas(Array.isArray(d) ? d : (d?.campanhas || d?.rows || []));
      })
      .catch(e => setError(e?.message || 'Erro a carregar'))
      .finally(() => setLoading(false));
    CtdAPI.brands().then(d => {
      const list = Array.isArray(d) ? d : (d?.brands || d?.rows || []);
      const sorted = list.filter(b => b.active !== false && b.slug !== 'todas');
      setAllBrands(sorted);
    });
  }, [vista]);
  const depoisDeRestaurar = React.useCallback((info) => {
    const noQuadro = ['em_producao', 'em_activacao', 'publicado'].includes(info?.status);
    const nome = info?.titulo || 'Campanha';
    setRestaurarCamp(null);
    setSelected(null);
    setArchiveNote(noQuadro
      ? `«${nome}» voltou ao quadro de produção.`
      : `«${nome}» restaurada. Fica na lista de Campanhas.`);
    setTimeout(() => setArchiveNote(''), 4200);
    if (noQuadro) {
      if (vista === 'quadro') load();
      else setVista('quadro');
    } else if (vista === 'arquivo') load();
    else setVista('arquivo');
  }, [vista, load]);

  React.useEffect(() => { load(); }, [load]);

  React.useEffect(() => {
    if (!freshId || loading) return;
    const el = document.querySelector(`[data-campanha-id="${freshId}"]`);
    if (el) el.scrollIntoView({ block: 'nearest', inline: 'nearest' });
  }, [freshId, loading, campanhas]);

  React.useEffect(() => {
    try {
      const tab = localStorage.getItem('mkt-inicio-conteudo-tab');
      if (tab) { localStorage.removeItem('mkt-inicio-conteudo-tab'); setSelectedTab(tab); }
      const canal = localStorage.getItem('mkt-open-conteudo-canal');
      if (canal) { localStorage.removeItem('mkt-open-conteudo-canal'); setSelectedCanal(canal); }
    } catch {}
  }, []);

  const abrirCampanhaGuardada = React.useCallback((id, extra) => {
    if (!id) return;
    if (extra?.tab) setSelectedTab(extra.tab);
    if (extra?.canal) setSelectedCanal(extra.canal);
    const found = campanhas.find(c => String(c.id) === String(id));
    if (found) setSelected(found);
    else CtdAPI.campanhaFull(id).then(d => { if (d && d.id) setSelected(d); }).catch(() => {});
  }, [campanhas]);
  const campanhaJaAberta = React.useRef(null);

  React.useEffect(() => {
    if (!campanhas.length) return;
    let id = null;
    let extra = null;
    try {
      const q = new URLSearchParams(String(window.location.hash || '').replace(/^#/, ''));
      if (q.get('sub') === 'conteudos' && q.get('camp')) {
        id = q.get('camp');
        extra = { tab: q.get('tab') || null, canal: q.get('canal') || null };
      }
    } catch {}
    if (!id) {
      try { id = localStorage.getItem('mkt-open-conteudo-id'); } catch {}
    }
    if (!id || campanhaJaAberta.current === String(id)) return;
    campanhaJaAberta.current = String(id);
    try { localStorage.removeItem('mkt-open-conteudo-id'); } catch {}
    abrirCampanhaGuardada(id, extra);
  }, [campanhas, abrirCampanhaGuardada]);

  React.useEffect(() => {
    const onHash = () => {
      if (!campanhas.length) return;
      try {
        const q = new URLSearchParams(String(window.location.hash || '').replace(/^#/, ''));
        if (q.get('sub') !== 'conteudos' || !q.get('camp')) return;
        campanhaJaAberta.current = String(q.get('camp'));
        abrirCampanhaGuardada(q.get('camp'), { tab: q.get('tab') || null, canal: q.get('canal') || null });
      } catch {}
    };
    window.addEventListener('hashchange', onHash);
    return () => window.removeEventListener('hashchange', onHash);
  }, [campanhas, abrirCampanhaGuardada]);

  // Global handler: permite abrir uma campanha específica a partir de outras tabs (ex: Aprovação → Produção)
  React.useEffect(() => {
    window.mktProducaoOpenCampanha = (id, extra) => abrirCampanhaGuardada(id, extra);
    return () => { delete window.mktProducaoOpenCampanha; };
  }, [abrirCampanhaGuardada]);

  const daMarca = React.useMemo(
    () => campanhas.filter(c => filterBrand === 'all' || c.brand_slug === filterBrand),
    [campanhas, filterBrand]
  );
  const filtered = React.useMemo(() => {
    const list = daMarca.filter(c => {
      if (filterCol !== 'all') {
        const col = CTD_KANBAN_COLS.find(k => k.id === filterCol);
        if (col && !col.match(c)) return false;
      }
      if (filterCanal !== 'all' && !(c.canais || []).some(ch => ch.canal === filterCanal)) return false;
      if (filterPasso !== 'all' && !(c.canais || []).some(ch => !ch.fechado && ch.passo === filterPasso)) return false;
      if (filterProduto !== 'all' && (c.commercial_name || c.product_name) !== filterProduto) return false;
      if (!ctdNoPeriodo(vista === 'arquivo' ? { ...c, data_kickoff: c.arquivada_em || c.created_at } : c, periodo)) return false;
      return true;
    });
    list.sort((a, b) => {
      if (filterOrdem === 'nome') return String(a.titulo || '').localeCompare(String(b.titulo || ''), 'pt');
      if (filterOrdem === 'kickoff') {
        const da = a.data_kickoff ? new Date(a.data_kickoff).getTime() : Infinity;
        const db = b.data_kickoff ? new Date(b.data_kickoff).getTime() : Infinity;
        return da - db;
      }
      const da = new Date(a.ultima_actividade || a.created_at || 0).getTime();
      const db = new Date(b.ultima_actividade || b.created_at || 0).getTime();
      return db - da;
    });
    return list;
  }, [daMarca, filterCol, filterCanal, filterPasso, filterProduto, periodo, filterOrdem, vista]);

  const kpi = React.useMemo(() => {
    const total = campanhas.length;
    const counts = {};
    CTD_KANBAN_COLS.forEach(col => {
      counts[col.id] = campanhas.filter(c => col.match(c)).length;
    });
    return { total, fechadas: (counts.aprovado || 0) + (counts.activacao || 0), concluidas: counts.aprovado || 0, aprovacao: counts.em_validacao || 0, ...counts };
  }, [campanhas]);

  const opcoesEstado = CTD_KANBAN_COLS.map(col => ({
    value: col.id,
    label: col.label,
    n: daMarca.filter(c => col.match(c)).length,
    cor: col.color,
  }));
  const opcoesCanal = (() => {
    const n = new Map();
    daMarca.forEach(c => (c.canais || []).forEach(ch => { if (ch.canal) n.set(ch.canal, (n.get(ch.canal) || 0) + 1); }));
    return [...n.entries()]
      .sort((a, b) => (CTD_CANAL_ORDER.indexOf(a[0]) === -1 ? 99 : CTD_CANAL_ORDER.indexOf(a[0])) - (CTD_CANAL_ORDER.indexOf(b[0]) === -1 ? 99 : CTD_CANAL_ORDER.indexOf(b[0])))
      .map(([canal, count]) => ({ value: canal, label: canal, n: count }));
  })();
  const opcoesProduto = (() => {
    const n = new Map();
    daMarca.forEach(c => {
      const nome = c.commercial_name || c.product_name;
      if (nome) n.set(nome, (n.get(nome) || 0) + 1);
    });
    return [...n.entries()].sort((a, b) => a[0].localeCompare(b[0], 'pt')).map(([nome, count]) => ({ value: nome, label: nome, n: count }));
  })();
  const opcoesPasso = CTD_PASSOS_FILTRO.map(passo => ({
    value: passo,
    label: passo,
    n: daMarca.filter(c => (c.canais || []).some(ch => !ch.fechado && ch.passo === passo)).length,
  })).filter(o => o.n > 0);
  const limparFiltros = () => {
    setFilterBrand('all');
    setFilterCol('all');
    setFilterCanal('all');
    setFilterPasso('all');
    setFilterProduto('all');
    setFilterOrdem('recentes');
    setPeriodo(ctdPeriodoTodo());
  };
  const Filtro = window.ResFiltro || CtdDropdown;
  const Periodo = window.BriefingDateRangePicker;
  const universoBusca = React.useMemo(
    () => daMarca.filter(c => ctdNoPeriodo(vista === 'arquivo' ? { ...c, data_kickoff: c.arquivada_em || c.created_at } : c, periodo)),
    [daMarca, periodo, vista]
  );
  const fecharBusca = React.useCallback(() => setBusca(false), []);
  const abrirCampanha = (c) => {
    if (!c) return;
    const atrasado = c.coluna === 'por_fazer' ? null : ctdCanalAtrasado(c.canais);
    setBusca(false);
    setSelected(c);
    setSelectedCanal(atrasado?.canal || null);
    setSelectedTab(atrasado ? (atrasado.tab || 'copies') : 'contexto');
  };

  // Detail view
  if (selected) {
    return <CtdCampanhaDetail campanha={selected} initialTab={selectedTab || 'contexto'} initialCanal={selectedCanal} onRestaurada={depoisDeRestaurar} onBack={() => {
      setSelected(null); setSelectedTab(null); setSelectedCanal(null);
      campanhaJaAberta.current = null;
      try {
        const prev = new URLSearchParams(String(window.location.hash || '').replace(/^#/, ''));
        if (prev.get('camp')) {
          prev.delete('camp'); prev.delete('tab'); prev.delete('canal');
          history.replaceState(null, '', '#' + prev.toString());
        }
      } catch {}
    }} onRefresh={load} userEmail={userEmail} />;
  }

  // List view (kanban)
  return (
    <div data-theme="light" style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#f8fafc' }}>

      {/* Header fixo */}
      <div style={{ background: '#ffffff', borderBottom: '1px solid #ECEFF5', padding: '20px 40px 0', flexShrink: 0 }}>
        {/* Breadcrumb */}
        <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.07em', color: '#94A4C4', textTransform: 'uppercase', marginBottom: 6, fontFamily: 'var(--font-mono, monospace)' }}>
          MARKETING · PRODUÇÃO DE CONTEÚDOS
        </div>
        {/* Title + metrics + CTA */}
        <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16 }}>
          <div>
            <h1 style={{ margin: 0, fontSize: 21, fontWeight: 700, color: '#283252', fontFamily: 'var(--font-display, Montserrat, sans-serif)', letterSpacing: '-0.01em', lineHeight: 1.2 }}>
              Produção de Conteúdos
            </h1>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 6, flexWrap: 'wrap' }}>
              <span style={{ fontSize: 11, color: '#94A4C4' }}>
                {vista === 'arquivo'
                  ? `${kpi.total} arquivada${kpi.total !== 1 ? 's' : ''}`
                  : `${kpi.total} campanha${kpi.total !== 1 ? 's' : ''} · ${kpi.fechadas || 0} já fechada${kpi.fechadas !== 1 ? 's' : ''}`}
              </span>
              {vista !== 'arquivo' && kpi.total > 0 && (() => {
                const pct = Math.round((kpi.fechadas || 0) / kpi.total * 100);
                return (
                  <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                    <div style={{ width: 60, height: 4, borderRadius: 99, background: '#ECEFF5', overflow: 'hidden' }}>
                      <div style={{ width: `${pct}%`, height: '100%', borderRadius: 99, background: '#22c55e', transition: 'width 0.4s ease' }} />
                    </div>
                    <span style={{ fontSize: 11, color: '#94A4C4' }}>{pct}% já fechadas</span>
                  </div>
                );
              })()}
            </div>
          </div>
        </div>

        {/* Separador */}
        <div style={{ height: 1, background: '#ECEFF5', margin: '16px -40px 0' }} />

        {/* Brand tabs */}
        <div style={{ display: 'flex', gap: 0, overflowX: 'auto' }}>
          {[{ slug: 'all', name: 'Todas' }, ...allBrands].map(b => {
            const active = b.slug === filterBrand || (b.slug === 'all' && filterBrand === 'all');
            return (
              <button key={b.slug} onClick={() => setFilterBrand(b.slug === 'all' ? 'all' : b.slug)} style={{
                background: 'none', border: 'none', outline: 'none',
                borderBottom: `2px solid ${active ? '#3859D0' : 'transparent'}`,
                color: active ? '#283252' : '#94A4C4',
                fontSize: 13, fontWeight: active ? 600 : 500,
                fontFamily: 'var(--font-display, Montserrat, sans-serif)', letterSpacing: '.01em',
                padding: '10px 16px 12px', cursor: 'pointer', whiteSpace: 'nowrap',
                transition: 'color .15s, border-color .15s',
              }}>
                {b.name}
              </button>
            );
          })}
        </div>
      </div>

      {/* Body scrollable */}
      <div className="scrollbar" style={{ flex: 1, minHeight: 0, overflowY: 'auto', overflowX: 'auto', background: '#f8fafc' }}>
        <div style={{ padding: '28px 40px 48px', minWidth: 640, display: 'flex', flexDirection: 'column', gap: 16 }}>

          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
            <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
              <Filtro label="Estado" value={filterCol} marcado={filterCol !== 'all'} onChange={setFilterCol}
                options={[{ value: 'all', label: 'Todos' }, ...opcoesEstado]} />
              <Filtro label="Canal" value={filterCanal} marcado={filterCanal !== 'all'} onChange={setFilterCanal}
                options={[{ value: 'all', label: 'Todos' }, ...opcoesCanal]} />
              <Filtro label="Produto" value={filterProduto} marcado={filterProduto !== 'all'} onChange={setFilterProduto}
                options={[{ value: 'all', label: 'Todos' }, ...opcoesProduto]} />
              <Filtro label="Passo" value={filterPasso} marcado={filterPasso !== 'all'} onChange={setFilterPasso}
                options={[{ value: 'all', label: 'Todos' }, ...opcoesPasso]} />
              <Filtro label="Ordem" value={filterOrdem} marcado={filterOrdem !== 'recentes'} onChange={setFilterOrdem}
                options={[{ value: 'recentes', label: 'Recentes' }, { value: 'kickoff', label: 'Kick-off' }, { value: 'nome', label: 'Nome' }]} />
              {Periodo && <Periodo label="Período" align="left" value={periodo} onChange={setPeriodo} />}
            </div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginLeft: 'auto' }}>
              <button type="button" aria-label="Procurar campanhas" onMouseDown={e => e.stopPropagation()} onClick={() => setBusca(v => !v)} style={{
                width: 30, height: 30, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
                border: `1px solid ${busca ? 'var(--ai-500)' : 'var(--border, #E3E6EB)'}`, borderRadius: 6,
                background: busca ? 'color-mix(in oklch, var(--ai-500) 10%, #fff)' : '#fff',
                color: busca ? 'var(--ai-500)' : 'var(--text, #283252)', cursor: 'pointer', padding: 0,
              }}>
                <svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"><circle cx="7" cy="7" r="4.25" /><path d="M10.5 10.5L13.5 13.5" /></svg>
              </button>
              <CtdVistaMenu brands={allBrands} userEmail={userEmail} initialBrand={filterBrand} vista={vista} onVista={(v) => { setVista(v); setFilterCol('all'); setFilterPasso('all'); setSelected(null); }} onCreated={(row) => {
                if (row?.brand_slug) setFilterBrand(row.brand_slug);
                setFilterCol('all');
                setFilterCanal('all');
                setFilterPasso('all');
                setFilterProduto('all');
                setFilterOrdem('recentes');
                setPeriodo(ctdPeriodoTodo());
                setFreshId(row?.id || null);
                setArchiveNote(`«${row?.titulo || 'Conteúdo'}» entrou no quadro.`);
                setTimeout(() => setArchiveNote(''), 4200);
                if (vista !== 'quadro') setVista('quadro');
                setSelected(row?.id ? { ...row, one_shot: true, camp_status: 'em_producao' } : null);
                load();
              }} />
            </div>
          </div>

          {/* Kanban */}
          <div style={{ overflowX: 'auto', paddingBottom: 8 }}>
            {loading ? (
              <div style={{ display: 'flex', gap: 12, minWidth: 'max-content' }}>
                {CTD_KANBAN_COLS.map(col => (
                  <div key={col.id} style={{ width: 240, flexShrink: 0 }}>
                    <div style={{ height: 72, borderRadius: 8, background: '#fff', marginBottom: 12, opacity: 0.5 }} />
                    {[1, 2].map(i => <div key={i} style={{ height: 88, borderRadius: 8, background: '#fff', marginBottom: 8, opacity: 1 - i * 0.3 }} />)}
                  </div>
                ))}
              </div>
            ) : error ? (
              <div style={{ padding: '48px 0', textAlign: 'center', color: '#dc2626' }}>{error}</div>
            ) : vista === 'arquivo' ? (
              <>
                {filtered.length === 0 && campanhas.length > 0 && (
                  <div style={{ padding: '16px 0 20px', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
                    <div style={{ fontSize: 13, color: '#64748b' }}>Sem resultados para os filtros actuais.</div>
                    <button onClick={limparFiltros} className="btn" style={{ fontSize: 12 }}>Limpar filtros</button>
                  </div>
                )}
                <CtdArquivoLista
                  linhas={filtered}
                  canRestaurar={ctdCanArchive(userEmail)}
                  onOpen={abrirCampanha}
                  onRestaurar={setRestaurarCamp}
                />
              </>
            ) : (
              <>
                {filtered.length === 0 && campanhas.length > 0 && (
                  <div style={{ padding: '16px 0 20px', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
                    <div style={{ fontSize: 13, color: '#64748b' }}>Sem resultados para os filtros actuais.</div>
                    <button onClick={limparFiltros} className="btn" style={{ fontSize: 12 }}>Limpar filtros</button>
                  </div>
                )}
                <div style={{ display: 'flex', gap: 10, minWidth: 'max-content', alignItems: 'flex-start' }}>
                  {CTD_KANBAN_COLS.map(col => {
                    const cards       = filtered.filter(c => col.match(c));
                    const activityCnt = cards.length;
                    const collapsed   = activityCnt === 0 && !manualExpanded.has(col.id);
                    const colW        = collapsed ? 52 : 300;
                    const openCampanha = abrirCampanha;
                    return (
                      <div key={col.id} style={{ width: colW, minWidth: colW, flexShrink: 0, transition: 'width 0.22s ease' }}>
                        {collapsed ? (
                          <CtdCollapsedColStrip col={col} count={activityCnt} onExpand={() => toggleCol(col.id)} />
                        ) : (
                          <>
                            <CtdExpandedColHeader
                              col={col}
                              count={activityCnt}
                              total={Math.max(kpi.total, 1)}
                              onCollapse={() => toggleCol(col.id)}
                              subLine={null}
                            />
                            {cards.map(c => (
                              <CtdCampanhaCard
                                key={c.id}
                                campanha={c}
                                colId={col.id}
                                fresh={c.id === freshId}
                                onOpen={openCampanha}
                                canArchive={ctdCanArchive(userEmail)}
                                onArchive={setArchiveCamp}
                                onRename={setRenameCamp}
                              />
                            ))}
                            {activityCnt === 0 && <div style={{ padding: 12, textAlign: 'center', color: '#94a3b8', fontSize: 11 }}>—</div>}
                          </>
                        )}
                      </div>
                    );
                  })}
                </div>
              </>
            )}
          </div>

        </div>
      </div>
      {renameCamp && (
        <CtdRenameModal
          campanha={renameCamp}
          onClose={() => setRenameCamp(null)}
          onDone={(titulo) => {
            setCampanhas(prev => prev.map(c => c.id === renameCamp.id ? { ...c, titulo } : c));
            setSelected(cur => (cur && cur.id === renameCamp.id ? { ...cur, titulo } : cur));
            setRenameCamp(null);
            setArchiveNote(`Nome actualizado para «${titulo}».`);
            setTimeout(() => setArchiveNote(''), 4200);
          }}
        />
      )}
      {archiveCamp && (
        <CtdArchiveModal
          campanha={archiveCamp}
          userEmail={userEmail}
          onClose={() => setArchiveCamp(null)}
          onDone={() => {
            const nome = archiveCamp.titulo || 'Campanha';
            setArchiveCamp(null);
            setSelected(cur => (cur && cur.id === archiveCamp.id ? null : cur));
            setArchiveNote(`«${nome}» arquivada. Plano, copies, imagens e leads ficam para consulta.`);
            load();
            setTimeout(() => setArchiveNote(''), 4200);
          }}
        />
      )}
      {restaurarCamp && (
        <CtdRestaurarModal
          campanha={restaurarCamp}
          onClose={() => setRestaurarCamp(null)}
          onDone={depoisDeRestaurar}
        />
      )}
      {archiveNote && (
        <div style={{ position: 'fixed', bottom: 20, left: '50%', transform: 'translateX(-50%)', background: '#1d2e38', color: '#fff', padding: '10px 14px', borderRadius: 8, fontSize: 13, zIndex: 1100 }}>{archiveNote}</div>
      )}
      {busca && <CtdBusca campanhas={universoBusca} onOpen={abrirCampanha} onClose={fecharBusca} />}
    </div>
  );
};

window.MktConteudosScreen = MktConteudosScreen;
