"""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 = """
')
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'''
Cover mockup
Welcome
{TITLE}
A field book for advisors who want to use AI in their practice in ways they control.
BurnedOut Media / WeOwnNet
{icon("authors", "ico big")}
Multi-author field book
{icon("tested", "ico big")}
Tested AI workflows
{icon("updated", "ico big")}
Library updated often
''', 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', '
''', 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 = ''
if k == 'Steps': bars = ''
if k == 'Try it': bars = ''
if k == 'Safeguards': bars = ''
rows += (f'
Screenshots of the AiBook.fun reader (sample Real Estate chapter) in device frames.
''', 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'
'
f'{ICONS["unlock" if on else "lock"]}'
f'
{amt}' + ('Unlocked' if on else '') + ''
f'{name}{desc}
')
status = 'Not yet unlocked' if unlocked == 0 else ('Unlocked: ' + ', '.join(g[1] for g in GOALS[:unlocked]))
return f'''
Stretch goals
{status}
{ICONS["flag"]}
Goal $5,000
{marks}
'''
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'
{i}
{d}
{t}
'
for i, ((x, y), (d, t)) in enumerate(zip(pos, STOPS), 1))
page('11b-timeline', f'''
{stops}
''', 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'{ICONS["arrow"]}'
vis = f'' if i2.startswith('img:') else icon(i2, "ico ci")
st += f'
{vis}{t}' + (f'
{d}
' if d else '') + '
'
head = f'' if ic.startswith('img:') else icon(ic, "ico lhi")
return f'
{head}
{title}
{sub}
{st}
'
page('12b-shipping-and-delivery-lanes', '
' +
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')]) +
'
', 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', '
' + ''.join(
f'
{slot(f"reader-{i}", "Photo", "400 × 400", "qp")}
“{q}”
'
f'
{n} · {r}
' for i, (q, n, r) in enumerate(QUOTES, 1)) + '
', 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'
{slot("team-" + (h[1:].lower() if h else ("osinachi-patrick" if n.startswith("Osinachi") else "academy-lead")), "Photo", "800 × 800", "tp")}'
f'
{n}
' + (f'
{h}
' if h else '') + f'
{r}
' 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, Fractional C-Suite'),
('Name goes here', 'Co-author, Technology & RAAI')]
AUTHOR_HTML = ''.join(f'