# SurveyStream — Typography System

**Version** 1.0.0 · **Scale** 1.200 minor third, 16 px base · **Families** Archivo · Inter · JetBrains Mono

The product has two readers who want opposite things. The **aggregator** stares at a
dense metric table for an hour and needs figures that never shift and a zero that can
never be an O. The **survey taker** wants a form that feels short and a payout that
feels like it arrived. One scale, one family set, two behaviours — separated by which
numeral treatment and which weight range each surface is allowed.

Every font claim below was verified by parsing the actual font binaries. Where a face
was rejected, the measured reason is given.

---

## 1. Font pairing

### 1.1 Open-source — the recommended stack

| Role | Face | Measured x-height | Variable axes | Why this one |
| --- | --- | --- | --- | --- |
| Display / wordmark | **Archivo** | 0.526 em | `wght` 100–900, `wdth` 62–125 | The only display candidate with a **width axis**. The wordmark can be optically widened (`wdth` 112) without commissioning a custom cut, and headings can narrow for dense panels. Carries `zero`, `case`, `lnum`, `onum`. |
| UI / body | **Inter** | **0.546 em** — highest measured | `opsz` 14–32, `wght` 100–900 | Tallest x-height of every candidate, and the only one with an **optical-size axis**, so 13 px captions and 40 px titles get different letterforms rather than a scaled one. Richest feature set found: `tnum pnum zero case calt ss01 ss02 ss03 cv01 frac salt subs sups dlig`. |
| Mono / telemetry | **JetBrains Mono** | 0.550 em | `wght` 100–800 | Near-identical x-height to Inter, so mono cells and body copy sit on the same optical line. Has `zero` (slashed) and `case`, and its measured x-height is within **0.6 %** of Menlo — the closest fallback match of any mono tested. |

Alternate display face if the wordmark needs more overt personality: **Gabarito**
(distinctive geometric curves, `ss01`/`ss02`). The trade-off is measured and real —
x-height drops to 0.488, weights start at 400, and it has **no `zero` and no `case`
feature**, so it cannot set an all-caps label or a disambiguated numeral.

### 1.2 Faces rejected, with the measured reason

These are all common choices for data products. Each was disqualified on evidence:

| Face | Finding | Consequence |
| --- | --- | --- |
| IBM Plex Sans | **No `tnum`.** Features present: `lnum onum zero ss01 ss02 ss03 frac salt subs sups` | Widely assumed to have tabular figures. It does not. Currency columns would shift as digits change — disqualifying for a payouts product. |
| Source Sans 3 | **No `tnum`.** | Same failure. `pnum` and `onum` only. |
| Martian Mono | **No `zero`.** | x-height 0.600 is the tallest measured and the `wdth` axis is attractive, but with no disambiguated zero it cannot carry IDs or hashes. Usable for tracked uppercase labels; not for data. |
| Roboto Mono | **No features of interest at all.** | No `zero`, no `case`, no `calt`. Nothing to configure. |
| Outfit | x-height 0.460 — lowest measured | Geometric and clean, but the low x-height costs legibility at caption sizes and makes the wordmark look small at a given point size. |

### 1.3 Premium commercial — the recommended stack

**Klim Type Foundry, single-family system:** **Söhne Breit** (wordmark and display),
**Söhne** (UI and body), **Söhne Mono** (telemetry).

The argument is not that Söhne is prettier. It is that one foundry means **one license
negotiation, one set of vertical metrics, and one figure width shared across all three
faces** — so a number in a mono telemetry column optically aligns with the same number
in body copy, which no cross-foundry pairing gives you for free. For a product whose
core screen is a table of numbers, that alignment is the whole game.

Alternate display face if the wordmark needs more geometry and character:
**GT Walsheim** (Grilli Type). Accepts the cross-foundry metric cost in exchange for a
far more distinctive wordmark.

> **Verify before purchase.** Unlike the open-source stack above, I could not parse these
> binaries — they are paywalled. Confirm `tnum`, `zero`, `case` and the figure-width
> behaviour on the foundry's trial fonts before committing. Do not take the feature list
> on trust, including from me.

---

## 2. Scale and modular ratio

**1.200 (minor third) from a 16 px base.** Leading is snapped to a 4 px grid; tracking is
systematic rather than eyeballed: `track(px) = 0.096398 − 0.033684 · ln(px)`,
which yields +0.010em at 13 px and −0.034em at 48 px, tightening
monotonically as size grows — the behaviour type wants, expressed as a rule instead of a
table of guesses.

| Token | px | rem | Line height | Ratio | Weight | Tracking | Role |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `caption` | 13 px | 0.8125 rem | 20 px | 1.538 | 500 | +0.010em | Metadata, table captions, helper text, timestamps |
| `body` | 16 px | 1 rem | 24 px | 1.5 | 400 | +0.003em | Default UI and reading size. Survey questions, forms |
| `body-lg` | 19 px | 1.1875 rem | 28 px | 1.474 | 400 | −0.003em | Intro paragraphs, taker-facing prompts, empty states |
| `title` | 23 px | 1.4375 rem | 32 px | 1.391 | 600 | −0.009em | Card titles, table group headers, modal titles |
| `h3` | 28 px | 1.75 rem | 36 px | 1.286 | 600 | −0.016em | Subsection headings inside dashboards |
| `h2` | 33 px | 2.0625 rem | 40 px | 1.212 | 600 | −0.021em | Section headings, panel headers |
| `h1` | 40 px | 2.5 rem | 44 px | 1.1 | 700 | −0.028em | Page titles |
| `display` | 48 px | 3 rem | 52 px | 1.083 | 700 | −0.034em | Marketing hero, payout confirmation moment, the wordmark |

### 2.1 Why 1.200 and not 1.250

Both were computed. The major third is the more dramatic scale and produces rounder
numbers, but it is wrong for this product:

| Token | 1.200 | 1.250 |
| --- | --- | --- |
| `caption` | 13 px | 13 px |
| `body` | 16 px | 16 px |
| `body-lg` | 19 px | 20 px |
| `title` | 23 px | 25 px |
| `h3` | 28 px | 31 px |
| `h2` | 33 px | 39 px |
| `h1` | 40 px | 49 px |
| `display` | 48 px | 61 px |

At 1.250 the mid-range opens up too far for a dashboard — a 25 px card title and a 31 px
subsection heading inside a dense metric panel read as marketing, not instrumentation. The
minor third keeps `title` at 23 px and `h3` at 28 px, which sit correctly above 16 px body
without shouting. Marketing drama is recovered by the off-scale `display-xl` below rather
than by stretching the whole product's scale to reach it.

### 2.2 Two off-scale exceptions, declared

A geometric scale from a 16 px base produces only **three sizes at or below 20 px**
(13 / 16 / 19). A dense aggregator table needs a fourth. Rather than pretend the ratio
covers it, the exception is named:

| Token | px | rem | Line height | Weight | Tracking | Why it exists |
| --- | --- | --- | --- | --- | --- | --- |
| `body-dense` | 14 px | 0.875 rem | 20 px | 400 | +0.006em | High-density data tables. **Off-scale.** The 1.200 ratio has no step between 13 and 16, and 13 px is too small for a value a client reads for an hour. |
| `display-xl` | 64 px | 4 rem | 68 px | 700 | −0.039em | Marketing hero only. **Off-scale.** Never inside the product. |

Both are flagged `$offScale: true` in `type-tokens.json` so a linter can enforce that they
are only used where documented.

---

## 3. Numerical data rules

This is where a typography system either earns its keep or fails silently.

### 3.1 Reward payouts — currency and tokens

```css
.ss-currency {
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum" 1, "lnum" 1, "zero" 0;
}
```

- **Tabular, always.** A balance that changes from `$1,199.00` to `$1,284.50` must not
  change width. Proportional figures make a live balance visibly twitch.
- **Slashed zero OFF.** This is the rule people get backwards. Inside money a slashed zero
  reads as a correction mark or a struck-through figure — `$1,0̸24.00` looks like an
  annotation, not a balance. The slash belongs to identifiers, not to value.
- **Always two decimals**, even at `.00`. A payout of `$40` shown next to `$39.50` breaks
  the decimal column.
- **True minus for debits:** U+2212 `−`, not a hyphen. A hyphen is narrower than a digit
  even in a tabular font, because `tnum` governs figures, not punctuation.
- **Right-align in tables.** Tabular figures give you decimal alignment for free only if
  the column is right-aligned.
- Token counts follow the same rules minus the decimals: tabular, lining, integer,
  thin-space thousands separators if the locale wants them.

The one place numerals may leave Inter: the **payout confirmation moment**, where the
amount is set in Archivo at `display` size. Archivo carries `tnum` and `zero`, so the rules
still hold. Nowhere else.

### 3.2 Percentage completion

```css
.ss-percent {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  min-width: 4ch; text-align: right; display: inline-block;
}
```

- **Zero decimals.** `68%`, never `68.4%`. A completion bar is a glance target.
- **Reserve 4ch.** The label must already be as wide as `100%` when it reads `7%`, or the
  bar and its label shuffle sideways as the survey fills. This is the single most common
  progress-bar defect and it is a typography bug, not a layout one.
- **No space before `%`** in English locales; the glyph is part of the number.
- Percentages are **not** currency: they take no lining override and no zero rule, because
  a percentage never contains a leading zero worth disambiguating.

### 3.3 Telemetry — IDs, hashes, timestamps

```css
.ss-telemetry {
  font-family: var(--ss-font-mono);
  font-variant-numeric: slashed-zero;
  font-feature-settings: "zero" 1, "calt" 0;
  letter-spacing: 0;
}
```

- **No `tnum` needed, and this is not an omission.** A monospace face is fixed-advance by
  construction — every glyph already occupies one cell, so `tnum` has nothing to do. That
  is why none of the six monos tested carries the feature. Specifying `tnum` on a mono is
  a no-op that signals the author did not know why they were asking for it.
- **Slashed zero ON.** The opposite of the currency rule. In `0OB3D0` the distinction is
  the entire point.
- **Contextual alternates OFF.** `calt` in JetBrains Mono drives code ligatures; in an ID or
  a hash you need the literal character sequence, not `!=` fused into `≠`.
- **Never add letter-spacing to mono data.** It breaks the column grid that makes a mono
  face worth using.
- **ISO-8601 for timestamps**, `2026-08-26T13:04:22Z`. Sortable as a string, unambiguous
  across locales, and fixed-width so it columnises.

---

## 4. The wordmark

### 4.1 Casing

The name is **`SurveyStream`** — one word, one medial capital, no space, no hyphen.

The medial capital is not decoration. It is the structural device that splits the name into
its two halves, which is the same two-audience idea the mark encodes. Lose it and the brand
reads as a single undifferentiated word.

| Form | Verdict | Note |
| --- | --- | --- |
| `SurveyStream` | **Correct** | The only form permitted in prose, UI, and the lockup. |
| `Surveystream` | Never | Destroys the split. The most common error in third-party copy. |
| `Survey Stream` | Never | Two words is a different name. |
| `SURVEYSTREAM` | Never | All-caps collapses the medial capital — the one thing carrying the structure. This holds **even inside an all-caps context**: in a tracked uppercase eyebrow or table header, the brand name stays sentence case as a deliberate exception. |
| `surveystream` | Technical only | Domains, package names, handles, CSS classes: `surveystream.com`, `@surveystream`. Never in prose. |
| `SURVEYSTREAM_API_KEY` | Permitted | Screaming snake case in code is a language convention, not typography. |

Enforce the uppercase exception in CSS rather than trusting authors:

```css
.ss-brand { text-transform: none !important; hyphens: none; white-space: nowrap; }
```

### 4.2 Setting the wordmark

- **Face and weight:** Archivo 700 at `wdth` 112. The widened axis gives the mark presence
  without a heavier weight, which would close the counters and fight the loop.
- **Tracking −0.04em** — tighter than the scale's −0.034em at that size. The capital S in the
  middle opens an optical gap that the systematic rule does not know about; this is a
  deliberate manual override, and the only one in the system.
- **Two-tone** (`Survey` in `text-primary`, `Stream` in `interactive`) is for the lockup only.
  In running copy the name takes the colour of the surrounding text.
- **Minimum size:** 20 px cap height, roughly 96 px overall width. Below that use the mark alone.
- **Never break across lines**, never hyphenate, never letter-space to fill a measure.

---

## 5. Fallbacks

### 5.1 Web stacks

```css
--ss-font-display: "Archivo", "Archivo Fallback", "Helvetica Neue", "Arial Nova",
                   Helvetica, Arial, system-ui, sans-serif;
--ss-font-body:    "Inter", "Inter Fallback", system-ui, -apple-system, "Segoe UI",
                   Roboto, "Helvetica Neue", Arial, sans-serif;
--ss-font-mono:    "JetBrains Mono", "JBM Fallback", ui-monospace, "SF Mono",
                   SFMono-Regular, Menlo, Consolas, monospace;
```

### 5.2 Metric-matched fallbacks — measured, not guessed

A fallback with a different x-height makes the page reflow when the webfont lands. The fix
is `size-adjust`, and the correct value is a ratio of measured x-heights. Both sides were
read out of the actual binaries:

| Webfont | x-height | Fallback | x-height | `size-adjust` |
| --- | --- | --- | --- | --- |
| Inter | 0.546 | Helvetica Neue | 0.517 | **105.6 %** |
| Inter | 0.546 | Helvetica | 0.523 | 104.4 % |
| Inter | 0.546 | Arial | 0.519 | 105.3 % |
| Archivo | 0.526 | Helvetica Neue | 0.517 | **101.7 %** |
| Archivo | 0.526 | Helvetica | 0.523 | 100.6 % |
| JetBrains Mono | 0.550 | Menlo | 0.547 | **100.6 %** |

Note what the mono row says: JetBrains Mono and Menlo are within 0.6 %, so the mono column
barely moves on swap even before the override. That is a reason to choose it.

Full `@font-face` overrides are in `type.css`.

### 5.3 Print

- **Static instances, not variable fonts.** Some RIPs mishandle variable axes and silently
  render the default instance. Export named static cuts for print artwork.
- **Use Inter's text optical size** (`opsz` 14) for print body. The display cut's tighter
  spacing is built for backlit screens and looks cramped in ink.
- **Minimum sizes:** 8 pt body, 6 pt caption/legal. Below 8 pt the high x-height stops
  helping and the counters fill in on uncoated stock.
- **No fallback chain in print** — fonts are embedded. Confirm embedding permissions are set
  in the PDF export; OFL permits it, and a premium license may not.
- Pair with the two-spot-ink stationery spec in `../color-system/COLOR-SYSTEM.md` §4.2.

### 5.4 Licensing

- **Open-source stack:** all three are OFL. Free for commercial use, web embedding, and PDF
  embedding; the reserved-name clause means don't ship a modified font under the same name.
- **Premium stack:** needs desktop licenses per seat, a web license usually metered by
  monthly pageviews, and a separate app license if the faces ship inside a native client.
  Budget for all three, and re-check the pageview tier before launch.

---

## 6. Files

| File | Purpose |
| --- | --- |
| `type-tokens.json` | DTCG tokens. Off-scale steps carry `$offScale: true` for linting. |
| `type.css` | Scale classes, numeral classes, metric-matched fallbacks, wordmark rules. |
| `typography.html` | Live specimen — the numeral rules are demonstrated, not described. |
| `TYPOGRAPHY.md` | This document. |

## 7. Open decisions

1. **Archivo or Gabarito for the wordmark.** Archivo is the systems answer (width axis,
   `zero`, `case`, one family for mark and headings). Gabarito is the branding answer
   (more distinctive curves) and measurably weaker equipment. This is a brand call, not a
   technical one.
2. **Open-source or premium.** The OSS stack is genuinely strong — Inter's optical-size axis
   is something several premium families don't offer. Premium buys distinctiveness and
   cross-family metric unity, and costs real money annually. Recommend shipping on the OSS
   stack and revisiting at the point the wordmark gets drawn as custom lettering anyway.
3. **`body-dense` at 14 px.** It solves a real density problem and it breaks the ratio.
   Confirm the aggregator dashboard actually needs it before it becomes the default that
   quietly replaces 16 px everywhere.
