{
  "version": 1,
  "note": "The one record every colour, space and duration on the site comes from. tools/tokens.mjs renders it to site/src/styles/tokens.css and computes every contrast ratio, tools/check_tokens.mjs fails CI when the rendered file is stale, when a text pair falls under 4.5 to 1, or when a component writes a hex, a millisecond or a curve by hand. Nothing here is typed twice.",
  "colour": {
    "light": {
      "paper": "#f1f2ef",
      "ink": "#141619",
      "muted": "#62676d",
      "rule": "#d3d6d3",
      "field": "#e6e8e5",
      "marker": "#b92d22",
      "blueprint": "#35648a"
    },
    "dark": {
      "paper": "#0f1113",
      "ink": "#e6e8e4",
      "muted": "#9aa0a6",
      "rule": "#262a2e",
      "field": "#171a1d",
      "marker": "#ff6157",
      "blueprint": "#7aa7cc"
    },
    "plate": {
      "plate": "#0b0c0d",
      "plate-ink": "#e6e8e4",
      "plate-muted": "#9aa0a6",
      "plate-marker": "#ff6157",
      "plate-rule": "#1a1d20"
    },
    "roles": {
      "paper": "page ground",
      "ink": "text, the H1, rules on plates",
      "muted": "secondary text, captions, nav at rest, spec rail labels",
      "rule": "1px hairlines only, never text, about 1.3 to 1 on paper",
      "field": "code blocks, the spec sheet, the filmstrip track, a tone rather than a card",
      "marker": "the one accent: frame numbers, the active chapter, hover, focus, the record dot",
      "blueprint": "build chapters only: graph strokes, histogram bars, code links",
      "plate": "every media band in both themes, so the work reads at cinema contrast whatever the page does"
    },
    "pairs": [
      {
        "fg": "ink",
        "bg": "paper",
        "text": true
      },
      {
        "fg": "muted",
        "bg": "paper",
        "text": true
      },
      {
        "fg": "marker",
        "bg": "paper",
        "text": true
      },
      {
        "fg": "blueprint",
        "bg": "paper",
        "text": true
      },
      {
        "fg": "muted",
        "bg": "field",
        "text": true
      },
      {
        "fg": "ink",
        "bg": "field",
        "text": true
      },
      {
        "fg": "rule",
        "bg": "paper",
        "text": false
      },
      {
        "fg": "plate-ink",
        "bg": "plate",
        "text": true
      },
      {
        "fg": "plate-muted",
        "bg": "plate",
        "text": true
      },
      {
        "fg": "plate-marker",
        "bg": "plate",
        "text": true
      }
    ]
  },
  "type": {
    "display": "\"Archivo\", \"Helvetica Neue\", Arial, sans-serif",
    "body": "\"Source Serif 4\", Georgia, \"Times New Roman\", serif",
    "mono": "\"IBM Plex Mono\", ui-monospace, SFMono-Regular, Menlo, monospace",
    "display-weight": "600",
    "display-tracking": "-0.02em",
    "label-tracking": "0.06em",
    "leading-tight": "1.15",
    "leading-body": "1.5",
    "step-3": "clamp(1.6rem, 3.4vw, 2.3rem)",
    "step-2": "1.5rem",
    "step-1": "1.15rem",
    "step-0": "1rem",
    "step-x": "0.85rem",
    "step-xx": "0.68rem"
  },
  "grid": {
    "content-max": "72rem",
    "gutter": "clamp(1rem, 4vw, 3rem)",
    "grid-gap": "1.5rem",
    "measure": "66ch"
  },
  "display": {
    "note": "The screen (ADR-0018). One cell, one size, everywhere and always. There is no pitch scale and there cannot be one: a larger member of the type family is drawn across more cells, not with bigger cells. Optical size lives in display-face.json as named members.",
    "cell": "3px",
    "cell-shape": "diamond",
    "cell-coverage": "0.7"
  },
  "motion": {
    "note": "The work is the motion and the interface stays still. Names say what a movement is for rather than what it does, because a token named for a mechanism ends up serving two jobs with opposite needs and then neither can be tuned. Two columns: the value, and what it becomes under reduced motion, which is a decision per movement rather than a switch that zeroes everything. A colour change and an opacity fade are not movement.",
    "tokens": [
      {
        "name": "respond",
        "default": "120ms",
        "reduced": "120ms",
        "used_by": "link and row hover, focus, the press, the hover intent timer",
        "why": "A direct answer to a pointer, so it is spent while somebody waits. Past about 200ms that reads as lag and people click again. A colour change and a hair of scale are not the movement reduce protects against."
      },
      {
        "name": "beat",
        "default": "120ms",
        "reduced": "120ms",
        "used_by": "the gap between the hero sentence and the line under it",
        "why": "A delay rather than a duration. Nothing moves during it, so reduce has nothing to cut, and the offset is what makes two lines read as one arrival instead of two."
      },
      {
        "name": "enter-page",
        "default": "200ms",
        "reduced": "200ms",
        "used_by": "the hero rise on first paint",
        "why": "It stands between a reader and the sentence, so it takes the tightest expressive budget on the site. The travel is 8px, well under any vestibular threshold, so reduce keeps it rather than snapping the text into place."
      },
      {
        "name": "enter-overlay",
        "default": "480ms",
        "reduced": "0ms",
        "used_by": "the still growing from its tile to full size",
        "why": "Several hundred pixels and a scale, so the number that is slow on the hero is brisk here. Nothing is blocked while it runs, which makes it the one place the page performs. Zero under reduce, because a frame scaling across the viewport is the large area movement the setting exists to stop, and the dialog still opens."
      },
      {
        "name": "move-mark",
        "default": "230ms",
        "reduced": "0ms",
        "used_by": "the chapter rail mark sliding under its label",
        "why": "Position change becomes a cut, same end state."
      },
      {
        "name": "slip",
        "default": "230ms",
        "reduced": "0ms",
        "used_by": "the misregistration offset and the fringe tint that rides with it",
        "why": "Hover triggered, but nothing waits on it, because the word is already readable and the ghost is decoration. So it is spent as an expressive movement rather than as a response. Offset and tint share one value because they are one event, not two."
      },
      {
        "name": "ease-out",
        "default": "cubic-bezier(0.2, 0, 0, 1)",
        "reduced": "linear",
        "used_by": "anything entering or moving",
        "why": "Nothing moves under reduce, so the curve is moot and linear says so."
      },
      {
        "name": "ease-in",
        "default": "cubic-bezier(0.4, 0, 1, 1)",
        "reduced": "linear",
        "used_by": "anything leaving",
        "why": "As above."
      },
      {
        "name": "slip-x",
        "default": "0.17em",
        "reduced": "0.17em",
        "used_by": "how far a misregistered word slips",
        "why": "A distance, not a duration. The travel is cut under reduce by --slip going to zero, so the offset itself does not change and the fringe still prints. Halved from 0.34em on 2026-09-21: at the old value the ghosts spread far enough that the word underneath stopped being readable on hover, which is the one thing a decorative effect on a title must not do."
      },
      {
        "name": "rise-y",
        "default": "0.5rem",
        "reduced": "0.5rem",
        "used_by": "how far the hero rises on first paint",
        "why": "Same reason. --enter-page carries the same value in both columns because a fade is not the movement reduce protects against, so this distance holds too."
      },
      {
        "name": "press",
        "default": "0.985",
        "reduced": "1",
        "used_by": "the scale a tile takes while pressed",
        "why": "Unitless, fed to scale(). It flattens to 1 under reduce, because a press that moves is the one part of this that is movement rather than feedback."
      },
      {
        "name": "slip-band",
        "default": "13rem",
        "reduced": "13rem",
        "used_by": "how far from the cursor a word still lifts",
        "why": "A radius, not a duration. The fringe fades out with distance from the pointer so a title answers where the eye is rather than all at once, and the words themselves never move apart."
      },
      {
        "name": "drift",
        "default": "2000ms",
        "reduced": "0ms",
        "used_by": "one pass of the fringe sweeping a long title, after a finger lets go or a cursor rests",
        "why": "Touch has no hover, so on a phone the fringe froze wherever the finger last was once the page scrolled. Letting go hands it to a slow sweep, one end of the line to the other and back, instead. Two seconds a pass, chosen by eye on 2026-09-25 once it ran at its real speed, a wave rather than a crawl. The strobe it showed on the first night was a unit bug, the minified 3.2s read as 3.2 milliseconds, never this value. Zero under reduce, where letting go clears the fringe instead, because an effect that keeps moving on its own is what the setting exists to stop."
      },
      {
        "name": "drift-short",
        "default": "1000ms",
        "reduced": "0ms",
        "used_by": "one pass of the sweep on the shortest title",
        "why": "A pass takes longer the more letters it crosses, from this on a title of seven letters up to --drift on one of twenty or more, so a short title and a long one move at a similar pace instead of the short one crawling. Zero under reduce with --drift."
      },
      {
        "name": "linger",
        "default": "700ms",
        "reduced": "700ms",
        "used_by": "how long a cursor rests on a title before the fringe starts to sweep",
        "why": "A delay rather than a duration, so nothing moves during it. Long enough that a cursor passing over a title never starts it, short enough that one resting there sees it before looking away. The same under reduce, where --drift is zero and the sweep never starts anyway."
      }
    ],
    "scroll": {
      "default": "smooth",
      "reduced": "auto"
    }
  },
  "rules": [
    {
      "rule": "Rule is a hairline and never text. It measures about 1.3 to 1 on paper, so an absent chapter is muted with a line through it, never set in rule.",
      "enforcement": "tools/check_tokens.mjs"
    },
    {
      "rule": "The interface stays still and the work is the motion. Every movement has a name here and a reduced value that's a choice rather than a zero.",
      "enforcement": "tools/check_tokens.mjs"
    },
    {
      "rule": "Plate is dark in both themes and carries its own ink, muted and marker, because the light marker on it measures 3.2 to 1 and fails.",
      "enforcement": "data/design/tokens.json"
    },
    {
      "rule": "Nothing shifts. Every image and video declares its box before its bytes arrive, and the build fails on any layout shift at all.",
      "enforcement": "site/lighthouserc.json"
    },
    {
      "rule": "Nothing is defined for the first time inside a media query or a data attribute block. Light is the base and the dark blocks only redefine.",
      "enforcement": "tools/tokens.mjs"
    },
    {
      "rule": "A component never writes a literal. It names a token, and a hex, a millisecond or a curve written by hand fails the build.",
      "enforcement": "tools/check_tokens.mjs"
    },
    {
      "rule": "One inline script runs before paint, for the visitor's own preferences, and every page is complete without it.",
      "enforcement": "docs/decisions/0013-preferences-before-paint.md"
    },
    {
      "rule": "Nothing on a branded surface is a flat fill or a stroke. Boxes, rules, icons and display type are lit cells on the lattice, and the smallest tier of text comes off the display entirely because the display cannot do small.",
      "enforcement": "docs/decisions/0018-the-display.md"
    }
  ]
}