Skill: caopipeline/pencil-design-skill (version 3.2.0)

--- name: pencil-design-skill description: Design UIs in Pencil (.pen files) and generate production-ready code from them. Triggers on any task involving .pen files, UI prototyping, design system authoring, design-to-code conversion, or Pencil MCP tool usage. metadata: author: CaoPipeline version: "3.2.0" --- # Pencil Design Skill Design production-quality UIs in Pencil and generate clean code. Two modes: - **Mode A — Direct file writing**: write `.pen` JSON via `write_to_file` / `replace_in_file`. Always read [`references/pen-format.md`](references/pen-format.md) **before** writing any `.pen` file. - **Mode B — MCP tool-based**: when Pencil MCP is connected, use `pencil_batch_design`, `pencil_batch_get`, etc. See [MCP tools](#mcp-tool-quick-reference) below. ## When to Use - Creating/editing `.pen` files (UI screens, components, design systems) - Converting Pencil designs to React/Vue/Svelte + Tailwind v4 + shadcn/ui code - Syncing design tokens between Pencil variables and code (`@theme` blocks) --- ## Critical Rules (MUST follow) 1. **Read `pen-format.md` first** before writing any `.pen` file (Mode A) — contains all node types, variables, layout rules, default templates. 2. **Confirm style first** — if user did not specify a style, ask via `ask_followup_question` using the menu in [`styles/style-picker.md`](styles/style-picker.md). Never silently default to Shadcn Dark. 3. **Enforce file split** — count requested artboards before designing: - ≤ 3: proceed - 4: ask user (keep together vs split) - ≥ 5: MUST ask user for split strategy (1/file, 3/file, or 4/file). See [File split policy](#file-split-policy). 4. **Always use variables** — never hardcode colors / radius / fonts. Use `"$--background"`, not `"#09090b"`. If a variable doesn't exist, create it. 5. **Layout integrity is non-negotiable (HIGHEST PRIORITY)** — applies to ALL outputs (PPT slides, web, mobile, components). No content may extend beyond its parent or the artboard. No overlaps. No crooked / misaligned elements. See [`references/layout-integrity.md`](references/layout-integrity.md) + [`references/overflow-prevention.md`](references/overflow-prevention.md). Summary: - Every top-level artboard MUST use `"layout": "vertical"` or `"horizontal"` (NOT `"none"`) whenever it holds flowing content. Only use `"none"` for intentional absolute-positioned decoration (cover / hero slides), and in that case every child MUST stay within the artboard rect. - Every text node inside an auto-layout parent: `"width": "fill_container"` + `"textGrowth": "fixed-width"`. - Every child frame whose content may grow: `"width": "fill_container"` and/or `"height": "fit_content"`. Never fix a width larger than the parent's inner width (parent width − horizontal padding). - Artboards have **fixed width + fixed height**; sum of header + body (`fill_container`) + footer MUST equal artboard height. Body area content must never exceed its computed height. - Horizontal rows of cards: use `gap` + `width: "fill_container"` on each card. Never hand-pick fixed pixel widths that don't divide evenly. - Buttons / badges: use `"fit_content"` (padding `[v,h]`) + `"justifyContent": "center"` + `"alignItems": "center"`. Never fix a width smaller than the label. - Absolutely no hand-computed `x`/`y` for content blocks inside an auto-layout parent. 6. **Default UI language is Simplified Chinese** — all user-facing text in `.pen` files uses 简体中文 unless user explicitly requests otherwise. Technical terms / brand names may stay English. 7. **Multi-artboard layout** — when ≥ 2 artboards exist in one file, **default direction is HORIZONTAL** (画板横向排列，x 递增，100 px gap). Each top-level frame needs `x` and `y`. - **Default (horizontal)**: y = 0 for all; x = 0, `W+100`, `2(W+100)`, … Example for 1440×900: x = 0, 1540, 3080, 4620. - **Vertical (only when user explicitly asks)**: x = 0 for all; y = 0, `H+100`, `2(H+100)`, … Example for 1440×900: y = 0, 1000, 2000, 3000. For PPT (1280×720): y = 0, 820, 1640, 2460. - **Always ask the user before designing** (in both English and Chinese), unless they have already specified a direction: > Your request has **N** artboards. Arrange them: 您的请求包含 **N** 个画板，排列方向： > 1. Horizontal (default) / 横向排列（默认） > 2. Vertical / 竖向排列 8. **Icon font family** — always `"iconFontFamily": "lucide"`. No other families. 9. **Reuse assets** — before generating images/logos, search existing nodes (Mode B uses `pencil_batch_get`). Copy & resize, do not regenerate. See [`mcp/image-reuse.md`](mcp/image-reuse.md). 10. **Build section by section in Mode B** — screenshot + layout-check after each section, fix issues before moving on. See [`mcp/screenshot-qa.md`](mcp/screenshot-qa.md). 11. **Mandatory pre-completion layout audit** — before declaring any `.pen` task done, run the [Layout Integrity Checklist](references/layout-integrity.md#pre-completion-checklist). In Mode B: `pencil_snapshot_layout` with `problemsOnly: true` for every artboard must return empty. In Mode A: manually verify each artboard against the checklist. 12. **Code generation requires `frontend-design` skill** if available — load it for aesthetic direction. --- ## File Split Policy A single `.pen` file must NEVER contain more than 4 artboards (causes performance issues). When ≥ 4 artboards are requested, ask the user (in both English and Chinese): > Your request includes **N** artboards. Please choose a splitting strategy: > 您的请求包含 **N** 个画板。请选择拆分方式： > > 1. One artboard per file / 每个画板一个文件 > 2. Max 3 artboards per file / 每个文件最多 3 个画板 > 3. Max 4 artboards per file / 每个文件最多 4 个画板 > 4. No split (not recommended) / 不拆分（不推荐） Naming: `feature-01.pen`, `feature-02.pen`, … or by logical groups (`feature-onboarding.pen`). --- ## Quick Templates ### Single artboard (Shadcn Dark default) ```json { "version": "2.8", "children": [ { "type": "frame", "id": "scrn1", "name": "页面", "theme": { "Mode": "Dark" }, "clip": true, "width": 1440, "height": 900, "fill": "$--background", "layout": "vertical", "children": [ ... ] } ], "themes": { "Mode": ["Light", "Dark"] }, "variables": { ...copy from pen-format.md "Shadcn Dark Zinc Variables"... } } ``` ### Multiple artboards — default HORIZONTAL (x 递增, 100 px gap) ```json { "type": "frame", "id": "scrn1", "x": 0, "y": 0, "width": 1440, "height": 900, ... }, { "type": "frame", "id": "scrn2", "x": 1540, "y": 0, "width": 1440, "height": 900, ... }, { "type": "frame", "id": "scrn3", "x": 3080, "y": 0, "width": 1440, "height": 900, ... } ``` ### Multiple artboards — VERTICAL (only when user picks it, y 递增, 100 px gap) ```json { "type": "frame", "id": "scrn1", "x": 0, "y": 0, "width": 1440, "height": 900, ... }, { "type": "frame", "id": "scrn2", "x": 0, "y": 1000, "width": 1440, "height": 900, ... }, { "type": "frame", "id": "scrn3", "x": 0, "y": 2000, "width": 1440, "height": 900, ... } ``` Common screen sizes: Desktop `1440×900` / `1920×1080` · Tablet `768×1024` · Mobile `375×812` / `393×852`. --- ## Default Styles > Named style specs (Vercel / Linear / Stripe / Notion / Raycast / Supabase / Airtable / Apple) are distilled from <https://getdesign.md/>. | Trigger | Style file | Notes | |---|---|---| | User chose a named style | `styles/style-<name>.md` | Read only the matching file (vercel / linear / stripe / notion / raycast / supabase / airtable / apple) | | User skipped / chose "default" | Shadcn Dark Zinc | Variables block lives in `pen-format.md`. Dark surfaces `#09090b` / `#18181b`, light text `#fafafa`, subtle borders `#27272a`. | | PPT / slide deck | [`ppt/presets-ppt.md`](ppt/presets-ppt.md) | 1280×720, blue gradient header, white body | --- ## Workflow Summary ### Mode A — Direct file writing ``` 1. Confirm style (Rule 2) -> ask user OR read styles/style-<name>.md 2. Read pen-format.md -> mandatory for first .pen task in session 3. Read layout-integrity.md -> mandatory for EVERY .pen task (Rule 5, 11) 4. Plan layout & artboard count -> apply file split policy if ≥ 4 If ≥ 2 artboards, ask arrangement direction (Rule 7) — default HORIZONTAL 5. Write .pen with variables block and auto-layout (NEVER layout:"none" for content) 6. read_lints to validate JSON 7. Manual Layout Integrity Checklist pass (Rule 11) — fix before declaring done ``` ### Mode B — MCP tool-based ``` 1. Confirm style + load frontend-design skill if available 2. pencil_get_editor_state -> get .pen schema, current state 3. pencil_batch_get reusable=true -> discover existing components (REUSE > recreate) 4. pencil_get_variables -> read tokens; pencil_set_variables to create new 5. pencil_find_empty_space -> place new artboards 6. pencil_batch_design -> build ONE section at a time 7. pencil_get_screenshot + pencil_snapshot_layout(problemsOnly:true) -> verify each section 8. Fix issues -> re-screenshot -> re-check (verify loop) 9. Final full-file layout audit (Rule 11) -> problemsOnly MUST be empty before declaring done ``` --- ## MCP Tool Quick Reference See [`mcp/mcp-tools.md`](mcp/mcp-tools.md) for the full tool table and Mode B call order. --- ## Code Generation (Pencil → React + Tailwind v4 + shadcn/ui) **Always do**: TypeScript · React 19 (`ref` as prop, no `forwardRef`) · semantic Tailwind classes (`bg-primary`, never `bg-[#3b82f6]`) · `@theme { --color-* / --radius-* }` blocks · Lucide icons · CVA + `cn()` for variants · OKLCH in tokens. **Never do**: arbitrary values (`bg-[#fff]`, `rounded-[6px]`, `p-[24px]`) · `tailwind.config.ts` (v4 uses CSS) · `forwardRef` · hardcoded artboard widths in components (use `w-full max-w-7xl`). Detailed mapping: [`references/codegen-workflow.md`](references/codegen-workflow.md) + [`references/codegen-mapping.md`](references/codegen-mapping.md). --- ## Reference Files (read on demand) ### Always-read (mandatory) | File | When | |------|------| | [`pen-format.md`](references/pen-format.md) | Before writing any `.pen` file | | [`layout-integrity.md`](references/layout-integrity.md) | Before writing any `.pen` file — hard rules preventing overflow, misalignment, crooked layouts | ### Style references (read 1, after style is selected) > Source for named style specs: <https://getdesign.md/> | Style | File | |-------|------| | Style picker / menu | [`style-picker.md`](styles/style-picker.md) | | Vercel | [`style-vercel.md`](styles/style-vercel.md) | | Linear | [`style-linear.md`](styles/style-linear.md) | | Stripe | [`style-stripe.md`](styles/style-stripe.md) | | Notion | [`style-notion.md`](styles/style-notion.md) | | Raycast | [`style-raycast.md`](styles/style-raycast.md) | | Supabase | [`style-supabase.md`](styles/style-supabase.md) | | Airtable | [`style-airtable.md`](styles/style-airtable.md) | | Apple | [`style-apple.md`](styles/style-apple.md) | | PPT preset | [`presets-ppt.md`](ppt/presets-ppt.md) | ### Topic references (read on demand) | File | Topic | |------|-------| | [`layout-integrity.md`](references/layout-integrity.md) | **MUST-READ.** Hard rules for all UI / PPT — sizing, auto-layout, no-overflow, no-overlap, no-crooked-alignment, artboard budget math | | [`overflow-prevention.md`](references/overflow-prevention.md) | Text `fill_container` rules, mobile constraints, fix patterns | | [`design-tokens.md`](references/design-tokens.md) | Variable system, theming, Tailwind v4 mapping | | [`component-reuse.md`](references/component-reuse.md) | `ref` instances, `descendants` overrides | | [`image-reuse.md`](mcp/image-reuse.md) | Search-before-generate, copy vs regenerate | | [`screenshot-qa.md`](mcp/screenshot-qa.md) | Section-by-section verification (Mode B) | | [`responsive.md`](references/responsive.md) | Artboard sizes → Tailwind breakpoints | | [`codegen-workflow.md`](references/codegen-workflow.md) | Full design-to-code workflow | | [`codegen-mapping.md`](references/codegen-mapping.md) | Tailwind / shadcn quick mapping tables | > **Output rule**: user-facing design files must always be saved as `<name>.pen` (never `.pen.json` — that suffix is reserved for skill-internal storage). --- ## Common Mistakes | Mistake | Fix | |---------|-----| | Hardcoded `#3b82f6` in `.pen` | Use `"$--primary"` | | Omitting `layout` on frame | Set `"layout": "vertical"` or `"horizontal"` (default is horizontal) | | Using `"layout": "none"` on a content artboard | Switch to `"vertical"`; reserve `"none"` for intentional absolute decoration (cover backgrounds only) | | Fixed-width text in auto-layout | Use `"width": "fill_container"` + `"textGrowth": "fixed-width"` | | Card/content wider than parent inner width | Use `"width": "fill_container"` on every row child; size cards via `gap` on parent | | Body height overflowing artboard (header + body > artboard) | Use `height: "fill_container"` on the body frame, fixed heights only for header/footer | | Button with hardcoded width smaller than label | Use `"fit_content"` + `padding: [v, h]` + `justifyContent: "center"` | | Hand-computed `x`/`y` inside auto-layout parent | Let auto-layout position children; use `gap`/`padding` instead | | Text touching artboard edges | Apply horizontal padding (desktop 40–64px, tablet 24–32px, mobile 16–20px) | | Long descriptive IDs | Use 5-char codes: `"btn01"`, `"crd1a"` | | `bg-[#fff]` in code | Use `bg-background`, `text-foreground` | | `tailwind.config.ts` | Use CSS `@theme` (Tailwind v4) | | English UI text | Default to Simplified Chinese | | Silently using Shadcn Dark | Always ask user for style first (Rule 2) | | Cramming 5+ artboards in one file | Apply file split policy (Rule 3) | | Skipping final layout audit | Run Layout Integrity Checklist before declaring done (Rule 11) |

path: styles/style-airtable.md
content: # Style: Airtable

> Source: <https://getdesign.md/airtable/design-md>
> Light editorial · Haas Grotesk · near-black pill CTA · full-bleed signature surface cards.
> Best for data tools, multi-color SaaS marketing pages, structured-data UIs.

## Identity (1-line)

White canvas (`#ffffff`) with **near-black ink** (`#181d26`) as the primary brand color — both the hero type and the primary CTA share this single near-black hue. Brand voltage comes not from gradients or accent walls, but from **full-bleed signature surface cards** in coral (`#aa2d00`), forest (`#0a2e0e`), cream (`#f5e9d4`), and navy (`#181d26`) that punctuate long-scroll editorial pages every two or three viewports. Type runs Haas Grotesk at modest weights (400 for display, 500 for buttons / sub-titles) — display headlines never bolder than 500. The pricing surface speaks its own dialect: **Inter Display** at mid-weight 475 with **pill-shaped CTAs** (the only place pill radius appears).

## Token Table

| Token | Value | Notes |
|-------|-------|-------|
| Canvas | `#ffffff` | Default page surface |
| Surface soft | `#f8fafc` | Tabbed feature cards, featured pricing tier |
| Surface strong | `#e0e2e6` | Light-gray CTA banner near footer |
| Surface dark | `#181d26` | Dark navy mid-page CTA cards (same hex as ink) |
| Surface dark elevated | `#1d1f25` | Articles hero base |
| Hairline | `#dddddd` | 1px borders for inputs, secondary buttons, dividers |
| Border strong | `#9297a0` | Disabled secondary outline |
| Primary / Ink | `#181d26` | Near-black — primary CTA bg AND h1/h2 type |
| Primary active | `#0d1218` | Press state on primary buttons |
| Body | `#333840` | Default running text |
| Muted | `#41454d` | Footer links, captions |
| On primary / on dark | `#ffffff` | Text on primary buttons & dark surfaces |
| Signature coral | `#aa2d00` | Largest signature card (oxide red) |
| Signature forest | `#0a2e0e` | Deep-green signature card |
| Signature cream | `#f5e9d4` | Cream callout band |
| Signature peach | `#fcab79` | Demo-card surface |
| Signature mint | `#a8d8c4` | Demo-card surface |
| Signature yellow | `#f4d35e` | Demo-card surface |
| Signature mustard | `#d9a441` | Demo-card surface |
| Link | `#1b61c9` | Inline body links — **NOT** primary button color |
| Link active | `#1a3866` | Press state |
| Info border | `#458fff` | Focused-input outline |
| Success | `#006400` / border `#39bf45` | Confirmation states |

## Typography

- Family: `Haas Groot Disp` (display 24px+) / `Haas Grotesk` (≤ 24px). Fallback: `Inter Display` for display + `Inter` for text.
- Pricing sub-system: `Inter Display` at custom weight **475** (variable font).
- **Weight ladder**: 400 (display + body) / 500 (sub-titles, buttons) / 600 only for legal/cookie surfaces. **No 700**.

| Role | Size | Weight | Line height | Spacing |
|------|------|--------|-------------|---------|
| Display XL | 48 | 500 | 1.10 | 0 |
| Display LG (h1) | 40 | 400 | 1.20 | 0 |
| Display MD (h2) | 32 | 400 | 1.20 | 0 |
| Title LG | 24 | 400 | 1.35 | +0.12 px |
| Title MD | 20 | 400 | 1.50 | 0 |
| Title SM | 18 | 500 | 1.40 | 0 |
| Label / Button | 16 | 500 | 1.40 | 0 |
| Body | 14 | 400 | 1.25 | 0 |
| Caption | 14 | 500 | 1.35 | +0.16 px |
| Legal | 13.12 | 600 | 1.20 | 0 |
| Pricing display | 44.8 | 475 | 1.10 | 0 (Inter Display) |
| Pricing card title | 20 | 475 | 1.30 | 0 (Inter Display) |

## Radius & Depth

- Radius: `xs` 2 (legal/cookie) · `sm` 6 (inputs) · `md` 10 (content cards, demo grids, cream callouts) · `lg` 12 (primary CTA, signature cards, tabbed feature cards) · `pill` 9999 (pricing CTAs only) · `full` 9999 (icon buttons).
- **Color-block first, shadow second.** Depth comes from contrast between white canvas and signature surface cards. No card shadow language, no atmospheric mesh, no hero gradient.
- Section vertical rhythm: **96 px** top + 96 px bottom on every editorial band. Universal.

## Pencil `variables` block (copy-ready)

```json
"variables": {
  "--background":         { "type": "color", "value": "#ffffff" },
  "--card":               { "type": "color", "value": "#ffffff" },
  "--card-soft":          { "type": "color", "value": "#f8fafc" },
  "--card-strong":        { "type": "color", "value": "#e0e2e6" },
  "--popover":            { "type": "color", "value": "#ffffff" },
  "--foreground":         { "type": "color", "value": "#181d26" },
  "--body":               { "type": "color", "value": "#333840" },
  "--muted-foreground":   { "type": "color", "value": "#41454d" },
  "--primary":            { "type": "color", "value": "#181d26" },
  "--primary-foreground": { "type": "color", "value": "#ffffff" },
  "--primary-active":     { "type": "color", "value": "#0d1218" },
  "--accent-coral":       { "type": "color", "value": "#aa2d00" },
  "--accent-forest":      { "type": "color", "value": "#0a2e0e" },
  "--accent-cream":       { "type": "color", "value": "#f5e9d4" },
  "--accent-peach":       { "type": "color", "value": "#fcab79" },
  "--accent-mint":        { "type": "color", "value": "#a8d8c4" },
  "--accent-yellow":      { "type": "color", "value": "#f4d35e" },
  "--accent-mustard":     { "type": "color", "value": "#d9a441" },
  "--surface-dark":       { "type": "color", "value": "#181d26" },
  "--link":               { "type": "color", "value": "#1b61c9" },
  "--border":             { "type": "color", "value": "#dddddd" },
  "--border-strong":      { "type": "color", "value": "#9297a0" },
  "--ring":               { "type": "color", "value": "#458fff" },
  "--radius-xs":          { "type": "number", "value": 2 },
  "--radius-sm":          { "type": "number", "value": 6 },
  "--radius-md":          { "type": "number", "value": 10 },
  "--radius-lg":          { "type": "number", "value": 12 },
  "--radius-pill":        { "type": "number", "value": 9999 },
  "--font-primary":       { "type": "string", "value": "Inter" },
  "--font-pricing":       { "type": "string", "value": "Inter" }
}
```

## Component Recipes

**Primary CTA**: `#181d26` fill, `#ffffff` text, **12 px radius**, padding `[16, 24]`, 16 px Haas weight 500. One per viewport — scarcity is the design.

**Secondary CTA**: `#ffffff` fill, `#181d26` text, **12 px radius**, padding `[16, 24]`, 1 px solid `#dddddd` border. Pairs with primary as the signature button row.

**Signature coral card**: `#aa2d00` fill, `#ffffff` text, **12 px radius**, padding `48`, h2 at 32 px Haas 400. Carries `button-secondary-on-dark` (still white fill, never translucent).

**Signature forest card**: `#0a2e0e` fill, `#ffffff` text, 12 px radius, padding `48`. Same shape as coral.

**Hero dark card**: `#181d26` fill, `#ffffff` text, 12 px radius, padding `48`. Same hex as ink — primary doubles as dark surface.

**Tabbed feature card**: `#f8fafc` fill, **12 px radius**, padding `32`. Left rail = vertical tab labels in 20 px Haas 400; right pane = active content.

**Cream callout**: `#f5e9d4` fill, `#181d26` text, **10 px radius**, padding `24`.

**Demo-grid card**: `#ffffff` (or peach/mint/yellow/mustard) fill, **10 px radius**, padding `16`. **Heights deliberately uneven** — uniform reads as spec sheet.

**Article card**: `#ffffff`, 10 px radius, padding `16`, illustrated 16:9 thumbnail, 18 px title weight 500.

**Logo strip**: `#ffffff` bg, monochrome `#41454d` logos, 32 px vertical padding, 6-up desktop / 3-up mobile.

**Text input**: `#ffffff` fill, **6 px radius**, padding `[12, 16]`, 44 px height, 1 px `#dddddd` border. Focus → border `#458fff`.

**Pricing tier card**: `#ffffff` fill, **10 px radius**, padding `32`. Featured tier = `#f8fafc` bg (no border, no badge — tone shift IS the signal). Price block: 44.8 px Inter Display weight 475.

**Pricing pill CTA**: `#ffffff` fill, `#1d1f25` text, **9999 radius**, padding `[12, 24]`. Pill only appears on pricing — sub-system signal.

**CTA banner light**: `#e0e2e6` fill, `#181d26` text, 12 px radius, padding `48`. The "Start building" strip near footer.

**Section rhythm rule**: white → coral → white → cream → dark navy → light gray → footer. Never two whites in a row.

## Do / Don't

✓ Primary CTA always near-black `#181d26` (NEVER link blue) · one primary per viewport · whitespace is the hero atmosphere (no gradient/mesh/aurora) · signature cards in coral/forest/dark/cream for voltage moments · demo-grid heights deliberately uneven · pricing sub-system stays self-contained (Inter Display 475 + pill radius) · 96 px vertical rhythm on every editorial band · weight ladder stops at 500 (legal exception 600).

✗ **Don't make `#1b61c9` link blue the primary button** — it's the link color only · no hero gradient/aurora/mesh · no display type at weight 700 · no `pill` radius outside the pricing surface · no two consecutive white bands · no shadow language for cards (color-block first) · no extra accent colors beyond the documented signature palette · no inverted/translucent buttons on dark signature cards (white stays white).


path: styles/style-apple.md
content: # Style: Apple

> Source: <https://getdesign.md/apple/design-md>
> Photography-first · SF Pro · single Action Blue · alternating full-bleed light/dark tiles · museum-gallery pacing.
> Best for premium consumer-product pages, cinematic marketing, single-accent luxury sites.

## Identity (1-line)

Edge-to-edge **product tiles** alternating light canvas (`#ffffff` / parchment `#f5f5f7`) with near-black tiles (`#272729`), each tile occupying ~one viewport. Type is SF Pro Display at weight 600 with **negative letter-spacing** (`-0.28 → -0.374 px`) — the signature "Apple tight" cadence. **One** interactive color across the entire system: Action Blue `#0066cc`. **One** drop shadow in the entire system, applied only to product photography (`rgba(0,0,0,0.22) 3px 5px 30px`). Body copy runs **17 px** (not 16) for an unmistakable reading pace. Pill-shaped (`9999`) primary CTA is the brand-action signal; everything else stays compact `8 / 11 / 18` radius. The wall disappears so the product can speak.

## Token Table

| Token | Value | Notes |
|-------|-------|-------|
| Action blue (primary) | `#0066cc` | The ONLY interactive color |
| Focus blue | `#0071e3` | Keyboard focus ring |
| Sky link blue (on dark) | `#2997ff` | Inline links on dark tiles only |
| Canvas | `#ffffff` | Pure white default |
| Canvas parchment | `#f5f5f7` | Signature off-white — alternating tiles, footer, sub-nav |
| Surface pearl | `#fafafc` | Ghost/secondary button fill |
| Surface tile 1 | `#272729` | Primary dark-tile bg |
| Surface tile 2 | `#2a2a2c` | Micro-step lighter (adjacent dark tile separation) |
| Surface tile 3 | `#252527` | Micro-step darker (player frames, stack bottom) |
| Surface black | `#000000` | True void — global nav, video |
| Chip translucent | `#d2d2d7` | Used at ~64% alpha over photography |
| Ink / body | `#1d1d1f` | Near-black — all text on light surfaces |
| Body on dark | `#ffffff` | Text on dark tiles |
| Body muted (dark) | `#cccccc` | Secondary copy on dark |
| Ink muted 80 | `#333333` | Pearl button body text |
| Ink muted 48 | `#7a7a7a` | Disabled & legal fine print |
| Divider soft | `#f0f0f0` | Soft button ring |
| Hairline | `#e0e0e0` | 1 px borders on utility cards / chips |

> **No decorative gradients exist.** Atmosphere is photographic — never CSS.

## Typography

- Display: `SF Pro Display, system-ui, -apple-system, sans-serif` (≥ 19 px).
- Text: `SF Pro Text, system-ui, -apple-system, sans-serif` (< 20 px).
- Fallback: `Inter` / `Inter Display` (variable). Tighten letter-spacing by `-0.01em` and line-height by `0.03` to match.
- **Weight ladder**: 300 / 400 / 600 / 700. **Weight 500 is deliberately absent.**

| Role | Size | Weight | Line height | Spacing |
|------|------|--------|-------------|---------|
| Hero display | 56 | 600 | 1.07 | **−0.28 px** |
| Display LG | 40 | 600 | 1.10 | 0 |
| Display MD | 34 | 600 | 1.47 | −0.374 px |
| Lead | 28 | 400 | 1.14 | +0.196 px |
| Lead airy | 24 | **300** | 1.50 | 0 (rare 300) |
| Tagline | 21 | 600 | 1.19 | +0.231 px |
| Body strong | 17 | 600 | 1.24 | −0.374 px |
| **Body** | **17** | **400** | **1.47** | **−0.374 px** |
| Dense link (footer) | 17 | 400 | **2.41** | 0 |
| Caption | 14 | 400 | 1.43 | −0.224 px |
| Caption strong | 14 | 600 | 1.29 | −0.224 px |
| Button large (store) | 18 | **300** | 1.00 | 0 (rare 300) |
| Button utility | 14 | 400 | 1.29 | −0.224 px |
| Fine print | 12 | 400 | 1.00 | −0.12 px |
| Nav link | 12 | 400 | 1.00 | −0.12 px |
| Micro legal | 10 | 400 | 1.30 | −0.08 px |

## Radius & Depth

- Radius: `none` 0 (full-bleed tiles) · `xs` 5 · `sm` 8 (dark utility buttons, nav cluster) · `md` 11 (Pearl Button) · `lg` 18 (utility cards, accessory grid) · `pill` 9999 (primary CTA, search input, configurator chip, sticky CTA) · `full` 9999 (icon chips).
- **Exactly ONE shadow** in the entire system: `rgba(0,0,0,0.22) 3px 5px 30px 0` — applied only to product photography, never to cards/buttons/text.
- Depth signals:
  - Surface-color change between tiles (light ↔ dark) IS the section divider — no borders, no rules.
  - `backdrop-filter: blur(20px) saturate(180%)` on parchment-80% for sub-nav and floating sticky bar.
  - 1 px `rgba(0,0,0,0.08)` border on utility cards / configurator chips.
- Active/press state on every button: `transform: scale(0.95)` — the system-wide micro-interaction.

## Pencil `variables` block (copy-ready)

```json
"variables": {
  "--background":         { "type": "color", "value": "#ffffff" },
  "--background-parchment": { "type": "color", "value": "#f5f5f7" },
  "--card":               { "type": "color", "value": "#ffffff" },
  "--card-pearl":         { "type": "color", "value": "#fafafc" },
  "--popover":            { "type": "color", "value": "#ffffff" },
  "--surface-tile-light": { "type": "color", "value": "#ffffff" },
  "--surface-tile-dark":  { "type": "color", "value": "#272729" },
  "--surface-tile-dark-2":{ "type": "color", "value": "#2a2a2c" },
  "--surface-tile-dark-3":{ "type": "color", "value": "#252527" },
  "--surface-black":      { "type": "color", "value": "#000000" },
  "--foreground":         { "type": "color", "value": "#1d1d1f" },
  "--foreground-on-dark": { "type": "color", "value": "#ffffff" },
  "--muted-foreground":   { "type": "color", "value": "#7a7a7a" },
  "--muted-on-dark":      { "type": "color", "value": "#cccccc" },
  "--ink-muted-80":       { "type": "color", "value": "#333333" },
  "--primary":            { "type": "color", "value": "#0066cc" },
  "--primary-foreground": { "type": "color", "value": "#ffffff" },
  "--primary-focus":      { "type": "color", "value": "#0071e3" },
  "--primary-on-dark":    { "type": "color", "value": "#2997ff" },
  "--ink":                { "type": "color", "value": "#1d1d1f" },
  "--border":             { "type": "color", "value": "#e0e0e0" },
  "--border-soft":        { "type": "color", "value": "#f0f0f0" },
  "--ring":               { "type": "color", "value": "#0071e3" },
  "--radius-xs":          { "type": "number", "value": 5 },
  "--radius-sm":          { "type": "number", "value": 8 },
  "--radius-md":          { "type": "number", "value": 11 },
  "--radius-lg":          { "type": "number", "value": 18 },
  "--radius-pill":        { "type": "number", "value": 9999 },
  "--radius-none":        { "type": "number", "value": 0 },
  "--font-primary":       { "type": "string", "value": "Inter" },
  "--font-display":       { "type": "string", "value": "Inter" }
}
```

## Component Recipes

**Primary pill CTA**: `#0066cc` fill, `#ffffff` text, **9999 radius (pill)**, padding `[11, 22]`, 17 px SF Pro Text weight 400. The full-pill IS the brand-action signal.

**Secondary ghost pill**: transparent fill, `#0066cc` text, `1px solid #0066cc`, 9999 radius, padding `[11, 22]`. Pairs as second CTA ("Learn more" / "Buy").

**Dark utility button** (nav cluster): `#1d1d1f` fill, `#ffffff` text, **8 px radius**, padding `[8, 15]`, 14 px weight 400.

**Pearl capsule button**: `#fafafc` fill, `#333333` text, **11 px radius**, padding `[8, 14]`, 14 px caption, 3 px solid `#f0f0f0` border (functions as a soft ring).

**Store hero CTA**: `#0066cc` fill, `#ffffff` text, 9999 radius, padding `[14, 28]`, **18 px SF Pro Text weight 300** (rare).

**Icon circular**: 44 × 44 px, `#d2d2d7` at 64% alpha, `#1d1d1f` icon, 9999 radius. Floats over photography.

**Global nav**: `#000000` fill, height 44 px, links 12 px / 400 / `−0.12 px`, white text. The only place pure black appears on most pages.

**Frosted sub-nav**: `#f5f5f7` at 80% opacity + `backdrop-filter: blur(20px) saturate(180%)`, height 52 px. Left = product name 21 px weight 600. Right = inline 14 px links + persistent primary pill.

**Product tile (light)**: `#ffffff`, **0 radius (full-bleed)**, vertical padding **80 px**. Centered stack: name (40 / 600) → tagline (28 / 400) → two pill CTAs → product render with the system shadow.

**Product tile (parchment)**: same as light tile but on `#f5f5f7`. Used to break two consecutive whites.

**Product tile (dark)**: `#272729`, full-bleed, padding 80 px. Same content stack with `text-link-on-dark` (`#2997ff`). Variants `-2` (`#2a2a2c`) and `-3` (`#252527`) for adjacent stacking.

**Store utility card**: `#ffffff`, 1 px solid `#e0e0e0`, **18 px radius**, padding `24`. Image 1:1 (8 px inner radius) → name 17 px / 600 → price 17 px / 400 → text-link.

**Configurator chip**: `#ffffff`, **9999 pill radius**, padding `[12, 16]`, 14 px caption. Selected = 2 px `#0071e3` border (no fill change).

**Search input**: `#ffffff`, **9999 pill radius** (search is also pill — matches CTA grammar), padding `[12, 20]`, 44 px height, 17 px body, 1 px `rgba(0,0,0,0.08)` border.

**Floating sticky bar** (buy page bottom): `#f5f5f7` at 80% + backdrop blur, height 64 px, padding `[12, 32]`. Right cluster = primary pill CTA.

**Footer**: `#f5f5f7` bg, link columns at **17 px / 400 / line-height 2.41** (the relaxed leading is what makes dense columns scannable). Headings 14 px / 600. Legal 12 px in `#7a7a7a`.

**Section rhythm rule**: light → dark tile → light → dark → parchment footer. The color change IS the divider (no border, no rule).

## Theme Mode

Mixed — the system alternates light and dark **tiles** within a single page, but the page chassis (nav, sub-nav, footer) is light-dominant by default. For Pencil: keep top-level frame `theme: { "Mode": "Light" }`. Apply `Mode: "Dark"` only on `surface-tile-dark` frames where you want true tile inversion.

## Do / Don't

✓ One accent only — Action Blue `#0066cc` carries every interactive element · headlines at SF Pro Display 600 with negative letter-spacing (`−0.28 → −0.374 px`) — the signature "Apple tight" cadence · body at **17 px / 400 / 1.47 / −0.374 px** (NEVER 16 px) · alternate light/dark tiles for section rhythm — the color change IS the divider · pill radius (9999) reserved for "this is an action" elements (primary CTA, search, configurator chip, sticky CTA) · the single product shadow is for product imagery only · `transform: scale(0.95)` as universal active state · keep global nav `#000000` (the only pure black on most pages).

✗ **No second accent color** — every "click me" is `#0066cc` · no shadows on cards/buttons/text — shadow is reserved for product photography · no decorative gradients — atmosphere is photographic · **no weight 500** anywhere — the ladder is 300 / 400 / 600 / 700 · no rounded corners on full-bleed tiles · no body line-height tighter than 1.47 · no mixing of radii grammars (sm 8 / lg 18 / pill — nothing in between except the rare 11 Pearl) · don't use `#2997ff` Sky Blue on light surfaces — it's the dark-tile-only variant.


path: styles/style-linear.md
content: # Style: Linear

> Source: <https://getdesign.md/>
> Deep dark · indigo accent · Inter weight 510 · ultra-minimal SaaS.
> Best for SaaS apps, task management, dark-mode UI.

## Identity (1-line)

Near-black canvas (`#08090a`) where content emerges from darkness. Inter Variable with `"cv01","ss03"` features and a signature **weight 510** (between regular and medium). Borders are semi-transparent white whispers; elevation comes from background-luminance stepping, not shadow opacity. Single brand color: indigo `#5e6ad2` / `#7170ff`.

## Token Table

| Token | Value | Notes |
|-------|-------|-------|
| Marketing bg | `#08090a` | Page background |
| Panel bg | `#0f1011` | Sidebar / panel |
| Surface (card) | `#191a1b` | Elevated surface |
| Surface 2 | `#28282c` | Hover state |
| Primary text | `#f7f8f8` | Near-white — never `#fff` |
| Body text | `#d0d6e0` | Silver gray |
| Tertiary | `#8a8f98` | Placeholder / metadata |
| Quaternary | `#62666d` | Disabled / timestamps |
| Brand indigo | `#5e6ad2` | CTA bg |
| Accent violet | `#7170ff` | Links / active |
| Hover violet | `#828fff` | Hover only |
| Border (default) | `rgba(255,255,255,0.08)` | Cards / inputs |
| Border (subtle) | `rgba(255,255,255,0.05)` | Lightest separator |
| Button bg | `rgba(255,255,255,0.02)`–`0.05` | Translucent ghost |
| Success | `#10b981` | Pill / status only |

## Typography

- Family: `Inter Variable` with `font-feature-settings: "cv01","ss03"` on **all text** (this IS Linear's typeface — without it, generic Inter).
- Mono: `Berkeley Mono` for code/technical labels.
- Three weights: **400 (read) / 510 (UI/emphasis) / 590 (announce)**. Weight 300 only for de-emphasized text. Never use 700.
- Letter-spacing scale:

| Size | Spacing |
|------|---------|
| 72 px | -1.584 px |
| 64 px | -1.408 px |
| 48 px | -1.056 px |
| 32 px | -0.704 px |
| 24 px | -0.288 px |
| ≤ 16 px | normal |

## Radius & Elevation

- Radius: micro 2 · small 4 · button 6 · card 8 · panel 12 · large 22 · pill 9999.
- **Elevation by luminance step**, not shadow opacity:
  - Card: `rgba(255,255,255,0.02)` bg + `1px solid rgba(255,255,255,0.08)`
  - Inset: `rgba(0,0,0,0.2) 0 0 12px 0 inset`
  - Ring border: `rgba(0,0,0,0.2) 0 0 0 1px`

## Pencil `variables` block (copy-ready)

```json
"variables": {
  "--background":         { "type": "color", "value": "#08090a" },
  "--card":               { "type": "color", "value": "#191a1b" },
  "--popover":            { "type": "color", "value": "#0f1011" },
  "--foreground":         { "type": "color", "value": "#f7f8f8" },
  "--muted-foreground":   { "type": "color", "value": "#8a8f98" },
  "--secondary":          { "type": "color", "value": "#28282c" },
  "--primary":            { "type": "color", "value": "#5e6ad2" },
  "--primary-foreground": { "type": "color", "value": "#ffffff" },
  "--accent":             { "type": "color", "value": "#7170ff" },
  "--border":             { "type": "color", "value": "#23252a" },
  "--ring":               { "type": "color", "value": "#7170ff" },
  "--success":            { "type": "color", "value": "#10b981" },
  "--radius-sm":          { "type": "number", "value": 4 },
  "--radius-md":          { "type": "number", "value": 6 },
  "--radius-lg":          { "type": "number", "value": 8 },
  "--radius-pill":        { "type": "number", "value": 9999 },
  "--font-primary":       { "type": "string", "value": "Inter" }
}
```

> Note: rgba borders cannot be expressed as variable values in `.pen`. Use the solid `#23252a` border above as a close approximation, OR put the rgba string directly into `stroke.fill` per-frame when you need the translucent look.

## Component Recipes

**Ghost button**: `rgba(255,255,255,0.02)` fill, `1px solid rgba(255,255,255,0.08)`, 6 px radius, 14 px Inter 510, color `#e2e4e7`.

**Brand CTA**: `#5e6ad2` fill, white text, 6 px radius, padding `[8,16]`, 14 px Inter 510.

**Card**: `rgba(255,255,255,0.02)` fill, `1px solid rgba(255,255,255,0.08)`, 8 px radius, 24 px padding.

**Pill badge**: transparent bg, `1px solid #23252a`, 9999 radius, padding `[0,10]`, 12 px Inter 510, `#d0d6e0` text.

## Do / Don't

✓ `"cv01","ss03"` on every Inter element · weight 510 default · luminance stepping for elevation · `#f7f8f8` not `#fff` · semi-transparent white borders · indigo only on CTAs.

✗ No pure white text · no solid colored button bg (translucent only) · no decorative use of indigo · no positive letter-spacing · no weight 700 · no warm colors in chrome.


path: styles/style-notion.md
content: # Style: Notion

> Source: <https://getdesign.md/>
> Warm minimalism · whisper borders · 4-layer low-opacity shadows · paper-like feel.
> Best for productivity tools, documentation, content apps.

## Identity (1-line)

White (`#ffffff`) + alternating warm-white sections (`#f6f5f4`) + near-black text via 95% alpha (`rgba(0,0,0,0.95)`). Warm-gray neutrals carry yellow-brown undertones — never blue-gray. Borders are 1-px whispers at `rgba(0,0,0,0.1)`. Shadows accumulate in 4-5 layers, each ≤0.05 opacity. Notion blue (`#0075de`) is the only saturated color in core UI.

## Token Table

| Token | Value | Notes |
|-------|-------|-------|
| Background | `#ffffff` | Page bg |
| Background alt | `#f6f5f4` | Warm white — for section alternation |
| Primary text | `rgba(0,0,0,0.95)` | Near-black, soft |
| Secondary text | `#615d59` | Warm gray 500 |
| Muted text | `#a39e98` | Warm gray 300 |
| Dark surface | `#31302e` | Dark section bg (warm) |
| Notion Blue | `#0075de` | CTA / link |
| Active Blue | `#005bab` | Pressed state |
| Focus Blue | `#097fe8` | |
| Badge bg blue | `#f2f9ff` | Pill background |
| Badge text blue | `#097fe8` | Pill text |
| Border whisper | `rgba(0,0,0,0.1)` | Cards / dividers |
| Input border | `#dddddd` | Inputs |

## Typography

- Family: `NotionInter` (fallback `Inter`). Enable `"lnum","locl"` on display & heading text.
- Four weights: **400 (read) / 500 (UI) / 600 (emphasis) / 700 (display)**.
- Letter-spacing scale:

| Size | Spacing |
|------|---------|
| 64 px | -2.125 px |
| 54 px | -1.875 px |
| 48 px | -1.5 px |
| 26 px | -0.625 px |
| 22 px | -0.25 px |
| ≤ 16 px | normal |
| 12 px badge | **+0.125 px** (only positive tracking in system) |

## Radius & Shadow

- Radius: button 4 · small 8 · standard card 12 · hero card 16 · pill 9999.
- Standard card shadow (4 layers, max 0.04 opacity):
  ```
  rgba(0,0,0,0.04)  0 4px    18px,
  rgba(0,0,0,0.027) 0 2.025px 7.847px,
  rgba(0,0,0,0.02)  0 0.8px   2.925px,
  rgba(0,0,0,0.01)  0 0.175px 1.041px
  ```
- Deep modal shadow: 5 layers up to `rgba(0,0,0,0.05) 0 23px 52px`.

## Pencil `variables` block (copy-ready)

```json
"variables": {
  "--background":         { "type": "color", "value": "#ffffff" },
  "--background-alt":     { "type": "color", "value": "#f6f5f4" },
  "--card":               { "type": "color", "value": "#ffffff" },
  "--foreground":         { "type": "color", "value": "#0a0a0a" },
  "--muted-foreground":   { "type": "color", "value": "#615d59" },
  "--placeholder":        { "type": "color", "value": "#a39e98" },
  "--primary":            { "type": "color", "value": "#0075de" },
  "--primary-foreground": { "type": "color", "value": "#ffffff" },
  "--primary-active":     { "type": "color", "value": "#005bab" },
  "--ring":               { "type": "color", "value": "#097fe8" },
  "--badge-bg":           { "type": "color", "value": "#f2f9ff" },
  "--badge-fg":           { "type": "color", "value": "#097fe8" },
  "--border":             { "type": "color", "value": "#e6e4e0" },
  "--radius-sm":          { "type": "number", "value": 4 },
  "--radius-md":          { "type": "number", "value": 8 },
  "--radius-lg":          { "type": "number", "value": 12 },
  "--radius-xl":          { "type": "number", "value": 16 },
  "--radius-pill":        { "type": "number", "value": 9999 },
  "--font-primary":       { "type": "string", "value": "Inter" }
}
```

> Note: `rgba(0,0,0,0.1)` whisper borders cannot be variables; the solid `#e6e4e0` above approximates the look. For exact rgba, set `stroke.fill` directly on the frame.

## Component Recipes

**Primary blue button**: `#0075de` fill, white text, 4 px radius, padding `[8,16]`, 15 px Inter 600.

**Secondary button**: `rgba(0,0,0,0.05)` fill, near-black text, 4 px radius.

**Pill badge**: `#f2f9ff` fill, `#097fe8` text, 9999 radius, padding `[4,8]`, 12 px Inter 600, letter-spacing **+0.125 px**.

**Standard card**: white fill, `1px solid rgba(0,0,0,0.1)`, 12 px radius, 4-layer shadow.

**Hero heading**: 64 px Inter weight 700, line-height 1.00, letter-spacing -2.125 px.

## Do / Don't

✓ Warm gray neutrals (yellow-brown undertone) · alternating warm-white sections for rhythm · 4-layer shadows max 0.05 opacity · whisper borders · 4 px radius for buttons · 12 px for cards · positive tracking only on 12-px badges.

✗ No cool/blue grays · no heavy borders · no single-layer drop shadows · no pure black text · no pill (9999) on primary CTAs (pills = badges) · no saturated colors in chrome (only `#0075de`).


path: styles/style-picker.md
content: # Style Picker

> Source: <https://getdesign.md/>
> Quick reference for choosing a style and routing to the correct full spec.
> The full ask-flow lives in `SKILL.md` (Critical Rule 2). This file is read AFTER the user has chosen.

## Decision Flow

```
User said "use <style>"?  ─yes─→  read styles/style-<style>.md
                          ─no──→  ask via ask_followup_question
                                  ↓
        ┌─────────────┬─────────────┬─────────────┐
        Vercel        Linear        Stripe        Notion
        Raycast       Supabase      Airtable      Apple
        Browse more   Default Shadcn Dark
        ↓             ↓             ↓             ↓
        style-X.md    style-X.md    getdesign.md  pen-format.md
                                                  (Shadcn Dark template)
```

## 1-Line Style Cards

| Style | Mode | Identity | Best for |
|-------|------|----------|----------|
| **Vercel** | Light | B&W precision · Geist · shadow-as-border | Dev tools, deployment, tech marketing |
| **Linear** | Dark | Indigo monochrome · Inter weight 510 · luminance-step elevation | SaaS, task management, dark UI |
| **Stripe** | Light | sohne-var weight 300 · blue-tinted shadows · navy headings | Fintech, payment, premium B2B |
| **Notion** | Light | Warm grays · whisper borders · 4-layer soft shadows | Productivity, docs, content apps |
| **Raycast** | Dark | macOS multi-layer shadows · positive tracking · red punctuation | Productivity, premium dark UI |
| **Supabase** | Dark | No shadows · border-hierarchy depth · emerald accent · 1.00 line-height | Dev portals, BaaS, OSS |
| **Airtable** | Light | Haas Grotesk · near-black pill CTA · full-bleed signature surface cards | Data tools, multi-color SaaS, structured data |
| **Apple** | Light/Dark mix | SF Pro · single Action Blue · alternating full-bleed tiles · museum gallery | Premium consumer products, cinematic marketing |
| **Shadcn Dark** | Dark | Zinc palette · unified token system | Generic UI (default fallback) |

## Selection Question (bilingual)

When asking the user, present this menu:

```
Please choose a design style / 请选择设计风格：

1. Vercel    — Monochrome precision, Geist font / 黑白精准，Geist 字体
2. Linear    — Deep dark, indigo accent / 深黑暗色，靛紫强调
3. Stripe    — Clean white, weight-300 luxury / 白底精致，weight-300 优雅
4. Notion    — Warm minimalism / 温暖极简
5. Raycast   — macOS-native dark, red punctuation / macOS 原生深黑，红色点缀
6. Supabase  — Dev-native dark, emerald accent / 开发者深黑，祖母绿强调
7. Airtable  — Editorial light, near-black pill CTA, signature color cards / 编辑感白底，近黑胶囊按钮，签名色块
8. Apple     — Photography-first, single Action Blue, alternating tiles / 照片优先，单一蓝色，明暗交替
9. Browse more / 浏览更多 → https://getdesign.md/
10. Default Shadcn Dark / 使用默认 Shadcn Dark 风格
```

After the user picks: read `style-<choice>.md`, copy its `variables` block into your `.pen` file.

## When User Cannot Decide

Direct them to **https://getdesign.md/** — visual gallery of 66+ brand styles with category filters (AI / dev tools / fintech / automotive / etc.). They browse, name a style, and you continue.

## Conversion Rules: style-spec → `.pen` variables

1. Copy the `variables` JSON block from the chosen `style-*.md` file directly.
2. For dark-first styles (Linear / Raycast / Supabase): set `theme: { "Mode": "Dark" }` on top-level frames.
3. For Apple (mixed-mode): keep top-level frame `Mode: "Light"`. Apply `Mode: "Dark"` only on dark-tile frames where you want true tile inversion.
4. If the style's font is not available in Pencil, fall back to `Inter` and keep the documented weight + letter-spacing values.
5. For rgba-based borders (Linear / Raycast / Notion): the variable file uses a solid hex approximation. For exact rgba, set `stroke.fill` directly on the frame (skipping the variable).
6. Apple's product shadow (`rgba(0,0,0,0.22) 3px 5px 30px`) and Airtable's sub-system rules (pricing-only `pill` radius) must be applied at the component level, not the variables level.

## See Also

- [`pen-format.md`](pen-format.md) — variables block syntax
- [`design-tokens.md`](design-tokens.md) — token system, theming
- Each `style-*.md` — full spec, copy-ready variables block, component recipes


path: styles/style-raycast.md
content: # Style: Raycast

> Source: <https://getdesign.md/>
> macOS-native dark · physical multi-layer shadows · positive letter-spacing · red accent.
> Best for productivity tools, premium dark UI, developer launchers.

## Identity (1-line)

Near-black blue-tinted background (`#07080a`, NOT pure black) with macOS-native multi-layer shadow stacks (outer ring + inset highlight + inset shadow) that simulate physical depth — like glass cards on a dark desk. Inter with **positive letter-spacing** (+0.2 px) and weight 500 baseline for airy dark-mode readability. Raycast Red (`#FF6363`) appears as punctuation only — hero stripes, error states.

## Token Table

| Token | Value | Notes |
|-------|-------|-------|
| Background | `#07080a` | Near-black blue-tinted — NEVER `#000` |
| Surface | `#101111` | Card bg |
| Card surface 2 | `#1b1c1e` | Badges / tags |
| Button dark fg | `#18191a` | Text on light buttons |
| Primary text | `#f9f9f9` | Near-white |
| Secondary | `#cecece` | Body text |
| Tertiary | `#9c9c9d` | Links default / secondary nav |
| Disabled | `#6a6b6c` | |
| Border default | `rgba(255,255,255,0.06)` (≈`#252829`) | Card containment |
| Border interactive | `rgba(255,255,255,0.1)` (≈`#2f3031`) | Buttons |
| Brand Red | `#FF6363` | Hero stripe / error only |
| Interactive Blue | `hsl(202,100%,67%)` ≈ `#55b3ff` | Links / focus |
| Success Green | `hsl(151,59%,59%)` ≈ `#5fc992` | |

## Typography

- Family: `Inter`, mono `GeistMono`.
- Enable `font-feature-settings: "calt","kern","liga","ss03"` on all Inter text. (Hero: `"liga" 0, "ss02", "ss08"`.)
- **Positive letter-spacing** (+0.2 to +0.4 px) on body text — opposite of most dark UIs.
- Weight 500 baseline for body (not 400) — improves dark-mode legibility.

| Role | Size | Weight | Spacing |
|------|------|--------|---------|
| Hero | 64 | 600 | 0 |
| Section | 56 | 400 | +0.2 |
| Card title | 22 | 400 | 0 |
| Sub-heading | 20 | 500 | +0.2 |
| Body | 16 | **500** | +0.2 |
| Button | 16 | 600 | +0.3 |
| Caption | 14 | 500 | +0.2 |
| Small | 12 | 600 | 0 |

## Radius & Shadow

- Radius: micro 2-3 · keys 4-5 · button 6 · input 8 · card 12 · feature 16-20 · primary pill **86+** (full pill).
- macOS-native button shadow (signature):
  ```
  rgba(255,255,255,0.05) 0 1px  0 0 inset,
  rgba(255,255,255,0.25) 0 0    0 1px,
  rgba(0,0,0,0.2)        0 -1px 0 0 inset
  ```
- Card double-ring: `#1b1c1e 0 0 0 1px outer + #07080a 0 0 0 1px inset`.
- Hover transition: **opacity 0.6** (not color change) — Raycast signature.

## Pencil `variables` block (copy-ready)

```json
"variables": {
  "--background":         { "type": "color", "value": "#07080a" },
  "--card":               { "type": "color", "value": "#101111" },
  "--secondary":          { "type": "color", "value": "#1b1c1e" },
  "--foreground":         { "type": "color", "value": "#f9f9f9" },
  "--muted-foreground":   { "type": "color", "value": "#9c9c9d" },
  "--primary":            { "type": "color", "value": "#ffffff" },
  "--primary-foreground": { "type": "color", "value": "#18191a" },
  "--accent":             { "type": "color", "value": "#FF6363" },
  "--info":               { "type": "color", "value": "#55b3ff" },
  "--success":            { "type": "color", "value": "#5fc992" },
  "--border":             { "type": "color", "value": "#252829" },
  "--ring":               { "type": "color", "value": "#55b3ff" },
  "--radius-sm":          { "type": "number", "value": 4 },
  "--radius-md":          { "type": "number", "value": 6 },
  "--radius-lg":          { "type": "number", "value": 12 },
  "--radius-xl":          { "type": "number", "value": 16 },
  "--radius-pill":        { "type": "number", "value": 86 },
  "--font-primary":       { "type": "string", "value": "Inter" }
}
```

## Component Recipes

**Primary pill CTA**: `hsla(0,0%,100%,0.815)` fill, `#18191a` text, 86 radius, padding `[10,20]`. Hover → opacity 1.

**Secondary button**: transparent fill, `1px solid rgba(255,255,255,0.1)`, 6 px radius, white text. Hover → opacity 0.6.

**Card**: `#101111` fill, `1px solid rgba(255,255,255,0.06)`, 16 px radius. Optional double-ring effect.

**Keyboard key**: gradient `#121212→#0d0d0d`, multi-layer shadow, 4 px radius, 12 px Inter 600 text, `#cecece` color.

**Alert (error)**: `#101111` fill, left `2 px solid #FF6363` accent, glow `rgba(255,99,99,0.15)`.

## Do / Don't

✓ `#07080a` not `#000` · positive letter-spacing on body · weight 500 baseline · multi-layer shadows with inset highlights · `rgba(255,255,255,0.06)` borders · pill 86+ for primary, 6-8 for secondary · OpenType `calt,kern,liga,ss03` always · opacity-based hover transitions.

✗ No pure black bg · no negative tracking on body · no single-layer flat shadows · no Raycast Red as decoration (punctuation only) · no weight 400 body text when 500 available · no mixing warm/cool borders.


path: styles/style-stripe.md
content: # Style: Stripe

> Source: <https://getdesign.md/>
> Clean white · weight-300 elegance · blue-tinted shadows · fintech luxury.
> Best for fintech, payment platforms, premium B2B SaaS.

## Identity (1-line)

White canvas + deep navy headings (`#061b31`) + saturated brand purple (`#533afd`). Signature is **sohne-var weight 300 at display sizes** with `"ss01"` stylistic set globally — lightness as luxury. Shadows are blue-tinted (`rgba(50,50,93,0.25)`), giving elevation a brand atmosphere. Border-radius stays conservative (4–8 px).

## Token Table

| Token | Value | Notes |
|-------|-------|-------|
| Background | `#ffffff` | Pure white |
| Heading | `#061b31` | Deep navy — not black |
| Label | `#273951` | Form labels / secondary heading |
| Body | `#64748d` | Slate gray |
| Brand Purple | `#533afd` | CTA / link / interactive |
| Purple Hover | `#4434d4` | |
| Purple Light | `#b9b9f9` | Outlined button border |
| Brand Dark | `#1c1e54` | Dark immersive sections |
| Border default | `#e5edf5` | Card / divider |
| Success | `#15be53` (bg α0.2) / `#108c3d` (text) | |
| Decorative Ruby | `#ea2261` | Gradient only |
| Decorative Magenta | `#f96bee` | Gradient only |

## Typography

- Family: `sohne-var` (fallback `Inter`). Enable `font-feature-settings: "ss01"` on all text.
- Mono: `SourceCodePro`.
- Two-weight simplicity: **300 (default headings + body) / 400 (UI / buttons)**. No bold.
- Letter-spacing scale:

| Size | Spacing |
|------|---------|
| 56 px | -1.4 px |
| 48 px | -0.96 px |
| 32 px | -0.64 px |
| 26 px | -0.26 px |
| 22 px | -0.22 px |
| ≤ 16 px | normal |

- Use `"tnum"` for numeric data (financial tables/charts).

## Radius & Shadow

- Radius: standard 4 · card 5–6 · featured 8. **Never** pill (9999) on primary buttons.
- Signature shadow stack (blue-tinted multi-layer):
  ```
  rgba(50,50,93,0.25) 0 30px 45px -30px,
  rgba(0,0,0,0.1)     0 18px 36px -18px
  ```
- Ambient shadow: `rgba(23,23,23,0.08) 0 15px 35px 0`.

## Pencil `variables` block (copy-ready)

```json
"variables": {
  "--background":         { "type": "color", "value": "#ffffff" },
  "--card":               { "type": "color", "value": "#ffffff" },
  "--foreground":         { "type": "color", "value": "#061b31" },
  "--muted-foreground":   { "type": "color", "value": "#64748d" },
  "--label":              { "type": "color", "value": "#273951" },
  "--primary":            { "type": "color", "value": "#533afd" },
  "--primary-foreground": { "type": "color", "value": "#ffffff" },
  "--primary-hover":      { "type": "color", "value": "#4434d4" },
  "--secondary":          { "type": "color", "value": "#b9b9f9" },
  "--brand-dark":         { "type": "color", "value": "#1c1e54" },
  "--border":             { "type": "color", "value": "#e5edf5" },
  "--success":            { "type": "color", "value": "#15be53" },
  "--success-foreground": { "type": "color", "value": "#108c3d" },
  "--radius-sm":          { "type": "number", "value": 4 },
  "--radius-md":          { "type": "number", "value": 6 },
  "--radius-lg":          { "type": "number", "value": 8 },
  "--font-primary":       { "type": "string", "value": "Inter" }
}
```

## Component Recipes

**Primary button**: `#533afd` fill, white text, 4 px radius, padding `[8,16]`, 16 px sohne-var 400.

**Outlined button**: transparent fill, `1px solid #b9b9f9`, `#533afd` text, 4 px radius.

**Card**: white fill, `1px solid #e5edf5`, 6 px radius, blue-tinted shadow stack above.

**Hero heading**: 48 px sohne-var weight **300**, line-height 1.15, letter-spacing -0.96 px, `#061b31`.

**Dark section**: `#1c1e54` bg, white text, 32 px weight 300 heading.

## Do / Don't

✓ Weight 300 for everything except UI · `"ss01"` on every sohne-var text · blue-tinted shadows · `#061b31` not `#000` · radius 4–8 only · `"tnum"` for tabular numbers.

✗ No weight 600/700 in headings · no pill shapes on buttons/cards · no neutral gray shadows (always tint blue) · no positive letter-spacing at display · no Ruby/Magenta on buttons (decorative only).


path: styles/style-supabase.md
content: # Style: Supabase

> Source: <https://getdesign.md/>
> Dark developer-native · emerald accent · **no shadows** · border-hierarchy depth · terminal aesthetic.
> Best for backend-as-a-service, developer portals, open-source project sites.

## Identity (1-line)

Dark canvas (`#171717` page, `#0f0f0f` buttons) with depth expressed entirely through a **border color hierarchy** (`#242424` → `#2e2e2e` → `#363636`) — Supabase deliberately avoids shadows. Circular at weight 400 baseline with **1.00 line-height** at hero size (terminal density). Emerald green (`#3ecf8e` / `#00c573`) is an identity marker — used only for logo, links, and accent borders.

## Token Table

| Token | Value | Notes |
|-------|-------|-------|
| Background | `#171717` | Page bg — NEVER lighten above this |
| Button bg | `#0f0f0f` | Deeper than page |
| Border subtle | `#242424` | Horizontal rules |
| Border default | `#2e2e2e` | Cards |
| Border prominent | `#363636` | Buttons / interactive |
| Border heavier | `#393939` | Dividers |
| Border (charcoal) | `#434343` | Tertiary |
| Primary text | `#fafafa` | |
| Secondary text | `#b4b4b4` | |
| Muted text | `#898989` | Links default |
| Disabled | `#6a6b6c` | |
| Brand green | `#3ecf8e` | Logo / accent borders |
| Link green | `#00c573` | Interactive |
| Green border accent | `rgba(62,207,142,0.3)` | Brand-highlighted elements |

## Typography

- Family: `Circular` (fallback `Inter`).
- Mono: `Source Code Pro` — used in **uppercase with letter-spacing 1.2 px** for technical labels.
- **Two weights only**: 400 (everything) / 500 (nav links, button labels). No bold.

| Role | Size | Weight | Line height | Spacing |
|------|------|--------|-------------|---------|
| Hero | 72 | 400 | **1.00** | normal |
| Section | 36 | 400 | 1.25 | normal |
| Card title | 24 | 400 | 1.33 | -0.16 px |
| Sub-heading | 18 | 400 | 1.56 | normal |
| Body | 16 | 400 | 1.50 | normal |
| Nav / Button | 14 | 500 | 1.14-1.43 | normal |
| Code label | 12 (mono) | 400 | 1.33 | +1.2 px UPPERCASE |

## Radius & Depth

- Radius: ghost 6 · cards 8-16 · primary CTA **9999 (full pill)**.
- **No box-shadows** in dark theme. Depth = border color hierarchy:
  - Default: `1px solid #2e2e2e`
  - Hover: `1px solid #363636` or `#393939`
  - Brand-elevated: `1px solid rgba(62,207,142,0.3)` (green border = "elevated")
  - Focus only: `rgba(0,0,0,0.1) 0 4px 12px`

## Pencil `variables` block (copy-ready)

```json
"variables": {
  "--background":         { "type": "color", "value": "#171717" },
  "--card":               { "type": "color", "value": "#171717" },
  "--popover":            { "type": "color", "value": "#0f0f0f" },
  "--foreground":         { "type": "color", "value": "#fafafa" },
  "--muted-foreground":   { "type": "color", "value": "#898989" },
  "--secondary-foreground": { "type": "color", "value": "#b4b4b4" },
  "--primary":            { "type": "color", "value": "#0f0f0f" },
  "--primary-foreground": { "type": "color", "value": "#fafafa" },
  "--accent":             { "type": "color", "value": "#3ecf8e" },
  "--accent-link":        { "type": "color", "value": "#00c573" },
  "--border":             { "type": "color", "value": "#2e2e2e" },
  "--border-strong":      { "type": "color", "value": "#363636" },
  "--border-subtle":      { "type": "color", "value": "#242424" },
  "--ring":               { "type": "color", "value": "#3ecf8e" },
  "--radius-sm":          { "type": "number", "value": 6 },
  "--radius-md":          { "type": "number", "value": 8 },
  "--radius-lg":          { "type": "number", "value": 16 },
  "--radius-pill":        { "type": "number", "value": 9999 },
  "--font-primary":       { "type": "string", "value": "Inter" }
}
```

## Component Recipes

**Primary pill CTA**: `#0f0f0f` fill, `#fafafa` text, **9999 radius**, padding `[8,32]`, `1px solid #fafafa` border, 14 px Circular 500.

**Secondary pill**: same as primary but `1px solid #2e2e2e`, opacity 0.8.

**Ghost button**: transparent fill, `#fafafa` text, 6 px radius, padding `[8]`.

**Card**: `#171717` fill, `1px solid #2e2e2e`, 8-16 px radius, 16-24 px padding. **No `effect` shadows.**

**Pill tab**: 9999 radius, `1px solid #2e2e2e`. Active = green accent border `rgba(62,207,142,0.3)` or lighter surface.

**Code label**: Source Code Pro 12 px, UPPERCASE, letter-spacing +1.2 px, `#898989`.

**Hero heading**: 72 px Circular weight 400, **line-height 1.00**, `#fafafa`.

## Do / Don't

✓ Background ≤ `#171717` always · depth via border hierarchy `#242424 → #2e2e2e → #363636` · weight 400 baseline (500 only for nav/buttons) · hero line-height **1.00** · 9999 radius for primary, 6-16 for everything else · green only for logo/links/accent borders · uppercase Source Code Pro for technical labels.

✗ **No `effect` shadows** — they're invisible on dark and break the system · no weight 700 · no green on backgrounds/large surfaces · no warm colors as primary · no hero line-height > 1.00 · no medium radius (10-15) — pill or 6-16, nothing between · no surfaces lighter than `#171717`.


path: styles/style-vercel.md
content: # Style: Vercel

> Source: <https://getdesign.md/>
> Monochrome precision · Geist font · shadow-as-border · minimal engineering aesthetic.
> Best for dev tools, deployment platforms, technical landing pages.

## Identity (1-line)

White canvas (`#ffffff`) + near-black text (`#171717`) + Geist Sans with **aggressive negative letter-spacing** at display sizes. CSS borders replaced by `box-shadow 0 0 0 1px rgba(0,0,0,0.08)` (shadow-as-border). Workflow accents (Red/Pink/Blue) only mark pipeline stages.

## Token Table

| Token | Value | Notes |
|-------|-------|-------|
| Background | `#ffffff` | Pure white |
| Foreground | `#171717` | Slightly warm — never `#000` |
| Muted text | `#4d4d4d` (gray-600) / `#666666` (gray-500) | Body / tertiary |
| Border (shadow) | `rgba(0,0,0,0.08)` | Used as `0 0 0 1px` shadow |
| Border (light) | `#ebebeb` | Image / tab ring |
| Card surface | `#ffffff` | Same as bg |
| Subtle surface | `#fafafa` | Inner-ring highlight |
| Primary | `#171717` | Black CTA on white |
| Primary fg | `#ffffff` | |
| Link | `#0072f5` | |
| Focus ring | `hsla(212,100%,48%,1)` | 2 px outline |
| Workflow Ship | `#ff5b4f` | Red — production marker only |
| Workflow Preview | `#de1d8d` | Pink — preview marker only |
| Workflow Develop | `#0a72ef` | Blue — dev marker only |

## Typography

- Family: `Geist`, mono `Geist Mono`. Enable `"liga"` globally on all Geist text.
- Weights: **400 / 500 / 600** (no 700 except 7-px micro badges).
- Letter-spacing scale (tighter at larger sizes):

| Size | Spacing |
|------|---------|
| 48 px | -2.4 to -2.88 px |
| 32 px | -1.28 px |
| 24 px | -0.96 px |
| 16 px | -0.32 px (semibold) / normal |
| ≤ 14 px | normal |

## Radius & Shadow

- Radius: buttons 6 · cards 8 · image cards 12 · pills 9999.
- Card stack (the famous Vercel shadow):
  ```
  rgba(0,0,0,0.08) 0 0 0 1px,
  rgba(0,0,0,0.04) 0 2px 2px,
  rgba(0,0,0,0.04) 0 8px 8px -8px,
  #fafafa 0 0 0 1px
  ```
  The inner `#fafafa` ring is what makes the card "glow" — never skip it.

## Pencil `variables` block (copy-ready)

```json
"variables": {
  "--background":         { "type": "color", "value": "#ffffff" },
  "--foreground":         { "type": "color", "value": "#171717" },
  "--muted-foreground":   { "type": "color", "value": "#4d4d4d" },
  "--primary":            { "type": "color", "value": "#171717" },
  "--primary-foreground": { "type": "color", "value": "#ffffff" },
  "--card":               { "type": "color", "value": "#ffffff" },
  "--border":             { "type": "color", "value": "#ebebeb" },
  "--ring":               { "type": "color", "value": "#0072f5" },
  "--accent-ship":        { "type": "color", "value": "#ff5b4f" },
  "--accent-preview":     { "type": "color", "value": "#de1d8d" },
  "--accent-develop":     { "type": "color", "value": "#0a72ef" },
  "--radius-md":          { "type": "number", "value": 6 },
  "--radius-lg":          { "type": "number", "value": 8 },
  "--radius-pill":        { "type": "number", "value": 9999 },
  "--font-primary":       { "type": "string", "value": "Geist" }
}
```

## Component Recipes

**Primary button**: `#171717` bg, white text, 6 px radius, padding `[8,16]`, 14 px Geist 500.

**Outlined button**: white bg, 6 px radius, no `stroke` — use `effect` shadow `0 0 0 1px rgba(0,0,0,0.08)` (when supported), or fall back to `stroke: { thickness: 1, fill: "$--border" }`.

**Pill badge**: `#ebf5ff` bg, `#0068d6` text, 9999 radius, padding `[0,10]`, 12 px weight 500.

## Do / Don't

✓ Negative letter-spacing at display · `"liga"` always on · 3-weight system · `#171717` not `#000` · multi-layer card shadow with inner `#fafafa` ring.

✗ No positive tracking on Geist · no weight 700 on body · no traditional CSS `border` on cards · no warm colors in chrome · no pill (9999) on primary buttons (pills = badges only).


path: references/codegen-mapping.md
content: # Tailwind + shadcn/ui Mapping Reference

> **Applies to**: Code generation (Mode A + Mode B)
> **See also**: [codegen-workflow.md](codegen-workflow.md) · [design-tokens.md](design-tokens.md) · [responsive.md](responsive.md)

Quick-reference mapping from Pencil design properties to Tailwind v4 + shadcn/ui code.

---

## Color Tokens

### Pencil Variable → Tailwind Utility

| Pencil Variable | Background `bg-*` | Text `text-*` | Border `border-*` |
|----------------|------------------|--------------|------------------|
| `primary` | `bg-primary` | `text-primary` | `border-primary` |
| `primary-foreground` | `bg-primary-foreground` | `text-primary-foreground` | — |
| `secondary` | `bg-secondary` | `text-secondary` | `border-secondary` |
| `secondary-foreground` | — | `text-secondary-foreground` | — |
| `background` | `bg-background` | — | — |
| `foreground` | — | `text-foreground` | — |
| `muted` | `bg-muted` | — | — |
| `muted-foreground` | — | `text-muted-foreground` | — |
| `accent` | `bg-accent` | — | — |
| `accent-foreground` | — | `text-accent-foreground` | — |
| `destructive` | `bg-destructive` | `text-destructive` | `border-destructive` |
| `destructive-foreground` | — | `text-destructive-foreground` | — |
| `card` | `bg-card` | — | — |
| `card-foreground` | — | `text-card-foreground` | — |
| `border` | — | — | `border-border` |
| `ring` | — | — | `ring-ring` |

> `@theme` rule: All color variables use the `--color-` prefix, e.g. `--color-primary`, `--color-border`.

### Common Combinations

| Design Intent | Tailwind Classes |
|--------------|-----------------|
| Primary button | `bg-primary text-primary-foreground` |
| Secondary button | `bg-secondary text-secondary-foreground` |
| Destructive button | `bg-destructive text-destructive-foreground` |
| Ghost button | `hover:bg-accent hover:text-accent-foreground` |
| Outline button | `border border-border bg-background hover:bg-accent` |
| Card surface | `bg-card text-card-foreground border border-border` |
| Muted text | `text-muted-foreground` |
| Page background | `bg-background text-foreground` |
| Input field | `border border-border bg-background text-foreground placeholder:text-muted-foreground` |
| Badge | `bg-primary text-primary-foreground` or `bg-secondary text-secondary-foreground` |

---

## Radius Tokens

| Pencil Variable | `@theme` Declaration | Tailwind Utility |
|----------------|---------------------|-----------------|
| `radius-sm` | `--radius-sm: 0.25rem` | `rounded-sm` |
| `radius-md` | `--radius-md: 0.375rem` | `rounded-md` |
| `radius-lg` | `--radius-lg: 0.5rem` | `rounded-lg` |
| `radius-xl` | `--radius-xl: 0.75rem` | `rounded-xl` |

---

## Layout Properties

| Pencil Property | Tailwind Class |
|----------------|----------------|
| `layout: "vertical"` | `flex flex-col` |
| `layout: "horizontal"` | `flex` or `flex flex-row` |
| `gap: 4` | `gap-1` |
| `gap: 8` | `gap-2` |
| `gap: 12` | `gap-3` |
| `gap: 16` | `gap-4` |
| `gap: 20` | `gap-5` |
| `gap: 24` | `gap-6` |
| `gap: 32` | `gap-8` |
| `padding: 8` | `p-2` |
| `padding: 12` | `p-3` |
| `padding: 16` | `p-4` |
| `padding: 20` | `p-5` |
| `padding: 24` | `p-6` |
| `padding: 32` | `p-8` |
| `width: "fill_container"` | `w-full` |
| `height: "fill_container"` | `h-full` or `flex-1` |
| `alignItems: "center"` | `items-center` |
| `alignItems: "start"` | `items-start` |
| `alignItems: "end"` | `items-end` |
| `justifyContent: "center"` | `justify-center` |
| `justifyContent: "space-between"` | `justify-between` |
| `justifyContent: "end"` | `justify-end` |

---

## Typography

| Pencil Property | Tailwind Class |
|----------------|----------------|
| `fontSize: 12` | `text-xs` |
| `fontSize: 14` | `text-sm` |
| `fontSize: 16` | `text-base` |
| `fontSize: 18` | `text-lg` |
| `fontSize: 20` | `text-xl` |
| `fontSize: 24` | `text-2xl` |
| `fontSize: 30` | `text-3xl` |
| `fontSize: 36` | `text-4xl` |
| `fontSize: 48` | `text-5xl` |
| `fontWeight: "400"` | `font-normal` |
| `fontWeight: "500"` | `font-medium` |
| `fontWeight: "600"` | `font-semibold` |
| `fontWeight: "700"` | `font-bold` |

---

## Pencil Component → shadcn/ui Mapping

| Pencil Component | shadcn/ui Component | Key Classes |
|-----------------|--------------------|----|
| Button | `<Button>` | `bg-primary text-primary-foreground rounded-md` |
| Card | `<Card>` | `rounded-lg border border-border bg-card text-card-foreground shadow-sm` |
| Input | `<Input>` | `rounded-md border border-border bg-background` |
| Select | `<Select>` | Radix primitives |
| Badge | `<Badge>` | `rounded-md bg-primary text-primary-foreground` |
| Avatar | `<Avatar>` | `rounded-full` |
| Separator | `<Separator>` | `bg-border` |
| Switch | `<Switch>` | Radix primitives |
| Checkbox | `<Checkbox>` | Radix primitives |
| Dialog | `<Dialog>` | `bg-background border border-border rounded-lg shadow-lg` |
| Tabs | `<Tabs>` | TabsList: `bg-muted rounded-md` |
| Table | `<Table>` | `border-border text-foreground` |
| Tooltip | `<Tooltip>` | `bg-primary text-primary-foreground rounded-md` |
| Label | `<Label>` | `text-sm font-medium` |

---

## Icon Mapping (Material → Lucide)

| Pencil (Material) | Lucide Component | Recommended Size |
|-------------------|-----------------|-----------------|
| `search` | `<Search />` | `size-4` or `size-5` |
| `close` | `<X />` | `size-4` |
| `menu` | `<Menu />` | `size-5` |
| `arrow_forward` | `<ArrowRight />` | `size-4` |
| `arrow_back` | `<ArrowLeft />` | `size-4` |
| `person` | `<User />` | `size-4` |
| `settings` | `<Settings />` | `size-4` |
| `home` | `<Home />` | `size-4` or `size-5` |
| `notifications` | `<Bell />` | `size-4` |
| `edit` | `<Pencil />` | `size-4` |
| `delete` | `<Trash2 />` | `size-4` |
| `add` | `<Plus />` | `size-4` |
| `check` | `<Check />` | `size-4` |
| `visibility` | `<Eye />` | `size-4` |
| `visibility_off` | `<EyeOff />` | `size-4` |
| `chevron_right` | `<ChevronRight />` | `size-4` |
| `chevron_down` | `<ChevronDown />` | `size-4` |
| `more_vert` | `<MoreVertical />` | `size-4` |
| `more_horiz` | `<MoreHorizontal />` | `size-4` |
| `favorite` | `<Heart />` | `size-4` |
| `star` | `<Star />` | `size-4` |
| `download` | `<Download />` | `size-4` |
| `upload` | `<Upload />` | `size-4` |
| `filter_list` | `<Filter />` | `size-4` |
| `sort` | `<ArrowUpDown />` | `size-4` |
| `mail` | `<Mail />` | `size-4` |
| `calendar_today` | `<Calendar />` | `size-4` |
| `logout` | `<LogOut />` | `size-4` |

All Lucide icons accept `className` for sizing: `<Search className="size-4" />`

---

## Anti-Patterns Quick Reference

These patterns indicate a code generation error — fix immediately:

```
❌ Wrong                         ✅ Correct
────────────────────────────────────────────────
bg-[#3b82f6]                    bg-primary
text-[#ffffff]                  text-primary-foreground
text-[var(--primary)]           text-primary
bg-[var(--secondary)]           bg-secondary
rounded-[6px]                   rounded-md
rounded-[var(--radius-md)]      rounded-md
border-[#e2e8f0]                border-border
ring-[var(--ring)]              ring-ring
border-[1px]                    border
opacity-[0.5]                   opacity-50
gap-[16px]                      gap-4
p-[24px]                        p-6
text-[14px]                     text-sm
```

---

## See Also

- [codegen-workflow.md](codegen-workflow.md) — Complete code generation workflow
- [design-tokens.md](design-tokens.md) — Reading and mapping Pencil design tokens
- [responsive.md](responsive.md) — Artboard sizes to Tailwind breakpoints


path: references/codegen-workflow.md
content: # Design-to-Code Workflow

> Read this when generating code from a `.pen` file.
> Companion: [`codegen-mapping.md`](codegen-mapping.md) (quick mapping tables) · [`design-tokens.md`](design-tokens.md) · [`responsive.md`](responsive.md).

## Target Stack

React / Next.js · TypeScript · **Tailwind CSS v4** · shadcn/ui · Lucide Icons · CVA + `cn()`.

## Workflow (7 steps)

```
1. Load `frontend-design` skill        (MANDATORY — aesthetic direction)
2. pencil_get_guidelines topic=code|tailwind   (Mode B)
3. pencil_get_variables                (read tokens → @theme)
4. pencil_batch_get tree               (read design hierarchy)
5. pencil_batch_get reusable=true      (find components for shadcn mapping)
6. Generate code (CSS @theme + components + page)
7. (Optional) pencil_set_variables     (sync token changes back)
```

## Step 3 — Variable → Tailwind utility (1:1)

| Pencil variable | `@theme` declaration | Utility |
|----------------|---------------------|---------|
| `--background` | `--color-background` | `bg-background` |
| `--foreground` | `--color-foreground` | `text-foreground` |
| `--primary` | `--color-primary` | `bg-primary` |
| `--primary-foreground` | `--color-primary-foreground` | `text-primary-foreground` |
| `--muted` / `--muted-foreground` | `--color-muted` / `--color-muted-foreground` | `bg-muted` / `text-muted-foreground` |
| `--border` | `--color-border` | `border-border` |
| `--ring` | `--color-ring` | `ring-ring` |
| `--radius-md` | `--radius-md` | `rounded-md` |

**Prefix rules**: colors MUST use `--color-*`, radii MUST use `--radius-*`. Tailwind v4 auto-generates utilities from these prefixes.

Full token mapping: [`design-tokens.md`](design-tokens.md).

## Step 5 — Component mapping (Pencil → shadcn/ui)

| Pencil name (any of) | shadcn component | Import |
|---|---|---|
| Button / Btn | `<Button>` | `@/components/ui/button` |
| Card / Tile / Panel | `<Card>` + `<CardHeader>` `<CardContent>` `<CardFooter>` | `@/components/ui/card` |
| Input / TextField | `<Input>` | `@/components/ui/input` |
| Select / Dropdown | `<Select>` + Trigger/Content/Item | `@/components/ui/select` |
| Checkbox / Switch | `<Checkbox>` / `<Switch>` | matching path |
| Badge / Tag / Chip | `<Badge>` | `@/components/ui/badge` |
| Avatar | `<Avatar>` + Image/Fallback | `@/components/ui/avatar` |
| Dialog / Modal | `<Dialog>` + Trigger/Content | `@/components/ui/dialog` |
| Tabs / TabBar | `<Tabs>` + List/Trigger/Content | `@/components/ui/tabs` |
| Table / DataTable | `<Table>` + Header/Row/Cell | `@/components/ui/table` |
| Tooltip · Label · Separator | matching shadcn primitive | matching path |

For unmatched components: build with **CVA + `cn()` + React 19 ref-as-prop** (same conventions as shadcn primitives).

If unsure whether a shadcn component exists, query the registry:
```
shadcn_search_items_in_registries({ registries: ["@shadcn"], query: "data table" })
shadcn_view_items_in_registries({ items: ["@shadcn/data-table"] })
```

## Step 6 — Code patterns

### CSS setup (`app.css` / `globals.css`)

```css
@import "tailwindcss";

@theme {
  --color-background: oklch(100% 0 0);
  --color-foreground: oklch(14.5% 0.025 264);
  --color-primary: oklch(14.5% 0.025 264);
  --color-primary-foreground: oklch(98% 0.01 264);
  --color-muted: oklch(96% 0.01 264);
  --color-muted-foreground: oklch(46% 0.02 264);
  --color-border: oklch(91% 0.01 264);
  /* ... other --color-* and --radius-* tokens */

  --radius-sm: 0.25rem;
  --radius-md: 0.375rem;
  --radius-lg: 0.5rem;
}

@custom-variant dark (&:where(.dark, .dark *));

.dark {
  --color-background: oklch(14.5% 0.025 264);
  --color-foreground: oklch(98% 0.01 264);
  /* ... dark overrides */
}

@layer base {
  * { @apply border-border; }
  body { @apply bg-background text-foreground antialiased; }
}
```

Rules: (1) prefer OKLCH (convert hex if needed), (2) prefixes are required, (3) NO `tailwind.config.ts` in v4.

### Custom component (CVA pattern)

```tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const statusBadge = cva(
  "inline-flex items-center rounded-md px-2 py-1 text-xs font-medium",
  {
    variants: {
      status: {
        active:   "bg-primary text-primary-foreground",
        inactive: "bg-muted text-muted-foreground",
        error:    "bg-destructive text-destructive-foreground",
      },
    },
    defaultVariants: { status: "active" },
  }
)

interface Props
  extends React.HTMLAttributes<HTMLSpanElement>,
    VariantProps<typeof statusBadge> {}

export function StatusBadge({ className, status, ...props }: Props) {
  return <span className={cn(statusBadge({ status, className }))} {...props} />
}
```

Note: semantic classes only · React 19 (no `forwardRef`) · `cn()` for merging.

### `lib/utils.ts` (must exist)

```ts
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) }
```

## Responsive (multi-artboard)

When the `.pen` file has artboards at multiple widths (375 / 768 / 1280 etc.):

1. Read all artboards, compare structure
2. Generate **mobile-first** (base = smallest artboard)
3. Use `md:` `lg:` `xl:` to override
4. **Never hardcode artboard widths** — use `w-full`, `max-w-7xl mx-auto`, responsive grid columns

Full breakpoint mapping: [`responsive.md`](responsive.md).

## Layout property mapping (essentials)

| Pencil | Tailwind |
|---|---|
| `layout: "vertical"` | `flex flex-col` |
| `layout: "horizontal"` | `flex` |
| `gap: 16` / `24` | `gap-4` / `gap-6` |
| `padding: 16` / `24` | `p-4` / `p-6` |
| `width: "fill_container"` | `w-full` or `flex-1` |
| `alignItems: "center"` | `items-center` |
| `justifyContent: "space_between"` | `justify-between` |

Full mapping: [`codegen-mapping.md`](codegen-mapping.md).

## Icon mapping (Material → Lucide, common)

`search→Search` · `close→X` · `menu→Menu` · `arrow_forward→ArrowRight` · `arrow_back→ArrowLeft` · `person→User` · `settings→Settings` · `home→Home` · `notifications→Bell` · `edit→Pencil` · `delete→Trash2` · `add→Plus` · `check→Check` · `visibility→Eye` · `visibility_off→EyeOff` · `chevron_right→ChevronRight` · `chevron_down→ChevronDown` · `more_vert→MoreVertical` · `more_horiz→MoreHorizontal` · `mail→Mail` · `calendar_today→Calendar` · `favorite→Heart` · `star→Star` · `download→Download` · `upload→Upload` · `filter_list→Filter` · `sort→ArrowUpDown` · `logout→LogOut`.

Pencil files use `iconFontFamily: "lucide"` directly — names usually match Lucide kebab-case (`folder-open`, `chevron-down`).

## Rules Summary

✓ Load `frontend-design` skill · semantic Tailwind utilities only · `--color-*` / `--radius-*` prefixes · OKLCH preferred · CVA + `cn()` · React 19 (`ref` as prop) · TypeScript · Lucide icons · mobile-first · split files for multi-component screens.

✗ Arbitrary values (`bg-[#fff]`, `rounded-[6px]`, `p-[24px]`, `text-[14px]`) · `tailwind.config.ts` · `@tailwind base/components/utilities` · `forwardRef` · monolithic single-file pages · hardcoded artboard widths (`w-[375px]`).


path: references/component-reuse.md
content: # Design System Components

> **Applies to**: Mode A (Direct File Writing) + Mode B (MCP Tools)
> **See also**: [design-tokens.md](design-tokens.md) · [codegen-workflow.md](codegen-workflow.md)

---

## Core Principle: Reuse Over Rebuild

> **Inspired by Stitch** — Great design systems are built on **component thinking**: identify reusable UI elements, organize them into a shared component library, and maintain consistency through instance inheritance rather than redundant rebuilds. Stitch treats this as a fundamental workflow assumption.

Rebuilding existing components from scratch causes:

- Design inconsistencies (subtle padding, color, font-weight differences)
- Design system updates failing to propagate to manually rebuilt elements
- Code generation producing duplicate, non-DRY components
- File size bloat from redundant nodes

---

## Workflow: Discover and Use Components

### Step 1 — List All Reusable Components

**Run this at the start of every design task**:

```
pencil_batch_get({
  filePath: "path/to/file.pen",
  patterns: [{ reusable: true }],
  readDepth: 2,
  searchDepth: 3
})
```

Example result:

```json
{
  "id": "btn-primary",
  "name": "Button",
  "type": "frame",
  "reusable": true,
  "children": [
    { "id": "btn-label", "type": "text", "content": "Button" }
  ]
}
```

### Step 2 — Identify the Right Component

Evaluate reusability along these dimensions:

| Dimension | How to Judge |
|-----------|-------------|
| **Name match** | Button / Card / Input / NavBar / Avatar, etc. |
| **Structure match** | Need a card with image + title + description? Find a component with that structure. |
| **Variant match** | Design system may have multiple variants (Button Primary / Secondary) |

### Step 3 — Insert as a Ref Instance

Use the component's `id` as the `ref` value:

```javascript
btn=I("parentFrameId", { type: "ref", ref: "btn-primary", width: "fill_container" })
```

This creates a connected instance. Edits to the main component automatically update all instances.

### Step 4 — Customize via Descendants

Override specific properties on descendant paths:

```javascript
// Change button label text
U(btn+"/btn-label", { content: "Submit" })

// Deep nested path
U(btn+"/icon-container/icon", { iconFontName: "check" })
```

### Step 5 — Replace Slot Content

When a component has placeholder/slot areas, use Replace to swap content:

```javascript
newContent=R(btn+"/content-slot", { type: "text", content: "Custom content" })
```

---

## When to Create a New Component

Only create a new component from scratch when:

1. After checking `reusable: true`, **no similar component exists**
2. The existing component is **structurally different** (not just a color or text change)
3. You are building a **brand-new design system** (empty file)

When creating, set `reusable: true` to enable future reuse.

---

## Pre-Design Checklist

- [ ] Ran `pencil_batch_get` with `{ reusable: true }` to list all components?
- [ ] Checked whether these exist: buttons, inputs, cards, navbars, headers, footers, modals, badges, avatars, table rows?
- [ ] Inserting as `ref` instances (not rebuilding the structure)?
- [ ] Customizing instances via `U()` on descendant paths (not replacing the whole component)?

---

## Common Design System Components Quick Reference

| Need | Search for names containing |
|------|-----------------------------|
| Button | button, btn, cta |
| Text input | input, field, text-field |
| Card | card, tile, panel |
| Navigation | nav, navbar, sidebar, menu |
| Header | header, topbar, appbar |
| Footer | footer, bottom-bar |
| Modal/Dialog | modal, dialog, sheet |
| Badge/Tag | badge, tag, chip, label |
| Avatar | avatar, profile-pic |
| Table row | row, table-row, list-item |
| Icon | icon, symbol |
| Checkbox/Radio | checkbox, radio, toggle, switch |
| Select/Dropdown | select, dropdown, picker |
| Tabs | tab, tab-bar, segment |

---

## See Also

- [design-tokens.md](design-tokens.md) — Use variables when styling component instances
- [codegen-workflow.md](codegen-workflow.md) — Map reusable components to shadcn/ui
- [codegen-mapping.md](codegen-mapping.md) — Pencil component → shadcn/ui mapping table


path: references/design-tokens.md
content: # Variables and Design Tokens

> **Applies to**: Mode A (Direct File Writing) + Mode B (MCP Tools)
> **See also**: [codegen-mapping.md](codegen-mapping.md) · [codegen-workflow.md](codegen-workflow.md)

---

## Core Principle: Variables Are Contracts

> **Inspired by Stitch** — Stitch treats design tokens as the design system's "contract": colors, radii, and fonts are all bound through named variables, not isolated magic numbers. The essence of `DESIGN.md` is making these contracts machine-readable so AI can strictly enforce them when generating UI.

Hardcoded values (`fill: "#3b82f6"`, `cornerRadius: 8`) will:

- Force code generation to emit arbitrary values like `bg-[#3b82f6]`, making theming impossible
- Break dark mode (values don't adapt to theme changes)
- Require manual find-and-replace for global design system updates
- Disconnect the design from the codebase's token system

---

## Workflow: Read and Apply Variables

### Step 1 — Read All Variables (MCP Mode)

**Before starting any design task**:

```
pencil_get_variables({ filePath: "path/to/file.pen" })
```

Example output:

```json
{
  "variables": {
    "primary":    { "value": "#3b82f6" },
    "background": { "value": "#ffffff" },
    "foreground": { "value": "#0a0a0a" },
    "border":     { "value": "#e2e8f0" },
    "radius-md":  { "value": 6 }
  }
}
```

### Step 2 — Map Values to Variables

| Style needed | ❌ Don't use | ✅ Use variable instead |
|-------------|-------------|------------------------|
| Brand blue | `fill: "#3b82f6"` | `fill: "$--primary"` |
| White text on primary | `fill: "#ffffff"` | `fill: "$--primary-foreground"` |
| Border color | `stroke.fill: "#e2e8f0"` | `stroke.fill: "$--border"` |
| Medium radius | `cornerRadius: 6` | `cornerRadius: "$--radius-md"` |
| Page background | `fill: "#ffffff"` | `fill: "$--background"` |
| Body text color | `fill: "#0a0a0a"` | `fill: "$--foreground"` |

### Step 3 — Apply Variables in .pen Files

Variable reference syntax: prefix the variable name with `$`:

```json
{
  "fill": "$--background",
  "stroke": { "fill": "$--border" },
  "cornerRadius": "$--radius-md",
  "fontFamily": "$--font-primary"
}
```

Supported on: `fill`, `stroke.fill`, `cornerRadius`, `fontFamily`

### Step 4 — Create Missing Variables

If a needed token doesn't exist, create it immediately — **never hardcode**:

```
pencil_set_variables({
  filePath: "path/to/file.pen",
  variables: {
    "accent":            { "value": "#f59e0b" },
    "accent-foreground": { "value": "#ffffff" }
  }
})
```

---

## Theme Support (Multi-Value Variables)

Variables can hold different values per theme:

```json
{
  "--background": {
    "type": "color",
    "value": [
      { "value": "#fafafa" },
      { "value": "#09090b", "theme": { "Mode": "Dark" } }
    ]
  }
}
```

With themed variables, designs automatically adapt when switching themes. Hardcoded values completely break this mechanism.

---

## Variable-to-Code Mapping (Tailwind v4 + shadcn/ui)

> **Inspired by Stitch cross-tool interoperability** — Design tokens should not be locked inside the design tool. They should flow into code. Pencil variables map directly to Tailwind `@theme` declarations and semantic utility classes.

### Color Token Mapping

| Pencil Variable | `@theme` Declaration | Tailwind Utility |
|----------------|---------------------|-----------------|
| `primary` | `--color-primary` | `bg-primary` / `text-primary` / `border-primary` |
| `primary-foreground` | `--color-primary-foreground` | `text-primary-foreground` |
| `secondary` | `--color-secondary` | `bg-secondary` / `text-secondary` |
| `secondary-foreground` | `--color-secondary-foreground` | `text-secondary-foreground` |
| `background` | `--color-background` | `bg-background` |
| `foreground` | `--color-foreground` | `text-foreground` |
| `muted` | `--color-muted` | `bg-muted` |
| `muted-foreground` | `--color-muted-foreground` | `text-muted-foreground` |
| `accent` | `--color-accent` | `bg-accent` |
| `accent-foreground` | `--color-accent-foreground` | `text-accent-foreground` |
| `destructive` | `--color-destructive` | `bg-destructive` / `text-destructive` |
| `card` | `--color-card` | `bg-card` |
| `card-foreground` | `--color-card-foreground` | `text-card-foreground` |
| `border` | `--color-border` | `border-border` |
| `ring` | `--color-ring` | `ring-ring` |

### Radius Token Mapping

| Pencil Variable | `@theme` Declaration | Tailwind Utility |
|----------------|---------------------|-----------------|
| `radius-sm` | `--radius-sm: 0.25rem` | `rounded-sm` |
| `radius-md` | `--radius-md: 0.375rem` | `rounded-md` |
| `radius-lg` | `--radius-lg: 0.5rem` | `rounded-lg` |
| `radius-xl` | `--radius-xl: 0.75rem` | `rounded-xl` |

### Code Generation: No Arbitrary Values

The rule is simple: **if a Pencil variable exists for a value, there is a corresponding semantic utility. Use the utility, not arbitrary syntax.**

| ❌ Wrong (arbitrary) | ✅ Correct (semantic utility) |
|---------------------|------------------------------|
| `bg-[#3b82f6]` | `bg-primary` |
| `text-[#ffffff]` | `text-primary-foreground` |
| `text-[var(--primary)]` | `text-primary` |
| `rounded-[6px]` | `rounded-md` |
| `border-[#e2e8f0]` | `border-border` |

### Opacity Modifiers

```
bg-primary/90       → primary color at 90% opacity
text-foreground/70  → foreground color at 70% opacity
border-border/50    → border color at 50% opacity
```

Use these instead of arbitrary `opacity` values or `color-mix()`.

---

## Variable Categories Quick Reference

| Category | Common Variable Names | Tailwind Prefix |
|----------|----------------------|----------------|
| Brand colors | `primary`, `secondary`, `accent` | `--color-*` |
| Semantic colors | `destructive`, `success`, `warning`, `info` | `--color-*` |
| Surface colors | `background`, `foreground`, `card`, `card-foreground` | `--color-*` |
| UI colors | `border`, `ring`, `muted`, `muted-foreground` | `--color-*` |
| Border radius | `radius-sm/md/lg/xl` | `--radius-*` |
| Typography | `font-sans`, `font-mono`, `font-heading` | `--font-*` |
| Spacing | `spacing-xs/sm/md/lg` | `--spacing-*` |

---

## Pre-Application Checklist

- [ ] Ran `pencil_get_variables` to see available tokens?
- [ ] All colors using variable references instead of hardcoded hex?
- [ ] All radii using variable references instead of hardcoded pixel values?
- [ ] Missing variables created with `pencil_set_variables`?
- [ ] Code generation outputs semantic utilities (`bg-primary`) not arbitrary values (`bg-[#3b82f6]`)?

---

## See Also

- [codegen-mapping.md](codegen-mapping.md) — Complete quick-reference mapping tables
- [codegen-workflow.md](codegen-workflow.md) — Complete code generation workflow using tokens
- [responsive.md](responsive.md) — Breakpoint tokens and responsive patterns


path: references/layout-integrity.md
content: # Layout Integrity — Hard Rules (MUST read before every `.pen` task)

> **Priority**: HIGHEST. Overrides style preferences.
> **Applies to**: ALL outputs — PPT slides, desktop UI, tablet, mobile, components, landing pages.
> **See also**: [overflow-prevention.md](overflow-prevention.md) · [pen-format.md](pen-format.md) · [screenshot-qa.md](../mcp/screenshot-qa.md)

"Looks reasonable" is not enough. Every artboard must be:

- **Bounded** — no content extends beyond the artboard rectangle.
- **Contained** — every child fits inside its parent's inner box (parent size − padding).
- **Non-overlapping** — sibling nodes never collide unintentionally.
- **Aligned** — items on the same row share a baseline / centerline; items in the same column share a left edge.
- **Balanced** — padding, gap, font size, and icon size follow a consistent scale.

---

## 1. Artboard Budget (do the math BEFORE writing)

Every artboard has fixed `width` × `height`. Before laying out, compute the **height budget**:

```
artboard_height = header_height + body_height + footer_height
body_height     = artboard_height − header_height − footer_height
```

### Typical budgets

| Artboard | Header | Footer | Body (auto) | Body padding (vertical) |
|---|---|---|---|---|
| PPT slide 1280×720 (content) | 64 | 40 | 616 | 32–40 |
| PPT slide 1280×720 (cover, full-bleed) | 0 | 0 | 720 | — (use `layout: "none"` for decoration only) |
| Desktop 1440×900 | 72 | — | 828 | 48–64 |
| Mobile 375×812 | 56 (status+nav) | 64 (tab bar) | 692 | 16–20 |

### Rules

- Body frame MUST be `"height": "fill_container"`. Never give it a fixed height.
- Fixed-height children inside the body (KPI row 140, image 200, etc.) PLUS gaps PLUS body vertical padding MUST sum `≤ body_height`.
- When uncertain, prefer `"fit_content"` on intermediate frames and set the outer body to `"fill_container"`.

### Width budget

```
usable_width = artboard_width − parent_horizontal_padding × 2
```

For a row of N cards with `gap = G`:

```
card_width = (usable_width − (N − 1) × G) / N
```

**Don't hand-pick pixel widths** — set each card to `"width": "fill_container"` and let auto-layout with `gap` do the math.

---

## 2. Auto-layout is the Default — `"layout": "none"` is the exception

| Situation | Layout |
|---|---|
| Any screen containing flowing content (list, form, cards, text blocks, PPT content slide) | `"vertical"` (or `"horizontal"` for row) |
| Cover slide / hero with decorative absolute shapes | `"none"` — but ALL children MUST stay inside the artboard rect (0 ≤ x, y; x + width ≤ artboard_width; y + height ≤ artboard_height). Decorative shapes MAY extend slightly beyond only if the parent has `"clip": true`. |
| Free-form illustration | `"none"` with `"clip": true` |

**Never** mix auto-layout children with absolute `x`/`y` — `x`/`y` are ignored inside an auto-layout parent and create mental-model drift.

---

## 3. Sizing Matrix (copy this into your head)

| Node role | width | height | Notes |
|---|---|---|---|
| Top-level artboard | number (1440 / 1280 / 375…) | number | Fixed |
| Artboard direct child (section) | `"fill_container"` | fixed (header/footer) OR `"fill_container"` (body) | |
| Row of cards / columns | `"fill_container"` | `"fit_content"` or fixed | Use `layout: "horizontal"` + `gap` |
| Each card in a row | `"fill_container"` | fixed OR `"fit_content"` | Cards of equal weight → equal fill |
| Card containing vertical list | `"fill_container"` | `"fit_content"` | |
| Body text / paragraph | `"fill_container"` + `textGrowth: "fixed-width"` | — | Mandatory |
| Heading | `"fill_container"` + `textGrowth: "fixed-width"` | — | Use `maxLines` if overflow risk |
| Inline label (next to icon in a row) | `"fit_content"` (no width) | — | Let the text shrink-wrap |
| Button / badge | `"fit_content"` | `"fit_content"` | `padding: [v,h]` defines size |
| Icon | number (12/14/16/18/20/24) | same number | Square, never stretched |
| Divider line | `"fill_container"` | `1` | |
| Avatar / thumbnail | number | same number | Square or aspect-locked |

---

## 4. Spacing Scale (use these, not arbitrary numbers)

```
Padding:   4 · 6 · 8 · 10 · 12 · 14 · 16 · 20 · 24 · 28 · 32 · 40 · 48 · 64
Gap:       2 · 4 · 6 · 8 · 10 · 12 · 14 · 16 · 20 · 24 · 32
Radius:    2 · 4 · 6 · 8 · 10 · 12 · 16 · 20 · 999
Icon size: 12 · 14 · 16 · 18 · 20 · 24
Font size: 11 · 12 · 13 · 14 · 15 · 16 · 18 · 20 · 24 · 28 · 32 · 40 · 48 · 64 · 72
```

Consistency beats creativity. Don't pick `padding: [13, 21]` — pick `[12, 20]`.

---

## 5. Alignment Rules

- Items in the same row: choose ONE of `alignItems: "center"` / `"start"` / `"end"` for the parent. Don't mix vertical positions in a row by hand.
- Items in the same column: parent controls `alignItems`. Text blocks default to start (left). Buttons in a form default to `justifyContent: "end"`.
- Card titles across a row: same `fontSize`, same `fontWeight`, same `fill`. Same goes for card body text.
- Icons next to text of size N: icon size ≈ N × 1.1 (rounded to scale). Text 14 → icon 16.
- Two columns of cards: both columns must have `"width": "fill_container"` OR both must be identically sized. Never `"fill_container"` + `480` side-by-side.

---

## 6. Buttons & Badges — Common Pitfalls

**Do**:

```json
{
  "type": "frame", "id": "btn01",
  "fill": "$--primary", "cornerRadius": 6,
  "padding": [10, 16], "gap": 6,
  "justifyContent": "center", "alignItems": "center",
  "children": [
    { "type": "icon_font", "id": "bi01", "width": 16, "height": 16, "iconFontName": "check", "iconFontFamily": "lucide", "fill": "$--primary-foreground" },
    { "type": "text", "id": "bt01", "content": "确认", "fontSize": 14, "fontWeight": "500", "fill": "$--primary-foreground", "fontFamily": "Inter", "lineHeight": 1.4 }
  ]
}
```

- No `width` / `height` on the button → it shrinks to fit content (correct).
- Horizontal padding ≥ vertical padding × 1.5 (keeps label room).
- Icon comes BEFORE text, `gap: 6` or `8`.
- Label text has NO `width: "fill_container"` inside a shrink-to-fit button — it would force button to stretch.

**Don't**:

- Fix `width: 80` with a 5-character label at fontSize 14 → clipping.
- Put button in auto-layout row without `alignItems: "center"` → crooked vertical alignment.
- Use full-width (`fill_container`) buttons unless the design explicitly demands it (forms, mobile CTAs).

---

## 7. PPT-Specific Rules (1280×720)

1. **Cover / closing slides** (`layout: "none"` allowed) — anchor all text blocks with explicit `x`/`y` AND `width` so they can't overflow:
   - Left margin: 96px. Right margin: ≥ 96px (text `width` ≤ 1088).
   - Top text baseline: ≥ 160px. Bottom baseline: ≤ 680px.
2. **Content slides** — MUST use `layout: "vertical"` with:
   - Header 64px (fixed) · Body `fill_container` · Footer 40px (fixed).
   - Body padding: `[32, 48]` or `[40, 48]` (top/bottom, left/right).
   - All inner rows: `width: "fill_container"`.
3. **KPI card rows** (4 cards): each card `width: "fill_container"`, `height: 120–140`, `gap: 20`. Never `gap: 16` with `padding: [_, 48]` — totals get awkward.
4. **Two-column body** (left / right): both columns `width: "fill_container"`, `height: "fill_container"`, `gap: 20`.
5. **Page numbers / footer** must be inside the footer frame, not floated. Use `justifyContent: "space_between"`.
6. **Slide Y-coordinates**: 0, 820, 1640, 2460 (720 + 100 gap).

---

## 8. Mobile Rules (375 / 393)

- Screen frame: exact target width.
- Direct children: `width: "fill_container"`, horizontal padding 16–20.
- Any text: `width: "fill_container"`, never wider than `screen_width − 2 × 16`.
- Tab bar / bottom nav: fixed height 64, `width: "fill_container"`, anchored via `justifyContent: "end"` on the screen OR as the last child of a `fill_container` body.
- Long lists must not push the screen taller than the artboard — content that exceeds should be implied-scrollable (design note only).

---

## 9. Detection — Automated Checks (Mode B)

Run AFTER every section, and again at final audit:

```
pencil_snapshot_layout({
  filePath: "...",
  parentId: "<artboardId>",
  maxDepth: 4,
  problemsOnly: true
})
```

Expected result at completion: **empty** (no Clipped / Overflow / Overlapping nodes).

Also run `pencil_get_screenshot` for each artboard and visually confirm:

- Nothing touches or crosses the artboard border.
- No text is cut mid-word.
- Rows of cards are equal width, equal height.
- Footer page numbers align to the right edge with the same inset as the header.

---

## 10. Manual Audit (Mode A) — Pre-completion Checklist

Go through EVERY artboard:

- [ ] Artboard has `layout: "vertical"` (or `"horizontal"`); only cover/hero uses `"none"` with `clip: true`.
- [ ] Header + footer have fixed height; body frame has `height: "fill_container"`.
- [ ] Every section frame inside body has `width: "fill_container"`.
- [ ] Every text inside an auto-layout parent has `width: "fill_container"` + `textGrowth: "fixed-width"`.
- [ ] Every row of cards: children use `width: "fill_container"`, same height.
- [ ] Sum of fixed-height children + gaps + vertical padding ≤ body computed height.
- [ ] No `x`/`y` on any child inside an auto-layout parent.
- [ ] In `layout: "none"` parents: every child satisfies `x ≥ 0`, `y ≥ 0`, `x + width ≤ parent_width`, `y + height ≤ parent_height` (decorative shapes with `clip: true` exempted).
- [ ] Icon sizes from the allowed scale (12/14/16/18/20/24). Buttons use `fit_content`.
- [ ] All colors via variables. No hex literals inside `fill` / `stroke.fill` (except within a documented gradient color list).
- [ ] Padding / gap / radius / font size / icon size all drawn from the spacing scale (§4).
- [ ] `read_lints` passes on the `.pen` file.

If any box is unchecked → fix, don't ship.

---

## 11. Problem → Fix Quick Reference

| Symptom | Root cause | Fix |
|---|---|---|
| Text clipped at right edge | Fixed-width text or missing `fill_container` | Add `width: "fill_container"` + `textGrowth: "fixed-width"` |
| Card row overflows right edge | Child cards have hardcoded widths summing > parent inner width | Switch each card to `"fill_container"`, tune `gap` |
| Body content visually exceeds artboard bottom | Body has fixed height, or children + gaps > artboard body height | Set body `height: "fill_container"`; reduce content or split slide |
| Button label wraps or clips | Button has fixed width smaller than label | Remove width; use `fit_content` + `padding: [v,h]` |
| Row items at different vertical positions | Parent missing `alignItems: "center"` | Add `alignItems: "center"` on the row frame |
| Columns misaligned left edges | Different `padding` on each column | Unify padding across columns |
| Gap looks inconsistent | Mixed `gap` values across siblings | Pick ONE value per layout level (usually 16 or 20) |
| Cover slide text floats off | `x`/`y` too large; no `width` set | Add `width: <bounded>` so text can wrap; keep x + width ≤ artboard width |
| Two-column layout where left is `fill_container`, right is fixed | Uneven balance, overflow risk | Both sides `fill_container` |

---

## 12. Golden Rules Card (memorize)

1. Body uses `height: "fill_container"`. Header/footer are fixed.
2. Every row child: `width: "fill_container"`.
3. Every text in auto-layout: `width: "fill_container"` + `textGrowth: "fixed-width"`.
4. Buttons/badges: `fit_content` + padding `[v, h]` + center align.
5. No `x`/`y` inside auto-layout. No `layout: "none"` for content.
6. Use the spacing scale (§4). No odd numbers.
7. Audit (§10) before declaring done.


path: references/overflow-prevention.md
content: # Layout and Text Overflow

> **Applies to**: Mode A (Direct File Writing) + Mode B (MCP Tools)
> **See also**: [layout-integrity.md](layout-integrity.md) — the authoritative hard-rules document for ALL layout correctness (sizing, alignment, no-overlap, no-crooked). This file is a focused reference for **text/content overflow** specifically.
> **Also**: [screenshot-qa.md](../mcp/screenshot-qa.md) · [responsive.md](responsive.md)

---

## Why Overflow Is the Highest-Priority Defect

Text and content extending beyond their container or artboard is the most common and most visible design defect:

- Text gets clipped and becomes unreadable
- Mobile layouts break apart
- Generated code requires manual overflow fixes
- Creates an unprofessional, broken visual impression

Mobile screens (375–393px wide) are the most overflow-prone — handle with extra care.

---

## Core Principle: Responsive Layout Awareness

> **Inspired by Stitch** — Stitch automatically builds **responsive grids** when generating UI, ensuring layouts work at every screen size. The equivalent in Pencil: always use `fill_container` instead of fixed widths, use `gap` instead of margin hacks, and use `padding` to keep content away from edges.

---

## Prevention Strategy

### Text Elements

1. **Always set text width to fill its parent**

   ```javascript
   text=I(container, { type: "text", content: "Long text...", width: "fill_container" })
   ```

2. **Use the right text properties**
   - Text that should truncate (card titles, list items): set `maxLines`
   - Long paragraphs: let them wrap naturally within the container
   - Overly long headings: wrap or truncate with ellipsis

3. **Never** apply a fixed pixel width wider than the parent container on text nodes

### Container Frames

1. **Use auto-layout** (`layout: "vertical"` or `"horizontal"`) so children flow naturally

2. **Constrain child width to parent**

   ```javascript
   child=I(parent, { type: "frame", width: "fill_container", layout: "vertical" })
   ```

3. **Set padding on containers** to prevent content from touching edges

   ```javascript
   U("parentId", { padding: 16 })
   // Or per-side: paddingLeft, paddingRight, paddingTop, paddingBottom
   ```

4. **Use `gap` for spacing between children** — never use margin hacks

   ```javascript
   U("parentId", { layout: "vertical", gap: 12 })
   ```

### Mobile Screens (375–393px)

Mobile is the most overflow-prone context. Extra rules:

| Element | Requirement |
|---------|-------------|
| Screen frame | Width must exactly match the target width (e.g. 375px) |
| Direct children | `width: "fill_container"` + horizontal padding 16–20px |
| Text nodes | Always `width: "fill_container"`, never wider than ~335px (375 - 2×20) |
| Images | Constrain to container width or use `width: "fill_container"` |
| Horizontal scroll areas | Only use intentionally (e.g. carousels) — never by accident |

### Ref Component Instances

```javascript
// When an instance should fill its parent, set fill_container
card=I(container, { type: "ref", ref: "CardComponent", width: "fill_container" })
```

Verify that the component's internal layout can adapt to varying widths.

---

## Detection: Post-Build Verification

After completing each section, immediately run an overflow check:

```
pencil_snapshot_layout({
  filePath: "path/to/file.pen",
  parentId: "screenId",
  maxDepth: 3,
  problemsOnly: true
})
```

Returns nodes with layout problems:

| Problem Type | Meaning |
|-------------|---------|
| Clipped | Child node extends beyond parent container bounds |
| Overlapping | Sibling nodes unintentionally overlap |
| Overflow | Content is wider or taller than its container |

---

## Problem → Fix Quick Reference

| Problem | Likely Cause | Fix |
|---------|-------------|-----|
| Text clipped horizontally | Text has a fixed width | `width: "fill_container"` or reduce font size |
| Text clipped vertically | Parent has a fixed height | Increase height, switch to auto-height, or set `maxLines` |
| Child wider than parent | Child uses a fixed width | Change to `width: "fill_container"` |
| Children overlapping | Parent frame has no layout | Add `layout: "vertical"` or `"horizontal"` |
| Content outside artboard | Width/padding too large | Reduce widths, verify all descendants fit within screen width |

---

## Common Fix Snippets

```javascript
// Text overflowing parent
U("textNodeId", { width: "fill_container" })

// Children overflowing frame
U("parentFrameId", { layout: "vertical", gap: 8 })
U("child1Id", { width: "fill_container" })
U("child2Id", { width: "fill_container" })

// Content touching screen edges
U("contentContainerId", { paddingLeft: 16, paddingRight: 16 })

// Long title truncated to single line
U("titleTextId", { maxLines: 1, width: "fill_container" })
```

---

## Pre-Completion Checklist

- [ ] Run `pencil_snapshot_layout` (`problemsOnly: true`) on every section?
- [ ] All text inside auto-layout parents using `width: "fill_container"`?
- [ ] Mobile screens have appropriate padding (16–20px)?
- [ ] Long titles/descriptions that need truncation have `maxLines` set?
- [ ] All child frames using `width: "fill_container"` (not fixed widths wider than parent)?
- [ ] Verified the full screen with `pencil_get_screenshot`?

---

## See Also

- [screenshot-qa.md](../mcp/screenshot-qa.md) — Full workflow for catching visual overflow via screenshots
- [responsive.md](responsive.md) — Mobile layout constraints and responsive patterns


path: references/pen-format.md
content: # .pen File Format Reference

> **Applies to**: Mode A (Direct File Writing) + Mode B (MCP Tools)
> **Priority**: Core document — read before writing any `.pen` file
> **See also**: [design-tokens.md](design-tokens.md) · [overflow-prevention.md](overflow-prevention.md)

This document is the **single source of truth** for `.pen` file JSON format, distilled from all example files. **Do NOT read files in the `examples/` directory** unless debugging a specific format error.

## Top-Level Structure

```json
{
  "version": "2.8",
  "children": [ ... ],
  "themes": { ... },
  "variables": { ... }
}
```

| Field | Required | Description |
|-------|----------|-------------|
| `version` | Yes | Always `"2.8"` |
| `children` | Yes | Array of top-level nodes (frames, prompts, text) |
| `themes` | No | Theme axis definitions (for multi-theme support) |
| `variables` | No | Design token definitions |

## Node Types

All nodes have `type`, `id`, and optionally `name`.

| type | Description | Key Properties |
|------|-------------|----------------|
| `frame` | Container/layout node (the core building block) | layout, children, fill, stroke, cornerRadius, padding, gap |
| `text` | Text content | content, fontFamily, fontSize, fontWeight, lineHeight |
| `icon_font` | Icon from icon font library | iconFontName, iconFontFamily, fill |
| `ref` | Instance of a reusable component | ref (target component id), descendants |
| `rectangle` | Rectangle shape | fill, cornerRadius, stroke |
| `ellipse` | Ellipse/circle shape | fill, stroke |
| `line` | Line element | stroke |
| `prompt` | AI design prompt | content, model |

## ID Format

IDs are **5-character alphanumeric short codes**: `"MzSDs"`, `"PV1ln"`, `"6xhgh"`.
Use similar short random IDs when creating new nodes. Avoid descriptive IDs longer than ~12 chars.

## Frame Node (Most Important)

```json
{
  "type": "frame",
  "id": "abc12",
  "name": "Card",
  "clip": true,
  "reusable": true,
  "width": 360,
  "height": 200,
  "fill": "$--background",
  "cornerRadius": 6,
  "stroke": { ... },
  "effect": { ... },
  "opacity": 0.8,
  "enabled": true,
  "layout": "vertical",
  "gap": 16,
  "padding": [16, 24],
  "justifyContent": "center",
  "alignItems": "center",
  "children": [ ... ],
  "slot": ["child1", "child2"],
  "theme": { "Mode": "Dark" }
}
```

### Layout

| Value | Behavior |
|-------|----------|
| `"vertical"` | Children stack top-to-bottom (flex-column) |
| `"horizontal"` | Children flow left-to-right (flex-row). **This is the default when `layout` is omitted.** |
| `"none"` | Absolute positioning — children use `x`/`y` coordinates |

### Size

| Form | Example | Description |
|------|---------|-------------|
| number | `360` | Fixed pixel size |
| `"fill_container"` | — | Stretch to fill parent |
| `"fill_container(N)"` | `"fill_container(480)"` | Fill parent, default/fallback N px |
| `"fit_content"` | — | Shrink to content |
| `"fit_content(N)"` | `"fit_content(900)"` | Fit content, min-height N px |

### Padding

| Form | Example | Meaning |
|------|---------|---------|
| number | `16` | All sides equal |
| `[v, h]` | `[8, 16]` | Vertical, Horizontal |
| `[t, r, b, l]` | `[8, 6, 8, 32]` | Top, Right, Bottom, Left |

### Gap

```json
"gap": 16       // spacing between children (can be negative: -1)
```

### Alignment

| Property | Values |
|----------|--------|
| `justifyContent` | `"center"`, `"end"`, `"space_between"` |
| `alignItems` | `"center"` |

### Position (only in `layout: "none"` parent)

```json
"x": 200, "y": 100
```

## Fill

```json
// 1. Solid color (string)
"fill": "#0C0C0C"

// 2. Variable reference
"fill": "$--background"

// 3. Color object (with enabled toggle)
"fill": { "type": "color", "color": "$--white", "enabled": false }

// 4. Image fill
"fill": { "type": "image", "enabled": true, "url": "https://...", "mode": "fill" }

// 5. Gradient fill
"fill": {
  "type": "gradient",
  "gradientType": "linear",
  "enabled": true,
  "rotation": 135,
  "size": { "height": 1 },
  "colors": [
    { "color": "#FF5C00", "position": 0 },
    { "color": "#FF8C40", "position": 1 }
  ]
}

// 6. No fill
"fill": []
```

## Stroke

```json
"stroke": {
  "align": "inside",          // "inside" | "outside" | "center"
  "thickness": 1,             // uniform
  "fill": "$--border"         // stroke color (string or variable)
}

// Single-side stroke:
"stroke": {
  "align": "inside",
  "thickness": { "right": 1 }   // or "bottom", "top", "left"
}

// With join/cap:
"stroke": { "align": "inside", "thickness": 1, "join": "round", "cap": "round" }
```

## Corner Radius

```json
"cornerRadius": 6                                          // uniform
"cornerRadius": "$--radius-pill"                           // variable ref
"cornerRadius": [6, 0, 0, 6]                              // [topLeft, topRight, bottomRight, bottomLeft]
"cornerRadius": ["$--radius-pill", 0, 0, "$--radius-pill"] // mixed
```

## Effect (Shadow)

```json
// Single shadow
"effect": {
  "type": "shadow",
  "shadowType": "outer",
  "color": "#0000000d",
  "offset": { "x": 0, "y": 1 },
  "blur": 1.75,
  "spread": -1
}

// Multiple shadows
"effect": [ { ... }, { ... } ]

// No effects
"effect": []
```

## Text Node

```json
{
  "type": "text",
  "id": "txt01",
  "name": "Heading",
  "fill": "$--foreground",
  "content": "Hello World",
  "fontFamily": "Inter",               // or "$--font-primary"
  "fontSize": 14,
  "fontWeight": "normal",              // "normal" | "500" | "600" | "700"
  "lineHeight": 1.4285714285714286,    // multiplier
  "letterSpacing": -0.5,               // optional
  "textAlign": "center",               // "center" | "right" (default: left)
  "textAlignVertical": "middle",       // "middle" | "bottom"
  "textGrowth": "fixed-width",         // "fixed-width" | "auto"
  "width": "fill_container",           // use with textGrowth: "fixed-width"
  "textDecoration": "underline",       // optional
  "opacity": 0.6,                      // optional
  "maxLines": 2                        // optional, truncation
}
```

**Key rule**: For text inside auto-layout frames, always use `"width": "fill_container"` with `"textGrowth": "fixed-width"` to prevent overflow.

## Icon Font Node

```json
{
  "type": "icon_font",
  "id": "ico01",
  "width": 16,
  "height": 16,
  "iconFontName": "hexagon",        // Lucide icon name
  "iconFontFamily": "lucide",       // always "lucide"
  "fill": "$--foreground"
}
```

Common icon sizes: 12, 16, 20, 24. When inside `layout: "none"` parent, add `x`/`y`.

Common Lucide icons: `hexagon`, `plus`, `check`, `chevron-right`, `chevron-down`, `chevrons-up-down`, `search`, `x`, `ellipsis`, `mail`, `lock`, `eye`, `eye-off`, `github`, `arrow-right`, `star`, `heart`, `settings`, `user`, `home`, `menu`

## Ref Node (Component Instance)

```json
// Basic ref
{
  "type": "ref",
  "id": "ref01",
  "ref": "VSnC2",                    // ID of reusable: true component
  "x": 200, "y": 100                // position (in layout: "none" parent)
}

// Ref with overrides
{
  "type": "ref",
  "id": "ref02",
  "ref": "VSnC2",
  "reusable": true,                  // ref itself can be reusable
  "name": "Button/Primary",
  "width": "fill_container",
  "descendants": {
    "Tr3Fv": { "content": "Submit" },       // override text content
    "SlKX1": { "iconFontName": "check" },   // override icon
    "soROh": { "enabled": false }            // hide a child
  }
}

// Ref with children replacement (for slots)
{
  "type": "ref",
  "id": "ref03",
  "ref": "ctKFD",
  "children": [
    { "type": "icon_font", "id": "ico99", ... }
  ]
}
```

## Reusable Components

Mark a frame as reusable to make it available for `ref` instances:

```json
{
  "type": "frame",
  "id": "VSnC2",
  "name": "Button/Default",
  "reusable": true,
  ...
}
```

## Slot

Declare replaceable child positions in a frame:

```json
{
  "type": "frame",
  "slot": ["childId1", "childId2"],
  "children": [...]
}
```

## Variables

### Variable Types

| Type | Example Value |
|------|---------------|
| `"color"` | `"#fafafa"`, `"#09090b"` |
| `"number"` | `999`, `6`, `0` |
| `"string"` | `"Inter"`, `"Geist"` |

### Variable Reference Syntax

Prefix `$` before variable name: `"$--background"`, `"$--radius-pill"`, `"$--font-primary"`

Can be used in: `fill`, `stroke.fill`, `cornerRadius`, `fontFamily`

### Variable Definition

```json
"variables": {
  "--background": {
    "type": "color",
    "value": "#fafafa"
  },
  "--radius-pill": {
    "type": "number",
    "value": 999
  },
  "--font-primary": {
    "type": "string",
    "value": "Inter"
  }
}
```

### Themed Variables (multi-value)

```json
"--background": {
  "type": "color",
  "value": [
    { "value": "#fafafa" },
    { "value": "#09090b", "theme": { "Mode": "Dark" } },
    { "value": "#09090b", "theme": { "Mode": "Dark", "Base": "Zinc" } }
  ]
}
```

First entry without `theme` = default. Entries with `theme` = overrides for that theme combination.

## Themes

```json
"themes": {
  "Mode": ["Light", "Dark"],
  "Base": ["Neutral", "Gray", "Slate", "Stone", "Zinc"]
}
```

For simple Light/Dark only:
```json
"themes": {
  "Mode": ["Light", "Dark"]
}
```

Apply a theme to a frame:
```json
{
  "type": "frame",
  "theme": { "Mode": "Dark", "Base": "Zinc" }
}
```

## Shadcn Dark Zinc Variables (Default Template)

Use this complete variables block for the default Shadcn Dark style:

```json
"themes": {
  "Mode": ["Light", "Dark"]
},
"variables": {
  "--background":            { "type": "color", "value": [{ "value": "#fafafa" }, { "value": "#09090b", "theme": { "Mode": "Dark" } }] },
  "--foreground":            { "type": "color", "value": [{ "value": "#0a0a0a" }, { "value": "#fafafa", "theme": { "Mode": "Dark" } }] },
  "--card":                  { "type": "color", "value": [{ "value": "#fafafa" }, { "value": "#18181b", "theme": { "Mode": "Dark" } }] },
  "--card-foreground":       { "type": "color", "value": [{ "value": "#0a0a0a" }, { "value": "#fafafa", "theme": { "Mode": "Dark" } }] },
  "--popover":               { "type": "color", "value": [{ "value": "#fafafa" }, { "value": "#18181b", "theme": { "Mode": "Dark" } }] },
  "--popover-foreground":    { "type": "color", "value": [{ "value": "#0a0a0a" }, { "value": "#fafafa", "theme": { "Mode": "Dark" } }] },
  "--primary":               { "type": "color", "value": [{ "value": "#18181b" }, { "value": "#fafafa", "theme": { "Mode": "Dark" } }] },
  "--primary-foreground":    { "type": "color", "value": [{ "value": "#fafafa" }, { "value": "#18181b", "theme": { "Mode": "Dark" } }] },
  "--secondary":             { "type": "color", "value": [{ "value": "#f4f4f5" }, { "value": "#27272a", "theme": { "Mode": "Dark" } }] },
  "--secondary-foreground":  { "type": "color", "value": [{ "value": "#18181b" }, { "value": "#fafafa", "theme": { "Mode": "Dark" } }] },
  "--muted":                 { "type": "color", "value": [{ "value": "#f4f4f5" }, { "value": "#27272a", "theme": { "Mode": "Dark" } }] },
  "--muted-foreground":      { "type": "color", "value": [{ "value": "#71717a" }, { "value": "#a1a1aa", "theme": { "Mode": "Dark" } }] },
  "--accent":                { "type": "color", "value": [{ "value": "#f4f4f5" }, { "value": "#27272a", "theme": { "Mode": "Dark" } }] },
  "--accent-foreground":     { "type": "color", "value": [{ "value": "#18181b" }, { "value": "#fafafa", "theme": { "Mode": "Dark" } }] },
  "--destructive":           { "type": "color", "value": [{ "value": "#ef4444" }, { "value": "#7f1d1d", "theme": { "Mode": "Dark" } }] },
  "--border":                { "type": "color", "value": [{ "value": "#e4e4e7" }, { "value": "#27272a", "theme": { "Mode": "Dark" } }] },
  "--input":                 { "type": "color", "value": [{ "value": "#e4e4e7" }, { "value": "#27272a", "theme": { "Mode": "Dark" } }] },
  "--ring":                  { "type": "color", "value": [{ "value": "#18181b" }, { "value": "#d4d4d8", "theme": { "Mode": "Dark" } }] },
  "--white":                 { "type": "color", "value": "#ffffff" },
  "--black":                 { "type": "color", "value": "#000000" }
}
```

## Common Component Patterns

### Button

```json
{
  "type": "frame",
  "id": "btn01",
  "name": "Button/Default",
  "reusable": true,
  "fill": "$--primary",
  "cornerRadius": 6,
  "gap": 6,
  "padding": [10, 16],
  "justifyContent": "center",
  "alignItems": "center",
  "children": [
    {
      "type": "text",
      "id": "btxt1",
      "name": "Button Label",
      "fill": "$--primary-foreground",
      "content": "Button",
      "lineHeight": 1.43,
      "textAlign": "center",
      "textAlignVertical": "middle",
      "fontFamily": "Inter",
      "fontSize": 14,
      "fontWeight": "500"
    }
  ]
}
```

Button variants by fill:
- **Default/Primary**: `fill: "$--primary"`, text: `"$--primary-foreground"`
- **Secondary**: `fill: "$--secondary"`, text: `"$--secondary-foreground"`
- **Destructive**: `fill: "$--destructive"`, text: `"$--white"`
- **Outline**: `fill: "$--background"`, `stroke.fill: "$--border"`, text: `"$--foreground"`
- **Ghost**: `fill` disabled/empty, text: `"$--foreground"`

Button sizes:
- **Default**: `padding: [10, 16]`, fontSize 14
- **Large**: `padding: [8, 24]`, fontSize 14
- **Small**: `padding: [6, 12]`, fontSize 13

### Input Group (Label + Input)

```json
{
  "type": "frame",
  "id": "ig001",
  "name": "Input Group",
  "width": "fill_container",
  "layout": "vertical",
  "gap": 6,
  "children": [
    {
      "type": "text",
      "id": "lbl01",
      "name": "Label",
      "fill": "$--foreground",
      "textGrowth": "fixed-width",
      "width": "fill_container",
      "content": "Email",
      "lineHeight": 1.43,
      "fontFamily": "Inter",
      "fontSize": 14,
      "fontWeight": "500"
    },
    {
      "type": "frame",
      "id": "inp01",
      "name": "Input",
      "width": "fill_container",
      "height": 40,
      "fill": "$--background",
      "cornerRadius": 6,
      "stroke": { "align": "inside", "thickness": 1, "fill": "$--input" },
      "gap": 8,
      "padding": [10, 12],
      "alignItems": "center",
      "children": [
        {
          "type": "text",
          "id": "ph001",
          "name": "Placeholder",
          "fill": "$--muted-foreground",
          "content": "Enter your email...",
          "lineHeight": 1.43,
          "fontFamily": "Inter",
          "fontSize": 14,
          "fontWeight": "normal"
        }
      ]
    }
  ]
}
```

### Card

```json
{
  "type": "frame",
  "id": "crd01",
  "name": "Card",
  "width": 360,
  "fill": "$--card",
  "cornerRadius": 8,
  "stroke": { "align": "inside", "thickness": 1, "fill": "$--border" },
  "layout": "vertical",
  "padding": 24,
  "gap": 16,
  "children": [ ... ]
}
```

### Divider (OR separator)

```json
{
  "type": "frame",
  "id": "div01",
  "name": "Divider",
  "width": "fill_container",
  "gap": 16,
  "alignItems": "center",
  "children": [
    { "type": "frame", "id": "dl001", "width": "fill_container", "height": 1, "fill": "$--border" },
    { "type": "text", "id": "dt001", "fill": "$--muted-foreground", "content": "OR", "fontSize": 12, "fontWeight": "500", "fontFamily": "Inter", "lineHeight": 1.4 },
    { "type": "frame", "id": "dr001", "width": "fill_container", "height": 1, "fill": "$--border" }
  ]
}
```

## Complete Minimal .pen File Template

```json
{
  "version": "2.8",
  "children": [
    {
      "type": "frame",
      "id": "scrn1",
      "name": "Screen Name",
      "theme": { "Mode": "Dark" },
      "clip": true,
      "width": 1440,
      "height": 900,
      "fill": "$--background",
      "layout": "vertical",
      "children": [ ... ]
    }
  ],
  "themes": {
    "Mode": ["Light", "Dark"]
  },
  "variables": {
    ...shadcn dark zinc variables from above...
  }
}
```

Common screen sizes:
- Desktop: `1440×900`, `1920×1080`
- Tablet: `768×1024`
- Mobile: `375×812`, `393×852`


path: references/responsive.md
content: # Responsive Breakpoints

> **Applies to**: Mode A (Direct File Writing) + Mode B (MCP Tools)
> **See also**: [overflow-prevention.md](overflow-prevention.md) · [codegen-workflow.md](codegen-workflow.md)

---

## Artboard Size → Tailwind Breakpoint Mapping

### Standard Artboard Width Reference

| Device | Pencil Artboard Width | Tailwind Breakpoint | Prefix |
|--------|----------------------|--------------------|----|
| Mobile (small) | 320px | Default (no prefix) | — |
| Mobile (standard) | 375px | Default (no prefix) | — |
| Mobile (large) | 393–430px | Default (no prefix) | — |
| Tablet (portrait) | 768px | `md` | `md:` |
| Tablet (landscape) | 1024px | `lg` | `lg:` |
| Desktop | 1280px | `xl` | `xl:` |
| Desktop (wide) | 1440px | `2xl` | `2xl:` |
| Desktop (ultrawide) | 1920px | `2xl` or custom | `2xl:` |

### Tailwind v4 Built-in Breakpoint Values

```css
/* Built into Tailwind — no @theme declaration needed */
sm  →  640px
md  →  768px
lg  →  1024px
xl  →  1280px
2xl →  1536px
```

Custom breakpoints (Tailwind v4):

```css
@theme {
  --breakpoint-xs: 475px;
  --breakpoint-3xl: 1920px;
}
```

---

## Multi-Artboard Design → Code Generation Strategy

### Reading All Artboards

```
pencil_batch_get({
  filePath: "path/to/file.pen",
  patterns: [{ type: "frame", name: "Mobile|Tablet|Desktop" }],
  readDepth: 4
})
```

Or read top-level nodes directly:

```
pencil_batch_get({ filePath: "path/to/file.pen" })
```

### Mobile-First Code Generation

Use the smallest artboard (mobile) as base styles, then layer breakpoint overrides for larger sizes:

```tsx
// base styles = mobile artboard
// md: styles = tablet artboard
// lg: styles = desktop artboard

<div className="flex flex-col gap-4 p-4 md:flex-row md:gap-6 md:p-6 lg:gap-8 lg:p-8">
  {/* Sidebar: stacks on mobile, side-by-side on tablet+ */}
  <aside className="w-full md:w-64 lg:w-72">
    {/* ... */}
  </aside>

  {/* Main content: full-width on mobile, flexible on tablet+ */}
  <main className="flex-1">
    {/* ... */}
  </main>
</div>
```

---

## Common Responsive Patterns

| Pencil Design Pattern | Tailwind Implementation |
|----------------------|------------------------|
| 1 column (mobile) → 2 columns (tablet) → 3 columns (desktop) | `grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3` |
| Stacked sidebar (mobile) → side-by-side (desktop) | `flex flex-col lg:flex-row` |
| Hidden on mobile, visible on desktop | `hidden lg:block` |
| Visible on mobile, hidden on desktop | `block lg:hidden` |
| Full-width mobile, constrained desktop | `w-full max-w-7xl mx-auto` |
| Small text mobile, larger desktop | `text-sm md:text-base lg:text-lg` |
| Less padding mobile, more padding desktop | `p-4 md:p-6 lg:p-8` |
| Card grid 1/2/3 columns | `grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6` |
| Mobile hamburger menu → desktop full nav | Mobile: `<Sheet>` / Desktop: `<nav className="hidden md:flex">` |

---

## Artboard Diff Quick Reference

When comparing mobile vs. desktop artboards, focus on these changes:

| What Changes | Mobile Artboard | Desktop Artboard | Code Pattern |
|-------------|----------------|-----------------|--------------|
| Layout direction | `layout: "vertical"` | `layout: "horizontal"` | `flex flex-col lg:flex-row` |
| Column count | 1 column | 2–4 columns | `grid-cols-1 lg:grid-cols-3` |
| Visibility | Element absent | Element present | `hidden lg:block` |
| Font size | Smaller | Larger | `text-2xl lg:text-4xl` |
| Padding | 16px | 24–32px | `p-4 lg:p-8` |
| Gap | 16px | 24px | `gap-4 lg:gap-6` |
| Sidebar | Hidden or stacked | Side-by-side | `hidden lg:block lg:w-64` |
| Image size | Smaller/cropped | Full size | `h-48 lg:h-80` |

---

## Container Queries (Advanced)

When a component needs to respond to its **parent container** width (not the viewport):

```tsx
<div className="@container">
  <div className="flex flex-col @md:flex-row @lg:gap-8">
    {/* Responds to parent container width, not viewport */}
  </div>
</div>
```

Use when the same component appears in different contexts (sidebar vs. main content) and must adapt accordingly.

---

## Anti-Patterns

| ❌ Wrong | ✅ Correct |
|---------|----------|
| Hardcoding artboard pixel widths into code | Use Tailwind breakpoints + responsive utilities |
| Writing separate mobile/desktop components | One component with responsive classes |
| Using `max-width` media queries | Mobile-first `min-width` (Tailwind default) |
| Ignoring the mobile artboard | Always start from mobile, add `md:` / `lg:` |
| Writing `@media` in CSS for breakpoints | Use Tailwind responsive prefixes in className |
| `w-[375px]` from mobile artboard | `w-full` with responsive max-width |
| `w-[1440px]` from desktop artboard | `max-w-7xl mx-auto` or similar |

---

## Code Generation Checklist

- [ ] Identified all artboard sizes and mapped them to Tailwind breakpoints?
- [ ] Generating mobile-first code (base styles = mobile artboard)?
- [ ] Tablet/desktop differences use breakpoint prefixes (`md:`, `lg:`)?
- [ ] Compared artboards to find what changes between sizes?
- [ ] Column count changes use `grid-cols-*` with breakpoint prefixes?
- [ ] Visibility changes use `hidden` / `block` with breakpoint prefixes?
- [ ] No hardcoded artboard pixel dimensions in class names?

---

## See Also

- [overflow-prevention.md](overflow-prevention.md) — Overflow prevention and mobile layout rules
- [codegen-workflow.md](codegen-workflow.md) — Complete code generation workflow
- [codegen-mapping.md](codegen-mapping.md) — Property-to-Tailwind class quick reference


path: ppt/presets-ppt.md
content: # PPT / Slide Deck Preset

Default style for presentations / slide decks when the user does not specify a color scheme.

> **Before designing any slide, also read [`layout-integrity.md`](../references/layout-integrity.md).**
> PPT slides are fixed-size canvases (1280×720) with zero scroll — overflow and misalignment are immediately visible and must be prevented by construction, not fixed afterward.

## Slide Setup

- **Size**: `1280 × 720` px
- **Stacking**: **horizontal by default** (x 递增, 100 px gap, y = 0). Switch to vertical (y 递增, x = 0, 100 px gap) only when the user picks it. Follow the multi-artboard ask in [`SKILL.md`](../SKILL.md) Critical Rule 7.
- **Font**: `Inter` for all text (not PingFang SC)

## Color Tokens

```json
"variables": {
  "--bg-page":       { "type": "color", "value": "#FFFFFF" },
  "--bg-card":       { "type": "color", "value": "#F0F7FF" },
  "--bg-footer":     { "type": "color", "value": "#E8F4FD" },
  "--border-card":   { "type": "color", "value": "#C5D9F2" },
  "--text-heading":  { "type": "color", "value": "#0D1B8E" },
  "--text-body":     { "type": "color", "value": "#1565C0" },
  "--accent-sky":    { "type": "color", "value": "#29B6F6" },
  "--white":         { "type": "color", "value": "#FFFFFF" }
}
```

## Header Bar (content slides)

```json
{
  "type": "frame", "id": "hdr1",
  "width": "fill_container", "height": 64,
  "fill": {
    "type": "gradient", "gradientType": "linear", "rotation": 270,
    "colors": [
      { "color": "#0D1B8E", "position": 0 },
      { "color": "#29B6F6", "position": 1 }
    ]
  }
}
```

## Cover / Closing Slide (full-bleed gradient)

```json
"fill": {
  "type": "gradient", "gradientType": "linear", "rotation": 45,
  "colors": [
    { "color": "#29B6F6", "position": 0 },
    { "color": "#1565C0", "position": 0.5 },
    { "color": "#0D1B8E", "position": 1 }
  ]
}
```
Deep color (`#0D1B8E`) lands top-left, sky blue bottom-right.

## Default Slide Sequence (when building a complete deck)

1. **Cover** — full diagonal gradient · large title · subtitle · presenter info
2. **Table of Contents** — left gradient sidebar (180 px) + right card list
3. **Content (3-column)** — header bar + white body + 3 icon cards
4. **Data Dashboard** — header bar + 4 KPI cards + bar chart
5. **Gantt Chart** — header bar + month axis + task rows with `#29B6F6` bars + status badges
6. **Thank You** — full diagonal gradient · centered large text · decorative circles

## Component Recipes

**Card / panel**: `fill: "$--bg-card"`, `stroke: { thickness: 1, fill: "$--border-card" }`, `cornerRadius: 8`, `padding: [16, 20]`.

**Footer bar**: full-width, `fill: "$--bg-footer"`, `text: "$--text-body"`, height `40`.

**Heading text**: `fontFamily: "Inter"`, `fontWeight: "700"`, `fill: "$--text-heading"`.

**Body text**: `fontFamily: "Inter"`, `fontWeight: "400"`, `fill: "$--text-body"`, `fontSize: 14–16`.

**Data callout / progress bar / Gantt bar**: `fill: "$--accent-sky"`.


path: mcp/image-reuse.md
content: # Asset Reuse

> **Applies to**: Mode B (MCP Tools)
> **See also**: [component-reuse.md](../references/component-reuse.md) · [screenshot-qa.md](screenshot-qa.md)

---

## Core Principle: Search Before Generate

> **Inspired by Stitch** — Stitch emphasizes **efficiency and reuse**: avoid reinventing the wheel, maintain brand consistency through existing assets, and accelerate the path from idea to implementation. Regenerating a logo every time is a direct violation of this principle.

AI image generation is non-deterministic. Every generation produces a different result. If one screen already has a logo and you generate a new one for another screen, you end up with two different brand identities — design loses consistency.

The same applies to:
- Product images reused across screens
- Illustrations or decorative graphics
- Brand elements (logos, wordmarks, icons)
- Profile photos or avatars appearing in different contexts

---

## Workflow: Discover and Reuse Assets

### Step 1 — Search for Existing Assets

**Before generating any image**, search the document first:

```
pencil_batch_get({
  filePath: "path/to/file.pen",
  patterns: [
    { name: "logo" },
    { name: "brand" },
    { name: "icon" },
    { name: "image" }
  ],
  searchDepth: 5
})
```

Or search by node type:

```
pencil_batch_get({
  filePath: "path/to/file.pen",
  patterns: [{ type: "frame", name: "logo|brand|hero" }],
  searchDepth: 5
})
```

### Step 2 — Copy the Existing Asset

When found, use the Copy operation — not regeneration:

```javascript
// Copy logo from another artboard into the current screen
logoCopy=C("existingLogoNodeId", "targetParentId", { width: 120, height: 40 })
```

`C()` preserves the original image fill, styling, and structure.

If the logo lives inside a reusable Header component, insert the whole component:

```javascript
header=I("screenId", { type: "ref", ref: "HeaderComponent", width: "fill_container" })
```

### Step 3 — Adjust Size

```javascript
U("copiedLogoId", { width: 100, height: 32 })
```

**Always maintain aspect ratio** — never stretch or distort.

---

## When to Generate New Images

Only use `G()` to generate in these cases:

| Condition | Description |
|-----------|-------------|
| No similar asset in the document | Confirmed via search |
| Image is unique to this screen | Specific hero photo, unique illustration |
| Building the first screen | No assets exist yet |

```javascript
// Only when no existing asset matches
heroImg=I("heroSection", { type: "frame", name: "Hero Image", width: "fill_container", height: 400 })
G(heroImg, "stock", "modern office workspace")
```

---

## Strict Rules for Logos

Logos have the strictest reuse requirements:

1. **ALWAYS search first** — if any other screen has been designed, a logo almost certainly already exists
2. **ALWAYS copy** — never regenerate an existing logo; generated logos will never match
3. **Maintain proportions** — scale proportionally, never stretch
4. **Check inside components** — the logo may be inside a reusable Header/Navbar component

---

## Decision Tree

```
Need an image or logo?
│
├── Is it a logo or brand element?
│   ├── Already exists in the document? → COPY IT
│   └── First screen, nothing exists yet? → Generate or ask user for asset
│
├── Is it a product photo or hero image?
│   ├── Same image used on another screen? → COPY IT
│   └── Unique to this screen? → Generate with G() or use stock
│
└── Is it an icon?
    ├── Already in design system components? → Use ref instance
    └── Need a new icon? → Use icon_font type or generate
```

---

## Pre-Generation Checklist

- [ ] Searched for existing logos and images with `pencil_batch_get`?
- [ ] Used name patterns like `logo`, `brand`, `image`, `hero` in the search?
- [ ] Checked if reusable components (Navbar, Header) contain a logo?
- [ ] Copying existing assets rather than regenerating?
- [ ] For logos specifically: **absolutely confirmed** no logo exists in the document?

---

## See Also

- [component-reuse.md](component-reuse.md) — Check reusable components that may contain logos/icons
- [screenshot-qa.md](screenshot-qa.md) — Verify copied assets are placed correctly


path: mcp/mcp-tools.md
content: # MCP Tool Quick Reference

> **Applies to**: Mode B (MCP Tools)
> **See also**: [image-reuse.md](image-reuse.md) · [screenshot-qa.md](screenshot-qa.md)

Quick reference for the Pencil MCP tools used in Mode B. For the full Mode B workflow, see the "Mode B — MCP tool-based" section in `SKILL.md`.

---

## Tool Overview

| Tool | Use |
|------|-----|
| `pencil_get_editor_state` | First call — get file state and `.pen` schema |
| `pencil_batch_get` | Read nodes, find `reusable: true` components |
| `pencil_batch_design` | Insert / copy / update / delete / move / generate-image |
| `pencil_get_variables` / `pencil_set_variables` | Read / write design tokens |
| `pencil_get_screenshot` | Visual verification |
| `pencil_snapshot_layout` | Detect clipping, overflow, overlap (`problemsOnly: true`) |
| `pencil_get_guidelines` | Topics: `code`, `tailwind`, `landing-page`, `design-system`, `table` |
| `pencil_find_empty_space_on_canvas` | Place new screens |
| `pencil_search_all_unique_properties` / `pencil_replace_all_matching_properties` | Audit & bulk-edit properties |
| `pencil_open_document` | Open / create document |

---

## Typical Call Order

```
1. pencil_get_editor_state        -> get .pen schema, current state
2. pencil_batch_get reusable=true -> discover existing components (REUSE > recreate)
3. pencil_get_variables           -> read tokens; pencil_set_variables to create new
4. pencil_find_empty_space        -> place new artboards
5. pencil_batch_design            -> build ONE section at a time
6. pencil_get_screenshot + pencil_snapshot_layout(problemsOnly:true) -> verify each section
7. Fix issues -> re-screenshot -> re-check (verify loop)
8. Final full-file layout audit -> problemsOnly MUST be empty before declaring done
```

## Verification Discipline

- Always screenshot and run `pencil_snapshot_layout(problemsOnly: true)` after **each** section, not only at the end. See [screenshot-qa.md](screenshot-qa.md).
- Before generating new images/logos, search existing nodes with `pencil_batch_get` and copy/resize rather than regenerate. See [image-reuse.md](image-reuse.md).
- For bulk property audits or token migrations, prefer `pencil_search_all_unique_properties` + `pencil_replace_all_matching_properties` over manual per-node edits.


path: mcp/screenshot-qa.md
content: # Visual Verification

> **Applies to**: Mode B (MCP Tools)
> **See also**: [overflow-prevention.md](../references/overflow-prevention.md) · [image-reuse.md](image-reuse.md)

---

## Why Screenshot Verification Cannot Be Skipped

Layout and spacing problems in the node tree are invisible. Only the rendered output reveals them. The following issues are only discoverable through screenshots:

| Issue Type | In the Node Tree | In the Screenshot |
|------------|-----------------|-------------------|
| Wrong font weight | Values look correct | Text appears too thin or too bold |
| Inconsistent padding | Values differ slightly | Cards have visibly uneven whitespace |
| Color too close to background | Values are individually correct | Element "disappears" or is hard to read |
| Alignment drift | Coordinate values are close | Elements are visibly misaligned |
| Missing gap | gap/padding exists but direction is wrong | Two sections run directly into each other |
| Auto-layout direction wrong | layout property exists | Children stack in unexpected direction |
| Image aspect ratio wrong | fill type is correct | Image appears stretched or squished |

---

## Core Principle: Build in Sections, Verify in Sections

> **Inspired by Stitch** — AI-driven design should be **conversational and iterative**: verify, get feedback, and fix after each section — not build everything at once and check at the end. This mirrors Stitch's "real-time suggestions" and "conversational refinement" philosophy.

```
Build Header    → Screenshot Header    → Find issues → Fix → Confirm
Build Hero      → Screenshot Hero      → Find issues → Fix → Confirm
Build Features  → Screenshot Features  → Find issues → Fix → Confirm
Build Footer    → Screenshot Footer    → Find issues → Fix → Confirm
Complete        → Screenshot full page → Final review
```

**Never** build a full page and then screenshot only at the end.

---

## Verification Workflow

### Step 1 — Screenshot the Current Section

```
pencil_get_screenshot({
  filePath: "path/to/file.pen",
  nodeId: "sectionNodeId"
})
```

For the final full-screen verification:

```
pencil_get_screenshot({
  filePath: "path/to/file.pen",
  nodeId: "screenNodeId"
})
```

### Step 2 — Visual Analysis Checklist

After receiving the screenshot, check against each dimension:

**Alignment**
- [ ] Elements properly centered/left/right aligned?
- [ ] Multiple columns equal width? Grid items consistently spaced?

**Spacing**
- [ ] Adequate padding inside containers?
- [ ] Gaps between elements consistent? Following 8px grid?

**Typography**
- [ ] Text readable (sufficient size and contrast)?
- [ ] Headings visually distinct from body text?
- [ ] Any text clipped, overlapping, or overflowing?

**Color & Contrast**
- [ ] Colors sourced from design variables (not hardcoded)?
- [ ] Contrast sufficient for readability?
- [ ] Overall color scheme cohesive?

**Completeness**
- [ ] All expected elements present?
- [ ] Icons/images placed correctly?
- [ ] Any empty areas or rendering breaks?

### Step 3 — Layout Problem Detection

Run in parallel with the screenshot:

```
pencil_snapshot_layout({
  filePath: "path/to/file.pen",
  parentId: "sectionNodeId",
  maxDepth: 3,
  problemsOnly: true
})
```

Catches programmatic issues the screenshot may not make obvious:
- **Clipped**: Child node extends beyond parent container bounds
- **Overlapping**: Sibling nodes unintentionally overlap
- **Out of bounds**: Node positioned outside its parent

### Step 4 — Fix and Re-Verify

```
Issue found
  → pencil_batch_design (fix with U/R operations)
  → pencil_get_screenshot (confirm fix)
  → pencil_snapshot_layout (confirm no new issues introduced)
  → Continue to next section
```

**Never skip the re-verify step.**

---

## Screenshot Timing Reference

| Moment | What to Screenshot |
|--------|-------------------|
| After completing a section | The section's root frame |
| After fixing an issue | The affected area |
| When the full design is complete | The complete screen/artboard |
| After modifying an existing design | The changed section |
| After bulk property updates | At least one affected area |
| When comparing two variants | Both variants side by side |

---

## Final Full-Screen Review Checklist

Before submitting the final screenshot, confirm all items:

- [ ] Overall visual hierarchy makes sense?
- [ ] Sections clearly separated with visual distinction?
- [ ] Spacing consistent from top to bottom?
- [ ] Looks professional and polished?
- [ ] Mobile: all content within the 375px frame?
- [ ] No empty gaps or rendering breaks?
- [ ] Color scheme harmonious as a whole?
- [ ] Adequate whitespace?

---

## See Also

- [overflow-prevention.md](overflow-prevention.md) — Fix patterns for overflow issues
- [image-reuse.md](image-reuse.md) — Verify that copied assets are placed correctly


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal:  | SkillSpector: suspicious (MEDIUM)