Typography
Senco uses the system font stack for fast rendering on all platforms — especially budget Android devices where custom web fonts add latency.
Typography in a money app has one job that ordinary product typography does not: a figure has to be unmistakable. A user reading "1,250.00" as "1,25000" is not a legibility inconvenience, it is a failed transfer. Most of the rules below trace back to that.
Font families
| Role | Font stack | Token | Usage |
|---|---|---|---|
| Sans (primary) | system-ui, -apple-system, "Segoe UI", sans-serif | --ds-font-sans | All UI text — headings, body, labels, buttons, amounts |
| Mono (code only) | ui-monospace, "SF Mono", "Cascadia Code", monospace | --ds-font-mono | Code blocks, tokens, developer-facing text |
| Documentation | Arial, Helvetica, sans-serif | — (see drift note) | Emails, PDFs, external documents, help articles |
Why system fonts
- Zero download cost — no web font requests, instant text render
- Native feel — text matches the OS, builds familiarity and trust
- Performance — critical for users on 3G / low-end devices
- Localization-ready — system fonts handle non-Latin scripts automatically
- Pipeline-compatible — the Figma MCP server does not return custom typefaces. A system stack is the one choice that survives design-to-code without manual repair.
The performance argument is not theoretical. A webfont is a render-blocking request on the critical path; on a metered 3G connection it delays the moment a user can read their balance. Senco's audience is defined by that connection.
Why Arial for documentation
- Universal availability — installed on every OS, renders in all email clients
- Consistent rendering — PDFs, print, and email look identical across devices
- Professional tone — clean, neutral, appropriate for financial documents
- Email-safe — no fallback surprises in transactional emails (receipts, confirmations)
system-ui is not available in email clients, and a receipt that renders in Times New Roman on one client and Helvetica on another undermines the one document a user is most likely to forward to their bank.
The two stacks never mix
In-product text is always --ds-font-sans. Exported artifacts — receipt PDF, confirmation email — are always the documentation stack. A receipt shown in the app uses the product stack; the same receipt emailed uses Arial. The switch happens at the export boundary, not per component.
--ds-font-sansStack: system-ui, -apple-system, "Segoe UI", sans-serifRole: All UI — headings, body, labels, buttons--ds-font-docStack: Arial, Helvetica, sans-serifRole: Emails, PDFs, help articles, external documentsClear heading hierarchy (H3, body, label) with distinct sizes and weights.
All text the same size with no hierarchy. Flat and hard to scan.
Type scale
Font: system-ui (--ds-font-sans) · Ratio: 1.200 (Minor Third) · Base: 16px · Grid: 4px line-height
Implemented size tokens
These are the values defined in ds.css. Every type role resolves to one of them.
| Token | rem | px | Role |
|---|---|---|---|
--ds-text-2xs | 0.625rem | 10px | Dense metadata, badge micro-text |
--ds-text-xs | 0.75rem | 12px | Captions, helper text, timestamps |
--ds-text-sm | 0.875rem | 14px | Labels, buttons, secondary body |
--ds-text-base | 1rem | 16px | Default reading text |
--ds-text-lg | 1.125rem | 18px | Lead text, H4 |
--ds-text-xl | 1.375rem | 22px | H3, card titles |
--ds-text-2xl | 1.75rem | 28px | H2, section headings |
--ds-text-3xl | 2.25rem | 36px | H1 (fixed) |
--ds-text-display | 2.5rem | 40px | Account balance, hero numbers |
--ds-text-page-title | clamp(1.75rem, 1.25rem + 1.5vw, 2.063rem) | 28→33px | Fluid page title |
Letter-spacing rule
Larger text needs tighter tracking; smaller text needs looser tracking for legibility.
| Size range | Letter-spacing | Why |
|---|---|---|
| 28px+ (Display, H1, H2) | -0.02em to -0.01em | System-ui splays at large sizes; tighten for cohesion |
| 16–23px (H3, H4, Body) | -0.01em to 0 | Neutral zone; system-ui optimized for this range |
| 10–14px (Body Sm, Caption, 2XS) | +0.005em to +0.02em | Counter-forms close up at small sizes; open for readability |
Only one tracking value is tokenized: --ds-tracking-tight (-0.02em), applied to Display and H1. The rest are applied per role.
Line-height rule
| Size range | Line-height ratio | Why |
|---|---|---|
| 28px+ (headings) | 1.2x–1.3x | Large headings need tight leading; too much air disconnects |
| 16–18px (body) | 1.5x | WCAG 1.4.12 minimum for sustained reading |
| 10–14px (small) | 1.5x | Small text needs generous leading, especially on low-DPI |
Headings
| Level | Size | Weight | Line height | Letter spacing | Usage |
|---|---|---|---|---|---|
| Display | --ds-text-display 40px | 700 | 1.2 (48px) | -0.02em | Account balance, hero numbers |
| H1 | --ds-text-page-title 28→33px | 700 | 1.21 | -0.02em | Page titles |
| H2 | --ds-text-2xl 28px | 700 | 1.29 (36px) | -0.015em | Section headings |
| H3 | --ds-text-xl 22px | 700 | 1.27 (28px) | -0.01em | Card titles, subsections |
| H4 | --ds-text-lg 18px | 600 | 1.33 (24px) | -0.005em | Minor headings, labels |
H2 and H3 use weight 700 rather than 600 so they qualify as WCAG "large text" (bold ≥ 18.66px / 14pt), which lowers their contrast requirement to 3:1. H4 at 18px/600 does not qualify — it is semibold, not bold — so H4 must clear the full 4.5:1. This is the single most commonly missed contrast rule in the system, because the heading looks large.
Body
| Level | Token | Weight | Line height | Letter spacing | Usage |
|---|---|---|---|---|---|
| body-lg | --ds-text-lg 18px | 400 | 1.56 (28px) | 0 | Emphasized body, lead text |
| body | --ds-text-base 16px | 400 | 1.5 (24px) | 0 | Default reading text |
| body-sm | --ds-text-sm 14px | 400 | 1.5 (21px) | 0.005em | Secondary info, metadata |
| caption | --ds-text-xs 12px | 500 | 1.5 (18px) | 0.01em | Timestamps, helper text |
| 2xs | --ds-text-2xs 10px | 500 | 1.5 (15px) | 0.02em | Badge micro-text, non-essential only |
10px text is capped at non-essential content by rule. Nothing a user must read to complete or verify a transfer may be set at --ds-text-2xs. If a fact matters, it gets 12px minimum.
Weight scale
| Weight | Token | Usage |
|---|---|---|
| 400 (Regular) | --ds-font-regular | Body text, descriptions, secondary content |
| 500 (Medium) | --ds-font-medium | Small-text reinforcement, labels, navigation |
| 600 (SemiBold) | --ds-font-semibold | H4, card titles, form labels, buttons |
| 700 (Bold) | --ds-font-bold | Display, H1–H3, account balances, primary emphasis |
Four weights, no more. System stacks do not carry a reliable 300 or 800 across platforms, and a weight that renders on macOS and silently snaps to 400 on Android is a design decision that only exists on the designer's machine.
Responsive scaling
Headings scale up on desktop. Body text stays fixed — the reading task is the same size on every device.
| Level | Mobile | Desktop |
|---|---|---|
| Display | 40px | 57px |
| H1 | 28px | 33px (via clamp) |
| H2 | 28px | 33px |
| H3 | 22px | 28px |
| H4 | 18px | 22px |
| Body and below | same | same |
H1 uses clamp() rather than a breakpoint so the page title scales continuously. Every other level steps at the breakpoint.
Do and don't
| Do | Don't |
|---|---|
| Set an amount at Display weight 700 | Set an amount in mono because it "looks financial" |
| Use H2 at 700 to earn the large-text contrast allowance | Use H4 at 600 and assume 3:1 is enough |
| Keep body at 16px on every device | Shrink body to 14px on mobile to fit more in |
Use --ds-text-2xs for a badge count | Use --ds-text-2xs for a fee disclosure |
| Let a long recipient name wrap to two lines | Truncate a recipient name without a title attribute |
The quick brown fox jumps over the lazy dog
1.125rem / 400 / 1.56The quick brown fox jumps over the lazy dog
1rem / 400 / 1.5The quick brown fox jumps over the lazy dog
0.875rem / 400 / 1.5The quick brown fox jumps over the lazy dog
0.75rem / 500 / 1.5The quick brown fox jumps over the lazy dog
0.688rem / 500 / 1.5Currency & numbers
Senco handles money — number rendering must be clear and consistent.
- Use
font-variant-numeric: tabular-numsfor aligned columns in transaction lists, exchange rates, and amount displays - Use the system sans-serif font (not monospace) for all financial data
- Account balances use Display size (40px) at weight 700
Why tabular, not monospace
Proportional numerals shift column alignment row to row, which makes a transaction list impossible to scan for an outlier. Monospace fixes alignment but reads as code and carries the wrong tone for a balance. tabular-nums gives fixed-width digits inside the system sans — alignment without the terminal aesthetic.
Amount composition
An amount is not one text run. It is composed so that magnitude dominates and denomination stays legible:
| Part | Size | Weight | Color |
|---|---|---|---|
| Integer | Display / H2 | 700 | --ds-heading |
| Decimal separator + cents | One step smaller | 700 | --ds-heading |
| Currency code | --ds-text-sm | 600 | --ds-muted |
| Conversion line | --ds-text-sm | 400 | --ds-muted |
Cents step down one size rather than changing color. Dimming the cents makes the amount read as rounded, which in a fee breakdown is exactly the wrong impression.
This composition applies to amounts that are displayed — balances, receipts, fee rows. An amount being typed into a field is a single run at the field's size, because an <input> renders one font size.
Formatting rules
- Group separators and decimal marks follow the recipient's locale on the receive side and the sender's on the send side. Both are labelled with the currency code so the two are never ambiguous.
- Currency code (
USD,NGN,PHP) always accompanies an amount. A bare number in a multi-currency product is a defect. - A displayed amount has a spoken form. The visible text keeps the compact code and is
aria-hidden; a visually hidden sibling carries the currency name, so a screen reader says "1,250.00 US Dollar" rather than spelling out "U S D".
<span aria-hidden="true">1,250.00 USD</span>
<span class="sr-only">1,250.00 US Dollar</span>
This applies only to amounts that are displayed. An amount inside an editable field is read as typed, and its currency is supplied through the field's description — see Amount input.
- Negative amounts use a leading minus and
--ds-error-text, never parentheses. Parentheses are an accounting convention that does not survive screen readers. - Exchange rates render at exactly 6 significant figures; amounts render at the currency's minor-unit precision (2 for USD, 0 for JPY). See Tokens.
- Times are absolute, 24-hour, in the user's local time zone. Never relative ("2 hours ago") — a relative time read later is wrong, and in a screenshot sent to support it cannot be answered.
- Same calendar day: time only —
10:42. Earlier in the current year:12 Mar, 10:42. Earlier years:12 Mar 2025, 10:42. Month names come from the locale throughIntl.DateTimeFormat, withhourCycle: "h23"fixed. - Status timestamps: a non-terminal status reads
{Status} since {time}—Pending since 10:42. A terminal status reads{Status} · {time}—Completed · 12 Mar, 10:42.
Tabular numerals keep columns aligned in transaction lists and amount displays.
Documentation typography
For emails, PDFs, and external documents, use Arial to ensure consistent rendering across all clients and devices.
| Level | Size | Weight | Usage |
|---|---|---|---|
| Doc heading | 24px | Bold (700) | Email subject, document title |
| Doc subheading | 18px | Semibold (600) | Section headers |
| Doc body | 14px | Regular (400) | Email body, document text |
| Doc caption | 12px | Regular (400) | Footer, legal, disclaimers |
Doc body is 14px where product body is 16px. Email and PDF are read at a closer viewing distance on a settled screen, not one-handed on a moving train.
Accessibility
- Heading hierarchy must be sequential — no skipping levels (h1 → h2 → h3)
- Body text line height minimum 1.5 for readability
- Minimum readable text size: 10px (
--ds-text-2xs) — weight 500, positive letter-spacing, non-essential content only - Text spacing must be adjustable without loss of content (WCAG 1.4.12)
- Support assistive technology navigation via semantic heading structure
- Contrast: 4.5:1 for normal text, 3:1 for large text (≥18.66px bold or ≥24px)
- Sizes are set in
rem, so browser and OS text-size settings scale the interface. No type token usespx. - Text must reflow without horizontal scrolling at 320px width and 200% zoom (WCAG 1.4.10)
Known gaps and drift
These are places where this spec and ds.css do not currently agree. They are listed rather than reconciled silently, because an agent generating code from this file needs to know which lines are contracts.
| Item | Spec says | ds.css has | Resolution |
|---|---|---|---|
| Documentation font | --ds-font-doc token | No such token | Add the token, or drop the token name from this spec — the Arial stack is currently applied by the export layer, not by a DS variable |
| Weight 300 | Previously documented for display numerals | No --ds-font-light token | Removed from this spec; system stacks do not carry 300 reliably |
| Smallest size | Previously 11px (0.688rem) | --ds-text-2xs is 10px | This spec now follows the token |
| H3 / H4 sizes | Previously 23px / 19px | --ds-text-xl 22px, --ds-text-lg 18px | This spec now follows the tokens |
| Line-height tokens | Ratios documented per role | No --ds-leading-* tokens exist | Ratios are applied per role in CSS; tokenizing them is open work |