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.cssis it—lib/renderer.tsinlines 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 atdesign-system/components/tokens.cssexisted from the 2026-05brand-refsmigration until 2026-07-31; it sat frozen at 2026-05-19, 19 variables behind, and got published to brand.os whilesite.cssused 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:
- Visualising customer sentiment data — its primary job.
- 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 withcolor-mixso 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:
- Dark-slide values pass 4.5:1 as text on off-black (periwinkle 4.9, dusty rose 5.5, blue 6.9, orchid 7.4) and 3:1 as bars on dark grey (periwinkle 3.0, dusty rose 3.4, blue 4.2, orchid 4.5). On dark grey only orchid and yellow pass as small text, so an eyebrow there takes the section colour only if it passes 4.5:1, otherwise
--wn-off-black-muted. - Light-slide values pass 3:1 as bars and icons on white and light grey (blue deep 3.8 / 3.3, dusty rose deep 5.3 / 4.5, periwinkle deep 6.3 / 5.4, orchid deep 6.6 / 5.7). Text and eyebrows on light slides stay off-black whatever the section colour.
- Text on a filled accent: off-black on the dark-slide values and on yellow; white on the three deeper set colours; no small text on
--wn-brand-blue-deep(off-black 4.0, white 3.8). - Brand yellow on white or light grey (1.8:1 and 1.5:1) appears only as a filled block with off-black text on it, never as a thin line or as text. The deck's yellow top strip is the one exception: it is house chrome. On dark slides yellow is used freely (8.7:1).
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
--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). |
| 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)
- 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
Nav login lozenge (.site-nav__login) — added 2026-06-11
- Background:
--wn-brand-blue; text:--wn-off-black(white-on-brand-blue fails WCAG contrast at nav-label size — same rationale as off-black-on-yellow) - Same geometry as the yellow nav CTA (
.site-nav__cta):0.6em 1.2empadding,--wn-radius-card,--fs-xsuppercase - Dark nav chrome only. The existing-customer route into the platform (
https://app.wordnerds.ai/auth/2fa, new tab), grouped with the primary CTA in.site-nav__actions - Blue = secondary CTA (§2): the lozenge never competes with yellow — the yellow nav CTA remains the surface's single yellow element
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:
- "…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 only for sentiment data or a labelled status
- 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