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:
problem-elaboration→guide-proofguide-proof→how-it-workspersona-pathways→proof-outcome
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:
- White card background + brand-yellow top border (4px) +
box-shadow: var(--wn-shadow-card) <p><mark>"…"</mark></p>— the yellow<mark>sits behind the quote text- Font:
--wn-font-rounded,--fs-xl,font-weight: 800 - Attribution: plain
<cite>below the<p>—--fs-sm,--wn-mid-grey,font-style: normal
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:
- A transitional CTA (yellow — the primary visual)
- 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:
- 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.
- Every CTA: is it a named conversion action? If yes, button lozenge. If navigation shortcut, plain link is fine.
- Blockquotes: every
<blockquote>must wrap its text in<p>with a<mark>around the quote text. Check your fragment output. - Icon sets: uniform colour within a group — no alternating.
- Flex column containers: any
.cta-secondaryinside one needsalign-self: flex-start. - Hero (if present): use
dark-splitby default — notcentred. When no commissioned asset exists yet, supply a placeholder with explicit dimensions so the asset commission is unambiguous. Standarddark-splitplaceholder dimensions (.wn-col-5right column, portrait): 480×540px. Usecentredonly when the page genuinely has no visual slot (e.g. a text-only comparison page). Do not addmargin-bottombleed to.hero__visual. - Image placeholders: always include concrete pixel dimensions (e.g.
640×360px,48×48) in every imagesrcplaceholder string — not just aspect ratios. The renderer parses these to size the placeholder box honestly. See §12 for the full rule. - Numbered chip colour: yellow chip = step sequence (StepCards); blue chip = numbered capability list (FeaturePillars 4-up). See §10.
- CTA bands carry a graphic (Pete 2026-06-22): a conversion band (
PromoBandTemplate/offer-band, and any standalone CTA section that supports amediaslot) 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'smediaslot (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. - 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.
- 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-pageStage 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:
- Left column (body text): flows naturally from the answer-capsule above; the text starts at the top of the grid row.
- Right column (blockquote): tops align with the left-column body, not with the answer-capsule.
- Whitespace at the bottom of the shorter column is intentional — do not add
height: 100%to equalise.
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 map — design-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 |
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:
- Synthetic faces, never a real person's published photo. Grounded in the real CRM base (UK Housing-dominant, business-casual register) for demographic accuracy only — see the brief for the HubSpot grounding method.
- Framing matches the existing customer headshots (
Kat-Eastwood-Sainsburys.png): tight head-and-upper-shoulders, small headroom, cut at the upper chest. Crop from the full-res original, don't upscale. - Soft, slightly-warm mid-grey studio gradient background; soft even lighting; natural skin texture (avoid the over-smoothed "AI plastic" look).
- 1024px square JPEG (~q80, ~120–155 KB) — crisp at the 48px card crop, usable at sub-page hero size.
- Genders retained per canon (Anna F, Mark M, Sarah F). Sarah is fuller-figured — true to the seniority cohort we actually deal with.
- The whole set must read as one shoot: same background, lighting, crop, and register.
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.
- Visibility is slug-depth driven. A
.breadcrumbsband renders at the top of<main>only on genuine subpages (≥2 slug segments). Top-level pages (reached straight from the nav —/pricing,/consultancy,/about) stay chrome-free: a crumb trail back to Home from a nav page is noise. - Listing collapse. When the first slug segment is a listing root (
playbooks,webinars,customer-stories,sectors,blog,cx-corner), the trail collapses to Home ›<Listing>›<this page>rather than one crumb per segment. Synthetic segments that aren't real URLs (e.g. the trailingtranscripton/webinars/<x>/transcript) are skipped — the meaningful label comes from the<x>segment. - Label override. The current page's crumb label defaults from the slug segment (
crumbLabeltitle-cases and de-hyphenates); setpage.meta.breadcrumb_labelto override when the slug doesn't read well. - One source for trail + schema.
buildCrumbsfeeds both the visible trail and theBreadcrumbListJSON-LD, so the two can never diverge. Don't emitBreadcrumbListseparately. - Style.
.breadcrumbsis a quiet white band,--fs-sm, mid-grey links with a›separator (li + li::before); the current page is off-black weight-600 and carriesaria-current="page". No yellow — it's wayfinding, not a CTA.
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.
- Inline freshness dateline, not a band. The "Published … · Last updated …" dateline (AEO freshness, mirrors the
ArticleJSON-LD) is not a standalone section below the hero — that read as an orphan band. On playbook pages (page_type: "playbook") the renderer injects it inline, right-aligned, at the top of the firstProblemStatementTemplate(the TL;DR) via.page-dateline__text--inline. Two regressions to avoid (both fixed 2026-06-19): the inline variant needs its own bold/off-black date rule (the band-only.page-dateline timerule doesn't reach it), and it needsmax-width: none(the globalp { max-width: 68ch }otherwise caps the<p>and makestext-align: righthug the middle of a half-width box). - Answer-capsule subheaders, not plain intros. A section's lede goes in the
answer_capsuleslot (yellow-bar + italic subheader, §11) rather than a plainintro— it reads as authoritative framing and is an AEO citation surface. This is the default for playbook section ledes. - Equal-height cards per row. Card grids (e.g. FeaturePillars icon-grid) use
align-items: stretchso boxes in a row match the tallest and their bottoms align. This is a general, harmless fix — applied site-wide via.guide-proof__pillars--icon-grid. - Sticky-nav framework section. The multi-part teach-the-model section uses
FrameworkLayersTemplatesticky-nav(scroll-spy "you are here" cue), notalternating— see TEMPLATES.md. - Contributors at the top; priced offer-band as the single CTA. Contributors/credibility sit near the top. The priced offer-band (a
media-leftPromoBandTemplateco-branded via FeaturePillarsheader_media) is the single conversion CTA — playbooks drop theFooterCTATemplateso there's one clear ask, not two competing closes. - Co-brand lockup + backlink. Partner co-branding uses FeaturePillars
header_media+header_media_href(logo level with the<h2>, doubling as an outbound backlink) — see TEMPLATES.md. - Delivery-team section for service offers (added 2026-06-22). When the playbook's offer is a done-for-you service (not a self-serve download), add a "The team who'll run your report"
PresentersTemplategrid after the offer — real photoreal headshots of the people who deliver it, two rows of three with a dashed ghost 6th card (.presenters__card--ghost). Personalises the human deliverable at the point of conversion. Seerubrics/playbook.md§10b; referencesmart-segmentation-playbook. (Download/worksheet playbooks like c-rating don't need it.)
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):
- Quiet hero. Centred dark hero = company logo (enlarged) + title + a "Last updated" dateline (injected from the manifest). No CTA buttons and no hero image — the hero is an identity-and-headline moment, not a conversion or visual one. The back-link eyebrow (
← All customer stories) renders brand-blue (.page-type-case-study .hero__eyebrow), matching the CX Corner archive back-link. - "Key Metrics" band. The
StatBandTemplateresults band carries aneyebrow"Key Metrics" (centred above the figures;.stat-band__eyebrowremoves the top-heavy grid margin so the numbers sit balanced top-and-bottom in the light-grey bar). Theeyebrowslot was added toStatBandTemplatefor this. - The page illustration lives in the challenge, not the hero. It's the
ProblemStatementTemplateillustrationslot (right aside); where the challenge also has aquote, the image sits above it in the aside. - The impact quote box has no button.
ProofOutcomeTemplatedrops itsctaslot — the page's single direct CTA is the footer band, so the proof quote stays clean (mirrors the playbook "one clear ask" rule, §19). - Full-bleed rule above "About". A hairline rule precedes the "About
Customer" section, bleeding edge to edge (border-top on the full-bleed.wn-section, with the inner.wn-containercarrying the top padding) — the same full-screen rule treatment as the CX Corner page foot. Gives the page foot structure.
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
- During authoring — checklist item §8.11: run it and glance at the mobile capture before calling a page done.
- At QA —
review-pageStage 5.12 runs it and maps FAIL→BLOCKER, WARN→WARNING/NOTE, plus a mandatory look at the 375px WebKit screenshot. - In CI —
.github/workflows/smoke.ymlruns it on every PR tomain.
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.