# Presentation Engine — Production Runtime Specification

Artifact system for generating stunning, animation-rich HTML presentations
from agent research output. Adapted from [zarazhangrui/frontend-slides](https://github.com/zarazhangrui/frontend-slides),
redesigned for NodeBench's artifact pipeline, iframe rendering, and live agent infrastructure.

---

## Architecture Overview

```
Agent Research Output (structured data)
    ↓
┌─────────────────────────────────────────┐
│  PRESENTATION ENGINE                     │
│                                          │
│  1. Content Shaping                      │
│     └─ Data → slide outline (≤12 slides) │
│                                          │
│  2. Preset Selection                     │
│     └─ Auto-match or user override       │
│                                          │
│  3. HTML Generation                      │
│     ├─ Base template (viewport-base CSS) │
│     ├─ Preset theme (CSS variables)      │
│     ├─ Slide DOM (section.slide > .reveal)│
│     ├─ Animation patterns (CSS-only)     │
│     └─ SlidePresentation class (JS)      │
│                                          │
│  4. Artifact Registration                │
│     └─ ARTIFACTS[key] = html string      │
└─────────────────┬───────────────────────┘
                  ↓
┌─────────────────────────────────────────┐
│  ARTIFACT PIPELINE (existing)            │
│                                          │
│  Chat inline preview  →  680×180 iframe  │
│  Split-pane panel     →  full interactive│
│  Code editor          →  live HTML edit  │
│  Save to report       →  Convex persist  │
│  Share URL             →  self-contained │
└─────────────────────────────────────────┘
```

---

## Production Runtime Flow

### Step 1: Agent Determines Presentation Format

The coverage/research agent produces structured analysis. When the output
warrants a multi-slide visual narrative (not just a data card), the agent
selects the presentation artifact type.

**Trigger heuristics:**
- Competitive comparison across 3+ entities → presentation
- Weekly briefing with 4+ signal categories → presentation
- Portfolio review with tiering + sector breakdown → presentation
- Timeline/narrative with chronological progression → presentation
- Single metric or quick comparison → standard artifact (pricing card, matrix)

**Agent decision in the orchestrator:**
```typescript
// In the structuring pass, after scratchpad analysis
const artifactType = selectArtifactType(scratchpad);
// artifactType: 'comparison' | 'matrix' | 'decay' | 'presentation' | ...

if (artifactType === 'presentation') {
  const preset = autoSelectPreset(scratchpad.contentType, scratchpad.mood);
  const slides = shapeContentToSlides(scratchpad, { maxSlides: 12 });
  const html = generatePresentation(slides, preset);
  emitArtifact({ key: `${entitySlug}-briefing.html`, type: 'presentation', html });
}
```

### Step 2: Content Shaping (Data → Slide Outline)

**Content density limits (non-negotiable for readability):**

| Slide type | Constraint |
|------------|-----------|
| Title slide | 1 heading + 1 subtitle + optional byline |
| Content slide | 1 heading + 4-6 bullets OR 2 short paragraphs |
| Metric slide | 1-4 large numbers with labels |
| Comparison grid | Max 6 cards (2×3 or 3×2) |
| Timeline | Max 6 events |
| Chart/visual | 1 visual + 1 caption |
| Quote/callout | 1 quote + attribution |
| Summary/CTA | 3-5 key takeaways |

**Slide count rules:**
- Quick briefing: 4-6 slides
- Full analysis: 8-12 slides
- Never exceed 12 (attention budget)
- Title slide always first, summary/CTA always last

**Outline structure generated by agent:**
```json
{
  "title": "AI Infrastructure Coverage — Week of May 12",
  "subtitle": "5 entities tracked · 12 sources · 3 alerts",
  "preset": "bold-signal",
  "slides": [
    { "type": "title", "heading": "...", "subtitle": "..." },
    { "type": "metrics", "items": [
      { "value": "2,847", "label": "Entities classified", "delta": "+12%" },
      { "value": "50", "label": "Strategic tier", "delta": "1.8%" }
    ]},
    { "type": "content", "heading": "Key Movements", "bullets": ["...", "..."] },
    { "type": "comparison", "heading": "Pricing Landscape", "cards": [...] },
    { "type": "timeline", "heading": "Signal Timeline", "events": [...] },
    { "type": "summary", "takeaways": ["...", "...", "..."] }
  ]
}
```

### Step 3: Preset Selection

**Auto-selection matrix (content type → preset):**

| Content type | Default preset | Mood override |
|-------------|---------------|---------------|
| Competitive analysis | Bold Signal | "clean" → Swiss Modern |
| Financial briefing | Swiss Modern | "dramatic" → Neon Cyber |
| Weekly digest | Notebook Tabs | "dark" → Electric Studio |
| Portfolio review | Glass NodeBench | "editorial" → Vintage Editorial |
| Technical deep-dive | Terminal Green | "friendly" → Pastel Geometry |
| Entity intelligence | Electric Studio | "elegant" → Dark Botanical |
| Trend analysis | Creative Voltage | "minimal" → Paper & Ink |
| Team presentation | Split Pastel | "serious" → Bold Signal |

**User override via chat:**
- "make it dark and techy" → Terminal Green
- "something clean and modern" → Swiss Modern
- "editorial style" → Vintage Editorial
- "our brand" → Glass NodeBench (custom preset)

### Step 4: HTML Generation

The engine assembles a single self-contained HTML string:

```
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
  <title>{presentation.title}</title>
  <link rel="stylesheet" href="{preset.fontUrl}"/>
  <style>
    /* === viewport-base.css (mandatory, inlined) === */
    {VIEWPORT_BASE_CSS}

    /* === Preset theme (CSS variables) === */
    :root {
      {preset.cssVariables}
    }

    /* === Slide-type styles === */
    {SLIDE_TYPE_CSS}

    /* === Animation patterns === */
    {ANIMATION_CSS}

    /* === Preset signature elements === */
    {preset.signatureCss}
  </style>
</head>
<body>
  <div class="progress-bar" aria-hidden="true"></div>
  <nav class="nav-dots" aria-label="Slide navigation"></nav>

  {slides.map(slide => renderSlide(slide)).join('\n')}

  <script>
    {SLIDE_PRESENTATION_JS}
  </script>
</body>
</html>
```

### Step 5: Artifact Registration & Chat Emission

```typescript
// Agent emits the artifact into the chat stream
emitArtifact({
  key: 'coverage-briefing-may12.html',
  type: 'presentation',
  html: generatedHtml,
  metadata: {
    preset: 'bold-signal',
    slideCount: 8,
    entities: ['Anthropic', 'OpenAI', 'Mistral'],
    generatedAt: Date.now()
  }
});
```

**Chat thread shows:**
```
┌──────────────────────────────────────────┐
│  ◆ AI Infrastructure Coverage — Week of  │
│    May 12                                │
│  ┌────────────────────────────────────┐  │
│  │  [Scaled iframe preview — slide 1] │  │
│  │  (2x scale-down, pointer-events:   │  │
│  │   none, 680×180px viewport)        │  │
│  └────────────────────────────────────┘  │
│  ● Live preview · coverage-briefing.html │
│                                  [Open]  │
└──────────────────────────────────────────┘
```

### Step 6: Split-Pane Interactive Preview

When user clicks "Open":
1. `openArtifact(null, 'coverage-briefing-may12.html')` fires
2. Panel slides in from right (50% default width)
3. Chat content reflows via `padding-right: var(--artifact-panel-width)`
4. iframe loads with `sandbox="allow-same-origin allow-scripts"`
5. SlidePresentation class initializes inside iframe
6. User can navigate slides via keyboard (←→), dots, or scroll
7. Progress bar updates in real-time

**iframe sandbox for presentations:**
```html
<iframe id="artifact-preview"
  sandbox="allow-same-origin allow-scripts"
  title="Artifact live preview">
</iframe>
```

`allow-scripts` is required for SlidePresentation class (keyboard nav,
intersection observer, progress bar, nav dots). Standard data artifacts
continue using `allow-same-origin` only.

### Step 7: Live Editing

Code toggle reveals the full HTML source in the textarea editor.
User edits → 300ms debounce → iframe srcdoc updates → live preview.

**Common edits:**
- Change slide content (text, numbers)
- Swap preset by replacing CSS variables
- Add/remove slides
- Adjust animations (change delay values)

### Step 8: Export & Persistence

| Action | Implementation |
|--------|---------------|
| Save to report | Convex mutation: `upsertArtifact({ entitySlug, key, html, type: 'presentation' })` |
| Share URL | Generate `/share/{artifactId}` route → serves raw HTML |
| Download HTML | Browser download of the self-contained file |
| Export PDF | Playwright: screenshot each slide → assemble PDF |
| Embed in memo | Reference in Decision Memo as `{{artifact:coverage-briefing}}` |

---

## Adapted Style Presets (10 presets)

### NodeBench-Native Presets (3)

#### 1. Glass NodeBench (default for internal artifacts)
- **Mood:** Professional, warm, branded
- **Fonts:** Manrope (display) + JetBrains Mono (data)
- **Colors:** `--bg-primary: #151413`, `--bg-secondary: #1c1b1a`, `--accent: #d97757` (terracotta), `--text-primary: #e8e4df`
- **Signature:** Glass card borders (`border: 1px solid rgba(255,255,255,0.06)`), backdrop-blur panels, terracotta accent on metrics
- **Best for:** Entity briefings, portfolio reviews, internal dashboards

#### 2. Signal Dark
- **Mood:** Alert-driven, data-dense, operational
- **Fonts:** Space Grotesk (display) + IBM Plex Mono (data)
- **Colors:** `--bg-primary: #0a0f1c`, `--accent: #00ffcc` (cyan), `--warn: #ff5722`, `--text-primary: #e2e8f0`
- **Signature:** Status dots (green/amber/red), monospace data tables, grid-line backgrounds
- **Best for:** Signal digests, monitoring dashboards, alert briefings

#### 3. Investor Deck
- **Mood:** Confident, clean, persuasive
- **Fonts:** Plus Jakarta Sans (display) + DM Sans (body)
- **Colors:** `--bg-primary: #ffffff`, `--bg-secondary: #f8f9fa`, `--accent: #2563eb` (blue), `--text-primary: #111827`
- **Signature:** Large metric callouts, minimal decoration, blue accent bars, light/professional
- **Best for:** Fundraising decks, board presentations, external-facing reports

### Adopted from frontend-slides (7)

#### 4. Bold Signal
- High-impact dark theme. Archivo Black + Space Grotesk.
- Colored card focal points, large section numbers.

#### 5. Notebook Tabs
- Editorial light theme. Bodoni Moda + DM Sans.
- Cream paper background, colorful tabs, binder holes.

#### 6. Neon Cyber
- Futuristic dark theme. Clash Display + Satoshi.
- Particle backgrounds, cyan/magenta glow effects.

#### 7. Terminal Green
- Hacker aesthetic. JetBrains Mono throughout.
- Green-on-black, code blocks, terminal chrome.

#### 8. Swiss Modern
- Bauhaus-minimal. Clean sans-serif.
- Geometric alignment, generous whitespace.

#### 9. Vintage Editorial
- Literary personality. Fraunces + Work Sans.
- Drop caps, pull quotes, strong typographic hierarchy.

#### 10. Creative Voltage
- Energetic retro-modern. Syne + Space Mono.
- Electric blue, neon yellow, halftone textures.

---

## Animation System (CSS-Only + JS Enhancement)

### Core Pattern: Reveal on Visibility

```css
/* Base state — hidden */
.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1),
              transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Triggered state — visible */
.slide.visible .reveal {
  opacity: 1;
  transform: translateY(0);
}

/* Staggered children */
.slide.visible .reveal:nth-child(1) { transition-delay: 0.0s; }
.slide.visible .reveal:nth-child(2) { transition-delay: 0.1s; }
.slide.visible .reveal:nth-child(3) { transition-delay: 0.2s; }
.slide.visible .reveal:nth-child(4) { transition-delay: 0.3s; }
.slide.visible .reveal:nth-child(5) { transition-delay: 0.4s; }
.slide.visible .reveal:nth-child(6) { transition-delay: 0.5s; }
```

### Animation Library

| Effect | CSS | Use when |
|--------|-----|----------|
| Fade up | `opacity:0; translateY(24px)` → `opacity:1; translateY(0)` | Default for all content |
| Scale in | `opacity:0; scale(0.92)` → `opacity:1; scale(1)` | Hero metrics, images |
| Slide from left | `opacity:0; translateX(-30px)` → visible | Timeline entries, lists |
| Blur in | `opacity:0; filter:blur(8px)` → `opacity:1; filter:blur(0)` | Dramatic reveals |
| Counter animate | JS `requestAnimationFrame` interpolation | Large numbers, KPIs |
| Bar grow | `width:0` → `width:var(--bar-pct)` with transition | Comparison charts |
| Typewriter | CSS `steps()` animation on `width` | Terminal Green preset |

### Reduced Motion Support

```css
@media (prefers-reduced-motion: reduce) {
  .reveal {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
    filter: none !important;
  }
  .progress-bar,
  .counter-animate { animation: none !important; }
}
```

---

## Slide Navigation (SlidePresentation Class)

```javascript
class SlidePresentation {
  constructor() {
    this.slides = document.querySelectorAll('.slide');
    this.current = 0;
    this.total = this.slides.length;

    this.initObserver();
    this.initKeyboard();
    this.initTouch();
    this.initProgress();
    this.initDots();
  }

  // Intersection Observer — adds .visible class for CSS reveals
  initObserver() {
    const obs = new IntersectionObserver((entries) => {
      entries.forEach(e => {
        if (e.isIntersecting) {
          e.target.classList.add('visible');
          this.current = [...this.slides].indexOf(e.target);
          this.updateProgress();
          this.updateDots();
        }
      });
    }, { threshold: 0.5 });
    this.slides.forEach(s => obs.observe(s));
  }

  // Keyboard: arrows, space, page up/down
  initKeyboard() {
    document.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ') {
        e.preventDefault();
        this.goTo(Math.min(this.current + 1, this.total - 1));
      }
      if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
        e.preventDefault();
        this.goTo(Math.max(this.current - 1, 0));
      }
    });
  }

  // Touch: swipe detection
  initTouch() {
    let startY = 0;
    document.addEventListener('touchstart', e => { startY = e.touches[0].clientY; });
    document.addEventListener('touchend', e => {
      const dy = startY - e.changedTouches[0].clientY;
      if (Math.abs(dy) > 50) {
        this.goTo(dy > 0
          ? Math.min(this.current + 1, this.total - 1)
          : Math.max(this.current - 1, 0));
      }
    });
  }

  // Progress bar
  initProgress() {
    this.progressBar = document.querySelector('.progress-bar');
    this.updateProgress();
  }
  updateProgress() {
    if (!this.progressBar) return;
    this.progressBar.style.width = ((this.current + 1) / this.total * 100) + '%';
  }

  // Navigation dots
  initDots() {
    const nav = document.querySelector('.nav-dots');
    if (!nav) return;
    nav.innerHTML = '';
    this.slides.forEach((_, i) => {
      const dot = document.createElement('button');
      dot.className = 'nav-dot' + (i === 0 ? ' active' : '');
      dot.setAttribute('aria-label', 'Go to slide ' + (i + 1));
      dot.addEventListener('click', () => this.goTo(i));
      nav.appendChild(dot);
    });
    this.dots = nav.querySelectorAll('.nav-dot');
  }
  updateDots() {
    if (!this.dots) return;
    this.dots.forEach((d, i) => d.classList.toggle('active', i === this.current));
  }

  // Navigate to slide
  goTo(i) {
    this.slides[i].scrollIntoView({ behavior: 'smooth' });
  }
}

new SlidePresentation();
```

---

## iframe Integration Constraints

### Inline Chat Preview (680×180px)
- `sandbox="allow-same-origin"` (no scripts)
- `pointer-events: none` (static thumbnail)
- 2x scale-down: `width:200%; height:200%; transform:scale(0.5)`
- Shows first slide only (scroll position = 0)
- No navigation, no animations — just the visual

### Split-Pane Panel (interactive)
- `sandbox="allow-same-origin allow-scripts"` (scripts enabled)
- Full keyboard/touch navigation
- Animations play on slide visibility
- Progress bar and nav dots functional
- `postMessage` channel for parent↔iframe communication

### PostMessage Protocol (parent ↔ presentation iframe)

```typescript
// Parent → iframe: navigate to slide
iframe.contentWindow.postMessage({ type: 'goToSlide', index: 3 }, '*');

// iframe → parent: slide changed
parent.postMessage({ type: 'slideChanged', index: 3, total: 8 }, '*');

// iframe → parent: presentation metadata
parent.postMessage({
  type: 'presentationMeta',
  title: '...',
  slideCount: 8,
  preset: 'bold-signal'
}, '*');
```

---

## PPTX Conversion Pipeline (Agent-Assisted)

### When user uploads a .pptx file:

```
User uploads presentation.pptx
    ↓
Agent detects .pptx MIME type
    ↓
Python extraction (server-side):
  python scripts/extract-pptx.py presentation.pptx --output slides.json
    ↓
JSON output: { slides: [{ title, content, images, notes }] }
    ↓
Agent maps each slide to a slide type:
  - Title text only → type: "title" or "content"
  - Bullet list → type: "content" with bullets
  - Table → type: "comparison" grid
  - Chart image → type: "visual" with embedded image
  - Speaker notes → preserved as data-notes attribute
    ↓
Agent selects preset based on original deck's color scheme
    ↓
HTML generation using standard pipeline
    ↓
Artifact emitted with key: "{original-name}-converted.html"
```

### Image Handling in Converted Decks
- Small images (<100KB): base64-encoded inline (`src="data:image/png;base64,..."`)
- Large images (>100KB): uploaded to Convex file storage, referenced by URL
- SVG diagrams: inlined directly in HTML

---

## Convex Schema Extensions

```typescript
// convex/schema.ts additions for presentation artifacts

presentationArtifacts: defineTable({
  entitySlug: v.string(),
  key: v.string(),                    // "coverage-briefing-may12.html"
  type: v.literal("presentation"),
  html: v.string(),                   // Full self-contained HTML
  preset: v.string(),                 // "bold-signal"
  slideCount: v.number(),
  title: v.string(),
  subtitle: v.optional(v.string()),
  metadata: v.object({
    entities: v.array(v.string()),
    generatedAt: v.number(),
    sourceRunId: v.optional(v.string()),
    pptxSource: v.optional(v.string()),  // Original filename if converted
  }),
  rev: v.number(),                    // Monotonic version
  createdAt: v.number(),
  updatedAt: v.number(),
})
  .index("by_entity", ["entitySlug"])
  .index("by_key", ["key"]),
```

---

## MCP Tool Extension

```typescript
// New MCP tool: generate_presentation
{
  name: "generate_presentation",
  description: "Generate an animation-rich HTML presentation artifact from structured analysis data",
  inputSchema: {
    type: "object",
    properties: {
      title: { type: "string", description: "Presentation title" },
      subtitle: { type: "string", description: "Subtitle or byline" },
      preset: {
        type: "string",
        enum: ["glass-nodebench", "signal-dark", "investor-deck",
               "bold-signal", "notebook-tabs", "neon-cyber",
               "terminal-green", "swiss-modern", "vintage-editorial",
               "creative-voltage"],
        description: "Visual style preset"
      },
      slides: {
        type: "array",
        items: {
          type: "object",
          properties: {
            type: { type: "string", enum: ["title", "content", "metrics", "comparison", "timeline", "visual", "quote", "summary"] },
            heading: { type: "string" },
            subtitle: { type: "string" },
            bullets: { type: "array", items: { type: "string" } },
            items: { type: "array", description: "Metric items or comparison cards" },
            events: { type: "array", description: "Timeline events" },
            takeaways: { type: "array", items: { type: "string" } }
          },
          required: ["type"]
        },
        maxItems: 12
      },
      entitySlug: { type: "string", description: "Entity this presentation belongs to" }
    },
    required: ["title", "slides"]
  },
  handler: async (args) => {
    const html = buildPresentation(args);
    const key = slugify(args.title) + '.html';
    // Store in artifact system
    await emitArtifact({ key, type: 'presentation', html, metadata: { ... } });
    return { key, slideCount: args.slides.length, preset: args.preset || 'glass-nodebench' };
  }
}
```

---

## Viewport Base CSS (Mandatory, Inlined in Every Presentation)

```css
/* === Reset === */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-snap-type: y mandatory; scroll-behavior: smooth; -webkit-font-smoothing: antialiased; }
body { overflow-x: hidden; }
img, svg { display: block; max-width: 100%; }

/* === Slide container === */
.slide {
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: var(--slide-padding, clamp(1rem, 4vw, 4rem));
  position: relative;
}

/* === Progress bar === */
.progress-bar {
  position: fixed;
  top: 0; left: 0;
  height: 3px;
  background: var(--accent, #d97757);
  z-index: 100;
  transition: width 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

/* === Nav dots === */
.nav-dots {
  position: fixed;
  right: clamp(8px, 2vw, 20px);
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 100;
}
.nav-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  border: 1px solid var(--text-secondary, rgba(255,255,255,0.3));
  background: transparent;
  cursor: pointer;
  transition: all 0.2s;
  padding: 0;
}
.nav-dot.active {
  background: var(--accent, #d97757);
  border-color: var(--accent, #d97757);
  transform: scale(1.3);
}

/* === Responsive scaling === */
@media (max-height: 700px) {
  :root {
    --title-size: clamp(1.5rem, 5vw, 3rem);
    --h2-size: clamp(1rem, 3vw, 2rem);
  }
}
@media (max-width: 600px) {
  .nav-dots { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1 !important; transform: none !important; transition: none !important; }
  .progress-bar { transition: none; }
}
```

---

## Quality Gates

### Agent-Side (before emission)
1. Slide count ≤ 12
2. Content density within limits per slide type
3. All font URLs resolve (Google Fonts / Fontshare)
4. No external fetch dependencies in HTML
5. `prefers-reduced-motion` support present
6. Title and at least 2 content slides exist

### Frontend-Side (on render)
1. iframe loads without console errors
2. First slide visible within 500ms
3. Navigation responds to keyboard within iframe focus
4. Progress bar updates on slide change
5. `postMessage` metadata received by parent

### Pre-Release (QA checklist addition)
- [ ] Presentation artifact opens from Chat inline "Open" button
- [ ] Presentation artifact opens from Report strip card
- [ ] Slide navigation (←→ keys) works in panel
- [ ] Progress bar and nav dots functional
- [ ] Code editor shows full HTML, live preview updates
- [ ] Animations play on slide enter (unless reduced-motion)
- [ ] Mobile viewport (375px): readable, no overflow
- [ ] All 10 presets render without errors

---

## File Inventory

| File | Purpose |
|------|---------|
| `public/proto/PRESENTATION_ENGINE.md` | This document — full production spec |
| `public/proto/home-v3.html` | Prototype with demo presentation artifact |
| `public/proto/HOME_V3_STATE_MATRIX.md` | QA tests including presentation tests |
| `src/lib/presentationEngine.ts` | (future) TypeScript engine for HTML generation |
| `server/routes/presentationArtifact.ts` | (future) Server-side generation endpoint |
| `convex/domains/artifacts/presentations.ts` | (future) Convex storage + queries |
| `packages/mcp-local/src/tools/presentationTools.ts` | (future) MCP tool: generate_presentation |
| `scripts/extract-pptx.py` | (adopt) PPTX extraction script |

---

## Prior Art & Attribution

- **Pattern:** Self-contained HTML presentation engine
- **Source:** [zarazhangrui/frontend-slides](https://github.com/zarazhangrui/frontend-slides)
- **Adopted:** 12 style presets (7 kept, 3 NodeBench-native added), viewport-base CSS,
  SlidePresentation class, animation patterns, content density rules
- **Adapted:** iframe sandbox integration, postMessage protocol, Convex persistence,
  MCP tool interface, agent auto-selection, split-pane reflow integration
- **NodeBench-original:** Glass NodeBench preset, Signal Dark preset, Investor Deck preset,
  artifact pipeline integration, content shaping heuristics, PPTX agent-assisted conversion
