/* ================================================================
   Khronus — Atendimentos (WhatsApp)
   Tudo dentro de uma IIFE: os .jsx do Khronus são carregados como
   script global no navegador (sem bundler), então dois arquivos
   declarando a mesma const no escopo de topo derrubam o app inteiro.

   Quem fala com o WhatsApp é a extensão-ponte, rodando na aba do
   WhatsApp Web. Esta tela nunca toca no WhatsApp: ela lê o banco e,
   pra enviar, escreve na fila que a ponte consome. É isso que permite
   escrever aqui mesmo com o WhatsApp Web fechado.

   Fase 1: lista com filtros, thread e indicador de ponte.
   Fase 4: mídia nos dois sentidos.
   Fases 5 a 8: tags, vínculo com cliente, biblioteca de mensagens
   prontas, negociação e analytics — em abas internas, porque são
   periféricos ao fluxo do dia, que é atender a conversa.
   ================================================================ */
(function () {
  const { useState, useEffect, useMemo, useRef } = React;
  const { LucideIcon, Btn, Badge, TopBar, IniciaisAvatar, useEscapeToClose, CardKPI, fmtBRL } = window;

  /* As cinco etapas fixas que moravam aqui viraram etiquetas comuns,
     guardadas no banco e editáveis pelo usuário. O que sobrou com o
     nome "etapa" na tela é a etapa do FUNIL, que são as colunas de
     negociação. Ver BotaoTagsEtapa. */

  const STATUS_NEGOCIACAO = [
    { id: 'pendente', label: 'Em aberto', cor: '#eaaa41' },
    { id: 'ganha',    label: 'Fechada',   cor: '#4ade80' },
    { id: 'perdida',  label: 'Perdida',   cor: '#f87171' },
  ];
  const STATUS_POR_ID = Object.fromEntries(STATUS_NEGOCIACAO.map(s => [s.id, s]));

  const TIPOS_MENSAGEM = [
    { id: 'texto',      label: 'Texto',      icon: 'message-square' },
    { id: 'imagem',     label: 'Imagem',     icon: 'image' },
    { id: 'video',      label: 'Vídeo',      icon: 'video' },
    { id: 'audio',      label: 'Áudio',      icon: 'mic' },
    { id: 'documento',  label: 'Documento',  icon: 'file' },
    { id: 'pix',        label: 'Pix',        icon: 'qr-code' },
    { id: 'link',       label: 'Link com banner', icon: 'link' },
  ];

  const CORES_COLUNA = ['#60a5fa', '#eaaa41', '#a78bfa', '#f472b6', '#4ade80',
    'var(--clr-neg)', '#fb923c', '#34d399', '#94a3b8'];

  /* Biblioteca NÃO é aba: é painel lateral que abre ao lado da
     conversa. O ponto dela é mandar uma mensagem pronta sem sair de
     onde você está — virar aba obrigaria a ir e voltar, que é
     exatamente o atrito que ela existe pra remover. */
  const ABAS = [
    { id: 'caixa',       label: 'Caixa de entrada', icon: 'inbox' },
    { id: 'negociacoes', label: 'Negociações',      icon: 'handshake' },
    { id: 'analytics',   label: 'Relatórios',       icon: 'bar-chart-3' },
  ];

  const LARGURA_BIBLIOTECA_PADRAO = 340;

  // A ponte pinga a cada 30s. 90s dá folga pra uma batida perdida sem
  // acusar offline à toa, e ainda avisa rápido quando cai de verdade.
  const LIMITE_PONTE_OFFLINE_MS = 90000;
  const INTERVALO_RECARGA_MS = 15000;

  /* Variáveis de mensagem pronta. Mesma ideia do MQV: o texto guarda
     [[nome]] e a resolução acontece no envio, com o contato em mãos.
     Variável desconhecida fica intacta em vez de virar "undefined" —
     melhor o atendente ver o defeito do que o cliente. */
  const VARIAVEIS = [
    { chave: 'nome',        descricao: 'Primeiro nome do contato' },
    { chave: 'nomecompleto',descricao: 'Nome completo do contato' },
    { chave: 'saudacao',    descricao: 'Bom dia / Boa tarde / Boa noite' },
  ];

  function saudacaoAgora() {
    const h = new Date().getHours();
    if (h < 12) return 'Bom dia';
    if (h < 18) return 'Boa tarde';
    return 'Boa noite';
  }

  function resolverVariaveis(texto, contato) {
    if (!texto) return '';
    const valores = {
      nome: (contato?.nome || '').split(' ')[0] || '',
      nomecompleto: contato?.nome || '',
      saudacao: saudacaoAgora(),
    };
    return texto.replace(/\[\[([a-zA-Z0-9_]+)\]\]/g, (inteiro, chave) => {
      const v = valores[chave];
      return (v === undefined || v === '') ? inteiro : v;
    });
  }

  /* Converte um Modelo (mensagem pronta) no formato que a fila de
     envio entende. Pix vira texto formatado (o WhatsApp não tem
     bolha nativa de pix); link com banner vira imagem com o link na
     legenda. Usado tanto pra "usar mensagem" quanto pra cada passo
     de um Script. */
  function montarPassoEnvio(m, contato) {
    if (m.tipo === 'pix') {
      const linhas = [];
      if (m.conteudo) linhas.push(resolverVariaveis(m.conteudo, contato));
      linhas.push('');
      linhas.push(`Chave Pix: ${m.pix_chave || ''}`);
      if (m.pix_titular) linhas.push(`Titular: ${m.pix_titular}`);
      if (m.pix_valor) {
        linhas.push(`Valor: ${(Number(m.pix_valor) || 0).toLocaleString('pt-BR',
          { style: 'currency', currency: 'BRL' })}`);
      }
      return { tipo: 'texto', corpo: linhas.join('\n').trim(), midiaUrl: null };
    }
    if (m.tipo === 'link') {
      const texto = `${resolverVariaveis(m.conteudo, contato)}\n${m.link_url || ''}`.trim();
      return m.link_banner_url
        ? { tipo: 'imagem', corpo: texto, midiaUrl: m.link_banner_url }
        : { tipo: 'texto', corpo: texto, midiaUrl: null };
    }
    if (m.tipo && m.tipo !== 'texto') {
      return { tipo: m.tipo, corpo: resolverVariaveis(m.conteudo, contato) || null,
        midiaUrl: m.midia_url || null };
    }
    return { tipo: 'texto', corpo: resolverVariaveis(m.conteudo, contato), midiaUrl: null };
  }

  /* Miniatura do que VOCÊ está mandando.

     Precisa ser gerada aqui porque o arquivo de saída é apagado da
     nuvem assim que o WhatsApp confirma a entrega: sem miniatura, a
     bolha enviada ficaria só com o nome do arquivo, e você não veria
     a foto que acabou de mandar.

     320px na aresta maior com JPEG a 60% dá uns 15 kB, mesma ordem da
     miniatura que o WhatsApp entrega. Aresta MAIOR, não a largura: com
     largura, uma foto em pé de 1000x4000 passaria sem redução. */
  async function gerarMiniatura(file) {
    if (!file?.type?.startsWith('image/')) return null;
    try {
      const bitmap = await createImageBitmap(file);
      const escala = Math.min(1, 320 / Math.max(bitmap.width, bitmap.height));
      const largura = Math.max(1, Math.round(bitmap.width * escala));
      const altura = Math.max(1, Math.round(bitmap.height * escala));
      const tela = document.createElement('canvas');
      tela.width = largura;
      tela.height = altura;
      tela.getContext('2d').drawImage(bitmap, 0, 0, largura, altura);
      bitmap.close?.();
      return tela.toDataURL('image/jpeg', 0.6);
    } catch (_) {
      // Ficar sem prévia é aceitável. Travar o envio por causa dela não.
      return null;
    }
  }

  /* ── Helpers ─────────────────────────────────────────────────*/
  function formatarTelefone(tel) {
    if (!tel) return '';
    const d = String(tel).replace(/\D/g, '');
    // 55 + DDD (2) + 9 dígitos
    if (d.length === 13 && d.startsWith('55')) {
      return `(${d.slice(2, 4)}) ${d.slice(4, 9)}-${d.slice(9)}`;
    }
    if (d.length === 12 && d.startsWith('55')) {
      return `(${d.slice(2, 4)}) ${d.slice(4, 8)}-${d.slice(8)}`;
    }
    return tel;
  }

  /* Como chamar a conversa na tela. Desde a migração do WhatsApp pro
     LID, contato pode não ter telefone conhecido — nesse caso mostrar
     o identificador interno seria pior que nada, então cai num rótulo
     honesto em vez de um número que não é número. */
  function rotuloContato(c) {
    if (!c) return '';
    if (c.nome) return c.nome;
    if (c.is_grupo) return 'Grupo sem nome';
    if (c.telefone) return formatarTelefone(c.telefone);
    return 'Contato sem número';
  }

  /* ── Avatar do contato ───────────────────────────────────────
     Diferente do IniciaisAvatar de shared.jsx: aquele é pra gente do
     TIME, com fotos fixas por nome (Felipe, Amanda…) — usar ele aqui
     faria um contato chamado "Felipe" ganhar a foto do Felipe de
     verdade. Este é local, mostra a foto de perfil que a ponte busca
     no WhatsApp, com as iniciais como fallback. */
  function IniciaisContato({ nome, cor, size, isGrupo }) {
    if (isGrupo) {
      return (
        <div style={{ width: size, height: size, borderRadius: '50%', flexShrink: 0,
          background: `${cor || '#94a3b8'}2a`, border: `1px solid ${cor || '#94a3b8'}55`,
          display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <LucideIcon icon="users" size={Math.max(10, Math.round(size * 0.5))}
            style={{ color: cor || '#94a3b8' }} />
        </div>
      );
    }
    if (!nome) {
      return (
        <div style={{ width: size, height: size, borderRadius: '50%', flexShrink: 0,
          background: 'rgba(148,163,184,.18)', border: '1px solid rgba(148,163,184,.35)',
          display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <LucideIcon icon="user" size={Math.max(10, Math.round(size * 0.5))}
            style={{ color: '#94a3b8' }} />
        </div>
      );
    }
    const iniciais = nome.trim().split(/\s+/).map(p => p[0]).slice(0, 2).join('').toUpperCase();
    return (
      <div style={{ width: size, height: size, borderRadius: '50%', flexShrink: 0,
        background: `${cor || '#94a3b8'}2a`, border: `1px solid ${cor || '#94a3b8'}55`,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        color: cor || '#94a3b8', fontFamily: 'Roboto, sans-serif', fontWeight: 800,
        fontSize: Math.max(9, Math.round(size * 0.4)), letterSpacing: '0.02em' }}>
        {iniciais}
      </div>
    );
  }

  function AvatarContato({ contato, cor, size = 36 }) {
    const [erro, setErro] = useState(false);
    if (contato?.avatar_url && !erro) {
      return (
        <img src={contato.avatar_url} alt="" title={rotuloContato(contato)}
          onError={() => setErro(true)}
          style={{ width: size, height: size, borderRadius: '50%', flexShrink: 0,
            objectFit: 'cover', border: `1.5px solid ${cor || '#94a3b8'}` }} />
      );
    }
    return <IniciaisContato nome={contato?.nome || null} cor={cor} size={size}
      isGrupo={!!contato?.is_grupo} />;
  }

  /* ── Valor em reais, com máscara ──────────────────────────────
     Guarda e devolve em REAIS (número), igual ao resto do app —
     só a exibição vira centavos-pra-moeda enquanto a pessoa digita,
     do jeito que qualquer app de banco brasileiro faz. */
  function formatarCentavos(centavos) {
    return ((Number(centavos) || 0) / 100).toLocaleString('pt-BR', {
      style: 'currency', currency: 'BRL',
    });
  }

  function CampoValorBRL({ valor, onChange, style }) {
    const centavos = Math.round((Number(valor) || 0) * 100);
    return (
      <input style={style} inputMode="numeric" value={formatarCentavos(centavos)}
        onChange={e => {
          const digitos = e.target.value.replace(/\D/g, '');
          onChange((digitos ? parseInt(digitos, 10) : 0) / 100);
        }} placeholder="R$ 0,00" />
    );
  }

  function horaCurta(iso) {
    if (!iso) return '';
    const d = new Date(iso);
    return d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
  }

  function diaRelativo(iso) {
    if (!iso) return '';
    const d = new Date(iso);
    const hoje = new Date();
    const ontem = new Date(); ontem.setDate(ontem.getDate() - 1);
    const mesmoDia = (a, b) => a.toDateString() === b.toDateString();
    if (mesmoDia(d, hoje)) return horaCurta(iso);
    if (mesmoDia(d, ontem)) return 'Ontem';
    return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
  }

  function dataCompleta(iso) {
    if (!iso) return '';
    return new Date(iso).toLocaleDateString('pt-BR', {
      day: '2-digit', month: 'long', year: 'numeric',
    });
  }

  /* ── Indicador da ponte ──────────────────────────────────────*/
  function IndicadorPonte({ status }) {
    const online = status?.online;
    const detalhe = status?.detalhe || '';
    // A ponte conta no 'detalhe' o que está fazendo. Isso evita dois
    // problemas: tela parada sem explicação durante a varredura, e o
    // pior de todos, dizer "ativa" quando ela subiu mas travou.
    const falhou = online && detalhe.startsWith('falha:');
    const trabalhando = online && detalhe.startsWith('recuperando');
    const cor = (!online || falhou) ? 'var(--clr-neg)' : trabalhando ? 'var(--accent)' : 'var(--clr-pos)';

    /* Quanto tempo parada. Isso muda o diagnóstico: minutos parada é
       WhatsApp Web fechado agora; horas ou dias parada com a aba
       aberta é a extensão que caiu (normalmente login vencido), e aí
       dizer só "abra o WhatsApp Web" manda a pessoa pro lugar errado. */
    const paradaHaMs = status?.ultimo_ping
      ? Date.now() - new Date(status.ultimo_ping).getTime() : null;
    const horasParada = paradaHaMs != null ? paradaHaMs / 3600000 : null;
    const caiuFaz = horasParada == null ? null
      : horasParada >= 48 ? `${Math.round(horasParada / 24)} dias`
      : horasParada >= 1.5 ? `${Math.round(horasParada)} horas`
      : `${Math.max(1, Math.round(paradaHaMs / 60000))} min`;

    // Parada há mais de uma hora não é aba fechada por um instante.
    const provavelExtensao = !online && horasParada != null && horasParada >= 1;

    const texto = !online
      ? (provavelExtensao ? `Ponte parada faz ${caiuFaz}` : 'Ponte offline. Abra o WhatsApp Web')
      : falhou ? 'Ponte com problema'
      : trabalhando ? detalhe
      : 'Ponte ativa';

    const explicacao = falhou ? detalhe.replace(/^falha: /, '')
      : online ? 'A aba do WhatsApp Web está aberta e sincronizando.'
      : provavelExtensao
        ? `Sem sinal da extensão faz ${caiuFaz}. Se o WhatsApp Web está aberto, o mais comum é o login da extensão ter vencido: abra as opções dela e entre de novo. Um "!" vermelho no ícone confirma isso.`
        : 'Sem a aba do WhatsApp Web aberta, nada entra e nada sai. O que você escrever fica na fila.';

    return (
      <div title={explicacao}
        style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 10px',
          borderRadius: 'var(--r-full)', background: `${cor}14`, border: `1px solid ${cor}33`,
          fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', fontWeight: 700, color: cor,
          whiteSpace: 'nowrap' }}>
        <span style={{ width: 7, height: 7, borderRadius: '50%', background: cor, flexShrink: 0 }} />
        {texto}
      </div>
    );
  }

  /* ── Botão de tags + etapa, no cabeçalho da conversa ──────────
     As duas coisas moraram numa faixa própria embaixo do nome; virou
     um popover só, disparado por um botão na mesma linha do nome —
     tira a faixa fixa e some por padrão, só aparece quando pedida. */
  /* Paleta usada tanto por etiqueta quanto por etapa do funil: as duas
     coisas aparecem lado a lado na tela, então compartilhar a paleta é
     o que evita um verde aqui brigar com outro verde ali. */
  function PaletaCores({ valor, onEscolher }) {
    return (
      <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
        {CORES_COLUNA.map(cor => (
          <button key={cor} onClick={() => onEscolher(cor)} title={cor}
            style={{ width: 20, height: 20, borderRadius: '50%', background: cor,
              cursor: 'pointer', padding: 0,
              border: valor === cor ? '2px solid #fff' : '1px solid rgba(255,255,255,.2)',
              boxShadow: valor === cor ? `0 0 0 2px ${cor}` : 'none' }} />
        ))}
      </div>
    );
  }

  /* ── Etiquetas e etapa do funil ──────────────────────────────
     As cinco etapas fixas que existiam aqui (Lead novo, Em conversa,
     Negociando, Cliente, Perdido) viraram etiquetas comuns: mesma
     coisa que qualquer outra etiqueta, criável e colorível por você.
     Ter dois sistemas de rotulagem lado a lado era só confusão, e o
     fixo obrigava mexer no código pra acrescentar um estado novo.

     No lugar de "Etapa" entra a etapa do FUNIL, que é onde a
     negociação está (Proposta enviada, Follow-up 01...). Essa é a
     mesma lista do kanban, então mover por aqui e arrastar lá são a
     mesma ação. */
  function BotaoTagsEtapa({ contato, tags, tagsAtivas, onAlternarTag,
                            colunas, negociacao, onMoverColuna,
                            onCriarTag, onMudarCorTag }) {
    const [aberto, setAberto] = useState(false);
    const [criando, setCriando] = useState(false);
    const [nomeNovo, setNomeNovo] = useState('');
    const [corNova, setCorNova] = useState(CORES_COLUNA[0]);
    const [editandoCor, setEditandoCor] = useState(null);
    const ref = useRef(null);

    useEffect(() => {
      if (!aberto) return;
      function fechar(ev) { if (ref.current && !ref.current.contains(ev.target)) setAberto(false); }
      document.addEventListener('mousedown', fechar);
      return () => document.removeEventListener('mousedown', fechar);
    }, [aberto]);

    async function salvarNova() {
      const nome = nomeNovo.trim();
      if (!nome) return;
      await onCriarTag(nome, corNova);
      setNomeNovo('');
      setCriando(false);
    }

    return (
      <div ref={ref} style={{ position: 'relative', flexShrink: 0 }}>
        <button onClick={() => setAberto(v => !v)}
          title="Etiquetas e etapa do funil"
          style={{ padding: '4px 10px', borderRadius: 'var(--r-sm)', fontSize: 'var(--fs-sm)',
            fontFamily: 'Roboto,sans-serif', fontWeight: 700, cursor: 'pointer',
            display: 'flex', alignItems: 'center', gap: 4,
            background: aberto ? 'rgba(234,170,65,.15)' : 'var(--overlay-05)',
            border: `1px solid ${aberto ? 'rgba(234,170,65,.4)' : 'var(--overlay-10)'}`,
            color: aberto ? 'var(--accent)' : 'var(--text-2)' }}>
          <LucideIcon icon="tags" size={13} />
          {tagsAtivas.length > 0 ? `${tagsAtivas.length} etiqueta${tagsAtivas.length > 1 ? 's' : ''}` : 'Etapa'}
        </button>
        {aberto && (
          <div style={{ position: 'absolute', top: 'calc(100% + 6px)', right: 0, width: 250, zIndex: 30,
            background: 'var(--app-surface)', border: '1px solid var(--app-border)', borderRadius: 'var(--r-md)',
            padding: 12, boxShadow: '0 10px 28px rgba(0,0,0,.4)', display: 'flex',
            flexDirection: 'column', gap: 10 }}>
            <div>
              <div style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif', fontWeight: 900,
                letterSpacing: '0.08em', textTransform: 'uppercase', color: 'var(--text-3)',
                marginBottom: 6 }}>Etapa do funil</div>
              {negociacao ? (
                <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
                  {colunas.map(c => (
                    <button key={c.id} onClick={() => onMoverColuna(negociacao.id, c.id)}
                      style={{ padding: '4px 8px', borderRadius: 'var(--r-full)', fontSize: 'var(--fs-xs)', cursor: 'pointer',
                        fontFamily: 'Roboto,sans-serif', fontWeight: 700,
                        background: negociacao.coluna_id === c.id ? `${c.cor}22` : 'var(--app-surface-2)',
                        border: `1px solid ${negociacao.coluna_id === c.id ? `${c.cor}66` : 'var(--app-border)'}`,
                        color: negociacao.coluna_id === c.id ? c.cor : 'var(--text-2)' }}>
                      {c.nome}
                    </button>
                  ))}
                </div>
              ) : (
                /* Sem negociação aberta não existe etapa: a etapa É a
                   posição da negociação no funil. Dizer isso é melhor
                   que mostrar botões que não fariam nada. */
                <span style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)' }}>
                  Aparece depois que você abrir uma negociação.
                </span>
              )}
            </div>
            <div>
              <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
                <div style={{ flex: 1, fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif', fontWeight: 900,
                  letterSpacing: '0.08em', textTransform: 'uppercase',
                  color: 'var(--text-3)' }}>Etiquetas</div>
                <button onClick={() => { setCriando(v => !v); setEditandoCor(null); }}
                  title="Nova etiqueta"
                  style={{ background: 'none', border: 'none', cursor: 'pointer',
                    color: 'var(--text-3)', display: 'flex' }}>
                  <LucideIcon icon={criando ? 'x' : 'plus'} size={13} />
                </button>
              </div>

              {criando && (
                <div style={{ marginBottom: 8, padding: 8, borderRadius: 'var(--r-sm)',
                  background: 'var(--app-surface-2)', border: '1px solid var(--app-border)' }}>
                  <input autoFocus value={nomeNovo} placeholder="Nome da etiqueta"
                    onChange={e => setNomeNovo(e.target.value)}
                    onKeyDown={e => { if (e.key === 'Enter') salvarNova(); }}
                    style={{ ...estiloCampo, marginBottom: 7 }} />
                  <PaletaCores valor={corNova} onEscolher={setCorNova} />
                  <button onClick={salvarNova} disabled={!nomeNovo.trim()}
                    style={{ marginTop: 8, width: '100%', padding: '6px 0', borderRadius: 'var(--r-sm)',
                      cursor: nomeNovo.trim() ? 'pointer' : 'not-allowed', fontWeight: 700,
                      fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif',
                      background: 'rgba(234,170,65,.15)', border: '1px solid rgba(234,170,65,.4)',
                      color: 'var(--accent)', opacity: nomeNovo.trim() ? 1 : 0.5 }}>
                    Criar etiqueta
                  </button>
                </div>
              )}

              <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
                {tags.length === 0 ? (
                  <span style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)' }}>
                    Nenhuma etiqueta criada ainda.
                  </span>
                ) : tags.map(t => {
                  const ativa = tagsAtivas.includes(t.id);
                  return (
                    <span key={t.id} style={{ display: 'inline-flex', alignItems: 'center',
                      borderRadius: 'var(--r-full)', background: ativa ? `${t.cor}22` : 'var(--app-surface-2)',
                      border: `1px solid ${ativa ? `${t.cor}66` : 'var(--app-border)'}` }}>
                      <button onClick={() => onAlternarTag(t.id)}
                        style={{ padding: '4px 4px 4px 10px', background: 'none', border: 'none',
                          cursor: 'pointer', fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif',
                          fontWeight: 700, color: ativa ? t.cor : 'var(--text-2)' }}>
                        {t.nome}
                      </button>
                      {/* A bolinha muda a cor da etiqueta em si, em todo
                          lugar. Fica separada do nome pra clicar na
                          etiqueta continuar sendo aplicar, não editar. */}
                      <button onClick={() => setEditandoCor(v => v === t.id ? null : t.id)}
                        title="Mudar a cor desta etiqueta"
                        style={{ padding: '4px 8px 4px 4px', background: 'none', border: 'none',
                          cursor: 'pointer', display: 'flex', alignItems: 'center' }}>
                        <span style={{ width: 9, height: 9, borderRadius: '50%', background: t.cor,
                          border: '1px solid rgba(255,255,255,.25)' }} />
                      </button>
                    </span>
                  );
                })}
              </div>

              {editandoCor && (
                <div style={{ marginTop: 8, padding: 8, borderRadius: 'var(--r-sm)',
                  background: 'var(--app-surface-2)', border: '1px solid var(--app-border)' }}>
                  <div style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)',
                    marginBottom: 6 }}>
                    Cor de “{tags.find(t => t.id === editandoCor)?.nome}”
                  </div>
                  <PaletaCores valor={tags.find(t => t.id === editandoCor)?.cor}
                    onEscolher={cor => { onMudarCorTag(editandoCor, cor); setEditandoCor(null); }} />
                </div>
              )}
            </div>
          </div>
        )}
      </div>
    );
  }

  /* ── Item da lista de conversas ──────────────────────────────*/
  // Arquivar mora só no cabeçalho da conversa, não aqui na lista: um
  // botão que aparece ao passar o mouse sobre cada linha vira clique
  // acidental no meio de uma lista que se percorre rápido.
  function ItemConversa({ contato, ativo, onClick, etiqueta }) {
    const [hov, setHov] = useState(false);
    // A cor da linha vem da primeira etiqueta do contato. Antes vinha
    // da etapa fixa, que deixou de existir.
    const etapa = etiqueta || null;
    const naoLidas = contato.nao_lidas || 0;
    return (
      <div onClick={onClick}
        onMouseEnter={() => setHov(true)} onMouseLeave={() => setHov(false)}
        style={{ display: 'flex', gap: 10, padding: '10px 12px', cursor: 'pointer',
          borderRadius: 'var(--r-md)', alignItems: 'center',
          background: ativo ? 'rgba(234,170,65,.12)' : hov ? 'var(--app-surface-3)' : 'transparent',
          border: `1px solid ${ativo ? 'rgba(234,170,65,.35)' : 'transparent'}`,
          transition: 'background 130ms' }}>
        <AvatarContato contato={contato} cor={etapa?.cor} size={36} />
        <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 4 }}>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
            <span style={{ fontSize: 'var(--fs-lg)', fontFamily: 'Roboto,sans-serif', fontWeight: 700,
              color: 'var(--text-1)', whiteSpace: 'nowrap', overflow: 'hidden',
              textOverflow: 'ellipsis', flex: 1 }}>
              {rotuloContato(contato)}
            </span>
            <span style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif',
              color: 'var(--text-3)', flexShrink: 0 }}>
              {diaRelativo(contato.ultima_msg_em)}
            </span>
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            <span style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)',
              whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', flex: 1 }}>
              {/* "Você:" na frente quando a última foi nossa, igual ao
                  WhatsApp. Sem isso, olhando a lista não dá pra saber se
                  a bola está com você ou com o cliente — que é a única
                  pergunta que importa numa lista de atendimento. */}
              {contato.previa_direcao === 'saida' && (
                <span style={{ color: 'var(--text-2)' }}>Você: </span>
              )}
              {contato.previa || (contato.telefone ? formatarTelefone(contato.telefone) : '')}
            </span>
            {etapa && (
              <span style={{ width: 7, height: 7, borderRadius: '50%',
                background: etapa.cor, flexShrink: 0 }} title={etapa.nome} />
            )}
            {naoLidas > 0 && (
              <span style={{ minWidth: 18, height: 18, borderRadius: 'var(--r-full)', background: 'var(--clr-pos)',
                color: '#0f1013', fontSize: 'var(--fs-xs)', fontWeight: 900, display: 'flex',
                alignItems: 'center', justifyContent: 'center', padding: '0 5px', flexShrink: 0 }}>
                {naoLidas}
              </span>
            )}
          </div>
        </div>
      </div>
    );
  }

  /* ── Anexo ───────────────────────────────────────────────────
     O armazenamento é privado (é foto e documento de cliente real),
     então a tela não tem uma URL fixa: pede uma URL assinada de curta
     duração na hora de mostrar. */
  const TIPOS_COM_ARQUIVO = new Set(['imagem', 'audio', 'video', 'documento', 'figurinha']);

  /* Visualizador de imagem. Abrir em aba nova tira a pessoa da
     conversa; aqui a imagem cresce por cima e o Esc devolve o lugar
     de onde ela saiu — mesmo comportamento do WhatsApp Web. */
  function Lightbox({ url, onClose }) {
    useEscapeToClose(onClose);
    if (!url) return null;
    return (
      <div onClick={onClose}
        style={{ position: 'fixed', inset: 0, zIndex: 1200, background: 'rgba(0,0,0,.85)',
          display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 32,
          cursor: 'zoom-out' }}>
        <button onClick={onClose} title="Fechar (Esc)"
          style={{ position: 'absolute', top: 18, right: 20, width: 34, height: 34,
            borderRadius: 'var(--r-sm)', background: 'rgba(255,255,255,.1)', border: 'none',
            color: '#fff', cursor: 'pointer', display: 'flex', alignItems: 'center',
            justifyContent: 'center' }}>
          <LucideIcon icon="x" size={18} />
        </button>
        <a href={url} target="_blank" rel="noreferrer" download
          onClick={e => e.stopPropagation()} title="Baixar"
          style={{ position: 'absolute', top: 18, right: 62, width: 34, height: 34,
            borderRadius: 'var(--r-sm)', background: 'rgba(255,255,255,.1)',
            color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <LucideIcon icon="download" size={17} />
        </a>
        <img src={url} alt="" onClick={e => e.stopPropagation()}
          style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain',
            borderRadius: 'var(--r-sm)', cursor: 'default' }} />
      </div>
    );
  }

  const ROTULO_MIDIA = { imagem: 'Foto', figurinha: 'Figurinha', audio: 'Áudio',
    video: 'Vídeo', documento: 'Documento' };
  const ACAO_MIDIA = { imagem: 'Ver foto', figurinha: 'Ver figurinha', audio: 'Ouvir',
    video: 'Assistir', documento: 'Baixar' };
  const ICONE_MIDIA = { imagem: 'image', figurinha: 'sticker', audio: 'mic',
    video: 'video', documento: 'file' };

  /* ── Anexo ───────────────────────────────────────────────────
     O arquivo não está na nuvem, e isso é a escolha certa: quem guarda
     mídia de WhatsApp é o WhatsApp. O que vem do banco é a miniatura
     que ele entrega junto da mensagem; o arquivo em tamanho real é
     buscado na hora em que você abre, pela extensão, e fica guardado
     neste navegador pra não ser buscado de novo.

     Três estados de falha, tratados separado porque a saída é
     diferente em cada um: extensão não instalada (instalar), WhatsApp
     Web fechado (abrir a aba) e arquivo que não veio (tentar de novo).
     Um "não consegui abrir o arquivo" genérico faria você mexer na
     coisa errada. */
  function Anexo({ msg, onAbrirImagem }) {
    const [url, setUrl] = useState(null);
    const [carregando, setCarregando] = useState(false);
    const [erro, setErro] = useState(null);
    // Áudio só toca sozinho quando FOI VOCÊ que clicou. Sem esta
    // distinção, abrir uma conversa com cinco áudios já ouvidos
    // dispararia os cinco ao mesmo tempo, porque eles vêm do cache
    // sem passar por clique nenhum.
    const pediu = useRef(false);

    const ehImagem = msg.tipo === 'imagem' || msg.tipo === 'figurinha';
    const legado = !!msg.midia_url;

    // Arquivo antigo, da época em que toda mídia subia pra nuvem.
    // Continua sendo servido de lá enquanto existir. Mídia nova não
    // passa mais por aqui.
    useEffect(() => {
      let vivo = true;
      if (!legado) return;
      window.db.storage.from('whatsapp-midia')
        .createSignedUrl(msg.midia_url, 3600)
        .then(({ data, error }) => {
          if (!vivo) return;
          if (error || !data?.signedUrl) return setErro('arquivo antigo não está mais lá');
          setUrl(data.signedUrl);
        });
      return () => { vivo = false; };
    }, [msg.midia_url]);

    async function carregar(porClique) {
      if (carregando || url) return;
      if (porClique) pediu.current = true;
      setCarregando(true);
      setErro(null);
      const r = await window.khronusMidia.obter(msg.wa_message_id);
      setCarregando(false);
      if (r.erro) return setErro(r.erro);
      setUrl(r.url);
    }

    // Já buscado antes nesta máquina: aparece sozinho, sem pedir
    // clique. Não dispara busca nenhuma, só olha o que já está aqui.
    useEffect(() => {
      let vivo = true;
      if (legado || !msg.wa_message_id) return;
      window.khronusMidia?.jaTenho(msg.wa_message_id).then((tem) => {
        if (vivo && tem) carregar();
      });
      return () => { vivo = false; };
    }, [msg.wa_message_id]);

    /* ── com o arquivo em mãos ── */
    if (url) {
      if (ehImagem) {
        return (
          <img src={url} alt={msg.tipo}
            onClick={() => onAbrirImagem?.(url)}
            style={{ maxWidth: '100%', maxHeight: 320, borderRadius: 'var(--r-sm)', display: 'block',
              cursor: 'zoom-in', marginBottom: msg.corpo ? 6 : 0 }} />
        );
      }
      if (msg.tipo === 'audio') {
        return <audio controls autoPlay={pediu.current} src={url}
          style={{ width: 260, marginBottom: msg.corpo ? 6 : 0 }} />;
      }
      if (msg.tipo === 'video') {
        return (
          <video controls src={url}
            style={{ maxWidth: '100%', maxHeight: 320, borderRadius: 'var(--r-sm)', display: 'block',
              marginBottom: msg.corpo ? 6 : 0 }} />
        );
      }
      return (
        <a href={url} download={msg.midia_nome || undefined} target="_blank" rel="noreferrer"
          style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 10px',
            borderRadius: 'var(--r-sm)', background: 'var(--overlay-05)', border: '1px solid var(--app-border)',
            textDecoration: 'none', color: 'var(--text-1)', fontSize: 'var(--fs-md)',
            fontFamily: 'Roboto,sans-serif', marginBottom: msg.corpo ? 6 : 0 }}>
          <LucideIcon icon="file" size={14} />
          {msg.midia_nome || 'Abrir documento'}
        </a>
      );
    }

    const aviso = erro === 'extensao-ausente'
      ? 'Precisa da extensão do Khronus instalada'
      : erro === 'WhatsApp Web fechado'
        ? 'Abra o WhatsApp Web para ver'
        : erro;

    /* ── miniatura: prévia com um toque pra carregar ── */
    if (msg.midia_thumb && (ehImagem || msg.tipo === 'video')) {
      return (
        <div onClick={() => carregar(true)}
          style={{ position: 'relative', display: 'inline-block', cursor: 'pointer',
            marginBottom: msg.corpo ? 6 : 0, maxWidth: '100%' }}>
          <img src={msg.midia_thumb} alt={ROTULO_MIDIA[msg.tipo] || msg.tipo}
            style={{ maxWidth: '100%', maxHeight: 280, borderRadius: 'var(--r-sm)', display: 'block',
              // Escurecida de propósito: é o que faz o rótulo em cima
              // dela ficar legível em foto clara.
              filter: carregando ? 'brightness(.55)' : 'brightness(.82)' }} />
          <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center',
            justifyContent: 'center' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 6,
              padding: '6px 12px', borderRadius: 20, background: 'rgba(0,0,0,.62)',
              color: '#fff', fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', fontWeight: 600 }}>
              <LucideIcon icon={carregando ? 'loader' : msg.tipo === 'video' ? 'play' : 'eye'}
                size={13} />
              {carregando ? 'Buscando…' : (ACAO_MIDIA[msg.tipo] || 'Abrir')}
            </div>
          </div>
          {aviso && (
            <div style={{ position: 'absolute', left: 0, right: 0, bottom: 0,
              padding: '4px 8px', background: 'rgba(180,40,40,.85)', color: '#fff',
              fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif',
              borderBottomLeftRadius: 8, borderBottomRightRadius: 8 }}>
              {aviso}
            </div>
          )}
        </div>
      );
    }

    /* ── sem miniatura: barra com o tipo e a ação ── */
    return (
      <div onClick={() => carregar(true)}
        style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px',
          borderRadius: 'var(--r-sm)', background: 'var(--overlay-05)',
          border: `1px solid ${aviso ? 'rgba(248,113,113,.4)' : 'var(--app-border)'}`,
          cursor: 'pointer', marginBottom: msg.corpo ? 6 : 0 }}>
        <LucideIcon icon={carregando ? 'loader' : (ICONE_MIDIA[msg.tipo] || 'file')} size={15} />
        <div style={{ minWidth: 0, flex: 1 }}>
          <div style={{ fontSize: 'var(--fs-md)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-1)',
            overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
            {msg.midia_nome || ROTULO_MIDIA[msg.tipo] || msg.tipo}
          </div>
          <div style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif',
            color: aviso ? 'var(--clr-neg)' : 'var(--text-3)' }}>
            {carregando ? 'buscando no WhatsApp…' : (aviso || ACAO_MIDIA[msg.tipo] || 'Abrir')}
          </div>
        </div>
      </div>
    );
  }

  /* ── Seletor de emoji ────────────────────────────────────────
     Lista curada em vez de biblioteca: o app não tem empacotador, e
     puxar um pacote de emoji de CDN só pra isto custaria mais que o
     benefício. São os que aparecem de verdade em conversa de trabalho,
     agrupados como no WhatsApp, com os recentes no topo — porque na
     prática quase todo mundo usa sempre os mesmos cinco. */
  const EMOJIS = {
    'Rostos': ['😀','😃','😄','😁','😊','🙂','😉','😍','🥰','😘','😗','🤗','🤔','😐','😑','🙄',
               '😏','😴','😪','😌','🤤','😢','😭','😤','😡','🥺','😳','🤯','😬','🙃','😅','😂',
               '🤣','😇','🥳','😎','🤓','🫡','🤝'],
    'Gestos': ['👍','👎','👌','🤌','✌️','🤞','🫶','👏','🙌','🙏','💪','👋','🤙','☝️','👇','👈','👉'],
    'Coração': ['❤️','🧡','💛','💚','💙','💜','🖤','🤍','💔','❣️','💕','💞','💓','💗','💖','💘'],
    'Trabalho': ['📸','📷','🎥','🎬','💡','📝','📋','📅','⏰','📞','📱','💻','📧','📎','📁','💰',
                 '💵','💳','🧾','✅','❌','⚠️','🔥','⭐','✨','🎯','🚀','📈','🤖'],
    'Festa': ['🎉','🎊','🥂','🍾','🎂','🎁','💐','🌹','🌻','🎈','👰','🤵','💍','👶','🐶','🐱'],
  };

  function SeletorEmoji({ onEscolher }) {
    const [aberto, setAberto] = useState(false);
    const [grupo, setGrupo] = useState('Rostos');
    const [recentes, setRecentes] = useState(() => {
      try { return JSON.parse(localStorage.getItem('khronus.emoji.recentes') || '[]'); }
      catch (_) { return []; }
    });
    const ref = useRef(null);

    useEffect(() => {
      if (!aberto) return;
      function fechar(ev) { if (ref.current && !ref.current.contains(ev.target)) setAberto(false); }
      document.addEventListener('mousedown', fechar);
      return () => document.removeEventListener('mousedown', fechar);
    }, [aberto]);

    function escolher(e) {
      onEscolher(e);
      setRecentes(prev => {
        const novo = [e, ...prev.filter(x => x !== e)].slice(0, 24);
        try { localStorage.setItem('khronus.emoji.recentes', JSON.stringify(novo)); } catch (_) {}
        return novo;
      });
      // Não fecha: mandar três emojis seguidos é comum, e reabrir a
      // cada um seria uma briga com a ferramenta.
    }

    const lista = grupo === 'Recentes' ? recentes : (EMOJIS[grupo] || []);
    const abas = [...(recentes.length ? ['Recentes'] : []), ...Object.keys(EMOJIS)];

    return (
      <div ref={ref} style={{ position: 'relative', flexShrink: 0 }}>
        <button onClick={() => setAberto(v => !v)} title="Emoji"
          style={{ height: 40, width: 40, borderRadius: 'var(--r-md)',
            background: aberto ? 'var(--accent-soft)' : 'var(--app-surface-2)',
            border: `1px solid ${aberto ? 'var(--accent-line)' : 'var(--app-border)'}`,
            color: aberto ? 'var(--accent)' : 'var(--text-2)', cursor: 'pointer',
            display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <LucideIcon icon="smile" size={16} />
        </button>
        {aberto && (
          <div style={{ position: 'absolute', bottom: 'calc(100% + 6px)', right: 0, width: 300,
            zIndex: 40, background: 'var(--app-surface)', border: '1px solid var(--app-border)',
            borderRadius: 'var(--r-lg)', boxShadow: '0 10px 28px rgba(0,0,0,.45)',
            padding: 8 }}>
            <div style={{ display: 'flex', gap: 4, marginBottom: 8, overflowX: 'auto' }}>
              {abas.map(a => (
                <button key={a} onClick={() => setGrupo(a)}
                  style={{ padding: '4px 9px', borderRadius: 'var(--r-sm)', cursor: 'pointer',
                    fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif', fontWeight: 600,
                    whiteSpace: 'nowrap',
                    background: grupo === a ? 'var(--accent-soft)' : 'transparent',
                    border: `1px solid ${grupo === a ? 'var(--accent-line)' : 'transparent'}`,
                    color: grupo === a ? 'var(--accent)' : 'var(--text-3)' }}>
                  {a}
                </button>
              ))}
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(8, 1fr)', gap: 2,
              maxHeight: 200, overflowY: 'auto' }}>
              {lista.map((e, i) => (
                <button key={e + i} onClick={() => escolher(e)}
                  style={{ fontSize: 20, lineHeight: '32px', height: 32, borderRadius: 'var(--r-sm)',
                    background: 'transparent', border: 'none', cursor: 'pointer' }}>
                  {e}
                </button>
              ))}
            </div>
          </div>
        )}
      </div>
    );
  }

  /* ── Bolha de mensagem ───────────────────────────────────────*/
  function Bolha({ msg, onAbrirImagem }) {
    const daGente = msg.direcao === 'saida';
    const falhou = msg.status === 'falhou';
    const naFila = msg._pendente;
    const temAnexo = TIPOS_COM_ARQUIVO.has(msg.tipo);
    return (
      <div style={{ display: 'flex', justifyContent: daGente ? 'flex-end' : 'flex-start' }}>
        <div style={{ maxWidth: '72%', padding: '8px 12px', borderRadius: 'var(--r-lg)',
          borderBottomRightRadius: daGente ? 3 : 12, borderBottomLeftRadius: daGente ? 12 : 3,
          background: daGente ? 'rgba(234,170,65,.14)' : 'var(--app-surface-2)',
          border: `1px solid ${falhou ? 'rgba(248,113,113,.4)' : daGente ? 'rgba(234,170,65,.22)' : 'var(--app-border)'}`,
          opacity: naFila ? 0.6 : 1 }}>
          {temAnexo && !naFila && <Anexo msg={msg} onAbrirImagem={onAbrirImagem} />}
          {(msg.corpo || !temAnexo) && (
            <div style={{ fontSize: 'var(--fs-lg)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-1)',
              lineHeight: 1.5, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
              {msg.corpo || <em style={{ color: 'var(--text-3)' }}>({msg.tipo})</em>}
            </div>
          )}
          <div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 4,
            justifyContent: 'flex-end' }}>
            {naFila && (
              <span style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif', color: 'var(--accent)',
                fontWeight: 700 }}>na fila</span>
            )}
            {falhou && (
              <span style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif', color: 'var(--clr-neg)',
                fontWeight: 700 }}>falhou</span>
            )}
            {/* Resposta dada pelo celular fica marcada, pra não parecer
                que saiu daqui — ajuda a entender o histórico depois. */}
            {daGente && msg.canal_envio === 'celular' && !naFila && (
              <span title="Enviada pelo celular"
                style={{ display: 'inline-flex' }}>
                <LucideIcon icon="smartphone" size={10} style={{ color: 'var(--text-3)' }} />
              </span>
            )}
            <span style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)' }}>
              {horaCurta(msg.created_at)}
            </span>
          </div>
        </div>
      </div>
    );
  }

  /* ── Campo de formulário ─────────────────────────────────────*/
  const estiloCampo = {
    width: '100%', boxSizing: 'border-box', padding: '8px 12px', borderRadius: 'var(--r-sm)',
    background: 'var(--app-surface-2)', border: '1px solid var(--app-border)',
    color: 'var(--text-1)', fontFamily: 'Roboto, sans-serif', fontSize: 'var(--fs-lg)', outline: 'none',
  };

  function Campo({ label, children, dica }) {
    return (
      <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
        <span style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto, sans-serif', fontWeight: 700,
          letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--text-3)' }}>{label}</span>
        {children}
        {dica && (
          <span style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)',
            lineHeight: 1.5 }}>{dica}</span>
        )}
      </div>
    );
  }

  function ModalBase({ titulo, children, onClose, rodape, largura = 480 }) {
    useEscapeToClose(onClose);
    return (
      <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,.55)', zIndex: 1000,
        display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }}
        onClick={onClose}>
        <div style={{ width: '100%', maxWidth: largura, background: 'var(--app-surface)',
          border: '1px solid var(--app-border)', borderRadius: 'var(--r-xl)', padding: 22,
          display: 'flex', flexDirection: 'column', gap: 12, maxHeight: '90vh', overflowY: 'auto' }}
          onClick={e => e.stopPropagation()}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
            <span style={{ fontFamily: 'Roboto, sans-serif', fontWeight: 900, fontSize: 'var(--fs-xl)',
              color: 'var(--text-1)' }}>{titulo}</span>
            <button onClick={onClose} style={{ background: 'none', border: 'none',
              cursor: 'pointer', color: 'var(--text-3)', display: 'flex' }}>
              <LucideIcon icon="x" size={18} />
            </button>
          </div>
          {children}
          {rodape && (
            <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, flexWrap: 'wrap' }}>
              {rodape}
            </div>
          )}
        </div>
      </div>
    );
  }

  /* ── Modal de mensagem pronta ────────────────────────────────*/
  function ModalMensagemPronta({ item, categorias, onClose, onSalvo }) {
    const [titulo, setTitulo] = useState(item?.titulo || '');
    const [conteudo, setConteudo] = useState(item?.conteudo || '');
    const [categoriaId, setCategoriaId] = useState(item?.categoria_id || '');
    const [tipo, setTipo] = useState(item?.tipo || 'texto');
    const [midiaUrl, setMidiaUrl] = useState(item?.midia_url || '');
    const [midiaNome, setMidiaNome] = useState(item?.midia_url ? item.midia_url.split('/').pop() : '');
    const [pixChave, setPixChave] = useState(item?.pix_chave || '');
    const [pixTitular, setPixTitular] = useState(item?.pix_titular || '');
    const [pixValor, setPixValor] = useState(item?.pix_valor != null ? Number(item.pix_valor) : 0);
    const [linkUrl, setLinkUrl] = useState(item?.link_url || '');
    const [linkBannerUrl, setLinkBannerUrl] = useState(item?.link_banner_url || '');
    const [enviarApos, setEnviarApos] = useState(item?.enviar_apos_segundos ?? 0);
    const [simular, setSimular] = useState(item?.simular_digitando ?? true);
    const [salvando, setSalvando] = useState(false);
    const [subindo, setSubindo] = useState(false);
    const areaRef = useRef(null);

    // Insere a variável onde o cursor está, não no fim: quem escreve
    // "Oi , tudo bem?" quer o nome no meio, não no rodapé.
    function inserir(chave) {
      const el = areaRef.current;
      const marca = `[[${chave}]]`;
      if (!el) return setConteudo(c => c + marca);
      const ini = el.selectionStart ?? conteudo.length;
      const fim = el.selectionEnd ?? conteudo.length;
      const novo = conteudo.slice(0, ini) + marca + conteudo.slice(fim);
      setConteudo(novo);
      requestAnimationFrame(() => {
        el.focus();
        el.setSelectionRange(ini + marca.length, ini + marca.length);
      });
    }

    // Mesmo bucket do que sai pela conversa, prefixo próprio: sobe na
    // hora, o modelo guarda só o caminho, igual a qualquer anexo daqui.
    async function subirArquivo(file, aplicar) {
      setSubindo(true);
      const seguro = file.name.replace(/[^a-zA-Z0-9._-]/g, '_');
      const caminho = `modelos/${Date.now()}_${seguro}`;
      const { error } = await window.db.storage.from('whatsapp-midia')
        .upload(caminho, file, { contentType: file.type || undefined, upsert: false });
      setSubindo(false);
      if (error) return alert('Não consegui subir o arquivo: ' + error.message);
      aplicar(caminho);
    }

    const precisaMidia = ['imagem', 'video', 'audio', 'documento'].includes(tipo);

    async function salvar() {
      if (!titulo.trim()) return;
      if (tipo === 'texto' && !conteudo.trim()) return;
      setSalvando(true);
      const dados = {
        titulo: titulo.trim(),
        conteudo: conteudo.trim() || null,
        categoria_id: categoriaId || null,
        tipo,
        midia_url: precisaMidia ? (midiaUrl || null) : null,
        pix_chave: tipo === 'pix' ? (pixChave.trim() || null) : null,
        pix_titular: tipo === 'pix' ? (pixTitular.trim() || null) : null,
        pix_valor: tipo === 'pix' ? (Number(pixValor) || 0) : null,
        link_url: tipo === 'link' ? (linkUrl.trim() || null) : null,
        link_banner_url: tipo === 'link' ? (linkBannerUrl || null) : null,
        enviar_apos_segundos: Number(enviarApos) || 0,
        simular_digitando: !!simular,
        updated_at: new Date().toISOString(),
      };
      const { error } = item?.id
        ? await window.db.from('crm_whatsapp_mensagens_prontas').update(dados).eq('id', item.id)
        : await window.db.from('crm_whatsapp_mensagens_prontas').insert(dados);
      setSalvando(false);
      if (error) return alert('Não consegui salvar: ' + error.message);
      onSalvo();
    }

    async function excluir() {
      if (!confirm(`Excluir "${titulo}"?`)) return;
      await window.db.from('crm_whatsapp_mensagens_prontas').delete().eq('id', item.id);
      onSalvo();
    }

    return (
      <ModalBase titulo={item?.id ? 'Editar modelo' : 'Novo modelo'} onClose={onClose} largura={520}
        rodape={<>
          {item?.id && <Btn variant="danger" icon="trash-2" onClick={excluir}>Excluir</Btn>}
          <Btn variant="ghost" onClick={onClose}>Cancelar</Btn>
          <Btn variant="primary" onClick={salvar}
            disabled={salvando || !titulo.trim() || (tipo === 'texto' && !conteudo.trim())}>
            {salvando ? 'Salvando…' : 'Salvar'}
          </Btn>
        </>}>
        <Campo label="Título">
          <input style={estiloCampo} value={titulo} onChange={e => setTitulo(e.target.value)}
            placeholder="Ex.: Resposta ao primeiro contato" />
        </Campo>

        <Campo label="Tipo de mensagem">
          <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
            {TIPOS_MENSAGEM.map(t => (
              <button key={t.id} onClick={() => setTipo(t.id)}
                style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 10px',
                  borderRadius: 'var(--r-sm)', cursor: 'pointer', fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif',
                  fontWeight: 700,
                  background: tipo === t.id ? 'rgba(234,170,65,.15)' : 'var(--app-surface-2)',
                  border: `1px solid ${tipo === t.id ? 'rgba(234,170,65,.4)' : 'var(--app-border)'}`,
                  color: tipo === t.id ? 'var(--accent)' : 'var(--text-2)' }}>
                <LucideIcon icon={t.icon} size={12} />{t.label}
              </button>
            ))}
          </div>
        </Campo>

        <Campo label="Categoria">
          <select style={estiloCampo} value={categoriaId} onChange={e => setCategoriaId(e.target.value)}>
            <option value="">Sem categoria</option>
            {categorias.map(c => <option key={c.id} value={c.id}>{c.nome}</option>)}
          </select>
        </Campo>

        {precisaMidia && (
          <Campo label={TIPOS_MENSAGEM.find(t => t.id === tipo)?.label}>
            <input type="file"
              accept={tipo === 'imagem' ? 'image/*' : tipo === 'video' ? 'video/*'
                : tipo === 'audio' ? 'audio/*' : undefined}
              onChange={e => {
                const f = e.target.files?.[0]; e.target.value = '';
                if (f) { setMidiaNome(f.name); subirArquivo(f, setMidiaUrl); }
              }} />
            {subindo && <span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-3)' }}>Subindo…</span>}
            {!subindo && midiaUrl && (
              <span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-3)' }}>
                Arquivo: {midiaNome || midiaUrl.split('/').pop()}
              </span>
            )}
          </Campo>
        )}

        {tipo === 'pix' && (
          <>
            <Campo label="Chave Pix">
              <input style={estiloCampo} value={pixChave} onChange={e => setPixChave(e.target.value)}
                placeholder="CPF, e-mail, telefone ou chave aleatória" />
            </Campo>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
              <Campo label="Titular">
                <input style={estiloCampo} value={pixTitular} onChange={e => setPixTitular(e.target.value)} />
              </Campo>
              <Campo label="Valor (opcional)">
                <CampoValorBRL style={estiloCampo} valor={pixValor} onChange={setPixValor} />
              </Campo>
            </div>
          </>
        )}

        {tipo === 'link' && (
          <>
            <Campo label="URL do link">
              <input style={estiloCampo} value={linkUrl} onChange={e => setLinkUrl(e.target.value)}
                placeholder="https://…" />
            </Campo>
            <Campo label="Banner (imagem)">
              <input type="file" accept="image/*"
                onChange={e => {
                  const f = e.target.files?.[0]; e.target.value = '';
                  if (f) subirArquivo(f, setLinkBannerUrl);
                }} />
              {subindo && <span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-3)' }}>Subindo…</span>}
              {!subindo && linkBannerUrl && (
                <span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-3)' }}>Banner enviado</span>
              )}
            </Campo>
          </>
        )}

        <Campo label={tipo === 'pix' ? 'Texto de apoio (opcional)'
          : tipo === 'link' ? 'Texto que acompanha o link' : 'Mensagem'}>
          <textarea ref={areaRef} value={conteudo} onChange={e => setConteudo(e.target.value)}
            style={{ ...estiloCampo, minHeight: 100, resize: 'vertical', lineHeight: 1.5 }}
            placeholder="Oi [[nome]], [[saudacao]]! Que bom seu contato…" />
        </Campo>
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
          <span style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)' }}>
            Inserir:
          </span>
          {VARIAVEIS.map(v => (
            <button key={v.chave} onClick={() => inserir(v.chave)} title={v.descricao}
              style={{ padding: '4px 8px', borderRadius: 'var(--r-full)', fontSize: 'var(--fs-sm)',
                fontFamily: 'Roboto,sans-serif', fontWeight: 700, cursor: 'pointer',
                background: 'rgba(234,170,65,.12)', border: '1px solid rgba(234,170,65,.3)',
                color: 'var(--accent)' }}>
              [[{v.chave}]]
            </button>
          ))}
        </div>

        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
          <Campo label="Enviar após (segundos)"
            dica="Só conta dentro de um Script: tempo desde o passo anterior.">
            <input style={estiloCampo} type="number" min="0" value={enviarApos}
              onChange={e => setEnviarApos(e.target.value)} placeholder="0" />
          </Campo>
          <Campo label="Simulação">
            <button onClick={() => setSimular(v => !v)}
              style={{ ...estiloCampo, display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer',
                color: simular ? 'var(--accent)' : 'var(--text-2)' }}>
              <LucideIcon icon={simular ? 'toggle-right' : 'toggle-left'} size={18} />
              {simular ? (tipo === 'audio' ? 'Mostra "gravando áudio…"' : 'Mostra "digitando…"') : 'Sem simulação'}
            </button>
          </Campo>
        </div>
      </ModalBase>
    );
  }

  /* ── Modal de script ──────────────────────────────────────────
     Um script é só uma ORDEM de mensagens já cadastradas em Modelos.
     O atraso e a simulação de cada passo vêm da própria mensagem
     (é lá que eles foram definidos), não são redefinidos aqui — assim
     a mesma mensagem se comporta igual em qualquer script que a use. */
  function ModalScript({ item, categorias, prontas, onClose, onSalvo }) {
    const [nome, setNome] = useState(item?.nome || '');
    const [categoriaId, setCategoriaId] = useState(item?.categoria_id || '');
    const [passos, setPassos] = useState(() =>
      (item?.crm_whatsapp_script_mensagens || [])
        .slice().sort((a, b) => (a.ordem ?? 0) - (b.ordem ?? 0))
        .map(p => p.mensagem_id));
    const [salvando, setSalvando] = useState(false);

    function adicionarPasso(mensagemId) {
      if (!mensagemId) return;
      setPassos(prev => [...prev, mensagemId]);
    }
    function removerPasso(i) {
      setPassos(prev => prev.filter((_, idx) => idx !== i));
    }
    function moverPasso(i, dir) {
      setPassos(prev => {
        const j = i + dir;
        if (j < 0 || j >= prev.length) return prev;
        const novo = [...prev];
        [novo[i], novo[j]] = [novo[j], novo[i]];
        return novo;
      });
    }

    async function salvar() {
      if (!nome.trim() || !passos.length) return;
      setSalvando(true);
      const dados = { nome: nome.trim(), categoria_id: categoriaId || null,
        updated_at: new Date().toISOString() };
      let scriptId = item?.id;
      if (scriptId) {
        await window.db.from('crm_whatsapp_scripts').update(dados).eq('id', scriptId);
        await window.db.from('crm_whatsapp_script_mensagens').delete().eq('script_id', scriptId);
      } else {
        const { data, error } = await window.db.from('crm_whatsapp_scripts')
          .insert(dados).select('id').single();
        if (error) { setSalvando(false); return alert('Não consegui salvar: ' + error.message); }
        scriptId = data.id;
      }
      const linhas = passos.map((mensagemId, i) => ({ script_id: scriptId, mensagem_id: mensagemId, ordem: i }));
      const { error: erroPassos } = await window.db.from('crm_whatsapp_script_mensagens').insert(linhas);
      setSalvando(false);
      if (erroPassos) return alert('Não consegui salvar os passos: ' + erroPassos.message);
      onSalvo();
    }

    async function excluir() {
      if (!confirm(`Excluir o script "${nome}"?`)) return;
      await window.db.from('crm_whatsapp_scripts').delete().eq('id', item.id);
      onSalvo();
    }

    const disponiveis = prontas.filter(p => !passos.includes(p.id));

    return (
      <ModalBase titulo={item?.id ? 'Editar script' : 'Novo script'} onClose={onClose} largura={480}
        rodape={<>
          {item?.id && <Btn variant="danger" icon="trash-2" onClick={excluir}>Excluir</Btn>}
          <Btn variant="ghost" onClick={onClose}>Cancelar</Btn>
          <Btn variant="primary" onClick={salvar} disabled={salvando || !nome.trim() || !passos.length}>
            {salvando ? 'Salvando…' : 'Salvar'}
          </Btn>
        </>}>
        <Campo label="Nome do script">
          <input style={estiloCampo} value={nome} onChange={e => setNome(e.target.value)}
            placeholder="Ex.: Boas-vindas + orçamento" />
        </Campo>
        <Campo label="Categoria">
          <select style={estiloCampo} value={categoriaId} onChange={e => setCategoriaId(e.target.value)}>
            <option value="">Sem categoria</option>
            {categorias.map(c => <option key={c.id} value={c.id}>{c.nome}</option>)}
          </select>
        </Campo>
        <Campo label="Passos, na ordem de envio"
          dica="Atraso e simulação vêm de cada mensagem — ajuste isso em Modelos.">
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
            {passos.length === 0 && (
              <div style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)',
                padding: '8px 0' }}>
                Nenhum passo ainda. Escolha uma mensagem abaixo.
              </div>
            )}
            {passos.map((mid, i) => {
              const m = prontas.find(p => p.id === mid);
              if (!m) return null;
              return (
                <div key={mid + '-' + i} style={{ display: 'flex', alignItems: 'center', gap: 6,
                  padding: '6px 10px', borderRadius: 'var(--r-sm)', background: 'var(--app-surface-2)',
                  border: '1px solid var(--app-border)' }}>
                  <span style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif',
                    color: 'var(--text-3)', width: 18 }}>{i + 1}º</span>
                  <span style={{ flex: 1, fontSize: 'var(--fs-md)', fontFamily: 'Roboto,sans-serif',
                    color: 'var(--text-1)', overflow: 'hidden', textOverflow: 'ellipsis',
                    whiteSpace: 'nowrap' }}>{m.titulo}</span>
                  <span style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif',
                    color: 'var(--text-3)', flexShrink: 0 }}>
                    {m.enviar_apos_segundos ? `+${m.enviar_apos_segundos}s` : 'na hora'}
                  </span>
                  <button onClick={() => moverPasso(i, -1)} disabled={i === 0}
                    style={{ background: 'none', border: 'none',
                      cursor: i === 0 ? 'default' : 'pointer', color: 'var(--text-3)',
                      opacity: i === 0 ? 0.3 : 1, display: 'flex' }}>
                    <LucideIcon icon="chevron-up" size={13} />
                  </button>
                  <button onClick={() => moverPasso(i, 1)} disabled={i === passos.length - 1}
                    style={{ background: 'none', border: 'none',
                      cursor: i === passos.length - 1 ? 'default' : 'pointer', color: 'var(--text-3)',
                      opacity: i === passos.length - 1 ? 0.3 : 1, display: 'flex' }}>
                    <LucideIcon icon="chevron-down" size={13} />
                  </button>
                  <button onClick={() => removerPasso(i)} title="Remover"
                    style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--clr-neg)',
                      display: 'flex' }}>
                    <LucideIcon icon="x" size={13} />
                  </button>
                </div>
              );
            })}
          </div>
        </Campo>
        <Campo label="Adicionar mensagem">
          <select style={estiloCampo} value="" onChange={e => adicionarPasso(e.target.value)}>
            <option value="">Escolher uma mensagem de Modelos…</option>
            {disponiveis.map(p => <option key={p.id} value={p.id}>{p.titulo}</option>)}
          </select>
        </Campo>
      </ModalBase>
    );
  }

  /* ── Modal de negociação ─────────────────────────────────────
     Autosave: não existe botão "Salvar". Ao abrir sem um item já
     pronto, ou reaproveita a negociação em aberto que o contato já
     tinha (o índice único só permite uma pendente por vez), ou cria
     um rascunho na hora — daí em diante cada mudança só grava depois
     de meio segundo sem digitar, pra não martelar o banco a cada
     tecla. Fechar (X, Esc ou clique fora) é a única ação manual. */
  function ModalNegociacao({ item, contato, origens, colunas, onClose, onContatoMudou,
                            onNegociacaoMudou }) {
    const contatoId = contato?.id || item?.contato_id || null;
    const idRef = useRef(item?.id || null);
    const montouRef = useRef(false);

    const [titulo, setTitulo] = useState(item?.titulo || '');
    const [valor, setValor] = useState(item?.valor != null ? Number(item.valor) : 0);
    const [status, setStatus] = useState(item?.status || 'pendente');
    const [motivo, setMotivo] = useState(item?.motivo_perda || '');
    const [origemId, setOrigemId] = useState(item?.origem_id || contato?.origem_id || '');
    const [colunaId, setColunaId] = useState(item?.coluna_id || '');
    const [obs, setObs] = useState(item?.observacoes || '');
    const [pronto, setPronto] = useState(!!item?.id);
    const [salvando, setSalvando] = useState(false);

    /* Rascunho: a negociação nasce guardada, mas ainda não aberta.

       Antes, abrir o modal já criava negociação em aberto, e só de dar
       uma olhada o contato entrava no funil e nos números. Agora o que
       você digita continua sendo salvo na hora, mas a negociação só
       existe para o resto do sistema quando você clica em "Abrir
       negociação".

       Rascunho não aparece no kanban nem no cabeçalho, porque as duas
       telas filtram por 'pendente'. E não briga com o índice único de
       uma negociação aberta por contato, que também é só de 'pendente'. */
    useEffect(() => {
      if (idRef.current) return;
      (async () => {
        if (!contatoId) return;
        const { data: existentes } = await window.db.from('crm_whatsapp_negociacoes')
          .select('*').eq('contato_id', contatoId)
          .in('status', ['pendente', 'rascunho'])
          // Aberta ganha do rascunho: 'pendente' vem antes de 'rascunho'
          // em ordem alfabética, então a primeira linha é a certa.
          .order('status', { ascending: true })
          .limit(1);
        const existente = existentes?.[0];
        if (existente) {
          idRef.current = existente.id;
          setTitulo(existente.titulo || '');
          setValor(Number(existente.valor) || 0);
          setStatus(existente.status);
          setMotivo(existente.motivo_perda || '');
          setOrigemId(existente.origem_id || '');
          setColunaId(existente.coluna_id || '');
          setObs(existente.observacoes || '');
        } else {
          const primeiraColuna = colunas?.[0]?.id || null;
          const { data: novo } = await window.db.from('crm_whatsapp_negociacoes')
            .insert({ contato_id: contatoId, status: 'rascunho',
              origem_id: contato?.origem_id || null, coluna_id: primeiraColuna })
            .select('id').single();
          if (novo) {
            idRef.current = novo.id;
            setColunaId(primeiraColuna || '');
            setStatus('rascunho');
          }
        }
        setPronto(true);
      })();
      // eslint-disable-next-line
    }, []);

    useEffect(() => {
      if (!montouRef.current) { montouRef.current = true; return; }
      if (!idRef.current) return;
      setSalvando(true);
      const t = setTimeout(salvar, 550);
      return () => clearTimeout(t);
      // eslint-disable-next-line
    }, [titulo, valor, status, motivo, origemId, colunaId, obs]);

    async function salvar() {
      const dados = {
        titulo: titulo.trim() || null,
        valor: Number(valor) || 0,
        status,
        motivo_perda: status === 'perdida' ? (motivo.trim() || null) : null,
        origem_id: origemId || null,
        coluna_id: colunaId || null,
        observacoes: obs.trim() || null,
        // Só negociação encerrada tem data de fim. Rascunho não é fim de
        // nada: sem esta distinção, um rascunho nasceria já finalizado.
        finalizada_em: (status === 'ganha' || status === 'perdida')
          ? new Date().toISOString() : null,
        updated_at: new Date().toISOString(),
      };
      await window.db.from('crm_whatsapp_negociacoes').update(dados).eq('id', idRef.current);
      setSalvando(false);
      /* Avisa a tela na hora, em vez de esperar o eco do tempo real dar
         a volta pelo servidor. O modal já sabe o valor novo: fazer o
         cabeçalho depender do retorno do banco era o que obrigava a
         recarregar a página pra ver o valor mudar. */
      onNegociacaoMudou?.({ id: idRef.current, contato_id: contatoId, ...dados });
    }

    // Fechar como ganha ou perdida etiqueta o contato sozinho: ganha
    // vira Cliente, perdida vira Perdido. É o que faz "entrar nas
    // métricas" e "encerrar o atendimento" acontecerem sem um segundo
    // passo manual. Antes isso mexia na etapa fixa, que virou etiqueta.
    function mudarStatus(novo) {
      setStatus(novo);
      if ((novo === 'ganha' || novo === 'perdida') && contatoId) {
        onContatoMudou?.(contatoId, novo === 'ganha' ? 'Cliente' : 'Perdido');
      }
    }

    /* É este clique que faz a negociação existir para o resto do
       sistema: entra no kanban, aparece no cabeçalho do contato e passa
       a contar nos relatórios. Antes disso, é só rascunho guardado. */
    async function abrirNegociacao() {
      if (!idRef.current) return;
      setSalvando(true);
      await window.db.from('crm_whatsapp_negociacoes')
        .update({ status: 'pendente', iniciada_em: new Date().toISOString(),
          finalizada_em: null, updated_at: new Date().toISOString() })
        .eq('id', idRef.current);
      setStatus('pendente');
      setSalvando(false);
      onNegociacaoMudou?.({ id: idRef.current, contato_id: contatoId, status: 'pendente',
        titulo, valor: Number(valor) || 0, coluna_id: colunaId || null });
      // A lista e o cabeçalho se atualizam sozinhos pela assinatura de
      // tempo real em crm_whatsapp_negociacoes. Não dá pra avisar por
      // onContatoMudou aqui: essa função carrega a ETAPA do contato, e
      // abrir negociação não muda etapa nenhuma.
    }

    async function excluir() {
      if (!idRef.current) return;
      if (!confirm('Excluir esta negociação?')) return;
      await window.db.from('crm_whatsapp_negociacoes').delete().eq('id', idRef.current);
      onClose();
    }

    return (
      <ModalBase titulo="Negociação" onClose={onClose}
        rodape={<>
          {idRef.current && <Btn variant="danger" icon="trash-2" onClick={excluir}>Excluir</Btn>}
          <span style={{ flex: 1, fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif',
            color: 'var(--text-3)', display: 'flex', alignItems: 'center', gap: 4 }}>
            <LucideIcon icon={salvando ? 'loader' : 'check'} size={11} />
            {salvando ? 'Salvando…' : 'Salvo'}
          </span>
          {status === 'rascunho'
            ? <Btn variant="primary" icon="handshake" onClick={abrirNegociacao}>Abrir negociação</Btn>
            : <Btn variant="primary" onClick={onClose}>Fechar</Btn>}
        </>}>
        {status === 'rascunho' ? null : (
          <Campo label="Situação">
            <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
              {STATUS_NEGOCIACAO.map(s => (
                <button key={s.id} onClick={() => mudarStatus(s.id)}
                  style={{ flex: '1 1 auto', padding: '8px 12px', borderRadius: 'var(--r-sm)', cursor: 'pointer',
                    fontSize: 'var(--fs-md)', fontFamily: 'Roboto,sans-serif', fontWeight: 700,
                    background: status === s.id ? `${s.cor}22` : 'var(--app-surface-2)',
                    border: `1px solid ${status === s.id ? `${s.cor}77` : 'var(--app-border)'}`,
                    color: status === s.id ? s.cor : 'var(--text-2)' }}>
                  {s.label}
                </button>
              ))}
            </div>
          </Campo>
        )}
        <Campo label="O que está sendo negociado">
          <input style={estiloCampo} value={titulo} onChange={e => setTitulo(e.target.value)}
            placeholder="Ex.: Ensaio gestante, Casamento novembro" />
        </Campo>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
          <Campo label="Valor">
            <CampoValorBRL style={estiloCampo} valor={valor} onChange={setValor} />
          </Campo>
          <Campo label="Coluna do kanban">
            <select style={estiloCampo} value={colunaId} onChange={e => setColunaId(e.target.value)}>
              <option value="">Sem coluna</option>
              {(colunas || []).map(c => <option key={c.id} value={c.id}>{c.nome}</option>)}
            </select>
          </Campo>
        </div>
        {status === 'perdida' && (
          <Campo label="Por que perdeu" dica="Saber isso é o que faz o número virar aprendizado.">
            <input style={estiloCampo} value={motivo} onChange={e => setMotivo(e.target.value)}
              placeholder="Ex.: preço, data indisponível, sumiu" />
          </Campo>
        )}
        <Campo label="Origem do lead">
          <select style={estiloCampo} value={origemId} onChange={e => setOrigemId(e.target.value)}>
            <option value="">Não sei</option>
            {origens.map(o => <option key={o.id} value={o.id}>{o.nome}</option>)}
          </select>
        </Campo>
        <Campo label="Observações">
          <textarea style={{ ...estiloCampo, minHeight: 70, resize: 'vertical' }}
            value={obs} onChange={e => setObs(e.target.value)} placeholder="Opcional" />
        </Campo>
        {!pronto && (
          <span style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)' }}>
            Abrindo…
          </span>
        )}
      </ModalBase>
    );
  }

  /* ── Painel lateral: biblioteca de mensagens prontas ─────────
     Altura inteira, à DIREITA da conversa, com as categorias em
     sanfona — o desenho que o MQV já tinha. O ponto é escolher a
     mensagem sem sair da conversa: por isso painel, e não aba. */
  function PainelBiblioteca({ prontas, categorias, scripts, contato, largura,
                              onUsar, onNovo, onEditar, onNovoScript, onEditarScript,
                              onUsarScript, onFechar, onRedimensionar }) {
    const [busca, setBusca] = useState('');
    const [abertas, setAbertas] = useState(() => new Set());
    const [subaba, setSubaba] = useState('mensagens');

    const termo = busca.trim().toLowerCase();

    // Buscando, todas as categorias abrem: esconder resultado atrás de
    // sanfona fechada faz a busca parecer que não achou nada.
    const grupos = useMemo(() => {
      const casa = (p) => !termo || `${p.titulo} ${p.conteudo}`.toLowerCase().includes(termo);
      const lista = [];
      for (const c of categorias) {
        const itens = prontas.filter(p => p.categoria_id === c.id && casa(p));
        if (itens.length) lista.push({ id: c.id, nome: c.nome, cor: c.cor, itens });
      }
      const soltas = prontas.filter(p => !p.categoria_id && casa(p));
      if (soltas.length) lista.push({ id: 'sem', nome: 'Sem categoria', cor: '#94a3b8', itens: soltas });
      return lista;
    }, [prontas, categorias, termo]);

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

    // Arrastar a borda pra redimensionar, como no MQV. O painel fica à
    // direita, então arrastar pra ESQUERDA é que aumenta — daí o sinal
    // invertido em relação a um painel da esquerda.
    function iniciarArraste(ev) {
      ev.preventDefault();
      const inicioX = ev.clientX;
      const inicial = largura;
      const mover = (e) => onRedimensionar(
        Math.min(560, Math.max(260, inicial - (e.clientX - inicioX))));
      const soltar = () => {
        document.removeEventListener('mousemove', mover);
        document.removeEventListener('mouseup', soltar);
      };
      document.addEventListener('mousemove', mover);
      document.addEventListener('mouseup', soltar);
    }

    return (
      <div style={{ width: largura, flexShrink: 0, position: 'relative',
        borderLeft: '1px solid var(--app-border)', background: 'var(--app-surface)',
        display: 'flex', flexDirection: 'column', minHeight: 0 }}>

        {/* Borda de arraste, na lateral esquerda do painel */}
        <div onMouseDown={iniciarArraste}
          style={{ position: 'absolute', top: 0, left: -3, width: 6, height: '100%',
            cursor: 'col-resize', zIndex: 5 }} />

        <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '12px 12px',
          borderBottom: '1px solid var(--app-border)', flexShrink: 0 }}>
          <LucideIcon icon="book-text" size={15} style={{ color: 'var(--accent)' }} />
          <span style={{ flex: 1, fontSize: 'var(--fs-md)', fontFamily: 'Roboto,sans-serif',
            fontWeight: 900, letterSpacing: '0.04em', textTransform: 'uppercase',
            color: 'var(--text-1)' }}>
            Modelos
          </span>
          <button onClick={subaba === 'scripts' ? onNovoScript : onNovo}
            title={subaba === 'scripts' ? 'Novo script' : 'Nova mensagem'}
            style={{ width: 26, height: 26, borderRadius: 'var(--r-sm)', cursor: 'pointer',
              background: 'rgba(234,170,65,.14)', border: '1px solid rgba(234,170,65,.3)',
              color: 'var(--accent)', display: 'flex', alignItems: 'center',
              justifyContent: 'center' }}>
            <LucideIcon icon="plus" size={14} />
          </button>
          <button onClick={onFechar} title="Fechar Modelos"
            style={{ background: 'none', border: 'none', cursor: 'pointer',
              color: 'var(--text-3)', display: 'flex' }}>
            <LucideIcon icon="panel-right-close" size={16} />
          </button>
        </div>

        <div style={{ display: 'flex', gap: 4, padding: '8px 12px 0', flexShrink: 0 }}>
          {[{ id: 'mensagens', label: 'Mensagens' }, { id: 'scripts', label: 'Scripts' }].map(a => (
            <button key={a.id} onClick={() => setSubaba(a.id)}
              style={{ flex: 1, padding: '6px 10px', borderRadius: 'var(--r-sm)', cursor: 'pointer',
                fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', fontWeight: 700,
                background: subaba === a.id ? 'rgba(234,170,65,.15)' : 'var(--app-surface-2)',
                border: `1px solid ${subaba === a.id ? 'rgba(234,170,65,.4)' : 'var(--app-border)'}`,
                color: subaba === a.id ? 'var(--accent)' : 'var(--text-2)' }}>
              {a.label}
            </button>
          ))}
        </div>

        {subaba === 'mensagens' && (
          <div style={{ padding: '10px 12px', flexShrink: 0 }}>
            <input value={busca} onChange={e => setBusca(e.target.value)}
              placeholder="Buscar mensagem" autoFocus
              style={{ ...estiloCampo, padding: '6px 10px', fontSize: 'var(--fs-md)' }} />
          </div>
        )}

        {subaba === 'scripts' ? (
          <div style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '10px 12px 16px',
            display: 'flex', flexDirection: 'column', gap: 6 }}>
            {(scripts || []).length === 0 ? (
              <div style={{ padding: '24px 12px', textAlign: 'center', fontSize: 'var(--fs-md)',
                fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)', lineHeight: 1.7 }}>
                Nenhum script ainda.<br />
                Um script é uma sequência de mensagens que sai sozinha, no ritmo que
                você definir em cada uma.
              </div>
            ) : scripts.map(s => {
              const passos = (s.crm_whatsapp_script_mensagens || []).length;
              return (
                <div key={s.id}
                  style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 10px',
                    borderRadius: 'var(--r-md)', background: 'var(--app-surface-2)',
                    border: '1px solid var(--app-border)' }}>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 'var(--fs-md)', fontFamily: 'Roboto,sans-serif',
                      fontWeight: 700, color: 'var(--text-1)' }}>{s.nome}</div>
                    <div style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif',
                      color: 'var(--text-3)' }}>{passos} passo{passos === 1 ? '' : 's'}</div>
                  </div>
                  <button onClick={() => onEditarScript(s)} title="Editar script"
                    style={{ background: 'none', border: 'none', cursor: 'pointer',
                      color: 'var(--text-3)', display: 'flex', padding: 2 }}>
                    <LucideIcon icon="pencil" size={13} />
                  </button>
                  <button onClick={() => onUsarScript(s)} disabled={!contato}
                    title={contato ? 'Disparar este script na conversa' : 'Abra uma conversa primeiro'}
                    style={{ padding: '4px 10px', borderRadius: 'var(--r-sm)', cursor: contato ? 'pointer' : 'default',
                      fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', fontWeight: 700,
                      background: 'rgba(74,222,128,.14)', border: '1px solid rgba(74,222,128,.3)',
                      color: contato ? 'var(--clr-pos)' : 'var(--text-3)', opacity: contato ? 1 : 0.5 }}>
                    Usar
                  </button>
                </div>
              );
            })}
          </div>
        ) : (
        <div style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '0 8px 16px',
          display: 'flex', flexDirection: 'column', gap: 4 }}>
          {prontas.length === 0 ? (
            <div style={{ padding: '24px 12px', textAlign: 'center', fontSize: 'var(--fs-md)',
              fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)', lineHeight: 1.7 }}>
              Nenhuma mensagem ainda.<br />
              Guarde aqui o que você digita toda semana. Use <strong>[[nome]]</strong> e
              o Khronus preenche na hora.
            </div>
          ) : grupos.length === 0 ? (
            <div style={{ padding: '24px 12px', textAlign: 'center', fontSize: 'var(--fs-md)',
              fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)' }}>
              Nada com essa busca.
            </div>
          ) : grupos.map(g => {
            const aberta = termo ? true : abertas.has(g.id);
            return (
              <div key={g.id} style={{ display: 'flex', flexDirection: 'column' }}>
                <button onClick={() => alternar(g.id)}
                  style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px',
                    borderRadius: 'var(--r-sm)', cursor: termo ? 'default' : 'pointer', border: 'none',
                    background: 'transparent', textAlign: 'left' }}
                  onMouseEnter={e => e.currentTarget.style.background = 'var(--app-surface-2)'}
                  onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
                  <LucideIcon icon={aberta ? 'chevron-down' : 'chevron-right'} size={13}
                    style={{ color: 'var(--text-3)' }} />
                  <span style={{ width: 8, height: 8, borderRadius: '50%', background: g.cor,
                    flexShrink: 0 }} />
                  <span style={{ flex: 1, fontSize: 'var(--fs-md)', fontFamily: 'Roboto,sans-serif',
                    fontWeight: 700, color: 'var(--text-1)' }}>{g.nome}</span>
                  <span style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif',
                    color: 'var(--text-3)' }}>{g.itens.length}</span>
                </button>

                {aberta && g.itens.map(p => (
                  <div key={p.id}
                    style={{ display: 'flex', alignItems: 'flex-start', gap: 6,
                      padding: '8px 10px 8px 30px', borderRadius: 'var(--r-sm)' }}
                    onMouseEnter={e => e.currentTarget.style.background = 'var(--app-surface-2)'}
                    onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
                    <div onClick={() => onUsar(p)}
                      style={{ flex: 1, minWidth: 0, cursor: contato ? 'pointer' : 'default' }}
                      title={contato ? 'Colocar no campo de mensagem' : 'Abra uma conversa primeiro'}>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
                        <LucideIcon icon={TIPOS_MENSAGEM.find(t => t.id === p.tipo)?.icon || 'message-square'}
                          size={11} style={{ color: 'var(--text-3)', flexShrink: 0 }} />
                        <span style={{ fontSize: 'var(--fs-md)', fontFamily: 'Roboto,sans-serif',
                          fontWeight: 700, color: 'var(--text-1)' }}>{p.titulo}</span>
                      </div>
                      <div style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif',
                        color: 'var(--text-3)', lineHeight: 1.45, marginTop: 2,
                        display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical',
                        overflow: 'hidden' }}>
                        {resolverVariaveis(p.conteudo, contato) || '(sem texto)'}
                      </div>
                    </div>
                    <button onClick={() => onEditar(p)} title="Editar"
                      style={{ background: 'none', border: 'none', cursor: 'pointer',
                        color: 'var(--text-3)', display: 'flex', padding: 2, flexShrink: 0 }}>
                      <LucideIcon icon="pencil" size={12} />
                    </button>
                  </div>
                ))}
              </div>
            );
          })}
        </div>
        )}
      </div>
    );
  }
  /* ── Painel: negociações ─────────────────────────────────────*/
  /* ── Negociações em barras ────────────────────────────────────
     Mesma ideia do kanban, mas respeitando que aqui a lista é uma
     coluna estreita ao lado da thread — não tem largura pra colunas
     lado a lado. Em vez disso, cada etapa vira uma barra empilhada,
     com os cards dela embaixo. Clicar num card seleciona o contato
     e abre a mesma thread de sempre, ao lado — sem tela nova. */
  function ItemConversaNegociacao({ contato, negociacao, ativo, draggable,
                                    onDragStart, onDragEnd, onClick, onEditar }) {
    const [hov, setHov] = useState(false);
    return (
      <div draggable={draggable} onDragStart={onDragStart} onDragEnd={onDragEnd}
        onClick={onClick} onMouseEnter={() => setHov(true)} onMouseLeave={() => setHov(false)}
        style={{ display: 'flex', gap: 8, padding: '8px 10px', cursor: draggable ? 'grab' : 'pointer',
          borderRadius: 'var(--r-md)', alignItems: 'center',
          background: ativo ? 'rgba(234,170,65,.12)' : hov ? 'var(--app-surface-3)' : 'transparent',
          border: `1px solid ${ativo ? 'rgba(234,170,65,.35)' : 'transparent'}` }}>
        <AvatarContato contato={contato} size={30} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 'var(--fs-md)', fontFamily: 'Roboto,sans-serif', fontWeight: 700,
            color: 'var(--text-1)', overflow: 'hidden', textOverflow: 'ellipsis',
            whiteSpace: 'nowrap' }}>
            {contato ? rotuloContato(contato) : 'contato removido'}
          </div>
          <div style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)',
            overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
            {negociacao.titulo || 'Negociação'}
          </div>
        </div>
        <span style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', fontWeight: 700,
          color: 'var(--accent)', flexShrink: 0 }}>{fmtBRL(negociacao.valor)}</span>
        <button onClick={e => { e.stopPropagation(); onEditar(); }} title="Editar negociação"
          style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-3)',
            display: 'flex', flexShrink: 0 }}>
          <LucideIcon icon="pencil" size={12} />
        </button>
      </div>
    );
  }

  function ListaNegociacoesPorColuna({ negociacoes, colunas, contatos, selecionadoId,
                                        onSelecionar, onEditar, onMover }) {
    const [dragId, setDragId] = useState(null);
    const [dragOverCol, setDragOverCol] = useState(null);
    /* Grupos fechados, guardados por etapa. Com o funil cheio, ver
       cinco etapas de uma vez vira rolagem; fechando as que não
       interessam agora, sobra só a que você está trabalhando.
       Guardado no próprio navegador pra escolha sobreviver ao recarregar. */
    const [fechados, setFechados] = useState(() => {
      try {
        return new Set(JSON.parse(localStorage.getItem('khronus.negociacoes.fechados') || '[]'));
      } catch (_) { return new Set(); }
    });

    function alternarGrupo(id) {
      setFechados(prev => {
        const novo = new Set(prev);
        const chave = id || 'sem-coluna';
        novo.has(chave) ? novo.delete(chave) : novo.add(chave);
        try {
          localStorage.setItem('khronus.negociacoes.fechados', JSON.stringify([...novo]));
        } catch (_) { /* sem armazenamento: vale só nesta sessão */ }
        return novo;
      });
    }

    const abertas = negociacoes.filter(n => n.status === 'pendente');
    const colunasOrdenadas = [...colunas].sort((a, b) => (a.ordem ?? 0) - (b.ordem ?? 0));
    const idsColuna = new Set(colunasOrdenadas.map(c => c.id));
    const semColuna = abertas.filter(n => !n.coluna_id || !idsColuna.has(n.coluna_id));

    function barra(col, itens) {
      const id = col?.id || null;
      const cor = col?.cor || '#94a3b8';
      const fechado = fechados.has(id || 'sem-coluna');
      const total = itens.reduce((s, n) => s + (Number(n.valor) || 0), 0);
      return (
        <div key={id || 'sem'}
          onDragOver={e => { e.preventDefault(); setDragOverCol(id); }}
          onDragLeave={() => setDragOverCol(prev => prev === id ? null : prev)}
          onDrop={e => { e.preventDefault(); setDragOverCol(null);
            if (dragId) onMover(dragId, id); setDragId(null); }}
          style={{ borderRadius: 'var(--r-md)', padding: '2px 2px 6px',
            background: dragOverCol === id ? `${cor}12` : 'transparent',
            border: `1px dashed ${dragOverCol === id ? `${cor}66` : 'transparent'}` }}>
          <div onClick={() => alternarGrupo(id)}
            title={fechado ? 'Abrir esta etapa' : 'Fechar esta etapa'}
            style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 8px 4px',
              cursor: 'pointer', userSelect: 'none' }}>
            <LucideIcon icon={fechado ? 'chevron-right' : 'chevron-down'} size={12} />
            <span style={{ width: 7, height: 7, borderRadius: '50%', background: cor, flexShrink: 0 }} />
            <span style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif', fontWeight: 900,
              letterSpacing: '0.05em', textTransform: 'uppercase', color: 'var(--text-3)' }}>
              {col ? col.nome : 'Sem coluna'} · {itens.length}
            </span>
            {/* Com o grupo fechado, o total em dinheiro é o que evita
                precisar abrir só pra saber se vale a pena olhar. */}
            {fechado && total > 0 && (
              <span style={{ marginLeft: 'auto', fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif',
                fontWeight: 700, color: 'var(--accent)' }}>{fmtBRL(total)}</span>
            )}
          </div>
          {fechado ? null : itens.length === 0 ? (
            <div style={{ padding: '2px 10px 8px', fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif',
              color: 'var(--text-3)' }}>
              Arraste um card pra cá
            </div>
          ) : itens.map(n => {
            const contato = contatos.find(c => c.id === n.contato_id) || n.crm_whatsapp_contatos;
            return (
              <ItemConversaNegociacao key={n.id} contato={contato} negociacao={n} draggable
                ativo={!!contato && selecionadoId === contato.id}
                onDragStart={() => setDragId(n.id)} onDragEnd={() => setDragId(null)}
                onClick={() => contato && onSelecionar(contato)}
                onEditar={() => onEditar(n)} />
            );
          })}
        </div>
      );
    }

    if (!colunasOrdenadas.length) {
      return (
        <div style={{ padding: '30px 16px', textAlign: 'center', color: 'var(--text-3)',
          fontSize: 'var(--fs-md)', lineHeight: 1.7 }}>
          Nenhuma etapa ainda. Use "Colunas", ali em cima, pra montar o funil
          (ex.: Negociação aberta, Proposta enviada, Follow-up).
        </div>
      );
    }

    return (
      <div style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '6px 6px 16px',
        display: 'flex', flexDirection: 'column', gap: 4 }}>
        {colunasOrdenadas.map(col => barra(col, abertas.filter(n => n.coluna_id === col.id)))}
        {semColuna.length > 0 && barra(null, semColuna)}
      </div>
    );
  }

  /* ── Modal: colunas do kanban de negociação ──────────────────*/
  function ModalColunas({ colunas, onClose, onCriar, onEditar, onExcluir, onMover }) {
    const [novoNome, setNovoNome] = useState('');
    const ordenadas = [...colunas].sort((a, b) => (a.ordem ?? 0) - (b.ordem ?? 0));

    function criar() {
      if (!novoNome.trim()) return;
      onCriar(novoNome);
      setNovoNome('');
    }

    return (
      <ModalBase titulo="Etapas do funil" onClose={onClose} largura={420}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          {ordenadas.length === 0 && (
            <div style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)' }}>
              Nenhuma etapa ainda.
            </div>
          )}
          {ordenadas.map((c, i) => (
            <div key={c.id} style={{ display: 'flex', alignItems: 'center', gap: 6,
              padding: '6px 8px', borderRadius: 'var(--r-sm)', background: 'var(--app-surface-2)',
              border: '1px solid var(--app-border)' }}>
              <div style={{ display: 'flex', gap: 4, flexShrink: 0 }}>
                {CORES_COLUNA.map(cor => (
                  <button key={cor} onClick={() => onEditar(c.id, { cor })}
                    title={cor}
                    style={{ width: 13, height: 13, borderRadius: '50%', background: cor,
                      cursor: 'pointer',
                      border: c.cor === cor ? '2px solid var(--text-1)' : '1px solid var(--app-border-2)' }} />
                ))}
              </div>
              <input value={c.nome} onChange={e => onEditar(c.id, { nome: e.target.value })}
                style={{ flex: 1, minWidth: 0, background: 'transparent', border: 'none', outline: 'none',
                  color: 'var(--text-1)', fontFamily: 'Roboto,sans-serif', fontSize: 'var(--fs-md)' }} />
              <button onClick={() => onMover(c.id, -1)} disabled={i === 0}
                style={{ background: 'none', border: 'none', cursor: i === 0 ? 'default' : 'pointer',
                  opacity: i === 0 ? 0.3 : 1, color: 'var(--text-3)', display: 'flex', flexShrink: 0 }}>
                <LucideIcon icon="chevron-up" size={13} />
              </button>
              <button onClick={() => onMover(c.id, 1)} disabled={i === ordenadas.length - 1}
                style={{ background: 'none', border: 'none',
                  cursor: i === ordenadas.length - 1 ? 'default' : 'pointer', color: 'var(--text-3)',
                  opacity: i === ordenadas.length - 1 ? 0.3 : 1, display: 'flex', flexShrink: 0 }}>
                <LucideIcon icon="chevron-down" size={13} />
              </button>
              <button onClick={() => onExcluir(c.id)} title="Excluir coluna"
                style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--clr-neg)',
                  display: 'flex', flexShrink: 0 }}>
                <LucideIcon icon="trash-2" size={13} />
              </button>
            </div>
          ))}
        </div>
        <div style={{ display: 'flex', gap: 6, marginTop: 4 }}>
          <input value={novoNome} onChange={e => setNovoNome(e.target.value)}
            onKeyDown={e => { if (e.key === 'Enter') criar(); }}
            placeholder="Nova coluna" style={{ ...estiloCampo, flex: 1 }} />
          <Btn variant="primary" icon="plus" onClick={criar}>Adicionar</Btn>
        </div>
      </ModalBase>
    );
  }

  /* ── Painel: números ─────────────────────────────────────────*/
  const PERIODOS_RELATORIO = [
    { id: 'hoje', label: 'Hoje' },
    { id: '7d',   label: 'Últimos 7 dias' },
    { id: '30d',  label: 'Últimos 30 dias' },
    { id: 'mes',  label: 'Este mês' },
  ];

  function limitesPeriodo(periodo) {
    const agora = new Date();
    const fim = new Date(agora);
    let inicio;
    if (periodo === 'hoje') { inicio = new Date(agora); inicio.setHours(0, 0, 0, 0); }
    else if (periodo === '7d') { inicio = new Date(agora); inicio.setDate(inicio.getDate() - 6); inicio.setHours(0, 0, 0, 0); }
    else if (periodo === 'mes') { inicio = new Date(agora.getFullYear(), agora.getMonth(), 1); }
    else { inicio = new Date(agora); inicio.setDate(inicio.getDate() - 29); inicio.setHours(0, 0, 0, 0); }
    return { inicio, fim };
  }

  /* ── Gráficos, em SVG puro. O Khronus não carrega biblioteca de
     gráfico nenhuma (sem bundler, sem CDN externo) — pizza, barra e
     linha aqui são desenhadas na mão, o suficiente pro que a tela
     precisa mostrar. */
  function GraficoPizza({ fatias, size = 128 }) {
    const comValor = fatias.filter(f => f.valor > 0);
    const total = comValor.reduce((s, f) => s + f.valor, 0);
    const raio = size / 2;
    if (!total) {
      return (
        <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
          <circle cx={raio} cy={raio} r={raio - 1} fill="none" stroke="var(--app-border)" />
        </svg>
      );
    }
    if (comValor.length === 1) {
      return (
        <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
          <circle cx={raio} cy={raio} r={raio} fill={comValor[0].cor} />
        </svg>
      );
    }
    const toRad = a => (a * Math.PI) / 180;
    let anguloAcumulado = -90;
    const caminhos = comValor.map(f => {
      const fracao = f.valor / total;
      const anguloInicio = anguloAcumulado;
      const anguloFim = anguloAcumulado + fracao * 360;
      anguloAcumulado = anguloFim;
      const grandeArco = (anguloFim - anguloInicio) > 180 ? 1 : 0;
      const x1 = raio + raio * Math.cos(toRad(anguloInicio));
      const y1 = raio + raio * Math.sin(toRad(anguloInicio));
      const x2 = raio + raio * Math.cos(toRad(anguloFim));
      const y2 = raio + raio * Math.sin(toRad(anguloFim));
      return { d: `M ${raio} ${raio} L ${x1} ${y1} A ${raio} ${raio} 0 ${grandeArco} 1 ${x2} ${y2} Z`, cor: f.cor };
    });
    return (
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
        {caminhos.map((c, i) => <path key={i} d={c.d} fill={c.cor} />)}
      </svg>
    );
  }

  function GraficoLinha({ pontos, height = 90, cor = '#a78bfa' }) {
    const largura = 320;
    if (!pontos.length) return null;
    const max = Math.max(1, ...pontos.map(p => p.valor));
    const passoX = pontos.length > 1 ? largura / (pontos.length - 1) : 0;
    const coords = pontos.map((p, i) => {
      const x = pontos.length > 1 ? i * passoX : largura / 2;
      const y = height - 6 - (p.valor / max) * (height - 14);
      return `${x},${y}`;
    });
    const linha = coords.join(' ');
    const area = `0,${height} ${linha} ${largura},${height}`;
    return (
      <svg width="100%" height={height} viewBox={`0 0 ${largura} ${height}`} preserveAspectRatio="none">
        <polygon points={area} fill={`${cor}20`} />
        <polyline points={linha} fill="none" stroke={cor} strokeWidth="2" />
      </svg>
    );
  }

  function BarraHorizontal({ label, valor, max, cor, formatar }) {
    const pct = max ? Math.round((valor / max) * 100) : 0;
    return (
      <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
        <span style={{ width: 96, flexShrink: 0, fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif',
          color: 'var(--text-2)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
          {label}
        </span>
        <div style={{ flex: 1, height: 8, borderRadius: 'var(--r-full)', background: 'var(--overlay-05)',
          overflow: 'hidden' }}>
          <div style={{ width: `${pct}%`, height: '100%', background: cor, borderRadius: 'var(--r-full)' }} />
        </div>
        <span style={{ width: 78, flexShrink: 0, textAlign: 'right', fontSize: 'var(--fs-sm)', fontWeight: 700,
          fontFamily: 'Roboto,sans-serif', color: 'var(--text-1)' }}>
          {formatar ? formatar(valor) : valor}
        </span>
      </div>
    );
  }

  function SecaoRelatorio({ titulo, children, largura }) {
    return (
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10, flex: largura ? `1 1 ${largura}px` : undefined }}>
        <span style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', fontWeight: 900,
          letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--text-3)' }}>
          {titulo}
        </span>
        <div style={{ padding: 16, borderRadius: 'var(--r-lg)', background: 'var(--app-surface-2)',
          border: '1px solid var(--app-border)' }}>
          {children}
        </div>
      </div>
    );
  }

  function PainelNumeros({ negociacoes, origens }) {
    const [periodo, setPeriodo] = useState('30d');
    const { inicio, fim } = useMemo(() => limitesPeriodo(periodo), [periodo]);

    const doPeriodo = useMemo(() => negociacoes.filter(x => {
      if (!x.iniciada_em) return false;
      const d = new Date(x.iniciada_em);
      return d >= inicio && d <= fim;
    }), [negociacoes, inicio, fim]);

    const n = useMemo(() => {
      const ganhas = doPeriodo.filter(x => x.status === 'ganha');
      const perdidas = doPeriodo.filter(x => x.status === 'perdida');
      const abertas = doPeriodo.filter(x => x.status === 'pendente');
      const decididas = ganhas.length + perdidas.length;
      const receita = ganhas.reduce((s, x) => s + Number(x.valor || 0), 0);
      const emAberto = abertas.reduce((s, x) => s + Number(x.valor || 0), 0);

      const porOrigem = {};
      for (const x of doPeriodo) {
        const chave = x.origem_id || 'sem';
        porOrigem[chave] ||= { total: 0, ganhas: 0, perdidas: 0, receita: 0 };
        porOrigem[chave].total++;
        if (x.status === 'ganha') { porOrigem[chave].ganhas++; porOrigem[chave].receita += Number(x.valor || 0); }
        if (x.status === 'perdida') porOrigem[chave].perdidas++;
      }

      const motivos = {};
      for (const x of perdidas) {
        const m = (x.motivo_perda || 'não informado').trim().toLowerCase();
        motivos[m] = (motivos[m] || 0) + 1;
      }

      return {
        ganhas: ganhas.length, perdidas: perdidas.length, abertas: abertas.length, decididas,
        receita, emAberto,
        conversao: decididas ? Math.round((ganhas.length / decididas) * 100) : null,
        ticket: ganhas.length ? receita / ganhas.length : 0,
        porOrigem, ganhasLista: ganhas,
        motivos: Object.entries(motivos).sort((a, b) => b[1] - a[1]),
      };
    }, [doPeriodo]);

    const evolucao = useMemo(() => {
      const dias = [];
      const cursor = new Date(inicio); cursor.setHours(0, 0, 0, 0);
      const limite = new Date(fim);
      let guarda = 0;
      while (cursor <= limite && guarda < 366) {
        dias.push({ chave: cursor.toISOString().slice(0, 10),
          label: cursor.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' }), valor: 0 });
        cursor.setDate(cursor.getDate() + 1);
        guarda++;
      }
      const porChave = Object.fromEntries(dias.map(d => [d.chave, d]));
      for (const g of n.ganhasLista) {
        const dataRef = g.finalizada_em || g.iniciada_em;
        if (!dataRef) continue;
        const chave = dataRef.slice(0, 10);
        if (porChave[chave]) porChave[chave].valor += Number(g.valor || 0);
      }
      return dias;
    }, [inicio, fim, n.ganhasLista]);

    const origensComDados = useMemo(() => Object.entries(n.porOrigem)
      .map(([id, dados]) => ({ id, origem: origens.find(o => o.id === id), ...dados }))
      .sort((a, b) => b.receita - a.receita), [n.porOrigem, origens]);

    // Só entra no ranking quem já teve alguma proposta decidida —
    // origem só com negociação em aberto ainda não tem taxa de
    // conversão de verdade pra mostrar, mesma regra da "Taxa de
    // fechamento" geral lá em cima.
    const rankingConversao = useMemo(() => origensComDados
      .map(o => ({ ...o, decididas: o.ganhas + o.perdidas }))
      .filter(o => o.decididas > 0)
      .map(o => ({ ...o, taxa: Math.round((o.ganhas / o.decididas) * 100) }))
      .sort((a, b) => b.taxa - a.taxa)
      .slice(0, 3), [origensComDados]);

    const maxOrigemReceita = Math.max(1, ...origensComDados.map(o => o.receita));
    const vazio = doPeriodo.length === 0;

    return (
      <div style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '16px 20px 32px' }}>
        <div style={{ display: 'flex', gap: 6, marginBottom: 18, flexWrap: 'wrap' }}>
          {PERIODOS_RELATORIO.map(p => (
            <button key={p.id} onClick={() => setPeriodo(p.id)}
              style={{ padding: '6px 12px', borderRadius: 'var(--r-full)', cursor: 'pointer', fontSize: 'var(--fs-sm)',
                fontFamily: 'Roboto,sans-serif', fontWeight: 700,
                background: periodo === p.id ? 'rgba(234,170,65,.15)' : 'var(--overlay-05)',
                border: `1px solid ${periodo === p.id ? 'rgba(234,170,65,.4)' : 'var(--overlay-10)'}`,
                color: periodo === p.id ? 'var(--accent)' : 'var(--text-2)' }}>
              {p.label}
            </button>
          ))}
        </div>

        {vazio ? (
          <div style={{ margin: '40px auto', maxWidth: 420, textAlign: 'center',
            color: 'var(--text-3)', fontSize: 'var(--fs-lg)', lineHeight: 1.7 }}>
            Sem negociação aberta nesse período.
          </div>
        ) : (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
            <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
              <CardKPI label="Faturamento" value={fmtBRL(n.receita)} icon="trending-up" accent />
              <CardKPI label="Propostas fechadas" value={n.ganhas} icon="check" />
              <CardKPI label="Propostas perdidas" value={n.perdidas} icon="x" />
              <CardKPI label="Propostas finalizadas" value={n.decididas} icon="clipboard-check" />
            </div>
            <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
              <CardKPI label="Em aberto" value={fmtBRL(n.emAberto)} icon="hourglass" />
              <CardKPI label="Ticket médio" value={fmtBRL(n.ticket)} icon="receipt" />
            </div>

            <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
              <SecaoRelatorio titulo="Desempenho das propostas" largura={280}>
                <div style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)',
                  marginBottom: 14 }}>
                  Total de {n.decididas + n.abertas}
                </div>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 16 }}>
                  <BarraHorizontal label="Fechadas" valor={n.ganhas}
                    max={Math.max(n.ganhas, n.perdidas, 1)} cor="#4ade80" />
                  <BarraHorizontal label="Perdidas" valor={n.perdidas}
                    max={Math.max(n.ganhas, n.perdidas, 1)} cor="#f87171" />
                </div>
                <div style={{ textAlign: 'center' }}>
                  <div style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif', fontWeight: 900,
                    letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--text-3)' }}>
                    Taxa de fechamento
                  </div>
                  <div style={{ fontSize: 'var(--fs-display)', fontFamily: 'Roboto,sans-serif', fontWeight: 900,
                    color: 'var(--accent)', lineHeight: 1.3 }}>
                    {n.conversao === null ? '—' : `${n.conversao}%`}
                  </div>
                  <div style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)' }}>
                    {n.ganhas} fechada{n.ganhas === 1 ? '' : 's'} / {n.decididas} finalizada{n.decididas === 1 ? '' : 's'}
                  </div>
                </div>
              </SecaoRelatorio>

              <SecaoRelatorio titulo="Origem dos leads" largura={280}>
                {origensComDados.length === 0 ? (
                  <div style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)' }}>
                    Sem origem registrada ainda.
                  </div>
                ) : (
                  <div style={{ display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap' }}>
                    <GraficoPizza fatias={origensComDados.map(o => ({
                      valor: o.total, cor: o.origem?.cor || '#94a3b8' }))} />
                    <div style={{ flex: 1, minWidth: 140, display: 'flex', flexDirection: 'column', gap: 6 }}>
                      {origensComDados.map(o => {
                        const totalLeads = origensComDados.reduce((s, x) => s + x.total, 0);
                        const pct = totalLeads ? Math.round((o.total / totalLeads) * 100) : 0;
                        return (
                          <div key={o.id} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                            <span style={{ width: 7, height: 7, borderRadius: '50%',
                              background: o.origem?.cor || '#94a3b8', flexShrink: 0 }} />
                            <span style={{ flex: 1, fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif',
                              color: 'var(--text-1)', overflow: 'hidden', textOverflow: 'ellipsis',
                              whiteSpace: 'nowrap' }}>
                              {o.origem?.nome || 'Não informada'}
                            </span>
                            <span style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif',
                              color: 'var(--text-3)' }}>{pct}%</span>
                            <span style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif',
                              fontWeight: 700, color: 'var(--text-1)' }}>{fmtBRL(o.receita)}</span>
                          </div>
                        );
                      })}
                    </div>
                  </div>
                )}
              </SecaoRelatorio>

              <SecaoRelatorio titulo="Faturamento por origem" largura={280}>
                {origensComDados.filter(o => o.receita > 0).length === 0 ? (
                  <div style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)' }}>
                    Nenhuma venda fechada nesse período ainda.
                  </div>
                ) : (
                  <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
                    {origensComDados.filter(o => o.receita > 0).map(o => (
                      <BarraHorizontal key={o.id} label={o.origem?.nome || 'Não informada'}
                        valor={o.receita} max={maxOrigemReceita} cor={o.origem?.cor || '#94a3b8'}
                        formatar={fmtBRL} />
                    ))}
                  </div>
                )}
              </SecaoRelatorio>

              <SecaoRelatorio titulo="Ranking de conversão" largura={280}>
                {rankingConversao.length === 0 ? (
                  <div style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)' }}>
                    Sem dado suficiente ainda.
                  </div>
                ) : (
                  <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
                    {rankingConversao.map((o, i) => (
                      <div key={o.id} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                        <span style={{ width: 18, fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif',
                          fontWeight: 900, color: 'var(--text-3)' }}>{i + 1}º</span>
                        <span style={{ width: 7, height: 7, borderRadius: '50%',
                          background: o.origem?.cor || '#94a3b8', flexShrink: 0 }} />
                        <span style={{ flex: 1, fontSize: 'var(--fs-md)', fontFamily: 'Roboto,sans-serif',
                          color: 'var(--text-1)' }}>{o.origem?.nome || 'Não informada'}</span>
                        <span style={{ fontSize: 'var(--fs-md)', fontFamily: 'Roboto,sans-serif', fontWeight: 700,
                          color: 'var(--accent)' }}>
                          {o.taxa}%
                        </span>
                      </div>
                    ))}
                  </div>
                )}
              </SecaoRelatorio>
            </div>

            <SecaoRelatorio titulo="Evolução do faturamento">
              <GraficoLinha pontos={evolucao} />
              <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 4 }}>
                <span style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)' }}>
                  {evolucao[0]?.label}
                </span>
                <span style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)' }}>
                  {evolucao[evolucao.length - 1]?.label}
                </span>
              </div>
            </SecaoRelatorio>

            {/* Por que perdeu */}
            {n.motivos.length > 0 && (
              <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
                <span style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', fontWeight: 900,
                  letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--text-3)' }}>
                  Por que perdeu
                </span>
                {n.motivos.map(([motivo, qtd]) => (
                  <div key={motivo} style={{ display: 'flex', alignItems: 'center', gap: 12,
                    padding: '8px 12px', borderRadius: 'var(--r-md)', background: 'var(--app-surface-2)',
                    border: '1px solid var(--app-border)' }}>
                    <span style={{ flex: 1, fontSize: 'var(--fs-md)', fontFamily: 'Roboto,sans-serif',
                      color: 'var(--text-1)', textTransform: 'capitalize' }}>{motivo}</span>
                    <span style={{ fontSize: 'var(--fs-md)', fontFamily: 'Roboto,sans-serif',
                      fontWeight: 700, color: 'var(--clr-neg)' }}>{qtd}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        )}
      </div>
    );
  }

  /* ── Tela ────────────────────────────────────────────────────*/
  function ConversasScreen() {
    const [contatos, setContatos] = useState([]);
    const [origens, setOrigens] = useState([]);
    const [selecionado, setSelecionado] = useState(null);
    const [mensagens, setMensagens] = useState([]);
    const [ponte, setPonte] = useState({ online: false });
    const [carregando, setCarregando] = useState(true);
    const [rascunho, setRascunho] = useState('');
    const [enviando, setEnviando] = useState(false);
    const [anexos, setAnexos] = useState([]);
    const campoTexto = useRef(null);
    const inputArquivo = useRef(null);
    const [imagemAberta, setImagemAberta] = useState(null);
    const [gravando, setGravando] = useState(false);
    const gravadorRef = useRef(null);

    // Filtros
    const [busca, setBusca] = useState('');
    const [origemFiltro, setOrigemFiltro] = useState('todas');
    const [soNaoLidas, setSoNaoLidas] = useState(false);
    // Arquivada some da lista por padrão, igual ao WhatsApp. Arquivar
    // é o gesto natural de "resolvido", e é sincronizado nos dois
    // sentidos: arquivou no celular, some daqui, e vice-versa.
    const [verArquivadas, setVerArquivadas] = useState(false);
    const [tagFiltro, setTagFiltro] = useState('todas');

    // Abas internas e o que cada uma carrega
    const [aba, setAba] = useState('caixa');
    const [tags, setTags] = useState([]);
    const [tagsPorContato, setTagsPorContato] = useState({});
    const [prontas, setProntas] = useState([]);
    const [categoriasMsg, setCategoriasMsg] = useState([]);
    const [negociacoes, setNegociacoes] = useState([]);
    const [colunasNeg, setColunasNeg] = useState([]);
    const [modalPronta, setModalPronta] = useState(null);
    const [modalNegociacao, setModalNegociacao] = useState(null);
    const [modalColunas, setModalColunas] = useState(false);
    const [scripts, setScripts] = useState([]);
    const [modalScript, setModalScript] = useState(null);
    const [bibliotecaAberta, setBibliotecaAberta] = useState(false);
    // Largura fica guardada: quem ajustou uma vez não quer reajustar
    // toda vez que abre. Mesmo comportamento do MQV.
    const [larguraBiblioteca, setLarguraBiblioteca] = useState(() => {
      const salva = Number(localStorage.getItem('khronus-biblioteca-largura'));
      return salva >= 260 && salva <= 560 ? salva : LARGURA_BIBLIOTECA_PADRAO;
    });

    function ajustarLargura(px) {
      setLarguraBiblioteca(px);
      try { localStorage.setItem('khronus-biblioteca-largura', String(px)); } catch (e) {}
    }
    // Numera cada inscrição de tempo real, pra nenhum canal reusar o
    // nome de outro que ainda esteja sendo removido.
    const contadorCanal = useRef(0);
    const fimDaThread = useRef(null);
    const rascunhosRef = useRef({});

    /* ── Carga da lista ────────────────────────────────────────*/
    async function carregarContatos() {
      const { data: cts } = await window.db
        .from('crm_whatsapp_contatos')
        .select('*')
        .eq('is_spam', false)
        .order('ultima_msg_em', { ascending: false, nullsFirst: false })
        .limit(300);

      const lista = cts || [];

      /* Prévia e não lidas vêm de uma view que olha CADA contato.
         Antes isto era feito aqui, buscando as 1.500 mensagens mais
         recentes de todos juntos: quem não escrevia há um tempo caía
         fora da janela e ficava sem prévia e, pior, com a mensagem não
         lida sumida da contagem. O filtro "não lidas" mentia por
         construção, e ia piorando a cada mensagem nova. */
      if (lista.length) {
        const { data: resumo } = await window.db
          .from('crm_whatsapp_contato_resumo').select('*');

        const previaPor = {};
        const previaDirecaoPor = {};
        const naoLidasPor = {};
        for (const r of resumo || []) {
          previaPor[r.contato_id] = r.previa_corpo || (r.previa_tipo ? `(${r.previa_tipo})` : null);
          previaDirecaoPor[r.contato_id] = r.previa_direcao;
          naoLidasPor[r.contato_id] = r.nao_lidas || 0;
        }
        lista.forEach(c => {
          c.previa = previaPor[c.id] || null;
          c.previa_direcao = previaDirecaoPor[c.id] || null;
          c.nao_lidas = naoLidasPor[c.id] || 0;
        });
      }

      /* Fixado sempre no topo, como no WhatsApp. A ordenação do banco
         é por data; a de fixado é feita aqui porque depende de duas
         chaves e o banco já devolveu tudo que precisamos. */
      lista.sort((a, b) => {
        if (!!a.fixado !== !!b.fixado) return a.fixado ? -1 : 1;
        return (b.ultima_msg_em || '').localeCompare(a.ultima_msg_em || '');
      });

      setContatos(lista);
      setCarregando(false);
    }

    async function carregarPonte() {
      const { data } = await window.db
        .from('crm_whatsapp_ponte_status')
        .select('ultimo_ping, versao_extensao, detalhe')
        .limit(1)
        .maybeSingle();
      const online = data?.ultimo_ping
        ? (Date.now() - new Date(data.ultimo_ping).getTime()) < LIMITE_PONTE_OFFLINE_MS
        : false;
      setPonte({ online, ...data });
    }

    async function carregarOrigens() {
      const { data } = await window.db
        .from('crm_whatsapp_lead_origins')
        .select('id, nome, cor')
        .eq('ativo', true)
        .order('ordem');
      setOrigens(data || []);
    }

    async function carregarTags() {
      const [{ data: lista }, { data: aplicadas }] = await Promise.all([
        window.db.from('crm_whatsapp_tags').select('*').eq('ativo', true).order('ordem'),
        // removida_em nulo = etiqueta ativa agora. O histórico fica
        // guardado, mas quem pinta a tela é só o que está valendo.
        window.db.from('crm_whatsapp_contato_tags')
          .select('contato_id, tag_id').is('removida_em', null),
      ]);
      setTags(lista || []);
      const porContato = {};
      for (const a of aplicadas || []) {
        (porContato[a.contato_id] ||= []).push(a.tag_id);
      }
      setTagsPorContato(porContato);
    }

    async function carregarBiblioteca() {
      const [{ data: msgs }, { data: cats }] = await Promise.all([
        window.db.from('crm_whatsapp_mensagens_prontas').select('*')
          .eq('ativo', true).order('usos', { ascending: false }),
        window.db.from('crm_whatsapp_categorias_mensagem').select('*')
          .eq('ativo', true).order('ordem'),
      ]);
      setProntas(msgs || []);
      setCategoriasMsg(cats || []);
    }

    async function carregarNegociacoes() {
      const { data } = await window.db
        .from('crm_whatsapp_negociacoes')
        .select('*, crm_whatsapp_contatos(id, nome, telefone, avatar_url)')
        .order('iniciada_em', { ascending: false })
        .limit(500);
      setNegociacoes(data || []);
    }

    async function carregarColunasNeg() {
      const { data } = await window.db
        .from('crm_whatsapp_negociacao_colunas')
        .select('*').eq('ativo', true).order('ordem');
      setColunasNeg(data || []);
    }

    async function carregarScripts() {
      const { data } = await window.db
        .from('crm_whatsapp_scripts')
        .select('*, crm_whatsapp_script_mensagens(id, ordem, mensagem_id)')
        .eq('ativo', true).order('ordem');
      setScripts(data || []);
    }

    /* ── Tags ──────────────────────────────────────────────────*/

    /* Etiqueta nova, criada de dentro do popover. Não existe tela
       separada de gestão de propósito: etiqueta se cria no momento em
       que você precisa dela, com o contato na frente. */
    async function criarTag(nome, cor) {
      const { data, error } = await window.db.from('crm_whatsapp_tags')
        .insert({ nome, cor, ordem: (tags.length + 1) * 10 })
        .select('*').single();
      if (error) { alert('Não consegui criar a etiqueta: ' + error.message); return null; }
      setTags(prev => [...prev, data]);
      return data;
    }

    /* Cor da etiqueta em si: muda em todo lugar onde ela aparece, e
       não só neste contato. É o comportamento esperado de etiqueta. */
    async function mudarCorTag(tagId, cor) {
      setTags(prev => prev.map(t => t.id === tagId ? { ...t, cor } : t));
      const { error } = await window.db.from('crm_whatsapp_tags')
        .update({ cor }).eq('id', tagId);
      if (error) { alert('Não consegui mudar a cor: ' + error.message); carregarTags?.(); }
    }

    /* Primeira etiqueta do contato, usada como cor da linha e do avatar.
       Respeita a ordem definida pelo usuário, então a etiqueta que ele
       considera mais importante é a que dá a cor. */
    function primeiraEtiqueta(contatoId) {
      const ids = tagsPorContato[contatoId] || [];
      if (!ids.length) return null;
      return tags.filter(t => ids.includes(t.id))
        .sort((a, b) => (a.ordem ?? 999) - (b.ordem ?? 999))[0] || null;
    }

    /* Aplica uma etiqueta pelo nome. Usado quando a negociação fecha:
       ganha vira Cliente, perdida vira Perdido. Antes isso mexia na
       etapa fixa do contato, que deixou de existir. */
    async function aplicarEtiquetaPorNome(contatoId, nome) {
      const tag = tags.find(t => t.nome.toLowerCase() === nome.toLowerCase())
        || await criarTag(nome, nome === 'Cliente' ? 'var(--clr-pos)' : '#94a3b8');
      if (!tag) return;
      const atuais = tagsPorContato[contatoId] || [];
      if (atuais.includes(tag.id)) return;
      await alternarTag(contatoId, tag.id);
    }

    /* Move a negociação de etapa do funil. É a mesma ação de arrastar
       o card no kanban, então usa o mesmo caminho. */
    async function moverNegociacaoColuna(negociacaoId, colunaId) {
      setNegociacoes(prev => prev.map(n =>
        n.id === negociacaoId ? { ...n, coluna_id: colunaId } : n));
      await window.db.from('crm_whatsapp_negociacoes')
        .update({ coluna_id: colunaId, updated_at: new Date().toISOString() })
        .eq('id', negociacaoId);
    }

    async function alternarTag(contatoId, tagId) {
      const atuais = tagsPorContato[contatoId] || [];
      const tinha = atuais.includes(tagId);

      // Atualiza a tela antes do banco: etiqueta é gesto rápido e
      // esperar ida e volta a cada clique deixa a coisa pastosa.
      setTagsPorContato(prev => ({
        ...prev,
        [contatoId]: tinha ? atuais.filter(t => t !== tagId) : [...atuais, tagId],
      }));

      if (tinha) {
        // Marca a saída em vez de apagar: é isso que preserva o
        // histórico de quando a etiqueta esteve aplicada.
        await window.db.from('crm_whatsapp_contato_tags')
          .update({ removida_em: new Date().toISOString() })
          .eq('contato_id', contatoId).eq('tag_id', tagId).is('removida_em', null);
      } else {
        await window.db.from('crm_whatsapp_contato_tags')
          .insert({ contato_id: contatoId, tag_id: tagId });
      }
    }

    /* ── Modelo → composer ou envio direto ───────────────────────
       Texto vai pro campo de digitar: a pessoa revisa antes de
       mandar. Os outros tipos (imagem, pix, link…) não têm o que
       revisar de digitação, então entram direto na fila, do mesmo
       jeito que um passo de Script entraria. */
    async function usarPronta(msg) {
      // Sem conversa aberta não há [[nome]] pra resolver, então não
      // faz sentido jogar o texto em lugar nenhum.
      if (!selecionado) return;
      if (!msg.tipo || msg.tipo === 'texto') {
        setRascunho(resolverVariaveis(msg.conteudo, selecionado));
      } else {
        const { tipo, corpo, midiaUrl } = montarPassoEnvio(msg, selecionado);
        await window.db.from('crm_whatsapp_fila_envio').insert({
          contato_id: selecionado.id, telefone: selecionado.telefone || null,
          tipo, corpo, midia_url: midiaUrl,
        });
        carregarMensagens(selecionado.id);
      }
      // O painel fica aberto: é comum mandar duas ou três seguidas.
      await window.db.from('crm_whatsapp_mensagens_prontas')
        .update({ usos: (msg.usos || 0) + 1 }).eq('id', msg.id);
      setProntas(prev => prev.map(p => p.id === msg.id ? { ...p, usos: (p.usos || 0) + 1 } : p));
    }

    async function carregarMensagens(contatoId) {
      if (!contatoId) return setMensagens([]);
      const { data: msgs } = await window.db
        .from('crm_whatsapp_mensagens')
        .select('*')
        .eq('contato_id', contatoId)
        .order('created_at', { ascending: true })
        .limit(400);

      // Fila pendente entra como bolha fantasma no fim, pra a pessoa
      // ver o que escreveu mesmo com a ponte offline.
      const { data: fila } = await window.db
        .from('crm_whatsapp_fila_envio')
        .select('id, corpo, criado_em, status')
        .eq('contato_id', contatoId)
        .in('status', ['pendente', 'enviando'])
        .order('criado_em');

      const fantasmas = (fila || []).map(f => ({
        id: 'fila-' + f.id, direcao: 'saida', tipo: 'texto', corpo: f.corpo,
        created_at: f.criado_em, _pendente: true,
      }));

      setMensagens([...(msgs || []), ...fantasmas]);
    }

    async function marcarLidas(contatoId) {
      await window.db
        .from('crm_whatsapp_mensagens')
        .update({ lida_pelo_time: true })
        .eq('contato_id', contatoId)
        .eq('direcao', 'entrada')
        .eq('lida_pelo_time', false);
    }

    /* Arquivar aqui só grava a intenção. A ponte lê, aplica no
       WhatsApp e confirma. Enquanto a marca existe, a reconciliação
       não mexe neste contato, senão o estado antigo do WhatsApp
       desfaria o que acabou de ser feito. */
    async function alternarArquivo(contato) {
      const novo = !contato.arquivado;
      setContatos(prev => prev.map(c =>
        c.id === contato.id ? { ...c, arquivado: novo, arquivar_pendente: novo } : c));
      if (selecionado?.id === contato.id) setSelecionado(null);

      await window.db.from('crm_whatsapp_contatos')
        .update({ arquivado: novo, arquivar_pendente: novo })
        .eq('id', contato.id);
    }

    /* Funde na lista a negociação que o modal acabou de salvar. Se ela
       ainda não está na lista (recém-aberta), entra; se está, atualiza. */
    function negociacaoMudouLocal(neg) {
      if (!neg?.id) return;
      setNegociacoes(prev => {
        const tem = prev.some(n => n.id === neg.id);
        return tem ? prev.map(n => n.id === neg.id ? { ...n, ...neg } : n) : [{ ...neg }, ...prev];
      });
    }

    /* Fixar conversa no topo, como no WhatsApp. Diferente de arquivar,
       fixar não tira nada da lista: só muda a ordem. Por isso mexe
       apenas no banco e na ordenação, sem fechar a conversa aberta. */
    async function alternarFixado(contato) {
      const novo = !contato.fixado;
      setContatos(prev => {
        const lista = prev.map(c => c.id === contato.id ? { ...c, fixado: novo } : c);
        return lista.sort((a, b) => {
          if (!!a.fixado !== !!b.fixado) return a.fixado ? -1 : 1;
          return (b.ultima_msg_em || '').localeCompare(a.ultima_msg_em || '');
        });
      });
      if (selecionado?.id === contato.id) setSelecionado({ ...selecionado, fixado: novo });
      await window.db.from('crm_whatsapp_contatos')
        .update({ fixado: novo }).eq('id', contato.id);
    }

    /* ── Ciclo de vida ─────────────────────────────────────────*/
    useEffect(() => {
      carregarOrigens();
      carregarContatos();
      carregarPonte();
      carregarTags();
      carregarBiblioteca();
      carregarScripts();
      carregarColunasNeg();
      // Negociações carregam sempre, não só na aba delas: o cabeçalho
      // da conversa mostra o que está em aberto com aquele contato,
      // não importa em qual aba a pessoa está.
      carregarNegociacoes();
      const t = setInterval(() => { carregarContatos(); carregarPonte(); }, INTERVALO_RECARGA_MS);

      // Tempo real pras negociações: autosave do modal e arraste no
      // kanban precisam refletir na hora, sem esperar o próximo
      // clique de aba ou o polling de contatos.
      //
      // Nome único por montagem, e não fixo: o Supabase devolve o
      // canal EXISTENTE quando o nome se repete, e chamar .on() num
      // canal já inscrito lança exceção que derruba a tela inteira.
      // Como removeChannel é assíncrono, uma remontagem rápida pega
      // o canal antigo ainda vivo. Sufixo único elimina a corrida.
      const canalNeg = window.db.channel('negociacoes-' + (++contadorCanal.current))
        .on('postgres_changes',
          { event: '*', schema: 'khronus', table: 'crm_whatsapp_negociacoes' },
          () => carregarNegociacoes())
        .subscribe();

      return () => { clearInterval(t); window.db.removeChannel(canalNeg); };
    }, []);

    // Tempo real só na conversa aberta: é onde a espera incomoda.
    useEffect(() => {
      if (!selecionado) return;
      carregarMensagens(selecionado.id);
      marcarLidas(selecionado.id);

      // Mesmo motivo do canal de negociações: nome único por inscrição,
      // senão reabrir a mesma conversa rápido reaproveita um canal já
      // inscrito e o .on() estoura.
      const canal = window.db
        .channel(`thread-${selecionado.id}-${++contadorCanal.current}`)
        .on('postgres_changes',
          { event: '*', schema: 'khronus', table: 'crm_whatsapp_mensagens',
            filter: `contato_id=eq.${selecionado.id}` },
          () => carregarMensagens(selecionado.id))
        .on('postgres_changes',
          { event: '*', schema: 'khronus', table: 'crm_whatsapp_fila_envio',
            filter: `contato_id=eq.${selecionado.id}` },
          () => carregarMensagens(selecionado.id))
        .subscribe();

      return () => { window.db.removeChannel(canal); };
    }, [selecionado?.id]);

    useEffect(() => {
      fimDaThread.current?.scrollIntoView({ behavior: 'smooth' });
    }, [mensagens.length]);

    /* ── Filtro ────────────────────────────────────────────────*/
    /* Busca dentro do CONTEÚDO das mensagens, não só na prévia.
       O campo sempre prometeu "ou texto", mas olhava apenas nome,
       telefone e a última mensagem — procurar uma palavra dita no meio
       da conversa nunca achava nada. A consulta vai ao banco a partir
       de 3 letras, com um respiro pra não disparar a cada tecla. */
    const [achadosPorTexto, setAchadosPorTexto] = useState(null);

    useEffect(() => {
      const termo = busca.trim();
      if (termo.length < 3) { setAchadosPorTexto(null); return; }
      let vivo = true;
      const t = setTimeout(async () => {
        const { data } = await window.db
          .from('crm_whatsapp_mensagens')
          .select('contato_id')
          .ilike('corpo', `%${termo}%`)
          .limit(500);
        if (vivo) setAchadosPorTexto(new Set((data || []).map(m => m.contato_id)));
      }, 350);
      return () => { vivo = false; clearTimeout(t); };
    }, [busca]);

    const filtrados = useMemo(() => {
      const termo = busca.trim().toLowerCase();
      return contatos.filter(c => {
        if (!!c.arquivado !== verArquivadas) return false;
        if (origemFiltro !== 'todas' && c.origem_id !== origemFiltro) return false;
        if (soNaoLidas && !(c.nao_lidas > 0)) return false;
        if (tagFiltro !== 'todas' && !(tagsPorContato[c.id] || []).includes(tagFiltro)) return false;
        if (termo) {
          const alvo = `${c.nome || ''} ${c.telefone || ''} ${c.previa || ''}`.toLowerCase();
          // Casa por dados do contato OU por algo dito na conversa.
          if (!alvo.includes(termo) && !(achadosPorTexto && achadosPorTexto.has(c.id))) return false;
        }
        return true;
      });
    }, [contatos, busca, origemFiltro, soNaoLidas, verArquivadas, tagFiltro, tagsPorContato,
        achadosPorTexto]);

    // Números: cálculo (período, KPIs, gráficos) mora dentro do
    // próprio PainelNumeros agora, porque o filtro de período é
    // estado da tela de relatório, não da conversa.

    // Negociação em aberto do contato selecionado, pra mostrar no
    // cabeçalho perto do nome — sem precisar abrir o modal só pra
    // lembrar o que está sendo negociado.
    const negociacaoAtual = useMemo(() => {
      if (!selecionado) return null;
      return negociacoes.find(n => n.contato_id === selecionado.id && n.status === 'pendente') || null;
    }, [negociacoes, selecionado]);

    const totalArquivadas = useMemo(
      () => contatos.filter(c => c.arquivado).length, [contatos]);

    // Arquivada não entra na conta: arquivar é justamente dizer
    // "resolvido", então não pode continuar cobrando resposta.
    const totalNaoLidas = useMemo(
      () => contatos.reduce((s, c) => s + (c.arquivado ? 0 : (c.nao_lidas || 0)), 0), [contatos]);

    /* ── Gravação de áudio ─────────────────────────────────────
       Duas exigências que só se descobrem na prática, as duas com
       falha silenciosa se ignoradas:

       1. channelCount: 1. Áudio ESTÉREO passa em toda validação
          técnica, e depois o celular de quem recebe recusa tocar,
          dizendo que o áudio não está mais disponível.

       2. Converter pra OGG/Opus antes de subir. O WhatsApp recusa
          webm em áudio de voz: os primeiros envios morreram na fila
          com "InvalidMediaCheckRepairFailedType". E o Chrome não sabe
          gravar ogg direto (isTypeSupported diz não), então a
          conversão tem que ser nossa. Como os dois formatos embalam o
          mesmo codec Opus, é só troca de embalagem, sem reencode. */
    async function alternarGravacao() {
      if (gravando) {
        gravadorRef.current?.stop();
        return;
      }
      try {
        const trilha = await navigator.mediaDevices.getUserMedia({
          audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true },
        });
        const pedacos = [];
        const gravador = new MediaRecorder(trilha, { mimeType: 'audio/webm' });

        gravador.ondataavailable = (e) => { if (e.data.size) pedacos.push(e.data); };
        gravador.onstop = async () => {
          trilha.getTracks().forEach(t => t.stop());
          setGravando(false);
          if (!pedacos.length) return;
          const webm = new Blob(pedacos, { type: 'audio/webm' });
          try {
            const ogg = await window.webmOpusParaOgg(webm);
            setAnexos([new File([ogg], `audio-${Date.now()}.ogg`, { type: 'audio/ogg' })]);
          } catch (e) {
            // Sem conversão o envio falharia na fila sem o usuário
            // entender por quê. Melhor recusar aqui, explicando.
            alert('Não consegui preparar o áudio para o WhatsApp: '
              + (e?.message || e) + '\nTente gravar de novo.');
          }
        };

        gravadorRef.current = gravador;
        gravador.start();
        setGravando(true);
      } catch (e) {
        alert('Não consegui acessar o microfone: ' + (e?.message || e));
      }
    }

    /* ── Ações ─────────────────────────────────────────────────*/
    function selecionar(c) {
      if (selecionado) rascunhosRef.current[selecionado.id] = rascunho;
      setSelecionado(c);
      setRascunho(rascunhosRef.current[c.id] || '');
    }

    function tipoPeloMime(mime) {
      const m = String(mime || '');
      if (m.startsWith('image/')) return 'imagem';
      if (m.startsWith('audio/')) return 'audio';
      if (m.startsWith('video/')) return 'video';
      return 'documento';
    }

    /* Vários arquivos de uma vez. Cada um vira uma mensagem própria no
       WhatsApp, que é como ele funciona: não existe "uma mensagem com
       cinco fotos". A legenda digitada acompanha só a primeira, também
       igual ao WhatsApp. */
    async function escolherArquivo(ev) {
      const escolhidos = Array.from(ev.target.files || []);
      ev.target.value = '';  // permite escolher o mesmo arquivo de novo
      if (!escolhidos.length) return;

      const grandes = escolhidos.filter(a => a.size > 45 * 1024 * 1024);
      if (grandes.length) {
        alert(grandes.length === 1
          ? `"${grandes[0].name}" passa de 45 MB e não vai.`
          : `${grandes.length} arquivos passam de 45 MB e não vão.`);
      }
      const cabem = escolhidos.filter(a => a.size <= 45 * 1024 * 1024);
      if (cabem.length) setAnexos(prev => [...prev, ...cabem]);
    }

    function tirarAnexo(i) {
      setAnexos(prev => prev.filter((_, idx) => idx !== i));
    }

    /* Insere o emoji onde o cursor está, não no fim: quem escreveu
       "combinado , te mando amanhã" quer a carinha no meio da frase. */
    function inserirEmoji(e) {
      const el = campoTexto.current;
      if (!el) { setRascunho(r => r + e); return; }
      const ini = el.selectionStart ?? rascunho.length;
      const fim = el.selectionEnd ?? rascunho.length;
      const novo = rascunho.slice(0, ini) + e + rascunho.slice(fim);
      setRascunho(novo);
      requestAnimationFrame(() => {
        el.focus();
        el.setSelectionRange(ini + e.length, ini + e.length);
      });
    }

    async function enviar() {
      const texto = rascunho.trim();
      if ((!texto && !anexos.length) || !selecionado || enviando) return;
      setEnviando(true);

      /* Sem anexo: uma linha de texto e pronto. */
      const itens = [];
      if (!anexos.length) {
        itens.push({ tipo: 'texto', corpo: texto || null, midia_url: null, midia_thumb: null });
      } else {
        for (let i = 0; i < anexos.length; i++) {
          const arquivo = anexos[i];
          const thumb = await gerarMiniatura(arquivo);
          // Sobe pro armazenamento porque é de lá que a ponte lê pra
          // mandar. É passagem, não arquivo: depois de entregue, a
          // ponte apaga.
          const seguro = arquivo.name.replace(/[^a-zA-Z0-9._-]/g, '_');
          const caminho = `saida/${Date.now()}_${i}_${seguro}`;
          const { error: erroUp } = await window.db.storage
            .from('whatsapp-midia')
            .upload(caminho, arquivo, { contentType: arquivo.type || undefined, upsert: false });
          if (erroUp) {
            setEnviando(false);
            alert(`Não consegui subir "${arquivo.name}": ` + erroUp.message);
            return;
          }
          itens.push({
            tipo: tipoPeloMime(arquivo.type),
            // Legenda só no primeiro, igual ao WhatsApp.
            corpo: i === 0 ? (texto || null) : null,
            midia_url: caminho,
            midia_thumb: thumb,
          });
        }
      }

      // Só o contato_id: o destino real (wa_chat_id) a ponte busca no
      // contato na hora de enviar. Guardar o destino aqui daria uma
      // segunda cópia da verdade, que envelhece.
      //
      // agendado_para escalonado em 1 segundo por item: a ponte manda na
      // ordem em que você escolheu, e não numa rajada simultânea.
      const agora = Date.now();
      const { error } = await window.db.from('crm_whatsapp_fila_envio').insert(
        itens.map((item, i) => ({
          contato_id: selecionado.id,
          telefone: selecionado.telefone || null,
          agendado_para: i === 0 ? null : new Date(agora + i * 1000).toISOString(),
          ...item,
        })));
      setEnviando(false);
      if (error) { alert('Não consegui enfileirar: ' + error.message); return; }
      setRascunho('');
      setAnexos([]);
      rascunhosRef.current[selecionado.id] = '';
      carregarMensagens(selecionado.id);
    }

    // Reflexo local de uma etapa que mudou de fora (ex: negociação
    // fechada como ganha/perdida) — a tela não espera o próximo
    // polling pra mostrar a etapa nova.
    // Chamado quando a negociação fecha. Recebe o NOME da etiqueta a
    // aplicar (Cliente ou Perdido), não mais uma etapa fixa.
    function mudarContatoLocal(contatoId, nomeEtiqueta) {
      if (!nomeEtiqueta) return;
      aplicarEtiquetaPorNome(contatoId, nomeEtiqueta);
    }

    /* ── Colunas do kanban de negociação ─────────────────────────*/
    async function criarColuna(nome) {
      if (!nome.trim()) return;
      const ordem = (colunasNeg[colunasNeg.length - 1]?.ordem ?? 0) + 1;
      await window.db.from('crm_whatsapp_negociacao_colunas').insert({ nome: nome.trim(), ordem });
      carregarColunasNeg();
    }
    async function editarColuna(id, campos) {
      setColunasNeg(prev => prev.map(c => c.id === id ? { ...c, ...campos } : c));
      await window.db.from('crm_whatsapp_negociacao_colunas').update(campos).eq('id', id);
    }
    async function excluirColuna(id) {
      const emUso = negociacoes.some(n => n.coluna_id === id && n.status === 'pendente');
      if (emUso && !confirm(
        'Esta coluna tem negociação em aberto. Excluir mesmo assim? As negociações ficam sem coluna.')) return;
      await window.db.from('crm_whatsapp_negociacao_colunas').delete().eq('id', id);
      carregarColunasNeg();
    }
    async function moverColuna(id, direcao) {
      const ordenadas = [...colunasNeg].sort((a, b) => (a.ordem ?? 0) - (b.ordem ?? 0));
      const i = ordenadas.findIndex(c => c.id === id);
      const j = i + direcao;
      if (i < 0 || j < 0 || j >= ordenadas.length) return;
      const a = ordenadas[i], b = ordenadas[j];
      setColunasNeg(prev => prev.map(c => {
        if (c.id === a.id) return { ...c, ordem: b.ordem };
        if (c.id === b.id) return { ...c, ordem: a.ordem };
        return c;
      }));
      await Promise.all([
        window.db.from('crm_whatsapp_negociacao_colunas').update({ ordem: b.ordem }).eq('id', a.id),
        window.db.from('crm_whatsapp_negociacao_colunas').update({ ordem: a.ordem }).eq('id', b.id),
      ]);
    }
    async function moverNegociacaoColuna(negId, colunaId) {
      setNegociacoes(prev => prev.map(n => n.id === negId ? { ...n, coluna_id: colunaId } : n));
      await window.db.from('crm_whatsapp_negociacoes').update({ coluna_id: colunaId }).eq('id', negId);
    }

    /* ── Disparar um script na conversa aberta ───────────────────
       Cada passo entra na fila já com o horário calculado (soma dos
       atrasos anteriores), e a simulação de digitando/gravando é
       decidida por passo — quem manda de fato, no ritmo certo, é a
       ponte lendo agendado_para e simular. */
    async function usarScript(script) {
      if (!selecionado) return;
      const passos = (script.crm_whatsapp_script_mensagens || [])
        .slice().sort((a, b) => (a.ordem ?? 0) - (b.ordem ?? 0))
        .map(p => prontas.find(m => m.id === p.mensagem_id))
        .filter(Boolean);
      if (!passos.length) return alert('Este script não tem mensagens cadastradas.');

      let acumulado = 0;
      for (const passo of passos) {
        acumulado += passo.enviar_apos_segundos || 0;
        const agendadoPara = acumulado > 0
          ? new Date(Date.now() + acumulado * 1000).toISOString() : null;
        const { tipo, corpo, midiaUrl } = montarPassoEnvio(passo, selecionado);
        await window.db.from('crm_whatsapp_fila_envio').insert({
          contato_id: selecionado.id, telefone: selecionado.telefone || null,
          tipo, corpo, midia_url: midiaUrl,
          agendado_para: agendadoPara,
          simular: passo.simular_digitando ? (tipo === 'audio' ? 'gravando' : 'digitando') : null,
        });
      }
      carregarMensagens(selecionado.id);
    }

    /* ── Render ────────────────────────────────────────────────*/
    const estiloFiltro = {
      padding: '4px 10px', borderRadius: 'var(--r-sm)', fontSize: 'var(--fs-sm)',
      fontFamily: 'Roboto,sans-serif', fontWeight: 700,
      background: 'var(--overlay-05)', border: '1px solid var(--overlay-10)',
      color: 'var(--text-2)', outline: 'none', cursor: 'pointer',
    };

    /* Thread + Modelos: o mesmo painel de conversa, usado tanto na
       Caixa de entrada quanto na visão Kanban de Negociações — é o
       que evita ter uma tela nova (ou um drawer) só pra abrir o chat
       a partir de um card do kanban. */
    function renderThreadEBiblioteca() {
      return (
        <>
          <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
            {!selecionado ? (
              <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center',
                color: 'var(--text-3)', fontSize: 'var(--fs-lg)' }}>
                Escolha uma conversa
              </div>
            ) : (
              <>
                {/* Cabeçalho da conversa. Tags e etapa moraram numa
                    faixa própria; agora são um botão só, que abre um
                    popover — a faixa de baixo deixou de existir, e a
                    negociação subiu pra linha do nome. */}
                <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '12px 20px',
                  borderBottom: '1px solid var(--app-border)', flexShrink: 0 }}>
                  <AvatarContato contato={selecionado}
                    cor={primeiraEtiqueta(selecionado.id)?.cor} size={34} />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
                      <span style={{ fontSize: 'var(--fs-lg)', fontFamily: 'Roboto,sans-serif', fontWeight: 700,
                        color: 'var(--text-1)' }}>
                        {rotuloContato(selecionado)}
                      </span>
                      {negociacaoAtual ? (
                        <button onClick={() => setModalNegociacao({ item: negociacaoAtual })}
                          title="Ver ou editar esta negociação"
                          style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '4px 8px',
                            borderRadius: 'var(--r-full)', cursor: 'pointer', background: 'rgba(234,170,65,.14)',
                            border: '1px solid rgba(234,170,65,.35)', color: 'var(--accent)',
                            fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif', fontWeight: 700 }}>
                          <LucideIcon icon="handshake" size={11} />
                          {negociacaoAtual.titulo || 'Negociação'} · {fmtBRL(negociacaoAtual.valor)}
                        </button>
                      ) : (
                        <button onClick={() => setModalNegociacao({ contato: selecionado })}
                          title="Registrar orçamento ou negociação"
                          style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '4px 8px',
                            borderRadius: 'var(--r-full)', cursor: 'pointer', background: 'var(--overlay-05)',
                            border: '1px solid var(--overlay-10)', color: 'var(--text-3)',
                            fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif', fontWeight: 700 }}>
                          <LucideIcon icon="handshake" size={11} />
                          Abrir negociação
                        </button>
                      )}
                    </div>
                    <div style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', color: 'var(--text-3)' }}>
                      {selecionado.is_grupo ? 'Grupo'
                        : selecionado.telefone ? formatarTelefone(selecionado.telefone)
                        : 'sem número conhecido'}
                    </div>
                  </div>
                  <BotaoTagsEtapa contato={selecionado} tags={tags}
                    tagsAtivas={tagsPorContato[selecionado.id] || []}
                    onAlternarTag={tagId => alternarTag(selecionado.id, tagId)}
                    colunas={colunasNeg} negociacao={negociacaoAtual}
                    onMoverColuna={moverNegociacaoColuna}
                    onCriarTag={criarTag} onMudarCorTag={mudarCorTag} />
                  <button onClick={() => alternarFixado(selecionado)}
                    title={selecionado.fixado ? 'Desafixar conversa' : 'Fixar no topo'}
                    style={{ background: 'none', border: 'none', cursor: 'pointer',
                      display: 'flex', flexShrink: 0,
                      color: selecionado.fixado ? 'var(--accent)' : 'var(--text-3)' }}>
                    <LucideIcon icon="pin" size={16} />
                  </button>
                  <button onClick={() => alternarArquivo(selecionado)}
                    title={selecionado.arquivado
                      ? 'Desarquivar aqui e no WhatsApp'
                      : 'Arquivar aqui e no WhatsApp'}
                    style={{ ...estiloFiltro, display: 'flex', alignItems: 'center', gap: 4 }}>
                    <LucideIcon icon={selecionado.arquivado ? 'archive-restore' : 'archive'} size={13} />
                    {selecionado.arquivado ? 'Desarquivar' : 'Arquivar'}
                  </button>
                </div>

                {/* Mensagens */}
                <div style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '16px 20px',
                  display: 'flex', flexDirection: 'column', gap: 8 }}>
                  {mensagens.map((m, i) => {
                    const anterior = mensagens[i - 1];
                    const mudouDeDia = !anterior ||
                      new Date(m.created_at).toDateString() !== new Date(anterior.created_at).toDateString();
                    return (
                      <React.Fragment key={m.id}>
                        {mudouDeDia && (
                          <div style={{ alignSelf: 'center', margin: '6px 0', padding: '4px 10px',
                            borderRadius: 'var(--r-full)', background: 'var(--overlay-05)', fontSize: 'var(--fs-xs)',
                            fontFamily: 'Roboto,sans-serif', fontWeight: 700, color: 'var(--text-3)' }}>
                            {dataCompleta(m.created_at)}
                          </div>
                        )}
                        <Bolha msg={m} onAbrirImagem={setImagemAberta} />
                      </React.Fragment>
                    );
                  })}
                  {mensagens.length === 0 && (
                    <div style={{ margin: 'auto', color: 'var(--text-3)', fontSize: 'var(--fs-md)' }}>
                      Nenhuma mensagem nesta conversa ainda.
                    </div>
                  )}
                  <div ref={fimDaThread} />
                </div>

                {/* Composer */}
                <div style={{ flexShrink: 0, borderTop: '1px solid var(--app-border)', padding: '12px 20px' }}>
                  {!ponte.online && (
                    <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 9,
                      padding: '6px 10px', borderRadius: 'var(--r-sm)', background: 'rgba(248,113,113,.08)',
                      border: '1px solid rgba(248,113,113,.2)' }}>
                      <LucideIcon icon="alert-triangle" size={13} style={{ color: 'var(--clr-neg)' }} />
                      <span style={{ fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif', color: 'var(--clr-neg)',
                        lineHeight: 1.5 }}>
                        A ponte está offline. Pode escrever: fica na fila e sai assim que você
                        abrir a aba do WhatsApp Web.
                      </span>
                    </div>
                  )}
                  {/* Anexos escolhidos, antes de enviar */}
                  {anexos.length > 0 && (
                    <div style={{ display: 'flex', flexDirection: 'column', gap: 4,
                      marginBottom: 9 }}>
                      {anexos.map((a, i) => (
                        <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8,
                          padding: '8px 10px', borderRadius: 'var(--r-sm)',
                          background: 'var(--overlay-05)', border: '1px solid var(--app-border)' }}>
                          <LucideIcon icon={a.type?.startsWith('audio/') ? 'mic' : 'paperclip'}
                            size={13} style={{ color: 'var(--accent)' }} />
                          <span style={{ flex: 1, fontSize: 'var(--fs-md)',
                            fontFamily: 'Roboto,sans-serif', color: 'var(--text-1)',
                            whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                            {a.type?.startsWith('audio/') ? 'Áudio gravado' : a.name}
                          </span>
                          {/* A legenda vai só com o primeiro, então vale
                              dizer qual é o primeiro. */}
                          {i === 0 && anexos.length > 1 && rascunho.trim() && (
                            <span style={{ fontSize: 'var(--fs-xs)',
                              fontFamily: 'Roboto,sans-serif', color: 'var(--accent)' }}>
                              com a legenda
                            </span>
                          )}
                          <span style={{ fontSize: 'var(--fs-xs)', fontFamily: 'Roboto,sans-serif',
                            color: 'var(--text-3)' }}>
                            {(a.size / 1024 / 1024).toFixed(1)} MB
                          </span>
                          <button onClick={() => tirarAnexo(i)} title="Tirar este anexo"
                            style={{ background: 'none', border: 'none', cursor: 'pointer',
                              color: 'var(--text-3)', display: 'flex' }}>
                            <LucideIcon icon="x" size={14} />
                          </button>
                        </div>
                      ))}
                    </div>
                  )}

                  {/* Ordem: anexo à esquerda, campo de texto, e à
                      direita — perto do Enviar — áudio e biblioteca,
                      igual ao WhatsApp de verdade. */}
                  <div style={{ display: 'flex', gap: 8, alignItems: 'flex-end' }}>
                    <input type="file" multiple ref={inputArquivo} onChange={escolherArquivo}
                      style={{ display: 'none' }} />
                    <SeletorEmoji onEscolher={inserirEmoji} />
                    <button onClick={() => inputArquivo.current?.click()}
                      title="Anexar imagem, vídeo ou documento (pode escolher vários)"
                      style={{ height: 40, width: 40, flexShrink: 0, borderRadius: 'var(--r-md)',
                        background: 'var(--app-surface-2)', border: '1px solid var(--app-border)',
                        color: 'var(--text-2)', cursor: 'pointer', display: 'flex',
                        alignItems: 'center', justifyContent: 'center' }}>
                      <LucideIcon icon="paperclip" size={16} />
                    </button>
                    <textarea ref={campoTexto} value={rascunho}
                      onChange={e => setRascunho(e.target.value)}
                      onKeyDown={e => {
                        if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); enviar(); }
                      }}
                      placeholder={anexos.length
                        ? 'Legenda (opcional, vai com o primeiro). Enter envia.'
                        : 'Escreva a mensagem. Enter envia, Shift+Enter quebra linha.'}
                      rows={1}
                      style={{ flex: 1, resize: 'none', minHeight: 40, maxHeight: 140,
                        padding: '10px 12px', borderRadius: 'var(--r-md)', background: 'var(--app-surface-2)',
                        border: '1px solid var(--app-border)', color: 'var(--text-1)',
                        fontFamily: 'Roboto,sans-serif', fontSize: 'var(--fs-lg)', outline: 'none',
                        lineHeight: 1.5 }} />
                    <button onClick={alternarGravacao}
                      title={gravando ? 'Parar de gravar' : 'Gravar áudio'}
                      style={{ height: 40, width: 40, flexShrink: 0, borderRadius: 'var(--r-md)',
                        background: gravando ? 'rgba(248,113,113,.18)' : 'var(--app-surface-2)',
                        border: `1px solid ${gravando ? 'rgba(248,113,113,.5)' : 'var(--app-border)'}`,
                        color: gravando ? 'var(--clr-neg)' : 'var(--text-2)', cursor: 'pointer',
                        display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                      <LucideIcon icon={gravando ? 'square' : 'mic'} size={16} />
                    </button>
                    <button onClick={() => setBibliotecaAberta(v => !v)}
                      title="Modelos de mensagem"
                      style={{ height: 40, width: 40, flexShrink: 0, borderRadius: 'var(--r-md)',
                        background: bibliotecaAberta ? 'rgba(234,170,65,.15)' : 'var(--app-surface-2)',
                        border: `1px solid ${bibliotecaAberta ? 'rgba(234,170,65,.4)' : 'var(--app-border)'}`,
                        color: bibliotecaAberta ? 'var(--accent)' : 'var(--text-2)',
                        cursor: 'pointer', display: 'flex', alignItems: 'center',
                        justifyContent: 'center' }}>
                      <LucideIcon icon="book-text" size={16} />
                    </button>
                    <Btn variant="primary" onClick={enviar}
                      disabled={enviando || (!rascunho.trim() && !anexos.length)} icon="send">
                      {enviando ? 'Enviando…' : 'Enviar'}
                    </Btn>
                  </div>
                </div>
              </>
            )}
          </div>

          {bibliotecaAberta && (
            <PainelBiblioteca prontas={prontas} categorias={categoriasMsg} scripts={scripts}
              contato={selecionado} largura={larguraBiblioteca}
              onUsar={usarPronta}
              onNovo={() => setModalPronta({})}
              onEditar={m => setModalPronta(m)}
              onNovoScript={() => setModalScript({})}
              onEditarScript={s => setModalScript(s)}
              onUsarScript={usarScript}
              onFechar={() => setBibliotecaAberta(false)}
              onRedimensionar={ajustarLargura} />
          )}
        </>
      );
    }

    return (
      <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, overflow: 'hidden' }}>
        <TopBar title="Atendimentos" actions={
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            {totalNaoLidas > 0 && (
              <Badge tone="success">{totalNaoLidas} sem responder</Badge>
            )}
            <IndicadorPonte status={ponte} />
          </div>
        } />

        {/* Abas internas. Biblioteca, Negociações e Números são
            periféricos: o fluxo do dia é atender a conversa, então eles
            não merecem item próprio no menu lateral. */}
        <div style={{ display: 'flex', gap: 4, padding: '8px 20px 0',
          borderBottom: '1px solid var(--app-border)' }}>
          {ABAS.map(a => {
            const ativa = aba === a.id;
            return (
              <button key={a.id} onClick={() => setAba(a.id)}
                style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 12px',
                  border: 'none', borderBottom: `2px solid ${ativa ? 'var(--accent)' : 'transparent'}`,
                  background: 'transparent', cursor: 'pointer', marginBottom: -1,
                  fontSize: 'var(--fs-md)', fontFamily: 'Roboto,sans-serif', fontWeight: 700,
                  color: ativa ? 'var(--accent)' : 'var(--text-3)' }}>
                <LucideIcon icon={a.icon} size={14} />
                {a.label}
              </button>
            );
          })}
        </div>

        {aba === 'negociacoes' && (
          <>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 20px',
              borderBottom: '1px solid var(--app-border)' }}>
              <button onClick={() => setModalColunas(true)}
                style={{ ...estiloFiltro, marginLeft: 'auto', display: 'flex', alignItems: 'center',
                  gap: 4 }}>
                <LucideIcon icon="settings" size={12} />Etapas
              </button>
            </div>
            {
              /* Mesma estrutura lista+thread da Caixa de entrada — só
                 a lista muda, agrupada em barras por coluna em vez de
                 ordenada por última mensagem. Clicar num card usa o
                 mesmo `selecionar` de sempre, então a thread ao lado é
                 a mesma, com Modelos, tags, tudo. */
              <div style={{ flex: 1, minHeight: 0, display: 'flex', overflow: 'hidden' }}>
                <div style={{ width: 300, flexShrink: 0, borderRight: '1px solid var(--app-border)',
                  display: 'flex', flexDirection: 'column', minHeight: 0 }}>
                  <ListaNegociacoesPorColuna negociacoes={negociacoes} colunas={colunasNeg}
                    contatos={contatos} selecionadoId={selecionado?.id}
                    onSelecionar={selecionar}
                    onEditar={n => setModalNegociacao({ item: n })}
                    onMover={moverNegociacaoColuna} />
                </div>
                {renderThreadEBiblioteca()}
              </div>
            }
          </>
        )}
        {aba === 'analytics' && <PainelNumeros negociacoes={negociacoes} origens={origens} />}

        {aba === 'caixa' && <>
        {/* Filtros */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
          padding: '10px 20px', borderBottom: '1px solid var(--app-border)' }}>
          <div style={{ position: 'relative', flex: '1 1 220px', maxWidth: 320 }}>
            <span style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
              display: 'flex', pointerEvents: 'none' }}>
              <LucideIcon icon="search" size={13} style={{ color: 'var(--text-3)' }} />
            </span>
            <input value={busca} onChange={e => setBusca(e.target.value)}
              placeholder="Buscar por nome, telefone ou texto"
              style={{ ...estiloFiltro, width: '100%', boxSizing: 'border-box',
                paddingLeft: 30, fontWeight: 400, cursor: 'text', color: 'var(--text-1)' }} />
          </div>

          <select value={origemFiltro} onChange={e => setOrigemFiltro(e.target.value)} style={estiloFiltro}>
            <option value="todas">Todas as origens</option>
            {origens.map(o => <option key={o.id} value={o.id}>{o.nome}</option>)}
          </select>

          <select value={tagFiltro} onChange={e => setTagFiltro(e.target.value)} style={estiloFiltro}>
            <option value="todas">Todas as etiquetas</option>
            {tags.map(t => <option key={t.id} value={t.id}>{t.nome}</option>)}
          </select>

          <button onClick={() => setSoNaoLidas(v => !v)}
            style={{ ...estiloFiltro,
              background: soNaoLidas ? 'rgba(74,222,128,.15)' : 'var(--overlay-05)',
              border: `1px solid ${soNaoLidas ? 'rgba(74,222,128,.4)' : 'var(--overlay-10)'}`,
              color: soNaoLidas ? 'var(--clr-pos)' : 'var(--text-2)' }}>
            Sem responder
          </button>

          <button onClick={() => { setVerArquivadas(v => !v); setSelecionado(null); }}
            title="Arquivamento é sincronizado com o WhatsApp nos dois sentidos"
            style={{ ...estiloFiltro, display: 'flex', alignItems: 'center', gap: 4,
              background: verArquivadas ? 'rgba(148,163,184,.18)' : 'var(--overlay-05)',
              border: `1px solid ${verArquivadas ? 'rgba(148,163,184,.45)' : 'var(--overlay-10)'}`,
              color: verArquivadas ? 'var(--text-1)' : 'var(--text-2)' }}>
            <LucideIcon icon="archive" size={12} />
            Arquivadas{totalArquivadas > 0 ? ` (${totalArquivadas})` : ''}
          </button>

          {(busca || origemFiltro !== 'todas' || soNaoLidas || tagFiltro !== 'todas') && (
            <button onClick={() => { setBusca(''); setOrigemFiltro('todas');
              setSoNaoLidas(false); setTagFiltro('todas'); }}
              style={{ ...estiloFiltro, color: 'var(--text-3)' }}>
              Limpar
            </button>
          )}

          <span style={{ marginLeft: 'auto', fontSize: 'var(--fs-sm)', fontFamily: 'Roboto,sans-serif',
            color: 'var(--text-3)' }}>
            {filtrados.length} de {contatos.length}
          </span>
        </div>

        {/* Corpo: lista + thread + biblioteca (à direita, quando aberta) */}
        <div style={{ flex: 1, minHeight: 0, display: 'flex', overflow: 'hidden' }}>

          {/* Lista */}
          <div style={{ width: 330, flexShrink: 0, borderRight: '1px solid var(--app-border)',
            display: 'flex', flexDirection: 'column', minHeight: 0 }}>
            <div style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '8px 8px 16px',
              display: 'flex', flexDirection: 'column', gap: 4 }}>
              {carregando ? (
                <div style={{ padding: 30, textAlign: 'center', color: 'var(--text-3)', fontSize: 'var(--fs-md)' }}>
                  Carregando…
                </div>
              ) : filtrados.length === 0 ? (
                <div style={{ padding: '30px 20px', textAlign: 'center', color: 'var(--text-3)',
                  fontSize: 'var(--fs-md)', lineHeight: 1.6 }}>
                  {contatos.length === 0
                    ? 'Nenhuma conversa ainda. Assim que alguém te escrever no WhatsApp, aparece aqui.'
                    : 'Nada com esse filtro.'}
                </div>
              ) : filtrados.map(c => (
                <ItemConversa key={c.id} contato={c}
                  ativo={selecionado?.id === c.id}
                  etiqueta={primeiraEtiqueta(c.id)}
                  onClick={() => selecionar(c)} />
              ))}
            </div>
          </div>

          {renderThreadEBiblioteca()}
        </div>
        </>}

        {imagemAberta && (
          <Lightbox url={imagemAberta} onClose={() => setImagemAberta(null)} />
        )}
        {modalPronta && (
          <ModalMensagemPronta item={modalPronta.id ? modalPronta : null}
            categorias={categoriasMsg} onClose={() => setModalPronta(null)}
            onSalvo={() => { setModalPronta(null); carregarBiblioteca(); }} />
        )}
        {modalScript && (
          <ModalScript item={modalScript.id ? modalScript : null}
            categorias={categoriasMsg} prontas={prontas} onClose={() => setModalScript(null)}
            onSalvo={() => { setModalScript(null); carregarScripts(); }} />
        )}
        {modalNegociacao && (
          <ModalNegociacao item={modalNegociacao.item} contato={modalNegociacao.contato}
            origens={origens} colunas={colunasNeg}
            onContatoMudou={mudarContatoLocal}
            onNegociacaoMudou={negociacaoMudouLocal}
            onClose={() => { setModalNegociacao(null); carregarContatos(); }} />
        )}
        {modalColunas && (
          <ModalColunas colunas={colunasNeg} onClose={() => setModalColunas(false)}
            onCriar={criarColuna} onEditar={editarColuna}
            onExcluir={excluirColuna} onMover={moverColuna} />
        )}
      </div>
    );
  }

  window.ConversasScreen = ConversasScreen;
})();
