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
@@ -0,0 +1,124 @@
1
+ // Experiências completas, locais e editáveis. A direção muda a narrativa, não só a paleta.
2
+ // Os dados ilustrativos ficam identificados; nenhuma experiência depende de IA ou de imagens remotas.
3
+
4
+ export const EXPERIENCES = [
5
+ { id: "executivo", name: "Sala de decisão", subtitle: "Clareza que convence.", description: "Uma recomendação, evidências e a decisão. Elegância sóbria para reuniões importantes.", theme: "noite", accent: "#E4B660", background: "#0F1115", color: "#EDEBE6", font: "Segoe UI", category: "executive", tags: ["Estratégia", "Dados", "Sóbrio"], slideCount: 8 },
6
+ { id: "editorial", name: "Matéria de capa", subtitle: "Uma história bem contada.", description: "Tipografia editorial, enquadramentos amplos e pistas que se revelam até a virada.", theme: "editorial", accent: "#E0432B", background: "#FAF9F6", color: "#141414", font: "Georgia", category: "creative", tags: ["Storytelling", "Editorial", "Autoral"], slideCount: 8 },
7
+ { id: "palco", name: "Modo espetáculo", subtitle: "A próxima ideia já vem.", description: "Contraste, pausas dramáticas e participação da plateia. Feito para ocupar o palco.", theme: "aurora", accent: "#00F2FE", background: "#0A0D17", color: "#F8FAFC", font: "Segoe UI", category: "stage", tags: ["Keynote", "Impacto", "Interativo"], slideCount: 8 },
8
+ { id: "pop", name: "Clube criativo", subtitle: "Ideias que saem da linha.", description: "Formas geométricas, cores primárias e um workshop com espaço para experimentar.", theme: "bauhaus", accent: "#D93A2B", background: "#F3EEE3", color: "#1A1A1A", font: "Century Gothic", category: "creative", tags: ["Workshop", "Geométrico", "Divertido"], slideCount: 8 },
9
+ { id: "aula", name: "Laboratório vivo", subtitle: "Entender. Prever. Experimentar.", description: "Código por etapas, terminal, screenshot com foco e desafios para ensinar programação.", theme: "terminal", accent: "#FFB224", background: "#0D0E0D", color: "#DAD6CC", font: "Cascadia Mono", category: "code", tags: ["Programação", "Código", "Aulas"], slideCount: 9 },
10
+ { id: "minimal", name: "Uma ideia por vez", subtitle: "Só o que importa.", description: "Uma ideia por vez. Muito espaço, tipografia precisa e detalhes que deixam o conteúdo respirar.", theme: "prata", accent: "#0071E3", background: "#F5F5F7", color: "#1D1D1F", font: "Segoe UI", category: "executive", tags: ["Clean", "Produto", "Respiro"], slideCount: 8 },
11
+ ];
12
+
13
+ const svg = (body, width = 700, height = 700) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" aria-hidden="true">${body}</svg>`;
14
+ const gold = svg('<g fill="none" stroke="#E4B660"><circle cx="365" cy="335" r="235" stroke-width="1" opacity=".25"/><circle cx="365" cy="335" r="160" stroke-width="1" opacity=".45"/><path d="M85 490 255 405 390 438 610 165" stroke-width="8"/><path d="m540 165 70 0 0 70" stroke-width="8"/></g><circle cx="255" cy="405" r="10" fill="#E4B660"/><circle cx="390" cy="438" r="10" fill="#E4B660"/>');
15
+ const bauhaus = svg('<circle cx="220" cy="235" r="172" fill="#D93A2B"/><path d="M320 100h290v290H320z" fill="#1F4AA8"/><path d="m90 645 240-350 240 350z" fill="#F2B33D"/><circle cx="535" cy="525" r="92" fill="none" stroke="#1A1A1A" stroke-width="30"/>');
16
+ const orbit = svg('<g fill="none"><ellipse cx="520" cy="370" rx="465" ry="185" stroke="#00F2FE" stroke-width="3" transform="rotate(-28 520 370)"/><ellipse cx="520" cy="370" rx="310" ry="300" stroke="#7F00FF" stroke-width="38" opacity=".6"/><ellipse cx="520" cy="370" rx="170" ry="440" stroke="#FF007A" stroke-width="3" transform="rotate(48 520 370)"/></g><circle cx="860" cy="170" r="22" fill="#00F2FE"/>', 1050, 760);
17
+ const magazine = svg('<path d="M1060 0h860v1080h-860z" fill="#E0432B"/><circle cx="1510" cy="450" r="365" fill="#F7D9D2"/><path d="m1270 775 240-540 240 540z" fill="#141414"/><circle cx="1510" cy="560" r="130" fill="#E0432B"/><path d="M80 960h900" stroke="#141414" stroke-width="2"/>', 1920, 1080);
18
+ const quiet = svg('<circle cx="355" cy="330" r="215" fill="#E8E8ED"/><circle cx="355" cy="330" r="148" fill="#F5F5F7"/><circle cx="505" cy="180" r="40" fill="#0071E3"/>');
19
+ const terminal = svg('<rect x="55" y="130" width="590" height="425" rx="16" fill="#171816" stroke="#393A35" stroke-width="2"/><path d="M55 195h590" stroke="#393A35" stroke-width="2"/><g fill="#85817A"><circle cx="95" cy="164" r="8"/><circle cx="125" cy="164" r="8"/><circle cx="155" cy="164" r="8"/></g><path d="m125 290 75 65-75 65m125 0h125" fill="none" stroke="#FFB224" stroke-width="17"/><rect x="420" y="270" width="150" height="200" rx="8" fill="#252821"/><path d="m450 366 25 25 65-65" fill="none" stroke="#9EC5A6" stroke-width="15"/>');
20
+
21
+ // Mockup local de uma ferramenta de rede; mantém screenshot + focos disponíveis offline.
22
+ const networkImage = `data:image/svg+xml;base64,${Buffer.from(svg('<rect width="1280" height="800" rx="20" fill="#171B22"/><path d="M0 92h1280" stroke="#343B48" stroke-width="2"/><text x="45" y="57" fill="#F4F6FA" font-size="25" font-family="Consolas,monospace">DevTools · Network</text><rect x="45" y="128" width="1190" height="65" rx="10" fill="#252D3B"/><text x="75" y="169" fill="#99ACCA" font-size="23" font-family="Consolas,monospace">GET /api/profile</text><rect x="45" y="240" width="450" height="480" rx="12" fill="#202631"/><text x="75" y="292" fill="#9EC5A6" font-size="25" font-family="Consolas,monospace">200 OK</text><text x="75" y="347" fill="#F4F6FA" font-size="23" font-family="Consolas,monospace">Content-Type:</text><text x="75" y="387" fill="#FFB224" font-size="23" font-family="Consolas,monospace">application/json</text><text x="75" y="481" fill="#99ACCA" font-size="22" font-family="Consolas,monospace">Duration: 124 ms</text><text x="75" y="526" fill="#99ACCA" font-size="22" font-family="Consolas,monospace">Size: 52 B</text><rect x="530" y="240" width="705" height="480" rx="12" fill="#0E1219"/><text x="565" y="295" fill="#99ACCA" font-size="22" font-family="Consolas,monospace">Response</text><g fill="#E9EDF5" font-size="27" font-family="Consolas,monospace"><text x="565" y="358">{</text><text x="600" y="413">"id": 42,</text><text x="600" y="468">"name": "Ada",</text><text x="600" y="523">"active": true</text><text x="565" y="578">}</text></g>', 1280, 800)).toString("base64")}`;
23
+
24
+ const PRESETS = {
25
+ executivo: {
26
+ title: "Libere tempo.\nCrie valor.", duration: 12, markStyle: "cor",
27
+ slides: (title) => [
28
+ { layout: "cover", title, kicker: "Estratégia · uma decisão", subtitle: "Um piloto pequeno. Uma mudança relevante.", figure: { svg: gold }, notes: "Modelo ilustrativo: substitua os dados pelo seu cenário. Abra com a decisão que você precisa, antes de mostrar o diagnóstico." },
29
+ { layout: "statement", kicker: "A recomendação", text: "Aprovar um piloto de ==30 dias==.", size: 145, notes: "Diga qual decisão precisa ser tomada nesta reunião. Defina o responsável e o teto de investimento." },
30
+ { layout: "number", value: 12, suffix: " h", label: "por semana em tarefas repetitivas", context: "Tempo que poderia voltar para o cliente.", source: "Cenário ilustrativo · substitua pela sua medição", notes: "Explique como mediu o tempo. Um número só: não empilhe métricas de apoio neste slide." },
31
+ { layout: "chart", title: "Comece onde há mais atrito.", chart: { chart: "bar", data: [{ label: "Retrabalho", value: 6 }, { label: "Busca", value: 4 }, { label: "Repasse", value: 2 }], suffix: " h", highlight: [0] }, side: "Retrabalho concentra ==metade== do tempo.", source: "Cenário ilustrativo · horas por semana", notes: "Mostre a evidência que sustenta a prioridade. Substitua os valores pela análise da equipe." },
32
+ { layout: "compare", tone: "dark", title: "Uma mudança com limite claro.", left: { label: "Hoje", value: "Manual", valueSize: 104, text: "A equipe confere cada caso." }, right: { label: "Piloto", value: "Assistido", valueSize: 104, text: "A equipe decide as exceções.", hl: true }, notes: "O escopo do piloto deve ser pequeno o suficiente para reverter, e relevante o suficiente para aprender." },
33
+ { layout: "matrix", title: "Risco visível. Resposta pronta.", x: ["Baixa exposição", "Alta exposição"], y: ["Alto impacto", "Baixo impacto"], cells: [{ title: "Validar", text: "Revisão humana nas exceções.", hl: true }, { title: "Limitar", text: "Sem decisões irreversíveis." }, { title: "Observar", text: "Métricas toda semana." }, { title: "Simplificar", text: "Menos integrações no piloto." }], notes: "A matriz é um ponto de partida. Troque os riscos genéricos pelos riscos concretos do projeto." },
34
+ { layout: "timeline", title: "Trinta dias para aprender.", events: [{ when: "01", title: "Medir", text: "Definir a linha de base." }, { when: "15", title: "Testar", text: "Operar com um grupo." }, { when: "30", title: "Decidir", text: "Expandir, ajustar ou parar." }], highlight: [2], notes: "Defina agora a data da próxima decisão. O piloto termina com um critério, não com uma promessa." },
35
+ { layout: "end", tone: "accent", title: "Vamos testar?", subtitle: "Aprovar o piloto · nomear o responsável · marcar a revisão", notes: "Feche repetindo a decisão pedida, o responsável e o próximo marco." },
36
+ ],
37
+ },
38
+ editorial: {
39
+ title: "O detalhe\nque muda tudo.", duration: 10, markStyle: "sublinhado",
40
+ slides: (title) => [
41
+ { layout: "full", title, titleSize: 148, kicker: "Ensaio visual · 01", caption: "Uma investigação sobre aquilo que deixamos de ver.", overlay: "left", figure: { image: `data:image/svg+xml;base64,${Buffer.from(magazine).toString("base64")}` }, notes: "Apresente um detalhe aparentemente comum que será explicado no final. Este ensaio é um modelo de narrativa, não uma reportagem factual." },
42
+ { layout: "quote", quote: "Todo mundo olhava para a tela. Ninguém olhava para a espera.", by: "Uma cena para investigar", notes: "Conte uma situação concreta. A frase é autoral e ilustrativa: substitua pela voz real de alguém do seu projeto." },
43
+ { layout: "section", tone: "light", number: "01", title: "A pista", subtitle: "O problema não estava onde procurávamos.", notes: "Mude o enquadramento. O próximo slide deve mostrar uma evidência, não uma opinião." },
44
+ { layout: "compare", title: "O que vemos × o que acontece", left: { label: "Na tela", value: "3 passos", valueSize: 120, text: "Parece simples." }, right: { label: "Na experiência", value: "8 esperas", valueSize: 120, text: "Parece interminável.", hl: true }, source: "Exemplo narrativo · substitua pela sua observação", notes: "Dê um exemplo de contraste. Números ilustrativos: use os resultados da sua investigação." },
45
+ { layout: "full", figure: { diagram: "flow", steps: ["Observar", "Perguntar", "Redesenhar"], highlight: 1 }, overlay: "bottom", kicker: "A virada", title: "Uma pergunta melhor.", titleSize: 104, notes: "Explique a mudança de método. A seta conduz a leitura antes de você detalhar cada etapa." },
46
+ { layout: "statement", tone: "dark", text: "O detalhe não era pequeno.\nEra ==invisível==.", size: 132, notes: "Faça uma pausa. Deixe a conclusão ocupar a tela antes de avançar." },
47
+ { layout: "split", title: "Ver de novo.", body: "Volte ao começo.\nAgora procure o que falta.", figure: { svg: gold }, notes: "Revisite a cena de abertura usando o novo enquadramento. Troque a ilustração por uma imagem relevante ao seu assunto." },
48
+ { layout: "end", tone: "light", title: "O que você\nainda não viu?", subtitle: "A próxima boa ideia pode começar por aí.", notes: "Termine com uma pergunta aberta que conecte a história à realidade da audiência." },
49
+ ],
50
+ },
51
+ palco: {
52
+ title: "A próxima\nvirada.", duration: 10, markStyle: "cor",
53
+ slides: (title) => [
54
+ { layout: "headline", title, text: title, size: 246, kicker: "Uma ideia para levar além", caption: "Começa com uma pergunta.", background: { svg: orbit }, backgroundStyle: "opacity:.32;left:44%;width:65%;", notes: "Espere a sala ficar em silêncio. Abra com a pergunta do próximo slide, sem antecipar a resposta." },
55
+ { layout: "question", tone: "dark", kicker: "Mãos para cima", question: "O que prende você\nem uma história?", options: ["Uma surpresa", "Um conflito", "Uma possibilidade"], notes: "Peça uma votação com as mãos. Use a resposta da sala para conectar a sua abertura ao tema." },
56
+ { layout: "statement", tone: "accent", lines: [{ text: "Primeiro, uma tensão.", size: 112 }, { text: "Depois, uma ==possibilidade==.", size: 112, step: 1 }], notes: "Leia a primeira linha. Pause antes do clique que revela a segunda. O ritmo vem da fala, não de um avanço automático." },
57
+ { layout: "number", value: 1, label: "ideia que vale lembrar amanhã", size: 430, context: "O restante existe para sustentá-la.", notes: "Defina a sua ideia central em uma frase. Não é uma estatística: o número representa a escolha de foco." },
58
+ { layout: "full", tone: "dark", figure: { svg: orbit }, title: "Faça a ideia\nganhar espaço.", titleSize: 145, overlay: "left", notes: "Este é o momento de demonstração: substitua pela sua imagem, produto ou visualização. Deixe espaço para a audiência olhar." },
59
+ { layout: "compare", title: "De assistir a participar.", left: { label: "Uma tela", value: "Olhar", valueSize: 145 }, right: { label: "Uma experiência", value: "Agir", valueSize: 145, hl: true }, build: true, notes: "Revele as duas colunas separadamente. Mostre a ação que a audiência pode realizar depois da palestra." },
60
+ { layout: "question", tone: "alert", question: "Qual seria o seu\nprimeiro movimento?", options: ["Testar uma ideia", "Mudar uma pergunta", "Chamar alguém"], timer: 20, notes: "Reserve vinte segundos para uma escolha pessoal. A pausa prepara a chamada final." },
61
+ { layout: "end", title: "Agora é\ncom você.", subtitle: "Uma ideia. Um primeiro movimento.", figure: { svg: orbit }, notes: "Feche com uma ação pequena e concreta, em vez de uma lista de promessas." },
62
+ ],
63
+ },
64
+ pop: {
65
+ title: "Bora tirar\na ideia do papel?", duration: 20, markStyle: "marca-texto",
66
+ slides: (title) => [
67
+ { layout: "cover", title, titleSize: 159, kicker: "Clube criativo · oficina", subtitle: "Curiosidade é o único pré-requisito.", figure: { svg: bauhaus }, notes: "Convide o grupo a experimentar. Ninguém precisa ter uma ideia pronta para começar." },
68
+ { layout: "question", tone: "alert", question: "Se sua ideia fosse\numa forma…", options: ["Um círculo", "Um triângulo", "Um quadrado"], timer: 15, notes: "Quebra-gelo rápido: peça uma escolha e uma frase de explicação. Não há resposta certa." },
69
+ { layout: "headline", tone: "accent", text: "Rascunho\nnão morde.", size: 246, caption: "Dê uma chance à primeira versão.", notes: "Normalize uma primeira tentativa imperfeita. Evite alongar este momento: o exercício vem agora." },
70
+ { layout: "steps", title: "Três movimentos. Uma ideia.", steps: [{ title: "Misture", text: "Junte duas coisas distantes.", icon: "shuffle" }, { title: "Desenhe", text: "Mostre em vez de explicar.", icon: "pencil" }, { title: "Teste", text: "Conte para alguém.", icon: "message-circle" }], build: true, notes: "Revele cada etapa enquanto explica a dinâmica. O grupo deve passar mais tempo fazendo do que ouvindo." },
71
+ { layout: "bento", title: "Seu kit de possibilidades", tiles: [{ title: "E se…?", text: "A pergunta que abre espaço.", size: "big", hl: true }, { icon: "scissors", title: "Corte" }, { icon: "refresh-cw", title: "Inverta" }, { icon: "plus", title: "Combine" }, { icon: "maximize", title: "Exagere" }], notes: "Cada pessoa escolhe uma operação. Use as opções como gatilhos, não como uma lista obrigatória." },
72
+ { layout: "question", tone: "light", question: "Mostre a ideia\nem um rabisco.", context: "Papel, caneta e um minuto.", options: ["Sem apagar", "Sem explicar", "Sem perfeição"], timer: 60, notes: "Inicie o timer ao começar a atividade. Circule pela sala ou desenhe junto na tela com a caneta do apresentador." },
73
+ { layout: "compare", title: "A ideia muda quando circula.", left: { label: "Meu começo", value: "E se…", valueSize: 140 }, right: { label: "Nossa próxima versão", value: "E também!", valueSize: 110, hl: true }, notes: "Peça que cada pessoa acrescente algo ao rascunho de outra. Comentários devem construir uma possibilidade." },
74
+ { layout: "end", tone: "alert", title: "Leve uma ideia.\nDeixe outra.", subtitle: "O próximo rascunho pode ser o melhor.", notes: "Cada pessoa sai com um próximo experimento e compartilha uma descoberta do processo." },
75
+ ],
76
+ },
77
+ aula: {
78
+ title: "Da requisição\nà resposta.", duration: 25, markStyle: "cor",
79
+ slides: (title) => [
80
+ { layout: "cover", title, titleSize: 153, kicker: "Laboratório 01 · APIs", subtitle: "Uma conversa entre o navegador e o servidor.", figure: { svg: terminal }, notes: "Aula de exemplo, executável como apresentação sem servidor de API. As saídas do terminal são ilustrações, não comandos executados." },
81
+ { layout: "question", question: "O que chega primeiro?", options: ["O corpo em JSON", "A resposta HTTP", "O console.log"], hint: "Faça uma previsão antes de ler o código.", notes: "Colete hipóteses sem revelar a resposta. Retome esta pergunta depois do código por etapas." },
82
+ { layout: "split", title: "Um pedido.\nUma resposta.", body: "O navegador pede.\nO servidor responde.", figure: { diagram: "flow", steps: ["Navegador", "API", "JSON"], highlight: 1 }, notes: "Desenhe o caminho do pedido com a caneta. A API responde HTTP; JSON é um possível formato do corpo, não a resposta inteira." },
83
+ { layout: "codewalk", title: "Leia o código em três tempos.", filename: "profile.js", language: "JavaScript", code: "const response = await fetch('/api/profile');\nif (!response.ok) throw new Error('Falha HTTP');\nconst profile = await response.json();\nconsole.log(profile.name);", steps: [{ title: "1. Pedir", text: "fetch resolve com a resposta HTTP. Confira o status.", highlight: [1, 2], output: "HTTP 200 OK" }, { title: "2. Interpretar", text: "Ler o corpo também é uma operação assíncrona.", highlight: [3], output: '{ "id": 42, "name": "Ada", "active": true }' }, { title: "3. Usar", text: "Agora temos um objeto JavaScript.", highlight: [4], output: "Ada" }], notes: "Use os botões de etapa ou as setas. As saídas são previstas para o exemplo; o slide não executa código nem faz requisições.", maxWords: 110 },
84
+ { layout: "spotlight", title: "O pedido por dentro.", image: networkImage, caption: "Exemplo ilustrativo do painel Network.", hotspots: [{ x: 3.5, y: 16, width: 93, height: 8, title: "A rota", text: "GET identifica a leitura de /api/profile." }, { x: 3.5, y: 30, width: 35, height: 24, title: "O contrato", text: "Status e Content-Type descrevem a resposta." }, { x: 42, y: 38, width: 54, height: 38, title: "O corpo", text: "Este JSON vira o objeto profile." }], notes: "Explore cada ponto da imagem. Troque o mockup por um screenshot do seu projeto mantendo os focos nas mesmas coordenadas.", maxWords: 100 },
85
+ { layout: "code", kicker: "A mesma conversa, no terminal", title: "Sem navegador. Com curl.", code: "curl -i http://localhost:3000/api/profile\n\nHTTP/1.1 200 OK\nContent-Type: application/json\n\n{ \"id\": 42, \"name\": \"Ada\" }", highlight: [1, 3, 4], size: 33, note: "-i mostra os cabeçalhos junto com o corpo.", notes: "Saída ilustrativa. Para testar de verdade, substitua a URL pela sua API local e execute o comando no seu terminal." },
86
+ { layout: "question", tone: "accent", question: "E se a API devolver 404?", options: ["fetch lança sozinho", "response.ok é false", "O JSON fica vazio"], timer: 30, notes: "Resposta: response.ok é false. fetch não rejeita só por um status HTTP 404; falhas de rede podem rejeitar. Retome a linha 2 do exemplo." },
87
+ { layout: "compare", title: "Duas falhas. Duas pistas.", left: { label: "HTTP 404", value: "Resposta", valueSize: 100, text: "O servidor respondeu: recurso não encontrado." }, right: { label: "Falha de rede", value: "Rejeição", valueSize: 100, text: "fetch não obteve uma resposta utilizável.", hl: true }, notes: "Diferencie o contrato HTTP de um problema de transporte. CORS, desconexão e DNS também podem aparecer como erro de rede no navegador." },
88
+ { layout: "end", title: "Agora investigue\num pedido seu.", subtitle: "Preveja → observe → explique com suas palavras.", notes: "Atividade: abra Network, escolha um pedido e identifique método, status e corpo. Não exponha tokens ou dados pessoais em capturas compartilhadas." },
89
+ ],
90
+ },
91
+ minimal: {
92
+ title: "Menos ruído.\nMais clareza.", duration: 8, markStyle: "cor",
93
+ slides: (title) => [
94
+ { layout: "statement", title, text: title, size: 165, center: true, kicker: "Essencial", notes: "Uma abertura com espaço. Diga em uma frase o que a audiência vai compreender ao final." },
95
+ { layout: "number", value: 1, label: "ideia por vez", size: 360, notes: "O número é uma regra de composição, não uma estatística. Qual ideia este slide precisa deixar?" },
96
+ { layout: "split", title: "Espaço\ntambém fala.", body: "Uma imagem.\nUma mensagem.", figure: { svg: quiet }, notes: "Troque a ilustração por um visual relacionado ao seu assunto. Retire explicações que podem ser ditas em voz alta." },
97
+ { layout: "compare", title: "A diferença aparece no foco.", left: { label: "Antes", value: "Tudo", valueSize: 150, text: "Muitas mensagens competindo." }, right: { label: "Depois", value: "O centro", valueSize: 132, text: "Uma hierarquia clara.", hl: true }, notes: "Mostre um antes e depois real do seu produto, processo ou ideia." },
98
+ { layout: "statement", tone: "dark", text: "Se tudo é importante,\nnada ==se destaca==.", size: 137, center: true, notes: "Uma pausa visual para fixar a tese. Segure este slide por alguns segundos." },
99
+ { layout: "steps", title: "Um caminho simples.", steps: [{ title: "Escolher", text: "O que precisa ficar." }, { title: "Organizar", text: "O que vem primeiro." }, { title: "Retirar", text: "O que pode sair." }], notes: "Apresente três movimentos para colocar a ideia em prática. Se não forem necessários três, remova um." },
100
+ { layout: "quote", quote: "Qual parte você\nquer que lembrem?", notes: "Esta é uma pergunta para a audiência, sem autoria atribuída. Convide uma pessoa a responder." },
101
+ { layout: "end", tone: "light", title: "É isso.", subtitle: "Uma ideia que fica.", notes: "Repita a ideia central e termine. O silêncio também encerra bem uma apresentação." },
102
+ ],
103
+ },
104
+ };
105
+
106
+ /** Cria uma cópia independente. O título personaliza a abertura; o roteiro continua sendo um exemplo editável. */
107
+ export function createExperienceDeck(id, options = {}) {
108
+ const experience = EXPERIENCES.find((item) => item.id === id);
109
+ if (!experience) throw new Error(`Experiência desconhecida: ${id}`);
110
+ const preset = PRESETS[id];
111
+ const title = typeof options.title === "string" && options.title.trim() ? options.title.trim() : preset.title;
112
+ return {
113
+ title,
114
+ theme: experience.theme,
115
+ experience: id,
116
+ lang: "pt-BR",
117
+ duration: preset.duration,
118
+ markStyle: preset.markStyle,
119
+ maxWords: 48,
120
+ defaults: { transition: "fade", footer: false },
121
+ ...(typeof options.topic === "string" && options.topic.trim() ? { briefing: options.topic.trim() } : {}),
122
+ slides: preset.slides(title),
123
+ };
124
+ }
@@ -0,0 +1,31 @@
1
+ // Contrato compartilhado por apresentação, agente e material de estudo.
2
+ import { calcModel, calcEvaluate } from './lessons.js';
3
+
4
+ export function explorationStates(slide) {
5
+ const model = calcModel(slide);
6
+ const base = Object.fromEntries(model.inputs.map(i => [i.name, i.value]));
7
+ return [{ label: 'Inicial', values: base, explanation: slide.explanation || '' },
8
+ ...(slide.scenarios || []).map(s => ({ label: String(s.label || 'Cenário'), values: { ...base, ...s.values }, explanation: String(s.explanation || '') }))]
9
+ .map(s => ({ ...s, results: calcEvaluate(model, s.values) }));
10
+ }
11
+
12
+ export function auditExploration(slide) {
13
+ if (slide.layout !== 'calc') return [];
14
+ const model = calcModel(slide), errors = [];
15
+ const names = model.inputs.map(i => i.name);
16
+ if (slide.sweep && !names.includes(slide.sweep)) errors.push('O eixo da curva precisa ser uma entrada existente.');
17
+ for (const i of model.inputs) {
18
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(i.name)) errors.push(`Entrada inválida: ${i.name}`);
19
+ if (![i.value, i.min, i.max, i.step].every(Number.isFinite) || i.min >= i.max || i.step <= 0 || (!i.fixed && (i.value < i.min || i.value > i.max))) errors.push(`Faixa inválida: ${i.name}`);
20
+ }
21
+ const all = [...names, ...model.outputs.map(o => o.name)];
22
+ if (new Set(all).size !== all.length) errors.push('Entradas e resultados precisam de nomes únicos.');
23
+ for (const s of slide.scenarios || []) for (const [name, value] of Object.entries(s.values || {})) {
24
+ const i = model.inputs.find(i => i.name === name);
25
+ if (!i || i.fixed || !Number.isFinite(value) || value < i.min || value > i.max) errors.push(`Cenário ${s.label}: valor inválido de ${name}`);
26
+ }
27
+ const samples = explorationStates(slide);
28
+ for (const i of model.inputs.filter(i => !i.fixed)) for (const value of [i.min, (i.min + i.max) / 2, i.max]) samples.push({ label: `${i.name}=${value}`, results: calcEvaluate(model, { [i.name]: value }) });
29
+ for (const s of samples) for (const [name, r] of Object.entries(s.results)) if (r.error || (r.value != null && !Number.isFinite(r.value))) errors.push(`${s.label}, ${name}: ${r.error || 'resultado não finito'}`);
30
+ return [...new Set(errors)];
31
+ }
@@ -0,0 +1,53 @@
1
+ // Abre o deck num Chrome/Edge headless (usa o navegador já instalado; nada é baixado).
2
+ import fs from "node:fs";
3
+ import { pathToFileURL } from "node:url";
4
+ import { chromium } from "playwright-core";
5
+
6
+ const CANDIDATES = [
7
+ process.env.SAGADECK_BROWSER,
8
+ "C:/Program Files/Google/Chrome/Application/chrome.exe",
9
+ "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe",
10
+ "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe",
11
+ "C:/Program Files/Microsoft/Edge/Application/msedge.exe",
12
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
13
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
14
+ "/usr/bin/google-chrome", "/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/microsoft-edge",
15
+ ].filter(Boolean);
16
+
17
+ export function findBrowser() {
18
+ const p = CANDIDATES.find((c) => fs.existsSync(c));
19
+ if (!p) throw new Error("Não achei Chrome nem Edge. Defina SAGADECK_BROWSER com o caminho do executável.");
20
+ return p;
21
+ }
22
+
23
+ export async function openDeck(htmlFile, { scale = 1 } = {}) {
24
+ const browser = await chromium.launch({ executablePath: findBrowser() });
25
+ try {
26
+ const page = await browser.newPage({ viewport: { width: 1920, height: 1080 }, deviceScaleFactor: scale });
27
+ const errors = [];
28
+ page.on("pageerror", (e) => errors.push(e.message));
29
+ page.on("console", (m) => { if (m.type() === "error") errors.push(m.text()); });
30
+ await page.goto(pathToFileURL(htmlFile).href + "?export=1");
31
+ await page.waitForFunction(() => window.sagadeck && window.sagadeck.cur >= 0 && document.fonts.status === "loaded");
32
+ // a janela com o tamanho do slide (deck.aspect: 4:3 → 1920 × 1440): foto, PDF e PPTX sem faixa nem corte
33
+ const size = await page.evaluate(() => window.sagadeck.size || { w: 1920, h: 1080 });
34
+ if (size.h !== 1080) { await page.setViewportSize({ width: size.w, height: size.h }); await page.waitForTimeout(80); }
35
+ await waitForResources(page);
36
+ return { browser, page, errors };
37
+ } catch (error) { await browser.close(); throw error; }
38
+ }
39
+
40
+ export async function waitForResources(page, { timeout = 15000 } = {}) {
41
+ await page.evaluate(async (timeout) => {
42
+ let timer;
43
+ try {
44
+ await Promise.race([
45
+ Promise.all([window.SagaScienceReady, window.SagaDiagramsReady, document.fonts.ready,
46
+ ...[...document.images].map(img => img.decode())]),
47
+ new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('Tempo excedido ao preparar gráficos, fontes ou imagens para exportação.')), timeout); }),
48
+ ]);
49
+ } finally { clearTimeout(timer); }
50
+ document.querySelectorAll('[data-calc-reveal]').forEach(button => button.click());
51
+ }, timeout);
52
+ await page.waitForFunction(() => document.fonts.status === 'loaded' && [...document.images].every(img => img.complete && img.naturalWidth > 0), null, { timeout: 15000 });
53
+ }
@@ -0,0 +1,67 @@
1
+ // Material de estudo: a outra visão do mesmo deck. O palco mostra pouco texto e deixa o detalhe para quem fala; o
2
+ // aluno recebe cada slide inteiro (tudo revelado) com o texto de consulta (`consulta:` do slide) logo depois.
3
+ // As notas do apresentador não entram. Sai como HTML (um arquivo, imagens embutidas) e como PDF (A4).
4
+ import fs from "node:fs";
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { chromium } from "playwright-core";
8
+ import { findBrowser } from "./browser.js";
9
+ import { esc } from "../markup.js";
10
+ import { buildHTML } from '../build.js';
11
+
12
+ function interactiveStudy(s){if(!s.interactiveModel)return '';const m=s.interactiveModel,html=buildHTML({theme:m.theme,slides:[m.slide]}).html;return `<details class="study-experience"><summary>Experimentar ${s.layout==='graphlab'?'esta rede':s.layout==='playground'?'esta experiência':'este código'}</summary><iframe title="${esc(s.title||'Laboratório interativo')}" sandbox="allow-scripts" srcdoc="${esc(html)}" style="border:0;width:100%;height:540px"></iframe></details>`;}
13
+
14
+ function liveStudy(s) {
15
+ const m = s.explorationModel;
16
+ if (!m) return '';
17
+ return `<details class="study-live"><summary>Experimentar estes valores</summary><div data-calc>
18
+ <div>${m.inputs.filter(i => !i.fixed).map(i => `<label>${esc(i.label)} <input type="range" data-calc-in="${esc(i.name)}" min="${i.min}" max="${i.max}" step="${i.step}" value="${i.value}"><output data-calc-show="${esc(i.name)}"></output> ${esc(i.unit)}</label>`).join('')}</div>
19
+ <div>${m.scenarios.map((c, i) => `<button data-calc-scenario="${i}">${esc(c.label)}</button>`).join('')}<button data-calc-freeze>Comparar com este</button><button data-calc-reset>Restaurar</button></div>
20
+ <div class="calc-outputs">${m.outputs.map(o => `<p>${esc(o.label)}: <strong data-calc-out="${esc(o.name)}"></strong> ${esc(o.unit)} <span data-calc-badge="${esc(o.name)}"></span><small data-calc-compare="${esc(o.name)}" hidden></small></p>`).join('')}</div><p data-calc-explanation aria-live="polite"></p>
21
+ <script type="application/json" class="calc-model">${JSON.stringify({ ...m, prediction: '' }).replace(/</g, '\\u003c')}</script></div></details>`;
22
+ }
23
+ function studyRuntime() {
24
+ const here = path.dirname(fileURLToPath(import.meta.url));
25
+ const dir = [path.join(here, '../runtime'), path.join(here, 'runtime')].find(p => fs.existsSync(path.join(p, 'calc.js')));
26
+ return ['formula.js', 'calc.js'].map(f => `<script>${fs.readFileSync(path.join(dir, f), 'utf8')}</script>`).join('');
27
+ }
28
+
29
+ export function estudoHTML({ title, author, date, slidesMeta, shotFiles }) {
30
+ const img = (f) => (f ? `<img src="data:image/${f.endsWith(".jpg") ? "jpeg" : "png"};base64,${fs.readFileSync(f).toString("base64")}" alt="">` : "");
31
+ const exploration = s => !s.exploration ? '' : `<div class="txt"><h3>Explore e compare</h3>${s.illustrative ? '<p>Simulação ilustrativa.</p>' : ''}${s.prediction ? `<p>${esc(s.prediction)}</p>` : ''}${s.exploration.map(state => `<article><h4>${esc(state.label)}</h4><p>Entradas: ${Object.entries(state.values).map(([k, v]) => `${esc(k)} = ${esc(v)}`).join('; ')}</p><ul>${Object.entries(state.results).map(([k, r]) => `<li>${esc(s.outputLabels?.[k] || k)}: ${r.error ? esc(r.error) : r.value != null ? esc(Number(r.value).toLocaleString('pt-BR', { maximumSignificantDigits: 6 })) : ''} ${esc(r.text)}</li>`).join('')}</ul>${state.explanation ? `<p>${esc(state.explanation)}</p>` : ''}</article>`).join('')}</div>`;
32
+ const rows = slidesMeta.map((s, i) => `<section class="sl"><div class="shot">${img(shotFiles[i])}</div>${liveStudy(s)}${interactiveStudy(s)}${exploration(s)}${s.consulta ? `<div class="txt">${s.consulta}</div>` : ""}</section>`).join("\n");
33
+ const withText = slidesMeta.filter((s) => s.consulta).length;
34
+ return `<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc(title || "Material de estudo")}</title><style>
35
+ :root{--fg:#1b1d22;--muted:#6b7080;--line:#e2e4ea;--bg:#fff}
36
+ @media (prefers-color-scheme:dark){:root{--fg:#e8e9ee;--muted:#9aa0ad;--line:#2d3038;--bg:#16181d}}
37
+ @page{size:A4;margin:14mm 14mm 16mm}
38
+ body{margin:0;background:var(--bg);color:var(--fg);font:16px/1.6 'Segoe UI',Arial,sans-serif}
39
+ main{max-width:920px;margin:0 auto;padding:32px 16px 64px}
40
+ header{border-bottom:2px solid var(--fg);padding-bottom:10px;margin-bottom:28px}
41
+ header h1{margin:0;font:700 30px/1.15 'Segoe UI',Arial,sans-serif}
42
+ header p{margin:6px 0 0;color:var(--muted);font-size:14px}
43
+ .sl{margin:0 0 34px;break-inside:avoid}
44
+ .shot img{width:100%;display:block;border:1px solid var(--line);border-radius:6px}
45
+ .txt{margin-top:14px;font-size:15.5px}
46
+ .txt p{margin:0 0 10px}.txt ul,.txt ol{margin:0 0 10px;padding-left:22px}
47
+ .txt h4{margin:14px 0 4px;font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
48
+ .txt code{font:13.5px Consolas,monospace;background:#8881;padding:1px 4px;border-radius:3px}
49
+ .txt mark{background:#ffe07a;color:#1b1d22}
50
+ .study-live{border:1px solid var(--line);padding:16px;margin-top:16px;border-radius:8px}.study-live summary{cursor:pointer;font-weight:600}.study-live label{display:block;margin:12px 0}.study-live input{vertical-align:middle;width:45%}.study-live button{font:inherit;margin:6px;padding:6px;cursor:pointer}.study-live small{display:block}.study-live [hidden]{display:none}.calc-changed{color:#065dc3}@media print{.study-live{display:none}}
51
+ @media print{body{font-size:11pt}main{max-width:none;padding:0}.sl{margin-bottom:8mm}.shot img{border-radius:3px}.txt{font-size:10.5pt}}
52
+ </style></head><body><main><header><h1>${esc(title || "")}</h1><p>${[author, date].filter(Boolean).map(esc).join(" · ")}${author || date ? " · " : ""}${slidesMeta.length} slides${withText ? ` · ${withText} com texto de consulta` : ""}</p></header>
53
+ ${rows}
54
+ </main>${slidesMeta.some(s => s.explorationModel) ? studyRuntime() : ''}</body></html>`;
55
+ }
56
+
57
+ export async function estudoPDF(html, outFile) {
58
+ const browser = await chromium.launch({ executablePath: findBrowser() });
59
+ try {
60
+ const page = await browser.newPage();
61
+ await page.emulateMedia({ media: "print", colorScheme: "light" });
62
+ await page.setContent(html, { waitUntil: "load" });
63
+ await page.pdf({ path: outFile, format: "A4", printBackground: true, displayHeaderFooter: true, headerTemplate: "<span></span>",
64
+ footerTemplate: `<div style="font:8pt Segoe UI;color:#999;width:100%;text-align:center"><span class="pageNumber"></span>/<span class="totalPages"></span></div>`,
65
+ margin: { top: "14mm", bottom: "16mm", left: "14mm", right: "14mm" } });
66
+ } finally { await browser.close(); }
67
+ }