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

1. No green, ever. The fern family is retired across every surface. Confirmation / success / live = the blue pair (--confirm, --confirm-soft). The contrast test fails any build that reintroduces a fern hex.

2. Stock shadcn, smallest delta. Reuse shadcn/ui components as-is; the system is a theme plus eleven extension tokens (--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.

3. Color choices are testable claims. Every pairing ships with a measured WCAG ratio asserted in 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.

The rule: wide web = a browser window >= 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.
VariantPhone (typography)Wide web (typographyWide)
display34 / 40, 700, -0.628 / 34, 700, -0.5
title27 / 33, 700, -0.422 / 28, 700, -0.3
heading20 / 26, 700, -0.218 / 24, 700, -0.2
subheading17 / 23, 600, -0.115 / 21, 600, -0.1
body17 / 23, 40015 / 21, 400
bodyStrong17 / 23, 60015 / 21, 600
callout15 / 22, 40014 / 21, 400
label13 / 18, 60012 / 17, 600
caption13 / 18, 40012 / 17, 400
eyebrow12 / 16, 600, +1.611 / 15, 600, +1.4

Columns read fontSize / lineHeight, weight, letterSpacing (omitted where zero).

Semantic rules

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.

TokenNativeWebWhat it sets
width300300Full rail width, padding included.
paddingVertical2020The rail's own padding, top and bottom.
paddingHorizontal1212The rail's own padding, each side.
rowGap22Between two nav rows.
rowPaddingVertical106One nav row's padding, above and below its label.
rowPaddingHorizontal1210One nav row's padding, each side.
glyphGap1210Glyph column to label.
rowRadius108The active capsule's corner.
glyphSlot2418The fixed glyph column, so every label starts on one x.
glyphSize2016The glyph drawn in that column.
labelSize1514Row label size.
labelLineHeight2120Row label line height.
headGap1010Portrait to name in the head.
headPaddingTop66Above the head, under the rail's own padding.
headPaddingBottom1818Under the head's actions, before the first row.
headPortrait108108Her portrait in the head, the door to About.
footPaddingVertical108The pinned foot (Settings), above and below.
footPaddingHorizontal1210The same foot, each side.
The active row is a soft capsule, not an edge bar. It is filled with 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.

TokenValueWhat it sets
layout.largeTitleTop58Top 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.borderWidth1 (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.borderToneruleWhich tone that hairline is drawn in. Never border, which is toned for the page ground and disappears on a card.
layout.card.elevation1The 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.secondaryBorderWidth1The secondary control is an OUTLINE in rule with an ink label, not a groundAlt fill at 1.05:1.
layout.control.disabledOpacity1The 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.inset21Inset from each screen edge — the phone bar is a floating capsule, not an edge-to-edge band.
layout.tabBar.height / radius52 / 26Capsule height (glyph + label) and its fully-rounded corner.
layout.tabBar.bottom / insetRelief28 / 6Gap 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.elevation2The shadow under the floating capsule.
layout.tabBar.activeCapsule50 × 30The soft capsule behind the selected tab's glyph — groundAlt light, surfaceRaised dark. Never confirm-blue, same reasoning as the rail.
The icon family is FILLED. The fourth ruling is not a number: the app's glyphs were hairline strokes where native used solid SF Symbols (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.
The stage rail is untouched by this pass — it is an iPad/desktop shape and was measured off the mockup, not the phone. The capsule geometry above is the phone bar only.

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