Files
Cursor Agentandz c3af929c56 site: one API, one account, better cheaper faster
The bar is Models, Dev, Platform, Pricing, Docs, Enterprise, Log in, and Get API key. The homepage is seven sections. Launch pages stay in the tree and drop out of the index through lib/launch.ts. /platform forwards to /cloud. Model and repository floors come from the snapshots the build already keeps.

Co-authored-by: z <[email protected]>
2026-10-26 21:53:40 +00:00

783 lines
28 KiB
JavaScript
Raw Permalink 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.
import { chromium } from '@playwright/test'
import { createHash } from 'node:crypto'
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { TOPICS, picture } from '../lib/hanzo/topics.ts'
/**
* The share cards: `node scripts/generate-og.mjs` draws every one,
* `node scripts/generate-og.mjs pricing api` the named ones alone.
*
* Each draw records, in scripts/og.json, the hash of the page a card was drawn
* from and of the image it made, so e2e/unit/cards.spec.ts can hold every image
* to the words below.
*/
/** Where a card's image lives: the default card at the root of `public/`, the rest in `public/og/`. */
export const file = (name) => (name === 'og-image' ? 'public/og-image.png' : `public/og/${name}.png`)
/** The draw record. */
export const MANIFEST = new URL('./og.json', import.meta.url)
const sha = (bytes) => createHash('sha256').update(bytes).digest('hex')
const HANZO_MARK_SVG = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 67 67" width="36" height="36" role="img" aria-label="Hanzo">
<path fill="#ffffff" d="M22.21 67V44.6369H0V67H22.21Z"/>
<path fill="#ffffff" d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z"/>
<path fill="#ffffff" d="M22.21 0H0V22.3184H22.21V0Z"/>
<path fill="#ffffff" d="M66.7198 0H44.5098V22.3184H66.7198V0Z"/>
<path fill="#ffffff" d="M66.7198 67V44.6369H44.5098V67H66.7198Z"/>
</svg>
`
export const CARDS = [
// The landing's own words (components/home), and what anyone can check: a Free
// plan, one API at api.hanzo.ai, and the CLI the landing shows.
{
name: 'og-image',
badge: 'HANZO AI',
title: "Better, cheaper, faster. Pick all three.",
lede: 'The AI cloud and app builder. Describe an app, an agent or a company, and Hanzo builds it, ships it and runs it.',
pills: ['Free to start', 'One API at api.hanzo.ai', 'Chat, Dev and agents'],
accent: '#3b82f6',
accentRgb: '59, 130, 246',
visual: 'terminal',
},
{
name: 'home',
badge: 'HANZO AI',
title: "Better, cheaper, faster. Pick all three.",
lede: 'The AI cloud and app builder. Describe an app, an agent or a company, and Hanzo builds it, ships it and runs it.',
pills: ['Free to start', 'One API at api.hanzo.ai', 'Chat, Dev and agents'],
accent: '#3b82f6',
accentRgb: '59, 130, 246',
visual: 'terminal',
},
// /beta's own title and description (app/beta/layout.tsx): the card a member's link unfurls to.
{
name: 'beta',
badge: 'BETA',
title: 'Get there first.',
lede: 'Early access to Dev, Studio, Kai, Enso and Zen.',
pills: ['Dev', 'Studio', 'Kai'],
accent: '#8b5cf6',
accentRgb: '139, 92, 246',
visual: 'terminal',
},
// The sign-in page's providers (components/auth/panel.tsx) and the CLI's device sign-in.
{
name: 'login',
badge: 'SIGN IN',
title: 'Welcome back to Hanzo.',
lede: 'One Hanzo account for Chat, Dev, the console and your API keys.',
pills: ['Google', 'GitHub', 'Email'],
accent: '#06b6d4',
accentRgb: '6, 182, 212',
visual: 'terminal',
},
// The Free plan's own terms (GET /v1/billing/plans, `free`).
{
name: 'signup',
badge: 'GET STARTED',
title: 'Create your account.',
lede: 'Start free with Enso Free and Zen Free, with 50 free requests every day. No card needed.',
pills: ['Free to start', 'No card needed', 'Google, GitHub or email'],
accent: '#3b82f6',
accentRgb: '59, 130, 246',
visual: 'terminal',
},
// /pricing's own description, and the Free, Pro and Team rows of GET /v1/billing/plans.
{
name: 'pricing',
badge: 'PRICING',
title: 'Start free.',
lede: 'Enso Free and Zen Free cost nothing. Paid plans open every model, include usage and add your own workspace. Team is per member; Enterprise is custom.',
pills: ['Free: $0', 'Pro: $20 a month', 'Team: $25 a member'],
accent: '#10b981',
accentRgb: '16, 185, 129',
visual: 'pricing',
},
// Hanzo's own model ids as GET /v1/models serves them; Zen is Zoo Labs', served here.
{
name: 'models',
badge: 'MODELS',
title: 'Every model, one key.',
lede: 'Enso and Kai are Hanzo’s own. Zen and models from other labs answer on the same endpoint with the same key.',
pills: ['Enso', 'Kai', 'Zen'],
accent: '#8b5cf6',
accentRgb: '139, 92, 246',
visual: 'rows',
window: 'GET /v1/models',
mono: true,
rows: [
['enso-ultra', '1M-token context · reads images'],
['enso-flash', '900K-token context · reads images'],
['kai', 'Answers typed questions at /v1/decisions'],
['zen6', '1M-token context · free'],
],
},
// /api's own title and description.
{
name: 'api',
badge: 'API',
title: 'One API for models, agents and tools.',
lede: 'Every Hanzo capability answers under one base URL, behind one key. If you have used the OpenAI API, you already know most of this.',
pills: ['api.hanzo.ai/v1', 'One key, one bill', 'Clients from OpenAPI'],
accent: '#06b6d4',
accentRgb: '6, 182, 212',
visual: 'api',
},
{
// The Enterprise plan's own terms (GET /v1/billing/plans, `enterprise`).
name: 'enterprise',
badge: 'HANZO ENTERPRISE',
title: 'Give every team AI.',
lede: 'Run Hanzo in your cloud, on your servers or fully offline, with dedicated compute and 24/7 engineering support.',
pills: ['Your cloud or ours', 'Fully offline', 'Single sign-on'],
accent: '#f59e0b',
accentRgb: '245, 158, 11',
visual: 'enterprise',
},
// /careers' own words: the team, how it works, and the four hiring steps.
{
name: 'careers',
badge: 'CAREERS',
title: 'Build an open AI cloud with us.',
lede: 'Hanzo is a small team building an open AI cloud: the inference engine, the models, the backend services and the agents that use them.',
pills: ['Remote', 'Mostly open source', 'No whiteboarding'],
accent: '#10b981',
accentRgb: '16, 185, 129',
visual: 'rows',
window: 'How we hire',
rows: [
['1. We read your application', 'Your resume and what you send with it'],
['2. We get to know you', 'And the work you have done'],
['3. You show your skills', 'No whiteboarding'],
['4. You meet the team', 'And we talk about next steps'],
],
},
// /research's own words and four of the specifications it links.
{
name: 'research',
badge: 'RESEARCH',
title: 'Work we publish.',
lede: 'The reasoning behind what Hanzo builds is published: papers at papers.hanzo.ai and specifications at hips.hanzo.ai.',
pills: ['papers.hanzo.ai', 'hips.hanzo.ai', 'Code on GitHub'],
accent: '#8b5cf6',
accentRgb: '139, 92, 246',
visual: 'rows',
window: 'hips.hanzo.ai',
mono: true,
rows: [
['HIP-0000', 'The architecture framework'],
['HIP-1211', 'The model API'],
['HIP-1332', 'Kai, the decision model'],
['HIP-1145', 'How an experiment is recorded'],
],
},
// Only what /trust states, each a mechanism in the code: one identity service,
// argon2id, PKCE S256, single-use refresh tokens, a database file per
// organization on Hanzo Base under a key derived for it, SCIM 2.0, ML-DSA-65
// among the signature algorithms, and the audit trail.
{
name: 'security',
badge: 'SECURITY',
title: 'Security you can check.',
lede: 'One identity service for every sign-in, a database file per organization on Hanzo Base, and an audit trail of who did what.',
pills: ['argon2id passwords', 'SCIM 2.0', 'Audit trail'],
accent: '#3b82f6',
accentRgb: '59, 130, 246',
visual: 'security',
},
// /products: Hanzo OS, its positioning and its layers (lib/layers.ts).
{
name: 'products',
badge: 'PRODUCT',
title: 'Hanzo OS.',
lede: 'A human signs a company into existence. After genesis, agents can run it.',
pills: ['Company', 'Agents', 'Machines'],
accent: '#06b6d4',
accentRgb: '6, 182, 212',
visual: 'rows',
window: 'Hanzo OS',
rows: [
['Company', 'Formation, cap table, documents and filings'],
['Agents', 'A model, instructions, tools and a budget'],
['Models', 'Every model behind one endpoint and one key'],
['Identity', 'Who is acting, and what it may reach'],
['Record', 'Every act on every layer, in one trail'],
],
},
// /about's own timeline (components/about/HistoryTimeline.tsx).
{
name: 'about',
badge: 'ABOUT',
title: 'Building since 2016.',
lede: "Hanzo AI started in 2016 and went through Techstars '17. Today it builds an AI cloud, Hanzo Dev, and the Enso and Kai models.",
pills: ['Since 2016', "Techstars '17", 'Enso and Kai'],
accent: '#3b82f6',
accentRgb: '59, 130, 246',
visual: 'rows',
window: 'Timeline',
rows: [
['2016', 'Hanzo AI starts'],
['2017', "Techstars '17"],
['2025', 'An MCP server, Operative and Hanzo Dev'],
['2026', 'Enso in July, Kai in September'],
],
},
]
/** A beta group's accent, one of the five the cards above use; a product takes the beta card's. */
const ACCENTS = {
Business: ['#f59e0b', '245, 158, 11'],
Data: ['#06b6d4', '6, 182, 212'],
Agents: ['#8b5cf6', '139, 92, 246'],
'Build & run': ['#3b82f6', '59, 130, 246'],
Security: ['#10b981', '16, 185, 129'],
Creative: ['#8b5cf6', '139, 92, 246'],
Models: ['#06b6d4', '6, 182, 212'],
}
const BETA = ['#8b5cf6', '139, 92, 246']
/**
* A share topic's card (lib/hanzo/topics; app/beta/for/[topic] names it): its
* name large, its group, its line, and the beta's list with its row lit beside
* the next three of its kind. Every word is the topic's own or BETAS's, so the
* card says only what the beta page says. Hanzo's is the beta card above.
*/
function topicCard(topic) {
const kin = TOPICS.filter((t) => t.kind === topic.kind)
const at = kin.indexOf(topic)
const rows = [0, 1, 2, 3].map((n) => kin[(at + n) % kin.length]).map((t) => [t.name, t.line])
const [accent, accentRgb] = topic.group ? ACCENTS[topic.group] : BETA
// A short name is drawn as the card's subject; a long one at the title's own size.
const size = topic.name.length <= 11 ? 'font-size: 84px; line-height: 1.04;' : ''
return {
name: picture(topic.id),
badge: topic.group ? `Beta · ${topic.group}` : 'Beta',
title: topic.name,
lede: `${topic.line}.`,
pills: ['In beta', 'Early access at hanzo.ai/beta'],
accent,
accentRgb,
visual: 'rows',
window: 'hanzo.ai/beta',
rows,
at: 0,
css: `.title { ${size} overflow-wrap: anywhere; } .row.on { margin-inline: -22px; padding-inline: 19px 22px; border-left: 3px solid ${accent}; background: rgba(${accentRgb}, 0.1); } .row.on .row-key { color: ${accent}; }`,
}
}
CARDS.push(...TOPICS.filter((t) => t.kind !== 'hanzo').map(topicCard))
function renderVisual(card) {
switch (card.visual) {
case 'terminal':
// The landing's demo (components/home/Demo.tsx), shortened to fit the card.
return `
<div class="mockup-terminal">
<div class="card-glow" style="--glow-rgb: ${card.accentRgb}"></div>
<div class="window-shell terminal-shell">
<div class="window-bar">
<span class="dot red"></span>
<span class="dot yellow"></span>
<span class="dot green"></span>
<span class="window-title">hanzo cli · zsh</span>
</div>
<div class="terminal-content">
<div class="line cmd"><span class="prompt">$</span> hanzo login</div>
<div class="line dim">Sign in to hanzo on any device:</div>
<div class="line out link">https://hanzo.id/device/WDJB-MJHT</div>
<div class="line dim">Waiting for approval…</div>
<div class="line success">✓ Signed in to hanzo as hanzo/ada</div>
<div class="line cmd" style="margin-top: 14px;"><span class="prompt">$</span> hanzo dev "build a shift board"</div>
<div class="line out">hanzo code dev · ~/shift-board · start</div>
<div class="line dim"> model routing: on → api.hanzo.ai</div>
<div class="line dim"> session stream: on → hanzo.bot</div>
<div class="line prompt-cursor"><span class="prompt">$</span> <span class="cursor"></span></div>
</div>
</div>
</div>
`
case 'pricing':
return `
<div class="mockup-pricing">
<div class="pricing-card">
<div class="plan-badge">FREE</div>
<div class="plan-price">$0</div>
<div class="plan-features">
<div>✓ Enso Free and Zen Free</div>
<div>✓ 50 free requests every day</div>
<div>✓ Search the web from chat</div>
</div>
</div>
<div class="pricing-card popular" style="--glow-rgb: ${card.accentRgb}">
<div class="plan-badge">PRO</div>
<div class="plan-price">$20<span>/month</span></div>
<div class="plan-features">
<div>✓ Build apps with Hanzo Dev</div>
<div>✓ Every model, with Enso Ultra and Kai</div>
<div>✓ Usage included on every model</div>
</div>
</div>
</div>
`
case 'rows':
return `
<div class="mockup-rows" style="--glow-rgb: ${card.accentRgb}">
<div class="card-glow"></div>
<div class="window-shell">
<div class="window-bar">
<span class="dot red"></span>
<span class="dot yellow"></span>
<span class="dot green"></span>
<span class="window-title">${card.window}</span>
</div>
<div class="rows">
${card.rows.map(([key, value], i) => `<div class="row${i === card.at ? ' on' : ''}"><span class="row-key${card.mono ? ' mono' : ''}">${key}</span><span class="row-value">${value}</span></div>`).join('')}
</div>
</div>
</div>
`
case 'api':
return `
<div class="mockup-code">
<div class="card-glow" style="--glow-rgb: ${card.accentRgb}"></div>
<div class="window-shell">
<div class="window-bar">
<span class="dot red"></span>
<span class="dot yellow"></span>
<span class="dot green"></span>
<span class="window-title">zsh</span>
</div>
<div class="code-content">
<span class="k">$</span> curl https://api.hanzo.ai/v1/chat/completions \\<br/>
&nbsp;&nbsp;-H <span class="s">"Authorization: Bearer $HANZO_API_KEY"</span> \\<br/>
&nbsp;&nbsp;-d <span class="s">'{"model": "enso", "stream": true,</span><br/>
<span class="s">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"messages": [{"role": "user",</span><br/>
<span class="s">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"content": "Hello"}]}'</span><br/><br/>
<span class="f">GET</span>&nbsp;&nbsp;/v1/models<br/>
<span class="f">POST</span> /v1/embeddings<br/>
<span class="f">GET</span>&nbsp;&nbsp;/v1/agents<br/>
<span class="f">GET</span>&nbsp;&nbsp;/v1/tools
</div>
</div>
</div>
`
case 'security':
return `
<div class="mockup-enterprise">
<div class="card-glow" style="--glow-rgb: ${card.accentRgb}"></div>
<div class="enterprise-box sec-box">
<div class="ent-header">
<div class="ent-shield">🔐</div>
<div>
<div class="ent-title">Each control is a mechanism</div>
<div class="ent-sub">Named on hanzo.ai/trust, readable in the source</div>
</div>
</div>
<div class="ent-badges sec-badges">
<span class="ent-badge">PKCE S256</span>
<span class="ent-badge">ARGON2ID</span>
<span class="ent-badge">HKDF-SHA256</span>
<span class="ent-badge">ML-DSA-65</span>
</div>
<div class="ent-metrics">
<div class="metric"><span class="m-val">1 file</span><span class="m-lbl">Per org on Base</span></div>
<div class="metric"><span class="m-val">Single-use</span><span class="m-lbl">Refresh tokens</span></div>
<div class="metric"><span class="m-val">Who · when</span><span class="m-lbl">Audit trail</span></div>
</div>
</div>
</div>
`
case 'enterprise':
return `
<div class="mockup-enterprise">
<div class="card-glow" style="--glow-rgb: ${card.accentRgb}"></div>
<div class="enterprise-box">
<div class="ent-header">
<div class="ent-shield">🛡️</div>
<div>
<div class="ent-title">Run it where you choose</div>
<div class="ent-sub">An uptime guarantee and security review in your contract</div>
</div>
</div>
<div class="ent-badges">
<span class="ent-badge">HANZO CLOUD</span>
<span class="ent-badge">YOUR VPC</span>
<span class="ent-badge">ON PREMISES</span>
<span class="ent-badge">AIR-GAPPED</span>
</div>
<div class="ent-metrics">
<div class="metric"><span class="m-val">Unlimited</span><span class="m-lbl">Members</span></div>
<div class="metric"><span class="m-val">Dedicated</span><span class="m-lbl">Compute</span></div>
<div class="metric"><span class="m-val">24/7</span><span class="m-lbl">Engineering support</span></div>
</div>
</div>
</div>
`
default:
throw new Error(`${card.name}: no visual named ${card.visual}`)
}
}
export function renderHtml(card) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
width: 1200px;
height: 630px;
background-color: #070709;
color: #fafafa;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 56px 64px;
}
/* Ambient background grid */
.bg-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(to right, rgba(255, 255, 255, 0.04) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
background-size: 48px 48px;
mask-image: radial-gradient(ellipse 90% 70% at 50% 0%, black 20%, transparent 80%);
pointer-events: none;
z-index: 0;
}
/* Top radial aura */
.bg-aura {
position: absolute;
top: -120px;
left: 50%;
transform: translateX(-50%);
width: 900px;
height: 480px;
background: radial-gradient(ellipse 60% 50% at 50% 0%, rgba(${card.accentRgb}, 0.22) 0%, rgba(${card.accentRgb}, 0.05) 55%, transparent 80%);
filter: blur(64px);
pointer-events: none;
z-index: 0;
}
.header {
position: relative;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.brand-name {
font-size: 26px;
font-weight: 700;
letter-spacing: -0.02em;
color: #ffffff;
}
.url-tag {
font-family: ui-monospace, Menlo, monospace;
font-size: 15px;
color: #a1a1aa;
padding: 6px 14px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 9999px;
}
.main {
position: relative;
z-index: 10;
display: grid;
grid-template-columns: 560px 480px;
gap: 32px;
align-items: center;
margin-block: auto;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 5px 14px;
border-radius: 9999px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(${card.accentRgb}, 0.35);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: ${card.accent};
margin-bottom: 20px;
}
.eyebrow-pip {
width: 7px;
height: 7px;
border-radius: 9999px;
background-color: ${card.accent};
box-shadow: 0 0 10px ${card.accent};
}
.title {
font-size: 54px;
font-weight: 800;
line-height: 1.12;
letter-spacing: -0.035em;
color: #ffffff;
margin-bottom: 18px;
}
.lede {
font-size: 21px;
line-height: 1.5;
color: #a1a1aa;
margin-bottom: 28px;
max-width: 520px;
}
.pills {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.pill {
font-size: 14px;
font-weight: 500;
color: #d4d4d8;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
padding: 6px 14px;
border-radius: 8px;
}
/* Mockups */
.mockup-terminal, .mockup-rows, .mockup-code, .mockup-enterprise, .mockup-pricing {
position: relative;
width: 100%;
}
.card-glow {
position: absolute;
inset: -12px;
background: radial-gradient(circle at 50% 50%, rgba(var(--glow-rgb), 0.25) 0%, transparent 70%);
filter: blur(28px);
z-index: 1;
}
.window-shell {
position: relative;
z-index: 2;
background: #111115;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 20px 50px rgba(0,0,0,0.6), 0 0 0 1px rgba(var(--glow-rgb), 0.2);
}
.window-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: #18181c;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.dot { width: 10px; height: 10px; border-radius: 9999px; }
.dot.red { background: #ef4444; }
.dot.yellow { background: #eab308; }
.dot.green { background: #22c55e; }
.window-title {
font-family: ui-monospace, Menlo, monospace;
font-size: 12px;
color: #71717a;
margin-left: 6px;
}
.k { color: #f43f5e; font-weight: 600; }
.f { color: #38bdf8; }
.s { color: #a3e635; }
/* Terminal */
.terminal-shell { background: #0c0c0e; }
.terminal-content {
padding: 20px;
font-family: ui-monospace, Menlo, monospace;
font-size: 13.5px;
line-height: 1.6;
}
.line { color: #a1a1aa; }
.line.cmd { color: #ffffff; font-weight: 600; }
.line.dim { color: #71717a; }
.line.out.link { color: #38bdf8; text-decoration: underline; text-underline-offset: 3px; }
.line.success { color: #34d399; font-weight: 600; margin-top: 4px; }
.prompt { color: #38bdf8; margin-right: 6px; }
.cursor {
display: inline-block;
width: 8px;
height: 15px;
background: #ffffff;
vertical-align: middle;
margin-left: 4px;
}
/* Pricing */
.mockup-pricing {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
position: relative;
z-index: 2;
}
.pricing-card {
background: #111115;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 14px;
padding: 22px 18px;
}
.pricing-card.popular {
border-color: rgba(var(--glow-rgb), 0.5);
background: #14141a;
box-shadow: 0 0 25px rgba(var(--glow-rgb), 0.2);
position: relative;
}
.plan-badge { font-size: 12px; font-weight: 700; color: #a1a1aa; letter-spacing: 0.08em; }
.plan-price { font-size: 34px; font-weight: 800; color: #fff; margin-block: 6px; }
.plan-price span { font-size: 14px; font-weight: 400; color: #a1a1aa; }
.plan-features div { font-size: 12.5px; color: #d4d4d8; margin-bottom: 6px; }
/* Models */
/* Code */
.code-content {
padding: 20px;
font-family: ui-monospace, Menlo, monospace;
font-size: 13.5px;
line-height: 1.6;
color: #e4e4e7;
}
/* Enterprise */
.enterprise-box {
position: relative;
z-index: 2;
background: #111115;
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: 16px;
padding: 24px;
box-shadow: 0 16px 40px rgba(0,0,0,0.5);
}
.ent-header { display: flex; gap: 14px; align-items: center; margin-bottom: 18px; }
.ent-shield { font-size: 32px; }
.ent-title { font-size: 17px; font-weight: 700; color: #fff; }
.ent-sub { font-size: 12.5px; color: #a1a1aa; }
.ent-badges { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
.ent-badge { font-size: 11px; font-weight: 700; color: #fbbf24; background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.25); padding: 4px 10px; border-radius: 6px; }
.sec-box { border-color: rgba(59, 130, 246, 0.35); }
.sec-badges .ent-badge { color: #93c5fd; background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.25); }
.ent-metrics { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; border-top: 1px solid rgba(255, 255, 255, 0.08); padding-top: 16px; }
.metric { text-align: center; }
.m-val { display: block; font-size: 20px; font-weight: 800; color: #fff; }
.m-lbl { font-size: 11px; color: #71717a; }
.rows { padding: 8px 22px 10px; }
.row { display: flex; flex-direction: column; gap: 4px; padding: 14px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.07); }
.row:last-child { border-bottom: none; }
.row-key { font-size: 16px; font-weight: 600; color: #fff; }
.row-key.mono { font-family: ui-monospace, Menlo, monospace; font-size: 15px; color: rgb(var(--glow-rgb)); }
.row-value { font-size: 14px; color: #a1a1aa; }
.footer {
position: relative;
z-index: 10;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid rgba(255, 255, 255, 0.07);
padding-top: 18px;
font-size: 14px;
color: #71717a;
}
${card.css ?? ''}</style>
</head>
<body>
<div class="bg-grid"></div>
<div class="bg-aura"></div>
<div class="header">
<div class="brand">
${HANZO_MARK_SVG}
<span class="brand-name">Hanzo AI</span>
</div>
<div class="url-tag">hanzo.ai</div>
</div>
<div class="main">
<div class="content">
<div class="eyebrow">
<span class="eyebrow-pip"></span>
${card.badge}
</div>
<h1 class="title">${card.title}</h1>
<p class="lede">${card.lede}</p>
<div class="pills">
${card.pills.map(p => `<span class="pill">${p}</span>`).join('')}
</div>
</div>
<div class="visual">
${renderVisual(card)}
</div>
</div>
<div class="footer">
<span>The AI cloud and app builder</span>
<span>Chat · Dev · API · Hanzo OS</span>
</div>
</body>
</html>
`
}
async function run() {
// Loaded here, not above: e2e/unit/cards.spec.ts imports this file for its words and draws nothing.
const { default: sharp } = await import('sharp')
const browser = await chromium.launch({ headless: true })
const page = await browser.newPage({ viewport: { width: 1200, height: 630 }, deviceScaleFactor: 2 })
const drawn = existsSync(MANIFEST) ? JSON.parse(readFileSync(MANIFEST, 'utf8')) : {}
const only = process.argv.slice(2)
for (const card of only.length ? CARDS.filter((c) => only.includes(c.name)) : CARDS) {
const html = renderHtml(card)
await page.setContent(html, { waitUntil: 'load' })
const path = fileURLToPath(new URL(`../${file(card.name)}`, import.meta.url))
// A palette of 256, dithered: a dark card with soft light looks the same at
// about a third of the bytes, and a share card stays a PNG.
const png = await sharp(await page.screenshot({ type: 'png' }))
.png({ palette: true, quality: 90, dither: 1, effort: 10, compressionLevel: 9 })
.toBuffer()
writeFileSync(path, png)
drawn[card.name] = { html: sha(html), png: sha(png) }
console.log(`drew ${file(card.name)}`)
}
await browser.close()
const names = CARDS.map((c) => c.name)
writeFileSync(MANIFEST, `${JSON.stringify(Object.fromEntries(names.filter((n) => drawn[n]).map((n) => [n, drawn[n]])), null, 2)}\n`)
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href)
run().catch((err) => {
console.error(err)
process.exit(1)
})