# 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

Four 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, the logo, the nav and the icons. Text black is the copy on the light ground, softer for reading. There is no accent beyond these and no second shade of any of them.

| Colour | Hex | Use |
| --- | --- | --- |
| Red | `#CD2C2C` | Emphasis, links, and the signature fields. |
| Black | `#080808` | Dark fields, the logo, nav and icons. The footer's black. |
| White | `#f5f5f7` | The light field, and text on red or black. |
| Text black | `#2c2c38` | Copy on the light field: softer than the black, for reading. |

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 a system serif for pull quotes only (ui-serif, 'Iowan Old Style', Charter, Georgia, serif). Eight roles, and no other content size; each is a class on the element. Titles and headings are 300, copy is 400, and everything that is not copy is 500. The condensed feature heading is reserved and not set: the site carries no condensed face.

| Role | Class | Desktop size / leading | At (max-width: 55.99rem) | Weight | Tracking |
| --- | --- | --- | --- | --- | --- |
| Large title | `.type-title` | var(--type-title-size) / 0.963 | as desktop | 300 | -0.03em |
| Hero | `.type-title.type-hero` | calc((100vw - 14rem) / 7.5) / 1 | 72px / 72px | 300 | -0.03em |
| Content heading | `.type-heading` | 42px / 0.963 | 30px /  | 300 | -0.02em |
| Editorial statement | `.type-statement` | 28px / 40px | 25px / 35px | 400 | -0.01em |
| Body copy | `.type-body` | 28px / 40px | 25px / 35px | 400 | -0.01em |
| Number or label | `.type-label` | 20px / 28px | as desktop | 500 | -0.015em |
| Large quotation | `.type-quote` | clamp(40px, 4vw, 56px) / 1.05 | as desktop | 300 | -0.02em |
| Pull quote | `.quote` | clamp(1.5rem, 3vw, 2.5rem) / 1.25 | as desktop | 400 | -0.01em |

## Page anatomy

Where each part of a page sits, at 1440 by 900 on desktop and 390 by 844 on a phone.

| Part | Desktop | Phone |
| --- | --- | --- |
| Gutter | 140px each side | 30px each side |
| Header | 140px tall: logo and page title left, nav and Digiguru right | Logo and title left, Digiguru top right |
| Nav | In the header, top right | A 40px bar fixed along the bottom |
| Back link | Fixed 140px down, 140px in | Fixed 80px down, 30px in |
| Section menu | Fixed at the left from 85rem, 200px wide at 1440 | Not shown |
| Page title | 16rem down on a long page | The page's own top |
| Running text | At most 47rem | The full column |
| Footer bar | Fixed, 140px: copyright left, social and back to top right | At the foot of the footer, stacked |
| Edge blur | Top and bottom 108px (12vh) | Top and bottom 101px (12vh) |

## Space

Five steps; things that belong together sit close and groups stand apart. Running text wraps at `--measure: 47rem`. Every long page starts its title at `padding-top: 16rem` on desktop. The page gutter is 1.5rem, and 7rem from the desktop breakpoint.

| Token | Value | Used for |
| --- | --- | --- |
| `--space-tight` | 1rem | A title and its meta line. |
| `--space-paragraph` | 3rem | One paragraph and the next. |
| `--space-heading` | calc(var(--space-paragraph) + 0.3rem) | A heading and its own text: the same visible gap as one paragraph and the next. |
| `--space-item` | var(--space-beside) | One entry in a list and the next. |
| `--space-group` | 5rem, 8rem from min-width: 56rem | The title block and the content, one section and the next, a listing’s header and its list. |

## 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 8. 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.
