1580 lines
47 KiB
Python
1580 lines
47 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
cover.py — Generate cover.html from tokens.json.
|
||
|
||
Usage:
|
||
python3 cover.py --tokens tokens.json --out cover.html
|
||
|
||
Reads tokens.json["cover_pattern"] and renders the matching HTML cover.
|
||
Cover fonts are loaded live via Google Fonts @import (no local caching).
|
||
Exit codes: 0 success, 1 bad args/missing file, 3 render error
|
||
"""
|
||
|
||
import argparse
|
||
import json
|
||
import sys
|
||
|
||
|
||
# ── Google Fonts loader ────────────────────────────────────────────────────────
|
||
def _gfonts_import(t: dict) -> str:
|
||
"""Return a CSS @import for the document's Google Fonts, if available."""
|
||
url = t.get("gfonts_import", "")
|
||
if url:
|
||
return f"@import url('{url}');"
|
||
return ""
|
||
|
||
|
||
# ── Shared CSS head (required by all patterns) ─────────────────────────────────
|
||
def _base_css(t: dict) -> str:
|
||
"""Critical reset + shared variables. Never remove these rules."""
|
||
return f"""
|
||
{_gfonts_import(t)}
|
||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||
html, body {{
|
||
width: 794px; height: 1123px;
|
||
overflow: hidden;
|
||
background: {t['cover_bg']};
|
||
font-family: '{t['font_body']}', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||
}}
|
||
.page {{
|
||
position: relative;
|
||
width: 794px; height: 1123px;
|
||
background: {t['cover_bg']};
|
||
overflow: hidden;
|
||
}}
|
||
"""
|
||
|
||
|
||
# ── Dot-grid SVG helper ─────────────────────────────────────────────────────────
|
||
def _dot_grid(x0, y0, cols, rows, *, gap, r, color, opacity) -> str:
|
||
"""Render a dot-grid as an absolutely positioned SVG element."""
|
||
dots = []
|
||
for row in range(rows):
|
||
for col in range(cols):
|
||
cx = x0 + col * gap
|
||
cy = y0 + row * gap
|
||
dots.append(f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="{color}"/>')
|
||
return (
|
||
f'<svg style="position:absolute;top:0;left:0;width:794px;height:1123px;'
|
||
f'pointer-events:none;opacity:{opacity}" xmlns="http://www.w3.org/2000/svg">'
|
||
+ "".join(dots) + "</svg>"
|
||
)
|
||
|
||
|
||
# ── Cross-hatch SVG helper ──────────────────────────────────────────────────────
|
||
def _cross_hatch(color, opacity, spacing=32, stroke_w=0.5) -> str:
|
||
lines = []
|
||
for i in range(-20, 60):
|
||
x = i * spacing
|
||
lines.append(f'<line x1="{x}" y1="0" x2="{x + 1200}" y2="1200" stroke="{color}" stroke-width="{stroke_w}"/>')
|
||
return (
|
||
f'<svg style="position:absolute;top:0;left:0;width:794px;height:1123px;'
|
||
f'pointer-events:none;opacity:{opacity};overflow:hidden" xmlns="http://www.w3.org/2000/svg">'
|
||
+ "".join(lines) + "</svg>"
|
||
)
|
||
|
||
|
||
# ── Pattern 1: Full-bleed block ────────────────────────────────────────────────
|
||
def _pattern_fullbleed(t: dict) -> str:
|
||
dot_grid = _dot_grid(
|
||
x0=500, y0=40, cols=10, rows=20, gap=24, r=1.8,
|
||
color=t["accent"], opacity=0.12
|
||
)
|
||
subtitle_block = ""
|
||
if t.get("subtitle"):
|
||
subtitle_block = f"""
|
||
<div style="font-size:14px;color:{t['muted']};letter-spacing:0.01em;
|
||
max-width:480px;line-height:1.5;margin-bottom:40px;">
|
||
{t['subtitle']}
|
||
</div>"""
|
||
|
||
return f"""<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="UTF-8">
|
||
<style>
|
||
{_base_css(t)}
|
||
.label {{
|
||
font-size: 9px; font-weight: 500; letter-spacing: 0.22em;
|
||
color: {t['accent']}; text-transform: uppercase; margin-bottom: 28px;
|
||
}}
|
||
.title {{
|
||
font-family: '{t['font_display']}', 'Times New Roman', Georgia, serif;
|
||
font-weight: 900; font-size: 60px; line-height: 1.0;
|
||
color: {t['text_light']}; letter-spacing: -0.015em;
|
||
margin-bottom: 10px; max-width: 560px;
|
||
word-wrap: break-word;
|
||
}}
|
||
.rule {{
|
||
width: 52%; height: 1.5px;
|
||
background: linear-gradient(to right, {t['accent']}, transparent);
|
||
margin: 24px 0 20px;
|
||
}}
|
||
.content {{
|
||
position: absolute; left: 68px; right: 60px;
|
||
top: 0; bottom: 0;
|
||
display: flex; flex-direction: column; justify-content: center;
|
||
padding-top: 60px;
|
||
}}
|
||
.footer {{
|
||
position: absolute; bottom: 0; left: 0; right: 0;
|
||
height: 70px;
|
||
background: rgba(0,0,0,0.22);
|
||
display: flex; align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 68px;
|
||
}}
|
||
.footer-author {{ font-size: 11px; color: rgba(240,237,230,0.75); letter-spacing:0.04em; }}
|
||
.footer-date {{ font-size: 11px; color: {t['muted']}; letter-spacing: 0.04em; }}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<!-- top-right accent strip -->
|
||
<div style="position:absolute;top:0;right:0;width:35%;height:4px;background:{t['accent']};"></div>
|
||
<!-- left vertical accent bar (gradient fade) -->
|
||
<div style="position:absolute;left:48px;top:18%;width:3px;height:60%;
|
||
background:linear-gradient(to bottom,{t['accent']},transparent);"></div>
|
||
<!-- dot grid background texture -->
|
||
{dot_grid}
|
||
|
||
<div class="content">
|
||
<div class="label">{t.get('doc_type','Document').upper()} · {t.get('date','')}</div>
|
||
<div class="title">{t['title']}</div>
|
||
<div class="rule"></div>
|
||
{subtitle_block}
|
||
</div>
|
||
|
||
<div class="footer">
|
||
<div class="footer-author">{t.get('author','')}</div>
|
||
<div class="footer-date">{t.get('date','')}</div>
|
||
</div>
|
||
</div>
|
||
</body></html>"""
|
||
|
||
|
||
# ── Pattern 2: Split panel ─────────────────────────────────────────────────────
|
||
def _pattern_split(t: dict) -> str:
|
||
dot_grid = _dot_grid(
|
||
x0=360, y0=120, cols=10, rows=18, gap=22, r=2,
|
||
color="#CCCCCC", opacity=0.25
|
||
)
|
||
return f"""<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="UTF-8">
|
||
<style>
|
||
{_base_css(t)}
|
||
.left-panel {{
|
||
position: absolute; top: 0; left: 0;
|
||
width: 330px; height: 1123px;
|
||
background: {t['cover_bg']};
|
||
display: flex; flex-direction: column;
|
||
justify-content: center;
|
||
padding: 0 44px;
|
||
}}
|
||
.right-panel {{
|
||
position: absolute; top: 0; left: 330px;
|
||
width: 464px; height: 1123px;
|
||
background: {t['page_bg']};
|
||
}}
|
||
.divider {{
|
||
position: absolute; top: 0; left: 329px;
|
||
width: 3px; height: 1123px;
|
||
background: {t['accent']};
|
||
}}
|
||
.left-top-bar {{
|
||
position: absolute; top: 0; left: 0;
|
||
width: 330px; height: 4px;
|
||
background: {t['accent']};
|
||
}}
|
||
.title {{
|
||
font-family: '{t['font_display']}', 'Times New Roman', serif;
|
||
font-weight: 900; font-size: 34px; line-height: 1.2;
|
||
color: {t['text_light']}; margin-bottom: 18px;
|
||
word-wrap: break-word;
|
||
}}
|
||
.rule {{
|
||
width: 55%; height: 1.5px;
|
||
background: {t['accent']};
|
||
margin-bottom: 14px;
|
||
}}
|
||
.subtitle {{
|
||
font-size: 12px; color: rgba(220,220,220,0.65);
|
||
line-height: 1.5; margin-bottom: 32px;
|
||
}}
|
||
.author {{
|
||
font-size: 11px; color: {t['text_light']}; margin-bottom: 4px;
|
||
}}
|
||
.date {{ font-size: 10px; color: {t['muted']}; }}
|
||
.right-label {{
|
||
position: absolute; bottom: 60px; right: 44px;
|
||
font-size: 9px; letter-spacing: 0.18em;
|
||
color: {t['muted']}; text-transform: uppercase;
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<div class="left-top-bar"></div>
|
||
<div class="left-panel">
|
||
<div class="title">{t['title']}</div>
|
||
<div class="rule"></div>
|
||
{'<div class="subtitle">' + t['subtitle'] + '</div>' if t.get('subtitle') else ''}
|
||
<div class="author">{t.get('author','')}</div>
|
||
<div class="date">{t.get('date','')}</div>
|
||
</div>
|
||
<div class="right-panel">
|
||
{dot_grid}
|
||
</div>
|
||
<div class="divider"></div>
|
||
<div class="right-label">{t.get('doc_type','').upper()}</div>
|
||
</div>
|
||
</body></html>"""
|
||
|
||
|
||
# ── Pattern 3: Typographic ─────────────────────────────────────────────────────
|
||
def _pattern_typographic(t: dict) -> str:
|
||
words = t['title'].split()
|
||
first = words[0] if words else ""
|
||
rest = " ".join(words[1:]) if len(words) > 1 else ""
|
||
return f"""<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="UTF-8">
|
||
<style>
|
||
{_base_css(t)}
|
||
html, body {{ background: {t['page_bg']}; }}
|
||
.page {{ background: {t['page_bg']}; }}
|
||
.content {{
|
||
position: absolute; left: 60px; top: 0; bottom: 0; right: 60px;
|
||
display: flex; flex-direction: column; justify-content: center;
|
||
}}
|
||
.first-word {{
|
||
font-family: '{t['font_display']}', 'Times New Roman', serif;
|
||
font-weight: 900; font-size: 72px; line-height: 1.0;
|
||
color: {t['accent']}; letter-spacing: -0.02em;
|
||
}}
|
||
.rest-words {{
|
||
font-family: '{t['font_display']}', 'Times New Roman', serif;
|
||
font-weight: 900; font-size: 72px; line-height: 1.0;
|
||
color: {t['dark']}; letter-spacing: -0.02em;
|
||
margin-bottom: 12px;
|
||
}}
|
||
.rule {{
|
||
width: 100%; height: 1.5px;
|
||
background: linear-gradient(to right, {t['accent']}, {t['accent']}40);
|
||
margin: 28px 0 20px;
|
||
}}
|
||
.meta-row {{
|
||
display: flex; justify-content: space-between; align-items: baseline;
|
||
}}
|
||
.author {{ font-size: 13px; color: {t['dark']}; letter-spacing: 0.02em; }}
|
||
.date {{ font-size: 12px; color: {t['muted']}; }}
|
||
.subtitle {{ font-size: 13px; color: {t['muted']}; margin-top: 8px; max-width: 500px; }}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<div class="content">
|
||
<div class="first-word">{first}</div>
|
||
{'<div class="rest-words">' + rest + '</div>' if rest else ''}
|
||
<div class="rule"></div>
|
||
<div class="meta-row">
|
||
<div class="author">{t.get('author','')}</div>
|
||
<div class="date">{t.get('date','')}</div>
|
||
</div>
|
||
{'<div class="subtitle">' + t['subtitle'] + '</div>' if t.get('subtitle') else ''}
|
||
</div>
|
||
</div>
|
||
</body></html>"""
|
||
|
||
|
||
# ── Pattern 4: Dark atmospheric ────────────────────────────────────────────────
|
||
def _pattern_atmospheric(t: dict) -> str:
|
||
dot_grid = _dot_grid(
|
||
x0=60, y0=60, cols=16, rows=22, gap=20, r=1.5,
|
||
color=t["accent"], opacity=0.08
|
||
)
|
||
return f"""<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="UTF-8">
|
||
<style>
|
||
{_base_css(t)}
|
||
.glow {{
|
||
position: absolute;
|
||
top: -100px; right: -80px;
|
||
width: 500px; height: 500px;
|
||
background: radial-gradient(circle, {t['accent']}2E 0%, transparent 68%);
|
||
border-radius: 50%;
|
||
}}
|
||
.glow2 {{
|
||
position: absolute;
|
||
bottom: -40px; left: 10%;
|
||
width: 300px; height: 300px;
|
||
background: radial-gradient(circle, {t['accent']}14 0%, transparent 70%);
|
||
border-radius: 50%;
|
||
}}
|
||
.content {{
|
||
position: absolute; left: 64px; right: 80px;
|
||
top: 0; bottom: 0;
|
||
display: flex; flex-direction: column; justify-content: center;
|
||
}}
|
||
.label {{
|
||
font-size: 9px; letter-spacing: 0.22em;
|
||
color: {t['accent']}; text-transform: uppercase; margin-bottom: 32px;
|
||
}}
|
||
.title {{
|
||
font-family: '{t['font_display']}', 'Times New Roman', serif;
|
||
font-weight: 900; font-size: 50px; line-height: 1.05;
|
||
color: {t['text_light']}; max-width: 520px;
|
||
word-wrap: break-word; margin-bottom: 12px;
|
||
}}
|
||
.rule {{ width: 48px; height: 2px; background: {t['accent']}; margin: 24px 0 20px; }}
|
||
.subtitle {{
|
||
font-size: 13px; color: {t['muted']}; line-height: 1.6;
|
||
max-width: 400px; margin-bottom: 40px;
|
||
}}
|
||
.footer {{
|
||
position: absolute; bottom: 0; left: 0; right: 0; height: 64px;
|
||
border-top: 1px solid rgba(255,255,255,0.06);
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 0 64px;
|
||
}}
|
||
.footer-l {{ font-size: 10.5px; color: rgba(240,237,230,0.6); }}
|
||
.footer-r {{ font-size: 10.5px; color: {t['muted']}; }}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<div class="glow"></div>
|
||
<div class="glow2"></div>
|
||
{dot_grid}
|
||
<div style="position:absolute;top:0;right:0;width:30%;height:3px;background:{t['accent']};"></div>
|
||
<div class="content">
|
||
<div class="label">{t.get('doc_type','').upper()} · {t.get('date','')}</div>
|
||
<div class="title">{t['title']}</div>
|
||
<div class="rule"></div>
|
||
{'<div class="subtitle">' + t['subtitle'] + '</div>' if t.get('subtitle') else ''}
|
||
</div>
|
||
<div class="footer">
|
||
<div class="footer-l">{t.get('author','')}</div>
|
||
<div class="footer-r">{t.get('date','')}</div>
|
||
</div>
|
||
</div>
|
||
</body></html>"""
|
||
|
||
|
||
# ── Pattern 5: Minimal — thick left bar, generous whitespace ───────────────────
|
||
def _pattern_minimal(t: dict) -> str:
|
||
"""
|
||
Ultra-restrained: white background, 8px left accent bar, oversized light-weight
|
||
title, nothing else but a hairline rule and minimal metadata. The bar is the only
|
||
color on the page — everything else is black on white.
|
||
"""
|
||
# Pick text color for page (minimal uses page_bg which is near-white)
|
||
text_dark = t.get("dark", "#111111")
|
||
muted = t.get("muted", "#999999")
|
||
accent = t["accent"]
|
||
|
||
subtitle_block = ""
|
||
if t.get("subtitle"):
|
||
subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
|
||
|
||
return f"""<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="UTF-8">
|
||
<style>
|
||
{_base_css(t)}
|
||
html, body {{ background: {t['page_bg']}; }}
|
||
.page {{ background: {t['page_bg']}; }}
|
||
|
||
/* Left accent bar — the only color element */
|
||
.bar {{
|
||
position: absolute;
|
||
top: 0; left: 0;
|
||
width: 8px; height: 1123px;
|
||
background: {accent};
|
||
}}
|
||
|
||
/* Main content column — offset from bar */
|
||
.content {{
|
||
position: absolute;
|
||
left: 64px; right: 64px;
|
||
top: 0; bottom: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
padding-bottom: 40px;
|
||
}}
|
||
|
||
.eyebrow {{
|
||
font-size: 9px;
|
||
font-weight: 500;
|
||
letter-spacing: 0.28em;
|
||
text-transform: uppercase;
|
||
color: {accent};
|
||
margin-bottom: 36px;
|
||
}}
|
||
|
||
.title {{
|
||
font-family: '{t['font_display']}', Georgia, 'Times New Roman', serif;
|
||
font-weight: 300;
|
||
font-size: 72px;
|
||
line-height: 1.0;
|
||
color: {text_dark};
|
||
letter-spacing: -0.02em;
|
||
max-width: 580px;
|
||
word-wrap: break-word;
|
||
margin-bottom: 0;
|
||
}}
|
||
|
||
.rule {{
|
||
width: 56px;
|
||
height: 1px;
|
||
background: {text_dark};
|
||
margin: 36px 0 24px;
|
||
opacity: 0.2;
|
||
}}
|
||
|
||
.subtitle {{
|
||
font-size: 13px;
|
||
font-weight: 300;
|
||
color: {muted};
|
||
line-height: 1.7;
|
||
max-width: 460px;
|
||
margin-bottom: 28px;
|
||
}}
|
||
|
||
.meta {{
|
||
font-size: 10px;
|
||
letter-spacing: 0.06em;
|
||
color: {muted};
|
||
margin-top: 4px;
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<div class="bar"></div>
|
||
<div class="content">
|
||
<div class="eyebrow">{t.get('doc_type','').upper()}</div>
|
||
<div class="title">{t['title']}</div>
|
||
<div class="rule"></div>
|
||
{subtitle_block}
|
||
<div class="meta">{t.get('author','')}{(' · ' + t.get('date','')) if t.get('date') else ''}</div>
|
||
</div>
|
||
</div>
|
||
</body></html>"""
|
||
|
||
|
||
# ── Pattern 6: Stripe — bold horizontal bands ──────────────────────────────────
|
||
def _pattern_stripe(t: dict) -> str:
|
||
"""
|
||
Page divided into three bold horizontal bands:
|
||
- Top band (accent, ~18%): document type label
|
||
- Middle band (dark, ~52%): large title in white
|
||
- Bottom band (page bg, ~30%): author / date / subtitle
|
||
Hard geometry, no gradients, no textures. Newspaper / brand poster aesthetic.
|
||
"""
|
||
top_h = 200 # accent band
|
||
mid_h = 580 # dark band
|
||
bot_y = top_h + mid_h # 780
|
||
|
||
accent = t["accent"]
|
||
dark = t.get("cover_bg", "#1A1A2E")
|
||
light = t.get("page_bg", "#FAFAF8")
|
||
text_l = t.get("text_light", "#FFFFFF")
|
||
muted = t.get("muted", "#888888")
|
||
|
||
subtitle_block = ""
|
||
if t.get("subtitle"):
|
||
subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
|
||
|
||
return f"""<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="UTF-8">
|
||
<style>
|
||
{_base_css(t)}
|
||
html, body {{ background: {light}; }}
|
||
.page {{ background: {light}; }}
|
||
|
||
/* Three bands */
|
||
.band-top {{
|
||
position: absolute; top: 0; left: 0;
|
||
width: 794px; height: {top_h}px;
|
||
background: {accent};
|
||
display: flex; align-items: flex-end;
|
||
padding: 0 64px 24px;
|
||
}}
|
||
.band-mid {{
|
||
position: absolute; top: {top_h}px; left: 0;
|
||
width: 794px; height: {mid_h}px;
|
||
background: {dark};
|
||
display: flex; flex-direction: column; justify-content: center;
|
||
padding: 0 64px;
|
||
}}
|
||
.band-bot {{
|
||
position: absolute; top: {bot_y}px; left: 0;
|
||
width: 794px; height: {1123 - bot_y}px;
|
||
background: {light};
|
||
display: flex; flex-direction: column; justify-content: center;
|
||
padding: 0 64px;
|
||
}}
|
||
|
||
/* Top band — doc type in large caps */
|
||
.eyebrow {{
|
||
font-family: '{t['font_display']}', sans-serif;
|
||
font-size: 11px; font-weight: 700;
|
||
letter-spacing: 0.32em; text-transform: uppercase;
|
||
color: {dark}; opacity: 0.85;
|
||
}}
|
||
|
||
/* Mid band — title */
|
||
.title {{
|
||
font-family: '{t['font_display']}', 'Times New Roman', Georgia, serif;
|
||
font-weight: 900;
|
||
font-size: 62px;
|
||
line-height: 0.97;
|
||
color: {text_l};
|
||
letter-spacing: -0.02em;
|
||
max-width: 620px;
|
||
word-wrap: break-word;
|
||
}}
|
||
|
||
/* Thin horizontal separator between mid and bot */
|
||
.sep {{
|
||
position: absolute; top: {bot_y}px; left: 0;
|
||
width: 794px; height: 2px;
|
||
background: {accent};
|
||
}}
|
||
|
||
/* Bottom band */
|
||
.author {{
|
||
font-size: 13px; font-weight: 500;
|
||
color: {t.get('dark','#111')}; margin-bottom: 4px;
|
||
}}
|
||
.date {{ font-size: 11px; color: {muted}; margin-bottom: 12px; }}
|
||
.subtitle {{
|
||
font-size: 12px; color: {muted}; line-height: 1.6;
|
||
max-width: 540px;
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<div class="band-top">
|
||
<div class="eyebrow">{t.get('doc_type','').upper()}</div>
|
||
</div>
|
||
<div class="band-mid">
|
||
<div class="title">{t['title']}</div>
|
||
</div>
|
||
<div class="sep"></div>
|
||
<div class="band-bot">
|
||
<div class="author">{t.get('author','')}</div>
|
||
<div class="date">{t.get('date','')}</div>
|
||
{subtitle_block}
|
||
</div>
|
||
</div>
|
||
</body></html>"""
|
||
|
||
|
||
# ── Pattern 7: Diagonal — angled color split ───────────────────────────────────
|
||
def _pattern_diagonal(t: dict) -> str:
|
||
"""
|
||
SVG polygon cuts the page diagonally: upper-left in dark cover color,
|
||
lower-right in light page bg. Title sits on the dark area, metadata on light.
|
||
One angled edge — no gradients, no curves.
|
||
"""
|
||
dark_bg = t.get("cover_bg", "#1B2A4A")
|
||
light_bg = t.get("page_bg", "#FAFCFF")
|
||
accent = t["accent"]
|
||
text_l = t.get("text_light", "#F8FAFF")
|
||
text_d = t.get("dark", "#0F1A2E")
|
||
muted = t.get("muted", "#7A8A99")
|
||
|
||
# Polygon: full upper-left to ~60% down on right side
|
||
# Points: top-left, top-right, (794, 620), (0, 820)
|
||
poly = "0,0 794,0 794,620 0,820"
|
||
|
||
subtitle_block = ""
|
||
if t.get("subtitle"):
|
||
subtitle_block = f'<div class="subtitle-lt">{t["subtitle"]}</div>'
|
||
|
||
return f"""<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="UTF-8">
|
||
<style>
|
||
{_base_css(t)}
|
||
html, body {{ background: {light_bg}; }}
|
||
.page {{ background: {light_bg}; overflow: hidden; }}
|
||
|
||
/* Title block — upper dark area */
|
||
.content-dark {{
|
||
position: absolute;
|
||
left: 64px; right: 64px;
|
||
top: 180px;
|
||
z-index: 2;
|
||
}}
|
||
.eyebrow {{
|
||
font-size: 9px; font-weight: 500;
|
||
letter-spacing: 0.26em; text-transform: uppercase;
|
||
color: {accent}; margin-bottom: 28px;
|
||
}}
|
||
.title {{
|
||
font-family: '{t['font_display']}', 'Helvetica Neue', sans-serif;
|
||
font-weight: 900;
|
||
font-size: 58px;
|
||
line-height: 1.0;
|
||
color: {text_l};
|
||
letter-spacing: -0.018em;
|
||
max-width: 560px;
|
||
word-wrap: break-word;
|
||
margin-bottom: 16px;
|
||
}}
|
||
.rule-accent {{
|
||
width: 52px; height: 3px;
|
||
background: {accent};
|
||
margin-top: 28px;
|
||
}}
|
||
|
||
/* Metadata — lower light area */
|
||
.content-light {{
|
||
position: absolute;
|
||
left: 64px; right: 64px;
|
||
bottom: 80px;
|
||
z-index: 2;
|
||
}}
|
||
.author {{
|
||
font-size: 12px; font-weight: 500;
|
||
color: {text_d}; margin-bottom: 4px;
|
||
}}
|
||
.date {{ font-size: 11px; color: {muted}; margin-bottom: 12px; }}
|
||
.subtitle-lt {{
|
||
font-size: 12px; color: {muted}; line-height: 1.6;
|
||
max-width: 480px;
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<!-- Diagonal dark polygon -->
|
||
<svg style="position:absolute;top:0;left:0;width:794px;height:1123px;z-index:1"
|
||
xmlns="http://www.w3.org/2000/svg">
|
||
<polygon points="{poly}" fill="{dark_bg}"/>
|
||
<!-- Accent edge line along the diagonal -->
|
||
<line x1="0" y1="820" x2="794" y2="620"
|
||
stroke="{accent}" stroke-width="2.5"/>
|
||
</svg>
|
||
|
||
<div class="content-dark">
|
||
<div class="eyebrow">{t.get('doc_type','').upper()} · {t.get('date','')}</div>
|
||
<div class="title">{t['title']}</div>
|
||
<div class="rule-accent"></div>
|
||
</div>
|
||
|
||
<div class="content-light">
|
||
<div class="author">{t.get('author','')}</div>
|
||
{subtitle_block}
|
||
</div>
|
||
</div>
|
||
</body></html>"""
|
||
|
||
|
||
# ── Pattern 8: Frame — elegant inset border ────────────────────────────────────
|
||
def _pattern_frame(t: dict) -> str:
|
||
"""
|
||
Classic formal layout: outer thin border line inset ~28px from page edges,
|
||
inner accent strip at top and bottom inside the frame.
|
||
Title centered in the frame space, classical serif typography.
|
||
Used for: academic papers, formal reports, legal docs, annual reports.
|
||
"""
|
||
bg = t.get("cover_bg", "#FAF8F3")
|
||
accent = t["accent"]
|
||
dark = t.get("dark", "#2A1A0A")
|
||
muted = t.get("muted", "#9A8A78")
|
||
|
||
pad = 28 # frame inset from page edge
|
||
inner_w = 794 - 2 * pad
|
||
inner_h = 1123 - 2 * pad
|
||
|
||
subtitle_block = ""
|
||
if t.get("subtitle"):
|
||
subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
|
||
|
||
return f"""<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="UTF-8">
|
||
<style>
|
||
{_base_css(t)}
|
||
html, body {{ background: {bg}; }}
|
||
.page {{ background: {bg}; }}
|
||
|
||
/* Outer frame rectangle */
|
||
.frame {{
|
||
position: absolute;
|
||
top: {pad}px; left: {pad}px;
|
||
width: {inner_w}px; height: {inner_h}px;
|
||
border: 1.2px solid {dark};
|
||
opacity: 0.35;
|
||
}}
|
||
|
||
/* Accent strips inside top and bottom of frame */
|
||
.frame-top-accent {{
|
||
position: absolute;
|
||
top: {pad + 10}px; left: {pad + 10}px;
|
||
width: {inner_w - 20}px; height: 3px;
|
||
background: {accent};
|
||
}}
|
||
.frame-bot-accent {{
|
||
position: absolute;
|
||
bottom: {pad + 10}px; left: {pad + 10}px;
|
||
width: {inner_w - 20}px; height: 3px;
|
||
background: {accent};
|
||
}}
|
||
|
||
/* Corner ornament squares */
|
||
.corner {{
|
||
position: absolute;
|
||
width: 8px; height: 8px;
|
||
background: {accent};
|
||
opacity: 0.6;
|
||
}}
|
||
.tl {{ top: {pad - 4}px; left: {pad - 4}px; }}
|
||
.tr {{ top: {pad - 4}px; right: {pad - 4}px; }}
|
||
.bl {{ bottom: {pad - 4}px; left: {pad - 4}px; }}
|
||
.br {{ bottom: {pad - 4}px; right: {pad - 4}px; }}
|
||
|
||
/* Main content centered in frame */
|
||
.content {{
|
||
position: absolute;
|
||
left: {pad + 56}px; right: {pad + 56}px;
|
||
top: 0; bottom: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
}}
|
||
|
||
.eyebrow {{
|
||
font-size: 8.5px;
|
||
font-weight: 500;
|
||
letter-spacing: 0.30em;
|
||
text-transform: uppercase;
|
||
color: {accent};
|
||
margin-bottom: 44px;
|
||
}}
|
||
|
||
.rule-top {{
|
||
width: 60px; height: 1px;
|
||
background: {dark};
|
||
opacity: 0.3;
|
||
margin-bottom: 28px;
|
||
}}
|
||
|
||
.title {{
|
||
font-family: '{t['font_display']}', Georgia, 'Times New Roman', serif;
|
||
font-weight: 400;
|
||
font-size: 44px;
|
||
line-height: 1.25;
|
||
color: {dark};
|
||
letter-spacing: 0.01em;
|
||
max-width: 540px;
|
||
word-wrap: break-word;
|
||
margin-bottom: 0;
|
||
}}
|
||
|
||
.rule-mid {{
|
||
width: 40px; height: 1.5px;
|
||
background: {accent};
|
||
margin: 28px 0 20px;
|
||
}}
|
||
|
||
.subtitle {{
|
||
font-size: 13px;
|
||
font-weight: 300;
|
||
font-style: italic;
|
||
color: {muted};
|
||
line-height: 1.6;
|
||
max-width: 400px;
|
||
margin-bottom: 20px;
|
||
}}
|
||
|
||
.meta {{
|
||
font-size: 10px;
|
||
letter-spacing: 0.08em;
|
||
color: {muted};
|
||
margin-top: 8px;
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<div class="frame"></div>
|
||
<div class="frame-top-accent"></div>
|
||
<div class="frame-bot-accent"></div>
|
||
<div class="corner tl"></div>
|
||
<div class="corner tr"></div>
|
||
<div class="corner bl"></div>
|
||
<div class="corner br"></div>
|
||
|
||
<div class="content">
|
||
<div class="eyebrow">{t.get('doc_type','').upper()}</div>
|
||
<div class="rule-top"></div>
|
||
<div class="title">{t['title']}</div>
|
||
<div class="rule-mid"></div>
|
||
{subtitle_block}
|
||
<div class="meta">{t.get('author','')}{(' · ' + t.get('date','')) if t.get('date') else ''}</div>
|
||
</div>
|
||
</div>
|
||
</body></html>"""
|
||
|
||
|
||
# ── Pattern 9: Editorial — oversized ghost letter + bold type ──────────────────
|
||
def _pattern_editorial(t: dict) -> str:
|
||
"""
|
||
Magazine / editorial feel:
|
||
- Oversized first-letter of title as a ghost background element (8–12% opacity)
|
||
- Bold category label at top in accent
|
||
- Title in very large condensed weight, flush-left
|
||
- Thin full-width rule separating title from metadata
|
||
- Author / date bottom-left, page type bottom-right
|
||
Designed for editorial reports, annual reviews, magazine-format content.
|
||
"""
|
||
bg = t.get("cover_bg", "#FFFFFF")
|
||
accent = t["accent"]
|
||
dark = t.get("dark", "#0A0A0A")
|
||
muted = t.get("muted", "#777777")
|
||
text_l = t.get("text_light", "#FFFFFF")
|
||
|
||
# Ghost letter — first character of title
|
||
ghost = t['title'][0].upper() if t['title'] else "A"
|
||
|
||
subtitle_block = ""
|
||
if t.get("subtitle"):
|
||
subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
|
||
|
||
# Determine if background is dark (use light text) or light (use dark text)
|
||
is_dark_bg = (
|
||
bg.startswith("#0") or bg.startswith("#1") or bg.startswith("#2")
|
||
)
|
||
title_color = text_l if is_dark_bg else dark # noqa: F841
|
||
body_color = text_l if is_dark_bg else dark
|
||
|
||
return f"""<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="UTF-8">
|
||
<style>
|
||
{_base_css(t)}
|
||
html, body {{ background: {bg}; }}
|
||
.page {{ background: {bg}; }}
|
||
|
||
/* Ghost letter — background texture */
|
||
.ghost {{
|
||
position: absolute;
|
||
right: -60px; top: -40px;
|
||
font-family: '{t['font_display']}', 'Arial Black', sans-serif;
|
||
font-weight: 900;
|
||
font-size: 680px;
|
||
line-height: 1;
|
||
color: {dark};
|
||
opacity: 0.055;
|
||
user-select: none;
|
||
letter-spacing: -0.05em;
|
||
}}
|
||
|
||
/* Top bar: accent stripe */
|
||
.topbar {{
|
||
position: absolute;
|
||
top: 0; left: 0; right: 0;
|
||
height: 5px;
|
||
background: {accent};
|
||
}}
|
||
|
||
/* Category label */
|
||
.category {{
|
||
position: absolute;
|
||
top: 40px; left: 60px;
|
||
font-size: 9px; font-weight: 700;
|
||
letter-spacing: 0.30em; text-transform: uppercase;
|
||
color: {accent};
|
||
}}
|
||
|
||
/* Main title block */
|
||
.content {{
|
||
position: absolute;
|
||
left: 60px; right: 60px;
|
||
top: 0; bottom: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
padding-bottom: 80px;
|
||
}}
|
||
|
||
.title {{
|
||
font-family: '{t['font_display']}', 'Arial Black', Impact, sans-serif;
|
||
font-weight: 900;
|
||
font-size: 80px;
|
||
line-height: 0.92;
|
||
color: {body_color};
|
||
letter-spacing: -0.03em;
|
||
max-width: 620px;
|
||
word-wrap: break-word;
|
||
text-transform: uppercase;
|
||
}}
|
||
|
||
.subtitle {{
|
||
font-size: 14px;
|
||
font-weight: 400;
|
||
color: {muted};
|
||
line-height: 1.6;
|
||
max-width: 500px;
|
||
margin-top: 20px;
|
||
}}
|
||
|
||
/* Full-width rule above footer */
|
||
.footer-rule {{
|
||
position: absolute;
|
||
bottom: 80px; left: 60px; right: 60px;
|
||
height: 1px;
|
||
background: {body_color};
|
||
opacity: 0.15;
|
||
}}
|
||
|
||
/* Footer row */
|
||
.footer {{
|
||
position: absolute;
|
||
bottom: 44px; left: 60px; right: 60px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: baseline;
|
||
}}
|
||
.footer-author {{ font-size: 11px; color: {muted}; letter-spacing: 0.04em; }}
|
||
.footer-date {{ font-size: 10px; color: {muted}; letter-spacing: 0.04em; }}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<div class="ghost">{ghost}</div>
|
||
<div class="topbar"></div>
|
||
<div class="category">{t.get('doc_type','').upper()}</div>
|
||
|
||
<div class="content">
|
||
<div class="title">{t['title']}</div>
|
||
{subtitle_block}
|
||
</div>
|
||
|
||
<div class="footer-rule"></div>
|
||
<div class="footer">
|
||
<div class="footer-author">{t.get('author','')}</div>
|
||
<div class="footer-date">{t.get('date','')}</div>
|
||
</div>
|
||
</div>
|
||
</body></html>"""
|
||
|
||
|
||
# ── Pattern 10: Magazine — elegant centered with optional hero image ────────────
|
||
def _pattern_magazine(t: dict) -> str:
|
||
"""
|
||
Upscale centered layout: company name + accent rule at top, large serif title,
|
||
decorative rule, italic subtitle, optional hero image, abstract block, author.
|
||
Used for: annual reports, strategic documents, formal publications.
|
||
"""
|
||
bg = t.get("cover_bg", "#F2F0EC")
|
||
accent = t["accent"]
|
||
dark = t.get("dark", "#0D1A2B")
|
||
muted = t.get("muted", "#888888")
|
||
org = t.get("doc_type", "").upper()
|
||
img_url = t.get("cover_image", "")
|
||
|
||
subtitle_block = ""
|
||
if t.get("subtitle"):
|
||
subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
|
||
|
||
image_block = ""
|
||
if img_url:
|
||
image_block = f"""
|
||
<div style="text-align:center;margin:32px 0 28px;">
|
||
<img src="{img_url}" style="max-width:340px;max-height:220px;
|
||
object-fit:cover;display:inline-block;"/>
|
||
</div>"""
|
||
|
||
abstract_block = ""
|
||
if t.get("abstract"):
|
||
abstract_block = f"""
|
||
<div style="font-size:11px;line-height:1.7;color:{muted};
|
||
text-align:justify;max-width:560px;margin:0 auto 0;">
|
||
<span style="font-weight:700;color:{accent};">Abstract:</span>
|
||
{t['abstract']}
|
||
</div>"""
|
||
|
||
return f"""<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="UTF-8">
|
||
<style>
|
||
{_base_css(t)}
|
||
html, body {{ background: {bg}; }}
|
||
.page {{ background: {bg}; display:flex; flex-direction:column;
|
||
align-items:center; justify-content:center; padding:60px 80px; }}
|
||
|
||
.org-name {{
|
||
font-size: 9px; font-weight: 500; letter-spacing: 0.30em;
|
||
text-transform: uppercase; color: {dark}; text-align:center;
|
||
margin-bottom: 10px;
|
||
}}
|
||
.org-rule {{
|
||
width: 56px; height: 2px; background: {accent};
|
||
margin: 0 auto 52px;
|
||
}}
|
||
.title {{
|
||
font-family: '{t['font_display']}', Georgia, 'Times New Roman', serif;
|
||
font-weight: 700; font-size: 52px; line-height: 1.08;
|
||
color: {dark}; text-align: center; letter-spacing: -0.015em;
|
||
max-width: 560px; word-wrap: break-word; margin-bottom: 18px;
|
||
}}
|
||
.title-rule {{
|
||
width: 44px; height: 2.5px; background: {accent};
|
||
margin: 0 auto 20px;
|
||
}}
|
||
.subtitle {{
|
||
font-family: '{t['font_display']}', Georgia, serif;
|
||
font-style: italic; font-size: 14px; color: {muted};
|
||
text-align: center; line-height: 1.5; max-width: 440px;
|
||
margin: 0 auto;
|
||
}}
|
||
.separator {{
|
||
width: 100%; max-width: 620px; height: 1px;
|
||
background: {dark}; opacity: 0.12;
|
||
margin: 28px auto;
|
||
}}
|
||
.author-name {{
|
||
font-family: '{t['font_display']}', Georgia, serif;
|
||
font-size: 16px; font-weight: 700; color: {accent};
|
||
text-align: center; margin-bottom: 6px;
|
||
}}
|
||
.date-line {{
|
||
font-size: 11px; color: {muted}; text-align: center;
|
||
letter-spacing: 0.03em;
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<div class="org-name">{org}</div>
|
||
<div class="org-rule"></div>
|
||
<div class="title">{t['title']}</div>
|
||
<div class="title-rule"></div>
|
||
{subtitle_block}
|
||
{image_block}
|
||
{abstract_block}
|
||
{'<div class="separator"></div>' if (t.get('abstract') or img_url) else '<div style="margin:28px 0;"></div>'}
|
||
<div class="author-name">{t.get('author','')}</div>
|
||
<div class="date-line">{t.get('date','')}</div>
|
||
</div>
|
||
</body></html>"""
|
||
|
||
|
||
# ── Pattern 11: Darkroom — dark magazine variant ────────────────────────────────
|
||
def _pattern_darkroom(t: dict) -> str:
|
||
"""
|
||
Dark-background centered layout. Same structure as magazine but inverted:
|
||
deep navy page, white/silver text, accent rules in lighter tone.
|
||
Used for: premium reports, tech annual reviews, dark-themed documents.
|
||
"""
|
||
bg = t.get("cover_bg", "#151C27")
|
||
accent = t["accent"]
|
||
text_l = t.get("text_light", "#F0EDE6")
|
||
muted = t.get("muted", "#8A9AB0")
|
||
org = t.get("doc_type", "").upper()
|
||
img_url = t.get("cover_image", "")
|
||
|
||
subtitle_block = ""
|
||
if t.get("subtitle"):
|
||
subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
|
||
|
||
image_block = ""
|
||
if img_url:
|
||
image_block = f"""
|
||
<div style="text-align:center;margin:32px 0 28px;">
|
||
<img src="{img_url}" style="max-width:340px;max-height:220px;
|
||
object-fit:cover;display:inline-block;
|
||
filter:grayscale(20%) brightness(0.9);"/>
|
||
</div>"""
|
||
|
||
abstract_block = ""
|
||
if t.get("abstract"):
|
||
abstract_block = f"""
|
||
<div style="font-size:11px;line-height:1.7;color:{muted};
|
||
text-align:justify;max-width:560px;margin:0 auto 0;">
|
||
<span style="font-weight:700;color:{accent};">Abstract:</span>
|
||
{t['abstract']}
|
||
</div>"""
|
||
|
||
return f"""<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="UTF-8">
|
||
<style>
|
||
{_base_css(t)}
|
||
html, body {{ background: {bg}; }}
|
||
.page {{ background: {bg}; display:flex; flex-direction:column;
|
||
align-items:center; justify-content:center; padding:60px 80px; }}
|
||
|
||
.org-name {{
|
||
font-size: 9px; font-weight: 500; letter-spacing: 0.30em;
|
||
text-transform: uppercase; color: {text_l}; text-align:center;
|
||
opacity: 0.75; margin-bottom: 10px;
|
||
}}
|
||
.org-rule {{
|
||
width: 56px; height: 2px; background: {text_l};
|
||
opacity: 0.35; margin: 0 auto 52px;
|
||
}}
|
||
.title {{
|
||
font-family: '{t['font_display']}', Georgia, 'Times New Roman', serif;
|
||
font-weight: 700; font-size: 52px; line-height: 1.08;
|
||
color: {text_l}; text-align: center; letter-spacing: -0.015em;
|
||
max-width: 560px; word-wrap: break-word; margin-bottom: 18px;
|
||
}}
|
||
.title-rule {{
|
||
width: 44px; height: 2.5px; background: {text_l};
|
||
opacity: 0.35; margin: 0 auto 20px;
|
||
}}
|
||
.subtitle {{
|
||
font-family: '{t['font_display']}', Georgia, serif;
|
||
font-style: italic; font-size: 14px; color: {muted};
|
||
text-align: center; line-height: 1.5; max-width: 440px;
|
||
margin: 0 auto;
|
||
}}
|
||
.separator {{
|
||
width: 100%; max-width: 620px; height: 1px;
|
||
background: {text_l}; opacity: 0.12;
|
||
margin: 28px auto;
|
||
}}
|
||
.author-name {{
|
||
font-family: '{t['font_display']}', Georgia, serif;
|
||
font-size: 16px; font-weight: 700; color: {text_l};
|
||
text-align: center; margin-bottom: 6px;
|
||
}}
|
||
.date-line {{
|
||
font-size: 11px; color: {muted}; text-align: center;
|
||
letter-spacing: 0.03em;
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<div class="org-name">{org}</div>
|
||
<div class="org-rule"></div>
|
||
<div class="title">{t['title']}</div>
|
||
<div class="title-rule"></div>
|
||
{subtitle_block}
|
||
{image_block}
|
||
{abstract_block}
|
||
{'<div class="separator"></div>' if (t.get('abstract') or img_url) else '<div style="margin:28px 0;"></div>'}
|
||
<div class="author-name">{t.get('author','')}</div>
|
||
<div class="date-line">{t.get('date','')}</div>
|
||
</div>
|
||
</body></html>"""
|
||
|
||
|
||
# ── Pattern 12: Terminal — cyber/hacker aesthetic ───────────────────────────────
|
||
def _pattern_terminal(t: dict) -> str:
|
||
"""
|
||
Dark terminal/IDE aesthetic: grid overlay, monospace font, neon accent,
|
||
corner brackets around the title block, status bar at bottom.
|
||
Used for: tech reports, developer docs, security audits, system documentation.
|
||
"""
|
||
bg = t.get("cover_bg", "#0D1117")
|
||
accent = t["accent"]
|
||
text_l = t.get("text_light", "#E6EDF3")
|
||
muted = t.get("muted", "#48897C")
|
||
dark = t.get("dark", "#010409")
|
||
org = t.get("doc_type", "DOCUMENT").upper()
|
||
date_s = t.get("date", "")
|
||
author = t.get("author", "")
|
||
|
||
subtitle_line = ""
|
||
if t.get("subtitle"):
|
||
subtitle_line = f'<div class="subtitle">> {t["subtitle"]}</div>'
|
||
|
||
abstract_block = ""
|
||
if t.get("abstract"):
|
||
abstract_block = f"""
|
||
<div class="abstract-text">{t['abstract']}</div>"""
|
||
|
||
# grid overlay: horizontal + vertical lines
|
||
h_lines = "".join(
|
||
f'<line x1="0" y1="{y}" x2="794" y2="{y}" stroke="{accent}" stroke-width="0.4"/>'
|
||
for y in range(0, 1124, 48)
|
||
)
|
||
v_lines = "".join(
|
||
f'<line x1="{x}" y1="0" x2="{x}" y2="1123" stroke="{accent}" stroke-width="0.4"/>'
|
||
for x in range(0, 795, 48)
|
||
)
|
||
grid_svg = (
|
||
f'<svg style="position:absolute;top:0;left:0;width:794px;height:1123px;'
|
||
f'pointer-events:none;opacity:0.07" xmlns="http://www.w3.org/2000/svg">'
|
||
+ h_lines + v_lines + "</svg>"
|
||
)
|
||
|
||
return f"""<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="UTF-8">
|
||
<style>
|
||
{_base_css(t)}
|
||
html, body {{ background: {bg}; }}
|
||
.page {{ background: {bg}; }}
|
||
|
||
/* Terminal label — top */
|
||
.term-label {{
|
||
position: absolute; top: 44px; left: 56px; right: 56px;
|
||
display: flex; align-items: center; gap: 10px;
|
||
}}
|
||
.dot {{
|
||
width: 8px; height: 8px; border-radius: 50%;
|
||
background: {accent}; flex-shrink: 0;
|
||
}}
|
||
.term-meta {{
|
||
font-family: '{t['font_body']}', 'Courier New', monospace;
|
||
font-size: 10px; color: {accent}; letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
}}
|
||
|
||
/* Title bracket block */
|
||
.bracket-block {{
|
||
position: absolute;
|
||
top: 310px; left: 56px; right: 56px;
|
||
border-left: 2px solid {accent}; border-top: 2px solid {accent};
|
||
padding: 24px 28px 28px;
|
||
box-shadow: inset 0 0 0 0;
|
||
}}
|
||
.bracket-block::after {{
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 0; right: 0;
|
||
width: 32px; height: 2px;
|
||
background: {accent};
|
||
}}
|
||
.bracket-block::before {{
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 0; right: 0;
|
||
width: 2px; height: 32px;
|
||
background: {accent};
|
||
}}
|
||
|
||
.title {{
|
||
font-family: '{t['font_display']}', 'Courier New', monospace;
|
||
font-weight: 700; font-size: 46px; line-height: 1.05;
|
||
color: {text_l}; letter-spacing: 0.01em;
|
||
text-transform: uppercase;
|
||
word-wrap: break-word; margin-bottom: 16px;
|
||
}}
|
||
.subtitle {{
|
||
font-family: '{t['font_body']}', 'Courier New', monospace;
|
||
font-size: 13px; color: {accent};
|
||
line-height: 1.5; letter-spacing: 0.02em;
|
||
margin-top: 8px;
|
||
}}
|
||
|
||
/* Content block below brackets */
|
||
.content-lower {{
|
||
position: absolute;
|
||
top: 640px; left: 56px; right: 56px;
|
||
display: flex; gap: 40px; align-items: flex-start;
|
||
}}
|
||
.abstract-text {{
|
||
font-family: '{t['font_body']}', 'Courier New', monospace;
|
||
font-size: 10.5px; line-height: 1.8; color: {muted};
|
||
flex: 1;
|
||
}}
|
||
.author-block {{
|
||
text-align: right; flex-shrink: 0; min-width: 160px;
|
||
}}
|
||
.author-label {{
|
||
font-family: '{t['font_body']}', monospace;
|
||
font-size: 8px; letter-spacing: 0.20em; color: {muted};
|
||
text-transform: uppercase; margin-bottom: 6px;
|
||
}}
|
||
.author-name {{
|
||
font-family: '{t['font_body']}', monospace;
|
||
font-size: 14px; font-weight: 700; color: {text_l};
|
||
}}
|
||
.author-org {{
|
||
font-family: '{t['font_body']}', monospace;
|
||
font-size: 10px; color: {accent}; margin-top: 4px;
|
||
}}
|
||
|
||
/* Bottom status bar */
|
||
.statusbar {{
|
||
position: absolute; bottom: 0; left: 0; right: 0;
|
||
height: 36px; background: {accent}; opacity: 0.12;
|
||
}}
|
||
.statusbar-text {{
|
||
position: absolute; bottom: 0; left: 0; right: 0;
|
||
height: 36px; display: flex; align-items: center;
|
||
justify-content: space-between; padding: 0 56px;
|
||
}}
|
||
.sb-item {{
|
||
font-family: '{t['font_body']}', monospace;
|
||
font-size: 9px; color: {muted}; letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
{grid_svg}
|
||
|
||
<div class="term-label">
|
||
<div class="dot"></div>
|
||
<div class="term-meta">SYSTEM_REPORT // {date_s}</div>
|
||
</div>
|
||
|
||
<div class="bracket-block">
|
||
<div class="title">{t['title']}</div>
|
||
{subtitle_line}
|
||
</div>
|
||
|
||
<div class="content-lower">
|
||
{abstract_block}
|
||
<div class="author-block">
|
||
<div class="author-label">AUTHOR_ID</div>
|
||
<div class="author-name">{author}</div>
|
||
<div class="author-org">{org}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="statusbar"></div>
|
||
<div class="statusbar-text">
|
||
<div class="sb-item">Ln 1, Col 1</div>
|
||
<div class="sb-item">UTF-8</div>
|
||
<div class="sb-item">GENERATED_BY_COVERGENIUS</div>
|
||
</div>
|
||
</div>
|
||
</body></html>"""
|
||
|
||
|
||
# ── Pattern 13: Poster — bold sidebar + oversized type ─────────────────────────
|
||
def _pattern_poster(t: dict) -> str:
|
||
"""
|
||
Bold minimalist poster: thick vertical sidebar on the left, oversized all-caps
|
||
title, typewriter-style metadata. Optional thumbnail on the right side.
|
||
Used for: portfolios, creative reports, journalism, photography books.
|
||
"""
|
||
bg = t.get("cover_bg", "#FFFFFF")
|
||
accent = t["accent"] # typically black or strong dark
|
||
dark = t.get("dark", "#0A0A0A")
|
||
muted = t.get("muted", "#888888")
|
||
text_l = t.get("text_light", "#FFFFFF")
|
||
img_url = t.get("cover_image", "")
|
||
|
||
sidebar_w = 52
|
||
|
||
subtitle_block = ""
|
||
if t.get("subtitle"):
|
||
subtitle_block = f'<div class="subtitle">{t["subtitle"]}</div>'
|
||
|
||
image_block = ""
|
||
if img_url:
|
||
image_block = f"""
|
||
<img src="{img_url}" style="
|
||
width:260px;height:340px;object-fit:cover;
|
||
display:block;margin-top:32px;
|
||
filter:grayscale(100%) contrast(1.1);"/>"""
|
||
|
||
meta_lines = []
|
||
if t.get("author"):
|
||
meta_lines.append(f'<div class="meta-line">{t["author"]}</div>')
|
||
if t.get("subtitle"):
|
||
meta_lines.append(f'<div class="meta-line meta-role">{t["subtitle"]}</div>')
|
||
if t.get("date"):
|
||
meta_lines.append(f'<div class="meta-line meta-date">{t["date"]}</div>')
|
||
meta_block = "\n".join(meta_lines)
|
||
|
||
return f"""<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="UTF-8">
|
||
<style>
|
||
{_base_css(t)}
|
||
html, body {{ background: {bg}; }}
|
||
.page {{ background: {bg}; }}
|
||
|
||
/* Left sidebar — the dominant color element */
|
||
.sidebar {{
|
||
position: absolute;
|
||
top: 0; left: 0;
|
||
width: {sidebar_w}px; height: 1123px;
|
||
background: {accent};
|
||
}}
|
||
|
||
/* Main content — offset from sidebar */
|
||
.content {{
|
||
position: absolute;
|
||
left: {sidebar_w + 52}px; right: 52px;
|
||
top: 100px; bottom: 80px;
|
||
}}
|
||
|
||
/* Oversized display title */
|
||
.title {{
|
||
font-family: '{t['font_display']}', 'Arial Black', Impact, sans-serif;
|
||
font-weight: 900;
|
||
font-size: 96px;
|
||
line-height: 0.92;
|
||
color: {dark};
|
||
letter-spacing: -0.03em;
|
||
text-transform: uppercase;
|
||
max-width: 620px;
|
||
word-wrap: break-word;
|
||
margin-bottom: 22px;
|
||
}}
|
||
|
||
.subtitle {{
|
||
font-family: '{t['font_body']}', 'Courier New', monospace;
|
||
font-size: 12px;
|
||
color: {muted};
|
||
letter-spacing: 0.05em;
|
||
margin-bottom: 0;
|
||
}}
|
||
|
||
/* Thin rule under title area */
|
||
.rule {{
|
||
width: 64px; height: 2px;
|
||
background: {dark};
|
||
margin: 24px 0 28px;
|
||
}}
|
||
|
||
/* Author / meta in typewriter font */
|
||
.meta-group {{
|
||
margin-top: 32px;
|
||
}}
|
||
.meta-line {{
|
||
font-family: '{t['font_body']}', 'Courier New', monospace;
|
||
font-size: 12px; color: {dark};
|
||
line-height: 1.8; letter-spacing: 0.02em;
|
||
}}
|
||
.meta-role {{
|
||
font-family: '{t['font_body']}', 'Courier New', monospace;
|
||
color: {muted};
|
||
}}
|
||
.meta-date {{
|
||
font-family: '{t['font_body']}', 'Courier New', monospace;
|
||
font-size: 12px; color: {dark};
|
||
margin-top: 8px;
|
||
}}
|
||
|
||
/* Right-side content area for thumbnail */
|
||
.right-col {{
|
||
position: absolute;
|
||
right: 52px;
|
||
top: 380px; bottom: 80px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
}}
|
||
|
||
/* Small accent square icon */
|
||
.icon-block {{
|
||
width: 64px; height: 64px;
|
||
background: {accent};
|
||
margin-top: 28px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
flex-shrink: 0;
|
||
}}
|
||
.icon-lines {{
|
||
display: flex; flex-direction: column; gap: 6px;
|
||
}}
|
||
.icon-line {{
|
||
height: 2px; background: {text_l};
|
||
}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<div class="sidebar"></div>
|
||
|
||
<div class="content">
|
||
<div class="title">{t['title']}</div>
|
||
{subtitle_block}
|
||
<div class="rule"></div>
|
||
<div class="meta-group">{meta_block}</div>
|
||
</div>
|
||
|
||
<div class="right-col">
|
||
{image_block}
|
||
<div class="icon-block">
|
||
<div class="icon-lines">
|
||
<div class="icon-line" style="width:32px;"></div>
|
||
<div class="icon-line" style="width:24px;"></div>
|
||
<div class="icon-line" style="width:28px;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</body></html>"""
|
||
|
||
|
||
# ── Dispatch ───────────────────────────────────────────────────────────────────
|
||
PATTERNS = {
|
||
"fullbleed": _pattern_fullbleed,
|
||
"split": _pattern_split,
|
||
"typographic": _pattern_typographic,
|
||
"atmospheric": _pattern_atmospheric,
|
||
"minimal": _pattern_minimal,
|
||
"stripe": _pattern_stripe,
|
||
"diagonal": _pattern_diagonal,
|
||
"frame": _pattern_frame,
|
||
"editorial": _pattern_editorial,
|
||
"magazine": _pattern_magazine,
|
||
"darkroom": _pattern_darkroom,
|
||
"terminal": _pattern_terminal,
|
||
"poster": _pattern_poster,
|
||
}
|
||
|
||
|
||
def render(tokens: dict) -> str:
|
||
"""Dispatch to the cover pattern function and return the HTML string."""
|
||
pattern = tokens.get("cover_pattern", "fullbleed")
|
||
fn = PATTERNS.get(pattern, _pattern_fullbleed)
|
||
return fn(tokens)
|
||
|
||
|
||
# ── CLI ───────────────────────────────────────────────────────────────────────
|
||
def main():
|
||
"""CLI entry point."""
|
||
parser = argparse.ArgumentParser(description="Render cover HTML from tokens.json")
|
||
parser.add_argument("--tokens", default="tokens.json")
|
||
parser.add_argument("--out", default="cover.html")
|
||
parser.add_argument("--subtitle", default="", help="Optional subtitle override")
|
||
args = parser.parse_args()
|
||
|
||
try:
|
||
with open(args.tokens, encoding="utf-8") as f:
|
||
tokens = json.load(f)
|
||
except FileNotFoundError:
|
||
print(json.dumps({"status": "error", "error": f"tokens file not found: {args.tokens}"}),
|
||
file=sys.stderr)
|
||
sys.exit(1)
|
||
except json.JSONDecodeError as e:
|
||
print(json.dumps({"status": "error", "error": f"invalid JSON: {e}"}), file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
if args.subtitle:
|
||
tokens["subtitle"] = args.subtitle
|
||
|
||
html = render(tokens)
|
||
|
||
try:
|
||
with open(args.out, "w", encoding="utf-8") as f:
|
||
f.write(html)
|
||
except OSError as e:
|
||
print(json.dumps({"status": "error", "error": str(e)}), file=sys.stderr)
|
||
sys.exit(3)
|
||
|
||
print(json.dumps({
|
||
"status": "ok",
|
||
"out": args.out,
|
||
"pattern": tokens.get("cover_pattern"),
|
||
}))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|