New. Codifies the stat-card composition shipped on /integrations/* and /industries/* during SOV R8. Builds on Material 3 type-scale ratios, IBM Carbon productive type sets, and the 4px grid established in foundations/spacing.md.
Stat Card — 3-Tier Spec
The stat card is the canonical Verdigris composition for numeric proof at hero scale. It presents one piece of evidence — a measured outcome from real Verdigris deployment — in three tiers: label (what is this), value (the number / phrase), sublabel (qualifier or unit context). All three are required for the card to read as evidence rather than ornament.
The SOV R8 pixel review flagged the previous rendering as “leading hard to read” because the three tiers were stacked without enough rhythm between them. This guide fixes that.
The three tiers
┌─────────────────────────────────────────┐
│ EARLY DETECTION │ ← label (caption, uppercase tracked)
│ │
│ 21 days │ ← value (metric.callout, bold tnum)
│ │
│ before first equipment alarm │ ← sublabel (body-small, neutral)
└─────────────────────────────────────────┘
| Tier | Role | Type token | Rendered (web) | Weight | Case |
|---|---|---|---|---|---|
| Label | Names the metric. 1–4 words. | caption (existing) |
0.875rem / 14px | 500 Inter | Sentence case (default) OR UPPERCASE tracking-0.10em when used as a quasi-eyebrow above a single-stat callout. |
| Value | The number or short phrase. | metric.callout (existing) |
1.5rem / 24px (mobile) → 1.875rem / 30px (sm+) | 700 Lato, font-variant-numeric: tabular-nums |
Sentence (the value’s natural case) |
| Sublabel | One short clause of context. Max 8 words. | body-small (existing) |
0.875rem / 14px | 400 Inter | Sentence |
All three sizes already exist in tokens/typography/scale.json. This rule fixes their vertical rhythm.
Vertical rhythm spec
Every gap is a multiple of the 4px base grid established in foundations/spacing.md. Inter-tier gaps follow Bringhurst’s principle that “space in typography is like time in music — a few proportional intervals are more useful than arbitrary quantities.”
| Element | Property | Value | Source |
|---|---|---|---|
| Card padding (inner) | padding |
1.5rem (24px, spacing.6) |
tokens/spacing/base.json |
| Card min-height | min-height |
8.75rem (140px) |
Enforces equal heights when label rows wrap |
| Label → value gap | margin-block-end on label |
0.5rem (8px, spacing.2) |
8px = 4-grid · 2; tight coupling reads as “the value belongs to this label” |
| Value → sublabel gap | margin-block-end on value |
0.5rem (8px, spacing.2) |
Same tight coupling; sublabel is qualifier, not separate |
| Label line-height | line-height |
1.4 | Caption tier — labels are short, need room for descenders |
| Value line-height | line-height |
1.1 | Display tier — large type compresses tightly |
| Sublabel line-height | line-height |
1.5 | Body-small tier — reads as a clause, needs reading rhythm |
Label min-height reserve |
min-height |
2.5rem (40px = 4-grid · 10) |
Reserves space for a hypothetical 2nd line so 1-line labels don’t drift the value baseline across cards in the same row. |
Why these specific gaps?
8px between every tier, not 12px or 16px. A larger gap between value and sublabel breaks the “this number means that” reading. Stat cards are read as a single visual unit; the three tiers must couple tightly. We compared:
- 4px: cramped, labels touch the value descenders.
- 8px: tight, three tiers read as one unit. ✓
- 12px: drifts; reader’s eye starts to wonder if sublabel belongs to the next card.
- 16px: broken — sublabel reads as separate content.
1.1 line-height on the value tier. The display tokens (tokens/typography/scale.json > lineHeight.tight) use 1.1 for H1; the value tier inherits this because it carries the same visual weight in the card. Tighter (1.0) clips ascenders/descenders on multi-line values like 21 days. Looser (1.2+) introduces visible internal leading that reads as separation.
1.4 line-height on labels. Labels are short (≤4 words) and uppercase, so the 1.6 body-line-height looks loose. 1.4 is the tracking-aware register for short uppercase tracked labels (matches Material 3 label-small spec and IBM Carbon productive label tier).
Compliant
<div class="vd-stat-card">
<p class="vd-stat-label">EARLY DETECTION</p>
<p class="vd-stat-value">21 days</p>
<p class="vd-stat-sublabel">before first equipment alarm</p>
</div>
With CSS:
.vd-stat-card {
padding: 1.5rem;
min-height: 8.75rem;
display: flex;
flex-direction: column;
}
.vd-stat-label {
font-family: var(--font-body);
font-weight: 500;
font-size: 0.875rem;
line-height: 1.4;
color: var(--vd-muted-fg);
min-height: 2.5rem;
margin-block-end: 0.5rem;
}
/* Quasi-eyebrow variant — single-stat callout where the label IS the eyebrow.
Use sparingly; multi-stat grids should keep sentence-case for readability. */
.vd-stat-label--eyebrow {
font-family: var(--font-display);
font-weight: 700;
font-size: 0.8125rem;
letter-spacing: 0.10em;
text-transform: uppercase;
}
.vd-stat-value {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.5rem;
line-height: 1.1;
font-variant-numeric: tabular-nums;
letter-spacing: -0.01em;
margin-block-end: 0.5rem;
/* No margin-block-start: auto. The label's min-height: 2.5rem already
* clamps the label slot, so every value starts at the same vertical
* position regardless of label line count. Using `auto` here would
* push the value to the card bottom and break the tight 0.5rem
* inter-tier gap rule. (Gemini review on PR #80.) */
}
.vd-stat-sublabel {
font-family: var(--font-body);
font-weight: 400;
font-size: 0.875rem;
line-height: 1.5;
color: var(--vd-muted-fg);
}
@media (min-width: 640px) {
.vd-stat-value { font-size: 1.875rem; }
}
Non-compliant
<!-- 1. Label and value with no sublabel -->
<div class="stat">
<p class="label">Early Detection</p>
<p class="value">21 days</p>
</div>
<!-- Two-tier composition. Reads as ornament, not evidence — no qualifier to ground the number in context. -->
<!-- 2. Sublabel grafted with body line-height -->
<div class="stat">
<p class="label">EARLY DETECTION</p>
<p class="value">21 days</p>
<p class="sublabel" style="line-height: 1.8; margin-top: 1.5rem">before first equipment alarm</p>
</div>
<!-- 24px gap + 1.8 line-height — sublabel drifts and reads as separate content. -->
<!-- 3. Value tier without tnum -->
<p class="value" style="font-variant-numeric: normal">$1.3M</p>
<!-- Proportional digits in value cards cause visual misalignment when stacked in a row of 3. -->
Where this applies
| Surface | Scope |
|---|---|
/integrations/* hero stats grid |
Required. Implemented in platform-page-layout.tsx. |
/industries/* hero stats grid |
Required. |
/platform/* hero stats grid |
Required. |
/hardware/* hero stats grid |
Required. |
| Whitepaper one-pager 3-up grid | Compatible — use metric.callout (24pt) per tokens/typography/scale.json. The 3-tier rhythm is web-scaled; the print equivalent is governed by categories/whitepapers/body.md. |
| Slide body-slide 3-up grid | Same — metric.callout at 24pt slide root. |
Sources
- Bringhurst, The Elements of Typographic Style, Chapter 2 “Rhythm & Proportion.” “Space in typography is like time in music — a few proportional intervals are more useful than a limitless choice of arbitrary quantities.” Cited via the 24ways “Compose to a vertical rhythm” article (which paraphrases Bringhurst’s chapter and is the most widely-cited web-typography application of his rhythm principle).
- Material Design 3 — Type scale tokens. Label-small spec: 11pt / 16px line-height / 0.5px tracking. The 11pt label / 16px lh ratio (1.45) maps to our 13px label / 18px lh (1.4) — same ratio register.
- IBM Carbon Design System — Productive type sets. Carbon’s “productive” tier (compact, data-heavy displays) uses 1.4 line-height on labels and 1.1 on display-numeric values; our rule matches this convention.
tokens/typography/scale.json—metric.callout(24pt) was previously specified for one-pager + body-slide 3-up grids; this rule extends the same token role to web stat cards.foundations/spacing.md— 4px base grid. Every gap value is on the grid.
See also
tokens/typography/scale.json>metric.callout,metric.deck,metric.anchortokens/spacing/base.json>spacing.2(8px),spacing.6(24px)rules/visual-rules.yml>composition.stat_card— machine-enforceable rulecategories/typography/eyebrow.md— eyebrow uses the same caption tier as the stat-card label