Design System

Wordnerds Design System

Version: 0.2
Last updated: 2026-05-28
Authoritative token values: design-system/tokens.css—the only token file, and the one the live site inlines.
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: design-system/tokens.css
See every colour shown, with its contrast and what it is for, on the Colour page (colour.html on the published design system). This section keeps the written rules.

One token file, deliberately. design-system/tokens.css is it—lib/renderer.ts inlines it into every built page, the component gallery links it as ../tokens.css, and the design-system build copies it to the published root. A second copy at design-system/components/tokens.css existed from the 2026-05 brand-refs migration until 2026-07-31; it sat frozen at 2026-05-19, 19 variables behind, and got published to brand.os while site.css used the variables it was missing. It was deleted rather than kept in sync, because a second copy is the defect. Don't reintroduce a per-directory token file—link ../tokens.css.

Primary brand

Token Hex Rule
--wn-brand-yellow #FAB316 Primary CTA and active states only. Do not use decoratively or as a background wash. One dominant yellow element per surface. On white or light grey, only as a filled block with off-black text; never a thin line or text. On dark, freely. The top strip on slides is house chrome and exempt.
--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.
--wn-brand-blue-deep #328BA8 Brand blue's partner on white and light grey, where brand blue measures 2.3:1 and 1.9:1. Bars, edges and large text on white or light grey; not small text. Brand blue 70% mixed with off-black 30%.

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 for data and explicit status — never decoration

The --wn-sent-* palette (green → grey → red) carries meaning, so it belongs only where that meaning is the point. Two sanctioned uses:

  1. Visualising customer sentiment data — its primary job.
  2. Explicit status signalling — traffic lights (on track / completed / off track, pass / fail / fix). Amended 2026-07-31 (Pete). The rule previously reserved the palette exclusively for sentiment, on the grounds that reusing it would dilute the semantic rigour the product sells. In practice readers hold both meanings at once — nobody reads a green "Completed" chip as a happy customer — and refusing the convention pushes status onto a weaker device. Two conditions, both non-negotiable:
    • Always pair status colour with a text label. Colour alone is invisible to a colour-blind reader, so a bare green dot is not a status.
    • Check the contrast. The raw tokens are not all safe to type on: --wn-sent-angry (#E94C1E) cannot carry white text at label size — mix it toward off-black (color-mix(in srgb, var(--wn-sent-angry) 78%, var(--wn-off-black)) measures 5.3:1). Derive tints and shades with color-mix so no new hex enters the system.

Never decoratively, and never as a section background wash on a customer-facing surface. A green section on the marketing site — where visitors have trained themselves to read that green as "very positive sentiment" — is confusion for no gain. A very happy green on an element that signals neither sentiment nor status is always wrong.

First use of the status permission: the internal company strategy page (os.wordnerds.ai/strategy.html), whose yearly priorities carry on-track / completed / off-track chips.

Sanctioned exception — sentiment-classified customer quotes. When a real customer quote is itself classified by sentiment (e.g. a positive vs a negative review), its highlight and/or card accent may use the sentiment palette — --wn-sent-very-happy for positive, --wn-sent-angry for negative — because the quote is sentiment data, not decoration. This is the one place the palette appears outside a chart. Conditions: it must map to an actual sentiment classification (star rating or model output), be labelled as such (a small "Positive"/"Negative" marker), and never be used for generic positive/negative framing that isn't sentiment-classified customer voice. Inside a <blockquote> this replaces the default yellow customer-voice <mark> (§9) — a quote card is either the neutral yellow treatment or the sentiment-coded treatment, never both.

Grey scale (use instead of tints)

Do not create tints or shades of brand yellow or blue. Use the grey scale instead. The only allowed exceptions are the two section tints, the blue ramp and blue deep, all below.

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.

Amended 2026-08-03 (Pete): a full-width band inside an article counts as a wrapper. The blog summary box (.blog-summary, PATTERNS §11.2) carries --wn-section-yellow-tint across the full width of the content column with no radius and no shadow. That is the point of the exception: it reads as a band, like a section wrapper, not as a tinted card. The rule it still enforces is the one that matters — do not tint a card, a pillar, a stat tile, or any element that has a radius and sits inside a section. If it has --wn-radius-card on it, it does not get a tint.

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 (sentiment data, or a labelled status)

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.

Deck section colours

Each section of a slide deck takes one accent colour, used on its box edges, bars and icon circles. In deck order: brand blue opens, then periwinkle, orchid and dusty rose, and brand yellow closes. Each colour has a value for dark slides (off-black or dark grey) and a deeper value for light slides (white or light grey). On light slides the eyebrow is off-black whatever the section; the section colour goes on bars, edges and dividers. On dark grey (#444444) an eyebrow whose section colour scores under 4.5 there uses off-black muted instead. Decks only: the website's accents stay yellow and blue.

Section Dark-slide token Hex Light-slide token Hex
1 --wn-brand-blue #39B8E1 --wn-brand-blue-deep #328BA8
2 --wn-deck-periwinkle #8285EB --wn-deck-periwinkle-deep #5554B6
3 --wn-deck-orchid #E495F0 --wn-deck-orchid-deep #9B2D8C
4 --wn-deck-rose #BD8C94 --wn-deck-rose-deep #A0555B
last --wn-brand-yellow #FAB316 --wn-brand-yellow, as a filled block only #FAB316

Contrast, measured with the WCAG 2.x formula and cut (not rounded up) to one decimal, as on the Colour page:

Illustration yellow

--wn-cartoon-yellow #FDCD01 is the yellow of the house cartoons' own backgrounds. It is the backdrop of a yellow cartoon only, so the picture sits on its own colour without a visible edge. It is not a second brand yellow: never for text, buttons, lines, flashes or anything brand yellow does.

Do not introduce new colours

Any new colour requires amending tokens.css and tokens/tokens.json in the same change, with its role sentence in the token's description so the Colour page shows it. Amend production-css-spec.md as well only when the product uses the colour. Do not add one-off hex values to individual components or pages. The approved additions are the two section tints, blue deep, the deck section colours (periwinkle, orchid and dusty rose, each with a deep partner) and the cartoon yellow above — no further tints needed.


3. Typography

Gallery reference: components/index.html#typography

Two families, used by role — not by size

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

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:

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)


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).
Nav login lozenge form-inputs.html .site-nav__login (site.css). Brand-blue lozenge, off-black text. Dark nav chrome only, beside the yellow nav CTA. See §7 Buttons.

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)

Secondary CTA (.wn-btn--secondary)

Ghost CTA (.wn-btn--ghost)

Arrow link (.cta-secondary)

Nav login lozenge (.site-nav__login) — added 2026-06-11


8. Design Rationale

Each of these decisions was tested against its alternative. 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 allowed for status but not for UI states? Wordnerds' product value is built on nuanced sentiment analysis, so the palette has to keep meaning something. The original rule banned every non-sentiment use to protect that. Amended 2026-07-31: an explicit, labelled status signal — "Completed", "Off track" — reads as its own convention and doesn't compete with sentiment, because the label disambiguates it. What still undermines the palette is using it for things that are neither: a green "success" toast, a red form-validation message, a decorative green panel. Error and validation states use the border-grey or off-black system, not the sentiment palette. The test: could a reader mistake this green for "the customer was happy"? If a text label rules that out, it's status; if nothing rules it out, don't use the 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, with one sanctioned exception: the hero H1 (below). Never use yellow <mark> anywhere else.
<mark> (no class, hero H1) .hero h1 mark --wn-brand-yellow --wn-off-black Hero headline customer-voice phrase (approved 2026-06-11, homepage). The marked phrase must be the customer-voice half of the H1 — what customers are saying/doing — never a product claim. Authored as [[mark]]…[[/mark]] in the headline slot; the renderer adds .hero--marked, which flips the primary CTA to a white fill so the mark stays the surface's single yellow element (§2). At most one marked phrase per H1.
<mark class="wn-mark-blue"> .wn-mark-blue --wn-brand-blue --wn-off-black Customer value — the value-translation half of a capability→value pair, appearing in body copy (rich_text slots). See scope rule below.

All three marks take off-black text. The blue mark's text colour was white until 2026-07-31, when it was measured at 2.3:1 against --wn-brand-blue. Because this mark sits in body copy the bar is 4.5:1, not the 3:1 that applies to large text, so white failed by a wide margin; off-black on the same blue measures 6.9:1. §7 already specifies off-black for the brand-blue nav login lozenge on the same grounds, and PATTERNS.md §10 was corrected the same day for the brand-blue numbered chip. Those were the only three white-on-brand-blue pairings in site.css. Never put white text on --wn-brand-blue—it is a mid-tone, and every combination that treats it as a dark surface needing light text is wrong for it.

All three 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:

Does NOT earn blue mark:

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

  1. Only use colours from tokens.css — no arbitrary hex values
  2. Use --wn-font-rounded for headings only, --wn-font-regular for everything else
  3. Brand yellow appears on at most one element per surface
  4. Sentiment colours only for sentiment data or a labelled status
  5. All radii come from the radius token set — no arbitrary px values
  6. All spacing comes from --wn-space-* or --wn-gap — no arbitrary values
  7. Shadows use --wn-shadow-card or --wn-shadow-pop — no custom shadow values