# Notion Media Generator

> SKILL: notion-media-generator

- Skill: `ralf-claude-skills/notion-media-generator` (Agent Skill, multi-file: 12 files)
- Install (CLI): `npx skillmds@latest add ralf-claude-skills/notion-media-generator`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ralf-claude-skills/notion-media-generator/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: ralf-claude-skills (https://skillmd.com/u/ralf-claude-skills)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/ralf-claude-skills/notion-media-generator

---

# SKILL: notion-media-generator

## TRIGGER PHRASES
`/media`, `generate media`, `create carousel`, `make instagram slides`, `make whop images`,
`create media for my template`, `generate promo images`, `build media kit`, `marketing images`,
or any Notion URL provided with intent to market the template.

---

## WHAT THIS SKILL PRODUCES
- **5 Instagram carousel slides** — 1080×1350px logical canvas, rendered at device_scale_factor=3 → 3240×4050px actual PNG
- **5 Whop listing images** — 1920×1080px logical canvas, rendered at device_scale_factor=3 → 5760×3240px actual PNG

Total: 10 high-resolution PNGs, all written to a single output folder: `D:\Claude\.claude\automations\notion\output`. Rendered via Playwright chromium. Pure HTML/CSS — no image files, no external assets.

---

## PHASE 0 — READ THE NOTION PAGE COMPLETELY

Do not generate a single word of copy until this phase is complete.

```
notion-fetch the URL provided.
Also search for child databases, subpages, and linked views.
Read every block — don't skim.
```

### REQUIRED EXTRACTIONS — record ALL of these explicitly before Phase 1

**Template identity**
- Exact name (with emoji if present in the page icon or title)
- Price (exact: `$X`)
- Tagline or one-liner (pull verbatim from page if present)
- Category (content, productivity, finance, fitness, etc.)

**Structure — from the actual Notion page**
- Every database name
- Every field/property name and type (Title, Select, Date, URL, Text, etc.)
- Every view name and what it filters or sorts on
- Status options (e.g. Idea / In Progress / Posted / Scrapped) — exact labels

**Sample data — read actual rows**
- Pull 4–6 verbatim entries from the template's real sample rows
- For each row: record its Title, key status field, and 1-2 other notable fields
- These rows go directly into the mock-database images — do NOT invent fictional data

**Problem & audience**
- The exact problem the template solves (their words, not marketing language)
- Who the buyer is: role, hobby, niche, platform they post on / use
- What they were doing before (the broken system: Notes app, memory, spreadsheet, DMs)
- The one action the template makes effortless that was painful before

**Differentiators**
- Why this vs a free template?
- Why this vs a spreadsheet or Notes app?
- The "power move" — the single habit or check the template enables

**Price anchor**
- What does one saved unit of time/money/mistake avoided cost vs the template price?
- (e.g. "one skipped duplicate post pays for this 20× over")

**CTA mechanics**
- How does the buyer get it? (DM keyword / link in bio / direct URL)
- DM keyword if applicable (e.g. "NOTEBOOK")

---

## PHASE 1 — AUDIENCE INTELLIGENCE

Read this section fully. It determines copy tone and slide structure.

### Instagram — Notion template audience
**Demographics:** Hobby creators, students, side-hustlers, productivity seekers, 18–34. Follows @notionhq, @aliabdaal, #notionsetup, #productivitytips, #studygram. Buys Notion templates because they want systems, not because they love Notion.

**What stops the scroll on slide 1:**
Not aspiration. Not product features. Their private embarrassment. The moment they lost something, forgot something, did something twice. Specific failure > abstract pain.
- ✅ "you already posted that." ← specific, accusatory, true
- ✅ "that idea you had in the shower is gone." ← moment they recognize
- ❌ "tired of disorganized content?" ← too vague, sounds like an ad
- ❌ "Introducing My Content Notebook" ← ignored instantly

**IG carousel algorithm signals:**
1. **Save rate drives reach** — carousels that teach/show something get saved. Every slide must feel "I'll need this later"
2. **Swipe-through rate** — slide 2 must be earned by slide 1. Not with a cliffhanger gimmick — with genuine continuation of the story
3. **DM keyword in caption** drives comments → comments signal to algorithm → more reach
4. **Lowercase tone** = creator peer, not brand. This audience trusts lowercase.
5. **Specific numbers** ("21st", "$9", "3 rows", "10 seconds") are more believable than vague claims

**Words that kill IG carousels for this audience:**
`introducing`, `features include`, `learn more`, `click the link`, `this template will help you`, `synergy`, `streamline`, any ALL CAPS body text, any corporate phrasing.

**Carousel narrative arc that performs:**
```
Slide 1: Recognition hit — their secret failure, named precisely
Slide 2: The story — lived timeline of the exact moment the failure happened
Slide 3: The reveal — "here's what the actual system looks like" (real data visible)
Slide 4: The objection killed — what the skeptic says + why they're wrong
Slide 5: The price reveal + CTA — justified, never apologized for
```

### Whop Discover — Notion template audience
**Demographics:** Active buyers. They searched "notion template" or browsed the discover page. Not casually scrolling — actively evaluating. 70% have already bought at least one Notion template. They're price-aware ($5–$25 range is normal) and scan-read listings.

**How buyers scan a Whop listing:**
1. Cover thumbnail at ~300px in the grid → if headline isn't readable, they don't click
2. After clicking → gallery images viewed left-to-right in 3–5 seconds each
3. Description skim → price check → buy or bounce

**What converts each gallery slot:**
- **Image 1 (cover):** Single benefit claim + product name. Must be legible at 300px. No clutter.
- **Image 2 (what's inside):** Specific, named features with descriptions of why they matter — not a feature list
- **Image 3 (the actual product):** Real database rows/UI mock. This is the highest-converting image. Buyers fear buying a hollow template. Show them it's real and pre-filled.
- **Image 4 (how it works):** 3-step workflow. Kills the "is this complicated" fear.
- **Image 5 (value anchor):** Price justification. Specifics. What they stop doing/losing. Kills the "$9 is a lot for a template" hesitation.

**Whop copy rules:**
- Amber eyebrow label → signals section type at a glance
- Pill tag top-right → confirms category (e.g. "notion template")
- Binding dots → consistent brand identity across all 5 images
- Price badge bottom-right → visible on every image, normalizes the price by repetition

---

## PHASE 2 — COPY PLANNING

Write ALL 10 slide/image copy blocks BEFORE generating any HTML. Print them clearly. Do not proceed to Phase 3 until copy is drafted.

### IG CAROUSEL — 5 SLIDES

**SLIDE 1 — HOOK**
- 1–2 lines. Lowercase. Weight: light/thin.
- Names the exact failure state. Not their aspiration.
- No product name. No solution hint. Just the mirror.
- Max 12 words on a single line.
- Format examples:
  - `you forgot you already posted that.`
  - `that idea you had on tuesday is gone.`
  - `you followed up three weeks late.`

**SLIDE 2 — LIVED STORY**
Format: 3–5 staccato lines narrating the exact scenario, then 2 smaller lines reframing it.
```
[specific event, present tense]
[next beat, specific date or number]
[consequence]
[another beat]

[reframe line 1 — dim/smaller]
[reframe line 2 — dim/smaller]
```
Example:
```
you had the idea at 11pm.
you typed it into notes.
it's been six days.
you haven't opened notes once.

that's not you being bad at this.
that's not having a system.
```

**SLIDE 3 — PRODUCT GLIMPSE**
- Small label at top: `the actual [database name]` — lowercase, dim
- Table with 3 real rows from the template's sample data:
  - Columns: Title | Key Field | Value/Status
  - Dividers: thin 1px lines
  - Bold Title, normal weight for other columns
- 2 lines below table (dim): the single sentence that explains why this table solves it
- Keep it simple — the "wait, that's it?" reaction is the goal

**SLIDE 4 — OBJECTION CRUSHER**
- Opens with skeptic's objection in quotes — small, dim, italicized feel
- Main body: respond in their voice, not defensively
- Differentiator: 1-2 lines about what's different about this specific template
- Small line: proof / use-before-buy moment

**SLIDE 5 — PRICE + CTA**
Format:
```
[value anchor — 1 dim line]

$[PRICE]

[once. yours forever. no subscription.]  ← dim, small

[TEMPLATE NAME]                           ← medium, line break
——

[template description, 2-3 lines]         ← dim

$X · one time · [CTA like "link in bio"] ← dim/small
[DM KEYWORD or action in bold]           ← bold, white
```

---

### WHOP — 5 IMAGES

**IMAGE 1 — COVER**
- Amber eyebrow: `[TEMPLATE NAME IN CAPS]`
- Tag pill: `notion template`
- Hero headline (2 lines, bold, 90–104px): captures core benefit in 6–10 words
- Sub (1–2 lines, dim): who it's for + what overhead they eliminate
- Bottom left: `[emoji] [platform list]` e.g. `📓 For YouTube · Instagram · TikTok · Blog`
- Bottom right price badge: `$X · lifetime access`

**IMAGE 2 — WHAT'S INSIDE**
- Eyebrow: `WHAT'S INSIDE`
- Tag: `[N] database · [N] views` (exact counts from the template)
- Headline: short, specific claim about what the user gets
- 3 feature cards (one per main view/database):
  - Emoji matching the view icon
  - View name as card title
  - 2–3 sentence description of WHY it matters (benefit, not feature definition)
- Brand + price badge

**IMAGE 3 — THE ACTUAL DATABASE**
- Eyebrow: `THE ACTUAL DATABASE`
- Tag: exact database name from Notion
- Headline: `No blank page. [N] sample [items] already inside.`
- Table with 4 real rows:
  - Columns derived from actual database properties
  - Status column uses colored pills matching the real status options
  - Pill colors: use the token values from Phase 3
- Brand + price badge

**IMAGE 4 — HOW IT WORKS**
- Eyebrow: `HOW IT WORKS`
- Tag: `[X] min setup`
- Headline: single sentence making the workflow sound effortless
- 3 steps with `01` / `02` / `03` in amber mono, `→` arrows between columns:
  - Each step: emoji + step name (bold) + 2-line description
- Brand + price badge

**IMAGE 5 — WHY $[PRICE]**
- Eyebrow: `WHY $[PRICE]`
- Tag: `one time · no subscription`
- Headline: the value equation in one bold sentence
- 3 value bullets using `→` (not bullet points or dashes):
  - Each: specific thing the buyer stops doing/losing
  - Grounded in extracted data (e.g. "stop searching Notes app for old ideas")
- 1 objection crusher line (the "you could just use..." rebuttal)
- Brand + price badge

---

## PHASE 3 — EXACT DESIGN SYSTEM

Copy these tokens exactly. Do not adjust colors, sizes, or layout proportions without explicit instruction.

### COLOR TOKENS
```
Background:       #141413   (not black — has warm brown undertone)
Text primary:     #F0F0EC   (warm off-white, NOT pure white)
Text body:        #A2A098   (medium warm gray — secondary copy)
Text dim:         #6F6E69   (muted gray — labels, mono tags, small print)
Card bg:          #1B1B1A   (slightly lighter than bg — Whop cards only)
Card border:      #2C2C2A   (subtle separation)
Table divider:    #343330   (row separator lines, thin 1px)
Accent amber:     #E7B24C   (WHOP ONLY — eyebrow labels, price badge bg, step numbers)
Binding dot fill: #3A3A38
Binding dot bdr:  #56564F

Status pill colors (IMAGE 3 only):
  Idea:      bg #3d3620  text #E7B24C
  In Progress: bg #1f2c3d  text #7fb0e0
  Posted:    bg #1f3320  text #7fce8a
  Scrapped:  bg #3a2323  text #e08a8a
  (adjust status names/colors to match the template's actual status options)
```

### TYPOGRAPHY
```
Primary:  'Liberation Sans', Arial, sans-serif   ← available in Playwright env
Mono:     'Liberation Mono', monospace           ← used for tags, labels, price badge
```

**Never use Google Fonts. Never use @import. Network is blocked in render env.**

### IG CAROUSEL SPECS (1080×1350 canvas, device_scale_factor=3)

```
Background:       #141413 full bleed
Left margin:      83px
Right margin:     83px
Max text width:   ~840px
Vertical center:  flex column, justify-content: center (slight upward bias with padding-bottom ~50px)

NO amber anywhere on IG slides
NO binding dots on IG slides
NO feature cards or decorative borders on IG slides
NO background textures, gradients, or shadows
```

**Slide-specific font sizes (at 1080px logical canvas):**

| Slide | Element | Size | Weight | Color |
|---|---|---|---|---|
| 1 | Hook text | 58–62px | 300 | #F0F0EC |
| 2 | Story lines (primary) | 42–46px | 300 | #F0F0EC |
| 2 | Reframe lines | 34–36px | 300 | #A2A098 |
| 2 | Gap between blocks | 40px | — | — |
| 3 | Table label | 22px | 400 | #6F6E69 |
| 3 | Table title (row) | 32px | 700 | #F0F0EC |
| 3 | Table other cols | 32px | 400 | #A2A098 |
| 3 | Footer insight | 28–30px | 300 | #A2A098 |
| 4 | Objection quote | 22px | 300 | #6F6E69 |
| 4 | Response body | 42–46px | 300 | #F0F0EC |
| 4 | Differentiator | 30–32px | 300 | #A2A098 |
| 5 | Value anchor | 32px | 300 | #A2A098 |
| 5 | Price (the number) | 150–170px | 700 | #F0F0EC |
| 5 | Terms line | 28px | 300 | #A2A098 |
| 5 | Product name | 44–48px | 700 | #F0F0EC |
| 5 | Divider rule | width 60px, height 3px | — | #3A3A38 |
| 5 | Description | 30px | 300 | #A2A098 |
| 5 | CTA / DM keyword | 36–38px | 700 | #F0F0EC |

Line heights: story/body = 1.3–1.4. Hook = 1.15–1.2.

### WHOP SPECS (1920×1080 canvas, device_scale_factor=3)

```
Binding strip:    position:absolute; left:64px; top:0; bottom:0; width:10px
Binding dot:      11×11px, border-radius:50%, bg:#3A3A38, border:1px solid #56564F
Dot count:        12 dots, justify-content:space-evenly
Content frame:    left:150px, right:110px, top:90px, bottom:90px
                  display:flex; flex-direction:column; justify-content:space-between
```

**Element sizing:**

| Element | Size | Weight | Color | Notes |
|---|---|---|---|---|
| Eyebrow label | 26px | 700 | #E7B24C | letter-spacing:4px, text-transform:uppercase |
| Tag pill | 22px | 400 | #6F6E69 | mono font, border 1px #3A3A38, radius:100px, padding:8px 18px |
| Hero headline | 90–104px | 700 | #F0F0EC | letter-spacing:-2px, line-height:1.05 |
| Sub text | 36–40px | 400 | #A2A098 | line-height:1.5, max-width:960px |
| Card bg | — | — | #1B1B1A | border:1px solid #2C2C2A, radius:16px, padding:36px 30px |
| Card emoji | 44px | — | — | |
| Card title | 30px | 700 | #F0F0EC | |
| Card desc | 22px | 400 | #A2A098 | line-height:1.5 |
| Step number | 24px | 400 | #E7B24C | mono font |
| Step title | 32px | 700 | #F0F0EC | |
| Step desc | 22px | 400 | #A2A098 | line-height:1.5 |
| Arrow (→) | 36px | 400 | #3A3A38 | |
| Table header | 18px | 400 | #6F6E69 | mono, letter-spacing:1px, UPPERCASE |
| Table row | row padding:26px 34px | border-bottom:1px solid #262624 | |
| Table title cell | 26px | 700 | #F0F0EC | |
| Table other cells | 22px | 400 | #A2A098 | |
| Status pill | 20px | 700 | per status | radius:100px, padding:6px 16px |
| Brand watermark | 26px | 400 | #8f8d86 | bottom-left |
| Price badge | 28–32px | 700 | bg:#E7B24C, text:#141413 | mono, padding:14px 26px, radius:8px |
| Value bullets (→) | 28–32px | 400 | #A2A098 | line-height:1.6 |

---

## PHASE 4 — HTML GENERATION

### File naming and directory

**There is exactly ONE working directory for this skill — HTML sources and final PNGs both live here. Never create `media_gen`, `outputs`, or `media_kit` subfolders.**

```
mkdir -p "D:\Claude\.claude\automations\notion\output"
```

Files (HTML build files; PNGs render to the same folder, same basename, `.png` extension):
```
D:\Claude\.claude\automations\notion\output\ig_1_hook.html
D:\Claude\.claude\automations\notion\output\ig_2_story.html
D:\Claude\.claude\automations\notion\output\ig_3_product.html
D:\Claude\.claude\automations\notion\output\ig_4_objection.html
D:\Claude\.claude\automations\notion\output\ig_5_cta.html
D:\Claude\.claude\automations\notion\output\whop_1_cover.html
D:\Claude\.claude\automations\notion\output\whop_2_inside.html
D:\Claude\.claude\automations\notion\output\whop_3_database.html
D:\Claude\.claude\automations\notion\output\whop_4_howto.html
D:\Claude\.claude\automations\notion\output\whop_5_value.html
```

No copy step. No second folder. If any script in this skill references `media_gen`, `outputs`, or `media_kit`, that is a bug — replace it with `output`.

### CROSS-CONTAMINATION RULE (hard rule — this is the #1 cause of visibly broken renders)

Each HTML file's `<body>` contains **only that slide's own copy** — nothing else. Never leave in, paste, or reuse a headline/paragraph/line that belongs to a different slide. This exact mistake is what produces the broken renders seen previously: slide 1's hook text rendering stacked on top of slide 2's story lines, or a Whop headline overlapping its own tag pill/price badge because two content blocks occupy the same space.

Before rendering, for every HTML file, search it for the unique opening words of every OTHER slide's Phase-2 copy block. If any match is found, delete the stray block before proceeding — it does not belong there.

### IG BASE TEMPLATE
```html
<!DOCTYPE html><html><head><meta charset="utf-8">
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html,body {
    width:1080px; height:1350px;
    background:#141413; overflow:hidden;
    font-family:'Liberation Sans', Arial, sans-serif;
    color:#F0F0EC;
  }
  .wrap {
    position:absolute; left:83px; right:83px;
    top:0; bottom:50px;           /* slight upward bias */
    display:flex; flex-direction:column; justify-content:center;
  }
</style></head>
<body><div class="wrap">
  <!-- SLIDE CONTENT HERE -->
</div></body></html>
```

### WHOP BASE TEMPLATE
```html
<!DOCTYPE html><html><head><meta charset="utf-8">
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html,body {
    width:1920px; height:1080px;
    background:#141413; overflow:hidden;
    font-family:'Liberation Sans', Arial, sans-serif;
    color:#F0F0EC;
  }
  .binding {
    position:absolute; left:64px; top:0; bottom:0; width:10px;
    display:flex; flex-direction:column; justify-content:space-evenly; align-items:center;
  }
  .dot { width:11px; height:11px; border-radius:50%; background:#3A3A38; border:1px solid #56564f; }
  .frame {
    position:absolute; left:150px; right:110px; top:90px; bottom:90px;
    display:flex; flex-direction:column;
  }
  .top-row { display:flex; justify-content:space-between; align-items:flex-start; flex:0 0 auto; }
  .eyebrow { font-size:26px; letter-spacing:4px; text-transform:uppercase; color:#E7B24C; font-weight:700; }
  .tag { font-family:'Liberation Mono',monospace; font-size:22px; color:#6f6e69;
         border:1px solid #3A3A38; padding:8px 18px; border-radius:100px; white-space:nowrap; }
  .mid {
    flex:1 1 auto; display:flex; flex-direction:column; justify-content:center;
    gap:36px; min-height:0; overflow:hidden;
  }
  .bottom-row { display:flex; justify-content:space-between; align-items:flex-end; flex:0 0 auto; }
  .brand { font-size:26px; color:#8f8d86; }
  .price-badge { font-family:'Liberation Mono',monospace; font-size:28px; color:#141413;
                 background:#E7B24C; padding:14px 26px; border-radius:8px; font-weight:700; white-space:nowrap; }
</style></head>
<body>
  <div class="binding">
    <span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="dot"></span>
    <span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="dot"></span>
    <span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="dot"></span>
  </div>
  <div class="frame">
    <div class="top-row">
      <div class="eyebrow">EYEBROW</div>
      <div class="tag">tag text</div>
    </div>
    <div class="mid">
      <!-- MID CONTENT GOES HERE, AND ONLY HERE.
           This is the ONLY place headline/cards/table/steps are placed.
           Never position headline/body text with position:absolute — it will
           overlap .top-row or .bottom-row. Everything flows inside .mid. -->
    </div>
    <div class="bottom-row">
      <div class="brand">[emoji] Template Name</div>
      <div class="price-badge">$X · lifetime access</div>
    </div>
  </div>
</body></html>
```

**Why this structure matters:** the frame no longer uses `justify-content:space-between` across three flex children of wildly different content length. Only `.mid` stretches (`flex:1`), and it centers its own children vertically with a fixed `gap`. This is what was broken in the previous render: a bare `space-between` on `.frame` spread `.top-row`, the mid content, and `.bottom-row` across the full 900px frame height with equal-feeling gaps regardless of how much content was in the middle — producing huge dead space on short slides (Image 3, 4, 5-style layouts) and, when a headline was instead positioned with `position:absolute`, direct overlap with the eyebrow/tag/price badge.

### IG SLIDE 3 — TABLE PATTERN
```html
<div style="font-size:22px;color:#6F6E69;margin-bottom:28px;">the actual [database name]</div>
<div style="border-top:1px solid #343330;">
  <!-- Row template: -->
  <div style="display:grid;grid-template-columns:2fr 1fr 1fr;align-items:center;
              padding:22px 0;border-bottom:1px solid #343330;">
    <span style="font-size:32px;font-weight:700;color:#F0F0EC;">[Title]</span>
    <span style="font-size:30px;color:#A2A098;">[Key Field]</span>
    <span style="font-size:30px;color:#A2A098;text-align:right;">[Value]</span>
  </div>
  <!-- repeat for each row -->
</div>
<div style="margin-top:36px;font-size:28px;color:#A2A098;line-height:1.4;">
  [insight line 1]<br>[insight line 2]
</div>
```

### WHOP IMAGE 3 — TABLE PATTERN

The table sits inside `.mid` as its ONLY child (mid is `display:flex; flex-direction:column; justify-content:center`) — the table itself must be one single block element (`display:block`, not a flex item spread by a parent `space-between`). Header and every row are children **inside that one block**, stacked by normal document flow, each with its own `border-bottom` — never let header/rows become siblings of `.top-row`/`.bottom-row`, and never let them be `justify-content:space-between` children of `.frame` directly. That mistake is what produced the previous bug: a big empty gap between the header row and the first data row, and the data rows floating far down the canvas.

```html
<div class="mid">
  <div style="background:#1B1B1A;border:1px solid #2C2C2A;border-radius:16px;overflow:hidden;">
    <!-- header row -->
    <div style="display:grid;grid-template-columns:[match real columns];
                padding:20px 34px;background:#202020;
                font-family:'Liberation Mono',monospace;font-size:18px;
                letter-spacing:1px;color:#6F6E69;text-transform:uppercase;">
      <span>TITLE</span><span>[FIELD]</span><span>STATUS</span><span>[FIELD]</span>
    </div>
    <!-- data rows — 4 rows from real sample data, stacked with NO gap between them -->
    <div style="display:grid;grid-template-columns:[same];align-items:center;
                padding:24px 34px;border-bottom:1px solid #262624;">
      <span style="font-size:24px;font-weight:700;color:#F0F0EC;">[Row Title]</span>
      <span style="font-size:22px;color:#A2A098;">[Field Value]</span>
      <span>
        <span style="display:inline-block;font-size:20px;font-weight:700;
                     padding:6px 16px;border-radius:100px;
                     background:[pill-bg];color:[pill-text];">[Status] [emoji]</span>
      </span>
      <span style="font-size:22px;color:#A2A098;">[Field Value]</span>
    </div>
    <!-- repeat for rows 2-4, last row has border-bottom:none -->
  </div>
</div>
```

### WHOP IMAGE 2 — FEATURE CARD GRID PATTERN

Every card MUST include emoji + title + description. A card with only a title (no description) is a render failure — the previous run produced exactly this by leaving descriptions out or clipping them with an undersized fixed height. Cards use `min-height`, never a fixed `height`, so description text is never silently clipped.

```html
<div class="mid" style="justify-content:center;">
  <div style="font-size:44px;font-weight:700;color:#F0F0EC;line-height:1.15;margin-bottom:44px;max-width:1400px;">
    [headline]
  </div>
  <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:28px;">
    <div style="background:#1B1B1A;border:1px solid #2C2C2A;border-radius:16px;padding:36px 30px;min-height:260px;">
      <div style="font-size:44px;margin-bottom:16px;">[emoji]</div>
      <div style="font-size:30px;font-weight:700;color:#F0F0EC;margin-bottom:14px;">[view name]</div>
      <div style="font-size:22px;color:#A2A098;line-height:1.5;">[2-3 sentence description — REQUIRED, never omit]</div>
    </div>
    <!-- repeat for card 2, card 3 -->
  </div>
</div>
```

### WHOP IMAGE 4 — STEPS PATTERN

Same rule: each step needs number + emoji + title + 2-line description, all present, laid out inside `.mid`, never spread by an outer `space-between`.

```html
<div class="mid" style="flex-direction:row;justify-content:space-between;align-items:flex-start;">
  <div style="max-width:400px;">
    <div style="font-family:'Liberation Mono',monospace;font-size:24px;color:#E7B24C;margin-bottom:12px;">01</div>
    <div style="font-size:32px;font-weight:700;color:#F0F0EC;margin-bottom:14px;">[emoji] [step name]</div>
    <div style="font-size:22px;color:#A2A098;line-height:1.5;">[line 1]<br>[line 2]</div>
  </div>
  <div style="font-size:36px;color:#3A3A38;padding-top:40px;">→</div>
  <!-- repeat step block + arrow for steps 2 and 3 (no arrow after step 3) -->
</div>
```

---

## PHASE 5 — RENDERING

HTML source and PNG output live in the same single folder — `OUTPUT_DIR` below. There is no separate build directory.

```python
from playwright.sync_api import sync_playwright
import os

OUTPUT_DIR = r"D:\Claude\.claude\automations\notion\output"
os.makedirs(OUTPUT_DIR, exist_ok=True)

jobs = [
    # (html_file, output_png, canvas_width, canvas_height)
    ("ig_1_hook.html",       "ig_1_hook.png",       1080, 1350),
    ("ig_2_story.html",      "ig_2_story.png",       1080, 1350),
    ("ig_3_product.html",    "ig_3_product.png",     1080, 1350),
    ("ig_4_objection.html",  "ig_4_objection.png",   1080, 1350),
    ("ig_5_cta.html",        "ig_5_cta.png",         1080, 1350),
    ("whop_1_cover.html",    "whop_1_cover.png",     1920, 1080),
    ("whop_2_inside.html",   "whop_2_inside.png",    1920, 1080),
    ("whop_3_database.html", "whop_3_database.png",  1920, 1080),
    ("whop_4_howto.html",    "whop_4_howto.png",     1920, 1080),
    ("whop_5_value.html",    "whop_5_value.png",     1920, 1080),
]

with sync_playwright() as p:
    browser = p.chromium.launch()
    for html_name, png_name, w, h in jobs:
        page = browser.new_page(
            viewport={"width": w, "height": h},
            device_scale_factor=3   # 3, not 2 — see note below. Higher DPI = crisper text/edges.
        )
        page.goto("file:///" + os.path.join(OUTPUT_DIR, html_name).replace("\\", "/"))
        page.wait_for_timeout(150)  # let webfont/layout settle before capture
        page.screenshot(path=os.path.join(OUTPUT_DIR, png_name))
        page.close()
        print(f"rendered: {png_name}")
    browser.close()

print("All done.")
```

**device_scale_factor=3, not 2:** the previous output looked visibly soft/blurry compared to the examples. Rendering at 3x internal resolution (e.g. the IG canvas rasterizes at 3240×4050 before final delivery) produces noticeably crisper text edges and pill borders, matching the sharpness of `examples/`. This is the main lever for the "quality has degraded" complaint — always render at 3x, never 1x, never 2x.

---

## PHASE 6 — QUALITY CHECKS

After rendering, `view` each PNG before proceeding. Fix-then-re-render for any failure.

### IG slide checks
- [ ] Left edge text ≥83px from canvas left
- [ ] No text touches or clips the right edge (max-width enforced)
- [ ] Nothing bleeds below 1350px
- [ ] No amber color anywhere (#E7B24C must NOT appear on any IG slide)
- [ ] No binding dots on IG slides
- [ ] Slide 1 headline reads in under 2 seconds — if longer, shorten it
- [ ] Slide 3 table rows show REAL sample data (not placeholder lorem ipsum)
- [ ] Slide 5 price number is large and dominant — the biggest element on the slide

### Whop image checks
- [ ] Binding dots visible on left edge
- [ ] Amber on eyebrow AND price badge
- [ ] Headline legible when mentally squinted to 300px (thumbnail test)
- [ ] Image 3 table uses REAL property names from the template — not invented columns
- [ ] Image 3 status pills match the EXACT status option names from the template
- [ ] Price badge shows the CORRECT extracted price
- [ ] All 5 images share the same visual grammar (binding, eyebrow, price badge, brand)

### Overlap / contamination / whitespace (the three failures seen previously)
- [ ] No two text blocks visually overlap anywhere in the image — eyeball every corner, especially top-row vs mid-content and mid-content vs bottom-row
- [ ] Zero cross-contamination: no slide's HTML contains another slide's headline/story/copy (re-run the grep check from Phase 4 if unsure)
- [ ] No single unexplained gap taller than ~15% of canvas height between two content blocks — if present, the layout is using `space-between` where it should use a centered `.mid` wrapper; fix per Phase 4 pattern
- [ ] Every feature card shows emoji + title + a non-empty description — a title-only card is a failure
- [ ] Table header sits directly above row 1 with no gap larger than the row padding itself

### Both
- [ ] Output PNG file size > 150KB (too small = render failure or under-rendered/blank canvas)
- [ ] Background pixel sampled from corner = rgb(20, 20, 19) — not pure black
- [ ] device_scale_factor=3 used in all renders — text/edges must look crisp, not soft, when zoomed in
- [ ] All 10 files exist in `D:\Claude\.claude\automations\notion\output` and nowhere else

---

## PHASE 7 — OUTPUT

No copy step. The PNGs already landed in `D:\Claude\.claude\automations\notion\output` in Phase 5 — that is the final delivery location. Just enumerate them:

```python
import os

OUTPUT_DIR = r"D:\Claude\.claude\automations\notion\output"
files = [
    "ig_1_hook.png", "ig_2_story.png", "ig_3_product.png",
    "ig_4_objection.png", "ig_5_cta.png",
    "whop_1_cover.png", "whop_2_inside.png", "whop_3_database.png",
    "whop_4_howto.png", "whop_5_value.png",
]
paths = [os.path.join(OUTPUT_DIR, f) for f in files]
for p in paths:
    assert os.path.exists(p) and os.path.getsize(p) > 150_000, f"missing or too small: {p}"
print("All 10 verified in", OUTPUT_DIR)
```

Call `present_files` with all 10 paths (all under `D:\Claude\.claude\automations\notion\output`).

Then print this usage guide:

---
**Instagram carousel — post order:**
Slides 1→5. Slide 1 is what shows in grid. Mirror slide 1's hook in your caption first line. Add DM keyword at end of caption AND as first comment (boosts reach).

**Whop listing — upload order:**
`whop_1_cover.png` → hero thumbnail (shown in discover grid). `whop_2` through `whop_5` → gallery in order. Whop allows 5 listing images maximum.

---

## ANTI-PATTERNS

### Copy — never do these
- Slide 1 must NOT name the product
- Never open with "introducing"
- Never use "features include:" anywhere in any image
- Never write "[Name] is a Notion template that..."
- Never apologize for the price — state it, justify it, move on
- Never exceed 12 words on an IG hook (slide 1)
- Never put the price before slide 4 in the IG carousel
- IG slides must be ALL LOWERCASE — no shouted words, no sentence-case titles

### Design — never do these
- Background = `#141413` not `#000000`
- Text primary = `#F0F0EC` not `#ffffff`
- Amber `#E7B24C` is WHOP-ONLY — never use in IG slides
- Binding dots are WHOP-ONLY — never add to IG slides
- No Google Fonts — network is blocked during render
- No background images, gradients, or box-shadow
- No border-radius on text blocks — only on cards (16px) and pill tags (100px)
- No more than 3 feature cards side-by-side (3-column grid max)

### Design — layout, never do these (added after prior render broke on these exactly)
- Never put `justify-content:space-between` on `.frame` directly with variable-length mid content — it spreads content into huge empty gaps. Use the `.mid` wrapper pattern (Phase 4).
- Never `position:absolute` a headline or body text block — it will overlap the eyebrow/tag row or the brand/price row. Everything flows inside `.mid`.
- Never let a table's header and rows become siblings of `.top-row`/`.bottom-row` — they must be nested inside one block container inside `.mid`.
- Never render a feature card with a title but no description — description is mandatory, card uses `min-height` not `height`.
- Never leave another slide's copy inside a file — one file, one slide's content, no exceptions (see Cross-Contamination Rule, Phase 4).

### Technical — never do these
- Never use `device_scale_factor=1` or `2` — always 3 (sharper text/edges; this is the main quality-vs-examples gap)
- Never write HTML or PNG files anywhere except `D:\Claude\.claude\automations\notion\output` — no `media_gen`, no `outputs\media_kit`, no other subfolder
- Never skip viewing rendered images before delivering — check every one against the Overlap/contamination/whitespace checklist in Phase 6
- Never fabricate sample data — read actual rows from the Notion page
- Never assume status names — extract them from the template's real Select options

---

## EXECUTION CHECKLIST

```
[ ] 1. notion-fetch the Notion page URL
[ ] 2. Extract all REQUIRED EXTRACTIONS — print them explicitly
[ ] 3. Draft all 10 copy blocks (Phase 2) — print before building
[ ] 4. Confirm copy with user (or proceed after 5 seconds if no objection)
[ ] 5. mkdir -p "D:\Claude\.claude\automations\notion\output"   (the ONLY folder used)
[ ] 6. Generate all 10 HTML files directly into that folder (Phase 4), using the .mid wrapper
       pattern — no position:absolute text, no space-between on .frame
[ ] 7. Grep each HTML file for other slides' copy — delete any cross-contamination found
[ ] 8. Write render script (device_scale_factor=3) and execute (Phase 5) — PNGs land in the
       same folder as the HTML
[ ] 9. view each PNG — run the full Phase 6 checklist (overlap, whitespace, contamination,
       card descriptions, table spacing, sharpness) — fix and re-render any failure
[ ] 10. Verify all 10 PNGs exist in D:\Claude\.claude\automations\notion\output and are >150KB
[ ] 11. present_files with all 10 paths
[ ] 12. Print usage guide
```
