sagadeck 1.2.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.
Files changed (233) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +305 -0
  3. package/SKILL.md +148 -0
  4. package/bin/sagadeck.js +511 -0
  5. package/docs/ANDAMENTO.md +1255 -0
  6. package/docs/EXPERIENCIAS.md +39 -0
  7. package/docs/PLANO-EXPERIENCIAS.md +39 -0
  8. package/docs/PLANO-TESTE-AULAS-TECNICAS.md +44 -0
  9. package/docs/PLANO-TESTE-DESINFORMACAO.md +38 -0
  10. package/docs/REFERENCIA.md +1131 -0
  11. package/package.json +59 -0
  12. package/src/adaptive-layouts.js +18 -0
  13. package/src/ai/art-direction.js +8 -0
  14. package/src/ai/commands.js +130 -0
  15. package/src/ai/context.js +245 -0
  16. package/src/ai/deck-ai.js +1135 -0
  17. package/src/ai/directions.js +74 -0
  18. package/src/ai/document-materials.js +49 -0
  19. package/src/ai/document-visuals.js +215 -0
  20. package/src/ai/evaluation.js +31 -0
  21. package/src/ai/ground.js +55 -0
  22. package/src/ai/llm.js +292 -0
  23. package/src/ai/quality.js +51 -0
  24. package/src/ai/relay.js +45 -0
  25. package/src/ai/staged-generation.js +24 -0
  26. package/src/ai/transform.js +858 -0
  27. package/src/ai/usage.js +17 -0
  28. package/src/ai/variety.js +80 -0
  29. package/src/ai/video-frames.js +24 -0
  30. package/src/ai/video-generation.js +126 -0
  31. package/src/ai/web-operation.js +31 -0
  32. package/src/algo-catalog.js +153 -0
  33. package/src/algo-trace.js +139 -0
  34. package/src/api-client.js +514 -0
  35. package/src/api-collections.js +14 -0
  36. package/src/api-demo.js +222 -0
  37. package/src/aspect.js +40 -0
  38. package/src/build.js +323 -0
  39. package/src/carousel.js +56 -0
  40. package/src/chrome.js +65 -0
  41. package/src/code-highlight.js +68 -0
  42. package/src/code-lab-browser.js +22 -0
  43. package/src/code-lab.js +3 -0
  44. package/src/code-language.js +52 -0
  45. package/src/csv.js +74 -0
  46. package/src/decision-lab.js +20 -0
  47. package/src/deck-file.js +119 -0
  48. package/src/diagram/napkin-examples.js +13 -0
  49. package/src/diagram/napkin.js +539 -0
  50. package/src/docx.js +128 -0
  51. package/src/dynamics/git-sim.js +200 -0
  52. package/src/dynamics/layouts.js +112 -0
  53. package/src/elements.js +554 -0
  54. package/src/experiences.js +124 -0
  55. package/src/exploration.js +31 -0
  56. package/src/export/browser.js +53 -0
  57. package/src/export/estudo.js +67 -0
  58. package/src/export/pptx.js +475 -0
  59. package/src/export/roteiro.js +40 -0
  60. package/src/export/shots.js +182 -0
  61. package/src/figures/charts.js +383 -0
  62. package/src/figures/diagrams.js +117 -0
  63. package/src/figures/icons.js +166 -0
  64. package/src/figures/material-pictos.js +20 -0
  65. package/src/figures/pictos.js +255 -0
  66. package/src/figures/points.js +106 -0
  67. package/src/figures/qr.js +29 -0
  68. package/src/figures/ufmap.js +48 -0
  69. package/src/fiscal/autofix.js +377 -0
  70. package/src/fiscal/normalize.js +115 -0
  71. package/src/graph-lab.js +24 -0
  72. package/src/hologram.js +30 -0
  73. package/src/identity.js +86 -0
  74. package/src/import/crop.js +59 -0
  75. package/src/import/index.js +132 -0
  76. package/src/import/legacy-word.js +42 -0
  77. package/src/import/merge.js +45 -0
  78. package/src/import/office.js +123 -0
  79. package/src/import/omml.js +69 -0
  80. package/src/import/pdf-render.js +56 -0
  81. package/src/import/pptx.js +701 -0
  82. package/src/import/xml.js +42 -0
  83. package/src/infographic.js +401 -0
  84. package/src/layouts.js +723 -0
  85. package/src/lessons.js +171 -0
  86. package/src/library.js +399 -0
  87. package/src/markup.js +116 -0
  88. package/src/master.js +214 -0
  89. package/src/mcp/server.js +371 -0
  90. package/src/meeting.js +42 -0
  91. package/src/motion.js +56 -0
  92. package/src/package.js +116 -0
  93. package/src/playground.js +19 -0
  94. package/src/portal-scene.js +20 -0
  95. package/src/preferences.js +61 -0
  96. package/src/protect.js +51 -0
  97. package/src/purpose.js +24 -0
  98. package/src/pytrace.js +1092 -0
  99. package/src/research/duckduckgo.js +73 -0
  100. package/src/research/research.js +202 -0
  101. package/src/research/svg-image.js +17 -0
  102. package/src/research/visuals.js +52 -0
  103. package/src/runtime/api-collection-ui.js +5 -0
  104. package/src/runtime/api-collections.js +28 -0
  105. package/src/runtime/api-core.js +530 -0
  106. package/src/runtime/api-ui.js +966 -0
  107. package/src/runtime/base.css +1774 -0
  108. package/src/runtime/calc.js +139 -0
  109. package/src/runtime/code-lab.js +371 -0
  110. package/src/runtime/decision-lab.js +49 -0
  111. package/src/runtime/diagram.js +341 -0
  112. package/src/runtime/fit.js +268 -0
  113. package/src/runtime/fonts/caveat.css +2 -0
  114. package/src/runtime/fonts/comic-neue.css +3 -0
  115. package/src/runtime/fonts/fredoka.css +2 -0
  116. package/src/runtime/fonts/index.json +10 -0
  117. package/src/runtime/fonts/inter.css +2 -0
  118. package/src/runtime/fonts/patrick-hand.css +2 -0
  119. package/src/runtime/fonts/plus-jakarta-sans.css +2 -0
  120. package/src/runtime/fonts/press-start-2p.css +2 -0
  121. package/src/runtime/fonts/vt323.css +2 -0
  122. package/src/runtime/formula.js +148 -0
  123. package/src/runtime/graph-lab.js +72 -0
  124. package/src/runtime/meeting.css +7 -0
  125. package/src/runtime/meeting.js +10 -0
  126. package/src/runtime/motion.js +15 -0
  127. package/src/runtime/playground.js +11 -0
  128. package/src/runtime/points.js +19 -0
  129. package/src/runtime/portal-scene.css +6 -0
  130. package/src/runtime/portal-scene.js +22 -0
  131. package/src/runtime/runtime.js +827 -0
  132. package/src/runtime/science.js +89 -0
  133. package/src/runtime/skins/aluminio.css +31 -0
  134. package/src/runtime/skins/arcade.css +25 -0
  135. package/src/runtime/skins/aurora.css +17 -0
  136. package/src/runtime/skins/bauhaus.css +19 -0
  137. package/src/runtime/skins/editorial.css +19 -0
  138. package/src/runtime/skins/jornal.css +17 -0
  139. package/src/runtime/skins/manual-noite.css +14 -0
  140. package/src/runtime/skins/manual.css +14 -0
  141. package/src/runtime/skins/noite.css +21 -0
  142. package/src/runtime/skins/oceano.css +17 -0
  143. package/src/runtime/skins/pop.css +23 -0
  144. package/src/runtime/skins/prata.css +18 -0
  145. package/src/runtime/skins/prisma.css +28 -0
  146. package/src/runtime/skins/rabisco.css +18 -0
  147. package/src/runtime/skins/relevo.css +33 -0
  148. package/src/runtime/skins/sinal.css +16 -0
  149. package/src/runtime/skins/terminal.css +23 -0
  150. package/src/runtime/vendor/KATEX-LICENSE.txt +21 -0
  151. package/src/runtime/vendor/MERMAID-LICENSE.txt +21 -0
  152. package/src/runtime/vendor/PLOTLY-LICENSE.txt +21 -0
  153. package/src/runtime/vendor/katex.css +1 -0
  154. package/src/runtime/vendor/mermaid.min.js +3636 -0
  155. package/src/runtime/vendor/plotly.min.js +3620 -0
  156. package/src/runtime/video-player.js +35 -0
  157. package/src/science.js +162 -0
  158. package/src/studio/ai-response.js +36 -0
  159. package/src/studio/api-routes.js +152 -0
  160. package/src/studio/assets/contexto/leia-me.md +12 -0
  161. package/src/studio/assets/contexto/vendas.csv +7 -0
  162. package/src/studio/assets/imagens/colecao-arquitetura.png +0 -0
  163. package/src/studio/assets/imagens/colecao-cor.png +0 -0
  164. package/src/studio/assets/imagens/colecao-equipe.png +0 -0
  165. package/src/studio/assets/imagens/lavanda-cover.jpg +0 -0
  166. package/src/studio/code-routes.js +32 -0
  167. package/src/studio/command-approval.js +48 -0
  168. package/src/studio/demo-decks.js +98 -0
  169. package/src/studio/direction-routes.js +29 -0
  170. package/src/studio/errors.js +65 -0
  171. package/src/studio/exploration-demo.js +17 -0
  172. package/src/studio/exporting.js +105 -0
  173. package/src/studio/generation-report.js +10 -0
  174. package/src/studio/instance.js +48 -0
  175. package/src/studio/layout-samples.js +152 -0
  176. package/src/studio/project.js +371 -0
  177. package/src/studio/public/api-collections.js +28 -0
  178. package/src/studio/public/app.js +5265 -0
  179. package/src/studio/public/art-preview.js +10 -0
  180. package/src/studio/public/calc-fields.js +11 -0
  181. package/src/studio/public/chat-progress.js +47 -0
  182. package/src/studio/public/developer-fields.js +22 -0
  183. package/src/studio/public/explorer.js +443 -0
  184. package/src/studio/public/history.js +58 -0
  185. package/src/studio/public/index.html +867 -0
  186. package/src/studio/public/inspector.js +270 -0
  187. package/src/studio/public/lab-fields.js +5 -0
  188. package/src/studio/public/library.css +275 -0
  189. package/src/studio/public/library.html +55 -0
  190. package/src/studio/public/library.js +471 -0
  191. package/src/studio/public/merge-decks.js +155 -0
  192. package/src/studio/public/review-ui.js +100 -0
  193. package/src/studio/public/screenshot-editor.js +140 -0
  194. package/src/studio/public/share-ui.js +72 -0
  195. package/src/studio/public/slide-clipboard.js +23 -0
  196. package/src/studio/public/slide-form.js +1263 -0
  197. package/src/studio/public/studio-awareness.js +154 -0
  198. package/src/studio/public/studio-next.css +303 -0
  199. package/src/studio/public/style.css +1262 -0
  200. package/src/studio/public/ui-icons.js +2 -0
  201. package/src/studio/public/viewers.js +372 -0
  202. package/src/studio/public/visual-editor.js +326 -0
  203. package/src/studio/public/workspace.js +18 -0
  204. package/src/studio/public-files.js +1 -0
  205. package/src/studio/request-helpers.js +41 -0
  206. package/src/studio/server.js +1583 -0
  207. package/src/studio/share-routes.js +146 -0
  208. package/src/studio/slide-copy-routes.js +26 -0
  209. package/src/studio/slide-copy.js +30 -0
  210. package/src/studio/snapshot.js +160 -0
  211. package/src/studio/style-routes.js +103 -0
  212. package/src/studio/template-collections.js +97 -0
  213. package/src/studio/versions.js +95 -0
  214. package/src/studio/video-download.js +29 -0
  215. package/src/studio/visual-keys.js +52 -0
  216. package/src/studio/workspaces.js +39 -0
  217. package/src/table.js +93 -0
  218. package/src/templates/scaffold.js +372 -0
  219. package/src/themes.js +604 -0
  220. package/src/trace-view.js +450 -0
  221. package/src/uid.js +19 -0
  222. package/src/video-player.js +40 -0
  223. package/src/visual-edits.js +99 -0
  224. package/src/ws.js +134 -0
  225. package/templates/cenario/Texto no cen/303/241rio.yaml" +171 -0
  226. package/templates/cenario/imagens/cidade.svg +30 -0
  227. package/templates/cenario/imagens/pessoa.svg +15 -0
  228. package/templates/cenario-e-ciencia.yaml +47 -0
  229. package/templates/ensaio-api.yaml +160 -0
  230. package/templates/exemplo-alegre.yaml +75 -0
  231. package/templates/exemplo-keynote.yaml +90 -0
  232. package/templates/exemplo-napkin.yaml +139 -0
  233. package/templates/exemplo.yaml +228 -0
package/src/layouts.js ADDED
@@ -0,0 +1,723 @@
1
+ import { manualPollHTML } from './meeting.js';
2
+ import { portalHTML } from './portal-scene.js';
3
+ // Layouts: cada um recebe o objeto do slide (YAML) e devolve o HTML da área útil.
4
+ // Todos aceitam: kicker, title, source, add (elementos extras no fim), tone, notes, time.
5
+ import { tableHTML } from "./table.js";
6
+ import { decisionLabHTML } from "./decision-lab.js";
7
+ import { graphLabHTML } from './graph-lab.js';
8
+ import { playgroundHTML } from './playground.js';
9
+ import { codeLabHTML } from './code-lab.js';
10
+ import { md, esc } from "./markup.js";
11
+ import { adaptiveHTML } from "./adaptive-layouts.js";
12
+ import { iconSVG, listIcons } from "./figures/icons.js";
13
+ let iconNames = null;
14
+ const diagramIconNames = () => (iconNames ||= new Set(listIcons()));
15
+ import { el, text, figureHTML, imageSize, attrs, SIZES, list, cards, stats, steps, poll, timer, counter, code } from "./elements.js";
16
+ import { displayCodeLanguage, resolveCodeLanguage } from "./code-language.js";
17
+ import { mathHTML, plotHTML } from "./science.js";
18
+ import { infographicHTML, itemColors } from "./infographic.js";
19
+ import { duelHTML, terminalsHTML, turnsHTML } from "./dynamics/layouts.js";
20
+ import { carouselHTML } from "./carousel.js";
21
+ import { slideSize } from "./aspect.js";
22
+ import { recreatedFrame } from "./master.js";
23
+ import { solutionHTML, calcHTML, algoHTML } from "./lessons.js";
24
+ import "./runtime/api-core.js"; // globalThis.SagadeckApiCore (o mesmo núcleo que roda na apresentação)
25
+
26
+ const plainTitle = (s) => String(s.title || s.kicker || s.layout || "").replace(/[*=^~`]/g, "").slice(0, 40);
27
+ const kicker = (s, d = 0) => (s.kicker ? `<div class="kicker t f-label e" style="--d:${d}">${md(s.kicker)}</div>` : "");
28
+ const title = (s, as = "h2", d = 1, extra = {}) => (s.title ? text(s.title, as, { class: "ttl e", style: `--d:${d};`, fit: s.fit, ...extra, ...(s.titleSize ? { size: s.titleSize } : {}) }) : "");
29
+ const head = (s, as = "h2") => (s.kicker || s.title ? `<header class="hd">${kicker(s)}${title(s, as)}</header>` : "");
30
+ const src = (s) => (s.source ? `<div class="src t f-body">${md(s.source)}</div>` : "");
31
+ const add = (s, ctx) => (s.add ? el(s.add, ctx) : "");
32
+ const fig = (f, ctx, w, h, cls = "") => (f ? `<div class="figbox ${cls}" style="${w ? `width:${w}px;` : ""}${h ? `height:${h}px;` : ""}">${typeof f === "object" ? el(f, ctx, w, h) : el({ image: f }, ctx)}</div>` : "");
33
+ const build = (s, i, base = 1) => (s.build ? base + i : undefined);
34
+ const codeEditor = (s, { source, highlight = s.highlight, footer = "CÓDIGO · LINHAS NUMERADAS" } = {}) => {
35
+ const filename = s.filename || "código";
36
+ const language = resolveCodeLanguage(s.language, filename);
37
+ const languageLabel = displayCodeLanguage(s.language, filename) || "Código";
38
+ return `<div class="codewalk-editor"><div class="codewalk-bar"><span class="codewalk-lights" aria-hidden="true"><i></i><i></i><i></i></span><span class="codewalk-file f-mono">${esc(filename)}</span><span class="codewalk-language f-label">${esc(languageLabel)}</span></div>${code({ code: source || s.code || "// Cole seu código aqui", language, filename, highlight, size: s.size || 30 })}<div class="codewalk-footer f-label">${footer}</div></div>`;
39
+ };
40
+ // As cenas de aula deixam todo o conteúdo no HTML: funcionam offline e têm resumo estático.
41
+ const lessonSteps = (items, fallback) => Array.isArray(items) && items.length ? items.map((p) => typeof p === "string" ? { title: p } : (p || {})) : [fallback];
42
+ const kineticPhrases = (value) => {
43
+ const words = String(value || "").match(/==[^=]+==|\*\*.*?\*\*|\*[^*]+\*|~~.*?~~|`[^`]+`|\[[^\]]+\]\([^)]+\)|\S+/g) || [];
44
+ const connectors = new Set(["a", "as", "de", "do", "da", "dos", "das", "e", "em", "no", "na", "nos", "nas", "para", "por", "com", "o", "os"]);
45
+ const phrases = [];
46
+ while (words.length) {
47
+ let take = Math.ceil(words.length / Math.ceil(words.length / 4));
48
+ const last = words[take - 1]?.replace(/^[^A-Za-zÀ-ÿ]+|[^A-Za-zÀ-ÿ]+$/g, "").toLowerCase();
49
+ if (take > 1 && connectors.has(last)) take--;
50
+ phrases.push(words.splice(0, take).join(" "));
51
+ }
52
+ return phrases;
53
+ };
54
+ const lessonControls = (items) => `<nav class="lesson-controls" aria-label="Etapas da explicação"><button type="button" data-lesson-prev aria-label="Etapa anterior" disabled>${iconSVG("arrow-left", { size: 26, stroke: 2 })}</button><div class="lesson-dots">${items.map((p, i) => `<button type="button" data-lesson-go="${i}" aria-label="Etapa ${i + 1}: ${esc(p.title || "Explicação")}" aria-current="${i === 0 ? "step" : "false"}">${String(i + 1).padStart(2, "0")}</button>`).join("")}</div><button type="button" data-lesson-next aria-label="Próxima etapa" ${items.length === 1 ? "disabled" : ""}>${iconSVG("arrow-right", { size: 26, stroke: 2 })}</button></nav>`;
55
+ const lessonPanels = (items, output = false) => `<div class="lesson-panels" aria-live="polite" aria-atomic="true">${items.map((p, i) => `<article class="lesson-panel${i === 0 ? " active" : ""}" data-lesson-panel="${i}" data-highlight="${esc(JSON.stringify([].concat(p.highlight || []).filter(Number.isFinite)))}"><div class="lesson-counter f-label">${String(i + 1).padStart(2, "0")} / ${String(items.length).padStart(2, "0")}</div>${text(p.title || `Etapa ${i + 1}`, "h3", { class: "lesson-title", size: 48 })}${p.text ? text(p.text, "body", { class: "lesson-text", size: 32 }) : ""}${output && p.output != null ? `<div class="lesson-output"><div class="lesson-output-label f-label">Saída esperada · simulação</div><pre class="f-mono">${esc(p.output)}</pre></div>` : ""}</article>`).join("")}</div>`;
56
+ const lessonSummary = (items) => `<div class="lesson-summary">${items.map((p, i) => `<article><div class="f-label lesson-summary-number">${String(i + 1).padStart(2, "0")}</div><div><strong class="f-heading">${md(p.title || `Etapa ${i + 1}`)}</strong>${p.text ? `<p class="f-body">${md(p.text)}</p>` : ""}${p.output != null ? `<pre class="f-mono">${esc(p.output)}</pre>` : ""}</div></article>`).join("")}</div>`;
57
+ const percent = (value, fallback, min = 0, max = 100) => Number.isFinite(Number(value)) ? Math.max(min, Math.min(max, Number(value))) : fallback;
58
+
59
+ // Texto no cenário: as composições. label = nome no formulário; size = tamanho padrão do título (titleSize vence);
60
+ // props = cenografia extra (só texto e caixas; os desenhos são CSS em base.css, nada de símbolo unicode).
61
+ const candles = () => Array.from({ length: 30 }, (_, i) => {
62
+ const y = 120 + Math.round(Math.sin(i * 0.55) * 90 + i * 11), h = 40 + ((i * 37) % 90), up = (i * 7) % 3 !== 0;
63
+ return `<i class="candle ${up ? 'up' : 'down'}" style="--x:${i * 64}px;--y:${y}px;--h:${h}px"></i>`;
64
+ }).join('');
65
+ export const SCENES = {
66
+ stage: { label: 'Palco e profundidade', size: 230, tone: 'dark' },
67
+ floor: { label: 'Chão em perspectiva', size: 230 },
68
+ signs: { label: 'Placas na cidade', size: 230, tone: 'dark' },
69
+ terminal: { label: 'Terminal hacker', size: 150, tone: 'dark', props: () => `<pre class="scene-code">${Array.from({ length: 34 }, (_, i) => `${(0x7f3a00 + i * 16).toString(16)} ${Array.from({ length: 12 }, (_, j) => ((i * 31 + j * 17) % 256).toString(16).padStart(2, '0')).join(' ')} ${['ssh saga@deck','sudo apresentar --ao-vivo','git push origin ideia','cat segredo.txt | grep plano','nmap -sV palco.local','tail -f plateia.log'][i % 6]}`).join('\n')}</pre><div class="scene-scan"></div>` },
70
+ cafe: { label: 'Lousa de café', size: 170, tone: 'dark', props: () => `<div class="scene-neon">aberto</div><ul class="scene-menu"><li><span>espresso</span><span>7</span></li><li><span>coado do dia</span><span>9</span></li><li><span>pão de queijo</span><span>6</span></li><li><span>ideia nova</span><span>grátis</span></li></ul>` },
71
+ travel: { label: 'Cartão de embarque', size: 140, props: () => `<div class="scene-pass"><div class="scene-pass-top">CARTÃO DE EMBARQUE</div><dl><div><dt>DE</dt><dd>GRU</dd></div><div><dt>PARA</dt><dd>LIS</dd></div><div><dt>VOO</dt><dd>SG 2026</dd></div><div><dt>PORTÃO</dt><dd>12</dd></div><div><dt>ASSENTO</dt><dd>7A</dd></div><div><dt>EMBARQUE</dt><dd>09:40</dd></div></dl><div class="scene-barcode"></div></div>` },
72
+ ticker: { label: 'Pregão financeiro', size: 190, tone: 'dark', props: () => `<div class="scene-tape"><span>IBOV 128.430 <b class="up">+1,24%</b></span><span>USD/BRL 5,02 <b class="down">-0,31%</b></span><span>SAGA3 42,10 <b class="up">+8,90%</b></span><span>IDEIA11 99,00 <b class="up">+12,5%</b></span><span>BTC 312.004 <b class="down">-2,05%</b></span><span>SELIC 10,50%</span><span>IBOV 128.430 <b class="up">+1,24%</b></span></div><div class="scene-candles">${candles()}</div>` },
73
+ marquee: { label: 'Letreiro de cinema', size: 150, tone: 'dark', props: () => `<div class="scene-curtain left"></div><div class="scene-curtain right"></div><div class="scene-ticket">SESSÃO 20H</div>` },
74
+ blueprint: { label: 'Planta técnica', size: 170, tone: 'accent', props: () => `<div class="scene-dim h"><span>1920</span></div><div class="scene-dim v"><span>1080</span></div><table class="scene-stamp"><tr><td>PROJETO</td><td>SAGADECK</td></tr><tr><td>FOLHA</td><td>01 / 13</td></tr><tr><td>ESCALA</td><td>1:1</td></tr><tr><td>REVISÃO</td><td>A</td></tr></table>` },
75
+ magazine: { label: 'Capa de revista', size: 210, props: () => `<div class="scene-masthead">SAGA</div><div class="scene-issue">EDIÇÃO 13 <i></i> SETEMBRO 2026 <i></i> R$ 24,90</div><ul class="scene-lines"><li><b>+10</b> ideias que mudam o jogo</li><li><b>Exclusivo</b> os bastidores do palco</li><li><b>Guia</b> como contar uma história</li></ul><div class="scene-barcode"></div>` },
76
+ orbit: { label: 'Órbita', size: 170, tone: 'dark', props: () => `<div class="scene-stars"></div><div class="scene-stars far"></div><div class="scene-hud"><span>LAT -23.55</span><span>LON -46.63</span><span>ALT 408 KM</span><span>V 7,66 KM/S</span></div>` },
77
+ synthwave: { label: 'Neon anos 80', size: 190, tone: 'dark', props: () => `<div class="scene-sun"></div><div class="scene-grid"></div><div class="scene-mountains"></div>` },
78
+ gallery: { label: 'Parede de galeria', size: 170, props: () => `<div class="scene-spot"></div><div class="scene-plaque"><b>Sem título</b><span>2026. Texto sobre parede.</span><span>Coleção particular</span></div><div class="scene-baseboard"></div>` },
79
+ };
80
+
81
+ // No blocks, "rótulo e depois o gráfico" é o título do gráfico, e dois ou mais gráficos seguidos ficam lado a lado:
82
+ // empilhados, cada um ganhava 200 e poucos px de altura e, mantendo a proporção, saía estreito no meio do slide.
83
+ function chartsSideBySide(content) {
84
+ if (!Array.isArray(content)) return content;
85
+ const isChart = (x) => x && typeof x === "object" && x.chart && !x.row;
86
+ const isLabel = (x) => x && typeof x === "object" && Object.keys(x).length === 1 && typeof x.label === "string";
87
+ const items = [];
88
+ for (let i = 0; i < content.length; i++) {
89
+ const x = content[i], next = content[i + 1];
90
+ if (isLabel(x) && isChart(next) && !next.title) { items.push({ ...next, title: x.label }); i++; } else items.push(x);
91
+ }
92
+ const out = [];
93
+ for (let i = 0; i < items.length;) {
94
+ let j = i; while (j < items.length && isChart(items[j])) j++;
95
+ if (j - i >= 2) { out.push({ row: items.slice(i, j) }); i = j; } else out.push(items[i++]);
96
+ }
97
+ return out;
98
+ }
99
+
100
+ export const LAYOUTS = {
101
+ portal(s,ctx){return portalHTML(s,ctx,head)+src(s)+add(s,ctx);},
102
+ playground(s,ctx){return playgroundHTML(s,ctx,head)+src(s)+add(s,ctx);},
103
+ codelab(s,ctx){return codeLabHTML(s,ctx,head)+src(s)+add(s,ctx);},
104
+ graphlab(s,ctx){return graphLabHTML(s,ctx,head)+src(s)+add(s,ctx);},
105
+ mosaic(s) { return `${head(s)}${adaptiveHTML(s,'mosaic')}${src(s)}`; },
106
+ ribbon(s) { return `${head(s)}${adaptiveHTML(s,'ribbon')}${src(s)}`; },
107
+ dossier(s) { return `${head(s)}${adaptiveHTML(s,'dossier')}${src(s)}`; },
108
+ decisionlab(s) { return `${head(s)}${decisionLabHTML(s.lab)}${src(s)}`; },
109
+ cover(s, ctx) {
110
+ return `<div class="L-cover">
111
+ <div class="cv-main">${kicker(s)}${text(s.title, "hero", { class: "ttl e", style: "--d:1;", fit: true, size: s.titleSize })}
112
+ ${s.subtitle ? text(s.subtitle, "lead", { class: "sub e", style: "--d:2;" }) : ""}
113
+ ${s.author ? `<div class="author e" style="--d:3">${text(s.author, "h3", { size: 40 })}${s.role ? text(s.role, "small", { class: "muted" }) : ""}</div>` : ""}</div>
114
+ ${s.figure ? `<div class="cv-fig e" style="--d:2">${el(s.figure, ctx, 700, 760)}</div>` : ""}
115
+ </div>${add(s, ctx)}`;
116
+ },
117
+
118
+ section(s, ctx) {
119
+ return `<div class="L-section">
120
+ ${s.number != null ? `<div class="sc-num t f-display e">${esc(s.number)}</div>` : ""}
121
+ <div class="sc-text">${kicker(s, 1)}${text(s.title, "title", { class: "ttl e", style: "--d:2;", size: s.titleSize || 150, fit: true })}${s.subtitle ? text(s.subtitle, "lead", { class: "sub e", style: "--d:3;" }) : ""}</div>
122
+ ${s.figure ? `<div class="sc-fig e" style="--d:3">${el(s.figure, ctx, 560, 560)}</div>` : ""}
123
+ </div>${add(s, ctx)}`;
124
+ },
125
+
126
+ statement(s, ctx) {
127
+ // o tamanho pelo comprimento: frase curta é manchete; parágrafo inteiro em letra de título ocupava a tela toda
128
+ const words = String(s.text || (s.lines || []).map((l) => (typeof l === "string" ? l : l?.text || "")).join(" ")).split(/\s+/).filter(Boolean).length;
129
+ const role = s.as || (words > 45 ? "lead" : words > 22 ? "h2" : "title");
130
+ const lines = s.lines
131
+ ? s.lines.map((l, i) => {
132
+ const o = typeof l === "string" ? { text: l } : l;
133
+ // a linha pode ser menor que o tamanho que o texto todo permite, nunca maior (três linhas longas, cada uma
134
+ // em h2, ocupavam a tela inteira); o as do slide inteiro (pedido da pessoa) continua valendo
135
+ const RANK = { title: 3, h2: 2, lead: 1, body: 0, small: -1 };
136
+ const as = !s.as && o.as && (RANK[o.as] ?? 0) > (RANK[role] ?? 0) ? role : o.as || role;
137
+ return text(o.text, as, { ...o, as: undefined, step: o.step ?? build(s, i, 0), class: `st-line ${o.class || ""}`, size: o.size || s.size });
138
+ }).join("")
139
+ : text(s.text, role, { class: "st-line e", style: "--d:1;", fit: true, size: s.size });
140
+ return `<div class="L-statement ${s.center ? "center" : ""}">${kicker(s)}<div class="st-body">${lines}</div>
141
+ ${s.by ? text(s.by, "label", { class: "st-by e", style: "--d:3;", step: s.byStep }) : ""}${src(s)}</div>${add(s, ctx)}`;
142
+ },
143
+
144
+ // Definição (verbete): o termo em destaque, a origem da palavra em partes (hydor — água · logos — ciência), a
145
+ // definição em letra de leitura e um ícone ou figura ilustrativa. Para "o que é X", no lugar de uma frase gigante.
146
+ definition(s, ctx) {
147
+ const term = s.term || s.title || "";
148
+ const parts = (Array.isArray(s.parts) ? s.parts : []).map((p) => (typeof p === "string" ? { word: p } : p || {})).filter((p) => p.word || p.meaning);
149
+ const origin = s.origin || parts.length ? `<div class="df-origin e" style="--d:2;"${s.build ? ' data-step="1"' : ""}>${s.origin ? `<span class="t f-label df-from">${md(String(s.origin))}</span>` : ""}${parts.map((p) => `<span class="df-part"><i class="t f-heading df-word">${md(String(p.word || ""))}</i>${p.meaning ? `<span class="t f-body df-mean">${md(String(p.meaning))}</span>` : ""}</span>`).join('<span class="df-plus" aria-hidden="true">+</span>')}</div>` : "";
150
+ const fig = s.figure ? `<div class="df-fig e" style="--d:3;">${el(s.figure, ctx, 640, 640)}</div>` : s.icon ? `<div class="df-fig df-icon e" style="--d:3;" aria-hidden="true">${iconSVG(s.icon, { size: 220, stroke: 1.6 })}</div>` : "";
151
+ return `<div class="L-definition${fig ? " has-fig" : ""}"><div class="df-main">${kicker(s)}${text(term, "hero", { class: "df-term ttl e", style: "--d:1;", fit: true, size: s.termSize })}${origin}
152
+ ${s.text ? `<div class="df-rule e" style="--d:2;"></div>${text(s.text, "lead", { class: "df-text e", style: "--d:3;", step: s.build ? 2 : undefined })}` : ""}</div>${fig}</div>${src(s)}${add(s, ctx)}`;
153
+ },
154
+
155
+ // Calendário (pedido: "um infográfico em forma de calendário dos próximos filmes"): um cartão por mês com evento, com
156
+ // o mini calendário do mês (os dias marcados na cor do evento) e a lista embaixo; o que não tem data vai no cartão
157
+ // "sem data". Datas: 2026-12-18, 18/12/2026, 2026-12 (só o mês) ou nada.
158
+ calendar(s, ctx) {
159
+ const MESES = ["jan", "fev", "mar", "abr", "mai", "jun", "jul", "ago", "set", "out", "nov", "dez"];
160
+ const parse = (d) => {
161
+ const t = String(d ?? "").trim();
162
+ let m = t.match(/^(\d{4})-(\d{1,2})(?:-(\d{1,2}))?$/); if (m) return { y: +m[1], mo: +m[2] - 1, d: m[3] ? +m[3] : null };
163
+ m = t.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); if (m) return { y: +m[3], mo: +m[2] - 1, d: +m[1] };
164
+ m = t.match(/^(\d{1,2})\/(\d{4})$/); if (m) return { y: +m[2], mo: +m[1] - 1, d: null };
165
+ return null;
166
+ };
167
+ const events = (Array.isArray(s.events) ? s.events : []).filter((e) => e && typeof e === "object").map((e, i) => ({ ...e, i, at: parse(e.date) }));
168
+ const undated = [...events.filter((e) => !e.at || e.at.mo < 0 || e.at.mo > 11), ...(Array.isArray(s.undated) ? s.undated.map((e) => (typeof e === "string" ? { title: e } : e)) : [])];
169
+ const months = new Map();
170
+ for (const e of events.filter((x) => x.at && x.at.mo >= 0 && x.at.mo <= 11).sort((a, b) => a.at.y - b.at.y || a.at.mo - b.at.mo || (a.at.d || 99) - (b.at.d || 99))) {
171
+ const key = `${e.at.y}-${e.at.mo}`; if (!months.has(key)) months.set(key, { y: e.at.y, mo: e.at.mo, list: [] }); months.get(key).list.push(e);
172
+ }
173
+ const colors = itemColors(ctx?.theme || { colors: {} }, 6).map((h) => `#${h}`); // as do infográfico: do tema, diferentes entre si e com contraste
174
+ const tagColor = new Map(); const colorOf = (e) => e.color ? (/^#?[0-9a-f]{6}$/i.test(e.color) ? `#${String(e.color).replace("#", "")}` : /^[a-z][\w-]*$/i.test(e.color) ? `var(--${e.color})` : "var(--hi)") : (e.tag ? (tagColor.has(e.tag) || tagColor.set(e.tag, colors[tagColor.size % colors.length]), tagColor.get(e.tag)) : colors[0]);
175
+ const card = (m, k) => {
176
+ const first = new Date(Date.UTC(m.y, m.mo, 1)).getUTCDay(), days = new Date(Date.UTC(m.y, m.mo + 1, 0)).getUTCDate();
177
+ const byDay = new Map(m.list.filter((e) => e.at.d).map((e) => [e.at.d, e]));
178
+ const cells = [...Array(first).fill(`<i></i>`), ...Array.from({ length: days }, (_, j) => { const e = byDay.get(j + 1); return e ? `<b style="--c:${colorOf(e)}">${j + 1}</b>` : `<i>${j + 1}</i>`; })].join("");
179
+ const items = m.list.map((e) => `<li class="ca-ev" style="--c:${colorOf(e)}"><span class="t f-label ca-day">${e.at.d ? String(e.at.d).padStart(2, "0") : "—"}</span><div class="ca-what">${text(e.title || "", "h3", { class: "ca-title", size: 26 })}${e.text ? text(e.text, "small", { class: "ca-text muted", size: 18 }) : ""}${e.tag ? `<span class="t f-label ca-tag">${md(String(e.tag))}</span>` : ""}</div></li>`).join("");
180
+ return `<section class="ca-month e" style="--d:${2 + k};"${s.build ? ` data-step="${k + 1}"` : ""}><header class="ca-head"><span class="t f-display ca-mo">${MESES[m.mo]}</span><span class="t f-label ca-y">${m.y}</span></header><div class="ca-grid" aria-hidden="true">${"DSTQQSS".split("").map((d) => `<u>${d}</u>`).join("")}${cells}</div><ul class="ca-list">${items}</ul></section>`;
181
+ };
182
+ const list = [...months.values()];
183
+ const und = undated.length ? `<section class="ca-month ca-undated e" style="--d:${2 + list.length};"><header class="ca-head"><span class="t f-display ca-mo">${md(String(s.undatedTitle || "sem data"))}</span></header><ul class="ca-list">${undated.map((e) => `<li class="ca-ev" style="--c:var(--muted)"><span class="t f-label ca-day">?</span><div class="ca-what">${text(e.title || "", "h3", { class: "ca-title", size: 24 })}${e.text ? text(e.text, "small", { class: "ca-text muted", size: 17 }) : ""}</div></li>`).join("")}</ul></section>` : "";
184
+ const n = list.length + (und ? 1 : 0), cols = s.cols || Math.min(n, n <= 4 ? n : n <= 6 ? 3 : 4);
185
+ const cardsHTML = list.map(card).join(""); // antes da legenda: os cartões é que dão a cor de cada tipo
186
+ const legend = tagColor.size > 1 ? `<div class="ca-legend">${[...tagColor].map(([t, c]) => `<span class="t f-label" style="--c:${c}"><i></i>${md(String(t))}</span>`).join("")}</div>` : "";
187
+ return `<div class="L-calendar">${head(s)}${legend}<div class="ca-months" style="--cols:${cols};">${cardsHTML}${und}</div></div>${src(s)}${add(s, ctx)}`;
188
+ },
189
+
190
+ // Pôster (infográfico de uma página, como uma figura de revista científica): ilustração principal, painéis com letra
191
+ // (a, b, c…), cada um com desenho, título, texto curto e números; flow: setas na ordem dos painéis; fonte embaixo.
192
+ poster(s, ctx) {
193
+ const panels = (Array.isArray(s.panels) ? s.panels : []).filter((p) => p && typeof p === "object");
194
+ const n = panels.length, hero = s.hero || s.figure;
195
+ // com a ilustração principal ao lado, mais de 2 ou 3 colunas espremem os painéis (o texto vira uma tira)
196
+ const cols = hero ? Math.min(s.cols || 3, n > 4 ? 3 : 2) : s.cols || (n <= 3 ? n : n === 4 ? 4 : 3);
197
+ const arrow = `<span class="ps-arrow" aria-hidden="true">${iconSVG("arrow-right", { size: 30, stroke: 2.4 })}</span>`;
198
+ const facts = (p) => (Array.isArray(p.facts) && p.facts.length ? `<div class="ps-facts">${p.facts.map((f) => (typeof f === "object" ? `<div class="ps-fact"><b class="t f-display ps-val">${md(String(f.value ?? ""))}</b>${f.label ? `<span class="t f-label ps-lab">${md(String(f.label))}</span>` : ""}</div>` : `<div class="ps-fact"><span class="t f-label ps-lab">${md(String(f))}</span></div>`)).join("")}</div>` : "");
199
+ const panel = (p, i) => {
200
+ const fig = p.figure || (p.image || p.image_prompt ? { image: p.image, image_prompt: p.image_prompt, image_ref: p.image_ref, fit: p.fit || "contain" } : null);
201
+ const pic = fig ? `<div class="ps-pic">${el(fig, ctx, 520, 300)}</div>` : p.icon ? `<div class="ps-pic ps-ico" aria-hidden="true">${iconSVG(p.icon, { size: 120, stroke: 1.6 })}</div>` : "";
202
+ return `<section class="ps-panel e" style="--d:${2 + i};"${s.build ? ` data-step="${i + 1}"` : ""}><header class="ps-head"><span class="t f-display ps-letter">${esc(String(p.label ?? String.fromCharCode(97 + i)))}</span>${p.title ? text(p.title, "h3", { class: "ps-title", size: 40 }) : ""}</header>${pic}${p.text ? text(p.text, "small", { class: "ps-text", size: 27 }) : ""}${facts(p)}${s.flow && i < n - 1 && (i + 1) % cols !== 0 ? arrow : ""}</section>`;
203
+ };
204
+ const heroHTML = hero ? `<figure class="ps-hero e" style="--d:1;">${el(typeof hero === "string" ? { image: hero } : { fit: "contain", ...hero }, ctx, 760, 760)}${hero.caption ? text(hero.caption, "small", { class: "ps-cap", size: 22 }) : ""}</figure>` : "";
205
+ return `<div class="L-poster${hero ? " has-hero" : ""}${s.flow ? " ps-flow" : ""}">${head(s)}${s.subtitle ? text(s.subtitle, "lead", { class: "ps-sub muted", size: 34 }) : ""}<div class="ps-body">${heroHTML}<div class="ps-grid" style="--cols:${cols};">${panels.map(panel).join("")}</div></div>${s.key ? text(s.key, "small", { class: "ps-key muted", size: 20 }) : ""}</div>${src(s)}${add(s, ctx)}`;
206
+ },
207
+
208
+ // Status semanal: saúde, avanço e as seções que tiverem conteúdo (feito, em andamento, bloqueios, riscos, próximos
209
+ // passos); screenshots ao lado quando houver. Semana sem nada "mostrável" = sem colunas vazias nem área de imagem.
210
+ status(s, ctx) {
211
+ const SEC = [["done", "Feito", "circle-check"], ["doing", "Em andamento", "loader-circle"], ["blocked", "Bloqueios", "octagon-alert"],
212
+ ["risks", "Riscos e problemas", "triangle-alert"], ["upcoming", "Próximos passos", "circle-arrow-right"]];
213
+ const HEALTH = { ok: "Em dia", risco: "Atenção", atrasado: "Atrasado" };
214
+ if (s.health && !HEALTH[s.health]) ctx.warnings?.push(`slide "${plainTitle(s)}": health: use ok, risco ou atrasado (veio "${s.health}")`);
215
+ const item = (o) => { const x = typeof o === "string" ? { text: o } : o || {}; const meta = [x.owner, x.due ? `até ${x.due}` : ""].filter(Boolean).join(" · ");
216
+ return `<li class="stt-item"><span class="stt-dot" aria-hidden="true"></span><div class="stt-it"><div class="t f-body">${md(x.text)}</div>${meta ? `<div class="stt-meta t f-label">${esc(meta)}</div>` : ""}</div></li>`; };
217
+ const secs = SEC.filter(([k]) => Array.isArray(s[k]) && s[k].length);
218
+ const nShots = (s.shots || []).slice(0, 3).length;
219
+ // grade de 6 trilhas: cada seção ocupa 6/colunas; a última linha, se incompleta, se reparte inteira (sem buraco)
220
+ const n = secs.length, per = nShots ? Math.min(2, n) : n === 4 ? 2 : Math.min(3, n), rest = n % per;
221
+ const span = (i) => (rest && i >= n - rest ? 6 / rest : 6 / per);
222
+ const nItems = secs.reduce((a, [k]) => a + s[k].length, 0);
223
+ const cols = secs.map(([k, label, icon], i) => `<div class="stt-col stt-${k} e" style="--d:${2 + i};grid-column:span ${span(i)};">
224
+ <div class="stt-h"><span class="stt-ic">${iconSVG(icon, { size: 30, stroke: 2 })}</span><span class="t f-label">${esc(s[`${k}Label`] || label)}</span></div>
225
+ <ul class="stt-list">${s[k].map(item).join("")}</ul></div>`);
226
+ const shots = (s.shots || []).slice(0, 3).map((sh) => { const x = typeof sh === "string" ? { image: sh } : sh || {};
227
+ return `<figure class="stt-shot">${el({ image: x.image, fit: "contain" }, ctx, 640, 360)}${x.caption ? `<figcaption class="t f-label">${md(x.caption)}</figcaption>` : ""}</figure>`; });
228
+ const pct = s.progress != null && Number.isFinite(+s.progress) ? Math.max(0, Math.min(100, +s.progress)) : null;
229
+ const health = HEALTH[s.health] ? `<span class="stt-health stt-${s.health}"><i aria-hidden="true"></i><span class="t f-label">${esc(s.healthLabel || HEALTH[s.health])}</span></span>` : "";
230
+ const prog = pct != null ? `<div class="stt-prog"><div class="stt-track"><div class="stt-bar" style="width:${pct}%"></div></div><span class="t f-label">${pct}%</span></div>` : "";
231
+ // semana magra (poucos itens, sem telas): letra maior e o bloco no meio, em vez de cartões pequenos no alto
232
+ const roomy = !shots.length && nItems <= 6;
233
+ return `<div class="L-status${shots.length ? " with-shots" : ""}${roomy ? " stt-roomy" : ""}">
234
+ <div class="stt-top">${head(s)}<div class="stt-side e" style="--d:1;">${health}${prog}</div></div>
235
+ ${s.highlight ? text(s.highlight, "lead", { class: "stt-highlight e", style: "--d:1;" }) : ""}
236
+ <div class="stt-body"><div class="stt-cols">${cols.join("")}</div>${shots.length ? `<div class="stt-shots e" style="--d:3;">${shots.join("")}</div>` : ""}</div>
237
+ ${src(s)}</div>${add(s, ctx)}`;
238
+ },
239
+
240
+ // One-page: tudo numa página só. Jornada (ícones e mini-frases), problema com números, solução e, quando houver,
241
+ // um painel (números grandes em cima, gráficos e mapa por UF embaixo). Só entram as partes preenchidas; o painel
242
+ // sozinho é um dashboard. Com problema/solução e painel juntos, os dois ficam lado a lado (cabe numa página).
243
+ onepage(s, ctx) {
244
+ // bloco vazio (o formulário cria {} ao abrir) não aparece
245
+ const filled = (b) => b && (b.text || (b.items || []).length || (b.numbers || []).length);
246
+ const sec = (v, dflt) => { const b = v == null ? null : typeof v === "string" ? { text: v } : Array.isArray(v) ? { items: v } : v;
247
+ return filled(b) ? { ...b, title: b.title || dflt } : null; };
248
+ const problem = sec(s.problem, "O problema"), solution = sec(s.solution, "A solução");
249
+ const jSteps = (Array.isArray(s.journey) ? s.journey : s.journey?.steps || []).filter((x) => x != null && x !== "");
250
+ const journey = { title: s.journeyTitle ?? s.journey?.title ?? "Jornada" };
251
+ // painel: { numbers: [...], figures: [...] } (também aceita uma lista misturada)
252
+ const D = s.dashboard || {}, mixed = Array.isArray(D) ? D : [];
253
+ const isKpi = (x) => (x.value != null || x.stat != null) && !x.chart && !x.ufmap;
254
+ const obj = (t) => (typeof t === "object" && t ? (t.chart && typeof t.chart === "object" ? { ...t.chart, title: t.title } : t) : { value: t });
255
+ const kpis = [...(D.numbers || []), ...mixed].filter((x) => x != null).map(obj).filter(isKpi);
256
+ const figAll = [...(D.figures || []), ...mixed].filter((x) => x != null).map(obj).filter((x) => !isKpi(x));
257
+ const tiles = [...kpis, ...figAll];
258
+ const maps = figAll.filter((x) => x.ufmap), figs = figAll.filter((x) => !x.ufmap);
259
+ const mid = [problem, solution].filter(Boolean).length;
260
+ const beside = mid && tiles.length;
261
+ const roomy = !tiles.length;
262
+ // tamanho de cada gráfico, para desenhar já na proporção certa (a letra do gráfico acompanha)
263
+ const bodyH = 850 - (s.kicker ? 170 : 120) - (s.subtitle ? 60 : 0) - (jSteps.length ? 200 : 0);
264
+ // o mapa por UF é alto: ganha uma coluna própria no painel, da altura toda
265
+ const mapW = maps.length ? Math.round((bodyH - 90) * 0.75) + 44 : 0;
266
+ const dashW = (beside ? 1680 * 0.58 - 12 : 1680) - (maps.length ? (mapW + 24) * maps.length : 0);
267
+ const figH = bodyH - (kpis.length ? 170 : 0) - 70, figW = figs.length ? (dashW - 24 * (figs.length - 1)) / figs.length - 44 : 0;
268
+ const nums = (arr) => (arr || []).length ? `<div class="op-nums">${arr.map((n) => { const x = typeof n === "object" ? n : { value: n };
269
+ return `<div class="op-num"><div class="t f-display op-num-v">${esc(String(x.value ?? ""))}</div>${x.label ? `<div class="t f-label op-num-l">${md(x.label)}</div>` : ""}</div>`; }).join("")}</div>` : "";
270
+ const bullets = (arr) => (arr || []).length ? `<ul class="op-items">${arr.map((x) => `<li class="t f-body">${md(typeof x === "object" ? x.text : x)}</li>`).join("")}</ul>` : "";
271
+ const block = (b, cls, d) => b ? `<section class="op-block ${cls} e" style="--d:${d};"><div class="op-h t f-label">${md(b.title)}</div>
272
+ ${b.text ? `<div class="t f-body op-text">${md(b.text)}</div>` : ""}${nums(b.numbers)}${bullets(b.items)}</section>` : "";
273
+ const jHTML = jSteps.length ? `<section class="op-journey e" style="--d:2;">${journey.title ? `<div class="op-h t f-label">${md(journey.title)}</div>` : ""}<ol class="op-steps" style="--n:${jSteps.length}">${jSteps.map((st, i) => { const x0 = typeof st === "string" ? { text: st } : st, x = x0.title ? x0 : { ...x0, title: x0.text, text: "" }; // só texto = frase curta, em destaque
274
+ return `<li class="op-step${x.goto ? " goto" : ""}"${x.goto ? ` data-goto="${esc(x.goto)}"` : ""}>${x.icon ? `<span class="op-ic">${iconSVG(x.icon, { size: 40, stroke: 1.8 })}</span>` : `<span class="op-ic op-ic-n t f-label">${i + 1}</span>`}
275
+ <div class="op-st">${x.title ? `<div class="t f-heading op-st-t">${md(x.title)}</div>` : ""}${x.text ? `<div class="t f-body op-st-x">${md(x.text)}</div>` : ""}</div></li>`; }).join("")}</ol></section>` : "";
276
+ const tHead = (x) => (x.title ? `<div class="op-h t f-label">${md(x.title)}</div>` : "");
277
+ const kpi = (x) => { const up = x.trendUp !== false && !String(x.trend || "").startsWith("-");
278
+ return `<div class="op-tile op-kpi">${tHead(x)}<div class="t f-display op-kpi-v">${esc(String(x.value ?? x.stat))}</div>${x.label ? `<div class="t f-body op-kpi-l">${md(x.label)}</div>` : ""}
279
+ ${x.trend ? `<div class="st-trend ${up ? "trend-up" : "trend-down"}">${iconSVG(up ? "trending-up" : "trending-down", { size: 22, stroke: 2.2 })} ${esc(x.trend)}</div>` : ""}</div>`; };
280
+ // gráfico desenhado 1,5x maior e reduzido: a letra fica proporcional ao quadro pequeno
281
+ const fig = (x) => { const { title: _t, ...f } = x;
282
+ return `<div class="op-tile op-fig${x.ufmap ? " op-map" : ""}">${tHead(x)}<div class="op-fig-in">${x.ufmap ? el(f, ctx, mapW - 44) : el(f, ctx, Math.round(figW * 1.5), Math.round(Math.max(180, figH) * 1.5))}</div></div>`; };
283
+ const dMain = kpis.length || figs.length ? `<div class="op-dash-main">${kpis.length ? `<div class="op-kpis">${kpis.map(kpi).join("")}</div>` : ""}${figs.length ? `<div class="op-figs" style="--n:${figs.length}">${figs.map(fig).join("")}</div>` : ""}</div>` : "";
284
+ const dHTML = tiles.length ? `<section class="op-dash e" style="--d:4;${maps.length ? `--op-mapw:${mapW}px;` : ""}">${dMain}${maps.map(fig).join("")}</section>` : "";
285
+ const midHTML = mid ? `<div class="op-mid">${block(problem, "op-problem", 3)}${block(solution, "op-solution", 3)}</div>` : "";
286
+ const cls = ["L-onepage", beside ? "op-beside" : "", roomy ? "op-roomy" : "", tiles.length && !jSteps.length && !mid ? "op-only-dash" : ""].filter(Boolean).join(" ");
287
+ return `<div class="${cls}">${head(s)}
288
+ ${s.subtitle ? text(s.subtitle, "lead", { class: "op-sub e", style: "--d:1;" }) : ""}
289
+ <div class="op-main">${jHTML}<div class="op-body">${midHTML}${dHTML}</div></div>
290
+ ${src(s)}</div>${add(s, ctx)}`;
291
+ },
292
+
293
+ // Mapa de caminhos: uma pergunta e as opções; cada opção leva (goto) à seção dela. O fim de cada caminho volta
294
+ // ao mapa com next:/back: no último slide da seção (ver "Navegação por caminhos" na REFERENCIA).
295
+ hub(s, ctx) {
296
+ const opts = (s.options || s.items || []).map((o) => (typeof o === "string" ? { title: o } : o || {}));
297
+ const cols = s.cols || (opts.length <= 4 ? Math.max(1, opts.length) : opts.length <= 6 ? 3 : 4);
298
+ const body = opts.map((c, i) => `<div${attrs({ ...c, step: s.build ? 1 + i : c.step }, "hub-card")}>
299
+ ${c.icon ? `<div class="hub-ico">${iconSVG(c.icon, { size: 56, stroke: 1.8 })}</div>` : ""}
300
+ ${c.title ? `<div class="hub-title t f-heading">${md(c.title)}</div>` : ""}
301
+ ${c.text ? `<div class="hub-text t f-body">${md(c.text)}</div>` : ""}
302
+ <div class="hub-foot">${c.meta ? `<span class="hub-meta t f-label">${md(c.meta)}</span>` : "<span></span>"}${c.goto != null && c.goto !== "" ? `<span class="hub-go" aria-hidden="true">${iconSVG("arrow-right", { size: 30, stroke: 2 })}</span>` : ""}</div>
303
+ </div>`).join("");
304
+ return `<div class="L-hub">${head(s)}${s.question ? text(s.question, "lead", { class: "hub-q e", style: "--d:1;" }) : ""}
305
+ <div class="hub-grid e" style="--d:2;grid-template-columns:repeat(${cols},minmax(0,1fr))">${body}</div>${src(s)}</div>${add(s, ctx)}`;
306
+ },
307
+
308
+ quote(s, ctx) {
309
+ return `<div class="L-quote">${kicker(s)}
310
+ <div class="q-mark t f-quote e" aria-hidden="true">“</div>
311
+ ${text(s.quote, "quote", { class: "q-text e", style: "--d:1;", fit: true, size: s.size })}
312
+ ${s.by ? `<div class="q-by e" style="--d:2">${text(s.by, "h3", { size: 40 })}${s.role ? text(s.role, "small", { class: "muted" }) : ""}</div>` : ""}
313
+ ${s.after ? text(s.after, "lead", { class: "q-after", step: s.afterStep ?? 1, color: "em" }) : ""}
314
+ </div>${add(s, ctx)}`;
315
+ },
316
+
317
+ number(s, ctx) {
318
+ const v = typeof s.value === "object" ? s.value : { counter: s.value, prefix: s.prefix, suffix: s.suffix, decimals: s.decimals, from: s.from };
319
+ return `<div class="L-number">
320
+ <div class="nb-main">${kicker(s)}
321
+ <div class="nb-val e" style="--d:1;${s.valueColor ? `color:var(--${s.valueColor});` : ""}">${counter({ ...v, size: s.size || 300, fit: true })}</div>
322
+ ${s.label ? text(s.label, "lead", { class: "nb-label e", style: "--d:2;", size: s.labelSize || 52 }) : ""}
323
+ ${s.context ? text(s.context, "body", { class: "nb-ctx muted", step: s.contextStep }) : ""}
324
+ </div>
325
+ ${s.side ? `<div class="nb-side" ${s.sideStep ? `data-step="${s.sideStep}"` : ""}>${el(s.side, ctx, 640, 700)}</div>` : ""}
326
+ </div>${src(s)}${add(s, ctx)}`;
327
+ },
328
+
329
+ split(s, ctx) {
330
+ const image=s.figure?.image, dimensions=image?imageSize(image,ctx):null;
331
+ const documentImage=typeof image==='string'&&image.replace(/\\/g,'/').includes('contexto/visuais/');
332
+ const visual=s.figure&&(documentImage||s.figure.fit==='contain'||s.figure.chart||s.figure.diagram||s.figure.math||s.figure.table);
333
+ const stacked=s.arrangement==='stacked'||(!s.arrangement&&!s.ratio&&visual&&(!dimensions||dimensions.w/dimensions.h>1.2));
334
+ if(stacked) return `<div class="L-split sp-stacked">${head({...s,titleSize:s.titleSize||64})}<div class="sp-fig e" style="--d:2" ${s.figureStep?`data-step="${s.figureStep}"`:''}>${el({...s.figure,...(image?{fit:'contain'}:{})},ctx,1680,620)}</div><div class="sp-summary">${s.body?text(s.body,'body',{size:s.bodySize||32}):''}${s.bullets?list({list:s.bullets,build:s.build,size:s.bulletSize||30}):''}${s.content?el(s.content,ctx):''}</div></div>${src(s)}${add(s,ctx)}`;
335
+ const ratio = String(s.ratio || (visual?'1:2':'1:1')).split(":").map(Number);
336
+ const left = `${s.body ? text(s.body, s.bodyAs || "lead", { class: "sp-body e", style: "--d:2;" }) : ""}
337
+ ${s.bullets ? list({ list: s.bullets, build: s.build, size: s.bulletSize }) : ""}
338
+ ${s.content ? el(s.content, ctx) : ""}`;
339
+ const W = Math.round(1680 * ratio[1] / (ratio[0] + ratio[1]));
340
+ const right = s.figure ? `<div class="sp-fig e" style="--d:2;flex:${ratio[1]}" ${s.figureStep ? `data-step="${s.figureStep}"` : ""}>${el(documentImage?{...s.figure,fit:'contain'}:s.figure, ctx, W - 40, 740)}</div>` : "";
341
+ return `<div class="L-split ${s.reverse ? "rev" : ""}">
342
+ <div class="sp-text" style="flex:${ratio[0]}">${head(s, s.titleAs || "h2")}${left}</div>${right}
343
+ </div>${src(s)}${add(s, ctx)}`;
344
+ },
345
+
346
+ // tabela de verdade: título, a tabela (cores do tema) e, se houver, a conclusão ao lado (side) e a fonte
347
+ table(s, ctx) {
348
+ const t = { head: s.head || s.columns, rows: s.rows, csv: s.csv, style: s.style, color: s.color, align: s.align, highlight: s.highlight, total: s.total, widths: s.widths, rowHeader: s.rowHeader, size: s.size };
349
+ const tb = `<div class="tb-main e" style="--d:2;">${tableHTML(t, { theme: ctx?.theme })}</div>`;
350
+ // ao lado: texto, ou elementos (a IA às vezes manda [{ text, as }] como no blocks: saía "[object Object]")
351
+ const side = s.side ? `<div class="tb-side e" style="--d:3;">${typeof s.side === "string" ? text(s.side, "lead", { class: "tb-side-t" }) : el(s.side, ctx)}</div>` : "";
352
+ return `<div class="L-table${side ? " with-side" : ""}">${head(s)}<div class="tb-row">${tb}${side}</div>${s.caption ? text(s.caption, "small", { class: "tb-cap" }) : ""}</div>${src(s)}${add(s, ctx)}`;
353
+ },
354
+
355
+ cards(s, ctx) {
356
+ return `<div class="L-cards">${head(s)}${cards({ cards: s.items, cols: s.cols, build: s.build, class: "e", style: "--d:2;" }, ctx)}</div>${src(s)}${add(s, ctx)}`;
357
+ },
358
+
359
+ stats(s, ctx) {
360
+ const items = s.stats || s.kpis || s.items || [];
361
+ return `<div class="L-stats">${head(s)}${stats({ stats: items, cols: s.cols, build: s.build, class: "e", style: "--d:2;" }, ctx)}</div>${src(s)}${add(s, ctx)}`;
362
+ },
363
+
364
+ steps(s, ctx) {
365
+ const items = s.steps || s.process || s.flow || s.items || [];
366
+ return `<div class="L-steps">${head(s)}${steps({ steps: items, cols: s.cols, build: s.build, class: "e", style: "--d:2;" }, ctx)}</div>${src(s)}${add(s, ctx)}`;
367
+ },
368
+
369
+ list(s, ctx) {
370
+ return `<div class="L-list">${head(s)}${list({ list: s.items, numbered: s.numbered !== false, build: s.build, size: s.size })}</div>${src(s)}${add(s, ctx)}`;
371
+ },
372
+
373
+ timeline(s, ctx) {
374
+ const ev = s.events || [];
375
+ const hl = new Set([].concat(s.highlight ?? []));
376
+ const body = ev.map((e, i) => `<div${attrs({ step: e.step ?? build(s, i) }, `tl-ev ${hl.has(i) ? "hl" : ""}`)}>
377
+ <div class="tl-dot"></div><div class="tl-when t f-display">${md(e.when)}</div>
378
+ <div class="tl-title t f-heading">${md(e.title || "")}</div>${e.text ? `<div class="tl-text t f-body">${md(e.text)}</div>` : ""}
379
+ ${e.tag ? `<div class="tl-tag t f-label">${md(e.tag)}</div>` : ""}</div>`).join("");
380
+ return `<div class="L-timeline">${head(s)}<div class="tl" style="--n:${ev.length}"><div class="tl-line"></div>${body}</div>${s.after ? text(s.after, "h3", { class: "tl-after", step: s.afterStep ?? (s.build ? ev.length + 1 : 1), face: "quote", size: 48 }) : ""}</div>${src(s)}${add(s, ctx)}`;
381
+ },
382
+
383
+ chart(s, ctx) {
384
+ const side = s.side || s.note;
385
+ const w = side ? 1120 : 1680, h = s.chartHeight || (s.title ? 600 : 720);
386
+ // sem chart/type: o tipo pode vir como chave ({ line: { labels, series } }); sem tipo nenhum, o gráfico acusa
387
+ const ch = s.chart?.chart || s.chart?.type ? { ...s.chart, chart: s.chart.chart || s.chart.type } : { chart: s.chart || {} };
388
+ return `<div class="L-chart">${head(s)}<div class="ch-row">
389
+ <div class="ch-fig e" style="--d:2" ${s.chartStep ? `data-step="${s.chartStep}"` : ""}>${figureHTML(ch, ctx, w, h)}</div>
390
+ ${side ? `<div class="ch-side" ${s.sideStep ? `data-step="${s.sideStep}"` : ""}>${typeof side === "string" ? text(side, "lead") : el(side, ctx)}</div>` : ""}
391
+ </div></div>${src(s)}${add(s, ctx)}`;
392
+ },
393
+
394
+ compare(s, ctx) {
395
+ const col = (c, k, i) => `<div${attrs({ step: c.step ?? build(s, i) }, `cp-col cp-${k} ${c.hl ? "hl" : ""}`)}>
396
+ ${c.label ? text(c.label, "label", { class: "cp-label" }) : ""}
397
+ ${c.figure ? `<div class="cp-fig">${el(c.figure, ctx, 520, 320)}</div>` : ""}
398
+ ${c.value != null ? text(String(c.value), "h2", { class: "cp-value", size: c.valueSize || 150 }) : ""}
399
+ ${c.title ? text(c.title, "h3", { class: "cp-title" }) : ""}
400
+ ${c.text ? text(c.text, "body", { class: "cp-text" }) : ""}
401
+ ${c.items ? list({ list: c.items, size: 32 }) : ""}</div>`;
402
+ return `<div class="L-compare">${head(s)}<div class="cp-row">${col(s.left, "l", 0)}<div${attrs({ step: s.right?.step ?? build(s, 1) }, "cp-vs t f-display")}>${esc(s.vs ?? "×")}</div>${col(s.right, "r", 1)}</div>
403
+ ${s.after ? text(s.after, "h3", { class: "cp-after", step: s.afterStep ?? (s.build ? 3 : 1), face: "quote", size: 50 }) : ""}</div>${src(s)}${add(s, ctx)}`;
404
+ },
405
+
406
+ matrix(s, ctx) {
407
+ const cells = s.cells || [];
408
+ const body = cells.map((c, i) => `<div${attrs({ step: c.step ?? build(s, i) }, `mx-cell ${c.hl ? "hl" : ""}`)}>
409
+ ${c.icon || c.picto ? `<div class="mx-ico">${el(c.icon ? { icon: c.icon, size: 60 } : c, ctx)}</div>` : ""}
410
+ ${text(c.title, "h3", { class: "mx-title" })}${c.text ? text(c.text, "body", { class: "mx-text" }) : ""}${c.example ? text(c.example, "small", { class: "mx-ex" }) : ""}</div>`).join("");
411
+ const [xl, xr] = s.x || ["", ""], [yt, yb] = s.y || ["", ""];
412
+ return `<div class="L-matrix">${head(s)}<div class="mx">
413
+ <div class="mx-y"><span class="t f-label">${md(yt)}</span><span class="t f-label">${md(yb)}</span></div>
414
+ <div class="mx-grid">${body}</div>
415
+ <div class="mx-x"><span class="t f-label">${md(xl)}</span><span class="t f-label">${md(xr)}</span></div></div></div>${src(s)}${add(s, ctx)}`;
416
+ },
417
+
418
+ question(s, ctx) {
419
+ const letters = "ABCDEFGH";
420
+ const opts = (s.options || []).map((o, i) => {
421
+ const obj = typeof o === "string" ? { text: o } : o;
422
+ return `<div${attrs({ step: obj.step ?? build(s, i) }, "qs-opt")}><span class="qs-key t f-display">${esc(obj.key || (s.keys || letters)[i])}</span>${text(obj.text, "lead", { class: "qs-text", size: s.optionSize })}${obj.sub ? text(obj.sub, "small", { class: "qs-sub muted" }) : ""}</div>`;
423
+ }).join("");
424
+ return `<div class="L-question">
425
+ <div class="qs-top">${kicker(s)}${text(s.question || s.title, "h2", { class: "ttl e", style: "--d:1;", fit: true, size: s.titleSize })}${s.context ? text(s.context, "lead", { class: "muted e", style: "--d:2;" }) : ""}</div>
426
+ <div class="qs-row"><div class="qs-opts" style="grid-template-columns:repeat(${s.cols || Math.min(3, (s.options || []).length || 1)},1fr)">${opts}</div>
427
+ ${s.timer ? `<div class="qs-timer">${timer({ timer: s.timer, size: 230, label: s.timerLabel })}</div>` : ""}</div>
428
+ ${s.hint ? text(s.hint, "h3", { class: "qs-hint e", style: "--d:3;", size: 42 }) : ""}
429
+ </div>${add(s, ctx)}`;
430
+ },
431
+
432
+ poll(s, ctx) {
433
+ if(s.manual)return manualPollHTML(s,head)+add(s,ctx);
434
+ return `<div class="L-poll">${kicker(s)}${text(s.question || s.title, "h2", { class: "ttl e", style: "--d:1;", size: s.titleSize })}
435
+ ${s.context ? text(s.context, "lead", { class: "muted" }) : ""}
436
+ ${poll({ poll: s.id || s.poll, options: s.options, compare: s.compare, hint: s.hint, class: "e", style: "--d:2;" }, ctx)}</div>${add(s, ctx)}`;
437
+ },
438
+
439
+ image(s, ctx) {
440
+ // figura inteira à vista (fit: contain: gráfico, tabela, esquema): a legenda vai embaixo, sem cobrir; foto (cover)
441
+ // segue sangrando, com o cartão por cima
442
+ const contain = (s.figure?.fit || s.fit) === "contain" || (!s.fit&&!s.figure?.fit&&String(s.figure?.image||s.image||'').replace(/\\/g,'/').includes('contexto/visuais/'));
443
+ // figura inteira: título no alto, como nos outros slides (não a frase gigante embaixo), e a legenda pequena embaixo
444
+ if (contain) return `<div class="L-image im-contain">${s.title || s.kicker ? head(s) : ""}<div class="im-fig">${el(s.figure || { image: s.image, fit: "contain" }, ctx, 1680, slideSize(ctx.spec).h)}</div>${s.caption ? text(s.caption, "small", { class: "im-caption muted" }) : ""}</div>${src(s)}${add(s, ctx)}`;
445
+ return `<div class="L-image"><div class="im-fig">${el(s.figure || { image: s.image, fit: s.fit || "cover" }, ctx, 1920, slideSize(ctx.spec).h)}</div>
446
+ ${s.title || s.caption ? `<div class="im-cap">${kicker(s)}${s.title ? text(s.title, "h2", { class: "ttl" }) : ""}${s.caption ? text(s.caption, "body") : ""}</div>` : ""}</div>${add(s, ctx)}`;
447
+ },
448
+
449
+ code(s, ctx) {
450
+ return `<div class="L-code">${head(s)}<div class="cd-row">${codeEditor(s)}
451
+ ${s.note ? `<div class="cd-note" ${s.noteStep ? `data-step="${s.noteStep}"` : ""}>${typeof s.note === "string" ? text(s.note, "lead") : el(s.note, ctx)}</div>` : ""}</div></div>${src(s)}${add(s, ctx)}`;
452
+ },
453
+
454
+ codewalk(s, ctx) {
455
+ const frames = lessonSteps(s.steps, { title: "Acompanhe o código", text: "Adicione etapas com linhas destacadas e a saída esperada.", highlight: s.highlight });
456
+ return `<div class="L-codewalk" data-lesson="codewalk" data-lesson-count="${frames.length}">${head(s)}<div class="lesson-row">${codeEditor(s, { highlight: frames[0].highlight ?? s.highlight, footer: "LEIA · PREVEJA · REVELE" })}<aside class="lesson-aside">${lessonPanels(frames, true)}${lessonSummary(frames)}${lessonControls(frames)}</aside></div></div>${src(s)}${add(s, ctx)}`;
457
+ },
458
+
459
+ spotlight(s, ctx) {
460
+ const spots = lessonSteps(s.hotspots, { title: "Olhe mais de perto", text: "Adicione regiões para guiar o olhar da audiência.", x: 20, y: 20, width: 60, height: 60 });
461
+ const regions = spots.map((p, i) => {
462
+ const point = p.kind === "point";
463
+ const x = percent(p.x, 10, 0, point ? 100 : 96), y = percent(p.y, 10, 0, point ? 100 : 96);
464
+ const w = percent(p.width, 28, 4, 100 - x), h = percent(p.height, 24, 4, 100 - y);
465
+ // zoom lento no foco (opcional): o do foco vence o do slide; false = imagem inteira neste foco
466
+ const zoomOf = (z) => (z === true ? 1.8 : Number.isFinite(Number(z)) && Number(z) > 1 ? Math.min(4, Number(z)) : z === false ? 1 : null);
467
+ const zoom = zoomOf(p.zoom) ?? zoomOf(s.zoom) ?? 1;
468
+ return `<button type="button" class="spotlight-region${point ? " spotlight-point" : ""}${i === 0 ? " active" : ""}" data-lesson-go="${i}" data-spotlight-region="${i}"${zoom > 1 ? ` data-zoom="${zoom}"` : ""} aria-label="Detalhe ${i + 1}: ${esc(p.title || "Explicação")}" aria-current="${i === 0 ? "step" : "false"}" style="left:${x}%;top:${y}%;${point ? "" : `width:${w}%;height:${h}%`}"><span>${i + 1}</span></button>`;
469
+ }).join("");
470
+ const visual = s.figure || (s.image ? { image: s.image, alt: s.caption || s.title || "Imagem em análise", fit: "contain" } : { diagram: "flow", steps: ["Entrada", "Processamento", "Resultado"] });
471
+ return `<div class="L-spotlight" data-lesson="spotlight" data-lesson-count="${spots.length}">${head(s)}<div class="lesson-row"><div class="spotlight-visual"><div class="spotlight-canvas"><div class="spotlight-zoom"><div class="spotlight-image">${el(visual, ctx, 1100, 660)}</div><div class="spotlight-regions">${regions}</div></div></div>${s.caption ? text(s.caption, "small", { class: "spotlight-caption", size: 24 }) : ""}</div><aside class="lesson-aside">${lessonPanels(spots)}${lessonSummary(spots)}${lessonControls(spots)}</aside></div></div>${src(s)}${add(s, ctx)}`;
472
+ },
473
+
474
+ kinetic(s, ctx) {
475
+ const beats = Array.isArray(s.beats) && s.beats.length
476
+ ? s.beats.map((beat) => typeof beat === "string" ? { text: beat } : beat || {})
477
+ : [
478
+ ...kineticPhrases(s.title || s.text),
479
+ ...kineticPhrases(s.subtitle),
480
+ ].map((text, i) => ({
481
+ text, style: "clean", position: "left", size: "medium",
482
+ ...(i === 0 && s.kicker ? { tag: s.kicker } : {}),
483
+ }));
484
+ if (!beats.length) beats.push({ text: "Uma ideia em movimento", style: "clean", position: "left", color: "white", size: "large" });
485
+ const frames = beats.map((beat, i) => {
486
+ const style = ["clean", "poster", "editorial", "outline", "marker"].includes(beat.style) ? beat.style : "clean";
487
+ const position = ["left", "center", "right", "top", "bottom"].includes(beat.position) ? beat.position : "left";
488
+ const color = ["white", "gold", "pink", "cyan"].includes(beat.color) ? beat.color : "white";
489
+ const size = ["small", "medium", "large"].includes(beat.size) ? beat.size : "medium";
490
+ return `<article class="kinetic-frame${i === 0 ? " active" : ""}" data-lesson-panel="${i}" data-position="${position}" data-style="${style}" data-color="${color}" data-size="${size}" aria-hidden="${i !== 0}">
491
+ <div class="kinetic-word" data-fit>${md(beat.text || "")}</div>
492
+ ${beat.tag ? `<div class="kinetic-tag">${md(beat.tag)}</div>` : ""}
493
+ </article>`;
494
+ }).join("");
495
+ const scene = s.figure ? el(s.figure, ctx, 1920, slideSize(ctx.spec).h) : "";
496
+ const interval = Math.max(450, Math.min(5000, Number(s.interval) || 1000));
497
+ const autoplay = s.autoplay !== false && beats.length > 1;
498
+ const summary = `<div class="kinetic-summary" aria-label="Frases da sequência">${beats.map((beat) => `<span>${md(beat.text || "")}</span>`).join("")}</div>`;
499
+ return `<div class="L-kinetic" data-lesson="kinetic" data-lesson-count="${beats.length}" data-kinetic-interval="${interval}" data-kinetic-autoplay="${autoplay}">
500
+ <div class="kinetic-scene" aria-hidden="true">${scene}</div><div class="kinetic-shade" aria-hidden="true"></div>
501
+ <div class="kinetic-grain" aria-hidden="true"></div>
502
+ <div class="kinetic-sequence" aria-live="polite" aria-atomic="true">${frames}</div>
503
+ <div class="kinetic-controls"><button type="button" class="kinetic-toggle" data-kinetic-toggle aria-pressed="${autoplay}" aria-label="${autoplay ? "Pausar" : "Reproduzir"} sequência"${beats.length < 2 ? " disabled" : ""}>${autoplay ? "Pausar" : "Reproduzir"}</button>
504
+ <nav class="kinetic-dots" aria-label="Frases da sequência">${beats.map((beat, i) => `<button type="button" data-lesson-go="${i}" aria-label="Frase ${i + 1}: ${esc(beat.text || "")}" aria-current="${i === 0 ? "step" : "false"}">${String(i + 1).padStart(2, "0")}</button>`).join("")}</nav>
505
+ <button type="button" class="kinetic-next" data-lesson-next aria-label="Próxima frase">${iconSVG("arrow-right", { size: 26, stroke: 2 })}</button>
506
+ </div>${summary}
507
+ </div>${add(s, ctx)}`;
508
+ },
509
+
510
+ blocks(s, ctx) {
511
+ return `<div class="L-blocks">${head(s, s.titleAs || "h2")}<div class="bl-body">${el(chartsSideBySide(s.content || []), ctx)}</div>${s.caption ? text(s.caption, "small", { class: "bl-caption muted" }) : ""}</div>${src(s)}${add(s, ctx)}`;
512
+ },
513
+
514
+ end(s, ctx) {
515
+ return `<div class="L-end"><div class="en-main">${kicker(s)}${text(s.title || "Obrigado.", "hero", { class: "ttl e", style: "--d:1;", fit: true, size: s.titleSize })}
516
+ ${s.subtitle ? text(s.subtitle, "lead", { class: "sub e", style: "--d:2;" }) : ""}
517
+ ${s.contacts ? `<div class="en-contacts e" style="--d:3">${s.contacts.map((c) => text(c, "h3", { size: 38 })).join("")}</div>` : ""}</div>
518
+ ${s.figure ? `<div class="en-fig e" style="--d:2">${el(s.figure, ctx, 640, 700)}</div>` : ""}
519
+ ${s.qr ? `<div class="en-fig e" style="--d:3">${el(typeof s.qr === "object" ? { size: 420, ...s.qr } : { qr: s.qr, size: 420, label: s.qrLabel }, ctx)}</div>` : ""}</div>${add(s, ctx)}`;
520
+ },
521
+
522
+ references(s, ctx) {
523
+ const items = s.items || [];
524
+ return `<div class="L-refs">${head(s, "h3")}<div class="rf-cols">${items.map((r) => `<div class="rf t f-body">${md(r)}</div>`).join("")}</div></div>${add(s, ctx)}`;
525
+ },
526
+
527
+ video(s, ctx) {
528
+ return `<div class="L-video">${head(s)}<div class="vd-row">${el({ video: s.url, label: s.label, loop:s.loop, poster:s.poster, start:s.start, finish:s.finish, handoffAt:s.handoffAt, composite:s.composite, fadeOut:s.fadeOut, controls:s.controls, class: "e", style: "--d:2;" }, ctx)}${s.figure ? `<div class="vd-fig">${el(s.figure, ctx, 600, 560)}</div>` : ""}</div>${s.caption ? text(s.caption, "body", { class: "muted" }) : ""}</div>${add(s, ctx)}`;
529
+ },
530
+
531
+ // Página inteira: uma figura (imagem, imagem gerada pela IA, SVG, HTML, gráfico…) ocupa o slide todo;
532
+ // texto opcional por cima, com um véu para garantir leitura. overlay: bottom | left | center | none
533
+ full(s, ctx) {
534
+ const f = s.figure ?? (s.image || s.image_prompt ? { image: s.image, image_prompt: s.image_prompt } : null);
535
+ const fig = f == null ? "" : typeof f === "object" ? { fit: s.fit || "cover", ...f } : { image: f, fit: s.fit || "cover" };
536
+ const hasText = s.title || s.caption || s.kicker;
537
+ const pos = s.overlay || (hasText ? "bottom" : "none");
538
+ // figura desenhada (gráfico, ícone, diagrama) com texto à esquerda: vai para a direita em vez de ficar por baixo do texto
539
+ const drawn = fig && !(fig.image || fig.image_prompt || fig.video);
540
+ return `<div class="L-full${drawn && pos === "left" ? " fl-shift" : ""}">${fig ? `<div class="fl-fig">${el(fig, ctx, 1920, slideSize(ctx.spec).h)}</div>` : ""}
541
+ ${hasText && pos !== "none" ? `<div class="fl-over fl-${pos}"><div class="fl-txt">${kicker(s)}${s.title ? text(s.title, "h2", { class: "ttl e", style: "--d:1;", fit: true, size: s.titleSize }) : ""}${s.caption ? text(s.caption, "lead", { class: "e", style: "--d:2;" }) : ""}</div></div>` : ""}</div>`;
542
+ },
543
+
544
+ // Manchete: uma frase enorme que ocupa o slide (encolhe para caber)
545
+ headline(s, ctx) {
546
+ return `<div class="L-headline">${kicker(s)}${text(s.text || s.title, s.as || "hero", { class: "hl-text e", style: "--d:1;", fit: true, size: s.size || 300 })}
547
+ ${s.caption ? text(s.caption, "lead", { class: "hl-cap muted e", style: "--d:2;" }) : ""}</div>${src(s)}${add(s, ctx)}`;
548
+ },
549
+
550
+ // Funil: etapas que afunilam (largura decrescente), com valor e texto ao lado
551
+ funnel(s, ctx) {
552
+ const st = s.stages || s.items || [];
553
+ const n = Math.max(1, st.length);
554
+ const rows = st.map((x, i) => {
555
+ const o = typeof x === "string" ? { title: x } : x;
556
+ const w = 100 - i * (48 / Math.max(1, n - 1));
557
+ const mix = 100 - i * (40 / Math.max(1, n - 1));
558
+ return `<div${attrs({ step: o.step ?? build(s, i) }, `fn-row ${o.hl ? "hl" : ""}`)}>
559
+ <div class="fn-barwrap"><div class="fn-bar" style="width:${w.toFixed(1)}%;--mix:${mix.toFixed(0)}%">${o.value != null ? `<span class="fn-val t f-display">${md(String(o.value))}</span>` : ""}<span class="fn-title t f-heading">${md(o.title || "")}</span></div></div>
560
+ ${o.text ? `<div class="fn-text t f-body">${md(o.text)}</div>` : "<div></div>"}</div>`;
561
+ }).join("");
562
+ return `<div class="L-funnel">${head(s)}<div class="fn">${rows}</div></div>${src(s)}${add(s, ctx)}`;
563
+ },
564
+
565
+ // Pirâmide: níveis do topo (estreito) para a base (larga)
566
+ pyramid(s, ctx) {
567
+ const lv = s.levels || s.items || [];
568
+ const n = Math.max(1, lv.length);
569
+ const rows = lv.map((x, i) => {
570
+ const o = typeof x === "string" ? { title: x } : x;
571
+ const w = 34 + i * (66 / Math.max(1, n - 1));
572
+ return `<div${attrs({ step: o.step ?? build(s, i) }, `py-row ${o.hl ? "hl" : ""}`)}>
573
+ <div class="py-barwrap"><div class="py-bar" style="width:${w.toFixed(1)}%"><span class="t f-heading">${md(o.title || "")}</span></div></div>
574
+ ${o.text ? `<div class="py-text t f-body">${md(o.text)}</div>` : "<div></div>"}</div>`;
575
+ }).join("");
576
+ return `<div class="L-pyramid">${head(s)}<div class="py">${rows}</div></div>${src(s)}${add(s, ctx)}`;
577
+ },
578
+
579
+ // Agenda: seções numeradas; current = número da seção em que estamos (as anteriores ficam "feitas")
580
+ agenda(s, ctx) {
581
+ const it = s.items || [];
582
+ const cur = Number(s.current) || 0;
583
+ const rows = it.map((x, i) => {
584
+ const o = typeof x === "string" ? { title: x } : x;
585
+ const state = cur ? (i + 1 === cur ? "cur" : i + 1 < cur ? "done" : "") : "";
586
+ return `<div${attrs({ step: o.step ?? build(s, i) }, `ag-row ${state}`)}><span class="ag-n t f-display">${String(i + 1).padStart(2, "0")}</span>
587
+ <div class="ag-txt">${text(o.title || "", "h3", { class: "ag-title" })}${o.text ? text(o.text, "body", { class: "ag-sub muted" }) : ""}</div>${o.time ? `<span class="ag-time t f-label">${md(o.time)}</span>` : ""}</div>`;
588
+ }).join("");
589
+ return `<div class="L-agenda">${head(s)}<div class="ag">${rows}</div></div>${src(s)}${add(s, ctx)}`;
590
+ },
591
+
592
+ // Mosaico (bento): blocos de tamanhos diferentes. size: big (2×2) | wide (2×1) | tall (1×2) | normal
593
+ bento(s, ctx) {
594
+ const tiles = s.tiles || s.items || [];
595
+ const anySize = tiles.some((t) => t && typeof t === "object" && t.size);
596
+ const body = tiles.map((x, i) => {
597
+ const t = typeof x === "string" ? { title: x } : x;
598
+ const size = t.size || (!anySize && i === 0 ? "big" : "");
599
+ const fig = t.figure ? el(t.figure, ctx, 700, 500) : t.icon ? el({ icon: t.icon, size: size === "big" ? 180 : 96 }, ctx) : "";
600
+ return `<div${attrs({ step: t.step ?? build(s, i) }, `bt-tile ${size ? "bt-" + size : ""} ${t.hl ? "hl" : ""}`)}>
601
+ ${fig ? `<div class="bt-fig">${fig}</div>` : ""}${t.value != null ? `<div class="bt-val t f-display">${md(String(t.value))}</div>` : ""}
602
+ ${t.title ? text(t.title, size === "big" ? "h2" : "h3", { class: "bt-title" }) : ""}${t.text ? text(t.text, "body", { class: "bt-text" }) : ""}</div>`;
603
+ }).join("");
604
+ return `<div class="L-bento">${head(s)}<div class="bt" style="--cols:${s.cols || 4}">${body}</div></div>${src(s)}${add(s, ctx)}`;
605
+ },
606
+
607
+ // Requisição ao vivo (tipo Postman): URL, corpo e código (curl/Python) editáveis; "Executar" roda o
608
+ // pedido pelo Studio (síncrono, polling ou streaming) e mostra a resposta. Sem o Studio (HTML exportado,
609
+ // servidor multiusuário), mostra a última resposta gravada. Ver src/runtime/api-ui.js e src/api-client.js.
610
+ api(s, ctx) {
611
+ if (s.services?.length) return `<div class="L-api-collection" data-api-collection>${head(s)}<label class="api-service-picker">Serviço <select data-api-service>${s.services.map((x,i)=>`<option value="${i}">${esc(x.name||`Serviço ${i+1}`)}</option>`).join("")}</select></label>${s.services.map((x,i)=>`<div class="api-collection-panel"${i?" hidden":""}>${LAYOUTS.api({...s,...x,services:undefined,title:undefined,kicker:undefined,id:x.id||`${s.id||plainTitle(s)}-${i+1}`},ctx)}</div>`).join("")}</div>`;
612
+ const A = globalThis.SagadeckApiCore;
613
+ const a = A.normalize(s);
614
+ const cfg = { ...a, key: A.key(s), title: s.title || "" };
615
+ const MODE = { sync: "Síncrono", polling: "Polling", stream: "Streaming", realtime: "Tempo real · WebSocket" };
616
+ const rt = a.realtime;
617
+ const bodyTxt = rt ? JSON.stringify(rt.open, null, 2) : a.request.form ? Object.entries(a.request.form).map(([k, v]) => `${k}: ${v === "@file" ? "@" + (a.file ? String(a.file).split(/[\\/]/).pop() : "arquivo") : v}`).join("\n")
618
+ : a.request.body == null ? "" : typeof a.request.body === "string" ? a.request.body : JSON.stringify(a.request.body, null, 2);
619
+ const hdrTxt = Object.entries(a.request.headers).map(([k, v]) => `${k}: ${v}`).join("\n");
620
+ const LBL = (c) => (rt && c === "curl" ? "wscat" : A.LANGS[c]);
621
+ const tabs = rt ? [["body", "Ao conectar"], ["log", "Mensagens"], ...a.code.filter((c) => A.LANGS[c]).map((c) => [c, LBL(c)])] : [...(a.similarity ? [["texts", "Frases"]] : []), ["body", "Corpo"], ...(a.fields ? [["fields", "Parâmetros"]] : []), ["headers", "Cabeçalhos"], ...a.code.filter((c) => A.LANGS[c]).map((c) => [c, A.LANGS[c]])];
622
+ const first = tabs.some(([k]) => k === s.tab) ? s.tab : a.similarity ? "texts" : "body";
623
+ const fieldsPane = a.fields ? `<div class="api-fields" data-pane="fields"${first === "fields" ? "" : " hidden"}><table><thead><tr><th>Campo</th><th>Valor</th><th>O que faz</th></tr></thead><tbody>${Object.entries(a.fields).map(([k, why]) => { const v = A.get(a.request.body, k); return `<tr data-field="${esc(k)}"><td class="f-mono">${esc(k.replace(/^\$\.?/, ""))}</td><td class="f-mono api-fv">${v === undefined ? "—" : esc(JSON.stringify(v))}</td><td>${esc(why)}</td></tr>`; }).join("")}</tbody></table></div>` : "";
624
+ const textsPane = a.similarity ? `<div class="api-texts" data-pane="texts"${first === "texts" ? "" : " hidden"}><label class="f-label">Frase de referência</label><input class="api-edit-line" data-api-ref value="${esc(a.similarity.reference)}" spellcheck="false"><label class="f-label">Compare com (uma por linha)</label><textarea class="api-edit f-mono" data-api-texts spellcheck="false">${esc(a.similarity.texts.join("\n"))}</textarea></div>` : "";
625
+ const codePane = (lang) => {
626
+ const { code: txt, comments } = A.code(a, lang, {});
627
+ const cm = new Set(comments);
628
+ return `<div class="api-code f-mono" data-pane="${lang}"${first === lang ? "" : " hidden"}>${txt.split("\n").map((l, i) => `<div class="cl${cm.has(i + 1) ? " cm" : ""}" data-ln="${i + 1}"><span class="cn">${i + 1}</span><span class="cc">${esc(l) || " "}</span></div>`).join("")}</div>`;
629
+ };
630
+ const ICON = {
631
+ vars: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
632
+ run: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.2-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg>',
633
+ out: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
634
+ pip: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><rect x="12" y="11" width="7" height="6" rx="1"/></svg>',
635
+ clip: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 11.5 12.6 19.9a5.5 5.5 0 0 1-7.8-7.8l8.5-8.5a3.7 3.7 0 0 1 5.2 5.2l-8.5 8.5a1.8 1.8 0 0 1-2.6-2.6l7.8-7.8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
636
+ };
637
+ const fileZone = A.usesFile(a) ? `<div class="api-file" data-api-file>${ICON.clip}<span class="api-file-name">${esc(a.file ? String(a.file).split(/[\\/]/).pop() : "nenhum arquivo")}</span><span class="api-file-hint">arraste um arquivo aqui</span>${a.mic ? '<button type="button" class="api-mic" data-api-mic><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="3" width="6" height="12" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg><span>Gravar</span></button><canvas class="api-level" width="160" height="36" hidden></canvas>' : ""}<button type="button" data-api-pick>Trocar arquivo</button><input type="file" hidden data-api-input></div>` : "";
638
+ return `<div class="L-api" data-api="${esc(JSON.stringify(cfg))}">${head(s)}
639
+ <div class="api-bar">
640
+ <button type="button" class="api-env" data-api-env title="Ambiente (clique para trocar)"><span class="api-env-dot"></span><span class="api-env-name">sem Studio</span></button>
641
+ <button type="button" class="api-vars" data-api-vars title="Variáveis: o que está valendo agora (ambiente, token e o que os slides guardaram)">${ICON.vars}<span>Variáveis</span></button>
642
+ <span class="api-mode f-label">${MODE[a.mode]}</span>
643
+ ${s.text ? text(s.text, "small", { class: "api-text", size: 26 }) : ""}
644
+ <span class="api-spacer"></span>
645
+ <button type="button" class="api-pip" data-api-pip title="Controle flutuante: fica por cima de qualquer janela (ex.: o portal em tela cheia)">${ICON.pip}<span>Controle flutuante</span></button>
646
+ ${a.portal ? `<a class="api-portal" href="${esc(a.portal)}" target="_blank" rel="noopener">Abrir no portal${ICON.out}</a>` : ""}
647
+ <button type="button" class="api-run" data-api-run>${ICON.run}<span>${rt ? "Conectar" : "Executar"}</span></button>
648
+ </div>
649
+ <div class="api-row">
650
+ <section class="api-req" aria-label="Requisição">
651
+ <div class="api-line"><span class="api-method m-${rt ? "WS" : esc(a.request.method)}">${rt ? "WS" : esc(a.request.method)}</span><input class="api-url f-mono" data-api-url value="${esc(rt ? rt.url : a.request.url)}" spellcheck="false" aria-label="Endereço"></div>
652
+ <div class="api-resolved f-mono" data-api-resolved></div>
653
+ <div class="api-tabs" role="tablist">${tabs.map(([k, label]) => `<button type="button" role="tab" data-tab="${k}" aria-selected="${k === first}">${esc(label)}</button>`).join("")}</div>
654
+ <div class="api-panes">
655
+ ${textsPane}${fieldsPane}${rt ? `<div class="api-log f-mono" data-pane="log" hidden></div>` : ""}
656
+ <textarea class="api-edit f-mono" data-pane="body" data-api-body spellcheck="false" aria-label="Corpo"${a.request.form ? " readonly title=\"Formulário (multipart): campo: valor\"" : ""}${first === "body" ? "" : " hidden"}>${esc(bodyTxt)}</textarea>
657
+ <textarea class="api-edit f-mono" data-pane="headers" data-api-headers spellcheck="false" aria-label="Cabeçalhos" placeholder="Nome: valor"${first === "headers" ? "" : " hidden"}>${esc(hdrTxt)}</textarea>
658
+ ${a.code.filter((c) => A.LANGS[c]).map(codePane).join("")}
659
+ </div>
660
+ ${fileZone}
661
+ ${a.request.auth ? `<div class="api-auth f-label">Authorization: Bearer •••• <span>token do ambiente, automático</span></div>` : ""}
662
+ </section>
663
+ <section class="api-res" aria-label="Resposta" aria-live="polite">
664
+ <div class="api-status"><span class="api-hint">Clique em <b>${rt ? "Conectar" : "Executar"}</b></span></div>
665
+ <div class="api-out"></div>
666
+ ${rt ? `<div class="api-rt-ctl"><button type="button" class="api-mic" data-rt-mic disabled><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="3" width="6" height="12" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg><span>Falar</span></button><canvas class="api-level" width="160" height="36" hidden></canvas><input class="api-rt-text" data-rt-text placeholder="ou digite e aperte Enter" disabled><button type="button" data-rt-send disabled>Enviar</button></div>` : ""}
667
+ </section>
668
+ </div>
669
+ </div>${src(s)}${add(s, ctx)}`;
670
+ },
671
+
672
+ // Posicionamento livre (x, y, w, h em px numa tela de 1920 × 1080)
673
+ canvas(s, ctx) {
674
+ // apresentação recriada (tema novo): o slide original que ficou nela não traz a moldura antiga do PowerPoint (faixas,
675
+ // logos, número: o que se repete na mesma posição, src/master.js: recreatedFrame), que destoaria; o conteúdo fica
676
+ const frame = s.original ? recreatedFrame(ctx?.spec) : null;
677
+ return (s.elements || []).filter((e) => !(frame && frame(e))).map((e) => el({ ...e, x: e.x ?? 0, y: e.y ?? 0 }, ctx, e.w, e.h)).join("");
678
+ },
679
+ // Diagrama (Mermaid): fluxograma, sequência, estados, classes/UML, ER, jornada, mapa mental, linha do tempo, blocos.
680
+ // Quem desenha é o navegador (runtime/diagram.js), com a paleta do slide. Ícones: ":nome-do-icone:" nos rótulos.
681
+ diagram(s, ctx) {
682
+ const icons = {};
683
+ const known = diagramIconNames();
684
+ // %%{init}%% no código trocaria o tema do Mermaid e fugiria da paleta: sai. Rótulo com parênteses sem aspas
685
+ // (BL1[Boca de Lobo 1 (BL1)], D{Divisor (x)}) quebra o Mermaid: ganha as aspas.
686
+ const code = String(s.mermaid || s.code || "").replace(/%%\{[\s\S]*?\}%%\s*/g, "")
687
+ .replace(/(^|[\s;>|&-])([A-Za-z_][\w-]*)\[(?!["(\[\/\\])([^\]"\n]*[()][^\]"\n]*)\]/gm, '$1$2["$3"]')
688
+ .replace(/(^|[\s;>|&-])([A-Za-z_][\w-]*)\{(?![{"])([^}"\n]*[()][^}"\n]*)\}/gm, '$1$2{"$3"}')
689
+ .replace(/:([a-z](?:[a-z0-9-]*[a-z0-9])?):/g, (m, name) => { // :x: também (uma letra só)
690
+ if (!known.has(name)) return m;
691
+ try { icons[name] = iconSVG(name, { size: 40, stroke: 2 }); } catch { return m; }
692
+ return `<i class=dgi-${name}></i>`; // sem aspas: não briga com as aspas do Mermaid
693
+ });
694
+ // ligações: curvas (padrão: chegam limpas em cada caixa); "angulo" (em degraus, para arquitetura) ou "reta"
695
+ const curve = { angulo: "step", reta: "linear", curva: "basis" }[s.curve] || "basis";
696
+ return `<div class="L-diagram${s.caption ? " has-caption" : ""}">${head(s)}<div class="dg-box e" style="--d:2;"${s.autoDirection === false ? " data-dg-auto=\"0\"" : ""} data-dg-curve="${curve}" data-dg-icons="${esc(JSON.stringify(icons))}"><pre class="dg-src" hidden>${esc(code)}</pre></div>${s.caption ? text(s.caption, "small", { class: "dg-caption muted" }) : ""}</div>${src(s)}${add(s, ctx)}`;
697
+ },
698
+
699
+ // Infográfico: arco, ramos, lados, trilhas ou metrô, para qualquer quantidade de itens (src/infographic.js)
700
+ infographic(s, ctx) {
701
+ return `<div class="L-infographic">${head(s)}${infographicHTML(s, ctx)}${s.caption ? text(s.caption, "small", { class: "ig-caption muted" }) : ""}</div>${src(s)}${add(s, ctx)}`;
702
+ },
703
+
704
+ // dinâmicas ao vivo entre duas pessoas (src/dynamics): um quadro por clique
705
+ duel(s, ctx) { return `${duelHTML(s, ctx, head)}${src(s)}${add(s, ctx)}`; },
706
+ terminals(s, ctx) { return `${terminalsHTML(s, ctx, head)}${src(s)}${add(s, ctx)}`; },
707
+ turns(s, ctx) { return `${turnsHTML(s, ctx, head)}${src(s)}${add(s, ctx)}`; },
708
+ carousel(s, ctx) { return `${carouselHTML(s, ctx, head)}${src(s)}${add(s, ctx)}`; },
709
+ // aula: exercício resolvido passo a passo, calculadora ao vivo, algoritmo animado (src/lessons.js)
710
+ solution(s, ctx) { return `${solutionHTML(s, ctx, head)}${src(s)}${add(s, ctx)}`; },
711
+ calc(s, ctx) { return `${calcHTML(s, ctx, head)}${src(s)}${add(s, ctx)}`; },
712
+ algo(s, ctx) { return `${algoHTML(s, ctx, head)}${src(s)}${add(s, ctx)}`; },
713
+
714
+ science(s, ctx) {
715
+ return `<div class="L-science">${head(s)}<div class="science-body${s.plot === false ? ' equations-only' : !(s.equations || []).length ? ' plot-only' : ''}"><div class="science-equations">${mathHTML(s.equations || [])}</div>${s.plot === false ? '' : plotHTML(s.plot || {}, ctx)}</div>${s.caption ? text(s.caption,'small') : ''}</div>${add(s,ctx)}`;
716
+ },
717
+ scenography(s, ctx) {
718
+ const scene = Object.hasOwn(SCENES, s.scene) ? s.scene : 'stage';
719
+ const op = s.imageOpacity == null || s.imageOpacity === '' ? 0.85 : Math.max(0, Math.min(1, Number(s.imageOpacity) || 0));
720
+ const props = SCENES[scene].props?.() || '';
721
+ return `<div class="L-scenography raster scene-${scene}"><div class="scene-atmosphere"></div>${s.image ? `<div class="scene-image" style="opacity:${op}">${el({image:s.image},ctx,1920,slideSize(ctx.spec).h)}</div>` : ''}${props ? `<div class="scene-props" aria-hidden="true">${props}</div>` : ''}<div class="scene-architecture" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></div><div class="scene-type"><div class="scene-eyebrow t f-label">${md(s.kicker || 'IDEIAS QUE OCUPAM ESPAÇO')}</div>${text(s.title || 'ALÉM DO\nÓBVIO','hero',{class:'scene-title',fit:true,size:s.titleSize || SCENES[scene].size})}${s.subtitle ? text(s.subtitle,'lead',{class:'scene-subtitle'}) : ''}</div>${s.foreground ? `<div class="scene-foreground">${el({image:s.foreground,fit:'contain'},ctx,1920,1080)}</div>` : '<div class="scene-sculpture" aria-hidden="true"><i></i><b></b></div>'}<div class="scene-caption f-label">${esc(s.caption || 'SAGA / NOVAS PERSPECTIVAS')}</div></div>${add(s,ctx)}`;
722
+ },
723
+ };