import { existsSync, readFileSync, writeFileSync } from "fs" import { isAbsolute, normalize, resolve } from "path" import katex from "katex" import { getPageTemplateVocabulary } from "./vocabulary" export type PageTemplateStatus = "metadata-only" | "renderable" export interface PageTemplateField { name: string type: "string" | "string[]" | "items[]" | "metrics[]" | "milestones[]" | "rows[]" | "steps[]" | "members[]" | "placeholder" required?: boolean description: string } export interface PageTemplateDefinition { id: string title: string purpose: string status: PageTemplateStatus fields: PageTemplateField[] contentRules: string[] qaRules: string[] } export interface RenderTemplateSlideInput { templateId: string slideIndex: number content: Record designName?: string } export interface RenderTemplateScaffoldInput { templateId: string slideIndex: number seed?: Record designName?: string } export interface RenderTemplateSlideResult { ok: true templateId: string slideIndex: number designName: string html: string warnings: string[] } export interface RenderTemplateScaffoldResult extends RenderTemplateSlideResult { scaffold: true } export interface AddTemplateSlideInput extends RenderTemplateSlideInput { workspaceRoot: string outputPath: string } export interface AddTemplateScaffoldInput extends RenderTemplateScaffoldInput { workspaceRoot: string outputPath: string } export interface AddTemplateSlideResult extends RenderTemplateSlideResult { outputPath: string inserted: boolean } export interface AddTemplateScaffoldResult extends RenderTemplateScaffoldResult { outputPath: string inserted: boolean } export interface PageTemplateContractIssue { severity: "error" | "warning" templateId: string slideIndex?: number message: string } export interface PageTemplateContractReport { ok: boolean issues: PageTemplateContractIssue[] } export interface BoundedTemplateEditInput { beforeHtml: string afterHtml: string slideIndex: number } export interface BuiltInPreviewFixture { templateId: string content: Record } const templates: PageTemplateDefinition[] = [ define("cover", "Cover", "Open the deck with one clear artifact title and context.", [ field("eyebrow", "string", "Small context label."), field("title", "string", "Deck title.", true), ], ["Use one dominant title.", "Keep source/evidence details out of the cover."], ["Has one h1.", "Uses hero structure."]), define("section-divider", "Section Divider", "Mark a chapter transition with a short label and thesis.", [ field("eyebrow", "string", "Chapter label."), field("title", "string", "Section title.", true), ], ["Use between chapters only."], ["Has one h1.", "Counts as structural."]), define("closing", "Closing", "End with the final decision, ask, or next action.", [ field("title", "string", "Closing line.", true), ], ["Keep the close concise."], ["Has one h1.", "Uses closing/hero structure."]), define("agenda", "Agenda / TOC", "Orient the audience to the deck flow.", [ field("title", "string", "Agenda title.", true), field("items", "items[]", "Agenda items.", true), ], ["Use 3-6 items."], ["Numbers are in DOM order."]), define("executive-summary", "Executive Summary", "Compress the decision logic into a few takeaways.", [ field("title", "string", "Slide title.", true), field("items", "items[]", "Summary takeaways.", true), ], ["Use 3-4 takeaways.", "Each takeaway needs a short label and support line."], ["Contains summary cards."]), define("team", "Team", "Introduce the people behind the work with portrait-led member cards.", [ field("title", "string", "Slide title.", true), field("members", "members[]", "Team members with portrait, role, highlights, and education.", true), ], ["Use 3-4 members for a readable 16:9 page.", "Each member should have one portrait, two concise highlights, and one highest education line."], ["Contains team member cards.", "Each card keeps portrait and credentials distinct."]), define("problem-context", "Problem / Context", "Frame why the topic matters now.", [ field("title", "string", "Slide title.", true), field("body", "string", "Context paragraph.", true), field("items", "items[]", "Context bullets."), ], ["Separate situation from implication."], ["Main message remains outside cards."]), define("key-message-evidence", "Key Message + Evidence", "State a claim and show the supporting evidence items.", [ field("title", "string", "Claim title.", true), field("body", "string", "Claim explanation.", true), field("items", "items[]", "Evidence items.", true), ], ["Evidence cards must not invent unsupported facts."], ["Has claim and evidence region."]), define("claim-supporting-visual", "Claim + Supporting Visual", "Pair a claim with one visual or diagram placeholder.", [ field("title", "string", "Claim title.", true), field("body", "string", "Claim explanation.", true), field("visualTitle", "string", "Visual label."), field("items", "items[]", "Visual callouts."), ], ["Use for one visual argument, not many unrelated facts."], ["Visual region is present."]), define("metric-highlight", "Metric Highlight", "Let one or more metrics carry the page.", [ field("title", "string", "Slide title.", true), field("metrics", "metrics[]", "Metric cards.", true), field("body", "string", "Interpretation."), field("insightTitle", "string", "Insight panel title."), field("insightBody", "string", "Metric interpretation, reading note, or caveat."), field("insightIcon", "string", "Lucide icon name for the insight title."), field("insightPosition", "string", "top or bottom insight panel placement."), ], ["Every number needs an interpretation line."], ["Metric values are not hidden in body copy."]), define("chart-takeaways", "Chart + Takeaways", "Reserve space for a chart and explain what to read from it.", [ field("title", "string", "Slide title.", true), field("chartTitle", "string", "Chart title."), field("takeawaysTitle", "string", "Title for the interpretation text panel."), field("items", "items[]", "Takeaways.", true), field("quote", "string", "Optional italic quote text member inside the text panel."), field("formulaLatex", "string", "Optional LaTeX formula text member inside the text panel."), field("formulaCaption", "string", "Optional formula caption."), ], ["Chart area must be explicit and bounded."], ["Chart panel and takeaways both exist."]), define("table", "Table", "Explain a structured table with a left reading card and right table region.", [ field("title", "string", "Slide title.", true), field("textTitle", "string", "Left text card title."), field("textBody", "string", "Left text card body."), field("columns", "string[]", "Column labels.", true), field("rows", "rows[]", "Table rows.", true), ], ["Use the text card to tell the audience how to read the table.", "Keep table rows structured and scannable."], ["Left text card and right table region both exist.", "Table has headers and body rows."]), define("table-comparison", "Table / Comparison", "Compare options, segments, or facts in a structured table.", [ field("title", "string", "Slide title.", true), field("columns", "string[]", "Column labels.", true), field("rows", "rows[]", "Table rows.", true), field("insightTitle", "string", "Insight panel title."), field("insightBody", "string", "Interpretation, reading note, or caveat below the table."), field("insightIcon", "string", "Lucide icon name for the insight title."), ], ["Keep rows scannable.", "Do not use a table for pure prose."], ["Table has headers and body rows."]), define("milestone", "Milestone", "Show dated phases or milestones on a horizontal roadmap axis.", [ field("title", "string", "Slide title.", true), field("milestones", "milestones[]", "Horizontal milestones.", true), ], ["Use 3-6 milestones.", "Milestone cards sit above the axis and date labels sit below it.", "Each dot belongs to the same DOM item as its copy."], ["Timeline root exists.", "Every milestone has dot and copy.", "Dot and copy are sibling anchors inside one timeline item."]), define("timeline", "Timeline", "Show dated events, journey steps, or sequence as a vertical timeline.", [ field("title", "string", "Slide title.", true), field("milestones", "milestones[]", "Timeline events.", true), field("insightTitle", "string", "Side panel title."), field("insightBody", "string", "Timeline interpretation, so-what, or caveat."), field("insightSide", "string", "left or right side panel placement."), ], ["Use 3-6 milestones.", "Horizontal timelines use card copy above the axis and year labels below it.", "Each dot belongs to the same DOM item as its copy."], ["Timeline root exists.", "Every milestone has dot and copy.", "Dot and copy are sibling anchors inside one timeline item."]), define("process-steps", "Process / Steps", "Show a short ordered process or execution sequence.", [ field("title", "string", "Slide title.", true), field("steps", "steps[]", "Ordered steps.", true), ], ["Use 3-5 steps.", "Each step starts with an action."], ["Steps are numbered in DOM order."]), define("recommendation-decision", "Recommendation / Decision / Ask", "Make the requested decision and explain rationale and next steps.", [ field("title", "string", "Slide title.", true), field("recommendation", "string", "Recommended action.", true), field("image", "string", "Optional image card path for the recommendation panel."), field("imageAlt", "string", "Optional image alt text."), field("imageCaption", "string", "Optional image caption."), field("items", "items[]", "Rationale points."), field("steps", "steps[]", "Next steps."), ], ["State the ask plainly.", "Separate rationale from next steps."], ["Recommendation panel exists.", "Next steps are ordered."]), define("risks-tradeoffs", "Risks / Caveats / Tradeoffs", "Keep limitations and tradeoffs visible.", [ field("title", "string", "Slide title.", true), field("items", "items[]", "Risks or caveats.", true), ], ["Name uncertainty instead of hiding it."], ["Contains risk/tradeoff cards."]), define("free", "Free / Plain", "Start from a title and one flexible placeholder that can later hold images, charts, text, or mixed content.", [ field("title", "string", "Slide title.", true), field("placeholder", "placeholder", "One flexible placeholder region for agent-decided content composition.", true), ], ["Use for pages that need a simple title and one replaceable working area.", "The placeholder may later contain multiple images, charts, text blocks, tables, or mixed content selected by the agent."], ["Contains one free placeholder stage.", "The single placeholder remains visible and replaceable."]), ] export function listPageTemplates(): { ok: true; templates: PageTemplateDefinition[] } { return { ok: true, templates: templates.map((template) => ({ ...template, vocabulary: getPageTemplateVocabulary(template.id), } as PageTemplateDefinition & { vocabulary: ReturnType })), } } export function renderTemplateSlide(input: RenderTemplateSlideInput): RenderTemplateSlideResult { const requestedTemplateId = normalizeTemplateId(input.templateId) const template = getPageTemplate(requestedTemplateId) const slideIndex = positiveIndex(input.slideIndex) const content = input.content ?? {} const designName = input.designName || "lucent" const warnings = validateRequiredFields(template, content) const outputTemplate = requestedTemplateId === "timeline-roadmap" ? { ...template, id: "timeline-roadmap", title: "Timeline / Roadmap" } : template const html = renderSlideShell({ template: outputTemplate, slideIndex, designName, title: stringValue(content.title) || template.title, body: renderBody(requestedTemplateId, content), }) return { ok: true, templateId: outputTemplate.id, slideIndex, designName, html, warnings } } export function builtInPreviewFixtures(): BuiltInPreviewFixture[] { return [ fixture("cover", { title: "cover" }), fixture("section-divider", { title: "section-divider" }), fixture("closing", { title: "closing" }), fixture("agenda", { title: "agenda", items: [ { label: "Frame the decision" }, { label: "Show the evidence" }, { label: "Compare options" }, { label: "Close with action" }, ], }), fixture("executive-summary", { title: "executive-summary", items: [ { label: "Decision is ready", description: "The facts support moving from discussion to selection without adding another analysis cycle." }, { label: "Risk is bounded", description: "Known caveats are visible, named, and can be managed through rollout gates." }, { label: "Next step is narrow", description: "A pilot decision creates more learning without overcommitting capital or team capacity." }, ], }), fixture("team", { title: "team", members: [ { name: "Maya Chen", role: "Product and AI systems", image: "./assets/card-lens.jpg", imageAlt: "Portrait placeholder", highlights: ["Led the decision workspace launch from zero to enterprise pilot.", "Built source-linked review loops for regulated teams."], education: "M.S., Human-Computer Interaction" }, { name: "Jon Bell", role: "Go-to-market and partnerships", image: "./assets/report-visual.jpg", imageAlt: "Portrait placeholder", highlights: ["Scaled strategic accounts across finance and healthcare.", "Opened channel partnerships with implementation teams."], education: "MBA, Strategy and Operations" }, { name: "Ari Patel", role: "Data and platform engineering", image: "./assets/soft-texture.jpg", imageAlt: "Portrait placeholder", highlights: ["Architected reliable artifact generation and QA pipelines.", "Shipped analytics systems used by executive teams."], education: "B.S., Computer Science" }, { name: "Lina Gomez", role: "Research and narrative design", image: "./assets/toc-orb.png", imageAlt: "Portrait placeholder", highlights: ["Directed research synthesis for high-stakes board materials.", "Designed traceable evidence models for complex decisions."], education: "M.A., Communication Design" }, ], }), fixture("problem-context", { title: "problem-context", body: "Use this template when the audience needs the situation, tension, and implication before seeing recommendations.", items: [ { label: "Situation", description: "A shift has changed the operating baseline." }, { label: "Tension", description: "Current process cannot absorb the new variance cleanly." }, { label: "Implication", description: "Delay increases rework and weakens decision confidence." }, ], }), fixture("key-message-evidence", { title: "key-message-evidence", body: "This key message stays large and readable, while the supporting evidence is separated into traceable slots for source-backed proof.", items: [ { label: "Evidence 1", description: "The generated HTML separates the key-message panel from the evidence grid, so the claim cannot collapse into generic card content." }, { label: "Evidence 2", description: "Each evidence slot has a stable title and explanation area, giving the agent a predictable place for proof, caveat, or source-backed detail." }, { label: "Evidence 3", description: "QA can inspect the DOM contract before visual styling, which keeps template structure from depending on a design skin." }, ], }), fixture("claim-supporting-visual", { title: "claim-supporting-visual", claim: "A single visual should carry one argument.", body: "The template reserves a stable visual region while keeping explanatory copy close enough to guide interpretation.", items: [ { label: "Anchor", description: "State what the reader should inspect first." }, { label: "Callout", description: "Use short labels instead of a second paragraph." }, ], }), fixture("metric-highlight", { title: "metric-highlight", metrics: [ { value: "67%", label: "Adoption signal", description: "Primary number plus interpretation." }, { value: "3x", label: "Review speed", description: "Comparison is stated beside the metric." }, { value: "14d", label: "Pilot window", description: "Time bound keeps the ask concrete." }, ], insightTitle: "Read the signal", insightIcon: "scan-search", insightBody: "Treat the metric row as the evidence surface, then use this panel to state the decision implication, caveat, or next reading step.", }), fixture("chart-takeaways", { title: "chart-takeaways", takeawaysTitle: "What to read", formulaLatex: "\\mathrm{CAGR}=\\left(\\frac{\\mathrm{FY26\\ Plan}}{\\mathrm{FY25}}\\right)^{1/n}-1", formulaCaption: "Formula text member", items: [ { label: "Trend", description: "Call out the movement or comparison the chart is meant to prove, including the direction and the comparison baseline." }, { label: "Driver", description: "Name the likely reason without overclaiming; separate observed movement from the interpretation or hypothesis." }, { label: "Decision use", description: "Explain how the chart changes the recommendation, what threshold matters, and what follow-up evidence would reduce risk." }, ], }), fixture("table", { title: "table", textTitle: "Financial readout", textBody: "Read top-line growth first, then check margin, cash conversion, and retention to see whether the plan is financially durable.", quote: "Durability shows up when growth, margin, and cash all point in the same direction.", columns: ["Line item", "FY2025", "FY2026 Plan", "YoY / note"], rows: [ ["Revenue", "$84.2M", "$104.8M", "+24% planned growth"], ["Gross margin", "68.4%", "71.2%", "+280 bps mix shift"], ["Operating expense", "$42.7M", "$49.1M", "Scale hiring below revenue growth"], ["EBITDA", "$14.9M", "$23.6M", "+58% operating leverage"], ["Free cash flow", "$9.8M", "$16.4M", "Cash conversion improves to 69%"], ["Net retention", "116%", "121%", "Expansion supports plan quality"], ], }), fixture("table-comparison", { title: "table-comparison", columns: ["Layer", "Owns", "Agent task"], rows: [ ["Template", "Structure and DOM contract", "Select the page pattern"], ["Content", "Claim, evidence, caveat", "Fill the meaning"], ["Design", "Color, type, surfaces", "Skin stable classes"], ], insightTitle: "Insight", insightIcon: "lightbulb", insightBody: "The template layer owns structure, while the design layer owns visual treatment. This keeps agent edits bounded without freezing the final look.", }), fixture("milestone", { title: "milestone", milestones: [ { date: "2022", label: "Signal", description: "Map the baseline." }, { date: "2023", label: "Proof", description: "Validate the evidence threshold." }, { date: "2024", label: "Inflection", description: "Use the pivotal moment to frame the shift." }, { date: "2025", label: "Scale", description: "Use a taller card for the highlighted milestone.", highlight: true }, { date: "2026", label: "Decision", description: "Commit to the next path." }, ], }), fixture("timeline", { title: "timeline", insightTitle: "Reading the journey", insightBody: "The timeline should show sequence and decision rhythm, while the side panel explains why the milestones matter.", quote: "Sequence is evidence when each step changes what the audience can believe.", milestones: [ { date: "Mar 2019", label: "Launch", description: "Baseline mapping." }, { date: "Nov 2019", label: "Audit", description: "Evidence sprint." }, { date: "May 2020", label: "Scale", description: "Operating cadence." }, { date: "Feb 2021", label: "Review", description: "QA before export." }, ], }), fixture("process-steps", { title: "process-steps", steps: [ { label: "Choose", description: "Select the page template that matches the communication job." }, { label: "Fill", description: "Provide only the content fields the template needs." }, { label: "Style", description: "Let the active design control type, color, and surfaces." }, { label: "QA", description: "Run contract and visual checks before export." }, ], }), fixture("recommendation-decision", { title: "recommendation-decision", recommendation: "Adopt page templates as the structural layer, with designs remaining user-customizable.", image: "./assets/card-lens.jpg", imageAlt: "Lucent design asset", imageCaption: "Design asset example", items: [ { label: "Rationale", description: "This keeps generation reliable while leaving style expressive and replaceable." }, ], steps: [ { label: "Pilot", description: "Use the built-in preview to tune every template." }, { label: "Validate", description: "Promote only contracts that pass QA and browser review." }, { label: "Ship", description: "Document the add-slide workflow for agents." }, ], }), fixture("risks-tradeoffs", { title: "risks-tradeoffs", items: [ { label: "Constraint", description: "Templates can over-constrain if they become decorative presets instead of communication jobs.", image: "./assets/report-visual.jpg", imageAlt: "Report visual", imageCaption: "Design asset example" }, { label: "Mitigation", description: "Keep bounded HTML edit flow after scaffold insertion so agents can improve the page directly." }, { label: "Tradeoff", description: "More structure improves QA, but only if template contracts stay small and semantic." }, ], }), fixture("free", { title: "free", placeholder: { label: "Flexible content placeholder", description: "Agent may replace this single region with multiple images, charts, text blocks, tables, or a mixed composition.", hints: ["images", "charts", "text", "tables"], }, }), ] } export function renderBuiltInPreviewHtml(): string { const fixtures = builtInPreviewFixtures() const slides = fixtures .map((item, index) => renderTemplateSlide({ templateId: item.templateId, slideIndex: index + 1, designName: "built-in-preview", content: { eyebrow: previewEyebrow(index + 1, fixtures.length), ...item.content, }, }).html) .join("\n") const html = ` Revela Page Templates ${slides} ${slidePresentationRuntime()} ` return ensureInlineLucideRuntime(html) } export function addTemplateSlide(input: AddTemplateSlideInput): AddTemplateSlideResult { const outputPath = normalizeOutputPath(input.outputPath) const targetPath = resolve(input.workspaceRoot, outputPath) if (!existsSync(targetPath)) throw new Error(`Deck HTML does not exist: ${outputPath}. Create the deck foundation before adding template slides.`) const rendered = renderTemplateSlide(input) const html = readFileSync(targetPath, "utf-8") const markers = deckFoundationMarkers() if (!html.includes(markers.start) || !html.includes(markers.end)) throw new Error(`Deck HTML is missing Revela slide markers: ${outputPath}`) const withSlide = html.replace(markers.end, `${rendered.html}\n ${markers.end}`) const next = rendered.html.includes("data-lucide=") ? ensureInlineLucideRuntime(withSlide) : withSlide writeFileSync(targetPath, next, "utf-8") return { ...rendered, outputPath, inserted: true } } export function renderTemplateScaffold(input: RenderTemplateScaffoldInput): RenderTemplateScaffoldResult { const seed = scaffoldSeed(input.templateId, input.seed ?? {}) const rendered = renderTemplateSlide({ templateId: input.templateId, slideIndex: input.slideIndex, content: seed, designName: input.designName, }) return { ...rendered, scaffold: true } } export function addTemplateScaffold(input: AddTemplateScaffoldInput): AddTemplateScaffoldResult { const outputPath = normalizeOutputPath(input.outputPath) const targetPath = resolve(input.workspaceRoot, outputPath) if (!existsSync(targetPath)) throw new Error(`Deck HTML does not exist: ${outputPath}. Create the deck foundation before adding template slides.`) const rendered = renderTemplateScaffold(input) const html = readFileSync(targetPath, "utf-8") const markers = deckFoundationMarkers() if (!html.includes(markers.start) || !html.includes(markers.end)) throw new Error(`Deck HTML is missing Revela slide markers: ${outputPath}`) const withSlide = html.replace(markers.end, `${rendered.html}\n ${markers.end}`) const next = rendered.html.includes("data-lucide=") ? ensureInlineLucideRuntime(withSlide) : withSlide writeFileSync(targetPath, next, "utf-8") return { ...rendered, outputPath, inserted: true } } function deckFoundationMarkers(): { start: string; end: string } { return { start: "", end: "" } } function fixture(templateId: string, content: Record): BuiltInPreviewFixture { return { templateId, content } } function previewEyebrow(slideIndex: number, total: number): string { const index = String(slideIndex).padStart(2, "0") const count = String(total).padStart(2, "0") return `Template ${index} / ${count}` } function normalizeOutputPath(outputPath: string): string { const trimmed = String(outputPath || "").trim() if (!trimmed) throw new Error("outputPath is required") if (!trimmed.endsWith(".html")) throw new Error("Deck outputPath must end in .html") if (isAbsolute(trimmed)) throw new Error("Deck outputPath must be workspace-relative") const segments = trimmed.split(/[\\/]+/) if (segments.includes("..")) throw new Error("Deck outputPath must not contain parent-directory traversal") return normalize(trimmed).replace(/\\/g, "/") } export function templateDeckCss(input: { designName?: string; designAssetBasePath?: string } = {}): string { const designName = input.designName || "lucent" const assetBasePath = input.designAssetBasePath const lucentCoverBackground = designName === "lucent" && assetBasePath ? cssUrl(`${assetBasePath}/cover-background.jpg`) : "" const lucentClosingBackground = designName === "lucent" && assetBasePath ? cssUrl(`${assetBasePath}/closing-background.jpg`) : "" const lucentCoverBackgroundCss = lucentCoverBackground ? ` .template-slide[data-template="cover"] .slide-canvas { background: linear-gradient(90deg, rgba(7,17,31,0.82), rgba(7,17,31,0.42) 52%, rgba(7,17,31,0.24)), url("${lucentCoverBackground}") center center / cover no-repeat; } .template-slide[data-template="agenda"] .slide-canvas { background: linear-gradient(90deg, rgba(7,17,31,0.86), rgba(7,17,31,0.58) 52%, rgba(7,17,31,0.32)), url("${lucentCoverBackground}") center center / cover no-repeat; } .template-slide[data-template="section-divider"] .slide-canvas { background: linear-gradient(90deg, rgba(7,17,31,0.86), rgba(16,26,43,0.62) 58%, rgba(36,58,115,0.36)), url("${lucentCoverBackground}") center center / cover no-repeat; }` : "" const lucentClosingBackgroundCss = lucentClosingBackground ? ` .template-slide[data-template="closing"] .slide-canvas { background: linear-gradient(90deg, rgba(7,17,31,0.82), rgba(49,94,234,0.42) 58%, rgba(24,168,216,0.24)), url("${lucentClosingBackground}") center center / cover no-repeat; }` : "" return ` * { box-sizing: border-box; } html { scroll-snap-type: y mandatory; overflow-y: scroll; height: 100%; } body { margin: 0; background: var(--bg-frame, #07111f); color: var(--text-primary, #101a2b); font-family: var(--font-body, Arial, sans-serif); -webkit-font-smoothing: antialiased; } .slide { min-height: 100dvh; scroll-snap-align: start; display: flex; align-items: center; justify-content: center; overflow: hidden; background: var(--bg-frame, #07111f); } .slide-canvas { width: 1920px; height: 1080px; flex-shrink: 0; transform-origin: center center; position: relative; overflow: hidden; } .template-slide .slide-canvas { background: radial-gradient(circle at 82% 16%, rgba(49, 94, 234, 0.11), transparent 29%), linear-gradient(135deg, var(--bg-page), var(--bg-page-alt)); color: var(--text-primary); padding: 72px; box-sizing: border-box; } .template-frame { width: 100%; height: 100%; display: flex; flex-direction: column; gap: 34px; } .template-frame--catalog { gap: 26px; } .template-eyebrow { margin: 0 0 14px; font-size: 16px; text-transform: uppercase; letter-spacing: 0.18em; color: var(--text-muted); font-weight: 700; } .template-frame header { flex: 0 0 auto; padding-bottom: 8px; overflow: visible; } .template-title { margin: 0; max-width: 1320px; font-family: var(--font-display); font-size: 62px; line-height: 1.22; color: var(--text-primary); padding-bottom: 6px; overflow: visible; } .template-body { flex: 1; min-height: 0; } .template-grid { display: grid; gap: 24px; height: 100%; } .template-grid.cols-2 { grid-template-columns: 0.95fr 1.05fr; } .template-grid.cols-3 { grid-template-columns: repeat(3, 1fr); } .template-grid.cols-4 { grid-template-columns: repeat(4, 1fr); } .template-chart-layout { grid-template-columns: 2fr 1fr; } .template-card { background: rgba(255,255,255,0.82); border: 1px solid var(--line); border-radius: var(--surface-radius); padding: 28px; box-shadow: 0 18px 44px var(--shadow-soft); } .template-card h2, .template-card h3 { margin: 0 0 12px; font-size: 28px; line-height: 1.32; padding-bottom: 4px; overflow: visible; } .template-card p { margin: 10px 0; font-size: 21px; line-height: 1.42; color: var(--text-secondary); } .template-key-message-panel { display: flex; flex-direction: column; justify-content: flex-start; gap: 24px; padding: 0; background: transparent; border-radius: 0; box-shadow: none; } .template-key-message-kicker { margin: 0; max-width: 720px; font-size: 32px; line-height: 1.24; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent-primary); font-weight: 800; padding-bottom: 6px; overflow: visible; } .template-key-message-panel p { margin: 0; max-width: 760px; font-size: 25px; line-height: 1.5; color: var(--text-secondary); } .template-evidence-grid { display: grid; gap: 24px; min-height: 0; } .template-evidence-card { min-height: 0; } .template-claim-text-panel { min-height: 0; display: flex; flex-direction: column; justify-content: flex-start; align-items: flex-start; gap: 18px; padding: 0; background: transparent; border: 0; border-radius: 0; box-shadow: none; } .template-claim-text-title { margin: 0; max-width: 760px; font-size: 31px; line-height: 1.26; color: var(--text-primary); padding-bottom: 4px; overflow: visible; } .template-claim-text-body { margin: 0; max-width: 760px; font-size: 22px; line-height: 1.48; color: var(--text-secondary); } .template-claim-text-panel .template-list { margin-top: 4px; } .template-list { display: grid; gap: 18px; margin: 0; padding: 0; list-style: none; } .template-list li { position: relative; padding-left: 24px; font-size: 24px; line-height: 1.38; color: var(--text-secondary); } .template-list li::before { content: ""; position: absolute; left: 0; top: 14px; width: 7px; height: 7px; background: var(--accent-primary); } .template-hero { margin: 0; max-width: none; justify-content: center; align-items: flex-start; } .template-hero > [data-template-slot="hero"] { width: 100%; } .template-hero header { padding-bottom: 0; } .template-hero-title { font-size: 120px; line-height: 1.18; color: white; font-weight: 800; opacity: 0.8; padding: 12px 0 20px; max-width: 1320px; } .template-hero .template-eyebrow { color: rgba(255,255,255,0.78); } .template-hero--cover, .template-hero--section-divider { justify-content: center; align-items: flex-start; } .template-hero--closing { justify-content: flex-end; align-items: flex-end; } .template-hero--closing > [data-template-slot="hero"] { display: flex; justify-content: flex-end; text-align: right; } .template-hero--closing .template-hero-title { max-width: 1120px; } .template-slide[data-template="agenda"] .template-frame { display: grid; grid-template-rows: 1fr auto; gap: 28px; } .template-slide[data-template="cover"] .slide-canvas, .template-slide[data-template="section-divider"] .slide-canvas, .template-slide[data-template="closing"] .slide-canvas { background: radial-gradient(circle at 80% 14%, rgba(24,168,216,0.32), transparent 28%), linear-gradient(135deg, #07111f, #101a2b 62%, #243a73); } ${lucentCoverBackgroundCss} .template-slide[data-template="closing"] .slide-canvas { background: linear-gradient(135deg, #07111f, #315eea 58%, #18a8d8); } ${lucentClosingBackgroundCss} .template-agenda-panel { height: 100%; min-height: 0; display: flex; overflow: hidden; color: white; } .template-agenda-inner { width: 100%; display: grid; grid-template-columns: 37% minmax(0, 1fr); align-items: stretch; gap: 76px; } .template-agenda-header { display: flex; flex-direction: column; min-height: 0; padding: 10px 0 0; } .template-agenda-header .template-eyebrow { color: rgba(255,255,255,0.64); } .template-agenda-header .template-title { max-width: 440px; font-family: var(--font-display); font-size: 54px; line-height: 1.16; letter-spacing: 0; text-transform: uppercase; color: white; padding-bottom: 8px; overflow: visible; } .template-agenda-footer { margin: auto 0 0; font-family: var(--font-body); font-size: 13px; line-height: 1.4; letter-spacing: 0.12em; text-transform: uppercase; font-weight: 800; color: rgba(255,255,255,0.84); } .template-agenda-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; justify-content: center; gap: 40px; height: 100%; } .template-agenda-item { display: grid; grid-template-columns: 86px minmax(0, 1fr); gap: 44px; align-items: center; min-height: 58px; overflow: visible; } .template-agenda-item span { font-family: var(--font-display); font-size: 44px; line-height: 1; letter-spacing: 0.03em; color: var(--accent-cyan, #18a8d8); font-weight: 800; font-variant-numeric: tabular-nums; } .template-agenda-item strong { font-family: var(--font-body); font-size: 18px; line-height: 1.45; letter-spacing: 0.1em; text-transform: uppercase; font-weight: 700; color: rgba(255,255,255,0.92); } .template-metric-layout { height: 100%; min-height: 0; display: grid; gap: 26px; } .template-metric-layout--insight-top { grid-template-rows: auto minmax(0, 1fr); } .template-metric-layout--insight-bottom { grid-template-rows: minmax(0, 1fr) auto; } .template-stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; align-items: stretch; } .template-stat-value { display: block; min-height: 96px; font-size: 58px; line-height: 1.42; color: var(--accent-primary); font-weight: 800; margin-bottom: 18px; padding-bottom: 14px; overflow: visible; } .template-chart-panel { min-height: 520px; display: grid; place-items: center; border: 1px solid var(--line); background: rgba(255,255,255,0.72); } .template-chart-placeholder { width: 76%; height: 56%; border-left: 2px solid var(--line-strong); border-bottom: 2px solid var(--line-strong); display: flex; align-items: end; gap: 28px; padding: 0 28px 24px; } .template-visual-slot-panel { width: 100%; min-height: 520px; border: 1px dashed var(--line-strong); border-radius: var(--surface-radius); background: linear-gradient(135deg, rgba(49,94,234,0.08), rgba(24,168,216,0.08)); display: grid; place-items: center; padding: 0; } .template-visual-slot-label { font-size: 13px; line-height: 1.35; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); font-weight: 800; } .template-text-panel.template-chart-takeaway-panel { gap: 28px; } .template-text-panel--plain { background: rgba(255,255,255,0.74); border: 1px solid transparent; box-shadow: none; } .template-text-panel--clear { background: transparent; border: 0; border-radius: 0; box-shadow: none; padding-left: 0; padding-right: 0; } .template-text-panel--color { background: linear-gradient(135deg, #5f82c8 0%, var(--accent-primary) 58%, #18a8d8 115%); color: white; box-shadow: 0 22px 56px rgba(49,94,234,0.24); } .template-text-panel--color .template-text-panel-title { color: white; } .template-text-panel--color .template-text-panel-body { color: rgba(255,255,255,0.78); } .template-text-panel-quote { margin: 2px 0 0; font-size: 22px; line-height: 1.44; font-style: italic; color: var(--text-secondary); } .template-text-panel--color .template-text-panel-quote { color: rgba(255,255,255,0.82); } .template-text-panel-formula { margin: 0; width: 100%; display: grid; gap: 8px; color: var(--text-primary); } .template-text-panel--color .template-text-panel-formula { color: white; } .template-text-panel-formula .katex-display { margin: 0; overflow: visible; } .template-text-panel-formula .katex { font-size: 1.08em; color: inherit; } .template-text-panel-formula-fallback { display: block; white-space: normal; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.82em; line-height: 1.35; color: inherit; } .template-text-panel-formula-caption { margin: 0; font-size: 14px; line-height: 1.35; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); } .template-text-panel--color .template-text-panel-formula-caption { color: rgba(255,255,255,0.72); } .template-chart-takeaway-list { display: grid; gap: 22px; width: 100%; } .template-chart-takeaway-item { display: grid; gap: 7px; padding-top: 18px; border-top: 1px solid var(--line); } .template-text-panel--color .template-chart-takeaway-item { border-top-color: rgba(255,255,255,0.24); } .template-chart-takeaway-item:first-child { padding-top: 0; border-top: 0; } .template-chart-takeaway-item h3 { margin: 0; font-size: 25px; line-height: 1.24; color: var(--text-primary); } .template-text-panel--color .template-chart-takeaway-item h3 { color: white; } .template-chart-takeaway-item p { margin: 0; font-size: 20px; line-height: 1.46; color: var(--text-secondary); } .template-text-panel--color .template-chart-takeaway-item p { color: rgba(255,255,255,0.78); } .template-bar { flex: 1; background: linear-gradient(180deg, var(--accent-primary), var(--accent-cyan)); min-height: 80px; } .template-table-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 34px; height: 100%; align-items: stretch; } .template-table-layout .template-side-panel { grid-column: 1; grid-row: 1; } .template-table-region { grid-column: 2; grid-row: 1; min-width: 0; min-height: 0; height: 100%; } .template-table-region .template-table-wrap { height: 100%; } .template-table-wrap { display: grid; grid-template-rows: minmax(0, auto) auto; gap: 22px; height: 100%; align-content: start; } .template-table { width: 100%; border-collapse: collapse; background: rgba(255,255,255,0.86); box-shadow: 0 18px 44px var(--shadow-soft); } .template-table th, .template-table td { padding: 22px 24px; border-bottom: 1px solid var(--line); text-align: left; font-size: 21px; } .template-table th { color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.12em; font-size: 15px; } .template-text-panel { min-height: 0; display: flex; flex-direction: column; justify-content: flex-start; align-items: flex-start; gap: 20px; border-radius: var(--surface-radius); padding: 42px; } .template-text-panel-title { margin: 0; font-size: 34px; line-height: 1.28; color: var(--text-primary); padding-bottom: 4px; overflow: visible; } .template-text-panel-body { margin: 0; font-size: 23px; line-height: 1.52; color: var(--text-secondary); } .template-side-panel { align-self: stretch; } .template-side-panel-title { margin: 0; } .template-side-panel-body { margin: 0; } .template-insight-panel { display: grid; gap: 10px; background: rgba(255,255,255,0.88); border: 1px solid var(--line); border-radius: var(--surface-radius); padding: 22px 24px; box-shadow: 0 14px 34px var(--shadow-soft); } .template-insight-title { margin: 0; display: flex; align-items: center; gap: 12px; font-size: 24px; line-height: 1.24; color: var(--text-primary); } .template-insight-icon { width: 24px; height: 24px; color: var(--accent-primary); stroke-width: 2.2; flex: 0 0 auto; } .template-insight-body { margin: 0; font-size: 20px; line-height: 1.42; color: var(--text-secondary); } .template-metric-layout .template-insight-panel { border: 0; box-shadow: none; background: rgba(255,255,255,0.74); padding: 24px 28px; } .template-metric-layout .template-insight-title { font-size: 18px; line-height: 1.22; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted); } .template-metric-layout .template-insight-icon { width: 20px; height: 20px; } .template-metric-layout .template-insight-body { font-size: 24px; line-height: 1.42; color: var(--text-primary); } .template-timeline { position: relative; height: 100%; display: grid; align-items: center; } .template-timeline-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 34px; height: 100%; align-items: stretch; } .template-timeline-layout--left { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); } .template-timeline-layout--left .template-side-panel { grid-column: 1; grid-row: 1; } .template-timeline-layout--left .template-timeline { grid-column: 2; grid-row: 1; } .template-timeline-layout--right { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); } .template-timeline-layout--right .template-timeline { grid-column: 1; grid-row: 1; } .template-timeline-layout--right .template-side-panel { grid-column: 2; grid-row: 1; } .template-timeline-layout .template-text-panel--color { background: linear-gradient(135deg, #7a7fe8 0%, #5f82c8 58%, #315eea 115%); color: white; box-shadow: 0 22px 56px rgba(49,94,234,0.22); } .template-timeline--horizontal { grid-template-columns: repeat(var(--timeline-count), 1fr); column-gap: 18px; align-items: stretch; --timeline-axis-y: 86%; } .template-timeline--horizontal::before { content: ""; position: absolute; left: 4%; right: 4%; top: var(--timeline-axis-y); border-top: 2px solid var(--line-strong); transform: translateY(-1px); } .template-timeline-item { position: relative; min-height: 400px; display: grid; justify-items: center; align-items: center; } .template-timeline--horizontal .template-timeline-item { grid-template-rows: minmax(0, 1fr) 42px 86px; align-items: stretch; } .template-timeline-dot { z-index: 2; width: 22px; height: 22px; border-radius: 999px; background: var(--accent-primary); box-shadow: 0 0 0 8px rgba(49,94,234,0.12); } .template-timeline-copy { z-index: 2; width: 86%; padding: 18px 4px; background: transparent; border: 0; box-shadow: none; } .template-timeline--horizontal .template-timeline-copy.template-card { width: 94%; height: calc(100% - 55px); min-height: 254px; align-self: end; justify-self: center; display: flex; flex-direction: column; justify-content: flex-start; padding: 28px 24px 24px; margin-bottom: 22px; } .template-timeline--horizontal .template-timeline-item--highlight .template-timeline-copy.template-card { height: calc(100% - 22px); min-height: 254px; } .template-timeline--horizontal .template-insight-icon { width: 28px; height: 28px; margin: 0 auto 28px; color: var(--accent-primary); stroke-width: 2; flex: 0 0 auto; } .template-timeline--horizontal .template-timeline-item--highlight .template-insight-icon { color: #f37021; } .template-timeline--horizontal .template-timeline-item--highlight .template-timeline-copy h3 { color: #f37021; } .template-timeline--horizontal .template-timeline-dot { grid-row: 2; align-self: center; justify-self: center; } .template-timeline--horizontal .template-timeline-date { grid-row: 3; align-self: start; justify-self: center; margin: 8px 0 0; font-size: 38px; line-height: 1; font-weight: 300; letter-spacing: 0.03em; color: var(--text-muted); } .template-timeline-date { margin: 0 0 8px; font-size: 15px; text-transform: uppercase; letter-spacing: 0.14em; color: var(--accent-primary); font-weight: 800; } .template-timeline-copy h3 { margin: 0 0 8px; font-size: 27px; line-height: 1.28; padding-bottom: 4px; overflow: visible; } .template-timeline-copy p:last-child { margin: 0; font-size: 19px; color: var(--text-secondary); } .template-timeline--vertical { grid-template-columns: 1fr; align-items: stretch; padding: 18px 0; } .template-timeline--vertical::before { content: ""; position: absolute; top: 0; bottom: 0; left: 50%; border-left: 2px solid var(--line-strong); } .template-timeline--vertical .template-timeline-item { min-height: 128px; grid-template-columns: 1fr 56px 1fr; justify-items: stretch; } .template-timeline--vertical .template-timeline-dot { grid-column: 2; grid-row: 1; justify-self: center; align-self: center; } .template-timeline--vertical .template-timeline-copy { grid-row: 1; width: auto; align-self: center; background: transparent; border: 0; box-shadow: none; } .template-timeline--vertical .template-timeline-item:nth-child(odd) .template-timeline-copy { grid-column: 1; text-align: right; align-self: center; } .template-timeline--vertical .template-timeline-item:nth-child(even) .template-timeline-copy { grid-column: 3; text-align: left; align-self: center; } .template-steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 22px; } .template-step-number { font-size: 48px; color: var(--accent-primary); font-weight: 800; margin-bottom: 30px; } .template-team-grid { height: 100%; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; align-items: stretch; } .template-team-card { min-width: 0; min-height: 0; display: grid; grid-template-rows: 330px minmax(0, 1fr); overflow: hidden; background: rgba(255,255,255,0.86); border: 1px solid var(--line); border-radius: var(--surface-radius); box-shadow: 0 18px 44px var(--shadow-soft); } .template-team-photo { margin: 0; width: 100%; height: 100%; overflow: hidden; background: linear-gradient(135deg, var(--accent-soft), rgba(24,168,216,0.16)); display: grid; place-items: center; } .template-team-photo img { width: 100%; height: 100%; object-fit: cover; display: block; } .template-team-photo span { font-family: var(--font-display); font-size: 72px; line-height: 1; color: var(--accent-primary); font-weight: 800; } .template-team-copy { min-height: 0; display: flex; flex-direction: column; gap: 12px; padding: 26px; } .template-team-name { margin: 0; font-size: 30px; line-height: 1.18; color: var(--text-primary); padding-bottom: 3px; overflow: visible; } .template-team-role { margin: 0; font-size: 15px; line-height: 1.35; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent-primary); font-weight: 800; } .template-team-highlights { margin: 4px 0 0; padding: 0; list-style: none; display: grid; gap: 9px; } .template-team-highlights li { position: relative; padding-left: 18px; font-size: 17px; line-height: 1.36; color: var(--text-secondary); } .template-team-highlights li::before { content: ""; position: absolute; left: 0; top: 9px; width: 6px; height: 6px; background: var(--accent-primary); border-radius: 999px; } .template-team-education { margin: auto 0 0; padding-top: 14px; border-top: 1px solid var(--line); font-size: 14px; line-height: 1.35; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); font-weight: 800; } .template-free-stage { height: 100%; display: grid; grid-template-columns: minmax(0, 1fr); } .template-free-placeholder { min-width: 0; min-height: 0; display: flex; flex-direction: column; justify-content: flex-end; gap: 16px; padding: 42px; border: 1px dashed var(--line-strong); border-radius: var(--surface-radius); background: linear-gradient(135deg, rgba(49,94,234,0.06), rgba(24,168,216,0.07)); overflow: hidden; } .template-free-placeholder-label { align-self: flex-start; font-size: 13px; line-height: 1; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent-primary); font-weight: 800; } .template-free-placeholder h2 { margin: 0; font-size: 38px; line-height: 1.18; color: var(--text-primary); padding-bottom: 4px; overflow: visible; } .template-free-placeholder p { margin: 0; max-width: 980px; font-size: 23px; line-height: 1.42; color: var(--text-secondary); } .template-free-placeholder-hints { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 6px; } .template-free-placeholder-hint { display: inline-flex; align-items: center; min-height: 32px; padding: 7px 12px; border: 1px solid var(--line); border-radius: 999px; font-size: 13px; line-height: 1; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); font-weight: 800; background: rgba(255,255,255,0.58); } .template-image-card { width: 100%; margin: 18px 0 0; display: grid; gap: 8px; } .template-image-frame { width: 100%; height: 128px; border-radius: var(--surface-radius); overflow: hidden; background: var(--surface-tint, #f1f6fc); border: 1px solid var(--line); } .template-image-frame img { display: block; width: 100%; height: 100%; object-fit: cover; } .template-image-caption { margin: 0; font-size: 13px; line-height: 1.35; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; } .template-visual-placeholder { width: 100%; margin: 18px 0 0; display: grid; gap: 8px; } .template-visual-placeholder-frame { width: 100%; height: 148px; border-radius: var(--surface-radius); border: 1px dashed var(--line-strong); background: linear-gradient(135deg, rgba(49,94,234,0.08), rgba(24,168,216,0.08)); display: grid; place-items: center; } .template-visual-placeholder-label { font-size: 13px; line-height: 1.35; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); font-weight: 800; } .template-page-number { position: absolute; right: 72px; bottom: 52px; font-size: 15px; color: var(--text-muted); letter-spacing: 0.18em; } .template-catalog-panel { flex: 0 0 auto; margin-top: auto; background: rgba(255,255,255,0.9); border: 1px solid var(--line); border-radius: var(--surface-radius); box-shadow: 0 18px 44px var(--shadow-soft); padding: 16px 22px; color: var(--text-primary); } .template-hero .template-catalog-panel { background: rgba(247,249,252,0.92); } .template-catalog-kicker { margin: 0 0 4px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.16em; color: var(--accent-primary); font-weight: 800; } .template-catalog-title { margin: 0 0 10px; font-size: 20px; line-height: 1.28; font-weight: 800; } .template-catalog-grid { display: grid; grid-template-columns: 1.15fr 1fr 1fr; gap: 16px; } .template-catalog-section { min-width: 0; } .template-catalog-section h3 { margin: 0 0 6px; font-size: 13px; text-transform: uppercase; letter-spacing: 0.12em; color: var(--text-muted); } .template-catalog-section p { margin: 0; font-size: 15px; line-height: 1.36; color: var(--text-secondary); } .template-catalog-list { margin: 0; padding-left: 16px; display: grid; gap: 2px; } .template-catalog-list li { font-size: 14px; line-height: 1.3; color: var(--text-secondary); } .template-frame--catalog .template-title { font-size: 52px; line-height: 1.18; } .template-slide[data-template="agenda"] .template-frame--catalog .template-title { font-size: 54px; line-height: 1.04; } .template-frame--catalog .template-card { padding: 22px; } .template-frame--catalog .template-card h2, .template-frame--catalog .template-card h3 { font-size: 24px; line-height: 1.22; margin-bottom: 8px; } .template-frame--catalog .template-card p { font-size: 18px; line-height: 1.32; } .template-frame--catalog .template-key-message-panel { gap: 16px; } .template-frame--catalog .template-key-message-kicker { font-size: 23px; line-height: 1.2; } .template-frame--catalog .template-key-message-panel p { font-size: 19px; line-height: 1.42; } .template-frame--catalog .template-claim-text-panel { gap: 12px; } .template-frame--catalog .template-claim-text-title { font-size: 24px; line-height: 1.24; } .template-frame--catalog .template-claim-text-body { font-size: 18px; line-height: 1.36; } .template-frame--catalog .template-evidence-grid { gap: 18px; } .template-frame--catalog .template-list { gap: 12px; } .template-frame--catalog .template-list li { font-size: 20px; line-height: 1.28; } .template-frame--catalog .template-metric-layout { gap: 18px; } .template-frame--catalog .template-metric-layout .template-card { padding: 20px; } .template-frame--catalog .template-metric-layout .template-stat-value { min-height: 70px; font-size: 48px; line-height: 1.24; margin-bottom: 10px; padding-bottom: 8px; } .template-frame--catalog .template-metric-layout .template-insight-panel { padding: 18px 22px; gap: 7px; } .template-frame--catalog .template-metric-layout .template-insight-title { font-size: 13px; line-height: 1.22; } .template-frame--catalog .template-metric-layout .template-insight-icon { width: 16px; height: 16px; } .template-frame--catalog .template-metric-layout .template-insight-body { font-size: 19px; line-height: 1.34; } .template-frame--catalog .template-chart-panel { min-height: 360px; } .template-frame--catalog .template-visual-slot-panel { min-height: 360px; } .template-frame--catalog .template-visual-slot-label { font-size: 11px; } .template-frame--catalog .template-chart-takeaway-panel { padding: 24px; gap: 16px; } .template-frame--catalog .template-chart-takeaway-list { gap: 13px; } .template-frame--catalog .template-chart-takeaway-item { gap: 4px; padding-top: 11px; } .template-frame--catalog .template-chart-takeaway-item h3 { font-size: 19px; line-height: 1.2; } .template-frame--catalog .template-chart-takeaway-item p { font-size: 15px; line-height: 1.3; } .template-frame--catalog .template-table-layout { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 22px; } .template-frame--catalog .template-table-layout .template-text-panel { padding: 22px; gap: 10px; } .template-frame--catalog .template-table-layout .template-text-panel-title { font-size: 25px; line-height: 1.3; } .template-frame--catalog .template-table-layout .template-text-panel-body { font-size: 18px; line-height: 1.4; } .template-frame--catalog .template-table-wrap { gap: 16px; } .template-frame--catalog .template-table th, .template-frame--catalog .template-table td { padding: 14px 18px; font-size: 17px; line-height: 1.32; } .template-frame--catalog .template-table th { font-size: 12px; } .template-frame--catalog .template-insight-panel { padding: 16px 18px; gap: 6px; } .template-frame--catalog .template-insight-title { font-size: 20px; line-height: 1.2; } .template-frame--catalog .template-insight-icon { width: 20px; height: 20px; } .template-frame--catalog .template-insight-body { font-size: 16px; line-height: 1.32; } .template-frame--catalog .template-timeline--vertical { padding: 6px 0; } .template-frame--catalog .template-timeline--vertical .template-timeline-item { min-height: 96px; } .template-frame--catalog .template-timeline-layout { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 22px; } .template-frame--catalog .template-timeline-layout--left { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); } .template-frame--catalog .template-timeline-layout--right { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); } .template-frame--catalog .template-timeline-layout .template-text-panel { padding: 22px; gap: 10px; } .template-frame--catalog .template-timeline-layout .template-text-panel-title { font-size: 25px; line-height: 1.3; } .template-frame--catalog .template-timeline-layout .template-text-panel-body { font-size: 18px; line-height: 1.4; } .template-frame--catalog .template-timeline--horizontal .template-timeline-item { grid-template-rows: minmax(0, 1fr) 32px 58px; min-height: 330px; } .template-frame--catalog .template-timeline--horizontal .template-timeline-copy.template-card { height: calc(100% - 51px); min-height: 178px; padding: 20px 18px 18px; margin-bottom: 18px; } .template-frame--catalog .template-timeline--horizontal .template-timeline-item--highlight .template-timeline-copy.template-card { height: calc(100% - 18px); min-height: 178px; } .template-frame--catalog .template-timeline--horizontal .template-insight-icon { width: 22px; height: 22px; margin-bottom: 18px; } .template-frame--catalog .template-timeline--horizontal .template-timeline-date { font-size: 28px; } .template-frame--catalog .template-timeline-copy { padding: 8px 4px; } .template-frame--catalog .template-timeline-copy h3 { font-size: 21px; line-height: 1.18; margin-bottom: 4px; } .template-frame--catalog .template-timeline-date { font-size: 12px; margin-bottom: 4px; } .template-frame--catalog .template-timeline-copy p:last-child { font-size: 15px; line-height: 1.24; } .template-frame--catalog .template-steps { gap: 16px; } .template-frame--catalog .template-step-number { font-size: 40px; margin-bottom: 20px; } .template-frame--catalog .template-team-grid { gap: 16px; } .template-frame--catalog .template-team-card { grid-template-rows: 230px minmax(0, 1fr); } .template-frame--catalog .template-team-copy { padding: 18px; gap: 8px; } .template-frame--catalog .template-team-name { font-size: 22px; line-height: 1.14; } .template-frame--catalog .template-team-role { font-size: 11px; line-height: 1.25; } .template-frame--catalog .template-team-highlights { gap: 6px; } .template-frame--catalog .template-team-highlights li { font-size: 13px; line-height: 1.25; padding-left: 14px; } .template-frame--catalog .template-team-highlights li::before { top: 7px; width: 5px; height: 5px; } .template-frame--catalog .template-team-education { font-size: 10px; line-height: 1.25; padding-top: 9px; } .template-frame--catalog .template-free-placeholder { padding: 28px; gap: 10px; } .template-frame--catalog .template-free-placeholder-label { font-size: 10px; } .template-frame--catalog .template-free-placeholder h2 { font-size: 28px; line-height: 1.16; } .template-frame--catalog .template-free-placeholder p { font-size: 17px; line-height: 1.3; } .template-frame--catalog .template-free-placeholder-hint { min-height: 26px; padding: 6px 9px; font-size: 10px; } .template-frame--catalog .template-image-frame { height: 86px; } .template-frame--catalog .template-image-caption { font-size: 11px; } .template-frame--catalog .template-visual-placeholder-frame { height: 110px; } .template-frame--catalog .template-visual-placeholder-label { font-size: 11px; } ` } function ensureInlineLucideRuntime(html: string): string { if (html.includes("function revelaRenderLucideIcons")) return html const runtime = `` return html.replace("", ` ${runtime}\n`) } function slidePresentationRuntime(): string { return ` ` } function cssUrl(value: string): string { return value.replace(/\\/g, "/").replace(/"/g, "%22").replace(/\n|\r|\f/g, "") } export function validatePageTemplateContracts(filePath: string): PageTemplateContractReport { const html = readFileSync(filePath, "utf-8") const issues: PageTemplateContractIssue[] = [] for (const section of html.matchAll(/]*class=["'][^"']*\bslide\b[^"']*["'][^>]*data-template=["']([^"']+)["'][^>]*>([\s\S]*?)<\/section>/gi)) { const templateId = section[1] const body = section[2] const slideIndex = Number(/data-slide-index=["'](\d+)["']/i.exec(section[0])?.[1]) issues.push(...validateVocabularyContract(templateId, body, Number.isInteger(slideIndex) ? slideIndex : undefined)) if (isTimelineTemplateId(templateId)) issues.push(...validateTimelineContract(templateId, body, Number.isInteger(slideIndex) ? slideIndex : undefined)) } return { ok: !issues.some((issue) => issue.severity === "error"), issues } } export function formatPageTemplateContractReport(report: PageTemplateContractReport): string { if (report.issues.length === 0) return "Template contracts passed." return report.issues.map((issue) => `- ${issue.severity.toUpperCase()}: ${issue.templateId}${issue.slideIndex ? ` slide ${issue.slideIndex}` : ""}: ${issue.message}`).join("\n") } export function validateBoundedTemplateEdit(input: BoundedTemplateEditInput): PageTemplateContractReport { const issues: PageTemplateContractIssue[] = [] const slideIndex = positiveIndex(input.slideIndex) const beforeSlides = slideSections(input.beforeHtml) const afterSlides = slideSections(input.afterHtml) const beforeKeys = [...beforeSlides.keys()].sort((a, b) => a - b) const afterKeys = [...afterSlides.keys()].sort((a, b) => a - b) if (beforeKeys.join(",") !== afterKeys.join(",")) { issues.push({ severity: "error", templateId: "bounded-edit", message: "Bounded edit must preserve the slide index set." }) } for (const index of afterKeys) { if (index === slideIndex) continue if (beforeSlides.get(index) !== afterSlides.get(index)) issues.push({ severity: "error", templateId: "bounded-edit", slideIndex: index, message: "Bounded edit changed a slide outside the target slide." }) } const target = afterSlides.get(slideIndex) if (!target) { issues.push({ severity: "error", templateId: "bounded-edit", slideIndex, message: "Bounded edit target slide is missing." }) } else { const templateId = /data-template=["']([^"']+)["']/i.exec(target)?.[1] || "unknown" issues.push(...validateVocabularyContract(templateId, target, slideIndex)) if (isTimelineTemplateId(templateId)) issues.push(...validateTimelineContract(templateId, target, slideIndex)) } return { ok: !issues.some((issue) => issue.severity === "error"), issues } } function define(id: string, title: string, purpose: string, fields: PageTemplateField[], contentRules: string[], qaRules: string[]): PageTemplateDefinition { return { id, title, purpose, status: "renderable", fields, contentRules, qaRules } } function validateTimelineContract(templateId: string, html: string, slideIndex?: number): PageTemplateContractIssue[] { const issues: PageTemplateContractIssue[] = [] const root = /class=["'][^"']*\btemplate-timeline\b[^"']*["']/i.test(html) if (!root) { issues.push({ severity: "error", templateId, slideIndex, message: "Missing .template-timeline root." }) return issues } const itemMatches = [...html.matchAll(/]*class=["'][^"']*\btemplate-timeline-item\b[^"']*["'][^>]*>([\s\S]*?)<\/article>/gi)] if (itemMatches.length < 3) issues.push({ severity: "warning", templateId, slideIndex, message: "Timeline should usually contain at least three milestones." }) for (let index = 0; index < itemMatches.length; index++) { const item = itemMatches[index][1] if (!/class=["'][^"']*\btemplate-timeline-dot\b[^"']*["']/i.test(item)) issues.push({ severity: "error", templateId, slideIndex, message: `Milestone ${index + 1} is missing .template-timeline-dot inside its item.` }) if (!/class=["'][^"']*\btemplate-timeline-copy\b[^"']*["']/i.test(item)) issues.push({ severity: "error", templateId, slideIndex, message: `Milestone ${index + 1} is missing .template-timeline-copy inside its item.` }) } const dotCount = (html.match(/\btemplate-timeline-dot\b/g) ?? []).length const copyCount = (html.match(/\btemplate-timeline-copy\b/g) ?? []).length if (dotCount !== copyCount) issues.push({ severity: "error", templateId, slideIndex, message: `Timeline dot count (${dotCount}) must match copy count (${copyCount}).` }) return issues } function validateVocabularyContract(templateId: string, html: string, slideIndex?: number): PageTemplateContractIssue[] { const issues: PageTemplateContractIssue[] = [] let vocabulary try { vocabulary = getPageTemplateVocabulary(templateId) } catch { return issues } for (const className of vocabulary.requiredClasses) { if (!hasClass(html, className)) issues.push({ severity: "error", templateId, slideIndex, message: `Missing required template class .${className}.` }) } for (const slot of vocabulary.slots.filter((item) => item.required)) { if (!hasTemplateSlot(html, slot.name)) issues.push({ severity: "error", templateId, slideIndex, message: `Missing required template slot '${slot.name}'.` }) } for (const slot of vocabulary.slots.filter((item) => item.replaceable && item.name === "visual")) { if (hasTemplateSlot(html, slot.name) && !hasVisualSemanticContainer(html)) issues.push({ severity: "error", templateId, slideIndex, message: "Visual slot must keep an image, chart, table, diagram, or template visual slot semantic container." }) } return issues } function slideSections(html: string): Map { const sections = new Map() for (const match of html.matchAll(/]*class=["'][^"']*\bslide\b[^"']*["'][^>]*data-slide-index=["'](\d+)["'][^>]*>[\s\S]*?<\/section>/gi)) { sections.set(Number(match[1]), match[0]) } return sections } function hasClass(html: string, className: string): boolean { return new RegExp(`class=["'][^"']*\\b${escapeRegExp(className)}\\b[^"']*["']`, "i").test(html) } function hasTemplateSlot(html: string, slot: string): boolean { return new RegExp(`data-template-slot=["']${escapeRegExp(slot)}["']`, "i").test(html) } function hasVisualSemanticContainer(html: string): boolean { return /\b(template-visual-slot-panel|template-image-card|template-chart-panel|template-table|echart-panel|data-table|media-frame| item.id === id) if (!template) throw new Error(`Unknown page template: ${templateId}`) return template } function normalizeTemplateId(templateId: string): string { return String(templateId || "").trim() } function canonicalTemplateId(templateId: string): string { const id = normalizeTemplateId(templateId) if (id === "timeline-roadmap") return "milestone" return id } function isTimelineTemplateId(templateId: string): boolean { return ["milestone", "timeline", "timeline-roadmap"].includes(templateId) } function renderSlideShell(input: { template: PageTemplateDefinition; slideIndex: number; designName: string; title: string; body: string; catalog?: any }): string { const hero = ["cover", "section-divider", "closing"].includes(input.template.id) const slideQa = hero ? "false" : "true" const slideRole = input.template.id === "cover" || input.template.id === "closing" ? ` data-slide-role="${input.template.id}"` : "" const hasCatalog = Boolean(input.catalog) const heroModifier = hero ? ` template-hero--${input.template.id}` : "" return `
${input.body} ${renderCatalogPanel(input.template, input.catalog)}
${String(input.slideIndex).padStart(2, "0")}
` } function renderCatalogPanel(template: PageTemplateDefinition, content: any): string { if (!content || typeof content !== "object") return "" const fields = Array.isArray(content.fields) ? content.fields : template.fields.filter((field) => field.required).map((field) => field.name) const qa = Array.isArray(content.qa) ? content.qa : template.qaRules return `` } function renderHeader(content: Record, fallbackTitle = "", options: { hero?: boolean } = {}): string { const eyebrow = stringValue(content.eyebrow) const titleClass = options.hero ? "template-title template-hero-title" : "template-title" return `
${eyebrow ? `

${escapeHtml(eyebrow)}

` : ""}

${escapeHtml(stringValue(content.title) || fallbackTitle)}

` } function renderBody(templateId: string, content: Record): string { if (["cover", "section-divider", "closing"].includes(templateId)) return `
${renderHeader(content, templateId, { hero: true })}
` if (templateId === "agenda") return renderAgenda(content) if (templateId === "executive-summary") return `${renderHeader(content, "Executive Summary")}
${cards(items(content), "h2", { visualPlaceholder: true })}
` if (templateId === "team") return `${renderHeader(content, "Team")}
${teamCards(content.members)}
` if (templateId === "problem-context") return `${renderHeader(content, "Problem / Context")}

${escapeHtml(stringValue(content.body))}

${list(items(content))}
` if (templateId === "key-message-evidence") return `${renderHeader(content, "Key Message + Evidence")}
${keyMessagePanel(content)}
${evidenceCards(items(content))}
` if (templateId === "claim-supporting-visual") return `${renderHeader(content, "Claim + Supporting Visual")}
${claimTextPanel(content)}${visualSlotPanel()}
` if (templateId === "metric-highlight") return `${renderHeader(content, "Metric Highlight")}
${metricHighlight(content)}
` if (templateId === "chart-takeaways") return `${renderHeader(content, "Chart + Takeaways")}
${visualSlotPanel()}${chartTakeawayPanel(content)}
` if (templateId === "table") return `${renderHeader(content, "Table")}
${tablePage(content)}
` if (templateId === "table-comparison") return `${renderHeader(content, "Table / Comparison")}
${table(content)}
` if (templateId === "milestone") return `${renderHeader(content, "Milestone")}
${timeline({ ...content, orientation: "horizontal" })}
` if (templateId === "timeline") return `${renderHeader(content, "Timeline")}
${timeline({ ...content, orientation: "vertical" })}
` if (templateId === "timeline-roadmap") return `${renderHeader(content, "Timeline / Roadmap")}
${timeline(content)}
` if (templateId === "process-steps") return `${renderHeader(content, "Process / Steps")}
${steps(content.steps)}
` if (templateId === "recommendation-decision") return `${renderHeader(content, "Recommendation / Decision")}

Recommendation

${escapeHtml(stringValue(content.recommendation))}

${imageCard(content)}
${cards(items(content).slice(0, 1), "h3")}

Next steps

${orderedSteps(content.steps)}
` if (templateId === "risks-tradeoffs") return `${renderHeader(content, "Risks / Tradeoffs")}
${cards(items(content), "h3")}
` if (templateId === "free") return `${renderHeader(content, "Free / Plain")}
${freePlaceholder(content.placeholder ?? content.placeholders)}
` return renderHeader(content, templateId) } function renderAgenda(content: Record): string { const agendaItems = items(content) const eyebrow = stringValue(content.eyebrow) const footer = stringValue(content.footer) || "Structure-First-Design" return `
${eyebrow ? `

${escapeHtml(eyebrow)}

` : ""}

${escapeHtml(stringValue(content.title) || "Agenda")}

    ${agendaItems.map((item, index) => `
  1. ${String(index + 1).padStart(2, "0")}${escapeHtml(item.label)}
  2. `).join("")}
` } function keyMessagePanel(content: Record): string { return `

Key message

${escapeHtml(stringValue(content.body))}

` } function claimTextPanel(content: Record): string { return `

${escapeHtml(stringValue(content.claim) || stringValue(content.title) || "Claim")}

${escapeHtml(stringValue(content.body))}

${list(items(content))}
` } function evidenceCards(items: Array<{ label: string; description: string; image?: string; imageAlt?: string; imageCaption?: string }>): string { return items.map((item, index) => `

${escapeHtml(item.label || `Evidence ${index + 1}`)}

${escapeHtml(item.description)}

${imageCard(item)}
`).join("") } function teamCards(input: any): string { const members = Array.isArray(input) ? input : [] return members.slice(0, 6).map((member, index) => { const name = stringValue(member?.name) || `Member ${index + 1}` const role = stringValue(member?.role || member?.title) const image = safeImagePath(stringValue(member?.image)) const alt = stringValue(member?.imageAlt) || name const highlights = stringArray(member?.highlights).slice(0, 3) const education = stringValue(member?.education || member?.highestEducation) return `
${image ? `${escapeAttribute(alt)}` : `${escapeHtml(initials(name))}`}

${escapeHtml(name)}

${role ? `

${escapeHtml(role)}

` : ""} ${highlights.length > 0 ? `
    ${highlights.map((item) => `
  • ${escapeHtml(item)}
  • `).join("")}
` : ""} ${education ? `

${escapeHtml(education)}

` : ""}
` }).join("") } function freePlaceholder(input: any): string { const legacy = Array.isArray(input) ? input : [] const value = legacy.length > 0 ? { label: "Flexible content placeholder", description: "Agent may replace this single region with multiple images, charts, text blocks, tables, or a mixed composition.", hints: legacy.map((item) => stringValue(item?.type || item?.label)).filter(Boolean), } : input const label = stringValue(value?.label) || "Flexible content placeholder" const description = stringValue(value?.description) || "Agent may decide whether this placeholder becomes multiple images, charts, text blocks, tables, or a mixed composition." const hints = stringArray(value?.hints || value?.accepts).slice(0, 6) return `
placeholder

${escapeHtml(label)}

${escapeHtml(description)}

${hints.length > 0 ? `
${hints.map((hint) => `${escapeHtml(hint)}`).join("")}
` : ""}
` } function chartTakeawayPanel(content: Record): string { const takeawayItems = items(content) const title = stringValue(content.takeawaysTitle) || "What to read" return `

${escapeHtml(title)}

${takeawayItems.map((item) => `

${escapeHtml(item.label)}

${escapeHtml(item.description)}

`).join("")}
${renderTextMembers(content)}
` } function cards(items: Array<{ label: string; description: string; image?: string; imageAlt?: string; imageCaption?: string }>, heading: "h2" | "h3", options: { visualPlaceholder?: boolean } = {}): string { return items.map((item) => `
<${heading}>${escapeHtml(item.label)}

${escapeHtml(item.description)}

${imageCard(item) || (options.visualPlaceholder ? visualPlaceholder() : "")}
`).join("") } function list(items: Array<{ label: string; description: string }>): string { return `
    ${items.map((item) => `
  • ${escapeHtml(item.label)}${item.description ? ` ${escapeHtml(item.description)}` : ""}
  • `).join("")}
` } function metrics(input: any): string { const values = Array.isArray(input) ? input : [] return values.slice(0, 4).map((item) => `
${escapeHtml(stringValue(item.value) || "0")}

${escapeHtml(stringValue(item.label) || "Metric")}

${escapeHtml(stringValue(item.description))}

`).join("") } function metricHighlight(content: Record): string { const statGrid = `
${metrics(content.metrics)}
` const panel = renderInsightPanel(content) if (!panel) return statGrid const position = stringValue(content.insightPosition) === "top" ? "top" : "bottom" return `
${position === "top" ? `${panel}${statGrid}` : `${statGrid}${panel}`}
` } function table(content: Record): string { const columns = Array.isArray(content.columns) ? content.columns.map(stringValue).filter(Boolean) : ["Dimension", "Current", "Target"] const rows = Array.isArray(content.rows) ? content.rows : [] const insight = renderInsightPanel(content) return `
${columns.map((column) => ``).join("")}${rows.map((row) => `${columns.map((column, index) => ``).join("")}`).join("")}
${escapeHtml(column)}
${escapeHtml(Array.isArray(row) ? stringValue(row[index]) : stringValue(row[column]) || stringValue(row[slug(column)]))}
${insight}
` } function tablePage(content: Record): string { const panelContent = { insightTitle: stringValue(content.textTitle) || "What to read", insightBody: stringValue(content.textBody) || "Use this card to explain the comparison, caveat, or decision implication before the audience scans the table.", quote: stringValue(content.quote), } return `
${renderTextPanel(panelContent, "text-card", "clear")}
${table({ ...content, insightTitle: "", insightBody: "" })}
` } function renderInsightPanel(content: Record): string { const body = stringValue(content.insightBody) if (!body) return "" const title = stringValue(content.insightTitle) || "Insight" const icon = safeLucideIconName(stringValue(content.insightIcon) || "lightbulb") return `

${escapeHtml(title)}

${escapeHtml(body)}

` } function timeline(content: Record): string { const milestones = Array.isArray(content.milestones) ? content.milestones : [] const orientation = stringValue(content.orientation) === "vertical" ? "vertical" : "horizontal" const sidePanel = renderSidePanel(content) const side = stringValue(content.insightSide) === "right" ? "right" : "left" const timelineHtml = `
${milestones.map((item) => timelineMilestone(item, orientation)).join("")}
` if (!sidePanel) return timelineHtml return `
${side === "left" ? `${sidePanel}${timelineHtml}` : `${timelineHtml}${sidePanel}`}
` } function timelineMilestone(item: any, orientation: "horizontal" | "vertical"): string { const itemClass = item?.highlight ? "template-timeline-item template-timeline-item--highlight" : "template-timeline-item" const copyClass = orientation === "horizontal" ? "template-timeline-copy template-card" : "template-timeline-copy" if (orientation === "horizontal") { return `

${escapeHtml(stringValue(item.label))}

${escapeHtml(stringValue(item.description))}

${escapeHtml(stringValue(item.date))}

` } return `

${escapeHtml(stringValue(item.date))}

${escapeHtml(stringValue(item.label))}

${escapeHtml(stringValue(item.description))}

` } function renderSidePanel(content: Record): string { return renderTextPanel(content, "insight", "color") } function renderTextPanel(content: Record, slot = "insight", variant: "plain" | "clear" | "color" = "plain"): string { const body = stringValue(content.insightBody) if (!body) return "" const title = stringValue(content.insightTitle) || "Insight" return `

${escapeHtml(title)}

${escapeHtml(body)}

${renderTextMembers(content)}
` } function renderTextMembers(content: Record): string { const quote = stringValue(content.quote) const formulaLatex = stringValue(content.formulaLatex) const parts: string[] = [] if (quote) parts.push(`
${escapeHtml(quote)}
`) if (formulaLatex) parts.push(renderFormulaMember(formulaLatex, stringValue(content.formulaCaption))) return parts.join("") } function renderFormulaMember(latex: string, caption = ""): string { const escapedLatex = escapeAttribute(latex) const rendered = renderLatex(latex) const captionHtml = caption ? `

${escapeHtml(caption)}

` : "" return `
${rendered}${captionHtml}
` } function renderLatex(latex: string): string { try { return katex.renderToString(latex, { displayMode: true, output: "mathml", strict: "warn", throwOnError: true, trust: false, }) } catch (error) { const message = error instanceof Error ? error.message : String(error) return `${escapeHtml(latex)}` } } function imageCard(input: any): string { const image = safeImagePath(stringValue(input?.image)) if (!image) return "" const alt = stringValue(input?.imageAlt) || "" const caption = stringValue(input?.imageCaption) return `
${escapeAttribute(alt)}
${caption ? `
${escapeHtml(caption)}
` : ""}
` } function visualPlaceholder(): string { return `
image / chart slot (optional)
` } function visualSlotPanel(): string { return `
image / chart slot (optional)
` } function steps(input: any): string { const values = Array.isArray(input) ? input : [] return values.slice(0, 5).map((item, index) => `
${index + 1}

${escapeHtml(stringValue(item.label) || `Step ${index + 1}`)}

${escapeHtml(stringValue(item.description))}

${visualPlaceholder()}
`).join("") } function orderedSteps(input: any): string { const values = Array.isArray(input) ? input : [] return `
    ${values.slice(0, 5).map((item) => `
  1. ${escapeHtml(stringValue(item.label))}${stringValue(item.description) ? ` ${escapeHtml(stringValue(item.description))}` : ""}${imageCard(item)}
  2. `).join("")}
` } function items(content: Record): Array<{ label: string; description: string; image?: string; imageAlt?: string; imageCaption?: string }> { const raw = Array.isArray(content.items) ? content.items : [] return raw.map((item, index) => ({ label: stringValue(typeof item === "string" ? item : item.label) || `Item ${index + 1}`, description: stringValue(typeof item === "string" ? "" : item.description || item.body || item.text), image: typeof item === "string" ? "" : stringValue(item.image), imageAlt: typeof item === "string" ? "" : stringValue(item.imageAlt), imageCaption: typeof item === "string" ? "" : stringValue(item.imageCaption), })) } function stringArray(value: any): string[] { if (Array.isArray(value)) return value.map(stringValue).filter(Boolean) const single = stringValue(value) return single ? [single] : [] } function scaffoldSeed(templateId: string, seed: Record): Record { const title = stringValue(seed.title) || getPageTemplate(templateId).title const base = { ...seed, title } if (templateId === "cover") return { eyebrow: "Deck", ...base } if (templateId === "section-divider") return { eyebrow: "Section", ...base } if (templateId === "closing") return { ...base } if (templateId === "agenda") return { items: defaultItems(["Situation", "Evidence", "Decision"]), ...base } if (templateId === "executive-summary") return { items: defaultItems(["Decision is ready", "Risk is bounded", "Next step is narrow"]), ...base } if (templateId === "team") return { members: [{ name: "Member One", role: "Role or function", highlights: ["Replace with a standout project.", "Replace with a standout experience."], education: "Highest education" }, { name: "Member Two", role: "Role or function", highlights: ["Replace with a standout project.", "Replace with a standout experience."], education: "Highest education" }, { name: "Member Three", role: "Role or function", highlights: ["Replace with a standout project.", "Replace with a standout experience."], education: "Highest education" }], ...base } if (templateId === "problem-context") return { body: "Replace with context, tension, and why now.", items: defaultItems(["Context", "Implication"]), ...base } if (templateId === "key-message-evidence") return { body: "Replace with the key message the audience should remember.", items: defaultItems(["Evidence 1", "Evidence 2", "Evidence 3"]), ...base } if (templateId === "claim-supporting-visual") return { claim: "Replace with one visual claim.", body: "Use this copy to guide how the visual should be read.", items: defaultItems(["Anchor", "Callout"]), ...base } if (templateId === "metric-highlight") return { metrics: [{ value: "67%", label: "Metric", description: "Replace with interpretation." }, { value: "3x", label: "Comparison", description: "Replace with reading note." }, { value: "14d", label: "Window", description: "Replace with time context." }], insightTitle: "Read the signal", insightBody: "Replace with the decision implication, caveat, or next reading step.", ...base } if (templateId === "chart-takeaways") return { takeawaysTitle: "What to read", items: defaultItems(["Trend", "Driver", "Decision use"]), formulaLatex: "\\mathrm{CAGR}=\\left(\\frac{\\mathrm{FY26\\ Plan}}{\\mathrm{FY25}}\\right)^{1/n}-1", formulaCaption: "Formula text member", ...base } if (templateId === "table") return { textTitle: "Financial readout", textBody: "Replace with the table reading note, caveat, or decision implication.", quote: "Durability shows up when growth, margin, and cash all point in the same direction.", columns: ["Line item", "FY2025", "FY2026 Plan", "YoY / note"], rows: [["Revenue", "$84.2M", "$104.8M", "+24% planned growth"], ["Gross margin", "68.4%", "71.2%", "+280 bps mix shift"], ["Operating expense", "$42.7M", "$49.1M", "Scale hiring below revenue growth"], ["EBITDA", "$14.9M", "$23.6M", "+58% operating leverage"], ["Free cash flow", "$9.8M", "$16.4M", "Cash conversion improves"], ["Net retention", "116%", "121%", "Expansion supports plan quality"]], ...base } if (templateId === "table-comparison") return { columns: ["Dimension", "Current", "Target"], rows: [["Replace", "Current state", "Target state"], ["Caveat", "Known limit", "Next proof"]], insightTitle: "Insight", insightBody: "Replace with the table reading note or caveat.", ...base } if (templateId === "milestone" || templateId === "timeline-roadmap") return { orientation: "horizontal", milestones: [{ date: "2022", label: "Signal", description: "Name the starting condition." }, { date: "2023", label: "Proof", description: "Show the evidence threshold." }, { date: "2024", label: "Inflection", description: "Use the pivotal moment to frame the shift." }, { date: "2025", label: "Scale", description: "Use a taller card for the highlighted milestone.", highlight: true }, { date: "2026", label: "Decision", description: "State what changes next." }], ...base } if (templateId === "timeline") return { orientation: "vertical", insightTitle: "Reading the journey", insightBody: "Replace with the timeline interpretation or caveat.", quote: "Sequence is evidence when each step changes what the audience can believe.", milestones: [{ date: "Mar 2019", label: "Launch", description: "Name the starting event." }, { date: "Nov 2019", label: "Audit", description: "Show the evidence threshold." }, { date: "May 2020", label: "Scale", description: "Explain the operating cadence." }, { date: "Feb 2021", label: "Review", description: "State what changes next." }], ...base } if (templateId === "process-steps") return { steps: defaultItems(["Step 1", "Step 2", "Step 3"]), ...base } if (templateId === "recommendation-decision") return { recommendation: "Replace with the recommended decision.", items: defaultItems(["Rationale"]), steps: defaultItems(["Pilot", "Validate", "Ship"]), ...base } if (templateId === "risks-tradeoffs") return { items: defaultItems(["Risk", "Tradeoff", "Mitigation"]), ...base } if (templateId === "free") return { placeholder: { label: "Flexible content placeholder", description: "Replace this single region with one or more images, charts, text blocks, tables, or mixed content.", hints: ["images", "charts", "text", "tables"] }, ...base } return base } function defaultItems(labels: string[]): Array<{ label: string; description: string }> { return labels.map((label) => ({ label, description: "Replace with slide-specific content." })) } function validateRequiredFields(template: PageTemplateDefinition, content: Record): string[] { const warnings: string[] = [] for (const item of template.fields) { if (!item.required) continue const value = content[item.name] if (Array.isArray(value) ? value.length === 0 : !stringValue(value)) warnings.push(`Missing required template field '${item.name}'.`) } return warnings } function positiveIndex(value: number): number { if (!Number.isInteger(value) || value < 1) throw new Error("slideIndex must be a positive 1-based integer.") return value } function stringValue(value: any): string { if (value === undefined || value === null) return "" return String(value).trim() } function slug(value: string): string { return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") } function safeLucideIconName(value: string): string { const icon = value.trim().toLowerCase().replace(/[^a-z0-9-]/g, "") return icon || "lightbulb" } function safeImagePath(value: string): string { const image = value.trim() if (!image) return "" if (/^(?:https?:|data:|javascript:|file:)/i.test(image)) return "" if (image.includes("\0")) return "" const parts = image.split(/[\\/]+/) const parentRefs = parts.filter((part) => part === "..").length if (parentRefs > 0 && !image.startsWith("../designs/") && !image.startsWith("..\\designs\\")) return "" if (parentRefs > 1) return "" return image } function initials(value: string): string { const parts = value.split(/\s+/).filter(Boolean) return parts.slice(0, 2).map((part) => part.charAt(0).toUpperCase()).join("") || "?" } function escapeHtml(value: string): string { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'") } function escapeAttribute(value: string): string { return escapeHtml(value.trim()) } function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") }