---
version: alpha
name: mindmap.io
description: >
  A calm paper-toned canvas (#f7f7f4) with one living color: teal (#0d9488).
  The graphene lattice — haloed nodes and bonds — is the brand motif; the
  interface reads as a still, quiet frame with one living thing expanding
  inside it. Satoshi across every surface, white node-cards with hairline
  borders and soft shadows, restrained motion that animates branching and
  almost nothing else. No second chromatic color, no gradients on UI, no
  decoration that does not earn its place.

colors:
  # Ground
  canvas: "#f7f7f4"             # paper — the page background everywhere
  surface: "#ffffff"            # cards, nodes, dropdowns, modals
  scrim: "rgba(247, 247, 244, 0.8)"  # nav/footer panels over the lattice, always with backdrop-blur 12px
  # Accent (the ONLY chromatic color on UI surfaces)
  accent: "#0d9488"             # teal — primary CTAs, focus, links, the living node
  accent-hover: "#0f766e"       # darker teal — hover state of any accent fill
  accent-tint: "rgba(13, 148, 136, 0.1)"   # icon chips, subtle accent backgrounds
  accent-glow: "rgba(13, 148, 136, 0.12)"  # shadow tint for accent-bordered cards
  # Ink
  ink: "#1a1a2e"                # headings and primary text
  ink-muted: "#6b6b7b"          # body copy, secondary text
  ink-subtle: "#9b9bab"         # footer text, tertiary labels
  # Lines
  hairline: "#e5e5e0"           # default 1px border on warm surfaces
  node-border: "#d1d5db"        # 1.5px border on map node cards (cool gray, matches app)
  connector: "#9ca3af"          # map connector strokes and dot-grid dots
  # Semantic states (state feedback only — never decorative)
  error: "#dc2626"              # destructive actions, error icons and text
  error-soft: "#f87171"         # failed node-card borders
  warning: "#f59e0b"            # warning icons and text
  warning-soft: "#fbbf24"       # interrupted node-card borders
  # Node color coding (map objects only — never chrome, never text)
  node-tint-rose: "#fbeaec"
  node-tint-pink: "#faecf5"
  node-tint-amber: "#f9efdd"
  node-tint-sage: "#e8efe9"
  node-tint-mint: "#e4f4f2"
  node-tint-sky: "#e7f2fa"
  node-tint-lavender: "#f0eef9"
  node-tint-clay: "#f4ede9"
  # Same pastels at source strength — small marks only (the drawer's palette icon), never fills
  node-hue-rose: "#e15f6e"
  node-hue-pink: "#d65faf"
  node-hue-amber: "#d9961e"
  node-hue-sage: "#6e9b78"
  node-hue-mint: "#3cb4a0"
  node-hue-sky: "#3791d2"
  node-hue-lavender: "#8273cd"
  node-hue-clay: "#af7d5f"
  # Brand mark only (never UI fills)
  brand-gradient-start: "#41c9b9"
  brand-gradient-end: "#0a5f58"
  lattice-node: "#6b6b7b"
  lattice-bond: "#9b9bab"

typography:
  font-family: "'Satoshi', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif"
  hero:
    size: "clamp(2.5rem, 6vw, 4rem)"
    weight: 700
    line-height: 1.1
    letter-spacing: "-0.02em"
  section-title:
    size: "clamp(1.5rem, 1.5vw + 1rem, 2rem)"   # --text-2xl
    weight: 700
    letter-spacing: "-0.01em"
  card-title:
    size: "clamp(1.25rem, 1vw + 1rem, 1.5rem)"  # --text-xl
    weight: 600
  subhead:
    size: "clamp(1.125rem, 1.5vw + 0.875rem, 1.375rem)"  # --text-hero-sub
    weight: 400
    line-height: 1.6
  body-lg:
    size: "clamp(1.0625rem, 0.75vw + 0.9375rem, 1.25rem)"  # --text-lg
    weight: 400
  body:
    size: "clamp(0.9375rem, 0.5vw + 0.875rem, 1rem)"  # --text-base
    weight: 400
    line-height: 1.6
  body-sm:
    size: "clamp(0.8125rem, 0.5vw + 0.75rem, 0.875rem)"  # --text-sm
    weight: 400
    line-height: 1.5
  caption:
    size: "0.75rem"   # --text-xs — chips, badges, watermark, drawer labels
    weight: 500
  nav-link:
    size: "{typography.body-sm.size}"
    weight: 500
  button:
    size: "{typography.body-lg.size}"
    weight: 600
  weights: { normal: 400, medium: 500, semibold: 600, bold: 700 }

rounded:
  sm: 6px        # icon chips, small controls
  md: 12px       # buttons, dropdowns, media frames, message bubbles
  lg: 16px       # node cards, primary CTAs, modals
  full: 9999px   # dots, avatars

spacing:
  base: 4px
  scale: [4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96]  # --space-1..24 in px
  section: 80px      # vertical padding of a landing section (--space-20)
  gutter: 16px       # horizontal page padding (--space-4)

elevation:
  card: "0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06)"
  card-hover: "0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.04)"
  node: "0 1px 3px rgba(0, 0, 0, 0.06)"
  node-active: "0 2px 8px {colors.accent-glow}"
  control: "0 1px 2px 0 rgba(0, 0, 0, 0.05)"        # app toolbar buttons
  control-hover: "0 2px 4px 0 rgba(0, 0, 0, 0.08)"
  dropdown: "0 4px 12px rgba(0, 0, 0, 0.1)"
  modal: "0 8px 24px rgba(0, 0, 0, 0.12)"

motion:
  ease-out: "cubic-bezier(0.22, 1, 0.36, 1)"   # entrances, exits, draws — the house curve
  ease-micro: "ease"                            # color/background hover fades
  duration-micro: 150ms      # hover color/background changes
  duration-control: 200ms    # button lift, dropdown, small reveals
  duration-ui-max: 300ms     # hard ceiling for anything in the app
  duration-entrance: 600ms   # marketing-only card entrances
  duration-draw: 750ms       # marketing-only connector line draws
  entrance-transform: "translateY(10px) scale(0.97)"  # where entering cards start
  pressed: "scale(0.97)"     # :active on pressables

layout:
  max-width: 1120px
  nav-height: 64px
  breakpoint-desktop: 768px

components:
  nav:
    background: "{colors.scrim}"
    effects: "backdrop-filter: blur(12px); position: sticky"
    border-bottom: "1px solid {colors.hairline}"
    height: "{layout.nav-height}"
  nav-link:
    typography: "{typography.nav-link}"
    color: "{colors.ink-muted}"
  nav-link-hover:
    color: "{colors.ink}"
    transition: "color {motion.duration-micro} {motion.ease-micro}"
  nav-cta:
    background: "{colors.accent}"
    color: "#ffffff"
    padding: "8px 20px"
    rounded: "{rounded.md}"
    weight: 600
  nav-cta-hover:
    background: "{colors.accent-hover}"
    transition: "background {motion.duration-micro} {motion.ease-micro}"
  cta-primary:
    background: "{colors.accent}"
    color: "#ffffff"
    padding: "12px 32px"
    rounded: "{rounded.lg}"
    typography: "{typography.button}"
  cta-primary-hover:
    background: "{colors.accent-hover}"
    transition: "background {motion.duration-micro} {motion.ease-micro}"
  cta-secondary:
    background: "transparent"
    color: "{colors.ink-muted}"
    typography: "{typography.body} at weight 500"
  cta-secondary-hover:
    color: "{colors.accent}"
  card:
    background: "{colors.surface}"
    border: "1px solid {colors.hairline}"
    rounded: "{rounded.md}"
    shadow: "{elevation.card}"
  card-hover:
    shadow: "{elevation.card-hover}"
  node-card:
    background: "{colors.surface}"
    border: "1.5px solid {colors.node-border}"
    rounded: "{rounded.lg}"
    shadow: "{elevation.node}"
    padding: "8px 16px"
  node-card-active:
    border: "1.5px solid {colors.accent}"
    shadow: "{elevation.node-active}"
  node-card-failed:
    border: "1.5px solid {colors.error-soft}"
    shadow: "0 2px 8px rgba(239, 68, 68, 0.25)"
  node-card-interrupted:
    border: "1.5px solid {colors.warning-soft}"
  node-send-button:
    background: "{colors.accent-tint}"
    color: "{colors.accent}"
    rounded: "{rounded.sm}"
  connector:
    stroke: "{colors.connector}"
    stroke-width: 1.5
    linecap: round
    shape: "cubic bezier from parent edge to child edge, control points at horizontal midpoint"
  icon-chip:
    size: "48px"
    background: "{colors.accent-tint}"
    color: "{colors.accent}"
    rounded: "{rounded.sm}"
    icon: "24px, stroke-width 1.5, Lucide"
  media-frame:
    border: "1px solid {colors.hairline}"
    rounded: "{rounded.md}"
    overflow: hidden
    aspect-ratio: "4 / 3"
  dot-grid:
    pattern: "radial-gradient({colors.connector} 1px, transparent 1px)"
    size: "22px 22px"
    opacity: 0.16
  graphene-backdrop:
    asset: "/graphene-bg.svg"
    position: "fixed, z-index -1"
    effects: "blur(0.25px), opacity 0.7"
  footer:
    background: "{colors.scrim}"
    effects: "backdrop-filter: blur(12px)"
    border-top: "1px solid {colors.hairline}"
    color: "{colors.ink-subtle}"
    typography: "{typography.body-sm}"
  user-bubble:
    background: "#eff6ff"
    color: "#1f2937"
    rounded: "{rounded.md}"
    padding: "8px 12px"
    weight: 500
  toolbar-button:
    background: "{colors.surface}"
    border: "1px solid {colors.node-border}"
    rounded: "8px"
    shadow: "{elevation.control}"
    typography: "{typography.body-sm} at weight 500"
  toolbar-button-hover:
    shadow: "{elevation.control-hover}"
    transform: "translateY(-2px)"
    transition: "all {motion.duration-control} {motion.ease-micro}"
  dropdown-surface:
    background: "{colors.surface}"
    border: "1px solid {colors.hairline}"
    rounded: "8px"
    shadow: "{elevation.dropdown}"
  menu-item:                       # rows inside dropdown-surface (context menu, export menu)
    typography: "{typography.body-sm} at weight 500"
    color: "{colors.ink}"
    hover-background: "{colors.canvas}"
    padding: "6px 12px"
    shortcut-hint: "{colors.ink-subtle}, tabular numerals"
  menu-item-danger:
    color: "{colors.error}"
    hover-background: "rgba(220, 38, 38, 0.05)"
  canvas-control:                  # mini round controls on a node card's child edge (ADR 0025)
    size: "24px, circular"
    background: "{colors.surface}"
    border: "1px solid {colors.node-border}"
    color: "{colors.ink-muted}"
    shadow: "{elevation.control}"
    hover: "border and icon recolor to {colors.accent} — recolor, never thicken"
  canvas-badge:                    # collapsed-branch hidden-count badge; doubles as the expand affordance
    background: "{colors.surface}"
    border: "1px solid {colors.node-border}"
    color: "{colors.ink-muted}"
    rounded: "full"
    shadow: "{elevation.control}"
    hover: "border and count recolor to {colors.accent} — same quiet treatment as canvas-control"
    typography: "12px bold, tabular numerals"
  modal-surface:
    background: "{colors.surface}"
    rounded: "{rounded.lg}"
    shadow: "{elevation.modal}"
    overlay: "rgba(255, 255, 255, 0.6) with backdrop-blur 2px"
  text-input:
    background: "{colors.surface}"
    border: "1px solid {colors.hairline}"
    rounded: "8px"
    padding: "12px 16px"
    color: "{colors.ink}"
    placeholder: "{colors.ink-subtle}"
  text-input-focused:
    border: "1px solid {colors.accent}"
    ring: none
  text-input-disabled:
    background: "{colors.canvas}"
    color: "{colors.ink-subtle}"
  checkbox:
    accent-color: "{colors.accent}"
  toggle:
    track-on: "{colors.accent}"
    track-off: "#d1d5db"
    transition: "background {motion.duration-micro} {motion.ease-micro}"
  key-badge:
    background: "{colors.canvas}"
    border: "1px solid {colors.hairline}"
    rounded: "{rounded.sm}"
    typography: "{typography.body-sm} at weight 500, {colors.ink-muted}"
  node-card-editing:
    border: "1.5px solid {colors.accent}"
    shadow: "{elevation.node-active}"
    editor: "textarea inside the user bubble, view-mode rendering preserved: the prompt bubble keeps {components.user-bubble} styling and the response stays visible below — nothing vanishes, nothing recolors"
    rules: "the caret is the only editing signal; there is no mode badge or mode chrome anywhere. Click-away and zoom-level steps commit the text (never regenerate — regeneration stays behind an explicit Enter + confirm)"
  edit-spotlight:
    dim: "all other node cards to opacity 0.4, connectors to 0.3 (same treatment as search dimming)"
    transition: "opacity {motion.duration-control} {motion.ease-micro}"
    rules: "while a node is edited the rest of the map recedes; the edited node keeps full opacity — one living thing at a time"
  tooltip:
    background: "#111827"
    color: "#ffffff"
    typography: "{typography.caption}"
    rounded: "6px"
    motion: "opacity fade, {motion.duration-micro}; never scale, never on keyboard focus"
  brand-node:
    shape: "circle, hard-stop diagonal gradient {colors.brand-gradient-start} -> {colors.brand-gradient-end} (upper-left to lower-right)"
    companion: "optional solid {colors.lattice-node} grey node, always smaller than the teal one"
    sizes: "10-14px accents and lockups; 24-40px loading and empty-state art"
    rules: "decorative only: aria-hidden, never a button, never under text, one cluster per surface; pulse/pop on transform+opacity only, never spin; static under reduced motion"
---

# mindmap.io DESIGN.md

The agent-readable source of truth for how every mindmap.io surface looks,
moves, and reads. Landing page, app, embeds, videos, OG images — all of it.
If a UI or copy decision is not answered here, the answer is in
`brand/BRAND-IDENTITY.md`; if it is not there either, propose it, don't invent it.

This file is authoritative (ADR 0024). Its values were originally extracted
from the landing implementation, but the relationship now runs the other way:
`marketing/src/styles/tokens.css`, the app `@theme` block in `index.css`, and
`shared/theme.ts` are implementations that must conform to this spec (drift
tests pin the core palette). Reference tokens by their `{colors.x}` /
`{typography.y}` / `{motion.z}` keys — never restate raw hex in new specs or
code comments.

## 1. Visual theme & atmosphere

mindmap.io is **a still, quiet frame with one living thing expanding inside
it**. The brand metaphor is graphene: a lattice of organized thought — strong,
minimal, all structure.

Key characteristics:

- Warm paper canvas ({colors.canvas}), never pure white pages. White
  ({colors.surface}) is reserved for things that sit *on* the paper: cards,
  nodes, panels.
- **One chromatic color.** Teal ({colors.accent}) marks the living thought:
  primary CTAs, the active node, focus, links. Everything else is ink and
  paper. There is no secondary accent, no warning-orange headline, no
  gradient buttons. The single carve-out is **user-chosen node color**
  ({colors.node-tint-*}, see Map anatomy) — the user's meaning, not the
  product's voice, and it never leaves the map objects.
- Hairline borders (1px {colors.hairline}) and soft, low shadows carry
  hierarchy. Depth is whispered, not shouted.
- The graphene lattice appears as a faint fixed backdrop
  ({components.graphene-backdrop}) and as the logo motif. Halos — the gaps
  where bonds stop short of nodes — are genuine transparent negative space,
  never white discs.
- Density is calm: one idea per section, generous whitespace, content
  column capped at {layout.max-width}.
- Motion is scarce and physical. The one thing worth animating is
  **branching** — a connector drawing from parent to child, then the child
  card landing. Chrome never dances.

## 2. Color palette & roles

### Ground

- **Canvas** ({colors.canvas}): the page background of every surface,
  marketing and app. Warm paper, not gray.
- **Surface** ({colors.surface}): node cards, feature cards, dropdowns,
  modals — anything lifted off the paper.
- **Scrim** ({colors.scrim}): translucent paper for nav and footer panels,
  always paired with `backdrop-filter: blur(12px)` so the lattice reads
  through without fighting text.

### Accent

- **Accent** ({colors.accent}): the single chromatic color. Used ONLY for:
  primary CTA fills, the accented headline word, active/selected node
  borders, focus rings, link hovers, the living teal node in the lattice.
- **Accent hover** ({colors.accent-hover}): hover state of any accent fill.
- **Accent tint** ({colors.accent-tint}): icon chip backgrounds and other
  sub-10%-alpha accent washes.
- **Accent glow** ({colors.accent-glow}): the shadow tint under
  accent-bordered cards ({components.node-card-active}).

### Ink

- **Ink** ({colors.ink}): headings, primary text, the wordmark.
- **Ink muted** ({colors.ink-muted}): body copy, descriptions, secondary
  labels. Most paragraphs are this, not {colors.ink}.
- **Ink subtle** ({colors.ink-subtle}): footer links, tertiary metadata.

### Lines

- **Hairline** ({colors.hairline}): the default border. 1px, warm.
- **Node border** ({colors.node-border}): map node cards use this cooler
  gray at 1.5px — it matches the app canvas and reads as "map object", not
  "page card".
- **Connector** ({colors.connector}): SVG connector strokes and the hero
  dot-grid.

### Semantic states

- **Error** ({colors.error}, soft {colors.error-soft}) and **warning**
  ({colors.warning}, soft {colors.warning-soft}): state feedback only —
  failed/interrupted nodes, destructive confirms, error text. Never
  decorative, never a highlight, never a second brand color.

### Brand mark only

- **Brand gradient** ({colors.brand-gradient-start} →
  {colors.brand-gradient-end}, upper-left to lower-right diagonal): the
  teal node in the logo, favicon, and lattice motif. Never a UI fill,
  never a button, never text.
- **Brand node** ({components.brand-node}, `components/brand/BrandNode`):
  the one sanctioned way to place the mark inside UI — loading states,
  empty states, the sidebar lockup, the sign-in moment. Sparingly: one
  cluster per surface, and the grey lattice node is its only companion
  (never a second chromatic color).
- **Lattice** ({colors.lattice-node} nodes, {colors.lattice-bond} bonds):
  the grey structure of the motif.

## 3. Typography

One family everywhere: **Satoshi** (variable, weights 300–900), self-hosted
woff2 with `font-display: swap`, preloaded in the layout head. Fallback
stack per {typography.font-family}. No serif, no mono display face, no
second family on any surface.

| Style | Size | Weight | Notes |
|---|---|---|---|
| hero | clamp(2.5rem, 6vw, 4rem) | 700 | line-height 1.1, letter-spacing -0.02em |
| section-title | clamp(1.5rem, …, 2rem) | 700 | letter-spacing -0.01em |
| card-title | clamp(1.25rem, …, 1.5rem) | 600 | feature headings |
| subhead | clamp(1.125rem, …, 1.375rem) | 400 | section intro under a title, {colors.ink-muted} |
| body-lg | clamp(1.0625rem, …, 1.25rem) | 400 | lead paragraphs, button labels |
| body | clamp(0.9375rem, …, 1rem) | 400 | default copy, line-height 1.6 |
| body-sm | clamp(0.8125rem, …, 0.875rem) | 400–500 | nav links, node text, footer |
| caption | 0.75rem | 500 | chips, badges, watermark, drawer labels |

Principles:

- The whole scale is fluid (`clamp()`); never set a fixed px font-size on
  marketing surfaces.
- Display sizes tighten: -0.02em at hero, -0.01em at section titles, 0 at
  body. Never positive tracking.
- Hierarchy comes from weight and ink level before size: a 500-weight
  {colors.ink-muted} label beats adding another size to the scale.
- Accent one word, not phrases: the headline highlights a single key word
  in {colors.accent} (`Think <accent>outside</accent> the chatbox`).

## 4. Layout & spacing

- Base unit 4px; the scale is {spacing.scale} — pick from it, never
  invent 14px/18px one-offs.
- Content column: max-width {layout.max-width}, centered, with
  {spacing.gutter} horizontal padding.
- Sections separate by whitespace ({spacing.section} vertical), not by
  alternating background bands. The paper is continuous; the lattice
  backdrop is the only texture.
- Landing feature rows: 2-column grid ≥768px (text | media, alternating
  via `reverse`), single column below.
- The nav is sticky ({components.nav}); the hero fills
  `100vh - {layout.nav-height}` and centers its content.
- Whitespace philosophy: the calm IS the brand. When a section feels
  empty, resist filling it — tighten the copy instead.

## 5. Component stylings

### Buttons

- **Primary CTA** ({components.cta-primary}): accent fill, white text,
  {rounded.lg}, 12px × 32px padding. Hover → {components.cta-primary-hover}
  (darken, nothing moves). One per view section, maximum.
- **Nav CTA** ({components.nav-cta}): the compact primary — same colors,
  {rounded.md}, 8px × 20px.
- **Secondary CTA** ({components.cta-secondary}): bare text in
  {colors.ink-muted}, hover → {colors.accent}. No border, no background —
  the quiet option next to the one loud one.
- **App toolbar button** ({components.toolbar-button}): white, 1px
  {colors.node-border} border, {elevation.control}; hover lifts
  `translateY(-2px)` with {elevation.control-hover}; `:active` returns to
  rest. Disabled: 40% opacity, no lift, no shadow change.
- All pressables compress on press: `transform: {motion.pressed}`.
- **In app code, every button IS the primitive**: `components/ui/Button`
  (variants primary/secondary/ghost/danger/toolbar) or
  `components/ui/IconButton` (icon-only; label required — it renders the
  shared tooltip and the aria-label). A new look is a new variant on the
  primitive, never a hand-rolled `<button>`; a ratchet test
  (`__tests__/lint/ui-primitives-ratchet.test.ts`) fails on new raw buttons.
- **One tooltip** ({components.tooltip}, `components/Tooltip.tsx`): every
  icon-only clickable gets one; no native `title`, no second tooltip pattern.

### Cards & surfaces

- **Card** ({components.card}): white on paper, hairline border,
  {rounded.md}, {elevation.card}; hover may raise to {elevation.card-hover}.
- **Node card** ({components.node-card}): the map object. 1.5px
  {colors.node-border}, {rounded.lg}, {elevation.node}. Active/root:
  {components.node-card-active} — border goes accent, shadow goes
  {elevation.node-active}. Failed/interrupted states use
  {components.node-card-failed} / {components.node-card-interrupted}.
- **Media frame** ({components.media-frame}): videos and screenshots sit in
  a hairline-bordered {rounded.md} frame, 4:3, `overflow: hidden`.
- **Dropdown** ({components.dropdown-surface}) and **modal**
  ({components.modal-surface}): white, escalating shadow ladder
  {elevation.control} < {elevation.dropdown} < {elevation.modal}. Modal
  overlay is white at 60% with 2px backdrop blur — a lightbox, not a blackout.

### Forms

- **Text input** ({components.text-input}): white, 1px hairline, 8px
  radius. Focus recolors the border to accent ({components.text-input-focused})
  — no glow rings, no border thickening. Disabled sinks to the canvas
  ({components.text-input-disabled}).
- **Checkbox** ({components.checkbox}): native, `accent-color` teal.
- **Toggle** ({components.toggle}): accent track when on, neutral when off.
- **Key badge** ({components.key-badge}): keyboard hints render as flat
  1px-bordered chips on canvas — no fake 3D key bevels.

### Map anatomy (the product IS the design)

- **Connector** ({components.connector}): 1.5px {colors.connector} cubic
  bezier, round caps. Connectors leave the parent's edge and meet the
  child's vertical center.
- **User message** inside a node: {components.user-bubble}.
- **Icon chip** ({components.icon-chip}): 48px {colors.accent-tint} square,
  {rounded.sm}, holding a 24px stroke-1.5 Lucide icon (ADR 0015: Lucide only).
- **Dot grid** ({components.dot-grid}): the canvas texture behind hero maps.
- **Node color** ({colors.node-tint-*}): eight pastels a user can assign to a
  node to group or flag it. Rules: the tint fills the card body only, the
  border stays {colors.node-border} so selection teal and the error/warning
  borders still win, a folded corner shades its own tint toward
  {colors.connector}, and no color is the default white card. The control that
  sets it keeps its palette icon at every state, recolored to
  {colors.node-hue-*} when the node carries a color. The palette is
  fixed — a node stores the color key, never a hex.

### Navigation

- **Nav** ({components.nav}): scrim + blur, hairline bottom border, wordmark
  left (27px logo + bold name), links + CTA right. Links are
  {components.nav-link}, hover {components.nav-link-hover}.
- **Footer** ({components.footer}): same scrim treatment, {colors.ink-subtle}
  text, single row on desktop.
- Mobile: burger (36px, hairline border, {rounded.md}) morphs to an X by
  rotating its bars 45° in 200ms; menu links stack with hairline dividers.

## 6. Depth & elevation

| Level | Treatment | Used for |
|---|---|---|
| 0 | {colors.canvas}, no shadow | page, hero, sections |
| 1 | {colors.surface} + {elevation.card} / {elevation.node} | cards, node cards |
| 1-hover | {elevation.card-hover} / lift -2px + {elevation.control-hover} | hovered cards/controls |
| 2 | {elevation.dropdown} | dropdowns, popovers |
| 3 | {elevation.modal} + blurred white overlay | modals |
| accent | {elevation.node-active} | the selected/living node |

Rules: shadows are gray and soft — the only tinted shadow is the accent
glow on active nodes. Borders stay 1px (1.5px on map objects) regardless
of state; state changes recolor the border, they never thicken it (ADR 0013).

## 7. Motion

Motion is part of the design system, not a garnish. House rules (see
`.claude/skills/mindmap-design-system` and the animation skills it links):

- **The house curve** is {motion.ease-out} — every entrance, draw, and
  reveal uses it. Micro color fades use plain `ease`. Never `ease-in` on UI.
- **Durations**: hovers {motion.duration-micro}; control lifts and
  dropdowns {motion.duration-control}; nothing in the app exceeds
  {motion.duration-ui-max}. Marketing entrances may run
  {motion.duration-entrance}, connector draws {motion.duration-draw}.
- **Branching is the hero.** The signature sequence: connector draws
  parent→child ({motion.duration-draw}, stroke-dashoffset), then the child
  card enters from {motion.entrance-transform} to rest
  ({motion.duration-entrance}). Nobody else animates the act of branching;
  we do, and almost nothing else.
- **Physicality**: entering cards scale from 0.97, never 0. Pressables
  compress to {motion.pressed}. Hover lifts are -2px, not -8px.
- **Frequency discipline**: never animate keyboard-driven or
  many-times-a-day actions (typing, node navigation, zoom stepping).
  Modals, drawers, first-run moments may animate; the command path may not.
- **Only transform and opacity** animate. No width/height/layout
  transitions except deliberate one-offs (mobile menu).
- **Interruptible**: prefer CSS transitions (retargetable) over keyframes
  for anything a user can re-trigger.
- **Reduced motion**: `prefers-reduced-motion` gets the finished state
  immediately — same layout, zero choreography. Streaming text carets and
  pulsing dots stop.
- Implementation: CSS transitions first; the `motion` library
  (`motion/react` in the app, `motion` vanilla in Astro) when sequencing,
  springs, or interruption demand it.

## 8. Do's & don'ts

Do:

- Use {colors.accent} for exactly one job per view — the action or the
  living thought.
- Put every border at 1px {colors.hairline} (1.5px {colors.node-border} on
  map objects) with 1px-radius discipline from {rounded}.
- Lead every heading and CTA with the reader's outcome ("Explore your first
  idea", "Create your first mindmap, free") — the benefit opens, the
  mechanism follows. The landing hero headline is the one allowed exception:
  it may open with a positioning line ("Think outside the chatbox") when the
  value nodes directly beneath it carry the benefit in the reader's words.
- Prefer numbers over adjectives in copy ("scan a hundred branches", not
  "blazingly fast").
- Apply the remove-the-name test to copy: if any competitor could sign the
  sentence, rewrite it.
- Keep the graphene motif structural: lattice geometry (bond 132u, node
  r30u, halo gap 9u), real transparent halos, at most a few teal nodes.

Don't:

- **No em-dash in user-facing copy.** Write one straight, action-first
  sentence instead.
- No banned words: "in seconds", "instantly", "AI-powered", "seamless".
- No second chromatic color, no blue CTAs ({components.cta-primary} is
  teal; the app's legacy blue is drift, not precedent).
- No gradients on UI elements — the brand gradient lives in the logo and
  favicon only.
- No pure-white pages ({colors.canvas} is the ground) and no pure-black
  text ({colors.ink} is near-black).
- No `border-2`/heavy borders to signal state; recolor at 1px instead.
- No decorative animation on frequent actions, no `scale(0)` entrances, no
  animations past {motion.duration-ui-max} in the app.
- No drop-in component-library styles (Material ripples, Bootstrap wells);
  every element derives from the tokens above.

## 9. Responsive behavior

- **Single breakpoint of record: 768px** ({layout.breakpoint-desktop}).
  Below it: nav links collapse into the burger menu, feature rows stack
  (text above media), hero fans collapse to a vertical spine with rounded
  90° connectors, CTA rows stack vertically.
- Type never needs breakpoint overrides — the `clamp()` scale is the
  responsive system.
- Touch targets ≥36px (burger is the floor); primary CTAs are ≥44px tall.
- Hover-dependent affordances must have a visible rest state on touch
  (lifts and shadow raises are enhancements, not information).
- The graphene backdrop and dot-grid stay fixed and subtle at all sizes —
  never let texture crowd a phone screen.

## 10. Voice & copy

Copy is design. Same review bar as pixels.

- Benefit first: open with the reader's outcome, then the mechanism
  ("Every branch keeps full context" follows "explore different
  follow-ups", never leads).
- Calm confidence: short declaratives, no exclamation marks, no hype.
- Concrete over abstract: name the models, count the zoom levels, show the
  keystroke.
- The product noun is "map"; a node is a "node"; branching is "branching"
  (not "forking" in UI copy, though features may explain "fork").
- Sentence case everywhere except the wordmark "mindmap.io", which is
  always lowercase.
- Changelog entries are benefit-first like everything else, with no
  carve-out. The title names the thing the reader will feel ("Smoother
  zoom animation"), not the number that made it possible ("Zoom now goes
  down to 15%"). The description says plainly what is different for them,
  because it is also the line that lands in the weekly email, where it has
  to earn a click on its own (ADR 0030, amended 2026-07-31).

## 11. Agent prompt guide

Quick reference:

- Paper {colors.canvas} · White {colors.surface} · Teal {colors.accent}
  (hover {colors.accent-hover}) · Ink {colors.ink} / {colors.ink-muted} /
  {colors.ink-subtle} · Hairline {colors.hairline}
- Satoshi only · fluid scale · radius 6/12/16 · shadow ladder
  card < dropdown < modal · house curve cubic-bezier(0.22, 1, 0.36, 1)

How to work with this file:

1. Restyle one component at a time and name it by its `components:` key
   ("bring `cta-primary` on the pricing page in line with DESIGN.md").
2. Use `{token}` references in specs, reviews, and PR descriptions — never
   raw hex. If a value you need has no token, that's a finding: propose the
   token first.
3. New variants become new component entries (`node-card-failed`), not
   prose exceptions.
4. Before UI work, also load `/design-deliberately` (does this element earn
   its place?) and `/apple-design` (does it move right?); the
   `mindmap-design-system` skill bundles the workflow.
5. Drift between a surface and this file is a bug (ADR 0024). Fix the
   surface or PR a change to this file — never fork the truth silently.

Ready-to-use prompts:

- "Build a `<section>` for the landing page using {components.card} items in
  a 2-column grid, section-title + subhead heading block, one
  {components.cta-primary} at the end."
- "Restyle this app modal to {components.modal-surface} with the level-3
  elevation and the standard overlay."
- "Animate this node's entrance with the signature branching sequence from
  § Motion."

## 12. Known gaps

- **The app has not yet converged on this file.** Its primary buttons are
  still blue (`#2563eb`), its canvas `#f8f9fa`, its grays Tailwind-cool
  (issues #401/#402 track reconciliation). When touching app code, style
  new work to DESIGN.md and flag surrounding drift — don't copy it.
- Dark mode is unspecified (epic #246).
- Toasts and empty states have no extracted spec yet — derive from
  {components.card} + § 8 and propose entries.
- The brand voice chart in `brand/brand-guidelines.md` is still partially
  OPEN; this file reflects decisions locked as of 2026-07.
