---
name: deck-pages
description: >
  Constraints for producing presentation deck pages as HTML slide sequences that
  download as pixel-perfect PDFs via client-side html2canvas + jsPDF rendering.
  Use when creating or modifying any slide deck, pitch deck, presentation, or
  multi-slide document intended for PDF download. Also use when the output is a
  set of fixed-dimension landscape pages viewed on screen and exported as a PDF.
  Trigger phrases: "deck", "pitch deck", "slides", "presentation", "slide deck",
  "investor deck", "deck PDF", "download deck", or any request to create a
  multi-page slide-based document with a PDF download button.
---

# Deck Pages — Slide-Based PDF Documents

Constraints for producing HTML slide decks that render correctly on screen and export as pixel-perfect PDFs via client-side `html2canvas-pro` + `jsPDF`. Every rule exists because violating it caused a visible rendering problem in exported decks.

## How this differs from A4 print documents

A4 documents use the browser's native print pipeline (`@page` rules, the on-device `browser-pdf-save` tool). Deck pages bypass print entirely — each slide is rasterized to a canvas and composited into a PDF on the client. This means:

- Glassmorphism and `backdrop-filter` render correctly (no PNG fallbacks needed).
- Text in the PDF is raster, not vector — so the canvas `scale` factor matters for sharpness.
- The browser's `@page` margin boxes, `counter(page)`, and `orphans`/`widows` have no effect on the exported PDF. Page structure is controlled entirely by the slide DOM.

Print styles are still included as a fallback for users who Cmd+P, but they are not the primary export path.

## Rendering path

1. Build the HTML deck with fixed-dimension `.slide` containers.
2. Add a client-side download button that uses `html2canvas-pro` + `jsPDF`.
3. Verify each slide visually using `browser-screenshot` before declaring the deck complete.

## Slide dimensions

A4 landscape: `297mm` wide × `210mm` tall. Every slide uses identical dimensions.

```css
.slide {
  width: 297mm;
  height: 210mm;
  margin: 20px auto;
  position: relative;
  overflow: hidden;
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
}
```

The `overflow: hidden` is load-bearing — content that overflows a slide will be silently clipped in the PDF. If content doesn't fit, reduce font sizes or restructure the layout. Never let a slide scroll.

## Slide content container

Every slide's content lives inside a `.slide-content` div that provides consistent padding and flex layout:

```css
.slide-content {
  padding: 36px 48px;
  height: 100%;
  display: flex;
  flex-direction: column;
}
```

The outer `.slide` controls dimensions and background. The inner `.slide-content` controls content flow. Typical usage:

```html
<div class="slide bg-[#1a1c18]">
  <div class="slide-content justify-between relative z-10">
    <!-- slide content here -->
  </div>
</div>
```

Use `justify-between`, `justify-center`, or `gap-*` on `.slide-content` to distribute content vertically within the fixed height.

## Deck wrapper

The slides sit inside a wrapper that provides the grey background visible between slides on screen:

```css
.deck-wrapper {
  background: #e8e5e0;
  min-height: 100vh;
  padding: 20px 0;
}
```

## Client-side PDF generation

The download function dynamically imports `html2canvas-pro` and `jspdf`, iterates over every `.slide` element, rasterizes each to a canvas, and assembles them into a multi-page PDF.

```tsx
async function downloadAsPdf() {
  const [{ default: html2canvas }, { jsPDF }] = await Promise.all([
    import("html2canvas-pro"),
    import("jspdf"),
  ]);

  const slides = document.querySelectorAll(".slide");
  const pdf = new jsPDF({ orientation: "landscape", unit: "mm", format: "a4" });
  const pageW = 297;
  const pageH = 210;

  for (let i = 0; i < slides.length; i++) {
    const canvas = await html2canvas(slides[i] as HTMLElement, {
      scale: 2,
      useCORS: true,
      backgroundColor: null,
    });
    const imgData = canvas.toDataURL("image/jpeg", 0.92);
    if (i > 0) pdf.addPage();
    pdf.addImage(imgData, "JPEG", 0, 0, pageW, pageH);
  }

  pdf.save("deck.pdf");
}
```

**Critical settings:**

- `scale: 2` — renders at 2x resolution for sharp text on retina displays. Lower values produce blurry text. Higher values increase file size with diminishing returns.
- `useCORS: true` — required for any images loaded from external origins (Google Fonts icon sheets, CDN images). Without this, cross-origin images render as blank rectangles.
- `backgroundColor: null` — preserves the slide's own background rather than injecting a white fill behind it. Essential for dark slides.
- JPEG at `0.92` quality — balances file size against visual quality. PNG would be lossless but produces much larger PDFs.

**Dependencies:** Add `html2canvas-pro` and `jspdf` to `package.json`:

```json
{
  "dependencies": {
    "html2canvas-pro": "^2.0.2",
    "jspdf": "^4.2.1"
  }
}
```

Use `html2canvas-pro`, not `html2canvas` — the `-pro` fork has better support for modern CSS features (oklch colours, CSS variables, container queries).

## Download button

The download button sits in a fixed position overlay and shows a loading state during generation:

```tsx
const [generating, setGenerating] = useState(false);

async function handleDownload() {
  setGenerating(true);
  try {
    await downloadAsPdf();
  } finally {
    setGenerating(false);
  }
}
```

Place the button in a `fixed top-5 right-5 z-50` container. Mark it with `className="print-hidden"` so it doesn't appear in Cmd+P output. The button must be disabled during generation to prevent double-clicks queuing multiple exports.

## Dark slides and background colours

Dark slides (e.g. title slides, accent slides) use a dark `bg-*` on the `.slide` div. For the PDF export path (html2canvas), dark backgrounds render correctly with no special handling — `backgroundColor: null` preserves them.

For the Cmd+P fallback path, dark backgrounds require explicit print colour preservation:

```css
@media print {
  * {
    -webkit-print-color-adjust: exact !important;
    print-color-adjust: exact !important;
  }
}
```

## Glassmorphism on dark slides

Unlike A4 print documents, deck pages do **not** need PNG fallbacks for glassmorphism. The html2canvas rendering path captures `backdrop-filter: blur()` as it appears on screen, because it rasterizes the composited result.

Use glassmorphism freely on dark slides for stat cards, overlays, and frosted panels:

```html
<div class="bg-[rgba(250,250,248,0.06)] backdrop-blur-[16px]
            border border-[rgba(250,250,248,0.12)] rounded-md px-5 py-4">
  <!-- content -->
</div>
```

## Background images and gradients

For slides with background images (e.g. title slides), layer the image, gradient overlays, and content using absolute positioning:

```html
<div class="slide bg-[#1a1c18] relative overflow-hidden">
  <!-- Background image -->
  <div class="absolute inset-0 bg-center bg-cover bg-no-repeat"
       style="backgroundImage: url('/hero-bg.png'), linear-gradient(...)" />
  <!-- Gradient overlay for text readability -->
  <div class="absolute inset-0"
       style="background: linear-gradient(to right, rgba(26,28,24,0.92) 0%, ...)" />
  <!-- Content on top -->
  <div class="slide-content justify-between relative z-10">
    ...
  </div>
</div>
```

The background image should have a CSS gradient fallback so the slide still looks correct if the image fails to load. `html2canvas` will capture the gradient fallback automatically.

## Print styles (Cmd+P fallback)

Include print styles as a fallback for users who use the browser's native print dialog instead of the download button. These styles are not used by the html2canvas export path.

```css
@page {
  size: A4 landscape;
  margin: 0;
}

@media print {
  html { font-size: 16px; }
  body { background: white !important; }

  .deck-wrapper {
    background: white;
    padding: 0;
  }

  .print-hidden {
    display: none !important;
  }

  .slide {
    width: auto;
    height: 100vh;
    margin: 0;
    box-shadow: none;
    page-break-after: always;
  }

  .slide:last-child {
    page-break-after: auto;
  }

  * {
    -webkit-print-color-adjust: exact !important;
    print-color-adjust: exact !important;
  }
}
```

**Key resets:** The screen CSS uses `width: 297mm; margin: 20px auto; box-shadow: ...` for the card-on-grey-background look. The print CSS resets these to `width: auto; margin: 0; box-shadow: none;` — screen layout properties leak into print if not explicitly overridden.

## Slide structure patterns

### Section label

A horizontal rule + uppercase label to identify each slide's topic:

```tsx
function SlideLabel({ children, dark = false }) {
  return (
    <div className="flex items-center gap-3 mb-3">
      <span className={`w-8 h-px ${dark ? "bg-accent" : "bg-primary"}`} />
      <span className={`font-body text-[0.6rem] font-semibold tracking-[0.25em] uppercase
        ${dark ? "text-accent" : "text-primary"}`}>
        {children}
      </span>
    </div>
  );
}
```

### Standard content slide

Light background, section label, heading, body text, and a card grid:

```html
<div class="slide bg-bg">
  <div class="slide-content justify-center gap-8">
    <div>
      <SlideLabel>Section Title</SlideLabel>
      <h2 class="font-display text-[2rem]">Heading</h2>
      <p class="font-body text-[0.82rem] max-w-[80%]">Body text.</p>
    </div>
    <div class="grid grid-cols-2 gap-5">
      <!-- cards -->
    </div>
    <p class="font-body text-[0.68rem] text-text-tertiary pt-3
              border-t border-[rgba(124,140,114,0.12)]">
      Footer note.
    </p>
  </div>
</div>
```

### Title slide (dark, full-bleed)

Dark background with image overlay, hero text, stat strip, and attribution footer. See the background images section above for the layering pattern.

### Data table slide

Use an HTML `<table>` with tight font sizes (`text-[0.72rem]` for cells, `text-[0.6rem]` for headers). Highlight the "our row" with a subtle background tint so the reader's eye lands on it immediately.

## Typography scale for slides

Slides use a compressed typography scale that fits content within the fixed 210mm height:

| Element | Size | Weight |
|---------|------|--------|
| Hero title (h1) | `text-[3.8rem]` | normal |
| Slide heading (h2) | `text-[2rem]` | normal |
| Body paragraph | `text-[0.82rem]` | normal |
| Card description | `text-[0.68rem]` | normal |
| Section label | `text-[0.6rem]` | semibold, uppercase, tracked |
| Footer / attribution | `text-[0.62rem]` – `text-[0.7rem]` | normal |
| Stat number | `text-[1.5rem]` – `text-[1.8rem]` | semibold |
| Button text | `text-[0.72rem]` | semibold, uppercase, tracked |

These sizes are calibrated for 297mm × 210mm at `scale: 2` — they produce readable text in the exported PDF. Smaller sizes become illegible; larger sizes cause overflow.

## Verifying slides with on-device screenshots

Before declaring a deck complete, verify that every slide renders correctly by taking screenshots with `browser-screenshot`. This catches problems that are invisible in the dev server but appear in the PDF: clipped content, broken images, font loading failures, and layout overflow.

### Verification process

1. **Start a local server** — the deck must be served over HTTP. `file://` protocol blocks font loading and CORS resources. If Next.js, run `npm run dev` and wait for the ready signal before navigating.

2. **Navigate to the deck page** with `browser-navigate` and wait for fonts and images to load. Use `browser-wait-for` or `browser-evaluate` with `document.fonts.ready` to ensure custom fonts are loaded — otherwise screenshots capture system-font fallbacks that look correct in dev but produce the wrong PDF.

3. **Screenshot each slide individually** — call `browser-screenshot` with each `.slide` element's CSS selector, not a full-page screenshot. Full-page screenshots compress the entire deck into one image and hide per-slide problems. Read the slide selectors from `browser-snapshot` (or compute `.slide:nth-of-type(n)`), and capture each to its own `slide-<n>.png`.

4. **Verify each screenshot against these checks:**
   - Content fits within the slide bounds (no clipping at edges).
   - Text is readable (custom fonts loaded, not falling back to system fonts).
   - Background images and gradients render (no broken image icons).
   - Dark slides have correct dark backgrounds (not white).
   - Glassmorphism panels show the blur effect.
   - No unexpected scrollbars or overflow indicators.
   - Card grids and tables are evenly spaced.

5. **Test the PDF download** — `browser-click` the download button, then open the generated PDF and confirm the page count matches the slide count and no pages are blank.

### Common rendering failures caught by screenshots

| Symptom | Cause | Fix |
|---------|-------|-----|
| White rectangle where image should be | CORS blocked, `useCORS: true` missing | Add `useCORS: true` to html2canvas options |
| Blurry text in PDF | `scale` too low | Use `scale: 2` or higher |
| Content clipped at slide bottom | Too much content for 210mm height | Reduce font sizes, remove content, or split across two slides |
| System font instead of custom font | Font not loaded before screenshot | Wait for `document.fonts.ready` before capturing |
| Dark slide renders white | `backgroundColor` not `null` in html2canvas | Set `backgroundColor: null` |
| Download button visible in PDF | Button inside a `.slide` container | Keep button outside `.slide` divs, in a fixed overlay |
| Glassmorphism appears flat | Using the print pipeline instead of html2canvas | Use the download button, not Cmd+P |
| Text re-flows between screen and PDF | Relative font units inside fixed-size container | Use explicit `text-[Xrem]` sizes, not `text-lg` style shorthand |

## Layout component structure (Next.js)

The deck uses a dedicated layout that injects the wrapper and print styles:

```tsx
// app/deck/layout.tsx
export default function DeckLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="deck-wrapper">
      <style>{`
        .deck-wrapper { background: #e8e5e0; min-height: 100vh; padding: 20px 0; }
        .slide { width: 297mm; height: 210mm; margin: 20px auto; position: relative; overflow: hidden; box-shadow: 0 4px 24px rgba(0,0,0,0.12); }
        .slide:first-of-type { margin-top: 20px; }
        .slide-content { padding: 36px 48px; height: 100%; display: flex; flex-direction: column; }

        @page { size: A4 landscape; margin: 0; }
        @media print {
          html { font-size: 16px; }
          body { background: white !important; }
          .deck-wrapper { background: white; padding: 0; }
          .print-hidden { display: none !important; }
          .slide { width: auto; height: 100vh; margin: 0; box-shadow: none; page-break-after: always; }
          .slide:last-child { page-break-after: auto; }
          * { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
        }
      `}</style>
      {children}
    </div>
  );
}
```

The page component (`app/deck/page.tsx`) is a `"use client"` component containing the slides, the download function, and the download button. Keep the layout as a server component for metadata; the "use client" directive belongs on the page, not the layout.

## Reference implementation

A complete working deck lives at `/Users/neo/capacity-derivatives/app/deck/` — read `layout.tsx` and `page.tsx` there when building a new deck. The reference shows eight slides covering title, problem, market, solution, differentiation, roadmap, competitive landscape, and team/ask — the standard pitch-deck arc.

## Checklist before shipping

- [ ] Every slide is exactly `297mm × 210mm` with `overflow: hidden`.
- [ ] Content fits within each slide — no clipping at bottom or right edges.
- [ ] `html2canvas-pro` and `jspdf` are in `package.json` dependencies.
- [ ] Download button uses loading state and is disabled during generation.
- [ ] Download button is outside `.slide` containers (in a fixed overlay).
- [ ] Dark slides use `backgroundColor: null` in html2canvas config.
- [ ] Print fallback styles included (`@page`, `@media print`).
- [ ] `browser-screenshot` taken of every slide — content verified.
- [ ] PDF download tested — correct number of pages, no blank pages.
- [ ] Fonts loaded before any screenshot or export (check `document.fonts.ready`).
