# Design System

The rules craigjamieson.com is built on. Every value below is parsed out of public/theme/variables.css and public/theme/style.css when the site is built, so this document cannot drift from the stylesheet it describes.

## Colour

Three colours, one value each. Red carries emphasis, links and the signature fields. White is the light ground, off-white rather than stark white. Black is the dark ground and the text on the light one. There is no fourth accent and no second shade of any of the three.

| Colour | Hex | Use |
| --- | --- | --- |
| Red | `#CD2C2C` | Emphasis, links, and the signature fields. |
| Black | `#080808` | Dark fields, and text on the light one. The footer's black. |
| White | `#f5f5f7` | The light field, and text on red or black. |

Colour is applied through three custom properties rather than a scheme. A section declares `--bg-color`, `--text-color` and `--link-color`; theme/script.js reads them off whichever section is crossing the middle of the viewport and applies them to the page, the header, the nav and the footer. The chrome is written in `currentColor` and `inherit` so it tracks the section behind it rather than carrying values of its own.

| Mood | Background | Text | Link | Text contrast | Link contrast |
| --- | --- | --- | --- | --- | --- |
| `.section-hero` | #CD2C2C | #f5f5f7 | #f5f5f5 | 4.83:1 | 4.82:1 |
| `.section-light` | #f5f5f7 | #1b1b27 | #CD2C2C | 15.65:1 | 4.83:1 |
| `.section-dark` | #0f0f0f | #f5f5f7 | #CD2C2C | 17.6:1 | 3.64:1 |
| `.section-home-about` | #1d1d1f | #f5f5f7 | #CD2C2C | 15.46:1 | 3.2:1 |
| `.section-medium` | #CD2C2C | #f5f5f5 | #CD2C2C | 4.82:1 | 1:1 |

A case study is painted in its client's own brand colours for as long as it is on screen, through those same three properties. Those colours are that client's, not Craig's: they are content the work pages carry (135 of them) and they are deliberately not part of this system. What the system owns is the mechanism. A new case study follows the same trio rather than introducing styling of its own.

## Typography

One family and two weights. `:root` sets `font-size: 20px`, so every rem in the page is that number, but not in a media query, which is measured against the browser's initial 16px. The measure every long-form block is capped at is 48rem. The weight goes up as the type gets smaller: h1 to h3 are 300, h4 to h6 and the nav are 400, and nothing on the site is bold.

| Element | Size | Leading | Weight | Size at (min-width: 56rem) |
| --- | --- | --- | --- | --- |
| `h1` | clamp(2.4rem, 5vw, 6rem) | 1.2 | 300 | clamp(2.4rem, 5vw, 6rem) |
| `h2` | clamp(1.8rem, 4vw, 2.5rem) | 1.3 | 300 | clamp(1.8rem, 4vw, 2.5rem) |
| `h3` | clamp(1.8rem, 4vw, 2.2rem) | 1.2 | 300 | clamp(1.8rem, 4vw, 2.2rem) |
| `h4` | clamp(1rem, 4vw, 1.2rem) | 1.1 | 400 | clamp(1rem, 4vw, 1.2rem) |
| `h5` | clamp(0.8rem, 5vw, 1rem) | 1 | 400 | clamp(0.8rem, 5vw, 1rem) |
| `h6` | 1rem | not set | 400 | 2rem |
| `p` | 1rem | 1.5rem | 300 | 1.4rem |
| `ul, ol` | 1rem | 1.5rem | not set | 1.4rem |

## Space

The page gutter is 1.5rem below the desktop breakpoint and 7rem above it, a different idea rather than a scaled one. Lists indent to 0 1.5rem 0 9rem so their markers hang outside the text column. A full-bleed section is `min-height: 100vh` with `scroll-snap-align: start`; long-form pages opt out with `snap-scroll-disabled` on the body.

| Value | At 20px root | Used for |
| --- | --- | --- |
| `0.5rem` | 10px | Nav link padding, social row gap, the copyright separator. |
| `1rem` | 20px | The Digiguru panel’s gutter. One step of separation inside a component. |
| `1.5rem` | 30px | The page gutter on a phone. Also the social icon size. |
| `2rem` | 40px | The header’s desktop padding. |
| `4rem` | 80px | The margin above an h4, the largest gap the type scale asks for. |
| `7rem` | 140px | The desktop text indent, and the header’s height. |
| `11rem` | 220px | The footer’s bottom row, and where the section menu starts. |

## Breakpoints

A media query's rem is 16px, not the 20px this site sets on `:root`, so `56rem` is 896px and not 1120px. The pixel column is the real width.

| Query | Actual width | What changes |
| --- | --- | --- |
| `30rem` | 480px | The newsletter form stops stacking and sits on one row. |
| `40rem` | 640px | The books grid and the career rows take their wider layout. |
| `56rem` | 896px | The theme’s own breakpoint. Nav moves to the header, the gutter goes to 7rem, type goes up a step, Digiguru becomes a panel instead of a page. |
| `85rem` | 1360px | The in-page section menu appears beside the copy. |
| `94rem` | 1504px | The footer copy shifts clear of the section menu. |

## Motion

One duration, `--transition-speed: 0.5s`, and one curve, `cubic-bezier(0.4, 0, 0.2, 1)`, for anything that travels; `ease-in-out` for anything that only changes colour. Hover is `filter: brightness(1.2)` rather than a second colour, so a link brightens against whatever field it is sitting in. Section imagery is faded by GSAP writing opacity inline, which is why several rules deliberately carry no transition of their own. `prefers-reduced-motion: reduce` keeps the fades and drops the travel.

## Edges

One focus ring for the whole site: `outline: 2px solid currentColor` at `outline-offset: 2px`, on `:focus-visible`. It is never removed, only redrawn. There is one raised surface, `figure.media-surface`, which takes `1rem` and `0.15rem solid #1b1b27` (stepping up to `0.3rem solid #1b1b27` at (min-width: 56rem)). Everywhere else a division is a hairline in the ink colour at low opacity. Form controls take `1px solid rgba(27, 27, 39, 0.18)` at `0.25rem`.

## The laws

1. **A section owns the colour. The page borrows it.** Nothing is painted with a fixed colour. A section sets --bg-color, --text-color and --link-color; theme/script.js reads them off whichever section is crossing the middle of the viewport and applies them to the page, the header, the nav and the footer. That is why chrome uses currentColor and inherit rather than a value of its own. It has to work on a red hero and a white page without a second set of rules.
2. **Panels push content. They never overlay it.** Digiguru narrows <body> by its own width and the page reflows into the remaining column, scrolling inside that box so its scrollbar sits beside the panel rather than at the window edge. Below 1200px the panel becomes its own full-screen page instead. This is the standing rule for any panel, drawer or sidebar on this site.
3. **Anything that travels with the panel animates transform.** The panel slides on the compositor. right, margin and width are recalculated on the main thread and visibly fall behind it while the page reflows, even at the same duration and curve. Same property, same pipeline, or it desyncs.
4. **The nav and the social row never change width or spacing.** Not when the panel opens, not at any breakpoint. Both keep the theme’s own metrics everywhere; what moves is their position, translated by the panel’s width. If a layout needs a wider minimum to hold them at full size, it gets one. Neither row is ever squashed to make something else fit.
5. **Nothing on the left edge moves when the panel opens.** The logo and the copyright are anchored to the page gutter and stay there.
6. **Repeated elements are site-wide components.** The header, navigation, footer, footer media, Digiguru and the page transitions have one shared implementation and behave the same on every route. A page-specific variation is a defect, not a feature.
7. **One stylesheet, and it is public/theme/style.css.** The theme’s own rules and everything the site adds on top of them, in one file, organised by what each section owns. There is no override sheet. If a rule needs to beat an earlier one it says so in a comment and sits below it, in the section that owns it. src/styles/bundle.css compiles it with three others into the single asset the pages link.
8. **Every section-* class also carries `section`.** Without it the section loses min-height: 100vh and scroll-snap-align, and script.js cannot find the element for the background fade.
9. **A focus ring is never removed, only redrawn.** 2px solid currentColor at 2px offset, on :focus-visible so it appears for the keyboard and not for a mouse click. currentColor is what lets it track the section behind it. Controls that suppress it draw their own: the section menu moves the ring onto the label so it does not box an empty rail.
10. **Nothing contacts a third party on page load.** Videos and X embeds are click-to-load facades with locally served posters. Fonts are the system stack, so there is nothing to fetch.

## What is not clean

The palette is three colours. The site's own moods use 7. Every extra value is a shade of one of the three, close enough that none is distinguishable from it on screen:

| In use | Should be | Apart | Where |
| --- | --- | --- | --- |
| `#0f0f0f` | `#080808` | 1.04:1 | `.section-dark` |
| `#1b1b27` | `#080808` | 1.18:1 | `.section-light` |
| `#1d1d1f` | `#080808` | 1.19:1 | `.section-home-about` |
| `#f5f5f5` | `#f5f5f7` | 1:1 | `.section-hero`, `.section-medium` |

`.section-medium` sets `--link-color` to its own `--bg-color`, so a link there is invisible. It has already caused a real defect: the footer copyright link carries `color: inherit` to escape it.

Red on a dark field is below AA for body copy: `.section-dark` at 3.64:1 and `.section-home-about` at 3.2:1. It clears the 3:1 large-text threshold, so a heading is fine and a sentence is not.

`p` declares `font-weight: 300`, and no rule in the sheet declares one for `ol` or `ul`, so lists fall back to the browser's 400 at the same size as the copy around them. One declaration would close it.

There is no semantic layer. A mood names its own hex values rather than resolving through `--surface`, `--ink` and `--accent`, so changing the red is several edits rather than one.
