# Dashboard typography audit (W-mq1c8og40003a7cf)

This document is the pre-migration audit that justified the token set
now declared in `dashboard/styles.css` + `dashboard/slim/styles.css`.
See `dashboard/docs/typography.md` for the contract.

## Findings

### Baseline before this WI

| Surface                       | font-size declarations | Already on tokens?            |
| ----------------------------- | ---------------------: | ----------------------------- |
| `dashboard/styles.css` (main) | 179                    | ✅ all `var(--text-*)` except `.prd-item-id { font-size: 0.9em }` (slipped past PR #66 tripwire because the pattern was px-only) |
| `dashboard/layout.html`       | 20                     | ✅ all `var(--text-*)`        |
| `dashboard/pages/*.html`      | 32                     | ✅ all `var(--text-*)`        |
| `dashboard/js/*.js`           | ~530                   | ✅ all `var(--text-*)` except `utils.js:263` `font-size:0.9em` (same em-pattern blind spot as above) |
| `dashboard/slim/styles.css`   | 82                     | ❌ all literals (slim was explicitly out of scope for PR #66) |
| `dashboard/slim/body.html`    | 2                      | ❌ inline literals            |
| **Total**                     | **~845**               | **~84 raw / non-tokenized**   |

The main dashboard was already ~99% tokenized by PR #66
(`refactor(dashboard): anchor typography scale at 12px, migrate raw px
to tokens`). Two `0.9em` declarations slipped past its `\d+\s*px`
tripwire because the regex didn't catch the em form.

The slim UX prototype (`dashboard/slim/*`) was deliberately out of scope
for PR #66 — slim is a feature-flagged playground anchored at 16px
body, and its CSS is served from disk on every request so the human can
hand-edit it (`dashboard/slim/styles.css` header comment).

### Distinct font-size values seen across the audit

(Unique px values, sorted.)

| px | Count | Maps to            | Where                                                      |
| -: | -----:| ------------------ | ---------------------------------------------------------- |
| 10 | many  | `--text-xs`        | main dashboard chips/badges                                |
| 11 | many  | `--text-sm`        | main dashboard status badges                               |
| 12 | ~30   | `--text-base`      | main dashboard captions + most slim chips/labels           |
| 13 | ~26   | `--text-md`        | main dashboard default + slim default                      |
| 14 | ~10   | `--text-lg`        | main dashboard body + slim body                            |
| 15 | 1     | `--text-lg` (round −1) | slim `.topbar-title`                                   |
| 16 | ~7    | `--text-xl`        | main dashboard body, slim chat input + msg                 |
| 17 | 1     | `--text-2xl` (round +1) | slim `.agent-detail-name`                              |
| 18 | many  | `--text-2xl`       | main dashboard h2 / modal headers                          |
| 20 | 1     | `--text-2xl` (round −2) | slim `.icon-btn`                                       |
| 22 | many  | `--text-stat`      | main + slim stat readouts                                  |
| 24 | 1     | `--text-stat` (round −2) | slim `.member-emoji` (decorative emoji glyph)         |
| 28 | many  | `--text-stat-lg`   | main + slim large readouts                                 |
| 32 | many  | `--text-display`   | main dashboard hero / H1                                   |
| 34 | 1     | `--text-display` (round −2) | slim `.agent-detail-emoji` (decorative emoji)      |
| em |       |                    |                                                            |
| 0.9em | 3  | `--text-code`      | inline `<code>` in main + slim                             |

### Rounding decisions (5 sites)

The task contract says: *"prefer rounding to the nearest token over
inventing a new token. If rounding would visibly change the UI in a way
you can't justify, flag it in the PR description."*

All five rounded sites are within ±2px of their assigned token:

| Selector                              | Was | Now (token)        | Δ    | Justification                                                  |
| ------------------------------------- | --: | ------------------ | ---: | -------------------------------------------------------------- |
| `slim .topbar-title`                  | 15  | 14 `--text-lg`     | −1px | Topbar title — 1px reduction not perceptible at glance         |
| `slim .agent-detail-name`             | 17  | 18 `--text-2xl`    | +1px | Detail-modal heading — 1px increase matches modal-h3 elsewhere |
| `slim .icon-btn`                      | 20  | 18 `--text-2xl`    | −2px | Hamburger / close icon — emoji-like glyph                      |
| `slim .member-emoji`                  | 24  | 22 `--text-stat`   | −2px | Decorative emoji avatar                                        |
| `slim .agent-detail-emoji`            | 34  | 32 `--text-display`| −2px | Decorative emoji in detail modal                               |

Each rounded site has a `/* Rounded from Npx → Mpx … */` inline comment.
All five sit inside the feature-flagged slim UX, so the blast radius is
minimal even if the human decides to revert. If any rounding visibly
breaks the slim layout, define a one-off custom property scoped to that
selector and reference it instead of expanding the global token set.

### Tokens chosen — and rejected

**Chosen** (7 role tokens + 1 code token, all alias size primitives):

`--text-role-display`, `--text-heading`, `--text-subheading`,
`--text-body`, `--text-meta`, `--text-caption`, `--text-micro`,
`--text-code`.

**Rejected**:

- `--text-h1` / `--text-h2` / `--text-h3` — semantic HTML headings don't
  map 1:1 to visual roles in this dashboard (`<h1>` in `header` is 18px;
  PRD section h1s render at 32px). Role names beat heading names.
- `--text-label` — would overlap with `--text-caption`. Same role.
- `--text-large` / `--text-medium` / `--text-small` — pure-size names
  duplicate the primitives without adding semantics.
- Slim-specific tokens (e.g. `--text-slim-body: 15px`) — would let slim
  drift further from main. Better to round once and converge.
- A separate `--icon-*` family for the decorative emoji glyphs — only
  3 sites, all single-use, and they all happen to round cleanly to an
  existing primitive. Not worth a parallel token family yet.

### Out of scope (deliberately)

- font-weight, line-height, letter-spacing, color — explicitly out of
  scope per the task contract; this WI is size-only.
- font-family — no change.
- SVG `font-size='90'` inside the favicon `data:image/svg+xml` URL —
  data URLs can't resolve CSS variables. Allowlisted.
- The 4 `:root[data-font-size="…"] { --minions-font-scale: … }` rules
  declare the dashboard zoom scale; they're attribute selectors, not
  text-size declarations. Allowlisted.

## Regression guard

Extended `test/unit/dashboard-font-size-tokens.test.js`:

- Already covered: main `dashboard/styles.css`, `dashboard/layout.html`,
  `dashboard/pages/*.html`, `dashboard/js/*.js` for raw `Npx` font-sizes.
- New in this WI:
  - Extended file set to include `dashboard/slim/styles.css` and
    `dashboard/slim/body.html`.
  - Added pattern for `font-size: Nem|Nrem|N%` (catches the `0.9em`
    blind spot that hid the two pre-existing inline-code declarations).
  - Allowlisted the `--text-code: 0.9em` token declaration itself.
  - Asserted that slim's typography tokens match the main ones
    (drift-detection between the two stylesheets).

The test runs in `npm test` and `npm run test:sequential`. ESLint
remains scoped to XSS-prevention (`eslint.config.js`); we don't add a
second lint plugin for CSS literals because the pure-Node tripwire
already covers every file type.
