<!-- demo:embed example="basic/3-custom-theme" -->

This is the headline doc. By the end of it you'll have a brand-new email sample shipped inside the `default` theme — appearing in `/gallery.php`, opening in `/builder.php`, surviving a save round-trip, passing the smoke E2E. Lock-step copy-along: read top to bottom, paste each block where the doc says, run the verify step. **Time budget: 30 minutes.**

The fixture you'll build: a "Welcome to Brand" onboarding email — a single-column layout with a hero image, a heading, two body paragraphs, a CTA button, and a footer. Same shape any SaaS sends a new signup.

> **Heads up — sandbox safety.** All file paths in this doc point inside `themes/default/`. If you'd rather not pollute the shipped theme during practice, copy the dir first: `cp -r demo/themes/default demo/themes/sandbox` and substitute `sandbox` for `default` in every command. The exercises work identically.

## Step 1 — Pre-flight

Your dev server must be running. From the repo root:

<!-- buyer:try -->
```bash
php -S localhost:8000 -t demo/
```

Then in your browser:

<!-- buyer:verify -->
- Open <http://localhost:8000/gallery.php>. You should see the theme browser with **43 cards** across "Standard email" / "Rich content" / "Page templates".
- Open <http://localhost:8000/builder.php>. The Minimal sample should render in the canvas.

If either fails, return to [Quickstart](/docs/01-quickstart/) — those are the boot-time blockers.

## Step 2 — Pick a target

Decide three things before you write a line of JSON:

1. **Internal slug** — the filename. Use `PascalCase` matching the rest of the shipped samples: `WelcomeToBrand` (not `welcome-to-brand` or `welcome_to_brand`).
2. **Display name** — what buyers see in the gallery: `"Welcome to Brand"`. Title-cased English; no markdown, no emoji.
3. **Layout shape** — what stack of rows the email needs:
   - Hero block (logo + h1 headline + sub-paragraph)
   - Body block (two paragraphs introducing the product)
   - CTA block (one button, centered)
   - Footer block (single paragraph + unsubscribe link)

For this walkthrough, every choice is fixed: slug `WelcomeToBrand`, name `Welcome to Brand`, the four-row layout above.

## Step 3 — Author the JSON

Below is the page tree for the new sample. Every node uses the shapes from [JSON structure](/docs/03-json-structure/) — verify each as you read.

<!-- buyer:try -->
```json
{
    "theme": "default",
    "name": "PageElement",
    "template": "Page",
    "formats": {
        "background_color": "#F4F4F4",
        "padding_top": 0,
        "padding_right": 0,
        "padding_bottom": 0,
        "padding_left": 0
    },
    "page_title": null,
    "blocks": [
        {
            "name": "BlockElement",
            "template": "Block",
            "formats": { "padding_top": 40, "padding_bottom": 30 },
            "elements": [
                {
                    "name": "ImageElement",
                    "template": "Image",
                    "formats": { "width": 130, "align": "center" },
                    "src": "master/assets/image/email/logo.png",
                    "alt": "Brand logo",
                    "url": "",
                    "target": "",
                    "rel": ""
                },
                {
                    "name": "HeadingElement",
                    "template": "H1",
                    "formats": { "text_align": "center", "text_color": "#111111", "font_size": 32 },
                    "text": "Welcome to Brand"
                },
                {
                    "name": "PElement",
                    "template": "P",
                    "formats": { "text_align": "center", "text_color": "#444444", "font_size": 16 },
                    "text": "We're glad you're here. Here's how to get started in three minutes."
                }
            ]
        },
        {
            "name": "BlockElement",
            "template": "Block",
            "formats": { "padding_top": 20, "padding_bottom": 20 },
            "elements": [
                {
                    "name": "PElement",
                    "template": "P",
                    "formats": { "text_color": "#212121", "font_size": 14, "line_height": 22 },
                    "text": "Brand is the simplest way for your team to ship beautiful emails. Drop the snippet on your site and you're ready to send in five minutes."
                },
                {
                    "name": "PElement",
                    "template": "P",
                    "formats": { "text_color": "#212121", "font_size": 14, "line_height": 22 },
                    "text": "If you have questions, hit reply — a real human reads every message."
                }
            ]
        },
        {
            "name": "BlockElement",
            "template": "Block",
            "formats": { "padding_top": 10, "padding_bottom": 30 },
            "elements": [
                {
                    "name": "ButtonElement",
                    "template": "Button",
                    "formats": {
                        "background_color": "#000000",
                        "text_color": "#FFFFFF",
                        "border_radius": 4,
                        "padding_top": 12,
                        "padding_bottom": 12,
                        "padding_left": 28,
                        "padding_right": 28,
                        "align": "center"
                    },
                    "text": "Open the dashboard",
                    "url": "https://example.com/dashboard",
                    "target": "_blank"
                }
            ]
        },
        {
            "name": "BlockElement",
            "template": "Block",
            "formats": { "padding_top": 30, "padding_bottom": 40 },
            "elements": [
                {
                    "name": "PElement",
                    "template": "P",
                    "formats": { "text_align": "center", "text_color": "#888888", "font_size": 12 },
                    "text": "You received this email because you signed up at brand.com. Don't want these any more? Unsubscribe."
                }
            ]
        }
    ]
}
```

Notice four invariants — verify each:

- Every node has `name`, `template`, `formats` (RULE-H6 — round-trip needs them).
- Every block carries only `padding_top` + `padding_bottom`. No margin, no horizontal padding (RULE-H11).
- Every element has explicit pixel widths / sizes. No `"width": "100%"` or `"font_size": "1em"` (RULE-H12).
- Three colours (`#F4F4F4`, `#000000`, `#FFFFFF`) plus `#111111` / `#212121` / `#444444` / `#888888` are all greyscale variants — counts as one chromatic family. Within the 4-colour cap (RULE-H8).

## Step 4 — Put the file in the right place

Save the JSON above as:

<!-- buyer:try -->
```
demo/themes/default/master/sample/email/WelcomeToBrand.json
```

Filename `WelcomeToBrand` (no `.html`, no `_`, no spaces). The engine looks for samples in this exact folder; mis-spelling silently means "this sample doesn't exist" (gallery card 404 on click).

## Step 5 — Render the HTML preview

Open the new sample in the live builder by passing it as a query param:

<!-- buyer:try -->
```
http://localhost:8000/builder.php?theme=default&sample=master/sample/email/WelcomeToBrand
```

<!-- buyer:verify -->
- The canvas should render the four blocks: hero → body → CTA → footer.
- Click the heading → the right-rail settings panel shows H1 controls.
- Click the button → the panel shows button-shape + colour controls.
- The four blocks should be vertically stacked with the padding values from the JSON; no horizontal scroll.

If the canvas is blank, open DevTools → Console. The most common cause is a JSON typo: a stray comma, a quoted boolean (`"true"` instead of `true`), a missing `template`. The console message names the offending node.

## Step 6 — Add the thumbnail

The gallery shows a card per sample — the card needs a thumbnail. Two paths:

**Auto-generated** (recommended): the shipped helper takes a screenshot at `496×558px`:

<!-- buyer:try -->
```bash
node scripts/core/gen-thumb.cjs WelcomeToBrand
```

That writes `demo/themes/default/master/sample/email/WelcomeToBrand.png` next to your JSON.

**Hand-crafted**: drop a `WelcomeToBrand.png` file at the same path yourself. Same dimensions (`496×558`); PNG only.

> **PNG vs SVG.** Extended templates use `.png` (full-bleed photo render); base templates use auto-detected `.svg`. Match the existing convention by shipping `.png` for any new sample registered under the `'extended'` SEED bucket.

## Step 7 — Register the sample in the seed

Open `demo/backend/_lib/ThemeRegistry.php`. Find `private const SEED`. Append a row to the `'extended'` array:

<!-- buyer:try -->
```php
'extended' => [
    /* …existing entries kept exactly as they are… */
    ['dir' => 'default', 'name' => 'Welcome to Brand', 'sample' => 'master/sample/email/WelcomeToBrand'],
],
```

Three keys per row:

- `dir` — the theme folder name. `default` (or your fork name).
- `name` — the gallery display label. Title-case English, ≤ 30 chars.
- `sample` — the path *under* `themes/<dir>/`, no leading slash, no `.json` suffix.

That's the whole registration.

## Step 8 — Verify it shipped

Reload the gallery:

<!-- buyer:verify -->
- Open <http://localhost:8000/gallery.php>.
- Find your new card under "Rich content".
- Click it. The builder should open with your new sample.
- The sample should be card #44 (43 shipped + your new one).

## Step 9 — Hard-rule audit

Walk the 8-rule checklist from [Theme anatomy](/docs/05-theme-anatomy/):

| Rule | Did you pass? | What to check |
|------|---------------|----------------|
| H1 — No copyrighted images | ✅ | You used `master/assets/image/email/logo.png`, the shipped AI-generated logo. |
| H2 — Hierarchy | ✅ | Page → Block → Element. No skipped layers. |
| H3 — No hardcoded image sizes (except logos) | ✅ | The logo's `"width": 130` is fine — logos are exempt. |
| H4 — Aspect-lock | ✅ | The image element doesn't override aspect ratio. |
| H5 — Crop is page-only | ✅ | You didn't add a crop format key. |
| H8 — 4-colour palette cap | ✅ | Black (#000), white (#FFF), accent (light grey #F4F4F4), greys for body text — ≤ 4 chromatic families. |
| H10 — 5-typography-tier cap | ✅ | One H1 + paragraphs at 14 + 16 + 12 (body / lede / footer). 4 tiers used; 5 max allowed. |
| H11 — No margin on Block | ✅ | Every block uses only `padding_top` / `padding_bottom`. |
| H12 — Explicit units | ✅ | Every dimension is an integer (pixels). |
| H13 — Dead keys forbidden | ✅ | Every key in your JSON is a valid format / attribute. |

## Step 10 — Round-trip gate

This proves your sample survives a save:

<!-- buyer:try -->
1. Open <http://localhost:8000/builder.php?theme=default&sample=master/sample/email/WelcomeToBrand>.
2. Click the H1 → change the text to `"Welcome aboard, friend"`.
3. Click **Save** in the top-right.
4. Reload the page.

<!-- buyer:verify -->
- The H1 still reads "Welcome aboard, friend".
- The save indicator showed "Saved ✓" before reload.

The save overwrites the sample's own `.json` (and writes its rendered `.html` beside it) under `demo/themes/`; the next load reads back from there. Saving needs the demo backend switched on — [Switch the demo backend on](/docs/13-backend/#switch-the-demo-backend-on). (When you ship a real backend, that storage layer changes — see [Wire a real backend](/docs/13-backend/) — but the round-trip rule stays the same.)

## Step 11 — Smoke E2E

The shipped Playwright test discovers every sample in `themes/default/master/sample/` automatically and renders each one to confirm it boots. Run it:

<!-- buyer:try -->
```bash
npx playwright test e2e/master-samples-smoke.spec.js -g WelcomeToBrand
```

<!-- buyer:verify -->
- One test executes (the one that targets your new sample).
- Output: `1 passed`.
- If it fails, the error names what's wrong (a missing thumbnail, a JSON parse error, a template that errored at render).

If you didn't pass `-g WelcomeToBrand` you'd run the full suite (~30 seconds) — useful to confirm you didn't accidentally break existing samples.

## Did you make it?

You should now have:

- ✅ A `WelcomeToBrand.json` shipped under `themes/default/master/sample/email/`.
- ✅ A matching `WelcomeToBrand.png` thumbnail.
- ✅ A new row in `ThemeRegistry::SEED['extended']`.
- ✅ A 44th card in `/gallery.php` that opens the sample in the builder.
- ✅ A passing `master-samples-smoke` E2E test.

If a step didn't work:

- 🩹 *Canvas blank on the new sample URL* → open DevTools, look for a JSON parse error. Most often a stray trailing comma or a typo'd `template` value.
- 🩹 *Thumbnail script fails* → `node scripts/core/gen-thumb.cjs <slug>` requires Playwright installed (`npm install`). Run the install once at the repo root.
- 🩹 *Card not appearing in gallery* → confirm the `'extended'` SEED array got your row. Watch out for missing comma at the end of the previous row.
- 🩹 *Round-trip drops your changes* → open DevTools → Network and look at the POST to `/backend/save.php`. **403** = the demo backend is switched off (start the server with `BJS_DEMO_BACKEND=open`). **404** = the sample's `.json` does not exist yet — `save.php` only overwrites a sample the builder could load. **500** = the theme tree is not writable (`chmod -R u+w demo/themes`).
- 🩹 *Smoke test fails with "thumbnail file does not exist"* → `gen-thumb.cjs` needs a sample that renders cleanly first. Reorder: render in builder OK → run gen-thumb → re-run E2E.

What's next?

- → [Create a theme](/docs/07-create-theme/) — graduate from one new sample to one new theme dir (logo, colours, custom Page template).
- 🔗 [`examples/basic/3-custom-theme/`](/examples/basic/3-custom-theme/) — runnable code that loads a custom theme via `?theme=<dir>` URL params.

Stuck?

- 🐛 grep `themes/default/master/sample/email/Minimal.json` for the smallest-possible reference shape.
- 🐛 the shipped 50+ samples are your library — if you want a feature you don't see in this doc (background image, full-width hero, animated GIF), open a similar sample's JSON and copy the relevant block.
- 🐛 file an issue with: your `WelcomeToBrand.json`, the gallery URL, exact error from console / E2E output.
