Usage
How every Airmond surface consumes this system. The repo is marbling-hq/mb-airmond-brandkit; tokens/tokens.json is the single source, everything else is generated.
The three rules
--confirm, --confirm-soft). The contrast test fails any build that reintroduces a fern hex.--confirm, --confirm-soft, --waiting, --waiting-soft, --bubble-member, --coral-deep, --coral-soft, --coral-hover, and since 2026-09-14 --rule, --disabled-fill, --on-disabled). No custom component variants unless a surface genuinely cannot be built from stock. The 2026-09-02 landing recipes (the button's six states, the sign-up card, the live chip, the status pills, the screen vignette, the trust chips, the footer) are on the Components page; their motion is on Motion.build/contrast-test.mjs. Adding a color means adding its claim.Web (Tailwind v4 / shadcn)
import 'mb-airmond-brandkit/dist/css/airmond.css';
Defines :root (Feather) and .dark (Low flame) variables plus the Tailwind v4 @theme inline mapping, so bg-primary, text-confirm etc. resolve to the tokens.
Direct downloads: airmond.css · tokens.ts · tokens.json (the source of truth, claims included).
React Native (mb-airmond-app)
cp mb-airmond-brandkit/dist/ts/tokens.ts mb-airmond-app/src/theme/tokens.ts
Same export shape the app already consumes — a one-file swap plus the mechanical rename fern* → confirm* at call sites. onCoral is now ink (white on coral fails AA), and the dark scheme's coral is the dusty #E38273.
Type scale: phone/native vs. wide web
typography is generous on purpose — right at arm's length on a phone, and it matches the native apps. At a desktop window it reads oversized, so tokens.ts ships a second scale, typographyWide, one notch down with the same weights.
>= layout.stageMinWindowWidth (720px since 2026-09-23, was 1000 — the same threshold the navigation rail uses to switch to the stage layout). The web THREAD does not key off it: its message type is threadWeb.bodyType at every web width (chat density). Native apps and narrow/phone-width web ALWAYS use typography. Consumers switch on Platform.OS === 'web' && width >= layout.stageMinWindowWidth, never on device type.| Variant | Phone (typography) | Wide web (typographyWide) |
|---|---|---|
| display | 34 / 40, 700, -0.6 | 28 / 34, 700, -0.5 |
| title | 27 / 33, 700, -0.4 | 22 / 28, 700, -0.3 |
| heading | 20 / 26, 700, -0.2 | 18 / 24, 700, -0.2 |
| subheading | 17 / 23, 600, -0.1 | 15 / 21, 600, -0.1 |
| body | 17 / 23, 400 | 15 / 21, 400 |
| bodyStrong | 17 / 23, 600 | 15 / 21, 600 |
| callout | 15 / 22, 400 | 14 / 21, 400 |
| label | 13 / 18, 600 | 12 / 17, 600 |
| caption | 13 / 18, 400 | 12 / 17, 400 |
| eyebrow | 12 / 16, 600, +1.6 | 11 / 15, 600, +1.4 |
Columns read fontSize / lineHeight, weight, letterSpacing (omitted where zero).
Semantic rules
- Coral is used SPARINGLY — identity marks (sign-in plate, avatar fallback, priority pills, unread badge) and THE one primary action per screen (an armed send button, Join a call, New task). Coral as text on light uses
--coral-deep. Coral is never furniture — the app-wide interactive tint is ink. (Cédric, 2026-09-01.) - The dark blue is the WORKING accent (
--confirm: #3E5D86 light / #8FB2DC dark): active-state marks, focus rings, links, selected states — and confirmation / success / live. Never green. Exception, 2026-09-02: the navigation rail's selected row is NOT confirm-blue — see the rail section below. - Controls stay quiet until they can act: the chat send button renders as a muted wash until the member has typed; it takes coral only when there is something to send.
- Waiting-on-you = amber/gold; the one state allowed louder than the accent.
- Destructive = stock shadcn red — distinct from coral by depth; never coral.
- Member's own chat bubble =
--bubble-memberwith page ink text, both schemes. - Paired actions sit side-by-side, one control bar, same order on every screen: quiet/secondary left, primary or destructive right. Full-width stacked buttons only for a single action.
- Two portrait treatments, both named on the Imagery page: the app's cutout on the dusk-slate plate (
#2A3646), identical on every in-product surface; and the marketing roster portrait (sage wall, window light from the left, cream linen blazer, eyes on the 39% row). Never a per-screen plate. - The hovered primary is
--coral-hover(#FF7A6B light / #E98F80 dark) with ink type, and the focus ring is the one universal ring: 3px--confirmat 2px offset. Never coral on coral. - A mark that separates or encloses takes
--rule, and is measured on the surface it ACTUALLY sits on: a card's hairline, a control's outline, the thread's section and quote rules, the code plate's edge.--borderis toned for the page ground and measures 1.16:1 on a card and 1.21:1 inside a bubble, so it carries texture and never a boundary. The one exception is a component that draws something else at the body bar carrying the same meaning: the unread divider's rule is texture because its label reads at 4.97:1. (2026-09-14.) - A disabled control is a token pair, never a group opacity:
disabledFill+onDisabledat full strength, keeping its outline. It should look unavailable, not absent, becauseopacity: 0.5halves the label and the fill together and hides the copy explaining what enabling it would cost. faintis retired from member-facing text (3.08:1 light, 3.35:1 dark, 2.43:1 at its worst). Provenance, dates and timestamps aremuted.- His long message folds; hers never does (2026-09-26). Only the member's own text message folds: never her answer, never a bubble with an attachment, a card or a failed send, never one still streaming. It is decided from the RENDERED height, never a character count: past
thread.longMessage.capLines(14) lines of the body's own line height, and only when at least 3 more lines would hide. The last 2.5 lines fade into the bubble's OWN fill (no new colour); “Show more” / “Show less” with one chevron is the body's ink at 600 (13.82:1 / 8.14:1), a real button with its expanded state, and the full text stays in the tree. Web clips withoverflow: clip. See LongMessage. - A finished action card steps back (2026-09-26). While live nothing changes; once its work is finished (ended, failed, approved, dismissed, done) the TITLE stays
inkand every explanatory line takesmuted, while labels and controls keep their tone. Measured on the surfaces it is drawn on: 6.69:1 / 5.58:1 on the card, 6.11:1 / 4.87:1 on its inset, all claimed. The quiet lines never leave the card for the call's bubble (4.05:1 there in Low flame). See A finished action card.
The navigation rail
On a regular-width iPad and on a browser window at or past layout.stageMinWindowWidth (720px on the web since 2026-09-23), navigation is a left rail instead of a bottom tab bar. Every number was measured off the approved “Airmond One” mockup's .rail frame and ships as layout.rail in dist/ts/tokens.ts; the web draws the same rail with compact rows under the same hero head, as railWeb (layoutWeb.rail). Spacing is the first thing judged here, so the rail is tokens, never literals at the call site. The table is generated from density.rail in tokens.json; the Mac's column is on the Components page.
| Token | Native | Web | What it sets |
|---|---|---|---|
width | 300 | 300 | Full rail width, padding included. |
paddingVertical | 20 | 20 | The rail's own padding, top and bottom. |
paddingHorizontal | 12 | 12 | The rail's own padding, each side. |
rowGap | 2 | 2 | Between two nav rows. |
rowPaddingVertical | 10 | 6 | One nav row's padding, above and below its label. |
rowPaddingHorizontal | 12 | 10 | One nav row's padding, each side. |
glyphGap | 12 | 10 | Glyph column to label. |
rowRadius | 10 | 8 | The active capsule's corner. |
glyphSlot | 24 | 18 | The fixed glyph column, so every label starts on one x. |
glyphSize | 20 | 16 | The glyph drawn in that column. |
labelSize | 15 | 14 | Row label size. |
labelLineHeight | 21 | 20 | Row label line height. |
headGap | 10 | 10 | Portrait to name in the head. |
headPaddingTop | 6 | 6 | Above the head, under the rail's own padding. |
headPaddingBottom | 18 | 18 | Under the head's actions, before the first row. |
headPortrait | 108 | 108 | Her portrait in the head, the door to About. |
footPaddingVertical | 10 | 8 | The pinned foot (Settings), above and below. |
footPaddingHorizontal | 12 | 10 | The same foot, each side. |
sidebarAccent (#EAF0F4 light / #2A3646 dark — the RN mirror of the web --sidebar-accent), the same treatment as the phone tab bar's active capsule. Two things it is deliberately not: not confirmSoft, because confirm-blue means “confirmed / succeeded / live” and a blue-washed row reads as a status rather than a location; and not a 3px bar hung at the row's trailing edge — that mark sat against the rail's own right border and read as an accident of the border rather than a mark on the row (Cédric, on the live app, 2026-09-02). The glyph and label carry ink when selected and muted at rest; coral appears on the rail only as the unread dot.Chrome: native-matched on 2026-09-02
Cédric put build 10 beside the retired native iOS app's own captures and picked the native look on four points. The three that are geometry now ship as tokens, so no surface re-derives them at a call site.
| Token | Value | What it sets |
|---|---|---|
layout.largeTitleTop | 58 | Top padding of a large title's own container, under the safe-area inset. Native's title cap top sits at 128 absolute on a 402pt iPhone; the app had drifted to an ad-hoc 8, putting every large title 50pt too high. |
layout.card.borderWidth | 1 (was 0 from 2026-09-02 to 2026-09-14) | Reversed on 2026-09-14. The 2026-09-02 ruling removed a hairline that measured 1.16:1 against the ground and read as a wireframe, and left the card separated by a 6% shadow at 1.05:1 light and 1.12:1 dark, which is a third of what a shape needs. The line that came back is a different line: rule, at 3.20:1 and 3.92:1. Its source is control.card.borderWidth in tokens.json; set that to 0 and the line goes from this guide and from every card in the app in one edit, with no call-site change. See the four controls. |
layout.card.borderTone | rule | Which tone that hairline is drawn in. Never border, which is toned for the page ground and disappears on a card. |
layout.card.elevation | 1 | The soft, wide shadow under the card. On the dark scheme a shadow cannot read, so the lift is tone instead — surface (#1F2833) on ground (#171E2A) — and the hairline is what carries the boundary in both. |
layout.control.secondaryBorderWidth | 1 | The secondary control is an OUTLINE in rule with an ink label, not a groundAlt fill at 1.05:1. |
layout.control.disabledOpacity | 1 | The retirement of opacity: 0.5, as a value rather than a rule to remember. A disabled control is disabledFill + onDisabled at full strength, keeping its outline. |
layout.tabBar.inset | 21 | Inset from each screen edge — the phone bar is a floating capsule, not an edge-to-edge band. |
layout.tabBar.height / radius | 52 / 26 | Capsule height (glyph + label) and its fully-rounded corner. |
layout.tabBar.bottom / insetRelief | 28 / 6 | Gap to the screen's bottom edge. On iOS this sits inside the 34pt home-indicator inset, exactly as native does; consumers lift it for a larger inset with Math.max(insets.bottom - insetRelief, bottom). |
layout.tabBar.elevation | 2 | The shadow under the floating capsule. |
layout.tabBar.activeCapsule | 50 × 30 | The soft capsule behind the selected tab's glyph — groundAlt light, surfaceRaised dark. Never confirm-blue, same reasoning as the rail. |
bubble.left.fill, person.fill, phone.fill, gearshape.fill, book.fill…), and at a row's 24pt they read as roughly 40% less ink than the bar they copy. Icons are drawn as fill paths on a shared 24-grid, not pulled from a pack; hairline internal detail (a calendar's grid, a checklist's ticks) stays as a stroke cut out of the fill. A new glyph joins the family filled, or it does not join it.Regenerating assets
npm run build # tokens -> dist/css + dist/ts
npm test # + the contrast test (every claim re-measured)
npm run icons # app icon SVG masters -> dist/icons PNGs
# kit pipeline (gtm/brand-asset-kit), then the site:
S=~/.claude/skills/brand-asset-kit
python3 $S/build_kit.py brand/brand.airmond.json --out dist/kit
python3 $S/build_kit.py brand/brand.airmond.json --out dist/kit --audit
python3 $S/build_applications.py brand/brand.airmond.json --kit dist/kit
python3 $S/build_guide.py brand/brand.airmond.json --kit dist/kit
npm run site # composes dist/site (this page)
CI/CD for consuming repos
- PR-only merges, never a direct push to main. Verify green by counting the checks by name — no branch protection exists on the free plan, so green is a step you perform.
ubuntu-latestis fine in themarbling-hqorg (the self-hosted-only rule is the code-and-state org's budget rule).- This repo's CI: PRs run the contrast test, token build (checked against the committed
dist/css+dist/ts), and site build; main additionally deploys to theairmond-brandkitVercel project and curl-verifies the live nav. - Consumers should pin a commit and re-run their own visual checks when bumping.