@arag 0.1.0

Typography tokens

Three system font stacks, a seven-step fluid scale whose slopes are calculated rather than guessed, four line heights sized for Cyrillic, and the small set of weight, tracking and numeral controls that everything else is built from.

Font stacks

Three tokens, and all three are system stacks. That is a decision, not an omission: zero network requests, zero flash of unstyled text, no font files to self-host, no licence to track, and nothing that can fail to load on a slow connection. Every face named in the stacks carries full Mongolian Cyrillic coverage, including ө and ү — the two characters that most Latin-first stacks fall back to a different face for, producing a visible mismatch mid-word.

The trade-off

A system stack renders differently on Windows, Android and iOS. The same heading is Segoe UI on a desktop, Roboto on a Pixel and San Francisco on an iPad, and their metrics do not match. For internal tooling and the kiosk app that is a fair price for the load behaviour. For anything print-adjacent — anything where a fixed line count or a fixed page break matters — check it on all three before signing off, because the wrap points will differ.

Sans

Everything by default: body copy, headings, controls, nav.

Create a new order

Шинэ захиалга үүсгэх

Payment details were updated successfully.

Төлбөрийн мэдээлэл амжилттай шинэчлэгдлээ.

Mono

Code, token names, identifiers, and anything that has to align by column.

Create a new order

Шинэ захиалга үүсгэх

Payment details were updated successfully.

Төлбөрийн мэдээлэл амжилттай шинэчлэгдлээ.

Display

Large headings. Currently an alias of the sans stack — a named seam, so a display face can be introduced later without touching call sites.

Create a new order

Шинэ захиалга үүсгэх

Payment details were updated successfully.

Төлбөрийн мэдээлэл амжилттай шинэчлэгдлээ.

The fluid scale

Seven steps, --text-xs through --text-3xl, each a single clamp(). The slopes are derived rather than eyeballed. For a 16px root, 1vi is V/1600 rem where V is the viewport inline size in pixels. Anchoring the minimum at 20rem (320px) and the maximum at 96rem (1536px) means the vi term has to cover the difference across 0.96 - 0.2 = 0.76:

slope     = (max - min) / 0.76
intercept = min - 0.2 * slope

/* --text-lg: min 1.125rem, max 1.375rem */
slope     = (1.375 - 1.125) / 0.76 = 0.3289
intercept = 1.125 - 0.2 * 0.3289   = 1.0592

--text-lg: clamp(1.125rem, 1.0592rem + 0.3289vi, 1.375rem);

The unit is vi rather than vw, so the scale stays correct if a consumer ever switches writing-mode.

If you retune

An earlier set of slopes topped out around 1230px instead of 1536px. Nothing looked broken — the type simply stopped growing two-thirds of the way to the --size-container-2xl ceiling, and every wide layout read a step too small. The two formulas are a pair: change either the minimum or the maximum and rerun both. Changing one anchor and leaving the slope alone is exactly how that bug happened.

Specimen

Latin on the left, Mongolian Cyrillic on the right, in equal columns at the step's own size. The length difference is the point: the same message runs roughly 15–25% longer, and it is a layout constraint, not a translation problem.

TokenDefinitionLatin / Cyrillic
--text-xs

Updated 2 minutes ago

2 минутын өмнө шинэчлэгдсэн

--text-sm

User settings

Хэрэглэгчийн тохиргоо

--text-base

Payment details were updated successfully.

Төлбөрийн мэдээлэл амжилттай шинэчлэгдлээ.

--text-lg

Create a new order

Шинэ захиалга үүсгэх

--text-xl

Account balance

Дансны үлдэгдэл

--text-2xl

Transfer confirmed

Гуйвуулга баталгаажлаа

--text-3xl

Welcome back

Дахин тавтай морил

Line height

Four steps. --leading-normal is deliberately more generous than a Latin-only system would choose, because Cyrillic sets taller at the same font size: the x-height is larger relative to the em, and descenders on у, р and ц reach further down. A leading that reads as comfortable in English closes up in Mongolian.

--leading-tight is for display headings only. It is safe on two or three words set large; it is not safe on running Cyrillic, where the descenders start touching the line below.

Each block below is the same pair of paragraphs at a narrow measure, so both wrap to several lines and the difference is visible rather than theoretical.

--leading-tight = — Display headings only. Do not put running Cyrillic on it.

Cyrillic sets taller than Latin at the same font size, so a line height that looks comfortable in English can feel cramped in Mongolian. Check the descenders against the next line.

Кирилл үсэг латинаас өндөр тогтдог тул англи хэл дээр тохиромжтой мөрийн зай монгол хэл дээр давчуу санагдаж болно. Доод мөртэй харьцуулж шалгана уу.

--leading-snug = — Sub-headings and short blocks that are read in one glance.

Cyrillic sets taller than Latin at the same font size, so a line height that looks comfortable in English can feel cramped in Mongolian. Check the descenders against the next line.

Кирилл үсэг латинаас өндөр тогтдог тул англи хэл дээр тохиромжтой мөрийн зай монгол хэл дээр давчуу санагдаж болно. Доод мөртэй харьцуулж шалгана уу.

--leading-normal = — Body default. Deliberately generous — see below.

Cyrillic sets taller than Latin at the same font size, so a line height that looks comfortable in English can feel cramped in Mongolian. Check the descenders against the next line.

Кирилл үсэг латинаас өндөр тогтдог тул англи хэл дээр тохиромжтой мөрийн зай монгол хэл дээр давчуу санагдаж болно. Доод мөртэй харьцуулж шалгана уу.

--leading-loose = — Long-form reading, and dense Cyrillic tables.

Cyrillic sets taller than Latin at the same font size, so a line height that looks comfortable in English can feel cramped in Mongolian. Check the descenders against the next line.

Кирилл үсэг латинаас өндөр тогтдог тул англи хэл дээр тохиромжтой мөрийн зай монгол хэл дээр давчуу санагдаж болно. Доод мөртэй харьцуулж шалгана уу.

TokenValueUse
--leading-tightDisplay headings only. Do not put running Cyrillic on it.
--leading-snugSub-headings and short blocks that are read in one glance.
--leading-normalBody default. Deliberately generous — see below.
--leading-looseLong-form reading, and dense Cyrillic tables.

Weight, tracking and numerals

Weight

Four steps, no more. The system stacks do not all ship every intermediate weight, and asking for one that does not exist gets you a synthesised face that looks wrong in Cyrillic before it looks wrong in Latin.

TokenValueLatinCyrillic
--weight-regularAccount balanceДансны үлдэгдэл
--weight-mediumAccount balanceДансны үлдэгдэл
--weight-semiboldAccount balanceДансны үлдэгдэл
--weight-boldAccount balanceДансны үлдэгдэл

Tracking

TokenValueLatinCyrillicUse
--tracking-tightCreate a new orderШинэ захиалга үүсгэхh1 and h2 only. Large type needs the correction; small type does not.
--tracking-normalCreate a new orderШинэ захиалга үүсгэхEverything else. An explicit token so a component can reset back to it.
--tracking-wideCreate a new orderШинэ захиалга үүсгэхSmall uppercase-ish labels and eyebrow text. Use it on Latin; on Cyrillic it widens an already longer string.

Tabular numerals

--font-numeric-tabular is a font-feature string, not a length: . tnum makes every digit the same advance width so columns line up; lnum forces lining figures so digits do not sit at different heights beside each other. Currency columns need both.

The same figures, right-aligned. Left column proportional, right column tabular.
WithoutWith
1,240,500 ₮1,240,500 ₮
98,013 ₮98,013 ₮
7,777,777 ₮7,777,777 ₮
111,111 ₮111,111 ₮
640,020 ₮640,020 ₮

src/base.css wires this up for you. Any cell marked data-numeric gets end alignment and both features, so a table author never writes the token by hand:

td[data-numeric],
th[data-numeric] {
  text-align: end;
  font-variant-numeric: tabular-nums;
  font-feature-settings: var(--font-numeric-tabular);
}
<table>
  <thead>
    <tr><th scope="col">Account</th><th scope="col" data-numeric>Balance</th></tr>
  </thead>
  <tbody>
    <tr><th scope="row">5401 2233</th><td data-numeric>1,240,500</td></tr>
  </tbody>
</table>

Underlines

Two tokens, applied by base.css to links.

TokenValueWhy
--text-underline-offset Em-relative, so it tracks the type scale instead of drifting at the large steps. A pixel offset that looks right at --text-sm is a hairline at --text-3xl. It is not zero because Cyrillic descenders reach further down than Latin ones: at zero offset the rule cuts through у and ц rather than passing under them.
--text-underline-thickness from-font defers to the face's own metric where one exists, which is what the Cyrillic faces in the stack want. Where none exists the browser falls back to its default, which is correct for that face.

How headings map

src/base.css assigns one scale step per heading level. Nothing else in the system sets a heading size, so this table is the whole mapping.

ElementSizeWeightLeadingTracking
h1--text-3xl--weight-bold--leading-tight--tracking-tight
h2--text-2xl--weight-semibold--leading-tight--tracking-tight
h3--text-xl--weight-semibold--leading-snug--tracking-normal
h4--text-lg--weight-semibold--leading-snug--tracking-normal
h5--text-base--weight-medium--leading-snug--tracking-normal
h6--text-sm--weight-medium--leading-snug--tracking-normal
Note

h6 is deliberately not uppercased. The usual small-caps or text-transform: uppercase treatment for the smallest heading costs nothing in Latin, but Mongolian Cyrillic uppercase runs materially wider than lowercase — on an element already sitting at --text-sm, that is exactly where a label starts wrapping or clipping. If you want the eyebrow look, reach for --tracking-wide instead and leave the case alone.