// Tráfego interno → Visão geral (spec 2026-09-30-trafego-interno-design §7 + redesign §12).
// Custo do RedTrack × receita do banco (régua bruta do dash restrita a pedidos_internos).
// Molde ui-fenix completo: LoadingScreen no 1º load, FullLoadingOverlay nas trocas de filtro,
// offline honesto com useAutoRetry, SEM mock (dado operacional de mídia).
// Prefixo top-level: Tf/tf/TF_. As peças compartilhadas (TfCabecalho, TfFiltros, TfCards,
// TfFaixa, TfSerie, TfDonut, TfCascata, TfFunil, TfTabela, tfSegs) são exportadas em window e
// reusadas pelo TrafegoFonte.jsx.

// Cores por canal: paleta de PLATFORM (data.jsx) validada no validador da skill dataviz
// (30/09, --pairs all: todas as checagens passam; laranja e verde < 3:1 contra o fundo, então
// cor nunca vai sozinha: a legenda do donut e as tabelas levam rótulo e valor). Cor segue o
// CANAL, nunca o ranking. Canal fora do mapa cai no "Outros" neutro.
const TF_CANAL_COR = {
  facebook: '#2a78d6', taboola: '#FD7119', newsbreak: '#4a3aa7', google: '#1baf7a', tiktok: '#c2185b',
};
const TF_OUTROS_COR = '#C7CDD9';
// Série: receita (laranja da marca), spend (azul Fênix), meta (cinza tracejado). Hex dos
// tokens (--fenix-orange, --fenix-blue, --gray-500): vão em atributo SVG.
const TF_CENARIO = { minima: 'Mínima', razoavel: 'Razoável', disney: 'Disney', misto: 'Misto' };
const TF_SERIE_COR = { receita: '#FD7119', spend: '#303880', meta: '#6B7488' };
// Réguas de saúde (§12.3): shape {color, bg} dos tokens semânticos, nada de cor nova.
const TF_SAUDE = {
  pos:  { color: 'var(--positive)', bg: 'var(--positive-bg)' },
  warn: { color: 'var(--warning)',  bg: 'var(--warning-bg)' },
  neg:  { color: 'var(--negative)', bg: 'var(--negative-bg)' },
  neu:  { color: 'var(--fg-3)',     bg: 'var(--bg-sunken)' },   // zero / sem movimento
};
// Movimento reduzido: sem translate no hover dos cards.
const tfReduz = () => { try { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; } catch (_e) { return false; } };

// Dinheiro com sinal na frente do símbolo (−$27,327 em vez de $-27,327).
const tfUsd = (n) => (n < 0 ? '−' + USD(-n) : USD(n));
const tfN = (fmt) => (v) => (v == null || Number.isNaN(v) ? 'n/d' : fmt(v));
// Dinheiro com centavos (CPC): mesma moeda/câmbio dos formatadores USD/USDk.
const tfCents = (n) => (FX.code === 'BRL' ? 'R$ ' : '$')
  + (n * FX.rate).toLocaleString(FX.code === 'BRL' ? 'pt-BR' : 'en-US',
    { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const tfRoas = (n) => n.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + 'x';
const tfBrl = (n) => 'R$ ' + Math.round(n).toLocaleString('pt-BR');
const tfHora = (iso) => new Date(iso).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
const tfDia = (iso) => (iso
  ? new Date(iso.slice(0, 10) + 'T12:00:00').toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' })
  : '');
// ROAS: verde ≥ meta do mês, âmbar entre 1,0 e a meta, vermelho < 1,0; sem meta: verde ≥ 1,0.
const tfRoasSaude = (roas, alvo) => (roas == null ? null
  : alvo != null ? (roas >= alvo ? TF_SAUDE.pos : roas >= 1 ? TF_SAUDE.warn : TF_SAUDE.neg)
  : (roas >= 1 ? TF_SAUDE.pos : TF_SAUDE.neg));
// Profit / lucro real: verde > 0, vermelho < 0, cinza no zero (zero não é lucro).
const tfSinalSaude = (v) => (v == null ? null : v > 0 ? TF_SAUDE.pos : v < 0 ? TF_SAUDE.neg : TF_SAUDE.neu);
// Δ% vs período anterior; sem base (anterior null/0) → null.
const tfDelta = (atual, ant) => (atual == null || ant == null || ant === 0 ? null
  : (atual - ant) / Math.abs(ant) * 100);

// Colunas das tabelas (mesmo shape de linha do backend: _linha em trafego_interno.py).
// tipo: 'pill' = pílula verde/vermelha pelo sinal; 'sinal' = texto colorido pelo sinal;
// 'roas' = régua do ROAS. extra: coluna secundária, só aparece com "Mais colunas".
const TF_COLS = [
  { key: 'spend',          label: 'Spend',           fmt: tfN(USD) },
  { key: 'receita',        label: 'Receita bruta',   fmt: tfN(USD), forte: true },
  { key: 'roas',           label: 'ROAS',            fmt: tfN(tfRoas), tipo: 'roas' },
  { key: 'profit',         label: 'Profit',          fmt: tfN(tfUsd), tipo: 'pill' },
  { key: 'receitaLiquida', label: 'Receita líquida', fmt: tfN(USD), extra: true },
  { key: 'lucroReal',      label: 'Lucro real',      fmt: tfN(tfUsd), tipo: 'sinal' },
  { key: 'pedidosFe',      label: 'Pedidos FE',      fmt: tfN(NUM) },
  { key: 'pedidosBe',      label: 'Upsells',         fmt: tfN(NUM), extra: true },
  { key: 'cpa',            label: 'CPA',             fmt: tfN(USD) },
  { key: 'cpc',            label: 'CPC',             fmt: tfN(tfCents), extra: true },
  { key: 'refundPct',      label: 'Refund %',        fmt: tfN(PCT), extra: true },
];
// Linha sem movimento: nada gasto, nada vendido (fica oculta por padrão nas tabelas).
const tfSemMov = (r) => !r.spend && !r.receita && !r.pedidosFe;

// ---- Popover genérico (clique/hover), fecha com clique fora ou Esc ----
const TfPopover = ({ gatilho, children, largura = 340, hover = false, alinhar = 'left' }) => {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const fora = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const esc = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', fora);
    document.addEventListener('keydown', esc);
    return () => { document.removeEventListener('mousedown', fora); document.removeEventListener('keydown', esc); };
  }, [open]);
  return (
    <span ref={ref} style={{ position: 'relative', display: 'inline-flex' }}
      onMouseEnter={hover ? () => setOpen(true) : undefined}
      onMouseLeave={hover ? () => setOpen(false) : undefined}>
      {gatilho({ open, toggle: () => setOpen(o => !o), setOpen })}
      {open && (
        <div role="dialog" style={{ ...tf.pop, width: largura, maxWidth: 'calc(100vw - 32px)',
          ...(alinhar === 'right' ? { right: 0 } : { left: 0 }) }}>
          {children}
        </div>
      )}
    </span>
  );
};

// ---- Ícone de info ao lado do filtro Reembolso (substitui a nota cinza) ----
const TfInfoReembolso = ({ refundBy }) => (
  <TfPopover hover largura={320} gatilho={({ toggle }) => (
    <button type="button" aria-label="Como o reembolso é contado" onClick={toggle} style={tf.infoBtn}>i</button>
  )}>
    {refundBy === 'cancelamento' ? (
      <>
        <div style={tf.popTitle}>Reembolso pela data cancelada</div>
        <div style={tf.popTxt}>Conta o reembolso no dia do cancelamento; o pedido pode ter sido feito dias
          ou semanas antes. Só a Buygoods registra a data de cancelamento. Taxa = devolvidos processados na
          janela ÷ vendas da janela.</div>
      </>
    ) : (
      <>
        <div style={tf.popTitle}>Reembolso pela data do pedido</div>
        <div style={tf.popTxt}>Conta o reembolso no dia da venda (coorte). Vendas recentes ainda podem
          ser reembolsadas, então a taxa dos últimos dias tende a subir.</div>
      </>
    )}
  </TfPopover>
);

// ---- Filtros (período, plataforma, régua de reembolso + extras da página) ----
const TfFiltros = ({ period, setPeriod, customRange, setCustomRange, platform, setPlatform,
                     refundBy, setRefundBy, children }) => (
  <div style={tf.filterRow}>
    <PeriodSelector period={period} setPeriod={setPeriod}
      customRange={customRange} setCustomRange={setCustomRange} />
    <PlatformDropdown value={platform} onChange={setPlatform} />
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
      <Dropdown icon="layers" label="Reembolso" value={refundBy} onChange={setRefundBy}
        options={REFUND_BY_OPTIONS} />
      <TfInfoReembolso refundBy={refundBy} />
    </span>
    {children}
  </div>
);

// ---- Avisos: lista de textos (popover do chip) ----
const tfAvisosLista = (data, err) => {
  const av = (data && data.avisos) || {};
  const sync = data && data.sync;
  const itens = [];
  // Falha de refetch não entra aqui: vira faixa âmbar visível (TfFalhaAviso) no cabeçalho.
  if (sync && (sync.atrasado || !sync.ultimo)) {
    itens.push(sync.ultimo
      ? `O último sync do RedTrack foi há ${sync.minutos >= 120 ? Math.floor(sync.minutos / 60) + ' h' : Math.round(sync.minutos) + ' min'}. Custo e cliques podem estar defasados.`
      : 'Não há sync do RedTrack registrado. Custo e cliques podem estar defasados.');
  }
  const sc = av.semCambio || {};
  if (sc.linhas > 0) itens.push(`${NUM(sc.linhas)} linhas de custo sem câmbio do dia (${tfBrl(sc.valor)}) ficaram fora do Spend. O Spend está incompleto.`);
  const csr = av.custoSemReceita || {};
  if (csr.valor > 0) {
    const top = (csr.ofertas || []).slice().sort((a, b) => (b.valor || 0) - (a.valor || 0)).slice(0, 3)
      .map(o => `${o.offer || '(sem oferta)'}${o.canal ? ' (' + tfCanalLabel(o.canal) + ')' : ''}: ${USDk(o.valor || 0)}`);
    itens.push({ txt: `${USD(csr.valor)} de custo${csr.linhas ? ` (${NUM(csr.linhas)} linhas)` : ''} sem receita rastreável. Fica fora de ROAS, CPA e Profit e entra só no Spend total.`, sub: top });
  }
  if (av.tinhaAfiliado > 0) itens.push(`${NUM(av.tinhaAfiliado)} ${av.tinhaAfiliado === 1 ? 'pedido confirmado' : 'pedidos confirmados'} pelo RedTrack ${av.tinhaAfiliado === 1 ? 'tinha' : 'tinham'} afiliado. O RedTrack vence e ${av.tinhaAfiliado === 1 ? 'ele conta' : 'eles contam'} como interno.`);
  const brl = av.moedaSemConversao || {};
  if (brl.linhas > 0) itens.push(`${NUM(brl.linhas)} linhas de custo vieram de contas em BRL; o RedTrack já converte para dólar, confirmar no gerenciador.`);
  return itens.map(i => (typeof i === 'string' ? { txt: i } : i));
};

// ---- Carimbo do RedTrack + chip de avisos (vermelho quando o sync está atrasado) ----
const TfStatus = ({ data, err }) => {
  const sync = data && data.sync;
  const atrasado = !!sync && (sync.atrasado || !sync.ultimo);
  const itens = tfAvisosLista(data, err);
  const sau = atrasado || err ? TF_SAUDE.neg : TF_SAUDE.warn;
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
      {sync && (
        <span style={tf.stamp} className="fenix-num"
          title={sync.ultimo ? 'Último sync bem-sucedido do RedTrack' : undefined}>
          {sync.ultimo ? `RedTrack ${tfHora(sync.ultimo)}` : 'RedTrack sem sync'}
        </span>
      )}
      {itens.length > 0 && (
        <TfPopover largura={380} gatilho={({ toggle, open }) => (
          <button type="button" onClick={toggle} aria-expanded={open}
            style={{ ...tf.chip, color: sau.color, background: sau.bg, borderColor: open ? sau.color : 'transparent' }}>
            <Icon name="alert" size={13} color="currentColor" />
            {itens.length} {itens.length === 1 ? 'aviso' : 'avisos'}
          </button>
        )}>
          <div style={tf.popTitle}>Avisos do período</div>
          <ul style={tf.popList}>
            {itens.map((it, i) => (
              <li key={i} style={tf.popItem}>
                {it.txt}
                {it.sub && it.sub.length > 0 && (
                  <ul style={tf.popSub}>{it.sub.map((s, j) => <li key={j}>{s}</li>)}</ul>
                )}
              </li>
            ))}
          </ul>
        </TfPopover>
      )}
    </span>
  );
};

// ---- Cabeçalho: título + status na 1ª linha, filtros numa linha própria abaixo (molde
// ui-fenix); as duas páginas do grupo usam o mesmo arranjo, sem quebra acidental ----
// escopo: filtros do recorte da página (Fonte/Conta/Comprador) na linha do título, à direita;
// a linha de baixo fica só com os filtros globais (Período, Plataforma, Reembolso).
const TfCabecalho = ({ titulo, data, err, escopo, children }) => (
  <div style={tf.header}>
    <div style={tf.headTop}>
      <div style={tf.titleWrap}>
        <h2 style={tf.title}>{titulo}</h2>
        <TfStatus data={data} err={err} />
      </div>
      {escopo && <div style={tf.filterRow}>{escopo}</div>}
    </div>
    {children}
    {err && data && <TfFalhaAviso data={data} />}
  </div>
);

// ---- Falha de refetch com dado velho (ui-fenix §1, offline honesto): faixa âmbar VISÍVEL.
// Sem ela o seletor já mostra o período novo e os números seguem os da janela antiga, e
// parece que o filtro "não funciona". O useAutoRetry da página tenta de novo em silêncio.
const TfFalhaAviso = ({ data }) => (
  <div role="status" style={tf.notice}>
    <Icon name="alert" size={14} color="currentColor" />
    <span>
      Falha ao atualizar. Os números abaixo ainda são
      {data.diaIni ? ` de ${tfDia(data.diaIni)} a ${tfDia(data.diaFim)}` : ' da última janela carregada'}
      {'; a tela tenta de novo sozinha a cada 15 s.'}
    </span>
  </div>
);

// ---- Δ vs anterior: seta + %; modo 'neutro' (cinza), 'maior' (subir é bom), 'menor' ----
const TfDelta = ({ d, modo = 'maior', pequeno = false }) => {
  if (d == null && pequeno) return null;
  if (d == null) return <span style={tf.deltaNulo} title="Sem dado no período anterior de mesmo tamanho">sem comparativo</span>;
  const sobe = d >= 0;
  const bom = modo === 'maior' ? sobe : !sobe;
  const cor = modo === 'neutro' ? 'var(--fg-3)' : bom ? 'var(--positive)' : 'var(--negative)';
  return (
    <span style={{ ...tf.delta, ...(pequeno ? tf.deltaSm : {}), color: cor }} className="fenix-num">
      <Icon name={sobe ? 'arrowUp' : 'arrowDown'} size={pequeno ? 11 : 13} color="currentColor" sw={2.4} />
      {PCT(Math.abs(d))}
    </span>
  );
};

// ---- Sparkline (SVG, escala própria; linha de zero quando a série cruza o zero) ----
const TfSpark = ({ values = [], color, id }) => {
  const vs = values.map(v => (v == null || !isFinite(v) ? null : v));
  const ok = vs.filter(v => v != null);
  if (ok.length < 2) return <div style={{ height: 38 }} />;
  const W = 120, H = 38, pad = 3;
  let lo = Math.min(...ok), hi = Math.max(...ok);
  if (hi === lo) { hi += 1; lo -= 1; }
  const x = (i) => (vs.length <= 1 ? 0 : (W * i) / (vs.length - 1));
  const y = (v) => pad + (H - 2 * pad) * (1 - (v - lo) / (hi - lo));
  // Dia sem valor (ex.: ROAS sem spend) é pulado e a linha liga os vizinhos: sparkline mostra
  // tendência, e quebrar a linha deixava traços soltos no começo da série.
  let d = '', first = null, last = null;
  vs.forEach((v, i) => {
    if (v == null) return;
    d += (first == null ? 'M' : 'L') + x(i).toFixed(1) + ' ' + y(v).toFixed(1) + ' ';
    if (first == null) first = i; last = i;
  });
  const area = d + `L ${x(last).toFixed(1)} ${H} L ${x(first).toFixed(1)} ${H} Z`;
  return (
    <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" width="100%" height={H} style={{ display: 'block', overflow: 'visible' }} aria-hidden="true">
      <defs>
        <linearGradient id={'tfsp-' + id} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor={color} stopOpacity="0.20" />
          <stop offset="100%" stopColor={color} stopOpacity="0" />
        </linearGradient>
      </defs>
      {lo < 0 && hi > 0 && (
        <line x1="0" x2={W} y1={y(0)} y2={y(0)} stroke="#C7CDD9" strokeWidth="1" strokeDasharray="3 3" vectorEffect="non-scaling-stroke" />
      )}
      <path d={area} fill={`url(#tfsp-${id})`} stroke="none" />
      <path d={d} fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" vectorEffect="non-scaling-stroke" />
    </svg>
  );
};

// ---- Card grande de KPI (§12.1): ícone, valor, Δ, sparkline; hover sobe 2px, sombra tingida,
// borda na cor do card e troca o rodapé pela fórmula + valor do período anterior ----
const TfCard = ({ id, icon, cor, hex, spark, tint, rotulo, valor, valorCor, d, modo, sub, formula, anterior }) => {
  const [hov, setHov] = React.useState(false);
  return (
    <div style={{ ...tf.bigCard, ...(hov ? {
        transform: tfReduz() ? 'none' : 'translateY(-2px)', borderColor: cor,
        boxShadow: `0 12px 28px -12px color-mix(in srgb, ${cor} 55%, transparent), var(--shadow-sm)` } : {}) }}
      onMouseEnter={() => setHov(true)} onMouseLeave={() => setHov(false)}>
      <div style={tf.bigTop}>
        <span style={{ ...tf.bigIc, background: tint }}><Icon name={icon} size={17} color={cor} /></span>
        <span style={tf.bigLbl}>{rotulo}</span>
      </div>
      <div style={{ ...tf.bigVal, ...(valorCor ? { color: valorCor } : {}) }} className="fenix-num">{valor}</div>
      <div style={tf.bigMid}>
        <TfDelta d={d} modo={modo} />
        <div style={{ flex: 1, maxWidth: 140, minWidth: 60 }}><TfSpark values={spark} color={hex} id={id} /></div>
      </div>
      <div style={tf.bigFoot} className="fenix-num">
        {hov ? <>{formula}{anterior != null ? <span style={{ color: 'var(--fg-2)' }}> · anterior {anterior}</span> : ' · sem período anterior'}</> : sub}
      </div>
    </div>
  );
};

// ---- Os 4 cards grandes: Spend, Receita bruta, Profit, ROAS ----
const TfCards = ({ k, ant, serie = [], meta, janelaAnterior }) => {
  const a = ant || {};
  const alvo = meta ? meta.roasAlvo : null;
  const roasS = tfRoasSaude(k.roas, alvo);
  const profS = tfSinalSaude(k.profit);
  // Sparklines começam no 1º dia com movimento (fonte que só rodou no fim da janela não
  // desenha dias vazios nem um traço solto no ROAS).
  const ini = serie.findIndex(p => (p.spend || 0) > 0 || (p.receita || 0) > 0);
  const sv = ini > 0 ? serie.slice(ini) : serie;
  const sp = sv.map(p => p.spend || 0);
  const rc = sv.map(p => p.receita || 0);
  const pf = sv.map(p => (p.profit != null ? p.profit : (p.receita || 0) - (p.spend || 0)));
  const ro = sv.map(p => (p.spend > 0 ? (p.receita || 0) / p.spend : null));
  const janAnt = janelaAnterior && janelaAnterior.diaIni ? ` (${tfDia(janelaAnterior.diaIni)} a ${tfDia(janelaAnterior.diaFim)})` : '';
  const hexSaude = (s) => (s === TF_SAUDE.neg ? '#D8453B' : s === TF_SAUDE.warn ? '#E0922B' : s === TF_SAUDE.pos ? '#1F9D6B' : '#6B7488');
  const alvoRec = meta && meta.alvoMes ? meta.alvoMes.revenue : null;
  return (
    <div style={tf.bigRow}>
      <TfCard id="spend" icon="dollar" cor="var(--fenix-blue)" hex={TF_SERIE_COR.spend} tint="var(--fenix-blue-100)" spark={sp}
        rotulo="Spend" valor={USD(k.spend || 0)} d={tfDelta(k.spend, a.spend)} modo="neutro"
        sub={k.spendIncompleto ? 'incompleto: há custo sem câmbio' : 'custo do RedTrack com receita rastreável'}
        formula="Custo do RedTrack" anterior={a.spend != null ? USD(a.spend) + janAnt : null} />
      <TfCard id="receita" icon="trending" cor="var(--fenix-orange)" hex={TF_SERIE_COR.receita} tint="var(--fenix-orange-100)" spark={rc}
        rotulo="Receita bruta" valor={USD(k.receitaBruta || 0)} d={tfDelta(k.receitaBruta, a.receitaBruta)}
        sub={alvoRec != null ? `meta do mês ${USDk(alvoRec)}` : 'Completed + devolvidos'}
        formula="Completed + devolvidos" anterior={a.receitaBruta != null ? USD(a.receitaBruta) + janAnt : null} />
      <TfCard id="profit" icon="chart" cor={profS ? profS.color : 'var(--fg-3)'} tint={profS ? profS.bg : 'var(--bg-sunken)'}
        spark={pf} hex={hexSaude(profS)}
        rotulo="Profit" valor={tfUsd(k.profit || 0)} valorCor={profS && profS.color}
        d={tfDelta(k.profit, a.profit)} sub={k.profit > 0 ? 'receita acima do spend' : k.profit < 0 ? 'spend acima da receita' : 'receita igual ao spend'}
        formula="Receita bruta − Spend" anterior={a.profit != null ? tfUsd(a.profit) + janAnt : null} />
      <TfCard id="roas" icon="bolt" cor={roasS ? roasS.color : 'var(--fg-3)'} tint={roasS ? roasS.bg : 'var(--bg-sunken)'}
        spark={ro} hex={roasS ? hexSaude(roasS) : '#6B7488'}
        rotulo="ROAS" valor={tfN(tfRoas)(k.roas)} valorCor={roasS && roasS.color}
        d={tfDelta(k.roas, a.roas)}
        sub={alvo != null ? `meta ${tfRoas(alvo)} (cenário ${TF_CENARIO[meta.cenario] || meta.cenario})` : 'sem meta no período, referência 1,00x'}
        formula="Receita bruta ÷ Spend" anterior={a.roas != null ? tfRoas(a.roas) + janAnt : null} />
    </div>
  );
};

// ---- Faixa compacta com 6 indicadores e Δ pequeno ----
const TfFaixa = ({ k, ant }) => {
  const a = ant || {};
  const rh = k.refundPct != null ? refundHealth(k.refundPct) : null;
  const itens = [
    { lbl: 'CPA', v: tfN(USD)(k.cpa), d: tfDelta(k.cpa, a.cpa), modo: 'menor', t: `Spend ÷ pedidos FE (${NUM(k.pedidosFe || 0)})` },
    { lbl: 'CPC', v: tfN(tfCents)(k.cpc), d: tfDelta(k.cpc, a.cpc), modo: 'menor', t: `Spend ÷ cliques (${NUMk(k.cliques || 0)})` },
    { lbl: 'AOV', v: tfN(USD)(k.aov), d: tfDelta(k.aov, a.aov), modo: 'maior', t: 'Régua única do dash (Completed + devolvidos + frete)' },
    { lbl: 'Refund %', v: tfN(PCT)(k.refundPct), cor: rh && rh.color, d: tfDelta(k.refundPct, a.refundPct), modo: 'menor', t: rh ? `Régua do setor: ${rh.label}` : undefined },
    { lbl: 'Take rate', v: k.takeRate != null ? PCT(k.takeRate * 100) : 'n/d', d: tfDelta(k.takeRate, a.takeRate), modo: 'maior', t: `Upsells ÷ pedidos FE (${NUM(k.pedidosBe || 0)} ÷ ${NUM(k.pedidosFe || 0)})` },
    { lbl: 'Interno ÷ resto', v: tfN(PCT)(k.pctFaturamento), d: tfDelta(k.pctFaturamento, a.pctFaturamento), modo: 'maior',
      t: k.faturamentoResto != null ? `Receita interna ÷ resto do faturamento do dash (${USDk(k.faturamentoResto)})` : 'Receita interna ÷ resto do faturamento do dash' },
  ];
  return (
    <div style={tf.stripRow}>
      {itens.map(it => (
        <div key={it.lbl} style={tf.stripCell} title={it.t}>
          <div style={tf.stripLbl}>{it.lbl}</div>
          <div style={tf.stripLine}>
            <span style={{ ...tf.stripVal, ...(it.cor ? { color: it.cor } : {}) }} className="fenix-num">{it.v}</span>
            <TfDelta d={it.d} modo={it.modo} pequeno />
          </div>
        </div>
      ))}
    </div>
  );
};

// ---- Série diária: receita × spend (mesma escala $) + meta tracejada ----
// semMeta: página filtrada (fonte/conta/comprador); a meta diária é do grupo inteiro.
const TfSerie = ({ data, chKey, semMeta }) => {
  const serie = data.serie || [];
  const pts = serie.map(p => ({ bucket: p.dia + 'T12:00:00', value: p.receita || 0 }));
  const meta = semMeta ? null : data.meta;
  const temMeta = !!meta && serie.some(p => p.metaReceita != null);
  const extra = [{ label: 'Spend', color: TF_SERIE_COR.spend, values: serie.map(p => p.spend || 0) }];
  if (temMeta) extra.push({ label: 'Meta de receita', color: TF_SERIE_COR.meta, dashed: true, values: serie.map(p => p.metaReceita) });
  const jan = data.diaIni ? `${tfDia(data.diaIni)} a ${tfDia(data.diaFim)}, dia de Nova York` : 'Dia de Nova York';
  const sub = temMeta && meta.pctMes != null
    ? `${jan} · meta ${TF_CENARIO[meta.cenario] || meta.cenario}: ${PCT(meta.pctMes)} do mês`
      + (meta.pctRevenue != null ? `, ritmo ${PCT(meta.pctRevenue)} do esperado` : '')
    : jan;
  return (
    <ChannelChart ch={{ key: chKey, available: pts.length > 0, series: pts }}
      meta={{ label: 'Receita bruta', color: TF_SERIE_COR.receita }} extra={extra} showTotal={false}
      subtitle={sub} granularity="day" height={330} />
  );
};

// ---- Segmentos de donut: top N + "Outros" (cor por canal ou pela paleta do donut) ----
const tfSegs = (rows = [], campo, corDe, topN = 6) => {
  const l = rows.filter(r => (r[campo] || 0) > 0).sort((a, b) => b[campo] - a[campo]);
  const segs = [];
  let outros = 0;
  l.forEach((r, i) => {
    const cor = corDe(r, segs.length);
    if (i < topN && cor) segs.push({ label: r.label, value: r[campo], color: cor });
    else outros += r[campo];
  });
  if (outros > 0) segs.push({ label: 'Outros', value: outros, color: TF_OUTROS_COR });
  return segs;
};

// ---- Donut com Segmented Receita|Spend (padrão Receita), legenda com valor e % ----
const TfDonut = ({ rows = [], dimensao, corDe }) => {
  const [modo, setModo] = React.useState('receita');
  const campo = modo === 'receita' ? 'receita' : 'spend';
  return (
    <CleanDonut title={`${modo === 'receita' ? 'Receita' : 'Spend'} por ${dimensao}`}
      segments={tfSegs(rows, campo, corDe)} fmt={USDk} legendFmt={USDk}
      headRight={<Segmented value={modo} onChange={setModo}
        options={[{ key: 'receita', label: 'Receita' }, { key: 'spend', label: 'Spend' }]} />} />
  );
};
const tfCorCanal = (r) => TF_CANAL_COR[r.canal || r.key] || null;
const tfCorPaleta = (r, i) => DONUT_PALETTE[i % (DONUT_PALETTE.length - 1)];

// ---- Cascata "Da receita ao lucro real" (§12.1): barras flutuantes ----
const TfCascata = ({ etapas = [] }) => {
  const [ref, w] = useWidth();
  const [hi, setHi] = React.useState(null);
  if (!etapas.length) return (
    <div style={tf.panel}><div style={tf.panelTitle}>Da receita ao lucro real</div><div style={tf.empty}>Sem dado no período.</div></div>
  );
  const bruta = etapas[0].valor || 0;
  const final = etapas[etapas.length - 1].valor || 0;
  let run = 0;
  const bars = etapas.map((e, i) => {
    const ultimo = i === etapas.length - 1;
    let de, ate;
    if (i === 0 || ultimo) { de = 0; ate = e.valor || 0; run = ate; }
    else { de = run; ate = run + (e.valor || 0); run = ate; }
    const cor = i === 0 ? TF_SERIE_COR.receita : ultimo ? (e.valor >= 0 ? '#1F9D6B' : '#D8453B') : '#D8453B';
    return { ...e, de, ate, cor, deducao: !ultimo && i > 0 };
  });
  const vals = bars.flatMap(b => [b.de, b.ate]).concat(0);
  const lo = Math.min(...vals), hiV = Math.max(...vals);
  const H = 250, padT = 26, padB = 30, padX = 8;
  const cw = Math.max(w, 300);
  const innerH = H - padT - padB;
  const y = (v) => padT + innerH * (1 - (v - lo) / ((hiV - lo) || 1));
  const slot = (cw - 2 * padX) / bars.length;
  const bw = Math.min(64, slot * 0.58);
  const cx = (i) => padX + slot * i + slot / 2;
  const sel = hi != null ? bars[hi] : null;
  const apertado = slot < 76;
  const fs = apertado ? 10 : 11.5;
  const fmtSig = (b) => (b.deducao || b.valor < 0 ? '−' + USDk(Math.abs(b.valor)) : USDk(b.valor));
  const lucroS = tfSinalSaude(final);
  return (
    <div style={tf.panel}>
      <div style={tf.panelHead}>
        <div>
          <div style={tf.panelTitle}>Da receita ao lucro real</div>
          <div style={tf.panelSub}>Comissão estimada de 5% sobre a receita líquida; taxa Buygoods de 7% sobre as vendas válidas.</div>
        </div>
        <div style={{ textAlign: 'right' }}>
          <div style={{ ...tf.panelKpi, color: lucroS.color }} className="fenix-num">{tfUsd(final)}</div>
          <div style={tf.panelKpiSub} className="fenix-num">{bruta ? PCT(final / bruta * 100) + ' da receita bruta' : ''}</div>
        </div>
      </div>
      <div ref={ref} style={{ width: '100%', position: 'relative' }} onMouseLeave={() => setHi(null)}>
        <svg width={cw} height={H} style={{ display: 'block' }} role="img" aria-label="Cascata da receita bruta ao lucro real">
          {lo < 0 && <line x1={padX} x2={cw - padX} y1={y(0)} y2={y(0)} stroke="#9AA3B5" strokeWidth="1" />}
          {bars.map((b, i) => i < bars.length - 1 && (
            <line key={'c' + i} x1={cx(i) + bw / 2} x2={cx(i + 1) - bw / 2} y1={y(b.ate)} y2={y(b.ate)}
              stroke="#C7CDD9" strokeWidth="1" strokeDasharray="3 3" />
          ))}
          {bars.map((b, i) => {
            const top = Math.min(y(b.de), y(b.ate));
            const h = Math.max(Math.abs(y(b.de) - y(b.ate)), 1.5);
            const on = hi === i;
            const dim = hi != null && !on;
            return (
              <g key={b.etapa} onMouseEnter={() => setHi(i)} style={{ cursor: 'default' }}>
                <rect x={cx(i) - slot / 2} y={padT - 20} width={slot} height={innerH + 20} fill="transparent" />
                <rect x={cx(i) - bw / 2} y={top} width={bw} height={h} rx="4"
                  fill={b.cor} fillOpacity={b.deducao ? (on ? 0.95 : 0.72) : 1} opacity={dim ? 0.45 : 1} />
                <text x={cx(i)} y={top - 7} textAnchor="middle" fontFamily="var(--font-num)" fontSize={fs} fontWeight="700"
                  fill={b.deducao ? '#D8453B' : i === bars.length - 1 ? b.cor : '#011B36'}>{fmtSig(b)}</text>
                <text x={cx(i)} y={H - (apertado && b.etapa.includes(' ') ? 20 : 10)} textAnchor="middle" fontFamily="var(--font-sans)" fontSize={fs}
                  fontWeight={on ? 700 : 600} fill={on ? '#011B36' : '#6B7488'}>
                  {apertado ? b.etapa.split(' ').map((p, j) => <tspan key={j} x={cx(i)} dy={j ? 12 : 0}>{p}</tspan>) : b.etapa}
                </text>
              </g>
            );
          })}
        </svg>
        {sel && (
          <div style={{ ...tf.tip, left: Math.min(Math.max(cx(hi), 90), cw - 90), top: Math.max(Math.min(y(sel.de), y(sel.ate)) - 64, 0) }}>
            <div style={tf.tipDay}>{sel.etapa}</div>
            <div style={tf.tipVal} className="fenix-num">{sel.deducao || sel.valor < 0 ? '−' : ''}{USD(Math.abs(sel.valor))}</div>
            {bruta > 0 && hi > 0 && <div style={tf.tipSub} className="fenix-num">{PCT(Math.abs(sel.valor) / bruta * 100)} da receita bruta</div>}
          </div>
        )}
      </div>
    </div>
  );
};

// ---- Funil em barras horizontais (padrão TopAffiliates) ----
// Rótulos em português (o backend manda as chaves do RedTrack). base = etapa de onde o
// visitante vem de fato: o checkout sai da LP e NÃO exige o pitch do vídeo, então a conversão
// dele é sobre as visitas à LP (sobre o pitch dava >100%). Taxa >100% nunca aparece.
const TF_FUNIL = {
  'Cliques':           { label: 'Cliques' },
  'LP views':          { label: 'Visitas à LP' },   // base = cliques: já está em "% cliques"
  'Vturb play':        { label: 'Play no vídeo',     base: 'LP views' },
  'Vturb pitch':       { label: 'Pitch do vídeo',    base: 'Vturb play' },
  'Initiate checkout': { label: 'Checkout iniciado', base: 'LP views' },
  'Purchase':          { label: 'Compra',            base: 'Initiate checkout' },
  'Upsell':            { label: 'Upsell',            base: 'Purchase' },
};
const tfPctFunil = (v) => (v == null ? '' : v >= 10 ? PCT(v)
  : v >= 0.01 ? v.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + '%' : '<0,01%');
const TfFunil = ({ etapas = [] }) => {
  const [ref, w] = useWidth();
  const estreito = w > 0 && w < 440;   // celular: some o trilho, ficam os números
  const max = Math.max(...etapas.map(e => e.valor || 0), 1);
  const val = {};
  etapas.forEach(e => { val[e.etapa] = e.valor || 0; });
  const topo = etapas.length ? etapas[0].valor || 0 : 0;
  const nomeDe = (k) => (TF_FUNIL[k] ? TF_FUNIL[k].label : k);
  const taxa = (a, b) => (b > 0 && a <= b ? (a / b) * 100 : null);
  return (
    <div style={tf.panel}>
      <div style={tf.panelTitle}>Funil de conversão</div>
      <div style={tf.panelSub}>Cliques até checkout vêm do RedTrack; compra e upsell são pedidos Completed do banco.</div>
      {etapas.length === 0 ? <div style={tf.empty}>Sem dado no período.</div> : (
        <div ref={ref}>
          <div style={{ ...tf.barRow, ...(estreito ? tf.barRowSm : {}), paddingTop: 12, paddingBottom: 2 }}>
            <span style={{ ...tf.barName, ...(estreito ? tf.barNameSm : {}), ...tf.barHead }} />
            <span style={{ flex: 1, minWidth: estreito ? 0 : 40 }} />
            <span style={{ ...tf.barVal, ...(estreito ? tf.barValSm : {}), ...tf.barHead, ...(estreito ? tf.barHeadSm : {}) }}>Total</span>
            <span style={{ ...tf.barConv, ...tf.barHead, ...(estreito ? tf.barHeadSm : {}) }} title="Etapa ÷ cliques">% cliques</span>
            <span style={{ ...tf.barConv, ...tf.barHead, ...(estreito ? tf.barHeadSm : {}) }} title="Etapa ÷ etapa de origem">Conversão</span>
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
            {etapas.map((e, i) => {
              const def = TF_FUNIL[e.etapa] || {};
              const v = e.valor || 0;
              const pTopo = i > 0 ? taxa(v, topo) : null;
              const pBase = def.base && val[def.base] != null ? taxa(v, val[def.base]) : null;
              return (
                <div key={e.etapa} style={{ ...tf.barRow, ...(estreito ? tf.barRowSm : {}) }}>
                  <span style={{ ...tf.barName, ...(estreito ? tf.barNameSm : {}) }}>{nomeDe(e.etapa)}</span>
                  {estreito ? <span style={{ flex: 1 }} /> : (
                    <div style={tf.barTrack}>
                      <div style={{ ...tf.barFill, width: Math.max(v / max * 100, v ? 0.6 : 0) + '%' }} />
                    </div>
                  )}
                  <span style={{ ...tf.barVal, ...(estreito ? tf.barValSm : {}) }} className="fenix-num">{NUM(v)}</span>
                  <span style={tf.barConv} className="fenix-num">{tfPctFunil(pTopo)}</span>
                  <span style={{ ...tf.barConv, color: 'var(--fg-2)' }} className="fenix-num"
                    title={pBase != null ? `${nomeDe(e.etapa)} ÷ ${nomeDe(def.base)}` : undefined}>{tfPctFunil(pBase)}</span>
                </div>
              );
            })}
          </div>
          <div style={tf.funilNota}>Conversão sobre a etapa de origem. O checkout conta sobre as visitas à LP porque não exige o pitch do vídeo.</div>
        </div>
      )}
    </div>
  );
};

// ---- Tabela padrão 'ac' (th sticky ordenável, hover, tdNum) com busca, "N de M" e limpar ----
// titulo: texto OU nó (ex.: Segmented das abas). q/setQ: busca controlada (opcional).
// campos(r): textos da busca (padrão label + key). nome(r): célula do nome (padrão label).
// titleDe(r): tooltip da célula do nome (padrão label).
// onRow + podeAbrir(r): linha só vira clicável quando podeAbrir devolve true.
// id: chave da preferência "Mais colunas" no localStorage (uma por tabela).
// Linhas sem movimento (spend, receita e pedidos zerados) ficam ocultas até o usuário pedir.
const TF_PAGINA = 100;
const tfLeExtra = (id) => { try { return localStorage.getItem('fenix_tf_colunas_' + id) === '1'; } catch (_e) { return false; } };
const tfGravaExtra = (id, on) => { try { localStorage.setItem('fenix_tf_colunas_' + id, on ? '1' : '0'); } catch (_e) { /* anônimo */ } };
const TfTabela = ({ id = 'geral', titulo, rows = [], rotulo = 'Nome', busca = true, q: qExt, setQ: setQExt, campos, nome,
                    titleDe, onRow, podeAbrir = () => true, rowTitle, vazio = 'Sem dado no período.', loading = false,
                    roasAlvo = null, placeholder = 'Buscar' }) => {
  const [sortKey, setSortKey] = React.useState('spend');
  const [dir, setDir] = React.useState(-1);
  const [qInt, setQInt] = React.useState('');
  const [lim, setLim] = React.useState(TF_PAGINA);
  const [extra, setExtra] = React.useState(() => tfLeExtra(id));
  const [verParados, setVerParados] = React.useState(false);
  const q = qExt != null ? qExt : qInt;
  const setQ = setQExt || setQInt;
  const cols = extra ? TF_COLS : TF_COLS.filter(c => !c.extra);
  const trocaExtra = () => { const n = !extra; setExtra(n); tfGravaExtra(id, n); };
  const sortBy = (k) => (k === sortKey ? setDir(-dir) : (setSortKey(k), setDir(-1)));
  const ind = (k) => (sortKey === k ? (dir < 0 ? ' ▾' : ' ▴') : '');
  const txt = campos || ((r) => [r.label, r.key]);
  const parados = React.useMemo(() => rows.filter(tfSemMov).length, [rows]);
  const base = verParados ? rows : rows.filter(r => !tfSemMov(r));
  const list = React.useMemo(() => {
    const t = q.trim().toLowerCase();
    const l = !t ? base.slice() : base.filter(r => txt(r).some(s => String(s || '').toLowerCase().includes(t)));
    l.sort((a, b) => {
      const av = a[sortKey], bv = b[sortKey];
      if (sortKey === 'label') return String(av || '').localeCompare(String(bv || '')) * dir;
      if (av == null && bv == null) return 0;
      if (av == null) return 1;          // sem valor sempre por último
      if (bv == null) return -1;
      return (av - bv) * dir;
    });
    return l;
  }, [rows, sortKey, dir, q, verParados]);
  React.useEffect(() => { setLim(TF_PAGINA); }, [rows, q]);
  const vis = list.slice(0, lim);
  const celula = (c, r) => {
    // CPA/CPC sem spend não é custo zero, é custo não medido: n/d em vez de "$0".
    const v = (c.key === 'cpa' || c.key === 'cpc') && !r.spend ? null : r[c.key];
    if (v == null) return <span style={{ color: 'var(--fg-3)' }}>n/d</span>;
    if (c.tipo === 'pill') {
      const s = tfSinalSaude(v);
      return <span style={{ ...tf.pill, color: s.color, background: s.bg }}>{c.fmt(v)}</span>;
    }
    if (c.tipo === 'roas') { const s = tfRoasSaude(v, roasAlvo); return <span style={{ color: s.color, fontWeight: 700 }}>{c.fmt(v)}</span>; }
    if (c.tipo === 'sinal') return <span style={{ color: tfSinalSaude(v).color }}>{c.fmt(v)}</span>;
    return c.fmt(v);
  };
  return (
    <div style={tf.tableCard}>
      <div style={tf.tblHead}>
        <div style={{ minWidth: 0, maxWidth: '100%' }}>{typeof titulo === 'string' ? <div style={tf.panelTitle}>{titulo}</div> : titulo}</div>
        <div style={tf.tblTools}>
          <span style={tf.count} className="fenix-num">
            {list.length !== base.length ? `${NUM(list.length)} de ${NUM(base.length)}` : `${NUM(base.length)} ${base.length === 1 ? 'linha' : 'linhas'}`}
          </span>
          <button type="button" onClick={trocaExtra} aria-pressed={extra} style={{ ...tf.toolBtn, ...(extra ? tf.toolBtnOn : {}) }}
            title={extra ? 'Esconder Receita líquida, Upsells, CPC e Refund %' : 'Mostrar Receita líquida, Upsells, CPC e Refund %'}>
            {extra ? 'Menos colunas' : 'Mais colunas'}
          </button>
          {busca && (
            <label style={tf.search}>
              <Icon name="search" size={15} color="var(--fg-3)" />
              <input value={q} onChange={e => setQ(e.target.value)} placeholder={placeholder} style={tf.searchInput}
                aria-label={placeholder} />
              {q && (
                <button type="button" onClick={() => setQ('')} style={tf.clearBtn} aria-label="Limpar busca" title="Limpar busca">
                  <Icon name="x" size={13} color="currentColor" />
                </button>
              )}
            </label>
          )}
        </div>
      </div>
      <div style={tf.scroll}>
        <table style={{ ...tf.table, minWidth: extra ? 1040 : 780 }}>
          <thead><tr>
            <th onClick={() => sortBy('label')} style={{ ...tf.th, textAlign: 'left', paddingLeft: 16, width: extra ? '24%' : '30%' }}>{rotulo}{ind('label')}</th>
            {cols.map(c => (
              <th key={c.key} onClick={() => sortBy(c.key)} style={{ ...tf.th, textAlign: 'right' }}>{c.label}{ind(c.key)}</th>
            ))}
          </tr></thead>
          <tbody>
            {vis.map(r => {
              const abre = onRow && podeAbrir(r);
              return (
                <tr key={r.key + '·' + r.label} onClick={abre ? () => onRow(r) : undefined}
                  title={abre && rowTitle ? rowTitle(r) : undefined}
                  style={abre ? { cursor: 'pointer' } : undefined}
                  onMouseEnter={e => { e.currentTarget.style.background = 'var(--gray-50)'; }}
                  onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}>
                  <td style={tf.tdName} title={titleDe ? titleDe(r) : r.label}>
                    <span style={{ display: 'flex', alignItems: 'center', minWidth: 0 }}>
                      {r.dot && <span style={{ ...tf.dot, background: r.dot }} />}
                      <span style={{ minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }}>{nome ? nome(r) : r.label}</span>
                      {r.spendIncompleto && <span style={tf.warnMark} title="Spend incompleto: linhas sem câmbio">*</span>}
                      {abre && <Icon name="chevron" size={14} color="var(--fg-4)" style={{ marginLeft: 'auto', flex: 'none' }} />}
                    </span>
                  </td>
                  {cols.map(c => (
                    <td key={c.key} className="fenix-num"
                      style={{ ...tf.tdNum, ...(c.forte ? { fontWeight: 700 } : {}) }}>
                      {celula(c, r)}
                    </td>
                  ))}
                </tr>
              );
            })}
            {!loading && list.length === 0 && (
              <tr><td colSpan={cols.length + 1} style={tf.empty}>
                {q.trim() ? <>Nada encontrado para "{q.trim()}". <button type="button" onClick={() => setQ('')} style={tf.linkBtn}>Limpar busca</button></> : vazio}
              </td></tr>
            )}
          </tbody>
        </table>
      </div>
      {(list.length > lim || parados > 0) && (
        <div style={tf.more}>
          {list.length > lim && <>
            <span className="fenix-num">Mostrando {NUM(vis.length)} de {NUM(list.length)}</span>
            <button type="button" onClick={() => setLim(lim + TF_PAGINA * 4)} style={tf.moreBtn}>Mostrar mais</button>
          </>}
          {parados > 0 && (
            <button type="button" onClick={() => setVerParados(v => !v)} style={tf.linkBtn}>
              {verParados ? `Ocultar ${NUM(parados)} sem movimento` : `Mostrar ${NUM(parados)} sem movimento`}
            </button>
          )}
        </div>
      )}
    </div>
  );
};

// ---- Página ----
const TrafegoVisaoView = () => {
  const [period, setPeriod] = React.useState('30d');
  const [customRange, setCustomRange] = React.useState(null);
  const [platform, setPlatform] = React.useState('todas');
  const [refundBy, setRefundBy] = React.useState('cancelamento');
  const [data, setData] = React.useState(null);
  const [err, setErr] = React.useState(false);
  const [loading, setLoading] = React.useState(true);
  const fullLoading = useFullLoading();
  const withLoad = (fn) => (...a) => { fullLoading.mark(); fn(...a); };
  const retry = useAutoRetry(err);   // falhou → tenta de novo a cada 15 s, sozinho

  // Recarregar a página volta para cá (o App lê o hash no 1º render).
  React.useEffect(() => {
    try { history.replaceState(null, '', '#trafego-visao'); } catch (_e) { /* sem history */ }
  }, []);

  React.useEffect(() => {
    const began = fullLoading.begin();
    let cancelled = false; setLoading(true);
    fetchTrafegoOverview({ period, customRange, platform, refundBy })
      .then(d => { if (!cancelled) { setData(d); setErr(false); } })
      .catch(() => { if (!cancelled) setErr(true); })
      .finally(() => { if (!cancelled) setLoading(false); fullLoading.end(began); });
    return () => { cancelled = true; };
  }, [period, customRange, platform, refundBy, retry]);

  if (err && !data) return (
    <>
      <LoadingScreen conn="offline" />
      <div style={tf.reconNota}>
        Custo do RedTrack × receita do banco. Se a API caiu, esta tela reconecta sozinha a cada 15 s.
      </div>
    </>
  );
  if (!data) return <LoadingScreen conn="connecting" />;

  const k = data.kpis || {};
  const porCanal = (data.porCanal || []).map(r => ({ ...r, dot: TF_CANAL_COR[r.canal] || TF_OUTROS_COR }));
  const podeFonte = window.FenixAuth?.canNav ? window.FenixAuth.canNav('trafego-fonte') : true;

  return (
    <div style={{ position: 'relative' }}>
      <TfCabecalho titulo="Visão geral" data={data} err={err}>
        <TfFiltros period={period} setPeriod={withLoad(setPeriod)}
          customRange={customRange} setCustomRange={withLoad(setCustomRange)}
          platform={platform} setPlatform={withLoad(setPlatform)}
          refundBy={refundBy} setRefundBy={withLoad(setRefundBy)} />
      </TfCabecalho>

      <TfCards k={k} ant={data.anterior} serie={data.serie || []} meta={data.meta} janelaAnterior={data.janelaAnterior} />
      <TfFaixa k={k} ant={data.anterior} />

      <div style={tf.chartRow}>
        <div style={tf.chartMain}><TfSerie data={data} chKey="tf-visao" /></div>
        <div style={tf.chartSide}><TfDonut rows={porCanal} dimensao="canal" corDe={tfCorCanal} /></div>
      </div>

      <div style={tf.duo}>
        <TfCascata etapas={data.cascata || []} />
        <TfFunil etapas={data.funil || []} />
      </div>

      <TfTabela id="visao-fontes" titulo="Por fonte de tráfego" rotulo="Fonte" rows={porCanal} loading={loading}
        roasAlvo={data.meta ? data.meta.roasAlvo : null} placeholder="Buscar fonte"
        onRow={(r) => tfIrPara('trafego-fonte?fonte=' + encodeURIComponent(r.canal))}
        podeAbrir={(r) => !!r.canal && podeFonte}
        rowTitle={(r) => `Abrir Fonte de tráfego filtrada em ${r.label}`} />
      <TfTabela id="visao-compradores" titulo="Por comprador de mídia" rotulo="Comprador" rows={data.porComprador || []} loading={loading}
        roasAlvo={data.meta ? data.meta.roasAlvo : null} placeholder="Buscar comprador" />

      <FullLoadingOverlay show={fullLoading.show && !!data} conn="connecting" />
    </div>
  );
};

const tf = {
  header: { display: 'flex', flexDirection: 'column', alignItems: 'stretch', gap: 12, marginBottom: 18 },
  titleWrap: { display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', minWidth: 0 },
  title: { font: '800 22px/1.1 var(--font-sans)', color: 'var(--fg-1)', letterSpacing: '-.01em', margin: 0 },
  filterRow: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' },
  headTop: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px 16px', flexWrap: 'wrap' },
  stamp: { display: 'inline-flex', alignItems: 'center', gap: 6, font: '600 11.5px/1 var(--font-mono)', color: 'var(--fg-3)', background: 'var(--bg-sunken)', padding: '7px 10px', borderRadius: 'var(--r-pill)', whiteSpace: 'nowrap' },
  chip: { display: 'inline-flex', alignItems: 'center', gap: 6, font: '700 11.5px/1 var(--font-sans)', padding: '6px 10px', borderRadius: 'var(--r-pill)', borderWidth: 1, borderStyle: 'solid', borderColor: 'transparent', cursor: 'pointer', whiteSpace: 'nowrap' },
  pop: { position: 'absolute', top: 'calc(100% + 8px)', zIndex: 40, background: '#fff', border: '1px solid var(--border-1)', borderRadius: 'var(--r-md)', boxShadow: 'var(--shadow-lg)', padding: '14px 16px', textAlign: 'left' },
  popTitle: { font: '700 13px/1.2 var(--font-sans)', color: 'var(--fg-1)', marginBottom: 8 },
  popTxt: { font: '500 12.5px/1.5 var(--font-sans)', color: 'var(--fg-2)' },
  popList: { margin: 0, padding: 0, listStyle: 'none', display: 'flex', flexDirection: 'column', gap: 10 },
  popItem: { font: '500 12.5px/1.5 var(--font-sans)', color: 'var(--fg-2)', paddingLeft: 10, borderLeft: '2px solid var(--border-2)' },
  popSub: { margin: '4px 0 0', paddingLeft: 16, font: '500 11.5px/1.5 var(--font-sans)', color: 'var(--fg-3)', wordBreak: 'break-word' },
  infoBtn: { width: 18, height: 18, borderRadius: '50%', border: '1.5px solid var(--gray-400)', background: '#fff', color: 'var(--fg-3)', font: 'italic 700 11px/1 Georgia, serif', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'help', padding: 0 },

  bigRow: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 16, marginBottom: 14 },
  bigCard: { background: '#fff', borderWidth: 1, borderStyle: 'solid', borderColor: 'var(--border-1)', borderRadius: 'var(--r-lg)', boxShadow: 'var(--shadow-sm)', padding: '16px 18px 14px', minWidth: 0, transition: 'transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease)' },
  bigTop: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 },
  bigIc: { width: 32, height: 32, borderRadius: 'var(--r-md)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none' },
  bigLbl: { font: '700 13.5px/1.2 var(--font-sans)', color: 'var(--fg-2)' },
  bigVal: { font: '800 28px/1 var(--font-num)', letterSpacing: '-.02em', color: 'var(--fg-1)', fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
  bigMid: { display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 12, marginTop: 10 },
  bigFoot: { font: '500 11.5px/1.35 var(--font-sans)', color: 'var(--fg-3)', marginTop: 10, paddingTop: 9, borderTop: '1px solid var(--border-1)', minHeight: 16, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
  delta: { display: 'inline-flex', alignItems: 'center', gap: 3, font: '700 12.5px/1 var(--font-num)', fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap', paddingBottom: 2 },
  deltaNulo: { font: '500 11.5px/1 var(--font-sans)', color: 'var(--fg-3)', whiteSpace: 'nowrap', paddingBottom: 2 },
  deltaSm: { font: '700 11px/1 var(--font-num)', gap: 2, paddingBottom: 0 },

  stripRow: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 12, marginBottom: 16 },
  stripCell: { background: '#fff', border: '1px solid var(--border-1)', borderRadius: 'var(--r-lg)', boxShadow: 'var(--shadow-sm)', padding: '11px 14px', minWidth: 0, cursor: 'default' },
  stripLbl: { font: '600 11.5px/1.2 var(--font-sans)', color: 'var(--fg-3)', marginBottom: 7, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
  stripLine: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' },
  stripVal: { font: '800 17px/1 var(--font-num)', color: 'var(--fg-1)', fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' },

  chartRow: { display: 'flex', flexWrap: 'wrap', gap: 16, marginBottom: 16, alignItems: 'stretch' },
  chartMain: { flex: '2 1 520px', minWidth: 0 },
  chartSide: { flex: '1 1 300px', minWidth: 0, display: 'flex', flexDirection: 'column' },
  duo: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))', gap: 16, marginBottom: 16, alignItems: 'stretch' },
  panel: { background: '#fff', border: '1px solid var(--border-1)', borderRadius: 'var(--r-lg)', padding: '18px 20px', boxShadow: 'var(--shadow-sm)', minWidth: 0 },
  panelHead: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, marginBottom: 8 },
  panelTitle: { font: '700 16px/1.25 var(--font-sans)', color: 'var(--fg-1)' },
  panelSub: { font: 'var(--text-body-sm)', color: 'var(--fg-3)', marginTop: 3 },
  panelKpi: { font: '800 20px/1 var(--font-num)', fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' },
  panelKpiSub: { font: '600 11px/1 var(--font-num)', color: 'var(--fg-3)', marginTop: 6, whiteSpace: 'nowrap' },
  tip: { position: 'absolute', transform: 'translateX(-50%)', background: 'var(--fenix-navy)', color: '#fff', borderRadius: 'var(--r-md)', padding: '8px 12px', pointerEvents: 'none', boxShadow: 'var(--shadow-md)', whiteSpace: 'nowrap', zIndex: 5 },
  tipDay: { font: '700 10.5px/1 var(--font-sans)', color: 'var(--fenix-orange-300)', marginBottom: 5 },
  tipVal: { font: '800 16px/1 var(--font-num)', fontVariantNumeric: 'tabular-nums' },
  tipSub: { font: '600 10.5px/1 var(--font-num)', color: 'var(--gray-300)', marginTop: 5 },
  barRow: { display: 'flex', alignItems: 'center', gap: 12, padding: '7px 8px', borderRadius: 'var(--r-sm)' },
  barName: { font: '700 12.5px/1 var(--font-sans)', color: 'var(--fg-1)', width: 124, flex: 'none', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
  barTrack: { flex: 1, minWidth: 40, height: 10, background: 'var(--bg-sunken)', borderRadius: 'var(--r-pill)', overflow: 'hidden' },
  barFill: { height: '100%', borderRadius: 'var(--r-pill)', background: 'linear-gradient(90deg, var(--fenix-orange-300), var(--fenix-orange))' },
  barVal: { font: '700 12.5px/1 var(--font-num)', color: 'var(--fg-1)', width: 76, textAlign: 'right', flex: 'none', fontVariantNumeric: 'tabular-nums' },
  barConv: { font: '600 11px/1 var(--font-num)', color: 'var(--fg-3)', width: 64, textAlign: 'right', flex: 'none', fontVariantNumeric: 'tabular-nums' },
  barRowSm: { gap: 4, paddingLeft: 0, paddingRight: 0 },
  barNameSm: { width: 100, fontSize: 11.5 },
  barValSm: { width: 60 },
  barHeadSm: { letterSpacing: 0, fontSize: 9.5 },
  barHead: { font: '700 10.5px/1 var(--font-sans)', letterSpacing: '.04em', textTransform: 'uppercase', color: 'var(--fg-3)', whiteSpace: 'nowrap' },
  funilNota: { font: '500 11.5px/1.45 var(--font-sans)', color: 'var(--fg-3)', marginTop: 10, paddingTop: 9, borderTop: '1px solid var(--border-1)' },

  tableCard: { background: '#fff', border: '1px solid var(--border-1)', borderRadius: 'var(--r-lg)', boxShadow: 'var(--shadow-sm)', overflow: 'hidden', marginBottom: 16 },
  tblHead: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, padding: '14px 16px', flexWrap: 'wrap', borderBottom: '1px solid var(--border-1)' },
  tblTools: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', minWidth: 0 },
  count: { font: '600 11.5px/1 var(--font-mono)', color: 'var(--fg-3)', whiteSpace: 'nowrap' },
  search: { display: 'flex', alignItems: 'center', gap: 8, background: '#fff', border: '1px solid var(--border-2)', borderRadius: 'var(--r-md)', padding: '7px 10px', width: 280, maxWidth: '100%', cursor: 'text' },
  searchInput: { border: 0, outline: 'none', font: 'var(--text-body-sm)', color: 'var(--fg-1)', background: 'transparent', flex: 1, minWidth: 0 },
  clearBtn: { border: 0, background: 'var(--bg-sunken)', color: 'var(--fg-2)', borderRadius: 'var(--r-pill)', width: 20, height: 20, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', padding: 0, flex: 'none' },
  linkBtn: { border: 0, background: 'transparent', color: 'var(--fenix-blue)', font: '700 13px/1 var(--font-sans)', cursor: 'pointer', padding: 0, marginLeft: 6 },
  scroll: { maxHeight: 560, overflowY: 'auto', overflowX: 'auto' },
  table: { width: '100%', borderCollapse: 'collapse' },
  th: { font: '700 10.5px/1 var(--font-sans)', letterSpacing: '.04em', textTransform: 'uppercase', color: 'var(--fg-3)', padding: '11px 10px', background: 'var(--gray-50)', borderBottom: '1px solid var(--border-1)', whiteSpace: 'nowrap', userSelect: 'none', cursor: 'pointer', position: 'sticky', top: 0, zIndex: 2 },
  // maxWidth 0: o nome não dita a largura da coluna (fica com a largura do th e corta com …).
  tdName: { padding: '10px 12px 10px 16px', borderBottom: '1px solid var(--border-1)', font: '600 13px/1.25 var(--font-sans)', color: 'var(--fg-1)', whiteSpace: 'nowrap', maxWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', textAlign: 'left' },
  tdNum: { padding: '10px 10px', borderBottom: '1px solid var(--border-1)', font: 'var(--text-body-sm)', color: 'var(--fg-1)', whiteSpace: 'nowrap', textAlign: 'right', fontFamily: 'var(--font-num)', fontVariantNumeric: 'tabular-nums' },
  pill: { display: 'inline-block', padding: '4px 8px', borderRadius: 'var(--r-pill)', font: '700 12px/1 var(--font-num)', fontVariantNumeric: 'tabular-nums' },
  dot: { display: 'inline-block', width: 8, height: 8, borderRadius: 2, marginRight: 8, flex: 'none' },
  warnMark: { color: 'var(--warning)', fontWeight: 800, marginLeft: 4 },
  empty: { padding: '34px 22px', textAlign: 'center', font: 'var(--text-body-sm)', color: 'var(--fg-3)' },
  more: { display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 12, padding: '10px 16px', borderTop: '1px solid var(--border-1)', font: '600 12px/1 var(--font-sans)', color: 'var(--fg-3)' },
  toolBtn: { borderWidth: 1, borderStyle: 'solid', borderColor: 'var(--border-2)', background: '#fff', color: 'var(--fg-2)', borderRadius: 'var(--r-md)', padding: '8px 11px', font: '700 12px/1 var(--font-sans)', cursor: 'pointer', whiteSpace: 'nowrap' },
  toolBtnOn: { borderColor: 'var(--fenix-blue-300)', background: 'var(--fenix-blue-100)', color: 'var(--fenix-blue)' },
  moreBtn: { border: '1px solid var(--border-2)', background: '#fff', color: 'var(--fg-1)', borderRadius: 'var(--r-md)', padding: '7px 12px', font: '700 12px/1 var(--font-sans)', cursor: 'pointer' },
  notice: { display: 'flex', alignItems: 'center', gap: 8, font: '600 12.5px/1.4 var(--font-sans)', color: 'var(--warning)', background: 'var(--warning-bg)', padding: '10px 16px', borderRadius: 'var(--r-md)' },
  reconNota: { textAlign: 'center', font: 'var(--text-body-sm)', color: 'var(--fg-4)', maxWidth: 520, margin: '-28px auto 0' },
};

Object.assign(window, {
  TrafegoVisaoView, TfCabecalho, TfFiltros, TfStatus, TfCards, TfFaixa, TfSerie, TfDonut, TfCascata,
  TfFunil, TfTabela, tfSegs, tfCorCanal, tfCorPaleta, TF_CANAL_COR,
});
