---
name: property-brochure
description: Create A4 property brochures (landscape default, portrait optional) from raw photos, transcripts, and floorplans. Produces print-ready HTML and a recompressed PDF deliverable. The skill ships an editorial "folio" template by default; if the property has a DESIGN doc that specifies its own visual scheme, follow that instead. Use when asked to create an estate agent brochure, property marketing material, or property listing document.
---

# Property Brochure

Invoked from `specialists:content-producer`. A property brochure is finished external-facing prose drafted under the agency's name, so it composes on the content-producer seat (which carries `plainly`, `prose-craft`, and `voice-retrieve-conditioning`). The admin seat delegates it from the first point: the admin is refused this skill and dispatches content-producer (Agent tool, `subagent_type: content-producer`).

Produce an A4 property brochure from raw assets. The default deliverable is a 16-page super-premium editorial **folio** in landscape A4. The brochure renders in-browser for live editing and exports to two image-only PDFs (web and print master) plus a self-contained web bundle including a companion landing page.

## Output location

By default the brochure is written to the **caller's current working directory**. The skill creates `./output/brochure.html`, `./output/web/index.html`, the print PNGs, and the two PDFs underneath `pwd` — no inference, no nesting under a brand workspace.

If you want the output elsewhere, pass `output_dir` explicitly (an absolute path or a relative path resolved against CWD). Common overrides:

| Scenario | `output_dir` |
|---|---|
| No override (the default) | `./output/` under the caller's CWD |
| Brochure should live next to an existing brand pack | `<brand_dir>/properties/<property_slug>-<id>/output/` |
| Brochure should land somewhere specific (a delivery folder, a customer's repo) | the supplied absolute path |

Do not silently nest the output under a brand workspace just because one is present. The `make-brochure` orchestrator may pass `output_dir` to align with its own routing; this skill on its own runs in the caller's CWD unless told otherwise.

## Step 1 — copy the canonical templates (do this first)

`references/template.html` and `references/index.html` are the **only** acceptable starting points. They are placeholder-only — every property value is a `{{ token }}`. Copy them before doing anything else. **Do not write brochure HTML or page HTML from scratch. Do not start from a sibling property's output. Do not improvise structure from prose.**

```bash
mkdir -p <output_dir>/web
cp ~/.claude/plugins/brochures/skills/property-brochure/references/template.html <output_dir>/brochure.html
cp ~/.claude/plugins/brochures/skills/property-brochure/references/index.html      <output_dir>/web/index.html
```

`<output_dir>` is whatever the **Output location** section above resolves to — typically `./output/` in the caller's CWD.

If the canonical template looks wrong for the brand, **patch the template** (open a discussion with the user about what to change). Don't substitute a different starting point for one run — that bakes drift into every brochure.

Full register/structure rules: [references/registers.md](references/registers.md). Forbidden starting points are listed there.

## Step 2 — verify the copy worked

Both files must contain canonical sentinels (proves they came from `references/`, not hand-written or hand-edited):

```bash
grep -q '{{ property_name }}'  <output_dir>/brochure.html  || echo "FAIL: brochure not from template"
grep -q 'hero-stage'           <output_dir>/web/index.html  || echo "FAIL: index.html not from references"
```

Both greps must succeed before populating content. If either fails, recopy.

## Step 3 — choose the visual register

| Register | What you change |
|---|---|
| **Branded** (default, DESIGN.md re-skin) | Swap font names at `--serif` / `--body-serif` / `--sans`; replace palette tokens with the DESIGN doc's values; substitute logo PNGs. Sixteen-page structure stays. |
| **Premium** (opt-in only, template scheme retained) | Keep all super-premium tokens (palette, type, layout) verbatim. Swap **only** the agent's logo and contact details. Do not apply the brand's palette or type tokens. |

Decision order:
1. User explicitly said **"premium" / "super-premium"** in the request → Premium (regardless of DESIGN.md).
2. Otherwise → **Branded**. If no DESIGN.md exists for the brand, **run `brand-design` first** to produce one. Do not fall back to Premium just because the brand pack is missing; Premium is opt-in, "missing brand pack" is a setup step, not a register decision.

Detail and forbidden patterns: [references/registers.md](references/registers.md).

## Step 3b — operator shorthands the request may carry

The operator's one-line request can carry instructions beyond the brand + property URLs. Interpret naturally; do not require the operator to memorise shortcuts. If a phrase is ambiguous, ask one targeted question.

### Skipping part of the seller brief

If the operator's request contains a phrase like "skip lifestyle", "skip questions", "skip extra info", "skip the brief", "no interview", "use the listing description", interpret it against the section headings of `make-brochure/references/seller-brief-template.md`:

| Operator phrase mentions… | Maps to | Effect |
|---|---|---|
| `lifestyle` | **Layout & Lifestyle** (Q5 to Q7) | Mark those questions `[SKIP]`; fall back to listing-derived copy for the chapter decks they would have informed. |
| `area`, `community`, `local` | **Local Area & Community** (Q11 to Q14) | Same: `[SKIP]` those; derive area copy from the listing only. |
| `improvements`, `upgrades` | **Improvements & Upgrades** (Q8 to Q10) | Same: particulars list emphasis comes from key features instead. |
| `motivation`, `story` | **Story & Motivation** (Q1 to Q4) | Same: opener anchors on facts (place, age, tenure) rather than the seller's voice. |
| `practical`, `buyer` | **Practical & Buyer Avatar** (Q15 to Q18, Q20) | Same: back-page tagline drawn from the listing rather than the seller. |
| `questions`, `the brief`, `interview`, `extra info` (broad) | the whole brief gate | Bypass the gate entirely; compose copy from the listing description, the photos, and the floor plan. |

If a phrase doesn't clearly map to a section, ask **one** targeted question (e.g. "Which sections do you want skipped: Layout & Lifestyle, Local Area, both?") and proceed from the answer. Record the resolved decision by writing `[SKIP]` into each affected question in `seller-brief.md` (parallel to the existing `[VERBATIM]` marker), so the next re-render behaves the same without re-asking.

A `[SKIP]` answer is a **valid** answer for the gate. An empty answer is still a defect.

### Adding socials to the same request

If the operator's request contains "and socials", "+socials", "with socials", "with OG images", or any equivalent, append a socials step after the brochure web bundle is built:

1. Invoke the `property-socials` skill on the same `<property_dir>` (it reads `output/web/index.html`, `property.json`, and the brand pack).
2. `property-socials` owns the full deliverable: OG JPGs in `output/web/socials/` (landscape 1200×628, square 1200×1200, portrait 1080×1350), the gallery page at `output/web/socials.html`, captions at `output/web/social-posts.md`, and the `og.html` template kept for re-runs. The orchestrator does **not** produce a separate gallery — everything lands when the skill returns, so a direct standalone invocation produces an identical bundle to the orchestrator-invoked one.
3. After the skill returns, extend the bundle's smoke-test to include `socials.html` and every JPG referenced; all must return 200 from the isolated HTTP server.
4. Re-zip the web bundle with the socials artefacts included.

If the operator did **not** request socials, do not run the step; adding 1 to 3 MB of imagery and a third HTML page silently is an over-reach.

## Step 4 — populate, render, deliver

**Before writing any copy, call `voice-retrieve-conditioning`** with `format: "article"`, `length: "long"`, `scope: "org"`, and `topic` set to the property's `addressLocality` or the marketing angle (e.g. "period townhouse in Hackney"). A brochure goes out under the agency's name, so it uses the **house voice** (`scope: "org"`), not any one operator's personal voice. When the tool returns a non-null `styleCard` (`status` of `ok` or `fallback-org`), condition the long-form narrative copy on it — the style card + exemplars feed the editorial copy register; brand-required structure rules still override. When `status` is `no-data` or `error`, fall back to the unconditioned register documented in `references/copy.md`. Emit narration `[property-brochure] voice-mirror styleCardBytes=<n> exemplars=<k>` when conditioning was applied.

Read the seller brief once before writing any copy. Then walk the `{{ placeholder }}` tokens in both files top-to-bottom, substituting from the brief and `property.json`. The complete substitution contract — every token, its source field, its expected format and voice — lives in [references/placeholders.md](references/placeholders.md). Each token also has an inline `<!-- REPLACE: ... -->` comment in the template giving the short version.

| Concern | Reference |
|---|---|
| **Placeholders — the substitution contract** (every `{{ token }}`, what fills it, what format) | [references/placeholders.md](references/placeholders.md) |
| Seller brief — per-question → brochure mapping, [VERBATIM] contract | [references/seller-brief.md](references/seller-brief.md) |
| Image preview safety, slot verification, requirements, optimisation, brand logo | [references/images.md](references/images.md) |
| Editorial copy register, em-dash policy, "home" vs "house", text-on-images, typography, stats rows, AI hero prompt, **per-section composition guide** | [references/copy.md](references/copy.md) |
| Orientation, 16-page layout, cover, floorplan, distinguishing features, Material Information, QR codes, location map | [references/structure.md](references/structure.md) |
| Live editing, snapshot capture, PDF deliverable, web bundle | [references/build.md](references/build.md) |
| `index.html` landing page — sections, hero rotator, mobile/drawer | [references/index-landing.md](references/index-landing.md) |

### Structured-data (JSON-LD) step

One token is special: `{{ jsonld_script_block }}` is filled by calling the `aeo-emit-jsonld` MCP tool, not by hand. The tool reads `property.json` fields and returns a verbatim `<script type="application/ld+json">` block to paste into the token. Without this block the page silently fails answer-engine indexing (`aeo-audit-page → jsonld-present: fail`).

Call once per render, with `label: "RealEstateListing"` and the property bag from `property.json`. Full token contract and call shape: [references/placeholders.md → Category 10](references/placeholders.md). The tool returns `{ scriptBlock }`; substitute `scriptBlock` into `{{ jsonld_script_block }}` in both `brochure.html` and `web/index.html`. If the tool throws `MissingPropertyError`, the source `property.json` is incomplete — fix the data, not the brochure.

### Substitution completion gate

A brochure with any `{{ x }}` still in the rendered HTML is **shipped broken**. Before any snapshot capture or PDF build, run:

```bash
unsubstituted=$(grep -c '{{' "$output_dir/brochure.html" "$output_dir/web/index.html" 2>/dev/null | awk -F: '{s+=$2} END {print s}')
[ "$unsubstituted" -eq 0 ] || { echo "DEFECT: $unsubstituted unsubstituted {{ tokens }} remain"; exit 1; }
```

`grep '{{ ' brochure.html` is the canonical bug-detector — any match is a defect.

## Required inputs

- **Property photographs** — minimum 16, ideally 25+
- **Property name and address** (see `property-extract → Property name vs address`)
- **Floor plan image** — MUST be `Read` before any floor-area, per-floor breakdown, or room-dimension substitution. The image carries measured ground / first / outbuilding totals; the listing's prose description does not.
- **EPC rating** — verified from the EPC chart image; if not legible, ask the operator.
- **Property walkthrough transcript or written description**
- **Seller brief** at `<property_dir>/seller-brief.md` — non-empty answers to all 20 questions in `make-brochure/references/seller-brief-template.md`. Stop and ask if missing; do not synthesise from the listing description.
- **Agent details** — from `property.json -> agent` (no built-in default). Logos follow the colour-of-the-art naming convention: `<brand>-logo-light.png` (for dark surfaces — cover/back) and `<brand>-logo-dark.png` (for light surfaces — TOC).
- **Page orientation** — `landscape` (default) or `portrait`
- **DESIGN doc** (optional) — controls register choice; never replaces the template structure

## Pre-substitution verification gate

Before any `{{ token }}` is substituted, walk every field whose primary source is a file already on disk or one direct question to the operator. **Read the source or ask. Never silently default to `TBC`, "approx.", or an empty string when the answer was a `Read` or a one-line question away.**

The recurring failure pattern is trusting a derived summary (a WebFetch markdown digest, a listing's prose description) over the primary source already in `<property_dir>/raw/`. The fix is to consult the primary source before the substitution dict is filled — not after the brochure renders. This rule is the factual counterpart to the contact-sheet rule: that one says "verify each photo by pixel content, not filename"; this one says "verify each fact by primary source, not by derived summary."

### Fields that need active verification

| Field | Primary source — Read or ask, never paraphrase |
|---|---|
| `floor_area`, `mi_floor_area`, per-floor breakdown, `floorplan_lead`, `floorplan_sub`, landing-page floor-plan rows | The floor plan image. Floor plans are almost always under 2000 px on the longest edge — safe to `Read` directly. Take ground, first, garage, outbuilding totals from the image. **Never substitute from the listing's prose ("3,000 sq ft", "spacious")**. |
| Room dimensions in chapter copy (principal bedroom, kitchen, sitting room) and en-suite / bathroom layout | Same floor plan. If the listing prose says "principal bedroom with en-suite" but the plan shows no adjacent shower room, the plan wins. |
| `video_url`, `video_short_url` | Inspect the live listing page directly (browser MCP, or a second targeted WebFetch asking specifically "is there a video tour, give the URL"). A WebFetch summary that omits the field is not evidence the field is absent. If still unclear, ask the operator. |
| `mi_council_tax`, `mi_broadband`, `mi_mobile`, `mi_tenure`, `mi_year_built`, `mi_uprn`, `mi_title_number`, `mi_flood_*`, `mi_local_authority`, `mi_easements`, `mi_covenants`, `mi_rights_of_way` | These come from a Sprift KFB report, the seller, or the agent. If not supplied, prompt the operator with one consolidated question listing every unknown — do not silently fill with `TBC`. |
| `epc_rating` | The EPC chart image. If the rating isn't legible, prompt. |
| `agent.phone`, `agent.email`, brand logo, brand name | From `property.json -> agent` and the brand pack. If either is absent, prompt — never invent a default. |

### Operator confirmation prompt

When any field above is unresolved after consulting its primary source, **stop before substitution** and present the operator with one consolidated prompt listing every unknown by name, the primary source already consulted, and the candidate value (if any). Operator answers in one pass; substitution then proceeds from confirmed values. **Never render a brochure with fields silently coerced to `TBC`, an approximation, or a default. A `TBC` shipped to a buyer must have been confirmed as a `TBC` by the operator, not chosen by the agent.**

**EPC is in a stricter tier — `TBC` is not a confirmable outcome.** UK law requires an EPC for any marketed property, so the brochure cannot ship with `epc_rating: "TBC"` even with operator confirmation. If `property-extract` returned `TBC` (the listing displays it that way, or the page lacks the data), the operator confirmation prompt for the EPC slot must demand an actual A–G band plus the current/potential scores and a path to the certificate PDF/image. The operator's path of last resort is the [EPC Register](https://www.epcregister.com/) — look up by address or postcode, download the certificate, and supply the band + scores back to the brochure step. Confirming `TBC` for `epc_rating` is the one answer the prompt must refuse to accept; the brochure halts until a real rating arrives.

## Hard rules (load-bearing — do not violate)

- **Never `Read` an image whose longest edge exceeds 2000px** — it can drop the session. Measure with `sips -g pixelWidth -g pixelHeight <file>` first.
- **Never substitute a floor-area value without first `Read`ing the floor plan image.** Listing-prose figures and round-number guesses are silent factual defects.
- **Never assign an image to a slot from filename alone.** Verify by pixel content via a contact-sheet montage.
- **Never synthesise a location map** from postcode, OSM call, or AI artwork. Extract the agent's screenshot or omit. ([references/structure.md → Location map](references/structure.md))
- **Page count must be even** (16 default; 12 if the property is too small). 14 is forbidden.
- **Image-only PDFs** — no fonts embedded, no Ghostscript anywhere in the chain.
- **Self-contained `output/`** — never reference `../` paths. Every asset the HTML uses must live under `output/images/`.
- **Web bundle must include `index.html`** — a bundle without it is incomplete.
- **Suppress screen-only chrome before snapshotting** — the template's `.download-bar` is hidden under `@media print` but Playwright's `element.screenshot()` captures the screen render, where the bar IS visible. The render script in `build.md` injects a style tag (`.download-bar { display: none !important; }`) before capture. Any custom render script must do the same.
- **Brand-token overrides MUST land at the `BRAND_TOKENS_OVERRIDE_SLOT` sentinel** — immediately before the closing `}` of the template's `:root` rule. Any earlier insertion site loses the CSS cascade: the template's own `--paper-25 / --serif / --gold-700` declarations win in source order, and the brochure renders in Premium register despite `register=branded`. The substitution gate and snapshot capture both pass — failure is silent until visual review.
- **Display-headline wrap contract on `{{ property_name }}`** — `.back-headline em`, `.cover-title`, and `.opener-title em` carry `white-space: nowrap` so multi-word property names sit on one line regardless of brand display family. Removing the nowrap rule reintroduces non-deterministic wrap (same name renders correctly in Premium and breaks in Branded when the display family changes). For a property name long enough to overflow at the column width, shrink the headline `font-size` or shorten the name — never delete the nowrap rule.
- **Web bundle must contain only referenced images.** Before the zip step, walk `output/web/images/` and remove every file not appearing in any of the bundle's HTML surfaces (`brochure.html`, the companion landing page, optional `og.html`). A bundle carrying unreferenced images is a build defect; the 30–50 MB target documented in `build.md → Web bundle` is enforced, not aspirational. See `build.md → Strip unreferenced images` for the recipe.
- **Web PDF defaults to JPEG-embedded at quality 85.** Snapshot PNGs in `output/.snapshots-web/` are re-encoded to JPEG q=85 before `img2pdf` binds them. PNG-embedded for the web grade is opt-in only (one-pagers / report-style where small-text crispness outweighs wire size). The print master never flips to JPEG — its audience archives for press runs. A web PDF over ~20 MB on a 16-page folio means the re-encode was skipped.
- **Companion landing page is named `index.html`.** Every reference (template, bundle, smoke-test, docs) uses `index.html` so the static host serves it as the default document without an explicit URL. Do not drift back to `page.html` — that name was deprecated in favour of hosting-platform default-document compatibility.
- **Floorplan ships as PNG on disk.** The brochure templates reference `images/<slug>-floorplan.png` and the contract is PNG-only on disk regardless of source format. If the operator-supplied source is JPG, convert at build time (`magick <src>.jpg <slug>-floorplan.png`) before substitution. JPEG compression artefacts on thin lines and small text are unacceptable for floorplans.
- **Brand-token override block must carry both vocabularies and target both templates.** The brochure (`template.html`) and the landing page (`index.html`) declare independent `:root` token vocabularies (`--paper-25 / --gold-700 / …` vs `--paper / --gold / …`). The brand-tokens block must emit **both** sets of token names with the same brand values and substitute at the `BRAND_TOKENS_OVERRIDE_SLOT` sentinel in **both** templates. A block that only carries the brochure's vocabulary leaves the landing page rendering Premium silently — the substitution gate passes and the snapshot capture proceeds. See `references/build.md → Brand-token override emission` for the mapping table and example block. Long-term vocabulary unification is a follow-up; until it lands the dual emission is structurally required.
- **Doc-comment strip pass runs against BOTH templates.** After substitution, the `<!-- REPLACE … -->` authoring notes must be removed from both `brochure.html` AND `index.html`. A strip pass that covers only the brochure leaves landing-page authoring notes rendering as visible body text whenever a comment delimiter is misplaced. The companion source-side check in `references/build.md → Template authoring hygiene + CI lint` catches the same defect class at template-edit time.
- **EPC is mandatory — `TBC` is not shippable.** UK law requires an EPC for any marketed property. The brochure must not render with `epc_rating: "TBC"`, no matter what `property-extract` returned. If the rating is missing or the listing displayed `TBC`, the operator confirmation prompt must demand an actual A–G band (plus current/potential scores and the certificate path) before substitution proceeds. The operator's authoritative fallback is the [EPC Register](https://www.epcregister.com/) — look up by address or postcode, download the certificate, supply the values. `TBC` is the one answer the operator cannot confirm for this field.
- **Web bundle ships the PDF, not per-page JPGs.** The bundle does not include `cover-print.jpg … backpage-print.jpg` — those duplicate the brochure content already embedded in `<slug>-brochure.pdf`. The web copy of `brochure.html` has its `.print-img` src attributes cleared so the print stylesheet drops to the live-DOM fallback; operators who want a printable PDF download the bundled PDF directly. See `references/build.md → Clear .print-img src in the web copy`. Shipping both was the historical bundle-bloat defect.
- **Bottom chrome on `.page` is absolutely-positioned and stacks deterministically.** Three classes of bottom element exist: (1) the `.folio` page-number strip (`bottom: 8mm`, ~8mm tall → occupies bottom ~16mm of every page); (2) `.back-disclaimer` on the back page (`bottom: 16mm`, wraps to up to 5 lines × 3.7mm → occupies bottom ~34.5mm); (3) `.mi-foot` on the Material Information page (`bottom: 22mm`, ~7mm tall → occupies bottom ~22–29mm). All are `position: absolute`, anchored to their nearest positioned ancestor (`.page` which is `position: relative`). In-flow content above (e.g. `.mi-grid` inside `.mi-page`, `.backpage-inner` centred content) is a SIBLING in the stacking flow — nothing in CSS structure enforces clearance from the absolute layer. The fix is to either (a) **make the relevant footer absolute** (the approach used for `.mi-foot` to prevent oversized `.mi-grid` from pushing the disclaimer into the folio), or (b) **pull the in-flow container up via its parent's `padding-bottom`** when the footer is small and predictable (e.g. `.backpage-inner` uses 42mm to clear folio + disclaimer with a 4mm gap). Never use `margin-*` on a `position: absolute` element whose anchor is `bottom` — it has no positional effect.
- **Folio is plain text on whitespace or absent — never chrome over imagery.** The `.folio > span` carries no background, blur, padding, border-radius, or shadow. It is plain text in `--teal-900` that sits on the clean whitespace reserved at the page bottom (see the previous rule on padding-bottom clearance). The folio is **hidden entirely** on full-bleed-bottom pages: `.cover`, `.page.intermission`, `.page.folio-dark`, and any `.page` whose body wraps a `.feature-page` (photo+panel layout where the photo bleeds to the page bottom). Never reintroduce a folio-pill background to "make it readable over the image" — that's competing chrome on the page; suppress the folio on that page instead. Any new full-bleed-bottom page class must be added to the `.folio` `display: none` selector in `references/template.html`.
- **Page 13's `{{ siteplan_src }}` is extract-or-omit-or-ask — NEVER fabricated.** Page 13 (the site-plan / plot page, `.page siteplan-page`, "Continued, Chapter VI · The plot") expects a real plot drawing, real site plan, or real aerial/exterior photograph — an image that EXISTS on disk because the listing supplied it, the operator staged it, or `property-extract` captured it. **The agent must never fabricate this image.** No mock plan drawn from coloured rectangles + text labels (e.g. "THE LODGE / DRIVEWAY · PARKING / SOUTH-WEST FACING LAWN" on a green background — a real-world failure observed on Plate VIII of a recent brochure), no SVG diagram synthesised from `property.json` dimensions, no AI-generated site sketch, no postcode-centroid satellite render, no Mapbox/Google static-map call with our API key, no `<canvas>` hand-drawing, no HTML/CSS box-and-label illustration. ALL of these are forbidden — the same "extract or omit" doctrine that governs `property-extract → Map detection` applies here. **The acceptable image set for `siteplan_src` is a closed list: (1) aerial / drone shot (taken from height, looking down on the property and its grounds), (2) site plan / plot drawing (line-art, supplied by the listing or staged by the operator), (3) local-area shot (street view, neighbourhood context, immediate surrounding setting that frames the plot's relationship to its area). Nothing else.** Ground-level exteriors of the house itself are NOT siteplan candidates — those belong to the cover, opener, intermission, or feature slots. Interior shots (kitchen, garden, terrace, lounge) are NOT candidates. The floorplan is NOT a candidate (page 14's content; banned above). A fabricated mock illustration is NOT a candidate (banned above). The hero photo is NOT a candidate (used on the cover).

**Before any "no image" fallback** (asking the operator, dropping to the 12-page layout, or — forbidden — fabricating), the agent MUST first **scan `<property_dir>/images/` and enumerate candidates that match the closed list above**. Visual scan via the contact-sheet montage technique already required elsewhere in this SKILL ("Never assign an image to a slot from filename alone") — read each candidate's pixel content, not the filename. Real-world failure observed today: a brochure shipped with a fabricated mock plan on page 13 even though THREE perfectly good aerial photos sat in `<property_dir>/images/` unused; the agent skipped the directory scan, jumped to fabrication, and shipped. A separate failure mode also observed: agent picked an inappropriate image (e.g. ground-level garden shot, interior moment) for the slot because it scanned but didn't filter against the closed list. Both fail the same test — the slot must be filled with an image that ACTUALLY conveys the plot's geography, not just any image that happens to be on disk.

If one or more candidates from the closed list exist, the operator confirmation prompt lists them by filename + one-line description (e.g. *"siteplan_src: 3 candidates found in images/ — <slug>-12.webp (drone, S-facing), <slug>-18.webp (street-view from the lane), <slug>-21.webp (aerial through trees). Pick one, or supply a different image."*) and the agent renders only after the operator picks. Only when zero candidates from the closed list exist on disk does the prompt request a fresh image; only when the operator declines does the brochure drop to the 12-page reduced layout. The order is **scan-and-filter first, ask second, drop-to-12 third** — fabricate, never; pick-an-inappropriate-image-just-because-it's-there, never. The brochure must NEVER ship with a fabricated illustration where a real plot/aerial belongs; this is a ship-stopper defect that misrepresents the property to a buyer. Also forbidden by the same doctrine: silently falling back to the floorplan (duplicates page 14). If the operator cannot supply a real image, the brochure drops to the 12-page reduced layout (no page 13) — never to a 16-page layout with fabricated or duplicated content on page 13.
- **Verification covers every page in the deliverable — sampling is a defect, not a shortcut.** `scrollHeight` fit checks, computed-style collision probes (`.mi-foot` ↔ `.folio` gap, `.back-disclaimer` ↔ in-flow content, any future absolute-bottom element vs in-flow neighbour), and visual review before declaring done all walk **every page** in the 16-page folio (or 12-page reduced layout). "Sample-checked 3 of 12 pages" or any variant ("spot-checked the spreads I changed", "skimmed a few representative pages") is not authorised — the skill explicitly says "verify every page" in `references/build.md → Live editing workflow` step 8 and in `references/structure.md`. The deliverable is the brochure as a whole; a defect on an unchecked page ships exactly as readily as a defect on a checked one. If a probe is expensive enough that walking 16 pages feels burdensome, the cost is the probe's, not the skill's — propose a faster probe rather than narrowing the coverage.
- **`scrollHeight > viewport budget` is ALWAYS a defect — never "negligible".** Landscape budget is **794px** (`min-height: 210mm` at 96dpi); portrait is **1123px**. Every `.page`'s `scrollHeight` must be `≤` the budget — strict, not "approximately equal". A 4px overflow IS a defect: `overflow: hidden` silently clips that excess, and on pages with absolute-positioned bottom chrome (`.folio`, `.back-disclaimer`) the clipped content paints over chrome that lives in the absolute layer. "It's only 4 pixels" is the rationalisation that ships defects — the rule does not have a tolerance band. If a page reports `scrollHeight > budget` even by 1px, the build halts until the operator addresses the content-volume mismatch (trim copy, shrink an image, reduce a row count, or split into a continuation page). Never paper over with `overflow: hidden`-as-clip; the warning sign is the deliverable's only signal that the content doesn't fit.
- **Material Information table has a hard row capacity — overflow is a content-volume defect, not a render bug.** At the default `.mi-row` sizing (9.5/11.5px fonts, 6px vertical padding, 18px `.mi-grid` row-gap), one column in the three-column MI grid fits **~13 rows** within the 22mm-clearance budget. A property with an unusually full table (e.g. 17+ rows in the tallest column, observed on a recent run) overruns by ~25mm / ~85px and clips into the folio zone. When MI's content volume exceeds the budget, the operator confirmation prompt MUST surface `mi.<column>.row_count` as an overflow item and ask the operator to either (a) trim the column to ≤ 13 rows by consolidating related disclosures (e.g. merge "easements" + "water/sewerage" + "rights of way" into one "Other matters" row that points at the seller's solicitor), or (b) opt into a continuation page (15a / 15b) — a separate skill-level decision that breaks the default 16-page contract and must be operator-confirmed. The build never silently truncates rows, never shrinks fonts to "make it fit", and never accepts a scrollHeight overflow as "negligible". The default `.mi-page` 22mm bottom-padding is sized for the typical property; properties outside the typical range MUST resolve via operator confirmation, not via the agent's judgement about which rows are "less important".
- **No `{{ token }}` may remain in rendered output** — `grep '{{' output/brochure.html` must return zero matches before PDFs are built.
- **`images.json` sidecar is mandatory.** After slot verification and before bundling, write `output/images.json` and `output/web/images.json` (byte-identical) per `references/images.md → images.json sidecar`. The downstream `:Listing` curator reads the bundle copy at `<accountDir>/pages/<slug>/images.json` after the Cloudflare Pages deploy and uses it in place of running a second per-image vision pass. A bundle without `images.json` forces the curator into its fallback path and costs one vision call per photo — silent on the brochure side, observable as `imagesFromBrochure=0` on the curator's log line.

## Scope

**Included**: HTML brochure + companion landing page, image organisation and per-slot optimisation, AI hero prompt, all print snapshots at 300 dpi, two image-only PDF deliverables (print master + web), a self-contained web bundle (`output/web/` + `<slug>-web.zip`), Material Information compliance, EPC chart, disclaimers.

**Not included**: Professional photography, actual EPC assessment, legal verification of Material Information values, agent CRM integration, print shop preparation (CMYK, bleed marks), executing the AI hero image prompt, selectable / searchable text in the PDFs (deliberate trade-off; see `a4-print-documents → Why image-only`).

**Depends on**: Playwright (Node `playwright` package), local HTTP server for browser preview, `a4-print-documents` skill, `cwebp` (libwebp), `img2pdf`, `qpdf`, ImageMagick (`magick`).
