primocode 8.36.0 → 8.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.36.0",
3
+ "version": "8.37.0",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/primocode.js",
11
- "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js && node test/vocabulario.test.js && node test/video-export.test.js && node test/monotonia.test.js && node test/clipe.test.js && node test/midia-envio.test.js && node test/pausas.test.js && node test/fala-repetida.test.js && node test/trilha.test.js && node test/pipeline.test.js && node test/app.test.js && node test/elementos.test.js && node test/remotion.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/codex.test.js && node test/chaves.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js && node test/vocabulario.test.js && node test/video-export.test.js && node test/monotonia.test.js && node test/clipe.test.js && node test/midia-envio.test.js && node test/pausas.test.js && node test/fala-repetida.test.js && node test/trilha.test.js && node test/anuncio.test.js && node test/pipeline.test.js && node test/app.test.js && node test/elementos.test.js && node test/remotion.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/codex.test.js && node test/chaves.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"
@@ -227,6 +227,106 @@ baratos de escrever e mudam completamente o resultado.
227
227
  último, é o vídeo que o usuário chamou de "sem efeito nenhum" — e não é falta
228
228
  de recurso: é este bloco não ter sido lido.
229
229
 
230
+ ## Anúncio de marca — o kit
231
+
232
+ Quando o pedido é o COMERCIAL de um produto (um cartão, um app, um plano), o
233
+ que separa uma peça de agência de um slide com música são quatro coisas, e as
234
+ quatro têm elemento próprio. Uma peça de marca sem elas sai genérica por mais
235
+ bonito que esteja o texto.
236
+
237
+ **Primeiro, declare a marca no documento.** Ela pinta o destaque de tudo —
238
+ número, botão, detalhe, o cartão — e libera o fundo `"marca"`:
239
+
240
+ ```json
241
+ "marca": { "cor": "#21C25E", "logo": "/midia/logo.svg" },
242
+ "tema": "claro"
243
+ ```
244
+
245
+ Tema CLARO na maioria dos anúncios de marca. O escuro é para a cena de
246
+ produto e para a vitrine; o resto da peça respira no branco.
247
+
248
+ ### `cartao` — o produto
249
+
250
+ ```json
251
+ { "tipo": "cartao", "nome": "PicPay", "variante": "Gold", "movimento": "flutuar" }
252
+ ```
253
+
254
+ Sai um cartão desenhado: chip, símbolo de aproximação, numeração mascarada,
255
+ nome e validade, com luz e sombra. `cor` (sem ela, a da marca), `largura`,
256
+ `numero` (só os 4 últimos aparecem), `titular`, `validade`, `face: "verso"`,
257
+ `movimento`: `flutuar` | `girar`.
258
+
259
+ **Mostre o produto antes de falar dele.** Um anúncio de cartão que só tem
260
+ frases é um anúncio sem produto.
261
+
262
+ ### `app` — a tela do aplicativo
263
+
264
+ ```json
265
+ { "tipo": "app", "topo": "Minha conta", "rotulo": "saldo disponível",
266
+ "valor": "R$ 1.284,90",
267
+ "linhas": [["Café da manhã", "R$ 18,90"], ["Mercado", "R$ 92,40"]],
268
+ "botao": "Pagar com o cartão" }
269
+ ```
270
+
271
+ Cabeçalho, um número grande, linhas com o nome à esquerda e o valor à direita
272
+ separadas por fio, e o botão cheio embaixo — que é o que faz o olho ler
273
+ "aplicativo". Aceita `["nome", "valor"]` e `"nome|valor"`. `escuro: true` para
274
+ a versão noturna.
275
+
276
+ **Ele vai DENTRO do `dispositivo`**, e aí ocupa a tela inteira do aparelho:
277
+
278
+ ```json
279
+ { "tipo": "dispositivo", "forma": "celular", "dentro": [ { "tipo": "app", ... } ] }
280
+ ```
281
+
282
+ Solto na cena ele vira o print da campanha. É o mesmo elemento de propósito:
283
+ com dois, a tela do celular e o print divergiriam na primeira mudança.
284
+
285
+ ### `vitrine` — a linha de produtos
286
+
287
+ ```json
288
+ { "tipo": "vitrine", "titulo": "Escolha seu cartão", "atual": 1, "nome": "PicPay",
289
+ "itens": [ { "nome": "Gold", "cor": "#C9A227", "detalhe": "sem anuidade" },
290
+ { "nome": "Verde", "detalhe": "cashback" },
291
+ { "nome": "Black", "cor": "#181B1F", "detalhe": "sala VIP" } ] }
292
+ ```
293
+
294
+ Os cartões numa fila, o `atual` à frente e os outros recuados. Vai bem sobre
295
+ fundo escuro (`"fundo": "#0b0f0c"`), que é como toda tela de "escolha o seu" é
296
+ desenhada.
297
+
298
+ ### `assinatura` — o ponto final
299
+
300
+ ```json
301
+ { "tipo": "assinatura", "nome": "PicPay", "frase": "o jeito fácil de pagar", "sobre": "marca" }
302
+ ```
303
+
304
+ Ela toma a CENA INTEIRA: o logo sozinho, grande, sobre a cor da casa. `sobre`:
305
+ `marca` | `claro` | `escuro`. Com `logo` ela usa a imagem; sem, o `nome`.
306
+
307
+ **Todo anúncio termina assim.** Sem a assinatura a peça acaba numa frase, e
308
+ ninguém fica sabendo quem falou.
309
+
310
+ ### A ordem que funciona
311
+
312
+ Uma peça de marca de 20 a 30 segundos, com 8 a 10 cenas:
313
+
314
+ | # | cena | o que entra |
315
+ |---|---|---|
316
+ | 1 | a promessa, em uma frase curta | `texto` pequeno, muito ar, fundo claro |
317
+ | 2 | o produto | `cartao` com `movimento` |
318
+ | 3 | o mote, na cor da casa | `fundo: "marca"`, `texto` branco, `anim: "palavra"` |
319
+ | 4 | funcionando | `dispositivo` + `app` |
320
+ | 5 | o benefício | `texto` |
321
+ | 6 | a linha | `vitrine` sobre fundo escuro |
322
+ | 7 | a última razão | `texto` |
323
+ | 8 | assinatura | `assinatura` |
324
+
325
+ E o rosto de quem fala, quando houver, entra por `clipe` entre a 4 e a 5 — o
326
+ depoimento é o que dá confiança, e nenhum desenho substitui.
327
+
328
+ ---
329
+
230
330
  ### `icone` — o desenho ao lado do argumento
231
331
 
232
332
  ```json
package/studio/server.py CHANGED
@@ -893,6 +893,28 @@ def revisar_monotonia(doc, ferramenta):
893
893
  "(aproximar, afastar, esquerda, direita, cima), e uma ou duas peças por "
894
894
  "vídeo levam chaves de animação.")
895
895
 
896
+ # ── ANÚNCIO DE MARCA SEM PRODUTO E SEM ASSINATURA ───────────────────
897
+ #
898
+ # "Ainda não tá bom, tá genérico. Quero nesse nível." — o dono, sobre um
899
+ # comercial de fintech mandado como referência.
900
+ #
901
+ # O que separava a peça daqui daquela não era tema nem cor: era não ter o
902
+ # PRODUTO na tela e não assinar no fim. Quem declarou uma `marca` está
903
+ # fazendo peça de marca, e aí estas duas ausências são o defeito inteiro.
904
+ if doc.get("marca") and len(cenas) >= 4:
905
+ if not (tipos & {"cartao", "app", "vitrine", "dispositivo", "imagem", "clipe"}):
906
+ avisos.append(
907
+ "esta peça tem marca declarada e nenhuma cena mostra o PRODUTO — são %d cenas "
908
+ "de texto sobre fundo. Um anúncio de cartão mostra o cartão (`cartao`), o app "
909
+ "funcionando dentro do celular (`dispositivo` + `app`) e a linha de produtos "
910
+ "(`vitrine`). É essa ausência que se lê como genérico." % len(cenas))
911
+ if "assinatura" not in tipos:
912
+ avisos.append(
913
+ 'o anúncio acaba sem assinar. A última cena de uma peça de marca é '
914
+ '{"tipo":"assinatura","nome":"…","sobre":"marca"} — o logo sozinho, grande, '
915
+ "sobre a cor da casa. Sem ela a peça termina numa frase e ninguém fica "
916
+ "sabendo quem falou.")
917
+
896
918
  # Só a voz com tela montada: existe e quase nunca é lembrado, porque é o
897
919
  # contrário do `mudo` e o contrário não estava no vocabulário até agora.
898
920
  clipes = [e for e in todos if e.get("tipo") == "clipe"]
@@ -1061,9 +1083,16 @@ def validar_doc(modelo, doc):
1061
1083
  # default do renderizador: texto cru de 16px num canvas de 1920 — ou nada,
1062
1084
  # quando o texto estava noutro campo. Era o "vídeo com música mas tudo
1063
1085
  # preto": documento válido na forma, invisível na tela.
1086
+ # O kit de anúncio (cartao, app, vitrine, assinatura) entra na lista abaixo.
1087
+ #
1088
+ # A LISTA É LIDA POR UM TESTE (`vocabulario.test.js`), que a recorta com uma
1089
+ # expressão regular e pega as palavras — então comentário DENTRO dela vira
1090
+ # "tipo que o render precisa desenhar", e o teste passa a cobrar `produto`,
1091
+ # `linha` e `fim`. Texto sobre os tipos mora aqui fora.
1064
1092
  TIPOS_VALIDOS = {"texto", "lista", "cartoes", "numeros", "tabela", "codigo",
1065
1093
  "citacao", "forma", "imagem", "icone", "grafico", "no", "conector",
1066
1094
  "objeto3d", "clipe",
1095
+ "cartao", "app", "vitrine", "assinatura",
1067
1096
  "selo", "progresso", "avatar", "dispositivo", "conversa",
1068
1097
  "passos", "opcoes"}
1069
1098
  # Os sete últimos são o vocabulário do vídeo de referência. O comentário
@@ -115,6 +115,13 @@ function aplicarFundo(no, fundo) {
115
115
  // Era metade do "os vídeos ficaram todos iguais".
116
116
  if (/^(malha|brilho|grade|pontos|suave|vinheta|aurora|nevoa|ondas|raios|xadrez)$/.test(fundo)) {
117
117
  no.setAttribute("data-fundo", fundo);
118
+ } else if (fundo === "marca") {
119
+ /* A cor da casa, chapada e sem efeito nenhum. Num anúncio de marca é o
120
+ fundo mais usado de todos — e escrevê-lo como `"#21C25E"` obrigaria a
121
+ repetir o hexadecimal em cada cena, que é como uma delas acaba com a
122
+ cor de ontem depois de a marca mudar de tom. */
123
+ no.style.background = "var(--c-tom)";
124
+ no.setAttribute("data-fundo", "marca");
118
125
  } else no.style.background = fundo;
119
126
  return;
120
127
  }
@@ -214,11 +221,30 @@ function svgGrafico(e, l, a) {
214
221
 
215
222
  /* -------------------------------------------------------------- elementos */
216
223
 
224
+ /* O KIT DE ANÚNCIO — os quatro últimos da lista abaixo.
225
+ *
226
+ * Nasceram de um vídeo de referência mandado pelo dono — um comercial de
227
+ * fintech — com uma frase só: "ainda não tá bom, tá genérico. Quero nesse
228
+ * nível."
229
+ *
230
+ * Medindo a peça de hoje contra aquela, o que faltava não era tema nem cor:
231
+ * tema claro e `marca.cor` já existiam e resolvem o mundo da marca. O que não
232
+ * existia era o PRODUTO. Um anúncio de cartão mostra o cartão, mostra o app
233
+ * funcionando dentro do celular, mostra a linha de produtos e assina no fim.
234
+ * Com o vocabulário antigo, "o cartão" era um retângulo verde com a palavra
235
+ * "cartão" escrita dentro — e é exatamente isso que se lê como genérico.
236
+ *
237
+ * ── A LISTA ABAIXO É LIDA POR UM TESTE, e por isso não tem comentário dentro.
238
+ * O `vocabulario.test.js` recorta este objeto com uma expressão regular e
239
+ * pega as palavras: qualquer texto entre as chaves vira "tipo que o servidor
240
+ * precisa aceitar", e o teste passa a cobrar `fintech`, `genérico` e `dono`.
241
+ * Comentário sobre os tipos mora AQUI FORA. */
217
242
  var TIPOS_CONHECIDOS = { texto: 1, lista: 1, cartoes: 1, numeros: 1, tabela: 1, codigo: 1,
218
243
  citacao: 1, forma: 1, imagem: 1, icone: 1, grafico: 1, no: 1, conector: 1,
219
244
  objeto3d: 1, clipe: 1,
220
245
  selo: 1, progresso: 1, avatar: 1, dispositivo: 1, conversa: 1, passos: 1,
221
- opcoes: 1 };
246
+ opcoes: 1,
247
+ cartao: 1, app: 1, vitrine: 1, assinatura: 1 };
222
248
  /* Os sete últimos são o vocabulário do vídeo de referência: pílula de estado,
223
249
  trilha de progresso, foto de pessoa, celular/notebook com conteúdo dentro,
224
250
  balões de conversa, etapas com uma acesa, e pergunta com alternativas.
@@ -955,6 +981,202 @@ function renderElemento(e, ctx) {
955
981
  break;
956
982
  }
957
983
 
984
+ /* ── O CARTÃO ────────────────────────────────────────────────────────
985
+ Um produto desenhado, não um retângulo com o nome dele escrito dentro.
986
+ O que faz o olho ler "cartão" são quatro detalhes pequenos e nenhum
987
+ deles é o formato: o CHIP, o símbolo de aproximação, a numeração
988
+ mascarada e o pé com nome e validade. Sem eles o desenho é uma caixa;
989
+ com eles, qualquer pessoa reconhece em um quadro de vídeo.
990
+
991
+ A cor sai da marca (`--c-tom`) quando ninguém diz outra, para o cartão
992
+ pertencer à peça em vez de ser mais um objeto colorido nela. */
993
+ case "cartao": {
994
+ var cc = e.cor || "var(--c-tom)";
995
+ no.style.setProperty("--cc", cc);
996
+ if (e.largura) no.style.width = _px(e.largura);
997
+ if (e.movimento) no.setAttribute("data-mov", e.movimento);
998
+ if (e.face === "verso") no.setAttribute("data-face", "verso");
999
+
1000
+ var corpo = document.createElement("div");
1001
+ corpo.className = "cartao-corpo";
1002
+
1003
+ var topo = document.createElement("div");
1004
+ topo.className = "cartao-topo";
1005
+ var marca = document.createElement("span");
1006
+ marca.className = "cartao-marca";
1007
+ marca.textContent = e.nome || "";
1008
+ var vari = document.createElement("span");
1009
+ vari.className = "cartao-variante";
1010
+ vari.textContent = e.variante || "";
1011
+ topo.appendChild(marca); topo.appendChild(vari);
1012
+
1013
+ var meio = document.createElement("div");
1014
+ meio.className = "cartao-meio";
1015
+ var chip = document.createElement("i");
1016
+ chip.className = "cartao-chip";
1017
+ // O símbolo de aproximação: três arcos. Em SVG porque em texto ele
1018
+ // depende de uma fonte que a máquina pode não ter.
1019
+ var onda = document.createElement("span");
1020
+ onda.className = "cartao-onda";
1021
+ onda.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true">'
1022
+ + '<path d="M8 5a10 10 0 0 1 0 14M13 8a5.6 5.6 0 0 1 0 8M17.6 2.4a15 15 0 0 1 0 19.2"'
1023
+ + ' fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
1024
+ meio.appendChild(chip); meio.appendChild(onda);
1025
+
1026
+ var num = document.createElement("div");
1027
+ num.className = "cartao-numero";
1028
+ // Os quatro últimos dígitos e mais nada: é o que aparece num cartão de
1029
+ // verdade em foto de campanha, e é o que não vira dado de ninguém.
1030
+ num.textContent = "•••• •••• •••• " + String(e.numero || "4821").slice(-4);
1031
+
1032
+ var pe = document.createElement("div");
1033
+ pe.className = "cartao-pe";
1034
+ var quem = document.createElement("span");
1035
+ quem.textContent = e.titular || "SEU NOME AQUI";
1036
+ var val = document.createElement("span");
1037
+ val.textContent = e.validade || "12/29";
1038
+ pe.appendChild(quem); pe.appendChild(val);
1039
+
1040
+ var brilho = document.createElement("i");
1041
+ brilho.className = "cartao-brilho";
1042
+
1043
+ corpo.appendChild(brilho);
1044
+ corpo.appendChild(topo);
1045
+ corpo.appendChild(meio);
1046
+ corpo.appendChild(num);
1047
+ corpo.appendChild(pe);
1048
+ no.appendChild(corpo);
1049
+ break;
1050
+ }
1051
+
1052
+ /* ── A TELA DO APP ───────────────────────────────────────────────────
1053
+ O `dispositivo` já desenhava o aparelho; o que ele mostrava DENTRO era
1054
+ texto centralizado no meio do vidro, que não se parece com aplicativo
1055
+ nenhum. O que faz uma tela parecer um app é a ARRUMAÇÃO: cabeçalho em
1056
+ cima, um número grande, linhas com o nome à esquerda e o valor à
1057
+ direita separadas por fio, e um botão cheio embaixo.
1058
+
1059
+ Vale sozinho na cena e vale dentro do `dispositivo` — é o mesmo
1060
+ elemento, e por isso a tela do celular e a do print da campanha não
1061
+ divergem. */
1062
+ case "app": {
1063
+ if (e.escuro) no.setAttribute("data-escuro", "1");
1064
+ if (e.topo) {
1065
+ var cab = document.createElement("div");
1066
+ cab.className = "app-topo";
1067
+ cab.innerHTML = "<span>" + _fugir(e.topo) + "</span><i></i>";
1068
+ no.appendChild(cab);
1069
+ }
1070
+ if (e.valor) {
1071
+ var caixaV = document.createElement("div");
1072
+ caixaV.className = "app-valor";
1073
+ if (e.rotulo) {
1074
+ var rot = document.createElement("small");
1075
+ rot.textContent = e.rotulo;
1076
+ caixaV.appendChild(rot);
1077
+ }
1078
+ var vv = document.createElement("strong");
1079
+ vv.textContent = e.valor;
1080
+ caixaV.appendChild(vv);
1081
+ no.appendChild(caixaV);
1082
+ }
1083
+ var linhas = e.linhas || e.itens || [];
1084
+ if (linhas.length) {
1085
+ var lista = document.createElement("div");
1086
+ lista.className = "app-linhas";
1087
+ linhas.slice(0, 8).forEach(function (li) {
1088
+ /* Aceita ["nome", "valor"] e "nome|valor" — o segundo é a forma que
1089
+ todo o resto da spec usa (lista, cartoes, passos), e quem escreve
1090
+ o documento vai chutar a que já conhece. */
1091
+ var nome = li, valor = "";
1092
+ if (Array.isArray(li)) { nome = li[0]; valor = li[1] || ""; }
1093
+ else if (typeof li === "string" && li.indexOf("|") > -1) {
1094
+ var par = li.split("|"); nome = par[0]; valor = par.slice(1).join("|");
1095
+ }
1096
+ var linha = document.createElement("div");
1097
+ linha.className = "app-linha";
1098
+ linha.innerHTML = "<span>" + _fugir(String(nome).trim()) + "</span>"
1099
+ + "<b>" + _fugir(String(valor).trim()) + "</b>";
1100
+ lista.appendChild(linha);
1101
+ });
1102
+ no.appendChild(lista);
1103
+ }
1104
+ if (e.botao) {
1105
+ var bt = document.createElement("div");
1106
+ bt.className = "app-botao";
1107
+ bt.textContent = e.botao;
1108
+ no.appendChild(bt);
1109
+ }
1110
+ break;
1111
+ }
1112
+
1113
+ /* ── A VITRINE ───────────────────────────────────────────────────────
1114
+ A linha de produtos, com um escolhido. O `cartoes` genérico resolvia
1115
+ isso com três caixinhas de vidro do tamanho de um selo; aqui os itens
1116
+ são CARTÕES, o atual vem à frente e os outros recuam — que é o desenho
1117
+ de toda tela de "escolha o seu". */
1118
+ case "vitrine": {
1119
+ var itens = (e.itens || []).slice(0, 5);
1120
+ var atual = e.atual != null ? Number(e.atual) : 0;
1121
+ if (e.titulo) {
1122
+ var tt = document.createElement("div");
1123
+ tt.className = "vitrine-titulo";
1124
+ tt.textContent = e.titulo;
1125
+ no.appendChild(tt);
1126
+ }
1127
+ var fila = document.createElement("div");
1128
+ fila.className = "vitrine-fila";
1129
+ itens.forEach(function (it, i) {
1130
+ var dado = typeof it === "string"
1131
+ ? { nome: it.split("|")[0], detalhe: it.split("|").slice(1).join("|") }
1132
+ : (it || {});
1133
+ var mini = renderElemento({
1134
+ tipo: "cartao", nome: e.nome || dado.marca || "",
1135
+ variante: dado.nome || dado.variante || "",
1136
+ cor: dado.cor || e.cor, numero: dado.numero,
1137
+ titular: " ", validade: " ",
1138
+ }, ctx);
1139
+ var col = document.createElement("div");
1140
+ col.className = "vitrine-item" + (i === atual ? " atual" : "");
1141
+ col.appendChild(mini);
1142
+ if (dado.detalhe) {
1143
+ var d = document.createElement("small");
1144
+ d.textContent = dado.detalhe;
1145
+ col.appendChild(d);
1146
+ }
1147
+ fila.appendChild(col);
1148
+ });
1149
+ no.appendChild(fila);
1150
+ break;
1151
+ }
1152
+
1153
+ /* ── A ASSINATURA ────────────────────────────────────────────────────
1154
+ O fim do anúncio. Toda peça de marca fecha assim — o logo sozinho,
1155
+ grande, sobre a cor da casa, e nada mais na tela. Sem este elemento a
1156
+ última cena do vídeo era mais uma frase, e a peça acabava sem assinar
1157
+ quem falou. */
1158
+ case "assinatura": {
1159
+ no.setAttribute("data-sobre", /^(marca|claro|escuro)$/.test(e.sobre || "") ? e.sobre : "marca");
1160
+ if (e.logo) {
1161
+ var img2 = document.createElement("img");
1162
+ img2.className = "assinatura-logo";
1163
+ img2.src = e.logo; img2.alt = e.nome || "";
1164
+ no.appendChild(img2);
1165
+ } else if (e.nome) {
1166
+ var nm = document.createElement("div");
1167
+ nm.className = "assinatura-nome";
1168
+ nm.textContent = e.nome;
1169
+ no.appendChild(nm);
1170
+ }
1171
+ if (e.frase) {
1172
+ var fr = document.createElement("div");
1173
+ fr.className = "assinatura-frase";
1174
+ fr.textContent = e.frase;
1175
+ no.appendChild(fr);
1176
+ }
1177
+ break;
1178
+ }
1179
+
958
1180
  case "icone": {
959
1181
  var t = e.tamanho || 90;
960
1182
  no.style.width = _px(e.l || t);
@@ -1188,6 +1410,11 @@ function renderCena(cena, doc, op) {
1188
1410
  elementos = elementos.filter(function (e) {
1189
1411
  if (!e || e.x != null || e.y != null) return true;
1190
1412
  if (e.tipo === "clipe") { deChao.push(e); return false; }
1413
+ /* A ASSINATURA É A CENA INTEIRA, e por isso entra pelo chão e não pelo
1414
+ fluxo. Pelo fluxo ela vira uma caixinha colorida no meio da tela, com a
1415
+ margem do layout em volta — que é o oposto de assinar: o logo tem de
1416
+ encostar nas bordas do quadro, como em qualquer fim de comercial. */
1417
+ if (e.tipo === "assinatura") { deChao.push(e); return false; }
1191
1418
  // Legenda de fala mora no terço de baixo, como toda legenda de vídeo.
1192
1419
  // Pelo fluxo ela iria para o meio da tela, em cima do rosto de quem fala.
1193
1420
  if (e.tipo === "texto" && e.papel === "fala") { falas.push(e); return false; }
@@ -1203,6 +1430,21 @@ function renderCena(cena, doc, op) {
1203
1430
  // tela vertical pede tipografia maior: os padrões são calibrados em 1920x1080
1204
1431
  var escalaTipo = H > W ? 1.35 : 1;
1205
1432
  no.style.setProperty("--esc", escalaTipo);
1433
+ // A altura do quadro, para o CSS dimensionar o que precisa ocupar a cena
1434
+ // (o aparelho, hoje) em proporção e não em pixels calibrados num formato só.
1435
+ no.style.setProperty("--alto", H + "px");
1436
+ /* A UNIDADE DA CENA: 1% da largura do quadro.
1437
+
1438
+ Sem ela, tudo o que o CSS escreve em `em` cai na fonte herdada — que numa
1439
+ cena é a do navegador, ~14px — e o desenho colapsa sem erro nenhum. Foi
1440
+ medido num quadro de 1080x1920: a linha do extrato saiu com 7,84px e o
1441
+ nome da assinatura com 30,8px, num quadro de 1920 de altura. Na tela
1442
+ aquilo não é "pequeno": é invisível.
1443
+
1444
+ O resto do sistema resolve isso mandando PIXEL do documento (`tamanho`).
1445
+ Peça que não tem `tamanho` — a tela do app, a vitrine, a assinatura — se
1446
+ mede por aqui, e assim acompanha o formato em vez de ser calibrada num só. */
1447
+ no.style.setProperty("--u", (W / 100) + "px");
1206
1448
  var ctx = {
1207
1449
  i: 0, escalaTipo: escalaTipo,
1208
1450
  animPadrao: op.animar ? (cena.anim || "subir") : null,
@@ -511,8 +511,17 @@
511
511
  }
512
512
 
513
513
  .el.dispositivo[data-forma="celular"] { width: 100%; }
514
+ /* O TETO DO APARELHO ACOMPANHA O QUADRO. Ele era 780px fixos, calibrados em
515
+ 1920x1080 — e num vídeo vertical de 1080x1920 isso deixa o celular ocupando
516
+ um terço da altura, boiando no meio do branco. Num anúncio o aparelho é o
517
+ assunto da cena: ele ocupa a tela. `--alto` é a altura da cena, posta pelo
518
+ renderCena. */
514
519
  .el.dispositivo[data-forma="celular"] .moldura {
515
- height: min(100%, 780px); width: auto; max-width: 100%;
520
+ /* Altura em PIXEL vindo do quadro, sem porcentagem: `min(100%, …)` dentro de
521
+ um pai de altura automática é percentagem circular — o navegador desiste
522
+ dela e o aparelho volta a ser medido pela largura. Medido: 408px de altura
523
+ num quadro de 1920, um quinto da tela. */
524
+ height: calc(var(--alto, 1080px) * .62); width: auto; max-width: 100%;
516
525
  aspect-ratio: 9 / 19;
517
526
  border-radius: 42px; padding: 12px;
518
527
  }
@@ -761,8 +770,14 @@
761
770
  .cena.animar .el[data-an="cortina"] { animation-name: anCortina; animation-duration: .85s; }
762
771
  .cena.animar .el[data-an="elastico"] { animation-name: anElastico; animation-duration: .95s; }
763
772
 
764
- /* palavra a palavra — cada palavra entra sozinha */
765
- .el.txt .pal { display: inline-block; opacity: 0; }
773
+ /* palavra a palavra — cada palavra entra sozinha.
774
+
775
+ A palavra nasce VISÍVEL, e quem a esconde é a animação (com `both`, o
776
+ estado inicial do keyframe já vale antes de ela começar). Com `opacity: 0`
777
+ aqui na base, toda cena com `anim: "palavra"` saía EM BRANCO fora do player
778
+ — na miniatura, na prévia parada do editor, em qualquer render estático.
779
+ Texto que some sem erro nenhum é o defeito mais caro deste arquivo. */
780
+ .el.txt .pal { display: inline-block; }
766
781
  .cena.animar .el[data-an="palavra"] { animation: none; opacity: 1; }
767
782
  .cena.animar .el[data-an="palavra"] .pal {
768
783
  animation: anSubir .58s cubic-bezier(.22, 1, .36, 1) both;
@@ -933,3 +948,212 @@
933
948
  display: grid; place-items: center; min-height: 0; overflow: visible;
934
949
  }
935
950
  .cena .fluxo .lado-imagem.com-aparelho > .el { max-height: 100%; }
951
+
952
+ /* ══ O KIT DE ANÚNCIO ═══════════════════════════════════════════════════════
953
+ Nasceu de um vídeo de referência mandado pelo dono — um comercial de
954
+ fintech — com uma frase só: "ainda não tá bom, tá genérico. Quero nesse
955
+ nível." Medindo a peça de hoje contra aquela, o que faltava não era tema
956
+ nem cor (tema claro e `marca.cor` já existiam): era o PRODUTO.
957
+ ══════════════════════════════════════════════════════════════════════════ */
958
+
959
+ /* ── cartão ───────────────────────────────────────────────────────────────
960
+ A proporção é a de verdade (85,6 × 54 mm = 1,586). Ela importa mais que o
961
+ desenho: um retângulo na proporção errada não é lido como cartão por mais
962
+ chip que tenha. */
963
+ .el.cartao {
964
+ --cc: var(--c-tom);
965
+ width: min(100%, 620px);
966
+ perspective: 1400px;
967
+ }
968
+ .el.cartao .cartao-corpo {
969
+ position: relative;
970
+ aspect-ratio: 1.586;
971
+ border-radius: 6.2%;
972
+ padding: 7% 7.5%;
973
+ display: flex; flex-direction: column; justify-content: space-between;
974
+ color: #fff;
975
+ /* Duas fontes de luz e uma sombra longa: é o que separa "objeto pousado na
976
+ cena" de "retângulo colado por cima". */
977
+ background:
978
+ radial-gradient(120% 140% at 12% 4%, rgba(255, 255, 255, .30), transparent 52%),
979
+ linear-gradient(145deg,
980
+ color-mix(in srgb, var(--cc) 88%, #fff),
981
+ var(--cc) 46%,
982
+ color-mix(in srgb, var(--cc) 72%, #000) 100%);
983
+ box-shadow:
984
+ inset 0 1px 0 rgba(255, 255, 255, .38),
985
+ inset 0 -1px 0 rgba(0, 0, 0, .22),
986
+ 0 2.2% 5% -1% rgba(0, 0, 0, .45);
987
+ transform: rotate(-1.6deg);
988
+ }
989
+ /* O reflexo diagonal. Sem ele o plástico vira papel colorido. */
990
+ .el.cartao .cartao-brilho {
991
+ position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
992
+ background: linear-gradient(102deg, transparent 34%, rgba(255, 255, 255, .20) 46%,
993
+ rgba(255, 255, 255, .05) 54%, transparent 62%);
994
+ }
995
+ .el.cartao .cartao-topo {
996
+ display: flex; align-items: baseline; justify-content: space-between; gap: 1em;
997
+ }
998
+ .el.cartao .cartao-marca {
999
+ font-weight: 800; font-size: 6.4cqw; letter-spacing: -.01em;
1000
+ }
1001
+ .el.cartao .cartao-variante {
1002
+ font-size: 3.4cqw; letter-spacing: .18em; text-transform: uppercase;
1003
+ opacity: .82; font-weight: 600;
1004
+ }
1005
+ .el.cartao .cartao-meio { display: flex; align-items: center; gap: 4%; }
1006
+ /* O chip: dourado, com os contatos. É o detalhe que mais faz o desenho ser
1007
+ reconhecido — some ele e sobra um cartão de visita. */
1008
+ .el.cartao .cartao-chip {
1009
+ width: 13%; aspect-ratio: 1.32; border-radius: 14%;
1010
+ background: linear-gradient(150deg, #f7e3a1, #d9b558 46%, #b8912f);
1011
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .18);
1012
+ position: relative; flex: none;
1013
+ }
1014
+ .el.cartao .cartao-chip::before,
1015
+ .el.cartao .cartao-chip::after {
1016
+ content: ""; position: absolute; left: 12%; right: 12%; height: 1px;
1017
+ background: rgba(0, 0, 0, .28);
1018
+ }
1019
+ .el.cartao .cartao-chip::before { top: 36%; }
1020
+ .el.cartao .cartao-chip::after { top: 64%; }
1021
+ .el.cartao .cartao-onda { width: 7%; opacity: .9; display: block; flex: none; }
1022
+ .el.cartao .cartao-onda svg { width: 100%; height: auto; display: block; }
1023
+ .el.cartao .cartao-numero {
1024
+ font-family: var(--f-mono);
1025
+ font-size: 5.6cqw; letter-spacing: .06em; opacity: .95;
1026
+ }
1027
+ .el.cartao .cartao-pe {
1028
+ display: flex; justify-content: space-between; gap: 1em;
1029
+ font-size: 3.1cqw; letter-spacing: .13em; text-transform: uppercase;
1030
+ opacity: .8; font-weight: 600;
1031
+ }
1032
+ /* As medidas de dentro são `cqw` — por cento da LARGURA DO CARTÃO, não da
1033
+ tela. Sem o contexto de contêiner o mesmo cartão sai com o chip do tamanho
1034
+ de uma unha na vitrine e do tamanho de uma caixa na cena cheia. */
1035
+ .el.cartao .cartao-corpo { container-type: inline-size; }
1036
+
1037
+ .el.cartao[data-face="verso"] .cartao-corpo { transform: rotate(1.6deg); }
1038
+ .el.cartao[data-mov="flutuar"] .cartao-corpo { animation: cartao-flutua 6s ease-in-out infinite; }
1039
+ .el.cartao[data-mov="girar"] .cartao-corpo { animation: cartao-gira 9s ease-in-out infinite; }
1040
+ @keyframes cartao-flutua {
1041
+ 0%, 100% { transform: rotate(-1.6deg) translateY(0); }
1042
+ 50% { transform: rotate(-.6deg) translateY(-2.2%); }
1043
+ }
1044
+ @keyframes cartao-gira {
1045
+ 0%, 100% { transform: rotate(-1.6deg) rotateY(-16deg); }
1046
+ 50% { transform: rotate(-1.6deg) rotateY(16deg); }
1047
+ }
1048
+ @media (prefers-reduced-motion: reduce) {
1049
+ .el.cartao .cartao-corpo { animation: none; }
1050
+ }
1051
+
1052
+ /* ── a tela do app ────────────────────────────────────────────────────────
1053
+ Vale sozinha na cena e vale dentro do `dispositivo`. É o mesmo elemento de
1054
+ propósito: com dois, o print da campanha e a tela do celular divergiriam na
1055
+ primeira mudança. */
1056
+ .el.app {
1057
+ width: 100%;
1058
+ font-size: calc(var(--u, 10px) * 3.4);
1059
+ display: flex; flex-direction: column; gap: 4%;
1060
+ text-align: left;
1061
+ background: #fff; color: #10161d;
1062
+ border-radius: 22px; padding: 7% 6.5%;
1063
+ box-shadow: 0 18px 50px rgba(0, 0, 0, .16);
1064
+ }
1065
+ .el.app[data-escuro] { background: #0e1319; color: #eaf0f6; box-shadow: none; }
1066
+ .el.app .app-topo {
1067
+ display: flex; align-items: center; justify-content: space-between;
1068
+ font-size: .62em; font-weight: 700; opacity: .75;
1069
+ }
1070
+ .el.app .app-topo i {
1071
+ width: 2.2em; height: .34em; border-radius: 999px;
1072
+ background: currentColor; opacity: .25; display: block;
1073
+ }
1074
+ .el.app .app-valor { display: flex; flex-direction: column; gap: .12em; }
1075
+ .el.app .app-valor small { font-size: .5em; opacity: .6; font-weight: 600; }
1076
+ .el.app .app-valor strong {
1077
+ font-size: 1.5em; font-weight: 800; letter-spacing: -.02em;
1078
+ font-variant-numeric: tabular-nums;
1079
+ }
1080
+ .el.app .app-linhas { display: flex; flex-direction: column; }
1081
+ /* O FIO entre as linhas é o que faz o olho ler "extrato". Sem ele são frases
1082
+ empilhadas, que é como a tela saía antes. */
1083
+ .el.app .app-linha {
1084
+ display: flex; align-items: center; justify-content: space-between; gap: 1em;
1085
+ padding: .62em 0; font-size: .56em;
1086
+ border-top: 1px solid color-mix(in srgb, currentColor 14%, transparent);
1087
+ }
1088
+ .el.app .app-linha:first-child { border-top: 0; }
1089
+ .el.app .app-linha span { opacity: .82; }
1090
+ .el.app .app-linha b { font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
1091
+ .el.app .app-botao {
1092
+ margin-top: auto;
1093
+ background: var(--c-tom); color: #fff;
1094
+ border-radius: 999px; padding: .78em 1em;
1095
+ text-align: center; font-weight: 700; font-size: .58em;
1096
+ }
1097
+
1098
+ /* Dentro do aparelho a tela do app OCUPA o vidro: com o padrão do
1099
+ `dispositivo` (conteúdo centrado com folga) ela ficava boiando no meio, e o
1100
+ que se vê num celular de verdade é a tela cheia. */
1101
+ .el.dispositivo .tela { justify-content: flex-start; }
1102
+ .el.dispositivo .tela > .el.app {
1103
+ flex: 1; border-radius: 0; box-shadow: none;
1104
+ margin: -26px -20px; padding: 34px 22px 26px;
1105
+ background: transparent; color: var(--c-texto);
1106
+ }
1107
+
1108
+ /* ── vitrine ──────────────────────────────────────────────────────────────
1109
+ A linha de produtos com um escolhido. O item atual vem à frente; os outros
1110
+ recuam — sem isso são quatro cartões do mesmo tamanho e ninguém sabe qual
1111
+ está sendo oferecido. */
1112
+ .el.vitrine {
1113
+ width: 100%; font-size: calc(var(--u, 10px) * 3.4);
1114
+ display: flex; flex-direction: column; gap: 1.2em;
1115
+ }
1116
+ .el.vitrine .vitrine-titulo {
1117
+ text-align: center; font-size: .62em; font-weight: 700;
1118
+ letter-spacing: .04em; opacity: .82;
1119
+ }
1120
+ .el.vitrine .vitrine-fila {
1121
+ display: flex; align-items: center; justify-content: center; gap: 2.2%;
1122
+ }
1123
+ .el.vitrine .vitrine-item {
1124
+ flex: 1 1 0; min-width: 0;
1125
+ display: flex; flex-direction: column; align-items: center; gap: .5em;
1126
+ /* Recuado, não apagado: a .42 sobre fundo escuro os outros cartões somem, e
1127
+ uma vitrine com um produto só não é vitrine. */
1128
+ opacity: .58; transform: scale(.86);
1129
+ transition: opacity .4s ease, transform .4s ease;
1130
+ }
1131
+ .el.vitrine .vitrine-item.atual { opacity: 1; transform: scale(1.06); z-index: 2; }
1132
+ .el.vitrine .vitrine-item small {
1133
+ font-size: .42em; opacity: .75; text-align: center; font-weight: 600;
1134
+ }
1135
+ .el.vitrine .el.cartao { width: 100%; }
1136
+
1137
+ /* ── assinatura ───────────────────────────────────────────────────────────
1138
+ O fim do anúncio: o logo sozinho, grande, sobre a cor da casa. Ela toma a
1139
+ cena INTEIRA porque é isso que uma assinatura é — se dividisse espaço com
1140
+ outra coisa, seria mais um bloco em vez do ponto final. */
1141
+ .el.assinatura {
1142
+ position: absolute; inset: 0; z-index: 4;
1143
+ font-size: calc(var(--u, 10px) * 3);
1144
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
1145
+ gap: .5em; text-align: center; padding: 8%;
1146
+ }
1147
+ .el.assinatura[data-sobre="marca"] { background: var(--c-tom); color: #fff; }
1148
+ .el.assinatura[data-sobre="claro"] { background: #fff; color: #10161d; }
1149
+ .el.assinatura[data-sobre="escuro"] { background: #080b0e; color: #fff; }
1150
+ .el.assinatura .assinatura-logo { width: min(52%, 520px); height: auto; display: block; }
1151
+ .el.assinatura .assinatura-nome {
1152
+ font-size: 3.6em; font-weight: 800; letter-spacing: -.03em; line-height: 1;
1153
+ }
1154
+ .el.assinatura .assinatura-frase { font-size: .62em; opacity: .82; letter-spacing: .02em; }
1155
+
1156
+ /* Título de anúncio quebra EQUILIBRADO. "Um cartão. Seu jeito." saía em
1157
+ quatro linhas — três palavras e um órfão —, e é assim que uma frase curta
1158
+ vira um bloco desengonçado no meio do quadro. */
1159
+ .el.texto.titulo, .el.texto[data-papel="titulo"] { text-wrap: balance; }