The decision document for how the brand looks and behaves on screen. The canonical token file is /brand/styles/tokens.css; this document explains the why behind each decision and how to use the tokens correctly. Every rule below was chosen against the positioning: operator-led, AI-native, weeks-not-quarters. If a visual choice doesn't earn that frame, it doesn't ship.
1. Visual posture
Confident, modern consulting. The firm presents the way thgadvisor.com presents today — white-dominant, type-led, with the green-and-blue brand marks used confidently — but with deliberate refinement. The brand should feel adult and considered: senior operators in a boardroom, not a startup chasing attention.
This rules out three common defaults. No dark-default, monospaced "AI lab" posture — the buyer is a PE-backed CEO who wants a partner, not a research demo. No big-firm corporate blue-and-grey — that puts us next to McKinsey on the page, which is exactly where the positioning says we lose. No gradient-and-glow startup palettes — soft pastels and chromatic abstractions undercut the seriousness of what we sell.
What is in: a white canvas, deep ink for type, generous white space, hairline-bordered cards, pill-shaped buttons, and the two brand colors — #37CA37 signal-green and #188BF6 signal-blue — used confidently as the primary brand marks. Green is the call-to-action color. Blue is the link and interactive color. Photography is named operators in real working settings. The aesthetic vocabulary is recognisably the same family as the live site, refined and tightened — not a wholesale aesthetic shift.
Dark mode exists as an optional [data-theme="dark"] override so a theme toggle works and sections that genuinely need a dark surface (footer, code blocks) can opt in. It is not the canonical brand mode. The brand is light.
2. Color
Token model
Three layers, defined in /brand/styles/tokens.css:
| Layer | Lives where | When to reference |
|---|---|---|
Primitives (--c-signal-green-500, --c-ink-950…) |
tokens.css only |
Never in components |
Semantic (--color-surface, --color-text, --color-brand, --color-link…) |
tokens.css |
Everywhere in components |
Component (--button-radius, --card-padding, --nav-height…) |
tokens.css |
When the component must deviate |
Palette anchors
| Token (primitive) | Hex | Role |
|---|---|---|
--c-paper-white |
#FFFFFF |
Dominant page canvas. The brand sits on white. |
--c-paper-50 |
#FAFBFC |
Soft sunken band between sections |
--c-paper-100 |
#F5F7FA |
Stronger sunken section (--smoke equivalent on live) |
--c-graphite-100 |
#E6E8EC |
Default hairline border on white |
--c-ink-950 |
#0B0D10 |
Primary text on white (~19.5:1, AAA) |
--c-ink-500 |
#3F4654 |
Muted body text |
--c-paper-500 |
#8A92A0 |
Subtle text, captions |
--c-signal-green-500 |
#37CA37 |
Brand anchor. CTA fill (pairs with dark ink text) |
--c-signal-green-700 |
#208620 |
Green text on white — passes AA (~6.5:1) |
--c-signal-green-ink |
#08361A |
Dark text used ON the green CTA fill (matches live site) |
--c-signal-blue-500 |
#188BF6 |
Brand anchor. Blue fill / large-text marks |
--c-signal-blue-600 |
#0E6FD0 |
Link text on white — passes AA (~5.6:1) |
--c-error-600 |
#B92626 |
Destructive / failure text on white |
Semantic mapping
Components consume these. Theme swap re-points the same names to different primitives.
--color-surface #FFFFFF — default page surface
--color-surface-sunken #F5F7FA — alternating section band
--color-surface-elevated #FFFFFF — cards (differentiated by border + shadow, not fill)
--color-surface-inverse #0B0D10 — dark surface on light page (footer)
--color-text #0B0D10 — primary copy
--color-text-muted #3F4654 — secondary copy
--color-text-subtle #8A92A0 — captions, meta
--color-text-on-brand #08361A — dark text on green CTA
--color-text-on-accent #FFFFFF — white text on blue fill
--color-border #E6E8EC — hairline default
--color-border-strong #D1D5DB — emphasis border
--color-brand #37CA37 — CTA fill (green)
--color-brand-hover #2BAA2B — CTA hover
--color-brand-text #208620 — green text when needed on white
--color-accent #0E6FD0 — link / interactive text (blue)
--color-accent-hover #0B57A4
--color-accent-fill #188BF6 — blue button fill (uses white text)
--color-link (alias of --color-accent)
--color-focus-ring #188BF6 — keyboard focus
Usage rules
- The dominant color of the page is white. Sections alternate between
--color-surface(white) and--color-surface-sunken(light grey-blue) to give rhythm. Avoid large green or blue fills as section backgrounds — those colors do work as marks, not surfaces. - Green is the CTA color. Primary buttons fill green with dark ink text. Use green sparingly elsewhere — icon-halo tints (
--c-signal-green-50), the "shipped" or "active" mark, and the focus ring. - Blue is the link / interactive color. Inline links, arrow links, "read more" affordances, focus rings. Blue may also appear as the secondary button fill (white text on blue) when a section already uses a green primary nearby.
- One primary CTA per visible viewport. Two greens fighting on a hero is the most common mistake — demote the second to secondary or ghost.
- Don't put green or blue text on white below the AA threshold. Body text uses
--color-text(ink-950). Brand-colored text uses the-text/600step (--color-brand-textfor green,--color-accentfor blue), not the 500 anchor. - Don't use color alone to carry meaning. Pair it with a weight change, an icon, or a label.
Contrast verified
All combinations tested against WCAG 2.1 AA (4.5:1 normal text, 3:1 large text / UI):
| Foreground | Background | Ratio | Pass |
|---|---|---|---|
ink-950 #0B0D10 |
paper-white #FFFFFF |
19.5:1 | AAA |
ink-950 |
paper-100 #F5F7FA |
18.3:1 | AAA |
ink-500 #3F4654 |
paper-white |
9.0:1 | AAA |
paper-500 #8A92A0 |
paper-white |
3.4:1 | AA (large/UI only) |
signal-green-700 #208620 |
paper-white |
6.5:1 | AA |
signal-blue-600 #0E6FD0 |
paper-white |
5.6:1 | AA |
error-600 #B92626 |
paper-white |
6.5:1 | AA |
signal-green-ink #08361A |
signal-green-500 #37CA37 |
8.9:1 | AAA |
paper-white |
signal-blue-500 #188BF6 |
3.5:1 | AA (large/UI only — use for buttons, not body) |
paper-white |
signal-blue-600 #0E6FD0 |
5.6:1 | AA |
paper-white |
ink-950 (footer) |
19.5:1 | AAA |
The 500-step of each brand color does not pass AA for body text on white (#37CA37 ≈ 1.85:1, #188BF6 ≈ 3.5:1). That's by design — those values are CTA fills and large brand marks, not body text. The 600/700 steps exist so text-level usage always passes.
3. Typography
Faces and roles
| Role | Family | Why |
|---|---|---|
| Display + body | Inter | Editorially neutral, excellent at large display weights, ships with the variable axes we need (400/500/600/700/800). Carries both display and body so the page reads cohesive. Matches the live site's primary face. |
| Accent | Space Grotesk | Used sparingly — eyebrows, section labels, occasional headline accents where Inter would read too neutral. Slightly more character without breaking the frame. |
| Mono | System monospace | Reserved for code blocks and technical metadata only. Stack: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, .... No custom monospace dependency. This change ships one fewer font load and removes the JetBrains Mono Google Fonts URL. |
Two web fonts in total: Inter + Space Grotesk. Both load from Google Fonts (or self-host) using the existing site's mechanism.
Scale
Modular 1.25 base. Display sizes are softened compared to a magazine-cover scale (display-xl tops at 64px, not 72px) so the page reads as "modern consulting" rather than "editorial".
| Token | Size / line-height | Use |
|---|---|---|
--fs-display-xl |
clamp(40 → 64) / 1.05 | Hero headline, one per page |
--fs-display-l |
clamp(32 → 48) / 1.10 | Section opener |
--fs-display-m |
clamp(26 → 36) / 1.15 | Sub-section headline |
--fs-heading-l |
24 / 1.25 | H2 in long-form |
--fs-heading-m |
20 / 1.35 | H3 |
--fs-heading-s |
18 / 1.45 | H4, card titles |
--fs-body-l |
18 / 1.55 | Lede paragraph |
--fs-body |
16 / 1.60 | Default body |
--fs-body-s |
15 / 1.55 | Secondary body, meta |
--fs-caption |
12 / 1.50 + 0.12em tracking, uppercase |
Eyebrows, labels |
Do
- Hierarchy through weight and scale, not color or boxes. A display set in Inter 700/800 against quiet body in Inter 400 carries the page.
- Use Space Grotesk eyebrows above section headlines for a hint of structural rhythm —
Strategic Exec Alignment,Outcomes,Bench. Keep them small (caption size), uppercase, tracked +0.12em, weight 600. - Set body at 65–75 characters per line. The
--container-content(72ch) and--container-narrow(880px) tokens enforce this.
Don't
- Don't pair Inter with another sans-serif body face — three sans families is indecision.
- Don't use Space Grotesk for body text — it gets clunky below 16px.
- Don't underline links inside a paragraph body unless the surrounding text is body-size and the underline is hover-only.
- Don't justify body text. Don't track display text positive. Don't all-caps anything bigger than caption.
4. Spacing and layout
Scale
Single 4px-base geometric scale, no in-between values. Token names match the size in px so a value can be inferred from the name:
--space-4 4px --space-32 32px
--space-8 8px --space-48 48px
--space-12 12px --space-64 64px
--space-16 16px --space-96 96px
--space-24 24px --space-128 128px
If you reach for --space-20 it doesn't exist — pick a side.
Section rhythm
One token controls vertical breathing between sections, fluid via clamp():
--space-section: clamp(64px, 8vw, 96px); /* default section padding */
--space-section-lg: clamp(80px, 10vw, 128px); /* hero / major panels */
Use these for vertical section padding. Don't write padding-top: 88px in a section style — it'll break responsively. Use the token.
Container widths
| Token | Width | Use |
|---|---|---|
--container-content |
72ch |
Long-form reading (articles, doc body) |
--container-narrow |
880px |
Narrow content (forms, post bodies) |
--container-wide |
1240px |
Default marketing pages (matches live site) |
--container-full |
1440px |
Edge / hero layouts |
Page edges get a fluid gutter (--gutter: clamp(16px, 4vw, 24px)) so content never crashes into the viewport edge on small screens.
Grid
12-column grid inside --container-wide. Gutter 24px desktop, 16px tablet, 16px mobile. Most marketing layouts collapse into 1 or 2 columns on mobile and lean on the grid only above ~880px.
5. Iconography direction
Style: thin-stroke, geometric, square terminals. Inspiration: Lucide, Phosphor (regular weight), Tabler. Stroke: 1.5px at 24px size, scales linearly with size. Corner treatment: 1px radius — soft enough not to feel utilitarian, sharp enough to read modern.
What this rules out: cute illustration-style icons, dual-tone icons, glyph icons with internal shading, anything that reads like a B2C consumer app.
Icon sizes follow the spacing scale: 16, 20, 24, 32. The default in nav and body inline is 20px. Buttons get 16 or 20 depending on button size.
Where icons get a coloured background tile (e.g., service-line icons, trust-card icons — see live site's .service-tile__icon / .trust-card__icon), the tile is a --radius-md square in --color-brand-muted (light green) or --color-accent-muted (light blue), sized 56–64px, with the icon centred inside. Two-tone tile schemes (mixed green and blue across a grid) read well; pick one tone per row.
For specialty marks (council badges, service-line glyphs) commission a small custom set later — they should share the 1.5px stroke and square terminals so they read as one family with the UI icons.
6. Photography direction
The brand's positioning rests on named operators with real careers. Photography has to reflect that.
When to use real people
- Named bench portraits are the primary use of photography. Walt Carter, Humberto Castillo, Marty Smith, Bill Price, and the broader bench get studio portraits in a consistent style: medium-tight crop, soft directional lighting from camera-left, slightly desaturated, eyes-engaged-with-camera, neutral background (warm grey or muted blue), business-formal but not stiff. The portrait style is the brand asset — every operator on the bench has one in the same frame.
- Engagement-context photography comes second: an operator in a real working setting (whiteboard, table, screen). Same lighting language. No stock-styled "diverse team smiling at laptop." If we can't shoot it ourselves with our actual people, we don't fake it.
- Hero treatment. When a hero uses a photograph, apply the
--gradient-hero-overlay(dark blue-tinted gradient, transparent at top → near-opaque at bottom) so white display copy and CTAs read against any image. Matches the existing site's industry-tile and council-tile treatment.
When to use illustration or data-viz
- Anywhere a stock photo would be needed. Service line headers, hero secondaries, blog accent imagery — these are illustration or data-viz, not stock.
- Illustration style: abstract, geometric, neutral. Limited palette: ink, paper, one brand color per piece (green or blue, not both). Stroke-based, never gradient-filled.
- Data-viz: brand chart palette is
paper-200grid lines,ink-950text,signal-green-500andsignal-blue-500as primary series colors,error-500only for failure/decline. Charts always include a small Space Grotesk caption label (% YoY,H1 2026).
What we never use
- Stock photography of generic professionals.
- Office photography that wasn't shot at our office.
- Illustration with cartoon characters or "abstract people."
- Gradient mesh backgrounds, AI-generated abstract art, "tech particles."
7. Component principles
This section names the components and their rules. The CSS itself lives in the brand book build — components.css — and references the tokens above. Each component below documents anatomy, variants, states, tokens consumed, and one "don't."
Button (pill)
- Anatomy: pill-shaped (radius 999px), label (required), optional leading icon, optional trailing arrow glyph, focus ring.
- Variants:
primary— green fill (--color-brand) with dark ink text (--color-text-on-brand). The default CTA.secondary— blue fill (--color-accent-fill) with white text. Use when a section already uses green elsewhere, or when the action is more "info" than "convert" (e.g., Learn more, See the bench).dark— deep ink fill (--color-surface-inverse) with white text. Used for high-formality CTAs in light sections.outline— transparent fill, current-color border, ink text. The quiet alternative for grouped CTA rows.ghost— transparent fill, no border, ink text. Used on dark sections (hero overlays) where outline + ghost combine for the secondary CTA.danger— red fill (--color-danger) with white text. Used only for destructive actions in product UI.
- Sizes:
sm(36px),md(48px default),lg(56px). - States: default, hover (subtle
translateY(-1px)+ brand-hover background + soft brand-tinted shadow), focus-visible (2px focus ring with 2px offset), active (translate-y returns to 0), disabled (50% opacity, no pointer), loading (label swapped with spinner glyph). - Tokens:
--color-brand,--color-text-on-brand,--button-radius(999px),--button-height-md,--button-padding-x-md,--color-focus-ring. - Don't: don't use the primary green button more than once per visible viewport. If two CTAs need to coexist, the second becomes secondary (blue) or outline.
Link
- Anatomy: text, optional trailing arrow glyph for "go-to-page" links, no underline by default.
- Variants:
inline— body-text link, color--color-accent(blue-600), underline on hover only.arrow link— no underline, arrow→translates 4px on hover, color--color-accent. Used for "Read more", "See all services".
- States: default, hover (underline + 120ms opacity ramp), focus-visible (focus ring), visited (no change — we're not Wikipedia).
- Tokens:
--color-link,--color-link-hover,--color-focus-ring. - Don't: don't make link text green. Green is the brand CTA fill; blue is the interaction color. Two interaction colors fight.
Input + Textarea
- Anatomy: label (small Space Grotesk caption above), input field, helper text (below, body-s muted), error message (below, error-600, replaces helper on error).
- States: default (graphite-100 border on white), hover (border-strong), focus-visible (2px blue focus ring; border becomes accent), error (error-600 border, error message visible), disabled (50% opacity).
- Tokens:
--input-radius,--input-height,--input-padding-x,--color-border,--color-border-strong,--color-focus-ring,--color-danger. - Don't: don't use placeholder text instead of a label. Always label above. Placeholders disappear and screen readers under-handle them.
Card
- Anatomy: optional eyebrow (Space Grotesk caption), title, body, optional footer (link or meta), optional image (above or beside).
- Variants:
default— white surface, hairline border (--color-border), soft shadow (--card-shadow).image-top— image fills top edge of card, content below.quiet— transparent fill, no border, no shadow — for tight grids where the page already has rhythm.dark-tile— used for industry / council tiles. Image fills the card; a--gradient-tile-overlay(transparent → black at bottom) ensures the white title reads.
- States: default, hover (subtle
translateY(-3px)+--card-shadow-hover+ border-strong), focus-within when card is interactive. - Tokens:
--card-radius(12px),--card-padding,--color-surface-elevated,--color-border,--card-shadow,--card-shadow-hover. - Don't: don't pile a heavy drop shadow, a heavy border, and a hover lift onto one card. Hairline + soft shadow is the resting state; the lift is the only added affordance on hover.
Nav (header)
- Anatomy: logo (left), primary links (centre/right), CTA button (right, green primary), mobile toggle (right, below 1024px).
- Variants:
default— translucent white background (--nav-bg) with backdrop blur (saturate(140%) blur(8px)) and a hairline bottom border. Sticky on scroll.solid— opaque white when the page is dark behind it (interior heroes that aren't gradient-tinted).mobile drawer— full-height panel from right, opaque white, contains link list + CTA button.
- States: default, scrolled (background opacity steps to near-solid), submenu hover (dropdown card with shadow + hairline), mobile-open (drawer slides in, focus traps inside drawer).
- Tokens:
--nav-height(76px),--nav-bg,--nav-bg-scrolled,--nav-border,--nav-blur. - Don't: don't use brand green for the nav background. The CTA stays green; the nav stays a quiet white surface. The contrast is the point.
Hero
- Default hero — soft gradient page background (
--gradient-page-hero: very subtle green + blue radial wash over near-white), display headline in ink-950, lede in muted body color, CTA row with green primary + outline secondary. Optional right-column image with floating "trust card" overlapping the bottom edge. Matches the live site.hero-homerecipe. - Photo hero — full-bleed image,
--gradient-hero-overlayapplied on top, white display copy and CTAs over it. Reserved for industry, council, and named-operator pages. - Page hero (interior) — same soft gradient, smaller display size, single column, with eyebrow + title + lede + CTA row.
- Don't: don't combine a photo hero and a heavy gradient on the same page. One hero treatment per page.
Footer
- Anatomy: dark surface (
--color-surface-inverse), four columns on desktop (brand block, two link groups, contact), single column on mobile, legal row at the bottom with copyright + status. Social icons get smallrgba(255,255,255,.08)circular tiles, hover state fills with brand green. - Tokens:
--color-surface-inverse,--color-text-inverse,--space-section. - Don't: don't repeat the homepage hero CTA in the footer at the same visual weight — demote to secondary or outline so the page has one primary action.
Stat block
- Anatomy: small Space Grotesk caption label, large display-l or display-m number, body-s descriptor.
- Variants:
default(ink-950 number on white),accent(signal-green-700 number for shipped/positive results — uses the AA-safe 700 step, not the 500 brand anchor). - Tokens:
--fs-display-l,--color-text,--color-brand-text. - Don't: don't use stat blocks for vanity metrics ("100% client satisfaction"). They earn their place only when the number is a receipt —
13+ decades on the bench,7 service lines,20+ enterprise programs implemented.
Testimonial card
- Anatomy: white surface, hairline border, soft shadow, optional small green quote-mark tile, quote (body-l or display-m, ink), attribution (body-s, name in ink, role in muted).
- Tokens:
--card-radius,--card-padding,--color-surface-elevated,--shadow-soft. - Don't: don't use stylized quote marks bigger than the quote itself. The quote does the work.
Service / industry tile
- Service tile — white card variant. Coloured icon tile (green-tinted or blue-tinted background), title, body, arrow link at the bottom. Hover lifts 3px + shadow swap.
- Industry tile —
dark-tilecard variant. Photographic background, gradient overlay, title bottom-left in white. Aspect ratio 3/4. - Don't: don't mix the two card styles in a single grid. Pick one card vocabulary per section.
Accordion
- Anatomy: trigger (heading-s + chevron, full-width, left-aligned), panel (body, animates max-height + opacity).
- States: collapsed, expanded, focus-visible on trigger.
- Tokens:
--space-16,--space-24,--color-border,--ease-standard,--duration-base. - Don't: don't auto-collapse other panels when one opens unless the user explicitly opts into that pattern. Multi-open is the safer default for FAQs.
Tabs
- Use only when the IA needs them — usually never on a marketing page. If you find yourself adding tabs to a hero, split it into two sections instead.
8. Motion principles
Motion exists to communicate state, not to entertain.
Easing
| Token | Curve | Use |
|---|---|---|
--ease-standard |
cubic-bezier(0.2, 0, 0.2, 1) |
Default — buttons, hovers, links |
--ease-emphatic |
cubic-bezier(0.16, 1, 0.3, 1) |
Hero reveals, feature transitions |
--ease-exit |
cubic-bezier(0.4, 0, 1, 1) |
Dismissals, exit transitions |
Duration
| Token | Value | Use |
|---|---|---|
--duration-fast |
120ms | Hover color shifts, focus rings |
--duration-base |
200ms | Default state transitions, link arrows |
--duration-slow |
320ms | Panel/accordion open, drawer slide |
--duration-slower |
480ms | Hero entry animation (one per page) |
When to animate
- Yes: state transitions (hover, focus, active, expand/collapse, drawer open). Card hover lift (
translateY(-3px)). - Yes, but once: hero text fade-up on first load. Never repeat-animate on scroll back.
- No: decorative loops, parallax, scroll-jacked sequences, anything that defers content visibility past 100ms.
All motion respects prefers-reduced-motion: reduce — the tokens automatically zero out durations under that media query.
9. Accessibility
The system is AA across the board; the brand book is the proof.
- Contrast: every text-on-bg pair documented in §2 is tested. Don't introduce a new color combination without testing it.
- Focus: every interactive element shows a visible 2px focus ring (
--color-focus-ring, blue on light) with 2px offset. Neveroutline: nonewithout an equivalent replacement. - Touch targets: minimum 44px square for any tap target. Button
mdsize (48px) is the floor. - Semantics: use real HTML (
button,a,nav,section, headings in order). The system doesn't make components out of<div>— it makes them out of the right element. - Motion: respect
prefers-reduced-motion. Tokens handle this; components inherit. - Alt text: every logo variant has descriptive alt. Portraits use the operator's name + role. Decorative imagery uses
alt="".
10. What lives where
| Authoritative file | Contains |
|---|---|
/brand/styles/tokens.css |
Every token. Single source of truth. The brand book renders itself from this file. |
/brand/design-system.md |
This document — the why behind the tokens. |
/brand/styles/base.css |
Reset + element defaults (built in brand-book phase). |
/brand/styles/components.css |
Component CSS that consumes tokens (built in brand-book phase). |
/brand/index.html |
The brand book itself (built in brand-book phase). |
If a token's value needs to change, change it in tokens.css once. If a component's behavior needs to change, change it in components.css once. The brand book updates everywhere.