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()
|