/* mkt_analise_viz.jsx
   Gráficos SVG da Análise da Digi AI em Resultados (window.An*).
   Sem bibliotecas: SVG inline e divs, como o RPC e Resultados.
   Animações ao estilo dos blocos shadcn/Recharts: entrada (barras a crescer, linhas a desenhar),
   tooltip que desliza entre pontos, cursor por categoria, ponto activo e destaque do elemento em hover. */

const AN_LUZ = { verde: 'var(--success, #16a34a)', amarelo: 'var(--warning, #f59e0b)', vermelho: 'var(--danger, #dc2626)' };
const AN_BP = 'var(--text-dim, #94a3b8)';
const AN_REAL = 'var(--ai-500, #3859D0)';
const AN_MESES = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
const AN_EASE = 'cubic-bezier(.22,1,.36,1)';

const AN_CSS = `
@keyframes an-grow-y { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes an-grow-x { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes an-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
@keyframes an-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes an-enter { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes an-pop { from { opacity: 0; transform: scale(.35); } to { opacity: 1; transform: scale(1); } }
@keyframes an-cell-in { from { opacity: 0; transform: scale(.86); } to { opacity: 1; transform: none; } }
@keyframes an-ping { 0% { transform: scale(1); opacity: .45; } 75%, 100% { transform: scale(2.6); opacity: 0; } }

.an-card { transition: border-color .2s ease; animation: an-enter .45s ${AN_EASE} backwards; }
.an-card:not([data-activo="1"]):hover { border-top-color: var(--border-strong, #bec9de) !important; border-right-color: var(--border-strong, #bec9de) !important; border-bottom-color: var(--border-strong, #bec9de) !important; }
.an-card:not([data-activo="1"]):not([data-acento]):hover { border-left-color: var(--border-strong, #bec9de) !important; }
.an-kpi { transition: border-color .15s ease, background-color .15s ease, transform .15s ease; }
.an-kpi:hover { border-color: var(--border-strong, #bec9de) !important; background-color: var(--bg-elev) !important; transform: translateY(-1px); }

.an-bar-y { transform-box: fill-box; transform-origin: 50% 100%; animation: an-grow-y .75s ${AN_EASE} backwards; animation-delay: calc(var(--i, 0) * 38ms); }
.an-bar-yh { transform-origin: 50% 100%; animation: an-grow-y .75s ${AN_EASE} backwards; animation-delay: calc(var(--i, 0) * 38ms); }
.an-bar-x { transform-origin: 0 50%; animation: an-grow-x .8s ${AN_EASE} backwards; animation-delay: calc(var(--i, 0) * 45ms); }
.an-bar-xr { transform-origin: 100% 50%; animation: an-grow-x .8s ${AN_EASE} backwards; animation-delay: calc(var(--i, 0) * 45ms); }
.an-bar-xc { transform-origin: 50% 50%; animation: an-grow-x .8s ${AN_EASE} backwards; animation-delay: calc(var(--i, 0) * 60ms); }
.an-draw { stroke-dasharray: 1; animation: an-draw 1.2s cubic-bezier(.65,0,.35,1) backwards; animation-delay: var(--d, 0ms); }
.an-fade { animation: an-fade .7s ease backwards; animation-delay: var(--d, 0ms); }
.an-pop { transform-box: fill-box; transform-origin: center; animation: an-pop .5s cubic-bezier(.34,1.56,.64,1) backwards; animation-delay: calc(var(--i, 0) * 40ms); }
.an-pop-fast { transform-box: fill-box; transform-origin: center; animation: an-pop .22s ${AN_EASE} backwards; }
.an-cell-in { animation: an-cell-in .45s ${AN_EASE} backwards; animation-delay: calc(var(--i, 0) * 14ms); }
.an-ping { transform-box: fill-box; transform-origin: center; animation: an-ping 2.2s cubic-bezier(0,0,.2,1) infinite; pointer-events: none; }

.an-dim { transition: opacity .18s ease, filter .18s ease; }
.an-row { transition: background-color .15s cubic-bezier(.4,0,.2,1); border-radius: 6px; }
.an-row:hover { background-color: color-mix(in oklch, var(--bg-sunken, #e8edf5) 65%, transparent); }
.an-tr { transition: background-color .15s cubic-bezier(.4,0,.2,1); }
.an-tr:hover { background-color: color-mix(in oklch, var(--bg-sunken, #e8edf5) 55%, transparent); }
.an-host .an-hbar { transition: filter .18s ease; }
.an-host:hover .an-hbar { filter: brightness(1.07) saturate(1.2); }
.an-cell { transition: transform .15s ease, opacity .18s ease; }
.an-cell:hover { transform: scale(1.1); position: relative; z-index: 2; }
.an-dot { transition: transform .15s ease; }
.an-dot:hover { transform: scale(1.3); }
.an-link { transition: color .15s ease; }
.an-link:hover { color: var(--ai-500, #3859D0) !important; }
.an-gauge .an-gauge-arc { transition: stroke-width .2s ease; }
.an-gauge:hover .an-gauge-arc { stroke-width: 14; }
.an-bolha { transition: opacity .18s ease, fill-opacity .18s ease, stroke-width .18s ease; }

@media (prefers-reduced-motion: reduce) {
  .an-card, .an-bar-y, .an-bar-yh, .an-bar-x, .an-bar-xr, .an-bar-xc, .an-draw, .an-fade, .an-pop, .an-pop-fast, .an-cell-in, .an-ping { animation: none !important; }
}
`;
(function anInjectCss() {
  if (typeof document === 'undefined' || document.getElementById('an-viz-css')) return;
  const s = document.createElement('style');
  s.id = 'an-viz-css';
  s.textContent = AN_CSS;
  document.head.appendChild(s);
})();

let anUidN = 0;
function useAnId(prefixo) {
  const ref = React.useRef(null);
  if (!ref.current) ref.current = `${prefixo}-${++anUidN}`;
  return ref.current;
}

function anNum(v) { const x = Number(v); return Number.isFinite(x) ? x : 0; }
function anFmtEur(v) {
  if (v == null || !Number.isFinite(Number(v))) return '—';
  const x = Number(v);
  if (Math.abs(x) >= 1e6) return `${(x / 1e6).toLocaleString('pt-PT', { maximumFractionDigits: 2 })} M€`;
  if (Math.abs(x) >= 1e3) return `${Math.round(x / 1e3).toLocaleString('pt-PT')} k€`;
  return `${Math.round(x).toLocaleString('pt-PT')} €`;
}
function anFmtN(v, casas = 0) {
  if (v == null || !Number.isFinite(Number(v))) return '—';
  return Number(v).toLocaleString('pt-PT', { maximumFractionDigits: casas });
}
function anFmtPct(v) { return v == null || !Number.isFinite(Number(v)) ? '—' : `${anFmtN(v, 1)} %`; }
function anLuzPct(p, verde = 95, amarelo = 70) {
  if (p == null || !Number.isFinite(Number(p))) return null;
  return p >= verde ? 'verde' : p >= amarelo ? 'amarelo' : 'vermelho';
}
// Barra horizontal com gradiente para a direita e remate sólido de 2 px.
function anBarraH(cor, { forte = 88, fraco = 22, dir = 'right' } = {}) {
  return {
    background: `linear-gradient(to ${dir}, color-mix(in oklch, ${cor} ${fraco}%, transparent), color-mix(in oklch, ${cor} ${forte}%, transparent))`,
    [dir === 'right' ? 'borderRight' : 'borderLeft']: `2px solid ${cor}`,
    boxSizing: 'border-box',
  };
}

// Tooltip: fica montado depois do primeiro hover e desliza entre posições.
// Posição fixa no ecrã para não ser cortado por contentores com scroll.
function useAnTip() {
  const ref = React.useRef(null);
  const [tip, setTip] = React.useState(null);
  const show = (e, content, i = null) => {
    setTip({ x: e.clientX, y: e.clientY, content, i, on: true });
  };
  const hide = () => setTip(t => (t ? { ...t, on: false, i: null } : t));
  return { ref, tip, show, hide, activo: tip?.on ? tip.i : null };
}

const AnTipBox = ({ tip }) => {
  if (!tip) return null;
  const larg = 240; const alt = 150;
  const vw = typeof window !== 'undefined' ? window.innerWidth : 1200;
  const vh = typeof window !== 'undefined' ? window.innerHeight : 800;
  const left = tip.x + 16 + larg > vw ? Math.max(4, tip.x - 16 - larg) : tip.x + 16;
  const top = tip.y + 14 + alt > vh ? Math.max(4, tip.y - 14 - alt) : tip.y + 14;
  return (
    <div style={{
      position: 'fixed', left: 0, top: 0, zIndex: 1000, pointerEvents: 'none',
      transform: `translate(${left}px, ${top}px)`,
      transition: `transform 280ms ${AN_EASE}, opacity 140ms ease`,
      opacity: tip.on ? 1 : 0,
      background: 'var(--bg-elev, #fff)', border: '1px solid color-mix(in oklch, var(--border, #dde3ef) 85%, transparent)', borderRadius: 10,
      boxShadow: '0 20px 25px -5px rgba(17,41,84,0.10), 0 8px 10px -6px rgba(17,41,84,0.10)',
      padding: '7px 10px', fontSize: 11.5, lineHeight: 1.45, color: 'var(--text, #112954)', minWidth: 140, maxWidth: larg,
    }}>
      <div style={{ display: 'grid', gap: 4 }}>{tip.content}</div>
    </div>
  );
};

const AnTipTitulo = ({ children }) => <div style={{ fontWeight: 600, color: 'var(--text)' }}>{children}</div>;
const AnTipLinha = ({ cor, label, valor, tracejado, contorno }) => (
  <div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
    {cor && <span style={{ width: 10, height: 10, borderRadius: 2, flexShrink: 0, boxSizing: 'border-box', background: tracejado || contorno ? 'transparent' : cor, border: `1.5px ${tracejado ? 'dashed' : 'solid'} ${cor}` }} />}
    <span style={{ color: 'var(--text-muted)', flex: 1, whiteSpace: 'nowrap' }}>{label}</span>
    {valor != null && <span style={{ fontFamily: 'var(--font-mono)', fontVariantNumeric: 'tabular-nums', fontWeight: 600, color: 'var(--text)', marginLeft: 12, whiteSpace: 'nowrap' }}>{valor}</span>}
  </div>
);
const AnTipNota = ({ children, cor }) => <div style={{ color: cor || 'var(--text-muted)', fontSize: 11 }}>{children}</div>;

const AnVazio = ({ altura = 80, texto = 'sem dados' }) => (
  <div className="an-fade" style={{ height: altura, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-dim)', fontSize: 12, border: '1px dashed var(--border)', borderRadius: 8 }}>{texto}</div>
);

const AnNota = ({ texto }) => {
  if (!texto) return null;
  return (
    <div className="an-fade" style={{ '--d': '250ms', display: 'flex', gap: 8, alignItems: 'flex-start', marginTop: 10, padding: '8px 10px', borderRadius: 8, background: 'color-mix(in oklch, var(--ai-500, #3859D0) 6%, transparent)', fontSize: 12, lineHeight: 1.5, color: 'var(--text)' }}>
      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--ai-500, #3859D0)" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0, marginTop: 3 }}><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" /></svg>
      <span>{texto}</span>
    </div>
  );
};

const AnCartao = ({ titulo, sub, direita, nota, children, style, acento }) => (
  <div className="card an-card" data-acento={acento ? '1' : undefined} style={{ padding: 16, borderRadius: 10, border: '1px solid var(--border)', background: 'var(--bg-elev)', minWidth: 0, ...(acento ? { borderLeft: `3px solid ${acento}` } : null), ...style }}>
    {(titulo || direita) && (
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8, marginBottom: 10 }}>
        <div style={{ minWidth: 0 }}>
          <div className="font-display" style={{ fontSize: 13.5, fontWeight: 650, color: 'var(--text)' }}>{titulo}</div>
          {sub && <div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>{sub}</div>}
        </div>
        {direita}
      </div>
    )}
    {children}
    <AnNota texto={nota} />
  </div>
);

// Meio círculo com o semáforo. pct 0..120 ocupa o arco; o traço marca 100 %.
const AnGauge = ({ pct, valor, label, sub, luz, size = 150 }) => {
  const t = useAnTip();
  const p = pct == null ? null : Math.max(0, Math.min(120, Number(pct)));
  const w = size; const h = size * 0.62; const cx = w / 2; const cy = h - 6; const r = w / 2 - 12;
  const ponto = (f) => [cx - r * Math.cos(Math.PI * f), cy - r * Math.sin(Math.PI * f)];
  const arco = (f0, f1) => {
    const [x0, y0] = ponto(f0); const [x1, y1] = ponto(f1);
    return `M ${x0} ${y0} A ${r} ${r} 0 0 1 ${x1} ${y1}`;
  };
  const cor = AN_LUZ[luz || anLuzPct(pct)] || AN_REAL;
  const [mx, my] = ponto(100 / 120);
  const [mx2, my2] = [cx - (r - 10) * Math.cos(Math.PI * 100 / 120), cy - (r - 10) * Math.sin(Math.PI * 100 / 120)];
  return (
    <div ref={t.ref} className="an-gauge" style={{ position: 'relative', textAlign: 'center', minWidth: 0, '--c': cor }}
      onMouseMove={e => t.show(e, <><AnTipTitulo>{label}</AnTipTitulo><AnTipLinha cor={cor} label="Actual" valor={valor ?? anFmtPct(pct)} />{sub && <AnTipNota>{sub}</AnTipNota>}</>)}
      onMouseLeave={t.hide}>
      <svg viewBox={`0 0 ${w} ${h}`} width="100%" style={{ maxWidth: size, display: 'block', margin: '0 auto', overflow: 'visible' }}>
        <path d={arco(0.0001, 0.9999)} stroke="var(--bg-sunken, #e8edf5)" strokeWidth="12" fill="none" strokeLinecap="round" />
        {p != null && p > 0 && <path className="an-gauge-arc an-draw" pathLength="1" style={{ '--d': '120ms' }} d={arco(0.0001, Math.max(0.001, p / 120))} stroke={cor} strokeWidth="12" fill="none" strokeLinecap="round" />}
        <line x1={mx} y1={my} x2={mx2} y2={my2} stroke="var(--text)" strokeWidth="2" />
        <text className="an-fade" x={cx} y={cy - 12} textAnchor="middle" style={{ fontSize: 20, fontWeight: 700, fill: 'var(--text)', fontFamily: 'var(--font-display)', '--d': '300ms' }}>{valor ?? anFmtPct(pct)}</text>
      </svg>
      <div style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--text)', marginTop: 2 }}>{label}</div>
      {sub && <div style={{ fontSize: 10.5, color: 'var(--text-muted)', marginTop: 1 }}>{sub}</div>}
      <AnTipBox tip={t.tip} />
    </div>
  );
};

// Linha com área em gradiente, desenhada à entrada; em hover mostra cursor vertical e ponto activo.
const AnCurva = ({ ys, W, H, pad, cor, Y, step, activo, id, espessura = 2, glow = true, area = true }) => {
  const pts = ys.map((v, i) => [pad + i * step, Y(v)]);
  const d = pts.map((p, i) => `${i ? 'L' : 'M'}${p[0]},${p[1]}`).join(' ');
  const dArea = `${d} L${pts[pts.length - 1][0]},${H - pad} L${pts[0][0]},${H - pad} Z`;
  return (
    <>
      <defs>
        <linearGradient id={`${id}-a`} x1="0" x2="0" y1="0" y2="1">
          <stop offset="0%" stopColor={cor} stopOpacity="0.22" />
          <stop offset="100%" stopColor={cor} stopOpacity="0" />
        </linearGradient>
        {glow && (
          <filter id={`${id}-g`} x="-20%" y="-40%" width="140%" height="180%">
            <feGaussianBlur stdDeviation="3" result="blur" />
            <feComposite in="SourceGraphic" in2="blur" operator="over" />
          </filter>
        )}
      </defs>
      {area && <path className="an-fade" style={{ '--d': '350ms' }} d={dArea} fill={`url(#${id}-a)`} />}
      {activo != null && pts[activo] && <line x1={pts[activo][0]} x2={pts[activo][0]} y1={pad / 2} y2={H - pad} stroke="var(--border-strong, #bec9de)" strokeWidth="1" />}
      <path className="an-draw" pathLength="1" d={d} fill="none" stroke={cor} strokeWidth={espessura} strokeLinejoin="round" strokeLinecap="round" filter={glow ? `url(#${id}-g)` : undefined} />
      {activo != null && pts[activo]
        ? <circle key={`a${activo}`} className="an-pop-fast" cx={pts[activo][0]} cy={pts[activo][1]} r="4" fill={cor} stroke="var(--bg-elev, #fff)" strokeWidth="2" />
        : <circle className="an-pop" style={{ '--i': 20 }} cx={pts[pts.length - 1][0]} cy={pts[pts.length - 1][1]} r="2.8" fill={cor} />}
    </>
  );
};

const AnSpark = ({ valores, labels, cor = AN_REAL, w = 120, h = 28, fmt = anFmtN }) => {
  const t = useAnTip();
  const id = useAnId('ansp');
  const vals = (valores || []).map(v => (v == null ? null : Number(v)));
  const validos = vals.filter(v => v != null && Number.isFinite(v));
  if (validos.length < 2) return <span style={{ fontSize: 10.5, color: 'var(--text-dim)' }}>sem histórico</span>;
  const ys = vals.map(v => (v == null || !Number.isFinite(v) ? validos[0] : v));
  const min = Math.min(...validos); const max = Math.max(...validos);
  const pad = 3;
  const Y = (v) => (max === min ? h / 2 : h - pad - ((v - min) / (max - min)) * (h - pad * 2));
  const step = (w - pad * 2) / (ys.length - 1);
  const onMove = (e) => {
    const r = t.ref.current.getBoundingClientRect();
    const i = Math.max(0, Math.min(ys.length - 1, Math.round(((e.clientX - r.left) / r.width * w - pad) / step)));
    t.show(e, <><AnTipTitulo>{labels?.[i] || `#${i + 1}`}</AnTipTitulo><AnTipLinha cor={cor} label="Valor" valor={fmt(vals[i])} /></>, i);
  };
  return (
    <span ref={t.ref} style={{ position: 'relative', display: 'inline-block', width: w, height: h }} onMouseMove={onMove} onMouseLeave={t.hide}>
      <svg viewBox={`0 0 ${w} ${h}`} width={w} height={h} style={{ display: 'block', overflow: 'visible' }}>
        <AnCurva ys={ys} W={w} H={h} pad={pad} cor={cor} Y={Y} step={step} activo={t.activo} id={id} espessura={1.6} glow={false} />
      </svg>
      <AnTipBox tip={t.tip} />
    </span>
  );
};

// Barras mensais BP contra real, com acumulados e o ritmo projectado até Dezembro.
const AnMesBars = ({ bp, real, mes, altura = 210, fmt = anFmtEur, nomes }) => {
  const t = useAnTip();
  const id = useAnId('anmb');
  const b = (bp || []).map(anNum); const rr = (real || []).map(anNum);
  if (!b.some(Boolean) && !rr.some(Boolean)) return <AnVazio altura={altura} />;
  const m = Math.max(1, Math.min(12, Number(mes) || 12));
  const W = 640; const H = altura; const padL = 44; const padR = 44; const padT = 12; const padB = 24;
  const cw = (W - padL - padR) / 12;
  const maxBar = Math.max(1, ...b, ...rr.slice(0, m)) * 1.08;
  const acBp = []; const acReal = [];
  b.reduce((s, v, i) => (acBp[i] = s + v), 0);
  rr.reduce((s, v, i) => (acReal[i] = i < m ? s + v : s), 0);
  const ritmo = m > 0 ? acReal[m - 1] / m : 0;
  const proj = Array.from({ length: 12 }, (_, i) => (i >= m - 1 ? acReal[m - 1] + ritmo * (i - (m - 1)) : null));
  const maxAc = Math.max(1, acBp[11], proj[11] || 0) * 1.08;
  const yBar = (v) => padT + (H - padT - padB) * (1 - v / maxBar);
  const yAc = (v) => padT + (H - padT - padB) * (1 - v / maxAc);
  const xC = (i) => padL + cw * i + cw / 2;
  const linha = (arr, n = 12) => arr.slice(0, n).map((v, i) => (v == null ? null : `${xC(i)},${yAc(v)}`)).filter(Boolean).join(' ');
  const hi = t.activo;
  const onMove = (e) => {
    const r = t.ref.current.getBoundingClientRect();
    const x = (e.clientX - r.left) / r.width * W;
    const i = Math.max(0, Math.min(11, Math.floor((x - padL) / cw)));
    const pct = b[i] > 0 && i < m ? Math.round(rr[i] / b[i] * 100) : null;
    const labA = nomes?.a || 'BP';
    const labB = nomes?.b || 'Real';
    t.show(e, <>
      <AnTipTitulo>{AN_MESES[i]}{i === m - 1 ? ' · mês em curso' : ''}</AnTipTitulo>
      <AnTipLinha cor={AN_BP} label={labA} valor={fmt(b[i])} />
      {i < m && <AnTipLinha cor={AN_REAL} label={labB} valor={`${fmt(rr[i])}${pct != null ? ` · ${pct} %` : ''}`} />}
      <AnTipLinha cor={AN_BP} tracejado label={nomes ? `${labA} acumulado` : 'BP acumulado'} valor={fmt(acBp[i])} />
      {i < m
        ? <AnTipLinha cor={AN_REAL} contorno label={nomes ? `${labB} acumulado` : 'Real acumulado'} valor={fmt(acReal[i])} />
        : proj[i] != null && <AnTipLinha cor={AN_REAL} tracejado label="Ao ritmo actual" valor={fmt(proj[i])} />}
    </>, i);
  };
  const barra = (x, v, grad, cor, i, extra) => {
    const y = yBar(v); const hh = Math.max(0, H - padB - y);
    if (hh <= 0) return null;
    return (
      <g className="an-bar-y" style={{ '--i': i + (extra || 0) }}>
        <rect x={x} y={y} width={cw * 0.34} height={hh} fill={`url(#${grad})`} rx="2" />
        <rect x={x} y={y} width={cw * 0.34} height={Math.min(2, hh)} fill={cor} rx="1" />
      </g>
    );
  };
  const acAct = hi == null ? null : hi < m ? acReal[hi] : proj[hi];
  return (
    <div ref={t.ref} style={{ position: 'relative' }} onMouseMove={onMove} onMouseLeave={t.hide}>
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: 'block', overflow: 'visible' }}>
        <defs>
          <linearGradient id={`${id}-r`} x1="0" x2="0" y1="0" y2="1">
            <stop offset="0%" stopColor={AN_REAL} stopOpacity="0.92" />
            <stop offset="100%" stopColor={AN_REAL} stopOpacity="0.28" />
          </linearGradient>
          <linearGradient id={`${id}-b`} x1="0" x2="0" y1="0" y2="1">
            <stop offset="0%" stopColor={AN_BP} stopOpacity="0.55" />
            <stop offset="100%" stopColor={AN_BP} stopOpacity="0.1" />
          </linearGradient>
          <filter id={`${id}-g`} x="-10%" y="-30%" width="120%" height="160%">
            <feGaussianBlur stdDeviation="3" result="blur" />
            <feComposite in="SourceGraphic" in2="blur" operator="over" />
          </filter>
        </defs>
        <rect x="0" y={padT - 4} width={cw} height={H - padB - padT + 4} rx="5" fill="var(--bg-sunken, #e8edf5)"
          style={{ transform: `translateX(${padL + cw * (hi ?? 0)}px)`, opacity: hi == null ? 0 : 0.75, transition: `transform 220ms ${AN_EASE}, opacity 150ms ease` }} />
        {[0, 0.5, 1].map(f => (
          <g key={f}>
            <line x1={padL} x2={W - padR} y1={yBar(maxBar * f)} y2={yBar(maxBar * f)} stroke="var(--border)" strokeDasharray={f ? '2 3' : ''} />
            <text x={padL - 6} y={yBar(maxBar * f) + 3} textAnchor="end" style={{ fontSize: 9, fill: 'var(--text-dim)' }}>{fmt(maxBar * f)}</text>
            <text x={W - padR + 6} y={yAc(maxAc * f) + 3} style={{ fontSize: 9, fill: 'var(--text-dim)' }}>{fmt(maxAc * f)}</text>
          </g>
        ))}
        {b.map((v, i) => (
          <g key={i} className="an-dim" style={{ opacity: hi != null && hi !== i ? 0.42 : 1 }}>
            {barra(padL + cw * i + cw * 0.14, v, `${id}-b`, AN_BP, i)}
            {i < m && <g style={{ opacity: i === m - 1 ? 0.65 : 1 }}>{barra(padL + cw * i + cw * 0.5, rr[i], `${id}-r`, AN_REAL, i, 1)}</g>}
            <text x={xC(i)} y={H - 8} textAnchor="middle" style={{ fontSize: 9.5, fill: i === hi || i === m - 1 ? 'var(--text)' : 'var(--text-dim)', fontWeight: i === hi || i === m - 1 ? 700 : 400, transition: 'fill .15s' }}>{AN_MESES[i]}</text>
          </g>
        ))}
        <polyline className="an-fade" style={{ '--d': '450ms' }} points={linha(acBp)} fill="none" stroke={AN_BP} strokeWidth="1.6" strokeDasharray="5 4" />
        <polyline className="an-draw" pathLength="1" style={{ '--d': '350ms' }} points={linha(acReal, m)} fill="none" stroke={AN_REAL} strokeWidth="2" strokeLinejoin="round" filter={`url(#${id}-g)`} />
        <polyline className="an-fade" style={{ '--d': '1300ms' }} points={linha(proj)} fill="none" stroke={AN_REAL} strokeWidth="1.6" strokeDasharray="2 3" />
        <circle className="an-ping" cx={xC(m - 1)} cy={yAc(acReal[m - 1])} r="4" fill={AN_REAL} />
        <circle className="an-pop" style={{ '--i': 30 }} cx={xC(m - 1)} cy={yAc(acReal[m - 1])} r="3.5" fill={AN_REAL} />
        {hi != null && (
          <g key={`act${hi}`}>
            <circle className="an-pop-fast" cx={xC(hi)} cy={yAc(acBp[hi])} r="4" fill={AN_BP} stroke="var(--bg-elev, #fff)" strokeWidth="2" />
            {acAct != null && <circle className="an-pop-fast" cx={xC(hi)} cy={yAc(acAct)} r="4.5" fill={AN_REAL} stroke="var(--bg-elev, #fff)" strokeWidth="2" />}
          </g>
        )}
      </svg>
      <div style={{ display: 'flex', gap: 14, flexWrap: 'wrap', fontSize: 10.5, color: 'var(--text-muted)', marginTop: 4 }}>
        <span><span style={{ display: 'inline-block', width: 9, height: 9, background: AN_BP, opacity: 0.45, borderRadius: 2, marginRight: 4 }} />{nomes ? `${nomes.a} mensal` : 'BP mensal'}</span>
        <span><span style={{ display: 'inline-block', width: 9, height: 9, background: AN_REAL, borderRadius: 2, marginRight: 4 }} />{nomes ? `${nomes.b} mensal` : 'Real mensal'}</span>
        <span>- - {nomes ? `${nomes.a} acumulado` : 'BP acumulado'}</span>
        <span style={{ color: AN_REAL }}>— {nomes ? `${nomes.b} acumulado` : 'real acumulado'} · ··· ritmo actual</span>
      </div>
      <AnTipBox tip={t.tip} />
    </div>
  );
};

// Barra real, traço do BP à data e fim do BP anual.
const AnBullet = ({ real, bpData, bpAno, label, fmt = anFmtEur, altura = 14, sub, nomes, escala, onClick }) => {
  const t = useAnTip();
  const r = anNum(real); const d = anNum(bpData); const a = anNum(bpAno);
  const max = Math.max(1, a, r, d) * 1.04;
  const pct = d > 0 ? r / d * 100 : null;
  const luzCusto = pct == null ? null : pct <= 100 ? 'azul' : pct <= 110 ? 'amarelo' : 'vermelho';
  const luz = escala === 'custo' ? luzCusto : anLuzPct(pct);
  const cor = luz === 'azul' ? AN_REAL : (AN_LUZ[luz] || AN_REAL);
  const lab = { real: 'Real', marca: 'BP à data', fim: 'BP do ano', nota: '% do BP à data', ...nomes };
  return (
    <div ref={t.ref} className="an-host" style={{ position: 'relative', minWidth: 0, cursor: onClick ? 'pointer' : 'default' }}
      onClick={onClick}
      onMouseMove={e => t.show(e, <>
        <AnTipTitulo>{label || 'BP'}</AnTipTitulo>
        <AnTipLinha cor={cor} label={lab.real} valor={fmt(r)} />
        <AnTipLinha cor="var(--text)" contorno label={lab.marca} valor={fmt(d)} />
        <AnTipLinha cor={AN_BP} label={lab.fim} valor={fmt(a)} />
        {pct != null && <AnTipNota cor={cor}>{anFmtN(pct, 1)} {lab.nota}</AnTipNota>}
      </>)}
      onMouseLeave={t.hide}>
      {label && <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, marginBottom: 3, gap: 6 }}><span style={{ color: 'var(--text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{label}</span><span style={{ color: cor, fontWeight: 600, flexShrink: 0 }}>{pct != null ? `${anFmtN(pct, 0)} %` : fmt(r)}</span></div>}
      <div style={{ position: 'relative', height: altura, background: 'var(--bg-sunken, #e8edf5)', borderRadius: 4, overflow: 'visible' }}>
        <div className="an-bar-x an-hbar" style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: `${r / max * 100}%`, borderRadius: '4px 2px 2px 4px', ...anBarraH(cor) }} />
        {d > 0 && <div className="an-fade" style={{ '--d': '500ms', position: 'absolute', left: `${d / max * 100}%`, top: -3, bottom: -3, width: 2, background: 'var(--text)', borderRadius: 1 }} />}
        {a > 0 && <div style={{ position: 'absolute', left: `calc(${a / max * 100}% - 1px)`, top: 0, bottom: 0, width: 2, background: AN_BP }} />}
      </div>
      {sub && <div style={{ fontSize: 10.5, color: 'var(--text-muted)', marginTop: 3 }}>{sub}</div>}
      <AnTipBox tip={t.tip} />
    </div>
  );
};

// Real contra necessário por etapa, do lead ao €.
const AnFunilInvertido = ({ etapas }) => {
  const t = useAnTip();
  const lista = (etapas || []).filter(e => e.necessario_mes != null || e.real_mes != null);
  if (!lista.length) return <AnVazio altura={140} />;
  const hi = t.activo;
  return (
    <div ref={t.ref} style={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: 3 }}>
      {lista.map((e, k) => {
        const eur = e.key === 'eur';
        const fmt = eur ? anFmtEur : (v) => anFmtN(v, 1);
        const real = anNum(e.real_mes); const nec = anNum(e.necessario_mes);
        const max = Math.max(1, real, nec);
        const defice = nec - real;
        return (
          <div key={e.key} className="an-row an-host an-dim"
            onMouseMove={ev => t.show(ev, <>
              <AnTipTitulo>{e.label} por mês</AnTipTitulo>
              <AnTipLinha cor={AN_REAL} label={e.base ? `Real (${e.base})` : 'Real'} valor={fmt(real)} />
              {e.media_ano_mes != null && <AnTipLinha cor={AN_REAL} contorno label="Média do ano" valor={fmt(e.media_ano_mes)} />}
              <AnTipLinha cor={AN_BP} contorno label="Necessário" valor={fmt(nec)} />
              <AnTipNota cor={defice > 0 ? AN_LUZ.vermelho : AN_LUZ.verde}>{defice > 0 ? `Défice de ${fmt(defice)} por mês` : 'Coberto'}</AnTipNota>
            </>, k)}
            onMouseLeave={t.hide}
            style={{ display: 'grid', gridTemplateColumns: '84px 1fr 86px', alignItems: 'center', gap: 8, padding: '3px 6px', margin: '0 -6px', opacity: hi != null && hi !== k ? 0.55 : 1 }}>
            <div style={{ fontSize: 11.5, fontWeight: 600, color: 'var(--text)' }}>{e.label}</div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
              <div className="an-bar-x an-hbar" style={{ '--i': k, height: 9, borderRadius: '3px 2px 2px 3px', width: `${Math.max(1, real / max * 100)}%`, ...anBarraH(AN_REAL) }} />
              <div className="an-bar-x" style={{ '--i': k + 2, height: 9, borderRadius: 3, border: `1.5px solid ${AN_BP}`, width: `${Math.max(1, nec / max * 100)}%`, boxSizing: 'border-box' }} />
            </div>
            <div style={{ fontSize: 11, textAlign: 'right', color: defice > 0 ? AN_LUZ.vermelho : AN_LUZ.verde, fontWeight: 600, fontVariantNumeric: 'tabular-nums' }}>
              {defice > 0 ? `−${fmt(defice)}` : '✓'}
            </div>
          </div>
        );
      })}
      <div style={{ display: 'flex', gap: 14, fontSize: 10.5, color: 'var(--text-muted)', marginTop: 4 }}>
        <span><span style={{ display: 'inline-block', width: 10, height: 8, background: AN_REAL, borderRadius: 2, marginRight: 4 }} />real por mês{lista.some(e => e.base) ? ' (leads: últimos 3 meses; restantes: média do ano)' : ''}</span>
        <span><span style={{ display: 'inline-block', width: 10, height: 8, border: `1.5px solid ${AN_BP}`, borderRadius: 2, marginRight: 4, boxSizing: 'border-box' }} />necessário por mês</span>
      </div>
      <AnTipBox tip={t.tip} />
    </div>
  );
};

// Equipa × mês com a % do BP. Em hover, a linha e a coluna da célula ficam em destaque.
const AnHeatmap = ({ linhas, onPick, nomes, larguraRotulo = 120 }) => {
  const t = useAnTip();
  const rows = (linhas || []).filter(l => (l.meses || []).length);
  if (!rows.length) return <AnVazio altura={120} />;
  const nMeses = Math.max(...rows.map(l => l.meses.length));
  const [hr, hc] = t.activo ? t.activo.split(':').map(Number) : [null, null];
  const labPlano = nomes?.plano || 'BP';
  const labReal = nomes?.real || 'Real';
  return (
    <div ref={t.ref} style={{ position: 'relative' }} onMouseLeave={t.hide}>
      <div style={{ overflowX: 'auto', padding: 4, margin: -4 }}>
      <div style={{ display: 'grid', gridTemplateColumns: `${larguraRotulo}px repeat(${nMeses}, minmax(38px, 1fr))`, gap: 3, minWidth: larguraRotulo + nMeses * 40 }}>
        <div />
        {AN_MESES.slice(0, nMeses).map((m, i) => <div key={m} style={{ fontSize: 9.5, color: i === hc ? 'var(--text)' : 'var(--text-dim)', fontWeight: i === hc ? 700 : 400, textAlign: 'center', fontFamily: 'var(--font-mono)', transition: 'color .15s' }}>{m}</div>)}
        {rows.map((l, ri) => (
          <React.Fragment key={l.equipa}>
            <button type="button" className="an-link" onClick={() => onPick && onPick(l.equipa)} style={{ fontSize: 11, fontWeight: 600, textAlign: 'left', background: 'none', border: 'none', padding: '0 4px 0 0', color: ri === hr ? 'var(--ai-500, #3859D0)' : 'var(--text)', cursor: onPick ? 'pointer' : 'default', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontFamily: 'inherit' }}>{l.label}</button>
            {Array.from({ length: nMeses }, (_, i) => {
              const c = l.meses[i];
              const p = c?.pct;
              const luz = c?.cor ? null : anLuzPct(p);
              const corCel = c?.cor || (luz ? AN_LUZ[luz] : null);
              const foco = hr == null || ri === hr || i === hc;
              const nota = c?.nota || (p == null ? (nomes?.vazio || 'sem BP neste mês') : `${anFmtN(p, 1)} % ${nomes?.pct || 'do BP'}`);
              return (
                <div key={i} className="an-cell an-cell-in"
                  onClick={() => onPick && onPick(l.equipa)}
                  onMouseMove={e => c && t.show(e, <>
                    <AnTipTitulo>{l.label} · {AN_MESES[i]}</AnTipTitulo>
                    <AnTipLinha cor={AN_BP} label={labPlano} valor={anFmtEur(c.bp)} />
                    <AnTipLinha cor={corCel || AN_REAL} label={labReal} valor={anFmtEur(c.real)} />
                    <AnTipNota cor={corCel || (luz ? AN_LUZ[luz] : undefined)}>{nota}</AnTipNota>
                  </>, `${ri}:${i}`)}
                  style={{
                    '--i': ri * nMeses + i,
                    height: 26, borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: onPick ? 'pointer' : 'default',
                    fontSize: 10, fontWeight: 600, color: corCel ? '#fff' : 'var(--text-dim)', fontVariantNumeric: 'tabular-nums',
                    background: corCel ? `color-mix(in oklch, ${corCel} ${c?.cor ? 82 : (luz === 'verde' ? 78 : 88)}%, transparent)` : 'var(--bg-sunken, #e8edf5)',
                    opacity: foco ? 1 : 0.45,
                  }}>{c?.texto != null ? c.texto : (p == null ? '—' : Math.round(p))}</div>
              );
            })}
          </React.Fragment>
        ))}
      </div>
      </div>
      <AnTipBox tip={t.tip} />
    </div>
  );
};

// 4 mini barras com a linha do objectivo.
const AnMiniKpis = ({ itens }) => {
  const t = useAnTip();
  return (
    <div ref={t.ref} style={{ position: 'relative', display: 'grid', gridTemplateColumns: `repeat(${(itens || []).length || 1}, 1fr)`, gap: 8 }}>
      {(itens || []).map((k, i) => {
        const v = k.valor == null ? null : Number(k.valor);
        const max = Math.max(anNum(k.max), anNum(k.obj) * 1.5, v || 0, 0.0001);
        const ok = v != null && (k.inverso ? v <= k.obj : v >= k.obj);
        const amarelo = v != null && k.amarelo != null && (k.inverso ? v <= k.amarelo : v >= k.amarelo);
        const cor = v == null ? AN_BP : ok ? AN_LUZ.verde : amarelo ? AN_LUZ.amarelo : AN_LUZ.vermelho;
        const fmtK = (x) => (k.fmt ? k.fmt(x) : anFmtN(x, 1));
        return (
          <div key={k.label} className="an-host an-row" style={{ padding: '3px 5px', margin: '-3px -5px' }}
            onMouseMove={e => t.show(e, <><AnTipTitulo>{k.label}</AnTipTitulo><AnTipLinha cor={cor} label="Actual" valor={v == null ? '—' : fmtK(v)} /><AnTipLinha cor="var(--text)" contorno label="Objectivo" valor={fmtK(k.obj)} /></>)} onMouseLeave={t.hide}>
            <div style={{ fontSize: 9.5, color: 'var(--text-dim)', textTransform: 'uppercase', letterSpacing: '.04em', fontFamily: 'var(--font-mono)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{k.label}</div>
            <div style={{ fontSize: 12.5, fontWeight: 650, color: cor, margin: '2px 0 3px', fontVariantNumeric: 'tabular-nums' }}>{v == null ? '—' : fmtK(v)}</div>
            <div style={{ position: 'relative', height: 6, background: 'var(--bg-sunken, #e8edf5)', borderRadius: 3 }}>
              <div className="an-bar-x an-hbar" style={{ '--i': i, position: 'absolute', left: 0, top: 0, bottom: 0, width: `${Math.min(100, (v || 0) / max * 100)}%`, borderRadius: '3px 1px 1px 3px', ...anBarraH(cor) }} />
              <div style={{ position: 'absolute', left: `${Math.min(100, k.obj / max * 100)}%`, top: -2, bottom: -2, width: 2, background: 'var(--text)' }} />
            </div>
          </div>
        );
      })}
      <AnTipBox tip={t.tip} />
    </div>
  );
};

const AnEmpilhada = ({ partes, altura = 12, legenda = true, fmt = anFmtN }) => {
  const t = useAnTip();
  const ps = (partes || []).filter(p => anNum(p.valor) > 0);
  const total = ps.reduce((s, p) => s + anNum(p.valor), 0);
  if (!total) return <span style={{ fontSize: 11, color: 'var(--text-dim)' }}>sem dados</span>;
  const hi = t.activo;
  return (
    <div ref={t.ref} style={{ position: 'relative' }} onMouseLeave={t.hide}>
      <div className="an-bar-x" style={{ display: 'flex', gap: 2, height: altura, borderRadius: 4, overflow: 'hidden' }}>
        {ps.map((p, i) => (
          <div key={p.label} className="an-dim" style={{ width: `${anNum(p.valor) / total * 100}%`, background: p.cor, opacity: hi != null && hi !== i ? 0.35 : 1, filter: hi === i ? 'brightness(1.08) saturate(1.15)' : 'none' }}
            onMouseMove={e => t.show(e, <><AnTipTitulo>{p.label}</AnTipTitulo><AnTipLinha cor={p.cor} label="Valor" valor={fmt(p.valor)} /><AnTipNota>{anFmtN(anNum(p.valor) / total * 100, 1)} % do total</AnTipNota></>, i)} />
        ))}
      </div>
      {legenda && (
        <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', fontSize: 10.5, color: 'var(--text-muted)', marginTop: 4 }}>
          {ps.map((p, i) => <span key={p.label} className="an-dim" style={{ opacity: hi != null && hi !== i ? 0.45 : 1 }}><span style={{ display: 'inline-block', width: 8, height: 8, borderRadius: 2, background: p.cor, marginRight: 4 }} />{p.label} {fmt(p.valor)}</span>)}
        </div>
      )}
      <AnTipBox tip={t.tip} />
    </div>
  );
};

// Barras para a esquerda (queda) e para a direita (subida) a partir do zero.
const AnDivergente = ({ itens, fmt = anFmtEur, modo }) => {
  const t = useAnTip();
  const lista = (itens || []).filter(i => anNum(i.valor) !== 0);
  if (!lista.length) return <AnVazio altura={100} />;
  const max = Math.max(1, ...lista.map(i => Math.abs(anNum(i.valor))));
  const hi = t.activo;
  return (
    <div ref={t.ref} style={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: 1 }}>
      {lista.map((i, k) => {
        const v = anNum(i.valor); const w = Math.abs(v) / max * 50;
        const custo = modo === 'custo';
        const cor = v < 0 ? (custo ? AN_REAL : AN_LUZ.vermelho) : (custo ? AN_LUZ.vermelho : AN_LUZ.verde);
        const etiqueta = v < 0 ? (custo ? 'Abaixo do ritmo' : 'Queda') : (custo ? 'Acima do ritmo' : 'Subida');
        const face = custo ? 'face ao ritmo' : 'face ao ano anterior';
        return (
          <div key={k} className="an-row an-host an-dim" onClick={i.onClick} style={{ display: 'grid', gridTemplateColumns: 'minmax(90px, 32%) 1fr', alignItems: 'center', gap: 8, padding: '3px 6px', margin: '0 -6px', opacity: hi != null && hi !== k ? 0.55 : 1, cursor: i.onClick ? 'pointer' : 'default' }}
            onMouseMove={e => t.show(e, <><AnTipTitulo>{i.label}</AnTipTitulo>{i.sub && <AnTipNota>{i.sub}</AnTipNota>}<AnTipLinha cor={cor} label={etiqueta} valor={`${v > 0 ? '+' : ''}${fmt(v)}`} />{i.pct != null && <AnTipNota cor={cor}>{v > 0 ? '+' : ''}{anFmtN(i.pct, 1)} % {face}</AnTipNota>}</>, k)} onMouseLeave={t.hide}>
            <div style={{ fontSize: 11, color: 'var(--text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{i.label}</div>
            <div style={{ position: 'relative', height: 12 }}>
              <div style={{ position: 'absolute', left: '50%', top: -3, bottom: -3, width: 1, background: 'var(--border-strong, #bec9de)' }} />
              <div className={`${v < 0 ? 'an-bar-xr' : 'an-bar-x'} an-hbar`} style={{ '--i': k, position: 'absolute', top: 1, bottom: 1, left: v < 0 ? `${50 - w}%` : '50%', width: `${w}%`, borderRadius: v < 0 ? '3px 1px 1px 3px' : '1px 3px 3px 1px', ...anBarraH(cor, { dir: v < 0 ? 'left' : 'right' }) }} />
            </div>
          </div>
        );
      })}
      <AnTipBox tip={t.tip} />
    </div>
  );
};

// Comerciais: % do BP (x) contra leads ÷ necessárias (y), bolha pelo gap.
const AnDispersao = ({ pontos, onPick, altura = 320 }) => {
  const t = useAnTip();
  const ps = (pontos || []).filter(p => p.x != null);
  if (!ps.length) return <AnVazio altura={altura} />;
  const W = 640; const H = altura; const pad = 34;
  const xMax = 150; const yMax = 2;
  const X = (v) => pad + (Math.max(0, Math.min(xMax, v)) / xMax) * (W - pad * 2);
  const Y = (v) => H - pad - (Math.max(0, Math.min(yMax, v)) / yMax) * (H - pad * 2);
  const gMax = Math.max(1, ...ps.map(p => Math.abs(anNum(p.r))));
  const R = (v) => 4 + Math.sqrt(Math.abs(anNum(v)) / gMax) * 16;
  const quad = [
    { x: pad + 6, y: H - pad - 8, t: 'Abaixo do BP e sem leads · captação', a: 'start' },
    { x: pad + 6, y: pad + 12, t: 'Abaixo do BP com leads · conversão', a: 'start' },
    { x: W - pad - 6, y: H - pad - 8, t: 'Acima do BP sem leads', a: 'end' },
    { x: W - pad - 6, y: pad + 12, t: 'Acima do BP com leads', a: 'end' },
  ];
  const hi = t.activo;
  const pa = hi != null ? ps.find(p => p.id === hi) : null;
  return (
    <div ref={t.ref} style={{ position: 'relative' }}>
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: 'block', overflow: 'visible' }}>
        <rect className="an-fade" x={pad} y={Y(1)} width={X(100) - pad} height={H - pad - Y(1)} fill="color-mix(in oklch, var(--danger, #dc2626) 6%, transparent)" />
        <line x1={X(100)} x2={X(100)} y1={pad} y2={H - pad} stroke="var(--border-strong, #bec9de)" strokeDasharray="4 3" />
        <line x1={pad} x2={W - pad} y1={Y(1)} y2={Y(1)} stroke="var(--border-strong, #bec9de)" strokeDasharray="4 3" />
        <line x1={pad} x2={W - pad} y1={H - pad} y2={H - pad} stroke="var(--border)" />
        <line x1={pad} x2={pad} y1={pad} y2={H - pad} stroke="var(--border)" />
        {[0, 50, 100, 150].map(v => <text key={v} x={X(v)} y={H - pad + 13} textAnchor="middle" style={{ fontSize: 9, fill: 'var(--text-dim)' }}>{v} %</text>)}
        {[0, 1, 2].map(v => <text key={v} x={pad - 5} y={Y(v) + 3} textAnchor="end" style={{ fontSize: 9, fill: 'var(--text-dim)' }}>{v}×</text>)}
        <text x={W / 2} y={H - 4} textAnchor="middle" style={{ fontSize: 9.5, fill: 'var(--text-muted)' }}>% do BP à data</text>
        <text x={10} y={H / 2} textAnchor="middle" transform={`rotate(-90 10 ${H / 2})`} style={{ fontSize: 9.5, fill: 'var(--text-muted)' }}>leads ÷ necessárias</text>
        {quad.map(q => <text key={q.t} x={q.x} y={q.y} textAnchor={q.a} style={{ fontSize: 9.5, fill: 'var(--text-dim)', fontWeight: 600 }}>{q.t}</text>)}
        {pa && (
          <g key={`guia${pa.id}`} className="an-fade" style={{ animationDuration: '.2s' }}>
            <line x1={X(pa.x)} x2={X(pa.x)} y1={Y(pa.y)} y2={H - pad} stroke="var(--text-muted)" strokeDasharray="3 3" />
            <line x1={pad} x2={X(pa.x)} y1={Y(pa.y)} y2={Y(pa.y)} stroke="var(--text-muted)" strokeDasharray="3 3" />
          </g>
        )}
        {[...ps].sort((a, b) => Math.abs(anNum(b.r)) - Math.abs(anNum(a.r))).map((p, i) => {
          const cor = AN_LUZ[anLuzPct(p.x)] || AN_REAL;
          const act = hi === p.id;
          return (
            <g key={p.id} className="an-pop" style={{ '--i': i }}>
              <circle className="an-bolha" cx={X(p.x)} cy={Y(p.y)} r={R(p.r)} fill={cor} fillOpacity={act ? 0.62 : 0.35} stroke={act ? 'var(--text)' : cor} strokeWidth={act ? 2.2 : 1.4}
                style={{ cursor: onPick ? 'pointer' : 'default', opacity: hi != null && !act ? 0.35 : 1 }}
                onClick={() => onPick && onPick(p.id)}
                onMouseMove={e => t.show(e, <>
                  <AnTipTitulo>{p.label}</AnTipTitulo>{p.sub && <AnTipNota>{p.sub}</AnTipNota>}
                  <AnTipLinha cor={cor} label="% do BP" valor={`${anFmtN(p.x, 1)} %`} />
                  <AnTipLinha cor={AN_REAL} contorno label="Leads ÷ necessárias" valor={p.yTxt || anFmtN(p.y, 2)} />
                  <AnTipLinha cor={AN_BP} label="Gap projectado" valor={anFmtEur(p.r)} />
                </>, p.id)}
                onMouseLeave={t.hide} />
            </g>
          );
        })}
      </svg>
      <AnTipBox tip={t.tip} />
    </div>
  );
};

// Entradas → em qualificação → classificadas → HOT e QUALIFICADO → handoff.
const AnFunilSdr = ({ etapas }) => {
  const t = useAnTip();
  const lista = etapas || [];
  const max = Math.max(1, ...lista.map(e => anNum(e.n)));
  if (!lista.some(e => anNum(e.n) > 0)) return <AnVazio altura={120} texto="sem entradas nos últimos 30 dias" />;
  const hi = t.activo;
  return (
    <div ref={t.ref} style={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: 2, alignItems: 'center' }}>
      {lista.map((e, i) => {
        const prev = i ? anNum(lista[i - 1].n) : null;
        const taxa = prev ? Math.round(anNum(e.n) / prev * 100) : null;
        return (
          <div key={e.key} className="an-row an-host an-dim" style={{ width: '100%', display: 'grid', gridTemplateColumns: '1fr 44px', alignItems: 'center', gap: 6, padding: '2px 4px', opacity: hi != null && hi !== i ? 0.55 : 1 }}
            onMouseMove={ev => t.show(ev, <><AnTipTitulo>{e.label}</AnTipTitulo><AnTipLinha cor={AN_REAL} label="Leads" valor={anFmtN(e.n)} />{taxa != null && <AnTipNota>{taxa} % da etapa anterior</AnTipNota>}</>, i)} onMouseLeave={t.hide}>
            <div style={{ display: 'flex', justifyContent: 'center' }}>
              <div className="an-bar-xc an-hbar" style={{ '--i': i, width: `${Math.max(14, anNum(e.n) / max * 100)}%`, height: 22, borderRadius: 4, background: `color-mix(in oklch, var(--ai-500, #3859D0) ${90 - i * 13}%, transparent)`, color: '#fff', fontSize: 10.5, fontWeight: 600, display: 'flex', alignItems: 'center', justifyContent: 'center', whiteSpace: 'nowrap', overflow: 'hidden', padding: '0 6px' }}>
                {e.label} · {anFmtN(e.n)}
              </div>
            </div>
            <div style={{ fontSize: 10, color: 'var(--text-dim)', textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>{taxa != null ? `${taxa} %` : ''}</div>
          </div>
        );
      })}
      <AnTipBox tip={t.tip} />
    </div>
  );
};

// Quanto do gap cada campanha proposta cobre, com a parte já com briefing.
const AnImpacto = ({ itens, gap, fmt = anFmtEur, gapLabel = 'gap a cobrir' }) => {
  const t = useAnTip();
  const lista = (itens || []).filter(i => anNum(i.valor) > 0);
  if (!lista.length) return <AnVazio altura={90} texto="sem impacto estimado" />;
  const total = lista.reduce((s, i) => s + anNum(i.valor), 0);
  const coberto = lista.filter(i => i.coberto).reduce((s, i) => s + anNum(i.valor), 0);
  const max = Math.max(1, anNum(gap), total);
  const hi = t.activo;
  return (
    <div ref={t.ref} style={{ position: 'relative' }}>
      <div className="an-host" style={{ position: 'relative', height: 16, background: 'var(--bg-sunken)', borderRadius: 4, marginBottom: 4 }}
        onMouseMove={e => t.show(e, <><AnTipTitulo>Total das propostas</AnTipTitulo><AnTipLinha cor={AN_REAL} label="Propostas" valor={fmt(total)} /><AnTipLinha cor={AN_LUZ.verde} label="Com briefing" valor={fmt(coberto)} /><AnTipLinha cor="var(--text)" contorno label={gapLabel} valor={fmt(gap)} /><AnTipNota>{gap > 0 ? anFmtN(total / gap * 100, 0) : '—'} % do gap</AnTipNota></>)} onMouseLeave={t.hide}>
        <div className="an-bar-x an-hbar" style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: `${total / max * 100}%`, borderRadius: '4px 2px 2px 4px', ...anBarraH(AN_REAL, { forte: 45, fraco: 12 }) }} />
        {coberto > 0 && <div className="an-bar-x an-hbar" style={{ '--i': 3, position: 'absolute', left: 0, top: 0, bottom: 0, width: `${coberto / max * 100}%`, borderRadius: '4px 2px 2px 4px', ...anBarraH(AN_LUZ.verde) }} />}
        {gap > 0 && <div className="an-fade" style={{ '--d': '600ms', position: 'absolute', left: `calc(${anNum(gap) / max * 100}% - 1px)`, top: -4, bottom: -4, width: 2, background: 'var(--text)' }} />}
      </div>
      <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 10.5, color: 'var(--text-muted)', marginBottom: 8 }}>
        <span>Propostas {fmt(total)} · com briefing {fmt(coberto)}</span>
        <span>{gapLabel} {fmt(gap)}</span>
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
        {lista.map((i, k) => {
          const cor = i.coberto ? AN_LUZ.verde : AN_REAL;
          return (
            <div key={k} className="an-row an-host an-dim" style={{ display: 'grid', gridTemplateColumns: 'minmax(120px, 38%) 1fr 64px', gap: 8, alignItems: 'center', padding: '3px 6px', margin: '0 -6px', opacity: hi != null && hi !== k ? 0.55 : 1 }}
              onMouseMove={e => t.show(e, <><AnTipTitulo>{i.label}</AnTipTitulo><AnTipLinha cor={cor} label="Impacto estimado" valor={fmt(i.valor)} /><AnTipNota>{i.coberto ? 'Briefing criado' : 'Proposta'} · {gap > 0 ? anFmtN(anNum(i.valor) / gap * 100, 1) : '—'} % do gap</AnTipNota></>, k)} onMouseLeave={t.hide}>
              <div style={{ fontSize: 11, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{i.label}</div>
              <div style={{ height: 9, background: 'var(--bg-sunken)', borderRadius: 3, position: 'relative' }}>
                <div className="an-bar-x an-hbar" style={{ '--i': k, position: 'absolute', left: 0, top: 0, bottom: 0, width: `${anNum(i.valor) / max * 100}%`, borderRadius: '3px 1px 1px 3px', ...anBarraH(cor) }} />
              </div>
              <div style={{ fontSize: 10.5, textAlign: 'right', color: 'var(--text-muted)', fontVariantNumeric: 'tabular-nums' }}>{fmt(i.valor)}</div>
            </div>
          );
        })}
      </div>
      <AnTipBox tip={t.tip} />
    </div>
  );
};

const AnTomTimeline = ({ dias, onPick, activo }) => {
  const t = useAnTip();
  const lista = [...(dias || [])].sort((a, b) => String(a.dia).localeCompare(String(b.dia)));
  if (!lista.length) return <AnVazio altura={50} texto="sem análises no período" />;
  return (
    <div ref={t.ref} style={{ position: 'relative', display: 'flex', gap: 5, flexWrap: 'wrap', alignItems: 'center', padding: 3 }}>
      {lista.map((d, i) => (
        <button key={d.dia} type="button" className="an-dot an-pop" onClick={() => onPick && onPick(d.dia)}
          onMouseMove={e => t.show(e, <><AnTipTitulo>{d.dia}</AnTipTitulo><AnTipLinha cor={AN_LUZ[d.tom] || AN_BP} label="Tom" valor={d.tom || '—'} />{d.titulo && <AnTipNota>{d.titulo}</AnTipNota>}</>)}
          onMouseLeave={t.hide}
          style={{ '--i': i, width: 14, height: 14, borderRadius: '50%', padding: 0, cursor: onPick ? 'pointer' : 'default', background: AN_LUZ[d.tom] || AN_BP, border: activo === d.dia ? '2px solid var(--text)' : '2px solid transparent', outline: 'none' }} />
      ))}
      <AnTipBox tip={t.tip} />
    </div>
  );
};

const AnLinha = ({ pontos, cor = AN_REAL, altura = 90, fmt = anFmtN, alvo }) => {
  const t = useAnTip();
  const id = useAnId('anln');
  const ps = (pontos || []).filter(p => p.y != null && Number.isFinite(Number(p.y)));
  if (ps.length < 2) return <AnVazio altura={altura} texto="sem histórico suficiente" />;
  const W = 320; const H = altura; const pad = 8;
  const ys = ps.map(p => Number(p.y)).concat(alvo != null ? [alvo] : []);
  const min = Math.min(...ys); const max = Math.max(...ys);
  const Y = (v) => (max === min ? H / 2 : H - pad - (v - min) / (max - min) * (H - pad * 2));
  const step = (W - pad * 2) / (ps.length - 1);
  const onMove = (e) => {
    const r = t.ref.current.getBoundingClientRect();
    const i = Math.max(0, Math.min(ps.length - 1, Math.round(((e.clientX - r.left) / r.width * W - pad) / step)));
    t.show(e, <><AnTipTitulo>{ps[i].x}</AnTipTitulo><AnTipLinha cor={cor} label="Valor" valor={fmt(ps[i].y)} />{alvo != null && <AnTipLinha cor={AN_BP} tracejado label="Objectivo" valor={fmt(alvo)} />}</>, i);
  };
  return (
    <div ref={t.ref} style={{ position: 'relative' }} onMouseMove={onMove} onMouseLeave={t.hide}>
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: 'block', overflow: 'visible' }}>
        {alvo != null && <line x1={pad} x2={W - pad} y1={Y(alvo)} y2={Y(alvo)} stroke={AN_BP} strokeDasharray="4 3" />}
        <AnCurva ys={ps.map(p => Number(p.y))} W={W} H={H} pad={pad} cor={cor} Y={Y} step={step} activo={t.activo} id={id} />
      </svg>
      <AnTipBox tip={t.tip} />
    </div>
  );
};

const AnBarrasSemana = ({ semanas, altura = 120 }) => {
  const t = useAnTip();
  const lista = semanas || [];
  if (!lista.length) return <AnVazio altura={altura} texto="sem propostas no período" />;
  const max = Math.max(1, ...lista.map(s => Math.max(anNum(s.propostas), anNum(s.briefings), anNum(s.descartadas))));
  const series = [
    { k: 'propostas', label: 'Propostas', cor: AN_REAL },
    { k: 'briefings', label: 'Briefings criados', cor: AN_LUZ.verde },
    { k: 'descartadas', label: 'Descartadas', cor: AN_BP },
  ];
  const hi = t.activo;
  const larg = lista.length < 8 ? 16 : 9;
  return (
    <div ref={t.ref} style={{ position: 'relative' }}>
      <div style={{ display: 'flex', justifyContent: lista.length < 8 ? 'space-around' : 'flex-start', gap: 10, padding: '0 4px', overflowX: 'auto' }}>
        {lista.map((s, si) => (
          <div key={s.semana} className="an-row an-dim" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', flexShrink: 0, padding: '4px 6px 2px', opacity: hi != null && hi !== si ? 0.5 : 1 }}
            onMouseMove={e => t.show(e, <><AnTipTitulo>Semana de {String(s.semana).slice(0, 10)}</AnTipTitulo>{series.map(x => <AnTipLinha key={x.k} cor={x.cor} label={x.label} valor={anNum(s[x.k])} />)}</>, si)} onMouseLeave={t.hide}>
            <div style={{ display: 'flex', gap: 2, alignItems: 'flex-end', height: altura, borderBottom: '1px solid var(--border)', padding: '0 3px' }}>
              {series.map((x, xi) => {
                const v = anNum(s[x.k]);
                return (
                  <div key={x.k} className="an-bar-yh" style={{
                    '--i': si * 3 + xi, width: larg, height: `${v / max * 100}%`, minHeight: v ? 2 : 0, borderRadius: '2px 2px 0 0',
                    background: `linear-gradient(to bottom, color-mix(in oklch, ${x.cor} 92%, transparent), color-mix(in oklch, ${x.cor} 28%, transparent))`,
                    borderTop: v ? `2px solid ${x.cor}` : 'none', boxSizing: 'border-box',
                  }} />
                );
              })}
            </div>
            <div style={{ fontSize: 9.5, color: hi === si ? 'var(--text)' : 'var(--text-dim)', fontFamily: 'var(--font-mono)', marginTop: 3 }}>{String(s.semana).slice(8, 10)}/{String(s.semana).slice(5, 7)}</div>
          </div>
        ))}
      </div>
      <div style={{ display: 'flex', gap: 12, fontSize: 10.5, color: 'var(--text-muted)', marginTop: 5 }}>
        {series.map(x => <span key={x.k}><span style={{ display: 'inline-block', width: 8, height: 8, background: x.cor, borderRadius: 2, marginRight: 4 }} />{x.label}</span>)}
      </div>
      <AnTipBox tip={t.tip} />
    </div>
  );
};

Object.assign(window, {
  AnGauge, AnSpark, AnMesBars, AnBullet, AnFunilInvertido, AnHeatmap, AnMiniKpis, AnEmpilhada,
  AnDivergente, AnDispersao, AnFunilSdr, AnImpacto, AnTomTimeline, AnNota, AnLinha, AnBarrasSemana,
  AnCartao, AnVazio, AnTipBox, AnTipTitulo, AnTipLinha, AnTipNota,
  AN_LUZ, AN_MESES, anFmtEur, anFmtN, anFmtPct, anLuzPct, anBarraH, useAnTip,
});
