Design System

Wordnerds Site
— Proven Page Patterns

Version: 0.5
Last updated: 2026-06-04
Status: Living doc — add to this whenever a homepage or page-level design decision is validated in the browser.

This file captures implementation decisions that emerged from building real pages. It is distinct from DESIGN.md (which covers tokens and atomic rules) and GRID.md (which covers grid mechanics). Read this before authoring any page schema, styled spec, or fragment.


1. CTA Rules

Every CTA is a lozenge button — never a plain text link

CTAs that ask a user to take a meaningful action must be rendered as a .wn-btn button, not a plain <a> link or inline text.

Action type When to use a button When a plain link is acceptable
Book / sign up / subscribe Always a button Never
Navigate to a key destination (case studies, how it works) Button if it is the section's primary exit Plain .cta-secondary arrow link if it is a subordinate option
Utility navigation (read all posts, browse playbooks, find out more) Not required Plain .cta-secondary or unstyled <a> is fine

Rule: if the action is named in marketing copy as a call to action (e.g. "Subscribe to CX Corner", "Book a diagnostic", "See how it works"), it must be in a button lozenge. If it is a navigation shortcut that a user might also find in the nav or footer, a plain link is acceptable.

Button style by surface

Surface Primary CTA Secondary CTA
White / light-grey section .wn-btn--primary (yellow) .wn-btn--ghost (off-black outline)
Yellow section (.wn-section--yellow) .wn-btn--primary inverts automatically to off-black bg + yellow text .wn-btn--ghost (off-black outline on yellow — reads cleanly)
Dark section (.wn-section--dark) .wn-btn--primary (yellow) .wn-btn--ghost (white outline — requires .wn-section--dark .wn-btn--ghost rule already in site.css)
Hero (dark gradient — .hero) .wn-btn--primary (yellow) .wn-btn--ghost (white outline — requires .hero .wn-btn--ghost rule in site.css)

Never render a secondary CTA in a dark or coloured section as .cta-secondary (arrow link) if it is a conversion action. Arrow links are for subordinate navigation only.

CTA groups

Use .cta-group (display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-m)) for any side-by-side CTA pair. Do not place two button-style CTAs without a cta-group wrapper.


2. Section Rhythm and Padding

Section backgrounds — white-dominant, don't alternate by default

Default to white, and do not alternate backgrounds section-by-section. Per-section toggling (light/dark, or white/grey) reads as formulaic and dated. White is the dominant content background; a grey (wn-section--alt) is an occasional breather to break up a long white run. A rough rhythm of roughly two clear sections then a grey is typical, but it is a judgement call, not a fixed cadence — vary it to suit the page (updated 2026-05-27 per Pete).

Dark is reserved for the hero and the footer zone (hero wn-section--dark; the footer-cta band + footer organism). Avoid making a mid-page content section dark to "break things up" — the white-dominant rhythm with the occasional grey does that work, and the two dark bookends frame the page. wn-section--yellow / tints stay deliberate, sparing accents (e.g. the footer-cta close).

Regression note: strict light/dark alternation was reverted twice; the prior framing was "sparse, deliberate accent". The current principle: white-dominant, grey as an occasional breather (used with judgement, not a fixed every-third cadence), dark only at hero + footer. Don't reintroduce per-section alternation.

White-on-white section joins collapse automatically

When two consecutive sections both have a white background (no background modifier), the CSS in site.css collapses the second section's padding-top to 0. This prevents the double-padding gap that otherwise makes adjacent sections look too far apart.

What this means for schema authoring: you do not need to manually adjust padding on white sections. The rule fires automatically. The affected section pairs on the homepage are:

What the rule does NOT collapse: sections on different backgrounds (dark → white, light-grey → white, etc.). Those transitions need the full padding on both sides to signal the background change.

When to use --generous padding

Use wn-section--generous (padding-block: var(--space-2xl-3xl)) for sections that need extra breathing room: feature pillars, proof outcomes, hero CTAs. Use default wn-section padding for most content sections.

Because of the white-on-white collapsing rule, a --generous section that follows a plain white section will still have its padding-top collapsed. Its padding-bottom is unaffected. This is intentional — the generous bottom gives the section visual weight; the collapsed top removes the double-gap.

Hero bottom edge

The hero section (.wn-section--hero) has padding-bottom: 0 and overflow: hidden. The product visual fills to the very bottom of the dark section and is clipped cleanly there. The following white section begins immediately below with its own top padding intact.

Do not reintroduce a negative margin-bottom bleed on .hero__visual. The clean cut is the intended look — the dark section sits flush against the white one.


3. Blockquote Typography

Canonical blockquote treatment (validated 2026-05-28)

Blockquotes are real-person quotes only — never for marketing framing (use .answer-capsule for that). The canonical treatment is the Sainsbury's proof-stat style:

This replaces the prior blue-left-border + opening-glyph treatment. This is now site-wide. Do not reintroduce the old treatment.

Fragment requirement: always wrap quote text in <p> inside <blockquote>. Example correct pattern: <blockquote><p><mark>"…"</mark></p><cite>…</cite></blockquote>.

Blockquote vs answer-capsule — do not confuse them

Element Purpose Border Background
blockquote Real person quote with attribution Yellow top border (4px) White card with shadow
.answer-capsule Our prose framing — section lede/thesis Yellow left border (3px) None (inherits section background)

Never use a blockquote for marketing copy. Never use an answer-capsule for a customer or interviewee quote.


4. Pillar and Card Icons

Uniform icon colour within a section

All icons within a single section (e.g. feature pillars) must use the same colour chip. Do not alternate yellow/blue/yellow across siblings — it reads as arbitrary rather than intentional.

Default: yellow (--wn-brand-yellow) for icon chips. Use blue only if the section is already using yellow for something else (e.g. a yellow background section where yellow chips would disappear).

The ::before top-bar accent on .guide-proof__pillar must also be uniform — remove any nth-child colour overrides.


5. Proof Stat Panel (ProofOutcomeTemplate)

CTA alignment inside flex columns

The .proof-stat container is display: flex; flex-direction: column. Without an explicit override, flex children stretch to full width. Any .cta-secondary inside .proof-stat must have align-self: flex-start so the underline animation spans only the link text, not the full column width.

This rule is already in site.css:

.proof-stat .cta-secondary { align-self: flex-start; }

General rule: whenever a .cta-secondary (arrow link) sits inside a flex-direction: column container, it will stretch to full width unless align-self: flex-start is set. Check for this in any new fragment that uses flex column layout.


6. Resources Row

CTA-secondary links in resource columns

Read all posts, Browse playbooks, and similar column CTAs are navigation shortcuts — plain .cta-secondary arrow links are correct here. Do not promote these to button lozenges.

Exception: if a resource column is actively promoting a conversion action (e.g. "Download the playbook now"), a button is appropriate.


7. CTA Hierarchy by Funnel Stage

Definitions

Direct CTA: A high-commitment action — booking a demo, requesting a diagnostic, signing up for a trial. The visitor is entering a sales conversation. Example: "Book a diagnostic."

Transitional CTA: A low-to-medium commitment action — reading a case study, downloading a playbook, watching an explainer, routing to a deeper use-case page. The visitor goes deeper but does not commit to a sales conversation. Examples: "See how it works," "Read how [Customer] did it," "Download the playbook."

Visual hierarchy by funnel stage

The yellow button (.wn-btn--primary) signals the action Wordnerds most wants the visitor to take at their current stage of the journey. The ghost button (.wn-btn--ghost) is the secondary, lower-risk route.

Page funnel stage Yellow / primary button Ghost / secondary button Rationale
EXPLORING Transitional CTA Direct CTA Most visitors are not yet ready for a sales conversation. The transitional route keeps them in orbit. The direct CTA must still appear — it's there for the rare TRYING visitor who landed on an awareness page. The site nav also always carries the direct CTA for these visitors.
TRYING Direct CTA Transitional CTA (optional) The visitor has evaluated and is ready to book. Make the direct step the dominant action.
INTEGRATING / COMMITMENT Direct CTA One CTA. No warming needed.

Post-proof rule: after a named proof point (ProofOutcomeTemplate, FooterCTATemplate), the direct CTA earns the yellow button regardless of the page's primary funnel stage. The visitor has scrolled past the evidence; by that point they are temporarily TRYING even if the page is otherwise EXPLORING.

Both CTA types must appear on EXPLORING pages

A hero section on an EXPLORING-stage page must have both:

  1. A transitional CTA (yellow — the primary visual)
  2. A direct CTA (ghost — the secondary visual)

A hero with only the direct CTA on an EXPLORING page excludes the majority of visitors who are not yet ready to book. A hero with only a transitional CTA loses the visitors who arrived ready. Both are constraint breaks.

The site nav always carries the direct CTA

SiteNavOrganism.primary_cta always renders the direct CTA ("Book a diagnostic"). This is a permanent escape hatch for TRYING-stage visitors regardless of which section of the page they're reading. It does not replace the hero's direct CTA — it's an additional safety net.

Consistent direct CTA label

The direct CTA label must be consistent across the site: "Book a diagnostic" on all pages and in the navigation. Do not vary the label per page. Variant labels fragment recognition on return visits.


8. Adding a New Page — Checklist

Before authoring a new page schema, verify:

  1. Section sequence: does any white section immediately follow another white section? The padding will auto-collapse. You do not need to do anything — just know it happens.
  2. Every CTA: is it a named conversion action? If yes, button lozenge. If navigation shortcut, plain link is fine.
  3. Blockquotes: every <blockquote> must wrap its text in <p> with a <mark> around the quote text. Check your fragment output.
  4. Icon sets: uniform colour within a group — no alternating.
  5. Flex column containers: any .cta-secondary inside one needs align-self: flex-start.
  6. Hero (if present): use dark-split by default — not centred. When no commissioned asset exists yet, supply a placeholder with explicit dimensions so the asset commission is unambiguous. Standard dark-split placeholder dimensions (.wn-col-5 right column, portrait): 480×540px. Use centred only when the page genuinely has no visual slot (e.g. a text-only comparison page). Do not add margin-bottom bleed to .hero__visual.
  7. Image placeholders: always include concrete pixel dimensions (e.g. 640×360px, 48×48) in every image src placeholder string — not just aspect ratios. The renderer parses these to size the placeholder box honestly. See §12 for the full rule.
  8. Numbered chip colour: yellow chip = step sequence (StepCards); blue chip = numbered capability list (FeaturePillars 4-up). See §10.
  9. CTA bands carry a graphic (Pete 2026-06-22): a conversion band (PromoBandTemplate/offer-band, and any standalone CTA section that supports a media slot) should include an image — the graphic draws the eye to the action. When authoring a new page, always wire a sized image placeholder into the CTA band's media slot (e.g. 480×600px portrait) even before the real asset exists, so the band lands as a media-left layout and the commission is unambiguous. A bare-text CTA band is the exception, not the default.
  10. Eyebrow plan (§17): a page uses eyebrows as a rhythm across several sections, or none — never one orphan. Note most templates lack an eyebrow slot, so "none" is often the proportionate outcome. Never brand-blue on a yellow surface.
  11. Mobile / rendered-output smoke (§22): after building, run npm run smoke -- --pages=<slug> and look at the 375px WebKit screenshot. This is the only step that renders the page in a real browser engine, and the only one that catches Safari/iOS layout breaks (text-over-image overlap, sideways scroll) that a desktop Chrome preview hides. A clean smoke run + a glance at the mobile capture is required before a page is "done". review-page Stage 5.12 enforces this; do it during authoring too, not just at QA. See §22.

9. Visual density — marketing pages are visual-led, low-text

Validated against the reference-site research (2026-05-27). The B2B SaaS reference sites we benchmarked are markedly more visual and lower on text than a default copy-led page. Wordnerds marketing pages should match that: every content section earns a visual, and body copy is trimmed to what the visual cannot carry. This is a SYSTM-aligned principle (reduce reading load; let the visitor scan and grunt-test in seconds), not a per-page whim — apply it on every marketing page unless a page-type rubric overrides.

What "visual-led" means in slots the renderer already supports:

Section / template Visual slot to use Default expectation
Feature pillars (e.g. "Why organisations choose Wordnerds") per-pillar image (PillarSlot.image → .guide-proof__pillar--with-image) each pillar carries a supporting image; body trimmed to ~2 sentences
Step cards (the 1-2-3 plan) per-step image (StepSlot.image → .step__image) each step carries a visual of that step; body ~1 sentence
Pathway / persona cards ("Who are you?") per-card image (PathwayCardSlot.image → .persona-card__avatar) each ICP card carries an avatar headshot
Problem statement quote verbatim.headshot (→ .blockquote__headshot) named-person quotes carry a headshot
Proof outcome named-customer logo + (optional) headshot proof carries the customer's logo

Text economy rule. When a section gains a visual, cut its body copy to match — the image carries weight the prose no longer has to. Keep the AEO answer-capsules intact (they are citation surfaces), but trim pillar/step/card bodies to scannable length. A wall of text next to an image is the failure mode.

Concision + cross-page distinctness are pipeline policy, stated once in docs/PIPELINE-CONVENTIONS.md ("Concision within AEO" — target the AEO-minimum word count, capsule is usually the whole prose; "Visual-led, visually-distinct pages" — section vocabulary is a palette, vary it per page). Not restated here.

This is also a learning-capture rule. Visual + text-density decisions like these were previously hand-edited into the rendered schema and lost on regeneration. They belong here (cross-page principle), in the page brief's "Additional context" (page-specific intent), and in the layout-spec (the actual slots) — never only in the schema. See briefs/homepage/decisions.md 2026-05-27 for the worked example.

9.1 Reinforcement: hero CTA on EXPLORING pages (cross-ref §7)

Confirmed on the homepage (2026-05-27): the hero primary (yellow) CTA is the transitional "See how it works"; the direct "Book a diagnostic" is the ghost secondary. Most homepage visitors are EXPLORING and not ready to book — leading with "Book a diagnostic" as the primary is lower-value than routing them into the method. The nav always carries "Book a diagnostic" as the standing direct CTA for the ready-now minority. This is §7's EXPLORING row; it is restated here because a regeneration flipped it.


10. Numbered Circle Chips

Two distinct numbered-chip primitives live across the site. They share the circle shape but differ in colour and semantics.

Class Size Background Text colour Semantics
.guide-proof__pillar-number 40px diameter --wn-brand-blue white Numbered capability list — used in FeaturePillars 4-up icon-grid variant. "This is a numbered list of named capabilities."
.step__number 28px diameter --wn-brand-yellow --wn-off-black Step sequence — used in StepCards. "This is a step in the plan."

Design rule: yellow circle = action sequence; blue circle = numbered capability list. Do not use yellow chips in FeaturePillars 4-up, and do not use blue chips in StepCards. The colour difference is load-bearing — it signals two semantically different things.

Leading zeros: strip at render time. "01""1". Chips never display 01, 02, etc.

Alignment: .step__header { align-items: flex-start; } — the chip top-aligns with the first line of a wrapped h3. Do not centre-align.


11. Unified Answer-Capsule Style

.answer-capsule has a single global style (validated 2026-05-28). Do not add per-section overrides.

.answer-capsule {
  font-size: var(--fs-md);
  font-style: italic;
  color: var(--wn-dark-grey);     /* same as body — intentional */
  border-left: 3px solid var(--wn-brand-yellow);
  padding-left: var(--space-s);
  margin-bottom: var(--space-m);
}

Rule: the capsule is the section's lede/thesis paragraph. The yellow line + italics + slight size bump (--fs-md vs --fs-base) differentiate it from body without changing colour — deliberately the same colour as body so it reads as authoritative prose, not a callout.

Prior per-section .proof-outcome .answer-capsule override was removed — the global default now matches what proof-outcome previously applied.

11.1 The subtitle rule — a section subtitle is ALWAYS styled (never raw body text)

Validated 2026-06-19 (Pete, recurring). Every section has at most one subtitle — the lede line beneath the <h2>. It must always carry subtitle styling; it must never render as a plain default paragraph. This kept regressing because each template names its subtitle slot differently and some of those classes had no CSS, so the subtitle silently fell back to body text.

Subtitle styling is determined by the section header's alignment — there are exactly two treatments, and a template belongs to one or the other. This is the full classification (audited + standardised site-wide 2026-06-19); both treatments live in the one CSS block under the /* SECTION SUBTITLE */ comment in site.css:

A. LEFT-header sections → italic + yellow left bar (the answer-capsule look; color: --wn-dark-grey, --wn-section--dark/--yellow recolour the text + bar):

Template Header Subtitle class
ProofOutcome · IntegrationCallout · MediaTextSplit · DiagramExplainer left .answer-capsule
FeaturePillars (guide-proof) left .guide-proof__intro
PricingTiers left .pricing-tiers__intro
Presenters left .presenters__intro
RelatedWebinars left .related-webinars__intro
ResourceCards left .resource-cards__intro
StatBand left .stat-band__intro
TestimonialPair left .testimonial-pair__intro
IntegrationsGrid left .integrations__intro

B. CENTRED-header sections → italic, NO bar (a yellow bar reads wrong centred under a centred title):

Template Header Subtitle class
AlternatingWalkthrough centred .walkthrough__intro
StepCards (how-it-works) centred (heading centred 2026-06-19; #stairs is the lone left exception) .step-cards__intro
FrameworkLayers centred .framework-layers__intro

So: italic is non-negotiable for a subtitle. The yellow bar is only for left-aligned subtitles. A centred subtitle drops the bar but stays italic. Match the subtitle to the header alignment, not the other way round — if you change a section's header alignment, move its subtitle to the matching group.

(Hub/listing pages — blog, customer-stories, book-a-diagnostic, transcript, HubSpot-form — carry a listing-page intro, not a marketing-section subtitle; they are deliberately outside this rule.)

Subtitle vs body. A subtitle is a single framing line. Detail/explanatory prose is body text — upright, not italic, no bar. If a section needs both (e.g. StepCards intro + body), the subtitle is italic and the body is plain; never style two stacked paragraphs as subtitles (that "pre-subtitle" stack reads as a mistake — see the platform onboarding fix, 2026-06-19).

Process rule (every stage of the pipeline): whenever a stage introduces, moves, or restyles a subtitle slot (answer_capsule / intro / template-specific lede), it must confirm the slot resolves to one of the styled classes above. A new template's subtitle slot is not done until its class is added to this rule and given CSS. Don't invent a new unstyled *__intro class.


12. Image Storage Path + Placeholder Dimensions

Storage path — the central library (where the file lives)

Every page-content image lives in the central library site/images/<role>/, organised by component role (hero/, card/ 16:9, half-column/ 4:3, diagram/, screenshot/, blog/<slug>/, plus shared entity sets people/, customer-stories/, customer-logos/, cx-corner/, webinars/, playbooks/). build.ts (ensureSiteImages) copies the whole tree to output/images/, served as /images/.... Every schema src is /images/<role>/<name> — never a per-page /<slug>/images/... path, never an output/... path, never a third-party CDN URL for page-owned imagery. Reuse an existing role file before adding a near-duplicate. (Full rules: CLAUDE.md "Page imagery — the central library". The old per-page site/page-images/<slug>/ split was retired 2026-06-18.) design-system/brand-assets/ (logos/, illustrations/, certifications/) is the separate design-system-furniture tree — not for page content.

Placeholder dimensions (the size of the box)

The renderer (lib/util.ts renderImage) parses pixel dimensions from image src placeholder strings and applies them as aspect-ratio + max-width inline styles. This makes placeholder boxes occupy the same footprint as the final production asset, keeping layout review honest.

Convention: always include explicit pixel dimensions in every image placeholder string, formatted as <W>×<H>px (e.g. 640×360px, 760×960px, 48×48). Aspect-ratio notation alone (e.g. 16:9, 4:3) is insufficient — it does not produce a sized box.

Context Standard dimensions
Hero dark-split media (.wn-col-5 portrait) 760×960px
Pillar/step supporting images 640×360px (16:9) or 640×480px (4:3)
Pathway card avatars 48×48px
Customer headshots (proof) 80×80px
Problem statement headshots 40×40px

Copy-author rule: use concrete pixel sizes. If the dimensions are not yet decided, use the standard dimensions from the table above and flag [CONFIRM DIMENSIONS].


13. Problem-Elaboration Column Alignment

The ProblemStatementTemplate renders a two-column grid. The correct alignment is align-items: start on the grid — not align-items: stretch. This means:

The CSS rule is:

.problem-elaboration .wn-grid { align-items: start; }
.problem-elaboration__inner blockquote { margin-top: 0; }

Do not revert to align-items: stretch — an earlier attempt to lift paragraph 1 above the grid to fill the gap produced a worse layout (low-right gap, content flow disrupted).


14. ICP Persona Avatars

Three photoreal headshots represent the canonical Ideal Customer Profiles on persona-routing surfaces (homepage persona-pathways cards; reusable on the /for-analysts, /for-cx-managers, /for-cx-leaders sub-page heroes).

Asset mapdesign-system/images/people/:

File Persona Role Identity (resolved state)
icp-anna.jpg Anna Insight Analyst credibility-armoured analyst
icp-mark.jpg Mark CX Manager / Head of Insight evidence-led decision broker
icp-sarah.jpg Sarah Transformation Leader (VP CX / CCO) cultural transformer

Anna — Insight Analyst Mark — CX Manager Sarah — Transformation Leader

Canonical persona detail is NOT duplicated here. Goals, pains, quotes, tone register, and the full identity-shift rationale live in wordnerds-wiki/assets/canonical/strategic/personas.md — the single source of truth. Link to it; do not copy persona copy into the design system (it drifts).

Visual convention — match these when adding or regenerating:

Generation recipe: nano-banana-pro via scripts/generate-image.py with --no-ref-images (the script's default ref-set is brand illustrations — wrong for photoreal people). Full briefs + provenance: briefs/icp-avatars/icp-avatar-briefs.md.


15. Balanced Card Grids — count drives the column span

Validated 2026-06-04 (Pete, repeat of an earlier /customer-feedback-analysis-software correction). A card grid must never leave a lonely card stranded on its own row. The number of cards decides the column span — not a fixed default, and not the template variant name:

Cards in the row Column span Layout
2 wn-col-6 two halves, side by side
3 wn-col-4 one row of thirds
4 wn-col-6 2×2 (two rows of two halves) — not four-across, never 3 + 1
6 wn-col-4 two rows of thirds

The rule: even counts (2, 4) use half-width columns; counts that divide into threes (3, 6) use thirds. Four half-width cards wrap to a balanced 2×2 with the standard --space-m gutter. The failure mode this fixes: hardcoding wn-col-4 on every card meant 4 cards summed to 16 grid columns and overflowed to an ugly 3 + 1.

This is enforced in code, not left to the schema author. cardSpanClass(count) in lib/util.ts returns the span; step-cards.ts and pathway-cards.ts call it with the item count. feature-pillars.ts reaches the same result through its 4-up → col-6 variant logic. Any new card-grid fragment must use cardSpanClass (or match its output) — do not hardcode a span. This is the design-process safeguard Pete asked for: balanced grids happen by default, every time, without per-page intervention.

Mobile (≤768px) collapses every span to full width regardless (GRID.md §5), so this only governs the desktop row shape.


16. Webinar Pages

Added 2026-06-04. The webinar-page vocabulary (transcript pages + /webinars listings). See TEMPLATES.md → "Webinar templates" for slots/variants. Three patterns are load-bearing:

16.1 YouTube embed — privacy-friendly click-to-load facade

Never embed an eager <iframe>. A webinar page is already heavy (full transcript inlined + inlined CSS); an always-on YouTube player pulls ~1MB of JS on every view and sets tracking cookies before the visitor presses play. WebinarVideoTemplate instead renders a facade: a lazy i.ytimg.com/vi/<id>/maxresdefault.jpg poster + a brand-yellow play button (.webinar-video__play), 16:9 via aspect-ratio. A scoped inline <script> (keyed by node.id, same isolation discipline as the book-diagnostic HubSpot embed) swaps the facade for a https://www.youtube-nocookie.com/embed/<id>?autoplay=1 iframe on click. Reuse this facade for any future video embed — do not introduce an eager iframe.

16.2 Transcript — server-rendered, speaker-labelled

The full transcript must be in the server-rendered HTML (AEO: AI crawlers do not execute JS). TranscriptTemplate emits the text directly; the optional disclosure variant folds it inside a native <details> (still crawler-readable) — never a JS accordion. Speaker turns render as paragraphs with a bold .transcript__speaker label on the first paragraph; editorial subheadings render as <h3>. Per-line timestamps live only in the YouTube chapters, not the page.

16.3 Webinar cards + the cross-link module are build-generated

RelatedWebinarsTemplate cards are never hand-authored — they are injected at build time from site/webinars-index.json by scripts/build.ts (injectWebinarCards): a transcript page gets its 3 "most related" webinars (same sector → same partner → most recent), the /webinars listing gets the full set grouped by sector. To add a webinar to every page's cross-links: add/refresh its entry (npm run sync:webinars mirrors the wiki webinars-index.yaml), then rebuild. The .webinar-card is a self-contained card component (thumbnail + title + one-liner + date · N min · sector meta), not a 12-col span. The module renders nothing when there are no cards.


17. Eyebrows — one canonical treatment, used as a deliberate page rhythm

Added 2026-06-19 (Pete). An eyebrow is the short label above an <h2> (e.g. "Why Wordnerds", "Two sides of Wordnerds"). It keeps a consistent treatment everywhere.

Canonical style — .eyebrow. Brand-blue, uppercase, --fs-xs, weight 700, letter-spacing 0.12em, margin-bottom: --space-s. Blue is a secondary accent (DESIGN §2) — eyebrows are never yellow (yellow stays the one CTA per surface) and never a sentiment colour. On a dark section the eyebrow goes white/light; on a yellow section, off-black (overrides already in site.css). The PromoBandTemplate kicker is the same thing under a different slot name — .promo-band__kicker is aliased to .eyebrow, so a promo kicker matches a section eyebrow exactly. Don't invent a new unstyled eyebrow class (the kicker shipped unstyled once and fell back to body text — 2026-06-19).

Contextual variants (intentional, not the section eyebrow): .hero__eyebrow (light/link colour on the dark hero), .blog-post__eyebrow (post category), and card-level kickers (.customer-story-card__eyebrow, .resource-card__kicker) are muted/recoloured for their surface. These are deliberate — a card kicker is metadata, not a section label.

Alignment follows the header (same as subtitles, §11.1). An eyebrow sits directly above the heading and takes the section's alignment: on a left-header section it sits left; on a centred-header section it must be centred too (a left eyebrow under a centred title is a mismatch). Today every eyebrow on the site is on a left-header section; if you add one to a centred-header section (e.g. a StepCards section), centre the eyebrow as well.

Use them as a rhythm, never as an orphan. Whether a page uses eyebrows is an editorial choice, but it is a page-level choice: either the page uses eyebrows across several of its major sections as a deliberate rhythm, or it uses none. A single lone eyebrow on a page that otherwise has none reads as a stray (this is exactly why the platform "Two sides of Wordnerds" kicker was first removed, then reinstated alongside "Why Wordnerds" + "Meet the Nerds" — 2026-06-19). Eyebrow copy is a short label or a characterful phrase; keep the register consistent within a page.

Never brand-blue on a yellow surface (contrast rule, Pete 2026-06-22). Brand-blue (#39B8E1) on brand-yellow (#FAB316) fails legibility — so on .wn-section--yellow the eyebrow goes off-black, never blue. The override has to name every eyebrow class, including the alias .promo-band__kicker (the base rule colours the kicker blue; if the yellow override lists only .eyebrow, a yellow PromoBand's kicker stays an unreadable blue — the exact bug fixed 2026-06-22). This generalises beyond eyebrows: don't put brand-blue text or glyphs on a yellow fill anywhere.

Applying the §17 plan to a page where most templates lack an eyebrow slot. Only some templates carry an eyebrow/kicker slot (e.g. FeaturePillars, PromoBand); ProblemStatement, DiagramExplainer, Presenters and FAQ currently do not. If a page is built mostly from no-eyebrow templates, a page-wide rhythm isn't available without adding eyebrow slots to those fragments — so the proportionate §17 outcome is usually none (remove the lone supported-template kicker rather than leave it an orphan). Adding eyebrow-slot support to more templates is a deliberate enhancement, not a per-page fix (smart-segmentation-playbook landed on "none" for this reason, 2026-06-22).


18. Breadcrumbs — automatic on subpages, off on top-level pages

Added 2026-06-18 (formalised during the c-rating-playbook rebuild). The breadcrumb trail is rendered by the renderer (renderBreadcrumbs / buildCrumbs in lib/renderer.ts) — never hand-authored in a schema. It is derived from the page slug, so it can't drift from the URL.


19. Playbook page conventions (the c-rating reference spine)

Added 2026-06-18 (Pete); c-rating-playbook is the canonical reference — see rubrics/playbook.md. A playbook page is a long lead-magnet read, and these conventions are what make it cohere. They are the proven spine; a new playbook follows them unless it has a documented reason not to.


20. Page-type body class — scope per-class styling without per-slug rules

Added 2026-06-23 (Pete). The renderer puts a page-type class on <body> alongside the per-slug class: page-type-<page_type> (e.g. page-type-case-study, page-type-playbook), derived from page.context.page_type. Use it to scope shared styling for a whole page class — refinements that should apply to every page of a type — without targeting each slug or adding a per-section modifier slot to the schema. This is the clean hook for "make all customer stories do X". (The per-slug page-<slug> class is still there for one-off page tweaks.)

21. Customer story (case-study) page conventions (the Sainsbury's reference spine)

Added 2026-06-23 (Pete); /customer-stories/sainsburys is the canonical reference — see rubrics/customer-story.md. A case study reads top-to-bottom as a flowing narrative, not a stack of boxed panels. The redesign that established this rejected an earlier attempt to graft enriched-blog furniture (a "customer snapshot" panel, a story-in-brief blue panel, a light-split image hero) onto the page — "too many boxed areas at the top and a lack of page structure that gives it flow". The proven spine (all scoped to .page-type-case-study, §20):


22. Mobile / rendered-output smoke — npm run smoke

Added 2026-06-26 (Pete). The structural check (npm run check) and most of review-page read the static HTML; they never render it. npm run smoke (scripts/smoke.ts) is the rendered-output sibling — the only step that opens each page in a real browser engine at mobile/tablet/desktop widths. It exists because the highest-impact bug class is a WebKit (Safari + every iOS browser) layout break that desktop Chrome renders correctly — invisible to a normal desktop preview, and easy for even a vision pass over screenshots to misjudge.

Worked example (the reason this exists). The homepage image-pillars used CSS subgrid, which WebKit collapses — printing each heading on top of its photo, on iPhone and desktop Safari. It shipped twice (an ineffective first fix, then a vision review that misread a dark-text-on-dark-dashboard overlap as clean). A geometric detector caught it immediately. Fix: image-pillars are plain block flow now — do not reintroduce subgrid for cross-pillar row alignment (DESIGN/TEMPLATES note it too).

What it does

For every page × chromium+webkit × 375/768/1280px it: asserts no horizontal overflow and no JS console error (→ FAIL); detects text geometrically sitting on top of an image (→ FAIL — the overlap class above); flags off-screen elements (→ WARN); and writes a full-page screenshot grid to .smoke/index.html. It forces lazy images to load and ignores designed overlays (absolute/fixed/sticky), closed <details>, and offline external-resource errors, so the FAILs are real.

How to use it

npm run smoke:setup                 # one-off — installs Playwright browsers
npm run build:<slug>                # smoke serves output/, so build first
npm run smoke -- --pages=<slug>     # this page, both engines, 3 viewports, + screenshots
npm run smoke                       # whole site (also runs on every PR to main via CI)

Then look at .smoke/screenshots/webkit/375/<slug>.png — the assertions catch overflow and overlap, but only your eye catches clipping, mis-wrapped headings, and broken aspect ratios.

Where it sits in the process

A green smoke run is not sufficient on its own (it can't see "ugly"); a green run plus a glance at the mobile screenshot is the bar.