@rubytech/create-maxy-code 0.1.68 → 0.1.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/config/brand.json +1 -0
  3. package/payload/platform/plugins/admin/PLUGIN.md +1 -1
  4. package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +163 -102
  5. package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +127 -13
  6. package/payload/platform/plugins/docs/references/admin-session.md +22 -18
  7. package/payload/platform/plugins/docs/references/platform.md +4 -0
  8. package/payload/platform/plugins/memory/PLUGIN.md +9 -1
  9. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.d.ts +2 -0
  10. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.d.ts.map +1 -0
  11. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.js +116 -0
  12. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.js.map +1 -0
  13. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js +100 -0
  14. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js.map +1 -1
  15. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js +197 -5
  16. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js.map +1 -1
  17. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.d.ts +33 -0
  18. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.d.ts.map +1 -0
  19. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.js +44 -0
  20. package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.js.map +1 -0
  21. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts +29 -1
  22. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts.map +1 -1
  23. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js +57 -5
  24. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js.map +1 -1
  25. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts +9 -0
  26. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts.map +1 -1
  27. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js +36 -2
  28. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js.map +1 -1
  29. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js +7 -3
  30. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js.map +1 -1
  31. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-open.test.js +1 -0
  32. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-open.test.js.map +1 -1
  33. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  34. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +22 -11
  35. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  36. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +6 -0
  37. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
  38. package/payload/platform/plugins/memory/references/schema-base.md +2 -2
  39. package/payload/platform/scripts/lib/read-brand-json.sh +69 -0
  40. package/payload/platform/scripts/seed-neo4j.sh +67 -8
  41. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
  42. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +73 -1
  43. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
  44. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +8 -1
  45. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  46. package/payload/platform/services/claude-session-manager/dist/http-server.js +49 -4
  47. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  48. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts +5 -0
  49. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts.map +1 -1
  50. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js +11 -3
  51. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js.map +1 -1
  52. package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts.map +1 -1
  53. package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js +45 -20
  54. package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js.map +1 -1
  55. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +7 -0
  56. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  57. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +5 -1
  58. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  59. package/payload/platform/templates/specialists/agents/database-operator.md +4 -67
  60. package/payload/premium-plugins/real-agent/plugins/brochures/skills/brand-design/SKILL.md +48 -0
  61. package/payload/premium-plugins/real-agent/plugins/brochures/skills/make-brochure/SKILL.md +1 -1
  62. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +2 -0
  63. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/build.md +284 -11
  64. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/index.html +1 -0
  65. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/registers.md +66 -8
  66. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/template.html +4 -1
  67. package/payload/server/public/assets/{admin-BPRjc4z9.js → admin-Bej-Q1IZ.js} +1 -1
  68. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Asw7tNG5.js → architectureDiagram-Q4EWVU46-umb1qQ5e.js} +1 -1
  69. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-Blvay4_b.js → blockDiagram-DXYQGD6D-C8kHw50g.js} +1 -1
  70. package/payload/server/public/assets/{brand-CwHvVF8s.css → brand-DxX912hX.css} +1 -1
  71. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-C2st6jZP.js → c4Diagram-AHTNJAMY-CtKxKvPu.js} +1 -1
  72. package/payload/server/public/assets/channel-BshbIIpE.js +1 -0
  73. package/payload/server/public/assets/{chunk-336JU56O-Cp7B2yh1.js → chunk-336JU56O-_xUA1Yj-.js} +2 -2
  74. package/payload/server/public/assets/{chunk-426QAEUC-B0x3RXvj.js → chunk-426QAEUC-d_A8vQqM.js} +1 -1
  75. package/payload/server/public/assets/{chunk-4TB4RGXK-B0yM0pgq.js → chunk-4TB4RGXK-BgkVBpN_.js} +1 -1
  76. package/payload/server/public/assets/{chunk-5FUZZQ4R-CqGhfArO.js → chunk-5FUZZQ4R-CGOqTZDS.js} +1 -1
  77. package/payload/server/public/assets/{chunk-5PVQY5BW-jSeD3Yrj.js → chunk-5PVQY5BW-CW78oYeu.js} +1 -1
  78. package/payload/server/public/assets/{chunk-EDXVE4YY-C7HuvG2r.js → chunk-EDXVE4YY-D_w_ZcbQ.js} +1 -1
  79. package/payload/server/public/assets/{chunk-ENJZ2VHE-DzCrge6H.js → chunk-ENJZ2VHE-MIcdFlwQ.js} +1 -1
  80. package/payload/server/public/assets/{chunk-ICPOFSXX-BPvCh0Yc.js → chunk-ICPOFSXX-BaRHp6jX.js} +1 -1
  81. package/payload/server/public/assets/{chunk-OYMX7WX6-DamxwrTn.js → chunk-OYMX7WX6-Cgo3QttE.js} +1 -1
  82. package/payload/server/public/assets/{chunk-U2HBQHQK-DDiVd0qV.js → chunk-U2HBQHQK-BVZMJI1z.js} +1 -1
  83. package/payload/server/public/assets/{chunk-X2U36JSP-CDof681v.js → chunk-X2U36JSP-DdgOyRDq.js} +1 -1
  84. package/payload/server/public/assets/{chunk-YZCP3GAM-DYzaYjrN.js → chunk-YZCP3GAM-CO61zu06.js} +1 -1
  85. package/payload/server/public/assets/{chunk-ZZ45TVLE-j0bcHHzU.js → chunk-ZZ45TVLE-B3BNOUKS.js} +1 -1
  86. package/payload/server/public/assets/classDiagram-6PBFFD2Q-mzmkY4eP.js +1 -0
  87. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-Co3bBdih.js +1 -0
  88. package/payload/server/public/assets/clone-BRSMOear.js +1 -0
  89. package/payload/server/public/assets/{dagre-C8nbdG6y.js → dagre-BY0FcwR1.js} +1 -1
  90. package/payload/server/public/assets/{dagre-KV5264BT-1RnGN_fy.js → dagre-KV5264BT-YvKOR5T0.js} +1 -1
  91. package/payload/server/public/assets/data-LkSd6fEQ.js +1 -0
  92. package/payload/server/public/assets/{device-url-actions-C_Keyr0L.js → device-url-actions-Cq5VIBKc.js} +1 -1
  93. package/payload/server/public/assets/{diagram-5BDNPKRD-C6d3Rrui.js → diagram-5BDNPKRD-DL3SiNlA.js} +1 -1
  94. package/payload/server/public/assets/{diagram-G4DWMVQ6-aD4Dik01.js → diagram-G4DWMVQ6-C_IwINPH.js} +1 -1
  95. package/payload/server/public/assets/{diagram-MMDJMWI5-CAub49YE.js → diagram-MMDJMWI5-vumL8N8U.js} +1 -1
  96. package/payload/server/public/assets/{diagram-TYMM5635-CAKnAfEr.js → diagram-TYMM5635-ewBaf58r.js} +1 -1
  97. package/payload/server/public/assets/{erDiagram-SMLLAGMA-BrqOworC.js → erDiagram-SMLLAGMA-kiidRkW3.js} +1 -1
  98. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-DV4eU-T2.js → flowDiagram-DWJPFMVM-C-1rI_Iw.js} +1 -1
  99. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-DayXUN42.js → ganttDiagram-T4ZO3ILL-B_9YdwWh.js} +1 -1
  100. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CK1xV1OV.js → gitGraphDiagram-UUTBAWPF-BmwivVe3.js} +1 -1
  101. package/payload/server/public/assets/graph-CPXz4A4I.js +1 -0
  102. package/payload/server/public/assets/graph-labels-CZR9j5Ge.js +1 -0
  103. package/payload/server/public/assets/{graphlib-CkF9o1XT.js → graphlib-Dvt3mNtm.js} +1 -1
  104. package/payload/server/public/assets/{infoDiagram-42DDH7IO-CiepM2H6.js → infoDiagram-42DDH7IO-B2n7mCmp.js} +1 -1
  105. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-DwNkdQ6z.js → ishikawaDiagram-UXIWVN3A-C2ejJadN.js} +1 -1
  106. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-BlDLMfu_.js → journeyDiagram-VCZTEJTY-BaIXYVwB.js} +1 -1
  107. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-ayZTc0Oj.js → kanban-definition-6JOO6SKY-B1NCV7hH.js} +1 -1
  108. package/payload/server/public/assets/{line-LhPUiuH-.js → line-BEjGPKWc.js} +1 -1
  109. package/payload/server/public/assets/{mermaid-parser.core-DyZYsQv_.js → mermaid-parser.core-DwLM_Bkq.js} +1 -1
  110. package/payload/server/public/assets/{mermaid.core-CauxgRdz.js → mermaid.core-BwedTSna.js} +3 -3
  111. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CQzcaJuh.js → mindmap-definition-QFDTVHPH-D6q_XhF7.js} +1 -1
  112. package/payload/server/public/assets/{page-DAw3-U_s.js → page-BBRZF8Z8.js} +1 -1
  113. package/payload/server/public/assets/{page-BGv81dGN.js → page-CLr-z4E2.js} +1 -1
  114. package/payload/server/public/assets/{pieDiagram-DEJITSTG-BPhAeTm9.js → pieDiagram-DEJITSTG-CAKQ-eUh.js} +1 -1
  115. package/payload/server/public/assets/{public-CGI24wrL.js → public-CfVPuA1x.js} +3 -3
  116. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CxrzL3ZF.js → quadrantDiagram-34T5L4WZ-B8_Xl5p8.js} +1 -1
  117. package/payload/server/public/assets/{requirementDiagram-MS252O5E-ZBSC4fGX.js → requirementDiagram-MS252O5E-BWSq9eFs.js} +1 -1
  118. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-BTDBEisC.js → sankeyDiagram-XADWPNL6-V3uWSRXp.js} +1 -1
  119. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-Bv7fA5_J.js → sequenceDiagram-FGHM5R23-DJ_F3-vT.js} +1 -1
  120. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-D07lcZrk.js → stateDiagram-FHFEXIEX-odlwuCTP.js} +1 -1
  121. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DE5qFFDY.js +1 -0
  122. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-DyeyUqKa.js → timeline-definition-GMOUNBTQ-DXE7m_zC.js} +1 -1
  123. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-B9FqDmwS.js → vennDiagram-DHZGUBPP-DEiEepoV.js} +1 -1
  124. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-4-5uCIAD.js → wardleyDiagram-NUSXRM2D-CRhVx95h.js} +1 -1
  125. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-WdkP5T_y.js → xychartDiagram-5P7HB3ND-9Gv4VNaG.js} +1 -1
  126. package/payload/server/public/data.html +5 -5
  127. package/payload/server/public/graph.html +5 -5
  128. package/payload/server/public/index.html +7 -7
  129. package/payload/server/public/public.html +4 -4
  130. package/payload/server/public/assets/channel-DTh_uFbz.js +0 -1
  131. package/payload/server/public/assets/classDiagram-6PBFFD2Q-Dsupydrn.js +0 -1
  132. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DrwAXnBf.js +0 -1
  133. package/payload/server/public/assets/clone-COx5mf-H.js +0 -1
  134. package/payload/server/public/assets/data-DOI26n9p.js +0 -1
  135. package/payload/server/public/assets/graph-DP9P7TKZ.js +0 -1
  136. package/payload/server/public/assets/graph-labels-BoLyECfJ.js +0 -1
  137. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Cr1Qet-f.js +0 -1
  138. /package/payload/server/public/assets/{brand-B8JhGW9e.js → brand-BT-mNX7-.js} +0 -0
@@ -4,17 +4,19 @@
4
4
 
5
5
  1. Copy template to property directory, populate with content
6
6
  2. Stage `output/images/` per `images.md → Image renaming and optimisation`
7
- 3. Serve locally: `python3 -m http.server <port>` from the project root
8
- 4. Navigate Playwright to the brochure URL
9
- 5. Present to user in browser for review
10
- 6. User requests changes iterate on content, image sizing, page balance
11
- 7. Verify every page fits A4: measure `scrollHeight` for all `.page` elements
12
- 8. Verify no 404s in the browser console
13
- 9. Repeat 5–8 until user approves the final copy
14
- 10. **Only after user approval**: render snapshots (two passes — 300 dpi to `output/`, 192 dpi to `output/.snapshots-web/`) per **Print snapshot capture** below
15
- 11. Build both PDFs (img2pdf + qpdf) per **PDF deliverable** below
16
- 12. Build the web bundle (96 dpi JPGs + bundled web PDF + zip) per **Web bundle** below
17
- 13. User downloads the web PDF via brochure.html's Download button or receives the print master separately for press/print runs
7
+ 3. **If branded register and `<brand_dir>/DESIGN.md` contains a `## Type roles` block** — emit the role overlay into both `brochure.html` and `page.html`/`index.html` per **Type-role overlay generation** below. Run this **before** any snapshot capture; the overlay is part of what the user reviews in the browser, not a post-process applied to PDFs.
8
+ 4. Serve locally: `python3 -m http.server <port>` from the project root
9
+ 5. Navigate Playwright to the brochure URL
10
+ 6. Present to user in browser for review
11
+ 7. User requests changes iterate on content, image sizing, page balance
12
+ 8. Verify every page fits A4: measure `scrollHeight` for all `.page` elements
13
+ 9. Verify no 404s in the browser console
14
+ 10. Repeat 6–9 until user approves the final copy
15
+ 11. **Preview manifest gate** — render `<property_dir>/output/.review/` thumbnails (per-page PNGs at 1× DPR) plus `<property_dir>/output/.review/manifest.json` capturing the active register, computed `--paper-25 / --serif / --gold-700 / --ink` token values, risk probes for `.mi-row .v` / `.folio em` / `.opener-title`, and which always-emit blocks were emitted (`DARK_SURFACE_PROTECTION`, `LIGHT_SURFACE_DATA_PROTECTION`, `STRUCTURAL_CLAMP_LOOSEN`). Halt and wait until the operator writes `<property_dir>/output/.review/approved`. The interactive form of the same gate is the browser-review loop in steps 6–10; this file-based halt is the structural form that scripted or autonomous runs cannot bypass — the build refuses to advance until `approved` exists.
16
+ 12. **Only after `approved` exists**: render snapshots (two passes — 300 dpi to `output/`, 192 dpi to `output/.snapshots-web/`) per **Print snapshot capture** below
17
+ 13. Build both PDFs (img2pdf + qpdf) per **PDF deliverable** below
18
+ 14. Build the web bundle (96 dpi JPGs + bundled web PDF + zip) per **Web bundle** below
19
+ 15. User downloads the web PDF via brochure.html's Download button or receives the print master separately for press/print runs
18
20
 
19
21
  Do not capture snapshots during the editing cycle — they are expensive and invalidated by every change.
20
22
 
@@ -268,3 +270,274 @@ The brochure should render the same as the canonical preview, just lighter on th
268
270
  | `.snapshots-web/*-print.png` | ✓ intermediate (192 dpi PNGs used by the web-PDF build; deletable after) | — |
269
271
 
270
272
  The convention: `output/` is the canonical archive for the property's lifetime. `output/web/` is a derived bundle, regenerable any time from the canonical artefacts.
273
+
274
+ ## Type-role overlay generation
275
+
276
+ If the brand pack's `DESIGN.md` contains a `## Type roles` block (declared per `references/registers.md → Type-role overrides`), parse it and emit a single `<style id="brand-type-roles">…</style>` block at the `<!-- BRAND_TYPE_ROLES_OVERRIDE_SLOT -->` sentinel immediately before the closing `</style>` of the template's main stylesheet. The override CSS is generated **mechanically** from the recipe map below — the build does not invent selectors, it emits the documented ones using values from the DESIGN doc.
277
+
278
+ Two agents running this build on different days for the same brand pack produce byte-identical overlays. That is the contract.
279
+
280
+ The role-overlay step runs **before** snapshot capture (step 10 in the live-editing workflow), not after. The overlay is part of the rendered HTML the user reviews, not a post-process applied to PDFs.
281
+
282
+ ### Recipe map — reference implementation
283
+
284
+ ```python
285
+ ROLE_RECIPES = {
286
+ 'display-headline-style': {
287
+ 'normal': [
288
+ '.cover-title, .opener-title, .ed-headline, .chapter-head h3, '
289
+ '.feature-page h3, .gallery-head h3, .siteplan-page h3, .toc-title, '
290
+ '.back-headline, .mi-head h2, .mi-head h3 '
291
+ '{ font-style: normal !important; }',
292
+ ],
293
+ },
294
+ 'display-em-style': {
295
+ 'weight-shift': [
296
+ '.cover-title em, .opener-title em, .ed-headline em, .toc-title em, '
297
+ '.back-headline em, .mi-head em '
298
+ '{ font-style: normal !important; font-weight: 500 !important; }',
299
+ ],
300
+ 'normal': [
301
+ '.cover-title em, .opener-title em, .ed-headline em, .toc-title em, '
302
+ '.back-headline em, .mi-head em '
303
+ '{ font-style: normal !important; }',
304
+ ],
305
+ },
306
+ 'display-letterspacing': {
307
+ # tuned to family: Cormorant=wide (0.18em), humanist sans=medium (0.10em),
308
+ # geometric sans=tight (0.04em). Applies only to the two largest display lines.
309
+ 'medium': [
310
+ '.cover-title { letter-spacing: 0.10em !important; text-indent: 0.10em !important; }',
311
+ '.opener-title { letter-spacing: -0.012em !important; }',
312
+ ],
313
+ 'tight': [
314
+ '.cover-title { letter-spacing: 0.04em !important; text-indent: 0.04em !important; }',
315
+ '.opener-title { letter-spacing: -0.02em !important; }',
316
+ ],
317
+ },
318
+ 'deck-family': {
319
+ 'sans': [
320
+ '.ed-deck, .ed-deck em, .feature-deck, .gallery-deck, .siteplan-deck '
321
+ '{ font-family: var(--sans) !important; font-style: normal !important; }',
322
+ ],
323
+ },
324
+ 'deck-case': {
325
+ 'sentence': [
326
+ '.ed-deck, .feature-deck, .gallery-deck, .siteplan-deck '
327
+ '{ letter-spacing: 0.02em !important; text-transform: none !important; }',
328
+ '.ed-deck em { font-weight: 500 !important; }',
329
+ ],
330
+ },
331
+ 'subtitle-family': {
332
+ 'sans-tracked-caps': [
333
+ '.opener-title em, .cover-subtitle, .opener-spec '
334
+ '{ font-family: var(--sans) !important; font-style: normal !important; }',
335
+ '.opener-spec em { font-style: normal !important; font-weight: 500 !important; }',
336
+ ],
337
+ 'sans': [
338
+ '.opener-title em, .cover-subtitle, .opener-spec '
339
+ '{ font-family: var(--sans) !important; font-style: normal !important; '
340
+ 'letter-spacing: 0.04em !important; text-transform: none !important; }',
341
+ ],
342
+ },
343
+ 'caption-family': {
344
+ 'sans': [
345
+ '.ed-caption, .ed-caption strong, .caption, .caption-pill, .caption-text, '
346
+ '.img-caption, .photo-caption, .intermission .caption-pill '
347
+ '{ font-family: var(--sans) !important; font-style: normal !important; }',
348
+ ],
349
+ },
350
+ 'caption-em-style': {
351
+ 'weight-shift': [
352
+ '.ed-caption em, .caption em, .caption-text em, .img-caption em, '
353
+ '.photo-caption em, .intermission .caption-pill em '
354
+ '{ font-style: normal !important; font-weight: 500 !important; }',
355
+ ],
356
+ },
357
+ 'dropcap': {
358
+ 'ink': [
359
+ '.ed-body.dropcap::first-letter { color: var(--ink) !important; }',
360
+ ],
361
+ 'ink-bold-sans': [
362
+ '.ed-body.dropcap::first-letter { '
363
+ 'font-family: var(--sans) !important; font-style: normal !important; '
364
+ 'font-weight: 700 !important; color: var(--ink) !important; }',
365
+ ],
366
+ 'hidden': [
367
+ '.ed-body.dropcap::first-letter { '
368
+ 'font-family: var(--sans) !important; font-style: normal !important; '
369
+ 'font-size: inherit !important; font-weight: 400 !important; '
370
+ 'line-height: inherit !important; color: inherit !important; '
371
+ 'float: none !important; padding: 0 !important; }',
372
+ ],
373
+ },
374
+ 'hairline-rules': {
375
+ 'eyebrow-only': [
376
+ '.opener .ed-rule, .opener-text .ed-rule, .opener-body .ed-rule, '
377
+ '.feature-page .ed-rule, .gallery-head .ed-rule, .siteplan-page .ed-rule, '
378
+ '.chapter-head .ed-rule { display: none !important; }',
379
+ ],
380
+ 'hidden': [
381
+ '.ed-rule, .toc-rule { display: none !important; }',
382
+ ],
383
+ },
384
+ 'body-em-style': {
385
+ 'weight-shift': [
386
+ '.ed-body em, .opener-body em, .opener-text em, .gallery-cell em, '
387
+ '.siteplan-panel em, .spec-list li em, .mi-row em, .floorplan-lead em, '
388
+ '.floorplan-sub em, .epc-caption em '
389
+ '{ font-style: normal !important; font-weight: 500 !important; }',
390
+ ],
391
+ },
392
+ 'folio-em-style': {
393
+ 'weight-shift': [
394
+ '.folio em, .folio .folio-mid em '
395
+ '{ font-style: normal !important; font-weight: 500 !important; '
396
+ 'font-family: var(--sans) !important; }',
397
+ ],
398
+ },
399
+ 'backpage-em-style': {
400
+ 'weight-shift': [
401
+ '.back-headline em, .back-tagline em, .back-foot em, .back-contact-grid em, '
402
+ '.back-rule em { font-style: normal !important; font-weight: 500 !important; }',
403
+ ],
404
+ },
405
+ }
406
+
407
+
408
+ def emit_overlay(role_values: dict[str, str]) -> str:
409
+ """Compose the <style id='brand-type-roles'> block from a {role: value} dict.
410
+
411
+ role_values is parsed from the DESIGN.md `## Type roles` block — either a
412
+ role/value table or a flat `role: value` line list. Missing roles inherit
413
+ the template default (no CSS emitted). Unknown roles raise ValueError so
414
+ they surface during the build, not silently in the rendered brochure.
415
+
416
+ DARK_SURFACE_PROTECTION and STRUCTURAL_CLAMP_LOOSEN are emitted FIRST and
417
+ UNCONDITIONALLY whenever any role values are declared. They are the
418
+ foundation of any branded re-skin; see "Always-emit recipes" above.
419
+ """
420
+ if not role_values:
421
+ return "" # No role block declared — emit nothing.
422
+ lines = ["<style id=\"brand-type-roles\">"]
423
+ lines.append(" /* === DARK SURFACE PROTECTION === */")
424
+ lines.extend(DARK_SURFACE_PROTECTION)
425
+ lines.append(" /* === STRUCTURAL CLAMP LOOSEN === */")
426
+ lines.extend(STRUCTURAL_CLAMP_LOOSEN)
427
+ for role, value in role_values.items():
428
+ if role not in ROLE_RECIPES:
429
+ raise ValueError(f"Unknown type role: {role}")
430
+ if value not in ROLE_RECIPES[role]:
431
+ raise ValueError(f"Unknown value '{value}' for role '{role}'")
432
+ lines.append(f" /* === ROLE: {role}: {value} === */")
433
+ lines.extend(ROLE_RECIPES[role][value])
434
+ lines.append("</style>")
435
+ return "\n".join(lines)
436
+ ```
437
+
438
+
439
+ ### Always-emit recipes — dark-surface protection, light-surface data protection, structural clamps
440
+
441
+ Three sections of the overlay are emitted **whenever the brand pack's `## Type roles` block is present**, regardless of which roles the operator declared. They are the foundation of any branded re-skin — without them the role API's targeted overrides can blow up adjacent template assumptions.
442
+
443
+ #### Dark-surface protection (fixed exclusion list)
444
+
445
+ The role API's color rules deliberately stop short of selectors that sit on dark surfaces — the toc-image caption pill, intermission caption pills, the spread-6 chapter-head overlay on a dark photo, the back-page masthead and tagline, and the back-page contact-grid name lines. The brand-token swap will have remapped `--gold-700` (the template's italic-emphasis color) to the brand's accent — typically navy or a deep brand colour. On a light surface, that's correct. On a dark surface, navy on dark is unreadable.
446
+
447
+ The recipe is a **fixed exclusion list**, not a heuristic. Ship it as part of the overlay:
448
+
449
+ ```python
450
+ DARK_SURFACE_PROTECTION = [
451
+ # Body text on dark surfaces — keep paper-light regardless of brand-token swap.
452
+ '.toc-image .caption, .toc-image .caption *, '
453
+ '.intermission .caption-pill, .intermission .caption-pill *, '
454
+ '.spread-6 .top-image .overlay, .spread-6 .top-image .overlay *, '
455
+ '.back-headline, .back-tagline, .back-foot, .back-contact-grid, .back-rule '
456
+ '{ color: var(--paper-25) !important; }',
457
+ # Emphasis on dark surfaces — use the brand accent (--gold-300) which the
458
+ # brand-tokens block typically maps to the brand's bright/highlight colour.
459
+ '.toc-image .caption em, '
460
+ '.intermission .caption-pill em, '
461
+ '.spread-6 .top-image .overlay em, '
462
+ '.spread-6 .top-image .overlay .ed-headline em, '
463
+ '.back-headline em, '
464
+ '.back-tagline em, '
465
+ '.back-contact-grid .cell .name em, '
466
+ '.back-foot em '
467
+ '{ color: var(--gold-300) !important; }',
468
+ ]
469
+ ```
470
+
471
+ The selector list is closed. Two agents producing brochures for the same brand pack emit byte-identical dark-surface blocks. If the template adds a new dark-surface region in a future revision, this list (in `build.md`) is the authoritative update site.
472
+
473
+ #### Light-surface data protection (fixed selector list)
474
+
475
+ The template uses `var(--gold-700)` both for genuine accents (em emphasis, eyebrow rules, particulars numbering) **and** for data-value treatment in the Material Information rows, opener meta row, EPC chart rows, floorplan caption, and the spec list. Premium binds `--gold-700` to a muted bronze (`#7A6235`) so the data cells read as subtle accents on cream. Branded binds `--gold-700` to whatever the brand's primary accent is — for some brands a saturated green or red — and every data cell turns into a hyperlink-looking blob.
476
+
477
+ The light-surface twin of dark-surface protection forces data-cell text back to `var(--ink)` (and the folio strip to `var(--slate-600)`), regardless of which brand token swap happens.
478
+
479
+ ```python
480
+ LIGHT_SURFACE_DATA_PROTECTION = [
481
+ # Material info, opener meta, EPC data values, floorplan caption, spec list —
482
+ # always read as ink, never brand-accent.
483
+ '.mi-row .v, .mi-row .v em, '
484
+ '.opener-meta-row .v, .opener-meta-row .v em, '
485
+ '.epc-row .v, .epc-row .v em, '
486
+ '.spec-list li, .spec-list li em, .spec-list li strong, '
487
+ '.floorplan-sub, .floorplan-sub em '
488
+ '{ color: var(--ink) !important; }',
489
+ # Folio strip — muted slate, never full brand colour.
490
+ '.folio, .folio em, .folio-mid, .folio-mid em '
491
+ '{ color: var(--slate-600) !important; }',
492
+ # Particulars numbering keeps the brand accent intentionally; the existing
493
+ # `.spec-list li .num` rule already scopes that to the number span only —
494
+ # no override needed here.
495
+ ]
496
+ ```
497
+
498
+ Same closed-list contract as the dark-surface block: two agents emit byte-identical output for the same brand. If a future template revision adds a new data-cell region, the entry is added here. A cleaner long-term fix is to split `--gold-700` into `--accent-strong` (em, eyebrow rules, `.num`) and `--data-text` (the selectors above, defaulting to `--ink`) so Branded only swaps `--accent-strong`; until that token split lands, this override block is structurally mandatory in Branded runs, not optional polish.
499
+
500
+ #### Structural clamp loosen
501
+
502
+ The template's chapter-heading grid rows are sized for the Cormorant Garamond display setup (italic, weight 400, letter-spacing tuned for didone). When the role overlay swaps display family, weight, or letter-spacing, headings can grow a few millimetres beyond what the fixed-height rows accommodate — and the template's `overflow: hidden` on those rows clips the descenders. The proposal author's term for this is "structural-element clamps that need to be loosened together with the type-role overlay."
503
+
504
+ The recipe is bounded: only the known fixed-height heading rows in spreads 2 and 3 are loosened, and only to `minmax(<original>, max-content)`. A heading that already fits the original budget renders identically; a heading that needs a few extra mm no longer clips.
505
+
506
+ ```python
507
+ STRUCTURAL_CLAMP_LOOSEN = [
508
+ # spread-3: chapter heading row fixed at 60mm in the template.
509
+ # Loosen to "60mm but grow if needed" — image cells in the second row
510
+ # absorb the shrinkage when the heading takes extra height.
511
+ '.spread-3 { grid-template-rows: minmax(60mm, max-content) 1fr !important; }',
512
+ '.spread-3 .head { overflow: visible !important; }',
513
+ # spread-2: bottom strip fixed at 56mm. Same treatment.
514
+ '.spread-2 { grid-template-rows: 1fr minmax(56mm, max-content) !important; }',
515
+ # Other spreads use auto 1fr or auto auto already — no loosening needed.
516
+ ]
517
+ ```
518
+
519
+ If a future template revision adds another fixed-height heading row, the entry is added here. The substitute step's `emit_overlay()` always emits these three sections first, then the per-role recipes.
520
+
521
+
522
+ ### Where the overlay lands
523
+
524
+ The template's main `<style>` ends with the sentinel line:
525
+
526
+ ```html
527
+ /* BRAND_TYPE_ROLES_OVERRIDE_SLOT — branded register may inject role overrides here */
528
+ </style>
529
+ ```
530
+
531
+ The build replaces that sentinel comment with the emitted `<style id="brand-type-roles">…</style>` block (or with nothing if no role values are declared). The original `<style>` continues to its closing `</style>` immediately after. Both `brochure.html` and the landing page (`page.html` / `index.html`) carry the sentinel and receive the same overlay — the role API is global to the property's output.
532
+
533
+ ### Verification
534
+
535
+ After the overlay is emitted, run a quick check:
536
+
537
+ ```bash
538
+ grep -c 'id="brand-type-roles"' output/brochure.html output/web/page.html
539
+ # both must return 1 if the brand pack declared roles, 0 otherwise.
540
+ ```
541
+
542
+ A branded run whose DESIGN.md declares `## Type roles` but whose rendered HTML lacks `id="brand-type-roles"` is **shipped broken** — the overlay step was skipped or failed silently.
543
+
@@ -834,6 +834,7 @@
834
834
  .key-facts .grid { grid-template-columns: 1fr; }
835
835
  .subnav .left .price { display: none; }
836
836
  }
837
+ /* BRAND_TYPE_ROLES_OVERRIDE_SLOT — branded register may inject role overrides here */
837
838
  </style>
838
839
  </head>
839
840
  <body>
@@ -13,17 +13,75 @@ The skill template ships with a default super-premium visual scheme. Two registe
13
13
 
14
14
  | Register | What you change |
15
15
  |---|---|
16
- | **Premium** (template scheme retained) | Keep all super-premium tokens (palette, type, layout) verbatim. Swap **only** the agent's logo (back-page masthead + TOC credit) and the agent's contact details. Make subtle consistency adjustments e.g. the accent dot in "Brand · Town" can pick up the brand's primary colour if doing so doesn't disrupt the ivory/ink/bronze relationship. **Do not apply the brand's palette or type tokens.** |
17
- | **Branded** (full DESIGN.md re-skin) | Swap font names at the `--serif` / `--body-serif` / `--sans` custom properties; replace palette tokens (`--paper-25`, `--ink`, `--gold-500`, `--gold-700`, `--gold-300`, `--rule` etc.) with the DESIGN doc's values; adjust copy register to match the brand's voice; substitute the brand's logo PNGs at the back-page masthead and TOC credit. The chapter structure (sixteen pages, Roman-numeral chapters, italic headlines, editorial decks, intermissions, gallery, site plan) stays. |
16
+ | **Branded** (default full DESIGN.md re-skin) | Swap font names at the `--serif` / `--body-serif` / `--sans` custom properties; replace palette tokens (`--paper-25`, `--ink`, `--gold-500`, `--gold-700`, `--gold-300`, `--rule` etc.) with the DESIGN doc's values; **apply role-level type overrides from the DESIGN doc's `## Type roles` block** (see *Type-role overrides* below); substitute the brand's logo PNGs at the back-page masthead and TOC credit. The chapter structure (sixteen pages, Roman-numeral chapters, intermissions, gallery, site plan) stays *layout, image slots, page geometry and folio chrome positions remain identical, only the styling of named type roles changes*. |
17
+ | **Premium** (opt-in only template scheme retained) | Keep all super-premium tokens (palette, type, layout) verbatim. Swap **only** the agent's logo (back-page masthead + TOC credit) and the agent's contact details. Make subtle consistency adjustments — e.g. the accent dot in "Brand · Town" can pick up the brand's primary colour if doing so doesn't disrupt the ivory/ink/bronze relationship. **Do not apply the brand's palette or type tokens.** |
18
18
 
19
19
  ## Selecting the register — decide in this order
20
20
 
21
- 1. **The user said "premium" or "super-premium"** in the request → **Premium**, regardless of whether a DESIGN.md exists.
22
- 2. **A DESIGN.md is provided** and the user did *not* say "premium" **Branded**.
23
- 3. **Neither a DESIGN.md nor the word "premium"** → **Premium** (the template ships with super-premium defaults already in place).
21
+ 1. **The user explicitly said "premium" or "super-premium"** in the request → **Premium**, regardless of whether a DESIGN.md exists.
22
+ 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.
24
23
 
25
24
  The signal is the user's wording, not the property. Don't infer premium from a high guide price or a country-house name when the user has handed over a DESIGN.md without qualifying language — the agent may want their brand on every brochure regardless of property tier. Conversely, when the user says "premium" but the property is plainly mid-market (a £450k semi, a city flat), surface the mismatch in one line ("Going premium register — confirm you want the country-folio look on this listing?") before generating, so the user can correct the wording rather than discover the mismatch in the rendered output.
26
25
 
26
+ ## Type-role overrides
27
+
28
+ The branded register's font-family and palette swap alone produces a folio painted in the brand's colours but still wearing the template's editorial conventions — italic display, small-caps decks, gold hairlines under every heading, dropcap, italic emphasis on `<em>`. For brands whose live identity does not use those conventions (Cooper Adams' upright TrajanPro, eXp UK's Montserrat, Knight-Frank-adjacent vs corporate-residential), that's a mismatch. The brochure reads as a country-house folio with brand paint, not as a brand document.
29
+
30
+ To close the gap without leaving branded territory, the brand pack's `DESIGN.md` may declare a `## Type roles` block. Each row maps a named brochure type-role to a brand styling decision. The build pipeline reads these and emits a single override CSS block — see `build.md → Type-role overlay generation` for the recipe map.
31
+
32
+ The role API is **fixed and bounded**. Only the roles below may be overridden; the build does not invent selectors. Anything else stays at template defaults.
33
+
34
+ | Role | Targets (CSS selectors) | Allowed values |
35
+ |---|---|---|
36
+ | `display-headline-style` | `.cover-title`, `.opener-title`, `.ed-headline`, `.chapter-head h3`, `.feature-page h3`, `.gallery-head h3`, `.siteplan-page h3`, `.toc-title`, `.back-headline`, `.mi-head h2`, `.mi-head h3` | `italic` (default) / `normal` |
37
+ | `display-em-style` | `<em>` inside the headline selectors above | `italic` (default) / `weight-shift` / `normal` |
38
+ | `display-letterspacing` | `.cover-title`, `.opener-title` | `wide` (default, ≈0.18em — tuned for Cormorant/didone) / `medium` (≈0.10em — humanist sans) / `tight` (≈0.04em — geometric sans) |
39
+ | `deck-family` | `.ed-deck`, `.feature-deck`, `.gallery-deck`, `.siteplan-deck` | `serif` (default) / `sans` |
40
+ | `deck-case` | same as `deck-family` | `caps-tracked` (default) / `sentence` |
41
+ | `eyebrow-family` | `.ed-eyebrow`, `.toc-eyebrow`, `.chapter-eyebrow`, `.ed-eyebrow-light` | `sans` (default — documented for explicitness) |
42
+ | `subtitle-family` | `.opener-title em`, `.cover-subtitle`, `.opener-spec` | `serif-smallcaps` (default) / `sans-tracked-caps` / `sans` |
43
+ | `caption-family` | `.ed-caption`, `.caption-pill`, `.caption-text`, `.img-caption`, `.photo-caption`, `.intermission .caption-pill` | `serif` (default) / `sans` |
44
+ | `caption-em-style` | `<em>` inside any caption selector | `italic` (default) / `weight-shift` / `normal` |
45
+ | `dropcap` | `.ed-body.dropcap::first-letter` | `gold` (default — italic serif in `--gold-700`) / `ink` (italic serif in `--ink`) / `ink-bold-sans` (bold sans in `--ink`, no italic) / `hidden` (neutralised — first letter inline at body size) |
46
+ | `hairline-rules` | `.ed-rule`, `.toc-rule` | `visible` (default) / `eyebrow-only` (rules beside eyebrows only, not under chapter heads) / `hidden` |
47
+ | `body-em-style` | `.ed-body em`, `.opener-body em`, `.opener-text em`, `.gallery-cell em`, `.siteplan-panel em`, `.spec-list li em`, `.mi-row em`, `.floorplan-lead em`, `.floorplan-sub em`, `.epc-caption em` | `italic` (default) / `weight-shift` / `normal` |
48
+ | `folio-em-style` | `.folio em`, `.folio .folio-mid em` | `italic` (default) / `weight-shift` / `normal` |
49
+ | `backpage-em-style` | `.back-headline em`, `.back-tagline em`, `.back-foot em`, `.back-contact-grid em`, `.back-rule em` | `italic-gold` (default) / `weight-shift` / `normal` |
50
+
51
+ ### What the role API permits
52
+
53
+ Each `value` maps to a deterministic, named CSS recipe (the full map lives in `build.md → Type-role overlay generation`). For example:
54
+
55
+ - `display-em-style: weight-shift` → emits `{ font-style: normal !important; font-weight: 500 !important; }`
56
+ - `deck-family: sans` + `deck-case: sentence` → emits `{ font-family: var(--sans) !important; font-style: normal !important; letter-spacing: 0.02em !important; text-transform: none !important; }`
57
+ - `dropcap: hidden` → emits a `::first-letter` override that neutralises the float, size, color and font-family overrides from the template
58
+
59
+ ### What the role API does NOT permit
60
+
61
+ - Changing page layout, grid structure, image slot positions, page count, folio position
62
+ - Adding new CSS classes or DOM elements
63
+ - Changing colour assignments (those belong to the palette tokens, not the role API)
64
+ - Restyling the back page's QR strip or contact grid layout
65
+ - Restyling the MI three-column grid layout
66
+ - Defining new roles ad hoc — the role list above is the complete API
67
+
68
+ A branded brochure must remain layout-identical to the premium brochure; only the *styling of named type roles* may differ.
69
+
70
+ ### Backward compatibility
71
+
72
+ Existing brand packs whose `DESIGN.md` files do not contain a `## Type roles` block are **unaffected**. Omitted roles inherit template defaults — which match the template's current behaviour exactly. Two agents producing brochures for the same brand on different days produce identical results because the role list is fixed and the recipe per value is fixed.
73
+
74
+
75
+ ### Foundation: always-emit recipes
76
+
77
+ Two sections of the role overlay are emitted **whenever the brand pack declares any `## Type roles`**, regardless of which specific roles the operator chose. They are the foundation of any branded re-skin:
78
+
79
+ 1. **Dark-surface protection.** A fixed exclusion list of selectors that sit on dark surfaces — the TOC hero caption pill, intermission caption pills, the spread-6 chapter-head overlay on its dark photo, the back-page masthead and tagline, and the back-page contact-grid name lines. The brand-token swap remaps `--gold-700` (the template's italic-emphasis colour) to the brand's accent. On a light surface that's correct. On a dark surface, a brand-accent like navy or maroon on dark photographic background is unreadable. The dark-surface protection forces text on these selectors to `var(--paper-25)` and emphasis to `var(--gold-300)` (the brand's accent / highlight). This is **not** a per-implementer concern — the exclusion list is fixed in `build.md → Always-emit recipes`.
80
+ 2. **Light-surface data protection.** The template uses `--gold-700` for two semantically different things — genuine accents (em emphasis, eyebrow rules, particulars numbering) and data-value treatment in `.mi-row .v`, `.opener-meta-row .v`, `.epc-row .v`, `.spec-list li`, and `.floorplan-sub`. In Premium that's invisible because `--gold-700` is a muted bronze on cream. In Branded, where `--gold-700` is rebound to whatever the brand's primary accent is (often a saturated green, red, or navy), every data cell turns into a hyperlink-looking blob. The light-surface twin of dark-surface protection forces those data cells to `var(--ink)` and the folio strip to `var(--slate-600)` regardless of the brand token swap. The fixed selector list lives in `build.md → Always-emit recipes` as `LIGHT_SURFACE_DATA_PROTECTION`. This is structurally mandatory in Branded runs, not optional polish — the long-term cleanup is to split `--gold-700` into `--accent-strong` and `--data-text` tokens so Branded only swaps the accent.
81
+ 3. **Structural clamp loosen.** The template's chapter-heading rows in `.spread-2` and `.spread-3` are sized for the Cormorant Garamond display setup (italic, weight 400, didone-tuned letter-spacing). When the role overlay changes display family, weight, or letter-spacing, headings can grow a few millimetres beyond what those fixed-height rows accommodate, and the template's `overflow: hidden` clips the descenders. The clamp-loosen recipe converts the affected rows to `minmax(<original>, max-content)` and lifts the heading-row `overflow` from `hidden` to `visible`. A heading that already fits renders identically; a heading that needs a few extra mm no longer clips. If a future template revision adds another fixed-height heading row, the entry is added in `build.md` — not handwritten per property.
82
+
83
+ A branded brochure built with the role overlay must show all three blocks in the emitted `<style id="brand-type-roles">`. Their absence is a build-time defect, not a stylistic choice.
84
+
27
85
  ## Forbidden template sources
28
86
 
29
87
  **Do not** start from any of the following, even when they exist in the same brand workspace and look "more on-brand" than the skill template:
@@ -31,9 +89,9 @@ The signal is the user's wording, not the property. Don't infer premium from a h
31
89
  - `<brand>/properties/<other-listing>/output/brochure.html` — a sibling property's rendered brochure
32
90
  - A previous brochure that was hand-edited after the last template release
33
91
  - A "brand brochure" or "marketing PDF" supplied by the agency
34
- - An older version of `template.html` cached anywhere outside `~/.claude/plugins/brochures/skills/property-brochure/references/`
92
+ - An older version of `template.html` cached anywhere outside `~/.claude/plugins/property-brochure/skills/property-brochure/references/`
35
93
 
36
- Sibling brochures contain property-specific copy edits, hand-tuned image positions, ad-hoc CSS overrides, and drift from the canonical layout. Starting from one bakes that drift into the new brochure and silently diverges every property's structure over time. The DESIGN doc supplies design **tokens** (fonts, palette, voice) — it does not authorise reaching for an existing brochure as a starting point.
94
+ Sibling brochures contain property-specific copy edits, hand-tuned image positions, ad-hoc CSS overrides, and drift from the canonical layout. Starting from one bakes that drift into the new brochure and silently diverges every property's structure over time. The DESIGN doc supplies design **tokens** (fonts, palette, voice) and **role declarations** — it does not authorise reaching for an existing brochure as a starting point.
37
95
 
38
96
  If the canonical `references/template.html` looks wrong for the brand, the right action is to **patch the template** (open a discussion with the user about what to change), not to substitute a different starting point for one run.
39
97
 
@@ -44,4 +102,4 @@ If the canonical `references/template.html` looks wrong for the brand, the right
44
102
  - Adding new pages, removing pages, or merging spreads to "look more like" the brand's website
45
103
  - Importing CSS from the brand's website into the brochure to "match the live experience"
46
104
 
47
- If a branded re-skin is genuinely incompatible with the sixteen-page editorial folio (rare — most brands sit happily in this structure with token swaps), surface that to the user before generating anything. Don't unilaterally substitute structure. Any alternative count must still be **even** — see `a4-print-documents` → Even-page count for duplex printing.
105
+ If a branded re-skin is genuinely incompatible with the sixteen-page editorial folio (rare — most brands sit happily in this structure with token swaps plus role overrides), surface that to the user before generating anything. Don't unilaterally substitute structure. Any alternative count must still be **even** — see `a4-print-documents` → Even-page count for duplex printing.
@@ -208,6 +208,7 @@
208
208
  text-transform: uppercase;
209
209
  color: var(--ink);
210
210
  white-space: nowrap;
211
+ white-space: nowrap;
211
212
  }
212
213
 
213
214
  .cover-subtitle {
@@ -514,6 +515,7 @@
514
515
  font-weight: 400;
515
516
  color: var(--gold-700);
516
517
  font-size: 0.36em;
518
+ white-space: nowrap;
517
519
  line-height: 1.2;
518
520
  margin-top: 8px;
519
521
  letter-spacing: 0.18em;
@@ -1121,7 +1123,7 @@
1121
1123
  color: var(--paper-0);
1122
1124
  margin-bottom: 12px;
1123
1125
  }
1124
- .back-headline em { font-style: italic; color: var(--gold-300); }
1126
+ .back-headline em { font-style: italic; color: var(--gold-300); white-space: nowrap; }
1125
1127
 
1126
1128
  .back-tagline {
1127
1129
  font-family: var(--serif);
@@ -1360,6 +1362,7 @@
1360
1362
 
1361
1363
  * { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
1362
1364
  }
1365
+ /* BRAND_TYPE_ROLES_OVERRIDE_SLOT — branded register may inject role overrides here */
1363
1366
  </style>
1364
1367
  </head>
1365
1368
  <body>