19 KiB
DESIGN.md — hanzo.ai design system
A short, opinionated design spec. Every product page on hanzo.ai obeys these rules — uniform feel, zero decorative color, all signal carried by typography and opacity. Mirrors the spirit of Material 3 / Google Web Design Spec but collapses to a strictly monochrome surface that suits an AI infrastructure brand.
This file lives next to LLM.md — LLM.md describes what's built, this file
describes how it should look.
1. Foundation
1.1 Voice
Direct, technical, understated. Never advertise; describe. No exclamation marks, no emoji in copy, no superlatives without numbers attached. One sentence per idea.
1.2 Brand atom
Monochrome. Black background, white type, neutral grays. No hue except where state requires it (see § 3 Color tokens / Semantic).
1.3 Layout grid
- Max content width:
max-w-7xl(1280px) for grids,max-w-3xl(768px) for centered text blocks - Horizontal padding:
px-4 sm:px-6 lg:px-8 - Vertical rhythm:
py-20 lg:py-32for hero sections,py-16topy-20for content sections,py-16for the final CTA
1.4 Density
Generous. The brand reads quiet. If a section feels crowded, remove a component before reducing spacing.
2. Color tokens
2.1 Source of truth
All color tokens live in lib/constants/brand.ts. CSS variables are
declared in app/globals.css/themes.css. Never hardcode hex literals
in component files. Two exceptions: the /brand page color swatches
themselves, and the SVG logo source.
2.2 Palette
These are read from @hanzo/design (tokens/colors.css), which is the one place
they are set. The values below are quoted so this table can be checked against
it — they are not a second declaration, and a copy that drifts is how a #000
page canvas outlived the token that replaced it.
| Token | Value | Use |
|---|---|---|
--background |
#0a0a0a |
Page surface |
--foreground |
#fafafa |
Primary type |
--brand |
#e4e4e7 |
Active accent (rare) |
--brand-muted |
#a3a3a3 |
Hover / muted accent |
text-white |
rgb(255 255 255) |
Headings, primary copy |
text-white/80 |
white @ 80% opacity | Secondary copy |
text-white/60 |
white @ 60% | Tertiary copy / captions |
text-muted-foreground |
neutral-400 | Helper text |
border-border |
neutral-800 | Default border |
border-white/10 to /30 |
borders w/ opacity | Card / pill borders |
2.3 Opacity scale (the real palette)
Hanzo's color palette is a single hue rendered through an opacity ladder. Use these steps and only these steps:
white/5 white/10 white/15 white/20 white/30 white/40 white/60 white/80 white
▒▒ ▒▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒
Higher opacity = higher rank. A "ultra" tier badge is bg-white/20,
"pro" is bg-white/15, "edge" is bg-white/5. Never reach for hue to
communicate rank.
2.4 Semantic colors (the only hued exceptions)
| Where | Color | Reason |
|---|---|---|
| Error / destructive states | red-500/10 bg + red-300/400 text |
User-blocking error or destructive confirmation |
| Success / online indicator | green-400/500 (often a tiny dot) |
Live-status pulse, "online" affordance |
| Pricing — "Free" / "Save N%" callouts | green-400 text |
Positive financial callout (UX convention) |
| macOS window-chrome dots | red-500/60 yellow-500/60 green-500/60 |
OS-affordance reproduction in code-window mocks |
| External brand logos (Discord blue, LinkedIn blue, Instagram pink, etc.) | brand's official hex | Recognizing third-party identity |
These are the ONLY places where hue is permitted. Anything else decorative — tier badges, language pills, category accents — must use the opacity scale (§ 2.3).
3. Typography
3.1 Family
font-sans is Zen, declared and self-hosted by @hanzo/design. Mono
blocks use font-mono, which is Zen Mono. No third typeface.
3.2 Type scale (Tailwind classes)
| Class | Use |
|---|---|
text-5xl lg:text-7xl font-bold |
Hero <h1> |
text-4xl font-bold |
Section <h2> |
text-2xl font-bold |
Subsection <h3> |
text-xl text-muted-foreground |
Hero tagline, lead paragraph |
text-sm text-muted-foreground |
Body |
text-xs text-muted-foreground |
Helper, footnote, badge label |
text-[10px] font-semibold uppercase tracking-wider text-muted-foreground |
Category eyebrow |
3.3 Hero h1 treatment
Wrap hero h1 text in <ChromeText> for the canonical chrome gradient
(from-white via-white/80 to-white/60 bg-clip-text text-transparent).
Never use a saturated rainbow gradient.
4. Components
4.1 Card
<div className="p-6 rounded-xl border border-neutral-800 bg-neutral-900/50">
<Icon className="h-6 w-6 mb-3 text-foreground/80" />
<h3 className="font-semibold mb-2">Title</h3>
<p className="text-sm text-muted-foreground">Body…</p>
</div>
Variants: bg-neutral-900/50 for grid items, bg-neutral-900/80 for
emphasis, bg-gradient-to-br from-neutral-800/60 to-neutral-900/60 for
hero/featured tiles.
4.2 Button (primary / secondary)
NEVER use <Button asChild> — it expects exactly one child element and
breaks the build under React 18 strict children. Use plain <a> or
<Link> with className strings:
{/* Primary */}
<Link href="/x" className="inline-flex items-center justify-center gap-2 bg-primary text-primary-foreground hover:bg-primary/90 px-6 py-3 rounded-md text-sm font-medium">
Get started <ArrowRight className="h-4 w-4" />
</Link>
{/* Secondary */}
<Link href="/y" className="inline-flex items-center justify-center gap-2 border border-border hover:bg-accent px-6 py-3 rounded-md text-sm font-medium">
Learn more
</Link>
4.3 Badge / pill
<div className="bg-primary/10 border border-border rounded-full px-4 py-1 inline-block">
<span className="text-sm flex items-center gap-2">
<Icon className="w-4 h-4" /> Label
</span>
</div>
For tier / language / category badges, use § 2.3 opacity scale.
4.4 Hero pattern (every product page starts with this)
<section className="py-20 lg:py-32 relative">
<div className="absolute inset-0 bg-gradient-to-b from-white/10 to-transparent" />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<div className="text-center max-w-3xl mx-auto mb-16">
{/* eyebrow badge */}
<h1 className="text-5xl lg:text-7xl font-bold mb-6">
<ChromeText>Product Title</ChromeText>
</h1>
<p className="text-xl text-muted-foreground mb-8">
One-sentence value prop, ≤140 chars.
</p>
{/* Primary + secondary CTA row */}
</div>
</div>
</section>
4.5 Final resources block (every product page ends with this)
<section className="py-16 border-t border-neutral-800">
<div className="max-w-3xl mx-auto px-4 text-center">
<h2 className="text-2xl font-bold mb-4">Get started with <Product></h2>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<a href="https://docs.hanzo.ai/<slug>" target="_blank" rel="noopener noreferrer" className="...primary button classes...">
Read the docs <ArrowRight className="h-4 w-4" />
</a>
<a href="https://github.com/hanzoai/<slug>" target="_blank" rel="noopener noreferrer" className="...secondary button classes...">
View on GitHub
</a>
</div>
</div>
</section>
This block is mandatory on every product page — e2e/products.spec.ts
asserts both links exist and resolve.
5. Motion
- Library:
framer-motion - Hero entry:
initial={{ opacity: 0, y: 20 }}→animate={{ opacity: 1, y: 0 }}withtransition={{ duration: 0.5 }}and per-elementdelay: 0.05 * indexstagger - Hover: pure CSS via
transition-colors, never JS-animated - Background motion: a single radial gradient pulse, 8s ease-in-out infinite
- No bouncy springs. No carousel auto-play. No parallax.
6. Iconography
- Library:
lucide-react. No other icon set. - Sizing:
h-3 w-3(inline next to body text),h-4 w-4(CTA button),h-6 w-6(card eyebrow),h-10 w-10(section accent) - Color: inherit, or
text-foreground/80for emphasis,text-muted-foregroundfor inline body icons - Stroke: default
Allow-listed semantic icons (used consistently across pages):
- Primary action:
ArrowRight - Code / SDK:
Code,Terminal - Docs:
FileText - Settings / config:
Settings - Identity / user:
UserCheck - Security / locks:
Shield,ShieldCheck,Lock,KeyRound
7. Page rules
7.1 Every product page MUST
- Live at
/<slug>underapp/(marketing)/<slug>/page.tsx - Have a unique
<h1>— no two product pages share the same title - Have a unique first-600-char body signature (asserted by
e2e/products.spec.ts) - End with the resources block (§ 4.5)
- Carry only the colors permitted by § 2.3 / § 2.4
7.2 Every product MUST be in the menu
- Listed in
productsNav(lib/constants/navigation-data.ts) - Carries
github:field linking to a realhttps://github.com/hanzoai/<repo>URL (or org root if private/non-existent) - Carries
docs:field linking tohttps://docs.hanzo.ai/<slug>
7.3 Forbidden
<Button asChild>— breaks build- Hardcoded hex literals (use CSS vars or Tailwind tokens)
- Decorative hue (only § 2.4 semantic uses permitted)
- Multiple typefaces
- Multiple URL schemes for the same product (no more
/products/<cat>/<x>and/<x>; pick/<x>) :latest/:main/:devimage tag references on cluster pins (semver only — see~/work/hanzo/CLAUDE.md)
8. Accessibility (non-negotiable)
- Contrast: every body-text on background combination clears WCAG AA at 16px
- Focus rings: visible — never
outline-nonewithout a custom ring - Keyboard: every dropdown / dialog / mobile menu reachable via Tab; closeable via Escape
- Reduced motion: honor
prefers-reduced-motion— disable infinite gradient pulses - Alt text: every
<img>and decorative SVG either has alt oraria-hidden="true"
9. Verification
Before shipping a new page or design change:
# Build clean
pnpm build
# Serve static export
(cd out && python3 -m http.server 8086) &
# Run product e2e (uniqueness + GitHub-link + page-live tests)
BASE_URL=http://localhost:8086 pnpm test e2e/products.spec.ts
The e2e suite gates: page lives, h1 unique, body unique, GitHub link non-404. If any of those fail, the design isn't done.
10. The product grammar
Four kinds of thing, and nothing is two of them. Every page, every nav label and every sentence uses these words in these senses.
| what it is | |
|---|---|
| Hanzo | the open AI cloud. The computer. |
| Enso | the agent. The intelligence. |
| Hanzo VM | the substrate — a microVM that boots in milliseconds and runs the same image on a laptop or in the cloud |
| IDE · Browser · CLI · Cloud | surfaces. Where a person works with Enso. |
| Extensions | Hanzo inside somebody else's surface |
| /v1 | the one interface, the same in all three environments |
ENSO
agent
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
HANZO IDE HANZO BROWSER HANZO CLI
└─────────────────┼─────────────────┘
│
/v1
│
HANZO VM
│
HANZO
Open AI Cloud
Read down: Hanzo is the computer. Enso is the intelligence. IDE, Browser and CLI are where you work with it. Build what's next sits above all of it.
Two consequences worth stating, because both get asked:
- Hanzo IDE competes with Cursor, and Hanzo also works inside Cursor. Both are true and neither is a hedge. Say "Bring Hanzo with you", never "Cursor compatible" — the second makes Cursor the platform.
- Hanzo VM is why the handoff is real. "Start on a laptop, close it, it keeps going" is not a redeploy; it is the same image moving. Introduce the VM wherever that claim is made, and nowhere as a product tile of its own.
10.1 The chain is plumbing, not the thesis
Linking machines into shared compute is a differentiator; it is not the pitch.
It appears in the language of COMPUTE — your idle machines are a cluster — on
/cloud and /network. Settlement, identity, provider registry and metering
are Lux underneath, and they belong on /enterprise and /research, where the
reader is already asking the question they answer. At most one sentence on the
apex. Leading with the chain narrows the audience to people who already agree.
11. The dot matrix has a grammar
The lattice is not decoration and not a per-page effect. It is how this site draws computation, so a reader learns to read it once and it means the same thing everywhere. Specify an animation in these terms or do not add it.
| element | meaning |
|---|---|
| dot | information, or a unit of compute |
| cluster | a machine, an application, a logical component |
| bright | active work |
| movement | data or control flowing |
| split | parallelism, agents |
| join | synthesis |
| boundary | security, locality |
| density | scale of compute |
| pulse | an event |
11.1 The six verbs
Every animation on the site is a composition of these. There are no others.
MATERIALIZE the system forms around a request
········ ··▒▒·· ▒████▒
········ → ·▒██▒· → ██████
ROUTE a request takes one path of several
● · · · · · ○
FORK one becomes many JOIN many become one
● ● ● ●
╱│╲ ╲│╱
● ● ● ●
HANDOFF the same work, somewhere else POLICY authority, not secrets
LOCAL CLOUD agent → │ boundary │ → API
█████ · · · → █████ ↑ policy
POLICY carries the one claim the architecture has to make in five seconds:
the secret never moves. Draw the credential stationary inside the KMS
boundary and the authorization travelling. An animation that moves the key is
wrong about the product.
11.2 What each page's field does
The verbs, assigned. A page's field shows the thing the page is claiming.
| page | line | the field |
|---|---|---|
/ |
Build what's next. | MATERIALIZE — the whole system forms from one prompt |
/enso |
One agent. Every surface. | FORK then JOIN — one core into tasks, tools, models, and back |
/ide |
Code with the whole cloud behind you. | the file tree becomes a dependency graph; edits propagate |
/browser |
The web is a tool. | a rendered page decomposes into its DOM; the action path lights |
/cli |
An AI engineer in your terminal. | glyphs leave the terminal and assemble into a topology |
/cloud |
Keep it running. | HANDOFF — one local node expands into a cluster |
/extensions |
Use ours. Or keep yours. | external surfaces plug into one core |
/models |
Any model. One interface. | ROUTE — the request visibly takes a path |
/kai |
Decision intelligence. | JOIN then FORK — evidence streams join one shared state, typed decisions fork out of it; the uncertain pulse once and resolve last |
/developers |
One API. All of Hanzo. | three environments collapse onto one endpoint |
/research |
Research that ships. | the lattice stops being UI and becomes a field, mesh, wave |
/enterprise |
Your AI infrastructure. Your rules. | POLICY — boundaries, and a secret that does not move |
11.3 Continuity
The field does not tear down on navigation. It REORGANIZES: /enso's core
becomes /ide's repository graph, which collapses into an agent and moves into
/cloud's cluster, which collapses into /models' endpoints, which spread into
/research's numerical field. The reader should come away feeling this is all
one machine, which is the product claim.
At rest the loose dots fall slowly back into the Hanzo mark, which acts as a gravity source in the corner. Interaction dissolves it back into computation. Very slow, very restrained. No gradients.
11.4 The bright dot is the work
Not the agent, not the model, not the machine — those are clusters, and they are all replaceable. The single brightest dot in any field is the piece of work itself, and it is the SAME dot from the first frame to the last. It survives HANDOFF between surfaces (App, CLI, Cloud, editor, phone), and it survives a navigation, because the lattice does.
That is the one sentence the product is: your work survives the surface. A page that draws a second bright dot has said there are two pieces of work, and had better mean it.
11.5 Where it lives
lib/lattice.ts is the engine and the only place the grammar is written.
Everything is a Field — brightness at a point, given pixels and a clock — so a
verb is a Field factory, sequence is a maximum, and the backdrop stays one
loop over one grid with no per-page machinery.
materialize(where, radius, beat) route(from, to, beat)
fork(from, [to…], beat) join([from…], to, beat)
handoff(from, to, beat) policy(gate, from, to, beat)
work([{ where, at }…]) the bright dot, and its itinerary
Every verb takes a beat — at and span, in seconds from the moment the field
was shown — is zero before at, and HOLDS ITS END STATE after. That last rule
is what lets a topology assemble and stay drawn, what makes composition a plain
maximum, and what gives a reduced-motion reader the finished picture for free.
Positions are fractions of the viewport and distances are measured in width units, so a scene is written once and is correct at every size. The verbs read their points inside the field rather than capturing coordinates, so a page can measure its own elements into the same objects on scroll and the drawing follows.
A page binds a scene with components/backdrop/MatrixStage — while mounted, or
within an element, or for a hold in seconds. An INTERACTION that has to
outlive its component across a navigation calls show directly; components/ home/Ask.tsx is the one of those.
11.6 The rule that keeps it honest
A field that does not correspond to something the software actually does is decoration, and decoration here is a bug. If a page cannot say which verb its animation is and what the dots stand for, it does not get one.
12. When in doubt
Look at /cloud, /iam, /insights, /dev — these four pages are the
shape every other product page should match. If your page reads visibly
different from those four, something is wrong.
The brand looks black, white, and quiet. That's the whole spec.