# avonlake.city — Public Record Design System

> Category: Project Design System
> Surface: web (responsive · light, with an opt-in dark theme)
> Registered design-system id: `user:untitled-design-system`
> Source project: "Untitled" (`da261e01-2b41-4d44-883d-9ea1db5cdfca`) · linked pipeline repo: **lorain-data**, where this package is vendored at `design-system/`
> Governs the **public avonlake.city surfaces**; the private Map Room consumes
> these tokens through `explorer/src/theme.css`'s production bridge.

**One sentence:** An annotated-document system for civic accountability — cool paper-green canvas, white evidence cards, a single 150° green ramp that carries structure *and* data, a highlighter-yellow rationed to four roles, and a serif/sans/mono split where the serif speaks, the sans explains, and the mono cites.

## Product context & scope of evidence

**The product.** avonlake.city is an independent, resident-built public-record project for
Avon Lake, Ohio. It collects budgets, council votes, land deals and county property
records from public registers across county, state, and federal sources. This is prototype
copy, not a live inventory; verify it against `lib/pipeline/registry.ex` before publication.
It publishes what those records say with a link to each source document and states plainly
what it cannot show. Its lead investigation is the
former GenOn coal-plant site: four 2021 deeds recording no price, and a $15M planned city
purchase of 23 acres for a public park. It is not a news site and not a city site — the
disclaimer *"not affiliated with, endorsed by, or operated by the City of Avon Lake"*
travels with the wordmark everywhere.

**Its readers** are residents arriving with one question ("what happened to the lakefront?"),
and its job is to answer with the record rather than a summary. That is why the interface
reads as an annotated document: a corpus you can inspect, an evidence panel per claim, a
margin note carrying the methodological caveat, and a receipt under every figure.

**Primary surfaces:** a home page (the catalog of open files), a dashboard, a case file per
investigation, and — added by this kit — a register browser and a corrections/records-request
intake.

This system is derived **only from `index.html`** — the redesigned avonlake.city home page — at the user's explicit instruction, so it can then be applied outward to the remaining pages.

| Evidence | Status |
|---|---|
| `index.html` (redesigned home page, 521 lines) | **Sole source.** Tokens, type, spacing, components, motion, copy voice, accessibility patterns all read from it. |
| `examples/source-home-page.html` | Byte-preserved copy of the source at extraction time. |
| `index-v1.html` | Excluded — superseded by the redesign. |
| `avonlake-city.html`, `alerg-case-file.html` | **Targets — now fully rebuilt.** Still not sources: nothing was read *from* them. The migration shim is deleted; both pages load `ui_kits/app/app.css` and are composed entirely from system components. What page-local CSS remains is composition plus two devices that belong to one page only (the council calendar, the schematic map), and it declares no colour or type of its own. |
| `brand-spec.md` | Superseded. It described the earlier burgundy/orange/newsprint direction; the redesign replaced it with the green/volt direction documented here. Kept for provenance — see `context/provenance.md`. |
| `image.png` (City of Avon Lake site screenshot) | Excluded from this scope. It was the palette source for the superseded burgundy direction. |
| `sketch-2026-07-29T03-04-43.sketch.json` | 254 B stub, no design content. |

Nothing below is invented. Where the home page does not settle a question (dark mode, tables, modals, form inputs), this document says so and gives a rule derived from the page's own logic rather than a guess presented as fact.

---

## 1. Visual Theme & Atmosphere

The page is a **civic document that has been annotated**, not a product marketing site. Its atmosphere comes from four decisions visible in the source:

1. **The canvas is paper, not screen.** `--bg:#f5f7f1` — a cool green-tinted off-white. White (`--surface`) is spent deliberately: it means *this is evidence*. The record-field section (`.field`) flips to a white background precisely because it is the corpus itself.
2. **Structure is the ornament.** There are no illustrations, no photography, no icons anywhere in the source. Meaning is carried by grids, 1px rules, a numbered method list, a filing-tab card, a ledger, and the prototype's 197-cell ALERG match field. The single decorative gesture in the whole page is one radial green glow behind the dark featured band (`.feat:before`).
3. **Two dark slabs punctuate a light page.** `.feat` (`--forest-deep`) and `footer` (`--ink-band`) are the only dark regions. They mark *the lead story* and *the colophon* — the two moments where the project speaks in its own voice rather than quoting the record.
4. **Data is decoration.** The prototype's 197-cell ALERG field is the hero graphic. Its cells are keyword matches grouped across the early case-source fixture (70 + 44 + 39 + 13 + 6 + 5 + 4 + 16 = 197), not a live warehouse total or the current source-register count. It is interactive and doubles as the page's largest block of texture.

**Feeling:** rigorous, resident-built, unhurried, slightly severe — a project that would rather show you a limitation than a claim.

## 2. Color

Full token list with computed OKLch and measured contrast: `colors_and_type.css`.

### Two layers: primitives and roles

The palette ships as **primitives** (a named colour, no opinion about where it goes) and a
**semantic role layer** on top (what a thing is *for*). Build with roles; reach for a
primitive only when you are defining a new role.

The layer exists because `--forest` was doing three unrelated jobs at once — the action
colour, register step 2 of the data scale, and structural dark — with no name for any of
them. That is survivable until you need a dark theme or a second status, at which point
"make the links lighter" also silently moves a data series.

| Role family | Tokens | Themes? |
|---|---|---|
| Surfaces | `--canvas --evidence --evidence-inset --slab --slab-feature --rule --rule-soft --rule-strong` | yes |
| Text | `--ink --ink-prose --ink-quiet --ink-margin` | yes |
| Action | `--action --action-strong --action-on --action-edge --focus --focus-on-dark` | yes |
| Emphasis | `--mark --figure-lead --cta-dark --wordmark-tld --mark-ink` | no (volt is dark-only in both themes) |
| Status | `--status-held --status-pending --status-quiet` (+ `-bg` pairs) | yes |
| Ordinal data | `--reg-1…8` (registers) · `--grade-1…4` (identity grade) | ramp inverts in dark |
| Cartographic | `--map-water --map-land` | yes |

`ui_kits/app/app.css` contains **zero** raw surface or text primitives — the whole component
layer consumes roles. Two deliberate exceptions keep their primitives: the brand ribbon
(`.topline`) and the two permanently-dark bands (`.feat`, `.panel-top`), which are fixed
devices rather than themed surfaces. Live: `preview/token-roles.html`.

### Roles

| Token | Hex | OKLch | Role in the source |
|---|---|---|---|
| `--bg` | `#f5f7f1` | `oklch(0.973 0.008 121.6)` | Page canvas |
| `--surface` | `#ffffff` | `oklch(1 0 0)` | Evidence: cards, panel rows, the record-field band |
| `--surface-2` | `#eaeee3` | `oklch(0.943 0.015 123.0)` | Inset fills — panel footnote, callout rule, ghost-button hover |
| `--ink-band` | `#0f2a1a` | `oklch(0.258 0.045 155.4)` | Footer slab |
| `--fg` | `#12241a` | `oklch(0.241 0.031 158.6)` | Primary ink — **15.05:1** on `--bg` |
| `--fg-2` | `#3d5044` | `oklch(0.411 0.031 157.0)` | Body prose — **8.01:1** |
| `--muted` | `#5a6d61` | `oklch(0.515 0.030 157.5)` | Captions, card prose — **5.13:1** |
| `--meta` | `#596d62` | `oklch(0.515 0.029 161.2)` | Margin notes — **5.14:1** |
| `--border` | `#d5ddcd` | `oklch(0.887 0.023 128.8)` | Every structural 1px rule |
| `--border-soft` | `#e4eadd` | `oklch(0.929 0.018 127.2)` | Dividers *inside* a card |

### The green ramp — one hue, two jobs

Seven steps of a ~150° green run from `--forest-deep #14572f` to `--mist #cfd8c9`. It is the only chromatic family in the interface, and it does double duty:

- **Structure/action:** `--forest #166534` is the single action color — links, primary buttons, eyebrows, nav underline, focus ring, the numbered method rail. **7.13:1** on white, **6.61:1** on `--bg`. White on `--forest` is **7.13:1**; on `--forest-deep`, **8.63:1**.
- **Data:** the same ramp is the register scale (`--reg-1`…`--reg-8`), ordered so the largest register is darkest. Charts encode with **fills**, never bare outlines.

### Signals

- **`--volt #faff69`** — the highlighter. It appears **four times in the entire page**, and the budget is per role, not a flat count: one marker swipe behind a single word in the H1; **one** volt figure per data block (the `$0` in the ledger — the number that is the story); **one** `.btn.volt` primary CTA on a dark band; and the `.city` in the footer wordmark. The featured band legitimately carries two — the CTA and the figure — because they are different roles. What is never allowed is two volt *figures* in one ledger, or a second volt CTA. It is a light color (L 0.969) so it is only ever used *on dark* or *as a mark behind dark text* — `--volt-ink #1a2410` on volt is **15.01:1**; volt on `--forest-deep` is **8.04:1**. Never volt text on the light canvas.
- **`--amber #a8781f` / `--amber-ink #8a5f12`** — the one deliberate hue break. It means "on record but not yet obtainable": the county-auditor register swatch, and the `Records requested` stamp (`--amber-ink` on `--stamp-prep-bg #f8f2e4`, **5.05:1**).

### States

- Hover on light: fill flips to `--forest` with white text, `translateY(-1px)`, `--shadow`.
- Hover on a card: `translateY(-4px)`, border → `--sage`, filing tab inverts to `--forest`/white.
- Active/pressed: `translateY(0)` + `--shadow-in` (an inset press, not a color change).
- Focus: 2px `--forest` outline, 3px offset — swapped to `--volt` inside dark regions.
- Live/verified: `.stamp.live` — `--forest` on `--stamp-live-bg #e9f2e8` (**6.22:1**).
- Not-yet-public: `.stamp.prep` — amber pair above.

### Dark theme

Not in the source — the page declares `color-scheme: light`. It is defined now as an
**opt-in extension**, and it is deliberately not an inversion and not a second palette:
the canvas is `--ink-band`, the footer slab the source already shipped, and every text
value is a ramp the source already measured. Enable with `data-theme="dark"` on any
element; because roles are inherited custom properties, it scopes at any depth.

Exactly three values are new, and each was forced by a measurement:

| Addition | Forced by | Derivation |
|---|---|---|
| Surface ladder `#1d3828 · #2a4534 · #304b39 · #3c5745` | A dark canvas with no raised surface has nowhere to put a card | Same hue and chroma as `--ink-band`, stepped in OKLch L: .258 → .315 → .363 → .385 → .430 |
| Action `#7fbc8a` | `--forest` measures **2.16:1** on this canvas — it fails AA outright | Lifted up the ramp past `--moss` until it clears 4.5:1 on **all three** dark surfaces (6.93 / 5.74 / 4.73). `--moss` itself measured only 3.74:1 on `--evidence-inset`, where the record table's header text lives |
| Amber `#e0ae5d` | `--amber-ink` is unreadable on a dark surface, and status must never depend on a colour you cannot see | Same hue (77.7°), lifted to `oklch(.78 .115 77.7)` |

Rules that reverse in dark: **hover goes lighter, not darker** (`--action-strong` is
`--forest-deep` in light and `--sage` in dark); **the grade ramp inverts** so weight still
reads as confidence; and **depth comes from the surface ladder, not shadows** — a drop
shadow is nearly invisible on a dark canvas, so `--shadow-*` become hairline rings plus a
deeper ambient blur. Volt is unchanged: it was always a dark-only colour.

Do **not** wire `prefers-color-scheme` until a product decision says to follow the OS.
Every pair measured in both themes: `preview/theme-light-dark.html`.

## 3. Typography

Three faces, three jobs, never blurred. **The serif and the sans are self-hosted; the mono is a system face.**
The two bound faces are the ones a reader actually reads at length — the voice and the interface — and both
were costing real variance when left to the OS. The mono carries short apparatus strings where every
candidate resolves to something competent, so it stays free. Together the bound faces are **68 KB**, and
the system still makes **no third-party request** — `fonts/` is served from the project.

| Role | Stack | Used for |
|---|---|---|
| Display / **voice** | `'Charis SIL','Iowan Old Style','Charter',Cambria,'Georgia','Times New Roman',serif` | h1–h3, wordmark, **every headline figure** |
| Body / **interface** | `'IBM Plex Sans',system-ui,-apple-system,'Segoe UI','Helvetica Neue',sans-serif` | Prose, nav, labels, card copy |
| Mono / **apparatus** | `ui-monospace,'SF Mono','SFMono-Regular',Menlo,Consolas,monospace` | Eyebrows, source lines, counts, stamps, buttons, margin notes |

The colophon states the rule out loud: *"Set in Charis SIL and IBM Plex Sans, with SF Mono for every figure."*

**Why the serif is bound.** Charis SIL (SIL International, OFL 1.1 — `fonts/OFL-CharisSIL.txt`) is a Bitstream
Charter derivative, and Charter was already named at fallback position 2, so binding it guarantees a face
the system always intended rather than introducing a new one. Two static weights, **28 KB total**. It was
chosen over Source Serif 4 (119 KB) and Newsreader (129 KB) because a civic project has to load on any
connection, and weight beat an optical-size axis. `Cambria` sits ahead of `Georgia` in the fallback chain
because it ships with Windows/Office and has lining tabular figures by default — so if the webfont fails,
the figure rule below still holds for most readers.

**Why the sans is bound.** IBM Plex Sans (IBM Corp., OFL 1.1 — `fonts/OFL-IBMPlexSans.txt`), one variable
file covering 400–600, **40 KB**. The sans was the last role still resolving to whatever the OS owned —
SF Pro, Segoe UI, Roboto, or a Liberation/Arial metric clone at the tail — which left the interface voice
as unpredictable as the display face had been, and ended the chain on Arial. Plex is humanist rather than
neutral-grotesque: flared terminals, a tailed `l`, a distinctly drawn `a` and `g`. It gives the interface
grain without asking to be looked at, which is the correct amount of personality for a surface whose job
is to disappear behind the record.

It is also **the body face the sibling product already declares** — the Map Room's own design system names
`"IBM Plex Sans"` for body — so the public site and the private operator surface now share an interface
voice while keeping their own display faces. That is evidence, not taste.

`Arial` was removed from the fallback chain outright. Full reasoning and live specimens:
`preview/type-audit.html`.

### Scale (as shipped)

| Step | Size | Face | Spec |
|---|---|---|---|
| H1 | `clamp(28px,3.5vw,44px)` | serif 600 | `1.14` / `-.019em` / max `19ch` |
| H2 | `clamp(23px,2.5vw,31px)` | serif 600 | `1.18` / `-.012em` / max `24ch` |
| H2 on dark | `clamp(24px,2.9vw,36px)` | serif 600 | max `22ch` |
| Ledger figure | `27px` | serif, tabular | on dark |
| Panel figure | `21px` | serif, tabular | |
| H3 card | `20px` / standards `18px` | serif 600 | |
| Lede | `17px` | sans, `--fg-2` | max `52ch` |
| Body | `16px` / `1.6` | sans | |
| Section sub-copy | `15px` | sans, `--muted` | max `58ch` |
| UI / nav / card prose | `13.5px` | sans | card prose `1.5` |
| Legend chip, disclaimer | `12.5px` | sans | |
| Source line, counts | `11.5px` | mono, `--muted` | max `72ch`, `.02em` |
| Eyebrow | `11px` | mono uppercase | `.16em`, `--forest` |
| Stamp | `10px` | mono uppercase | `.11em` — **the size floor** |

### Rules

1. **Every headline number is serif and `tabular-nums`.** `$0`, `$15M`, `90.6%`, `197`, `43.23 ac`. Sans numerals are a violation.
2. **Every eyebrow is mono, uppercase, `.16em`, `--forest`** — and becomes `--volt` on dark. It reads as a file label, not a kicker.
3. **Every claim is followed by a `.src` line** in mono at `11.5px` naming the register, the date, or the limitation.
4. **Measure is capped everywhere.** Nothing runs long: `19ch` headlines, `52ch` ledes, `72ch` source notes.
5. **10px is the size floor.** Nothing in the system goes below it. The stamp sits exactly there because it carries status, which is the last thing a reader should squint at.
6. **Mono tracking is a maximum, not a target.** `.16em` eyebrow, `.11em` stamp and `.07em` button were tuned on SF Mono. On a wider mono — Cascadia Mono on Windows, Roboto Mono on Android — reduce toward `.12em` for the eyebrow; never increase past the declared value.

## 4. Spacing

Full scale in `tokens.css`. The page uses an unrounded, document-derived rhythm rather than a strict 8pt grid — this is deliberate and should be preserved.

- **Gutter** `28px`, dropping to `20px` at ≤560px. **Max width** `1180px`.
- **Band rhythm:** `64px` light bands, `66px` the dark featured band, `60px` the record field, `56px` hero top, `52/56px` footer. Section head → content is `26px`.
- **Column gaps:** hero `48px`, featured `44px`, margin-note → content `38px`, card grids `16px`, record-field cells `4px`.
- **Card padding:** issue cards `22px 20px 20px`; panel rows `12px 17px`; standards cells `26px 24px`; ledger rows `15px 18px`.
- **Density:** medium-dense. Rows are 44–52px tall, comfortably above the 44px touch minimum; the record field is the only genuinely dense element at 13px cells with 4px gutters (11px cells ≤560px).

## 5. Layout & Composition

### The annotated-document grid

The signature layout is `.doc` — a `172px` margin-note column plus content, `38px` gap. The note (`.mg`) is mono `12px`, `--meta`, with a `2px` top rule and a `--forest` uppercase label. It carries the *methodological caveat* for the section beside it. It collapses to a single column below 1000px (rule thins to 1px). Use it for any section that makes a claim about how the record was read.

### Page skeleton (in source order)

1. `.topline` — 3px tri-segment ribbon, `forest-deep 42% / forest 32% / volt 26%`. The system's only pure graphic device.
2. `header.mast` — sticky, `62px`, `--bg` at 90% with `blur(10px)`, 1px bottom rule. Wordmark + disclaimer tag + nav + a pulsing "Updated" chip. Collapses to a `≡` drawer at ≤960px.
3. `.hero` — asymmetric `1.12fr / .88fr`: editorial column (eyebrow, H1 with highlighter, lede, two CTAs, a mono provenance line) against the **filing-card panel**.
4. `.field` — white band. Section head is `1fr / 240px` with a right-aligned caption; then the prototype's 197-cell ALERG match field and its legend chips.
5. `.feat` — the dark slab. `1.1fr / .9fr`: argument against a four-row ledger.
6. `.band#method` — `.doc` grid; six numbered rows plus a callout rule.
7. `.band#issues` — two rows of three filing-tab cards.
8. `.band#ethos` — `.doc` grid; a three-cell standards strip inside one bordered box.
9. `footer` — ink slab, `1.5fr / .8fr / .8fr`, closing with a three-part mono colophon.

### Responsive

Four breakpoints, all present in the source:

| ≤ | Change |
|---|---|
| 1000px | `.doc` → 1 col; card grids and standards → 1 col; footer → 2 col |
| 960px | tag hidden; nav → drawer; hero and featured → 1 col; method row → `44px 1fr` with prose wrapping to col 2 |
| 860px | field head → 1 col, caption left-aligns |
| 560px | gutter → 20px; cells → 11px; footer → 1 col |
| 430px | panel rows stack so the serif figure keeps its line; filing tab wraps inline; ledger rows stack. **Designed, not inherited** — see `preview/narrow-390.html` |

`overflow-x:hidden` on body; no horizontal scroll at any width.

## 6. Components

Every component below exists in `index.html`; component markup is reproduced in `preview/components-*.html` and applied in `ui_kits/app/`.

### The contract every component owes

Before the catalogue — four questions each component must answer, and the shape of the
answer. A component that cannot answer all four is not finished.

| Question | The system's answer |
|---|---|
| **What content is required?** | Named slots, not "whatever fits". A panel without a footnote strip is a panel missing its caveat. |
| **What must never go in it?** | Stated explicitly. A split bar with no denominator, a stamp with no word, a ledger with two volt figures. |
| **What is its a11y contract?** | Which ARIA it owns, what happens with no JS, what happens on failure, and what `Escape` does. |
| **How does it fail?** | Honestly and in words. The record field removes its `role="img"` and prints a text pointer rather than showing an empty box. |

**Required-content quick reference:**

| Component | Required slots | Never |
|---|---|---|
| `.panel` | header label + serif title · ≥1 key/value row · footnote strip | A figure with no receipt anywhere on the surface |
| `.sbar` | label above · label below · stated total | A bare bar with no denominator |
| `.stamp` | a **word** | Colour alone; a third hue for a third meaning |
| `.ic` | `data-tab` · title · prose · status + `→` | An icon in place of the mono arrow |
| `.ledger` | ≤24ch label · serif tabular figure | Two `.lv.volt` in one ledger |
| `.grid197` | `role="img"` + full `aria-label` · `<noscript>` breakdown · caption defining the unit | An outline chart; a count with no definition of what it counts |
| `.mg` | uppercase label · the section's methodological caveat | Marketing copy in the margin |
| `.rule` | the standing rule for the section | Decoration |
| `.brow` | label · filled track · stated value | A bar with no value beside it; an axis instead of a number |
| `.gaps` | uppercase label · ≥1 named gap | A gap list that only contains solved things |

### Buttons (`.btn`)
Mono `11.5px`, uppercase, `.07em`, `12px 18px`, `--r-ctrl`, 1px border. Five variants:
`.solid` (forest fill, white — primary on light) · default outline · `.ghost` (border `--border`, `--fg`; hover `--surface-2` + `--sage` border) · `.ondark` (50% white border → white fill, `--forest-deep` text) · `.volt` (volt fill, `--volt-ink` — **primary on dark only**).
Hover lifts 1px and adds `--shadow`; active drops back with `--shadow-in`.

### Filing-card panel (`.panel`)
The system's evidence container. `--forest-deep` header with a volt mono label over a serif title, then key/value rows (sans key, serif tabular value), an optional split bar, and a `--surface-2` footnote strip. `--shadow-lg`. Values count up from 0 on reveal.

### Split bar (`.sbar`)
22px tall, `--r-bar`, 1px border, two fills (`--forest` / `--sage`), width animated over `1.1s`. Labelled above and below — never a bare bar.

### Record field (`.grid197`)
One `13px` square per prototype ALERG keyword match, tinted by its source group. This is a reusable visual encoding, not a claim that the live registry has 197 records or fourteen sources. Hover/focus/click a legend chip to isolate: unselected cells drop to `13%` opacity, matches scale to `1.18`. Click locks (`aria-pressed`), `Escape` clears. The container carries `role="img"` with a full text `aria-label`, and a `<noscript>` breakdown. If cells fail to render, the role is removed and a plain-text fallback replaces it.

### Legend chip (`.lg`)
The only pill in the system. `999px`, 1px border, `6px 11px`, a 10px square swatch, sans label, mono count. Real `<button>` elements — hover *and* focus preview, click locks.

### Filing-tab card (`.ic`)
Radius `0 8px 8px 8px` with a `::before` tab pulled from `data-tab` sitting `-21px` above the top-left corner. Title, an optional `.fig` figure slot (serif, tabular — a card leading with a quantity still has to set it as one), prose, then a footer rule with a status stamp and a mono `→` affordance. Hover: `-4px`, `--shadow`, `--sage` border, tab inverts to forest/white.

As a `<button role="tab">` it is also the system's **track selector**: `aria-selected="true"` inverts the tab exactly the way hover does, so the affordance and the state read as one gesture. `.cat.four` puts four of them in a row, folding 4 → 2 → 1. Used this way it owns `aria-selected` / `aria-controls`, roving `tabIndex`, and ←/→ keys; the panel it controls is a plain `[role="tabpanel"]` toggled with the native `hidden` attribute, so no-JS lands on the first panel rather than on nothing.

### Stamp (`.stamp`)
Mono `10px`, uppercase `.11em`, `3px 8px`, `--r-bar`, `border:1px solid currentColor`. `.live` forest/`#e9f2e8`; `.prep` amber-ink/`#f8f2e4`; `.quiet` (`--status-quiet` on `--status-quiet-bg`) for the third class of state — `draft`, `open`, `withdrawn` — with `.quiet.struck` for a withdrawn record that keeps its identity. The neutral chip was in the status table in `IMPLEMENTATION.md` §4 long before it was a class; the target-page migration is what finally needed it.

### Labelled bar row (`.brow`)
`150px / 1fr / 92px` — sans label, a filled track on `--evidence-inset`, and a serif tabular value. The `.sbar` fill logic unrolled into a list, for a quantity whose parts each need naming. **Every row states its own value**, so the chart is readable without an axis; fills come from the ordinal ramp so rank still reads as weight. Animates on reveal via `data-fill`, or on tab activation via `data-w`. Never an outline chart.

### Method row (`.mrow`)
`64px / 1fr / 1.15fr` — mono number, serif H3, sans explanation. Hover tints to `--surface` *and* nudges padding-left `4px→14px`, so the row physically steps forward.

### Callout rule (`.rule`)
`--surface-2`, 3px `--forest` left border, radius `0 6px 6px 0`. Reserved for the standing methodological rule of a section.

### Ledger (`.ledger`)
On-dark evidence list: 1px white-18% border, rows with a `24ch` label and a `27px` serif figure; `.lv.volt` marks the single most important number.

### Standards strip (`.stand`)
Three cells inside one bordered, radiused box divided by 1px verticals — not three floating cards. Each: mono `01 · Label`, serif H3, sans prose. Stacks with horizontal dividers ≤1000px. `.stand.five` widens it to five cells for a process rail, where each cell may close with a `.stamp` carrying the stage's state.

### Stated-limits list (`.gaps`)
An inset box on `--evidence-inset`, a mono uppercase label, and an auto-fitting list whose items are marked with an em-dash in `--action`. This system names a gap as loudly as a finding, so *what we can't show* has a fixed shape rather than depending on someone remembering to write the paragraph. Use it wherever the honest answer is a list of things not yet on record; use `.stand` when the gaps each need a paragraph of their own.

### Masthead + nav
Wordmark is serif `22px`, `-.025em`, with the TLD in `--forest` (volt in the footer). Nav links get a 2px `--forest` underline that scales in from the left. The `.live` chip pairs a `--leaf` dot — which pulses exactly three times then stops — with a mono timestamp.

### Layout and sub-part selectors

The catalogue above names the components a designer reaches for. These are the
layout wrappers and named sub-parts they are built from — styled in `app.css`,
and listed here so nothing in the component layer is undocumented.

| Selector | Belongs to | What it is |
|---|---|---|
| `.skip` | page | The skip link. Precedes everything; visible only on focus. |
| `.menu-btn` | masthead | The `≡` drawer toggle, shown ≤960px. |
| `.dot` | `.live` chip | The 7px pulse. Three iterations, then stops. |
| `.cta` | hero · `.feat` | The button row. Wraps; gap `11px`. |
| `.fieldhead` | `.field` | `1fr / 240px` section head with a right-aligned caption. |
| `.legend` | `.grid197` · any keyed fill | The chip row. Its children are `.lg`. As a `<ul>` of static `li.lg` it also keys a stacked `.sbar` or a schematic map — a chip that isn't a control loses the pointer cursor rather than pretending. |
| `.cat` · `.catrow` | `.band#issues` | The three-up filing-card grids. `.cat.four` is the four-up track selector. |
| `.icf` | `.ic` | The card's footer rule — status stamp + mono `→`. |
| `.fgrid` | `.feat` | `1.1fr / .9fr` — argument against ledger. |
| `.foot-grid` · `.fl-list` | footer | `1.5fr / .8fr / .8fr` and its link lists. |
| `.rt-scroll` | `.rt` | The record table's scroll container. Present at **every** width — before the 390px pass the table pushed the whole page sideways on a narrow viewport. |
| `.demo-dark` | kit index only | A dark surround for the React demo. Deliberately **not** `.feat`, which is the page-level lead band and is limited to one per page. |

### Not defined by this source
Modals, toasts, pagination. Tables, form inputs, tabs, the labelled bar row and the stated-limits list are **extensions** — built strictly from the tokens and posture above, and labelled as such wherever they appear. `ui_kits/app/components.html` carries them with their provenance. The extensions exist because the target pages needed them; each one was added when a real page could not be built without it, never speculatively.

## 7. Motion & Interaction

One easing curve — `cubic-bezier(.2,.7,.2,1)` — for everything.

| Motion | Spec |
|---|---|
| Scroll reveal `.rv` | `opacity` + `14px` rise, `.65s`, IntersectionObserver at `threshold .15`, `rootMargin 0 0 -40px`, unobserved after firing |
| Count-up | `1000ms`, cubic ease-out, `toLocaleString` with the source's decimal precision, prefix/suffix from `data-` attributes, guarded by `dataset.done` |
| Highlighter swipe | `scaleX(0→1)` from left origin, `.95s`, `.3s` delay, on one word only |
| Split bar | width `0→53.7%/46.3%`, `1.1s`, `200ms` after the panel reveals |
| Live dot | 7px expanding ring, `2.6s`, **3 iterations then stops** — a signal, not a loop |
| Buttons / chips / rows / nav | `.18s` |
| Record-field isolate | `.25s` opacity + scale |

**Reduced motion is a first-class path, not a fallback.** `prefers-reduced-motion:reduce` zeroes all durations, forces `.rv` visible and the highlighter lit; the JS reads the same query and prints final count values instantly. Never ship a state that only exists after an animation.

**Interaction rules from the source:**
- Every interactive affordance works on hover, focus, **and** click/tap — the legend chips bind all five events for exactly this reason.
- Locked states expose `aria-pressed`; `Escape` unlocks.
- A skip link precedes everything; the nav drawer maintains `aria-expanded` / `aria-controls` and closes on link click.
- No `scrollIntoView` — anchor navigation uses `html{scroll-behavior:smooth}`.

## 8. Voice & Brand

The wordmark is `avonlake` + `.city` in serif, TLD tinted forest (volt on dark). Beside it, always, the disclaimer: *"Independent public-record project — Not affiliated with the City of Avon Lake."*

**Copy posture, observed:**

1. **A figure never travels alone.** Every number carries its register, its date, or its limitation: *"197 prototype case-term matches"* → *"A mention is a keyword match on a case term — not a verified substantive discussion or a live warehouse count."*
2. **Name the gap as loudly as the finding.** Method step 06 is *"What we can't show."* The safety file's stamp says `Records requested`, not "coming soon."
3. **Headlines state the record, not a verdict.** *"Four deeds that record no price, and a $15M public purchase."* — conjunction, not accusation. The page says it outright: *"it does not allege wrongdoing. You draw the conclusions."*
4. **Sentence case throughout.** Uppercase is reserved for the mono apparatus: eyebrows, stamps, buttons, section numbers, footer headings.
5. **Second person, present tense, no hype.** *"Start with a question you already have."* No "revolutionary", no "AI-powered", no growth-marketing verbs.
### Terminology — the product's own glossary

Re-measured against the linked pipeline (`lorain-data/CONTEXT.md`), which maintains a
closed vocabulary with explicit *avoid* lists. Where the published site and the pipeline
differ, both are correct for their surface — that boundary is itself the rule.

| Use | For | Never say |
|---|---|---|
| **Source** | One public dataset behind one adapter, identified by a `source_id` | feed, dataset, integration |
| **Register** | The same thing, *on the published page* — reader-facing language for a source | — |
| **Record** | One upserted row, always carrying an `external_id` and its payload lineage | entry, item |
| **Receipt** | The provenance block: source id · fetch date · record occurrence · content hash where available · counts · caveat line | citations, metadata |
| **Finding** | A derived anomaly row: rule + subject + period | alert, insight |
| **Governing body** | One deliberative public body with a stable `body_id`. *A source is not a body.* | board, committee (loosely) |
| **Case file** | One investigation's assembled cards and records | story, article |
| **Corroboration** | Independent evidence agreeing | confirmation, proof |
| **Identity grade** | `attested` / `identified` / `keyed` / `ambiguous` | **confidence** (it conflated two axes) |
| **Withdrawn** | Retired, but retaining identity and provenance for audit | deleted, stale |
| **Co-occurrence** | Two records mentioning the same term | connection, link, tie |
| **Map Room** | The private analytical explorer | dashboard, admin panel |
| **The Gazette** | The public, static, aggregate-by-default publication | portal, public site |

**"Receipt" is stricter than the design system originally implied.** It is not "a link to a
document" — it is source · date · count · caveat, and *a mutable record id alone is not a
receipt*. A `.src` line naming only the register is a citation, not a receipt. Build the
four slots or state which one is missing. See `IMPLEMENTATION.md` § Receipts.

**Two audiences, one voice.** The published site speaks to a **resident** arriving with one
question. The pipeline's Map Room serves a **trusted operator** doing evening research.
Same restraint, different density: the resident surface leads with the question, the
operator surface leads with the corpus. Neither gets marketing.

## 9. Anti-patterns

Do not, when generating with this system:

- **Add a second hue.** The palette is one green ramp + volt + amber. No blue links, no red errors, no purple gradients. Status is carried by the green/amber pair and by copy.
- **Spend volt outside its four roles.** One highlighted word, one volt figure per data block, one volt CTA per dark band, the footer TLD. Two volt figures in one ledger means neither is the story; a second volt CTA means neither is the action.
- **Put volt text on the light canvas.** It is L 0.969 — dark-only, or as a mark behind dark ink.
- **Set a figure in sans.** Headline numbers are serif + `tabular-nums`, always.
- **Use icons, emoji, illustration, or stock photography.** The source has none. Meaning comes from rules, grids, tabs, and real data. A `→` in mono is the only glyph affordance.
- **Round things into softness.** 8px cards, 4px controls, 2–3px data. The only 999px pill is the register legend chip.
- **Use neutral black shadows.** Elevation is green-tinted `rgba(18,36,26,…)`, two-layer.
- **Ship a claim without a `.src` line.** Every material figure needs a receipt and a stated limitation.
- **Invent a metric to fill a card.** The source ships `Records requested` where data is missing. Use an honest labelled placeholder.
- **Turn the record field into an outline chart.** Data encodes with fills.
- **Loop an attention animation.** The live dot pulses three times and stops.
- **Make a state reachable only through motion.** Reduced-motion users must land on the same page.
- **Blur the six registers into one narrative.** Resident asks, official statements, votes, authorization, outcome, and gaps stay separate — that separation is the product.
- **Drop the "not affiliated" disclaimer** from any masthead or footer.

### Anti-references, from the product itself

The linked project states what it must not resemble. These are stronger than style
preferences — each one names a way this kind of interface loses a reader's trust:

- **Dark SaaS dashboards, generic admin templates, card grids.** The `.doc` grid and the filing-tab card exist precisely so the page does not read as a template.
- **Big-number KPI theatre and decorative charts.** A figure earns its size by being the story, not by being a number. The record field is large because it *is* the corpus, not because big things look impressive.
- **Government, campaign or advocacy branding.** Anything implying official status or a predetermined conclusion. This is why the disclaimer travels with the wordmark and why headlines state conjunctions rather than verdicts.
- **Flattening distinct evidence states, hiding uncertainty, or separating a summary from its receipts.** The seven-state status vocabulary and the receipt contract exist to prevent exactly this.
- **Sparse consumer styling that makes dense research slower merely to appear simple.** Density is a feature here. Do not add whitespace at the cost of a reader having to page for a figure they were comparing.

## 10. Accessibility & responsive

**Target: WCAG 2.2 AA** — the linked product's stated bar, now this system's too.

| Requirement | How the system meets it |
|---|---|
| Body text ≥ 4.5:1 | Every `--ink*` role measured on `--canvas`, `--evidence` **and** `--evidence-inset`, in both themes. Lowest is 4.60:1 (dark `--ink-quiet` on the inset). |
| No colour-only meaning | Every stamp carries a **word**; the record field carries a full `aria-label` plus a `<noscript>` breakdown; the grade ramp is always labelled. |
| Visible focus | 2px `--focus`, 3px offset, swapped to `--volt` inside dark regions. Never removed. |
| Keyboard | Legend chips are real `<button>`s bound to click **and** focus; `Escape` clears a lock; the drawer maintains `aria-expanded` / `aria-controls`. |
| Target size | WCAG 2.2 AA asks **24×24 CSS px**; every control clears it comfortably. Buttons compute to **40px** tall (12px padding + an 11.5px mono line + 1px borders) and sit inside 44–52px rows, so the *touch surface* is 44px+ even though the control itself is 40px. The system is dense by design and the source's `12px 18px` button padding is preserved rather than inflated to hit 44. |
| Reduced motion | Every duration zeroed; the JS reads the same query and prints final values instantly. |
| Size floor | **10px.** Nothing below it. |

**`--rule` is a hairline, and there is now a token for when a border must carry meaning.**
`--rule` measures 1.29:1 (light) / 1.93:1 (dark) against the canvas — fine for organising a
page, not for saying *this is a control*. WCAG 2.2 AA asks 3:1 of a non-text UI boundary, so
**`--rule-strong`** (`--leaf`, 3.70:1 canvas / 3.99:1 evidence) now carries form-control edges
and the selected tab. Use it any time the border **is** the signal; use `--rule` when it is
only structure.

**390px is verified, not assumed.** The source's smallest breakpoint was 560px, so everything
between 390 and 560 was inherited rather than designed. `preview/narrow-390.html` renders every
surface in a 390px frame; it exposed three real failures (the record table pushed the page
sideways, panel rows crowded their figure, filing tabs could clip), all fixed at a new
**≤430px** breakpoint — the fifth, and the first one designed. The auditor fails the package if
that breakpoint disappears.

## 11. Working with this system

`IMPLEMENTATION.md` carries the wiring this document implies: cascade-layer setup, the
naming convention for new roles, theme persistence without a flash, the full status
mapping table, the porting rule for framework bindings ("emit class names and slots, never
inlined token values"), how to extend without breaking provenance, and a pre-ship
checklist.
