Files
aibook-fun/ks-research/story-images/src/build.py
T

629 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Build and render the We Own AI Kickstarter story images.
Usage: python build.py (render everything into ../)
Owner photos: drop files into src/assets/ using the names in SLOT_FILES (jpg/png/webp) and re-run;
images whose photo slots are still empty get a -PLACEHOLDER suffix.
Sources of truth: ks-research/kickstarter-campaign-guide.md (v2), Story section and Part 3.1."""
import asyncio, json, math, pathlib, re, sys
from icons import ICONS, ILLUS, MARK, icon
HERE = pathlib.Path(__file__).parent
OUT = HERE.parent
PAGES = HERE / 'pages'
PAGES.mkdir(exist_ok=True)
MAP = json.loads((HERE / 'assets-gen' / 'us-map.json').read_text())
MAP_SVG = (HERE / 'assets-gen' / 'us-map.svg').read_text()
SCREENS = json.loads((HERE / 'assets-gen' / 'screens.json').read_text())
TITLE = 'We Own AI — The New American Dream'
HEAD = """<!doctype html><html lang="en"><head><meta charset="utf-8">
{extra_css}<link rel="stylesheet" href="../styles.css"><style>{css}</style></head><body>{body}
<script>
window.__ready = (async () => {{
let empty = 0;
for (const s of document.querySelectorAll('.slot[data-src]')) {{
let ok = false;
for (const ext of ['jpg', 'jpeg', 'png', 'webp']) {{
const url = s.dataset.src + '.' + ext;
ok = await new Promise(r => {{ const i = new Image(); i.onload = () => r(true); i.onerror = () => r(false); i.src = url; }});
if (ok) {{ s.style.backgroundImage = 'url("' + url + '")'; s.classList.add('filled'); break; }}
}}
if (!ok) empty++;
}}
await document.fonts.ready;
for (const el of document.querySelectorAll('[data-fit]')) {{
let fs = parseFloat(getComputedStyle(el).fontSize);
const max = el.dataset.fit ? parseFloat(el.dataset.fit) : el.parentElement.clientWidth;
while (el.scrollWidth > max && fs > 10) {{ fs -= 0.5; el.style.fontSize = fs + 'px'; }}
}}
if (window.afterLayout) await window.afterLayout();
return empty;
}})();
</script></body></html>"""
def slot(src, label, size, cls='', style=''):
return (f'<div class="slot {cls}" data-src="../assets/{src}" style="{style}"><div class="ph">{ICONS["camera"]}'
f'<b>{label}</b><small>{size}</small></div></div>')
COVER_3D = '../assets-gen/cover-3d.png' # rendered from the landing page (render_cover.py)
COVER_FLAT = '../assets-gen/cover-flat.png'
pages = [] # dicts: name, body, css, sel, w, h(optional), dpr
def page(name, body, css='', w=680, h=None, extra_css='', px_h=None):
pages.append(dict(name=name, body=body, css=css, w=w, h=h, extra_css=extra_css, px_h=px_h))
# ---------------------------------------------------------------- headers (1360 x 300)
HEADERS = [
('01', 'welcome', '👋', 'Welcome'), ('02', 'why-this-book', '❤️', 'Why this book'),
('03', 'who-its-for', '🎯', 'Who it’s for'), ('04', 'whats-inside', '📖', 'What’s inside'),
('05', 'try-one-workflow', '🧪', 'Try one workflow'), ('06', 'three-ways-to-back', '🧭', 'Three ways to back'),
('07', 'the-retreat', '🏕️', 'The Retreat'), ('08', 'the-academy', '🎓', 'The Academy'),
('09', 'cloud-and-aibook-fun', '☁️', 'Cloud and AiBook.fun'), ('10', 'stretch-goals', '🎁', 'Stretch goals'),
('11', 'timeline', '⏳', 'Timeline'), ('12', 'shipping-and-delivery', '📦', 'Shipping and delivery'),
('13', 'early-readers', '👀', 'Early readers'), ('14', 'authors-and-team', '✍️', 'The authors and team'),
('15', 'special-thanks', '🙏', 'Special thanks'), ('16', 'how-to-help', '📣', 'How to help'),
]
def header(num, slug, emoji, title):
body = (f'<div class="hdr" id="shot"><div class="tabs"><span></span><span></span><span></span><span></span><span></span></div>'
f'<div class="mark">{MARK}</div><div class="badge"><span class="emoji">{emoji}</span></div>'
f'<div style="min-width:0;flex:1"><div class="kicker">{TITLE}</div><h1 data-fit>{title}</h1></div><div class="rule"></div></div>')
return body
HDR = {n: (f'{n}-header-{s}', header(n, s, e, t)) for n, s, e, t in HEADERS}
def add_header(n):
name, body = HDR[n]; page(name, body, w=680, h=150)
# ---------------------------------------------------------------- 01 welcome
add_header('01')
page('01b-welcome-cover-icons', f'''
<div class="canvas" id="shot">
<div class="wel">
<figure class="bookwrap"><img src="{COVER_3D}" alt=""><figcaption>Cover mockup</figcaption></figure>
<div class="wtxt">
<h2>Welcome</h2>
<p class="t">{TITLE}</p>
<p class="s">A field book for advisors who want to use AI in their practice in ways they control.</p>
<p class="pub">BurnedOut Media / WeOwnNet</p>
</div>
</div>
<div class="trio">
<div>{icon("authors", "ico big")}<p>Multi-author field book</p></div>
<div>{icon("tested", "ico big")}<p>Tested AI workflows</p></div>
<div>{icon("updated", "ico big")}<p>Library updated often</p></div>
</div>
</div>''', css='''
.wel{display:flex;gap:30px;align-items:center;padding:0 6px 22px}
.bookwrap{flex:none;margin:0;width:196px;text-align:center}
.bookwrap img{width:196px;display:block}
.bookwrap figcaption{font-family:var(--mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-top:-14px}
.wtxt h2{font-family:var(--display);font-weight:900;font-size:50px;letter-spacing:-.03em;line-height:1;color:var(--pine-deep)}
.wtxt .t{font-family:var(--display);font-weight:600;font-size:22px;letter-spacing:-.02em;line-height:1.2;margin:14px 0 12px}
.wtxt .s{font-size:16px;line-height:1.5;color:var(--muted);max-width:25rem}
.wtxt .pub{font-family:var(--mono);font-size:13px;letter-spacing:.06em;color:var(--pine);margin-top:16px}
.trio{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
.trio>div{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:20px 14px 18px;text-align:center;box-shadow:var(--sh-sm)}
.ico.big{width:58px;height:58px;padding:13px;border-radius:999px;background:var(--pine-wash);border:1px solid var(--pine-tint);display:block;margin:0 auto 12px}
.trio p{font-weight:700;font-size:16px;line-height:1.3}
''')
# ---------------------------------------------------------------- 02 why this book
add_header('02')
WHY = [('newsletters', 'Too much noise', 'Each week brings a new tool and a new prediction, and very little of it fits a real client file.'),
('lockedwindow', 'Processes you don’t control', 'When your process lives inside a vendor’s product, a price change or a feature change can undo it.'),
('clock11', 'No time to learn', 'The job already takes up the evenings, and learning AI on top of it feels like a second job.'),
('chair', 'Clients are changing', 'Firms and clients are changing how they work faster than most playbooks are updated.')]
page('02b-why-this-book-cards', '<div class="canvas" id="shot"><div class="g">' + ''.join(
f'<div class="card c"><div class="il">{ILLUS[k]}</div><h3>{t}</h3><p>{d}</p></div>' for k, t, d in WHY) + '</div></div>', css='''
.g{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.c{padding:20px 22px 22px}
.il{width:120px;height:84px;margin:0 0 12px -6px}
.c h3{font-family:var(--display);font-weight:600;font-size:21px;letter-spacing:-.02em;line-height:1.15;margin-bottom:8px}
.c p{font-size:15px;line-height:1.5;color:var(--muted)}
''')
# ---------------------------------------------------------------- 03 who it's for
add_header('03')
page('03b-audience-cards', f'''<div class="canvas" id="shot">
<p class="spec">Mainly for</p>
<div class="two">
<div class="card a">{icon("accounting", "ico big")}<h3>Accounting advisors</h3><p class="k">Exit to Client</p></div>
<div class="card a">{icon("chart", "ico big")}<h3>Financial advisors</h3><p class="k">CKA® and CEPA® focus</p></div>
</div>
<p class="spec" style="margin-top:20px">Also for</p>
<div class="card b">{icon("briefcase", "ico mid")}<h3>Other professionals who advise clients</h3></div>
</div>''', css='''
.two{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:12px}
.a{padding:24px 24px 26px;border-top:4px solid var(--pine)}
.ico.big{width:60px;height:60px;padding:14px;border-radius:14px;background:var(--pine-wash);border:1px solid var(--pine-tint);display:block;margin-bottom:18px}
.a h3{font-family:var(--display);font-weight:900;font-size:28px;letter-spacing:-.025em;line-height:1.08}
.a .k{font-family:var(--mono);font-size:15px;font-weight:600;letter-spacing:.02em;color:var(--pine);margin-top:10px;background:var(--pine-wash);border:1px solid var(--pine-tint);display:inline-block;padding:5px 12px;border-radius:999px}
.b{margin-top:12px;padding:16px 22px;display:flex;align-items:center;gap:16px}
.ico.mid{width:44px;height:44px;padding:10px;border-radius:12px;background:var(--paper-2);border:1px solid var(--line);color:var(--muted)}
.b h3{font-family:var(--display);font-weight:600;font-size:20px;letter-spacing:-.02em}
''')
# ---------------------------------------------------------------- 04 what's inside
add_header('04')
RECIPE = [('Problem', 'The problem', 'The real task it solves.'),
('Who runs it', 'Who runs it', 'A person, a role, or an AI agent working under a person’s supervision.'),
('Setup', 'Setup', 'The tools, sample files, and expected cost.'),
('Safeguards', 'Safeguards', 'How to handle client data, where a person must review the work, and what can go wrong. These come before the steps.'),
('Steps', 'The steps', 'With the actual output the tools produced.'),
('Evidence', 'Evidence', 'What the result looked like when we ran it.'),
('Try it', 'Try it', 'A short exercise of about 15 minutes, using sample data.'),
('Record', 'Costs, author, and sources', '')]
rows = ''
for i, (k, t, d) in enumerate(RECIPE, 1):
first = ' first' if i == 1 else ''; last = ' last' if i == len(RECIPE) else ''
bars = '<i style="width:88%"></i><i style="width:64%"></i>'
if k == 'Steps': bars = '<i style="width:80%"></i><span class="term"></span>'
if k == 'Try it': bars = '<i style="width:72%"></i><span class="pill"></span>'
if k == 'Safeguards': bars = '<span class="gd"></span><span class="gd"></span>'
rows += (f'<div class="pc{first}{last}"><span class="k">{k}</span><span class="bars">{bars}</span><b class="n">{i}</b></div>'
f'<div class="ln"></div><div class="co"><h4>{t}</h4>' + (f'<p>{d}</p>' if d else '') + '</div>')
page('04b-workflow-recipe-diagram', f'''<div class="canvas" id="shot">
<p class="spec">Every workflow follows the same recipe</p>
<div class="rg">
<div class="ptop"><span>Workflow page</span><em>~15 min</em></div><div></div><div></div>
{rows}
</div></div>''', css='''
.rg{display:grid;grid-template-columns:250px 34px 1fr;margin-top:16px;align-items:stretch}
.ptop{background:linear-gradient(#fff,var(--paper));border:1px solid var(--line);border-bottom:1px solid var(--line);border-radius:16px 16px 0 0;padding:12px 16px;display:flex;justify-content:space-between;align-items:center}
.ptop span{font-family:var(--mono);font-size:12px;color:var(--muted)}
.ptop em{font-style:normal;font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--pine);background:var(--pine-wash);border:1px solid var(--pine-tint);padding:3px 9px;border-radius:999px}
.pc{background:#fff;border-left:1px solid var(--line);border-right:1px solid var(--line);padding:11px 30px 11px 16px;position:relative;display:flex;flex-direction:column;justify-content:center;gap:7px;border-top:1px dashed var(--line-2)}
.pc.first{border-top:none;padding-top:16px}
.pc.last{border-bottom:1px solid var(--line);border-radius:0 0 16px 16px;padding-bottom:16px}
.pc .k{font-family:var(--mono);font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--pine)}
.bars{display:flex;flex-direction:column;gap:5px}
.bars i{display:block;height:6px;border-radius:3px;background:#ece6db}
.bars .term{display:block;height:20px;border-radius:5px;background:#14110e}
.bars .pill{display:block;width:70px;height:14px;border-radius:999px;background:rgba(180,121,31,.14);border:1px solid rgba(180,121,31,.3)}
.bars .gd{display:block;height:6px;border-radius:3px;background:linear-gradient(90deg,var(--pine) 0 8px,transparent 8px 14px,#ece6db 14px);width:86%}
.pc .n{position:absolute;right:-13px;top:50%;transform:translateY(-50%);width:26px;height:26px;border-radius:999px;background:var(--pine);color:#fff;font-family:var(--mono);font-size:13px;font-weight:600;display:grid;place-items:center;box-shadow:0 0 0 3px var(--paper-2);z-index:2}
.ln{position:relative}.ln::before{content:"";position:absolute;left:13px;right:4px;top:50%;border-top:1.5px solid var(--pine);opacity:.5}
.co{padding:8px 0 8px 4px;display:flex;flex-direction:column;justify-content:center}
.co h4{font-weight:700;font-size:16px;line-height:1.25}
.co p{font-size:14px;line-height:1.42;color:var(--muted);margin-top:2px}
''')
# Published on the landing page: marketing-site/components/Contents.jsx ("The founding library")
CHAPTERS = [('Real Estate', 'Market briefs & listings, without leaking client data'),
('Financial Advisors', 'Portfolio summaries that stay compliant & reviewed'),
('Fractional <span style="white-space:nowrap">C-Suite</span>', 'Board updates & plans with real escalation paths'),
('Higher Education', 'Course & research workflows with academic integrity'),
('Communities', 'Moderation & programming that respects members'),
('Technology', 'Agent patterns, evals & the responsible defaults')]
page('04c-chapter-tiles', '<div class="canvas" id="shot"><p class="spec">The founding library</p><div class="tg">' + ''.join(
f'<div class="card t"><span class="n">{i:02d}</span><h3>{c}</h3><p>{d}</p></div>' for i, (c, d) in enumerate(CHAPTERS, 1)) + '</div></div>', css='''
.tg{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:14px}
.t{padding:16px 16px 18px;display:flex;flex-direction:column}
.t .n{font-family:var(--mono);font-size:13px;color:var(--pine);letter-spacing:.08em}
.t h3{font-family:var(--display);font-weight:600;font-size:21px;letter-spacing:-.02em;line-height:1.12;margin:10px 0 6px}
.t p{font-size:14px;line-height:1.4;color:var(--muted)}
''')
# ---------------------------------------------------------------- 05 try one workflow
add_header('05')
page('05b-sample-workflow-try-it', '''<div class="canvas" id="shot">
<div class="win">
<div class="bar"><i></i><i></i><i></i><span class="url">aibook.fun/read/recipe/RE-01</span></div>
<img src="../assets-gen/re01-top.png" alt="">
<div class="gap"><span>Problem · Actors · Setup · Safeguards · Method · Evidence</span></div>
<img src="../assets-gen/re01-tryit.png" alt="">
</div>
<p class="foot-note">Screenshots of sample workflow RE-01 at aibook.fun/read/recipe/RE-01</p>
</div>''', css='''
.win{background:var(--paper);border:1px solid var(--line-2);border-radius:14px;overflow:hidden;box-shadow:var(--sh-lg)}
.bar{display:flex;align-items:center;gap:7px;padding:10px 14px;background:#f1eee7;border-bottom:1px solid var(--line)}
.bar i{width:11px;height:11px;border-radius:999px;background:#d8d1c4;display:block}
.bar .url{margin-left:14px;flex:1;background:#fff;border:1px solid var(--line);border-radius:999px;padding:4px 14px;font-family:var(--mono);font-size:13px;color:var(--muted)}
.win img{display:block;width:614px}
.gap{padding:10px 0;text-align:center;background:linear-gradient(transparent calc(50% - 1px),var(--line-2) calc(50% - 1px) calc(50% + 1px),transparent calc(50% + 1px))}
.gap span{font-family:var(--mono);font-size:12.5px;color:var(--muted);background:var(--paper);padding:2px 10px;letter-spacing:.02em}
.foot-note{text-align:center}
''')
page('05c-button-try-the-workflow', f'''<div class="canvas" id="shot">
<div class="cta"><span class="b">Try the sample workflow {ICONS["arrow"]}</span>
<p>Free · about 15 minutes · sample data · aibook.fun</p></div></div>''', css='''
.canvas{padding:26px 32px 24px}
.cta{text-align:center}
.b{display:inline-flex;align-items:center;gap:12px;background:var(--pine);color:#f7fbf9;font-weight:700;font-size:24px;letter-spacing:-.01em;border-radius:999px;padding:18px 34px;box-shadow:0 10px 24px -10px rgba(28,93,76,.6)}
.b svg{width:24px;height:24px}
.cta p{font-family:var(--mono);font-size:14px;color:var(--muted);letter-spacing:.03em;margin-top:14px}
''')
# ---------------------------------------------------------------- 06 three ways to back
add_header('06')
def three_ways(paperback=True):
cloud_items = [('$19', 'Early Bird eBook', 'First 200 backers', False),
('$25', 'eBook and backer-only updates', '', False),
('$47', 'eBook and 12 months of the AiBook.fun library', '', True)]
rows = ''.join(f'<li class="{"feat" if f else ""}"><b>{p}</b><span>{t}' + (f'<small>{s}</small>' if s else '') + '</span></li>'
for p, t, s, f in cloud_items)
if paperback:
rows += f'<li><b class="np"><img src="{COVER_3D}" alt=""></b><span>Signed paperback and eBook<small>Price on the reward</small></span></li>'
cloud_deliv = ('<li>Library access: within about 14 days of a successful campaign</li><li>eBook: January 2027</li>'
+ ('<li>Signed paperback: February 2027</li>' if paperback else ''))
inc = f'<ul class="inc"><li>{icon("seat")}Your seat</li><li>{icon("book")}The eBook</li><li>{icon("library")}12 months of AiBook.fun</li></ul>'
return f'''<div class="canvas" id="shot"><div class="cols">
<div class="col card">
<p class="nm">Retreat</p><h3>Burned Out Advisor Retreat</h3>
<div class="sec"><p class="lb">What it is</p><p>Three days in person with the authors and other advisors.</p>
<p class="fact">{icon("people")}369 seats</p></div>
<div class="sec"><p class="lb">Includes</p>{inc}</div>
<div class="sec"><p class="lb">When</p><p class="fact">{icon("calendar")}4–6 November 2026</p></div>
<div class="pr"><p class="price">Price on the reward</p></div>
</div>
<div class="col card">
<p class="nm">Academy</p><h3>AiCoach + AOP.biz BootCamp</h3>
<div class="sec"><p class="lb">What it is</p><p>A seat in the next cohort at one of three sites:</p>
<p class="fact">{icon("pin")}Orlando, Florida</p><p class="fact">{icon("pin")}North Denver, Colorado</p><p class="fact">{icon("pin")}Villa Park, Chicagoland, Illinois</p></div>
<div class="sec"><p class="lb">Includes</p>{inc}</div>
<div class="sec"><p class="lb">When</p><p class="fact">{icon("calendar")}The next cohort at your chosen site</p></div>
<div class="pr"><p class="price">Price on the reward</p></div>
</div>
<div class="col card cloud">
<p class="nm">Cloud</p><div class="ch"><h3>Do it yourself, online</h3><img src="{COVER_FLAT}" alt=""></div>
<div class="sec span2"><p class="lb">Choose one</p><ul class="prices">{rows}</ul></div>
<div class="sec"><p class="lb">When</p><ul class="dl">{cloud_deliv}</ul></div>
<div class="pr"></div>
</div>
</div></div>'''
TW_CSS = '''
.cols{display:grid;grid-template-columns:1fr 1fr 1.3fr;grid-template-rows:repeat(6,auto);column-gap:10px}
.col{padding:18px 15px 16px;display:grid;grid-template-rows:subgrid;grid-row:span 6}
.nm{font-family:var(--mono);font-size:13px;letter-spacing:.12em;text-transform:uppercase;color:var(--pine);font-weight:600}
.col h3{font-family:var(--display);font-weight:600;font-size:19px;letter-spacing:-.02em;line-height:1.15;margin:6px 0 10px}
.sec{border-top:1px dashed var(--line-2);padding:10px 0 12px}
.span2{grid-row:span 2}
.lb{font-family:var(--mono);font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin-bottom:6px}
.sec p{font-size:14px;line-height:1.4}
.fact{display:flex;gap:7px;align-items:flex-start;margin-top:6px;font-weight:600}
.lb+.fact{margin-top:0}
.fact .ico,.inc .ico{width:17px;height:17px;margin-top:1px}
.inc li{display:flex;gap:7px;align-items:flex-start;font-size:14px;line-height:1.35;padding:2px 0}
.pr{align-self:end}
.price{font-family:var(--mono);font-size:12.5px;font-weight:600;color:var(--pine);background:var(--pine-wash);border:1px solid var(--pine-tint);border-radius:999px;padding:7px 6px;text-align:center;white-space:nowrap}
.cloud{border-color:rgba(28,93,76,.28)}
.prices li{display:grid;grid-template-columns:58px 1fr;gap:8px;align-items:baseline;padding:7px 8px;border-radius:10px;font-size:14px;line-height:1.35}
.prices li+li{margin-top:4px}
.prices b{font-family:var(--display);font-weight:900;font-size:24px;letter-spacing:-.03em;color:var(--ink)}
.prices b.np{align-self:start;width:30px;margin:0 0 0 6px}.prices b.np img{width:30px;display:block}
.ch{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}
.ch img{width:46px;border-radius:2px 4px 4px 2px;box-shadow:0 6px 12px -6px rgba(18,63,52,.5);margin:4px 0 8px}
.prices small{display:block;font-size:13px;color:var(--muted)}
.prices li.feat{background:var(--pine);color:#eaf1ec}
.prices li.feat b{color:#fff}
.dl li{font-size:14px;line-height:1.4;padding:2px 0 2px 14px;position:relative}
.dl li::before{content:"";position:absolute;left:0;top:8px;width:6px;height:6px;border-radius:999px;background:var(--pine)}
'''
page('06b-three-ways-to-back', three_ways(True), css=TW_CSS)
page('06c-three-ways-to-back-no-paperback', three_ways(False), css=TW_CSS)
# ---------------------------------------------------------------- 07 retreat
add_header('07')
page('07b-retreat-venue', f'''<div class="canvas" id="shot">
{slot("retreat-venue", "Venue photo goes here", "Landscape · at least 1360 × 765 px", "venue")}
<div class="ov"><p class="spec">{TITLE.split(" — ")[0]}</p><h3>Burned Out Advisor Retreat</h3><p class="d">{icon("calendar")}4 to 6 November 2026</p></div>
<div class="seats"><b>369</b><span>seats</span></div>
</div>''', h=383, px_h=765, css='''
.canvas{padding:0;height:383px}
.venue{position:absolute;inset:0}
.venue .ph{border-radius:0;border-width:0;justify-content:flex-start;padding-top:70px}
.ov{position:absolute;left:28px;bottom:26px;background:rgba(252,251,248,.96);border-radius:16px;padding:16px 22px 18px;box-shadow:var(--sh-lg);max-width:420px}
.ov h3{font-family:var(--display);font-weight:900;font-size:30px;letter-spacing:-.025em;line-height:1.05;margin:6px 0 10px}
.ov .d{display:flex;align-items:center;gap:9px;font-weight:700;font-size:18px}
.ov .d .ico{width:22px;height:22px}
.seats{position:absolute;right:28px;top:26px;width:108px;height:108px;border-radius:999px;background:var(--pine);color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;box-shadow:0 0 0 5px rgba(252,251,248,.9),var(--sh-lg)}
.seats b{font-family:var(--display);font-weight:900;font-size:38px;line-height:1;letter-spacing:-.03em}
.seats span{font-family:var(--mono);font-size:14px;letter-spacing:.12em;text-transform:uppercase;color:#cfe0d6;margin-top:3px}
''')
# ---------------------------------------------------------------- 08 academy
add_header('08')
W, H = MAP['w'], MAP['h']
pins = MAP['pins']
def pct(k): return pins[k][0] / W * 100, pins[k][1] / H * 100
SITES = [('villapark', 'Chicagoland, Illinois', 'Villa Park', 'lab-r'),
('denver', 'North Denver, Colorado', '#NoDe', 'lab-l'),
('orlando', 'Orlando, Florida', 'Azur Resorts', 'lab-l')]
pin_html = ''
for k, city, sub, cls in SITES:
x, y = pct(k)
pin_html += (f'<div class="pin" style="left:{x:.2f}%;top:{y:.2f}%">{ICONS["pin"]}'
f'<div class="lab {cls}"><b>{sub}</b><span>{city}</span></div></div>')
page('08b-academy-map', f'''<div class="canvas" id="shot">
<p class="spec" style="text-transform:none;letter-spacing:.04em;font-size:14px">AiCoach + AOP.biz BootCamp · three sites</p>
<div class="map">{MAP_SVG}{pin_html}</div>
</div>''', css=f'''
.map{{position:relative;margin-top:14px;aspect-ratio:{W}/{H}}}
.map>svg{{position:absolute;inset:0;width:100%;height:100%}}
.pin{{position:absolute;width:0;height:0}}
.pin>svg{{position:absolute;width:40px;height:40px;left:-20px;top:-36px;color:#fff;fill:var(--pine);stroke:#fff;stroke-width:1.6;filter:drop-shadow(0 3px 4px rgba(0,0,0,.25))}}
.lab{{position:absolute;background:#fff;border:1px solid var(--line);border-radius:12px;padding:8px 13px 9px;box-shadow:var(--sh-md);white-space:nowrap}}
.lab b{{display:block;font-family:var(--display);font-weight:600;font-size:18px;letter-spacing:-.02em;line-height:1.1}}
.lab span{{display:block;font-size:14px;color:var(--muted);margin-top:2px}}
.lab-r{{left:26px;top:-46px}}
.lab-l{{right:26px;top:-46px}}
''')
SITE_CARDS = [('site-orlando', 'Azur Resorts', 'Orlando, Florida'), ('site-north-denver', '#NoDe', 'North Denver, Colorado'),
('site-villa-park', 'Villa Park', 'Chicagoland, Illinois')]
page('08c-academy-sites', '<div class="canvas" id="shot"><div class="sg">' + ''.join(
f'<div class="card s">{slot(src, "Site photo goes here", "At least 800 × 600 px", "sp")}<div class="b"><p class="n">Site {i}</p><h3>{name}</h3><p class="pl">{icon("pin")}{place}</p></div></div>'
for i, (src, name, place) in enumerate(SITE_CARDS, 1)) + '</div></div>', css='''
.sg{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
.s{overflow:hidden}
.sp{aspect-ratio:4/3}
.sp .ph{border-radius:0;border-width:0 0 2px 0}
.sp .ph b{font-size:14px}.sp .ph small{font-size:11.5px}
.b{padding:14px 15px 16px}
.n{font-family:var(--mono);font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
.b h3{font-family:var(--display);font-weight:600;font-size:21px;letter-spacing:-.02em;margin:4px 0 6px}
.pl{display:flex;gap:6px;align-items:flex-start;font-size:14px;line-height:1.35;color:var(--ink)}
.pl .ico{width:17px;height:17px;margin-top:1px}
''')
# ---------------------------------------------------------------- 09 cloud and aibook.fun (device mockup)
add_header('09')
L, Ph = SCREENS['laptop'], SCREENS['phone']
def tgt(box, vw, vh, name, side='left', gap=16):
"""Callout target just outside the text box (left or right side), vertically centred."""
x, y, w, h = box
cx = x - gap if side == 'left' else x + w + gap
return f'<i class="tg" data-t="{name}" style="left:{cx / vw * 100:.2f}%;top:{(y + h / 2) / vh * 100:.2f}%"></i>'
page('09b-aibook-fun-device-mockup', f'''<div class="canvas" id="shot">
<div class="stage">
<div class="laptop"><div class="lid"><div class="scr"><img src="../assets-gen/screen-laptop.png">
{tgt(L["essay"], L["vw"], L["vh"], "essay", gap=22)}{tgt(L["workflow"], L["vw"], L["vh"], "workflow", gap=22)}</div></div><div class="base"></div></div>
<div class="phone"><div class="scr"><img src="../assets-gen/screen-phone.png">
{tgt(Ph["tryit"], Ph["vw"], Ph["vh"], "tryit", "right", 22)}{tgt(Ph["updated"], Ph["vw"], Ph["vh"], "updated", "right", 22)}</div></div>
<span class="co" data-c="essay" style="left:0;top:0">Chapter essay</span>
<span class="co" data-c="workflow" style="left:0;bottom:0">Workflow</span>
<span class="co" data-c="tryit" style="right:0;top:0">Try-it (about 15 minutes)</span>
<span class="co" data-c="updated" style="right:0;bottom:0">Last updated</span>
<svg class="lines"></svg>
</div>
<p class="foot-note">Screenshots of the AiBook.fun reader (sample Real Estate chapter) in device frames.</p>
</div>
<script>
window.afterLayout = async () => {{
await Promise.all([...document.images].map(i => i.complete ? 1 : new Promise(r => i.onload = r)));
const st = document.querySelector('.stage').getBoundingClientRect();
const svg = document.querySelector('.lines');
let s = '';
for (const c of document.querySelectorAll('.co')) {{
const t = document.querySelector('.tg[data-t="' + c.dataset.c + '"]').getBoundingClientRect();
const r = c.getBoundingClientRect();
const tx = t.left - st.left, ty = t.top - st.top;
const top = r.top - st.top < st.height / 2;
const cx = Math.min(Math.max(tx, r.left - st.left + 16), r.right - st.left - 16);
const cy = top ? r.bottom - st.top : r.top - st.top;
s += `<path d="M${{cx}} ${{cy}} L${{cx}} ${{ty}} L${{tx}} ${{ty}}" />`;
s += `<circle cx="${{tx}}" cy="${{ty}}" r="4.5" class="dot"/><circle cx="${{tx}}" cy="${{ty}}" r="8" class="halo"/>`;
}}
svg.innerHTML = s;
}};
</script>''', css='''
.canvas{padding:24px 26px 22px}
.stage{position:relative;height:500px}
.laptop{position:absolute;left:18px;top:56px;width:470px}
.lid{background:#1f1b17;border-radius:14px 14px 4px 4px;padding:11px 11px 13px;box-shadow:var(--sh-lg)}
.laptop .scr{position:relative;aspect-ratio:1120/700;overflow:hidden;border-radius:3px;background:#fff}
.base{height:14px;margin:0 -30px;background:linear-gradient(#d9d4cb,#bdb6aa);border-radius:0 0 14px 14px;position:relative;box-shadow:0 14px 22px -12px rgba(0,0,0,.35)}
.base::before{content:"";position:absolute;left:50%;top:0;width:80px;height:6px;transform:translateX(-50%);background:#a9a295;border-radius:0 0 8px 8px}
.phone{position:absolute;right:18px;top:62px;width:176px;background:#1f1b17;border-radius:30px;padding:8px;box-shadow:var(--sh-lg),0 0 0 1px #3a342b inset}
.phone .scr{position:relative;aspect-ratio:390/844;overflow:hidden;border-radius:23px;background:#fff}
.scr img{position:absolute;inset:0;width:100%;height:100%;display:block}
.tg{position:absolute;width:0;height:0}
.co{position:absolute;z-index:3;background:#fff;border:1.5px solid var(--pine);color:var(--pine-deep);font-weight:700;font-size:16px;padding:8px 16px;border-radius:999px;box-shadow:var(--sh-md);white-space:nowrap}
.lines{position:absolute;inset:0;width:100%;height:100%;z-index:2;overflow:visible}
.lines path{fill:none;stroke:var(--pine);stroke-width:1.8}
.lines .dot{fill:var(--pine);stroke:#fff;stroke-width:2}
.lines .halo{fill:none;stroke:var(--pine);stroke-width:1.5;opacity:.35}
.foot-note{text-align:center;margin-top:14px}
''')
# ---------------------------------------------------------------- 10 stretch goals (1360 x 500)
add_header('10')
GOALS = [(15000, '$15,000', 'Bonus chapter', 'PDF for every backer whose reward includes the eBook'),
(35000, '$35,000', 'Audiobook sample', 'One chapter recorded as audio, for every backer'),
(54000, '$54,000', 'Live Q&A replay pack', 'Recording of a live Q&A with the authors, for every backer')]
GX = [16, 50, 84] # schematic milestone positions (%), evenly spaced so labels never collide
def stretch(unlocked):
fill = 0 if unlocked == 0 else GX[unlocked - 1]
marks = ''
for i, ((v, amt, name, desc), x) in enumerate(zip(GOALS, GX), 1):
on = i <= unlocked
marks += (f'<div class="m {"on" if on else ""}" style="left:{x}%">'
f'<span class="lk">{ICONS["unlock" if on else "lock"]}</span>'
f'<div class="lbl"><b>{amt}' + ('<span class="ul">Unlocked</span>' if on else '') + '</b>'
f'<span class="nm">{name}</span><span class="ds">{desc}</span></div></div>')
status = 'Not yet unlocked' if unlocked == 0 else ('Unlocked: ' + ', '.join(g[1] for g in GOALS[:unlocked]))
return f'''<div class="canvas" id="shot">
<div class="top"><p class="spec">Stretch goals</p><p class="st">{status}</p></div>
<div class="track"><div class="fill" style="width:{fill}%"></div>
<div class="goal"><span class="gf">{ICONS["flag"]}</span><div class="gl"><b>Goal $5,000</b></div></div>
{marks}
</div></div>'''
ST_CSS = '''
.canvas{height:250px;padding:24px 32px 18px}
.top{display:flex;justify-content:space-between;align-items:baseline}
.st{font-family:var(--mono);font-size:13px;color:var(--muted);letter-spacing:.02em}
.track{position:relative;height:14px;border-radius:999px;background:#e6e0d4;margin:58px 0 0;box-shadow:inset 0 1px 2px rgba(0,0,0,.08)}
.fill{position:absolute;left:0;top:0;bottom:0;border-radius:999px;background:linear-gradient(90deg,#23745f,var(--pine))}
.m{position:absolute;top:50%;width:0;height:0}
.lk{position:absolute;left:-20px;top:-20px;width:40px;height:40px;border-radius:999px;background:#fff;border:2px solid var(--line-2);color:var(--muted);display:grid;place-items:center;box-shadow:var(--sh-sm)}
.lk svg{width:21px;height:21px}
.m.on .lk{background:var(--pine);border-color:#fff;color:#fff;box-shadow:0 0 0 2px var(--pine),var(--sh-md)}
.m.on .lbl b{color:var(--pine-deep)}
.ul{display:inline-block;vertical-align:4px;margin-left:8px;font-family:var(--mono);font-size:11.5px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:#fff;background:var(--pine);padding:3px 10px;border-radius:999px;white-space:nowrap}
.lbl{position:absolute;top:30px;width:196px;left:-98px;text-align:center}
.lbl b{display:block;font-family:var(--display);font-weight:900;font-size:23px;letter-spacing:-.03em;line-height:1.1}
.lbl .nm{display:block;font-weight:700;font-size:15.5px;margin-top:2px}
.lbl .ds{display:block;font-size:13.5px;line-height:1.35;color:var(--muted);margin-top:3px}
.goal{position:absolute;left:0;top:50%;width:0;height:0}
.goal::before{content:"";position:absolute;left:0;top:-7px;width:14px;height:14px;border-radius:999px;background:#fff;border:3px solid var(--amber)}
.gf{position:absolute;left:3px;top:-44px;width:24px;height:24px;color:var(--amber)}
.gf svg{width:24px;height:24px}
.gl{position:absolute;left:28px;top:-42px;white-space:nowrap}
.gl b{font-family:var(--mono);font-size:13px;font-weight:600;letter-spacing:.04em;color:var(--amber)}
'''
for n, nm in [(0, '10b-stretch-goals-locked'), (1, '10c-stretch-goals-15k-unlocked'), (2, '10d-stretch-goals-35k-unlocked'), (3, '10e-stretch-goals-54k-unlocked')]:
page(nm, stretch(n), css=ST_CSS, h=250)
# ---------------------------------------------------------------- 11 timeline
add_header('11')
STOPS = [('Fri 2 Oct 2026', 'The campaign opens.'),
('Wed 4 Nov 2026', 'The campaign ends. The Burned Out Advisor Retreat runs 4 to 6 November.'),
('By mid-November 2026', 'AiBook.fun access for backers whose reward includes it, and the backer survey.'),
('Dates to be announced', 'Academy BootCamp cohorts in Orlando, North Denver, and Villa Park.'),
('January 2027', 'Final layout and eBook files. The eBook is delivered and the paperbacks are signed.'),
('February 2027', 'Signed paperbacks are delivered.')]
XS = [96, 290, 484]
Y1, Y2 = 30, 250
pos = [(XS[0], Y1), (XS[1], Y1), (XS[2], Y1), (XS[2], Y2), (XS[1], Y2), (XS[0], Y2)]
stops = ''.join(f'<div class="stop{" tba" if i == 4 else ""}" style="left:{x}px;top:{y}px"><span class="num">{i}</span><div class="tx"><b>{d}</b><p>{t}</p></div></div>'
for i, ((x, y), (d, t)) in enumerate(zip(pos, STOPS), 1))
page('11b-timeline', f'''<div class="canvas" id="shot"><div class="tl">
<svg class="path" viewBox="0 0 616 420"><path d="M{XS[0]} {Y1} H{XS[2]} C{XS[2]+160} {Y1} {XS[2]+160} {Y2} {XS[2]} {Y2} H{XS[0]}" /></svg>
{stops}</div></div>''', css=f'''
.tl{{position:relative;height:396px}}
.path{{position:absolute;inset:0;width:616px;height:420px}}
.path path{{fill:none;stroke:var(--pine);stroke-width:3;stroke-dasharray:2 9;stroke-linecap:round;opacity:.7}}
.stop{{position:absolute;width:0;height:0}}
.num{{position:absolute;left:-23px;top:-23px;width:46px;height:46px;border-radius:999px;background:var(--pine);color:#fff;font-family:var(--display);font-weight:900;font-size:22px;display:grid;place-items:center;box-shadow:0 0 0 5px var(--paper-2),var(--sh-md)}}
.stop.tba .num{{background:#fff;color:var(--pine);border:2.5px solid var(--pine)}}
.tx{{position:absolute;top:34px;left:-92px;width:184px;text-align:center}}
.tx b{{display:block;font-family:var(--display);font-weight:600;font-size:18px;letter-spacing:-.02em;line-height:1.15;color:var(--ink)}}
.tx p{{font-size:14px;line-height:1.4;color:var(--muted);margin-top:5px}}
.stop.tba .tx b{{color:var(--pine)}}
''')
# ---------------------------------------------------------------- 12 shipping and delivery
add_header('12')
def lane(title, sub, ic, steps):
st = ''
for j, (i2, t, d) in enumerate(steps):
if j: st += f'<span class="ar">{ICONS["arrow"]}</span>'
vis = f'<img class="cv" src="{i2[4:]}" alt="">' if i2.startswith('img:') else icon(i2, "ico ci")
st += f'<div class="stp">{vis}<b>{t}</b>' + (f'<p>{d}</p>' if d else '') + '</div>'
head = f'<img class="lhc" src="{ic[4:]}" alt="">' if ic.startswith('img:') else icon(ic, "ico lhi")
return f'<div class="card lane"><div class="lh">{head}<div><h3>{title}</h3><p>{sub}</p></div></div><div class="steps n{len(steps)}">{st}</div></div>'
page('12b-shipping-and-delivery-lanes', '<div class="canvas" id="shot">' +
lane('Digital rewards', 'Arrive by email', 'mail', [
('mail', 'Email', 'To the address on your Kickstarter account'),
('library', 'Library access', 'Within about 14 days of a successful campaign'),
('img:' + COVER_FLAT, 'eBook', 'January 2027')]) +
lane('Signed paperbacks', 'Ship in February 2027', 'img:' + COVER_3D, [
('survey', 'Survey', 'Kickstarter’s Pledge Manager asks for your address and charges shipping and any taxes'),
('printer', 'Printing', ''),
('pen', 'Signing', 'January 2027'),
('truck', 'Shipping', 'February 2027')]) +
'</div>', css='''
.lane{padding:16px 18px 18px}
.cv{display:block;height:46px;margin:0 auto 8px;border-radius:1px 3px 3px 1px;box-shadow:0 4px 8px -4px rgba(18,63,52,.5)}
.lhc{height:52px;display:block;margin:-6px 2px -4px 0}
.lane+.lane{margin-top:14px}
.lh{display:flex;align-items:center;gap:12px;padding-bottom:12px;border-bottom:1px dashed var(--line-2);margin-bottom:14px}
.ico.lhi{width:40px;height:40px;padding:9px;border-radius:12px;background:var(--pine);color:#fff}
.lh h3{font-family:var(--display);font-weight:600;font-size:21px;letter-spacing:-.02em;line-height:1.1}
.lh p{font-family:var(--mono);font-size:13px;color:var(--muted);margin-top:3px}
.steps{display:flex;align-items:flex-start}
.stp{flex:1;min-width:0;text-align:center;padding:0 4px}
.ar{flex:none;width:18px;height:18px;color:var(--line-2);margin-top:14px}
.ico.ci{width:46px;height:46px;padding:11px;border-radius:999px;background:var(--pine-wash);border:1px solid var(--pine-tint);display:block;margin:0 auto 8px}
.stp b{display:block;font-size:15.5px;font-weight:700}
.stp p{font-size:13.5px;line-height:1.38;color:var(--muted);margin-top:3px}
''')
# ---------------------------------------------------------------- 13 early readers (placeholder cards)
add_header('13')
# Fill in once early readers approve their quotes in writing (photo files: assets/reader-1.jpg etc.).
QUOTES = [ # (quote, name, role and firm)
('Early reader quote 1 goes here.', 'Name goes here', 'Role and firm'),
('Early reader quote 2 goes here.', 'Name goes here', 'Role and firm'),
('Early reader quote 3 goes here.', 'Name goes here', 'Role and firm'),
]
page('13b-early-reader-quotes', '<div class="canvas" id="shot">' + ''.join(
f'<div class="card q">{slot(f"reader-{i}", "Photo", "400 × 400", "qp")}<div class="qt"><p class="qq">“{q}”</p>'
f'<p class="who"><b>{n}</b> · {r}</p></div></div>' for i, (q, n, r) in enumerate(QUOTES, 1)) + '</div>', css='''
.q{display:flex;gap:18px;align-items:center;padding:18px 20px}
.q+.q{margin-top:12px}
.qp{width:84px;height:84px;border-radius:999px;flex:none}
.qp .ph{border-width:2px;gap:2px}.qp .ph svg{width:20px;height:20px}.qp .ph b{font-size:12px}.qp .ph small{font-size:10px}
.qq{font-family:var(--display);font-style:italic;font-size:21px;letter-spacing:-.01em;color:#8d8579;line-height:1.3}
.who{font-size:14.5px;color:var(--muted);margin-top:6px}
.who b{color:var(--ink)}
''')
# ---------------------------------------------------------------- 14 authors and team (placeholders)
add_header('14')
TEAM = [('@THY', 'Name goes here', 'Author and Retreat host'),
('@LFG', 'Name goes here', 'Creative lead for design, website, campaign page, and video'),
('@CEO', 'Name goes here', 'Business strategy and partnerships'),
('@GTM', 'Name goes here', 'Payments, email, and technical setup'),
('', 'Osinachi Patrick', 'Product lead, AiBook.fun'),
('', 'Name goes here', 'Academy BootCamp lead')]
team_html = ''.join(
f'<div class="card tm">{slot("team-" + (h[1:].lower() if h else ("osinachi-patrick" if n.startswith("Osinachi") else "academy-lead")), "Photo", "800 × 800", "tp")}'
f'<div><p class="tn">{n}</p>' + (f'<p class="th">{h}</p>' if h else '') + f'<p class="tr">{r}</p></div></div>' for h, n, r in TEAM)
# Landing page (Authors.jsx) lists three co-authors as confirmed, by chapter; names are not published yet.
AUTHORS = [('Name goes here', 'Co-author, Real Estate & Finance'), ('Name goes here', 'Co-author, <span style="white-space:nowrap">Fractional C-Suite</span>'),
('Name goes here', 'Co-author, Technology & RAAI')]
AUTHOR_HTML = ''.join(f'<div class="card au">{slot(f"author-{i}", "Author photo goes here", "800 × 800", "ap")}<p class="an">{n}</p><p class="ar">{r}</p></div>'
for i, (n, r) in enumerate(AUTHORS, 1))
page('14b-authors-and-team', f'''<div class="canvas" id="shot">
<p class="spec">The authors</p>
<div class="ag">{AUTHOR_HTML}</div>
<p class="spec" style="margin-top:22px">The team</p>
<div class="tg">{team_html}</div>
</div>''', css='''
.ag{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:12px}
.au{padding:14px 14px 18px}
.ap{aspect-ratio:1/1;border-radius:10px}
.ap .ph b{font-size:13px}
.an{font-family:var(--display);font-weight:600;font-size:19px;letter-spacing:-.02em;margin-top:12px;color:#8d8579}
.ar{font-size:14px;line-height:1.35;color:var(--muted);margin-top:2px}
.tg{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:12px}
.tm{padding:12px;display:flex;gap:11px;align-items:flex-start}
.tp{width:60px;height:60px;border-radius:999px;flex:none}
.tp .ph{gap:1px}.tp .ph svg{width:16px;height:16px}.tp .ph b{font-size:11px}.tp .ph small{display:none}
.tn{font-weight:700;font-size:14.5px;line-height:1.25}
.th{font-family:var(--mono);font-size:12px;color:var(--pine);margin-top:2px}
.tr{font-size:13px;line-height:1.35;color:var(--muted);margin-top:3px}
''')
page('14c-author-headshot-800x800', f'''<div class="canvas" id="shot">{slot("headshot-template", "Author photo goes here", "800 × 800 px · square", "hs")}</div>''',
w=400, h=400, css='''.canvas{width:400px;height:400px;padding:0}.hs{position:absolute;inset:0}.hs .ph{border-radius:0;border-width:3px}.hs .ph b{font-size:20px}.hs .ph small{font-size:14px}.hs .ph svg{width:44px;height:44px}''')
# ---------------------------------------------------------------- 15, 16
add_header('15')
add_header('16')
# ---------------------------------------------------------------- render
async def render(only=None):
from playwright.async_api import async_playwright
results = []
async with async_playwright() as p:
b = await p.chromium.launch(executable_path='/usr/bin/google-chrome')
for pg in pages:
if only and not re.search(only, pg['name']): continue
html = HEAD.format(body=pg['body'], css=pg['css'], extra_css=pg['extra_css'])
f = PAGES / f"{pg['name']}.html"; f.write_text(html)
page_ = await b.new_page(viewport={'width': pg['w'], 'height': math.ceil(pg['h'] or 800)}, device_scale_factor=2)
await page_.goto(f.as_uri())
empty = await page_.evaluate('window.__ready')
name = pg['name'] + ('-PLACEHOLDER' if empty else '')
for old in OUT.glob(pg['name'] + '*.png'): old.unlink()
el = await page_.query_selector('#shot')
if pg['h']: # fixed-size artwork: clip exactly (element screenshots round fractional heights up)
await page_.screenshot(path=str(OUT / f'{name}.png'), clip={'x': 0, 'y': 0, 'width': pg['w'], 'height': pg['h']})
else:
await el.screenshot(path=str(OUT / f'{name}.png'))
if pg.get('px_h'): # exact pixel height (e.g. 16:9 = 1360 x 765)
from PIL import Image
im = Image.open(OUT / f'{name}.png'); im.crop((0, 0, im.width, pg['px_h'])).save(OUT / f'{name}.png')
results.append((name, empty))
await page_.close()
await b.close()
for r in results: print(*r)
if __name__ == '__main__':
asyncio.run(render(sys.argv[1] if len(sys.argv) > 1 else None))