# Editing the preferred reduced-text ads — final handoff 0.2.0

Accepted source: checkpoint 0.2.3. Work in a copy when an edit is authorized; no rebuilding is needed to use the delivered images. The existing macOS/Pillow/fontTools pipeline remains in use. The generator’s default selector targets the **earlier fuller-text** collection. Always select `message-alternatives` for the preferred reduced-text collection.

## Fields that control the artwork

| Control | Source and field | Behavior |
|---|---|---|
| On-image headline | [concepts.json](../sources/ads-message-alternatives/concepts.json), `concepts[].headline` | Authoritative wording. Plain text for benefit concepts; `{monthly_total}` and `{promotion}` resolve from that concept’s authorized suite. Tokens are not printed on the artwork. |
| Explicit line breaks | `headline_lines.square` / `.portrait` | Arrays of lines whose joined text must equal `headline`, including punctuation and tokens. Both formats are checked. After editing the headline, update or remove stale hints; a conflict stops the build. Without hints, the existing measured wrapper uses the current font size and a maximum of two lines. Text is never silently substituted or reduced to fit. |
| Dynamic promotion line breaks | `headline_break_after.square` / `.portrait` | Word-boundary indices in the resolved headline, used for the offer so its wording stays linked to suite data. `[1]` breaks after the first word. Indices must be unique, increasing and inside the headline. Do not supply both hint types for one format. |
| Caption headline and body | `caption.headline`, `caption.primary_text_short`, `caption.primary_text` | These are separate from the on-image headline. `caption.cta_wording` and `caption.suggested_button` describe the ad CTA. |
| On-image CTA where used | `artwork_cta` | Wording only; the layout determines whether a CTA is displayed. No CTA is added to layouts that omit it. |
| Photo selection | `photos[].file`, `photos[].shared` | File from the suite’s photo directory, or the shared directory for a shared photo. The first image is the private-space hero. |
| Photo crop focus | `photos[].focus.square` / `.portrait` | `[x,y]` in the range 0–1, measured from the source image’s top-left. Each format reads its own coordinates. Focus moves a rectangular crop only along axes where excess source pixels exist; clamping at an edge is normal. |
| Full-frame contain placement | `photos[].fit: "contain"` | Keeps every source pixel in frame, then proportionally resizes. Its layout frame must have the source ratio; a mismatched frame stops the build. The two-window concept uses this in both formats. Focus does not crop a contain image. Legacy `photo_panel: "contain"` is retained. |
| Shared-area label | `photos[].label` | Printed on the shared inset; must identify the shared area. Keep `SHARED 201 LOUNGE` for the current lounge. |
| Price and promotion | [suites.json](../sources/ads/suites.json), `suites.<id>.monthly_total` and `.promotion.headline` | Value-led headlines use `{monthly_total}`. The offer headline uses `{promotion}`; other 104 callouts read `.promotion.headline` directly. Office promotions remain null. |
| Destination | `suites.<id>.url` | Exported to each concept’s captions; copy-library destinations are synchronized from here. |
| Geometry and type sizes | [message_layouts.py](../tools/message_layouts.py) | Panel/photo positions and typography remain in the renderer. Marketing headlines and crop-focus overrides no longer live there. |

For example, to change the home/work concept to `YOUR OFFICE. NOT YOUR KITCHEN.`, set `headline` to that text and use `["YOUR OFFICE.", "NOT YOUR KITCHEN."]` for both `headline_lines` arrays, or remove the hints for measured wrapping. Run the composition check before exporting. A stale hint is an error, not permission to show the old wording.

The effective 0.2.2 crop settings were migrated into these fields before enabling them. The current image framing is preserved. The concept ID `suite-201f-private-shared-break` remains stable; its visible name is now **Private office. Shared extras.**

## Literal captions and synchronization

For the nine selected concepts, edit `concepts.json`, then run `tools/sync_message_copy.py`. This updates the selected entries and all rendered Markdown in [copy-library.json](../sources/ads-message-alternatives/copy-library.json) / [copy-library.md](../sources/ads-message-alternatives/copy-library.md). Edit the three unselected entries in the library JSON directly; the sync keeps them. The generator then writes matching [captions.json](../exports/ads-message-alternatives/captions.json) / [captions.md](../exports/ads-message-alternatives/captions.md). The review builder reads those same concept/library sources.

An authorized price change updates `monthly_total` and related suite facts; value artwork follows the token. Captions are **literal editorial text**, not automatic suite-data substitutions. After an authorized price or promotion change, manually update `caption.primary_text_short`, `caption.primary_text` and `caption.headline` wherever they state the changed value or offer. For selected concepts, edit those fields in `concepts.json`; for the three copy-only approaches, edit their `caption` fields in `copy-library.json`. Update literal qualifiers, sizes and URLs when applicable too. Then sync the library and regenerate captions with the selected collection’s build. Changing suite data alone does not update all captions. Current fact checks deliberately reject unauthorized changes; update those checks only when the owner authorizes new facts. Do not alter the accepted collection merely to refresh these alternatives.

`cover` is the default photo fit: a rectangular crop fills the layout’s frame without stretching, using the selected format’s focus. `contain` keeps the complete image and requires the frame to match its source ratio; this pipeline rejects an incompatible frame rather than stretching or silently cropping it. Photo frame geometry stays in `message_layouts.py`.

## Photographs and promotion

Use an untouched source photograph, record its provenance, dimensions and SHA-256 in [manifest.json](../assets/listings/manifest.json), and update the selected file and per-format focus. Only rectangular cropping, proportional LANCZOS resizing and separate graphic layers are permitted. No generation, recoloring, object removal, virtual staging or room distortion. Shared photographs remain clearly labeled and outside the private office’s stated area.

The owner-authorized Suite 104 special must remain visible in every current 104 concept and in its captions. No exact promotional rate, duration, discount, deadline, eligibility, deposit or lease terms were supplied; those fields stay unset. Changing its wording in suite data drives the artwork, including the offer-led headline. An authorized removal is a separate brief change: update affected copy, template choice and promotion requirements together. Setting it to null alone stops this brief’s offer build. Never give the office suites that promotion.

## Commands and output layers

From the project root on macOS:

```sh
/usr/bin/python3 tools/build-ads.py --preflight
/usr/bin/python3 tools/sync_message_copy.py
/usr/bin/python3 tools/test_message_editability.py
/usr/bin/python3 tools/test_message_alternatives.py
/usr/bin/python3 tools/test_verify_suite_ads.py
/usr/bin/python3 tools/build-ads.py --collection message-alternatives --layout-check
/usr/bin/python3 tools/build-ads.py --collection message-alternatives
```

`check_message_compositions.py` compares new in-memory compositions with the separate immutable checkpoint 0.2.2 archive. That archive is intentionally excluded from the new ZIP; retain it beside the extracted project to rerun that historical comparison. Other source/composition tests do not require the old archive. The retained `build-message-review.py` is the **historical checkpoint documentation builder**; it can restore old review-status wording. Do not run it just to refresh this final handoff. `build-final-handoff-docs.py` records this accepted delivery and its evidence; update the evidence and version deliberately for a later handoff. Neither documentation builder generates ad imagery.

Read-only verification of existing exports (these commands need Pillow, not Avenir):

```sh
/usr/bin/python3 tools/verify_suite_ads.py --collection message-alternatives --report docs/current-reduced-checks.json
/usr/bin/python3 tools/verify_suite_ads.py --report docs/current-fuller-checks.json
```

Both collections and the three regression suites were checked read-only during final packaging; no artwork generator was run. See the [actual command record](handoff-0.2.0-validation.md). Use new report filenames when checking later edits so historical evidence is preserved. `verify_accepted_collections.py` additionally compares accepted bytes against separately retained 0.2.3 and 0.1.0 archives; those ZIPs are intentionally excluded from this delivery.

The explicit collection selector writes only `exports/ads-message-alternatives/`. Every concept/format gets an opaque PNG and JPEG, `photo-N.png`, `00-base.png`, transparent `03-design-overlay.png`, and live-text/outlined SVGs. Optional `02-contrast.png` remains supported; current layouts do not need it. The overlay is already exported separately—use that PNG without the photograph. To reconstruct a flattened ad, alpha-composite base → optional contrast → design, then convert to RGB. Keep square and portrait sources distinct; do not stretch one format into the other.

The generator still requires macOS `/System/Library/Fonts/Avenir Next.ttc` (Bold, Demi Bold, Heavy), Pillow, numpy and fontTools. It is **not portable as-is**. No font binaries are bundled. PNG/JPEG viewing and upload do not require fonts. Live-text SVGs depend on the viewer’s fonts; outlined text avoids that dependency. **Keep SVGs beside their linked `photo-N.png` files.**

All original photographs, accepted advertisements, covers, profiles, retained social assets and reusable templates keep their existing paths. Final creative handoff is accepted. Publishing, campaigns, website changes and spending remain unauthorized. The [pre-publication checklist](../START-HERE.md#pre-publication-checklist) remains open.
