# Politick — Design System

**Identity:** Broadcast · Soft red. The parliamentary record opened and transmitted to
citizens. This document is the **source of truth** for colour, type, space, shape, elevation
and motion. It is encoded in three places that must stay in exact sync:

- `design/DESIGN.md` — this spec (prose + rationale).
- `design/system/tokens/tokens.css` — CSS custom properties (`--pk-*`), the runtime contract.
- `design/system/tokens/tokens.ts` — the same values as a typed module for JS/Svelte.

Fonts: `design/system/tokens/fonts.css` (+ `fonts.md`). Live specimen:
`design/system/tokens/preview.html`.

> **Aesthetic in one line:** editorial, record/archive calm — a printed document, not a SaaS
> dashboard. No rounded corners. No cards-with-shadows. The soft red is a scalpel, not paint.

---

## 1. Colour

### 1.1 Core (FIXED — never altered)

| Token | Hex | Role | Usage rule |
|-------|-----|------|-----------|
| `--pk-ink` | `#1C2024` | Primary text & strokes | Default foreground on paper. The mark's arcs. |
| `--pk-paper` | `#F6F3EC` | Primary surface | Default page background (warm paper). |
| `--pk-accent` | `#B84A38` | The soft red | **Sparingly**: active state, one key word in a headline, the mark's tittle dot, small emphasis, the "now" marker. Never a large fill, never decorative area. |
| `--pk-alert` | `#D7372A` | Alert / error | **RESERVED** for error & alert states only (validation errors, destructive confirms, "processing failed"). Never decorative, never the same as the accent. |

The accent is muted from vermilion `#C5482F` so it carries **no party charge**. There is
**no green** in the brand. Party colours appear only as ≤8px dots (§1.4).

### 1.2 Neutral ramp (warm greys, derived from ink↔paper)

Greys are warm — they carry the paper's beige cast, not a neutral grey, so the whole surface
reads as one printed material.

| Token | Hex | Role | Contrast on paper |
|-------|-----|------|-------------------|
| `--pk-ink-strong` | `#3A3B3C` | Secondary headings, strong sub-text | 10.1 (AAA) |
| `--pk-muted` | `#5C564E` | Muted / secondary text, captions, labels | 6.54 (AA, AAA-large) |
| `--pk-faint` | `#8C8A84` | Disabled, placeholder, **non-essential** UI only | 3.1 — **not for body text** |
| `--pk-rule` | `#D8D2C4` | Hairlines, dividers, table borders | — (rule, not text) |
| `--pk-rule-faint` | `#E6E2D9` | Very subtle dividers, zebra rows, inset rules | — |
| `--pk-paper-raised` | `#FBFAF5` | Raised surface (table head, panels) | — |

**`--pk-faint` is decorative/disabled only.** It fails AA for body text by design; never use
it for information a user must read.

### 1.3 Accent companions

| Token | Hex | Role |
|-------|-----|------|
| `--pk-accent-strong` | `#A23E2D` | Pressed / active accent on paper (darker soft red). |
| `--pk-accent-tint` | `#EFD9D2` | Faint accent wash — text selection, search-hit highlight. |

### 1.4 Party dots — **PENDING real party data**

Party colour is **data, not brand**. Rules:

- Party colour appears **only as a dot of ≤ 8px** (`--pk-party-size`), never as a fill,
  background, or text colour.
- It must map to each party's **real** colour, loaded from the party register — these are
  facts about the world, so the "no green" brand rule does **not** apply (e.g. a party whose
  colour is green is drawn green).
- It must never be confused with `--pk-accent` (the soft red) or `--pk-alert`.

Placeholders (`--pk-party-a … --pk-party-f`, arbitrary muted hues) are stand-ins for layout
only and **must be replaced** before launch.

### 1.5 Dark / inverted surface

For hero bands, footers and "live sitting" sections, set `data-theme="ink"` (or class
`.pk-on-ink`). The **same token names** remap to ink-surface values — components and motifs
need no per-theme code (strokes use `currentColor`; the broadcast dot uses `var(--pk-accent)`).

| Token | Light | On ink | Note |
|-------|-------|--------|------|
| `--pk-paper` (surface) | `#F6F3EC` | `#1C2024` | |
| `--pk-paper-raised` | `#FBFAF5` | `#25292E` | |
| `--pk-ink` (text) | `#1C2024` | `#F6F3EC` | CR 14.8 (AAA) |
| `--pk-muted` | `#5C564E` | `#A6A39C` | CR ~6.4 |
| `--pk-rule` | `#D8D2C4` | `#3C4147` | |
| `--pk-accent` | `#B84A38` | `#CF6A55` | lifted soft red, CR 4.56 on ink (AA) |

### 1.6 Contrast (WCAG)

All checked with the WCAG 2.1 relative-luminance formula:

- **Body on paper** — ink `#1C2024` on `#F6F3EC`: **14.8** ✓ AAA.
- **Muted text on paper** — `#5C564E` on `#F6F3EC`: **6.54** ✓ AA (normal), AAA (large).
- **Accent text on paper** — `#B84A38` on `#F6F3EC`: **4.65** ✓ AA (normal).
- **Body on ink** — paper `#F6F3EC` on `#1C2024`: **14.8** ✓ AAA.
- **Accent on ink** — must use lifted `#CF6A55`: **4.56** ✓ AA. (`#B84A38` on ink is only
  3.18 — use it for dots/large only, not body-size text.)
- **Alert on paper** — `#D7372A` on `#F6F3EC`: **4.24** — passes AA-large / UI / icon /
  border use; for small alert text pair with ink or use ≥ 18.66px / bold.

---

## 2. Typography

Three faces, all SIL OFL 1.1 (self-hosted — see `fonts.md`). The pairing is deliberate: a
**high-contrast Didone** for the masthead voice, an **institutional sans** for everything
operational, and a **mono from the same sans superfamily** for the record's hard data.

### 2.1 Display serif — **Playfair Display** (`--pk-font-display`)

Wordmark + headlines + serif leads/pull-quotes.

- **Why:** Playfair is a high-contrast Didone with vertical stress, fine flat serifs and ball
  terminals — the engraved, "newspaper-of-record" masthead voice the brief asks for. It
  matches the chosen wordmark exactly (verified against `key-visual.png`: the wordmark is
  Playfair ~600). It carries authority and editorial gravitas without feeling corporate.
- **Optical range:** a *display* cut — superb ≥ 20px, used for the wordmark, h1–h3 and serif
  leads. **Do not** use it for body or anything < 18px (it gets fragile); that is the sans's job.
- **Weights:** 400 (leads / pull-quotes), 600 (wordmark + h1), 700 (strong headlines),
  italic 400 (editorial emphasis).
- **Fallback:** `"Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif`.
- *Considered & rejected:* Libre Caslon Display (too light/"book", loses the engraved
  contrast next to the wordmark); Spectral / Source Serif 4 (excellent text serifs but
  low-contrast — they read "humanist editorial", not "engraved masthead").

### 2.2 UI / body sans — **IBM Plex Sans** (`--pk-font-sans`)

Body copy, UI chrome, labels, buttons, tables, navigation — the operational majority.

- **Why:** Plex Sans is *institutional* — it was drawn as a corporate/institutional voice, so
  it reads neutral and official without the over-exposed "startup product" association of
  Inter. It is warm enough to sit beside a Didone, exceptionally legible at small sizes, and
  has true tabular figures. Crucially it is a **superfamily** with Plex Mono, giving the data
  layer free coherence.
- **Weights:** 400 (body), 500 (UI medium), 600 (labels / buttons / kickers), 700 (strong),
  italic 400.
- **Fallback:** `-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif`.
- *Considered:* Inter (cleaner but reads "SaaS", which the brand explicitly avoids).

### 2.3 Data / mono — **IBM Plex Mono** (`--pk-font-mono`)

Dates, counts, vote tallies, citations (Hansard date · page · column), IDs, code.

- **Why:** same superfamily as Plex Sans, so it harmonises by construction. Monospace is
  tabular by nature (figures align in columns — essential for the record's numbers), and
  Plex Mono's slab-humanist warmth suits citation/provenance text without feeling like a
  terminal. Reinforces the "this is a verifiable record" promise.
- **Weights:** 400 (data / citations), 500 (emphasis). Apply `font-feature-settings:
  "tnum" 1, "lnum" 1` (`--pk-font-tabular`) wherever figures must align.
- **Fallback:** `ui-monospace, "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace`.

> **Future:** Sinhala/Tamil content (i18n) will add Noto Serif/Sans Sinhala + Tamil as a
> separate font track when localized content lands — out of scope for this English-first set.

### 2.4 Type scale — Major Third (ratio **1.250**), base 16px

A modular scale gives the masthead its big editorial jumps while staying systematic. The
three fine UI sizes sit off-grid for small-size legibility (a normal UI convention).

| Token | px | rem | Face / role |
|-------|----|-----|-------------|
| `--pk-text-2xs` | 11 | 0.6875 | sans 600 — kickers / eyebrow labels (uppercase, tracked) |
| `--pk-text-xs` | 13 | 0.8125 | mono / sans — captions, citations, fine print |
| `--pk-text-sm` | 14 | 0.875 | sans — secondary UI, dense tables |
| `--pk-text-base` | 16 | 1 | sans 400 — **body** |
| `--pk-text-md` | 20 | 1.25 | sans 500 sub-head / serif 400 small lead |
| `--pk-text-lg` | 25 | 1.5625 | display — h3 |
| `--pk-text-xl` | 31.25 | 1.953 | display — h2 |
| `--pk-text-2xl` | 39 | 2.441 | display — h1 |
| `--pk-text-3xl` | 49 | 3.052 | display — page hero |
| `--pk-text-4xl` | 61 | 3.815 | display 600 — wordmark / masthead |

### 2.5 Line-height, measure, tracking, features

- **Line-height:** display/masthead `1.08` (`--pk-leading-display`); sub-heads `1.2`; serif
  lead `1.4`; long-form body `1.6` (`--pk-leading-body`); dense UI & mono `1.5`.
- **Measure:** reading column `66ch` (`--pk-measure`); article max `42rem`; never run body
  wider than ~75ch.
- **Tracking:** headlines `-0.01em` (slight tighten); wordmark `+0.005em`; body `0`;
  **uppercase kickers** `+0.14em` (`--pk-track-caps`); **masthead tagline**
  (`PARLIAMENT. ON RECORD. FOR EVERY CITIZEN.`) `+0.18em` (`--pk-track-tagline`). The tagline
  is set in **uppercase Plex Sans + tracking**, not OpenType small-caps (neither face ships
  reliable `smcp`); this reproduces the tracked-caps look in the logo.
- **Figures:** apply `--pk-font-tabular` (`"tnum" 1, "lnum" 1`) to all data — tables, counts,
  dates, citations — so columns of numbers align. Body prose uses default proportional figures.

| Role | Face | Size | Weight | Leading | Tracking |
|------|------|------|--------|---------|----------|
| Wordmark | display | 4xl | 600 | 1.08 | +0.005em |
| H1 | display | 2xl | 600 | 1.08 | -0.01em |
| H2 | display | xl | 600 | 1.1 | -0.01em |
| H3 | display | lg | 600 | 1.15 | 0 |
| Lead | display | md | 400 | 1.4 | 0 |
| Body | sans | base | 400 | 1.6 | 0 |
| UI / button | sans | sm | 500–600 | 1.5 | 0 |
| Kicker / label | sans | 2xs | 600 | 1 | +0.14em, uppercase |
| Caption | sans/mono | xs | 400 | 1.5 | 0 |
| Data / citation | mono | xs–sm | 400 | 1.5 | 0, tabular |

---

## 3. Space & layout

- **Base unit 4px.** Scale (`--pk-space-1…10`): 4, 8, 12, 16, 24, 32, 48, 64, 96, 128.
- **Grid:** 12 columns, gutter `1.5rem` (24px). Content max `75rem` (1200px,
  `--pk-content-max`); article/reading max `42rem` (`--pk-content-narrow`).
- **Measure:** body never wider than `66ch`.

## 4. Shape

- **`border-radius: 0` everywhere** (`--pk-radius: 0`). This is a hard rule — corners are
  square, like a printed page. No pill buttons, no rounded cards, no rounded inputs.
- **Rules / borders:** hairline `1px` (`--pk-hairline`, table cells, inset dividers);
  default `1.5px` (`--pk-rule-w`); emphasis `2px` (`--pk-rule-w-strong`, section/footer top
  borders). Default border colour `--pk-rule`; structural borders use `--pk-ink`.
- The dash/dot **record-divider** motif (see `_explore/dash-dot-motif.md`) is the preferred
  divider between debate records/speeches, in place of a plain hairline.

## 5. Elevation

**Flat. No drop shadows, ever** (`--pk-elevation: none`). Separation comes from **rules** and
**surface tint** (`--pk-paper-raised`), the way panels are separated on a printed page. A
"card" is a bordered or tinted rectangle, not a floating shadowed object.

## 6. Motion

Calm, short, rare — a record of government, not a live dashboard.

- **Durations:** `--pk-motion-fast` 120ms (hover/press), `--pk-motion` 200ms (default
  transitions), `--pk-motion-slow` 320ms (entrances).
- **Easing:** `--pk-ease` `cubic-bezier(0.2,0,0,1)` (calm decelerate) as default;
  `--pk-ease-inout` for symmetric moves.
- **Broadcast pulse:** the mark's arcs may ripple outward **once** on page load and gently on
  hover/focus of the logo and live-sitting indicators (`--pk-pulse` 1200ms). Nothing loops.
- **Reduced motion:** `@media (prefers-reduced-motion: reduce)` collapses all of the above to
  `0ms`. Animation is always an enhancement; the static state is the truth.

---

## 7. Token contract (for the other workers & the site)

These names are stable — the mark and motif workers consume them:

- Colour: `--pk-ink` `--pk-paper` `--pk-accent` `--pk-alert` `--pk-ink-strong` `--pk-muted`
  `--pk-faint` `--pk-rule` `--pk-rule-faint` `--pk-paper-raised` `--pk-accent-strong`.
- Type: `--pk-font-display` `--pk-font-sans` `--pk-font-mono` + the `--pk-text-*` scale.
- Shape: `--pk-radius` (0) `--pk-hairline` `--pk-rule-w` `--pk-rule-w-strong`.
- The **mark worker** renders the wordmark as live text in `--pk-font-display` (Playfair
  Display 600) before outlining it to vector — the `@font-face` is defined in `fonts.css`.
- The **motif worker** uses `--pk-rule` as the hairline/rule grey and `var(--pk-accent)` for
  the broadcast dot (already matching the tiling generator's `--pattern-accent` default).
