@nguyenphp/antigravity-marketing 1.0.18 → 1.0.20

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 (231) hide show
  1. package/README.md +130 -78
  2. package/package.json +4 -3
  3. package/templates/.agent/skills/marketing-report-expert/SKILL.md +70 -0
  4. package/templates/.agent/skills/minimax-docx/LICENSE +21 -0
  5. package/templates/.agent/skills/minimax-docx/SKILL.md +274 -0
  6. package/templates/.agent/skills/minimax-docx/assets/styles/academic_styles.xml +250 -0
  7. package/templates/.agent/skills/minimax-docx/assets/styles/corporate_styles.xml +284 -0
  8. package/templates/.agent/skills/minimax-docx/assets/styles/default_styles.xml +449 -0
  9. package/templates/.agent/skills/minimax-docx/assets/xsd/aesthetic-rules.xsd +470 -0
  10. package/templates/.agent/skills/minimax-docx/assets/xsd/business-rules.xsd +130 -0
  11. package/templates/.agent/skills/minimax-docx/assets/xsd/common-types.xsd +159 -0
  12. package/templates/.agent/skills/minimax-docx/assets/xsd/wml-subset.xsd +589 -0
  13. package/templates/.agent/skills/minimax-docx/references/cjk_typography.md +357 -0
  14. package/templates/.agent/skills/minimax-docx/references/cjk_university_template_guide.md +184 -0
  15. package/templates/.agent/skills/minimax-docx/references/comments_guide.md +191 -0
  16. package/templates/.agent/skills/minimax-docx/references/design_good_bad_examples.md +829 -0
  17. package/templates/.agent/skills/minimax-docx/references/design_principles.md +819 -0
  18. package/templates/.agent/skills/minimax-docx/references/openxml_element_order.md +308 -0
  19. package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part1.md +4061 -0
  20. package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part2.md +2820 -0
  21. package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part3.md +3381 -0
  22. package/templates/.agent/skills/minimax-docx/references/openxml_namespaces.md +82 -0
  23. package/templates/.agent/skills/minimax-docx/references/openxml_units.md +72 -0
  24. package/templates/.agent/skills/minimax-docx/references/scenario_a_create.md +284 -0
  25. package/templates/.agent/skills/minimax-docx/references/scenario_b_edit_content.md +295 -0
  26. package/templates/.agent/skills/minimax-docx/references/scenario_c_apply_template.md +456 -0
  27. package/templates/.agent/skills/minimax-docx/references/track_changes_guide.md +200 -0
  28. package/templates/.agent/skills/minimax-docx/references/troubleshooting.md +506 -0
  29. package/templates/.agent/skills/minimax-docx/references/typography_guide.md +294 -0
  30. package/templates/.agent/skills/minimax-docx/references/xsd_validation_guide.md +158 -0
  31. package/templates/.agent/skills/minimax-docx/scripts/doc_to_docx.sh +40 -0
  32. package/templates/.agent/skills/minimax-docx/scripts/docx_preview.sh +37 -0
  33. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/MiniMaxAIDocx.Cli.csproj +19 -0
  34. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/Program.cs +18 -0
  35. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/AnalyzeCommand.cs +147 -0
  36. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ApplyTemplateCommand.cs +322 -0
  37. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/CreateCommand.cs +324 -0
  38. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/DiffCommand.cs +155 -0
  39. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/EditContentCommand.cs +487 -0
  40. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/FixOrderCommand.cs +108 -0
  41. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/MergeRunsCommand.cs +122 -0
  42. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ValidateCommand.cs +107 -0
  43. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/MiniMaxAIDocx.Core.csproj +15 -0
  44. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/CommentSynchronizer.cs +169 -0
  45. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/ElementOrder.cs +80 -0
  46. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/NamespaceConstants.cs +42 -0
  47. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/RunMerger.cs +81 -0
  48. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/StyleAnalyzer.cs +81 -0
  49. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/TrackChangesHelper.cs +99 -0
  50. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/UnitConverter.cs +23 -0
  51. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples.cs +1832 -0
  52. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch1.cs +910 -0
  53. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch2.cs +999 -0
  54. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch3.cs +1048 -0
  55. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch4.cs +1038 -0
  56. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/CharacterFormattingSamples.cs +1020 -0
  57. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/DocumentCreationSamples.cs +1121 -0
  58. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FieldAndTocSamples.cs +624 -0
  59. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FootnoteAndCommentSamples.cs +675 -0
  60. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/HeaderFooterSamples.cs +838 -0
  61. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ImageSamples.cs +917 -0
  62. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ListAndNumberingSamples.cs +826 -0
  63. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ParagraphFormattingSamples.cs +1199 -0
  64. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/StyleSystemSamples.cs +1487 -0
  65. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TableSamples.cs +1163 -0
  66. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TrackChangesSamples.cs +595 -0
  67. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/CjkHelper.cs +39 -0
  68. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/FontDefaults.cs +24 -0
  69. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/PageSizes.cs +20 -0
  70. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/BusinessRuleValidator.cs +224 -0
  71. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/GateCheckValidator.cs +148 -0
  72. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/ValidationResult.cs +23 -0
  73. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/XsdValidator.cs +69 -0
  74. package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.slnx +4 -0
  75. package/templates/.agent/skills/minimax-docx/scripts/env_check.sh +196 -0
  76. package/templates/.agent/skills/minimax-docx/scripts/setup.ps1 +274 -0
  77. package/templates/.agent/skills/minimax-docx/scripts/setup.sh +504 -0
  78. package/templates/.agent/skills/minimax-multimodal-toolkit/SKILL.md +359 -0
  79. package/templates/.agent/skills/minimax-pdf/README.md +222 -0
  80. package/templates/.agent/skills/minimax-pdf/SKILL.md +201 -0
  81. package/templates/.agent/skills/minimax-pdf/design/design.md +381 -0
  82. package/templates/.agent/skills/minimax-pdf/scripts/cover.py +1579 -0
  83. package/templates/.agent/skills/minimax-pdf/scripts/fill_inspect.py +200 -0
  84. package/templates/.agent/skills/minimax-pdf/scripts/fill_write.py +242 -0
  85. package/templates/.agent/skills/minimax-pdf/scripts/make.sh +491 -0
  86. package/templates/.agent/skills/minimax-pdf/scripts/merge.py +112 -0
  87. package/templates/.agent/skills/minimax-pdf/scripts/palette.py +559 -0
  88. package/templates/.agent/skills/minimax-pdf/scripts/reformat_parse.py +374 -0
  89. package/templates/.agent/skills/minimax-pdf/scripts/render_body.py +1055 -0
  90. package/templates/.agent/skills/minimax-pdf/scripts/render_cover.cjs +111 -0
  91. package/templates/.agent/skills/minimax-xlsx/SKILL.md +138 -0
  92. package/templates/.agent/skills/minimax-xlsx/references/create.md +691 -0
  93. package/templates/.agent/skills/minimax-xlsx/references/edit.md +684 -0
  94. package/templates/.agent/skills/minimax-xlsx/references/fix.md +37 -0
  95. package/templates/.agent/skills/minimax-xlsx/references/format.md +768 -0
  96. package/templates/.agent/skills/minimax-xlsx/references/ooxml-cheatsheet.md +231 -0
  97. package/templates/.agent/skills/minimax-xlsx/references/read-analyze.md +97 -0
  98. package/templates/.agent/skills/minimax-xlsx/references/validate.md +772 -0
  99. package/templates/.agent/skills/minimax-xlsx/scripts/formula_check.py +422 -0
  100. package/templates/.agent/skills/minimax-xlsx/scripts/libreoffice_recalc.py +248 -0
  101. package/templates/.agent/skills/minimax-xlsx/scripts/shared_strings_builder.py +163 -0
  102. package/templates/.agent/skills/minimax-xlsx/scripts/style_audit.py +575 -0
  103. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_add_column.py +395 -0
  104. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_insert_row.py +274 -0
  105. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_pack.py +87 -0
  106. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_reader.py +362 -0
  107. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_shift_rows.py +396 -0
  108. package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_unpack.py +130 -0
  109. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/[Content_Types].xml +9 -0
  110. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/_rels/.rels +6 -0
  111. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/_rels/workbook.xml.rels +19 -0
  112. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/sharedStrings.xml +33 -0
  113. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/styles.xml +160 -0
  114. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/workbook.xml +30 -0
  115. package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/worksheets/sheet1.xml +70 -0
  116. package/templates/.agent/skills/pptx-generator/SKILL.md +249 -0
  117. package/templates/.agent/skills/pptx-generator/references/design-system.md +392 -0
  118. package/templates/.agent/skills/pptx-generator/references/editing.md +162 -0
  119. package/templates/.agent/skills/pptx-generator/references/pitfalls.md +112 -0
  120. package/templates/.agent/skills/pptx-generator/references/pptxgenjs.md +420 -0
  121. package/templates/.agent/skills/pptx-generator/references/slide-types.md +413 -0
  122. package/templates/.agent/skills/tutorial-video-expert/SKILL.md +88 -0
  123. package/templates/.agent/skills/ui-ux-pro-max/SKILL.md +170 -585
  124. package/templates/.agent/skills/vision-analysis/SKILL.md +174 -0
  125. package/templates/.agent/workflows/analyze.md +3 -0
  126. package/templates/.agent/workflows/brand-report.md +44 -0
  127. package/templates/.agent/workflows/report.md +49 -0
  128. package/templates/.agent/agents/backend-specialist.md +0 -263
  129. package/templates/.agent/agents/database-architect.md +0 -226
  130. package/templates/.agent/agents/debugger.md +0 -225
  131. package/templates/.agent/agents/devops-engineer.md +0 -242
  132. package/templates/.agent/agents/frontend-specialist.md +0 -527
  133. package/templates/.agent/agents/game-developer.md +0 -162
  134. package/templates/.agent/agents/mobile-developer.md +0 -377
  135. package/templates/.agent/agents/penetration-tester.md +0 -188
  136. package/templates/.agent/agents/security-auditor.md +0 -170
  137. package/templates/.agent/agents/test-engineer.md +0 -158
  138. package/templates/.agent/skills/api-patterns/SKILL.md +0 -81
  139. package/templates/.agent/skills/api-patterns/api-style.md +0 -42
  140. package/templates/.agent/skills/api-patterns/auth.md +0 -24
  141. package/templates/.agent/skills/api-patterns/documentation.md +0 -26
  142. package/templates/.agent/skills/api-patterns/graphql.md +0 -41
  143. package/templates/.agent/skills/api-patterns/rate-limiting.md +0 -31
  144. package/templates/.agent/skills/api-patterns/response.md +0 -37
  145. package/templates/.agent/skills/api-patterns/rest.md +0 -40
  146. package/templates/.agent/skills/api-patterns/scripts/api_validator.py +0 -211
  147. package/templates/.agent/skills/api-patterns/security-testing.md +0 -122
  148. package/templates/.agent/skills/api-patterns/trpc.md +0 -41
  149. package/templates/.agent/skills/api-patterns/versioning.md +0 -22
  150. package/templates/.agent/skills/app-builder/SKILL.md +0 -75
  151. package/templates/.agent/skills/app-builder/agent-coordination.md +0 -71
  152. package/templates/.agent/skills/app-builder/feature-building.md +0 -53
  153. package/templates/.agent/skills/app-builder/project-detection.md +0 -34
  154. package/templates/.agent/skills/app-builder/scaffolding.md +0 -118
  155. package/templates/.agent/skills/app-builder/tech-stack.md +0 -40
  156. package/templates/.agent/skills/app-builder/templates/SKILL.md +0 -39
  157. package/templates/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +0 -76
  158. package/templates/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +0 -92
  159. package/templates/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +0 -88
  160. package/templates/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +0 -88
  161. package/templates/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +0 -83
  162. package/templates/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +0 -90
  163. package/templates/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +0 -90
  164. package/templates/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +0 -82
  165. package/templates/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +0 -100
  166. package/templates/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +0 -106
  167. package/templates/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +0 -101
  168. package/templates/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +0 -83
  169. package/templates/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +0 -93
  170. package/templates/.agent/skills/architecture/SKILL.md +0 -55
  171. package/templates/.agent/skills/architecture/context-discovery.md +0 -43
  172. package/templates/.agent/skills/architecture/examples.md +0 -94
  173. package/templates/.agent/skills/architecture/pattern-selection.md +0 -68
  174. package/templates/.agent/skills/architecture/patterns-reference.md +0 -50
  175. package/templates/.agent/skills/architecture/trade-off-analysis.md +0 -77
  176. package/templates/.agent/skills/bash-linux/SKILL.md +0 -199
  177. package/templates/.agent/skills/behavioral-modes/SKILL.md +0 -242
  178. package/templates/.agent/skills/clean-code/SKILL.md +0 -201
  179. package/templates/.agent/skills/code-review-checklist/SKILL.md +0 -109
  180. package/templates/.agent/skills/database-design/SKILL.md +0 -52
  181. package/templates/.agent/skills/database-design/database-selection.md +0 -43
  182. package/templates/.agent/skills/database-design/indexing.md +0 -39
  183. package/templates/.agent/skills/database-design/migrations.md +0 -48
  184. package/templates/.agent/skills/database-design/optimization.md +0 -36
  185. package/templates/.agent/skills/database-design/orm-selection.md +0 -30
  186. package/templates/.agent/skills/database-design/schema-design.md +0 -56
  187. package/templates/.agent/skills/database-design/scripts/schema_validator.py +0 -172
  188. package/templates/.agent/skills/deployment-procedures/SKILL.md +0 -241
  189. package/templates/.agent/skills/docker-expert/SKILL.md +0 -409
  190. package/templates/.agent/skills/game-development/2d-games/SKILL.md +0 -119
  191. package/templates/.agent/skills/game-development/3d-games/SKILL.md +0 -135
  192. package/templates/.agent/skills/game-development/SKILL.md +0 -167
  193. package/templates/.agent/skills/game-development/game-art/SKILL.md +0 -185
  194. package/templates/.agent/skills/game-development/game-audio/SKILL.md +0 -190
  195. package/templates/.agent/skills/game-development/game-design/SKILL.md +0 -129
  196. package/templates/.agent/skills/game-development/mobile-games/SKILL.md +0 -108
  197. package/templates/.agent/skills/game-development/multiplayer/SKILL.md +0 -132
  198. package/templates/.agent/skills/game-development/pc-games/SKILL.md +0 -144
  199. package/templates/.agent/skills/game-development/vr-ar/SKILL.md +0 -123
  200. package/templates/.agent/skills/game-development/web-games/SKILL.md +0 -150
  201. package/templates/.agent/skills/lint-and-validate/SKILL.md +0 -45
  202. package/templates/.agent/skills/lint-and-validate/scripts/lint_runner.py +0 -172
  203. package/templates/.agent/skills/lint-and-validate/scripts/type_coverage.py +0 -173
  204. package/templates/.agent/skills/mcp-builder/SKILL.md +0 -176
  205. package/templates/.agent/skills/nestjs-expert/SKILL.md +0 -552
  206. package/templates/.agent/skills/nextjs-best-practices/SKILL.md +0 -203
  207. package/templates/.agent/skills/nodejs-best-practices/SKILL.md +0 -333
  208. package/templates/.agent/skills/parallel-agents/SKILL.md +0 -175
  209. package/templates/.agent/skills/performance-profiling/SKILL.md +0 -143
  210. package/templates/.agent/skills/performance-profiling/scripts/lighthouse_audit.py +0 -76
  211. package/templates/.agent/skills/powershell-windows/SKILL.md +0 -167
  212. package/templates/.agent/skills/prisma-expert/SKILL.md +0 -355
  213. package/templates/.agent/skills/python-patterns/SKILL.md +0 -441
  214. package/templates/.agent/skills/react-patterns/SKILL.md +0 -198
  215. package/templates/.agent/skills/red-team-tactics/SKILL.md +0 -199
  216. package/templates/.agent/skills/server-management/SKILL.md +0 -161
  217. package/templates/.agent/skills/systematic-debugging/SKILL.md +0 -109
  218. package/templates/.agent/skills/tdd-workflow/SKILL.md +0 -149
  219. package/templates/.agent/skills/testing-patterns/SKILL.md +0 -178
  220. package/templates/.agent/skills/testing-patterns/scripts/test_runner.py +0 -219
  221. package/templates/.agent/skills/typescript-expert/SKILL.md +0 -429
  222. package/templates/.agent/skills/vue-expert/SKILL.md +0 -374
  223. package/templates/.agent/skills/vulnerability-scanner/SKILL.md +0 -276
  224. package/templates/.agent/skills/vulnerability-scanner/checklists.md +0 -121
  225. package/templates/.agent/skills/vulnerability-scanner/scripts/security_scan.py +0 -458
  226. package/templates/.agent/skills/webapp-testing/SKILL.md +0 -187
  227. package/templates/.agent/skills/webapp-testing/scripts/playwright_runner.py +0 -173
  228. package/templates/.agent/workflows/debug.md +0 -103
  229. package/templates/.agent/workflows/deploy.md +0 -176
  230. package/templates/.agent/workflows/enhance.md +0 -63
  231. package/templates/.agent/workflows/test.md +0 -144
@@ -0,0 +1,1579 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ cover.py — Generate cover.html from tokens.json.
4
+
5
+ Usage:
6
+ python3 cover.py --tokens tokens.json --out cover.html
7
+
8
+ Reads tokens.json["cover_pattern"] and renders the matching HTML cover.
9
+ Cover fonts are loaded live via Google Fonts @import (no local caching).
10
+ Exit codes: 0 success, 1 bad args/missing file, 3 render error
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+
17
+
18
+ # ── Google Fonts loader ────────────────────────────────────────────────────────
19
+ def _gfonts_import(t: dict) -> str:
20
+ """Return a CSS @import for the document's Google Fonts, if available."""
21
+ url = t.get("gfonts_import", "")
22
+ if url:
23
+ return f"@import url('{url}');"
24
+ return ""
25
+
26
+
27
+ # ── Shared CSS head (required by all patterns) ─────────────────────────────────
28
+ def _base_css(t: dict) -> str:
29
+ """Critical reset + shared variables. Never remove these rules."""
30
+ return f"""
31
+ {_gfonts_import(t)}
32
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
33
+ html, body {{
34
+ width: 794px; height: 1123px;
35
+ overflow: hidden;
36
+ background: {t['cover_bg']};
37
+ font-family: '{t['font_body']}', 'Helvetica Neue', Helvetica, Arial, sans-serif;
38
+ }}
39
+ .page {{
40
+ position: relative;
41
+ width: 794px; height: 1123px;
42
+ background: {t['cover_bg']};
43
+ overflow: hidden;
44
+ }}
45
+ """
46
+
47
+
48
+ # ── Dot-grid SVG helper ─────────────────────────────────────────────────────────
49
+ def _dot_grid(x0, y0, cols, rows, *, gap, r, color, opacity) -> str:
50
+ """Render a dot-grid as an absolutely positioned SVG element."""
51
+ dots = []
52
+ for row in range(rows):
53
+ for col in range(cols):
54
+ cx = x0 + col * gap
55
+ cy = y0 + row * gap
56
+ dots.append(f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="{color}"/>')
57
+ return (
58
+ f'<svg style="position:absolute;top:0;left:0;width:794px;height:1123px;'
59
+ f'pointer-events:none;opacity:{opacity}" xmlns="http://www.w3.org/2000/svg">'
60
+ + "".join(dots) + "</svg>"
61
+ )
62
+
63
+
64
+ # ── Cross-hatch SVG helper ──────────────────────────────────────────────────────
65
+ def _cross_hatch(color, opacity, spacing=32, stroke_w=0.5) -> str:
66
+ lines = []
67
+ for i in range(-20, 60):
68
+ x = i * spacing
69
+ lines.append(f'<line x1="{x}" y1="0" x2="{x + 1200}" y2="1200" stroke="{color}" stroke-width="{stroke_w}"/>')
70
+ return (
71
+ f'<svg style="position:absolute;top:0;left:0;width:794px;height:1123px;'
72
+ f'pointer-events:none;opacity:{opacity};overflow:hidden" xmlns="http://www.w3.org/2000/svg">'
73
+ + "".join(lines) + "</svg>"
74
+ )
75
+
76
+
77
+ # ── Pattern 1: Full-bleed block ────────────────────────────────────────────────
78
+ def _pattern_fullbleed(t: dict) -> str:
79
+ dot_grid = _dot_grid(
80
+ x0=500, y0=40, cols=10, rows=20, gap=24, r=1.8,
81
+ color=t["accent"], opacity=0.12
82
+ )
83
+ subtitle_block = ""
84
+ if t.get("subtitle"):
85
+ subtitle_block = f"""
86
+ <div style="font-size:14px;color:{t['muted']};letter-spacing:0.01em;
87
+ max-width:480px;line-height:1.5;margin-bottom:40px;">
88
+ {t['subtitle']}
89
+ </div>"""
90
+
91
+ return f"""<!DOCTYPE html>
92
+ <html>
93
+ <head><meta charset="UTF-8">
94
+ <style>
95
+ {_base_css(t)}
96
+ .label {{
97
+ font-size: 9px; font-weight: 500; letter-spacing: 0.22em;
98
+ color: {t['accent']}; text-transform: uppercase; margin-bottom: 28px;
99
+ }}
100
+ .title {{
101
+ font-family: '{t['font_display']}', 'Times New Roman', Georgia, serif;
102
+ font-weight: 900; font-size: 60px; line-height: 1.0;
103
+ color: {t['text_light']}; letter-spacing: -0.015em;
104
+ margin-bottom: 10px; max-width: 560px;
105
+ word-wrap: break-word;
106
+ }}
107
+ .rule {{
108
+ width: 52%; height: 1.5px;
109
+ background: linear-gradient(to right, {t['accent']}, transparent);
110
+ margin: 24px 0 20px;
111
+ }}
112
+ .content {{
113
+ position: absolute; left: 68px; right: 60px;
114
+ top: 0; bottom: 0;
115
+ display: flex; flex-direction: column; justify-content: center;
116
+ padding-top: 60px;
117
+ }}
118
+ .footer {{
119
+ position: absolute; bottom: 0; left: 0; right: 0;
120
+ height: 70px;
121
+ background: rgba(0,0,0,0.22);
122
+ display: flex; align-items: center;
123
+ justify-content: space-between;
124
+ padding: 0 68px;
125
+ }}
126
+ .footer-author {{ font-size: 11px; color: rgba(240,237,230,0.75); letter-spacing:0.04em; }}
127
+ .footer-date {{ font-size: 11px; color: {t['muted']}; letter-spacing: 0.04em; }}
128
+ </style>
129
+ </head>
130
+ <body>
131
+ <div class="page">
132
+ <!-- top-right accent strip -->
133
+ <div style="position:absolute;top:0;right:0;width:35%;height:4px;background:{t['accent']};"></div>
134
+ <!-- left vertical accent bar (gradient fade) -->
135
+ <div style="position:absolute;left:48px;top:18%;width:3px;height:60%;
136
+ background:linear-gradient(to bottom,{t['accent']},transparent);"></div>
137
+ <!-- dot grid background texture -->
138
+ {dot_grid}
139
+
140
+ <div class="content">
141
+ <div class="label">{t.get('doc_type','Document').upper()} &nbsp;·&nbsp; {t.get('date','')}</div>
142
+ <div class="title">{t['title']}</div>
143
+ <div class="rule"></div>
144
+ {subtitle_block}
145
+ </div>
146
+
147
+ <div class="footer">
148
+ <div class="footer-author">{t.get('author','')}</div>
149
+ <div class="footer-date">{t.get('date','')}</div>
150
+ </div>
151
+ </div>
152
+ </body></html>"""
153
+
154
+
155
+ # ── Pattern 2: Split panel ─────────────────────────────────────────────────────
156
+ def _pattern_split(t: dict) -> str:
157
+ dot_grid = _dot_grid(
158
+ x0=360, y0=120, cols=10, rows=18, gap=22, r=2,
159
+ color="#CCCCCC", opacity=0.25
160
+ )
161
+ return f"""<!DOCTYPE html>
162
+ <html>
163
+ <head><meta charset="UTF-8">
164
+ <style>
165
+ {_base_css(t)}
166
+ .left-panel {{
167
+ position: absolute; top: 0; left: 0;
168
+ width: 330px; height: 1123px;
169
+ background: {t['cover_bg']};
170
+ display: flex; flex-direction: column;
171
+ justify-content: center;
172
+ padding: 0 44px;
173
+ }}
174
+ .right-panel {{
175
+ position: absolute; top: 0; left: 330px;
176
+ width: 464px; height: 1123px;
177
+ background: {t['page_bg']};
178
+ }}
179
+ .divider {{
180
+ position: absolute; top: 0; left: 329px;
181
+ width: 3px; height: 1123px;
182
+ background: {t['accent']};
183
+ }}
184
+ .left-top-bar {{
185
+ position: absolute; top: 0; left: 0;
186
+ width: 330px; height: 4px;
187
+ background: {t['accent']};
188
+ }}
189
+ .title {{
190
+ font-family: '{t['font_display']}', 'Times New Roman', serif;
191
+ font-weight: 900; font-size: 34px; line-height: 1.2;
192
+ color: {t['text_light']}; margin-bottom: 18px;
193
+ word-wrap: break-word;
194
+ }}
195
+ .rule {{
196
+ width: 55%; height: 1.5px;
197
+ background: {t['accent']};
198
+ margin-bottom: 14px;
199
+ }}
200
+ .subtitle {{
201
+ font-size: 12px; color: rgba(220,220,220,0.65);
202
+ line-height: 1.5; margin-bottom: 32px;
203
+ }}
204
+ .author {{
205
+ font-size: 11px; color: {t['text_light']}; margin-bottom: 4px;
206
+ }}
207
+ .date {{ font-size: 10px; color: {t['muted']}; }}
208
+ .right-label {{
209
+ position: absolute; bottom: 60px; right: 44px;
210
+ font-size: 9px; letter-spacing: 0.18em;
211
+ color: {t['muted']}; text-transform: uppercase;
212
+ }}
213
+ </style>
214
+ </head>
215
+ <body>
216
+ <div class="page">
217
+ <div class="left-top-bar"></div>
218
+ <div class="left-panel">
219
+ <div class="title">{t['title']}</div>
220
+ <div class="rule"></div>
221
+ {'<div class="subtitle">' + t['subtitle'] + '</div>' if t.get('subtitle') else ''}
222
+ <div class="author">{t.get('author','')}</div>
223
+ <div class="date">{t.get('date','')}</div>
224
+ </div>
225
+ <div class="right-panel">
226
+ {dot_grid}
227
+ </div>
228
+ <div class="divider"></div>
229
+ <div class="right-label">{t.get('doc_type','').upper()}</div>
230
+ </div>
231
+ </body></html>"""
232
+
233
+
234
+ # ── Pattern 3: Typographic ─────────────────────────────────────────────────────
235
+ def _pattern_typographic(t: dict) -> str:
236
+ words = t['title'].split()
237
+ first = words[0] if words else ""
238
+ rest = " ".join(words[1:]) if len(words) > 1 else ""
239
+ return f"""<!DOCTYPE html>
240
+ <html>
241
+ <head><meta charset="UTF-8">
242
+ <style>
243
+ {_base_css(t)}
244
+ html, body {{ background: {t['page_bg']}; }}
245
+ .page {{ background: {t['page_bg']}; }}
246
+ .content {{
247
+ position: absolute; left: 60px; top: 0; bottom: 0; right: 60px;
248
+ display: flex; flex-direction: column; justify-content: center;
249
+ }}
250
+ .first-word {{
251
+ font-family: '{t['font_display']}', 'Times New Roman', serif;
252
+ font-weight: 900; font-size: 72px; line-height: 1.0;
253
+ color: {t['accent']}; letter-spacing: -0.02em;
254
+ }}
255
+ .rest-words {{
256
+ font-family: '{t['font_display']}', 'Times New Roman', serif;
257
+ font-weight: 900; font-size: 72px; line-height: 1.0;
258
+ color: {t['dark']}; letter-spacing: -0.02em;
259
+ margin-bottom: 12px;
260
+ }}
261
+ .rule {{
262
+ width: 100%; height: 1.5px;
263
+ background: linear-gradient(to right, {t['accent']}, {t['accent']}40);
264
+ margin: 28px 0 20px;
265
+ }}
266
+ .meta-row {{
267
+ display: flex; justify-content: space-between; align-items: baseline;
268
+ }}
269
+ .author {{ font-size: 13px; color: {t['dark']}; letter-spacing: 0.02em; }}
270
+ .date {{ font-size: 12px; color: {t['muted']}; }}
271
+ .subtitle {{ font-size: 13px; color: {t['muted']}; margin-top: 8px; max-width: 500px; }}
272
+ </style>
273
+ </head>
274
+ <body>
275
+ <div class="page">
276
+ <div class="content">
277
+ <div class="first-word">{first}</div>
278
+ {'<div class="rest-words">' + rest + '</div>' if rest else ''}
279
+ <div class="rule"></div>
280
+ <div class="meta-row">
281
+ <div class="author">{t.get('author','')}</div>
282
+ <div class="date">{t.get('date','')}</div>
283
+ </div>
284
+ {'<div class="subtitle">' + t['subtitle'] + '</div>' if t.get('subtitle') else ''}
285
+ </div>
286
+ </div>
287
+ </body></html>"""
288
+
289
+
290
+ # ── Pattern 4: Dark atmospheric ────────────────────────────────────────────────
291
+ def _pattern_atmospheric(t: dict) -> str:
292
+ dot_grid = _dot_grid(
293
+ x0=60, y0=60, cols=16, rows=22, gap=20, r=1.5,
294
+ color=t["accent"], opacity=0.08
295
+ )
296
+ return f"""<!DOCTYPE html>
297
+ <html>
298
+ <head><meta charset="UTF-8">
299
+ <style>
300
+ {_base_css(t)}
301
+ .glow {{
302
+ position: absolute;
303
+ top: -100px; right: -80px;
304
+ width: 500px; height: 500px;
305
+ background: radial-gradient(circle, {t['accent']}2E 0%, transparent 68%);
306
+ border-radius: 50%;
307
+ }}
308
+ .glow2 {{
309
+ position: absolute;
310
+ bottom: -40px; left: 10%;
311
+ width: 300px; height: 300px;
312
+ background: radial-gradient(circle, {t['accent']}14 0%, transparent 70%);
313
+ border-radius: 50%;
314
+ }}
315
+ .content {{
316
+ position: absolute; left: 64px; right: 80px;
317
+ top: 0; bottom: 0;
318
+ display: flex; flex-direction: column; justify-content: center;
319
+ }}
320
+ .label {{
321
+ font-size: 9px; letter-spacing: 0.22em;
322
+ color: {t['accent']}; text-transform: uppercase; margin-bottom: 32px;
323
+ }}
324
+ .title {{
325
+ font-family: '{t['font_display']}', 'Times New Roman', serif;
326
+ font-weight: 900; font-size: 50px; line-height: 1.05;
327
+ color: {t['text_light']}; max-width: 520px;
328
+ word-wrap: break-word; margin-bottom: 12px;
329
+ }}
330
+ .rule {{ width: 48px; height: 2px; background: {t['accent']}; margin: 24px 0 20px; }}
331
+ .subtitle {{
332
+ font-size: 13px; color: {t['muted']}; line-height: 1.6;
333
+ max-width: 400px; margin-bottom: 40px;
334
+ }}
335
+ .footer {{
336
+ position: absolute; bottom: 0; left: 0; right: 0; height: 64px;
337
+ border-top: 1px solid rgba(255,255,255,0.06);
338
+ display: flex; align-items: center; justify-content: space-between;
339
+ padding: 0 64px;
340
+ }}
341
+ .footer-l {{ font-size: 10.5px; color: rgba(240,237,230,0.6); }}
342
+ .footer-r {{ font-size: 10.5px; color: {t['muted']}; }}
343
+ </style>
344
+ </head>
345
+ <body>
346
+ <div class="page">
347
+ <div class="glow"></div>
348
+ <div class="glow2"></div>
349
+ {dot_grid}
350
+ <div style="position:absolute;top:0;right:0;width:30%;height:3px;background:{t['accent']};"></div>
351
+ <div class="content">
352
+ <div class="label">{t.get('doc_type','').upper()} &nbsp;·&nbsp; {t.get('date','')}</div>
353
+ <div class="title">{t['title']}</div>
354
+ <div class="rule"></div>
355
+ {'<div class="subtitle">' + t['subtitle'] + '</div>' if t.get('subtitle') else ''}
356
+ </div>
357
+ <div class="footer">
358
+ <div class="footer-l">{t.get('author','')}</div>
359
+ <div class="footer-r">{t.get('date','')}</div>
360
+ </div>
361
+ </div>
362
+ </body></html>"""
363
+
364
+
365
+ # ── Pattern 5: Minimal — thick left bar, generous whitespace ───────────────────
366
+ def _pattern_minimal(t: dict) -> str:
367
+ """
368
+ Ultra-restrained: white background, 8px left accent bar, oversized light-weight
369
+ title, nothing else but a hairline rule and minimal metadata. The bar is the only
370
+ color on the page — everything else is black on white.
371
+ """
372
+ # Pick text color for page (minimal uses page_bg which is near-white)
373
+ text_dark = t.get("dark", "#111111")
374
+ muted = t.get("muted", "#999999")
375
+ accent = t["accent"]
376
+
377
+ subtitle_block = ""
378
+ if t.get("subtitle"):
379
+ subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
380
+
381
+ return f"""<!DOCTYPE html>
382
+ <html>
383
+ <head><meta charset="UTF-8">
384
+ <style>
385
+ {_base_css(t)}
386
+ html, body {{ background: {t['page_bg']}; }}
387
+ .page {{ background: {t['page_bg']}; }}
388
+
389
+ /* Left accent bar — the only color element */
390
+ .bar {{
391
+ position: absolute;
392
+ top: 0; left: 0;
393
+ width: 8px; height: 1123px;
394
+ background: {accent};
395
+ }}
396
+
397
+ /* Main content column — offset from bar */
398
+ .content {{
399
+ position: absolute;
400
+ left: 64px; right: 64px;
401
+ top: 0; bottom: 0;
402
+ display: flex;
403
+ flex-direction: column;
404
+ justify-content: center;
405
+ padding-bottom: 40px;
406
+ }}
407
+
408
+ .eyebrow {{
409
+ font-size: 9px;
410
+ font-weight: 500;
411
+ letter-spacing: 0.28em;
412
+ text-transform: uppercase;
413
+ color: {accent};
414
+ margin-bottom: 36px;
415
+ }}
416
+
417
+ .title {{
418
+ font-family: '{t['font_display']}', Georgia, 'Times New Roman', serif;
419
+ font-weight: 300;
420
+ font-size: 72px;
421
+ line-height: 1.0;
422
+ color: {text_dark};
423
+ letter-spacing: -0.02em;
424
+ max-width: 580px;
425
+ word-wrap: break-word;
426
+ margin-bottom: 0;
427
+ }}
428
+
429
+ .rule {{
430
+ width: 56px;
431
+ height: 1px;
432
+ background: {text_dark};
433
+ margin: 36px 0 24px;
434
+ opacity: 0.2;
435
+ }}
436
+
437
+ .subtitle {{
438
+ font-size: 13px;
439
+ font-weight: 300;
440
+ color: {muted};
441
+ line-height: 1.7;
442
+ max-width: 460px;
443
+ margin-bottom: 28px;
444
+ }}
445
+
446
+ .meta {{
447
+ font-size: 10px;
448
+ letter-spacing: 0.06em;
449
+ color: {muted};
450
+ margin-top: 4px;
451
+ }}
452
+ </style>
453
+ </head>
454
+ <body>
455
+ <div class="page">
456
+ <div class="bar"></div>
457
+ <div class="content">
458
+ <div class="eyebrow">{t.get('doc_type','').upper()}</div>
459
+ <div class="title">{t['title']}</div>
460
+ <div class="rule"></div>
461
+ {subtitle_block}
462
+ <div class="meta">{t.get('author','')}{(' · ' + t.get('date','')) if t.get('date') else ''}</div>
463
+ </div>
464
+ </div>
465
+ </body></html>"""
466
+
467
+
468
+ # ── Pattern 6: Stripe — bold horizontal bands ──────────────────────────────────
469
+ def _pattern_stripe(t: dict) -> str:
470
+ """
471
+ Page divided into three bold horizontal bands:
472
+ - Top band (accent, ~18%): document type label
473
+ - Middle band (dark, ~52%): large title in white
474
+ - Bottom band (page bg, ~30%): author / date / subtitle
475
+ Hard geometry, no gradients, no textures. Newspaper / brand poster aesthetic.
476
+ """
477
+ top_h = 200 # accent band
478
+ mid_h = 580 # dark band
479
+ bot_y = top_h + mid_h # 780
480
+
481
+ accent = t["accent"]
482
+ dark = t.get("cover_bg", "#1A1A2E")
483
+ light = t.get("page_bg", "#FAFAF8")
484
+ text_l = t.get("text_light", "#FFFFFF")
485
+ muted = t.get("muted", "#888888")
486
+
487
+ subtitle_block = ""
488
+ if t.get("subtitle"):
489
+ subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
490
+
491
+ return f"""<!DOCTYPE html>
492
+ <html>
493
+ <head><meta charset="UTF-8">
494
+ <style>
495
+ {_base_css(t)}
496
+ html, body {{ background: {light}; }}
497
+ .page {{ background: {light}; }}
498
+
499
+ /* Three bands */
500
+ .band-top {{
501
+ position: absolute; top: 0; left: 0;
502
+ width: 794px; height: {top_h}px;
503
+ background: {accent};
504
+ display: flex; align-items: flex-end;
505
+ padding: 0 64px 24px;
506
+ }}
507
+ .band-mid {{
508
+ position: absolute; top: {top_h}px; left: 0;
509
+ width: 794px; height: {mid_h}px;
510
+ background: {dark};
511
+ display: flex; flex-direction: column; justify-content: center;
512
+ padding: 0 64px;
513
+ }}
514
+ .band-bot {{
515
+ position: absolute; top: {bot_y}px; left: 0;
516
+ width: 794px; height: {1123 - bot_y}px;
517
+ background: {light};
518
+ display: flex; flex-direction: column; justify-content: center;
519
+ padding: 0 64px;
520
+ }}
521
+
522
+ /* Top band — doc type in large caps */
523
+ .eyebrow {{
524
+ font-family: '{t['font_display']}', sans-serif;
525
+ font-size: 11px; font-weight: 700;
526
+ letter-spacing: 0.32em; text-transform: uppercase;
527
+ color: {dark}; opacity: 0.85;
528
+ }}
529
+
530
+ /* Mid band — title */
531
+ .title {{
532
+ font-family: '{t['font_display']}', 'Times New Roman', Georgia, serif;
533
+ font-weight: 900;
534
+ font-size: 62px;
535
+ line-height: 0.97;
536
+ color: {text_l};
537
+ letter-spacing: -0.02em;
538
+ max-width: 620px;
539
+ word-wrap: break-word;
540
+ }}
541
+
542
+ /* Thin horizontal separator between mid and bot */
543
+ .sep {{
544
+ position: absolute; top: {bot_y}px; left: 0;
545
+ width: 794px; height: 2px;
546
+ background: {accent};
547
+ }}
548
+
549
+ /* Bottom band */
550
+ .author {{
551
+ font-size: 13px; font-weight: 500;
552
+ color: {t.get('dark','#111')}; margin-bottom: 4px;
553
+ }}
554
+ .date {{ font-size: 11px; color: {muted}; margin-bottom: 12px; }}
555
+ .subtitle {{
556
+ font-size: 12px; color: {muted}; line-height: 1.6;
557
+ max-width: 540px;
558
+ }}
559
+ </style>
560
+ </head>
561
+ <body>
562
+ <div class="page">
563
+ <div class="band-top">
564
+ <div class="eyebrow">{t.get('doc_type','').upper()}</div>
565
+ </div>
566
+ <div class="band-mid">
567
+ <div class="title">{t['title']}</div>
568
+ </div>
569
+ <div class="sep"></div>
570
+ <div class="band-bot">
571
+ <div class="author">{t.get('author','')}</div>
572
+ <div class="date">{t.get('date','')}</div>
573
+ {subtitle_block}
574
+ </div>
575
+ </div>
576
+ </body></html>"""
577
+
578
+
579
+ # ── Pattern 7: Diagonal — angled color split ───────────────────────────────────
580
+ def _pattern_diagonal(t: dict) -> str:
581
+ """
582
+ SVG polygon cuts the page diagonally: upper-left in dark cover color,
583
+ lower-right in light page bg. Title sits on the dark area, metadata on light.
584
+ One angled edge — no gradients, no curves.
585
+ """
586
+ dark_bg = t.get("cover_bg", "#1B2A4A")
587
+ light_bg = t.get("page_bg", "#FAFCFF")
588
+ accent = t["accent"]
589
+ text_l = t.get("text_light", "#F8FAFF")
590
+ text_d = t.get("dark", "#0F1A2E")
591
+ muted = t.get("muted", "#7A8A99")
592
+
593
+ # Polygon: full upper-left to ~60% down on right side
594
+ # Points: top-left, top-right, (794, 620), (0, 820)
595
+ poly = "0,0 794,0 794,620 0,820"
596
+
597
+ subtitle_block = ""
598
+ if t.get("subtitle"):
599
+ subtitle_block = f'<div class="subtitle-lt">{t["subtitle"]}</div>'
600
+
601
+ return f"""<!DOCTYPE html>
602
+ <html>
603
+ <head><meta charset="UTF-8">
604
+ <style>
605
+ {_base_css(t)}
606
+ html, body {{ background: {light_bg}; }}
607
+ .page {{ background: {light_bg}; overflow: hidden; }}
608
+
609
+ /* Title block — upper dark area */
610
+ .content-dark {{
611
+ position: absolute;
612
+ left: 64px; right: 64px;
613
+ top: 180px;
614
+ z-index: 2;
615
+ }}
616
+ .eyebrow {{
617
+ font-size: 9px; font-weight: 500;
618
+ letter-spacing: 0.26em; text-transform: uppercase;
619
+ color: {accent}; margin-bottom: 28px;
620
+ }}
621
+ .title {{
622
+ font-family: '{t['font_display']}', 'Helvetica Neue', sans-serif;
623
+ font-weight: 900;
624
+ font-size: 58px;
625
+ line-height: 1.0;
626
+ color: {text_l};
627
+ letter-spacing: -0.018em;
628
+ max-width: 560px;
629
+ word-wrap: break-word;
630
+ margin-bottom: 16px;
631
+ }}
632
+ .rule-accent {{
633
+ width: 52px; height: 3px;
634
+ background: {accent};
635
+ margin-top: 28px;
636
+ }}
637
+
638
+ /* Metadata — lower light area */
639
+ .content-light {{
640
+ position: absolute;
641
+ left: 64px; right: 64px;
642
+ bottom: 80px;
643
+ z-index: 2;
644
+ }}
645
+ .author {{
646
+ font-size: 12px; font-weight: 500;
647
+ color: {text_d}; margin-bottom: 4px;
648
+ }}
649
+ .date {{ font-size: 11px; color: {muted}; margin-bottom: 12px; }}
650
+ .subtitle-lt {{
651
+ font-size: 12px; color: {muted}; line-height: 1.6;
652
+ max-width: 480px;
653
+ }}
654
+ </style>
655
+ </head>
656
+ <body>
657
+ <div class="page">
658
+ <!-- Diagonal dark polygon -->
659
+ <svg style="position:absolute;top:0;left:0;width:794px;height:1123px;z-index:1"
660
+ xmlns="http://www.w3.org/2000/svg">
661
+ <polygon points="{poly}" fill="{dark_bg}"/>
662
+ <!-- Accent edge line along the diagonal -->
663
+ <line x1="0" y1="820" x2="794" y2="620"
664
+ stroke="{accent}" stroke-width="2.5"/>
665
+ </svg>
666
+
667
+ <div class="content-dark">
668
+ <div class="eyebrow">{t.get('doc_type','').upper()}&nbsp; · &nbsp;{t.get('date','')}</div>
669
+ <div class="title">{t['title']}</div>
670
+ <div class="rule-accent"></div>
671
+ </div>
672
+
673
+ <div class="content-light">
674
+ <div class="author">{t.get('author','')}</div>
675
+ {subtitle_block}
676
+ </div>
677
+ </div>
678
+ </body></html>"""
679
+
680
+
681
+ # ── Pattern 8: Frame — elegant inset border ────────────────────────────────────
682
+ def _pattern_frame(t: dict) -> str:
683
+ """
684
+ Classic formal layout: outer thin border line inset ~28px from page edges,
685
+ inner accent strip at top and bottom inside the frame.
686
+ Title centered in the frame space, classical serif typography.
687
+ Used for: academic papers, formal reports, legal docs, annual reports.
688
+ """
689
+ bg = t.get("cover_bg", "#FAF8F3")
690
+ accent = t["accent"]
691
+ dark = t.get("dark", "#2A1A0A")
692
+ muted = t.get("muted", "#9A8A78")
693
+
694
+ pad = 28 # frame inset from page edge
695
+ inner_w = 794 - 2 * pad
696
+ inner_h = 1123 - 2 * pad
697
+
698
+ subtitle_block = ""
699
+ if t.get("subtitle"):
700
+ subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
701
+
702
+ return f"""<!DOCTYPE html>
703
+ <html>
704
+ <head><meta charset="UTF-8">
705
+ <style>
706
+ {_base_css(t)}
707
+ html, body {{ background: {bg}; }}
708
+ .page {{ background: {bg}; }}
709
+
710
+ /* Outer frame rectangle */
711
+ .frame {{
712
+ position: absolute;
713
+ top: {pad}px; left: {pad}px;
714
+ width: {inner_w}px; height: {inner_h}px;
715
+ border: 1.2px solid {dark};
716
+ opacity: 0.35;
717
+ }}
718
+
719
+ /* Accent strips inside top and bottom of frame */
720
+ .frame-top-accent {{
721
+ position: absolute;
722
+ top: {pad + 10}px; left: {pad + 10}px;
723
+ width: {inner_w - 20}px; height: 3px;
724
+ background: {accent};
725
+ }}
726
+ .frame-bot-accent {{
727
+ position: absolute;
728
+ bottom: {pad + 10}px; left: {pad + 10}px;
729
+ width: {inner_w - 20}px; height: 3px;
730
+ background: {accent};
731
+ }}
732
+
733
+ /* Corner ornament squares */
734
+ .corner {{
735
+ position: absolute;
736
+ width: 8px; height: 8px;
737
+ background: {accent};
738
+ opacity: 0.6;
739
+ }}
740
+ .tl {{ top: {pad - 4}px; left: {pad - 4}px; }}
741
+ .tr {{ top: {pad - 4}px; right: {pad - 4}px; }}
742
+ .bl {{ bottom: {pad - 4}px; left: {pad - 4}px; }}
743
+ .br {{ bottom: {pad - 4}px; right: {pad - 4}px; }}
744
+
745
+ /* Main content centered in frame */
746
+ .content {{
747
+ position: absolute;
748
+ left: {pad + 56}px; right: {pad + 56}px;
749
+ top: 0; bottom: 0;
750
+ display: flex;
751
+ flex-direction: column;
752
+ align-items: center;
753
+ justify-content: center;
754
+ text-align: center;
755
+ }}
756
+
757
+ .eyebrow {{
758
+ font-size: 8.5px;
759
+ font-weight: 500;
760
+ letter-spacing: 0.30em;
761
+ text-transform: uppercase;
762
+ color: {accent};
763
+ margin-bottom: 44px;
764
+ }}
765
+
766
+ .rule-top {{
767
+ width: 60px; height: 1px;
768
+ background: {dark};
769
+ opacity: 0.3;
770
+ margin-bottom: 28px;
771
+ }}
772
+
773
+ .title {{
774
+ font-family: '{t['font_display']}', Georgia, 'Times New Roman', serif;
775
+ font-weight: 400;
776
+ font-size: 44px;
777
+ line-height: 1.25;
778
+ color: {dark};
779
+ letter-spacing: 0.01em;
780
+ max-width: 540px;
781
+ word-wrap: break-word;
782
+ margin-bottom: 0;
783
+ }}
784
+
785
+ .rule-mid {{
786
+ width: 40px; height: 1.5px;
787
+ background: {accent};
788
+ margin: 28px 0 20px;
789
+ }}
790
+
791
+ .subtitle {{
792
+ font-size: 13px;
793
+ font-weight: 300;
794
+ font-style: italic;
795
+ color: {muted};
796
+ line-height: 1.6;
797
+ max-width: 400px;
798
+ margin-bottom: 20px;
799
+ }}
800
+
801
+ .meta {{
802
+ font-size: 10px;
803
+ letter-spacing: 0.08em;
804
+ color: {muted};
805
+ margin-top: 8px;
806
+ }}
807
+ </style>
808
+ </head>
809
+ <body>
810
+ <div class="page">
811
+ <div class="frame"></div>
812
+ <div class="frame-top-accent"></div>
813
+ <div class="frame-bot-accent"></div>
814
+ <div class="corner tl"></div>
815
+ <div class="corner tr"></div>
816
+ <div class="corner bl"></div>
817
+ <div class="corner br"></div>
818
+
819
+ <div class="content">
820
+ <div class="eyebrow">{t.get('doc_type','').upper()}</div>
821
+ <div class="rule-top"></div>
822
+ <div class="title">{t['title']}</div>
823
+ <div class="rule-mid"></div>
824
+ {subtitle_block}
825
+ <div class="meta">{t.get('author','')}{(' · ' + t.get('date','')) if t.get('date') else ''}</div>
826
+ </div>
827
+ </div>
828
+ </body></html>"""
829
+
830
+
831
+ # ── Pattern 9: Editorial — oversized ghost letter + bold type ──────────────────
832
+ def _pattern_editorial(t: dict) -> str:
833
+ """
834
+ Magazine / editorial feel:
835
+ - Oversized first-letter of title as a ghost background element (8–12% opacity)
836
+ - Bold category label at top in accent
837
+ - Title in very large condensed weight, flush-left
838
+ - Thin full-width rule separating title from metadata
839
+ - Author / date bottom-left, page type bottom-right
840
+ Designed for editorial reports, annual reviews, magazine-format content.
841
+ """
842
+ bg = t.get("cover_bg", "#FFFFFF")
843
+ accent = t["accent"]
844
+ dark = t.get("dark", "#0A0A0A")
845
+ muted = t.get("muted", "#777777")
846
+ text_l = t.get("text_light", "#FFFFFF")
847
+
848
+ # Ghost letter — first character of title
849
+ ghost = t['title'][0].upper() if t['title'] else "A"
850
+
851
+ subtitle_block = ""
852
+ if t.get("subtitle"):
853
+ subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
854
+
855
+ # Determine if background is dark (use light text) or light (use dark text)
856
+ is_dark_bg = (
857
+ bg.startswith("#0") or bg.startswith("#1") or bg.startswith("#2")
858
+ )
859
+ title_color = text_l if is_dark_bg else dark # noqa: F841
860
+ body_color = text_l if is_dark_bg else dark
861
+
862
+ return f"""<!DOCTYPE html>
863
+ <html>
864
+ <head><meta charset="UTF-8">
865
+ <style>
866
+ {_base_css(t)}
867
+ html, body {{ background: {bg}; }}
868
+ .page {{ background: {bg}; }}
869
+
870
+ /* Ghost letter — background texture */
871
+ .ghost {{
872
+ position: absolute;
873
+ right: -60px; top: -40px;
874
+ font-family: '{t['font_display']}', 'Arial Black', sans-serif;
875
+ font-weight: 900;
876
+ font-size: 680px;
877
+ line-height: 1;
878
+ color: {dark};
879
+ opacity: 0.055;
880
+ user-select: none;
881
+ letter-spacing: -0.05em;
882
+ }}
883
+
884
+ /* Top bar: accent stripe */
885
+ .topbar {{
886
+ position: absolute;
887
+ top: 0; left: 0; right: 0;
888
+ height: 5px;
889
+ background: {accent};
890
+ }}
891
+
892
+ /* Category label */
893
+ .category {{
894
+ position: absolute;
895
+ top: 40px; left: 60px;
896
+ font-size: 9px; font-weight: 700;
897
+ letter-spacing: 0.30em; text-transform: uppercase;
898
+ color: {accent};
899
+ }}
900
+
901
+ /* Main title block */
902
+ .content {{
903
+ position: absolute;
904
+ left: 60px; right: 60px;
905
+ top: 0; bottom: 0;
906
+ display: flex;
907
+ flex-direction: column;
908
+ justify-content: center;
909
+ padding-bottom: 80px;
910
+ }}
911
+
912
+ .title {{
913
+ font-family: '{t['font_display']}', 'Arial Black', Impact, sans-serif;
914
+ font-weight: 900;
915
+ font-size: 80px;
916
+ line-height: 0.92;
917
+ color: {body_color};
918
+ letter-spacing: -0.03em;
919
+ max-width: 620px;
920
+ word-wrap: break-word;
921
+ text-transform: uppercase;
922
+ }}
923
+
924
+ .subtitle {{
925
+ font-size: 14px;
926
+ font-weight: 400;
927
+ color: {muted};
928
+ line-height: 1.6;
929
+ max-width: 500px;
930
+ margin-top: 20px;
931
+ }}
932
+
933
+ /* Full-width rule above footer */
934
+ .footer-rule {{
935
+ position: absolute;
936
+ bottom: 80px; left: 60px; right: 60px;
937
+ height: 1px;
938
+ background: {body_color};
939
+ opacity: 0.15;
940
+ }}
941
+
942
+ /* Footer row */
943
+ .footer {{
944
+ position: absolute;
945
+ bottom: 44px; left: 60px; right: 60px;
946
+ display: flex;
947
+ justify-content: space-between;
948
+ align-items: baseline;
949
+ }}
950
+ .footer-author {{ font-size: 11px; color: {muted}; letter-spacing: 0.04em; }}
951
+ .footer-date {{ font-size: 10px; color: {muted}; letter-spacing: 0.04em; }}
952
+ </style>
953
+ </head>
954
+ <body>
955
+ <div class="page">
956
+ <div class="ghost">{ghost}</div>
957
+ <div class="topbar"></div>
958
+ <div class="category">{t.get('doc_type','').upper()}</div>
959
+
960
+ <div class="content">
961
+ <div class="title">{t['title']}</div>
962
+ {subtitle_block}
963
+ </div>
964
+
965
+ <div class="footer-rule"></div>
966
+ <div class="footer">
967
+ <div class="footer-author">{t.get('author','')}</div>
968
+ <div class="footer-date">{t.get('date','')}</div>
969
+ </div>
970
+ </div>
971
+ </body></html>"""
972
+
973
+
974
+ # ── Pattern 10: Magazine — elegant centered with optional hero image ────────────
975
+ def _pattern_magazine(t: dict) -> str:
976
+ """
977
+ Upscale centered layout: company name + accent rule at top, large serif title,
978
+ decorative rule, italic subtitle, optional hero image, abstract block, author.
979
+ Used for: annual reports, strategic documents, formal publications.
980
+ """
981
+ bg = t.get("cover_bg", "#F2F0EC")
982
+ accent = t["accent"]
983
+ dark = t.get("dark", "#0D1A2B")
984
+ muted = t.get("muted", "#888888")
985
+ org = t.get("doc_type", "").upper()
986
+ img_url = t.get("cover_image", "")
987
+
988
+ subtitle_block = ""
989
+ if t.get("subtitle"):
990
+ subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
991
+
992
+ image_block = ""
993
+ if img_url:
994
+ image_block = f"""
995
+ <div style="text-align:center;margin:32px 0 28px;">
996
+ <img src="{img_url}" style="max-width:340px;max-height:220px;
997
+ object-fit:cover;display:inline-block;"/>
998
+ </div>"""
999
+
1000
+ abstract_block = ""
1001
+ if t.get("abstract"):
1002
+ abstract_block = f"""
1003
+ <div style="font-size:11px;line-height:1.7;color:{muted};
1004
+ text-align:justify;max-width:560px;margin:0 auto 0;">
1005
+ <span style="font-weight:700;color:{accent};">Abstract:</span>
1006
+ {t['abstract']}
1007
+ </div>"""
1008
+
1009
+ return f"""<!DOCTYPE html>
1010
+ <html>
1011
+ <head><meta charset="UTF-8">
1012
+ <style>
1013
+ {_base_css(t)}
1014
+ html, body {{ background: {bg}; }}
1015
+ .page {{ background: {bg}; display:flex; flex-direction:column;
1016
+ align-items:center; justify-content:center; padding:60px 80px; }}
1017
+
1018
+ .org-name {{
1019
+ font-size: 9px; font-weight: 500; letter-spacing: 0.30em;
1020
+ text-transform: uppercase; color: {dark}; text-align:center;
1021
+ margin-bottom: 10px;
1022
+ }}
1023
+ .org-rule {{
1024
+ width: 56px; height: 2px; background: {accent};
1025
+ margin: 0 auto 52px;
1026
+ }}
1027
+ .title {{
1028
+ font-family: '{t['font_display']}', Georgia, 'Times New Roman', serif;
1029
+ font-weight: 700; font-size: 52px; line-height: 1.08;
1030
+ color: {dark}; text-align: center; letter-spacing: -0.015em;
1031
+ max-width: 560px; word-wrap: break-word; margin-bottom: 18px;
1032
+ }}
1033
+ .title-rule {{
1034
+ width: 44px; height: 2.5px; background: {accent};
1035
+ margin: 0 auto 20px;
1036
+ }}
1037
+ .subtitle {{
1038
+ font-family: '{t['font_display']}', Georgia, serif;
1039
+ font-style: italic; font-size: 14px; color: {muted};
1040
+ text-align: center; line-height: 1.5; max-width: 440px;
1041
+ margin: 0 auto;
1042
+ }}
1043
+ .separator {{
1044
+ width: 100%; max-width: 620px; height: 1px;
1045
+ background: {dark}; opacity: 0.12;
1046
+ margin: 28px auto;
1047
+ }}
1048
+ .author-name {{
1049
+ font-family: '{t['font_display']}', Georgia, serif;
1050
+ font-size: 16px; font-weight: 700; color: {accent};
1051
+ text-align: center; margin-bottom: 6px;
1052
+ }}
1053
+ .date-line {{
1054
+ font-size: 11px; color: {muted}; text-align: center;
1055
+ letter-spacing: 0.03em;
1056
+ }}
1057
+ </style>
1058
+ </head>
1059
+ <body>
1060
+ <div class="page">
1061
+ <div class="org-name">{org}</div>
1062
+ <div class="org-rule"></div>
1063
+ <div class="title">{t['title']}</div>
1064
+ <div class="title-rule"></div>
1065
+ {subtitle_block}
1066
+ {image_block}
1067
+ {abstract_block}
1068
+ {'<div class="separator"></div>' if (t.get('abstract') or img_url) else '<div style="margin:28px 0;"></div>'}
1069
+ <div class="author-name">{t.get('author','')}</div>
1070
+ <div class="date-line">{t.get('date','')}</div>
1071
+ </div>
1072
+ </body></html>"""
1073
+
1074
+
1075
+ # ── Pattern 11: Darkroom — dark magazine variant ────────────────────────────────
1076
+ def _pattern_darkroom(t: dict) -> str:
1077
+ """
1078
+ Dark-background centered layout. Same structure as magazine but inverted:
1079
+ deep navy page, white/silver text, accent rules in lighter tone.
1080
+ Used for: premium reports, tech annual reviews, dark-themed documents.
1081
+ """
1082
+ bg = t.get("cover_bg", "#151C27")
1083
+ accent = t["accent"]
1084
+ text_l = t.get("text_light", "#F0EDE6")
1085
+ muted = t.get("muted", "#8A9AB0")
1086
+ org = t.get("doc_type", "").upper()
1087
+ img_url = t.get("cover_image", "")
1088
+
1089
+ subtitle_block = ""
1090
+ if t.get("subtitle"):
1091
+ subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
1092
+
1093
+ image_block = ""
1094
+ if img_url:
1095
+ image_block = f"""
1096
+ <div style="text-align:center;margin:32px 0 28px;">
1097
+ <img src="{img_url}" style="max-width:340px;max-height:220px;
1098
+ object-fit:cover;display:inline-block;
1099
+ filter:grayscale(20%) brightness(0.9);"/>
1100
+ </div>"""
1101
+
1102
+ abstract_block = ""
1103
+ if t.get("abstract"):
1104
+ abstract_block = f"""
1105
+ <div style="font-size:11px;line-height:1.7;color:{muted};
1106
+ text-align:justify;max-width:560px;margin:0 auto 0;">
1107
+ <span style="font-weight:700;color:{accent};">Abstract:</span>
1108
+ {t['abstract']}
1109
+ </div>"""
1110
+
1111
+ return f"""<!DOCTYPE html>
1112
+ <html>
1113
+ <head><meta charset="UTF-8">
1114
+ <style>
1115
+ {_base_css(t)}
1116
+ html, body {{ background: {bg}; }}
1117
+ .page {{ background: {bg}; display:flex; flex-direction:column;
1118
+ align-items:center; justify-content:center; padding:60px 80px; }}
1119
+
1120
+ .org-name {{
1121
+ font-size: 9px; font-weight: 500; letter-spacing: 0.30em;
1122
+ text-transform: uppercase; color: {text_l}; text-align:center;
1123
+ opacity: 0.75; margin-bottom: 10px;
1124
+ }}
1125
+ .org-rule {{
1126
+ width: 56px; height: 2px; background: {text_l};
1127
+ opacity: 0.35; margin: 0 auto 52px;
1128
+ }}
1129
+ .title {{
1130
+ font-family: '{t['font_display']}', Georgia, 'Times New Roman', serif;
1131
+ font-weight: 700; font-size: 52px; line-height: 1.08;
1132
+ color: {text_l}; text-align: center; letter-spacing: -0.015em;
1133
+ max-width: 560px; word-wrap: break-word; margin-bottom: 18px;
1134
+ }}
1135
+ .title-rule {{
1136
+ width: 44px; height: 2.5px; background: {text_l};
1137
+ opacity: 0.35; margin: 0 auto 20px;
1138
+ }}
1139
+ .subtitle {{
1140
+ font-family: '{t['font_display']}', Georgia, serif;
1141
+ font-style: italic; font-size: 14px; color: {muted};
1142
+ text-align: center; line-height: 1.5; max-width: 440px;
1143
+ margin: 0 auto;
1144
+ }}
1145
+ .separator {{
1146
+ width: 100%; max-width: 620px; height: 1px;
1147
+ background: {text_l}; opacity: 0.12;
1148
+ margin: 28px auto;
1149
+ }}
1150
+ .author-name {{
1151
+ font-family: '{t['font_display']}', Georgia, serif;
1152
+ font-size: 16px; font-weight: 700; color: {text_l};
1153
+ text-align: center; margin-bottom: 6px;
1154
+ }}
1155
+ .date-line {{
1156
+ font-size: 11px; color: {muted}; text-align: center;
1157
+ letter-spacing: 0.03em;
1158
+ }}
1159
+ </style>
1160
+ </head>
1161
+ <body>
1162
+ <div class="page">
1163
+ <div class="org-name">{org}</div>
1164
+ <div class="org-rule"></div>
1165
+ <div class="title">{t['title']}</div>
1166
+ <div class="title-rule"></div>
1167
+ {subtitle_block}
1168
+ {image_block}
1169
+ {abstract_block}
1170
+ {'<div class="separator"></div>' if (t.get('abstract') or img_url) else '<div style="margin:28px 0;"></div>'}
1171
+ <div class="author-name">{t.get('author','')}</div>
1172
+ <div class="date-line">{t.get('date','')}</div>
1173
+ </div>
1174
+ </body></html>"""
1175
+
1176
+
1177
+ # ── Pattern 12: Terminal — cyber/hacker aesthetic ───────────────────────────────
1178
+ def _pattern_terminal(t: dict) -> str:
1179
+ """
1180
+ Dark terminal/IDE aesthetic: grid overlay, monospace font, neon accent,
1181
+ corner brackets around the title block, status bar at bottom.
1182
+ Used for: tech reports, developer docs, security audits, system documentation.
1183
+ """
1184
+ bg = t.get("cover_bg", "#0D1117")
1185
+ accent = t["accent"]
1186
+ text_l = t.get("text_light", "#E6EDF3")
1187
+ muted = t.get("muted", "#48897C")
1188
+ dark = t.get("dark", "#010409")
1189
+ org = t.get("doc_type", "DOCUMENT").upper()
1190
+ date_s = t.get("date", "")
1191
+ author = t.get("author", "")
1192
+
1193
+ subtitle_line = ""
1194
+ if t.get("subtitle"):
1195
+ subtitle_line = f'<div class="subtitle">&gt; {t["subtitle"]}</div>'
1196
+
1197
+ abstract_block = ""
1198
+ if t.get("abstract"):
1199
+ abstract_block = f"""
1200
+ <div class="abstract-text">{t['abstract']}</div>"""
1201
+
1202
+ # grid overlay: horizontal + vertical lines
1203
+ h_lines = "".join(
1204
+ f'<line x1="0" y1="{y}" x2="794" y2="{y}" stroke="{accent}" stroke-width="0.4"/>'
1205
+ for y in range(0, 1124, 48)
1206
+ )
1207
+ v_lines = "".join(
1208
+ f'<line x1="{x}" y1="0" x2="{x}" y2="1123" stroke="{accent}" stroke-width="0.4"/>'
1209
+ for x in range(0, 795, 48)
1210
+ )
1211
+ grid_svg = (
1212
+ f'<svg style="position:absolute;top:0;left:0;width:794px;height:1123px;'
1213
+ f'pointer-events:none;opacity:0.07" xmlns="http://www.w3.org/2000/svg">'
1214
+ + h_lines + v_lines + "</svg>"
1215
+ )
1216
+
1217
+ return f"""<!DOCTYPE html>
1218
+ <html>
1219
+ <head><meta charset="UTF-8">
1220
+ <style>
1221
+ {_base_css(t)}
1222
+ html, body {{ background: {bg}; }}
1223
+ .page {{ background: {bg}; }}
1224
+
1225
+ /* Terminal label — top */
1226
+ .term-label {{
1227
+ position: absolute; top: 44px; left: 56px; right: 56px;
1228
+ display: flex; align-items: center; gap: 10px;
1229
+ }}
1230
+ .dot {{
1231
+ width: 8px; height: 8px; border-radius: 50%;
1232
+ background: {accent}; flex-shrink: 0;
1233
+ }}
1234
+ .term-meta {{
1235
+ font-family: '{t['font_body']}', 'Courier New', monospace;
1236
+ font-size: 10px; color: {accent}; letter-spacing: 0.08em;
1237
+ text-transform: uppercase;
1238
+ }}
1239
+
1240
+ /* Title bracket block */
1241
+ .bracket-block {{
1242
+ position: absolute;
1243
+ top: 310px; left: 56px; right: 56px;
1244
+ border-left: 2px solid {accent}; border-top: 2px solid {accent};
1245
+ padding: 24px 28px 28px;
1246
+ box-shadow: inset 0 0 0 0;
1247
+ }}
1248
+ .bracket-block::after {{
1249
+ content: '';
1250
+ position: absolute;
1251
+ bottom: 0; right: 0;
1252
+ width: 32px; height: 2px;
1253
+ background: {accent};
1254
+ }}
1255
+ .bracket-block::before {{
1256
+ content: '';
1257
+ position: absolute;
1258
+ bottom: 0; right: 0;
1259
+ width: 2px; height: 32px;
1260
+ background: {accent};
1261
+ }}
1262
+
1263
+ .title {{
1264
+ font-family: '{t['font_display']}', 'Courier New', monospace;
1265
+ font-weight: 700; font-size: 46px; line-height: 1.05;
1266
+ color: {text_l}; letter-spacing: 0.01em;
1267
+ text-transform: uppercase;
1268
+ word-wrap: break-word; margin-bottom: 16px;
1269
+ }}
1270
+ .subtitle {{
1271
+ font-family: '{t['font_body']}', 'Courier New', monospace;
1272
+ font-size: 13px; color: {accent};
1273
+ line-height: 1.5; letter-spacing: 0.02em;
1274
+ margin-top: 8px;
1275
+ }}
1276
+
1277
+ /* Content block below brackets */
1278
+ .content-lower {{
1279
+ position: absolute;
1280
+ top: 640px; left: 56px; right: 56px;
1281
+ display: flex; gap: 40px; align-items: flex-start;
1282
+ }}
1283
+ .abstract-text {{
1284
+ font-family: '{t['font_body']}', 'Courier New', monospace;
1285
+ font-size: 10.5px; line-height: 1.8; color: {muted};
1286
+ flex: 1;
1287
+ }}
1288
+ .author-block {{
1289
+ text-align: right; flex-shrink: 0; min-width: 160px;
1290
+ }}
1291
+ .author-label {{
1292
+ font-family: '{t['font_body']}', monospace;
1293
+ font-size: 8px; letter-spacing: 0.20em; color: {muted};
1294
+ text-transform: uppercase; margin-bottom: 6px;
1295
+ }}
1296
+ .author-name {{
1297
+ font-family: '{t['font_body']}', monospace;
1298
+ font-size: 14px; font-weight: 700; color: {text_l};
1299
+ }}
1300
+ .author-org {{
1301
+ font-family: '{t['font_body']}', monospace;
1302
+ font-size: 10px; color: {accent}; margin-top: 4px;
1303
+ }}
1304
+
1305
+ /* Bottom status bar */
1306
+ .statusbar {{
1307
+ position: absolute; bottom: 0; left: 0; right: 0;
1308
+ height: 36px; background: {accent}; opacity: 0.12;
1309
+ }}
1310
+ .statusbar-text {{
1311
+ position: absolute; bottom: 0; left: 0; right: 0;
1312
+ height: 36px; display: flex; align-items: center;
1313
+ justify-content: space-between; padding: 0 56px;
1314
+ }}
1315
+ .sb-item {{
1316
+ font-family: '{t['font_body']}', monospace;
1317
+ font-size: 9px; color: {muted}; letter-spacing: 0.12em;
1318
+ text-transform: uppercase;
1319
+ }}
1320
+ </style>
1321
+ </head>
1322
+ <body>
1323
+ <div class="page">
1324
+ {grid_svg}
1325
+
1326
+ <div class="term-label">
1327
+ <div class="dot"></div>
1328
+ <div class="term-meta">SYSTEM_REPORT // {date_s}</div>
1329
+ </div>
1330
+
1331
+ <div class="bracket-block">
1332
+ <div class="title">{t['title']}</div>
1333
+ {subtitle_line}
1334
+ </div>
1335
+
1336
+ <div class="content-lower">
1337
+ {abstract_block}
1338
+ <div class="author-block">
1339
+ <div class="author-label">AUTHOR_ID</div>
1340
+ <div class="author-name">{author}</div>
1341
+ <div class="author-org">{org}</div>
1342
+ </div>
1343
+ </div>
1344
+
1345
+ <div class="statusbar"></div>
1346
+ <div class="statusbar-text">
1347
+ <div class="sb-item">Ln 1, Col 1</div>
1348
+ <div class="sb-item">UTF-8</div>
1349
+ <div class="sb-item">GENERATED_BY_COVERGENIUS</div>
1350
+ </div>
1351
+ </div>
1352
+ </body></html>"""
1353
+
1354
+
1355
+ # ── Pattern 13: Poster — bold sidebar + oversized type ─────────────────────────
1356
+ def _pattern_poster(t: dict) -> str:
1357
+ """
1358
+ Bold minimalist poster: thick vertical sidebar on the left, oversized all-caps
1359
+ title, typewriter-style metadata. Optional thumbnail on the right side.
1360
+ Used for: portfolios, creative reports, journalism, photography books.
1361
+ """
1362
+ bg = t.get("cover_bg", "#FFFFFF")
1363
+ accent = t["accent"] # typically black or strong dark
1364
+ dark = t.get("dark", "#0A0A0A")
1365
+ muted = t.get("muted", "#888888")
1366
+ text_l = t.get("text_light", "#FFFFFF")
1367
+ img_url = t.get("cover_image", "")
1368
+
1369
+ sidebar_w = 52
1370
+
1371
+ subtitle_block = ""
1372
+ if t.get("subtitle"):
1373
+ subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
1374
+
1375
+ image_block = ""
1376
+ if img_url:
1377
+ image_block = f"""
1378
+ <img src="{img_url}" style="
1379
+ width:260px;height:340px;object-fit:cover;
1380
+ display:block;margin-top:32px;
1381
+ filter:grayscale(100%) contrast(1.1);"/>"""
1382
+
1383
+ meta_lines = []
1384
+ if t.get("author"):
1385
+ meta_lines.append(f'<div class="meta-line">{t["author"]}</div>')
1386
+ if t.get("subtitle"):
1387
+ meta_lines.append(f'<div class="meta-line meta-role">{t["subtitle"]}</div>')
1388
+ if t.get("date"):
1389
+ meta_lines.append(f'<div class="meta-line meta-date">{t["date"]}</div>')
1390
+ meta_block = "\n".join(meta_lines)
1391
+
1392
+ return f"""<!DOCTYPE html>
1393
+ <html>
1394
+ <head><meta charset="UTF-8">
1395
+ <style>
1396
+ {_base_css(t)}
1397
+ html, body {{ background: {bg}; }}
1398
+ .page {{ background: {bg}; }}
1399
+
1400
+ /* Left sidebar — the dominant color element */
1401
+ .sidebar {{
1402
+ position: absolute;
1403
+ top: 0; left: 0;
1404
+ width: {sidebar_w}px; height: 1123px;
1405
+ background: {accent};
1406
+ }}
1407
+
1408
+ /* Main content — offset from sidebar */
1409
+ .content {{
1410
+ position: absolute;
1411
+ left: {sidebar_w + 52}px; right: 52px;
1412
+ top: 100px; bottom: 80px;
1413
+ }}
1414
+
1415
+ /* Oversized display title */
1416
+ .title {{
1417
+ font-family: '{t['font_display']}', 'Arial Black', Impact, sans-serif;
1418
+ font-weight: 900;
1419
+ font-size: 96px;
1420
+ line-height: 0.92;
1421
+ color: {dark};
1422
+ letter-spacing: -0.03em;
1423
+ text-transform: uppercase;
1424
+ max-width: 620px;
1425
+ word-wrap: break-word;
1426
+ margin-bottom: 22px;
1427
+ }}
1428
+
1429
+ .subtitle {{
1430
+ font-family: '{t['font_body']}', 'Courier New', monospace;
1431
+ font-size: 12px;
1432
+ color: {muted};
1433
+ letter-spacing: 0.05em;
1434
+ margin-bottom: 0;
1435
+ }}
1436
+
1437
+ /* Thin rule under title area */
1438
+ .rule {{
1439
+ width: 64px; height: 2px;
1440
+ background: {dark};
1441
+ margin: 24px 0 28px;
1442
+ }}
1443
+
1444
+ /* Author / meta in typewriter font */
1445
+ .meta-group {{
1446
+ margin-top: 32px;
1447
+ }}
1448
+ .meta-line {{
1449
+ font-family: '{t['font_body']}', 'Courier New', monospace;
1450
+ font-size: 12px; color: {dark};
1451
+ line-height: 1.8; letter-spacing: 0.02em;
1452
+ }}
1453
+ .meta-role {{
1454
+ font-family: '{t['font_body']}', 'Courier New', monospace;
1455
+ color: {muted};
1456
+ }}
1457
+ .meta-date {{
1458
+ font-family: '{t['font_body']}', 'Courier New', monospace;
1459
+ font-size: 12px; color: {dark};
1460
+ margin-top: 8px;
1461
+ }}
1462
+
1463
+ /* Right-side content area for thumbnail */
1464
+ .right-col {{
1465
+ position: absolute;
1466
+ right: 52px;
1467
+ top: 380px; bottom: 80px;
1468
+ display: flex;
1469
+ flex-direction: column;
1470
+ align-items: flex-end;
1471
+ }}
1472
+
1473
+ /* Small accent square icon */
1474
+ .icon-block {{
1475
+ width: 64px; height: 64px;
1476
+ background: {accent};
1477
+ margin-top: 28px;
1478
+ display: flex; align-items: center; justify-content: center;
1479
+ flex-shrink: 0;
1480
+ }}
1481
+ .icon-lines {{
1482
+ display: flex; flex-direction: column; gap: 6px;
1483
+ }}
1484
+ .icon-line {{
1485
+ height: 2px; background: {text_l};
1486
+ }}
1487
+ </style>
1488
+ </head>
1489
+ <body>
1490
+ <div class="page">
1491
+ <div class="sidebar"></div>
1492
+
1493
+ <div class="content">
1494
+ <div class="title">{t['title']}</div>
1495
+ {subtitle_block}
1496
+ <div class="rule"></div>
1497
+ <div class="meta-group">{meta_block}</div>
1498
+ </div>
1499
+
1500
+ <div class="right-col">
1501
+ {image_block}
1502
+ <div class="icon-block">
1503
+ <div class="icon-lines">
1504
+ <div class="icon-line" style="width:32px;"></div>
1505
+ <div class="icon-line" style="width:24px;"></div>
1506
+ <div class="icon-line" style="width:28px;"></div>
1507
+ </div>
1508
+ </div>
1509
+ </div>
1510
+ </div>
1511
+ </body></html>"""
1512
+
1513
+
1514
+ # ── Dispatch ───────────────────────────────────────────────────────────────────
1515
+ PATTERNS = {
1516
+ "fullbleed": _pattern_fullbleed,
1517
+ "split": _pattern_split,
1518
+ "typographic": _pattern_typographic,
1519
+ "atmospheric": _pattern_atmospheric,
1520
+ "minimal": _pattern_minimal,
1521
+ "stripe": _pattern_stripe,
1522
+ "diagonal": _pattern_diagonal,
1523
+ "frame": _pattern_frame,
1524
+ "editorial": _pattern_editorial,
1525
+ "magazine": _pattern_magazine,
1526
+ "darkroom": _pattern_darkroom,
1527
+ "terminal": _pattern_terminal,
1528
+ "poster": _pattern_poster,
1529
+ }
1530
+
1531
+
1532
+ def render(tokens: dict) -> str:
1533
+ """Dispatch to the cover pattern function and return the HTML string."""
1534
+ pattern = tokens.get("cover_pattern", "fullbleed")
1535
+ fn = PATTERNS.get(pattern, _pattern_fullbleed)
1536
+ return fn(tokens)
1537
+
1538
+
1539
+ # ── CLI ───────────────────────────────────────────────────────────────────────
1540
+ def main():
1541
+ """CLI entry point."""
1542
+ parser = argparse.ArgumentParser(description="Render cover HTML from tokens.json")
1543
+ parser.add_argument("--tokens", default="tokens.json")
1544
+ parser.add_argument("--out", default="cover.html")
1545
+ parser.add_argument("--subtitle", default="", help="Optional subtitle override")
1546
+ args = parser.parse_args()
1547
+
1548
+ try:
1549
+ with open(args.tokens, encoding="utf-8") as f:
1550
+ tokens = json.load(f)
1551
+ except FileNotFoundError:
1552
+ print(json.dumps({"status": "error", "error": f"tokens file not found: {args.tokens}"}),
1553
+ file=sys.stderr)
1554
+ sys.exit(1)
1555
+ except json.JSONDecodeError as e:
1556
+ print(json.dumps({"status": "error", "error": f"invalid JSON: {e}"}), file=sys.stderr)
1557
+ sys.exit(1)
1558
+
1559
+ if args.subtitle:
1560
+ tokens["subtitle"] = args.subtitle
1561
+
1562
+ html = render(tokens)
1563
+
1564
+ try:
1565
+ with open(args.out, "w", encoding="utf-8") as f:
1566
+ f.write(html)
1567
+ except OSError as e:
1568
+ print(json.dumps({"status": "error", "error": str(e)}), file=sys.stderr)
1569
+ sys.exit(3)
1570
+
1571
+ print(json.dumps({
1572
+ "status": "ok",
1573
+ "out": args.out,
1574
+ "pattern": tokens.get("cover_pattern"),
1575
+ }))
1576
+
1577
+
1578
+ if __name__ == "__main__":
1579
+ main()