Appearance
Brand tokens — consume, don't extract
Part of the design-brief package (issue #263). This file tells a design AI where the NanaWall brand values come from, which names it may use, and what it must never do (invent brand values).
Source of truth
Brand tokens live in @nanawallweb/design-tokens (GitHub Packages; sibling repo nanawallweb/nanawall-design-tokens, reference site tokens.nanawall.com). Values are extracted from production www.nanawall.com computed styles — the package is the one place brand values are defined. NanaSelect already depends on it (package.json@nanawallweb/design-tokens ^1.0.0).
Rules for a design AI:
- Never invent a brand value. Colors, font stacks, type sizes, weights, and line-heights come from the package artifacts attached to this brief.
- Never re-extract from nanawall.com. The extraction pipeline upstream owns that; a design that "eyedroppers" the website reintroduces the drift the package exists to eliminate.
- Gaps go upstream, not inline. If the design needs a token the package lacks, the value is defined app-side and the gap is filed on
nanawallweb/nanawall-design-tokens(see the audit below — the known gaps are already filed). A generated design should flag needed-but-missing tokens explicitly rather than silently hardcoding.
Attached artifacts
| File | What it is |
|---|---|
assets/tokens.json | The full 3-tier token tree (core → semantic → context) from dist/tokens.json v1.0.0 — every extracted brand value: color scales (teal, gray, red, green), typography, spacing, borders, shadows, breakpoints, animation, plus per-component context tokens (button, card, form, navigation, modal, panel, video, layout). |
assets/contract-names.json | The canonical 21-name flat consumer contract snapshot. |
The flat --nw-* consumer contract (21 names)
The package's stable public API — the names apps may reference directly (@import '@nanawallweb/design-tokens/contract'). Changing or removing a name is a breaking change; additions are minor. Full decision record: docs/token-delivery-contract.md in the tokens repo.
| Group | Names | Values |
|---|---|---|
| Colors (5) | --nw-color-bg-primary, --nw-color-primary, --nw-color-primary-active, --nw-color-text-heading, --nw-color-text-primary | white; teal-300; teal-500; gray-14; gray-12 |
| Font families (2) | --nw-font-sans, --nw-font-serif | 'Source Sans Pro', sans-serif; 'Source Serif Pro', serif |
| Font sizes (7) | --nw-font-size-base … --nw-font-size-5xl | 19 / 21 / 22 / 24 / 26 / 28 / 31 px |
| Font weights (3) | --nw-font-weight-light/regular/semibold | 300 / 400 / 600 |
| Line heights (3) | --nw-line-height-relaxed/tight/tighter | 29px / 1.2 / 36px |
| Transition (1) | --nw-transition-fast | 0.15s ease-in-out |
Self-hosted weights (app-side).
static/fonts/ships Sans 300/400/600/700 and Serif 400 + 600 (the display weight, added by #829 so hero/section headings render a real semibold instead of faux-bold; latin subset from@fontsource/source-serif-pro@5.2.5). Allfont-display: swap; the two body weights and the serif display weight are preloaded — no font CDN at runtime.
Publishing caveat: published v1.0.0 predates the contract export — the flat contract isn't on GitHub Packages yet (upstream #162). Until it ships, the
nanawalltheme aliases the semantic tier (--nw-semantic-*) instead. Design output is unaffected: designs target the app's--ns-*contract (next section) either way.
Delivery mechanism: the NanaSelect theme contract (--ns-*)
Generated designs must be expressed as themeable tokens, not hardcoded values. The app's theme system (docs/reference/theming.md, issue #264) defines a 50-name --ns-* contract (THEME_CONTRACT in src/lib/themes/index.ts); a design is delivered as one theme CSS file assigning all 50 names, plus component markup that references only var(--ns-*) (or the Tailwind utilities mapped from them). Two build gates enforce this: a spec fails any theme missing a contract token, and a source-wide check rejects hex/rgb/font/radius literals outside src/lib/themes/.
The 50 names, by group:
- Surfaces:
bg,surface,surface-muted,surface-inset,surface-inverse,surface-inverse-hover - Text:
text,text-muted,text-subtle,text-faint,text-heading,text-inverse - Borders:
border,border-subtle - Accent:
accent,accent-hover,accent-strong,accent-soft,on-accent - States:
danger,danger-soft,danger-border,success,success-soft,success-border,warning,warning-soft,warning-border - Misc color:
focus,overlay,on-overlay,scrim - Fonts:
font-sans,font-serif,font-mono,font-size-base,font-display,font-display-weight,font-numeric - Radii:
radius-sm,radius-md,radius-lg,radius-full - Shadows:
shadow-sm,shadow-md,shadow-lg - Motion:
motion-duration,motion-ease - Imagery:
media-aspect,media-frame-pad
(All color names carry the --ns-color- prefix; fonts/radii/shadows/motion/media the --ns- prefix.)
Gap audit — NanaSelect needs vs. the 21-name contract
Audited 2026-07-02 against the app's real design needs (stepper, right-sizing nudge, coverage warning, comparison table, admin data tables). Three findings:
(a) Covered. Base brand identity aliases cleanly: page background, body and heading text, brand accent + active state, both font families, the type scale, weights, line-heights, and the house transition idiom.
(b) In the 3-tier tree but missing from the flat contract — additive contract candidates; values exist upstream, they just aren't public API:
| Need | Where it lives in tokens.json |
|---|---|
| Neutral surface scale (cards, insets, muted panels) | color.background.secondary/tertiary/dark/darkest, color.gray.* |
| Extended text roles (muted, subtle, inverse) | color.text.secondary/muted/inverted* |
| Border colors | color.border.* |
| Error/danger | color.feedback.error (#c8102e), color.red.* |
| Success-ish green | color.green.*, color.green-accent.* |
| Radii | border.radius.sm/md/full (11px / 13px / 50%) |
| Shadows | shadow.sm/md/drop |
| Focus treatment | color.interactive.* |
(c) Absent from the package entirely — no extracted value exists because production nanawall.com has no equivalent surface:
| Need | Why NanaSelect needs it |
|---|---|
Warning trio (warning, -soft, -border) | Coverage warnings on results; partial-data states in compare |
| Success soft/border variants | Right-sizing nudge — an affirmative call-out card needs a soft fill + border, not just a green |
| Mono font stack | Admin data tables, audit log, technical values |
| Stepper tokens (progress track/fill, step states) | The guided flow's signature component |
| Comparison-table tokens (sticky header, row hover, column highlight) | /compare and admin tables |
Category (c) values are currently defined app-side in the theme files (per the theming doc's rule: defined locally, filed upstream, never silently forked). The audit is filed upstream as nanawall-design-tokens#163 (contract additions from category (b) + new authored tokens from category (c)); the unpublished contract itself is #162.
Addendum (2026-07) — public-experience direction proposals
The 2026-07 direction (§§3, 7) adds five proposed tokens to category (c) — no extracted value exists upstream for any of them:
| Need | Why NanaSelect needs it |
|---|---|
--ns-color-scrim (theme token) | Legibility scrim over full-bleed photography; art direction bans duotones/heavy grading, so themes differentiate imagery via crop, scrim weight, and frame (Graphite deeper, Linen softer) |
--ns-motion-duration / --ns-motion-ease (theme tokens) | Motion personality per theme; prefers-reduced-motion zeroes them globally |
--ns-layout-hero-bleed / --ns-layout-band-bleed (layout tokens) | Full-bleed band vocabulary (hero, photo strips, CTA bands). Bleed is a layout property, not a theme property — these live in the layout files (Editorial on, Spotlight imagery-only, Standard contained), with the scrim that keeps text legible over the bled image staying a theme token: clean seam, no coupling |
Per the process above the values are defined app-side and the proposals are filed upstream as nanawall-design-tokens#164 (extends the #163 audit).
Shipped app-side (#843, 2026-07-06): --ns-color-scrim and --ns-motion-duration / --ns-motion-ease are now in the contract and all four themes, with per-theme AA-measured scrim values and motion personalities; prefers-reduced-motion zeroes durations globally. The same change added three type-register tokens to category (c) — --ns-font-display, --ns-font-display-weight, --ns-font-numeric (display voice per theme; numeric/spec register, mono in Graphite) — appended to the #164 proposal. See theming.md § Per-theme voice.
Shipped app-side (#844, 2026-07-06): two imagery-treatment tokens join category (c) — --ns-media-aspect (the theme's signature crop for hero-scale imagery: 3:2 / 21:9 / 4:3) and --ns-media-frame-pad (mat whitespace around the photograph; Linen's mount). Per-theme treatment table: art-direction.md § Imagery. Also appended to #164.
Implication for a design AI: design with the --ns-* contract as your palette-shaped hole. Where an --ns-* token has a brand source (categories a and b), take the value from assets/tokens.json. Where it doesn't (category c), you may propose values — harmonized with the brand scales in tokens.json — and must list them as proposed additions in your delivery notes.