Files
hanzo.ai/DESIGN.md

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-32 for hero sections, py-16 to py-20 for content sections, py-16 for 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 }} with transition={{ duration: 0.5 }} and per-element delay: 0.05 * index stagger
  • 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/80 for emphasis, text-muted-foreground for 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

  1. Live at /<slug> under app/(marketing)/<slug>/page.tsx
  2. Have a unique <h1> — no two product pages share the same title
  3. Have a unique first-600-char body signature (asserted by e2e/products.spec.ts)
  4. End with the resources block (§ 4.5)
  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 real https://github.com/hanzoai/<repo> URL (or org root if private/non-existent)
  • Carries docs: field linking to https://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 / :dev image 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-none without 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 or aria-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.