196 lines
8.4 KiB
Markdown
196 lines
8.4 KiB
Markdown
# 02 — Design system
|
||
|
||
Authority: `AGENTS.md` §3 D7 — *keep the palette and the infinity mark;
|
||
modernize the execution.* The look is not up for redesign. What follows is the
|
||
system that preserves it while fixing what the old build got wrong.
|
||
|
||
---
|
||
|
||
## What carries over unchanged
|
||
|
||
- The **palette**: cream, ink, maroon, gold.
|
||
- The **infinity mark** — SML Company Ltd.'s actual logo, and the metaphor holds:
|
||
a dispute is a loop, and the work is redrawing the loop into a line.
|
||
- The **type pairing**: Instrument Serif for display, Geist for text, Geist Mono
|
||
for eyebrows and labels.
|
||
- The **editorial register** — generous whitespace, restrained colour, serif
|
||
display type at large sizes.
|
||
|
||
## What changes
|
||
|
||
| Was | Is | Why |
|
||
|---|---|---|
|
||
| Google Fonts at runtime | Self-hosted, subset, preloaded | Removes a render-blocking third-party round trip from a page that collects legal inquiries |
|
||
| Fixed px type sizes | Fluid `clamp()` scale | One scale from 360 px to 1600 px with no breakpoint jumps |
|
||
| Ad hoc spacing values | 8 px base scale | Consistent vertical rhythm; no magic numbers |
|
||
| Gold used as a text colour on cream | Gold restricted to decorative and on-dark | **It fails WCAG AA at 2.10:1.** Measured, not assumed |
|
||
| Scroll-reveal on every element, always on | Reveal on major sections only, gated behind `prefers-reduced-motion` | Motion that reads as confident rather than decorative; accessible by default |
|
||
| 2.2 MB single file, ~1 MB of base64 logos | Optimized SVG mark, AVIF/WebP photography | The mark is geometry; it should be vector, not a 470 KB PNG |
|
||
| React 18 dev build + Babel Standalone in the browser | Static HTML, near-zero JS | The reason the site is invisible to crawlers |
|
||
|
||
---
|
||
|
||
## Colour
|
||
|
||
Tokens live in `src/styles/tokens.css`. Never write a raw hex value in a
|
||
component.
|
||
|
||
| Token | Value | Use |
|
||
|---|---|---|
|
||
| `--cream` | `#faf7f2` | Page background |
|
||
| `--cream-2` | `#f3ede0` | Alternating section background |
|
||
| `--cream-3` | `#ebe3d1` | Cards and insets on cream |
|
||
| `--ink` | `#1a1614` | Body text; dark section backgrounds |
|
||
| `--ink-soft` | `#3a322c` | Secondary text |
|
||
| `--muted` | `#6e6359` | Metadata, captions — **on cream only** |
|
||
| `--maroon` | `#5a1a1c` | Primary action, accents, dark panels |
|
||
| `--maroon-d` | `#3d1112` | Hover on maroon |
|
||
| `--maroon-l` | `#7a2a2c` | Links on cream |
|
||
| `--gold` | `#c9a876` | Rules, dividers, on-dark accent — **never text on cream** |
|
||
| `--gold-d` | `#a88858` | Large decorative text on cream only |
|
||
| `--gold-l` | `#e2c89a` | Text on ink or maroon |
|
||
|
||
### Contrast — measured, 2026-08-25
|
||
|
||
Against `--cream` `#faf7f2`:
|
||
|
||
| Colour | Ratio | AA body (4.5) | AA large (3.0) |
|
||
|---|---|---|---|
|
||
| `--ink` `#1a1614` | **16.81** | pass | pass |
|
||
| `--ink-soft` `#3a322c` | **11.75** | pass | pass |
|
||
| `--maroon` `#5a1a1c` | **12.29** | pass | pass |
|
||
| `--maroon-l` `#7a2a2c` | **8.95** | pass | pass |
|
||
| `--muted` `#6e6359` | **5.47** | pass | pass |
|
||
| `--gold-d` `#a88858` | **3.11** | **FAIL** | pass |
|
||
| `--gold` `#c9a876` | **2.10** | **FAIL** | **FAIL** |
|
||
|
||
Against `--maroon` `#5a1a1c`: cream 12.29 · gold-l 8.11 · gold 5.84 — all pass.
|
||
Against `--ink` `#1a1614`: cream 16.81 · gold-l 11.09 · gold 8.00 — all pass.
|
||
`--muted` on ink measures **3.07 and fails**; use `--gold-l` or cream at reduced
|
||
opacity for secondary text on dark.
|
||
|
||
**Hard rules.**
|
||
|
||
1. `--gold` is never a text colour on cream. Rules, borders, dividers, icon
|
||
strokes, and on-dark text only.
|
||
2. `--gold-d` on cream only at 24 px+ / 19 px bold, and only for decorative
|
||
display text — never for anything a reader must parse.
|
||
3. Body text on cream is `--ink` or `--ink-soft`. Metadata may use `--muted`.
|
||
4. Secondary text on dark is `--gold-l` or `--cream` at ≥ 70% opacity.
|
||
5. **No dark mode.** A single committed light identity is the right call for a
|
||
legal practice, and halving the surface area halves the ways contrast breaks.
|
||
|
||
---
|
||
|
||
## Typography
|
||
|
||
**Faces.** Instrument Serif (display) · Geist 300/400/500/600 (text) ·
|
||
Geist Mono 400/500 (eyebrows, labels, data).
|
||
|
||
Self-host all three. Subset to Latin + Latin Extended-A. `font-display: swap`.
|
||
Preload only the two faces used above the fold — Instrument Serif regular and
|
||
Geist 400.
|
||
|
||
**Scale.** Fluid, `clamp()`, `1.25` ratio at the small end widening to `1.333` at
|
||
the display end. Tokens `--text-xs` through `--text-6xl` in `tokens.css`.
|
||
|
||
**Rules.**
|
||
|
||
- Display type (`--font-serif`) at `--text-4xl` and above only. It has almost no
|
||
hinting at small sizes and looks weak below 32 px.
|
||
- Display line-height `0.95`–`1.05`; letter-spacing `-0.02em`.
|
||
- Body line-height `1.6`. Measure capped at `68ch` — the old site ran full-bleed
|
||
paragraphs at 1400 px, which is unreadable.
|
||
- Eyebrows: mono, 11–12 px, `0.18em` tracking, uppercase, always paired with a
|
||
real heading. An eyebrow is not a heading and never carries the `<h*>`.
|
||
- Italic display (`.it`) is the one flourish the design allows. One italic phrase
|
||
per headline, at most.
|
||
- Never skip a heading level. `<h1>` once per page.
|
||
|
||
---
|
||
|
||
## Spacing and layout
|
||
|
||
8 px base: `--space-1` `4px` · `-2` `8` · `-3` `12` · `-4` `16` · `-5` `24` ·
|
||
`-6` `32` · `-7` `48` · `-8` `64` · `-9` `96` · `-10` `128` · `-11` `160`.
|
||
|
||
Content width `1280px`; prose measure `68ch`; wide media `1440px`.
|
||
Gutters: `24px` mobile, `48px` desktop.
|
||
Section rhythm: `--space-9` (96px) mobile, `--space-11` (160px) desktop.
|
||
|
||
Grid: 12 columns desktop, 6 tablet, 4 mobile, `--space-5` gutter.
|
||
|
||
---
|
||
|
||
## Motion
|
||
|
||
The old build animated nearly everything on scroll. The replacement is
|
||
deliberate and quiet.
|
||
|
||
- **Reveal on section entry only** — not on every child element. Sub-element
|
||
stagger is limited to card grids, and capped at six children.
|
||
- Duration `600ms`, easing `cubic-bezier(.2,.7,.2,1)`. Transform and opacity
|
||
only — never layout properties.
|
||
- Implement with `IntersectionObserver` in one tiny inline script, or
|
||
`animation-timeline: view()` where supported. Not a framework, not a library.
|
||
- **Content is visible without JavaScript.** The reveal is an enhancement layered
|
||
on top of already-rendered HTML. If the observer never runs, the page reads
|
||
normally. The old build had this exactly backwards.
|
||
- Hover transitions `250ms`.
|
||
|
||
```css
|
||
@media (prefers-reduced-motion: reduce) {
|
||
*, *::before, *::after {
|
||
animation-duration: 0.01ms !important;
|
||
animation-iteration-count: 1 !important;
|
||
transition-duration: 0.01ms !important;
|
||
scroll-behavior: auto !important;
|
||
}
|
||
}
|
||
```
|
||
|
||
Nothing animates infinitely. Nothing autoplays. Nothing moves on page load
|
||
except the reveal of the hero.
|
||
|
||
---
|
||
|
||
## Components
|
||
|
||
| Component | Notes |
|
||
|---|---|
|
||
| `InfinityMark` | Inline SVG, `currentColor`, `aria-hidden` when decorative. Never a PNG |
|
||
| `SiteHeader` | Sticky, condenses on scroll. Practice dropdown as CSS-only `<details>` |
|
||
| `SiteFooter` | Three-column sitemap, contact block, designations, entity line |
|
||
| `Eyebrow` | Mono label with optional maroon dot |
|
||
| `SectionHeading` | Eyebrow + display heading + optional lede, one measure |
|
||
| `Button` | Variants `primary` (maroon) · `ghost` (outlined) · `gold` (ink bg, gold-l text). Renders `<a>` or `<button>` correctly |
|
||
| `Pill` | Small bordered label for designations and sector chips |
|
||
| `CredentialRow` | Three or four credential slots. **Never matter counts** — `AGENTS.md` §4 |
|
||
| `PracticeCard` | Sector chip, heading, one paragraph, arrow link |
|
||
| `ProcessStep` | Numbered step, timing, body |
|
||
| `ArticleCard` | Title, description, date, topic pills, reading time |
|
||
| `Prose` | Long-form wrapper. Owns all typographic defaults for MDX |
|
||
| `SEO` | Title, description, canonical, OG, Twitter, JSON-LD — see `04-seo-spec.md` |
|
||
|
||
**Focus states.** Every interactive element gets a visible focus ring:
|
||
`outline: 2px solid var(--maroon); outline-offset: 3px`. Use `:focus-visible`.
|
||
Never `outline: none` without a replacement — the old build removed it globally.
|
||
|
||
---
|
||
|
||
## Accessibility floor
|
||
|
||
Not a polish pass. A build requirement.
|
||
|
||
- One `<h1>` per page; heading levels never skipped.
|
||
- Landmarks: `<header>`, `<nav>`, `<main>`, `<footer>`. Skip-to-content link first
|
||
in tab order.
|
||
- Every image has `alt`. Decorative images get `alt=""`.
|
||
- Colour never carries meaning alone.
|
||
- All functionality reachable by keyboard; focus order matches visual order.
|
||
- Forms: real `<label>` elements, `aria-describedby` for hints, errors announced
|
||
with `role="alert"` and tied to their field.
|
||
- Touch targets ≥ 44 × 44 px.
|
||
- Test at 200% zoom and at 320 px width.
|
||
- Every page must be readable and navigable with JavaScript disabled.
|