/**
 * A minha marca — o que a NetScreen sabe da tua marca, o que falta, e de quem é a vez.
 *
 * Substitui o Conhecimento (23 Set). Desde 24 Set, o primeiro separador junta
 * a antiga Biblioteca e as Fontes em "Ficheiros e fontes" (MarcaFicheiros): uma
 * entrada, vários ficheiros de uma vez, e o estado da leitura em cada linha.
 * Os factos e o dossiê vêm em separadores, carregados só quando se abrem.
 * Ver docs/specs/2026-09-23-a-minha-marca-design.md e
 * docs/avaliacoes/2026-09-24-marca-ficheiros-e-voz.md.
 */
/**
 * Os grupos dos dados em "É esta a tua marca?" (parecer do Codex, 28 Set: 100
 * dados numa lista com scroll não são 100 dados vistos). Por palavras, na
 * ordem em que se testam; o que não cai em nenhum é "Quem é a marca".
 */
const GRUPOS_DE_DADOS = [
  { chave: 'alegacoes', titulo: 'O que a marca afirma de si', ajuda: 'Com prova, a NetScreen revê antes de entrar nas campanhas.',
    // (?<!\p{L}) e não \b: o \b não conhece o "ú" de "única" (revisor, 24 Set).
    e: (f) => Boolean(f.pede_prova) || /(?<!\p{L})(melhor|maior|únic[oa]|premiad[oa]s?|certificad[oa]s?(?!\s+de\s+oferta)|garant\p{L}*|n\.?º ?1|l[íi]der)(?!\p{L})/iu.test(f.afirmacao) },
  { chave: 'lojas', titulo: 'As lojas e os horários',
    // Sem as horas soltas: "o pão sai às 6h" é da padaria, não das lojas (verificação, 28 Set).
    e: (f) => /\b(lojas?|abre|aberta|horário|morada|rua|avenida|praça)\b/i.test(f.afirmacao) },
  { chave: 'servicos', titulo: 'Os serviços',
    e: (f) => /\b(servi[cç]os?|entregas?|encomendas?|cart[ãa]o|consultas?|atendimento|programa|reservas?|oficina)\b/i.test(f.afirmacao) },
  { chave: 'produtos', titulo: 'O que vende',
    e: (f) => /\b(vende|produtos?|gamas?|inclui|fabrica|produz|coleção|colecção|menu|cabaz)\b/i.test(f.afirmacao) },
  { chave: 'marca', titulo: 'Quem é a marca', e: () => true },
];

function agruparDados(dados) {
  const grupos = GRUPOS_DE_DADOS.map((g) => ({ ...g, dados: [] }));
  for (const d of dados) grupos.find((g) => g.e(d)).dados.push(d);
  return grupos.filter((g) => g.dados.length);
}

/** Uma citação entre « », sem dobrar as aspas que o texto já traz (ensaio de 6 Out: ««…»»). */
const entreAspas = (t) => `«${String(t || '').replace(/^["“«']+|["”»']+$/g, '')}»`;

const SEPARADORES_MARCA = [
  // A marca inteira numa página, com uma confirmação só (28 Set), e a entrada
  // quando já há ficheiros. Identidade e Dados ficam para corrigir em pormenor.
  { chave: 'resumo', rotulo: 'É esta a tua marca?' },
  { chave: 'fontes', rotulo: 'Ficheiros' },
  { chave: 'identidade', rotulo: 'Identidade' },
  { chave: 'factos', rotulo: 'Dados da marca' },
];

/**
 * "É esta a tua marca?" (28 Set). Os quatro agentes que avaliaram a app
 * convergiram: confirmar a marca estava espalhado por três separadores e três
 * botões, e o cliente decidia antes de ver o resultado. Aqui vê-se a marca num
 * painel de exemplo, a voz em três linhas e os dados que as campanhas vão
 * poder dizer, e confirma-se de uma vez o que falta. Cada parte continua a ir
 * pela sua rota, com as suas garantias (o kit, a voz, os factos com a versão
 * que se viu); os separadores ao lado ficam para corrigir em pormenor.
 */
function MarcaResumo({ slug, podeMexer, onMudou, onNavegar, onPropor, aPropor, avisoProposta = null, lidos = false, criarActivo = false }) {
  const [kitD, setKitD] = React.useState(null);
  const [vozD, setVozD] = React.useState(null);
  const [factosD, setFactosD] = React.useState(null);
  const [fora, setFora] = React.useState({});      // factos que o cliente diz "não é bem"
  // Os grupos deixados para depois, à parte do "não é bem" de cada dado: confirmar
  // um grupo outra vez não desfaz o que se tirou um a um (revisor, 28 Set).
  const [adiados, setAdiados] = React.useState({});
  const [ocupado, setOcupado] = React.useState(false);
  const [feito, setFeito] = React.useState(null);
  const [erro, setErro] = React.useState(null);

  const ler = React.useCallback(() => Promise.all([
    api.get(`/api/${slug}/marca/kit`), api.get(`/api/${slug}/marca/voz`), api.get(`/api/${slug}/marca/factos`),
  ]).then(([k, v, f]) => { setKitD(k); setVozD(v); setFactosD(f); setErro(null); })
    .catch((e) => setErro(e.message || 'Não deu para ler a marca.')), [slug]);
  React.useEffect(() => { setKitD(null); ler(); }, [ler, aPropor]);
  // Sem proposta ainda, pede-se uma vez, sozinha (cliente simulado, 28 Set: a
  // página mostrava as cores e a letra de omissão como se fossem a marca, e um
  // "Sim" apressado confirmava a marca errada).
  // E outra vez, uma só, quando os ficheiros acabam de ser lidos: aberto cedo, com nada lido, o pedido
  // voltava sem proposta e o ecrã ficava a dizer «quando estiverem lidos, propomos aqui» para sempre
  // (ensaio NESCAFÉ de 9 Out: 6 min parado com tudo lido). `lidos` vem do resumo, relido a cada 30 s.
  // Um só controlo: com os ficheiros já lidos ao abrir, dois efeitos pediam duas propostas no mesmo
  // desenho (parecer do Codex, 9 Out). `pedida` guarda em que estado já se pediu: 'cedo' ou 'lidos'.
  const pedida = React.useRef(null);
  React.useEffect(() => {
    if (!vozD || !kitD || aPropor || !podeMexer || !onPropor) return;
    if (vozD.voz || vozD.proposta || kitD.kit) return;
    if (pedida.current === 'lidos' || (pedida.current === 'cedo' && !lidos)) return;
    pedida.current = lidos ? 'lidos' : 'cedo';
    onPropor();
  }, [lidos, vozD, kitD, aPropor, podeMexer, onPropor]);

  if (erro) return <p className="text-sm text-accent" role="status">{erro}</p>;
  if (!kitD || !vozD || !factosD) return <div className="loading-skeleton h-72" />;

  const p = kitD.proposta || {};
  const k = kitD.kit;
  // O que se mostra: o que está guardado, ou a proposta.
  const kit = k ? {
    cor_primaria: k.cor_primaria, cor_secundaria: k.cor_secundaria, cor_accent: k.cor_accent, cor_texto: k.cor_texto,
    logo_material_id: k.logo_material_id, logo_negativo_material_id: k.logo_negativo_material_id,
    font_titulo: k.font_titulo, font_texto: k.font_texto,
  } : {
    cor_primaria: p.primaria, cor_secundaria: coresDaPropostaNoEcra(p).fundo, cor_accent: acentoDaProposta(p), cor_texto: coresDaPropostaNoEcra(p).texto,
    logo_material_id: p.logo_material_id, logo_negativo_material_id: p.logo_negativo_material_id,
    font_titulo: p.fontes && p.fontes.titulo ? p.fontes.titulo.familia : null,
    font_texto: p.fontes && p.fontes.texto ? p.fontes.texto.familia : null,
    font_titulo_original: p.fontes && p.fontes.titulo && p.fontes.titulo.substituta ? p.fontes.titulo.original : null,
    font_texto_original: p.fontes && p.fontes.texto && p.fontes.texto.substituta ? p.fontes.texto.original : null,
  };
  const temCores = COR_HEX.test(kit.cor_primaria || '') && COR_HEX.test(kit.cor_secundaria || '') && COR_HEX.test(kit.cor_accent || '');
  const previa = (formato) => {
    const q = new URLSearchParams({ formato, primaria: kit.cor_primaria, fundo: kit.cor_secundaria, acento: kit.cor_accent });
    if (kit.cor_texto) q.set('texto', kit.cor_texto);
    if (kit.logo_material_id) q.set('logo', kit.logo_material_id);
    if (kit.logo_negativo_material_id) q.set('logo_negativo', kit.logo_negativo_material_id);
    if (kit.font_titulo) q.set('titulo_fonte', kit.font_titulo);
    if (kit.font_texto) q.set('texto_fonte', kit.font_texto);
    return `/api/${slug}/marca/kit/previa.png?${q}`;
  };

  const va = vozD.voz;
  const vp = vozD.proposta;
  const TRAT = { tu: 'Trata por tu', voce: 'Trata por você', neutro: 'Sem tratamento directo' };
  // Com o público, os exemplos e o "evitar": vão no "Sim", por isso vêem-se
  // (advogado da simplicidade, 2.ª ronda, 28 Set).
  const voz = va ? {
    tratamento: va.tratamento, essencia: va.essencia, tracos: va.tracos || [],
    publico: va.publico || null, exemplos: va.exemplos || [], evitar: va.evitar || [],
  } : vp ? {
    tratamento: vp.tratamento, essencia: vp.essencia && vp.essencia.texto, tracos: (vp.tracos || []).map((x) => x.texto),
    publico: vp.publico ? vp.publico.texto : null,
    exemplos: (vp.exemplos || []).map((x) => x.texto), evitar: (vp.evitar || []).map((x) => x.texto),
  } : null;
  const regrasJa = new Set((vozD.regras || []).map((r) => String(r.texto).toLowerCase()));
  const nuncaNovas = !va && vp ? (vp.nunca || []).map((x) => x.texto).filter((t) => !regrasJa.has(t.toLowerCase())) : [];
  const nuncaTodas = [...(vozD.regras || []).filter((r) => r.tipo !== 'sempre').map((r) => r.texto), ...nuncaNovas];
  // As frases que o manual obriga a pôr sempre (28 Set, migração 051).
  const sempreNovas = !va && vp ? (vp.sempre || []).map((x) => x.texto).filter((t) => !regrasJa.has(t.toLowerCase())) : [];
  const sempreTodas = [...(vozD.regras || []).filter((r) => r.tipo === 'sempre').map((r) => r.texto), ...sempreNovas];
  // Uma proposta que não trouxe nada de como a marca fala não se apresenta como a voz da marca
  // (agentes, 20 Out: o resumo afirmava «Trata por tu» com tudo vazio, e o «Sim» confirmava-o).
  const vozVazia = Boolean(voz && !voz.essencia && !voz.tratamento && !voz.tracos.length && !voz.publico
    && !voz.exemplos.length && !voz.evitar.length);
  // Os logótipos que ficam guardados com o «Sim», à vista (agentes, 20 Out: os exemplos dos painéis
  // são de fundo escuro e mostravam outro, e guardava-se um logótipo que o cliente não tinha visto).
  const logoDoResumo = (id) => (id ? (kitD.logos || []).find((m) => m.id === id) || null : null);
  const logosDoKit = [[kit.logo_material_id, 'Para fundos claros'], [kit.logo_negativo_material_id, 'Para fundos escuros']]
    .map(([id, r]) => [logoDoResumo(id), r]).filter(([m]) => m);
  const logosApagados = k ? [k.logo_material_id, k.logo_negativo_material_id].filter((id) => id && !logoDoResumo(id)).length : 0;

  // Confirma-se só o que está à vista, e está tudo à vista (até 100, a rota não
  // aceita mais): o advogado da simplicidade, 28 Set, viu confirmarem-se 100
  // dados com 12 mostrados, contra a regra da versão vista.
  const porConfirmar = (factosD.por_confirmar || []).slice(0, 100);
  const grupoDe = (f) => GRUPOS_DE_DADOS.find((g) => g.e(f)).chave;
  const aConfirmar = porConfirmar.filter((f) => !fora[f.id] && !adiados[grupoDe(f)]);
  // A voz vazia não vai no «Sim»; as regras do manual, se as houver, vão.
  const falta = { kit: !k && temCores, voz: !va && Boolean(vp) && (!vozVazia || nuncaNovas.length > 0 || sempreNovas.length > 0), factos: aConfirmar.length > 0 };
  const tirados = porConfirmar.filter((f) => fora[f.id]).length;
  const nadaParaConfirmar = !falta.kit && !falta.voz && !falta.factos && !tirados;
  const semProposta = !k && !temCores && !va && !vp;
  // A ler: sem proposta do manual, o que se veria era o de omissão, não a marca.
  const aLer = !k && !va && !vp;

  // Um só pedido, numa só transacção (28 Set): ou fica tudo confirmado, ou nada.
  async function confirmarTudo() {
    setOcupado(true);
    setErro(null);
    const corpo = {};
    const partes = [];
    if (falta.kit) {
      corpo.kit = {
        cor_primaria: kit.cor_primaria, cor_secundaria: kit.cor_secundaria, cor_accent: kit.cor_accent,
        cor_texto: kit.cor_texto || null, logo_material_id: kit.logo_material_id || null,
        logo_negativo_material_id: kit.logo_negativo_material_id || null,
        font_titulo: kit.font_titulo || null, font_texto: kit.font_texto || null,
        font_titulo_original: kit.font_titulo_original || null, font_texto_original: kit.font_texto_original || null,
      };
      partes.push('as cores, a letra e o logótipo');
    }
    if (falta.voz) {
      corpo.regras = [
        ...nuncaNovas.map((t) => ({ texto: t, motivo: 'Do manual da marca.' })),
        ...sempreNovas.map((t) => ({ texto: t, motivo: 'Do manual da marca.', tipo: 'sempre' })),
      ];
      corpo.voz = {
        tratamento: vp.tratamento || null,
        essencia: vp.essencia ? vp.essencia.texto : null,
        publico: vp.publico ? vp.publico.texto : null,
        tracos: (vp.tracos || []).map((x) => x.texto),
        exemplos: (vp.exemplos || []).map((x) => x.texto),
        evitar: (vp.evitar || []).map((x) => x.texto),
      };
      partes.push(nuncaNovas.length ? `como fala, e ${nuncaNovas.length} ${nuncaNovas.length === 1 ? 'expressão' : 'expressões'} a nunca usar` : 'como fala');
    }
    if (falta.factos) corpo.factos = aConfirmar.map((f) => ({ id: f.id, versao: f.versao }));
    // O "Não é bem" vai no mesmo pedido e fica retirado: só no ecrã, voltava como
    // pendente depois de confirmar (cliente simulado, 6 Out).
    const retirar = porConfirmar.filter((f) => fora[f.id]).map((f) => f.id);
    if (retirar.length) corpo.retirar = retirar;
    try {
      const r = await api.post(`/api/${slug}/marca/confirmar`, corpo);
      // O que se retirou e o que foi à NetScreen, pela resposta: um dado que a equipa
      // decidiu entretanto não se retira, vai-lhe para rever (Codex, 6 Out, P2).
      if (r.retirados) partes.push(`${r.retirados} ${r.retirados === 1 ? 'dado retirado' : 'dados retirados'}`);
      if (r.enviados_a_equipa) {
        partes.push(`${r.enviados_a_equipa} ${r.enviados_a_equipa === 1 ? 'dado que a NetScreen já tinha revisto volta' : 'dados que a NetScreen já tinha revisto voltam'} para ela rever, por dizeres que não está certo`);
      }
      if (falta.factos) {
        partes.push(`${aConfirmar.length} ${aConfirmar.length === 1 ? 'dado' : 'dados'}`
          + (r.factos && r.factos.a_rever ? ` (${r.factos.a_rever} com alegações que a NetScreen revê antes de entrarem)` : ''));
      }
      setFeito(`Confirmado: ${partes.join('; ')}. As campanhas partem daqui.`);
      setFora({});
      setAdiados({});
      await ler();
      if (onMudou) onMudou();
      window.dispatchEvent(new Event('fila-mudou'));
    } catch (e) {
      // Relê primeiro e só depois diz o erro: o `ler()` limpa-o, e a falha ficava
      // invisível, um botão que não fazia nada (cliente simulado, 6 Out).
      const mensagem = `${e.message || 'Não foi possível confirmar.'} Não ficou nada confirmado.`;
      await ler();
      setErro(mensagem);
    } finally {
      setOcupado(false);
    }
  }

  const Corrigir = ({ para, children }) => (
    <button type="button" onClick={() => onNavegar(`/${slug}/marca/${para}`)} className="text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground">
      {children}
    </button>
  );

  if (aLer && !semProposta) {
    return (
      <div className="max-w-prose space-y-3 border border-border bg-card p-6" role="status">
        {aPropor ? (
          <AProcessar
            variante="bloco"
            texto="A ler a tua marca"
            detalhe="Estamos a tirar dos teus ficheiros as cores, a letra, como a marca fala e o que não diz. Leva uns 30 segundos."
          />
        ) : (
          temCores ? (
            // As cores e o logótipo já estão propostos e só a voz falta: «Ainda não lemos a tua marca» com um botão
            // que só podia falhar enganava (rondas 8–10 dos virgens).
            <>
              <h2 className="text-lg font-medium text-foreground">Já lemos as cores e o logótipo</h2>
              <p className="text-sm text-muted-foreground">
                Confirma-os em Identidade. Para propormos também como a marca fala, carrega o manual de normas ou indica o site em Ficheiros; até lá, a voz escreve-se em Identidade.
              </p>
              <button type="button" onClick={() => onNavegar(`/${slug}/marca/identidade`)} className="bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90">
                Ver as cores e o logótipo
              </button>
            </>
          ) : (
          <>
            <h2 className="text-lg font-medium text-foreground">Ainda não lemos a tua marca</h2>
            <p className="text-sm text-muted-foreground">
              Tiramos dos teus ficheiros as cores, a letra e como a marca fala, e mostramos aqui para confirmares.
            </p>
          </>
          )
        )}
        {podeMexer && onPropor && !aPropor && !temCores && (
          <button type="button" onClick={onPropor} className="bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90">
            Ler a minha marca
          </button>
        )}
        {/* A resposta do servidor (um 409 dizia porque não lia e não aparecia; ronda 7). */}
        {avisoProposta && !aPropor && <p role="alert" className="text-sm text-foreground">{avisoProposta}</p>}
      </div>
    );
  }

  // Proposta só com logótipo ou letra (sem cores): não é «não há marca», é «faltam as cores». O aviso da
  // fila mandava confirmar e aqui dizia que não havia nada (C-38, pastelaria virgem e Codex 2320).
  const soParcial = semProposta && !aPropor && Boolean(p.logo_material_id || p.fontes);
  if (soParcial) {
    return (
      <div className="max-w-prose space-y-3 border border-border bg-card p-6">
        <h2 className="text-lg font-medium text-foreground">Falta escolher as cores da marca</h2>
        <p className="text-sm text-muted-foreground">
          Já temos {[p.logo_material_id && 'o logótipo', p.fontes && 'a letra'].filter(Boolean).join(' e ')}. As cores não se leram dos ficheiros: escolhe-as em Identidade,
          ou carrega o manual de normas em Ficheiros.
        </p>
        <div className="flex flex-wrap gap-2">
          <button type="button" onClick={() => onNavegar(`/${slug}/marca/identidade`)} className="bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90">
            Escolher as cores
          </button>
          <button type="button" onClick={() => onNavegar(`/${slug}/marca/fontes`)} className="border border-border px-4 py-2 text-sm hover:bg-secondary">
            Ir para Ficheiros
          </button>
        </div>
      </div>
    );
  }

  if (semProposta) {
    return (
      <div className="max-w-prose space-y-3 border border-border bg-card p-6">
        {/* A ler, não se diz que não há marca: o topo já conta o que espera confirmação (cliente simulado, 6 Out). */}
        {aPropor
          ? <AProcessar variante="bloco" texto="A ler a tua marca" detalhe="Leva uns minutos." />
          : <h2 className="text-lg font-medium text-foreground">Ainda não há uma marca para confirmar</h2>}
        <p className="text-sm text-muted-foreground">
          Carrega o manual de normas, o logótipo ou o site em Ficheiros. Quando estiverem lidos, propomos aqui as cores,
          a letra, como a marca fala e o que as campanhas podem dizer.
        </p>
        <div className="flex flex-wrap gap-2">
          <button type="button" onClick={() => onNavegar(`/${slug}/marca/fontes`)} className="bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90">
            Ir para Ficheiros
          </button>
          {/* Sem nada lido, o botão não tinha o que propor e não respondia (percurso de raiz, 9 Out). */}
          {podeMexer && onPropor && lidos && (
            <button type="button" disabled={aPropor} onClick={onPropor} className="border border-border px-4 py-2 text-sm hover:bg-secondary disabled:opacity-50">
              {aPropor ? 'A ler os teus ficheiros…' : 'Propor a partir dos ficheiros já lidos'}
            </button>
          )}
        </div>
        {avisoProposta && !aPropor && <p role="status" className="text-sm text-foreground">{avisoProposta}</p>}
      </div>
    );
  }

  return (
    <div className="space-y-8">
      {/* Como aparece */}
      <section className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,20rem)]">
        <div className="space-y-2">
          <h2 className="text-base font-semibold tracking-tight text-foreground">Como aparece nos painéis</h2>
          {temCores
            ? <ImagemComEspera src={previa('bloco')} alt="A marca num painel de exemplo" proporcao={2560 / 800} className="w-full" />
            : <p className="text-sm text-muted-foreground">
                {kit.cor_primaria
                  ? 'Falta uma cor (a de fundo ou a de acento) para vermos o exemplo: escolhe-a em Identidade.'
                  : 'Sem cores ainda: escolhe-as em Identidade.'}
              </p>}
          {temCores && <ImagemComEspera src={previa('faixa')} alt="A marca numa faixa de exemplo" proporcao={1008 / 144} className="w-full" />}
          <p className="text-xs text-muted-foreground">Um exemplo com texto inventado. {k ? 'As cores e o logótipo estão guardados.' : 'É a nossa proposta: ainda não está guardada.'}</p>
        </div>
        <div className="space-y-4">
          <div className="space-y-2">
            <p className="text-xs text-muted-foreground">Cores</p>
            <div className="flex gap-2">
              {[['cor_primaria', 'Primária'], ['cor_secundaria', 'Fundo'], ['cor_accent', 'Acento'], ['cor_texto', 'Texto']]
                .filter(([c]) => kit[c]).map(([c, r]) => (
                  <div key={c} className="text-center">
                    <span className="block h-10 w-10 border border-border" style={{ background: kit[c] }} aria-hidden="true" />
                    <span className="mt-1 block text-[10px] text-muted-foreground">{r}</span>
                  </div>
                ))}
            </div>
          </div>
          <div className="space-y-1">
            <p className="text-xs text-muted-foreground">Letra</p>
            <p className="text-sm text-foreground">
              {kit.font_titulo || 'Bebas Neue'} nos títulos · {kit.font_texto || 'DM Sans'} no texto
            </p>
            {(kit.font_titulo_original || kit.font_texto_original) && (
              <p className="text-xs text-muted-foreground">
                {`O manual pede ${[kit.font_titulo_original, kit.font_texto_original].filter(Boolean).join(' e ')}, que é paga: usamos uma parecida e gratuita.`}
              </p>
            )}
          </div>
          <div className="space-y-1">
            <p className="text-xs text-muted-foreground">Logótipo</p>
            {logosDoKit.length ? (
              <div className="flex flex-wrap gap-2">
                {logosDoKit.map(([m, r]) => (
                  <figure key={r} className="w-28 space-y-1">
                    <span className="flex h-14 items-center justify-center rounded-sm border border-border p-1.5" style={{ background: r === 'Para fundos escuros' ? '#0b0b0f' : '#f4f4f5' }}>
                      <ImagemOuAviso src={m.url} alt={`${r}: ${m.original_name}`} texto="Não abre" className="max-h-10 max-w-full object-contain" />
                    </span>
                    <figcaption className="text-[10px] leading-tight text-muted-foreground">
                      <span className="block">{r}</span>
                      <span className="block truncate" title={m.original_name}>{nomeAVista(m.original_name)}</span>
                    </figcaption>
                  </figure>
                ))}
              </div>
            ) : (
              <p className="text-sm text-muted-foreground">Sem logótipo: as peças saem sem ele.</p>
            )}
            {logosApagados > 0 && (
              <p className="text-xs text-accent" role="alert">
                {logosApagados === 1 ? 'Um logótipo guardado foi apagado dos Ficheiros' : 'Os logótipos guardados foram apagados dos Ficheiros'}: as campanhas já não o usam. Escolhe outro em Identidade.
              </p>
            )}
          </div>
          <Corrigir para="identidade">Mudar as cores, a letra ou o logótipo</Corrigir>
        </div>
      </section>

      {/* Como fala */}
      <section className="space-y-2 border-t border-border pt-6">
        <h2 className="text-base font-semibold tracking-tight text-foreground">Como fala</h2>
        {voz && vozVazia && !nuncaTodas.length && !sempreTodas.length ? (
          <p className="text-sm text-muted-foreground">
            Não encontrámos nos teus ficheiros como a marca fala. Diz-nos em Identidade (o tratamento, a maneira de escrever, para quem falam); sem isso, as campanhas escrevem num tom neutro e tratam por tu.
          </p>
        ) : voz ? (
          <ul className="space-y-1 text-sm text-foreground">
            {voz.essencia && <li>{voz.essencia}</li>}
            <li className="text-muted-foreground">
              {/* Sem tratamento, diz-se que falta, e não um «Trata por tu» que ninguém escolheu (agentes, 20 Out). */}
              {[TRAT[voz.tratamento] || 'Tratamento por escolher (sem ele, as campanhas tratam por tu)', voz.tracos.length ? voz.tracos.join(', ') : null].filter(Boolean).join(' · ')}
            </li>
            {voz.publico && <li className="text-muted-foreground">{`Fala para: ${voz.publico}`}</li>}
            {voz.exemplos.length > 0 && (
              <li className="text-muted-foreground">{`Soa assim: ${voz.exemplos.map(entreAspas).join(' ')}`}</li>
            )}
            {voz.evitar.length > 0 && <li className="text-muted-foreground">{`Evita: ${voz.evitar.join(', ')}`}</li>}
            {vp && Array.isArray(vp.redes) && vp.redes.length === 1 && (
              <li className="text-muted-foreground">{`Nas redes: só no ${vp.redes[0] === 'instagram' ? 'Instagram' : 'Facebook'} (vem assim marcado nas campanhas novas)`}</li>
            )}
            {sempreTodas.length > 0 && (
              <li className="text-muted-foreground">{`Leva sempre: ${sempreTodas.map(entreAspas).join(', ')}`}</li>
            )}
            {/* Só se propõe a frase que o manual dá claramente como obrigatória em todas as peças:
                as outras ficam de fora, e o cliente tem de o saber (Codex, 6 Out). */}
            {/* Só a quem pode acrescentar; e «nos teus ficheiros», porque a proposta lê também o site (revisor, 6 Out). */}
            {podeMexer && (
              <li className="text-xs text-muted-foreground">
                {sempreTodas.length > 0
                  ? 'Falta alguma frase que tenha de ir em todas as peças (um aviso legal, por exemplo)? Acrescenta-a em Identidade, «Leva sempre».'
                  : 'Não encontrámos nos teus ficheiros uma frase que tenha de ir em todas as peças. Se a marca tiver uma (um aviso legal, por exemplo), acrescenta-a em Identidade, «Leva sempre».'}
              </li>
            )}
            {nuncaTodas.length > 0 && (
              <li className="text-muted-foreground">{`Nunca diz: ${nuncaTodas.join(', ')}`}</li>
            )}
          </ul>
        ) : (
          <p className="text-sm text-muted-foreground">Ainda não há proposta. Sem ela, as campanhas escrevem num tom neutro e tratam por tu.</p>
        )}
        <Corrigir para="identidade">Afinar a voz</Corrigir>
      </section>

      {/* O que as campanhas podem dizer */}
      <section className="space-y-2 border-t border-border pt-6">
        <h2 className="text-base font-semibold tracking-tight text-foreground">O que as campanhas podem dizer da marca</h2>
        {porConfirmar.length ? (
          <React.Fragment>
            <p className="text-xs text-muted-foreground">
              Lemos isto nos teus ficheiros, por grupos. Tira o que não estiver certo, ou deixa um grupo para depois:
              fica por confirmar em Dados da marca, e o resto confirma-se já.
            </p>
            {/* Por grupos, sem scroll interno: o que se confirma está à vista
                (parecer do Codex, 28 Set). */}
            {agruparDados(porConfirmar).map((g) => {
              const deixado = Boolean(adiados[g.chave]);
              return (
                <div key={g.chave} className="space-y-1.5 pt-2">
                  <div className="flex flex-wrap items-baseline justify-between gap-2">
                    <h3 className="text-sm font-medium text-foreground">{`${g.titulo} (${g.dados.length})`}</h3>
                    {podeMexer && (
                      <button
                        type="button"
                        onClick={() => setAdiados((x) => ({ ...x, [g.chave]: !deixado }))}
                        className="text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground"
                      >
                        {deixado ? 'Voltar a incluir este grupo' : 'Deixar para depois'}
                      </button>
                    )}
                  </div>
                  {g.ajuda && !deixado && <p className="text-xs text-muted-foreground">{g.ajuda}</p>}
                  {deixado ? (
                    <p className="text-xs text-muted-foreground">{`Fica por confirmar: ${g.dados.length === 1 ? '1 dado' : `${g.dados.length} dados`}, em Dados da marca.`}</p>
                  ) : (
                    <ul className="space-y-1.5">
                      {g.dados.map((f) => (
                        <li key={f.id} className={`flex items-start justify-between gap-3 border border-border px-3 py-2 text-sm ${fora[f.id] ? 'opacity-50' : ''}`}>
                          <span className={fora[f.id] ? 'line-through' : 'text-foreground'}>{f.afirmacao}</span>
                          {podeMexer && (
                            <button type="button" onClick={() => setFora((x) => ({ ...x, [f.id]: !x[f.id] }))} className="shrink-0 text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground">
                              {fora[f.id] ? 'Afinal está' : 'Não é bem'}
                            </button>
                          )}
                        </li>
                      ))}
                    </ul>
                  )}
                </div>
              );
            })}
          </React.Fragment>
        ) : (
          <p className="text-sm text-muted-foreground">Nada por confirmar. <Corrigir para="factos">Ver os dados da marca</Corrigir></p>
        )}
      </section>

      {erro && <p className="text-sm text-perigo-texto" role="alert">{erro}</p>}
      {feito && <p className="text-sm text-foreground" role="status">{feito}</p>}

      {!podeMexer && <SoQuemPode className="border-t border-border py-4" accao="confirmar ou corrigir a marca" />}
      {podeMexer && (
        <div className={`flex flex-wrap items-center gap-3 border-t border-border bg-background py-4 ${nadaParaConfirmar ? '' : 'sticky bottom-0'}`}>
          {nadaParaConfirmar ? (
            <p className="text-sm text-muted-foreground">
              {/* Com dados deixados para depois, não se diz que está tudo confirmado. */}
              {porConfirmar.length
                ? `Fica por confirmar ${porConfirmar.length === 1 ? '1 dado' : `${porConfirmar.length} dados`}, que deixaste para depois ou tiraste: estão em Dados da marca.`
                : !va && vozVazia
                ? `${k ? 'As cores e o logótipo estão confirmados.' : 'Faltam as cores e o logótipo: completa-os em Identidade.'} Falta dizer como a marca fala, também em Identidade.`
                : !k
                ? 'A voz e os dados estão confirmados. Faltam as cores e o logótipo: completa-os em Identidade.'
                : 'A tua marca está confirmada. Podes corrigir qualquer parte nos separadores ao lado.'}
            </p>
          ) : null}
          {/* E agora? Os passos seguintes dos «três passos» do Início (teste de raiz, 9 Out: confirmada a marca,
              não havia por onde seguir). */}
          {nadaParaConfirmar && !porConfirmar.length && k && onNavegar ? (
            <span className="flex flex-wrap items-center gap-2">
              <button type="button" onClick={() => onNavegar(`/${slug}/lojas`)} className="ns-btn ns-btn-sm ns-btn-primario">A seguir: as tuas lojas</button>
              <button type="button" onClick={() => onNavegar(criarActivo ? `/${slug}/criar` : `/${slug}/campanhas/nova`)} className="ns-btn ns-btn-sm ns-btn-secundario">{criarActivo ? 'Criar um vídeo' : 'Criar uma campanha'}</button>
            </span>
          ) : null}
          {!nadaParaConfirmar && (
            <React.Fragment>
              <button type="button" disabled={ocupado} onClick={confirmarTudo} className="bg-primary px-5 py-2.5 text-sm text-primary-foreground hover:bg-primary/90 disabled:bg-secondary disabled:text-muted-foreground">
                {ocupado ? 'A confirmar…' : 'Sim, é esta a minha marca'}
              </button>
              <span className="text-xs text-muted-foreground">
                {`Confirma ${[falta.kit && 'as cores, a letra e o logótipo', falta.voz && 'como fala', falta.factos && `${aConfirmar.length} ${aConfirmar.length === 1 ? 'dado' : 'dados'}`].filter(Boolean).join(', ') || 'o que está certo'}${tirados ? `, e retira ${tirados === 1 ? 'o dado que não está' : `os ${tirados} dados que não estão`} certo${tirados === 1 ? '' : 's'}` : ''}.`}
              </span>
            </React.Fragment>
          )}
        </div>
      )}
    </div>
  );
}

/**
 * O topo: o que é contigo, cada coisa a levar onde se resolve; o que está com
 * a NetScreen ou com a app, numa linha discreta; e o que falta para a marca
 * ficar pronta, só enquanto falta (segunda ronda de 24 Set: a caixa com três
 * colunas, "Nada à espera de ninguém" com tudo por fazer, e "vê porquê" sem
 * acção, confundiam mais do que ajudavam).
 */
function MarcaVez({ resumo, onIr }) {
  const tua = resumo.vez.cliente;
  const outros = [
    resumo.vez.netscreen.length ? `A NetScreen está ${resumo.vez.netscreen.map((v) => v.texto).join(' e ')}` : null,
    // «parado» já traz o verbo: «A app está a leitura está parada» (cliente virgem, 3 Nov).
    resumo.vez.sistema.some((v) => v.chave === 'parado') ? 'A leitura dos ficheiros está parada'
      : resumo.vez.sistema.length ? `A app está ${resumo.vez.sistema.map((v) => v.texto).join(' e ')}` : null,
  ].filter(Boolean);
  const falta = resumo.onboarding.filter((o) => !o.daNetscreen && !o.feito);
  // O perfil Leitura (becos de 9 Out, ponto 15): o que seria «contigo» é com quem pode mudar a marca.
  const doResponsavel = resumo.vez.responsavel || [];
  if (!tua.length && !doResponsavel.length && !outros.length && !falta.length) return null;

  return (
    <section className="space-y-2">
      {doResponsavel.length > 0 && (
        <div className="rounded-md border border-border p-2">
          <p className="px-2 pb-1 pt-1 text-xs font-medium text-muted-foreground">Com o responsável da conta</p>
          <ul className="space-y-1">
            {doResponsavel.map((v) => (
              <li key={v.chave}>
                <button
                  type="button"
                  onClick={() => onIr(v.separador || 'fontes')}
                  className="flex w-full items-center justify-between gap-3 rounded-md px-2 py-1.5 text-left text-sm text-foreground transition-colors hover:bg-secondary"
                >
                  <span className="min-w-0">{v.texto}</span>
                  <span className="ns-btn ns-btn-sm ns-btn-suave shrink-0 text-xs" aria-hidden="true">Ver</span>
                </button>
              </li>
            ))}
          </ul>
          <SoQuemPode className="px-2 pb-1 pt-1" accao="confirmar ou mudar a marca" />
        </div>
      )}
      {tua.length > 0 && (
        // Cada coisa contigo é uma linha inteira que se carrega, com "Ver" à
        // direita: o texto sublinhado não parecia a próxima coisa a fazer (28 Set).
        <div className="rounded-md border border-[hsl(var(--primary)/0.35)] bg-[hsl(var(--primary)/0.06)] p-2">
          <p className="px-2 pb-1 pt-1 text-xs font-medium text-muted-foreground">Contigo</p>
          <ul className="space-y-1">
            {tua.map((v) => (
              <li key={v.chave}>
                <button
                  type="button"
                  onClick={() => onIr(v.separador || 'fontes')}
                  className="group flex w-full items-center justify-between gap-3 rounded-md px-2 py-1.5 text-left text-sm text-foreground transition-colors hover:bg-[hsl(var(--primary)/0.1)]"
                >
                  <span className="min-w-0">{v.texto}</span>
                  <span className="ns-btn ns-btn-sm ns-btn-suave shrink-0 text-xs" aria-hidden="true">Ver</span>
                </button>
              </li>
            ))}
          </ul>
        </div>
      )}
      {outros.length > 0 && (
        <p role="status" className={`flex items-center gap-2 text-xs ${resumo.vez.sistema.some((v) => v.chave === 'parado') ? 'text-accent' : 'text-muted-foreground'}`}>
          {/* A trabalhar à vista (8 Out); com o worker parado não gira, porque não está. */}
          {!resumo.vez.sistema.some((v) => v.chave === 'parado') && <ATrabalhar className="text-primary" />}
          {`${outros.join('. ')}. Não precisas de fazer nada.`}
        </p>
      )}
      {/* «Para a marca ficar completa: …» saiu: repetia o botão do cabeçalho e o quadro (simplicidade, 3 Nov). */}
    </section>
  );
}

/**
 * Os factos: o que lemos sobre a marca, e o cliente confirma (24 Set,
 * seguimento do Codex, secção 4). Um resumo agrupado por ficheiro, com
 * "Está tudo certo" por grupo, e não dezenas de aprovações uma a uma. A
 * NetScreen fica com as excepções: o que pede prova e o que o cliente diz
 * estar errado.
 */
function MarcaFactos({ slug, podeMexer, onMudou, volta }) {
  const [dados, setDados] = React.useState(null);
  const [aSinalizar, setASinalizar] = React.useState(null);
  const [texto, setTexto] = React.useState('');
  const [ocupado, setOcupado] = React.useState(false);
  const [aviso, setAviso] = React.useState(null);

  const carregar = React.useCallback(
    () => api.get(`/api/${slug}/marca/factos`).then(setDados).catch((e) => setAviso(e.message)),
    [slug]
  );
  // Trocar de conta limpa o que estava à vista: não se mostra a marca de outra conta
  // nem por um instante (revisão do Codex, 23 Set).
  React.useEffect(() => { setDados(null); carregar(); }, [carregar]);
  // Cada volta do ecrã da marca: lê outra vez, sem apagar o que está à vista
  // (nem o texto de uma sinalização a meio, que é outro estado).
  // A mensagem de uma acção sai na primeira leitura depois de 10 segundos:
  // ficava "Prova enviada. A NetScreen volta a rever." por cima da recusa que
  // chegou depois (ronda 9). As voltas logo a seguir à acção são dela, e não contam.
  const avisoEm = React.useRef(0);
  // A mensagem de uma acção sobre um dado vai com o cartão, onde quer que ele
  // fique depois dela, e o ecrã leva-o à vista (ronda 10: o cartão saltava de
  // secção e a mensagem aparecia no topo, fora do ecrã).
  const [alvo, setAlvo] = React.useState(null);
  React.useEffect(() => {
    if (!volta) return;
    if (Date.now() - avisoEm.current > 10000) { setAviso(null); setAlvo(null); }
    carregar();
  }, [volta]);
  React.useEffect(() => {
    if (!alvo) return;
    const el = document.getElementById(`facto-${alvo.id}`);
    if (el) el.scrollIntoView({ block: 'center', behavior: 'smooth' });
  }, [alvo]);

  const fazer = async (fn, mensagem, idDoFacto = null) => {
    setOcupado(true);
    setAviso(null);
    setAlvo(null);
    const dizer = (texto) => {
      avisoEm.current = Date.now();
      if (idDoFacto) setAlvo({ id: idDoFacto, texto });
      else setAviso(texto);
    };
    try {
      const r = await fn();
      await carregar();
      if (mensagem) dizer(typeof mensagem === 'function' ? mensagem(r) : mensagem);
      // O topo ("de quem é a vez") conta o que está por confirmar.
      if (onMudou) onMudou();
      window.dispatchEvent(new Event('fila-mudou'));
    } catch (e) {
      // Mudou entretanto (versão), ou a NetScreen decidiu: lê-se outra vez para
      // se ver o que está agora.
      dizer(e.message);
      carregar();
    } finally {
      setOcupado(false);
    }
  };

  const confirmar = (lista) => fazer(
    () => api.post(`/api/${slug}/marca/factos/confirmar`, { factos: lista.map((f) => ({ id: f.id, versao: f.versao })) }),
    (r) => [
      r.em_uso ? `${r.em_uso === 1 ? '1 dado passou' : `${r.em_uso} dados passaram`} a entrar nas campanhas.` : null,
      r.a_rever ? `${r.a_rever === 1 ? '1 afirmação precisa' : `${r.a_rever} afirmações precisam`} de prova: a NetScreen revê antes de entrar.` : null,
    ].filter(Boolean).join(' ') || null,
    lista.length === 1 ? lista[0].id : null
  );
  // Desfazer diz o que mudou, como confirmar diz (cliente simulado, 24 Set).
  // A prova de uma alegação (migração 046): o ficheiro guarda-se como os
  // outros, sem ler, e fica ligado à alegação. A NetScreen decide.
  const campoProva = React.useRef(null);
  const [provaDe, setProvaDe] = React.useState(null);
  const escolherProva = (f) => { setProvaDe(f); if (campoProva.current) campoProva.current.click(); };
  const enviarProva = async (ficheiro) => {
    const f = provaDe;
    if (!ficheiro || !f) return;
    await fazer(async () => {
      const q = new URLSearchParams({ nome: ficheiro.name, ler: '0' });
      // Uma falha de rede dizia "Failed to fetch" (ronda 9).
      const r = await fetch(`/api/${slug}/materiais?${q}`, {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'Content-Type': ficheiro.type || 'application/octet-stream' },
        body: ficheiro,
      }).catch(() => { throw new Error('Sem ligação ao servidor. Tenta outra vez.'); });
      const corpo = await r.json().catch(() => null);
      if (!r.ok) throw new Error((corpo && corpo.error) || `Erro ${r.status}`);
      const material = corpo.material || corpo;
      return api.post(`/api/${slug}/marca/factos/${f.id}/prova`, { material_id: material.id });
    }, (r) => (r && r.voltou_a_rever
      ? 'Prova enviada. A NetScreen volta a rever.'
      : 'Prova enviada. A NetScreen vê-a quando rever.'), f.id);
    setProvaDe(null);
    if (campoProva.current) campoProva.current.value = '';
  };
  const botaoProva = (f) => <span key="p">{botao(f.prova ? 'Mandar outra prova' : 'Enviar a prova', () => escolherProva(f))}</span>;
  // Todas as provas mandadas, da primeira à mais recente (migração 047; ronda 10:
  // só se via a última, e não se sabia sobre qual a NetScreen tinha decidido).
  const linhaDaProva = (f) => ((f.provas || []).length
    ? (
      <p className="mt-1 text-xs text-muted-foreground">
        {f.provas.length === 1 ? 'Prova enviada: ' : 'Provas enviadas: '}
        {f.provas.map((p, i) => (
          <React.Fragment key={p.url}>
            {i > 0 && ' · '}
            <a href={p.url} target="_blank" rel="noreferrer" className="underline underline-offset-2">{p.nome}</a>
            {p.em ? ` (${formatDataCurta(p.em)})` : ''}
          </React.Fragment>
        ))}
      </p>
    )
    : null);

  const repor = (f) => fazer(() => api.post(`/api/${slug}/marca/factos/${f.id}/repor`, {}),
    'Voltou a "Por confirmar". Confirma-o para entrar nas campanhas.', f.id);
  const desfazer = (f) => fazer(() => api.post(`/api/${slug}/marca/factos/${f.id}/desconfirmar`, {}),
    'Voltou a "Por confirmar": deixa de entrar nas campanhas até o confirmares outra vez.', f.id);
  // Um dado que ainda não estava em uso sai logo; um que a NetScreen reviu vai
  // à equipa (advogado da simplicidade, 24 Set).
  const sinalizar = async () => {
    const idDoFacto = aSinalizar.id;
    await fazer(async () => {
      const r = await api.post(`/api/${slug}/marca/factos/${aSinalizar.id}/sinalizar`, { texto });
      setASinalizar(null);
      setTexto('');
      return r;
    }, (r) => {
      // As campanhas que já o dizem ficam marcadas (28 Set, D2): diz-se quantas.
      const n = r && r.campanhas ? r.campanhas.length : 0;
      const marcadas = n ? ` ${n === 1 ? 'Uma campanha já o diz' : `${n} campanhas já o dizem`}: ficam marcadas até se corrigir o texto.` : '';
      return (r && r.retirado
        ? 'Retirado. Não entra nas campanhas.'
        : 'Enviado à NetScreen. Até responder, isto não entra nas campanhas.') + marcadas;
    }, idDoFacto);
  };

  // Sem dados e com erro, mostra-se o erro — o esqueleto ficava lá para sempre
  // (revisão de 23 Set, menor 10).
  if (!dados) {
    return aviso
      ? <p className="text-sm text-accent" role="status">Não deu para ler os factos: {aviso}</p>
      : <div className="loading-skeleton h-40" />;
  }

  const origemDe = (f) => (f.fonte ? `De ${f.fonte}${f.pagina ? `, pág. ${f.pagina}` : ''}` : 'Sem ficheiro registado');
  const sinalizacaoAberta = (f) => f.sinalizacao && f.sinalizacao.aberta;
  // "Aparece em N campanhas": as não terminadas cujo texto o repete (28 Set, D2).
  const listaCampanhas = (cs) => cs.map((c) => `"${c.titulo}"`).join(', ');
  const fora = new Set([...dados.proibidos, ...(dados.retirados || [])].map((f) => f.id));
  const ondeAparece = (f) => {
    const cs = f.campanhas || [];
    if (!cs.length) return null;
    // Fora das campanhas e ainda num texto: é o que o cliente tem de corrigir.
    return fora.has(f.id)
      ? (
        <p className="mt-2 border-l-2 border-accent pl-3 text-xs text-foreground">
          Ainda aparece em {cs.length === 1 ? '1 campanha' : `${cs.length} campanhas`}: {listaCampanhas(cs)}. Corrige o texto: assim não vai para o ar.
        </p>
      )
      : <p className="mt-1 text-xs text-muted-foreground">Aparece em {cs.length === 1 ? '1 campanha' : `${cs.length} campanhas`}: {listaCampanhas(cs)}.</p>;
  };

  const linha = (f, { etiqueta, accoes, nota }) => (
    <li key={f.id} id={`facto-${f.id}`} className="border-t border-border px-4 py-3 first:border-t-0">
      <div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1">
        <p className="text-sm text-foreground">{f.afirmacao}</p>
        {etiqueta && <span className="shrink-0 text-xs text-muted-foreground">{etiqueta}</span>}
      </div>
      {f.citacao && <p className="mt-1 text-xs text-muted-foreground">"{f.citacao}"</p>}
      <p className="mt-1 text-xs text-muted-foreground">{origemDe(f)}</p>
      {nota && <p className="mt-2 border-l-2 border-accent pl-3 text-xs text-foreground">{nota}</p>}
      {ondeAparece(f)}
      {linhaDaProva(f)}
      {alvo && alvo.id === f.id && (
        <p className="mt-2 bg-[hsl(var(--primary)/0.08)] px-3 py-1.5 text-xs text-foreground" role="status">{alvo.texto}</p>
      )}
      {sinalizacaoAberta(f) && (
        <p className="mt-2 border-l-2 border-accent pl-3 text-xs text-foreground">
          Disseste a {formatDataCurta(f.sinalizacao.criada_em)}: «{f.sinalizacao.texto}» — à espera da NetScreen; até lá não entra nas campanhas.
        </p>
      )}
      {f.sinalizacao && !f.sinalizacao.aberta && f.sinalizacao.por_ti && (
        <p className="mt-2 border-l-2 border-border pl-3 text-xs text-muted-foreground">
          Retiraste a {formatDataCurta(f.sinalizacao.resolvida_em)}: «{f.sinalizacao.texto}»
        </p>
      )}
      {f.sinalizacao && !f.sinalizacao.aberta && !f.sinalizacao.por_ti && f.sinalizacao.resposta && (
        <p className="mt-2 border-l-2 border-border pl-3 text-xs text-muted-foreground">
          A NetScreen respondeu a {formatDataCurta(f.sinalizacao.resolvida_em)}: "{f.sinalizacao.resposta}"
        </p>
      )}
      {/* A mesma decisão escreve a resposta na correcção e no dado: aparecia duas
          vezes, uma com "…" e outra com «…» (avaliação da UX, 28 Set, D8/F11). */}
      {f.resposta && !(f.sinalizacao && !f.sinalizacao.aberta && !f.sinalizacao.por_ti && f.sinalizacao.resposta === f.resposta) && (
        <p className="mt-2 border-l-2 border-border pl-3 text-xs text-muted-foreground">{`A NetScreen respondeu${f.resposta_em ? ` a ${formatDataCurta(f.resposta_em)}` : ''}: «${f.resposta}»`}</p>
      )}
      {podeMexer && accoes && <div className="mt-2 flex flex-wrap gap-x-4 gap-y-1">{accoes}</div>}
    </li>
  );
  const botao = (rotulo, onClick, principal) => (
    <button
      type="button"
      disabled={ocupado}
      onClick={onClick}
      className={`text-xs underline-offset-2 hover:underline disabled:opacity-50 ${principal ? 'text-foreground underline' : 'text-muted-foreground hover:text-foreground'}`}
    >
      {rotulo}
    </button>
  );
  const vaiANetscreen = Boolean(aSinalizar && [...dados.verificados, ...dados.proibidos].some((x) => x.id === aSinalizar.id));
  const naoEstaCerto = (f) => !sinalizacaoAberta(f) && botao('Não está certo', () => { setASinalizar(f); setTexto(''); });

  // Por confirmar, agrupado pelo ficheiro de onde veio.
  const grupos = [];
  for (const f of dados.por_confirmar) {
    const chave = f.fonte || 'Sem ficheiro registado';
    let g = grupos.find((x) => x.chave === chave);
    if (!g) { g = { chave, factos: [] }; grupos.push(g); }
    g.factos.push(f);
  }
  const emUso = [
    ...dados.confirmados.map((f) => ({ ...f, origemDaDecisao: 'cliente' })),
    ...dados.verificados.map((f) => ({ ...f, origemDaDecisao: 'netscreen' })),
  ];

  return (
    <div className="space-y-8">
      {aviso && <p className="text-sm text-foreground" role="status">{aviso}</p>}
      <p className="max-w-prose text-sm text-muted-foreground">
        O que lemos nos teus ficheiros sobre a tua marca. As campanhas só afirmam o que confirmares aqui, ou o que a NetScreen rever.
      </p>

      <section className="space-y-3">
        <h3 className="ns-seccao-titulo">Por confirmar ({dados.por_confirmar.length})</h3>
        {dados.por_confirmar.length === 0 && (
          // Não manda carregar ficheiros a quem acabou de carregar 11 (agentes, 20 Out).
          <p className="text-xs text-muted-foreground">Nada por confirmar agora. Os dados que tirarmos dos teus ficheiros (dos que ainda estão a ser lidos, ou dos que carregares) aparecem aqui; um ficheiro só com imagens ou regras de marca pode não trazer nenhum.</p>
        )}
        {grupos.map((g) => (
          <div key={g.chave} className="border border-border">
            <div className="flex flex-wrap items-center justify-between gap-2 border-b border-border bg-secondary/40 px-4 py-2">
              <p className="text-xs text-muted-foreground">{g.chave}</p>
              {podeMexer && (
                <button
                  type="button"
                  disabled={ocupado}
                  onClick={() => confirmar(g.factos)}
                  className="bg-primary px-3 py-1 text-xs text-primary-foreground hover:bg-primary/90 disabled:bg-secondary disabled:text-muted-foreground"
                >
                  {g.factos.length === 1 ? 'Está certo' : `Está tudo certo (${g.factos.length})`}
                </button>
              )}
            </div>
            <ul>
              {g.factos.map((f) => linha(f, {
                nota: f.pede_prova ? `A NetScreen revê depois de confirmares: ${semPontoFinal(f.pede_prova)}.` : null,
                accoes: [
                  g.factos.length > 1 && <span key="c">{botao('Está certo', () => confirmar([f]), true)}</span>,
                  <span key="n">{naoEstaCerto(f)}</span>,
                ],
              }))}
            </ul>
          </div>
        ))}
      </section>

      <section className="space-y-3">
        <h3 className="ns-seccao-titulo">Entra nas campanhas ({emUso.length})</h3>
        {emUso.length === 0 ? (
          <p className="text-xs text-muted-foreground">Ainda nada. Sem dados confirmados, as campanhas não afirmam nada de concreto sobre a marca.</p>
        ) : (
          <ul className="border border-border">
            {emUso.map((f) => linha(f, {
              etiqueta: f.origemDaDecisao === 'cliente' ? 'Confirmado por ti' : 'Revisto pela NetScreen',
              accoes: [
                f.origemDaDecisao === 'cliente' && !sinalizacaoAberta(f) && <span key="d">{botao('Desfazer', () => desfazer(f))}</span>,
                <span key="n">{naoEstaCerto(f)}</span>,
              ],
            }))}
          </ul>
        )}
      </section>

      {/* Quatro secções e não cinco (advogado da simplicidade, 24 Set): o que
          espera pela NetScreen, e o que ficou de fora -- proibido ou retirado,
          cada linha diz porquê. */}
      {dados.com_netscreen.length > 0 && (
        <section className="space-y-3">
          <h3 className="ns-seccao-titulo">Com a NetScreen ({dados.com_netscreen.length})</h3>
          <ul className="border border-border">
            {dados.com_netscreen.map((f) => linha(f, {
              nota: f.confirmado && f.pede_prova ? `Confirmaste. A NetScreen revê antes de entrar, porque ${semPontoFinal(f.pede_prova)}.` : null,
              accoes: f.confirmado
                ? [<span key="d">{botao('Desfazer a confirmação', () => desfazer(f))}</span>, f.pode_mandar_prova && botaoProva(f)]
                : null,
            }))}
          </ul>
        </section>
      )}

      {(dados.proibidos.length > 0 || (dados.retirados && dados.retirados.length > 0)) && (
        <section className="space-y-3">
          <h3 className="ns-seccao-titulo">Fora das campanhas ({dados.proibidos.length + (dados.retirados || []).length})</h3>
          <ul className="border border-border">
            {dados.proibidos.map((f) => linha(f, {
              etiqueta: 'Nunca se escreve',
              accoes: [<span key="n">{naoEstaCerto(f)}</span>],
            }))}
            {(dados.retirados || []).map((f) => linha(f, {
              // Quem tirou: o cliente, ou a NetScreen (ronda 9: "Retirado" nas duas).
              etiqueta: f.retirado_por_ti ? 'Retiraste' : 'Recusado pela NetScreen',
              // O que o próprio cliente retirou repõe-se: volta a "por confirmar". O
              // resto segue o que a API aceita (pode_mandar_prova), e não uma
              // sinalização dos últimos 30 dias (revisor, 24 Set).
              accoes: f.retirado_por_ti
                ? [<span key="r">{botao('Afinal está certo', () => repor(f))}</span>]
                : f.pode_mandar_prova ? [botaoProva(f)] : null,
            }))}
          </ul>
        </section>
      )}

      <input
        ref={campoProva}
        type="file"
        className="hidden"
        aria-label="Escolher o ficheiro da prova"
        onChange={(e) => enviarProva(e.target.files && e.target.files[0])}
      />

      {/* O que acontece depende de quem decidiu: um dado revisto pela NetScreen
          vai-lhe; os outros saem logo (ensaio de ponta a ponta, 24 Set: o diálogo
          prometia uma resposta que não vinha). */}
      <Modal
        aberto={!!aSinalizar}
        onFechar={() => setASinalizar(null)}
        titulo="Isto não está certo"
        acoes={
          <>
            <button type="button" onClick={() => setASinalizar(null)} className="border border-border px-4 py-2 text-sm hover:bg-secondary">Cancelar</button>
            <button
              type="button"
              disabled={ocupado || !texto.trim()}
              onClick={sinalizar}
              className="bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90 disabled:bg-secondary disabled:text-muted-foreground"
            >
              {vaiANetscreen ? 'Enviar à NetScreen' : 'Retirar'}
            </button>
          </>
        }
      >
        {aSinalizar && (
          <div className="space-y-3">
            <p className="text-sm text-foreground">"{aSinalizar.afirmacao}"</p>
            <label htmlFor="sinalizar-texto" className="label-eyebrow">O que está errado</label>
            <textarea
              id="sinalizar-texto"
              value={texto}
              maxLength={1000}
              rows={4}
              onChange={(e) => setTexto(e.target.value)}
              className="w-full border border-border bg-background px-3 py-2 text-sm text-foreground outline-none focus-visible:border-primary"
            />
            <p className="text-xs text-muted-foreground">
              {vaiANetscreen
                ? 'Foi a NetScreen que o reviu: vai-lhe a tua correcção, e até responder isto não entra nas campanhas.'
                : 'Sai já, e não entra nas campanhas. O que escreveres fica registado, para lermos melhor da próxima vez.'}
            </p>
            {aSinalizar.campanhas && aSinalizar.campanhas.length > 0 && (
              <p className="border-l-2 border-accent pl-3 text-xs text-foreground">
                Este dado aparece em {aSinalizar.campanhas.length === 1 ? '1 campanha' : `${aSinalizar.campanhas.length} campanhas`}: {listaCampanhas(aSinalizar.campanhas)}.
                {' '}Ficam marcadas até se corrigir o texto, e não vão para o ar assim.
              </p>
            )}
          </div>
        )}
      </Modal>
    </div>
  );
}

// O dossiê saiu do ecrã do cliente a 24 Set: o que tinha de útil passou para
// "Como fala" (a marca numa frase, o tom) e para os factos. Ver a avaliação da
// marca, secção 0.

// ─── Identidade visual ───────────────────────────────────────────────────────
// A app propõe as cores (dos #hex do brand book) e o logótipo (uma imagem com
// "logo" no nome); o cliente confirma ou corrige, a ver o cartaz a mudar. Só o
// confirmado entra nos cartazes das campanhas. Ver lib/brandKit.js.

const CORES_KIT = [
  // Branco ou preto, o que se ler (agentes, 20 Out: dizia «texto branco» e o exemplo mostrava preto sobre o Volt).
  { chave: 'cor_primaria', proposta: 'primaria', rotulo: 'Primária', uso: 'O destaque (um «-20%», por exemplo), com o texto por cima a branco ou a preto, o que se ler melhor.' },
  { chave: 'cor_secundaria', proposta: 'fundo', rotulo: 'Fundo', uso: 'O fundo das imagens para os painéis.' },
  { chave: 'cor_accent', proposta: 'acento', rotulo: 'Acento', uso: 'O subtítulo e a chamada, escritos sobre o fundo.' },
];
const CORES_NEUTRAS = { cor_primaria: '#E8400C', cor_secundaria: '#1C1C1C', cor_accent: '#F5A623' };
const COR_HEX = /^#[0-9a-fA-F]{6}$/;

// Sem acento no manual, uma cor da marca no lugar dele: a do texto, ou a primária.
// O laranja neutro aparecia como proposta da NESCAFÉ, que não tem laranja nenhum, e
// o exemplo dos painéis ficava vazio por "faltar uma cor" (ensaio de 7 Out).
function acentoDaProposta(p) {
  return coresDaPropostaNoEcra(p).acento;
}

// O fundo e o texto da proposta, com um fundo quando o manual não o dá (2.º ensaio NESCAFÉ,
// 7 Out: o preto do manual vinha no texto, sem fundo, e o exemplo dos painéis ficava vazio).
// Num painel LED o fundo é escuro: um texto escuro sem fundo passa a fundo, e o texto a branco.
function coresDaPropostaNoEcra(p) {
  const q = p || {};
  let fundo = q.fundo || null;
  let texto = q.texto || null;
  if (!fundo) {
    if (texto && COR_HEX.test(texto) && luminanciaCor(texto) < 0.2) { fundo = texto; texto = '#FFFFFF'; }
    else fundo = CORES_NEUTRAS.cor_secundaria;
  }
  const acento = q.acento || (texto && texto !== fundo ? texto : null) || q.primaria || null;
  return { fundo, texto, acento };
}

/**
 * De onde vem o fundo ou o acento proposto, quando não é uma cor que o manual dá a esse papel
 * (agentes, 20 Out: o fundo era a primária escurecida e parecia a cor da marca). null quando o
 * manual a dá: aí diz-se o excerto.
 */
function deOndeVemACor(p, papel) {
  if (!p || !p.primaria || (papel !== 'fundo' && papel !== 'acento') || (p.origem && p.origem.site)) return null;
  if (p.origem && p.origem.manual) {
    if (p[papel]) return null;
    const noEcra = coresDaPropostaNoEcra(p);
    if (papel === 'fundo') {
      return noEcra.fundo === p.texto
        ? 'O manual não diz a cor de fundo: usamos a do texto, que é escura e se lê nos painéis.'
        : 'O manual não diz a cor de fundo: pusemos um escuro neutro, que se lê nos painéis.';
    }
    return noEcra.acento === p.primaria
      ? 'O manual não diz a cor de acento: usamos a primária.'
      : 'O manual não diz a cor de acento: usamos a do texto.';
  }
  const de = p.de || {};
  if (papel === 'fundo') {
    return de.fundo === 'logotipo' ? 'Uma cor escura do teu logótipo.' : de.fundo === 'documentos'
      ? 'Uma cor escura dos teus documentos.'
      : 'Não encontrámos uma cor escura nos teus documentos: é a primária escurecida, para o texto claro se ler nos painéis.';
  }
  if (de.acento === 'logotipo') {
    return de.acento_ajustado ? 'A segunda cor do teu logótipo, clareada para se ler sobre o fundo.' : 'A segunda cor do teu logótipo.';
  }
  if (de.acento === 'documentos') {
    return de.acento_ajustado
      ? 'A segunda cor dos teus documentos, clareada para se ler sobre o fundo.'
      : 'A segunda cor dos teus documentos.';
  }
  return p.origem && p.origem.logo ? 'O teu logótipo não tem uma segunda cor: é a primária clareada, para se ler sobre o fundo.' : 'Não encontrámos uma segunda cor nos teus documentos: é a primária clareada, para se ler sobre o fundo.';
}

/** O nome do manual de onde foi tirado um logótipo, para a frase («do manual X, página N»). */
function doManual(logos, id) {
  const m = (logos || []).find((l) => l.id === id);
  const pagina = paginaDoManual(logos, id);
  if (!pagina) return null;
  return `do manual ${m && m.manual_nome ? `«${m.manual_nome}» ` : 'de normas '}(página ${pagina})`;
}

/** O fundo do mosaico de um logótipo: escuro para um claro, claro para os outros (agentes, 20 Out). */
const fundoDoLogo = (m) => (m && m.tom === 'claro' ? '#0b0b0f' : '#f4f4f5');

function luminanciaCor(hex) {
  const [r, g, b] = [1, 3, 5].map((i) => {
    const c = parseInt(hex.slice(i, i + 2), 16) / 255;
    return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
  });
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function contrasteCores(a, b) {
  const [x, y] = [luminanciaCor(a), luminanciaCor(b)].sort((p, q) => q - p);
  return (x + 0.05) / (y + 0.05);
}

/**
 * A página do manual de onde o logótipo foi tirado, ou null. A origem vem da API (`do_manual`); o
 * nome do ficheiro só dá o número da página (um ficheiro do cliente com o mesmo nome não conta).
 */
function paginaDoManual(logos, id) {
  const m = (logos || []).find((l) => l.id === id);
  const r = m && m.do_manual && /-do-manual-p(\d+)\.svg$/.exec(m.original_name || '');
  return r ? r[1] : null;
}

// O kit no ecrã é diferente do que está guardado (ou proposto, antes do primeiro «Guardar»).
const CAMPOS_KIT = ['cor_primaria', 'cor_secundaria', 'cor_accent', 'cor_texto', 'font_titulo', 'font_texto', 'logo_material_id', 'logo_negativo_material_id'];
function kitDiferente(a, b) {
  return CAMPOS_KIT.some((c) => String(a[c] || '').toLowerCase() !== String(b[c] || '').toLowerCase());
}

function kitInicial(d) {
  const k = d.kit;
  const p = d.proposta || {};
  const sitePorEscolher = !k && p.origem?.site;
  return {
    cor_primaria: (k ? k.cor_primaria : p.primaria) || (sitePorEscolher ? '' : CORES_NEUTRAS.cor_primaria),
    cor_secundaria: (k ? k.cor_secundaria : coresDaPropostaNoEcra(p).fundo) || CORES_NEUTRAS.cor_secundaria,
    cor_accent: (k ? k.cor_accent : sitePorEscolher ? p.acento : acentoDaProposta(p)) || (sitePorEscolher ? '' : CORES_NEUTRAS.cor_accent),
    cor_texto: (k ? k.cor_texto : coresDaPropostaNoEcra(p).texto) || null,
    // Um logótipo apagado da Biblioteca continua no kit (o apagar é suave), mas
    // já não está na lista: sem isto, guardar dava erro sem se perceber porquê.
    logo_material_id: ((k ? k.logo_material_id : p.logo_material_id) || null) &&
      (d.logos || []).some((m) => m.id === (k ? k.logo_material_id : p.logo_material_id))
      ? (k ? k.logo_material_id : p.logo_material_id) : null,
    // As fontes (28 Set): as guardadas, ou as que o manual nomeia.
    ...fontesIniciais(k, p),
    // A versão para fundos escuros (28 Set): a guardada, ou a proposta pelo tom.
    logo_negativo_material_id: (() => {
      const v = k ? k.logo_negativo_material_id : p.logo_negativo_material_id;
      return v && (d.logos || []).some((m) => m.id === v) ? v : null;
    })(),
  };
}

function fontesIniciais(k, p) {
  const f = (p && p.fontes) || {};
  const doKit = k && (k.font_titulo || k.font_texto);
  return {
    font_titulo: (doKit ? k.font_titulo : f.titulo && f.titulo.familia) || null,
    font_texto: (doKit ? k.font_texto : f.texto && f.texto.familia) || null,
    // O nome do manual só quando se usa outra no lugar dela: "Inter SemiBold" é a Inter.
    font_titulo_original: (doKit ? k.font_titulo_original : f.titulo && f.titulo.substituta && f.titulo.original) || null,
    font_texto_original: (doKit ? k.font_texto_original : f.texto && f.texto.substituta && f.texto.original) || null,
  };
}

/** Algumas do Google Fonts, para sugerir no campo. Escreve-se qualquer outra. */
const FONTES_SUGERIDAS = ['Inter', 'Montserrat', 'Lexend', 'Poppins', 'DM Sans', 'Source Sans 3', 'Nunito Sans', 'Lato',
  'Oswald', 'Barlow Condensed', 'Bebas Neue', 'Anton', 'Playfair Display', 'Cormorant Garamond', 'EB Garamond',
  'Libre Baskerville', 'Merriweather', 'Jost', 'Work Sans', 'Raleway'];

/**
 * O visto no canto de um tile escolhido (28 Set). Os tiles com imagem têm o
 * fundo da marca por baixo, e a borda laranja sozinha perdia-se nele.
 */
function VistoEscolha() {
  return (
    <span className="absolute right-1.5 top-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-primary-foreground" aria-hidden="true">
      <svg viewBox="0 0 16 16" className="h-3 w-3"><path d="M3.5 8.5l3 3 6-7" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" /></svg>
    </span>
  );
}

/**
 * Carregar um logótipo ali mesmo (18 Out): o ficheiro entra na marca com o papel de logótipo
 * e fica escolhido no lugar onde se carregou. Guarda-se com o resto do kit.
 */
function CarregarLogo({ slug, rotulo, onCarregado, disabled }) {
  const entrada = React.useRef(null);
  const [aEnviar, setAEnviar] = React.useState(false);
  const [erro, setErro] = React.useState(null);
  async function enviar(ficheiro) {
    if (!ficheiro) return;
    setAEnviar(true); setErro(null);
    try {
      const q = new URLSearchParams({ nome: ficheiro.name, ler: '0' });
      const r = await fetch(`/api/${slug}/materiais?${q}`, {
        method: 'POST', credentials: 'same-origin',
        headers: { 'Content-Type': ficheiro.type || 'application/octet-stream' }, body: ficheiro,
      });
      const corpo = await r.json().catch(() => null);
      if (!r.ok) throw new Error((corpo && corpo.error) || `Erro ${r.status}`);
      const novo = corpo.material || corpo;
      if (novo.papel !== 'logotipo') await api.patch(`/api/${slug}/materiais/${novo.id}`, { papel: 'logotipo' });
      // O mesmo ficheiro já carregado só numa campanha volta desduplicado e ainda não é da marca
      // (revisor, 18 Out: ficava fora da grelha e o «Guardar» recusava-o).
      if (novo.campanha_id) await api.patch(`/api/${slug}/materiais/${novo.id}`, { ambito: 'marca' });
      await onCarregado(novo.id);
    } catch (e) {
      setErro(e.message);
    } finally {
      setAEnviar(false);
      if (entrada.current) entrada.current.value = '';
    }
  }
  return (
    <span className="inline-flex flex-col gap-1">
      <input ref={entrada} type="file" accept=".svg,.png,.webp,.jpg,.jpeg,image/svg+xml,image/png,image/webp,image/jpeg" className="hidden"
        onChange={(e) => enviar(e.target.files && e.target.files[0])} />
      <button type="button" disabled={disabled || aEnviar} onClick={() => entrada.current && entrada.current.click()}
        className="ns-btn ns-btn-sm ns-btn-suave self-start text-xs">
        {aEnviar ? <><ATrabalhar className="mr-1.5" />A carregar…</> : rotulo}
      </button>
      {erro && <span className="text-xs text-perigo-texto" role="alert">{erro}</span>}
    </span>
  );
}

/**
 * A avaliação do logótipo guardado (18 Out): resolução, transparência e as versões que faltam,
 * medidas nos bytes (lib/qualidadeFontes.js). `chave` muda quando o kit guardado muda.
 */
function QualidadeLogo({ slug, chave }) {
  const [q, setQ] = React.useState(undefined);
  React.useEffect(() => {
    let vivo = true;
    setQ(undefined);
    api.get(`/api/${slug}/marca/logo/qualidade`).then((r) => { if (vivo) setQ(r); }).catch(() => { if (vivo) setQ(null); });
    return () => { vivo = false; };
  }, [slug, chave]);
  if (q === undefined) return <p className="text-xs text-muted-foreground" role="status"><ATrabalhar className="mr-1.5" />A avaliar o logótipo…</p>;
  if (!q) return <p className="text-xs text-muted-foreground" role="status">Não deu para avaliar o logótipo agora.</p>;
  const linhas = [];
  for (const [rotulo, a] of [['Logótipo', q.principal], ['Para fundos escuros', q.escuros]]) {
    if (!a) continue;
    linhas.push({ k: rotulo, nivel: a.nivel, texto: a.nivel === 'bom' ? `${rotulo}: nítido em todos os formatos${a.medida ? ` (${a.medida.w} px)` : ''}.` : `${rotulo}: ${a.motivos.join(' ')}` });
  }
  for (const f of q.falta || []) linhas.push({ k: f, nivel: 'aceitavel', texto: f });
  if (!linhas.length) return null;
  return (
    <ul className="space-y-1" aria-label="Qualidade do logótipo guardado">
      {linhas.map((l) => (
        <li key={l.k} className={`text-xs leading-relaxed ${l.nivel === 'fraco' ? 'text-perigo-texto' : l.nivel === 'bom' ? 'text-muted-foreground' : 'text-accent'}`}>{l.texto}</li>
      ))}
    </ul>
  );
}

// `daEquipa`: a mesma identidade aberta na ficha do back office. Os textos passam à
// terceira pessoa — a equipa lia «as tuas campanhas» (ensaio da demo de 12 Out, P10).
// `semFicheiros`: a conta ainda não tem nada carregado (agentes, 20 Out: dizia «não encontrámos
// cores nos teus documentos» sem documento nenhum).
/**
 * O nome da marca, como sai nas peças e na frase do assistente na loja. Nascia com a conta e não se
 * mudava: numa agência ficava o nome interno («Cliente Beta») nas peças da Pulso (agentes, 20 Out).
 */
function NomeDaMarca({ slug, inicial, podeMexer, onGuardado }) {
  const [nome, setNome] = React.useState(inicial || '');
  const [estado, setEstado] = React.useState(null);
  React.useEffect(() => { setNome(inicial || ''); }, [inicial]);
  const mudou = nome.trim() && nome.trim() !== (inicial || '');
  async function guardar(e) {
    e.preventDefault();
    setEstado({ aGuardar: true });
    try {
      const r = await api.put(`/api/${slug}/marca/nome`, { nome });
      setEstado({ ok: 'Guardado. As peças novas usam este nome.' });
      onGuardado(r.nome_marca);
    } catch (err) {
      setEstado({ erro: err.message || 'Não foi possível guardar o nome.' });
    }
  }
  return (
    <form onSubmit={guardar} className="flex flex-wrap items-end gap-2">
      <label className="min-w-0 grow sm:max-w-sm">
        <span className="text-xs uppercase tracking-wider text-muted-foreground">Nome da marca</span>
        <input value={nome} disabled={!podeMexer || (estado && estado.aGuardar)} maxLength={120}
          onChange={(e) => { setNome(e.target.value); setEstado(null); }}
          className="mt-1 block h-10 w-full border border-border bg-background px-3 text-sm text-foreground" />
      </label>
      {podeMexer && mudou && (
        <button type="submit" disabled={estado && estado.aGuardar}
          className="h-10 border border-primary px-4 text-xs uppercase tracking-wider text-primary hover:bg-primary hover:text-primary-foreground">
          {estado && estado.aGuardar ? 'A guardar…' : 'Guardar o nome'}
        </button>
      )}
      <p className="w-full text-xs text-muted-foreground" role={estado && estado.erro ? 'alert' : 'status'}>
        {estado && (estado.erro || estado.ok) ? (estado.erro || estado.ok) : 'É o nome que aparece nas imagens, nos vídeos e na frase do assistente na loja.'}
      </p>
    </form>
  );
}

function MarcaIdentidade({ slug, podeMexer, onMudou, onNavegar, daEquipa = false, semFicheiros = false }) {
  const [d, setD] = React.useState(null);
  const [kit, setKit] = React.useState(null);
  const [ocupado, setOcupado] = React.useState(false);
  const [aviso, setAviso] = React.useState(null);
  // O selector de cor dispara a cada movimento; cada mudança são três cartazes
  // desenhados no servidor. A pré-visualização segue o kit com um atraso curto.
  const [kitVisto, setKitVisto] = React.useState(null);
  const [verTodas, setVerTodas] = React.useState(false);
  const [verTexto, setVerTexto] = React.useState(false);
  React.useEffect(() => {
    const t = setTimeout(() => setKitVisto(kit), 350);
    return () => clearTimeout(t);
  }, [kit]);

  const carregar = React.useCallback(() => api.get(`/api/${slug}/marca/kit`)
    .then((r) => { setD(r); setKit(kitInicial(r)); })
    .catch((e) => setD({ erro: e.message || 'sem resposta' })), [slug]);
  React.useEffect(() => { setD(null); setKit(null); setAviso(null); carregar(); }, [carregar]);

  // Sair com o kit mudado avisa (revisor, 18 Out: um logótipo carregado ficava escolhido, sem
  // guardar, e perdia-se ao voltar à campanha).
  const porGuardar = Boolean(podeMexer && d && !d.erro && kit && kitDiferente(kitInicial(d), kit));
  React.useEffect(() => {
    window.__porGuardar = porGuardar ? 'A identidade visual tem alterações por guardar.' : null;
    if (!porGuardar) return undefined;
    const avisar = (e) => { e.preventDefault(); e.returnValue = ''; };
    window.addEventListener('beforeunload', avisar);
    return () => { window.removeEventListener('beforeunload', avisar); window.__porGuardar = null; };
  }, [porGuardar]);

  // «Trocar o logótipo» da campanha desce até ao logótipo (cliente simulado, 18 Out).
  React.useEffect(() => {
    if (!d || d.erro || window.location.hash !== '#logotipo') return;
    // Depois do topo que o AppShell repõe a cada página (corre a seguir a este efeito).
    const t = setTimeout(() => {
      const el = document.getElementById('logotipo-da-marca');
      if (el) el.scrollIntoView({ block: 'start' });
    }, 100);
    return () => clearTimeout(t);
  }, [Boolean(d)]);

  if (!d) return <div className="loading-skeleton h-60" />;
  if (d.erro) return <p className="text-sm text-accent" role="status">Não deu para ler a identidade visual: {d.erro}</p>;

  const valido = CORES_KIT.every((c) => COR_HEX.test(kit[c.chave])) && (!kit.cor_texto || COR_HEX.test(kit.cor_texto));
  const referenciaSiteSemPaleta = !d.kit && d.proposta?.origem?.site && (!d.proposta.primaria || !d.proposta.acento);
  // O excerto do manual de onde veio cada cor (24 Set), enquanto a cor é a do
  // manual -- proposta ou já guardada. Guardada sem isto, parecia escolhida à mão
  // (cliente simulado, 24 Set).
  const CHAVE_DO_PAPEL = { primaria: 'cor_primaria', fundo: 'cor_secundaria', acento: 'cor_accent', texto: 'cor_texto' };
  // A citação do manual vem do modelo com pedaços colados («… R 225 | G 38 | … · R 225 | G 38 | B 28»):
  // para uma cor basta o Pantone e o RGB (C-5, NESCAFÉ 30 Out). Sem eles, o primeiro pedaço inteiro.
  const excertoLimpo = (t) => {
    if (!t) return t;
    const s = String(t).replace(/\s+/g, ' ');
    const pantone = (s.match(/PANTONE\s+(?:Process\s+)?[\w-]{1,12}(?:\s+C\b)?/i) || [])[0];
    const rgb = s.match(/R\s*[:=]?\s*(\d{1,3})\s*[|,;/]?\s*G\s*[:=]?\s*(\d{1,3})\s*[|,;/]?\s*B\s*[:=]?\s*(\d{1,3})/i);
    if (pantone || rgb) return [pantone, rgb && `R ${rgb[1]} G ${rgb[2]} B ${rgb[3]}`].filter(Boolean).join(' · ');
    return s.split(/…|\.\.\./)[0].trim();
  };
  const excertoDe = (papel) => {
    const pr = d.proposta;
    if (!pr || !pr.origem || !pr.origem.manual || !pr.origem.excertos || !pr[papel]) return null;
    return String(kit[CHAVE_DO_PAPEL[papel]] || '').toUpperCase() === String(pr[papel]).toUpperCase()
      ? pr.origem.excertos[papel] || null : null;
  };
  const logoMudado = ['logo_material_id', 'logo_negativo_material_id'].some((c) => (kit[c] || null) !== ((d.kit && d.kit[c]) || null));
  const mudou = (campo, valor) => { setKit({ ...kit, [campo]: valor }); setAviso(null); };
  // Um logótipo carregado aqui: relê a lista (para aparecer na grelha) sem perder o que já se
  // mudou no kit, e fica escolhido no seu lugar. Grava-se com «Guardar».
  const usarCarregado = async (campo, id) => {
    const r = await api.get(`/api/${slug}/marca/kit`);
    setD(r);
    setKit((k) => ({ ...k, [campo]: id }));
    // O nome do botão que está no ecrã: sem kit guardado é «Confirmar…» (agentes, 20 Out).
    setAviso(`Carregado e escolhido. As campanhas só o usam depois de «${d.kit ? 'Guardar as correcções' : 'Confirmar a identidade visual'}».`);
  };
  // Até 20 Out, as outras imagens só se escondiam quando alguém já tinha dito qual é o logótipo
  // (cliente simulado, 24 Set: com o papel só sugerido, uma fotografia chamada
  // "logo.png" ficava à frente e o logótipo verdadeiro escondido).
  // A versão para fundos escuros escolhida também (cliente simulado, 18 Out: carregada no próprio
  // lugar, sem papel confirmado, ficava escondida e o grupo parecia vazio).
  // Desde 20 Out, também os de papel só proposto: as fotografias e a story de preços apareciam como
  // logótipos, e com o kit guardado os outros negativos só se viam em «Ver todas» (agentes). O papel
  // proposto já separa as fotografias pelos bytes (lib/papel.js), e as outras imagens ficam a um clique.
  const soLogos = d.logos.filter((m) => m.papel === 'logotipo'
    || [kit.logo_material_id, kit.logo_negativo_material_id, d.kit?.logo_material_id, d.kit?.logo_negativo_material_id].includes(m.id));
  const logosVisiveis = verTodas || !soLogos.length ? d.logos : soLogos;
  const outrasImagens = d.logos.length - logosVisiveis.length;
  // O logótipo guardado que foi apagado dos Ficheiros (agentes, 20 Out: a versão para fundos escuros
  // passava a «Não tenho» sem aviso, e o cartão continuava «Guardada»).
  const apagadosDoKit = d.kit
    ? [['logo_material_id', 'O logótipo guardado'], ['logo_negativo_material_id', 'A versão para fundos escuros guardada']]
      .filter(([c]) => d.kit[c] && !d.logos.some((m) => m.id === d.kit[c])).map(([, r]) => r)
    : [];
  const logoDe = (id) => d.logos.find((m) => m.id === id) || null;
  const verTodasBotao = outrasImagens > 0 && !verTodas && (
    <button type="button" onClick={() => setVerTodas(true)} className="text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground">
      {`Ver todas as imagens (mais ${outrasImagens})`}
    </button>
  );
  // Um mosaico da grelha: a imagem num fundo onde se veja, e o nome por baixo (agentes, 20 Out:
  // os negativos brancos ficavam invisíveis, e os mosaicos não tinham nome).
  const mosaico = (m, campo, vazio) => {
    const escolhido = (kit[campo] || null) === m.id;
    return (
      <button
        key={m.id || 'nenhum'}
        type="button"
        role="radio"
        aria-checked={escolhido}
        disabled={!podeMexer}
        onClick={() => mudou(campo, m.id)}
        title={m.original_name}
        className={`ns-escolha relative flex h-24 flex-col items-center justify-center gap-1 overflow-hidden rounded-md border p-2 text-xs ${escolhido ? 'border-primary text-foreground ring-1 ring-primary' : `border-border text-muted-foreground ${m.id ? '' : 'border-dashed'}`}`}
      >
        {m.id ? (
          <>
            <span className="flex min-h-0 w-full flex-1 items-center justify-center rounded-sm p-1" style={{ background: fundoDoLogo(m) }}>
              <ImagemOuAviso src={m.url} alt={m.original_name} texto="Não abre" className="max-h-10 max-w-full object-contain" />
            </span>
            <span className="block w-full truncate text-[10px] leading-tight">{nomeAVista(m.original_name, true)}</span>
          </>
        ) : vazio}
        {escolhido && <VistoEscolha />}
      </button>
    );
  };
  // O tom do escolhido, antes de guardar (agentes, 20 Out: o aviso só aparecia depois de guardar).
  const principalClaro = logoDe(kit.logo_material_id)?.tom === 'claro';
  const negativoEscuro = logoDe(kit.logo_negativo_material_id)?.tom === 'escuro';
  // Depois de confirmado, os botões só aparecem com alguma coisa mudada (sessão
  // de 24 Set): "Guardar as correcções" sempre activo fazia pensar que não tinha
  // gravado, e carregava-se outra vez.
  const guardado = kitInicial(d);
  const alterado = !d.kit || ['cor_primaria', 'cor_secundaria', 'cor_accent', 'cor_texto'].some(
    (c) => String(kit[c] || '').toLowerCase() !== String(guardado[c] || '').toLowerCase()
  ) || (kit.logo_material_id || null) !== (guardado.logo_material_id || null)
    || ['font_titulo', 'font_texto', 'logo_negativo_material_id'].some((c) => (kit[c] || '') !== (guardado[c] || ''))
    // Um kit guardado antes das fontes, com o manual a nomeá-las: há o que guardar.
    || Boolean(d.kit && !d.kit.font_titulo && !d.kit.font_texto && (kit.font_titulo || kit.font_texto));
  const visto = kitVisto || kit;
  const previaNeutra = referenciaSiteSemPaleta && !valido;
  const coresVistas = previaNeutra ? { ...visto, cor_primaria: '#000000', cor_secundaria: '#FFFFFF', cor_accent: '#000000', cor_texto: '#000000' } : visto;
  const vistoValido = CORES_KIT.every((c) => COR_HEX.test(visto[c.chave]));
  const previa = (formato) => {
    const q = new URLSearchParams({
      formato, primaria: coresVistas.cor_primaria, fundo: coresVistas.cor_secundaria, acento: coresVistas.cor_accent,
    });
    if (visto.logo_material_id) q.set('logo', visto.logo_material_id);
    if (coresVistas.cor_texto && COR_HEX.test(coresVistas.cor_texto)) q.set('texto', coresVistas.cor_texto);
    if (visto.logo_negativo_material_id) q.set('logo_negativo', visto.logo_negativo_material_id);
    if (visto.font_titulo) q.set('titulo_fonte', visto.font_titulo);
    if (visto.font_texto) q.set('texto_fonte', visto.font_texto);
    return `/api/${slug}/marca/kit/previa.png?${q}`;
  };
  const avisos = [];
  // "Um fundo claro encandeia" não se diz como regra: não foi medido num painel
  // (parecer do Codex, 24 Set). Diz-se o que acontece ao cartaz.
  if (valido && !kit.cor_texto && contrasteCores(kit.cor_secundaria, '#F5F2ED') < 4.5) avisos.push('O fundo é claro: o texto passa a escuro.');
  if (valido && contrasteCores(kit.cor_accent, kit.cor_secundaria) < 3) avisos.push('O acento quase não se distingue do fundo: a chamada pode não se ler.');
  // O destaque (o "-20%") vai na primária sobre o fundo; se não se ler, o cartaz
  // troca-a (lib/composicao.js) e diz-se aqui porquê.
  if (valido && contrasteCores(kit.cor_primaria, kit.cor_secundaria) < 3) avisos.push('A primária não se lê sobre o fundo: nos painéis, os números em destaque saem noutra cor.');

  async function confirmar() {
    setOcupado(true);
    setAviso(null);
    try {
      const r = await api.put(`/api/${slug}/marca/kit`, kit);
      setD({ ...d, kit: r.kit });
      // Sem outra mensagem: a linha "Guardada a ... por ..." já o diz (eram
      // três a dizer o mesmo, cliente simulado de 24 Set).
      setAviso(null);
      if (onMudou) onMudou();
      window.dispatchEvent(new Event('fila-mudou'));
    } catch (e) {
      setAviso(e.message || 'Não foi possível confirmar.');
    } finally {
      setOcupado(false);
    }
  }

  const origem = d.proposta && d.proposta.origem;
  // Só a primária é a cor tal como está nos documentos: o acento é ajustado
  // para se ler, e "encontrada 2 vezes" num valor que lá não está enganava.
  const vezesDe = (c) => {
    if (!origem || d.kit || kit[c.chave] !== d.proposta[c.proposta]) return null;
    return c.proposta === 'primaria' ? origem.primaria : null;
  };
  // A segunda visita (cliente simulado, 24 Set): um manual novo com outras
  // cores não mudava nada nem dizia nada. O que está guardado não muda sozinho.
  const p = d.proposta;
  // Só os papéis que a proposta dá: um manual que só diz a primária deixava o
  // aviso preso para sempre (revisor, 24 Set, R3).
  const PAPEIS_KIT = [['primaria', 'cor_primaria', 'Primária'], ['fundo', 'cor_secundaria', 'Fundo'],
    ['acento', 'cor_accent', 'Acento'], ['texto', 'cor_texto', 'Texto']];
  // Sem a cor do texto que não se lê no fundo confirmado: o cliente mudou-a por isso, e o aviso
  // voltava a sugeri-la a cada visita (ensaio NESCAFÉ de 9 Out: «Texto — Process Black» sobre preto).
  const novas = d.kit && p && p.primaria
    ? PAPEIS_KIT.filter(([a, b]) => p[a] && String(p[a]).toUpperCase() !== String(d.kit[b] || '').toUpperCase())
      .filter(([a]) => a !== 'texto' || !d.kit.cor_secundaria || contrasteCores(p.texto, d.kit.cor_secundaria) >= 4.5)
    : [];
  // Já vistas e postas de lado (ao confirmar, ou com «Manter as minhas cores», migração 066): só
  // voltam a dizer-se quando a proposta mudar (9 Out). A mesma regra da fila (lib/brandKit.js, haCoresNovas).
  const vistas = d.kit && d.kit.proposta_dispensada;
  const jaVistas = Boolean(vistas && p && ['primaria', 'fundo', 'acento', 'texto'].every(
    (c) => (vistas[c] || null) === (p[c] ? String(p[c]).toUpperCase() : null)
  ));
  const coresNovas = novas.length > 0 && !jaVistas;
  async function manter() {
    setOcupado(true);
    setAviso(null);
    try {
      const r = await api.post(`/api/${slug}/marca/kit/manter`, {});
      setD({ ...d, kit: { ...d.kit, proposta_dispensada: r.kit.proposta_dispensada } });
      // O «Contigo» do topo lê a vez da marca outra vez, como depois de confirmar.
      if (onMudou) onMudou();
      window.dispatchEvent(new Event('fila-mudou'));
    } catch (e) {
      setAviso(e.message || 'Não foi possível guardar.');
    } finally {
      setOcupado(false);
    }
  }
  const deOndeVem = p && p.origem && p.origem.manual ? 'O teu manual' : 'Os teus ficheiros';
  // A origem do fundo e do acento, enquanto são os da proposta e ainda não estão guardados.
  const origemDaCor = (c) => (!d.kit && p && String(kit[c.chave] || '').toUpperCase() === String(
    c.proposta === 'fundo' ? coresDaPropostaNoEcra(p).fundo : c.proposta === 'acento' ? acentoDaProposta(p) : '',
  ).toUpperCase() ? deOndeVemACor(p, c.proposta) : null);
  const proibidas = new Set((d.cores_proibidas || []).map((x) => String(x).toUpperCase()));

  return (
    <div className="space-y-6">
      <NomeDaMarca slug={slug} inicial={d.nome_marca} podeMexer={podeMexer}
        onGuardado={(nome) => { setD((x) => ({ ...x, nome_marca: nome })); if (onMudou) onMudou(); }} />
      <div className="max-w-prose space-y-1 text-sm text-muted-foreground">
        <p>{daEquipa
          ? 'É o que entra nas imagens e nos vídeos das campanhas desta conta: as cores, a letra e o logótipo.'
          : 'É o que entra nas imagens e nos vídeos das tuas campanhas: as cores, a letra e o logótipo.'}</p>
        {d.kit ? (
          <p>Confirmado por {d.kit.confirmado_por_nome || d.kit.confirmado_por} a {formatDataCurta(d.kit.confirmado_em)}.{daEquipa ? ' Pode ser corrigido a qualquer momento.' : ' Podes corrigir quando quiseres.'}</p>
        ) : referenciaSiteSemPaleta ? (
          <p role="status">Referência recolhida do site, por rever. As cores da interface não definem uma paleta oficial da marca. Escolhe a primária e o acento, ou acrescenta as normas em Ficheiros.</p>
        ) : d.proposta && d.proposta.primaria ? (
          <p>{daEquipa
            ? 'Proposta feita a partir do que lemos da marca, à espera de confirmação. Até lá, as imagens saem com as cores da NetScreen.'
            : 'Propusemos isto a partir do que lemos da tua marca. Confirma, ou corrige primeiro. Até confirmares, as imagens saem com as cores da NetScreen.'}</p>
        ) : (
          // Sem ficheiros não se diz que não se encontraram cores neles; e as cores que vêm nos campos
          // são as da NetScreen, laranja, e não «neutras» (agentes, 20 Out).
          <p>{semFicheiros && soLogos.length
            // Logótipo carregado aqui, sem manual: falta só a cor (florista virgem, 4 Nov: lia «ainda não carregaste ficheiros» com o logótipo à vista).
            ? (daEquipa
              ? 'O logótipo já está. As cores nos campos são as da NetScreen: escolham-se as da marca aqui, ou o cliente carrega o manual de normas em Ficheiros.'
              : 'O logótipo já está. Falta escolher as cores da marca: as dos campos são as da NetScreen, para começares.')
            : semFicheiros
            ? (daEquipa
              ? 'Ainda não há ficheiros da marca. As cores nos campos são as da NetScreen, para começar: podem escolher-se aqui, ou o cliente carrega o manual de normas em Ficheiros.'
              : 'Ainda não carregaste ficheiros da marca. As cores nos campos são as da NetScreen, para começares: escolhe as tuas aqui, ou carrega o manual de normas em Ficheiros.')
            : (daEquipa
              ? 'Não encontrámos cores nos documentos da marca. As cores nos campos são as da NetScreen: podem escolher-se aqui, ou o cliente acrescenta o manual de normas em Ficheiros.'
              : 'Não encontrámos cores nos teus documentos. As cores nos campos são as da NetScreen: escolhe as tuas aqui, ou acrescenta o manual de normas em Ficheiros.')}</p>
        )}
      </div>

      {coresNovas && podeMexer && (
        <div className="space-y-2 border-l-2 border-accent bg-[hsl(var(--accent)/0.08)] px-4 py-3 text-sm">
          <p className="text-foreground">{`${deOndeVem} ${deOndeVem === 'O teu manual' ? 'diz' : 'têm'} outras cores para a marca:`}</p>
          <ul className="space-y-1">
            {novas.map(([papel, , rotulo]) => (
              <li key={papel} className="flex flex-wrap items-center gap-x-2 text-xs text-muted-foreground">
                <span className="h-4 w-4 shrink-0 border border-border" style={{ background: p[papel] }} aria-hidden="true" />
                <span className="text-foreground">{rotulo}</span>
                {p.origem && p.origem.excertos && p.origem.excertos[papel] && <span>{`«${p.origem.excertos[papel]}»`}</span>}
              </li>
            ))}
          </ul>
          <div className="flex flex-wrap items-center gap-3">
          <button
            type="button"
            onClick={() => setKit({ ...kit, cor_primaria: p.primaria, cor_secundaria: p.fundo || kit.cor_secundaria, cor_accent: p.acento || kit.cor_accent, cor_texto: p.texto || kit.cor_texto })}
            className="ns-btn ns-btn-sm ns-btn-suave"
          >
            Experimentar estas cores
          </button>
          <button type="button" onClick={manter} disabled={ocupado} className="ns-btn ns-btn-sm ns-btn-suave disabled:opacity-50">
            {ocupado ? 'A guardar…' : 'Manter as minhas cores'}
          </button>
          <span className="text-xs text-muted-foreground">Experimentar só muda se guardares. Manter tira este aviso até haver outras cores novas.</span>
          </div>
        </div>
      )}

      <div className="grid gap-6 lg:grid-cols-[minmax(0,22rem)_minmax(0,1fr)]">
        <div className="space-y-6">
          <section className="space-y-4">
            <h3 className="text-sm font-medium text-foreground">Cores</h3>
            {CORES_KIT.map((c) => {
              const vezes = vezesDe(c);
              return (
                <div key={c.chave} className="space-y-1">
                  <label htmlFor={`kit-${c.chave}`} className="ns-rotulo">{c.rotulo}</label>
                  <div className="flex items-center gap-2">
                    {COR_HEX.test(kit[c.chave]) ? <input
                      type="color"
                      aria-label={`${c.rotulo}: escolher`}
                      value={COR_HEX.test(kit[c.chave]) ? kit[c.chave].toLowerCase() : '#000000'}
                      disabled={!podeMexer}
                      onChange={(e) => mudou(c.chave, e.target.value.toUpperCase())}
                      className="h-9 w-12 cursor-pointer border border-border bg-background p-0.5"
                    /> : <span className="h-9 w-12 border border-border bg-background" aria-hidden="true" />}
                    <input
                      id={`kit-${c.chave}`}
                      value={kit[c.chave]}
                      placeholder="#RRGGBB"
                      maxLength={7}
                      disabled={!podeMexer}
                      onChange={(e) => mudou(c.chave, e.target.value.trim().toUpperCase())}
                      aria-invalid={Boolean(kit[c.chave]) && !COR_HEX.test(kit[c.chave]) || undefined}
                      aria-describedby={!kit[c.chave] ? `kit-${c.chave}-por-escolher` : undefined}
                      className={`w-28 border bg-background px-3 py-2 font-mono text-sm text-foreground outline-none focus-visible:border-primary ${COR_HEX.test(kit[c.chave]) ? 'border-border' : 'border-accent'}`}
                    />
                  </div>
                  {!kit[c.chave] && <p id={`kit-${c.chave}-por-escolher`} className="text-xs text-muted-foreground">Por escolher. Indica uma cor das normas da marca.</p>}
                  <p className="text-xs text-muted-foreground">
                    {/* «Encontrada 1 vez» não dizia nada a um leigo (agentes, 20 Out). */}
                    {c.uso}{vezes ? ` É a cor da marca que mais aparece nos teus documentos.` : ''}
                  </p>
                  {excertoDe(c.proposta) && (
                    <p className="text-xs text-muted-foreground">{`Do manual: ${excertoLimpo(excertoDe(c.proposta))}`}</p>
                  )}
                  {!excertoDe(c.proposta) && origemDaCor(c) && <p className="text-xs text-muted-foreground">{origemDaCor(c)}</p>}
                  {proibidas.has(String(kit[c.chave] || '').toUpperCase()) && (
                    <p className="text-xs text-accent" role="status">Os teus documentos dizem para não usar esta cor. Escolhe outra, a não ser que essa regra já não valha.</p>
                  )}
                </div>
              );
            })}
            {/* A cor do texto (24 Set): a do manual, se a der; vazia, a app escolhe
                clara ou escura pelo fundo. Só se usa se se ler no fundo. Sem manual
                que a dê e sem ninguém a escolher, é uma linha e não um campo: a
                maioria dos clientes vê três cores, e não quatro (advogado da
                simplicidade, 24 Set). */}
            {!kit.cor_texto && !(d.proposta && d.proposta.texto) && !verTexto ? (
              <p className="text-xs text-muted-foreground">
                {'Texto: automático, claro ou escuro conforme o fundo. '}
                {podeMexer && (
                  <button type="button" onClick={() => setVerTexto(true)} className="underline underline-offset-2 hover:text-foreground">
                    Escolher uma cor
                  </button>
                )}
              </p>
            ) : (
            <div className="space-y-1">
              <label htmlFor="kit-cor_texto" className="ns-rotulo">Texto</label>
              <div className="flex items-center gap-2">
                <input
                  type="color"
                  aria-label="Texto: escolher"
                  value={COR_HEX.test(kit.cor_texto || '') ? kit.cor_texto.toLowerCase() : '#f5f2ed'}
                  disabled={!podeMexer}
                  onChange={(e) => mudou('cor_texto', e.target.value.toUpperCase())}
                  className="h-9 w-12 cursor-pointer border border-border bg-background p-0.5"
                />
                <input
                  id="kit-cor_texto"
                  value={kit.cor_texto || ''}
                  placeholder="Automática"
                  maxLength={7}
                  disabled={!podeMexer}
                  onChange={(e) => mudou('cor_texto', e.target.value.trim().toUpperCase() || null)}
                  aria-invalid={(kit.cor_texto && !COR_HEX.test(kit.cor_texto)) || undefined}
                  className={`w-28 border bg-background px-3 py-2 font-mono text-sm text-foreground outline-none focus-visible:border-primary ${!kit.cor_texto || COR_HEX.test(kit.cor_texto) ? 'border-border' : 'border-accent'}`}
                />
                {kit.cor_texto && podeMexer && (
                  <button type="button" onClick={() => mudou('cor_texto', null)} className="text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground">
                    Automática
                  </button>
                )}
              </div>
              <p className="text-xs text-muted-foreground">
                Os títulos e o texto. Vazia, fica clara ou escura conforme o fundo.
              </p>
              {kit.cor_texto && COR_HEX.test(kit.cor_texto) && valido && contrasteCores(kit.cor_texto, kit.cor_secundaria) < 4.5 && (
                <p className="text-xs text-accent" role="status">Não se lê neste fundo: usamos a clara ou a escura, a que se ler.</p>
              )}
              {excertoDe('texto') && <p className="text-xs text-muted-foreground">{`Do manual: ${excertoLimpo(excertoDe('texto'))}`}</p>}
            </div>
            )}
          </section>

          {/* A letra da marca (28 Set). Sempre do Google Fonts, que se pode usar
              em painéis e em vídeo; uma comercial do manual tem uma parecida no
              lugar dela, e diz-se qual. Vazia, a de omissão. */}
          <section className="space-y-3">
            <h3 className="text-sm font-medium text-foreground">Letra</h3>
            <datalist id="fontes-sugeridas">
              {FONTES_SUGERIDAS.map((f) => <option key={f} value={f} />)}
            </datalist>
            {[['font_titulo', 'font_titulo_original', 'titulo', 'Títulos'], ['font_texto', 'font_texto_original', 'texto', 'Texto']].map(([campo, orig, papel, rotulo]) => {
              const pf = d.proposta && d.proposta.fontes && d.proposta.fontes[papel];
              const original = kit[orig];
              return (
                <div key={campo} className="space-y-1">
                  <label htmlFor={`kit-${campo}`} className="ns-rotulo">{rotulo}</label>
                  <input
                    id={`kit-${campo}`}
                    list="fontes-sugeridas"
                    value={kit[campo] || ''}
                    // "A de omissão" era a nossa palavra; o cliente lê o que acontece se não escolher (ensaio de 7 Out, P8).
                    placeholder={campo === 'font_titulo' ? 'Bebas Neue, se não escolheres outra' : 'DM Sans, se não escolheres outra'}
                    maxLength={100}
                    disabled={!podeMexer}
                    onChange={(e) => setKit({ ...kit, [campo]: e.target.value || null, [orig]: null })}
                    className="w-full border border-border bg-background px-3 py-2 text-sm text-foreground outline-none focus-visible:border-primary"
                  />
                  {original && original !== kit[campo] && (
                    <p className="text-xs text-muted-foreground">
                      {`O manual pede ${original}, que é paga: usamos ${kit[campo]}, parecida e gratuita, que pode ir para os painéis e para vídeo.`}
                    </p>
                  )}
                  {/* A lida das fontes de um PDF é uma nota nossa, e não uma citação: sem aspas
                      (agentes, 20 Out: «Do manual: «A letra… em «X.pdf»…»» parecia vir do manual). */}
                  {pf && pf.familia === kit[campo] && pf.excerto && (
                    <p className="text-xs text-muted-foreground">{pf.de === 'pdf' ? pf.excerto : `${d.proposta?.origem?.site ? 'Do site' : 'Do manual'}: «${pf.excerto}»`}</p>
                  )}
                </div>
              );
            })}
          </section>

          <section id="logotipo-da-marca" className="scroll-mt-20 space-y-3">
            <h3 className="text-sm font-medium text-foreground">Logótipo</h3>
            {/* As especificações (18 Out): o que dá peças nítidas em todos os formatos. */}
            <p className="text-xs leading-relaxed text-muted-foreground">
              Um SVG fica nítido em tudo, tal como um PNG com fundo transparente e pelo menos 1000 px de largura.
              Duas versões: a principal, escura ou de cor, para fundos claros; e uma clara (branca), para fundos escuros.
            </p>
            {apagadosDoKit.map((r) => (
              <p key={r} className="border-l-2 border-accent pl-3 text-xs text-foreground" role="alert">
                {`${r} foi apagado dos Ficheiros: as campanhas já não o usam. Escolhe outro aqui e guarda, ou volta a carregar o mesmo ficheiro em Ficheiros, que ele volta ao lugar.`}
              </p>
            ))}
            {/* A avaliação é do guardado: com outro escolhido e por guardar, diz-se isso em vez de
                avaliar o antigo (cliente simulado, 18 Out). Só para quem pode trocar. */}
            {podeMexer && d.kit && (logoMudado
              ? <p className="text-xs text-accent" role="status">O logótipo escolhido ainda não está guardado: a avaliação aparece depois de «{d.kit ? 'Guardar as correcções' : 'Confirmar a identidade visual'}».</p>
              : <QualidadeLogo slug={slug} chave={`${d.kit.logo_material_id || ''}:${d.kit.logo_negativo_material_id || ''}`} />)}
            {d.logos.length === 0 && podeMexer ? null : d.logos.length === 0 ? (
              <p className="text-xs text-muted-foreground">
                Ainda não carregaste nenhuma imagem.{' '}
                <button type="button" onClick={() => onNavegar(`/${slug}/marca/fontes`)} className="underline underline-offset-2 hover:text-foreground">
                  Carrega o logótipo em Ficheiros e fontes
                </button>
                {' '}e volta aqui.
              </p>
            ) : (
              // Cada logótipo no fundo onde se vê, pelo tom (20 Out); até aí, todos num fundo claro (19 Out),
              // e os negativos brancos ficavam invisíveis.
              <div role="radiogroup" aria-label="Logótipo" className="grid grid-cols-3 gap-2">
                {/* As versões brancas por último: aqui procura-se a dos fundos claros (2.º ensaio NESCAFÉ, 30 Out). */}
                {[{ id: null, original_name: 'Sem logótipo' }].concat([...logosVisiveis].sort((a, b) => (a.tom === 'claro') - (b.tom === 'claro'))).map((m) => mosaico(m, 'logo_material_id', 'Sem logótipo'))}
              </div>
            )}
            {/* As fotografias não são candidatas a logótipo (24 Set): primeiro só
                os ficheiros que são logótipos, e as outras imagens a um clique --
                uma classificação errada não esconde o ficheiro (parecer do Codex). */}
            {verTodasBotao}
            {principalClaro && (
              <div className="flex flex-wrap items-center gap-2 text-xs text-accent" role="status">
                <span>Este logótipo é claro: em fundos claros não se vê. Parece a versão para fundos escuros.</span>
                {podeMexer && (
                  <button type="button" onClick={() => setKit({ ...kit, logo_material_id: null, logo_negativo_material_id: kit.logo_material_id })} className="underline underline-offset-2 hover:text-foreground">
                    Usar como versão para fundos escuros
                  </button>
                )}
              </div>
            )}
            {/* Só enquanto o escolhido é o proposto (agentes, 20 Out: a frase ficava depois de trocar), e a dizer de que manual veio. */}
            {d.proposta && d.proposta.logo_material_id && !d.kit && kit.logo_material_id === d.proposta.logo_material_id && (
              <p className="text-xs text-muted-foreground">
                {doManual(d.logos, d.proposta.logo_material_id)
                  ? `Tirámos este logótipo ${doManual(d.logos, d.proposta.logo_material_id)}. Muda se não for este.`
                  : 'Escolhemos o ficheiro que parece ser o logótipo. Muda se não for este.'}
              </p>
            )}
            {podeMexer && <CarregarLogo slug={slug} disabled={ocupado} rotulo="Carregar o logótipo para fundos claros" onCarregado={(id) => usarCarregado('logo_material_id', id)} />}

            {/* A versão clara, para os painéis de fundo escuro (28 Set). Opcional:
                as imagens usam a que se lê no fundo. */}
            {/* Sempre à vista desde 18 Out: com um só logótipo, era aqui que faltava a versão clara. */}
            {(d.logos.length > 0 || podeMexer) && (
              <div className="space-y-2 pt-2">
                <p className="ns-rotulo">Versão para fundos escuros <span className="font-normal text-muted-foreground">(opcional)</span></p>
                {/* Os claros primeiro: é esta a versão que aqui se procura. */}
                <div role="radiogroup" aria-label="Logótipo para fundos escuros" className="grid grid-cols-3 gap-2">
                  {[{ id: null, original_name: 'Não tenho' }]
                    .concat(logosVisiveis.filter((m) => m.id !== kit.logo_material_id).sort((a, b) => (b.tom === 'claro') - (a.tom === 'claro')))
                    .map((m) => mosaico(m, 'logo_negativo_material_id', 'Não tenho'))}
                </div>
                {/* Cada secção com o seu «Ver todas» (agentes, 20 Out: quem procurava aqui não o via). */}
                {verTodasBotao}
                {negativoEscuro && (
                  <p className="text-xs text-accent" role="status">Este logótipo é escuro: em fundos escuros não se vê. Escolhe a versão clara (branca).</p>
                )}
                {podeMexer && <CarregarLogo slug={slug} disabled={ocupado} rotulo="Carregar a versão para fundos escuros" onCarregado={(id) => usarCarregado('logo_negativo_material_id', id)} />}
                {/* Só antes de guardar: guardada, já não é uma escolha nossa (agentes, 20 Out). */}
                {d.proposta && d.proposta.logo_negativo_material_id && !d.kit && kit.logo_negativo_material_id === d.proposta.logo_negativo_material_id && (
                  <p className="text-xs text-muted-foreground">
                    {doManual(d.logos, d.proposta.logo_negativo_material_id)
                      ? `Tirámos esta versão ${doManual(d.logos, d.proposta.logo_negativo_material_id)}. Nos painéis de fundo escuro, entra esta.`
                      : 'Escolhemos a versão clara que encontrámos. Nos painéis de fundo escuro, entra esta.'}
                  </p>
                )}
              </div>
            )}
          </section>

          {avisos.map((a) => <p key={a} className="text-xs text-accent">{a}</p>)}
          {aviso && <p className="text-xs text-muted-foreground" role="status">{aviso}</p>}

          {d.kit && !alterado && (
            <p className="text-xs text-muted-foreground" role="status">
              {`Guardada${d.kit.confirmado_em ? ` a ${formatData(d.kit.confirmado_em)}` : ''}${d.kit.confirmado_por ? ` por ${d.kit.confirmado_por_nome || d.kit.confirmado_por}` : ''}. `}
              {podeMexer && 'Muda uma cor, a letra ou o logótipo para guardar outra vez.'}
            </p>
          )}

          {podeMexer && alterado && (
            <div className="flex flex-wrap gap-2">
              <button
                type="button"
                disabled={!valido || ocupado}
                onClick={confirmar}
                className="bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90 disabled:bg-secondary disabled:text-muted-foreground"
              >
                {d.kit ? 'Guardar as correcções' : 'Confirmar a identidade visual'}
              </button>
              {(d.kit || d.proposta) && (
                <button type="button" onClick={() => setKit(kitInicial(d))} className="border border-border px-4 py-2 text-sm hover:bg-secondary">
                  {d.kit ? 'Desfazer' : 'Voltar à proposta'}
                </button>
              )}
            </div>
          )}
        </div>

        <section className="min-w-0 space-y-3">
          <h3 className="text-sm font-medium text-foreground">Como fica nos painéis</h3>
          {(valido && vistoValido) || previaNeutra ? (
            <div className="space-y-3">
              {previaNeutra && <p role="status" className="text-sm text-muted-foreground">Referência visual neutra. Não é uma paleta oficial da marca.</p>}
              <ImagemComEspera src={previa('faixa')} alt="Exemplo numa faixa de 1008 por 144" proporcao={1008 / 144} className="w-full" />
              <div className="flex items-start gap-3">
                <ImagemComEspera src={previa('retrato')} alt="Exemplo num painel vertical de 256 por 1024" proporcao={256 / 1024} className="h-72 flex-none" />
                <ImagemComEspera src={previa('bloco')} alt="Exemplo num ecrã de 2560 por 800" proporcao={2560 / 800} className="min-w-0 flex-1 self-start" />
              </div>
              <p className="text-xs text-muted-foreground">Um exemplo com texto inventado. Nas campanhas, o texto é o que a app escreve para cada uma.</p>
            </div>
          ) : (
            <p className="text-xs text-muted-foreground">Cada cor tem de ser #rrggbb para se ver o exemplo.</p>
          )}
        </section>
      </div>
    </div>
  );
}

function ScreenMarca({ slug, separador, podeMexer, onNavegar, criarActivo = false, marcaDeCliente = null }) {
  const [resumo, setResumo] = React.useState(null);
  // Sem separador no endereço: a marca, se já há ficheiros; senão, carregá-los.
  // Decide-se uma vez, quando o resumo chega: a meio de um carregamento o
  // ecrã não salta de Ficheiros para o resumo (revisor, 28 Set).
  const [entrada, setEntrada] = React.useState(null);
  React.useEffect(() => { setEntrada(null); }, [slug]);
  React.useEffect(() => {
    if (resumo && !entrada) setEntrada(resumo.fontes && resumo.fontes.length ? 'resumo' : 'fontes');
  }, [resumo, entrada]);
  const comSeparador = SEPARADORES_MARCA.some((s) => s.chave === separador);
  const activo = comSeparador ? separador : entrada || 'fontes';
  // Até se saber a entrada, não se monta nenhum separador.
  const aDecidir = !comSeparador && !entrada;
  const [erro, setErro] = React.useState(null);

  const carregar = React.useCallback(
    // Cada leitura do resumo faz o separador aberto ler o seu (ronda 6: o topo dizia
    // "2 à espera que confirmes" e a lista mostrava 0 até recarregar a página).
    () => api.get(`/api/${slug}/marca`).then((r) => { setResumo(r); setErro(null); setVolta((v) => v + 1); })
      .catch((e) => setErro(e.message || 'Não foi possível ler o estado da marca.')),
    [slug]
  );
  React.useEffect(() => { setResumo(null); carregar(); }, [carregar]);

  // Enquanto o sistema tiver trabalho, volta a perguntar — como o Conhecimento fazia.
  const aTrabalhar = Boolean(resumo && resumo.vez.sistema.length);
  React.useEffect(() => {
    if (!aTrabalhar) return undefined;
    const t = setInterval(carregar, 8000);
    return () => clearInterval(t);
  }, [aTrabalhar, carregar]);

  // E quando a vez é de uma pessoa, também (sessão de 24 Set): um facto revisto
  // na NetScreen, ou o dossiê escrito, só chegavam aqui ao recarregar. Cada
  // volta faz o separador aberto (factos, dossiê) ler outra vez o seu.
  const [volta, setVolta] = React.useState(0);

  // A proposta da marca: como aparece e como fala, num pedido (24 Set).
  const [aPropor, setAPropor] = React.useState(false);
  const [avisoProposta, setAvisoProposta] = React.useState(null);
  const [versaoProposta, setVersaoProposta] = React.useState(0);
  const propor = async () => {
    setAPropor(true);
    setAvisoProposta(null);
    try {
      await api.post(`/api/${slug}/marca/voz/propor`, {});
      setVersaoProposta((v) => v + 1);
      setAvisoProposta('Proposta feita a partir do que lemos. Vê se é a tua marca, corrige o que não for, e guarda.');
      carregar();
    } catch (e) {
      setAvisoProposta(e.message);
    } finally {
      setAPropor(false);
    }
  };
  // Guardado em Identidade, a frase «Proposta feita… corrige… e guarda» deixa de valer (agentes, 20 Out:
  // ficava até recarregar a página).
  const guardouNaIdentidade = React.useCallback(() => { setAvisoProposta(null); carregar(); }, [carregar]);
  const lerOutraVez = React.useCallback(async () => {
    const r = await api.get(`/api/${slug}/marca`);
    setResumo(r);
    setErro(null);
    setVolta((v) => v + 1);
  }, [slug]);
  const actualizacaoFalhou = useActualizar(lerOutraVez, { activo: !aTrabalhar });
  // Há material lido, e nada ainda a ler: a proposta automática pode ser pedida (ensaio de 9 Out).
  const etapaLido = (f) => ((f.etapas || []).find((e) => e.chave === 'lido') || {}).estado;
  const fontesLidas = Boolean(resumo && resumo.fontes.some((f) => etapaLido(f) === 'feito')
    && !resumo.fontes.some((f) => etapaLido(f) === 'em_curso'));

  // O mesmo critério da linha "Para a marca ficar completa", no MarcaVez.
  const porConfirmar = Boolean(resumo && resumo.onboarding.some((o) => !o.daNetscreen && !o.feito));
  // O separador da marca inteira ganha um ponto laranja enquanto falta confirmar.
  // Confirmada, a pergunta já não se faz: é o resumo da marca.
  const separadores = SEPARADORES_MARCA.map((s) => (s.chave !== 'resumo' ? s : porConfirmar
    ? { ...s, rotulo: <span className="inline-flex items-center gap-1.5">{s.rotulo}<span className="h-1.5 w-1.5 rounded-full bg-primary" aria-hidden="true" /></span> }
    : resumo ? { ...s, rotulo: 'Resumo' } : s));

  const etapasPorFonte = React.useMemo(
    () => (resumo ? Object.fromEntries(resumo.fontes.map((f) => [f.id, f.etapas])) : null),
    [resumo]
  );

  return (
    <div className="space-y-6">
      {/* Enquanto houver alguma coisa por confirmar, o principal da página é ir
          ver a marca inteira e confirmá-la (28 Set). No próprio separador sai. */}
      <header className="flex flex-wrap items-end justify-between gap-4">
        <div className="min-w-0">
          <h1 className="mt-1 font-display text-4xl uppercase leading-none tracking-wide">{marcaDeCliente ? `A marca de ${marcaDeCliente}` : 'A minha marca'}</h1>
          <p className="mt-3 max-w-prose text-sm leading-relaxed text-muted-foreground">
            Os teus ficheiros, como a marca aparece e como fala.
          </p>
        </div>
        {porConfirmar && activo !== 'resumo' && (
          <button
            type="button"
            onClick={() => onNavegar(`/${slug}/marca/resumo`)}
            className="shrink-0 bg-primary px-4 py-2 text-sm text-primary-foreground"
          >
            {podeMexer ? 'Ver a marca e confirmar' : 'Ver a marca'}
          </button>
        )}
      </header>

      {erro && <p className="text-sm text-accent">{erro}</p>}
      {actualizacaoFalhou && !erro && (
        <p className="text-xs text-accent" role="status">
          Não deu para actualizar agora: o que vês pode não ser o mais recente. Voltamos a tentar sozinhos.
        </p>
      )}
      {resumo ? (
        <MarcaVez resumo={resumo} onIr={(c) => onNavegar(`/${slug}/marca/${c}`)} />
      ) : !erro && <div className="loading-skeleton h-12" />}

      <Separadores
        rotulo="A minha marca"
        separadores={separadores}
        activo={activo}
        aoMudar={(c) => onNavegar(`/${slug}/marca/${c}`)}
      />

      {aDecidir && <div className="loading-skeleton h-40" />}
      {!aDecidir && (
      <div id={`painel-${activo}`} role="tabpanel" aria-labelledby={`separador-${activo}`}>
        {activo === 'fontes' && (
          <MarcaFicheiros slug={slug} podeMexer={podeMexer} etapasPorFonte={etapasPorFonte} onMudou={carregar} />
        )}
        {/* A identidade é como a marca aparece e como fala: uma proposta só,
            e não duas etapas (parecer do Codex, 24 Set). */}
        {activo === 'identidade' && (
          <div className="space-y-10">
            {/* Uma proposta só, para as duas metades: um pedido ao modelo com o que
                já lemos, o manual primeiro. Nada muda até se guardar. */}
            {podeMexer && resumo && resumo.onboarding.some((o) => ['kit', 'voz'].includes(o.chave) && !o.feito) && (
              <div className="flex flex-wrap items-center gap-3">
                <button
                  type="button"
                  disabled={aPropor}
                  onClick={propor}
                  className="ns-btn ns-btn-sm ns-btn-suave disabled:opacity-50"
                >
                  {aPropor ? 'A ler os teus ficheiros…' : 'Propor a partir dos teus ficheiros'}
                </button>
                {aPropor
                  ? <AProcessar texto="A ler a tua marca" detalhe="As cores e a voz em baixo mudam quando a proposta chegar. Leva uns 30 segundos." />
                  : (
                    <span className="text-xs text-muted-foreground">
                      {avisoProposta || 'Lemos os ficheiros que carregaste e propomos as cores, quem a marca é e como fala. Só muda o que guardares.'}
                    </span>
                  )}
              </div>
            )}
            <section key={`aparece-${versaoProposta}`}>
              <h2 className="mb-4 text-lg font-semibold tracking-tight">Como aparece</h2>
              <MarcaIdentidade slug={slug} podeMexer={podeMexer} onMudou={guardouNaIdentidade} onNavegar={onNavegar}
                semFicheiros={Boolean(resumo && !resumo.fontes.length)} />
            </section>
            <section key={`fala-${versaoProposta}`} className="border-t border-border pt-8">
              <h2 className="mb-4 text-lg font-semibold tracking-tight">Quem é e como fala</h2>
              <MarcaVoz slug={slug} podeMexer={podeMexer} onMudou={guardouNaIdentidade} />
            </section>
          </div>
        )}
        {activo === 'factos' && <MarcaFactos slug={slug} podeMexer={podeMexer} onMudou={carregar} volta={volta} />}
        {activo === 'resumo' && (
          <MarcaResumo slug={slug} podeMexer={podeMexer} onMudou={carregar} onNavegar={onNavegar} onPropor={propor} aPropor={aPropor} avisoProposta={avisoProposta} lidos={fontesLidas} criarActivo={criarActivo} />
        )}
      </div>
      )}
    </div>
  );
}
