/*  base.jsx — o kit de motion do PrimoCode para o EffectCraft (ES3: só var e function).

    Entra ANTES do script do modelo (concatenado pelo maquina.js; um `#include` também serve).
    O modelo só chama funções de alto nível; as curvas, as molas e a câmera ficam aqui.

    O MÉTODO vem dos projetos do Clube do Motion (curva por segmento, mola com quique, câmera em
    cadeia de nulls, botão que afunda, palavra-chave em itálico, entrada por palavra), reescrito com
    código nosso.

    LIMITES DO EFFECTCRAFT 0.11.1 (medidos rodando o bin; o kit já contorna cada um):
      1. `ADBE Text Selectors` nasce VAZIO e `.property('ADBE Text Range Advanced')` volta null: foi isso que
         parou o antes-e-agora/cenas.jsx na linha 233 ("cannot convert null/undefined to object"). O
         seletor existe só depois de `addProperty('ADBE Text Selector')`, e o grupo avançado se chama
         'Advanced' (filhos 'basedOn', 'smoothness', ...). O kit usa isso, e a forma "rampa" do seletor
         não existe (a entrada por letra usa Start animado, sem rampa).
      2. Texto sem justificação explícita sai CENTRALIZADO na posição: o kit sempre define a justificação.
      3. O nome da fonte casa por família nos .ttf de EFFECTCRAFT_FONT_DIRS ("Poppins", "Playfair Display"
         com o arquivo 700i): não há `app.fonts`, nem lista de fontes, nem aviso quando a fonte falta.
      4. 'ADBE Fast Blur' não existe; o desfoque é o 'ADBE Gaussian Blur 2'.
      5. `trackMatteType` precisa de uma camada logo acima; o kit usa máscara (Mask Atom), que funciona.
      6. A forma de máscara precisa de inTangents/outTangents explícitos (o kit os cria).
      7. `app.fonts` e `File.exists` volta SEMPRE false (medido na 0.11.1, até com o arquivo ali): o Node confere os arquivos e o kit só tenta `importFile` dentro de try/catch.
      8. O `writeLn` só chega ao Node na saída do script: o kit escreve uma linha "SOM {json}" por som
         e "AVISO ..." por problema de layout (texto fora da tela).

    Ordem de uso: novaComp -> fundo -> camera -> texto/forma/imagem/celular (parent = cam.no) -> som ->
    fecha(comp). O maquina.js já faz isso por cena e junta as cenas com troca().
*/

var KIT = { f: 1, entMax: 0, fps: 30, t0: 0, ajuste: 1, cams: {}, avisos: [], nSons: 0, marca: null, rects: [], voz: null, cenaIdx: 0, enq: 1, enqDy: 0, estilo: 'deriva', bpm: 0, elementos: [], ajusteCena: 1, MX: 0.08 };

// ---------------------------------------------------------------- básicos
function kCor(h) {
    if (h instanceof Array) return h;
    h = String(h).replace('#', '');
    if (h.length == 3) h = h.charAt(0) + h.charAt(0) + h.charAt(1) + h.charAt(1) + h.charAt(2) + h.charAt(2);
    return [parseInt(h.substr(0, 2), 16) / 255, parseInt(h.substr(2, 2), 16) / 255, parseInt(h.substr(4, 2), 16) / 255];
}
function kLum(h) { var c = kCor(h); return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]; }
function kMisturar(a, b, p) { var x = kCor(a), y = kCor(b), r = []; for (var i = 0; i < 3; i++) r.push(x[i] + (y[i] - x[i]) * p); return r; }
function kTr(L) { return L.property('ADBE Transform Group'); }
function kP(L) { return kTr(L).property('ADBE Position'); }
function kS(L) { return kTr(L).property('ADBE Scale'); }
function kR(L) { return kTr(L).property('ADBE Rotate Z'); }
function kO(L) { return kTr(L).property('ADBE Opacity'); }
function kA(L) { return kTr(L).property('ADBE Anchor Point'); }
function kFx(L, mn) { return L.property('ADBE Effect Parade').addProperty(mn); }
function kN(prop) {
    var t = prop.propertyValueType;
    if (t == PropertyValueType.TwoD || t == PropertyValueType.TwoD_SPATIAL) return 2;
    if (t == PropertyValueType.ThreeD || t == PropertyValueType.ThreeD_SPATIAL) return 3;
    return 1;
}
/* kPai(filho, pai): o EffectCraft COMPENSA escala e rotação do pai no parentesco (o filho de um pai a 78% ganha 128%).
   Aqui o filho volta a 100% e 0°; a posição o chamador define depois. */
function kPai(L, p) { L.parent = p; kS(L).setValue([100, 100]); kR(L).setValue(0); }
function kFrame(t) { return Math.round(t * KIT.fps) / KIT.fps; }

function novaComp(nome, w, h, dur, fps) {
    KIT.fps = fps || 30;
    var c = app.project.items.addComp(nome, w, h, 1, dur, KIT.fps);
    c.motionBlur = true;
    // obturador curto (90°, sempre ≤ 180°) e amostras de sobra: o desfoque acompanha o movimento sem virar mancha nem código de barras
    try { c.shutterAngle = 90; c.shutterPhase = -45; c.motionBlurSamplesPerFrame = 16; c.motionBlurAdaptiveSampleLimit = 128; } catch (e) { }
    c.bgColor = [0, 0, 0];
    return c;
}

// Fundo: cor lisa + gradiente que anda (Gradient Ramp com os pontos em órbita) + brilhos à deriva: o fundo nunca fica morto.
function fundo(comp, cor, o) {
    o = o || {};
    var s = comp.layers.addSolid(kCor(cor), 'fundo', comp.width, comp.height, 1);
    var b = o.brilho, W = comp.width, H = comp.height, d = comp.duration, i;
    if (b) {
        try {   // o gradiente se mexe: dois pontos giram em volta do centro e a cor de ponta é o brilho do tema
            var rp = kFx(s, 'ADBE Ramp');
            rp.property('ADBE Ramp-0002').setValue(kCor(cor));
            rp.property('ADBE Ramp-0004').setValue(kMisturar(cor, b, 0.9));
            rp.property('ADBE Ramp-0001').expression = '[' + (W / 2) + ' + Math.cos(time * 0.9) * ' + (W * 0.45) + ', ' + (H / 2) + ' + Math.sin(time * 0.7) * ' + (H * 0.45) + ']';
            rp.property('ADBE Ramp-0003').expression = '[' + (W / 2) + ' - Math.cos(time * 0.9) * ' + (W * 0.55) + ', ' + (H / 2) + ' - Math.sin(time * 0.7) * ' + (H * 0.55) + ']';
            rp.property('ADBE Ramp-0005').setValue(2);
        } catch (e) { }
        for (i = 0; i < 3; i++) {
            var L = comp.layers.addShape(); L.name = 'brilho ' + i;
            var g = L.property('ADBE Root Vectors Group').addProperty('ADBE Vector Group');
            var el = g.property('ADBE Vectors Group').addProperty('ADBE Vector Shape - Ellipse');
            var raio = Math.max(W, H) * (i == 1 ? 0.5 : i == 2 ? 0.4 : 0.7);
            el.property('ADBE Vector Ellipse Size').setValue([raio, raio]);
            var f = g.property('ADBE Vectors Group').addProperty('ADBE Vector Graphic - Fill');
            f.property('ADBE Vector Fill Color').setValue(kCor(b));
            kO(L).setValue(i == 0 ? 38 : i == 1 ? 30 : 24);
            var bl = kFx(L, 'ADBE Gaussian Blur 2'); bl.property('ADBE Gaussian Blur 2-0001').setValue(raio * 0.35);
            var a0 = [W * (i == 1 ? 0.85 : i == 2 ? 0.5 : 0.1), H * (i == 1 ? 0.75 : i == 2 ? 0.15 : 0.2)], a1 = [W * (i == 1 ? 0.2 : i == 2 ? 0.9 : 0.8), H * (i == 1 ? 0.3 : i == 2 ? 0.85 : 0.7)];
            curva(kP(L), [[0, a0, 30, 30], [d / 2, a1, 30, 30], [d, a0, 30, 30]]);
            L.motionBlur = false;
        }
    }
    return s;
}

// ---------------------------------------------------------------- curvas e molas
/* curva(prop, chaves): chaves = [[t, valor, influenciaEntrada, influenciaSaida], ...].
   TODA chave leva ease (padrão: sai rápida 28, chega macia 72 = "snap" de interface).
   influência -1 = linear; 'a' = automática contínua (a velocidade nunca zera: câmera). */
function curva(prop, chaves) {
    var i, k, e, n = kN(prop);
    var cs = [];
    for (i = 0; i < chaves.length; i++) {
        var tk = kFrame(chaves[i][0]);
        if (i > 0 && tk <= cs[i - 1][0] + 1e-6) tk = cs[i - 1][0] + 1 / KIT.fps;   // duas chaves no mesmo quadro: a segunda vence e a primeira some (foi o bug do opacity 0 das trocas)
        cs.push([tk].concat(chaves[i].slice(1)));
    }
    for (i = 0; i < cs.length; i++) prop.setValueAtTime(cs[i][0], cs[i][1]);
    for (i = 0; i < cs.length; i++) {
        k = prop.nearestKeyIndex(cs[i][0]);
        var ei = cs[i].length > 2 && cs[i][2] != null ? cs[i][2] : 72;
        var eo = cs[i].length > 3 && cs[i][3] != null ? cs[i][3] : 28;
        if (ei === 'a' || eo === 'a') {
            prop.setTemporalContinuousAtKey(k, true); prop.setTemporalAutoBezierAtKey(k, true);
            continue;
        }
        var ie = [], oe = [];
        for (e = 0; e < n; e++) { ie.push(new KeyframeEase(0, Math.max(0.1, Math.min(100, ei < 0 ? 33 : ei)))); oe.push(new KeyframeEase(0, Math.max(0.1, Math.min(100, eo < 0 ? 33 : eo)))); }
        prop.setTemporalEaseAtKey(k, ie, oe);
        if (ei < 0 || eo < 0) prop.setInterpolationTypeAtKey(k, ei < 0 ? KeyframeInterpolationType.LINEAR : KeyframeInterpolationType.BEZIER, eo < 0 ? KeyframeInterpolationType.LINEAR : KeyframeInterpolationType.BEZIER);
        if (prop.isSpatial && prop.matchName == 'ADBE Position') {
            var z = (n == 3) ? [0, 0, 0] : [0, 0];
            prop.setSpatialAutoBezierAtKey(k, false); prop.setSpatialContinuousAtKey(k, false);
            prop.setSpatialTangentsAtKey(k, z, z);
        }
    }
}

/* mola(prop, t, de, para, {quique, dur}): mola amortecida amostrada a cada quadro.
   quique 0 = chega sem passar; 0.45 = passa uns 7% e assenta; 0.8 = elástico. */
function mola(prop, t, de, para, o) {
    o = o || {};
    var q = o.quique != null ? o.quique : 0.45, dur = o.dur || 0.7;
    var z = Math.max(0.28, 0.95 - 0.7 * q), wn = 3.9 / (z * dur);
    var wd = wn * Math.sqrt(Math.max(0.0001, 1 - z * z));
    var arr = (de instanceof Array), n = arr ? de.length : 1, passos = Math.max(3, Math.round(dur * KIT.fps)), i, j;
    for (i = 0; i <= passos; i++) {
        var tau = dur * i / passos, p;
        if (i == passos) p = 1;
        else p = 1 - Math.exp(-z * wn * tau) * (Math.cos(wd * tau) + (z * wn / wd) * Math.sin(wd * tau));
        var v;
        if (arr) { v = []; for (j = 0; j < n; j++) v.push(de[j] + (para[j] - de[j]) * p); }
        else v = de + (para - de) * p;
        prop.setValueAtTime(kFrame(t) + tau, v);
    }
    return kFrame(t) + dur;
}

// ---------------------------------------------------------------- sons
/* som(t, tipo, ganho): registra um evento de som. O Node lê as linhas "SOM {json}" e põe o efeito no tempo. */
function som(t, tipo, ganho) {
    KIT.nSons++;
    writeLn('SOM ' + JSON.stringify({ t: Math.round((KIT.t0 + t / (KIT.f || 1)) * 1000) / 1000, tipo: tipo, ganho: ganho == null ? 1 : ganho }));
}

// ---------------------------------------------------------------- câmera
/* camera(comp): um null pai de tudo. O mundo tem as coordenadas do quadro; a câmera mira (âncora) e dá
   zoom (escala). Nunca para: sempre há um zoom lento, e as chaves usam curva automática contínua.
   cam.empurra(t, dx, dy, zoom): em t a câmera chega a (centro + dx, dy) com zoom (1 = quadro). */
function camera(comp) {
    var no = comp.layers.addNull(comp.duration); no.name = 'CAM'; no.enabled = false;
    var cx = comp.width / 2, cy = comp.height / 2;
    kA(no).setValue([cx, cy]); kP(no).setValue([cx, cy - KIT.enqDy]);
    var cam = { no: no, comp: comp, chaves: [], cx: cx, cy: cy, aplicado: false, estilo: KIT.estilo };
    cam.empurra = function (t, dx, dy, zoom) { cam.chaves.push([t, dx || 0, dy || 0, zoom || 1]); return cam; };
    KIT.cams[comp.name] = cam;
    return cam;
}
function kAplicaCamera(cam) {
    if (cam.aplicado) return; cam.aplicado = true;
    var d = cam.comp.duration, ch = cam.chaves.slice(0);
    ch.sort(function (a, b) { return a[0] - b[0]; });
    var z0 = 1, z1 = ch.length ? Math.max(1.02, ch[ch.length - 1][3] + 0.04) : 1.06;
    var lista = [[0, ch.length && ch[0][0] < 0.05 ? ch[0][1] : 0, ch.length && ch[0][0] < 0.05 ? ch[0][2] : 0, z0]];
    var i;
    for (i = 0; i < ch.length; i++) if (ch[i][0] >= 0.05) lista.push(ch[i]);
    var u = lista[lista.length - 1];
    if (u[0] < d - 0.05) lista.push([d, u[1] * 1.04, u[2] * 1.04 + 8, z1]);
    var ka = [], ks = [];
    for (i = 0; i < lista.length; i++) {
        ka.push([lista[i][0], [cam.cx + lista[i][1] * 0.5, cam.cy + lista[i][2] * 0.5], 'a', 'a']);
        var zc = Math.min(lista[i][3], 1.05);   // o zoom das chaves é limitado: o conteúdo precisa continuar na área segura com a câmera perto
        ks.push([lista[i][0], [zc * 100 * KIT.enq, zc * 100 * KIT.enq], 'a', 'a']);
    }
    curva(kA(cam.camNo || cam.no), ka); curva(kS(cam.camNo || cam.no), ks);
    kEstiloCamera(cam, d);
}
/* Os estilos de câmera: cada um é uma expressão por cima das chaves (a câmera nunca para), sempre SUAVE e com ease: nada de tremor,
   nada de corte seco de enquadramento (Joel, 18/10: "a tremedeira é horrível").
   deriva (zoom lento) · pulso (respira na batida) · trilho (travelling lateral) · orbita (balanço de eixo suave)
   · push (aproximação dramática) · chicote (chega varrendo, com ease) · cinema (eixo levemente torto + subida)
   POR CIMA de qualquer estilo, a câmera RESPIRA: um push-in/out suave (+3,5% com ease cosseno) a cada ~1 s (ou a cada 1–2 batidas da
   trilha) e, no começo da cena, um "speed ramp" (12% mais perto, fechando em 0,5 s com ease). */
function kPunchTimes(d) {   // só o período (e a fase) da respiração; o instante é REAL, a cena roda em tempo nominal (×f)
    var out = [], passo, tl, f = KIT.f || 1, dr = d / f;
    if (KIT.bpm > 40) {
        var bp = 60 / KIT.bpm; passo = bp < 0.45 ? bp * 2 : bp;
        var off = (((KIT.t0 - (KIT.batidaFase || 0)) % passo) + passo) % passo; tl = passo - off; if (tl < 0.45) tl += passo;
    } else { passo = 0.95; tl = 0.8; }
    KIT.passoRespira = passo;
    while (tl < dr - 0.35) { out.push(Math.round(tl * f * 1000) / 1000); tl += passo; }
    return out;
}
function kPunchCodigo(P) {   // mantido para as expressões da atmosfera: pe/pf agora são suaves (sem estouro): variam pouco, com ease
    return 'var P = [' + P.join(',') + ']; var cur = -1; for (var i = 0; i < P.length; i++) { if (time >= P[i]) cur = i; } var tt = cur >= 0 ? time - P[cur] : 9; var pe = 0; var pf = 0; var sg = 1; ';
}
function kEstiloCamera(cam, d) {
    var st = cam.estilo || 'deriva', u = Math.min(cam.comp.width, cam.comp.height) / 1080, W = cam.comp.width, f = KIT.f || 1;
    var cn = cam.camNo || cam.no, A = kA(cn), S = kS(cn), R = kR(cn);
    var P = kPunchTimes(d); KIT.punch = P;
    var per = (KIT.passoRespira || 0.95), U = u.toFixed(3);
    var fase = (KIT.bpm > 40 ? (KIT.t0 - (KIT.batidaFase || 0)) : KIT.t0);
    var mult = '1', dx = '0', dy = '0', rot = '0';
    if (st == 'pulso') { var bp = (KIT.bpm > 40 ? 60 / KIT.bpm : 0.5) * f; mult = '(1 + 0.02 * (0.5 - 0.5 * Math.cos(2 * Math.PI * (time % ' + bp.toFixed(4) + ') / ' + bp.toFixed(4) + ')))'; }
    else if (st == 'trilho') { dx = '(time / ' + d.toFixed(3) + ' - 0.5) * ' + (W * 0.07).toFixed(1); mult = '1.05'; }
    else if (st == 'orbita') { rot = 'Math.sin(time * 0.9 / ' + f + ') * 1.8'; mult = '1.05'; }
    else if (st == 'push') { mult = '(1 + 0.06 * Math.pow(Math.min(1, time / ' + d.toFixed(3) + '), 1.6))'; rot = '-0.8 * Math.min(1, time / ' + d.toFixed(3) + ')'; }
    else if (st == 'chicote') { dx = '(1 - Math.pow(Math.min(1, time / ' + (0.5 * f).toFixed(3) + '), 0.4)) * ' + (W * 0.08).toFixed(1); }
    else if (st == 'cinema') { rot = '-1.4 + Math.sin(time * 0.6 / ' + f + ') * 0.4'; dy = '-Math.min(1, time / ' + d.toFixed(3) + ') * ' + (28 * u).toFixed(1); }
    var ease = 'var e = Math.min(1, time / ' + (0.5 * f).toFixed(3) + '); e = e * e * (3 - 2 * e); ';
    var ramp = '(1 + 0.12 * (1 - e))';
    var resp = '(1 + 0.035 * (0.5 - 0.5 * Math.cos(2 * Math.PI * ((time / ' + f + ' + ' + fase.toFixed(3) + ') % ' + per.toFixed(3) + ') / ' + per.toFixed(3) + ')))';
    S.expression = 'var s = value; ' + ease + 'var k = ' + mult + ' * ' + resp + ' * ' + ramp + '; [s[0] * k, s[1] * k]';
    A.expression = 'value + [' + dx + ', ' + dy + ']';
    R.expression = rot;
}

// ---------------------------------------------------------------- atmosfera: luz, flash, brilho, aberração, grão
/* kAtmosfera(comp, pal): NO MÁXIMO 3 efeitos visuais ao mesmo tempo (hoje 2), e NUNCA flash branco (Joel, 18/10):
   1) Glow (brilho do que é claro), 2) um vazamento de luz suave (a aberração cromática VR foi tirada: deixava uma faixa de 1–2% na borda direita e na base) (mancha de cor
   borrada em ADD, varrendo o quadro). A energia vem do movimento e da câmera, não de entulho. */
function kAtmosfera(comp, P) {
    if (KIT.fx === false) return;
    var W = comp.width, H = comp.height, u = Math.min(W, H) / 1080, pun = KIT.punch || [], cod = kPunchCodigo(pun), d = comp.duration;
    try {   // 3) vazamento de luz: uma mancha, sem pulso
        var lk = comp.layers.addShape(); lk.name = 'vazamento de luz';
        var gg = lk.property('ADBE Root Vectors Group').addProperty('ADBE Vector Group');
        var rr = Math.max(W, H) * 0.8;
        gg.property('ADBE Vectors Group').addProperty('ADBE Vector Shape - Ellipse').property('ADBE Vector Ellipse Size').setValue([rr, rr * 0.7]);
        gg.property('ADBE Vectors Group').addProperty('ADBE Vector Graphic - Fill').property('ADBE Vector Fill Color').setValue(kCor(P.ac));
        kFx(lk, 'ADBE Gaussian Blur 2').property('ADBE Gaussian Blur 2-0001').setValue(rr * 0.3);
        lk.blendingMode = BlendingMode.ADD;
        lk.motionBlur = false;
        kP(lk).expression = '[' + (-W * 0.2) + ' + (time / ' + d.toFixed(2) + ') * ' + (W * 1.4) + ', ' + (H * 0.3).toFixed(0) + ' + Math.sin(time * 1.1) * ' + (H * 0.18).toFixed(0) + ']';
        kO(lk).setValue(20);
    } catch (e) { KIT.avisos.push('vazamento de luz indisponível'); }
    try {   // 1) e 2): a camada de ajuste
        var aj = comp.layers.addSolid([1, 1, 1], 'look', W, H, 1);
        aj.adjustmentLayer = true;
        var gl = kFx(aj, 'ADBE Glo2');
        gl.property('ADBE Glo2-0002').setValue(165); gl.property('ADBE Glo2-0003').setValue(40 * u);
        gl.property('ADBE Glo2-0004').setValue(0.8);
    } catch (e) { KIT.avisos.push('camada de look indisponível: ' + e); }
}

/* O título é SEMPRE a camada de cima da cena (nada atravessa a caixa dele): as palavras vão para o topo da pilha. */
function kTituloPorCima(comp) {
    var i, T = KIT.titulosL || [];
    if (comp && T.length && KIT.tituloB != null) kPlacaDoTitulo(comp);
    for (i = T.length - 1; i >= 0; i--) { try { T[i].moveToBeginning(); } catch (e) { } }
    KIT.titulosL = [];
}
/* A GUARDA do título sobre peça que sangra (mapa, parede, feed, pan, zoom: passam por trás do título de propósito): uma placa da cor do
   fundo da cena atrás da faixa do título, com a borda de baixo em degradê. O título nunca fica em cima de rua, grade ou cartão. */
function kPlacaDoTitulo(comp) {
    var i, sangra = false, c = KIT.cams[comp.name];
    for (i = 1; i <= comp.numLayers && !sangra; i++) { var L = comp.layer(i); if (/^(mapa|parede|feed|pan|zoom)/.test(L.name) && L.parent && kEhCam(L.parent)) sangra = true; }
    if (!sangra || !c || !c.camNo || !KIT.pal) return;
    var W = comp.width, H = comp.height, u = Math.min(W, H) / 1080, B = KIT.tituloB, top = -H * 0.2;
    var pl = forma(comp, 'retangulo', { x: W / 2, y: (top + B) / 2, w: W * 1.6, h: B - top, cor: KIT.pal.bg, parent: c.camNo, nome: 'fundo-titulo' });
    var deg = forma(comp, 'retangulo', { x: W / 2, y: B + 45 * u, w: W * 1.6, h: 90 * u, cor: KIT.pal.bg, alfa: 55, parent: c.camNo, nome: 'fundo-titulo-degrade' });
    try { var mk = deg.property('ADBE Mask Parade').addProperty('ADBE Mask Atom'); mk.property('ADBE Mask Shape').setValue(kRetShape(-W, -60 * u, W, 20 * u, 0)); mk.property('ADBE Mask Feather').setValue([0, 50 * u]); } catch (e) { }
    KIT.guardaPlaca = (KIT.guardaPlaca || 0) + 1;
    writeLn('GUARDA ' + JSON.stringify({ cena: KIT.cenaIdx, w: W, h: H, log: ['título sobre peça que sangra: placa da cor do fundo atrás do título'], caixas: [] }));
}
// fecha(comp): aplica a câmera da comp, a atmosfera (luz, flash, look) e confere o layout. Chame uma vez, depois de montar a cena.
function fecha(comp) { var c = KIT.cams[comp.name]; kTituloPorCima(comp); kAjustaConteudo(comp, c); kGuarda(comp); if (KIT.ambienteFn) KIT.ambienteFn(comp, c); if (c) kAplicaCamera(c); kAtmosfera(comp, KIT.pal || { ac: '#FFFFFF' }); kConfereCena(comp); kAuditoria(comp); }

// ---------------------------------------------------------------- formas
function kMascaraRect(L, x0, y0, x1, y1, r) {
    var m = L.property('ADBE Mask Parade').addProperty('ADBE Mask Atom');
    m.property('ADBE Mask Shape').setValue(kRetShape(x0, y0, x1, y1, r || 0));
    return m.property('ADBE Mask Shape');
}
function kRetShape(x0, y0, x1, y1, r) {
    var S = new Shape(), k = 0.5523;
    r = Math.max(0, Math.min(r, (x1 - x0) / 2, (y1 - y0) / 2));
    if (r < 0.5) {
        S.vertices = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]];
        S.inTangents = [[0, 0], [0, 0], [0, 0], [0, 0]]; S.outTangents = [[0, 0], [0, 0], [0, 0], [0, 0]];
    } else {
        var c = r * (1 - k);
        S.vertices = [[x0 + r, y0], [x1 - r, y0], [x1, y0 + r], [x1, y1 - r], [x1 - r, y1], [x0 + r, y1], [x0, y1 - r], [x0, y0 + r]];
        S.inTangents = [[0, 0], [-r * k, 0], [0, 0], [0, -r * k], [0, 0], [r * k, 0], [0, 0], [0, r * k]];
        S.outTangents = [[r * k, 0], [0, 0], [0, r * k], [0, 0], [-r * k, 0], [0, 0], [0, -r * k], [0, 0]];
    }
    S.closed = true;
    return S;
}
function kCircShape(cx, cy, r) {
    var S = new Shape(), k = 0.5523 * r;
    S.vertices = [[cx, cy - r], [cx + r, cy], [cx, cy + r], [cx - r, cy]];
    S.inTangents = [[-k, 0], [0, -k], [k, 0], [0, k]];
    S.outTangents = [[k, 0], [0, k], [-k, 0], [0, -k]];
    S.closed = true;
    return S;
}

/* entrada: 'pop' (estala com quique forte e balança) · 'sobe' (sobe do chão com mola) · 'fade' · 'cresce' · 'flip' (vira em 3D) · null.
   Toda entrada é EXAGERADA de propósito (motion é extravagante): a escala vai a ~125% e assenta; depois a camada respira. */
function kRespira(L, fase) {
    try {
        var ph = (fase == null ? (KIT.nResp = (KIT.nResp || 0) + 1) * 1.37 : fase).toFixed(2);
        kS(L).expression = 'var s = value; var k = 1 + 0.016 * Math.sin(time * 2.4 + ' + ph + '); [s[0] * k, s[1] * k]';
    } catch (e) { }
}
function kMarcaEntrada(t) { if (t > KIT.entMax) KIT.entMax = t; }
function kEntrada(L, t, tipo, quique) {
    if (!tipo) return;
    kMarcaEntrada(t);
    var esc = kS(L).value, p = kP(L).value, rot0 = kR(L).value;
    if (tipo == 'pop') {
        mola(kS(L), t, [0, 0], [esc[0], esc[1]], { quique: quique != null ? Math.max(quique, 0.78) : 0.85, dur: 0.7 });
        if (rot0 == 0) mola(kR(L), t, ((KIT.nResp = (KIT.nResp || 0) + 1) % 2 ? 1 : -1) * 11, 0, { quique: 0.7, dur: 0.7 });
        curva(kO(L), [[t, 0, null, 30], [t + 0.06, 100, 60, null]]);
        kRespira(L);
    } else if (tipo == 'sobe') {
        mola(kP(L), t, [p[0], p[1] + 130], [p[0], p[1]], { quique: 0.72, dur: 0.7 });
        curva(kO(L), [[t, 0, null, 30], [t + 0.18, 100, 60, null]]);
    } else if (tipo == 'cresce') {
        mola(kS(L), t, [esc[0] * 0.45, esc[1] * 0.45], [esc[0], esc[1]], { quique: 0.78, dur: 0.7 });
        curva(kO(L), [[t, 0, null, 30], [t + 0.12, 100, 60, null]]);
        kRespira(L);
    } else if (tipo == 'fade') {
        curva(kO(L), [[t, 0, null, 30], [t + 0.35, 100, 60, null]]);
    } else if (tipo == 'flip') {   // cartão que vira em 3D, com quique
        L.threeDLayer = true;
        var rx = L.property('ADBE Transform Group').property('ADBE Rotate X');
        mola(rx, t, -95, 0, { quique: 0.7, dur: 0.8 });
        curva(kO(L), [[t, 0, null, 30], [t + 0.1, 100, 60, null]]);
    }
}

/* forma(comp, 'circulo'|'retangulo'|'pilula', {x,y,w,h,r,cor,traco,tracoL,alfa,parent,t0,entrada,quique,nome}) */
function forma(comp, tipo, o) {
    o = o || {};
    var L = comp.layers.addShape(); L.name = o.nome || tipo;
    if (o.parent) kPai(L, o.parent);
    var g = L.property('ADBE Root Vectors Group').addProperty('ADBE Vector Group');
    var v = g.property('ADBE Vectors Group');
    var w = o.w || 200, h = o.h || (tipo == 'circulo' ? w : 100);
    if (tipo == 'circulo') {
        v.addProperty('ADBE Vector Shape - Ellipse').property('ADBE Vector Ellipse Size').setValue([w, w]);
    } else {
        var rc = v.addProperty('ADBE Vector Shape - Rect');
        rc.property('ADBE Vector Rect Size').setValue([w, h]);
        rc.property('ADBE Vector Rect Roundness').setValue(tipo == 'pilula' ? Math.min(w, h) / 2 : (o.r || 0));
    }
    if (o.cor) v.addProperty('ADBE Vector Graphic - Fill').property('ADBE Vector Fill Color').setValue(kCor(o.cor));
    if (o.traco) {
        var st = v.addProperty('ADBE Vector Graphic - Stroke');
        st.property('ADBE Vector Stroke Color').setValue(kCor(o.traco));
        st.property('ADBE Vector Stroke Width').setValue(o.tracoL || 4);
    }
    kP(L).setValue([o.x == null ? comp.width / 2 : o.x, o.y == null ? comp.height / 2 : o.y]);
    if (o.alfa != null) kO(L).setValue(o.alfa);
    if (o.esc) kS(L).setValue([o.esc, o.esc]);
    if (o.entrada) kEntrada(L, o.t0 || 0, o.entrada, o.quique);
    return L;
}

// o V de "feito": um traço aberto com ponta redonda, desenhado por trim
function check(comp, o) {
    o = o || {};
    var L = comp.layers.addShape(); L.name = o.nome || 'check';
    if (o.parent) kPai(L, o.parent);
    var g = L.property('ADBE Root Vectors Group').addProperty('ADBE Vector Group');
    var v = g.property('ADBE Vectors Group');
    var tam = o.tam || 40;
    var pa = v.addProperty('ADBE Vector Shape - Group');
    var S = new Shape(); S.vertices = [[-0.42 * tam, 0.02 * tam], [-0.1 * tam, 0.34 * tam], [0.44 * tam, -0.32 * tam]];
    S.inTangents = [[0, 0], [0, 0], [0, 0]]; S.outTangents = [[0, 0], [0, 0], [0, 0]]; S.closed = false;
    pa.property('ADBE Vector Shape').setValue(S);
    var st = v.addProperty('ADBE Vector Graphic - Stroke');
    st.property('ADBE Vector Stroke Color').setValue(kCor(o.cor || '#FFFFFF'));
    st.property('ADBE Vector Stroke Width').setValue(o.espessura || tam * 0.2);
    try { st.property('ADBE Vector Stroke Line Cap').setValue(2); st.property('ADBE Vector Stroke Line Join').setValue(2); } catch (e) { }
    kP(L).setValue([o.x || 0, o.y || 0]);
    if (o.t0 != null) {
        var tm = v.addProperty('ADBE Vector Filter - Trim');
        curva(tm.property('ADBE Vector Trim End'), [[o.t0, 0, null, 25], [o.t0 + 0.28, 100, 75, null]]);
    }
    return L;
}

// ---------------------------------------------------------------- texto
function kMedir(comp, s, fonte, tam, trk) {
    var L = comp.layers.addText(s);
    kDoc(L, fonte, tam, [1, 1, 1], { tracking: trk || 0 });
    var w = L.sourceRectAtTime(0, false).width;
    L.remove();
    return w;
}
function kDoc(L, fonte, tam, cor, extra) {
    var td = L.property('ADBE Text Properties').property('ADBE Text Document');
    var d = td.value;
    d.resetCharStyle();
    d.font = fonte; d.fontSize = tam; d.applyFill = true; d.fillColor = kCor(cor); d.applyStroke = false;
    d.justification = (extra && extra.just == 'left') ? ParagraphJustification.LEFT_JUSTIFY : ParagraphJustification.CENTER_JUSTIFY;
    if (extra && extra.tracking != null) d.tracking = extra.tracking;
    td.setValue(d);
    return td;
}
function kPalavras(s) {
    // *palavra* marca a palavra-chave; devolve [{t:'texto', kw:true|false}]
    var p = String(s).replace(/\s+/g, ' ').split(' '), r = [], i;
    for (i = 0; i < p.length; i++) {
        if (!p[i]) continue;
        var kw = false, t = p[i];
        if (t.charAt(0) == '*' ) { kw = true; t = t.replace(/\*/g, ''); }
        else if (t.indexOf('*') >= 0) { t = t.replace(/\*/g, ''); }
        r.push({ t: t, kw: kw });
    }
    return r;
}

/* texto(comp, str, {fonte, fonteDestaque, tamanho, cor, corDestaque, x, y, largura, maxLinhas, peso, entrada, t0, parent, passo, livre, sobrepoe, fala})
   entrada: 'palavra' | 'letra' | 'sobe' | 'mascara'. A palavra entre *asteriscos* é a palavra-chave; o REALCE vem do
   sistema tipográfico do vídeo (KIT.tipo.realce): 'italico' (itálico + cor + sublinhado) · 'caixa' (marca-texto atrás) ·
   'cor' (só a cor) · 'sublinha' (sublinhado sem trocar a fonte). Cada palavra é uma camada: o kit mede com
   sourceRectAtTime, quebra as linhas, encolhe o corpo até caber e ENCAIXA o bloco na área segura (parent = câmera).
   Com narração (KIT.voz), cada palavra entra NO INSTANTE em que é dita ("nada aparece antes de ser dito").
   Devolve {camadas, w, h, fim, tam, linhas}. */
function kNorm(s) {
    s = String(s).toLowerCase();
    var de = 'áàâãäéèêëíìîïóòôõöúùûüçñ', pa = 'aaaaaeeeeiiiiooooouuuucn', i;
    for (i = 0; i < de.length; i++) s = s.split(de.charAt(i)).join(pa.charAt(i));
    return s.replace(/[^a-z0-9]/g, '');
}
// o tempo (local da cena) em que a palavra é dita; null se não há voz ou a palavra não foi dita nesta cena
function kFalaBusca(w) {
    if (!KIT.voz) return null;
    var n = kNorm(w), P = KIT.voz.palavras, i;
    if (n.length < 2) return null;
    for (i = 0; i < P.length; i++) {
        if (P[i].cena != KIT.cenaIdx) continue;
        var pn = P[i].n || kNorm(P[i].w);
        if (pn == n || (n.length >= 5 && pn.length >= 5 && pn.substr(0, 5) == n.substr(0, 5))) return Math.max(0.05, P[i].t - KIT.t0);
    }
    return null;
}
/* Q(trecho, padrao): o instante local em que o trecho é dito (a 1ª palavra que casa); sem voz, o padrão. */
function Q(trecho, padrao) {
    if (!KIT.voz) return padrao;
    var ps = String(trecho).replace(/\*/g, '').split(' '), i, melhor = null;
    for (i = 0; i < ps.length; i++) { var t = kFalaBusca(ps[i]); if (t != null && (ps[i].length >= 3 || melhor == null)) { melhor = t; if (ps[i].length >= 3) break; } }
    return melhor != null ? melhor : Math.max(padrao, 0.25);
}
/* A zona segura vem em coordenadas do MUNDO (o que o espectador vê) e é traduzida para as do LAYOUT (antes da câmera), que o kit usa para
   posicionar: mundo = (cy − subida) + (y − cy) × enq × zoom, com zoom ≤ 1,08 (respiração × estilo). Assim o que está dentro da zona no layout
   continua dentro dela com a câmera perto. 9:16: texto fora das faixas de UI do Reels (14% em cima, 20% embaixo; com legenda, 27%). */
function kParaLayout(comp, xw, yw) {
    var f = KIT.enq * 1.12, cx = comp.width / 2, cy = comp.height / 2;
    return { x: cx + (xw - cx) / f, y: cy + (yw - cy + KIT.enqDy) / f };
}
function kZonaMundo(comp, texto) {
    var W = comp.width, H = comp.height, alto = H / W > 1.6;
    if (!texto) return { x0: W * KIT.MX, x1: W * (1 - KIT.MX), y0: H * KIT.MX, y1: H * (1 - KIT.MX) };
    return { x0: W * KIT.MX, x1: W * (1 - KIT.MX), y0: alto ? H * 0.14 : H * KIT.MX, y1: alto ? H * (KIT.enqDy > 0 ? 0.73 : 0.8) : H * (1 - KIT.MX) };
}
function kZonaTexto(comp, texto) {   // em coordenadas de LAYOUT: {x (margem esquerda e direita), top, bot (margem de baixo)}
    var W = comp.width, H = comp.height, z = kZonaMundo(comp, texto !== false), a = kParaLayout(comp, z.x0, z.y0), b = kParaLayout(comp, z.x1, z.y1);
    return { x: a.x, top: a.y, bot: H - b.y };
}
function kEhCam(p) { return !!p && (p.name == 'CAM' || p.name == 'CONTEUDO'); }
function kAreaSegura(comp, o) { return !o.livre && (!o.parent || kEhCam(o.parent)); }
function texto(comp, str, o) {
    o = o || {};
    var T = KIT.tipo || {};
    var fonte = o.fonte || (KIT.marca && KIT.marca.titulo) || 'Poppins';
    var fonteD = o.fonteDestaque || (KIT.marca && KIT.marca.destaque) || fonte;
    var realce = o.realce || T.realce || 'italico';
    if (realce != 'italico') fonteD = fonte;
    var cor = o.cor || '#FFFFFF', corD = o.corDestaque || cor;
    var tam = (o.tamanho || 90) * KIT.ajuste * KIT.ajusteCena;
    var W = comp.width, H = comp.height, seg = kAreaSegura(comp, o);
    if (T.caixa == 'alta' && o.caixa !== false) str = String(str).toUpperCase();
    var x = o.x == null ? W / 2 : o.x, y = o.y == null ? H / 2 : o.y;
    if (o.opcional && KIT.semRotulos) return { camadas: [], tempos: [], w: 0, h: 0, fim: o.t0 || 0, tam: 0, linhas: 0 };
    var tall = H / W > 1.6, princ = !!o.principal && tall;
    if (kEhCam(o.parent || null) || !o.parent) tam = Math.max(tam, H * 0.036 * (o.principal ? 1 : 1));   // nenhum texto fora de aparelho abaixo de 3,2% da altura (no mundo)
    if (princ) tam = Math.max(tam, 120);   // texto principal ≥ 7% da altura do quadro no 9:16 (linha de ~134 px)
    var maxW = o.largura || W * 0.84, maxLin = (o.maxLinhas || 4) + (princ ? 1 : 0);
    if (seg) maxW = Math.min(maxW, W * (1 - 2 * KIT.MX));
    var pal = kPalavras(str);
    if (!pal.length) return { camadas: [], w: 0, h: 0, fim: o.t0 || 0, tam: tam, linhas: 0 };
    var trk = T.tracking != null ? T.tracking : 0;
    var linhas, ws, sw, i, tent = 0;
    for (;;) {
        ws = []; for (i = 0; i < pal.length; i++) ws.push(kMedir(comp, pal[i].t, pal[i].kw ? fonteD : fonte, tam, trk));
        sw = Math.max(tam * 0.27, kMedir(comp, 'a a', fonte, tam, trk) - kMedir(comp, 'aa', fonte, tam, trk));
        var mx = 0; for (i = 0; i < ws.length; i++) if (ws[i] > mx) mx = ws[i];
        linhas = kQuebra(ws, sw, maxW);
        if (linhas.length > 1) {   // equilibra: linhas de tamanho parecido
            var tot = 0; for (i = 0; i < ws.length; i++) tot += ws[i] + sw;
            var alvo = Math.max(mx, tot / linhas.length * 1.12);
            var l2 = kQuebra(ws, sw, Math.min(maxW, alvo));
            if (l2.length <= linhas.length) linhas = l2;
        }
        if ((mx <= maxW && linhas.length <= maxLin) || tent > 14 || tam < (princ ? 112 : H * 0.036)) break;
        tam *= 0.92; tent++;
    }
    var lh = tam * (o.entreLinhas || T.entreLinhas || 1.12), bloco = linhas.length * lh, camadas = [], tempos = [], wMax = 0, fim = o.t0 || 0, t0 = o.t0 || 0;
    var k, l, idx = 0, lastT = t0;
    for (l = 0; l < linhas.length; l++) { var lw0 = 0; for (k = 0; k < linhas[l].length; k++) lw0 += ws[linhas[l][k]] + (k ? sw : 0); if (lw0 > wMax) wMax = lw0; }
    if (seg) {   // encaixa na área segura: 8% nos lados; no 9:16 respeita as faixas de UI do Reels (14% em cima, 20% embaixo)
        var zs = kZonaTexto(comp), mxp = zs.x, myp = zs.top, myb = zs.bot;
        x = Math.max(mxp + wMax / 2, Math.min(W - mxp - wMax / 2, x));
        y = Math.max(myp + bloco / 2 + (o.titulo ? (KIT.tituloDesce || 0) * H : 0), Math.min(H - myb - bloco / 2, y));   // tituloDesce: a câmera levou o título pra cima da faixa do Reels
        x = Math.round(x / 8) * 8; y = Math.round(y / 8) * 8;   // grade de 8 px
    }
    wMax = 0;
    for (l = 0; l < linhas.length; l++) {
        var lw = 0; for (k = 0; k < linhas[l].length; k++) lw += ws[linhas[l][k]] + (k ? sw : 0);
        if (lw > wMax) wMax = lw;
        var xs = x - lw / 2, yb = y - bloco / 2 + lh * l + lh * 0.5 + tam * 0.33;
        for (k = 0; k < linhas[l].length; k++) {
            var j = linhas[l][k], w = ws[j], xc = xs + w / 2; xs += w + sw;
            var ti = t0 + idx * (o.passo || T.passo || 0.1);
            if (KIT.voz && o.fala !== false) { var tv = kFalaBusca(pal[j].t); ti = tv != null ? tv : Math.max(lastT + 0.08, 0.25); }
            lastT = ti;
            var corTxtCaixa = kLum(corD) > 0.5 ? '#14110F' : '#FFFFFF';
            if (pal[j].kw && realce == 'caixa' && o.sublinhado !== false) {   // o marca-texto vem ANTES: o texto entra por cima
                var bw2 = w + tam * 0.3;
                var cx2 = forma(comp, 'retangulo', { x: xc - bw2 / 2, y: yb - tam * 0.3, w: bw2, h: tam * 1.0, r: tam * 0.12, cor: corD, parent: o.parent, nome: 'marca-texto' });
                kA(cx2).setValue([-bw2 / 2, 0]);
                curva(kS(cx2), [[ti, [0, 100], null, 30], [ti + 0.3, [100, 100], 80, null]]);
            }
            var L = comp.layers.addText(pal[j].t); L.name = (o.semAuditoria ? 'fundo-palavra ' : 'palavra ') + pal[j].t;
            if (o.parent) kPai(L, o.parent);
            var kwCor = pal[j].kw ? ((realce == 'caixa') ? corTxtCaixa : corD) : cor;
            kDoc(L, pal[j].kw ? fonteD : fonte, tam * (pal[j].kw ? (o.escalaDestaque || 1.0) : 1), kwCor, { tracking: trk });
            kP(L).setValue([xc, yb]);
            L.comment = ti.toFixed(3);   // o instante (nominal) em que a palavra entra: a auditoria usa
            if (o.titulo) { KIT.titulosL = KIT.titulosL || []; KIT.titulosL.push(L); }
            kAnimaPalavra(comp, L, o.entrada || T.entrada || 'palavra', ti, xc, yb, w, tam, l, idx, pal[j].t);
            if (pal[j].kw && realce != 'cor' && realce != 'caixa' && o.sublinhado !== false) {
                var ub = forma(comp, 'pilula', { x: xc - w / 2, y: yb + tam * 0.16, w: w, h: Math.max(6, tam * 0.07), cor: corD, parent: o.parent, nome: 'sublinhado' });
                kA(ub).setValue([-w / 2, 0]);
                curva(kS(ub), [[ti + 0.2, [0, 100], null, 30], [ti + 0.62, [100, 100], 80, null]]);
            }
            camadas.push(L); tempos.push(ti); fim = Math.max(fim, ti + 0.5); idx++;
        }
    }
    var tIni = tempos.length ? tempos[0] : t0;
    if (!o.parent || kEhCam(o.parent)) KIT.rects.push({ espaco: (o.parent && o.parent.name == 'CONTEUDO') ? 'c' : 't', titulo: !!o.titulo, livre: !seg, cena: KIT.cenaIdx, x0: x - wMax / 2, x1: x + wMax / 2, y0: y - bloco / 2, y1: y + bloco / 2, t0: tIni, t1: comp.duration, tam: tam, texto: true, nome: String(str).slice(0, 30), sobrepoe: o.sobrepoe === true });
    if (princ && tam * 1.2 < H * 0.07 - 2) KIT.avisos.push('cena ' + KIT.cenaIdx + ': texto principal pequeno (' + Math.round(tam * 1.2) + ' px < 7% da altura): ' + String(str).slice(0, 30));
    if (wMax > W * (1 - 2 * KIT.MX) + 4 && seg) KIT.avisos.push('cena ' + KIT.cenaIdx + ': texto largo demais: ' + String(str).slice(0, 40));
    return { camadas: camadas, tempos: tempos, w: wMax, h: bloco, fim: fim, tam: tam, linhas: linhas.length, x: x, y: y };
}
function kQuebra(ws, sw, maxW) {
    var linhas = [[]], larg = 0, i;
    for (i = 0; i < ws.length; i++) {
        var add = ws[i] + (linhas[linhas.length - 1].length ? sw : 0);
        if (larg + add > maxW && linhas[linhas.length - 1].length) { linhas.push([i]); larg = ws[i]; }
        else { linhas[linhas.length - 1].push(i); larg += add; }
    }
    return linhas;
}
/* Quanto tempo (nominal) a entrada leva até a palavra estar INTEIRA na tela. 'voa' e 'letra' montam letra por letra (em ordem
   aleatória no 'voa'): a palavra só se lê no fim. Quem tira a palavra da tela antes disso mostra "q … ho" no lugar de "quentinho". */
function kTempoMontagem(modo, txt) {
    var n = Math.max(1, String(txt || '').replace(/\*/g, '').length);
    if (modo == 'voa') return 0.3 + n * 0.06;
    if (modo == 'letra') return 0.12 + n * 0.045;
    if (modo == 'mascara') return 0.4;
    if (modo == 'sobe') return 0.35;
    return 0.2;   // 'palavra': inteira desde o primeiro quadro, crescendo; aos 0,2 s já passa de 90%
}
/* A entrada que cabe na janela da palavra (entra em ti, sai em fim): a pedida, se a palavra fica inteira por `leitura` s; senão a
   mais rápida que deixe ler; no último caso, 'palavra' (inteira desde o começo, só cresce). */
function kEntradaQueCabe(modo, txt, janela, leitura) {
    var ord = [modo, 'mascara', 'sobe', 'palavra'], i;
    for (i = 0; i < ord.length; i++) if (kTempoMontagem(ord[i], txt) + (leitura || 0) <= janela) return ord[i];
    return 'palavra';
}
function kAnimaPalavra(comp, L, modo, ti, x, y, w, tam, linha, idx, txt) {
    kMarcaEntrada(ti);
    var drift = Math.max(2, tam / 55);
    if (modo == 'letra' || modo == 'voa') {
        // um animador por caractere. 'letra': opacidade 0 + deslocamento; 'voa': a letra chega de longe, grande e girando,
        // em ordem aleatória (Randomize Order) e com suavização, como as letras voando dos reels de motion.
        var an = L.property('ADBE Text Properties').property('ADBE Text Animators').addProperty('ADBE Text Animator');
        var pr = an.property('ADBE Text Animator Properties');
        pr.addProperty('ADBE Text Opacity').setValue(0);
        pr.addProperty('ADBE Text Position 3D').setValue(modo == 'voa' ? [-tam * 0.5, -tam * 1.1, 0] : [0, tam * 0.35, 0]);
        if (modo == 'voa') {
            try { pr.addProperty('ADBE Text Scale 3D').setValue([320, 320, 320]); pr.addProperty('ADBE Text Rotation').setValue(80); } catch (e) { }
        }
        var sel = an.property('ADBE Text Selectors').addProperty('ADBE Text Selector');
        try {
            sel.property('Advanced').property('basedOn').setValue(2);
            sel.property('Advanced').property('smoothness').setValue(modo == 'voa' ? 100 : 0);
            if (modo == 'voa') sel.property('Advanced').property('randomize').setValue(1);
        } catch (e) { }
        var n = Math.max(1, txt.length), dur = (modo == 'voa' ? 0.3 : 0.12) + n * (modo == 'voa' ? 0.06 : 0.045);
        curva(sel.property('ADBE Text Percent Start'), [[ti, 0, null, 30], [ti + dur, 100, 60, null]]);
        kP(L).expression = 'value + [Math.sin(time*1.9+' + idx + ')*' + drift.toFixed(1) + ', Math.cos(time*1.5+' + idx + ')*' + drift.toFixed(1) + ']';
        return;
    }
    if (modo == 'mascara') {
        var m = kMascaraRect(L, -w / 2 - 20, -tam * 1.0, w / 2 + 20, tam * 0.4, 0);
        var cheio = kRetShape(-w / 2 - 20, -tam * 1.0, w / 2 + 20, tam * 0.4, 0), vazio = kRetShape(-w / 2 - 20, tam * 0.4, w / 2 + 20, tam * 0.4, 0);
        m.setValueAtTime(kFrame(ti), vazio); m.setValueAtTime(kFrame(ti) + 0.4, cheio);
        mola(kP(L), ti, [x, y + tam * 0.6], [x, y], { quique: 0.7, dur: 0.6 });
        return;
    }
    if (modo == 'sobe') {
        mola(kP(L), ti, [x, y + tam * 1.0], [x, y], { quique: 0.75, dur: 0.65 });
        curva(kO(L), [[ti, 0, null, 30], [ti + 0.16, 100, 60, null]]);
        kS(L).expression = 'var s = value; var k = 1 + 0.014 * Math.sin(time*2.2+' + idx + '); [s[0]*k, s[1]*k]';
        return;
    }
    // 'palavra': estala de ZERO a ~125% e assenta (sobe do chão, balança e respira)
    mola(kP(L), ti, [x, y + tam * 0.7], [x, y], { quique: 0.75, dur: 0.65 });
    mola(kS(L), ti, [0, 0], [100, 100], { quique: 0.84, dur: 0.62 });
    mola(kR(L), ti, (idx % 2 ? 1 : -1) * 10, 0, { quique: 0.7, dur: 0.6 });
    curva(kO(L), [[ti, 0, null, 30], [ti + 0.05, 100, 60, null]]);
    kP(L).expression = 'value + [Math.sin(time*1.9+' + idx + ')*' + drift.toFixed(1) + ', Math.cos(time*1.5+' + idx + ')*' + drift.toFixed(1) + ']';
}

// ---------------------------------------------------------------- imagem
function imagem(comp, arquivo, o) {
    o = o || {};
    var f = new File(arquivo), L;
    var w = o.w || 600, h = o.h || 600;
    var it;
    try { it = app.project.importFile(new ImportOptions(f)); } catch (er) { KIT.avisos.push('imagem não achada: ' + arquivo); L = forma(comp, 'retangulo', { x: o.x, y: o.y, w: w, h: h, cor: '#888888', parent: o.parent, r: o.r }); return L; }
    L = comp.layers.add(it); L.name = o.nome || 'imagem';
    if (o.parent) kPai(L, o.parent);
    var k = Math.max(w / it.width, h / it.height) * 100;   // cobre a caixa (cover)
    kS(L).setValue([k, k]);
    kP(L).setValue([o.x == null ? comp.width / 2 : o.x, o.y == null ? comp.height / 2 : o.y]);
    // máscara na caixa (em coordenadas da camada: antes da escala, em px da imagem)
    var mw = w / k * 100 / 2, mh = h / k * 100 / 2;
    kMascaraRect(L, it.width / 2 - mw, it.height / 2 - mh, it.width / 2 + mw, it.height / 2 + mh, (o.r || 0) / k * 100);
    kA(L).setValue([it.width / 2, it.height / 2]);
    if (o.entrada) kEntrada(L, o.t0 || 0, o.entrada, o.quique);
    return L;
}

// ---------------------------------------------------------------- celular
/* celular(comp, {tela, x, y, escala, parent, t0, entrada}): um iPhone de formas.
   tela = '#cor' (ou caminho de imagem). Devolve {no, tela:{w,h}, toque(t,lx,ly), ...}.
   As coordenadas locais têm a origem no centro da tela; 1 unidade = 1 px do celular a 100%. */
function celular(comp, o) {
    o = o || {};
    var no = comp.layers.addNull(comp.duration); no.name = 'celular'; no.enabled = false;
    if (o.parent) kPai(no, o.parent);
    var esc = (o.escala || 1) * 100;
    kA(no).setValue([0, 0]);
    kP(no).setValue([o.x == null ? comp.width / 2 : o.x, o.y == null ? comp.height / 2 : o.y]);
    kS(no).setValue([esc, esc]);
    var C = { no: no, comp: comp, tela: { w: 430, h: 920 }, filhos: [], dedo: null, fim: 0 };
    var corpo = forma(comp, 'retangulo', { x: 0, y: 0, w: 470, h: 960, r: 84, cor: o.corpo || '#17171C', traco: '#4A4A55', tracoL: 5, parent: no, nome: 'corpo' });
    var tc = o.tela || '#F6F6F8', telaL;
    if (String(tc).charAt(0) == '#') telaL = forma(comp, 'retangulo', { x: 0, y: 0, w: 430, h: 920, r: 66, cor: tc, parent: no, nome: 'tela' });
    else telaL = imagem(comp, tc, { x: 0, y: 0, w: 430, h: 920, r: 66, parent: no, nome: 'tela' });
    var ilha = forma(comp, 'pilula', { x: 0, y: -418, w: 128, h: 36, cor: '#050507', parent: no, nome: 'ilha' });
    var barra = forma(comp, 'pilula', { x: 0, y: 424, w: 140, h: 6, cor: '#101014', alfa: 55, parent: no, nome: 'barra' });
    C.corpo = corpo; C.telaL = telaL; C.ilha = ilha;
    // entrada do conjunto: sobe do chão, gira um pouco e assenta
    var t0 = o.t0 || 0, x = kP(no).value[0], y = kP(no).value[1];
    if (o.entrada !== false) {
        mola(kP(no), t0, [x, y + comp.height * 0.18], [x, y], { quique: 0.4, dur: 0.85 });
        mola(kR(no), t0, -7, 0, { quique: 0.5, dur: 0.9 });
        mola(kS(no), t0, [esc * 0.82, esc * 0.82], [esc, esc], { quique: 0.4, dur: 0.8 });
        som(t0 + 0.05, 'toque', 0.9);
    }
    /* toque(t, lx, ly, {nome}): o dedo (um círculo translúcido) vai até o ponto, afunda e solta uma onda. */
    C.toque = function (t, lx, ly, op) {
        op = op || {};
        if (!C.dedo) {
            C.dedo = forma(comp, 'circulo', { x: 0, y: 520, w: 70, cor: '#FFFFFF', alfa: 0, parent: no, nome: 'dedo' });
            C.dedoPos = [60, 520];
            kP(C.dedo).setValue([60, 520]);
            C.dedo.property('ADBE Effect Parade').addProperty('ADBE Drop Shadow');
        }
        var d = C.dedo, de = C.dedoPos;
        curva(kP(d), [[t - 0.5, de, 30, 20], [t - 0.04, [lx, ly], 78, 30]]);
        curva(kO(d), [[t - 0.5, de[1] > 400 ? 0 : 85, null, 30], [t - 0.3, 85, 60, 30]]);
        curva(kS(d), [[t - 0.04, [100, 100], null, 25], [t + 0.06, [72, 72], 70, 40], [t + 0.22, [100, 100], 70, null]]);
        C.dedoPos = [lx + 40, ly + 70];
        var r = forma(comp, 'circulo', { x: lx, y: ly, w: 90, traco: op.cor || '#FFFFFF', tracoL: 6, alfa: 0, parent: no, nome: 'onda' });
        curva(kS(r), [[t, [30, 30], null, 20], [t + 0.45, [210, 210], 80, null]]);
        curva(kO(r), [[t - 0.04, 0, null, 30], [t, 70, null, 30], [t + 0.45, 0, 70, null]]);
        som(t, 'clique', 0.9);
        C.fim = Math.max(C.fim, t + 0.45);
        return r;
    };
    C.sai = function (t) {   // o dedo some
        if (C.dedo) curva(kO(C.dedo), [[t, 85, null, 30], [t + 0.25, 0, 60, null]]);
    };
    return C;
}

/* botao(comp, {x,y,w,h,rotulo,cor,corTexto,parent,t0}) + botao.aperta(t): o botão que afunda e volta. */
function botao(comp, o) {
    // a pílula cabe o rótulo: o texto() não desce de 3,6% da altura (fora de aparelho), então mede ANTES e alarga (nunca texto vazando do botão)
    var tamB = (o.tam || 44) * KIT.ajuste * KIT.ajusteCena; if (kEhCam(o.parent || null) || !o.parent) tamB = Math.max(tamB, comp.height * 0.036);
    var wr = kMedir(comp, String(o.rotulo || 'Toque aqui'), o.fonte || (KIT.marca && KIT.marca.titulo) || 'Poppins', tamB);
    o.w = Math.min(comp.width * 0.84, Math.max(o.w || 360, wr / 0.86 + tamB * 0.4)); o.h = Math.max(o.h || 110, tamB * 1.6);
    var b = forma(comp, 'pilula', { x: o.x, y: o.y, w: o.w || 360, h: o.h || 110, cor: o.cor || '#FFFFFF', parent: o.parent, nome: 'botao', entrada: o.entrada === false ? null : 'pop', t0: o.t0 || 0, quique: 0.6 });
    var tx = texto(comp, o.rotulo || 'Toque aqui', { x: o.x, y: o.y, tamanho: o.tam || 44, cor: o.corTexto || '#000000', parent: o.parent, entrada: 'sobe', t0: (o.t0 || 0) + 0.08, largura: (o.w || 360) * 0.86, maxLinhas: 1, fonte: o.fonte });
    return { forma: b, texto: tx, w: o.w, h: o.h, aperta: function (t) {
        // 100%: o valor lido de uma propriedade com chaves é o do tempo 0 (a mola de entrada começa em 0)
        curva(kS(b), [[t, [100, 100], null, 25], [t + 0.07, [90, 90], 70, 40], [t + 0.25, [100, 100], 70, null]]);
        som(t, 'clique', 0.9);
    } };
}

// ---------------------------------------------------------------- trocas de cena
/* troca(cenaA, cenaB, t, tipo): cenaA e cenaB são camadas (comps de cena) na MAIN; a troca roda em [t, t+DUR]. B entra
   por cima de A. TROCAS (15): encolhe · empurra · mascara · zoom · flash · persiana · chicote · desfoque · cortina ·
   pontos · giro · tremor · abre · dissolve · corte. */
var TROCA_DUR = 0.45;
var TROCAS_TODAS = ['encolhe', 'empurra', 'mascara', 'zoom', 'persiana', 'chicote', 'desfoque', 'cortina', 'pontos', 'giro', 'tremor', 'abre', 'dissolve', 'corte'];
function kBlur(L, de, para, t, d) {
    var b = kFx(L, 'ADBE Gaussian Blur 2');
    curva(b.property('ADBE Gaussian Blur 2-0001'), [[t, de, null, 25], [t + d, para, 75, null]]);
}
function kMascaraAnim(L, de, para, t, d) {
    var m = L.property('ADBE Mask Parade').addProperty('ADBE Mask Atom');
    var p = m.property('ADBE Mask Shape');
    p.setValue(de);
    p.setValueAtTime(kFrame(t), de);
    p.setValueAtTime(kFrame(t) + d, para);
    return m;
}
function troca(cenaA, cenaB, t, tipo, cor) {
    var comp = KIT.main || cenaA.containingComp, W = comp.width, H = comp.height, cx = W / 2, cy = H / 2, d = TROCA_DUR, i, u = Math.min(W, H) / 1080;
    tipo = tipo || 'encolhe';
    var vert = H > W, R = Math.sqrt(W * W + H * H) / 2 + 20;
    cor = cor || '#FFFFFF';
    if (tipo == 'empurra') {
        var fora = vert ? [cx, cy - H] : [cx - W, cy], de = vert ? [cx, cy + H] : [cx + W, cy];
        curva(kP(cenaA), [[t, [cx, cy], null, 25], [t + d, fora, 78, null]]);
        curva(kP(cenaB), [[t, de, null, 25], [t + d, [cx, cy], 78, null]]);
    } else if (tipo == 'mascara') {
        kMascaraAnim(cenaB, kCircShape(cx, cy, 2), kCircShape(cx, cy, R), t, d + 0.1);
        curva(kS(cenaA), [[t, [100, 100], null, 30], [t + d + 0.1, [108, 108], 70, null]]);
    } else if (tipo == 'zoom') {
        curva(kS(cenaA), [[t, [100, 100], null, 22], [t + d, [190, 190], 80, null]]);
        curva(kO(cenaA), [[t + d * 0.3, 100, null, 30], [t + d, 0, 70, null]]);
        curva(kS(cenaB), [[t, [58, 58], null, 22], [t + d, [100, 100], 80, null]]);
        curva(kO(cenaB), [[t, 0, null, 30], [t + d * 0.6, 100, 70, null]]);
    } else if (tipo == 'persiana') {
        var n = 6;
        for (i = 0; i < n; i++) {
            var f0 = vert ? [0, i * H / n, W, i * H / n] : [i * W / n, 0, i * W / n, H], f1 = vert ? [0, i * H / n, W, (i + 1) * H / n + 2] : [i * W / n, 0, (i + 1) * W / n + 2, H];
            kMascaraAnim(cenaB, kRetShape(f0[0], f0[1], f0[2], f0[3], 0), kRetShape(f1[0], f1[1], f1[2], f1[3], 0), t + i * 0.035, d * 0.7);
        }
    } else if (tipo == 'chicote') {
        var dx = W * 0.9;
        curva(kP(cenaA), [[t, [cx, cy], null, 15], [t + d * 0.8, [cx - dx, cy], 85, null]]);
        kBlur(cenaA, 0, 26 * u, t, d * 0.8);
        curva(kP(cenaB), [[t + d * 0.1, [cx + dx, cy], null, 15], [t + d, [cx, cy], 85, null]]);
        kBlur(cenaB, 26 * u, 0, t + d * 0.1, d * 0.9);
    } else if (tipo == 'desfoque') {
        kBlur(cenaA, 0, 24 * u, t, d); curva(kO(cenaA), [[t, 100, null, 25], [t + d, 0, 75, null]]);
        kBlur(cenaB, 24 * u, 0, t, d); curva(kO(cenaB), [[t, 0, null, 25], [t + d, 100, 75, null]]);
    } else if (tipo == 'cortina') {
        var pn = comp.layers.addSolid(kCor(cor), 'cortina', W, H, 1);
        var d0 = vert ? [cx, cy + H] : [cx + W, cy], d1 = [cx, cy], d2 = vert ? [cx, cy - H] : [cx - W, cy];
        curva(kP(pn), [[t, d0, null, 20], [t + d * 0.5, d1, 70, 20], [t + d, d2, 70, null]]);
        curva(kO(cenaB), [[t, 0, null, null], [t + d * 0.5, 100, null, null]]);
        curva(kO(cenaA), [[t + d * 0.5, 100, null, null], [t + d * 0.5 + 0.1, 0, null, null]]);
    } else if (tipo == 'pontos') {
        var cols = vert ? 3 : 5, rows = vert ? 5 : 3, rr = Math.max(W / cols, H / rows) * 0.78;
        for (i = 0; i < cols * rows; i++) {
            var c0 = i % cols, r0 = Math.floor(i / cols), px = (c0 + 0.5) * W / cols, py = (r0 + 0.5) * H / rows;
            kMascaraAnim(cenaB, kCircShape(px, py, 1), kCircShape(px, py, rr), t + (c0 + r0) * 0.03, d * 0.7);
        }
    } else if (tipo == 'giro') {
        curva(kR(cenaA), [[t, 0, null, 25], [t + d, -22, 78, null]]);
        curva(kS(cenaA), [[t, [100, 100], null, 25], [t + d, [60, 60], 78, null]]);
        curva(kO(cenaA), [[t, 100, null, 25], [t + d, 0, 78, null]]);
        curva(kR(cenaB), [[t, 22, null, 25], [t + d, 0, 78, null]]);
        curva(kS(cenaB), [[t, [140, 140], null, 25], [t + d, [100, 100], 78, null]]);
        curva(kO(cenaB), [[t, 0, null, 25], [t + d * 0.6, 100, 78, null]]);
    } else if (tipo == 'tremor') {
        var tk = [], tk2 = [];
        for (i = 0; i < 7; i++) { var jx = (i % 2 ? 1 : -1) * (46 - i * 5) * u, jy = (i % 3 - 1) * (24 - i * 3) * u; tk.push([t + i * 0.033, [cx + jx, cy + jy], -1, -1]); tk2.push([t + 0.1 + i * 0.033, [cx - jx * 0.6, cy - jy * 0.6], -1, -1]); }
        tk.unshift([t - 0.1, [cx, cy], null, null]);   // antes da troca a cena fica CENTRADA (a 1ª chave de jitter deslocava a cena e mostrava a borda da comp)
        tk.push([t + 0.26, [cx, cy], -1, null]); curva(kP(cenaA), tk);
        curva(kO(cenaA), [[t + 0.06, 100, null, null], [t + 0.14, 0, null, null]]);
        curva(kO(cenaB), [[t + 0.04, 0, null, null], [t + 0.12, 100, null, null]]);
        tk2.unshift([t, [cx, cy], null, null]);
        tk2.push([t + 0.36, [cx, cy], -1, null]); curva(kP(cenaB), tk2);
    } else if (tipo == 'abre') {
        var rw = vert ? [0, cy - 4, W, cy + 4] : [cx - 4, 0, cx + 4, H], rf = [0, 0, W, H];
        kMascaraAnim(cenaB, kRetShape(rw[0], rw[1], rw[2], rw[3], 0), kRetShape(rf[0], rf[1], rf[2], rf[3], 0), t, d);
    } else if (tipo == 'dissolve') {
        curva(kO(cenaA), [[t, 100, null, 30], [t + d, 0, 70, null]]);
        curva(kO(cenaB), [[t, 0, null, 30], [t + d, 100, 70, null]]);
        curva(kS(cenaB), [[t, [104, 104], null, 30], [t + d, [100, 100], 70, null]]);
    } else if (tipo == 'corte') {
        curva(kO(cenaB), [[t - 0.08, 0, null, null], [t, 100, null, null]]);
        curva(kS(cenaB), [[t, [109, 109], null, 18], [t + 0.28, [100, 100], 80, null]]);
    } else {   // encolhe: A afunda e some, B cresce de fora para o quadro
        curva(kS(cenaA), [[t, [100, 100], null, 25], [t + d, [72, 72], 78, null]]);
        curva(kO(cenaA), [[t, 100, null, 25], [t + d, 0, 78, null]]);
        curva(kS(cenaB), [[t, [128, 128], null, 25], [t + d, [100, 100], 78, null]]);
        curva(kO(cenaB), [[t, 0, null, 25], [t + d * 0.7, 100, 78, null]]);
    }
    // o som da troca em CAMADAS: riser antes, whoosh na virada, impacto e sub-drop no pouso (o Node escolhe um arquivo diferente para cada)
    som(Math.max(0.02, t - 0.62), 'sobe', 0.5);
    som(t - 0.05, 'troca', 0.85);
    som(t + 0.12, 'impacto', 0.7); som(t + 0.12, 'sub', 0.5);
    if (tipo == 'tremor' || tipo == 'corte') som(t - 0.02, 'glitch', 0.4);
}

// ---------------------------------------------------------------- elementos reais, caminhos, partículas
/* elemento(comp, i|obj, {x,y,w,h,parent,t0,entrada,quique,flutua,nome}): um elemento REAL do assunto (PNG/SVG/sequência de
   PNG de um GIF animado), encaixado na caixa w×h sem cortar. KIT.elementos vem do plano (Node baixou e converteu). */
function kElem(i) { var n = KIT.elementos.length; return n ? KIT.elementos[(((i + KIT.cenaIdx) % n) + n) % n] : null; }   // cada cena começa por um elemento diferente
function kPoeEl(comp, it, e, o, t) {
    var L = comp.layers.add(it); L.name = o.nome || ('el ' + e.nome);
    if (o.parent) kPai(L, o.parent);
    var w = o.w || 300, h = o.h || w, k = Math.min(w / it.width, h / it.height) * 100;
    kS(L).setValue([k, k]);
    kP(L).setValue([o.x == null ? comp.width / 2 : o.x, o.y == null ? comp.height / 2 : o.y]);
    if (t) L.startTime = t;
    if (o.rot) kR(L).setValue(o.rot);
    return L;
}
function elemento(comp, e, o) {
    o = o || {};
    if (typeof e == 'number') e = kElem(e);
    var w = o.w || 300, h = o.h || w, L = null;
    if ((!o.parent || kEhCam(o.parent)) && w < comp.width * 0.12 && o.nome != 'ambiente') { var kk = comp.width * 0.12 / w; w *= kk; h *= kk; o.w = w; o.h = h; }   // ícone de apoio ≥ 12% da largura
    var rectEl = null, fx = o.folgaX || 0, fy = o.folgaY || 0;   // folga: o balanço do elemento (a guarda reserva o envelope inteiro)
    if ((!o.parent || kEhCam(o.parent)) && o.x != null) { var ze0 = kZonaTexto(comp, false); o.x = Math.max(ze0.x + w / 2, Math.min(comp.width - ze0.x - w / 2, Math.round(o.x / 8) * 8)); o.y = Math.max(ze0.top + h / 2, Math.min(comp.height - ze0.bot - h / 2, Math.round(o.y / 8) * 8)); if (o.nome != 'ambiente') { rectEl = { espaco: (o.parent && o.parent.name == 'CONTEUDO') ? 'c' : 't', cena: KIT.cenaIdx, x0: o.x - w / 2 - fx, x1: o.x + w / 2 + fx, y0: o.y - h / 2 - fy, y1: o.y + h / 2 + fy, t0: 0, t1: 1, texto: false, sobrepoe: true, nome: 'elemento ' + (e && e.nome || ''), camadas: [], infla: (w + 2 * fx) / w }; KIT.rects.push(rectEl); } }
    if (!e) {
        L = forma(comp, 'circulo', { x: o.x, y: o.y, w: Math.min(w, h) * 0.85, cor: o.corFallback || '#8A8A8A', parent: o.parent, nome: 'elemento-reserva' });
        if (rectEl) rectEl.camadas.push(L);
    } else {
        KIT.cache = KIT.cache || {};
        var it = KIT.cache[e.arquivo];
        if (!it) {
            var op = new ImportOptions(new File(e.arquivo));
            if (e.tipo == 'seq') op.sequence = true;
            try { it = app.project.importFile(op); KIT.cache[e.arquivo] = it; }
            catch (er) { KIT.avisos.push('cena ' + KIT.cenaIdx + ': elemento não importou: ' + e.arquivo); it = null; }
        }
        if (!it) return elemento(comp, null, o);
        L = kPoeEl(comp, it, e, o, o.t0 && e.tipo == 'seq' ? o.t0 : 0);
        if (rectEl) rectEl.camadas.push(L);
        if (e.tipo == 'seq' && it.duration > 0.1) {   // o GIF acaba em ~1 s: repete até o fim da cena
            var n, dur = it.duration, ini = (o.t0 || 0) + dur;
            for (n = 0; n < 12 && ini < comp.duration; n++, ini += dur) { var Lr = kPoeEl(comp, it, e, o, ini); if (rectEl) rectEl.camadas.push(Lr); }
        }
        KIT.nElem = (KIT.nElem || 0) + 1;
    }
    L.comment = String(o.t0 || 0);
    if (o.entrada) kEntrada(L, o.t0 || 0, o.entrada, o.quique);
    if (o.flutua) {
        var a = o.flutua * Math.min(comp.width, comp.height) / 1080, ph = (o.fase || 0);
        kP(L).expression = 'value + [Math.sin(time*1.7+' + ph + ')*' + a.toFixed(1) + ', Math.cos(time*1.3+' + ph + ')*' + (a * 1.1).toFixed(1) + ']';
    }
    return L;
}
// sombra de contato: uma elipse borrada no chão do objeto
function sombraChao(comp, x, y, w, o) {
    o = o || {};
    var L = forma(comp, 'circulo', { x: x, y: y, w: w, cor: o.cor || '#000000', alfa: o.alfa == null ? 28 : o.alfa, parent: o.parent, nome: 'sombra' });
    kS(L).setValue([100, 26]);
    try { var b = kFx(L, 'ADBE Gaussian Blur 2'); b.property('ADBE Gaussian Blur 2-0001').setValue(w * 0.09); } catch (e) { }
    if (o.t0 != null) curva(kO(L), [[o.t0, 0, null, 30], [o.t0 + 0.4, o.alfa == null ? 28 : o.alfa, 60, null]]);
    return L;
}
/* caminho(comp, pontos, {cor, esp, t0, dur, parent, fecha, preenche, suave}): polilinha (ou polígono) com o traço desenhado por trim. */
function caminho(comp, pts, o) {
    o = o || {};
    var L = comp.layers.addShape(); L.name = o.nome || 'caminho'; L.comment = 'caminho';   // o sourceRect de um caminho absoluto inclui a origem: fica fora da medida do herói
    if (o.parent) kPai(L, o.parent);
    var g = L.property('ADBE Root Vectors Group').addProperty('ADBE Vector Group');
    var v = g.property('ADBE Vectors Group');
    var pa = v.addProperty('ADBE Vector Shape - Group');
    var S = new Shape(), vs = [], it = [], ot = [], i;
    for (i = 0; i < pts.length; i++) { vs.push([pts[i][0], pts[i][1]]); it.push([0, 0]); ot.push([0, 0]); }
    if (o.suave) {
        for (i = 1; i < pts.length - 1; i++) { var dx = (pts[i + 1][0] - pts[i - 1][0]) / 6, dy = (pts[i + 1][1] - pts[i - 1][1]) / 6; it[i] = [-dx, -dy]; ot[i] = [dx, dy]; }
        if (pts.length > 1) { ot[0] = [(pts[1][0] - pts[0][0]) / 3, (pts[1][1] - pts[0][1]) / 3]; it[pts.length - 1] = [-(pts[pts.length - 1][0] - pts[pts.length - 2][0]) / 3, -(pts[pts.length - 1][1] - pts[pts.length - 2][1]) / 3]; }
    }
    S.vertices = vs; S.inTangents = it; S.outTangents = ot; S.closed = !!o.fecha;
    pa.property('ADBE Vector Shape').setValue(S);
    if (o.preenche) v.addProperty('ADBE Vector Graphic - Fill').property('ADBE Vector Fill Color').setValue(kCor(o.preenche));
    if (o.cor) {
        var st = v.addProperty('ADBE Vector Graphic - Stroke');
        st.property('ADBE Vector Stroke Color').setValue(kCor(o.cor));
        st.property('ADBE Vector Stroke Width').setValue(o.esp || 8);
        try { st.property('ADBE Vector Stroke Line Cap').setValue(2); st.property('ADBE Vector Stroke Line Join').setValue(2); } catch (e) { }
    }
    kP(L).setValue([0, 0]);
    if (o.alfa != null) kO(L).setValue(o.alfa);
    if (o.t0 != null && o.dur) {
        var tm = v.addProperty('ADBE Vector Filter - Trim');
        curva(tm.property('ADBE Vector Trim End'), [[o.t0, 0, null, 20], [o.t0 + o.dur, 100, 80, null]]);
    }
    return L;
}
/* grupo(comp, parent, x, y): um null para agrupar peças que se movem juntas (câmera em cadeia, cartões, painéis). */
function grupo(comp, o) {
    o = o || {};
    var no = comp.layers.addNull(comp.duration); no.name = o.nome || 'grupo'; no.enabled = false;
    if (o.parent) kPai(no, o.parent);
    kA(no).setValue([0, 0]);
    kP(no).setValue([o.x == null ? comp.width / 2 : o.x, o.y == null ? comp.height / 2 : o.y]);
    if (o.esc) kS(no).setValue([o.esc * 100, o.esc * 100]);
    return no;
}
/* particulas(comp, {n, cor, parent, tipo:'sobe'|'cai'|'flutua', raio, area:[x0,y0,x1,y1], t0}) */
function particulas(comp, o) {
    o = o || {};
    var n = o.n || 24, i, W = comp.width, H = comp.height, u = Math.min(W, H) / 1080, ar = o.area || [0, 0, W, H], out = [];
    for (i = 0; i < n; i++) {
        var rnd = function (k) { var q = Math.sin((i + 1) * 12.9898 + k * 78.233) * 43758.5453; return q - Math.floor(q); };
        var r = (o.raio || 14) * u * (0.5 + rnd(1)), x = ar[0] + rnd(2) * (ar[2] - ar[0]), y = ar[1] + rnd(3) * (ar[3] - ar[1]);
        var L = forma(comp, o.forma || 'circulo', { x: x, y: y, w: r * 2, h: r * 2 * (o.forma == 'retangulo' ? 0.5 : 1), cor: (o.cores ? o.cores[i % o.cores.length] : o.cor || '#FFFFFF'), parent: o.parent, nome: 'particula', alfa: 0 });
        var vel = 0.12 + rnd(4) * 0.2, off = rnd(5), amp = (20 + rnd(6) * 40) * u, f = 0.8 + rnd(7) * 1.6, dir = o.tipo == 'cai' ? 1 : -1, alc = (ar[3] - ar[1]) * (o.tipo == 'flutua' ? 0.12 : 0.8);
        kP(L).expression = 'var p = value; var q = ((time * ' + vel.toFixed(3) + ' + ' + off.toFixed(3) + ') % 1); [p[0] + Math.sin(time * ' + f.toFixed(2) + ' + ' + (i * 1.7).toFixed(2) + ') * ' + amp.toFixed(1) + ', p[1] + ' + dir + ' * q * ' + alc.toFixed(1) + ']';
        kO(L).expression = 'var q = ((time * ' + vel.toFixed(3) + ' + ' + off.toFixed(3) + ') % 1); Math.sin(Math.PI * q) * ' + (o.alfa || 70) + ' * (time > ' + (o.t0 || 0) + ' ? 1 : 0)';
        if (o.gira) kR(L).expression = 'time * ' + (30 + rnd(8) * 90).toFixed(0);
        out.push(L);
    }
    return out;
}

// ---------------------------------------------------------------- legenda por palavra (na MAIN)
/* legenda(main, chunks, o): faixa escura arredondada com o trecho falado; a palavra já dita acende, a palavra-chave
   fica na cor de destaque. chunks = [{t, t2, palavras:[{w, t, t2, chave}]}] (maquina-voz.js legendaChunks). */
function legenda(main, chunks, o) {
    o = o || {};
    var W = main.width, H = main.height, u = Math.min(W, H) / 1080, tam0 = (o.tamanho || 54) * u, fonte = o.fonte || 'Poppins';
    var yc = H * (o.y || 0.905), c, i, l;
    for (c = 0; c < chunks.length; c++) {
        var ch = chunks[c], P = ch.palavras, tam = tam0, sw = tam * 0.28;
        // a FRASE inteira, em até 2 linhas equilibradas (nunca um fragmento)
        var total = 0; for (i = 0; i < P.length; i++) total += P[i].w.length + 1;
        var nl = total > 34 ? 2 : 1, linhas = [[]], acum = 0;
        for (i = 0; i < P.length; i++) { if (nl == 2 && linhas.length == 1 && acum + P[i].w.length / 2 > total / 2 && linhas[0].length) linhas.push([]); linhas[linhas.length - 1].push(i); acum += P[i].w.length + 1; }
        var ws = [], lw = [], maxLw = 0;
        for (i = 0; i < P.length; i++) ws.push(kMedir(main, P[i].w, P[i].chave ? (o.fonteDestaque || fonte) : fonte, tam));
        for (l = 0; l < linhas.length; l++) { var t0w = 0, q; for (q = 0; q < linhas[l].length; q++) t0w += ws[linhas[l][q]] + (q ? sw : 0); lw.push(t0w); maxLw = Math.max(maxLw, t0w); }
        var esc = maxLw > W * 0.88 ? W * 0.88 / maxLw : 1;
        if (esc < 1) { tam *= esc; sw *= esc; for (i = 0; i < ws.length; i++) ws[i] *= esc; for (l = 0; l < lw.length; l++) lw[l] *= esc; maxLw *= esc; }
        var t = ch.t, t2 = Math.max(ch.t2, ch.t + 0.25), fim = c + 1 < chunks.length ? Math.min(t2 + 0.3, chunks[c + 1].t) : t2 + 0.3;
        var lh = tam * 1.45, hp = linhas.length * lh + tam * 0.35, ytop = yc - (linhas.length - 1) * lh / 2;
        var pill = forma(main, 'pilula', { x: W / 2, y: yc, w: maxLw + tam * 1.2, h: hp, cor: o.fundo || '#000000', alfa: o.alfaFundo == null ? 62 : o.alfaFundo, nome: 'legenda-fundo' });
        pill.inPoint = t - 0.04; pill.outPoint = fim;
        for (l = 0; l < linhas.length; l++) {
            var xs = W / 2 - lw[l] / 2, yy = ytop + l * lh;
            for (q = 0; q < linhas[l].length; q++) {
                i = linhas[l][q];
                var p = P[i], cx = xs + ws[i] / 2; xs += ws[i] + sw;
                var L = main.layers.addText(p.w); L.name = 'legenda ' + p.w;
                kDoc(L, p.chave ? (o.fonteDestaque || fonte) : fonte, tam, p.chave ? (o.corDestaque || '#FFD54A') : '#FFFFFF');
                kP(L).setValue([cx, yy + tam * 0.34]);
                L.inPoint = t - 0.04; L.outPoint = fim;
                var op = kO(L), tp = Math.max(p.t, t);
                op.setValueAtTime(kFrame(t - 0.04), 42);
                if (kFrame(tp) > kFrame(t - 0.04) + 0.02) { op.setValueAtTime(kFrame(tp) - 1 / KIT.fps, 42); }
                op.setValueAtTime(kFrame(tp), 100);
            }
        }
    }
}

// ---------------------------------------------------------------- conferência de layout (por cena)
/* Fora da área segura e sobreposição de textos que existem juntos: escreve "AVISO cena N: ..." para o Node refazer. */
function kRectLayout(r) {
    if (r.espaco != 'c' || !KIT.remap) return r;
    var m = KIT.remap, q = {}, k;
    for (k in r) q[k] = r[k];
    q.x0 = m.px + (r.x0 - m.ax) * m.s; q.x1 = m.px + (r.x1 - m.ax) * m.s;
    q.y0 = m.py + (r.y0 - m.ay) * m.s; q.y1 = m.py + (r.y1 - m.ay) * m.s;
    return q;
}
function kConfereCena(comp) {
    if (KIT.cenaHeroi) {   // cena de herói: o elemento maior ocupa ≥ 45% da largura útil
        var hmax = 0, hh;
        for (hh = 0; hh < KIT.rects.length; hh++) { var rr0 = KIT.rects[hh]; if (rr0.cena == KIT.cenaIdx && !rr0.texto && String(rr0.nome).indexOf('elemento') == 0) hmax = Math.max(hmax, rr0.x1 - rr0.x0); }
        if (hmax > 0 && hmax < comp.width * 0.84 * 0.45 - 2) KIT.avisos.push('cena ' + KIT.cenaIdx + ': herói pequeno (' + Math.round(hmax) + ' px < 45% da largura útil)');
    }
    var W = comp.width, H = comp.height, R = KIT.rects, i, j, a, b, zt = kZonaTexto(comp, true), ze = kZonaTexto(comp, false), mx = ze.x - 2, my = ze.top - 2;
    var mine = [];
    for (i = 0; i < R.length; i++) if (R[i].cena == KIT.cenaIdx && !R[i].conferido && !R[i].fora) { R[i].conferido = true; mine.push(kRectLayout(R[i])); }
    for (i = 0; i < mine.length; i++) {
        a = mine[i];
        if (a.banda) continue;
        for (j = 0; j < mine.length; j++) if (mine[j].banda && !a.titulo) { b = mine[j]; if (a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 - 2 && a.y1 > b.y0 + 2 && !a.livre) KIT.avisos.push('cena ' + KIT.cenaIdx + ': invade a faixa do título: ' + String(a.nome).slice(0, 30)); }
        if (a.livre) continue;
        if (a.espaco == 'c' && KIT.remap) continue;   // com o conteúdo ajustado à zona, quem mede é a auditoria quadro a quadro (matrizes reais)
        if (a.texto) { if (a.x0 < zt.x - 6 || a.x1 > W - zt.x + 6 || a.y0 < zt.top - 6 || a.y1 > H - zt.bot + 6) KIT.avisos.push('cena ' + KIT.cenaIdx + ': texto fora da área segura: ' + a.nome); }
        else if (a.x0 < mx || a.x1 > W - mx || a.y0 < my || a.y1 > H - ze.bot + 2) KIT.avisos.push('cena ' + KIT.cenaIdx + ': elemento fora da área segura: ' + a.nome);
        for (j = i + 1; j < mine.length; j++) {
            b = mine[j];
            if (a.sobrepoe || b.sobrepoe || a.livre || b.livre || !a.texto || !b.texto) continue;
            if (Math.max(a.t0, b.t0) >= Math.min(a.t1, b.t1) - 0.2) continue;
            var ox = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0), oy = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0);
            if (ox > 4 && oy > 4) {
                var ar = Math.min((a.x1 - a.x0) * (a.y1 - a.y0), (b.x1 - b.x0) * (b.y1 - b.y0));
                if (ox * oy > ar * 0.1) KIT.avisos.push('cena ' + KIT.cenaIdx + ': textos se sobrepõem: ' + a.nome + ' / ' + b.nome);
            } else if (ox > 4 && oy <= 4) {   // um acima do outro: respiro mínimo = metade da altura do texto
                var gap = -oy, min = 0.5 * Math.min(a.tam || 0, b.tam || 0);
                if (gap < min - 1) KIT.avisos.push('cena ' + KIT.cenaIdx + ': pouco espaço entre textos (' + Math.round(gap) + ' < ' + Math.round(min) + ' px): ' + a.nome + ' / ' + b.nome);
            }
        }
    }
}

// ---------------------------------------------------------------- a guarda de layout (kit-ae/guarda.jsx), com as caixas reais
/* kGuarda(comp): no fecha() de cada cena, ANTES do render. Junta as caixas registradas (texto medido com sourceRectAtTime, elementos
   com o envelope do balanço, a foto) e roda gConsertar: ícone sobre texto/foto/outro ícone muda de lugar, encolhe ou sai; a foto que
   encosta no texto sobe ou desce; o que não tem saída vira "AVISO cena N: guarda: troca a peça". Move por expressão (value + delta),
   que soma às chaves e ao balanço sem mexer neles. Escreve "GUARDA {json}" com o log e as caixas finais (o Node confere depois). */
function kZonaGuarda(comp) {
    var W = comp.width, H = comp.height, z = gZona(W, H, { legenda: KIT.enqDy > 0 }), a = kParaLayout(comp, z.x0, z.y0), b = kParaLayout(comp, z.x1, z.y1), tr = null;
    if (z.trilho) { var t = kParaLayout(comp, z.trilho[0], z.trilho[1]); tr = [t.x, t.y, W * 2, H * 2]; }
    return { x0: a.x, y0: a.y, x1: b.x, y1: b.y, trilho: tr };
}
function kExpr(prop) { var e = ''; try { e = prop.expression || ''; } catch (er) { e = ''; } return e ? '(' + e + ')' : 'value'; }
function kDesloca(L, dx, dy, k) {
    try {
        if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) { var p = kP(L); p.expression = 'var gv = ' + kExpr(p) + '; gv.length > 2 ? [gv[0] + ' + dx.toFixed(1) + ', gv[1] + ' + dy.toFixed(1) + ', gv[2]] : [gv[0] + ' + dx.toFixed(1) + ', gv[1] + ' + dy.toFixed(1) + ']'; }
        if (k && Math.abs(k - 1) > 0.005) { var sc = kS(L); sc.expression = 'var gs = ' + kExpr(sc) + '; gs.length > 2 ? [gs[0] * ' + k.toFixed(4) + ', gs[1] * ' + k.toFixed(4) + ', gs[2]] : [gs[0] * ' + k.toFixed(4) + ', gs[1] * ' + k.toFixed(4) + ']'; }
    } catch (e) { KIT.avisos.push('cena ' + KIT.cenaIdx + ': a guarda não conseguiu mover ' + L.name + ': ' + e); }
}
function kSome(L) { try { kO(L).expression = '0'; } catch (e) { } try { L.enabled = false; } catch (e2) { } }
function kGuarda(comp) {
    if (KIT.guarda === false) return;
    var W = comp.width, H = comp.height, R = KIT.rects, itens = [], refs = [], i, j;
    for (i = 0; i < R.length; i++) {
        var r0 = R[i];
        if (r0.cena != KIT.cenaIdx || r0.banda || r0.fora) continue;
        if (r0.texto && r0.livre) continue;   // texto dentro de aparelho (celular, janela): mora no desenho dele
        var r = kRectLayout(r0), tipo;
        if (r0.texto) tipo = 'texto';
        else if (r0.guarda == 'foto') tipo = 'foto';
        else if (!r0.camadas || !r0.camadas.length || KIT.cenaHeroi || (r.x1 - r.x0) / (r0.infla || 1) > W * 0.3) tipo = 'cartao';
        else tipo = 'icone';
        itens.push({ id: i, tipo: tipo, nome: tipo == 'texto' ? 'texto "' + String(r0.nome).slice(0, 24) + '"' : String(r0.nome || tipo).replace(/^elemento /, 'ícone '), c: [r.x0, r.y0, r.x1, r.y1], fixo: tipo == 'texto' || tipo == 'cartao', movel: tipo == 'foto', prio: tipo == 'icone' ? 1 : 5, infla: r0.infla || 1 });
        refs.push(r0);
    }
    if (!itens.length) return;
    var u = Math.min(W, H) / 1080;
    var res = gConsertar(itens, W, H, { zona: kZonaGuarda(comp), so: 'icones', folga: (KIT.guardaFolga || 10) * u, escalaMundo: KIT.enq * 1.03 });
    var caixas = [];
    for (j = 0; j < res.itens.length; j++) {
        var a = res.itens[j], q = refs[j], esc = (q.espaco == 'c' && KIT.remap) ? KIT.remap.s : 1, cl = q.camadas || [], m;
        caixas.push({ tipo: a.tipo, nome: a.nome, c: [Math.round(a.c[0]), Math.round(a.c[1]), Math.round(a.c[2]), Math.round(a.c[3])], fora: a.removido ? 1 : 0 });
        if (a.removido) { for (m = 0; m < cl.length; m++) kSome(cl[m]); q.fora = true; continue; }
        if (Math.abs(a.dx) > 0.5 || Math.abs(a.dy) > 0.5 || Math.abs(a.k - 1) > 0.005) {
            for (m = 0; m < cl.length; m++) kDesloca(cl[m], a.dx / esc, a.dy / esc, a.k);
            var cx = (q.x0 + q.x1) / 2 + a.dx / esc, cy = (q.y0 + q.y1) / 2 + a.dy / esc, hw = (q.x1 - q.x0) / 2 * a.k, hh = (q.y1 - q.y0) / 2 * a.k;
            q.x0 = cx - hw; q.x1 = cx + hw; q.y0 = cy - hh; q.y1 = cy + hh;
        }
    }
    if (res.trocar) KIT.avisos.push('cena ' + KIT.cenaIdx + ': guarda: troca a peça (' + res.log[res.log.length - 1] + ')');
    writeLn('GUARDA ' + JSON.stringify({ cena: KIT.cenaIdx, w: W, h: H, log: res.log, caixas: caixas, violacoes: res.violacoes }));
}

// ---------------------------------------------------------------- quando as coisas entram (a regra dos 30–40%)
/* kEntradaMax(comp): o instante (nominal) em que a ÚLTIMA camada de conteúdo fica visível, lido das chaves de opacidade, e o que
   kEntrada/kAnimaPalavra registraram. Cursor, ondas, partículas e atmosfera não contam: não são elementos novos. */
var KIT_NAO_ENTRADA = /^(fundo|brilho|vazamento|look|flash|ambiente|CAM|particula|sombra|onda|estouro|halo|cursor|dedo|seta|grade|rua|terra|rio|tampa|guia|linha-skel|barra-rolagem|digitando|badge)/;
function kEntradaMax(comp) {
    var mx = KIT.entMax, i;
    for (i = 1; i <= comp.numLayers; i++) {
        var L = comp.layer(i);
        if (KIT_NAO_ENTRADA.test(L.name) || L.nullLayer) continue;
        try {
            var tr = L.property('ADBE Transform Group'), vis = L.inPoint, k, t0;
            var op = tr.property('ADBE Opacity');
            if (op.numKeys > 0 && !op.expressionEnabled) { t0 = null; for (k = 1; k <= op.numKeys; k++) { if (op.keyValue(k) >= 40) { t0 = op.keyTime(k); break; } } if (t0 != null && t0 > vis) vis = t0; }
            var sc = tr.property('ADBE Scale');
            if (sc.numKeys > 0 && !sc.expressionEnabled) { t0 = null; for (k = 1; k <= sc.numKeys; k++) { if (sc.keyValue(k)[0] >= 40) { t0 = sc.keyTime(k); break; } } if (t0 != null && t0 > vis) vis = t0; }
            var cm = (L.comment != null && L.comment !== '') ? parseFloat(L.comment) : NaN;
            if (!isNaN(cm) && cm > vis) vis = cm;
            if (vis > mx) mx = vis;
        } catch (e) { }
    }
    return mx;
}

// ---------------------------------------------------------------- auditoria quadro a quadro
/* kAuditoria(comp): a cada 0,1 s (tempo real) mede a caixa de cada texto e de cada elemento JÁ com escala, posição, pais e câmera
   (matriz do layer × matriz dos pais, sourceRectAtTime para texto) e reprova: (1) fora da área segura, (2) colisão entre caixas visíveis
   no mesmo instante (texto sobre texto/elemento; só fundo e atmosfera ficam atrás). Só conta o que já assentou (entrada + 0,5 s) e
   termina antes da troca. Escreve "AVISO cena N: ..." (a máquina corrige e refaz) e "AUDIT {json}" com as contagens. */
function kMulMat(m, n) { return [m[0] * n[0] + m[2] * n[1], m[1] * n[0] + m[3] * n[1], m[0] * n[2] + m[2] * n[3], m[1] * n[2] + m[3] * n[3], m[0] * n[4] + m[2] * n[5] + m[4], m[1] * n[4] + m[3] * n[5] + m[5]]; }
function kMatLocal(L, t) {
    var tr = L.property('ADBE Transform Group');
    var p = tr.property('ADBE Position').valueAtTime(t, false), s = tr.property('ADBE Scale').valueAtTime(t, false), r = tr.property('ADBE Rotate Z').valueAtTime(t, false), an = tr.property('ADBE Anchor Point').valueAtTime(t, false);
    var rad = r * Math.PI / 180, cs = Math.cos(rad), sn = Math.sin(rad), sx = s[0] / 100, sy = s[1] / 100;
    var a = cs * sx, b = sn * sx, c = -sn * sy, d = cs * sy;
    return [a, b, c, d, p[0] - a * an[0] - c * an[1], p[1] - b * an[0] - d * an[1]];
}
function kMatMundo(L, t, cache) {
    var k = L.index;
    if (cache[k]) return cache[k];
    var m = kMatLocal(L, t);
    if (L.parent) m = kMulMat(kMatMundo(L.parent, t, cache), m);
    cache[k] = m;
    return m;
}
function kCaixaMundo(L, t, cache) {
    var r, m = kMatMundo(L, t, cache);
    if (L.name.indexOf('palavra ') == 0) { var q = L.sourceRectAtTime(t, false); r = [q.left, q.top, q.left + q.width, q.top + q.height]; }
    else { var sw = L.source ? L.source.width : 0, sh = L.source ? L.source.height : 0; r = [0, 0, sw, sh]; }
    var xs = [r[0], r[2], r[0], r[2]], ys = [r[1], r[1], r[3], r[3]], i, X = [], Y = [];
    for (i = 0; i < 4; i++) { X.push(m[0] * xs[i] + m[2] * ys[i] + m[4]); Y.push(m[1] * xs[i] + m[3] * ys[i] + m[5]); }
    return [Math.min(X[0], X[1], X[2], X[3]), Math.min(Y[0], Y[1], Y[2], Y[3]), Math.max(X[0], X[1], X[2], X[3]), Math.max(Y[0], Y[1], Y[2], Y[3])];
}
/* a caixa da setinha no mundo: o desenho dela (sourceRectAtTime da forma) com escala, pais e câmera */
function kCaixaCursorMundo(L, t, cache) {
    var q = L.sourceRectAtTime(t, false), m = kMatMundo(L, t, cache), r = [q.left, q.top, q.left + q.width, q.top + q.height];
    var xs = [r[0], r[2], r[0], r[2]], ys = [r[1], r[1], r[3], r[3]], i, X = [], Y = [];
    for (i = 0; i < 4; i++) { X.push(m[0] * xs[i] + m[2] * ys[i] + m[4]); Y.push(m[1] * xs[i] + m[3] * ys[i] + m[5]); }
    return [Math.min(X[0], X[1], X[2], X[3]), Math.min(Y[0], Y[1], Y[2], Y[3]), Math.max(X[0], X[1], X[2], X[3]), Math.max(Y[0], Y[1], Y[2], Y[3])];
}
var KIT_SKIP_CADEIA = /^(feed|mapa|parede|pan|zoom|celular|janela|papel|carimbo|slot|carta)/;
function kAuditoria(comp) {
    if (KIT.auditoria === false) return;
    var W = comp.width, H = comp.height, f = KIT.f || 1, zT = kZonaMundo(comp, true), zE = kZonaMundo(comp, false), i, j, k;
    var cand = [];
    for (i = 1; i <= comp.numLayers; i++) {
        var L = comp.layer(i), nm = L.name;
        var ehTexto = nm.indexOf('palavra ') == 0, ehEl = nm.indexOf('el ') == 0 || nm == 'elemento-reserva' || nm == 'ambiente', ehCur = nm == 'cursor';
        if (!ehTexto && !ehEl && !ehCur) continue;
        var skip = false, P = L.parent;
        while (P) { if (KIT_SKIP_CADEIA.test(P.name)) { skip = true; break; } P = P.parent; }
        var nominal = (L.comment != null && L.comment !== '') ? parseFloat(L.comment) : 0;
        if (isNaN(nominal)) nominal = 0;
        cand.push({ L: L, nome: nm, texto: ehTexto, cursor: ehCur, skip: skip && !ehCur, entra: ehCur ? 0 : nominal, amb: nm == 'ambiente' });
    }
    var passo = 0.1 * f, dur = comp.duration, fim = dur - 0.55 * f, t, amostras = 0, nFora = 0, nCol = 0, vistos = {}, exs = 0;
    for (t = 0.3 * f; t < fim; t += passo) {
        var cache = {}, vis = [];
        for (i = 0; i < cand.length; i++) {
            var c = cand[i];
            if (c.skip || (!c.cursor && t < c.entra + 0.5 * f)) continue;
            try {
                var tr = c.L.property('ADBE Transform Group');
                if (tr.property('ADBE Opacity').valueAtTime(t, false) < 40) continue;
                if (t < c.L.inPoint || t >= c.L.outPoint) continue;
                var bx = c.cursor ? kCaixaCursorMundo(c.L, t, cache) : kCaixaMundo(c.L, t, cache);
                if (c.cursor) { var bx2 = kCaixaCursorMundo(c.L, t + 0.05 * f, {}); if (Math.abs(bx2[0] - bx[0]) + Math.abs(bx2[1] - bx[1]) > 4) continue; }   // em trânsito: só conta parado (no clique)
                if (bx[2] - bx[0] < 2 || bx[3] - bx[1] < 2) continue;
                var h0 = bx[3] - bx[1];
                vis.push({ c: c, b: bx, tinta: c.texto ? [bx[0], bx[1] + h0 * 0.14, bx[2], bx[3] - h0 * 0.14] : bx });
            } catch (e) { }
        }
        amostras++;
        var foraT = false, colT = false;
        for (i = 0; i < vis.length; i++) {
            var a = vis[i], b = a.b;
            var zz = a.c.texto ? zT : zE, limX0 = zz.x0, limX1 = zz.x1, limY0 = zz.y0, limY1 = zz.y1;
            if (!a.c.cursor && b[0] < limX0 - 12 || b[2] > limX1 + 12 || b[1] < limY0 - 12 || b[3] > limY1 + 12) {   // 12 px (1%): a respiração da câmera passa disso e não encosta na borda
                foraT = true;
                var kf = 'f:' + a.c.nome;
                if (!vistos[kf] && exs < 8) { vistos[kf] = 1; exs++; KIT.avisos.push('cena ' + KIT.cenaIdx + ': ' + (a.c.texto ? 'texto' : 'elemento') + ' fora da área segura (quadro ' + (t / f).toFixed(1) + ' s): ' + a.c.nome.replace('palavra ', '') + ' [' + Math.round(b[0]) + ',' + Math.round(b[1]) + ',' + Math.round(b[2]) + ',' + Math.round(b[3]) + ']'); }
            }
            for (j = i + 1; j < vis.length; j++) {
                var bt = a.tinta, o2 = vis[j].tinta, ox = Math.min(bt[2], o2[2]) - Math.max(bt[0], o2[0]), oy = Math.min(bt[3], o2[3]) - Math.max(bt[1], o2[1]);
                if (a.c.cursor || vis[j].c.cursor) {   // o cursor é elemento: nunca em cima de letra (com elemento, pode)
                    var tx2 = a.c.cursor ? vis[j].c : a.c;
                    if (tx2.texto && ox > 2 && oy > 2) {
                        colT = true;
                        var kq = 'q:' + tx2.nome;
                        if (!vistos[kq] && exs < 8) { vistos[kq] = 1; exs++; KIT.avisos.push('cena ' + KIT.cenaIdx + ': cursor sobre o texto (quadro ' + (t / f).toFixed(1) + ' s): ' + tx2.nome.replace('palavra ', '')); }
                    }
                    continue;
                }
                if (ox > 5 && oy > 5) {
                    var A1 = (bt[2] - bt[0]) * (bt[3] - bt[1]), A2 = (o2[2] - o2[0]) * (o2[3] - o2[1]);
                    if (ox * oy > 0.05 * Math.min(A1, A2)) {
                        colT = true;
                        var kc = 'c:' + a.c.nome + '|' + vis[j].c.nome;
                        if (!vistos[kc] && exs < 8) { vistos[kc] = 1; exs++; KIT.avisos.push('cena ' + KIT.cenaIdx + ': colisão (quadro ' + (t / f).toFixed(1) + ' s): ' + a.c.nome.replace('palavra ', '') + ' x ' + vis[j].c.nome.replace('palavra ', '')); }
                    }
                }
            }
        }
        if (foraT) nFora++;
        if (colT) nCol++;
    }
    writeLn('AUDIT ' + JSON.stringify({ cena: KIT.cenaIdx, amostras: amostras, foraDaArea: nFora, colisoes: nCol }));
}

// ---------------------------------------------------------------- o herói ocupa a zona dele inteira
function kMatRel(L, t, cache, stop) {
    var k = L.index;
    if (cache[k]) return cache[k];
    var m = kMatLocal(L, t);
    if (L.parent && L.parent.index != stop) m = kMulMat(kMatRel(L.parent, t, cache, stop), m);
    cache[k] = m;
    return m;
}
var KIT_NAO_CONTA = /^(fundo|antes-fundo|depois-fundo|brilho|vazamento|look|ambiente|particula|sombra|estouro|onda|halo|cursor|dedo|tampa|guia|CAM|CONTEUDO)/;
/* kAjustaConteudo(comp, cam): mede a caixa do CONTEÚDO (tudo que é filho do CONTEUDO, já assentado) e põe essa caixa na zona dele —
   entre a faixa reservada do título (+24 px) e o fim da zona segura —, escalando de 70% a 140% e centrando. Cena sem título (ou com
   mapa, feed, parede… que sangram de propósito) fica como foi desenhada. */
function kAjustaConteudo(comp, cam) {
    if (!cam || !cam.camNo || KIT.tituloB == null || KIT.fit === false) return;
    var W = comp.width, H = comp.height, cont = cam.no, dur = comp.duration, i, k, ts = [0.45 * dur, 0.6 * dur];
    var x0 = 1e9, y0 = 1e9, x1 = -1e9, y1 = -1e9, n = 0;
    for (i = 1; i <= comp.numLayers; i++) {
        var L = comp.layer(i);
        if (KIT_NAO_CONTA.test(L.name) || L.nullLayer || L.comment == 'caminho') continue;
        var P = L.parent, dentro = false;
        while (P) { if (P.index == cont.index) { dentro = true; break; } if (KIT_SKIP_CADEIA.test(P.name) && P.name != 'celular' && P.name != 'janela' && P.name != 'papel' && P.name != 'carimbo') return; P = P.parent; }
        if (!dentro) continue;
        for (k = 0; k < ts.length; k++) {
            var t = ts[k];
            try {
                if (t < L.inPoint || t >= L.outPoint) continue;
                if (L.property('ADBE Transform Group').property('ADBE Opacity').valueAtTime(t, false) < 40) continue;
                var cache = {}, m = kMatRel(L, t, cache, cont.index), r;
                if (L.name.indexOf('palavra ') == 0) { var q = L.sourceRectAtTime(t, false); r = [q.left, q.top, q.left + q.width, q.top + q.height]; }
                else if (L.name.indexOf('el ') == 0 && L.source && L.source.width) r = [0, 0, L.source.width, L.source.height];
                else { var q2 = L.sourceRectAtTime(t, false); r = [q2.left, q2.top, q2.left + q2.width, q2.top + q2.height]; }
                var xs = [r[0], r[2], r[0], r[2]], ys = [r[1], r[1], r[3], r[3]], j;
                for (j = 0; j < 4; j++) { var X = m[0] * xs[j] + m[2] * ys[j] + m[4], Y = m[1] * xs[j] + m[3] * ys[j] + m[5]; if (KIT.debugFit && (X < 20 || Y < 20)) writeLn('FITDBG ' + L.name + ' ' + Math.round(X) + ',' + Math.round(Y)); x0 = Math.min(x0, X); y0 = Math.min(y0, Y); x1 = Math.max(x1, X); y1 = Math.max(y1, Y); }
                n++;
            } catch (e) { }
        }
    }
    if (!n || x1 - x0 < 50 || y1 - y0 < 50) return;
    var z = kZonaTexto(comp, true), top = KIT.tituloB, bot = H - z.bot, aw = W - 2 * z.x;
    var s = Math.max(0.7, Math.min(1.4, aw / (x1 - x0), (bot - top) / (y1 - y0)));
    var bx = (x0 + x1) / 2, by = (y0 + y1) / 2, py = (top + bot) / 2;
    kA(cont).setValue([bx, by]); kP(cont).setValue([W / 2, py]); kS(cont).setValue([s * 100, s * 100]);
    KIT.remap = { s: s, ax: bx, ay: by, px: W / 2, py: py };
    writeLn('FIT ' + JSON.stringify({ cena: KIT.cenaIdx, escala: Math.round(s * 100) / 100, caixa: [Math.round(x0), Math.round(y0), Math.round(x1), Math.round(y1)] }));
}
