primocode 8.26.0 → 8.30.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/studio/server.py CHANGED
@@ -784,7 +784,7 @@ def gravar_analise(ident, dados):
784
784
  # "Ele não alterna entre os temas, usa um tema só pra todos os vídeos, e o
785
785
  # vídeo sempre tem a mesma estrutura. Fica tudo genérico."
786
786
  #
787
- # O estúdio tem dez temas, onze fundos, oito transições e seis trilhas. O
787
+ # O estúdio tem doze temas, onze fundos, oito transições e seis trilhas. O
788
788
  # agente usa uma fatia estreita e repete — porque a spec tem UM exemplo
789
789
  # completo e modelo ancora em exemplo. Conselho escrito na spec ajuda pouco
790
790
  # quando ela nem sempre é relida.
@@ -922,7 +922,7 @@ def _texto_do_doc(doc):
922
922
  # Os temas que o render.css de fato define. Fora desta lista não existe cor
923
923
  # nenhuma — foi o que aconteceu com "noite", que passou como se fosse tema.
924
924
  TEMAS_VALIDOS = {"claro", "meia-noite", "papel", "neon", "brasa", "floresta",
925
- "retro", "oceano", "doce", "mono"}
925
+ "retro", "oceano", "doce", "mono", "ambar", "grafite"}
926
926
 
927
927
 
928
928
  def _tema_invalido(tema, onde=""):
@@ -989,7 +989,13 @@ def validar_doc(modelo, doc):
989
989
  # preto": documento válido na forma, invisível na tela.
990
990
  TIPOS_VALIDOS = {"texto", "lista", "cartoes", "numeros", "tabela", "codigo",
991
991
  "citacao", "forma", "imagem", "icone", "grafico", "no", "conector",
992
- "objeto3d", "clipe"}
992
+ "objeto3d", "clipe",
993
+ "selo", "progresso", "avatar", "dispositivo", "conversa",
994
+ "passos", "opcoes"}
995
+ # Os sete últimos são o vocabulário do vídeo de referência. O comentário
996
+ # mora aqui fora porque test/vocabulario.test.js lê esta lista com
997
+ # expressão regular, comparando com a do render.js — palavra de comentário
998
+ # dentro das chaves entrava na lista como se fosse um tipo válido.
993
999
 
994
1000
  # Tema inventado tinha o mesmo destino silencioso do tipo inventado: o CSS
995
1001
  # não casa com [data-tema="noite"], nenhuma cor é definida e a peça sai
@@ -2,10 +2,10 @@
2
2
  ferramentas.js — o que cada ferramenta oferece na interface.
3
3
  ========================================================================== */
4
4
 
5
- // Os dez que o render.css define. A lista tinha seis: quatro temas existiam
5
+ // Os doze que o render.css define. A lista tinha seis: quatro temas existiam
6
6
  // no CSS, a IA podia usá-los, e o editor não os oferecia a ninguém.
7
7
  var TEMAS = ["meia-noite", "claro", "papel", "neon", "brasa", "floresta",
8
- "retro", "oceano", "doce", "mono"];
8
+ "retro", "oceano", "doce", "mono", "ambar", "grafite"];
9
9
 
10
10
  var PALETA = ["#ff5a36", "#ffb03a", "#f4e04d", "#2fbf7f", "#1f8fe0", "#8b5cf6",
11
11
  "#f472b6", "#ffffff", "#9aa4b2", "#111318"];
@@ -162,7 +162,16 @@ function svgGrafico(e, l, a) {
162
162
 
163
163
  var TIPOS_CONHECIDOS = { texto: 1, lista: 1, cartoes: 1, numeros: 1, tabela: 1, codigo: 1,
164
164
  citacao: 1, forma: 1, imagem: 1, icone: 1, grafico: 1, no: 1, conector: 1,
165
- objeto3d: 1, clipe: 1 };
165
+ objeto3d: 1, clipe: 1,
166
+ selo: 1, progresso: 1, avatar: 1, dispositivo: 1, conversa: 1, passos: 1,
167
+ opcoes: 1 };
168
+ /* Os sete últimos são o vocabulário do vídeo de referência: pílula de estado,
169
+ trilha de progresso, foto de pessoa, celular/notebook com conteúdo dentro,
170
+ balões de conversa, etapas com uma acesa, e pergunta com alternativas.
171
+
172
+ O comentário mora AQUI FORA porque test/vocabulario.test.js lê esta lista
173
+ com expressão regular, comparando com a do servidor — e palavra de
174
+ comentário dentro das chaves entrava na lista como se fosse um tipo. */
166
175
 
167
176
  /* ── chaves de animação ──────────────────────────────────────────────────
168
177
  Transforma a lista de instantes de um elemento numa regra @keyframes de
@@ -528,6 +537,192 @@ function renderElemento(e, ctx) {
528
537
  break;
529
538
  }
530
539
 
540
+ /* ── selo ────────────────────────────────────────────────────────────
541
+ A pílula pequena, em versalete, que diz onde a cena está: "AVANÇADO",
542
+ "MÓDULO 2 DE 5", "RESULTADO 4". Custa quase nada e é o que separa uma
543
+ cena que parece produzida de uma cena que parece um slide.
544
+
545
+ Antes disso a saída era um `texto` com papel "chapeu", que é só letra
546
+ menor — sem a pílula, sem a marcação de estado. */
547
+ case "selo": {
548
+ no.textContent = "";
549
+ if (e.icone) {
550
+ var ic = document.createElement("span");
551
+ ic.className = "selo-icone";
552
+ ic.textContent = e.icone;
553
+ no.appendChild(ic);
554
+ }
555
+ var rot = document.createElement("span");
556
+ rot.textContent = e.texto || e.rotulo || "";
557
+ no.appendChild(rot);
558
+ if (e.estilo) no.setAttribute("data-estilo", e.estilo); // "vivo" | "solto"
559
+ break;
560
+ }
561
+
562
+ /* ── progresso ───────────────────────────────────────────────────────
563
+ "4 de 15", com os pontinhos. Diz ao espectador que existe um caminho e
564
+ onde ele está nele — é o que faz um vídeo de dez cenas não parecer dez
565
+ cenas soltas. */
566
+ case "progresso": {
567
+ var total = Math.max(1, Math.min(40, Number(e.total) || 1));
568
+ var atual = Math.max(1, Math.min(total, Number(e.atual) || 1));
569
+ if (e.numero !== false) {
570
+ var num = document.createElement("b");
571
+ num.textContent = atual + "/" + total;
572
+ no.appendChild(num);
573
+ }
574
+ var pontos = document.createElement("span");
575
+ pontos.className = "pontos";
576
+ for (var pi = 1; pi <= total; pi++) {
577
+ var pt = document.createElement("i");
578
+ if (pi === atual) pt.className = "aqui";
579
+ else if (pi < atual) pt.className = "feito";
580
+ pontos.appendChild(pt);
581
+ }
582
+ no.appendChild(pontos);
583
+ break;
584
+ }
585
+
586
+ /* ── avatar ──────────────────────────────────────────────────────────
587
+ A foto redonda com o anel aceso. É como se apresenta uma PESSOA — e
588
+ vídeo com rosto retém mais que vídeo sem. Antes só existia `imagem`,
589
+ que é retângulo. */
590
+ case "avatar": {
591
+ var d = e.tamanho || 260;
592
+ no.style.width = _px(d);
593
+ no.style.height = _px(d);
594
+ if (e.src) {
595
+ var foto = document.createElement("img");
596
+ foto.src = e.src;
597
+ foto.alt = e.nome || "";
598
+ no.appendChild(foto);
599
+ } else {
600
+ // Sem foto, as iniciais — melhor que um buraco cinza.
601
+ var ini = document.createElement("b");
602
+ // Em px, calculado do diâmetro. Em `%` o navegador mede contra a
603
+ // fonte do PAI, não contra a caixa: as iniciais saíam com 8px dentro
604
+ // de um círculo de 280 — um pontinho no meio do nada.
605
+ ini.style.fontSize = _px(Math.round(d * 0.34));
606
+ ini.textContent = String(e.nome || "?").trim().split(/\s+/)
607
+ .slice(0, 2).map(function (w) { return w[0]; }).join("").toUpperCase();
608
+ no.appendChild(ini);
609
+ }
610
+ break;
611
+ }
612
+
613
+ /* ── dispositivo ─────────────────────────────────────────────────────
614
+ O celular, o notebook ou a janela de navegador com conteúdo DENTRO.
615
+ É o enquadramento que faz uma tela parecer uma tela em vez de um
616
+ retângulo de texto — e o vídeo de referência usa isso o tempo todo,
617
+ com a conversa acontecendo dentro do celular.
618
+
619
+ O conteúdo é `dentro`: uma lista de elementos comuns, renderizados
620
+ recursivamente. Assim qualquer coisa cabe ali — conversa, lista,
621
+ imagem — sem inventar um vocabulário só para o miolo. */
622
+ case "dispositivo": {
623
+ var forma = /^(celular|notebook|navegador|tablet)$/.test(e.forma || "")
624
+ ? e.forma : "celular";
625
+ no.setAttribute("data-forma", forma);
626
+ if (e.altura) no.style.height = _px(e.altura);
627
+ if (e.largura) no.style.width = _px(e.largura);
628
+
629
+ var moldura = document.createElement("div");
630
+ moldura.className = "moldura";
631
+
632
+ if (forma === "navegador") {
633
+ var barra = document.createElement("div");
634
+ barra.className = "barra-nav";
635
+ barra.innerHTML = "<i></i><i></i><i></i>"
636
+ + (e.endereco ? '<span class="url">' + _fugir(e.endereco) + "</span>" : "");
637
+ moldura.appendChild(barra);
638
+ }
639
+
640
+ var tela = document.createElement("div");
641
+ tela.className = "tela";
642
+ if (e.imagem) {
643
+ tela.style.backgroundImage = "url('" + e.imagem + "')";
644
+ tela.style.backgroundSize = "cover";
645
+ tela.style.backgroundPosition = "center";
646
+ }
647
+ (e.dentro || []).forEach(function (filho) {
648
+ if (!filho || !TIPOS_CONHECIDOS[filho.tipo]) return;
649
+ ctx.i++;
650
+ tela.appendChild(renderElemento(filho, ctx));
651
+ });
652
+ moldura.appendChild(tela);
653
+ no.appendChild(moldura);
654
+
655
+ if (forma === "notebook") {
656
+ var base = document.createElement("div");
657
+ base.className = "base";
658
+ no.appendChild(base);
659
+ }
660
+ break;
661
+ }
662
+
663
+ /* ── conversa ────────────────────────────────────────────────────────
664
+ Balões de pergunta e resposta. É a forma mais direta de mostrar como
665
+ se usa uma ferramenta de IA — e é o que o vídeo de referência faz
666
+ dentro do celular. `de: "eu"` alinha à direita; qualquer outra coisa
667
+ vai para a esquerda, com destaque. */
668
+ case "conversa": {
669
+ (e.itens || []).forEach(function (fala, iFala) {
670
+ var b = document.createElement("div");
671
+ var quem = typeof fala === "string" ? (iFala % 2 ? "ela" : "eu") : (fala.de || "eu");
672
+ b.className = "balao " + (quem === "eu" ? "meu" : "dela");
673
+ b.style.setProperty("--atraso", (0.12 + iFala * 0.28).toFixed(2));
674
+ b.textContent = typeof fala === "string" ? fala : (fala.texto || "");
675
+ no.appendChild(b);
676
+ });
677
+ break;
678
+ }
679
+
680
+ /* ── passos ──────────────────────────────────────────────────────────
681
+ Os cartões de etapa com UM aceso. Diferente de `cartoes`: aqui existe
682
+ ordem e existe "onde estamos agora", e é isso que se quer mostrar. */
683
+ case "passos": {
684
+ var itens = e.itens || [];
685
+ var vivo = Number(e.atual) || 0; // 1-based; 0 = nenhum
686
+ no.style.gridTemplateColumns = "repeat(" + Math.max(1, itens.length) + ", 1fr)";
687
+ itens.forEach(function (it, k) {
688
+ var p = document.createElement("div");
689
+ p.className = "passo" + (k + 1 === vivo ? " agora" : (k + 1 < vivo ? " feito" : ""));
690
+ var partes = typeof it === "string" ? it.split("|") : [it.titulo, it.texto];
691
+ var cab = document.createElement("b");
692
+ cab.textContent = (partes[0] || "").trim();
693
+ p.appendChild(cab);
694
+ if (partes[1]) {
695
+ var sub = document.createElement("span");
696
+ sub.textContent = String(partes[1]).trim();
697
+ p.appendChild(sub);
698
+ }
699
+ no.appendChild(p);
700
+ });
701
+ break;
702
+ }
703
+
704
+ /* ── opcoes ──────────────────────────────────────────────────────────
705
+ A pergunta com alternativas e uma certa. Faz o espectador PENSAR antes
706
+ de a resposta aparecer — e uma pausa de pensamento é o que segura
707
+ alguém num vídeo. `certa` é o índice (1-based) da alternativa boa. */
708
+ case "opcoes": {
709
+ if (e.pergunta) {
710
+ var q = document.createElement("b");
711
+ q.className = "pergunta";
712
+ q.textContent = e.pergunta;
713
+ no.appendChild(q);
714
+ }
715
+ var certa = Number(e.certa) || 0;
716
+ (e.itens || []).forEach(function (it, k) {
717
+ var o = document.createElement("div");
718
+ o.className = "opcao" + (k + 1 === certa ? " certa" : "");
719
+ o.textContent = typeof it === "string" ? it : (it.texto || "");
720
+ o.style.setProperty("--atraso", (0.1 + k * 0.18).toFixed(2));
721
+ no.appendChild(o);
722
+ });
723
+ break;
724
+ }
725
+
531
726
  case "numeros": {
532
727
  var m = (e.itens || []).length;
533
728
  no.style.gridTemplateColumns = "repeat(" + Math.min(m || 1, e.colunas || m) + ", 1fr)";
@@ -975,9 +1170,21 @@ function renderCena(cena, doc, op) {
975
1170
  elementos.forEach(function (e) {
976
1171
  if (e.tipo === "conector") return;
977
1172
  if (e.x != null || e.y != null) { flutuantes.push(e); return; }
978
- if ((layout === "imagem-direita" || layout === "imagem-esquerda") && e.tipo === "imagem" && !imagem) { imagem = e; return; }
979
- if (e.papel === "chapeu" || e.papel === "titulo") cabecalho.push(e);
980
- else corpo.push(e);
1173
+ /* O aparelho também vai para o lado, não só a imagem. É o enquadramento
1174
+ do vídeo de referência: texto de um lado, celular com a conversa do
1175
+ outro. Sem isto o celular caía na coluna de texto e o outro lado ficava
1176
+ vazio — metade do quadro em branco. */
1177
+ if ((layout === "imagem-direita" || layout === "imagem-esquerda")
1178
+ && (e.tipo === "imagem" || e.tipo === "dispositivo") && !imagem) { imagem = e; return; }
1179
+ /* Selo, progresso e foto são material de CABEÇALHO: vêm antes do título,
1180
+ como sobrancelha da cena. Caindo no corpo, apareciam DEPOIS do título —
1181
+ a pílula "Módulo 2 de 5" embaixo do nome do módulo, e o rosto da pessoa
1182
+ embaixo do nome dela. Dentro do grupo a ordem é a do documento, então
1183
+ quem escreveu decide o resto. */
1184
+ if (e.papel === "chapeu" || e.papel === "titulo"
1185
+ || e.tipo === "selo" || e.tipo === "progresso" || e.tipo === "avatar") {
1186
+ cabecalho.push(e);
1187
+ } else corpo.push(e);
981
1188
  });
982
1189
 
983
1190
  function solta(lista, alvo) {
@@ -992,7 +1199,14 @@ function renderCena(cena, doc, op) {
992
1199
  solta(cabecalho, lado); solta(corpo, lado);
993
1200
  var box = document.createElement("div");
994
1201
  box.className = "lado-imagem";
995
- if (imagem) {
1202
+ if (imagem && imagem.tipo === "dispositivo") {
1203
+ // O aparelho tem moldura e sombra próprias: encostar nas bordas do
1204
+ // quadro comeria as duas. Ele entra com respiro.
1205
+ box.classList.add("com-aparelho");
1206
+ box.style.padding = margem + "px " + Math.round(W * 0.04) + "px";
1207
+ ctx.i++;
1208
+ box.appendChild(renderElemento(imagem, ctx));
1209
+ } else if (imagem) {
996
1210
  var im = renderElemento({ tipo: "imagem", src: imagem.src, ajuste: imagem.ajuste, raio: 0 }, ctx);
997
1211
  im.style.position = "absolute";
998
1212
  im.style.inset = "0";
@@ -65,6 +65,23 @@
65
65
  --c-fundo: #ffffff; --c-fundo2: #eeeeee; --c-texto: #000000; --c-fraco: #666666;
66
66
  --c-tom: #000000; --c-tom2: #000000; --c-linha: #000000; --c-cartao: #f2f2f2;
67
67
  }
68
+ /* "âmbar" — o marrom quente com dourado, que é a cara de curso e de aula.
69
+ Entrou porque faltava: os dez temas tinham roxo, ciano, laranja de fogo e
70
+ verde, e nenhum tom terroso onde o texto branco descansa. É o tema que
71
+ sustenta cena longa sem cansar a vista. */
72
+ [data-tema="ambar"] {
73
+ --f-display: "Primo Geo", "Space Grotesk", system-ui, sans-serif;
74
+ --c-fundo: #1a100b; --c-fundo2: #2a1710; --c-texto: #f7efe8; --c-fraco: #b8a08f;
75
+ --c-tom: #e8a33d; --c-tom2: #d0703a; --c-linha: rgba(232, 163, 61, .22); --c-cartao: rgba(232, 163, 61, .07);
76
+ }
77
+ /* "grafite" — o cinza-azulado sóbrio, para conteúdo técnico e relatório.
78
+ Sem cor de festa: quando o assunto é número, a cor não pode competir. */
79
+ [data-tema="grafite"] {
80
+ --f-display: "Primo Geo", "Space Grotesk", system-ui, sans-serif;
81
+ --c-fundo: #0d1117; --c-fundo2: #161d26; --c-texto: #e6edf3; --c-fraco: #8b97a6;
82
+ --c-tom: #4a9eff; --c-tom2: #7ee0c0; --c-linha: rgba(255, 255, 255, .1); --c-cartao: rgba(255, 255, 255, .04);
83
+ }
84
+
68
85
  [data-tema="mono"] .el.no,
69
86
  [data-tema="mono"] .el.cartoes .cartao { border-width: 3px; border-radius: 0; }
70
87
  [data-tema="mono"] .el.txt { letter-spacing: -.04em; }
@@ -382,6 +399,259 @@
382
399
  .el.cartoes .cartao b { font-family: var(--f-display); font-size: 27px; font-weight: 620; letter-spacing: -.02em; }
383
400
  .el.cartoes .cartao span { font-size: 19px; line-height: 1.45; color: var(--c-fraco); }
384
401
 
402
+ /* ═══════════════════════════════════════════════════════════════════════
403
+ O vocabulário do vídeo de referência.
404
+
405
+ Tudo aqui existe porque o dono mandou um vídeo e disse "pega tudo que tem
406
+ esse vídeo, e coloca pro Primo poder fazer também" — cartão com relevo,
407
+ vidro, foto, celular com conversa dentro.
408
+
409
+ O fio comum é o VIDRO: painel translúcido, borda clara em cima (onde a luz
410
+ bateria) e sombra embaixo. Não é enfeite — é o que dá profundidade sem
411
+ precisar de imagem, e o que separa "cartão" de "retângulo com borda".
412
+ ═══════════════════════════════════════════════════════════════════════ */
413
+
414
+ .vidro, .el.cartoes[data-estilo="vidro"] .cartao, .el.passos .passo {
415
+ background:
416
+ linear-gradient(180deg, rgba(255, 255, 255, .075), rgba(255, 255, 255, .015)),
417
+ var(--c-cartao);
418
+ border: 1px solid var(--c-linha);
419
+ /* A luz de cima e a sombra de baixo, dentro da própria caixa. É o truque
420
+ inteiro do "liquid glass": a borda superior clara finge um bisel. */
421
+ box-shadow:
422
+ inset 0 1px 0 rgba(255, 255, 255, .13),
423
+ 0 18px 44px rgba(0, 0, 0, .34);
424
+ }
425
+
426
+ /* ── selo ──────────────────────────────────────────────────────────────── */
427
+ .el.selo {
428
+ display: inline-flex; align-items: center; gap: .5em;
429
+ align-self: flex-start;
430
+ font-family: var(--f-corpo);
431
+ font-size: 21px; font-weight: 620;
432
+ letter-spacing: .13em; text-transform: uppercase;
433
+ color: var(--c-tom);
434
+ background: color-mix(in srgb, var(--c-tom) 13%, transparent);
435
+ border: 1px solid color-mix(in srgb, var(--c-tom) 42%, transparent);
436
+ border-radius: 999px;
437
+ padding: .42em 1.05em;
438
+ }
439
+ .el.selo[data-estilo="vivo"] {
440
+ color: var(--c-fundo);
441
+ background: var(--c-tom);
442
+ border-color: var(--c-tom);
443
+ }
444
+ .el.selo[data-estilo="solto"] {
445
+ background: none; border-color: transparent; padding-left: 0; padding-right: 0;
446
+ }
447
+ .el.selo .selo-icone { font-size: 1.15em; letter-spacing: 0; }
448
+ .cena .fluxo[data-layout="capa"] > .el.selo,
449
+ .cena .fluxo[data-layout="centro"] > .el.selo { align-self: center; }
450
+
451
+ /* ── progresso ─────────────────────────────────────────────────────────── */
452
+ .el.progresso {
453
+ display: flex; align-items: center; gap: 18px;
454
+ color: var(--c-tom); font-family: var(--f-corpo);
455
+ }
456
+ .el.progresso > b { font-size: 24px; font-weight: 620; letter-spacing: .04em; }
457
+ .el.progresso .pontos { display: flex; align-items: center; gap: 9px; }
458
+ .el.progresso .pontos i {
459
+ width: 9px; height: 9px; border-radius: 50%;
460
+ background: var(--c-linha);
461
+ transition: none;
462
+ }
463
+ .el.progresso .pontos i.feito { background: color-mix(in srgb, var(--c-tom) 45%, transparent); }
464
+ .el.progresso .pontos i.aqui {
465
+ background: var(--c-tom); width: 12px; height: 12px;
466
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--c-tom) 18%, transparent);
467
+ }
468
+
469
+ /* ── avatar ────────────────────────────────────────────────────────────── */
470
+ .el.avatar {
471
+ border-radius: 50%; overflow: hidden; position: relative;
472
+ display: grid; place-items: center; flex: 0 0 auto;
473
+ background: var(--c-cartao);
474
+ /* O anel aceso: dois anéis, um forte colado e um aberto e fraco. Um só
475
+ fica duro; dois viram luz. */
476
+ box-shadow:
477
+ 0 0 0 3px var(--c-tom),
478
+ 0 0 0 12px color-mix(in srgb, var(--c-tom) 16%, transparent),
479
+ 0 22px 50px rgba(0, 0, 0, .42);
480
+ }
481
+ .el.avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
482
+ .el.avatar b {
483
+ font-family: var(--f-display); font-weight: 640;
484
+ color: var(--c-tom); letter-spacing: .02em;
485
+ }
486
+ .cena .fluxo[data-layout="capa"] > .el.avatar,
487
+ .cena .fluxo[data-layout="centro"] > .el.avatar { align-self: center; }
488
+
489
+ /* ── dispositivo ───────────────────────────────────────────────────────── */
490
+ /* O aparelho ENCOLHE para caber na cena. Preso só pela largura, o celular de
491
+ 380px virava 800px de altura e vazava pelo pé do quadro — metade da
492
+ conversa ficava fora do vídeo, e na tela do editor parecia certo porque ali
493
+ a página rola. A altura é que manda; a largura vem da proporção. */
494
+ .el.dispositivo {
495
+ display: grid; align-content: center; justify-items: center;
496
+ flex: 0 1 auto; min-height: 0; max-height: 100%;
497
+ }
498
+ .el.dispositivo .moldura {
499
+ background: linear-gradient(180deg, #22262c, #14171b);
500
+ box-shadow:
501
+ inset 0 1px 0 rgba(255, 255, 255, .16),
502
+ 0 30px 70px rgba(0, 0, 0, .5);
503
+ overflow: hidden;
504
+ display: flex; flex-direction: column;
505
+ }
506
+ .el.dispositivo .tela {
507
+ flex: 1; overflow: hidden;
508
+ background: var(--c-fundo2);
509
+ display: flex; flex-direction: column; justify-content: center;
510
+ gap: 16px;
511
+ }
512
+
513
+ .el.dispositivo[data-forma="celular"] { width: 100%; }
514
+ .el.dispositivo[data-forma="celular"] .moldura {
515
+ height: min(100%, 780px); width: auto; max-width: 100%;
516
+ aspect-ratio: 9 / 19;
517
+ border-radius: 42px; padding: 12px;
518
+ }
519
+ .el.dispositivo[data-forma="celular"] .tela { border-radius: 31px; padding: 26px 20px; }
520
+ /* A ilha da câmera. Sem ela um retângulo arredondado é só um retângulo
521
+ arredondado — é este detalhe que faz o olho ler "celular". */
522
+ .el.dispositivo[data-forma="celular"] .moldura::before {
523
+ content: ""; position: absolute; left: 50%; top: 20px;
524
+ width: 88px; height: 9px; border-radius: 999px;
525
+ background: rgba(0, 0, 0, .55); transform: translateX(-50%); z-index: 2;
526
+ }
527
+ .el.dispositivo[data-forma="celular"] .moldura { position: relative; }
528
+
529
+ .el.dispositivo[data-forma="tablet"] .moldura {
530
+ height: min(100%, 740px); width: auto; max-width: 100%;
531
+ aspect-ratio: 3 / 4;
532
+ border-radius: 26px; padding: 16px;
533
+ }
534
+ .el.dispositivo[data-forma="tablet"] .tela { border-radius: 12px; padding: 26px; }
535
+
536
+ .el.dispositivo[data-forma="notebook"] { width: 100%; }
537
+ .el.dispositivo[data-forma="notebook"] .moldura {
538
+ width: 100%; max-height: 100%; aspect-ratio: 16 / 10;
539
+ border-radius: 16px 16px 4px 4px; padding: 12px 12px 14px;
540
+ }
541
+ .el.dispositivo[data-forma="notebook"] .tela { border-radius: 7px; padding: 26px 28px; }
542
+ .el.dispositivo[data-forma="notebook"] .base {
543
+ width: 116%; height: 13px; margin-top: -1px;
544
+ border-radius: 0 0 12px 12px;
545
+ background: linear-gradient(180deg, #2c3138, #171a1e);
546
+ box-shadow: 0 12px 26px rgba(0, 0, 0, .45);
547
+ }
548
+
549
+ .el.dispositivo[data-forma="navegador"] { width: 100%; }
550
+ .el.dispositivo[data-forma="navegador"] .moldura {
551
+ width: 100%; max-height: 100%; aspect-ratio: 16 / 10; border-radius: 14px; padding: 0;
552
+ }
553
+ .el.dispositivo .barra-nav {
554
+ display: flex; align-items: center; gap: 8px;
555
+ padding: 13px 16px; flex: 0 0 auto;
556
+ border-bottom: 1px solid rgba(255, 255, 255, .07);
557
+ }
558
+ .el.dispositivo .barra-nav i {
559
+ width: 11px; height: 11px; border-radius: 50%; background: rgba(255, 255, 255, .17);
560
+ }
561
+ .el.dispositivo .barra-nav .url {
562
+ margin-left: 12px; flex: 1;
563
+ font-family: var(--f-mono); font-size: 15px; color: rgba(255, 255, 255, .45);
564
+ background: rgba(0, 0, 0, .3); border-radius: 999px; padding: 5px 14px;
565
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
566
+ }
567
+ .el.dispositivo[data-forma="navegador"] .tela { padding: 24px 26px; }
568
+
569
+ /* ── conversa ──────────────────────────────────────────────────────────── */
570
+ .el.conversa {
571
+ display: flex; flex-direction: column; gap: 14px; justify-content: flex-end;
572
+ }
573
+ .el.conversa .balao {
574
+ max-width: 88%; padding: 13px 17px; line-height: 1.42;
575
+ font-size: 20px; border-radius: 16px;
576
+ }
577
+ .el.conversa .balao.meu {
578
+ align-self: flex-start;
579
+ background: var(--c-cartao); border: 1px solid var(--c-linha);
580
+ color: var(--c-texto); border-bottom-left-radius: 5px;
581
+ }
582
+ /* A resposta é o que importa: ela leva a cor do tema. */
583
+ .el.conversa .balao.dela {
584
+ align-self: flex-end;
585
+ background: color-mix(in srgb, var(--c-tom) 20%, transparent);
586
+ border: 1px solid color-mix(in srgb, var(--c-tom) 40%, transparent);
587
+ color: var(--c-texto); border-bottom-right-radius: 5px;
588
+ }
589
+ /* Os balões entram um de cada vez, como numa conversa de verdade. Todos ao
590
+ mesmo tempo lê como captura de tela, não como diálogo. */
591
+ .cena.animar .el.conversa .balao {
592
+ animation: balaoEntra .46s cubic-bezier(.22, 1, .36, 1) both;
593
+ animation-delay: calc(var(--atraso, 0) * 1s);
594
+ }
595
+ @keyframes balaoEntra {
596
+ from { opacity: 0; transform: translateY(14px) scale(.96); }
597
+ to { opacity: 1; transform: none; }
598
+ }
599
+
600
+ /* ── passos ────────────────────────────────────────────────────────────── */
601
+ /* Sem largura declarada, o grid encolhe até o conteúdo e as três etapas
602
+ viram três selinhos no meio da tela — em `capa` e `centro`, onde o eixo
603
+ está centralizado, isso é o padrão. Etapa é estrutura: ocupa a linha. */
604
+ .el.passos { display: grid; gap: 18px; align-items: stretch; width: 100%; }
605
+ .el.passos .passo {
606
+ border-radius: 18px; padding: 22px 20px;
607
+ display: grid; gap: 6px; align-content: start;
608
+ color: var(--c-fraco);
609
+ }
610
+ .el.passos .passo b {
611
+ font-family: var(--f-display); font-size: 23px; font-weight: 620;
612
+ letter-spacing: -.015em; color: inherit;
613
+ }
614
+ .el.passos .passo span { font-size: 17px; line-height: 1.4; }
615
+ .el.passos .passo.feito { color: var(--c-texto); }
616
+ /* O de agora cresce e acende. Escala pequena de propósito: 1.06 lê como
617
+ "este", 1.2 lê como erro de layout. */
618
+ .el.passos .passo.agora {
619
+ color: var(--c-texto);
620
+ transform: scale(1.06);
621
+ border-color: color-mix(in srgb, var(--c-tom) 55%, transparent);
622
+ background:
623
+ linear-gradient(180deg, color-mix(in srgb, var(--c-tom) 17%, transparent),
624
+ color-mix(in srgb, var(--c-tom) 6%, transparent));
625
+ box-shadow:
626
+ inset 0 1px 0 rgba(255, 255, 255, .16),
627
+ 0 20px 46px color-mix(in srgb, var(--c-tom) 16%, transparent);
628
+ }
629
+ .el.passos .passo.agora b { color: var(--c-tom); }
630
+
631
+ /* ── opcoes ────────────────────────────────────────────────────────────── */
632
+ .el.opcoes { display: grid; gap: 12px; align-content: center; }
633
+ .el.opcoes .pergunta {
634
+ font-family: var(--f-display); font-size: 26px; font-weight: 640;
635
+ letter-spacing: -.018em; margin-bottom: 6px; color: var(--c-texto);
636
+ }
637
+ .el.opcoes .opcao {
638
+ border: 1px solid var(--c-linha); border-radius: 12px;
639
+ padding: 14px 18px; font-size: 19px; color: var(--c-fraco);
640
+ background: var(--c-cartao);
641
+ }
642
+ .el.opcoes .opcao.certa {
643
+ color: var(--c-tom);
644
+ border-color: color-mix(in srgb, var(--c-tom) 60%, transparent);
645
+ background: color-mix(in srgb, var(--c-tom) 12%, transparent);
646
+ }
647
+ /* O ✓ vem do CSS, não do texto: assim o autor escreve só a alternativa e a
648
+ marca nunca aparece por engano na errada. */
649
+ .el.opcoes .opcao.certa::before { content: "✓ "; font-weight: 700; }
650
+ .cena.animar .el.opcoes .opcao {
651
+ animation: balaoEntra .4s cubic-bezier(.22, 1, .36, 1) both;
652
+ animation-delay: calc(var(--atraso, 0) * 1s);
653
+ }
654
+
385
655
  .el.numeros { display: grid; gap: calc(30px * var(--esc, 1)); align-content: center; }
386
656
  .el.numeros .num { display: grid; gap: 6px; }
387
657
  .el.numeros .num b {
@@ -656,3 +926,10 @@
656
926
 
657
927
  /* painel: sem moldura, é uma superfície flutuando em profundidade */
658
928
  .el.obj3d.f-painel .face3d { border: 0; box-shadow: 0 40px 90px -30px rgba(0,0,0,.7); }
929
+
930
+ /* O lado do aparelho não é o lado da imagem: a imagem sangra até a borda, o
931
+ celular precisa de ar em volta para a moldura e a sombra aparecerem. */
932
+ .cena .fluxo .lado-imagem.com-aparelho {
933
+ display: grid; place-items: center; min-height: 0; overflow: visible;
934
+ }
935
+ .cena .fluxo .lado-imagem.com-aparelho > .el { max-height: 100%; }