chore: project scaffold, specs, and working record
Build and deploy / build-and-deploy (push) Failing after 5s
Build and deploy / build-and-deploy (push) Failing after 5s
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user