# Typography: agent spec

Twin of `/typography`. Source of truth: `app/assets/css/main.css`, `app/app.config.ts`.

## Typefaces

| Family | Token | Role | Weight | Files (`public/fonts/`) |
|---|---|---|---|---|
| Overused Grotesk | `--font-sans` | All reading text | 500 only (variable file, 100–900) | `OverusedGroteskRoman-VF.woff2`, `.ttf` fallback |
| CommitMono | `--font-mono` | Values (figures, hex, dates) and the Label role | 400 only | `CommitMono-400-Regular.woff2`, `.otf` fallback |

## Scale

Weight: `--text-weight: 500`, set on `body`. Each role has `--text-<role>-size`, `-leading`, `-tracking`.

| Role | Size | Leading | Tracking | Below `nav` |
|---|---|---|---|---|
| Display | 7.25rem (116) | 6.75rem (108) | -0.02em | 16vw / 15vw |
| H1 | 4.5rem (72) | 4.5rem (72) | -0.045em | 3.75rem / 3.75rem (60/60) |
| H2 | 3rem (48) | 2.875rem (46) | -0.035em | 2.1875rem / 2.0625rem (35/33) |
| H3 | 1.625rem (26) | 1.875rem (30) | -0.02em | unchanged |
| Body | 1rem (16) | 1.375rem (22) | -0.01em | unchanged |
| Badge | 0.75rem (12) | 1rem (16) | 0.025em, CommitMono 400, uppercase (`text-caption` at `UBadge` `md` size) | unchanged |
| Label | `text-caption` utility: `--text-label-size` 0.625rem (10), `--text-label-leading` 0.6875rem (11) | | 0.025em, CommitMono 400, uppercase | unchanged |

- `nav` is `--breakpoint-nav` (70.25rem, 1124px). The phone values sit in one `@media (width < 70.25rem)` block in `main.css`.
- No guide page uses Display. Every page title is H1, the overview (`/`) too. Display is set in the `/typography` specimen, `SiteHero` and the report hero.
- There is no Caption role. A caption is Body size, in the ink (`text-highlighted`), at 16/24 (the `body` leading, outside prose), left-aligned, `mt-3` under its subject (`TileFrame`'s `label`). Values, swatch captions and component names are 16/24 too.
- `--text-caption-size` / `-leading` (13/18) exist for the report only. The `text-caption` utility is the Label role.

## Applied mapping

| Element | Role | Set in |
|---|---|---|
| Page title (`title`, `h1`) | H1 | `app/pages/[...slug].vue`, `UPageHeader` `ui.title` |
| Page lead (`description`) | H3 from `nav` up, Body below, `text-highlighted`, `max-w-[40rem]`, `mt-6` (24) | `UPageHeader` `ui.description` |
| `##` (`h2`) | H2, `mt-(--prose-h2-above)` 6rem, `mb-(--prose-h2-below)` 2rem | `ui.prose.h2` |
| `###` (`h3`) | H3, 3rem above, 1rem below | `ui.prose.h3` (unused) |
| Body, list items | Body (16/22) | `ui.prose.p` / `li` |
| Captions, values, card names | 16/24 | `body` in `main.css` (no leading set: 1.5) |
| Badges | CommitMono 12/16, caps | `BrandBadge` (`UBadge` `md` size) |
| `**strong**` | Body, `font-medium text-highlighted` | `ui.prose.strong` |
| Values | Body size, `font-mono` | `ColourSwatch`, `PalettePair` |
| Sidebar nav item | Body, `--nav-pitch` (40) apart, current page `text-highlighted` | `ui.contentNavigation.link` |
| Foot bar (Search, `ThemeSwitch`) | Body | `[...slug].vue` footer |
| Menu item (below `nav`) | H3, `py-1.5` (42 apart) | `MobileBar.vue` |
| `Wordmark` in the foot bar and top bar | font-size H3 (26) | `[...slug].vue`, `MobileBar.vue` |
| Component card name (`h2`) | Body | `ComponentCard` |

## Report example (`fullBleed`)

- The report uses the same scale and weight as every page. Its hero title is H2, H1 from `sm`, Display from `nav`, never larger.
- `/report` renders `::report-section` titles and their body headings as peer `<h2>`. Do not demote body headings to `###`.
- Presentation mode (`ReportDeck.vue`) uses the same roles. Never step an element up a role on a slide. Continue a long section on the next slide (`paginate`); never shrink it.

## Rules

1. Set scale roles only through the `--text-<role>-*` properties or `::type-tile{size="<role>"}`. Never use a Tailwind size, leading or tracking class for a role.
2. Use one weight. Never use `font-bold`, `font-semibold` or `font-normal` on guide text. Show emphasis with `text-highlighted`.
3. A child heading is always smaller than its parent. Never skip a DOM heading level.
4. Prose `p` and `li` take `leading-(--text-body-leading)`. Do not use Nuxt UI's `leading-7`.
5. Keep body copy at 50–80 characters a line. Cap paragraphs at `--text-measure` (56ch, 521px). Cap `ul` / `ol` at `calc(var(--text-measure) + --spacing(6))`. `ch` is the advance of "0" (9.16px), not a character count.
6. Align text left. Never justify.
7. Use sentence case. Use caps only through `text-caption`.
8. Never add `tracking-*` by hand.
9. Serve both faces as `.woff2`. Regenerate with fontTools when a desktop file changes.
10. Never request a bold or medium CommitMono. `.font-mono` sets weight 400 in `main.css`. `BrandBadge` keeps `font-normal` because `UBadge`'s base is `font-medium`.
11. Set `antialiased` once, on `UApp` in `app.vue`.
12. Every page sets a frontmatter `description`. Do not repeat it in the body.
13. The wordmark is artwork. Never set "Motive Unknown" in a typeface for it (`/agent/logo.md`).

## Craft rules (`better-typography`)

- Wrap: `text-wrap: balance` on headings, `pretty` on short descriptions and body, `overflow-wrap: break-word` where a long token can appear, `nowrap` on labels and badges. Never use `balance` or `pretty` on long-form prose.
- Use `font-variant-numeric: tabular-nums` on a value that changes in place. Never use `font-feature-settings: "tnum"`.
- Use a CSS property before a variation tag (`font-weight`, `font-optical-sizing`).
- Line-height is unitless off the scale: about 1.1 on headings, 1.5–1.6 on body, never below 1.4 on text of three lines or more.
- Track small uppercase labels slightly positive.
- Truncate with `text-overflow: ellipsis` or `line-clamp`. Keep the full value reachable where it matters.
- Store copy in natural case. Let `text-transform` set caps.
- Punctuation: curly quotes in prose, straight quotes in code, en dash for a range (`2010–2020`), never an em dash, the single ellipsis character, `&nbsp;` between a value and its unit.
- Underlines: `text-underline-position: from-font`, `text-decoration-thickness: from-font`, `text-decoration-skip-ink: auto`. Animate only the colour.
- Inputs are 16px or larger below `sm`.
- Use `user-select: none` on a drag surface only.
- Set `lang` on the document. Set `dir` where direction changes, and wrap mixed-direction values in `<bdi>`.

## `TypeTile` / `::type-tile`

| Prop | Value |
|---|---|
| `size` | `display` \| `h1` \| `h2` \| `h3` \| `body` \| `badge` \| `label` (default `body`) |
| `mono` | boolean |
| `label` | caption from `nav` up (all widths without `phone-label`) |
| `phone-label` | caption below `nav` |

- The sample is `Aa`, or `0123` with `mono`. There is no slot.
- The sample is trimmed to its cap height and baseline (`text-box: trim-both cap alphabetic`) and sits bottom left in a 3:2 tile with `p-6`. So every tile has a 24px margin to the cap line and the baseline, at every size.
- Lay tiles out with `::grid{columns="type"}` (`repeat(auto-fill, minmax(12rem, 1fr))`). 12rem is the smallest tile that holds the Display sample inside its margins.
- `size="label"` and `size="badge"` render through `text-caption`.

## Related

- `/agent/layout.md`: spacing, measure, hit areas, focus.
- `/agent/motion.md`: motion tokens.
- `/agent/colour.md`: contrast floors. Never fix contrast by changing type.
