/* kit/voxel.jsx — o vídeo GRAVADO em 3D, no visual de blocos (voxel).
 *
 * {"tipo":"kit","peca":"voxel","partitura":{…},"largura":1080,"altura":1920}
 *
 *   "esse 3D que é como eu quero e não tudo fudido como tá lá" (Joel, 11/10,
 *    com o vídeo de referência: bonecos de blocos, o bichinho laranja do
 *    Claude flutuando, chão de ladrilhos roxo, a parede de telas e o fundo
 *    desfocado de lente de cinema)
 *
 * A partitura é a mesma do kit/personagem.jsx (lib/estudio/personagem.js: quem
 * fala quando, os planos, a aparência). Muda o desenho:
 *  - tudo é bloco com a quina arredondada (RoundedBoxGeometry), nada de esfera;
 *  - o rosto é pixel: olho quadrado, sobrancelha grossa, a boca que abre com a voz;
 *  - quem é IA vira o mascote: o corpo laranja, os olhos em fenda, quatro
 *    perninhas, flutuando — e "fala" esticando o corpo no ritmo da voz;
 *  - o set: chão de ladrilhos, a parede de telas com os cartões do ASSUNTO do
 *    vídeo (os títulos saem da fala), luz de recorte roxa;
 *  - a lente: o desfoque de profundidade (BokehPass) e o brilho das telas
 *    (UnrealBloomPass), que é o que faz parecer render de cinema e não jogo;
 *  - a legenda é a da referência: frase inteira, branca, no meio, sem caixa alta.
 * Tudo é geometria do three.js: nada é baixado. */
import React, { useMemo } from 'react';
import { ThreeCanvas } from '@remotion/three';
import * as THREE from 'three';
import { useThree, useFrame } from '@react-three/fiber';
import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
import { BokehPass } from 'three/examples/jsm/postprocessing/BokehPass.js';
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js';
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
import { Mundo, Animal, Coisa, alturaDoBicho, useIcones } from './mundo.jsx';

const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
const lerp = (a, b, k) => a + (b - a) * k;
const suave = (x) => { const k = clamp(x); return k * k * (3 - 2 * k); };
const hash = (n) => { const s = Math.sin(n * 127.1 + 311.7) * 43758.5453; return s - Math.floor(s); };

/* ── O BLOCO ─────────────────────────────────────────────────────────────
   Toda peça é uma caixa de quina arredondada. A geometria é guardada pelo
   tamanho: o mesmo ladrilho repetido 200 vezes é UMA geometria. */
const GEOS = new Map();
function geo(w, h, d, r) {
  const raio = Math.min(r, w / 2 - 0.001, h / 2 - 0.001, d / 2 - 0.001);
  const k = `${w}|${h}|${d}|${raio}`;
  if (!GEOS.has(k)) GEOS.set(k, new RoundedBoxGeometry(w, h, d, 3, Math.max(0.001, raio)));
  return GEOS.get(k);
}
const Bloco = ({ s, p = [0, 0, 0], rot, c, r = 0.03, rough = 0.62, metal = 0, e, ei = 1, sombra = true }) => (
  <mesh position={p} rotation={rot} geometry={geo(s[0], s[1], s[2], r)} castShadow={sombra} receiveShadow>
    <meshStandardMaterial color={c} roughness={rough} metalness={metal} {...(e ? { emissive: e, emissiveIntensity: ei } : {})} />
  </mesh>
);
// O que é chapado no rosto (olho, boca, sobrancelha): fino, sem quina e sem sombra.
const Chapa = ({ s, p, c, rot }) => (
  <mesh position={p} rotation={rot}><boxGeometry args={s} /><meshStandardMaterial color={c} roughness={0.7} /></mesh>
);

/* ── A CENA POR TRÁS: a luz de ambiente e a câmera ───────────────────────── */
function Ambiente() {
  const { gl, scene } = useThree();
  useMemo(() => {
    const pm = new THREE.PMREMGenerator(gl);
    scene.environment = pm.fromScene(new RoomEnvironment(), 0.04).texture;
    scene.environmentIntensity = 0.22;
    pm.dispose();
  }, [gl, scene]);
  return null;
}

function Camera({ pos, alvo, fov }) {
  const { camera } = useThree();
  camera.position.set(pos[0], pos[1], pos[2]);
  camera.fov = fov;
  camera.near = 0.05;
  camera.far = 60;
  camera.lookAt(alvo[0], alvo[1], alvo[2]);
  camera.updateProjectionMatrix();
  return null;
}

/* A LENTE. O compositor desenha a cena, o brilho das telas e o desfoque de
   profundidade, nesta ordem. O useFrame com prioridade 1 tira o desenho do
   R3F e passa para o compositor — o Remotion chama o advance() a cada quadro,
   e é ele que dispara isto. */
function Lente({ W, H, foco, abertura, desfoqueMax, brilho }) {
  const { gl, scene, camera } = useThree();
  const fx = useMemo(() => {
    const comp = new EffectComposer(gl);
    comp.setPixelRatio(1);
    comp.setSize(W, H);
    comp.addPass(new RenderPass(scene, camera));
    const bloom = new UnrealBloomPass(new THREE.Vector2(W, H), 0.55, 0.65, 0.92);
    comp.addPass(bloom);
    const bokeh = new BokehPass(scene, camera, { focus: 3, aperture: 0.004, maxblur: 0.01 });
    comp.addPass(bokeh);
    comp.addPass(new OutputPass());
    return { comp, bokeh, bloom };
  }, [gl, scene, camera, W, H]);
  fx.bokeh.uniforms.focus.value = foco;
  fx.bokeh.uniforms.aperture.value = abertura;
  fx.bokeh.uniforms.maxblur.value = desfoqueMax;
  fx.bloom.strength = brilho;
  useFrame(() => { fx.comp.render(); }, 1);
  return null;
}

/* ── A BOCA, O GESTO E A PISCADA (os mesmos do kit/personagem.jsx) ──────── */
const VISEMA = (ch) => {
  if (/[aáàãâ]/.test(ch)) return [1, 0.6];
  if (/[eéê]/.test(ch)) return [0.6, 0.85];
  if (/[iíy]/.test(ch)) return [0.32, 1];
  if (/[oóôõ]/.test(ch)) return [0.78, 0.3];
  if (/[uúw]/.test(ch)) return [0.45, 0.15];
  if (/[mbp]/.test(ch)) return [0, 0.5];
  if (/[fv]/.test(ch)) return [0.12, 0.62];
  if (/[lnrdt]/.test(ch)) return [0.3, 0.6];
  return [0.22, 0.55];
};

function bocaEm(falas, quem, t) {
  for (const f of falas) {
    if (f.quem !== quem || t < f.de - 0.05 || t > f.ate + 0.05) continue;
    const w = f.palavras.find((x) => t >= x.de && t <= x.ate);
    if (!w) return [0, 0.55];
    const letras = String(w.t).toLowerCase().replace(/[^a-záàãâéêíóôõúçy]/g, '') || 'a';
    const p = clamp((t - w.de) / Math.max(0.05, w.ate - w.de), 0, 0.999);
    const pos = p * letras.length;
    const [a, l] = VISEMA(letras[Math.floor(pos)]);
    return [a * (0.62 + 0.38 * Math.sin(Math.PI * (pos % 1))), l];
  }
  return [0, 0.55];
}

function boca(falas, quem, t) {
  const amostras = [0, 1 / 60, 1 / 30].map((d) => bocaEm(falas, quem, t - d));
  return [amostras.reduce((s, x) => s + x[0], 0) / 3, amostras.reduce((s, x) => s + x[1], 0) / 3];
}

function gesto(falas, quem, t) {
  let esq = 0, dir = 0;
  for (const f of falas) {
    if (f.quem !== quem || t < f.de - 0.5 || t > f.ate + 1) continue;
    f.palavras.forEach((w, k) => {
      if (String(w.t).replace(/[^\p{L}\d]/gu, '').length < 6 && !/\d/.test(w.t)) return;
      const x = (t - (w.de - 0.15)) / 1.0;
      if (x < 0 || x > 1) return;
      const b = Math.sin(Math.PI * x);
      if (k % 2) esq = Math.max(esq, b); else dir = Math.max(dir, b);
    });
  }
  return [esq, dir];
}

function piscada(quem, t) {
  let base = 0.6 + quem * 1.3, k = 0;
  while (base < t + 1) {
    const d = t - base;
    if (d >= 0 && d < 0.14) return Math.sin((d / 0.14) * Math.PI);
    if (hash(quem * 31 + k) < 0.2) { const d2 = t - (base + 0.28); if (d2 >= 0 && d2 < 0.14) return Math.sin((d2 / 0.14) * Math.PI); }
    base += 2.4 + hash(quem * 17 + k) * 2.6;
    k++;
  }
  return 0;
}

const EMO = {
  neutro: { sobr: 0, inclina: 0 },
  feliz: { sobr: 0.3, inclina: 0.04 },
  animado: { sobr: 0.6, inclina: 0 },
  serio: { sobr: -0.5, inclina: 0 },
  surpreso: { sobr: 0.9, inclina: 0 },
  pensativo: { sobr: -0.15, inclina: 0.12 },
  curioso: { sobr: 0.45, inclina: 0.1 },
};

/* ── A PESSOA DE BLOCOS ─────────────────────────────────────────────────
   A cabeça é um cubo de 0,62: o rosto mora na face da frente (z = 0,31). */
const CAB = 0.62;
const FACE = CAB / 2 + 0.004;
const escurecer = (c, k) => new THREE.Color(c).multiplyScalar(k).getStyle();

function Cabelo({ estilo, cor }) {
  const s = CAB;
  const topo = <Bloco s={[s + 0.04, 0.16, s + 0.04]} p={[0, s / 2 + 0.02, -0.005]} c={cor} r={0.03} rough={0.85} />;
  const nuca = <Bloco s={[s + 0.04, s * 0.62, 0.1]} p={[0, s * 0.14, -s / 2 - 0.02]} c={cor} r={0.03} rough={0.85} />;
  const lados = [-1, 1].map((k) => <Bloco key={k} s={[0.06, s * 0.42, s * 0.7]} p={[k * (s / 2 + 0.02), s * 0.24, -0.08]} c={cor} r={0.02} rough={0.85} />);
  if (estilo === 'careca') return null;
  if (estilo === 'cacheado') {
    return (
      <group>
        {nuca}{lados}
        {[...Array(9)].map((_, i) => {
          const x = ((i % 3) - 1) * 0.22, z = (Math.floor(i / 3) - 1) * 0.22;
          return <Bloco key={i} s={[0.24, 0.2 + hash(i) * 0.06, 0.24]} p={[x, s / 2 + 0.06 + hash(i + 3) * 0.03, z - 0.01]} c={cor} r={0.05} rough={0.9} />;
        })}
      </group>
    );
  }
  return (
    <group>
      {topo}{nuca}{lados}
      {/* a franja: um degrau na testa, o que dá a cara do boneco da referência */}
      <Bloco s={[s * 0.62, 0.1, 0.1]} p={[-s * 0.16, s / 2 - 0.05, s / 2 - 0.02]} c={cor} r={0.02} rough={0.85} />
      {estilo === 'topete' && <Bloco s={[s * 0.7, 0.16, 0.26]} p={[0.04, s / 2 + 0.13, s / 2 - 0.12]} c={cor} r={0.04} rough={0.85} />}
      {estilo === 'coque' && <Bloco s={[0.24, 0.24, 0.24]} p={[0, s / 2 + 0.08, -s / 2 + 0.02]} c={cor} r={0.05} rough={0.85} />}
      {estilo === 'longo' && (
        <>
          <Bloco s={[s + 0.06, s * 1.05, 0.12]} p={[0, -0.06, -s / 2 - 0.04]} c={cor} r={0.03} rough={0.85} />
          {[-1, 1].map((k) => <Bloco key={k} s={[0.08, s * 0.95, s * 0.6]} p={[k * (s / 2 + 0.03), -0.02, -0.1]} c={cor} r={0.02} rough={0.85} />)}
        </>
      )}
    </group>
  );
}

function RostoPixel({ p, est }) {
  const e = EMO[est.emocao] || EMO.neutro;
  const sobr = e.sobr + est.enfase * 0.4;
  const [abre, larg] = est.boca;
  const pisca = clamp(est.pisca);
  const escuro = '#141016';
  const olhoH = 0.14 * (1 - pisca * 0.9);
  // O olhar: a pupila anda dentro do branco do olho.
  const ox = clamp(est.olhar[0], -1, 1) * 0.025, oy = clamp(est.olhar[1], -1, 1) * 0.02;
  return (
    <group>
      {[-1, 1].map((k) => (
        <group key={k} position={[k * 0.135, 0.035, FACE]}>
          <Chapa s={[0.13, olhoH, 0.012]} p={[0, 0, 0]} c="#d6d2cc" />
          {pisca < 0.6 && <Chapa s={[0.065, Math.min(olhoH, 0.085), 0.016]} p={[ox + k * 0.012, oy - 0.005, 0.002]} c={escuro} />}
          {/* a sobrancelha grossa: sobe e desce com a emoção, inclina no sério */}
          <Chapa s={[0.17, 0.05, 0.02]} p={[0, 0.125 + sobr * 0.03, 0.004]}
            rot={[0, 0, k * (est.emocao === 'serio' ? 0.22 : est.emocao === 'surpreso' ? -0.08 : 0.04) - k * sobr * 0.06]} c={p.corCabelo} />
        </group>
      ))}
      {/* o nariz: um degrauzinho de pele mais escura */}
      <Bloco s={[0.07, 0.11, 0.05]} p={[0, -0.04, FACE + 0.015]} c={escurecer(p.pele, 0.86)} r={0.012} sombra={false} />
      {/* a boca: um retângulo escuro que abre com a vogal e estica no "i" */}
      <Chapa s={[0.1 + 0.1 * larg, 0.028 + 0.1 * abre, 0.012]} p={[0, -0.165 - 0.03 * abre, FACE]} c="#3a1416" />
      {abre > 0.35 && <Chapa s={[0.08 + 0.06 * larg, 0.022, 0.014]} p={[0, -0.155 + 0.02 * abre, FACE + 0.001]} c="#efe9e1" />}
      {p.barba && (
        <>
          <Chapa s={[CAB + 0.006, 0.17, 0.008]} p={[0, -CAB / 2 + 0.085, FACE - 0.002]} c={escurecer(p.corCabelo, 1)} />
          {[-1, 1].map((k) => <Chapa key={k} s={[0.008, 0.26, CAB * 0.7]} p={[k * (CAB / 2 + 0.003), -CAB / 2 + 0.13, 0.04]} c={p.corCabelo} />)}
        </>
      )}
      {p.oculos && (
        <group position={[0, 0.035, FACE + 0.03]}>
          {[-1, 1].map((k) => (
            <group key={k} position={[k * 0.135, 0, 0]}>
              {[[0, 0.085, 0.2, 0.025], [0, -0.085, 0.2, 0.025], [-0.0875, 0, 0.025, 0.19], [0.0875, 0, 0.025, 0.19]].map(([x, y, w, h], i) => (
                <Chapa key={i} s={[w, h, 0.02]} p={[x, y, 0]} c="#111114" />
              ))}
            </group>
          ))}
          <Chapa s={[0.07, 0.022, 0.02]} p={[0, 0.03, 0]} c="#111114" />
        </group>
      )}
    </group>
  );
}


/* ── A CABEÇA FACETADA (o padrão desde a 2ª referência, 11/10) ────────────
   "olha como quero o 3D": o rosto da referência é LOW-POLY — uma esfera de
   poucas faces com sombreado chapado, nariz de pirâmide, sobrancelha grossa,
   o cabelo cacheado feito de dezenas de pedrinhas facetadas. O corpo segue
   de blocos. `cabeca: 'cubo'` volta ao rosto de pixel da 10.5. */
const GEO_FACETA = new Map();
function faceta(det) {
  if (!GEO_FACETA.has(det)) GEO_FACETA.set(det, new THREE.IcosahedronGeometry(1, det));
  return GEO_FACETA.get(det);
}
const Faceta = ({ p = [0, 0, 0], s = [1, 1, 1], rot, c, det = 1, rough = 0.75, sombra = true }) => (
  <mesh position={p} scale={s} rotation={rot} geometry={faceta(det)} castShadow={sombra} receiveShadow>
    <meshStandardMaterial color={c} roughness={rough} flatShading />
  </mesh>
);

function CabeloFacetado({ estilo, cor }) {
  const pedras = useMemo(() => {
    if (estilo === 'careca') return [];
    const out = [];
    const n = estilo === 'cacheado' ? 46 : estilo === 'longo' ? 34 : estilo === 'coque' ? 22 : 24;
    for (let i = 0; i < n; i++) {
      // Espalhadas na calota de cima e de trás da cabeça (nunca na testa baixa nem no rosto).
      const u = hash(i * 3.7 + 1), v = hash(i * 9.1 + 4);
      const az = u * Math.PI * 2;
      const el = estilo === 'longo' ? -0.9 + v * 2.2 : 0.15 + v * 1.25;
      const atras = Math.cos(az) < 0.35 || el > 0.75;
      if (!atras) continue;
      const r = estilo === 'cacheado' ? 0.105 + hash(i) * 0.05 : 0.09 + hash(i) * 0.03;
      out.push({ p: [Math.sin(az) * Math.cos(el) * 0.33, 0.04 + Math.sin(el) * 0.38, -Math.cos(az) * Math.cos(el) * 0.34 + (Math.cos(az) > 0 ? 0.0 : -0.02)], r, k: i });
    }
    if (estilo === 'coque') out.push({ p: [0, 0.36, -0.3], r: 0.16, k: 99 });
    return out;
  }, [estilo]);
  // A franja: a frente do cabelo, um pouco acima da sobrancelha.
  return (
    <group>
      {estilo !== 'careca' && <Faceta p={[0, 0.2, -0.02]} s={[0.36, 0.26, 0.36]} c={cor} det={1} rough={0.9} />}
      {pedras.map((x) => <Faceta key={x.k} p={x.p} s={[x.r, x.r * 0.9, x.r]} rot={[hash(x.k) * 3, hash(x.k + 1) * 3, 0]} c={escurecer(cor, 0.85 + hash(x.k + 2) * 0.3)} det={0} rough={0.95} />)}
    </group>
  );
}

function CabecaFacetada({ p, est }) {
  const e = EMO[est.emocao] || EMO.neutro;
  const sobr = e.sobr + est.enfase * 0.4;
  const [abre, larg] = est.boca;
  const pisca = clamp(est.pisca);
  const escuro = '#17110f';
  const fz = 0.335;   // a frente do rosto
  return (
    <group>
      {/* o crânio e o queixo: dois volumes facetados */}
      <Faceta s={[0.34, 0.39, 0.35]} c={p.pele} det={1} rough={0.7} />
      <Faceta p={[0, -0.17, 0.05]} s={[0.26, 0.2, 0.27]} c={p.pele} det={1} rough={0.7} />
      {[-1, 1].map((k) => <Faceta key={'o' + k} p={[k * 0.335, -0.01, -0.02]} s={[0.05, 0.08, 0.06]} c={escurecer(p.pele, 0.9)} det={0} />)}
      {/* os olhos: amêndoa escura que fecha na piscada, e a sobrancelha grossa */}
      {[-1, 1].map((k) => (
        <group key={k} position={[k * 0.125, 0.04, fz - 0.03]} rotation={[0, k * 0.28, 0]}>
          <mesh scale={[0.045, 0.05 * (1 - pisca * 0.88), 0.02]}><sphereGeometry args={[1, 8, 6]} /><meshStandardMaterial color={escuro} roughness={0.4} /></mesh>
          <mesh position={[0.012, 0.015, 0.016]} scale={[0.012, 0.012, 0.006]}><sphereGeometry args={[1, 6, 4]} /><meshStandardMaterial color="#ffffff" roughness={0.3} /></mesh>
          <mesh position={[0, 0.085 + sobr * 0.025, 0.012]} rotation={[0, 0, k * (est.emocao === 'serio' ? 0.25 : 0.05) - k * sobr * 0.06]}>
            <boxGeometry args={[0.14, 0.035, 0.04]} /><meshStandardMaterial color={p.corCabelo} roughness={0.9} flatShading />
          </mesh>
        </group>
      ))}
      {/* o nariz: uma pirâmide de quatro faces */}
      <mesh position={[0, -0.045, fz + 0.025]} rotation={[Math.PI / 2 - 0.25, Math.PI / 4, 0]} scale={[0.05, 0.11, 0.05]} castShadow>
        <coneGeometry args={[1, 1, 4]} /><meshStandardMaterial color={escurecer(p.pele, 0.95)} roughness={0.7} flatShading />
      </mesh>
      {/* a boca: abre com a vogal e estica no "i" */}
      <mesh position={[0, -0.17 - abre * 0.012, fz - 0.03]} scale={[0.05 + 0.04 * larg, 0.008 + 0.04 * abre, 0.02]}>
        <sphereGeometry args={[1, 8, 6]} /><meshStandardMaterial color="#3a1416" roughness={0.6} />
      </mesh>
      {p.bigode && [-1, 1].map((k) => (
        <mesh key={'b' + k} position={[k * 0.06, -0.115, fz + 0.005]} rotation={[0, 0, k * -0.35]} scale={[0.075, 0.03, 0.03]}>
          <icosahedronGeometry args={[1, 0]} /><meshStandardMaterial color={p.corCabelo} roughness={0.9} flatShading />
        </mesh>
      ))}
      {p.barba && <Faceta p={[0, -0.2, 0.08]} s={[0.27, 0.17, 0.26]} c={p.corCabelo} det={1} rough={0.95} />}
      {p.oculos && (
        <group position={[0, 0.04, fz + 0.02]}>
          {[-1, 1].map((k) => <mesh key={k} position={[k * 0.125, 0, 0]}><torusGeometry args={[0.065, 0.012, 6, 12]} /><meshStandardMaterial color="#111114" roughness={0.4} /></mesh>)}
          <mesh><boxGeometry args={[0.06, 0.012, 0.012]} /><meshStandardMaterial color="#111114" /></mesh>
        </group>
      )}
      <CabeloFacetado estilo={p.cabelo} cor={p.corCabelo} />
    </group>
  );
}

/* A AÇÃO da fala mexe o braço por cima do gesto: acenar (a mão direita no
   alto, abanando), apontar (o braço esticado para a frente), comemorar (os
   dois para cima), pensar (a mão no queixo), dançar (os dois balançando). */
function poseDoBraco(acao, lado, t, k) {
  if (!acao || k <= 0) return null;
  const dir = lado > 0;
  if (acao === 'acenar' && dir) return { x: -0.25, z: 2.55 + Math.sin(t * 9) * 0.25, c: -0.55 + Math.sin(t * 9) * 0.35 };
  if (acao === 'apontar' && dir) return { x: -1.45, z: 0.12, c: -0.05 };
  if (acao === 'comemorar') return { x: -0.2, z: lado * (2.7 + Math.sin(t * 8 + lado) * 0.15), c: -0.25 };
  if (acao === 'pensar' && dir) return { x: -1.5, z: -0.35, c: -2.1 };
  if (acao === 'dancar') return { x: -0.6 + Math.sin(t * 6 + lado) * 0.5, z: lado * (0.6 + Math.sin(t * 6) * 0.3), c: -1.1 };
  if (acao === 'correr') return { x: Math.sin(t * 13 + (lado > 0 ? 0 : Math.PI)) * 0.9, z: lado * 0.1, c: -1.3 };
  if (acao === 'andar') return { x: Math.sin(t * 7 + (lado > 0 ? 0 : Math.PI)) * 0.45, z: lado * 0.06, c: -0.25 };
  return null;
}

function BracoBloco({ lado, g, cor, pele, t, acao, k = 1 }) {
  // Repouso: o braço caído, um tico para fora. Gesto: o antebraço sobe e a mão mexe.
  let ombroX = lerp(0.08, -0.85, g) + Math.sin(t * 6 + lado) * 0.06 * g;
  let ombroZ = lado * lerp(0.06, 0.22, g);
  let cotovelo = lerp(-0.12, -1.05, g) + Math.sin(t * 7.3 + lado * 2) * 0.14 * g;
  const pose = poseDoBraco(acao, lado, t, k);
  if (pose) { ombroX = lerp(ombroX, pose.x, k); ombroZ = lerp(ombroZ, pose.z, k); cotovelo = lerp(cotovelo, pose.c, k); }
  return (
    <group position={[lado * 0.47, 1.4, 0]} rotation={[ombroX, 0, ombroZ]}>
      <Bloco s={[0.2, 0.36, 0.2]} p={[0, -0.17, 0]} c={cor} r={0.04} rough={0.8} />
      <group position={[0, -0.34, 0]} rotation={[cotovelo, 0, 0]}>
        <Bloco s={[0.19, 0.3, 0.19]} p={[0, -0.14, 0]} c={cor} r={0.04} rough={0.8} />
        <Bloco s={[0.17, 0.17, 0.17]} p={[0, -0.36, 0]} c={pele} r={0.035} />
      </group>
    </group>
  );
}

function PessoaBloco({ p, est }) {
  const resp = Math.sin(est.t * 1.7) * 0.008;
  const [cy, cp, cr] = est.cabeca;
  const calca = escurecer(p.roupa, 0.35);
  const a = est.acao, ka = est.forcaAcao || 0;
  // O corpo inteiro: pular e comemorar sobem, dançar balança, andar e correr marcham.
  const pulo = (a === 'pular' || a === 'comemorar') ? Math.abs(Math.sin(est.t * 6.5)) * 0.32 * ka : 0;
  const balanco = a === 'dancar' ? Math.sin(est.t * 6) * 0.14 * ka : 0;
  const marcha = (a === 'andar' || a === 'correr') ? (a === 'correr' ? 13 : 7) : 0;
  return (
    <group position={[0, pulo, 0]} rotation={[0, 0, balanco]}>
      {/* as pernas e o sapato */}
      {[-1, 1].map((k) => {
        const passo = marcha ? Math.sin(est.t * marcha + (k > 0 ? 0 : Math.PI)) * (a === 'correr' ? 0.7 : 0.4) * ka : 0;
        return (
          <group key={k} position={[k * 0.15, 0.76, 0]} rotation={[passo, 0, 0]}>
            <Bloco s={[0.25, 0.72, 0.27]} p={[0, -0.36, 0]} c={calca} r={0.03} rough={0.85} />
            <Bloco s={[0.27, 0.1, 0.36]} p={[0, -0.71, 0.04]} c="#1a1a1f" r={0.03} />
          </group>
        );
      })}
      <group position={[0, resp, 0]}>
        {/* o tronco, com a faixa branca da camisa da referência */}
        <Bloco s={[0.76, 0.66, 0.4]} p={[0, 1.1, 0]} c={p.roupa} r={0.05} rough={0.8} />
        <Bloco s={[0.06, 0.5, 0.02]} p={[0.17, 1.12, 0.205]} c="#dcd8d0" r={0.008} sombra={false} />
        <Bloco s={[0.22, 0.08, 0.22]} p={[0, 1.46, 0]} c={escurecer(p.pele, 0.88)} r={0.02} />
      </group>
      <BracoBloco lado={-1} g={est.gesto[0]} cor={p.roupa} pele={p.pele} t={est.t} acao={a} k={ka} />
      <BracoBloco lado={1} g={est.gesto[1]} cor={p.roupa} pele={p.pele} t={est.t} acao={a} k={ka} />
      <group position={[0, 1.8 + resp, 0]} rotation={[cp, cy, cr]}>
        {p.cabeca === 'cubo' ? (
          <>
            <Bloco s={[CAB, CAB, CAB]} c={p.pele} r={0.04} rough={0.6} />
            <RostoPixel p={p} est={est} />
            <Cabelo estilo={p.cabelo} cor={p.corCabelo} />
          </>
        ) : <CabecaFacetada p={p} est={est} />}
      </group>
    </group>
  );
}

/* ── O MASCOTE (quem é IA) ──────────────────────────────────────────────
   O da 2ª referência (11/10): um CUBO laranja de quina viva, dois olhos
   quadrados pretos, uma orelha de bloco de cada lado e uma fileira de
   perninhas, em pé no chão. Não tem boca: ele fala com o CORPO — estica para
   cima na vogal aberta, encolhe no "m" — e pisca fechando o olho em traço.
   `bigode: true` põe o bigode da referência. */
function Mascote({ p, est }) {
  const cor = p.cor || '#d9532b';
  const [abre] = est.boca;
  const pulo = (est.acao === 'pular' || est.acao === 'comemorar') ? Math.abs(Math.sin(est.t * 6)) * 0.25 * (est.forcaAcao || 0) : 0;
  const respira = Math.sin(est.t * 1.9 + 0.7) * 0.012;
  const estica = 1 + abre * 0.07 + respira, achata = 1 - abre * 0.035;
  const pisca = clamp(est.pisca);
  const [cy, cp, cr] = est.cabeca;
  const W = 1.0, H = 0.92, D = 0.78, perna = 0.46;
  const pernas = [-0.38, -0.19, 0, 0.19, 0.38];
  return (
    <group position={[0, pulo, 0]} rotation={[0, cy * 0.8, cr * 0.5]}>
      {pernas.map((x, i) => (
        <group key={i}>
          <Bloco s={[0.13, perna, 0.13]} p={[x, perna / 2 + Math.max(0, Math.sin(est.t * 9 + i)) * (est.falando ? 0.015 : 0), D / 2 - 0.12]} c={escurecer(cor, 0.9)} r={0.015} rough={0.6} />
          {i % 2 === 0 && <Bloco s={[0.13, perna, 0.13]} p={[x, perna / 2, -D / 2 + 0.12]} c={escurecer(cor, 0.8)} r={0.015} rough={0.6} />}
        </group>
      ))}
      <group position={[0, perna, 0]} rotation={[cp * 0.5, 0, 0]}>
        <group scale={[achata, estica, achata]}>
          <Bloco s={[W, H, D]} p={[0, H / 2, 0]} c={cor} r={0.025} rough={0.55} />
          {[-1, 1].map((k) => (
            <Bloco key={k} s={[0.16, 0.34, 0.3]} p={[k * (W / 2 + 0.08), H * 0.5 + (est.gesto[k > 0 ? 1 : 0] * 0.08), 0]} c={escurecer(cor, 0.93)} r={0.015} rough={0.55} />
          ))}
          {[-1, 1].map((k) => (
            <Chapa key={'o' + k} s={[0.13, pisca > 0.5 ? 0.025 : 0.155, 0.012]} p={[k * 0.23, H * 0.66, D / 2 + 0.006]} c="#0d0a0b" />
          ))}
          {/* o bigode da referência: a barra grossa no meio e as pontas que sobem em curva */}
          {p.bigode && (
            <group position={[0, H * 0.43, D / 2 + 0.012]}>
              {/* grosso no meio (dois gomos), afinando e subindo nas pontas */}
              {[-1, 1].map((k) => (
                <group key={k}>
                  <Chapa s={[0.2, 0.14, 0.024]} p={[k * 0.09, -0.005, 0]} rot={[0, 0, k * 0.12]} c="#0d0a0b" />
                  <Chapa s={[0.14, 0.1, 0.024]} p={[k * 0.235, 0.012, 0]} rot={[0, 0, k * 0.38]} c="#0d0a0b" />
                  <Chapa s={[0.08, 0.07, 0.024]} p={[k * 0.32, 0.07, 0]} rot={[0, 0, k * 0.95]} c="#0d0a0b" />
                </group>
              ))}
            </group>
          )}
        </group>
      </group>
    </group>
  );
}

/* ── O SET ─────────────────────────────────────────────────────────────── */

/* Um cartão de tela: o painel escuro com o ícone, o título do assunto, duas
   linhas de texto, a barra de progresso e a bolinha verde de "rodando". É
   desenhado num canvas 2D e vira textura que BRILHA (o bloom pega). */
function texturaCartao(titulo, linha, cor, acento, k) {
  const cv = document.createElement('canvas');
  cv.width = 512; cv.height = 320;
  const g = cv.getContext('2d');
  const r = 26;
  g.fillStyle = cor; g.beginPath(); g.roundRect(6, 6, 500, 308, r); g.fill();
  g.strokeStyle = 'rgba(160,175,255,0.35)'; g.lineWidth = 3; g.stroke();
  g.fillStyle = acento; g.beginPath(); g.roundRect(34, 34, 44, 44, 8); g.fill();
  g.fillStyle = '#eef1ff'; g.font = '600 34px Inter, "Helvetica Neue", Arial, sans-serif';
  g.fillText(String(titulo).slice(0, 18), 96, 68);
  g.fillStyle = '#3ddc84'; g.beginPath(); g.arc(462, 56, 10, 0, Math.PI * 2); g.fill();
  g.fillStyle = 'rgba(205,212,255,0.75)'; g.font = '400 24px Inter, "Helvetica Neue", Arial, sans-serif';
  g.fillText(String(linha).slice(0, 30), 36, 132);
  g.fillStyle = 'rgba(205,212,255,0.28)';
  g.beginPath(); g.roundRect(36, 160, 300 + hash(k) * 120, 16, 8); g.fill();
  g.beginPath(); g.roundRect(36, 192, 200 + hash(k + 4) * 160, 16, 8); g.fill();
  g.fillStyle = 'rgba(205,212,255,0.18)'; g.beginPath(); g.roundRect(36, 262, 440, 10, 5); g.fill();
  g.fillStyle = '#8fa2ff'; g.beginPath(); g.roundRect(36, 262, 120 + hash(k + 9) * 300, 10, 5); g.fill();
  const tx = new THREE.CanvasTexture(cv);
  tx.colorSpace = THREE.SRGBColorSpace;
  tx.anisotropy = 4;
  return tx;
}

function ParedeDeTelas({ c, paineis, t }) {
  const texturas = useMemo(() => {
    const lista = paineis && paineis.length ? paineis : [{ titulo: 'Agora', linha: '' }];
    return Array.from({ length: 16 }, (_, i) => {
      const pn = lista[i % lista.length];
      return texturaCartao(pn.titulo, pn.linha || '', c.tela, c.acento, i);
    });
  }, [paineis, c.tela, c.acento]);
  // Duas fileiras de seis telas, num arco suave atrás do set.
  const telas = [];
  for (let fila = 0; fila < 2; fila++) {
    for (let i = 0; i < 8; i++) {
      const x = (i - 3.5) * 1.62;
      const ang = -x * 0.07;
      const z = -3.4 + Math.abs(x) * 0.12;
      telas.push({ k: fila * 8 + i, x, y: 1.25 + fila * 1.08, z, ang });
    }
  }
  return (
    <group>
      {/* a estrutura escura onde as telas moram */}
      <Bloco s={[16, 3.2, 0.3]} p={[0, 1.78, -3.75]} c={c.parede} r={0.05} rough={0.9} sombra={false} />
      {telas.map((s) => {
        const vivo = 0.82 + 0.18 * Math.sin(t * 2 + s.k * 1.7);
        return (
          <group key={s.k} position={[s.x, s.y, s.z]} rotation={[0, s.ang, 0]}>
            <Bloco s={[1.54, 0.98, 0.07]} c="#0a0c18" r={0.04} rough={0.4} sombra={false} />
            <mesh position={[0, 0, 0.04]}>
              <planeGeometry args={[1.46, 0.9]} />
              <meshStandardMaterial map={texturas[s.k]} emissiveMap={texturas[s.k]} emissive="#ffffff" emissiveIntensity={0.55 * vivo} roughness={0.35} toneMapped />
            </mesh>
          </group>
        );
      })}
      {/* a fileira de luzinhas em cima da parede, como na referência */}
      {Array.from({ length: 9 }, (_, i) => (
        <Bloco key={'l' + i} s={[0.16, 0.1, 0.1]} p={[(i - 4) * 1.15, 3.15, -3.55]} c={c.acento} e={c.acento} ei={1.6 + 0.6 * Math.sin(t * 3 + i)} r={0.02} sombra={false} />
      ))}
    </group>
  );
}

function ChaoDeLadrilhos({ c }) {
  // Ladrilhos de 0,9 com junta escura: a grade que dá escala ao set.
  const pecas = useMemo(() => {
    const out = [];
    for (let i = -7; i <= 7; i++) for (let j = -5; j <= 6; j++) out.push([i * 0.92 + (j % 2 ? 0.46 : 0), j * 0.6, hash(i * 13 + j * 7)]);
    return out;
  }, []);
  return (
    <group>
      <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.02, 0]} receiveShadow>
        <planeGeometry args={[40, 40]} /><meshStandardMaterial color={escurecer(c.chao, 0.45)} roughness={0.9} />
      </mesh>
      {pecas.map(([x, z, k], i) => (
        <mesh key={i} position={[x, -0.03, z]} geometry={geo(0.88, 0.1, 0.56, 0.035)} receiveShadow>
          <meshStandardMaterial color={k > 0.5 ? c.chao : c.chao2} roughness={0.42} metalness={0.05} />
        </mesh>
      ))}
    </group>
  );
}

/* ── A CÂMERA ──────────────────────────────────────────────────────────── */
/* O LUGAR de cada um: um sozinho no meio; dois frente a frente; três e
   quatro num arco aberto para a câmera, todos virados para o centro. Bicho
   pequeno fica um passo à frente, para não sumir atrás de quem é alto. */
const ARCO = { 1: [[0, 0]], 2: [[-0.78, 0], [0.78, 0]], 3: [[-1.35, 0.1], [0, -0.35], [1.35, 0.1]], 4: [[-1.9, 0.25], [-0.65, -0.25], [0.65, -0.25], [1.9, 0.25]] };
function lugarDe(el, i) {
  const n = Math.min(4, el.length) || 1;
  const [x, z] = (ARCO[n] || ARCO[1])[Math.min(i, n - 1)];
  const baixo = el[i] && (el[i].tipo === 'animal' || el[i].tipo === 'coisa') && alturaDaCabeca(el[i]) < 1.2;
  return [x, 0, z + (baixo && n > 1 ? 0.35 : 0)];
}
function giroDe(el, i) {
  const n = el.length;
  if (n < 2) return 0;
  const [x] = lugarDe(el, i);
  return clamp(-x * 0.5, -0.45, 0.45);
}
function alturaDaCabeca(p) {
  if (!p) return 1.8;
  if (p.tipo === 'robo') return 0.46 + 0.62;   // o meio dos olhos do cubo
  if (p.tipo === 'animal') return alturaDoBicho(p.especie);
  if (p.tipo === 'coisa') return 1.1;
  return 1.8;
}
/* Os figurantes ficam nas pontas do arco, um pouco atrás: estão na cena, mas não roubam o plano. */
function lugarDoFigurante(el, fig, j) {
  const n = Math.max(1, el.length);
  const borda = n === 1 ? 1.15 : n === 2 ? 1.75 : 2.5;
  const lado = j % 2 ? 1 : -1;
  const baixo = fig.tipo === 'animal' || fig.tipo === 'coisa';
  return [lado * (borda + Math.floor(j / 2) * 0.9), 0, baixo ? 0.45 : -0.5];
}

function cameraDe(pl, t, part, aspecto) {
  const vert = aspecto < 1;
  const el = part.elenco;
  const dois = el.length > 1;
  const quem = el[pl.alvo] ? pl.alvo : 0;
  const cabeca = (i) => { const p = lugarDe(el, i); return [p[0], alturaDaCabeca(el[i]), p[2]]; };
  const frente = (i) => [Math.sin(giroDe(el, i)), 0, Math.cos(giroDe(el, i))];
  const k = clamp((t - pl.de) / Math.max(0.5, pl.ate - pl.de));
  const fovK = vert ? 1.45 : 1;
  let pos, alvo, fov;
  // Bicho e coisa falam DE FRENTE: por cima do ombro, o focinho ficava de perfil.
  if (pl.tipo === 'ombro' && el[quem] && (el[quem].tipo === 'animal' || el[quem].tipo === 'coisa')) pl = { ...pl, tipo: 'medio' };
  if (pl.tipo === 'aberto' || pl.tipo === 'largo') {
    // O plano aberto de cima, como o da referência: o set inteiro e a parede de telas.
    fov = (pl.tipo === 'largo' ? 38 : 34) * fovK;
    // Mais gente no set, a câmera recua; elenco baixo (bichos), ela desce.
    const largura = el.length > 2 ? 1 + (el.length - 2) * (vert ? 0.42 : 0.28) : 1;
    const dist = (pl.tipo === 'largo' ? (vert ? 8.2 : 6.6) : (vert ? 6.6 : 5.2)) * largura;
    const alturaMedia = el.length ? el.reduce((s, p) => s + alturaDaCabeca(p), 0) / el.length : 1.8;
    pos = [pl.lado * (el.length > 2 ? 0.2 : 0.5), (pl.tipo === 'largo' ? 3.1 : 2.5) * (0.55 + 0.45 * alturaMedia / 1.8), dist];
    alvo = [0, Math.max(0.6, alturaMedia - 0.35), -0.6];
  } else if (pl.tipo === 'ombro' && dois) {
    // Por cima do ombro de quem está MAIS PERTO de quem fala (com 3 ou 4 no set).
    let o = quem === 0 ? 1 : 0;
    for (let j = 0; j < el.length; j++) if (j !== quem && Math.abs(lugarDe(el, j)[0] - lugarDe(el, quem)[0]) < Math.abs(lugarDe(el, o)[0] - lugarDe(el, quem)[0])) o = j;
    const h = cabeca(o), s = cabeca(quem);
    const d = [h[0] - s[0], h[2] - s[2]], n = Math.hypot(d[0], d[1]) || 1;
    pos = [h[0] + (d[0] / n) * 0.9, h[1] + 0.25, h[2] + 1.6];
    alvo = [s[0], s[1] - 0.1, s[2]]; fov = 32 * fovK;
  } else {
    /* O close da referência é de BAIXO e perto, com a lente aberta: o rosto
       enorme e o resto derretendo no desfoque. */
    const dist = (pl.tipo === 'detalhe' ? 1.25 : pl.tipo === 'close' ? 1.7 : 2.7) * (vert ? 1.25 : 1);
    fov = (pl.tipo === 'detalhe' ? 30 : pl.tipo === 'close' ? 32 : 34) * fovK;
    const h = cabeca(quem), f = frente(quem);
    const lado = pl.lado * 0.42;
    const dir = [f[0] * Math.cos(lado) - f[2] * Math.sin(lado), 0, f[0] * Math.sin(lado) + f[2] * Math.cos(lado)];
    const baixo = pl.tipo === 'medio' ? 0.05 : -0.32;
    pos = [h[0] + dir[0] * dist, h[1] + baixo, h[2] + dir[2] * dist];
    alvo = [h[0], h[1] + (pl.tipo === 'medio' ? -0.35 : -0.02), h[2]];
  }
  // No vertical a legenda está no alto: a câmera mira acima, e o rosto desce no quadro.
  if (vert) alvo = [alvo[0], alvo[1] + (pl.tipo === 'aberto' || pl.tipo === 'largo' ? 0.45 : pl.tipo === 'medio' ? 0.3 : pl.tipo === 'ombro' ? 0.22 : 0.17), alvo[2]];
  const m = suave(k);
  if (pl.mov === 'entra' || pl.mov === 'sai') {
    const s = pl.mov === 'entra' ? m : 1 - m;
    pos = pos.map((v, i) => lerp(v, alvo[i], 0.12 * s));
  } else if (pl.mov === 'lateral') {
    pos = [pos[0] + (m - 0.5) * 0.45 * pl.lado, pos[1], pos[2]];
  }
  const mao = [Math.sin(t * 1.3) * 0.01 + Math.sin(t * 3.1) * 0.004, Math.sin(t * 1.1 + 1) * 0.008 + Math.sin(t * 2.7) * 0.003, 0];
  pos = pos.map((v, i) => v + (mao[i] || 0));
  alvo = alvo.map((v, i) => v + (mao[i] || 0) * 0.5);
  /* O FOCO é no ROSTO, não no centro da cabeça: a cara fica 0,3 m mais perto
     da câmera que o alvo. No plano aberto, no meio do set. A abertura é
     pequena (a lente de perto já borra muito por metro), e o teto do
     desfoque é que dá o fundo derretido da referência. */
  const aoAlvo = Math.hypot(pos[0] - alvo[0], pos[1] - alvo[1], pos[2] - alvo[2]);
  const deRosto = pl.tipo !== 'aberto' && pl.tipo !== 'largo';
  const foco = deRosto ? Math.max(0.4, aoAlvo - 0.28) : aoAlvo;
  const perto = pl.tipo === 'detalhe' || pl.tipo === 'close';
  return { pos, alvo, fov, foco, abertura: perto ? 0.012 : pl.tipo === 'ombro' ? 0.01 : pl.tipo === 'medio' ? 0.008 : 0.003, desfoqueMax: perto ? 0.03 : pl.tipo === 'ombro' ? 0.024 : 0.012 };
}

/* ── POR CIMA: a legenda da referência ─────────────────────────────────── */
const FONTE = "'Inter','SF Pro Display','Helvetica Neue',Arial,sans-serif";

// Palavra que não fecha frase: o pedaço da legenda não termina nela ("uma fogueira no").
const FRACA = /^(no|na|nos|nas|de|do|da|dos|das|o|a|os|as|e|que|um|uma|com|em|pra|pro|por|para|ao|se|mais|muito|meu|minha|seu|sua)$/i;
function pedacos(palavras) {
  const out = [];
  let cur = [];
  for (const w of palavras) {
    cur.push(w);
    const letras = cur.reduce((n, x) => n + String(x.t).length + 1, 0);
    if (letras > 30 || /[.!?;:]$/.test(w.t) || (cur.length >= 4 && /,$/.test(w.t))) {
      // Terminou numa palavra fraca: ela desce para o próximo pedaço.
      if (cur.length > 2 && FRACA.test(String(w.t).replace(/[^\p{L}]/gu, '')) && !/[.!?;:,]$/.test(w.t)) { out.push(cur.slice(0, -1)); cur = [w]; continue; }
      out.push(cur); cur = [];
    }
  }
  if (cur.length) out.push(cur);
  return out;
}

/* O LAYOUT DO QUE VAI POR CIMA (11/10, "nada fora do lugar nem sobrepondo"):
   - a legenda mora no ALTO no vertical (25% da altura, como na 2ª
     referência) e embaixo no horizontal (80%); a câmera do vertical mira
     acima do rosto (cameraDe), então o rosto fica do meio para baixo e a
     legenda nunca passa por cima dele;
   - cabe em no máximo DUAS linhas na largura útil (84%): a fonte encolhe para
     o pedaço mais longo, em vez de quebrar em três ou vazar da tela;
   - o cartão de destaque fica no alto (12% / 8%), numa linha só, e também
     encolhe para caber — os dois nunca dividem a mesma faixa. */
const LARGURA_LETRA = 0.54;   // a largura média de uma letra da Inter, em "em"
function tamanhoQueCabe(texto, base, largura, linhas) {
  const porLinha = Math.ceil(String(texto).length / linhas) + 2;
  return Math.max(Math.round(base * 0.6), Math.min(base, Math.floor(largura / (porLinha * LARGURA_LETRA))));
}

function Legenda({ part, t, W, H }) {
  const f = part.falas.find((x) => t >= x.de - 0.1 && t <= x.ate + 0.35);
  if (!f) return null;
  const grupos = pedacos(f.palavras);
  const g = grupos.find((gr) => t <= gr[gr.length - 1].ate + 0.2) || grupos[grupos.length - 1];
  const entra = suave((t - (g[0].de - 0.08)) / 0.18);
  const sai = 1 - suave((t - (f.ate + 0.15)) / 0.2);
  const vertical = H > W;
  const util = W * 0.84;
  const texto = g.map((w) => w.t).join(' ');
  const tam = tamanhoQueCabe(texto, Math.round(vertical ? W * 0.05 : H * 0.05), util, 2);
  return (
    <div style={{ position: 'absolute', left: 0, right: 0, top: H * (vertical ? 0.25 : 0.8), display: 'flex', justifyContent: 'center' }}>
      <div style={{ maxWidth: util, fontFamily: FONTE, fontWeight: 600, fontSize: tam, lineHeight: 1.18, color: '#ffffff', textAlign: 'center', letterSpacing: '-0.005em',
        overflowWrap: 'break-word', textWrap: 'balance', opacity: entra * sai, transform: `translateY(${(1 - entra) * tam * 0.15}px)`,
        textShadow: '0 2px 12px rgba(0,0,0,0.55), 0 1px 2px rgba(0,0,0,0.6)' }}>{texto}</div>
    </div>
  );
}

function Destaque({ part, t, W, H, acento }) {
  const f = part.falas.find((x) => x.destaque && t >= x.de + 0.3 && t <= Math.min(x.ate + 0.3, x.de + 3));
  if (!f) return null;
  const k = t - (f.de + 0.3);
  const fim = Math.min(f.ate + 0.3, f.de + 3) - (f.de + 0.3);
  const s = suave(k / 0.3) * (1 - suave((k - fim + 0.3) / 0.3));
  const util = W * 0.86;
  const tam = tamanhoQueCabe(f.destaque, Math.round(Math.min(W, H) * 0.045), util - Math.min(W, H) * 0.07, 1);
  return (
    <div style={{ position: 'absolute', left: 0, right: 0, top: H * (H > W ? 0.12 : 0.08), display: 'flex', justifyContent: 'center' }}>
      <div style={{ maxWidth: util, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', opacity: s, transform: `translateY(${(1 - s) * -12}px)`, background: 'rgba(12,14,32,0.72)', border: `2px solid ${acento}`, borderRadius: tam * 0.5,
        padding: `${tam * 0.3}px ${tam * 0.7}px`, fontFamily: FONTE, fontWeight: 700, fontSize: tam, color: '#fff', boxShadow: `0 0 ${tam * 0.8}px ${acento}66` }}>{f.destaque}</div>
    </div>
  );
}

/* ── A PEÇA ────────────────────────────────────────────────────────────── */
export function VoxelCena({ el, t }) {
  const part = el.partitura || {};
  const W = el.largura || part.largura || 1080, H = el.altura || part.altura || 1920;
  const c = { parede: '#07061a', chao: '#4338ca', chao2: '#3b32b8', acento: '#ff7a45', luz: '#ffd2b0', tela: '#101734', recorte: '#8b7bff', ...(part.cenario || {}) };
  const elenco = part.elenco || [];
  const falas = part.falas || [];
  const planos = part.planos || [{ de: 0, ate: part.duracao || 10, tipo: 'aberto', alvo: 0, mov: 'fixo', lado: 1 }];
  const dois = elenco.length > 1;
  const pl = planos.find((x) => t >= x.de && t < x.ate) || planos[planos.length - 1];
  const cam = cameraDe(pl, t, part, W / H);

  const falandoAgora = (i) => falas.some((f) => f.quem === i && t >= f.de && t <= f.ate);
  const emocaoDe = (i) => {
    let e = 'neutro';
    for (const f of falas) if (f.quem === i && f.de <= t + 0.2) e = t <= f.ate + 2.5 ? f.emocao : 'neutro';
    return e;
  };
  // A ação da fala vale da voz até 0,6 s depois, entrando e saindo suave.
  const acaoDe = (i) => {
    for (const f of falas) {
      if (f.quem !== i || !f.acao || f.acao === 'falar') continue;
      if (t >= f.de - 0.3 && t <= f.ate + 0.6) return [f.acao, Math.min(suave((t - f.de + 0.3) / 0.4), 1 - suave((t - f.ate - 0.1) / 0.5))];
    }
    return [null, 0];
  };
  const quemFala = () => { const f = falas.find((x) => t >= x.de && t <= x.ate); return f ? f.quem : -1; };
  const estado = (i, figurante = false) => {
    const fala = !figurante && falandoAgora(i);
    const qf = quemFala();
    const outro = qf >= 0 && qf !== i;
    const enfase = figurante ? [0, 0] : gesto(falas, i, t);
    const nod = Math.sin(t * (fala ? 5.2 : 1.4) + i) * (fala ? 0.04 : 0.015) + (outro ? Math.max(0, Math.sin(t * 2.1 + i)) * 0.04 : 0);
    const emo = figurante ? 'feliz' : emocaoDe(i);
    const e = EMO[emo] || EMO.neutro;
    const micro = (hash(Math.floor(t * 1.5) + i * 7) - 0.5) * 0.6;
    // Quem ouve vira um pouco para quem fala; quem fala olha para a câmera de vez em quando.
    const dx = outro && !figurante ? lugarDe(elenco, qf)[0] - lugarDe(elenco, i)[0] : 0;
    const paraQuemFala = clamp(dx * 0.35, -0.35, 0.35);
    const [acao, forcaAcao] = figurante ? [null, 0] : acaoDe(i);
    return {
      t, falando: fala, emocao: emo, boca: figurante ? [0, 0.55] : boca(falas, i, t), pisca: piscada(i + (figurante ? 5 : 0), t),
      gesto: enfase, enfase: Math.max(enfase[0], enfase[1]), acao, forcaAcao,
      olhar: [micro + (outro ? Math.sign(dx) * 0.8 : 0), 0.1],
      cabeca: [Math.sin(t * 0.7 + i * 2) * 0.07 + paraQuemFala, nod, (e.inclina || 0) * (i % 2 ? -1 : 1) + Math.sin(t * 0.5 + i) * 0.02],
    };
  };
  const desenhar = (p, est) => (p.tipo === 'robo' ? <Mascote p={p} est={est} />
    : p.tipo === 'animal' ? <Animal p={p} est={est} />
    : p.tipo === 'coisa' ? <Coisa p={p} est={est} icones={icones} />
    : <PessoaBloco p={p} est={est} />);
  /* O MUNDO da hora: o da fala que está (ou acabou de estar) no ar. A troca
     acontece no começo da fala — junto com o corte de câmera. */
  const mundos = part.mundos || [];
  let iMundo = 0;
  for (const f of falas) if (f.de - 0.1 <= t && f.mundo != null) iMundo = f.mundo;
  const mundo = mundos[iMundo];
  const ehTelas = !mundo || mundo.telas;
  const figurantes = part.figurantes || [];
  // Todo PNG que vira bloco (peças fora do catálogo e coisas que falam), lido antes do quadro.
  const srcs = useMemo(() => [...new Set([...mundos.flatMap((m) => m.props.map((p) => p.src)), ...elenco.map((e) => e.src), ...figurantes.map((e) => e.src)].filter(Boolean))].sort(), [part]);
  const icones = useIcones(srcs);
  const fadeIn = clamp(t / 0.5), fadeOut = clamp((part.duracao - t) / 0.6);

  return (
    <div style={{ position: 'absolute', left: el.x || 0, top: el.y || 0, width: W, height: H, overflow: 'hidden', background: '#000' }}>
      <div style={{ position: 'absolute', inset: 0, opacity: Math.min(fadeIn, fadeOut) }}>
        <ThreeCanvas width={W} height={H} shadows camera={{ position: [0, 2, 6], fov: 36 }}
          gl={{ antialias: true, preserveDrawingBuffer: true, toneMapping: THREE.ACESFilmicToneMapping, toneMappingExposure: 1.0 }}>
          <Camera pos={cam.pos} alvo={cam.alvo} fov={cam.fov} />
          <Ambiente />
          {ehTelas ? (
            <>
              <color attach="background" args={[c.parede]} />
              <fog attach="fog" args={[c.parede, 9, 20]} />
              <hemisphereLight args={[c.recorte, escurecer(c.chao, 0.4), 0.55]} />
              {/* a luz principal, quente, de cima e da frente: o que dá volume aos blocos */}
              <spotLight position={[2.2, 5.2, 4.4]} angle={0.5} penumbra={0.8} intensity={120} distance={22} decay={2} color={c.luz} castShadow
                shadow-mapSize-width={2048} shadow-mapSize-height={2048} shadow-bias={-0.0003} shadow-radius={4} />
              {/* o recorte roxo por trás, que separa o boneco do fundo */}
              <spotLight position={[-2.5, 3.6, -2.4]} angle={0.7} penumbra={1} intensity={60} distance={14} decay={2} color={c.recorte} />
              <pointLight position={[0, 2, -2.6]} intensity={16} distance={8} color="#6f7dff" />
              <directionalLight position={[-3, 2, 4]} intensity={0.35} color="#c9d4ff" />
              <ChaoDeLadrilhos c={c} />
              <ParedeDeTelas c={c} paineis={part.paineis} t={t} />
            </>
          ) : <Mundo key={iMundo} m={mundo} t={t} icones={icones} paineis={part.paineis} />}
          {elenco.map((p, i) => (
            <group key={i} position={lugarDe(elenco, i)} rotation={[0, giroDe(elenco, i), 0]}>
              {desenhar(p, estado(i))}
            </group>
          ))}
          {figurantes.map((p, j) => (
            <group key={'f' + j} position={lugarDoFigurante(elenco, p, j)} rotation={[0, (j % 2 ? -1 : 1) * 0.5, 0]}>
              {desenhar(p, estado(10 + j, true))}
            </group>
          ))}
          {/* Fora do set de telas o céu é claro: o brilho cai, senão o céu inteiro estoura. */}
          <Lente W={W} H={H} foco={cam.foco} abertura={cam.abertura} desfoqueMax={cam.desfoqueMax} brilho={ehTelas ? 0.5 : mundo.cobertura ? 0.5 : mundo.estrelas || mundo.hora === 'noite' ? 0.45 : 0.18} />
        </ThreeCanvas>
        <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse at center, rgba(0,0,0,0) 60%, rgba(0,0,0,0.45) 100%)' }} />
        <Destaque part={part} t={t} W={W} H={H} acento={c.acento} />
        {part.legenda !== false && <Legenda part={part} t={t} W={W} H={H} />}
      </div>
    </div>
  );
}

export const VOXEL = { voxel: VoxelCena };
