Skip to content
THG Advisors Brand showcase

Design system

Design System

The why behind every visual decision and token.

View source · design-system.md

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 / 600 step (--color-brand-text for green, --color-accent for 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-200 grid lines, ink-950 text, signal-green-500 and signal-blue-500 as primary series colors, error-500 only 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 buildcomponents.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.
  • 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.
  • 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-home recipe.
  • Photo hero — full-bleed image, --gradient-hero-overlay applied 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.
  • 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 small rgba(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 tiledark-tile card 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. Never outline: none without an equivalent replacement.
  • Touch targets: minimum 44px square for any tap target. Button md size (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.