Wordnerds Design System
Version: 0.2
Last updated: 2026-05-28
Authoritative token values: components/tokens.css
Component gallery: components/index.html (open in browser)
Production spec: production-css-spec.md
This file teaches AI tools the rules of the Wordnerds design system — not just the values, but when and how to apply them. Read this before generating any Wordnerds-branded output.
1. Brand Identity
Wordnerds is a professional text-analysis and Voice of Customer (VoC) platform used by enterprise insight teams. The visual language is bold but restrained: high-contrast dark chrome punctuated by a signature amber accent, with neutral greys forming the content canvas.
The emotional register is confident, data-first, and human. The design should feel like a serious analytical tool that people actually enjoy using. It is not playful or casual, but it is warm. The rounded typography and amber accent prevent it from feeling cold.
Everything is derived from the platform UI. The marketing site, presentations, diagrams, and lead magnets all use the same atomic vocabulary as the product itself. If something looks like it could be from a different brand, it is wrong.
The two design signals are: dark chrome + amber. Off-black (#222222) for structural chrome (navigation, sidebars, footers). Brand yellow (#FAB316) for the one thing that matters most on any given surface — the primary CTA, the active state, the thing the user should act on next. Everything else is grey.
2. Colour Palette
Token file: components/tokens.css
Gallery reference: components/index.html#colours
Primary brand
| Token | Hex | Rule |
|---|---|---|
--wn-brand-yellow |
#FAB316 |
Primary CTA and active states only. Do not use decoratively, as a background wash, or for text. One dominant yellow element per surface. |
--wn-off-black |
#222222 |
Navigation, sidebars, and structural chrome. Never use pure #000000 — off-black has warmth that pure black lacks. |
--wn-brand-blue |
#39B8E1 |
Secondary CTAs, info accents, automated/AI feature signalling. Never competes with yellow — use where yellow is already taken. |
Rule: yellow is brand signal, not state signal
Yellow means "this is the primary action." It does not mean "success," "warning," or "positive." When something is active or selected, it gets off-black with a yellow accent — not a yellow background unless it is itself the primary CTA. A filter button goes yellow when active. A table row does not.
Rule: sentiment colours are data-only
The --wn-sent-* palette (green → grey → red) is reserved exclusively for visualising customer sentiment data. Never use these colours for UI states, success/error messages, decorative elements, or marketing. A very happy green on a non-sentiment element is always wrong.
Grey scale (use instead of tints)
Do not create tints of brand yellow or blue. Use the grey scale instead.
| Token | Hex | Primary use |
|---|---|---|
--wn-dark-grey |
#444444 |
Body text, icons, hover states |
--wn-mid-grey |
#707070 |
Secondary text, descriptions |
--wn-border-grey |
#b9b9b9 |
Borders, dividers |
--wn-background-grey |
#EEEEEE |
Page background — the dominant surface |
--wn-light-grey |
#EAF0F4 |
Card footers, form backgrounds, cool surfaces |
--wn-lighter-grey |
#f8f8f8 |
Card backgrounds |
--wn-lightest-grey |
#fafafa |
Elevated surfaces, modals |
--wn-white |
#FFFFFF |
Card bodies, input backgrounds |
Section background tints
Two warm/cool alternatives for marketing page rhythm. Section wrappers only — never use on components, cards, or UI elements.
| Token | Hex | Class | Use |
|---|---|---|---|
--wn-section-yellow-tint |
#FEF4DC |
.wn-section--yellow-tint |
Warm golden tint — breaks up grey-scale section sequences with a brand-adjacent warmth without full yellow saturation |
--wn-section-blue-tint |
#E1F4FB |
.wn-section--blue-tint |
Cool sky tint — provides a cooler rhythm break between neutral sections |
Both colours inherit default text and eyebrow colours — no context overrides needed. The standard yellow primary CTA remains readable on both backgrounds.
These tokens are accent tools, not a rhythm pattern. Use them sparingly — at deliberate visual accent points — not to alternate mechanically between every section. Most sections on a page should be white; tints appear only where a background shift is intentionally earning its place. Do not create additional tints — use the existing palette.
Sentiment palette (data viz only)
| Token | Hex | Meaning |
|---|---|---|
--wn-sent-very-happy |
#6BB32D |
Very positive sentiment |
--wn-sent-happy |
#95C11F |
Mildly positive sentiment |
--wn-sent-neutral |
#CFD9DF |
Neutral sentiment |
--wn-sent-sad |
#EA752B |
Mildly negative sentiment |
--wn-sent-angry |
#E94C1E |
Very negative sentiment |
Sentiment always reads green → grey → red, left to right in diverging bars. Never reverse the scale.
Do not introduce new colours
Any new colour requires amending tokens.css, tokens/tokens.json, and production-css-spec.md in the same change. Do not add one-off hex values to individual components or pages. The two section tints above are the approved additions — no further tints needed.
3. Typography
Gallery reference: components/index.html#typography
Two families, used by role — not by size
--wn-font-regular('Museo Sans') — the default. Use for all body copy, UI labels, metadata, descriptions, and most marketing copy. This is the workhorse.--wn-font-rounded('Museo Sans Rounded') — reserved for display moments only: page headings (h1–h4), sidebar section labels, card header titles, hero text, login screen. Brings warmth at impact moments.
The rule is role, not size. A small heading uses Rounded. A large body paragraph uses Regular. Never swap them based on visual weight alone.
Type scale
Fluid type — Utopia methodology. Each step uses clamp(min, preferred, max) so sizes scale smoothly between 375px and 1440px viewports. No breakpoints needed.
Parameters: base 18px → 20px · ratio 1.2 (mobile) → 1.25 (desktop) · viewport 375–1440px
| Token | Mobile → Desktop | HTML role | Use |
|---|---|---|---|
--fs-3xl |
44.8px → 61.0px | H1 hero | Hero headline — largest marketing moment |
--fs-2xl |
37.3px → 48.8px | H1 page | Primary page title |
--fs-xl |
31.1px → 39.1px | H2 | Section headings, page sub-titles |
--fs-lg |
25.9px → 31.3px | H3 | Card headings, feature titles |
--fs-md |
21.6px → 25.0px | H4 / overline | Eyebrows, strong labels, small card titles |
--fs-base |
18.0px → 20.0px | p / default | All body copy, descriptions, list items |
--fs-sm |
15.0px → 16.0px | caption | Image captions, helper text, badges |
--fs-xs |
12.5px → 12.8px | fine print | Legal copy, timestamps, footnotes |
Heading-to-body ratio: ~2.5:1 (mobile) → ~3.05:1 (desktop). Conservative — enterprise data platform, not a tool startup.
Do not create sizes outside this scale. Do not hardcode px or rem font sizes in website/marketing components — always use a --fs-* token.
Product UI exception: emoji characters, checkbox checkmarks, and compact tag labels inside fixed-dimension containers (≤24px) are exempt — their size is constrained by the container, not by reading comfort, and fluid scaling would cause overflow. These remain hardcoded (10–14px) until a separate product UI token set is defined in Phase 3.
Font–role pairing
--wn-font-roundedon--fs-mdand above: H1–H4, hero text, card titles, eyebrow labels.--wn-font-regularbelow--fs-md: body copy, captions, fine print, UI labels in data surfaces.
The rule is role, not size. A small heading uses Rounded. A large body paragraph uses Regular.
4. Spacing
Gallery reference: components/index.html#spacing
Fluid space scale
Utopia methodology — same effective range as the type scale (360→1240px). Nine steps plus eight one-up pairs. Steps are multiples of the base space unit (18px mobile → 20px desktop).
| Token | Mobile → Desktop | Primary use |
|---|---|---|
--space-3xs |
5px (static) | Hairline gaps, icon nudges |
--space-2xs |
9px → 10px | Icon padding, tight inline gaps |
--space-xs |
14px → 15px | Small button padding, list indent |
--space-s |
18px → 20px | Base unit — component internal padding |
--space-m |
27px → 30px | Card gap, grid gap |
--space-l |
36px → 40px | Component margin, form field gap |
--space-xl |
54px → 60px | Between-component spacer |
--space-2xl |
72px → 80px | Section padding (smaller) |
--space-3xl |
108px → 120px | Section padding (generous) / hero |
One-up pairs (min from lower step, max from upper step — transition faster than a single step, good for layout flex):
--space-3xs-2xs · --space-2xs-xs · --space-xs-s · --space-s-m · --space-m-l · --space-l-xl · --space-xl-2xl · --space-2xl-3xl
Key mappings from layout research:
- Grid gap (24–32px target) →
--space-m(27→30px) ✓ - Section padding (80–100px target) →
--space-2xl(72→80px) or--space-2xl-3xlpair ✓ - Current
--wn-gap: 30px→--space-mat desktop ✓
Do not use arbitrary px or rem values for spacing in website/marketing components — always use a --space-* or --wn-space-* token.
Layout constants (not fluid)
--wn-content-max: 1280px— main content wrapper max-width.--wn-gap: 30px— retained for product UI and backwards-compat. Equivalent to--space-mat desktop.
5. Corner Radii
Gallery reference: components/index.html#spacing (radius section)
Rounded everything. Hard corners are rare and deliberate — used only when a component must look structural rather than interactive.
| Token | Value | Use |
|---|---|---|
--wn-radius-sm |
6px | Small chips, rule builder rows, table cells, tooltip bubbles |
--wn-radius-md |
1rem | Logic pills, rule rows |
--wn-radius-card |
1.5rem | Cards and buttons — the workhorse radius. Use for all cards, CTAs, and component containers unless a more specific token applies. |
--wn-radius-input |
2rem | Form inputs — pill style |
--wn-radius-input-lg |
3rem | Large form inputs |
--wn-radius-pill |
999px | Fully oval — filter pills, tags, lozenges |
--wn-radius-card (1.5rem) is the default. When in doubt, use it. Reach for --wn-radius-sm (6px) only for compact, data-dense components (table cells, tooltips). Never use hard right angles (0px) unless explicitly justified.
6. Shadows & Motion
Shadows
| Token | Value | Use |
|---|---|---|
--wn-shadow-card |
0 2px 20px 0 rgba(0,0,0,0.1) |
All elevated surfaces: cards, modals, floating panels. The default shadow. |
--wn-shadow-pop |
0 4px 12px rgba(0,0,0,0.12) |
Tooltip bubbles, dropdown menus, picker overlays. Slightly more prominent to indicate floating above the card layer. |
Shadows are soft and low-opacity. Depth is conveyed by shadow, not by hard borders. Never add borders to cards that already have shadows — pick one.
Motion
| Token | Value | Use |
|---|---|---|
--wn-motion-fast |
0.3s | Card hover lifts, component state transitions |
--wn-motion-base |
0.4s ease-in-out | Sidebar link transitions, most interactive transitions |
--wn-motion-slow |
0.5s ease-in-out | Sidebar expand/collapse, disclosure chevron rotation |
Never exceed --wn-motion-slow. Animations faster than --wn-motion-fast should be limited to micro-interactions. Always respect prefers-reduced-motion — wrap reveal animations in @media (prefers-reduced-motion: no-preference).
Card hover
Cards lift on hover: transform: translateY(-10px) at --wn-motion-fast (0.3s). Project-style cards scale instead: transform: scale(1.1) at 0.3s ease-in-out.
7. Core Components
All components live in components/. Each has a self-contained HTML file showing every state. The component gallery at components/index.html shows them all on one page.
Do not re-implement components from scratch. Lift the markup from the component file and parameterise the content.
Naming convention
Root class: .wn-<component> (BEM-style)
State modifiers: .wn-<component>--<state> (--inactive, --hover, --active, --disabled)
Child elements: .wn-<component>__<element>
Component inventory
| Component | File | Key rule |
|---|---|---|
| Filter button | filter-button.html |
Black pill + yellow icon. Active = yellow fill + black text. |
| Dropdown | dropdown.html |
Pill input with caret. Selected option shows yellow underline when closed. |
| Tooltip | tooltip.html |
Circular ? glyph. Very muted inactive; bubble floats above on active. |
| Text link | text-link.html |
Dark grey, underlined, weight 500. Hover thickens underline — no colour change. |
| Export button | export-button.html |
Circular icon. Very muted inactive; active = yellow fill. |
| Chart row | chart-row.html |
Diverging sentiment bar (green → grey → red). Hover = cool wash. Active = inverted to black. |
| Theme lozenge | theme-lozenge.html |
Pill tag. Inactive = quiet border. Active = yellow fill + close glyph. |
| Table styles | table-styles.html |
Three cell modes: text, wheel (donut, default yellow), bar (default blue). |
| Verbatim card | verbatim-card.html |
White body on cool-grey surround. Always shown against the grey page background. |
| Form inputs | form-inputs.html |
.wn-form-group wrapper; .wn-form-input / .wn-form-textarea (pill radius, --wn-radius-input); .wn-form-hint (mid-grey); .wn-form-error (border-grey + off-black, never sentiment red). |
| Buttons | form-inputs.html |
.wn-btn base + --primary / --secondary / --ghost modifiers. See §7 Buttons (marketing pages). |
Active state rule (universal)
When a component is active or selected: off-black fill + yellow accent. The exception is the filter button and lozenge, where the component itself becomes the primary CTA — in those cases, the whole component fills yellow. Never use a coloured fill (blue, green, red) to indicate active state. Never use yellow as a background for more than one element on the same surface.
Buttons (marketing pages)
Class: .wn-btn (base) + .wn-btn--primary / .wn-btn--secondary / .wn-btn--ghost
Component file: components/form-inputs.html
Primary CTA (.wn-btn--primary)
- Background:
--wn-brand-yellow - Text:
--wn-off-black(off-black on yellow — confirmed production choice; better legibility than white-on-yellow at all sizes) - Border-radius:
--wn-radius-card(1.5rem) - Padding:
var(--space-xs) var(--space-l)(14px × 36px) - On yellow backgrounds (
.wn-section--yellow): inverts to off-black background + yellow text
Secondary CTA (.wn-btn--secondary)
- Background:
--wn-off-black - Text:
--wn-white
Ghost CTA (.wn-btn--ghost)
- Background: transparent; border: 1.5px solid
--wn-off-black - On dark backgrounds (
.wn-section--dark): border and text become white; hover inverts to white fill
Arrow link (.cta-secondary)
- Inline text link with
→indicator; no background - Colour:
--wn-off-black; underline animates on hover
8. Design Rationale
These decisions are load-bearing. Understand the why before deviating.
Why off-black and not pure black? Pure black (#000000) reads as harsh and digital. The off-black (#222222) has warmth that keeps the interface from feeling cold, while still providing maximum contrast against white surfaces.
Why off-black text on yellow (not white)? White-on-yellow fails WCAG AA contrast at smaller sizes and reads as washed-out against a bright background. Off-black on yellow meets contrast requirements and maintains the brand's high-contrast visual identity. This was tested against the white alternative and confirmed as the production choice.
Why are sentiment colours banned from UI states? Wordnerds' entire product value is built on nuanced sentiment analysis. Diluting the sentiment palette — using green for "success" or red for "error" — would undermine the semantic rigour that makes the product credible. Error states use the border-grey or off-black system, not the sentiment palette.
Why Museo Sans Rounded only at display sizes? Museo Sans Rounded is warm and distinctive at large sizes. At body sizes it reads as decorative rather than functional, and long paragraphs in Rounded feel effortful to read. Regular Museo Sans is more legible for sustained reading.
Why 1.5rem card radius everywhere? Consistency. The card radius is the most visible design decision on any surface — deviating from the token creates visual inconsistency that is immediately noticeable even to non-designers. If you think you need a different radius, you almost certainly want --wn-radius-sm (6px) for something compact, or --wn-radius-pill (999px) for a tag.
Why section tints but no component tints? B2B SaaS reference sites (Pitch, Loom, Notion) use background variation sparingly — a coloured section is a deliberate accent, not a mechanical every-other-section pattern. The two section tints (--wn-section-yellow-tint, --wn-section-blue-tint) are the approved tools for those accent moments. Do not use them to alternate backgrounds across consecutive sections — that reads as formulaic. At component level, tints of brand yellow or brand blue still feel accidental and inconsistent — the grey scale continues to apply inside cards and UI elements.
Why a stronger hero gradient? The .hero section gradient was previously very subtle (7–8% opacity). B2B SaaS reference sites with dark heroes (Pitch, Superlist) use clearly visible colour moments — a strong warm glow behind the product visual signals brand colour immediately. The updated gradient uses a 28% peak yellow ellipse positioned at the right-centre (behind where the product screenshot sits) and a 16% blue accent at bottom-left. Both are still atmospheric — not flat shapes — so the visual quality stays high.
Why not sentiment colours on the marketing site? See the sentiment rule above. Worth emphasising: Wordnerds users who land on the marketing site may already know the product. A green section background they've trained themselves to read as "very positive sentiment" creates genuine confusion. The two section tints are brand-derived and carry no semantic meaning outside this system.
Why is the content max-width 1280px? This was calibrated for the product UI sidebar-plus-content layout. Marketing pages without a sidebar may benefit from a slightly narrower column (1120–1180px) for better line length. The site renderer uses 1180px for this reason.
Why are there two <mark> variants with different semantics? Wordnerds' methodology (SYSTM) identifies value translation — the Step 2 → Step 3 move from capability to customer value — as the hardest and highest-leverage copy discipline. Most copy names the capability but never lands the value. Blue <mark> makes the value translation visible to the reader (and to reviewers) so it cannot be skipped. Yellow <mark> is already taken for customer voice inside blockquotes; blue is the natural complement (brand blue signals information/AI features elsewhere in the system). Having two semantically distinct highlights reinforces the distinction between what customers say (yellow) and what they get (blue).
9. Inline Emphasis — <mark> Semantics
Two <mark> variants exist. Each has a strict semantic scope. Do not use them interchangeably.
| Variant | CSS | Background | Text | Semantic scope |
|---|---|---|---|---|
<mark> (no class) |
blockquote p mark |
--wn-brand-yellow |
--wn-off-black |
Customer voice — the highlighted quote text inside a <blockquote>. Applies only inside blockquotes. Never use yellow <mark> outside a blockquote. |
<mark class="wn-mark-blue"> |
.wn-mark-blue |
--wn-brand-blue |
white | Customer value — the value-translation half of a capability→value pair, appearing in body copy (rich_text slots). See scope rule below. |
Both use the same padding (0.06em 0.15em) and box-decoration-break: clone for multi-line wrapping.
Scope rule for blue <mark> (value translation)
A phrase earns .wn-mark-blue if and only if it is the value the customer gets as a direct result of a named Wordnerds capability. Specifically: a capability is named or implied on the left; the blue-highlighted phrase is what the customer receives on the right. This maps directly to the capability→value translation discipline in wordnerds-wiki/wiki/systm/concepts/capabilities-to-value-translation.md.
Earns blue mark:
- "…so everyone in the organisation can act on what customers are saying" (Cap 1: Power BI delivery → everyone acts)
- "…themes prioritised by impact, with the evidence behind every one" (Cap 2: pipeline → prioritised themes)
Does NOT earn blue mark:
- Villain descriptions or problem framing (those belong in the lede/capsule)
- Differentiator positioning (H2s/H3s carry that work)
- General emphasis (use italics or Rounded font weight instead)
- Any phrase not tied to a specific named capability
In-pipeline usage
The copy-composer and schema-composer write [[blue]]…[[/blue]] tokens in rich_text slot content. The renderer (lib/util.ts renderRichText) converts them to <mark class="wn-mark-blue">…</mark> at build time.
How to use this file
For Claude Code sessions
Add to CLAUDE.md at project root:
@/Users/pete/Code/wordnerds-site/design-system/DESIGN.md
Before generating any component, ask: does a component in components/ already do this? If yes, lift the markup and parameterise it. If no, follow the naming convention and token rules in this file.
Mandatory checks before any output
- Only use colours from
tokens.css— no arbitrary hex values - Use
--wn-font-roundedfor headings only,--wn-font-regularfor everything else - Brand yellow appears on at most one element per surface
- Sentiment colours appear nowhere except data visualisation
- All radii come from the radius token set — no arbitrary px values
- All spacing comes from
--wn-space-*or--wn-gap— no arbitrary values - Shadows use
--wn-shadow-cardor--wn-shadow-pop— no custom shadow values