| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579 |
- #!/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()
|