# 404HZ design contract (GRAY)

404HZ is a DJ-set tracklist engine. The tagline is drop a link, get the tracklist. The product has one colourway. The background is a warm gray field (`#bdbdc3`). Every word, line and accent uses one blue ink (`#2921cc`). Record artwork is the only other colour on a screen. This document is the contract for that surface. The authority for every value is the current effective style in `app/app.css`, the GRAY product-finish overrides. The living specimen is [`design-system/specimen.html`](design-system/specimen.html) and the canonical tokens are [`design-system/tokens.css`](design-system/tokens.css).

Specimen: [`design-system/specimen.html`](design-system/specimen.html).

---

## Quick start

1. Import the tokens before any other stylesheet: `@import url("../design-system/tokens.css");` (adjust the relative path to your folder). It carries the 7 Origin `@font-face` rules and the full `:root` set. Never re-declare a token locally.
2. Set the document shell: `<html lang="en" data-theme="gray">`. GRAY is the only theme. Do not add colourway switches, blue/hermes/orange blocks, or a theme cycle.
3. Build screens from the shells and component vocabulary below. Do not invent a hex, an alpha, a radius, or a font size; if a value is not here or in `tokens.css`, it is wrong.
4. Verify with the checks at the end of this document. Screenshots at 1440 and 375 beat any green checkmark.

## Priority order

When requirements compete, protect them in this order.

1. Facts. Track titles, artists, timestamps, catalog numbers, durations, confidence values. They are never shrunk or re-styled to fit chrome. When a phone row must collapse, the fact moves to the selected-row detail. The stamp stays reachable there.
2. Structure. The shells: top bar, left nav, one panel per screen, the fixed player band.
3. The reader's job. Paste a link, see the tracklist. One primary action per screen; secondary actions are quiet text links.
4. The system. Flat, ink-hierarchy, radius 0. No second hue, no shadow, no gradient where the tokens say otherwise.
5. Composition over template. Never a generic dashboard, never a marketing card grid, never an admin table skin.
6. Motion and detail, last, and only where they do not weaken 1 through 5.

## Tokens

Import `design-system/tokens.css`; the table is the readable mirror. Values are sourced from `app/app.css`. The contrast column is computed by compositing alphas in sRGB over `--field`, then applying WCAG relative luminance and ratio.

| Token | Value | Role | Contrast on field |
| --- | --- | --- | --- |
| `--field` | `#bdbdc3` | the one background | n/a |
| `--ink` | `#2921cc` | every mark, line, accent | n/a |
| `--i1` | `#2921cc` | primary text | 5.14:1 |
| `--i2` | `rgba(41,33,204,.96)` | secondary text, stamps, labels | 4.92:1 |
| `--i3` | `rgba(41,33,204,.95)` | quiet mono labels | 4.86:1 |
| `--line` | `rgba(41,33,204,.5)` | control outlines, primary dividers | 2.32:1 |
| `--line2` | `rgba(41,33,204,.34)` | row dividers, quiet separators | 1.75:1 |
| `--edge` | `rgba(41,33,204,.26)` | plate cut line | 1.52:1 |
| `--hot` | `rgba(41,33,204,.14)` | icon-button and row ground only | 1.25:1 |
| `--hot-strong` | `rgba(41,33,204,.6)` | strong hover marks | 2.78:1 |
| `--fill` | `rgba(41,33,204,.04)` | quiet tint, sleeve ground | 1.06:1 |
| `--track` / `--tick` | `.2` / `.9` | rail ground / fill | 1.38 / 4.56:1 |
| `--row-sel` | `rgba(41,33,204,.03)` | selected-row and row-hover tint | 1.05:1 |
| `--sel-bg` / `--sel-fg` | `var(--ink)` / `var(--field)` | text selection | 5.143:1 |
| `--focus` | `#2921cc` | `2px` outline, `2px` offset | n/a |

Read the ratios as three tiers. Text pairs must reach 4.5:1 and only text pairs carry that claim. `--i1` measures 5.143:1, `--i2` 4.921:1, `--i3` 4.863:1, field text on ink 5.143:1. Over a selected row the ramp holds at 4.706 / 4.652:1. Over a sleeve ground it holds at 4.636 / 4.583:1. Filled controls (SCAN, play, active chip) identify themselves with field text on ink. Where a boundary must read, use an ink border, because `--hot-strong` sits at 2.78:1 and fails 3:1. `--hot` is a ground for icon-only controls and rows. Ink on `--hot` computes 4.13:1 and `--i2` on `--hot` 3.97:1, so text never sits on `--hot`. Text hover and active states use `--soft` or an ink inversion. `--line2`, `--edge`, `--fill`, `--track` and `--row-sel` are decoration below 3:1 and never carry meaning alone.

Layout metrics: `--pad` gutter `clamp(16px,3.6vw,54px)`, `--max` measure `920px`, `--slv` sleeve `54px`, `--player-h` `88px`, `--r-flat` `0`, `--r-pill` `999px`, `--ease` `cubic-bezier(.16,1,.3,1)`.

## Type

The brand face is Origin, self-hosted from `site/fonts/` through the tokens import. It has no italic; a synthesised oblique is allowed but is not a second font. Never hotlink a Google font. Verify on every build that `document.fonts` reports the families `loaded`. A missing Origin falls back to Arial silently.

| Role | Face | Spec |
| --- | --- | --- |
| Wordmark `4[logo]4HZ` | Origin Display 900 | landing `clamp(4rem,24vw,23rem)`, tool bar `2rem`; `line-height:.82` to `.9`, tracking `-.035em` to `-.04em` |
| Screen title | Origin Text 700 | `clamp(1.5rem,3vw,2.1rem)`, `lh 1.02`, `ls -.01em`, max 24ch, sentence case |
| Set / track hero title | Origin Display 900 / 700 | `clamp(1.9rem,3.2vw,2.9rem)` set; track page up to `clamp(2.2rem,6.4vw,4.4rem)` |
| Scanner percent | Origin Display 900 | `clamp(2rem,5vw,3.4rem)`, tabular |
| Track title / artist | Origin Text 500 / 400 | `1rem` / `.95rem`, artist in `--i2` |
| Section label `h2` | Origin Text 600 | `13px`, uppercase, `ls .2em`, `--i3` |
| Data, stamps, labels, nav, errors, empty states | `--mono` | `12px` to `max(12px,.85vw)`; uppercase where structural (`ls .09em`–`.2em`), quiet `ls .03em`–`.06em` in rows; tabular numerals for every aligned number |
| Tagline | Origin Text 500 | `clamp(1rem,1.85vw,1.28rem)`, copy is `drop a link, get the tracklist` |

Body measure 60 to 70ch. Never shrink text to fit density; remove content instead.

## Shells

**Top bar (tool).** A sticky grid, `grid-template-columns:220px minmax(0,1fr)`, `min-height:76px`, field ground, `border-bottom:1px solid var(--line2)`. Column one holds the wordmark at Origin Display 900 `2rem`. Column two is an inner grid holding the scan form and the right cluster, capped at `min(100%, calc(var(--max) + 2 * var(--pad)))` and centred. The right cluster is the profile control (44px square, `--line2` border, ink fill on hover). There is no theme switch: GRAY is the only colourway.

**Scan form (tool).** Height `46px`, `border:1px solid var(--ink)`, radius 0. The input is Origin Text 500 `15px`, transparent, `padding:0 16px`, ellipsis on overflow, no inner outline. The form shows `focus-within` as `outline:2px solid var(--ink); offset 3px`.. The submit is a filled mono button: `font:700 11px var(--mono)`, `ls .1em`, `min 156px × 44px`, ground `--ink`, text `--field`, separated by `border-left:1px solid var(--ink)`. The solid ink primary is current house style. Outline-only submits are the stale version.

**Left nav.** `220px` column, `border-right:1px solid var(--line2)`, `padding:28px 12px 28px var(--pad)`. Links are mono uppercase, weight 600, `ls .09em`, `padding:12px 14px`, icon 17px (`--icon` mask). The current link sits on a `--soft` ground in ink. Sticky under the top bar on desktop. The five destinations are Dig, Discover, History, Liked and Playlists. Dig resumes the latest set; History opens the same canonical set page.

**Content.** `.wrap` is `width:min(100%, calc(var(--max) + 2 * var(--pad)))`, centred, `padding:36px var(--pad) 64px`, one column, gap `clamp(12px,2.1vh,20px)`. One main panel per screen. At `≥1280px` the shell adds a third column: `header.top` and `.tool-body` both become `220px minmax(0,1fr) var(--rail)` with `--rail:clamp(300px,23vw,340px)`, and inside that shell `.wrap` tightens to `width:min(100% - 48px, calc(var(--max) + 48px))` with a 24px gutter, so brand/sidebar, search/content and profile/rail share one set of vertical edges. The third column is the track-details rail.

**Player band.** Fixed to the bottom on every tool screen where audio exists: `height:calc(var(--player-h) + env(safe-area-inset-bottom))`, field ground, `border-top:1px solid var(--ink)`. Anatomy left to right: 52px sleeve with a `--line` border, now-playing (title Origin Text 600 `14px` ink, sub mono `11px` `--i2`), transport of bordered `42px` squares with the play control a `48px` square of field text on solid ink, a square heart for the current track, canvas waveform seek (`flex:1`, `role="slider"`, arrow-key seek), mono tabular time right-aligned in `78px`. The body reserves `padding-bottom` for it. At `≤640px` the band becomes a two-row grid. Art, now-playing and the heart occupy the top row. Previous, play/pause and next stay together below, followed by seek and time. The title keeps its width at every size.

**Landing.** No left nav. The wordmark carries the screen, the tagline and scan form sit in one column, and the hero may show the square lattice canvas behind a plate cut by a 1px `--edge` outline. Landing scan form: `border:2px solid var(--ink)`, input Origin Text `17px`, submit filled ink in Origin Text 700 `.78rem` uppercase `ls .16em`, label `scan`. Markup is fixed: `action="/pages/scan.html" method="get" novalidate`, `input name="url"`, button `scan`. The archived mockups used the label `find tracks →`. The final app uses `scan`, so the old label does not ship.

## Components

| Component | Anatomy and rules |
| --- | --- |
| Set hero | The tracklist screen opens with it. A `--soft` panel with a `1px --line2` border. Cover on the left at a clamped `150–232px` with a `--line` border. Heading column on the right holds a mono meta row with source, genre and duration, the set title at `clamp(1.9rem,3.2vw,2.9rem)`, a mono `12px` source line, a deterministic waveform strip at `54px` with `--i2` bars at `.65` opacity and a circular ink marker, a mono start and end foot, and a hairline-separated action row with outlined compact actions plus a `re-scan` text link. The facts line follows, then the rows. |
| Track row | Grid `auto / var(--slv) / minmax(0,1fr) / auto / auto`: mono tabular stamp (a button in the app), sleeve, `.who` (artist 400 `--i2` · title 500 `--i1` · catalog mono `--i3`), store links, row actions. `border-bottom:1px solid var(--line2)`, first row gets the top border. Selected row: `background:var(--row-sel)`. The timestamp selects a listening position. The artist/title opens track details; liking and adding to a playlist are separate controls. |
| Set row | Bordered flat row (`--line2`), `min-height:92px`, sleeve 68px, name Origin Text 600 `17px`, source/when/count/state mono with the state boxed in a `--line` rectangle. Hover lifts the row to `--soft` and inks the border; icon-only affordances inside it fill `--hot`. |
| Sleeve | `--slv` square, radius 0. With artwork, the photo fills it. Without, a `--fill` ground carrying the mono catalog number, corner-set, `9px`. Unmatched rows carry the round logo on `--soft`, not a stranger's image. |
| Store link | `36px` square, `border:1px solid var(--line2)`, 19px glyph, hover/focus inks the border and fills `--hot`. `44px` at `≤480px`. |
| Row actions | Quiet squares or bordered mono buttons (`play`, like, playlist), min `36px`, `44px` on touch. Hearts use the shared outline icon, `aria-pressed`, and a label naming the track or set. A liked heart inverts to field on ink. |
| Chips | Filter chips only: history chips are the single pill exception (`min-height:36px`, `border-radius:999px`, mono `12px`); active is `background:var(--ink); color:var(--field)`. Discover chips are square (`radius 0`) and express state with `aria-pressed` plus the ink fill. On phones (`≤480px`) history chips go square too. |
| Buttons | Primary action (`SCAN`, `play`, `.act.primary`) is a filled ink rectangle with field text. Secondary actions (`.act`) are `--line` outlined uppercase rectangles. Tertiary actions (`re-scan`) are underlined text links, never button-shaped. Squared controls throughout: radius 0. |
| Inputs | Transparent ground, `1px` `--line` or ink border, radius 0, placeholder in `--i2`, `16px` minimum on touch to stop iOS focus zoom. Live search (`.srch`) is the quiet bordered variant; the scan form is the ink-bordered one. |
| Scan progress | Round logo disc `clamp(88px,12vw,126px)` inside a `--line2` orbit with a 1×9px ink tick; logo spins `2.4s linear`, orbit counter-spins `3.6s`, pauses on hover, dead under reduced motion. Percent in Origin Display 900 tabular, stage line mono uppercase, then a 3px `--track` rail filled `--tick` via `scaleX` at `.28s`. Stage words: *reading the source · matching artists and titles · checking the gaps*. |
| Errors | `.err` is one mono line with a `! ` bold prefix in ink. No banner, no colour. The line wraps when it is long. It is never clipped and never truncated with an ellipsis. |
| Empty state | One mono line in `--i2`, max 70ch, or an `.empty-panel` (flat, `1px --line`, display title, one sentence, one action). Never a paragraph, never an illustration. |
| Panels | Flat rectangular regions separated by hairlines, space, or a `1px` outline (`.plate`, `.set-hero`, `.disc-filters`, `.empty-panel`). Ground is field or `--soft`. Nested panels are wrong. |
| Icons | One family, `--icon` mask at a single weight, `15px`–`20px` in controls. No icon soup, no icon-in-rounded-tile, no emoji standing in. |

Circles exist in exactly three places and they are exempt from the radius rule: the round logo in the wordmark, the scanner disc and orbit, and the waveform marker. Everything else is `0` or, for chips only, `--r-pill`.

## States

Icon-only controls fill `--hot` on hover and press, with ink borders. Rows and text-bearing controls lift to `--soft` or invert to ink. `@media(hover:hover)` guards pointer hovers. `@media(hover:none)` gives touch the same acknowledgement on `:active`. Keyboard focus is `outline:2px solid var(--focus); outline-offset:2px`, with `-3px` on filled inputs and form controls. Disabled controls sit at `opacity:.4` and keep their shape. Loading is the scanner or a quiet mono line. No other spinner exists. A selected row uses `--row-sel`. Its `.sel-detail` strip sits above the tracklist, with stamp, artist, title, catalog, play and store controls. Inspection never changes playback or automatically follows the next track. Errors use the `! ` mono line. Every control ships hover, focus, active, disabled, empty and error states.

## Music navigation and saving

A set has one canonical page, reached from Dig, History, Discover or Liked. A track has an addressable page with square artwork, known artist/title/catalog/genre, demo playback, store searches and its occurrences in sets. Show missing facts plainly; never invent BPM, key, release dates or credits.

Track details are one surface with two presentations. On screens at least 1280px wide the details fill the reserved right rail; the column exists whether or not details are open, so opening them reflows nothing — the set hero, the search bar, the sidebar and every row keep their position and size. Below 1280px the same details open as a sheet fixed over the content column, and the top bar, the sidebar and the player stay visible and usable. Keep the list readable and the artwork square. Close and Escape restore the originating control and list position. There is no separate track page: a `#/tool/track/<key>?from=…` link resolves in place to its originating screen with `?detail=<key>`, so old links and their contextual way back keep working, and an unresolvable key shows a quiet missing state. Every control on the details surface measures at least 44×44px at any width. Browser Back, Forward and reload retain valid route context. Details and listening are independent actions.

Liked is a collection with Tracks and Sets tabs. One heart saves or removes an item without navigating or opening a playlist picker. Every visible heart reflects the same saved state. Track snapshots survive a re-scan; unavailable sets explain that they have left this browser’s history. Likes live locally in the prototype. Playlists remain user-arranged collections.

## Responsive

Breakpoints, in order. At `1080px` the top-bar gaps and the scan submit tighten. At `900px` the nav narrows to `172px`; the set hero keeps a square cover beside a flexible content column. Below `520px` its timeline and actions span the full width under the cover and title. At `760px` the shell turns phone-native. The top bar becomes two rows, brand and cluster first and the full-width scan form second. The left nav becomes a fixed five-slot bottom tab bar with an icon and a mono label. The current tab gets `border-top:2px solid var(--ink)` and a `--soft` ground. The player sits above the tab bar and `.wrap` goes full-width. At `640px` the player becomes a two-row grid and sleeves square to `50px`. At `480px` row stamps and catalog lines leave the collapsed row, store links grow to `44px`, and history chips go square. At `380px` the scan submit compresses. The title and sleeve survive on every width. The player keeps the now-playing text readable.

## Motion

Elements settle with `--ease`. Colour and ground hovers run `.16s linear`. The scan rail runs `.28s`. The logo tilt runs `500ms`. The panel enter runs `.16s`. Nothing loops except the live scanner. A global `prefers-reduced-motion:reduce` rule kills every animation and transition and pins the logo at rest. Give a screen one authored moment at most. Motion never carries meaning that the text does not carry.

## Do not

1. No second hue. No success green, no error red, no coloured badges, no tinted icon tiles, no theme switch or alternate colourway.
2. No depth. No gradient, glow, blur, glass, shadow, or halftone on the field or on any panel.
3. No elevation cards. Flat hairline-bordered panels are the vocabulary; rounded cards, card grids, and a border under a shadow are not.
4. No radius outside `--r-flat`, `--r-pill` for chips, and the three exempt discs.
5. No marketing reflexes: eyebrow kickers, numbered sections, centered hero over a card grid, admin tables, dashboard sidebars, iconsoup.
6. No `powerful` or `seamless` adjectives, and no tiny gray-on-gray prose, arbitrary font sizes, or misaligned peer values. Em dashes inside data lines (`title — artist`) are house style; everywhere else stay plain.
7. No modal where a panel or a row expansion will do.

A page that follows every rule but looks sterile is also wrong. The target is exact alignment, ink hierarchy, a large quiet field, and photographs providing the colour.

## Checks

Run these in order on every new surface, and record the result honestly.

1. Contrast in tiers: text pairs ≥4.5:1 (recompute, do not assume), filled controls identified at ink level, decorative hairlines never load-bearing.
2. Peer alignment: tabular numerals line up down every column; stamps never drift.
3. Overflow: no document-level horizontal scroll at 375 or 1440 and no clipped player text. Local focusable scroll areas are intended. The token and type tables sit in `role="region"` wrappers with `tabindex="0"` that scroll themselves, not the page.
4. Console: no errors.
5. Focus: a visible 2px ink outline on every interactive element, reachable in source order.
6. Reduced motion: every animation dead under `prefers-reduced-motion`.
7. Fonts: Origin families report `loaded`; no fallback rendering.
8. Screenshots: capture 1440 and 375 and look at them. The screenshot is the verdict, not the checklist.

Artwork may carry any color belonging to the record sleeve. Keep artwork square and preserve its colors. Do not tint artwork into the product ink. The interface field, controls and text still use the canonical gray and blue tokens. Fictional color studies live in the specimen Artwork section.
