// Studio Uariní — Generator app
// Wires together: sidebar (template picker), form (per-template fields),
// preview (live render at scale), download (html-to-image), and AI captions (window.claude).

const { UARINI, SpiralPatch, Wordmark, LogoLockup } = window;
const TEMPLATES = window.UARINI_TEMPLATES;

// ─── Brand mark for sidebar ─────────────────────────────────────────────────
function BrandHeader() {
  return (
    <div className="brand">
      <div className="brand-mark">
        <SpiralPatch size={26} tone="green-on-dark" rotation={-6}/>
      </div>
      <div>
        <div className="name">Studio Uariní</div>
        <div className="meta">Gerador · v1</div>
      </div>
    </div>
  );
}

// ─── Sidebar ────────────────────────────────────────────────────────────────
function Sidebar({ activeId, onPick }) {
  const groups = {};
  TEMPLATES.forEach((t, i) => {
    (groups[t.group] = groups[t.group] || []).push({ ...t, idx: i });
  });
  return (
    <div>
      <BrandHeader/>
      {Object.entries(groups).map(([g, items]) => (
        <div key={g}>
          <div className="group">{g}</div>
          {items.map((t) => (
            <div
              key={t.id}
              className={'nav-item' + (t.id === activeId ? ' active' : '')}
              onClick={() => onPick(t.id)}
            >
              <span className="num">{String(t.idx + 1).padStart(2, '0')}</span>
              <span className="lbl">{t.label}</span>
            </div>
          ))}
        </div>
      ))}
      <div style={{ padding: '24px 10px 8px', fontSize: 11, color: '#aaa', lineHeight: 1.5, borderTop: '1px solid var(--border)', marginTop: 24 }}>
        Escolha um template, edite os campos e baixe a arte em PNG. As legendas são geradas pela IA.
      </div>
    </div>
  );
}

// ─── Field renderers ────────────────────────────────────────────────────────
function Field({ field, value, onChange }) {
  const { kind, key, label, hint, options } = field;
  if (kind === 'text') {
    return (
      <div className="field">
        <label>{label}</label>
        <input value={value ?? ''} onChange={(e) => onChange(key, e.target.value)}/>
        {hint && <div className="hint">{hint}</div>}
      </div>
    );
  }
  if (kind === 'textarea') {
    return (
      <div className="field">
        <label>{label}</label>
        <textarea value={value ?? ''} onChange={(e) => onChange(key, e.target.value)} rows={3}/>
        {hint && <div className="hint">{hint}</div>}
      </div>
    );
  }
  if (kind === 'select') {
    return (
      <div className="field">
        <label>{label}</label>
        <select value={value ?? ''} onChange={(e) => onChange(key, e.target.value)}>
          {options.map((o) => (
            <option key={o.value} value={o.value}>{o.label || o.name || o.value}</option>
          ))}
        </select>
      </div>
    );
  }
  if (kind === 'swatch') {
    return (
      <div className="field">
        <label>{label}</label>
        <div className="swatches">
          {options.map((o) => (
            <button
              key={o.value}
              title={o.name}
              className={'swatch-btn' + (value === o.value ? ' active' : '')}
              style={{ background: o.color }}
              onClick={() => onChange(key, o.value)}
            />
          ))}
        </div>
      </div>
    );
  }
  if (kind === 'color') {
    return (
      <div className="field">
        <label>{label}</label>
        <div className="swatches">
          {options.map((o) => (
            <button
              key={o.value}
              title={o.name}
              className={'swatch-btn' + (value === o.value ? ' active' : '')}
              style={{ background: o.value }}
              onClick={() => onChange(key, o.value)}
            />
          ))}
          {/* free pick */}
          <label
            style={{ width: 32, height: 32, borderRadius: 999, border: '2px dashed var(--border)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 14, color: 'var(--gray)', overflow: 'hidden', position: 'relative' }}
            title="Cor personalizada"
          >
            <span style={{ position: 'relative', zIndex: 2 }}>+</span>
            <input
              type="color"
              value={value || '#000000'}
              onChange={(e) => onChange(key, e.target.value)}
              style={{ position: 'absolute', inset: 0, opacity: 0, cursor: 'pointer' }}
            />
          </label>
          <div style={{ alignSelf: 'center', fontSize: 11, color: 'var(--gray)', fontFamily: 'JetBrains Mono, monospace', marginLeft: 4 }}>{value}</div>
        </div>
      </div>
    );
  }
  if (kind === 'heading') {
    return (
      <div style={{ margin: '18px 0 8px', paddingTop: 12, borderTop: '1px solid var(--border)', fontSize: 10, letterSpacing: '0.28em', textTransform: 'uppercase', color: 'var(--green)', fontWeight: 700 }}>{label}</div>
    );
  }
  if (kind === 'slider') {
    const v = value == null ? field.def : value;
    return (
      <div className="field">
        <label>{label} <span style={{ color: 'var(--green)', fontFamily: 'JetBrains Mono, monospace', letterSpacing: 0 }}>{Number(v).toFixed(field.step < 1 ? 2 : 0)}{field.unit || ''}</span></label>
        <input
          type="range" min={field.min} max={field.max} step={field.step}
          value={v}
          onChange={(e) => onChange(key, parseFloat(e.target.value))}
          style={{ width: '100%', accentColor: 'var(--green)' }}
        />
      </div>
    );
  }
  if (kind === 'toggle') {
    const on = value == null ? field.def : value;
    return (
      <div className="field">
        <label>{label}</label>
        <div className="seg" style={{ maxWidth: 220 }}>
          <button className={on ? 'on' : ''} onClick={() => onChange(key, true)}>{field.onLabel || 'Sim'}</button>
          <button className={!on ? 'on' : ''} onClick={() => onChange(key, false)}>{field.offLabel || 'Não'}</button>
        </div>
      </div>
    );
  }
  if (kind === 'media') {
    const onPick = (e) => {
      const file = e.target.files && e.target.files[0];
      if (!file) return;
      const url = URL.createObjectURL(file);
      onChange(key, { url, type: file.type, name: file.name });
    };
    const onDrop = (e) => {
      e.preventDefault();
      const file = e.dataTransfer.files && e.dataTransfer.files[0];
      if (!file) return;
      const url = URL.createObjectURL(file);
      onChange(key, { url, type: file.type, name: file.name });
    };
    const has = value && value.url;
    const isVideo = has && value.type && value.type.startsWith('video');
    return (
      <div className="field">
        <label>{label}</label>
        <label
          onDragOver={(e) => e.preventDefault()}
          onDrop={onDrop}
          style={{
            display: 'flex', alignItems: 'center', gap: 12,
            padding: 10, border: '1.5px dashed var(--border)', borderRadius: 10,
            background: '#fafbf6', cursor: 'pointer', minHeight: 64,
          }}
        >
          {has ? (
            <div style={{ width: 56, height: 56, borderRadius: 8, overflow: 'hidden', flex: 'none', background: '#000' }}>
              {isVideo ? (
                <video src={value.url} muted autoPlay loop playsInline style={{ width: '100%', height: '100%', objectFit: 'cover' }}/>
              ) : (
                <img src={value.url} style={{ width: '100%', height: '100%', objectFit: 'cover' }}/>
              )}
            </div>
          ) : (
            <div style={{ width: 56, height: 56, borderRadius: 8, flex: 'none', background: 'var(--cream)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 22, color: 'var(--green)' }}>↑</div>
          )}
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)', lineHeight: 1.2 }}>
              {has ? (value.name || 'mídia carregada') : 'Clique ou arraste foto/vídeo'}
            </div>
            <div style={{ fontSize: 11, color: 'var(--gray)', marginTop: 4 }}>
              {has ? (isVideo ? 'vídeo · auto-loop' : 'imagem · será cortada para encaixar') : 'JPG, PNG ou MP4'}
            </div>
          </div>
          {has && (
            <button
              onClick={(e) => { e.preventDefault(); onChange(key, null); }}
              style={{ width: 28, height: 28, borderRadius: 999, border: '1px solid var(--border)', background: '#fff', cursor: 'pointer', color: 'var(--gray)', flex: 'none' }}
              title="Remover"
            >×</button>
          )}
          <input type="file" accept="image/*,video/*" onChange={onPick} style={{ display: 'none' }}/>
        </label>
        {has && (
          <div style={{ marginTop: 10, padding: '12px 14px', border: '1px solid var(--border)', borderRadius: 10, background: '#fbfcf8' }}>
            <div style={{ fontSize: 10, letterSpacing: '0.28em', textTransform: 'uppercase', color: 'var(--gray)', fontWeight: 600, marginBottom: 10 }}>Ajustar imagem</div>
            {[
              ['scale', 'Zoom', 1, 3, 0.05, 1],
              ['posX', 'Horizontal', 0, 100, 1, 50],
              ['posY', 'Vertical', 0, 100, 1, 50],
            ].map(([mk, mlabel, mmin, mmax, mstep, mdef]) => {
              const cur = value[mk] == null ? mdef : value[mk];
              return (
                <div key={mk} style={{ marginBottom: 10 }}>
                  <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: 'var(--gray)', marginBottom: 4 }}>
                    <span>{mlabel}</span>
                    <span style={{ fontFamily: 'JetBrains Mono, monospace', color: 'var(--green)' }}>{mstep < 1 ? Number(cur).toFixed(2) : cur}{mk === 'scale' ? '×' : '%'}</span>
                  </div>
                  <input
                    type="range" min={mmin} max={mmax} step={mstep} value={cur}
                    onChange={(e) => onChange(key, { ...value, [mk]: parseFloat(e.target.value) })}
                    style={{ width: '100%', accentColor: 'var(--green)' }}
                  />
                </div>
              );
            })}
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 4 }}>
              <span style={{ fontSize: 11, color: 'var(--gray)' }}>Encaixe</span>
              <div className="seg" style={{ width: 200 }}>
                <button className={(value.fit || 'cover') === 'cover' ? 'on' : ''} onClick={() => onChange(key, { ...value, fit: 'cover' })}>Preencher</button>
                <button className={value.fit === 'contain' ? 'on' : ''} onClick={() => onChange(key, { ...value, fit: 'contain' })}>Conter</button>
              </div>
            </div>
            <button
              onClick={() => onChange(key, { url: value.url, type: value.type, name: value.name })}
              style={{ marginTop: 12, width: '100%', padding: '7px', border: '1px solid var(--border)', background: '#fff', borderRadius: 8, fontSize: 11, color: 'var(--gray)', cursor: 'pointer', fontFamily: 'Poppins', letterSpacing: '0.1em' }}
            >restaurar ajuste</button>
          </div>
        )}
      </div>
    );
  }
  if (kind === 'rows') {
    const cols = field.columns || [];
    const rows = Array.isArray(value) ? value : [];
    const setCell = (r, c, v) => {
      const next = rows.map((row) => [...row]);
      next[r][c] = v;
      onChange(key, next);
    };
    const addRow = () => onChange(key, [...rows, cols.map(() => '')]);
    const removeRow = (r) => onChange(key, rows.filter((_, i) => i !== r));
    return (
      <div className="field">
        <label>{label}</label>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          {rows.map((row, r) => (
            <div key={r} style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
              {row.map((cell, c) => (
                <input
                  key={c}
                  value={cell}
                  placeholder={cols[c]}
                  onChange={(e) => setCell(r, c, e.target.value)}
                  style={{ flex: c === 0 ? 2 : 1, minWidth: 0 }}
                />
              ))}
              <button
                onClick={() => removeRow(r)}
                style={{ width: 28, height: 28, border: '1px solid var(--border)', background: '#fafbf6', borderRadius: 6, cursor: 'pointer', color: '#888', flex: 'none' }}
                title="Remover"
              >×</button>
            </div>
          ))}
          <button
            onClick={addRow}
            style={{ marginTop: 4, padding: '8px 12px', border: '1px dashed var(--border)', background: 'transparent', borderRadius: 8, fontSize: 12, color: 'var(--gray)', cursor: 'pointer', fontFamily: 'Poppins' }}
          >+ adicionar linha</button>
        </div>
      </div>
    );
  }
  return null;
}

// ─── Live preview ───────────────────────────────────────────────────────────
// Global text scaler — multiplies every text element's font-size by `scale`.
// Runs after each render so it composes with React re-renders cleanly.
function TextScaler({ scale, children }) {
  const ref = React.useRef(null);
  React.useLayoutEffect(() => {
    const root = ref.current;
    if (!root) return;
    const s = scale == null ? 1 : scale;
    root.querySelectorAll('*').forEach((el) => {
      const hasText = Array.from(el.childNodes).some((n) => n.nodeType === 3 && n.textContent.trim());
      if (!hasText) return;
      const base = parseFloat(window.getComputedStyle(el).fontSize);
      if (!base) return;
      el.style.fontSize = (base * s) + 'px';
    });
  });
  return <div ref={ref} style={{ position: 'absolute', inset: 0 }}>{children}</div>;
}

function Preview({ template, data, captureRef }) {
  const { w, h } = template.size;
  // fit preview into ~720 wide
  const maxW = 720;
  const maxH = 760;
  const scale = Math.min(maxW / w, maxH / h);
  const dispW = w * scale, dispH = h * scale;
  return (
    <div className="canvas-wrap">
      <div
        className="canvas-stage"
        style={{ width: dispW, height: dispH, borderRadius: 10 }}
      >
        {/* Hidden full-size capture target */}
        <div
          ref={captureRef}
          style={{
            position: 'absolute', top: 0, left: 0,
            width: w, height: h,
            transformOrigin: '0 0', transform: `scale(${scale})`,
            background: '#000',
          }}
        >
          {template.render(data)}
        </div>
      </div>
    </div>
  );
}

// ─── AI Caption box ─────────────────────────────────────────────────────────
function AIBox({ template, data }) {
  const [seed, setSeed] = React.useState('');
  const [caption, setCaption] = React.useState('');
  const [loading, setLoading] = React.useState(false);
  const [err, setErr] = React.useState('');

  // Reset caption when template changes
  React.useEffect(() => { setCaption(''); setErr(''); }, [template.id]);

  const generate = async () => {
    setLoading(true);
    setErr('');
    try {
      const ctx = JSON.stringify(data, null, 2);
      const prompt =
`Você é o copywriter do @studiouarini, uma barbearia + terapia capilar com identidade ancestral indígena (símbolo da espiral, raiz, ritual). Tom: caloroso, contemplativo, lowercase, frases curtas, pouco emoji (no máximo um ✦).

Template do post: ${template.label}
Dados visíveis na arte:
${ctx}

${seed ? `Direção extra do usuário: ${seed}\n\n` : ''}Escreva uma legenda para Instagram em português brasileiro, com no máximo 5 linhas, terminando com 3 a 5 hashtags relevantes. Não comece com "Legenda:" — entregue só o texto pronto pra colar.`;

      const text = await window.claude.complete(prompt);
      setCaption(text.trim());
    } catch (e) {
      setErr('Falha ao gerar. Tenta de novo.');
    } finally {
      setLoading(false);
    }
  };

  const fallback = template.caption ? template.caption(data) : '';

  return (
    <div className="ai-box">
      <div className="ai-title">Legenda · com IA</div>
      <div className="ai-row">
        <input
          placeholder="direção extra (opcional) — ex: tom mais descontraído, focar em agenda…"
          value={seed}
          onChange={(e) => setSeed(e.target.value)}
        />
        <button onClick={generate} disabled={loading}>
          {loading ? '...' : 'Gerar'}
        </button>
      </div>
      <div className="ai-hint">
        A IA usa os dados que você preencheu ao lado para escrever a legenda. Pode pedir um tom diferente no campo acima.
        {err && <span style={{ color: 'var(--coral, #c83b48)', marginLeft: 6 }}>{err}</span>}
      </div>
      {(caption || fallback) && (
        <div className="ai-caption">
          <span className="label">{caption ? 'gerado pela IA' : 'sugestão base'}</span>
          {caption || fallback}
        </div>
      )}
    </div>
  );
}

// ─── Main App ───────────────────────────────────────────────────────────────
function App() {
  const [activeId, setActiveId] = React.useState(TEMPLATES[0].id);
  const template = TEMPLATES.find((t) => t.id === activeId);
  const [allData, setAllData] = React.useState(() => {
    const o = {};
    TEMPLATES.forEach((t) => { o[t.id] = { ...t.defaults }; });
    return o;
  });
  const data = allData[activeId];
  const setField = (key, value) =>
    setAllData((prev) => ({ ...prev, [activeId]: { ...prev[activeId], [key]: value } }));

  const reset = () =>
    setAllData((prev) => ({ ...prev, [activeId]: { ...template.defaults } }));

  const captureRef = React.useRef(null);

  const showToast = (msg = 'imagem baixada ✦') => {
    const el = document.getElementById('toast');
    if (!el) return;
    el.textContent = msg;
    el.classList.add('show');
    setTimeout(() => el.classList.remove('show'), 1600);
  };

  const download = async () => {
    const node = captureRef.current;
    if (!node) return;
    // Temporarily un-scale for clean export
    const prev = node.style.transform;
    node.style.transform = 'none';
    try {
      const dataUrl = await window.htmlToImage.toPng(node, {
        width: template.size.w,
        height: template.size.h,
        pixelRatio: 2,
        cacheBust: true,
        backgroundColor: '#000',
      });
      const a = document.createElement('a');
      a.href = dataUrl;
      a.download = `studiouarini-${template.id}.png`;
      a.click();
      showToast('imagem baixada ✦');
    } catch (e) {
      console.error(e);
      showToast('erro ao exportar');
    } finally {
      node.style.transform = prev;
    }
  };

  const copyCaption = async () => {
    const seed = template.caption ? template.caption(data) : '';
    try {
      await navigator.clipboard.writeText(seed);
      showToast('legenda copiada ✦');
    } catch {
      showToast('não consegui copiar');
    }
  };

  // The form column content
  const formNode = (
    <div>
      <div className="crumb">{template.group}</div>
      <h2>{template.label}</h2>
      <p className="lede">Edite os campos abaixo. A arte se atualiza em tempo real.</p>
      {template.fields.map((f) => (
        <Field
          key={f.key}
          field={f}
          value={data[f.key]}
          onChange={setField}
        />
      ))}
      <div className="actions">
        <button className="btn btn-primary" onClick={download}>baixar PNG</button>
        <button className="btn btn-secondary" onClick={copyCaption}>copiar legenda base</button>
        <button className="btn btn-ghost" onClick={reset}>restaurar padrão</button>
      </div>
    </div>
  );

  return { template, data, formNode, captureRef };
}

// ─── Mount three roots ──────────────────────────────────────────────────────
function Root() {
  const [activeId, setActiveId] = React.useState(TEMPLATES[0].id);
  const template = TEMPLATES.find((t) => t.id === activeId);
  const [allData, setAllData] = React.useState(() => {
    const o = {};
    TEMPLATES.forEach((t) => { o[t.id] = { ...t.defaults }; });
    return o;
  });
  const data = allData[activeId];
  const setField = (key, value) =>
    setAllData((prev) => ({ ...prev, [activeId]: { ...prev[activeId], [key]: value } }));
  const reset = () =>
    setAllData((prev) => ({ ...prev, [activeId]: { ...template.defaults } }));

  const captureRef = React.useRef(null);

  const showToast = (msg = 'imagem baixada ✦') => {
    const el = document.getElementById('toast');
    if (!el) return;
    el.textContent = msg;
    el.classList.add('show');
    setTimeout(() => el.classList.remove('show'), 1600);
  };

  // Detect if any media in current data is a video
  const hasVideo = () => {
    for (const k in data) {
      const v = data[k];
      if (v && typeof v === 'object' && v.type && v.type.startsWith('video')) return true;
    }
    return false;
  };

  const recordVideo = async (durationMs = 5000) => {
    const node = captureRef.current;
    if (!node) throw new Error('no node');

    // Find video elements inside the captured node
    const videos = Array.from(node.querySelectorAll('video'));
    if (videos.length === 0) throw new Error('no video');

    // Wait for videos to be ready
    await Promise.all(videos.map((v) => new Promise((res) => {
      if (v.readyState >= 2) return res();
      v.addEventListener('loadeddata', res, { once: true });
    })));

    const W = template.size.w, H = template.size.h;
    const canvas = document.createElement('canvas');
    canvas.width = W; canvas.height = H;
    const ctx = canvas.getContext('2d');

    // Use html-to-image once per frame is slow but works. We'll use a simpler
    // approach: rasterize the static layer once via html-to-image, then redraw
    // video frames on top each tick.
    // Simpler: render full DOM to image every frame? Too slow.
    // Best: snapshot the DOM (without videos) as bg image, then draw videos on top each frame.

    // Approach: snapshot the whole node (incl. video poster frame) once as backdrop,
    // then per-frame, redraw that snapshot + video frames on top at their positions.
    // This keeps text/spiral/grain animated-static and the video moving.

    // 1) Compute video positions relative to node
    const nodeRect = node.getBoundingClientRect();
    const videoRects = videos.map((v) => {
      const r = v.getBoundingClientRect();
      return {
        v,
        x: (r.left - nodeRect.left) * (W / nodeRect.width),
        y: (r.top - nodeRect.top) * (H / nodeRect.height),
        w: r.width * (W / nodeRect.width),
        h: r.height * (H / nodeRect.height),
      };
    });

    // 2) Snapshot the static layer: hide videos with visibility:hidden then render
    const prevVis = videos.map((v) => v.style.visibility);
    videos.forEach((v) => { v.style.visibility = 'hidden'; });
    const bgUrl = await window.htmlToImage.toPng(node, {
      width: W, height: H, pixelRatio: 1, cacheBust: true, backgroundColor: '#0E0D0D',
    });
    videos.forEach((v, i) => { v.style.visibility = prevVis[i] || ''; });
    const bg = await new Promise((res, rej) => {
      const im = new Image();
      im.onload = () => res(im);
      im.onerror = rej;
      im.src = bgUrl;
    });

    // 3) Restart videos
    videos.forEach((v) => { try { v.currentTime = 0; v.play(); } catch {} });

    // 4) Record canvas
    const stream = canvas.captureStream(30);
    const recorder = new MediaRecorder(stream, { mimeType: 'video/webm;codecs=vp9' });
    const chunks = [];
    recorder.ondataavailable = (e) => { if (e.data.size > 0) chunks.push(e.data); };
    const done = new Promise((res) => { recorder.onstop = () => res(new Blob(chunks, { type: 'video/webm' })); });
    recorder.start();

    const start = performance.now();
    let raf;
    const tick = () => {
      ctx.drawImage(bg, 0, 0, W, H);
      // draw videos with object-fit: cover behavior
      for (const { v, x, y, w, h } of videoRects) {
        const vw = v.videoWidth, vh = v.videoHeight;
        if (!vw || !vh) continue;
        const scale = Math.max(w / vw, h / vh);
        const dw = vw * scale, dh = vh * scale;
        const dx = x + (w - dw) / 2;
        const dy = y + (h - dh) / 2;
        ctx.save();
        ctx.beginPath();
        ctx.rect(x, y, w, h);
        ctx.clip();
        ctx.drawImage(v, dx, dy, dw, dh);
        ctx.restore();
      }
      if (performance.now() - start < durationMs) {
        raf = requestAnimationFrame(tick);
      } else {
        cancelAnimationFrame(raf);
        recorder.stop();
      }
    };
    raf = requestAnimationFrame(tick);

    return await done;
  };

  const download = async () => {
    const node = captureRef.current;
    if (!node) return;
    const prev = node.style.transform;
    node.style.transform = 'none';

    try {
      if (hasVideo()) {
        showToast('gravando vídeo · 5s ⏺');
        const blob = await recordVideo(5000);
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `studiouarini-${template.id}.webm`;
        a.click();
        setTimeout(() => URL.revokeObjectURL(url), 2000);
        showToast('vídeo baixado ✦');
      } else {
        const dataUrl = await window.htmlToImage.toPng(node, {
          width: template.size.w,
          height: template.size.h,
          pixelRatio: 2,
          cacheBust: true,
          backgroundColor: '#0E0D0D',
        });
        const a = document.createElement('a');
        a.href = dataUrl;
        a.download = `studiouarini-${template.id}.png`;
        a.click();
        showToast('imagem baixada ✦');
      }
    } catch (e) {
      console.error(e);
      showToast('erro ao exportar');
    } finally {
      node.style.transform = prev;
    }
  };

  const copyCaption = async () => {
    const seed = template.caption ? template.caption(data) : '';
    try {
      await navigator.clipboard.writeText(seed);
      showToast('legenda copiada ✦');
    } catch {
      showToast('não consegui copiar');
    }
  };

  const isVideo = hasVideo();

  return (
    <>
      {/* Sidebar */}
      {ReactDOM.createPortal(
        <Sidebar activeId={activeId} onPick={setActiveId}/>,
        document.getElementById('nav-mount')
      )}

      {/* Form */}
      {ReactDOM.createPortal(
        <div>
          <div className="crumb">{template.group}</div>
          <h2>{template.label}</h2>
          <p className="lede">Edite os campos abaixo. A arte se atualiza em tempo real.</p>
          {template.fields.map((f) => (
            <Field key={f.key} field={f} value={data[f.key]} onChange={setField}/>
          ))}
          <div className="actions">
            <button className="btn btn-primary" onClick={download}>{isVideo ? 'baixar vídeo (WebM · 5s)' : 'baixar PNG'}</button>
            <button className="btn btn-secondary" onClick={copyCaption}>copiar legenda base</button>
            <button className="btn btn-ghost" onClick={reset}>restaurar padrão</button>
          </div>
        </div>,
        document.getElementById('form-mount')
      )}

      {/* Preview */}
      {ReactDOM.createPortal(
        <>
          <div className="preview-header">
            <div className="preview-title">{template.id}</div>
            <div className="preview-meta">{template.size.w} × {template.size.h} px</div>
          </div>
          <Preview template={template} data={data} captureRef={captureRef}/>
          <AIBox template={template} data={data}/>
        </>,
        document.getElementById('preview-mount')
      )}
    </>
  );
}

// We need a single render root that owns shared state but paints into 3 DOM mounts.
// Mount onto a dummy host so portals can stamp into the real columns.
const _host = document.createElement('div');
_host.style.display = 'none';
document.body.appendChild(_host);
ReactDOM.createRoot(_host).render(<Root/>);
