Skip to content
MonogramDesign System
v1.0

Monogram Design System

The visual language behind Monogram AI — read out of the shipped iOS binary rather than off the screen. Twenty-eight semantic tokens with no accent among them, system SF Pro, flat cards that separate on tone alone, and a fixed catalog of ~38 components the model composes from. Every value here carries the class of evidence it came from, because the colors were extracted and the measurements were not.

Tokens

Color

Twenty-eight semantic tokens, each carrying a light and a dark value, read directly out of the app’s compiled asset catalog. Components reference content/primary and surface/secondary, never raw hex — which is the mechanism by which a screen the model composed five seconds ago is automatically correct in both appearances. There is no accent token. Nothing saturated, across all 28. Precisely 1 carries any chroma at all — surface/footer (#F5F4F0), a warm tint for the what's-next and cheat-sheet bands — the only token that isn't neutral, and nowhere near saturated. So “no accent” is the true claim and “fully achromatic” would be the overclaim. Either way, color in Monogram comes from content.

BinaryRead from the app's compiled asset catalog. Authoritative.
SymbolsRead from linked frameworks and extracted symbol names.
MeasuredPixel- or frame-measured from screen recordings. Best available evidence — the real token was stripped from the release build.
Site CSSFrom monogram.ai's compiled web CSS. True of the website; only true of the app where the app corroborates it.
DerivedA rule inferred from the evidence. Mine, not theirs.

Surface — light / dark · the canvas and everything on it

surface/primary#FFFFFF#000000the canvas an answer renders onBinary
surface/secondary#F5F5F5#141414entity + ingredient cards — flat, no shadowBinary
surface/secondary-muted#F8F8F8#121212Binary
surface/tertiary#EEEEEE#1A1A1ABinary
surface/footer#F5F4F0#141414the one warm tint — what's-next and cheat-sheet bandsBinary
surface/elevated-sectionrgba(0,0,0,0.01)rgba(255,255,255,0.01)a 1% wash — elevation without a shadowBinary
surface/inverse#1F1F1F#EDEDEDBinary
surface/monogram-bar#1F1F1F#EDEDEDthe mic / input bar — not pure blackBinary
surface/selected#1F1F1F#EDEDEDBinary
surface/overlayrgba(0,0,0,0.02)rgba(255,255,255,0.02)Binary

Content — light / dark · every mark that carries meaning

content/primary#1F1F1F#EDEDEDledes, headers, card namesBinary
content/primary-muted#4A4A4A#B8B8B8Binary
content/secondary#6B6B6B#949494captions — 5.33:1, the safe body inkBinary
content/secondary-muted#AAAAAA#777777subtitles, rationale copy — 2.32:1, fails AABinary
content/tertiary#C7C7C7#555555inactive step numbers — 1.69:1, fails AABinary
content/disabledrgba(0,0,0,0.25)rgba(255,255,255,0.25)Binary
content/inverse#FFFFFF#000000Binary
content/on-image#FFFFFF#EDEDEDtext over photography and 3D assetsBinary
content/monogram-bar#FFFFFF#000000the glyphs inside the input barBinary
content/overlayrgba(0,0,0,0.02)rgba(255,255,255,0.02)Binary

Two of those fail WCAG AA as text on the light canvas and the table says so: content/tertiary measures 1.69:1 and content/secondary-muted 2.32:1. They are real tokens and this is real system data, so they are documented rather than quietly corrected — but anything that has to be read should step up to content/secondary at 5.33:1. This page renders its own tertiary UI text at #757575 (4.61:1) for exactly that reason. Documented value is not always rendered ink.

Border — light / dark · eight edge tokens, which is how a flat system builds depth

border/default#E5E5E5#262626Binary
border/subtlergba(0,0,0,0.07)rgba(255,255,255,0.10)the workhorse hairlineBinary
border/strong#D8D8D8#3A3A3ABinary
border/input#D8D8D8#3A3A3Aresting state of an inputBinary
border/input-active#1F1F1F#EDEDEDan input under the fingerBinary
border/selected#C7C7C7#555555Binary
border/spotlight#1F1F1F#EDEDEDthe token Live Parameters binds its affordance toBinary
border/inverse#FFFFFF#000000Binary

Color that isn’t in the system · and why that is consistent, not contradictory

Saturated color does reach the screen — orange map pins, green and red judgment pills in the comparison flow. None of it is a token, which is precisely why it can be vivid without breaking the rule. The claim is about the chrome, and stating the exception is what makes it true.

map pin#FE6E00Not in the token system. It appears in the web CSS as an orange primitive, and in the app it arrives either at the content level or from MapKit — which of the two was never established, so the only claim worth making is that it is not a token.
judgment green#1F7A3C on #E8F6ECContent-level semantics — a verdict about the data, not a chrome color.
judgment red#9F2F2E on #FDECECContent-level semantics, paired with judgment green.

Corrections the binary forced · kept visible rather than quietly folded in

content/primary
#141414#1F1F1F
A codec-darkened pixel. Video compression pushes near-black darker.
surface/footer
#F2EFEC#F5F4F0
The warm band is lighter and less saturated than it measured.
surface/monogram-bar
#0A0A0A#1F1F1F
The input bar is not pure black — it is the same ink as primary content.
/* Usage — resolves per appearance, no hand-branching */
color: var(--content-primary);         /* #1F1F1F · light  |  #EDEDED · dark */
background: var(--surface-secondary);  /* #F5F5F5 · light  |  #141414 · dark */
border: 0.5px solid var(--border-subtle);

/* The affordance for an editable value. In-system by rule. */
border-bottom: 1.5px solid var(--border-spotlight);
/* NOT an invented accent — the system has no accent to invent from. */
Tokens

Typography

The app is system SF Pro, and the proof is an absence: there is no UIAppFonts key in the plist, and the only bundled face is a Latin Modern math font for rendering equations. So the Inter on monogram.ai is web-only. This also resolves something easy to misread — the in-app tracking looks like SF metrics because it is SF.

Type scale · measured in the 402pt frame, not extracted

detail-title28/32 · 700 · content/primary
Chewy, not cakey — brown sugar and a cold rest
lede20/26 · 600 · content/primary
Chewy, not cakey — brown sugar and a cold rest
section-header20/24 · 700 · content/primary
Chewy, not cakey — brown sugar and a cold rest
section-subtitle15/20 · 400 · content/secondary-muted
Chewy, not cakey — brown sugar and a cold rest
reason-claim15/20 · 650 · content/primary
Chewy, not cakey — brown sugar and a cold rest
reason-body14/19 · 400 · content/secondary-muted
Chewy, not cakey — brown sugar and a cold rest
card-name13.5/17 · 650 · content/primary
Chewy, not cakey — brown sugar and a cold rest
card-caption12.5/16 · 400 · content/secondary
Chewy, not cakey — brown sugar and a cold rest
pill-label12.5 · 500 · contextual
Chewy, not cakey — brown sugar and a cold rest

Weights run 400, 500, 600, 650, 700 — the 650 step is real and recurs, which is what a variable face buys over a four-cut family. The color column maps the measurement notes’ legacy ink ladder onto the binary’s token names (ink-2content/secondary, ink-3content/secondary-muted). Easy to get wrong: a first pass promoted both ink-3 roles two steps up the ladder.

Families

app-apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif
app — whySystem SF Pro. There is no `UIAppFonts` key in the plist and the only bundled face renders equations, so the app has no custom font at all. Rendered through the system stack — `system-ui` resolves more accurately than any self-hosted cut, and self-hosting SF Pro is licensed against.
web only"inter-variable", Inter, system-ui, sans-serif
bundled facelatinmodern-math.otfThe one bundled face, for equation rendering. Not UI type.
tracking, UI-0.32px / -0.41pxiOS's own SF tracking at phone-UI sizes. These are absolute, so the em equivalent moves with size — roughly -0.019em at 17pt and -0.027em at 15pt. Shipped verbatim on the web build too, which is part of why the site reads as an app. Site CSS
tracking, display-0.02em / -0.05em
/* The whole type system, in two lines. */
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui;
letter-spacing: -0.019em;   /* ≈ iOS's -0.32px at 17pt; the app's values are absolute */

/* Do not self-host SF Pro. system-ui resolves more accurately, and the
   licence does not permit it. */
Tokens

Spacing & Layout

Everything here is a measurement, not a token. Monogram’s numeric spacing, radius, and type-scale constants are compiled Swift values and are stripped from the release binary — a DesignTokens.Radius type exists, but its numbers do not survive as data. So these came off screen recordings at 804×1748, and the page labels them accordingly. It is the one place where this documentation is weaker than it looks.

Content margin · 32pt
Section rhythm · 40pt
Header → subtitle · 6pt
Subtitle → content · 16pt

Layout constants

Frame
402 × 874pt
recordings are 804×1748 @2x
Content margin
32pt
lede left edge at 65px / 2
Section rhythm
40pt
end of a block to the next header
Header → subtitle
6pt
Subtitle → content
16pt
Entity grid
3 columns, ~14pt gutter
→ 103pt cards
Card art box
~104pt tall
the 3D asset sits ~61pt inside it
First content below chrome
~143pt from screen top
Step card
240pt wide, swipeable

The steps that recur in Monogram’s own chrome are 6 / 16 / 32 / 40pt. Note what is not in that list: the 8pt and 9pt steps quoted elsewhere in this documentation are the parameter dock’s inter-card gap and interior spacing. Those are mine, not measurements of the app, and folding them into a list of the app’s rhythm would be the quiet kind of overclaim.

/* The rhythm that recurs everywhere in an answer */
padding-inline: 32pt;   /* content margin */
--section-rhythm: 40pt; /* block end → next header */
--header-to-sub: 6pt;
--sub-to-content: 16pt;
Tokens

Radius

A five-step ladder: buttons at 14, cards at 18, things that hover at 22, a modal sheet at 28, and anything fully interactive as a pill. It is tempting to read altitude into that — rounder the higher it floats — but the system’s own data refuses it. The POI overlay carries the heaviest shadow in the product at 22 while a flat sheet sits at 28, and the shadowless entity card at 18 is rounder than the equally flat action button at 14. The ladder tracks component family, not height. The parameter dock sits at 14, the button step — which is the first structural hint that it is chrome rather than content.

button14ptaction buttons — 44pt tall, icon + label. Also the dock card.
card18ptentity + ingredient cards, what's-next rows
floating22ptstep cards, POI overlays
sheet28ptmodal sheets — top corners only
pill999ptchips, judgment pills, the top-actions capsule, the mic
border-radius: 18pt;  /* entity card — the most-used surface in the product */
border-radius: 22pt;  /* step card, POI overlay */
border-radius: 28pt;  /* sheet, top corners only */
border-radius: 999pt; /* chips, judgment pills, the top-actions capsule */
Tokens

Elevation & Material

The entity card — the surface an answer uses more than any other — has no shadow and no border. It separates from the canvas on tone alone. Shadow is spent only on things that genuinely float: the top-actions capsule, a step card lifted off the warm band, an overlay above a map — 4 in total, and the heaviest of them belongs to the mic ring rather than to anything inside an answer. And there is a real material in the system, not just a gradient: iOS 26 glass on the bottom bar, so content ghosts through it as it scrolls underneath.

flat cardno shadow, no border — tone does the worksurface/secondary · none
washa 1% overlay; elevation without a shadowsurface/elevated-section · none
capsulethe top-actions capsule, and the dock card at restsurface/primary · 0 2px 10px rgba(30,25,15,0.08)
step cardwhite card on the warm bandsurface/primary · 0 4px 18px rgba(0,0,0,0.06)
mic ringcast off the white ring, not the black pill inside itsurface/monogram-bar · 0 6px 36px rgba(10,10,10,0.32)
overlayPOI / detail overlay above the mapsurface/primary · 0 12px 40px rgba(0,0,0,0.18)

One caveat on the hairline this page draws every surface with. Its width is not in any source: border/subtle is a color token with no width, and the only stroke width recovered from the prototype is the dock’s 0.8pt sheen. 0.5px is a reasonable read of a hairline on a 2× display and it is what renders here, but it is mine rather than theirs. Derived

Material & hardware

glassSwiftUI.GlassEffectContainer on the bottom input barcontent ghosts through the bar as it scrolls beneath Symbols
glass, replicatedThe prototype couldn’t build against the SDK that ships the native effect, so it reproduces the read with a thin material at 50% over a five-stop gradient across 168pt — an alpha ladder measured off a device frame (≈0.27 at 787pt, 0.57 at 811, ~0.82 at the edge). Frosted and content-ghosting, never a white wall.
sheet grabber36 × 4.5pt · rgba(0,0,0,0.08) Measured
/* Depth = tone + a hairline. Not a shadow. */
background: var(--surface-secondary);
box-shadow: none;
border: none;

/* All four shadows in the system, and what each is for */
box-shadow: 0 2px 10px rgba(30,25,15,0.08);  /* capsule, dock card */
box-shadow: 0 4px 18px rgba(0,0,0,0.06);     /* step card on the warm band */
box-shadow: 0 6px 36px rgba(10,10,10,0.32);  /* the mic RING, not the pill */
box-shadow: 0 12px 40px rgba(0,0,0,0.18);    /* overlay above the map */
Tokens

Motion

The signature isn’t a curve, it’s a cadence. An answer arrives as a typewriter of components, one block at a time, each fading up and rising about 13pt — roughly 530ms apart. There are no skeletons and no spinner; the screen simply becomes an answer. Hover the table below to replay the real stagger, or a track to compare the curves.

Generation cadence · frame-counted at 30fps · interval ≈530ms

Lede in1100ms
Header + subtitle in, as one unit1633ms+533ms
Card 1 in2200ms+567ms
Card 2 in2733ms+533ms
Card 3 in3300ms+567ms

Each element’s own entrance is fade 0→1 + rise ~13pt over ~190ms, ease-out. The arrival centroid decays 369.2 → 362.3 → 358.8 → 357.3 → 356.6, which is an expo-out.

Easing · recovered from the compiled web CSS

ease-reveal
cubic-bezier(0.22, 1, 0.36, 1)
entrances, thumbs — the default
ease-pop
cubic-bezier(0.34, 1.2, 0.64, 1)
gentle overshoot — documented for sheets; nothing in the build uses it
ease-bounce
cubic-bezier(0.34, 1.56, 0.64, 1)
playful overshoot — sparingly
ease-micro
cubic-bezier(0.4, 0, 0.2, 1)
the 150ms default for small state changes

Durations · and which one the prototype actually shipped

All eight are documented from the same place: one sentence in the measurement notes, sitting under an easing block sourced to the web CSS. Splitting them by provenance would invent a distinction the source doesn’t make. The built column is the honest one, and it is brutal — only the entrance exists in the prototype. A first pass at this table marked five of the eight as shipped, which is exactly the error the column was added to catch.

entrance190ms · ease-reveal
one component arriving — the one duration the prototype actually uses
micro-press120ms · ease-micronot built
documented with a 0.97 scale; the build presses with a spring instead
segmented thumb180ms · ease-revealnot built
documented; the build slides the thumb with a matched geometry effect under a spring
scrim250ms · ease-micronot built
documented; no scrim exists in the prototype
sheet380ms · ease-popnot built
documented; no sheet exists in the prototype
dock expand220ms · ease-popnot built
documented; the prototype uses a spring
receipt260ms · ease-popnot built
documented; no receipt view was ever built
recompute pulse1200ms · decaynot built
documented; superseded in the build by transitioning the value itself

What the build actually animates with · springs, not curves

Worth being precise about, because it is a genuine divergence: the curves above are real and they come from Monogram’s web build. The app’s feel is spring-driven, and the prototype followed the app. Any document that lists dock expand 220ms as shipped app behavior is describing the website.

springMorphinteractiveSpring(response 0.36, damping 0.78, blend 0.25)
dock card open/close, every parameter commit, the mini↔open layout change — the workhorse
springSnapspring(response 0.30, damping 0.72)
segmented label color, the conflict shake
springHoldspring(response 0.28, damping 0.60)
press feel on tappable cards and rows
dock entrancespring(response 0.44, damping 0.72)
the dock arriving, scaled 0.62→1 from the bottom anchor
slider glidespring(response 0.16, damping 0.86)
the value tracking a drag
knob swellspring(response 0.24, damping 0.70)
the slider knob taking a grab — scales to 1.12

Rules that outrank every curve

  • No skeletons.
    Content simply arrives. Nothing stands in for it first.
  • Nothing re-arranges after arrival.
    A component that has landed does not move again. This is the property Live Parameters had to preserve to be editable at all.
  • The answer page never shows a spinner.
    Streaming replaces waiting. A spinner would admit the screen is not yet an answer.
/* The cadence, which is the thing worth copying */
@keyframes arrive { from { opacity: 0; transform: translateY(13pt) } }
animation: arrive 190ms cubic-bezier(0.22, 1, 0.36, 1);
/* one block per ~530ms — no skeleton, no spinner, nothing re-arranges */
Vocabulary

Architecture

The reason a design system belongs in a case study about an AI product: Monogram’s model doesn’t draw a UI, it emits a typed document. A tree of components chosen from a fixed native vocabulary, serialized over protobuf, rendered as SwiftUI. The model owns which components and what content; the app owns how they look. Bounded generative composition, not generative rendering. The model owns which components and what content; the app owns how they look.

UI as protobuf

The response is a serialized component tree, not text. Actions are first-class.

SwiftProtobuf linked; OpenUrlAction / NavigateAction / UpdateAction{target,value,valueType}
Symbols
A fixed catalog

~38 native component types the model arranges. It selects and fills; it never draws.

component type names extracted from the binary
Binary
A semantic token layer

28 tokens, light and dark. Components reference names, never hex — so a screen composed five seconds ago is correct in both appearances.

DesignTokens_DesignTokens.bundle/Assets.car
Binary
A native renderer

Each node maps to a SwiftUI view. Text streams; 3D assets load by id from CDN so the payload stays small; some inference runs on-device.

MonoKit / MonogramUIKit, SwiftStreamingMarkdown, Kingfisher, FoundationModels
Symbols

The trade

A fixed vocabulary plus tokens cannot produce a broken or off-brand layout, and it runs at native speed no matter how strange the prompt. The cost is that anything outside the catalog cannot be shown — so the roadmap is the catalog. Every new capability is a new component, shipped to a client that must keep rendering every screen the model already knows how to compose. That is the constraint this study designed against, and it decided everything downstream: no new primitives.

min OSiOS 26.0 Symbols
linkedSwiftProtobuf · FoundationModels · Speech · MapKit · Kingfisher · SwiftStreamingMarkdown · GlassEffectContainer · StoreKit · Swinject · HighlightSwift · swift-crypto/nio
model  →  protobuf component tree  →  fixed catalog  →  token layer  →  SwiftUI

/* Actions are first-class in the contract */
UpdateAction { target, value, valueType }   /* a mutation primitive, shipped */
Vocabulary

Component Catalog

~38 component types — the model’s entire instruction set, extracted from the binary. Read the emphasized rows first. The eight interactive input controls and the three actions were already in the catalog; in every flow observed they appear at intake, gathering parameters before an answer generates, and then the answer renders and they are gone. That is the whole Live Parameters thesis in one picture: the primitives exist, pointed at the wrong moment.

The lists below name 43 things, which is not a contradiction: 36 component types, 4 league-pack variants of SportsScorecard, and 3 action types. ~38 is the count of things the model composes with.

Layout & containers

HomeMinimalpageStackmodalStacksheetBodymodalPageBodyanchorCellContentRows

Content blocks

MarkdownTextEntityViewDetailsTableStatsTableFeatureListLabelTitleRowTitleOnlyRowCapabilityBlockCodeBlock

Card lists

A size ramp — and it IS the emphasis system. If the model cannot draw, it says “this matters more” by picking a bigger card. Hierarchy without layout authority.

SmallCardListMediumCardListLargeCardListHeroCardListImagePreview

Rich domain cards

Hand-built for high-value verticals, down to individual league packs. The roadmap, made visible.

WeatherCardWeatherDetailsSportsScorecardEUROCUPEUROLEAGUEFIBAIIHF

Media

ImageIconAvatarIllustration

Interactive input controlsBinary

Already in the catalog. Emitted at intake, then retired the moment the answer renders — the whole Live Parameters thesis.

NumberStepperOptionsPickerDatePickerDateRangePickerTimePickerMapRegionPickerMessageComposerRecapForm

ActionsBinary

UpdateAction carries a target, a value, and a valueType — a mutation primitive, already shipped.

OpenUrlActionNavigateActionUpdateAction

The card ramp is the emphasis system

Worth isolating, because it answers a question the architecture raises. If the model can’t draw, how does it say “this matters more”? It picks a bigger card. SmallCardList through HeroCardList is a visual-hierarchy control expressed as component choice — hierarchy without layout authority. It is the most elegant idea in the catalog.

/* Same vocabulary every time. Only selection, order, and content change. */
"chewy cookie recipe"    → MarkdownText → EntityView grid → step timeline → FeatureList
"compare the Lucid Air"  → HeroCardList → DetailsTable + judgment pills
"plan a kid's party"     → RecapForm (OptionsPicker, NumberStepper) → FeatureList
                           ^^^^^^^^^ the controls, used once, at intake
Vocabulary

Icons

Hugeicons for content. Bespoke for chrome. Content glyphs are open-ended — the model might name any noun — so they need a library with breadth. Chrome is a closed set that users touch constantly, so it earns bespoke geometry tuned to its exact optical size. The Hugeicons, 6,756 glyphs, bundled whole is bundled whole for the content half; the chrome is the app’s own set. This is the correct call, and the study adopted it rather than trying to improve on it.

The glyphs below are drawn from the app’s own path data, reused verbatim. Strokes read oddly until you know the conversion: the app strokes at weight × size / 24 × 1.4, so in a 24-box the effective stroke is weight × 1.4 — which is why these are 3.00 and 2.24 and 1.5 rather than the raw weights, and why the source’s own comments check out.

chrome/plus13 units of arm · stroke 3.00 (weight 2.14 × 1.4)input bar addconflictdocumented: 16pt arms · 3pt rounded bars
chrome/keyboardbody x 2→22, y 4.5→19.5 · rows at y 9 / 12 / 15 · stroke 1.5 (weight 1.07 × 1.4 = 1.498)voice → typing
prototype/convoMic40-box source × 0.6 → capsule, shallow bowl, T-stand · stroke 1.2 (weight 0.86 × 1.4 = 1.204)voice input
stepCounter/plus12 units · stroke 2.52 (weight 1.8 × 1.4)NumberStepper up
stepCounter/minus12 units · stroke 2.52NumberStepper down
chevron-leftM15 18 L9 12 L15 6 · stroke 2.00 · round join, like every glyphback
chrome/menuthree unequal bars — 12 / 16 / 8 units · stroke 2.24menuHugeicons

Three details worth the space

  • chrome/menu draws three bars of unequal length.
    12, 16, and 8 units. The prototype’s first pass used SF Symbols’ line.3.horizontal, which draws three equal lines, and it was wrong. The taper is the whole character of the mark — and this is the one chrome glyph that is not bespoke: it matches Hugeicons menu-02 exactly, kept because it already fit. Redrawing it would have been craft for its own sake.
  • The remove glyph is an add glyph, rotated.
    The chip set’s ✕ is the app’s own add-01 turned 45°. No new asset was introduced for it. In a closed chrome set, that restraint is the system working.
  • Every glyph has round caps and round joins.
    Including chevron-left, which the teardown calls a “straight 2pt chevron”. That reads like a mitered join and isn’t — “straight” describes the path being straight-sided, versus the curved chevron used for arrow-right-01. A first pass here rendered it mitered with butt caps, contradicting the source.

The asset catalog embeds the original SVGs, LZFSE-compressed in `bvx` blocks — 44 documents. Recoverable as vectors with liblzfse. Used to verify geometry, not redistributed. Binary

/* stroke in a 24-box = weight × 1.4 */
chrome/plus         weight 2.14  →  3.00   "3pt rounded bars"
chrome/menu         weight 1.60  →  2.24
chevron-left        weight 1.43  →  2.00   "2pt chevron"
stepCounter/±       weight 1.80  →  2.52   "2.5 stroke"
chrome/keyboard     weight 1.07  →  1.50   "1.5 stroke"
prototype/convoMic  weight 0.86  →  1.20
Derivation

The Dock

The one genuinely new arrangement in the proposal, so it owes the clearest lineage of anything here. And the lineage is short: it is the intake card, kept alive.

The birthday flow already renders the model’s parameters as cards — Whose party? Which day? How many kids? — inside a RecapForm, built from OptionsPicker and NumberStepper. That is Monogram already conceding the job needs your parameters, in a grammar it already ships, at a moment it has already designed. The only thing Live Parameters changes is when it dies. Today the intake card goes inert the instant the plan generates; here it descends below the answer and stays. The intake becomes the dashboard.

Scope, stated before any of the numbers: the dock exists only in the SwiftUI prototype. The case-study page shows it as recorded video of that app. It was never re-implemented as DOM, and nothing here should be read as claiming otherwise.

It runs across all five of the interfaces in that section, not just the recipe: the recordings under Live Parameters are a cookie recipe, an EV comparison, a party plan, a movie pick and a restaurant search. Fourteen parameters across the five interfaces, carried by four card types — and the docks stay at two or three cards each. Every specimen below is one of those cards, at the values the footage shows, so the two pages can be checked against each other.

Parameter type → control · every type, as a specimen

Rebuilt here from the measured spec, so the geometry can be inspected rather than described. The card is the unit, not the control: a 40×34 thumb, a 10.5pt label over a 13pt semibold value, and a control that displaces that stack when the card opens. Tap a card to open or close it. The stepper glyphs are the app’s own; the segmented thumb runs at the measured 180ms; the press is 120ms to 0.97.

Three treatments, keyed to what is actually built. Solid cards are live and are in the footage. Dashed cards are drawings of the spec and do nothing, because a specimen you can operate is a claim that the thing exists. A partial row draws both halves — the built control live, the missing piece dashed beside it.

The receipt, and what replaced it

9 quantities recomputed · bake time unchanged

The receipt chip above is specified, not shipped — and the substitution the build made instead is the more interesting answer. Rather than flashing a box behind a changed value, it transitions the value itself, rolling its digits. The result is the receipt. Listing it as built would have been the easy lie.

The count it would have reported is nine: the recipe carries nine ingredients, and a servings change rescales all nine. Bake time is bound to texture rather than to batch size, so it genuinely doesn’t move — which is the harder half of the claim, and the reason the chip would have had to name what didn’t change too.

Bake at 350°F for 12 minutes, until the edges set and the centers still look underdone.

The bound-numeral affordance: border/spotlight at 1.5px, and nothing else. This is the load-bearing token choice in the whole proposal — a first pass gave it a warm underline, which wasn’t ugly so much as foreign, because the system it was joining has no accent to borrow. It is also, honestly, the piece the prototype never built: all manipulation ended up in the dock.

Shell anatomy · measured, save one authored value — none of it extracted

Radius14pt, continuousmeasured off Monogram's own entity dock — see the honesty ledgerMeasured
Fill#FFFFFFpure white, NOT surface/secondary — the dock reads as chrome, not contentMeasured
Sheen0.8pt white→clear gradient stroke, overlay blendMeasured
Shadow, rest0 2px 10px rgba(30,25,15,0.08)the shipped `capsule` token, verbatimMeasured
Shadow, open0 5px 20px rgba(30,25,15,0.14)an authored lift — the one value here that is neither measured nor extractedDerived
Card padding7pt vertical · 10pt horizontalMeasured
Interior spacing9ptMeasured
Leading thumb40 × 34pt, 8pt radius, #F5F5F5 fill, 18pt emojiMeasured
Collapsed label10.5pt, content/secondary-mutedrenders #8F8F94. The intake card ships 11pt — reduced to fit three cards across 402ptMeasured
Collapsed value13pt semibold, content/primarythe intake card ships 15ptMeasured
Inter-card gap8ptMeasured
Row inset18pt leadingaligns the first card with the input bar's + button belowMeasured

Three states, not two · the third one isn’t in the spec

openthumb + the live control
expanded == id
collapsedthumb + label + value
expanded == nil
minithumb only — label and value removed
expanded != nil && expanded != id — Not in the spec's state table. Invented by the horizontal layout.

Rules the dock holds itself to

  • One open card at a time.
    The open card is a single optional id, so opening one closes the last.
  • An out-of-range stepper button dims but stays tappable.
    A dead button would refuse silently. Dimmed-and-live can explain itself.
  • Sections never reorder.
    The layout-stability contract, enforced in the source by comment: a value change is a legal edit; re-arranging surviving sections is not.
  • The glass band never eats a tap.
    The 168pt bottom material is explicitly non-hit-testing.
  • The dock arrives after the answer.
    It is chrome over the answer, so its entrance is timed off the answer's own block count rather than a hand-authored delay — 11 blocks lands it at 2.40s.
/* The dock is chrome over the answer, so it arrives after the answer does.
   Its entrance is computed from the answer's own block count, not authored: */
dockDelay(blocks) = 0.7 + 0.11 × (blocks − 1) + 0.45 + 0.15
                  → 11 blocks = 2.40s

/* Open cards overrun a 402pt frame (415, 425, 431pt against a 384pt fade),
   so the row measures its own overflow and shifts. Measured, not constant —
   eyeballing it is how the clipping got missed the first time. */
shift = min(0, (384 − 8) − openMaxX)
Derivation

Asset Derivation

Every element in the prototype is one of three things, and this page says which. The point of the exercise is the third column: a proposal that needs a long New list hasn’t understood the system it is proposing to. Three entries here are new, and each owes a reason no shipped primitive covered it.

CopiedTaken verbatim from the shipped app.
RecomposedShipped primitives in a new arrangement. No new components.
NewAuthored, because no shipped primitive covered it. Each one owes a justification.
ElementAncestor & what changedClass
Card triplet — emoji · label · bold valuethe birthday flow's intake cards
Same grammar, one lifetime longer. Type reduced 11/15pt → 10.5/13pt to fit three cards across 402pt.
Copied
Card shell — white, r14, capsule shadowMonogram's own entity dock
The rest-state shadow is the shipped capsule token unchanged. The open-state lift is authored.
Copied
Color, radius family, ink ladderDesignTokens.bundle
Exact hex from the asset catalog. One deliberate deviation: content/secondary-muted renders #8F8F94 rather than the token's #AAAAAA — though at 3.2:1 that still fails AA for body text, so it is a mitigation, not a fix.
Copied
Stepper — − value +NumberStepper + the stepCounter glyphs
Same shipped glyphs, re-hosted in a 30pt circle inside a dock card instead of a RecapForm row.
Recomposed
Segmented controlOptionsPicker
Rendered as a segmented track with a matched-geometry thumb. No shipped segmented geometry is documented anywhere, so the r12/r9/3pt-inset metrics are authored.
Recomposed
Chip set with add and removeOptionsPicker chips + EntityView
The remove glyph is the app's own add-01 rotated 45°, rather than a new asset. Add and remove are built; swap is not.
Recomposed
Bottom glass bandSwiftUI.GlassEffectContainer
The native effect needs an SDK the prototype couldn't build against, so it's ultraThinMaterial plus a five-stop gradient measured off a device frame.
Recomposed
Slider — track, run, knobnothing
The shipped catalog has no continuous-range control at all. Range parameters — budget, runtime cap, radius — had no primitive to reuse. Re-authored once because the system Slider read as a bar rather than as draggable.
New
The mini statenothing
Intake cards stack vertically and never contend for width. A horizontal tray does.
New
Edge feather + nudge-into-framenothing
Open cards overrun the frame — 415, 425, 431pt against a 384pt fade — so the row measures its own overflow and shifts. Measured at runtime rather than hard-coded, because eyeballing it is how the clipping got missed the first time.
New

Specified and never built · the gap between the document and the artifact

A design system page that documents only what worked is marketing. Six things were specified and are not in the prototype — including, awkwardly, the rule the whole proposal leans on hardest.

  • The inline whisper affordance
    A barely-there underline on bound numerals. Accent tokens were declared in the prototype and used nowhere — all manipulation ended up in the dock.
  • Two recompute tiers
    Local under 100ms versus model under 2s. Every setter in the build is synchronous, so there is no tier distinction to observe.
  • The receipt chip
    Specified as the proof-of-work surface. Never built. The build instead transitions the changed value itself, rolling its digits — a deliberate reversal, and arguably the better answer: the result is the receipt.
  • The undo stack
    Specified alongside the receipt. Not built.
  • The conflict reason line
    The build ships the refusal shake and discards the reason string, with a comment reserving it for the voice layer. So a refusal is currently felt but not explained.
  • The four-parameter salience cap
    A written rule with no enforcement. Built docks carry two or three cards; nothing in the code prevents a fourth or a ninth. The most important rule in the system is the one least defended by it.

Claims I can’t stand behind · listed so you don’t have to find them

  • The dock's card shell descends from Monogram's own entity dock.
    The prototype credits an App Store screenshot for the 14pt radius, the 40×34 thumb, and the 8pt gaps. That screenshot is not in the research folder, so those figures can't be re-derived from anything kept. Either the source gets added or these downgrade to “authored in their idiom.”
  • chrome/plus has 16pt arms.
    The teardown records 16; the path actually in use spans 13 units in a 24-box. The stroke reconciles exactly at 3.00, the arm length does not. One of the two is wrong and it can't be settled without decompressing the shipped vector.
  • The controls are never bound to rendered answers.
    The binary proves the controls exist and that UpdateAction carries a target and a value. It does not prove the negative. This is an observation from the flows I watched — and if Monogram binds them somewhere I didn't see, the proposal gets smaller and more obvious.
  • The mic pill measures 88 × 44pt.
    An earlier measurement pass recorded 84 × 52. The build and the device frame agree on 88 × 44, so that is what ships here, but the two readings were never reconciled.
  • Each component entrance runs ~190ms.
    A later audit at 120fps put it nearer 150ms. The prototype uses 190ms. The discrepancy is real and unresolved; 190 is the value the motion shipped at, not necessarily the app's.
Appendix

Principles

The throughline behind the tokens — why the system looks the way it does, and the tie-breakers for cases the tables don’t cover.

Color comes from content

The chrome is achromatic by rule — there is no accent token to reach for. An affordance that introduces color is not bold, it is foreign.

Tone, not shadow

The most-used surface in the product has no shadow and no border. Depth is a tonal step and a hairline; shadow is reserved for things that genuinely float.

Composed, not drawn

The model selects components and fills them; the app decides how they look. Emphasis is a bigger card, not a custom layout. Hierarchy without layout authority.

Nothing re-arranges after arrival

A component that has landed does not move again. This is what makes an answer editable at all — the user's spatial map has to survive the edit.

Controls are earned, not sprayed

A parameter earns residency by being one someone is likely to move, not by existing. Cap the dock at four and leave the rest inline.

Open sets get a library, closed sets get bespoke

Content glyphs need breadth because the model may name any noun. Chrome is small and touched constantly, so it earns geometry tuned to one optical size.
Appendix

Anti-patterns

The guardrails. If a change introduces any of these, it has left the system.

  • Introducing an accent color for an affordance — the system has 28 tokens and none of them are saturated. Use border/spotlight.
  • Raw hex in a component — reference the semantic token so the screen is correct in both appearances for free.
  • A shadow on an entity card — it is flat by rule. Depth is the tonal step to surface/secondary.
  • Re-arranging surviving components after an edit — content may change and sections may come and go, but the spatial map has to hold.
  • A spinner or a skeleton on an answer — content arrives instead. A placeholder admits the screen is not yet an answer.
  • Self-hosting SF Pro — the app uses the system face. system-ui resolves more accurately and the licence does not permit the alternative.
  • Body text on content/tertiary or content/secondary-muted — 1.69:1 and 2.32:1. Step up to content/secondary at 5.33:1.
  • Adding a dock control because a parameter exists — four is the cap. Every quarter ships a component, every component has parameters, and an uncapped dock becomes a cockpit.
  • Quoting a duration from the web CSS as app behavior — the site ships curves, the app is spring-driven. They are not the same system.
Appendix

Source

Colors, the component catalog, the linked frameworks, and the icon bundles were read out of the shipped binary — Monogram AI v1.4.2 · ai.monogram.app, pulled July 2026 via ipatool → assetutil / nm / strings, plus frame-counted screen recordings. Type and motion were frame-counted at 30fps off six screen recordings at 804×1748. Where the binary disagreed with an earlier measurement, the binary wins, and the corrections stay visible in the Color section rather than being folded in quietly.

Numeric spacing, radius, and type-scale tokens could not be recovered. They are compiled Swift constants and are stripped from the release binary. A `DesignTokens.Radius` type exists; its values do not survive as data. Every size on this page is a measurement off video, labeled `measured`. The color tokens are the only values here read directly from the app.

The app’s own mark is also absent here. The bundle holds a launch-screen logo whose geometry was never extracted, so this page’s wordmark is typographic rather than a drawing — inventing a logo and setting it beside real extracted tokens would poison the evidence around it.

Unofficial concept study. Not affiliated with Monogram. Nothing redistributed; no assets scraped.

MonogramDesign.MD/
├── DESIGN.md        # written spec (agent-followable)
├── tokens.json      # machine-readable tokens, both appearances
└── README.md        # methodology + this page's route

// the page renders from:
src/components/monogram-design-log/tokens.ts