Components

Airmond paints one ground, in the member’s own scheme. Exactly one surface is allowed a world of its own, and only while it is live: the recording stage, because the question it answers — “can she hear us?” — is asked by people two metres away who cannot see a setting. The call is live too, so it takes the stage’s dusk, and so does Ada’s meeting tile, because the people looking at it cannot see the member’s setting; everything else on this page is drawn on the one ground. That rule is dated 2026-09-19 and it cost a black: see the call.

Stock shadcn/ui with the smallest possible delta: the system is a theme (tokens) plus a tiny set of extension tokens. Everything below is rendered purely with the CSS variables from dist/css/airmond.css — no component forks.

Coral is used sparingly — identity marks and the one primary action per screen; the dark blue --confirm is the working accent for active states, focus, links and confirmation. Primary buttons carry ink text (ink on coral 5.76:1; white on coral 2.80:1 fails). Confirmation is blue, never green. Waiting-on-you is amber/gold, the one state allowed louder than the accent. Destructive is stock shadcn red — distinct from coral by depth. A mark that separates or encloses takes --rule, never --border, and it is measured on the surface it actually sits on: see the thread's marks and the four controls a member could not find. The secondary button above is stock shadcn's filled neutral and keeps its fill; the APP's own secondary control is an outline, for the reason that section measures. A wart this records rather than hides: on the web theme --disabled-fill and --secondary are the same hex (#EAF0F4), so Not now and Not yet above are told apart by the outline and the muted label rather than by their plates. On the app side the question does not arise, because the app's secondary has no plate at all.

Feather (light)

Feather

Live Waiting on you Priority Draft
BoardCallsNotes
Tomorrow, 09:00
Ada confirmed the dentist and moved the standup.
Confirmed
✓Booked. The board fills itself.
●Waiting on you: approve the quote.
Can you move my Thursday flight?

Low flame (dark)

Low flame

Live Waiting on you Priority Draft
BoardCallsNotes
Tomorrow, 09:00
Ada confirmed the dentist and moved the standup.
Confirmed
✓Booked. The board fills itself.
●Waiting on you: approve the quote.
Can you move my Thursday flight?

The recording stage

This is the surface the one-ground rule makes an exception of, and the only one. It paints its own world edge to edge and does not follow the member’s scheme: while she is listening the ground is dusk; the moment the member goes off the record the whole screen repaints amber. That is the feature, not decoration: the phone is lying flat on a table, and “can she hear us?” is being asked by people two metres away, so the answer has to survive a glance across a room. A state whose colour depended on a setting nobody in the room can see would be a state two people could describe differently.

⚠️ The exception is the AMBER, not the dusk. The listening ground is the dark scheme, read straight out of it, so what this screen actually claims is the right to stop following the member’s setting while it is live and to repaint itself entirely when it stops capturing. The call takes the same dusk for the same reason and has no amber, because it has no off-the-record state to announce. Everything else on this page follows the member.

Why amber, and not red or grey. Red already means “recording” everywhere else on this screen, and grey reads as “disabled” — the recording died — which invites exactly the wrong conclusion. Amber is the one colour a room reads as “hold on” without being taught. Ink on it measures 7.43:1.
Three of these values were wrong, and this page is how that became visible. Until the stage palette was written down here it lived in one hand-maintained file in the app, covered by none of the measured claims. Writing the claims caught all three at once: Stop had drifted off systemRed, the amber captions failed AA, and one roster hue was a green that the no-green guard never saw. All three are fixed below, each with the measurement that forced it. Two further relationships turned out to have no colour fix at all; those are recorded as must-fail claims rather than quietly dropped.
The one thing no colour choice can fix, so do not try. Nothing legible on the dusk can also be legible on the amber. Clearing 3:1 on #171E2A needs a luminance of at least 0.138; clearing 3:1 on #E8A317 needs at most 0.111. Those windows do not overlap, for any colour that exists — a near-black would read on the amber and vanish on the dusk, and vice versa. So the stage never asks one colour to do both jobs: off the record a speaker is identified by their NAME and the capsule outline, and Stop is found by its ink LABEL rather than by its edge. Both are asserted as must-fail claims, which is honest about the measurement and still goes loud if either value moves.

The two grounds

The rule: each ground carries its own foreground, and one ink is drawn at a few opacities rather than a second set of hexes. The quiet fill is that ink at 0.1; the hairline is 0.3 on dusk and 0.28 on amber. Secondary is the one tone that needs two numbers — 0.68 on dusk, 0.75 on amber — because the amber ink has a far lighter ground to survive, and a shared value cannot serve both. The dusk pair is not a third scheme: it is the dark scheme, read straight out of it.

Dusk — she is listening

Primary ink on the ground
Secondary, the same ink at 0.68

Amber — off the record

Primary ink on the ground
Secondary, the same ink at 0.75
PairingDuskAmber
Primary ink on the ground12.61:17.43:1
Secondary caption (ink at 0.68 → #9DA2A9 on dusk, 0.75 → #4C4226 on amber)6.51:14.58:1

This is where the second bug was. Both grounds shared one 0.68, and on the amber that measured 3.91:1 — under the 4.5 body-text bar, for every caption on the screen: the tap hint, the privacy note, the honesty caveat under the controls, the count. The amber ink was being asked to be loud enough at full strength and quiet enough at 0.68 against a ground far lighter than the dusk. Fixed by splitting the token: 0.75 on amber is the lowest step that clears the bar (4.58:1; 0.74 measures 4.44 and misses), deliberately chosen over a rounder 0.8 because a secondary tone that stops receding has stopped being secondary. Dusk keeps 0.68 at 6.51:1 — it never had the problem.

The capture card

The rule: transparent with a currentColor-strength outline, never a filled card. A filled card would need a surface colour per ground and would fight the wash the whole screen just performed; an outline at the ink’s own 0.22 follows it for free. Radius 12, padding 12, a kind chip that hugs its label, the headline in the ground’s primary ink and the detail in secondary.

Dusk — she is listening

FACT Ines is allergic to shellfish Said while the starters were being ordered.

Amber — off the record

FACT Ines is allergic to shellfish Said while the starters were being ordered.
PartColoursDuskAmber
Kind chipconfirm blue on its own 18% wash (dusk) / the stage ink on its own 12% wash (amber)5.33:16.07:1
Headlineprimary ink on the ground12.61:17.43:1
Detail linesecondary on the ground6.51:14.58:1

The chip is blue on dusk and ink on amber rather than blue on both: on the amber the feed is historical (“she wrote down, before the pause”), and a confirm blue that means “live” would be claiming something untrue about rows captured before the member paused her. Never green — confirmation is the blue pair on every Airmond surface.

The speaker chip

The rule: the hue is a MARK, never the text. One capsule holding two controls — the name (tap to mark them talking) and a ✕ (remove) as a sibling, never nested — with the roster hue as a 10px dot, the capsule fill and the capsule border. The name itself stays in the ground’s own ink, so a hue never has to carry a word. Resting: fill 0.1, border 0.3, 1px. Speaking: fill 0.28, border 0.95, 2px, and the label goes bold — three signals, because one is missable across a table. 44px tall, Apple’s minimum, because the member is tapping while looking at a person rather than at the phone.

Dusk — she is listening

Ada✕ Joe✕ Ákos✕ Ines✕ Zola✕

Amber — off the record

Ada✕ Joe✕ Ákos✕ Ines✕ Zola✕

Ines is the one marked speaking in both.

Roster hueOn dusk — must clear 3.0 (a non-text mark)On amber — asserted to stay UNDER 3.0
#5CB8F0 — speaker 17.62:11.01:1 must stay under 3
#FAB55C — speaker 29.42:11.22:1 must stay under 3
#B899F5 — speaker 37.10:11.09:1 must stay under 3
#EC8DEC — speaker 47.64:11.01:1 must stay under 3
#F58FAD — speaker 57.50:11.03:1 must stay under 3
Speaker 4 was a green, and is now an orchid. It was a mint that Airmond's standing no green, ever rule should have caught and did not: the guard in build/color.mjs is a literal hex blocklist of the retired fern values, so a mint nine channel steps off one of them walked straight past it. The replacement is #EC8DEC, an orchid at hue 300°. The wheel it had to fit is small — green is banned outright, red already means “recording” on this very screen, and yellow collides with the amber ground — which leaves the arc between the purple (260°) and the pink (342°). It sits at that arc's midpoint, 40° clear of one neighbour and 42° of the other, is the exact complement of green rather than a neighbour of it, is not the brand coral (6°, reserved for identity marks), and measures 7.64:1 on dusk — inside the band the other four already occupy. The honest cost: the old roster's tightest pair sat 51° apart and the new one's sits at 40°, because dropping green gives up the only hue in a 169° arc and five chips must share what is left.
And the amber column is not a bad pick — it is arithmetic. All five hues clear 3.0 comfortably on dusk (7.10–9.42:1) and every one lands near 1:1 on the amber, because a hue light enough to read on the dusk is necessarily close to the amber's own luminance. No fifth colour fixes this and no rearrangement of these five does either. So the claims are recorded as must-fail rather than relaxed to 1.0 or deleted: the file states the true relationship, CI stays green, and changing either a hue or the amber breaks the claim and forces the decision back into the open. What carries the load off the record is the part that does not depend on hue — the name identifies the person, and speaking is signalled by the bold label and the 2px outline. If the roster ever must be readable by colour on the amber, the fix is structural (give the chip its own dark plate there), not another hex.

The status pill and its live dot

The rule: this is the one element that must read from across a table, so it is drawn at near-full foreground strength — a 1.5px outline at the ink’s 0.9, not the 0.3 hairline everything else uses. Listening: a live dot that breathes (0.45 → 1, 900ms, and static under prefers-reduced-motion). Off the record: the dot is replaced by a pause glyph in the stage ink. The dot is recording-red (#E8574A) rather than the stage foreground, because “recording” is red everywhere else in the app — and it has no amber counterpart on purpose, since a red dot on a screen that is not capturing anything would be the single most dangerous pixel in the product.

Dusk — she is listening

Listening to the room

Amber — off the record

Off the record
PartColoursMeasured
Label, duskprimary ink on the ground12.61:1
Label, amberprimary ink on the ground7.43:1
Live dot #E8574A on duska non-text mark, so 3.04.69:1

The stage control pair

The rule: two EQUAL halves of one switch, quiet on the left and the destructive on the right. Equal widths are the whole point — the pair reads as two directions of a single control rather than as an action with an afterthought beside it, which is also why the labels mirror each other (“Go off the record” / “Back on the record”) instead of describing what happens. 52px tall, radius 14, 12px between them, one line always. While listening the left half is quiet (the fill plus the hairline); off the record it is the solid ink, because coming back on the record is then the primary act.

Dusk — she is listening

Go off the recordStop

Amber — off the record

Back on the recordStop

iOS may still show its microphone dot: the recording stays open so she can pick up instantly, but no audio is being written.

PartColoursDuskAmber
The quiet / solid halfink on the fill, then the ground on solid ink12.61:17.43:1
Ink on Stop #FF453A / #FF3B3017px semibold — body text, so 4.54.73:14.55:1
White on Stop — the label it replacedasserted to stay under 4.5, exactly as white-on-coral is3.41:1 must stay under 4.53.55:1 must stay under 4.5
The Stop fill against its own groundclears 3.0 on dusk; on amber it is asserted to stay under it, because the ink LABEL identifies the button there4.91:11.64:1 must stay under 3
Stop kept its red; the label moved. The values are #FF453A and #FF3B30 — Color.red as the native app resolved it in each appearance (cs-airmond-ios/App/Sources/Screens/RecordingStageView.swift:847). The app had drifted to a muted #DC2626 on dusk and a near-black brick #8C1D12 on amber, and nobody noticed until the button was called faded. Note why that drift is easy to make: white measures 4.83:1 and 9.13:1 on those two, against 3.41:1 and 3.55:1 on systemRed — it reads as somebody fixing a real contrast failure and paying for it with the one colour a room already knows means Stop. Both problems were real; only one of them was about the red. So systemRed stays and the label is now the brand ink (#18222B): 4.73:1 and 4.55:1, both clear of 4.5. That is not a new rule — it is the one this system already applies to coral, where white measures 2.80:1 and ink 5.76:1, which is why onCoral is ink.
What the ink label does not fix: the fill on amber. The systemRed plate measures 1.64:1 against the amber ground. Look at the specimen above and it seems fine — and that is the trap. Red and amber are separated by hue, and a WCAG ratio measures luminance, so the two are all but the same brightness: the plate reads for a reader with ordinary colour vision on a good screen, and stops being a shape for a red-green colour-blind one, at low contrast-sensitivity, or on a phone lying flat in sunlight — the exact situation this screen was designed for. No red fixes it (a red dark enough to clear 3.0 on the amber vanishes on the dusk), so the button is identified by its label, which is why that label now measures 4.55:1 against the plate and 7.43:1 against the ground behind it. That 1.64 is recorded as a must-fail claim, so it can never drift unnoticed into being load-bearing.

The count pill

The rule: the count appears only when there is something to count, and it never competes with the eyebrow it sits beside. A 13px secondary line at the end of the feed header — no chip, no border, no colour of its own. A zero state says its sentence instead (“nothing yet”), because a pill reading “0 saved” invites the reading that something failed.

Dusk — she is listening

She's writing down 3 saved

Amber — off the record

She wrote down, before the pause 3 saved
PartColoursDuskAmber
Section eyebrowsecondary on the ground6.51:14.58:1
The countsecondary on the ground6.51:14.58:1

Same colour, same finding as the two grounds above: on the amber this is 4.58:1.

Consuming it

Web: the --stage-* variables in dist/css/airmond.css, in :root — scheme-independent by design, not by omission. React Native: stagePalette(listening) from dist/ts/tokens.ts, which returns the ground, its ink, the three opacity-derived tones and that ground’s Stop; the roster hues, the live dot and the raw opacities are on the stage export beside it. Until this page those values lived only in mb-airmond-app/src/features/recording/stage-palette.ts, which is what let both findings above ship unseen.

The call

The other surface that fills the screen while she is live, and as of 2026-09-19 the one that stopped painting a world of its own. It is on the one ground — the dusk, the same one the recording stage paints while she is listening — because it is the same moment: she is live on both. The two are told apart by what is on them, a portrait and a handset against a speaker roster and a capture feed, and never by two different blacks.

This section is late, and the lateness is the story. Until today the call was the one Airmond surface with no entry in this library at all, and it was the one surface that had invented a ground: #000000 with the dusty coral breathed into the top at 0.34, living in mb-airmond-app/src/features/voice/components/call-ground.tsx with nothing upstream of it. Those are not two facts. A surface nobody draws on the guide is a surface whose colours nobody measures, and that file said so in its own words: “Pure black rather than a token because there is no token for it and there should not be.”

What the ember was, so nobody re-derives it. The coral fell from 0.34 at the top to 0.09 at a quarter and to nothing by the half, compositing to about #4D2C27 — a glow rather than a wash, and its falloff was not transcribed from native but measured down the left edge of a committed iPad capture, because SwiftUI and SVG do not ramp a colour and an alpha toward each other the same way. That was careful work and it is not why it goes. It goes because of the sentence underneath it: painting the call in ground “would make the two screens siblings at a glance, which is the one thing they must not be”. That premise is what the decision reverses. They are siblings. She is live on both.

The ground

The rule: Airmond paints one ground, in the member’s own scheme, and one surface is allowed a world of its own only while it is live. That surface is the recording stage, because the question it answers is asked by people two metres away who cannot see a setting. The call is a phone held by one person, so it has no claim on a third ground — but it is live, which is why it takes the stage’s dusk rather than the member’s current scheme.

Was · a ground of its own

Is · the dusk, read from the dark scheme

PairingMeasured
Primary ink on the call ground12.61:1
Secondary caption (ink at 0.68 → #9DA2A9)6.51:1
Her portrait ring, an identity mark, against the ground6.14:1

Nothing in the table above is a new value. The ground and its ink are dark.background and dark.foreground read straight out of the dark scheme, exactly as the stage’s dusk is, so the two can never drift. The section below adds four things and borrows the rest.

Only one ground, so only one column

Every other component section on this page draws its specimens in both schemes side by side, because the surfaces they document follow the member’s setting. This one does not, and that is a decision rather than an omission: the call is pinned while it is live, exactly as the stage is. The one exception is the minimised banner, which is not on the call ground at all — it is a strip across the member’s own page, so it is drawn in both.

The specimen palette

The specimen carries the call palette on itself as the --ca-* set, mapped once in site/call.mjs from tokens/tokens.json. Two things in this section are not read from it, and both are deliberate: the retired black and its ember above, which have no token because they no longer exist, and the white on the minimised banner, which is white in both schemes and so belongs to no scheme set. Everything else re-renders when the token file changes.

VariableTokenWhat it draws
--ca-grounddark.backgroundThe one ground. The dusk — the same one the stage paints while she is listening, because it is the same moment.
--ca-inkdark.foregroundHer name, the clock, every glyph and every heading on the screen.
--ca-secondarydark.foreground at stage.duskSecondaryOpacityCaptions: the sentence under the pill, a feed row’s detail line, the tally.
--ca-filldark.foreground at stage.fillOpacityThe quiet chrome behind a feed row, at the stage’s own fill strength.
--ca-hairlinedark.foreground at stage.cardHairlineOpacityA capture card’s outline, the stage’s card hairline exactly.
--ca-rulemark.tone at mark.ruleOpacityThe one tone every mark that separates or encloses is drawn in. Here: the outline that makes a quiet control a shape.
--ca-end-fillstage.stopOnDuskThe End disc. Read from the stage so the control that ends a live conversation cannot drift from the one that ends a recording.
--ca-on-end-glyphcall.onEndGlyphThe handset on that disc. White, and the one place on an Airmond red where white is correct — see the controls.
--ca-live-dotstage.liveDotThe breathing dot in the status pill. The stage’s, because the pill is the stage’s.
--ca-coralrn.dark.coralIdentity, and nothing else on this screen: her portrait ring. Ending a call is NOT coral — see the controls.
--ca-confirmdark.confirmA row she got through. Never green.
--ca-control-washdark.foreground at call.controlWashOpacityThe quiet disc behind an unarmed control — on this screen, the unmuted mic. It does not identify the control: see the measurement.

The icon set

Seven symbols, drawn once into a <defs> block and referenced by <use>, each stroked in currentColor so it takes the colour of whatever it sits on. Nothing here is an emoji or a text character: a text glyph renders as an empty box, which is the defect the chat surface was revised to fix.

Mic, liveMic, mutedSpeakerEndLower the callA task she tookIt did not go through

CallPortrait, and the shape the screen starts in

The rule: her size is the outcome of a subtraction, never a constant. The screen has one hard requirement — Mute and End are always reachable — and exactly one flexible thing above them. So the portrait takes whatever height is left after her name, the pill, the controls and the clock have had theirs, between 88 and 240 points. An iPhone SE mid-call has about 120 points of slack; a 6.9" phone has three times that. The ring is coral, and coral on this screen means identity and nothing else — which is exactly why ending a call is not coral.

Ada

04:12

Listening

She will keep going until you say otherwise.

PartColoursMeasured
Her name and the clockprimary ink on the ground12.61:1
The ringcoral as a non-text identity mark, so 3.06.14:1
The caption under the pillsecondary on the ground6.51:1

CallHeader, once she has captured something

The rule: the clock is the same counter, left-aligned rather than centred — never a smaller one. Stacking her, her name and the counter cost over 250 points of header on an iPhone 17 and left the thing the screen exists to show with one visible row, so the working shape puts them in a row. The counter stays the largest thing in it: how long you have been on the phone is the one number a member looks up to check, and shrinking it the moment the screen got busier would make it harder to read exactly when there is more to read.

Ada

04:12

Listening

CallStatusPill

The rule: this is the recording stage’s pill, not a second one. Same radius, the same 1.5px outline at 0.9 of the foreground, the same paddings and label scale, and the same breathing dot at #E8574A. A member who lowers the screen mid-call and comes back to a busier one must not have to re-read a different status vocabulary to work out what changed, so one function turns a phase into words and both shapes mount this component rather than restating it. The dot is static under prefers-reduced-motion.

ListeningWorking on it

She will keep going until you say otherwise.

PartColoursMeasured
The labelprimary ink on the ground12.61:1
The live dot #E8574Aa non-text mark, so 3.04.69:1
The caption under itsecondary on the ground6.51:1

CallControls, and the one measurement this section came back with

The rule: ending a call is red, and on this screen red means only that. It used to be coral, which was right while coral did nothing else here; since the portrait grew its ring, a coral End button sits inches from a coral ring meaning something completely different, and one colour saying two things is how a signal stops working. The disc is #FF453A — the stage’s own Stop, read from there rather than typed again, so the control that ends a live conversation cannot drift from the one that ends a recording.

White on the handset, ink on the Stop button, and the two rules are not in conflict. “Keep the red, move the label” was measured for a word on the stage’s Stop button, read from two metres away, against WCAG’s 4.5 text bar — which white misses at 3.41:1. The End control carries no word: it is a 24pt handset glyph, whose bar is 3.0, and 3.41:1 clears it. A dark glyph there would read as a disabled control. Both halves are claimed in the token file, the 3.0 it must clear and the 4.5 it must fail, so the pair cannot quietly collapse into one rule.
And the quiet disc has never been a shape. An unarmed control — here the unmuted mic, the only one on this screen — is filled with the ground’s own ink at 0.18 and given no edge. That composites to #3A414C and measures 1.63:1 against the ground — about half of the 3.0 that a container the member has to find is meant to clear. This is not a cost of the move. On the retired black the same wash was #2E2E2E at 1.55:1, so the new ground improved it by 0.08 and left it failing. Raising the opacity does not rescue it either: 0.30 reaches only 2.33:1, by which point the disc has stopped being quiet. So the fix is the one this kit already shipped for the card on 2026-09-14: an outline in rule, the single tone every separating mark is drawn in (#6E7986, 3.78:1). A mark that separates must clear 3.0 on the surface it actually sits on.

Was · filled, and findable only by its glyph

Mute

End

Is · the same fill, with the rule outline

Mute

End

Two controls, not three. There is no audio-route button and that is not an oversight: AirmondLiveAudio follows the OS communication route — speaker, wired headset or Bluetooth — and reports the changes, so the platform stays the source of truth and the screen keeps only the controls whose behaviour is identical everywhere. The first version of this section drew a Speaker disc here, and the app has never had one. It is recorded rather than quietly deleted, because a specimen that invents a control is worse than no specimen: the next person builds it.

And muted is the loud state

Muted fills with primaryFill — the way a pressed key stays down — so “she cannot hear you” is legible without reading the pill. A filled disc takes no outline, and must not: it separates on its own, and an edge on top of a fill reads as a second state.

Live · she can hear you

Mute

End

Muted · she cannot

Unmute

End

PartColoursMeasured
The End disc against the grounda control must be findable, so 3.04.91:1
The white handset on the End disca 24pt NON-TEXT glyph, so 3.03.41:1
White on the End disc, against the 4.5 TEXT barasserted to stay under it — the record of why a WORD on this red is ink3.41:1 must stay under 4.5
The quiet wash against the groundasserted to stay under 3.0: it is not what finds the control1.63:1 must stay under 3
The rule outline against the groundthis is what finds it, so 3.03.78:1
The glyph inside a quiet discprimary ink on the wash7.76:1
The MUTED disc against the groundprimaryFill — it separates on its own, which is why it takes no outline12.61:1
Its glyphonPrimary on that fill12.61:1

The app draws this, as of 2026-09-19. call-controls.tsx takes call.controlOutlineTone at control.card.borderWidth on an unarmed control and nothing on a filled one, pinned by an e2e test that asserts both halves — a test checking only that Mute has a border would pass on a build that put one on every control. For about an hour this note read “a change the app has to pick up”, which is the state every token in this kit starts in and the state none of them should stay in.

CaptureFeed

The rule: it is the recording stage’s own headings, taken by reference and not retyped, plus one the stage has no use for. It stopped being a single flat stream when Cédric asked to see “what you’re capturing, what tasks, what knowledge updates, and so forth”, so the feed carries the stage’s two headings and a third, What didn’t go through. The tally sits beside the heading and outside the scroll view: on one 514-second call nineteen of twenty-eight completions were refused and the member found out from her, near the end, while reloading another tab. A count that can be scrolled away from is a count that arrives too late.

She is writing down3 saved
Call Tony's Barbershop

Pfäffikon SZ, to check today's slots

Pick the voice v0.1 ships with

Male and female, one each at most

What didn't go through1 failed
Add Thursday's slot to the calendar

She could not reach the calendar, and will try again

PartColoursMeasured
A row headingprimary ink on the ground12.61:1
Its detail line and the tallysecondary on the ground6.51:1
The row’s outlinethe stage’s card hairline, a non-text mark, so 3.01.84:1 — fails 3

The feed is the clearest argument for the one ground there is: it draws the recording stage’s own two headings. Two screens sharing a heading and not a ground was the thing to fix.

MinimisedCallBanner

The rule: it is the other half of the minimise control, and it ships with it or neither ships. Without the strip a lowered call is a call the member cannot find, and the only remaining way to end it is the system’s own call UI. This is the one component in this section drawn in both schemes, because it is not on the call ground: it is a push-down strip across the member’s own page, so it follows the member’s setting like everything else there.

Feather

Ada is on the line04:12

Low flame

Ada is on the line04:12
PartColoursFeatherLow flame
The sentence and the clockwhite on confirmDeep8.06:16.74:1
The shape is every phone’s in-call bar. The colour is not. A strip across the top with a handset, one sentence and the running clock is the shape every member has already been taught to read — but not its green, which left this system for good on 2026-09-01: live and active are the blue confirm pair. Nor native’s coral fill, because coral is reserved for identity marks, which on the call screen is her portrait ring. It is deliberately a sibling of the live-meeting banner rather than a new idea, down to the measurement.

Consuming it

Web: the --call-* variables in dist/css/airmond.css, in :root — scheme-independent by design, exactly as the --stage-* set is, because the call is pinned while it is live. React Native: the call export from dist/ts/tokens.ts, which carries the ground, its ink, the End disc and its glyph, the quiet wash and the outline that actually finds the control. Nothing in that export is a hex this file decided: the ground and ink are read from the dark scheme, the End disc from stage.stopOnDusk, the outline from rule. Until today these values lived only in mb-airmond-app/src/features/voice/components/, which is what let a whole ground exist without ever being measured.

Ada’s meeting tile

The Recall bot’s 1280x720 camera page: what everyone else in a Meet, Zoom or Teams call sees as Ada’s video. Her portrait and the meeting’s title at the top of a column, then what she is capturing as it happens: action items, notes, knowledge updates, answers and her summary, with the people who join. Decided 2026-09-30 while reviewing a mockup built from a real team call, and the speaking orb approved 2026-10-01. The decisions and their reasons are DESIGN.md’s “the meeting tile” section; this section is their values, drawn.

Drawn once, on dusk, and that is the rule rather than an omission. Every other component section on this page draws its specimens in both schemes side by side, because those surfaces follow the member’s setting. The tile does not: the people looking at it are the other people in the call, and none of them can see a setting, so it always paints the dusk ground and dusk cards. That is the recording stage’s exception, for the stage’s reason, and the call is drawn in one column for the same one. The ground is not a new colour: it is dark.background, lifted behind the column by a radial from dark.card to dark.sidebar.

The tile, state by state

Each frame is the real page at 1280x720, scaled to fit. The bot’s browser draws it at 1:1, so every distance in this section is in the frame’s own pixels.

Pricing review
Ada · taking notes · 5 people
AnswerReading aloud
The launch moved to the week of 12 October, after the pricing review, so the partner accounts join the beta a week later.
Asked by Tom Okafor
Action item
Send the revised pricing sheet to Marcus.
PSPriya ShahFriday
Note
Launch moves to the week of 12 October.

Speaking. The liquid orb behind her portrait, following her voice; “Reading aloud” on the answer she is saying; the live dot pulses. Once the meeting has a title it replaces her name in the header and her name moves to the subtitle. The button fades the orb out and back in.

Ada
AdaPriya’s assistant
Before anyone joins. Her portrait as a crisp circle in a hairline ring, her name and whose assistant she is. No fade into the ground.
Ada
Airmond · not taking notes · 5 people
Member confirmation needed
Open the Airmond app and confirm which participant is you.
PSMLNBTO
Priya, Marcus, Nina and 1 other joined
People join; the member has not confirmed. Joins that land together fold into one row. The pin asks the member to confirm which participant is them, and until then she is not taking notes, so the subtitle has no live dot.
Ada
Airmond · taking notes · 5 people
Answer
Yes, Priya, you’re confirmed as the participant now.
Asked by Priya Shah
PS
Priya ShahConfirmed · Ada works for Priya
PSMLNBTO
Priya, Marcus, Nina and 1 other joined
Confirmed. A blue check on the member’s avatar, then her answer. The subtitle gains the live dot: she is taking notes.
Ada
Airmond · taking notes · 5 people
Muted · still taking notes
Action item
Send the revised pricing sheet to Marcus.
PSPriya ShahFriday
Note
Launch moves to the week of 12 October.
Muted. Mute only silences her voice, so the pill says she is still taking notes, and the feed keeps filling.

Why the middle third, and why an orb rather than a border

The rule: once anyone has joined, everything sits in the middle third of the frame, and “she is speaking” is drawn inside it. Meet, Zoom and Teams crop the tile to fit their grids, and the worst case is the centre 720x720 square, which removes 280 px from each side. A coloured border round the whole frame, which is what the old tile drew, is invisible on at least two sides for everyone who sees that crop. The middle third runs from 427 to 853 px, inside the square, so the header, the orb and the feed survive every crop.

Ada
Airmond · taking notes · 5 people
Note
Launch moves to the week of 12 October.
cropped
cropped
the middle third

The shaded sides are what the 720x720 crop removes; the dashed band is the column every state draws in.

The kind icons

The rule: the app’s own icons for the same meanings, drawn bare, with no tinted box behind them. Phosphor regular, exactly as mb-airmond-app/src/ui/icon-glyphs.tsx maps them, so one set of icons means one set of things across the app and the tile. Each kind has one colour, and only the action item is loud: coral is reserved for it, so the summary, which is her speaking rather than a request to act, takes the confirm blue like an answer.

Action itemThe one thing on the tile that asks someone to act, so it alone takes the coral, and its text is a step larger. dark.coral-deep
NoteMost of the feed, so it recedes: the icon is muted. dark.muted-foreground
Knowledge updateOnly when a member asks her to remember something, so the card says whose request it was. dark.waiting
AnswerSomething she was asked in the call and answered, with who asked. dark.confirm
SummaryHer running summary: a heading, then one line per point, read out line by line. Confirm blue, like an answer: it is her speaking, not a request to act. dark.confirm

The tags, rows and pills use six more from the same set: PencilSimple (Edited), Waveform (Reading aloud, Connecting voice), Clock (due), Check (confirmed), Question (the confirmation pin) and MicrophoneSlash (muted). Path data from phosphor-react-native 3.0.6, MIT.

MeetingHeader

The rule: her portrait is a small circle top-left of the column, beside the title. 64px in a 2px ring, the title in Schibsted Grotesk 700 at 24px (her name until the meeting has a title, clamped to 2 lines), and a 15px subtitle in Instrument Sans that says whether she is taking notes and how many people are in the call. The title starts 106px from the portrait’s left edge, which is exactly clear of the orb at full level. App components that show her portrait beside a title may follow this header.

Taking notes

Ada
Airmond · taking notes · 5 people

Not yet confirmed

Ada
Airmond · not taking notes · 5 people

Speaking

Ada
Airmond · taking notes · 5 people

Titled

Pricing review
Ada · taking notes · 5 people

FeedCard, each kind

The rule: a feed card is a dusk card, never a white one. dark.card with the dusk ink and a 12% hairline, at the system’s card radius. Next to camera feeds in a video grid, white cards read as a screen share and pull the eye off the people. Card text is 21px (an action item 22px at 600), because people read it at grid size. Drawn at 1:1, in the column’s own width.

Action item
Send the revised pricing sheet to Marcus.
PSPriya ShahFriday
Note
Launch moves to the week of 12 October.
Answer
The launch moved to the week of 12 October, after the pricing review, so the partner accounts join the beta a week later.
Asked by Tom Okafor
Knowledge update
Priya wants pricing sheets shared as a doc, never as an attachment.
At Priya Shah’s request
Summary
Where the call has got to
DecidedPriya Shah
Launch moves to the week of 12 October.
NextMarcus Lee
Pricing sheet revised by Friday.
OpenNina Brandt
Whether the partner accounts join the beta.

FeedCard, each state

The rule: a state is a tag at the right of the kind row, and the words that changed stay visible for a moment. New has no tag: the card rises into the top of the feed over motion.arrive, and the next card pushes it down within seconds. Edited is the waiting amber, with the replaced words struck through under the new ones for six seconds. Removed strikes the text before the card leaves. Reading aloud is the coral wash, on the card she is saying.

New

Note
Launch moves to the week of 12 October.

Edited

NoteEdited
Launch moves to the week of 12 October.
Launch stays in the first week of October.

Removed

NoteRemoved
Tom will send the pricing sheet.

Reading aloud

AnswerReading aloud
The launch moved to the week of 12 October, after the pricing review, so the partner accounts join the beta a week later.
Asked by Tom Okafor
The Removed tag’s wash is 0.16, not the mockup’s 0.18. At 0.18 the word measured 4.40:1 against its wash over the card, under the 4.5 a 13 px word needs. 0.16 is the highest wash that clears it: rn.dark.danger at 0.16 (rgba(242, 133, 124, 0.16)) composites to #41373F over the card, and the word measures 4.57:1. Decided 2026-10-01, and claimed in tokens.json.

JoinRow, ConfirmedRow and the ConfirmationPin

The rule: people are rows, not cards, and joins that land together fold into one row. A row is lighter than a card: avatars in the roster hues with ink initials, then the words. Folded, at most 4 avatars overlap by 10px and at most 3 first names are spelled out, then “and N others”. The member she works for gets a blue check once they confirm in the app; until then the ConfirmationPin sits at the top of the feed, a dusk card with an amber hairline and the Question icon.

JoinRow · one person

TO
Tom Okafor joined

JoinRow · folded

PSMLNBTO
Priya, Marcus, Nina and 1 other joined

ConfirmedRow

PS
Priya ShahConfirmed · Ada works for Priya

ConfirmationPin

Member confirmation needed
Open the Airmond app and confirm which participant is you.

StatusPill

The rule: a state the room needs to know about is a pill on its own row between the header and the feed. The raised fill, the label in ink and the detail in muted. Muted says she is still taking notes, because mute only silences her voice, and a pill that just said “Muted” would read as “she has stopped”.

Hearing · connecting

Connecting voice…

Muted

Muted · still taking notes

LiquidOrb

The rule: she is speaking when the orb shows, and the orb follows her voice. Two conic layers of four hues, screen-blended behind her portrait: the brand coral and three of the roster (pink, amber, blue). The edge morphs through three shapes every 5s while the gradient turns every 7s (the inner layer 3.6s and 5s, reversed), and its size is 0.86 + level × 0.26, where level is her output amplitude from 0 to 1, the audio the page is already playing, eased over 130 ms. It fades in and out over 450 ms. While it shows, her ring becomes a 3px band of ground colour so her face stays separate from the light. Chosen over a halo, ripples, a waveform and an aurora because it is the one that still reads at Meet’s 3x3 grid size.

Behind the header portrait

Before anyone joins, at 1.2x the idle ring

Ada
AdaPriya’s assistant

The level here is a smoothed stand-in for her voice. On the tile it is the PCM amplitude of what she is saying. Under prefers-reduced-motion the shape stops morphing and turning, the level is held at 0.5, the live dot stops pulsing, and the fade takes motion.reduced. It animates only transform, opacity and border-radius, with no full-frame blur, so Recall’s headless Chrome draws it at full frame rate.

IdlePortrait

The rule: before anyone joins, her portrait is the whole tile, and it is crisp. A 330px ring, 3px wide, with the photo inset 7px from its outer edge so a band of dusk shows between them; centred at 50% across and 42% down, her name at 42px and whose assistant she is at 19px. When she speaks before anyone joins, the orb behind it is 1.2x the ring, and her name sits 236px below the centre, clear of the orb’s rim at full level (222 px); the token build fails if the two ever meet. An earlier version faded the photo into the ground with a vignette; Cédric did not like it, and the crisp circle matches the header she shrinks into.

Ada
AdaPriya’s assistant

Measured

Every pairing the tile introduces, measured on the surface it sits on. Text is measured against dark.card, which is also the ground’s lightest point, so a pairing that clears here clears anywhere on the ground. This table and the claims in tokens.json come from the same list (meetingPairs in build/meeting-tile.mjs), and the token build fails if a pairing has no claim.

PairingColoursMeasured
Title and every card’s text#DCE0E5 on #1F283311.24:1
Subtitle, kind labels, meta, struck words#93A0AD on #1F28335.58:1
Status pill detail on the raised fill#93A0AD on #2A36464.59:1
Owner pill and status pill label#DCE0E5 on #2A36469.24:1
Edited tag#D9BC6A on #3A35246.62:1
Due date#D9BC6A on #1F28338.05:1
Knowledge update icon#D9BC6A on #1F28338.05:1
Confirmation pin icon#D9BC6A on #1F28338.05:1
Reading aloud tag#FF9081 on #463A404.93:1
Removed tag#F2857C on #41373F4.57:1
Confirmed line under a member#8FB2DC on #1F28336.79:1
Answer icon#8FB2DC on #1F28336.79:1
Note icon#93A0AD on #1F28335.58:1
Action icon#FF9081 on #1F28336.78:1
Summary icon#8FB2DC on #1F28336.79:1
Live dot#FF9081 on #1F28336.78:1
Confirmed badge on the ground#8FB2DC on #171E2A7.62:1
Check on the confirmed badge#171E2A on #8FB2DC7.62:1
Initials on speaker hue 1#18222B on #5CB8F07.35:1
Initials on speaker hue 2#18222B on #FAB55C9.08:1
Initials on speaker hue 3#18222B on #B899F56.84:1
Initials on speaker hue 4#18222B on #EC8DEC7.36:1
Initials on speaker hue 5#18222B on #F58FAD7.23:1
Avatar 1 on the ground#5CB8F0 on #171E2A7.62:1
Avatar 2 on the ground#FAB55C on #171E2A9.42:1
Avatar 3 on the ground#B899F5 on #171E2A7.10:1
Avatar 4 on the ground#EC8DEC on #171E2A7.64:1
Avatar 5 on the ground#F58FAD on #171E2A7.50:1
Card hairline (recorded, not decided)#363E48 on #1F28331.38:1 must stay under 3
Card fill against the ground#1F2833 on #171E2A1.12:1 must stay under 3
Confirmation pin hairline#535142 on #1F28331.86:1 must stay under 3
Portrait ring at rest#525862 on #171E2A2.33:1 must stay under 3

Four marks are recorded rather than relied on. The card’s 12% hairline and its fill do not separate from the ground by luminance, the pin’s amber hairline is a tint, and the portrait’s ring frames a photo. The card is found by its text and its icon, the pin by its icon and title, and her portrait by her face. Whether a feed card on a video tile is a mark the mark rule covers is recorded here as a question for the next pass, not settled by this one.

The specimen palette

The tile carries its palette on itself as the --mt-* set; the published variables are the --meeting-* set in dist/css/airmond.css. Both are generated from the same resolved tones, and each role names the token it reads. Only two of these are new colour decisions, and both are an existing hue at a new opacity: the pin’s hairline and the Removed tag’s wash.

VariablePublished asReadsWhat it draws
--mt-ground--meeting-grounddark.backgroundThe dusk. Every state paints it; it never follows the member’s scheme.
--mt-ground-centre--meeting-ground-centredark.cardThe lift behind the column, the radial’s centre. The lightest point of the ground, so every text claim is measured against it.
--mt-ground-edge--meeting-ground-edgedark.sidebarThe radial’s edge, at the frame’s corners.
--mt-card--meeting-carddark.cardA feed card and the confirmation pin: a dusk card, never a white one.
--mt-ink--meeting-inkdark.foregroundThe title, every card’s text, names in a row.
--mt-muted--meeting-muteddark.muted-foregroundThe subtitle, kind labels, meta rows, struck words, the idle role line.
--mt-hairline--meeting-hairlinern.dark.borderA card’s 12% hairline. Texture: see the measurements.
--mt-raised--meeting-raiseddark.secondaryThe status pill’s fill.
--mt-portrait-ring--meeting-portrait-ringdark.foreground at stage.duskHairlineOpacityThe portrait’s ring at rest: the stage’s dusk hairline.
--mt-portrait-ring-speaking--meeting-portrait-ring-speakingdark.backgroundThe ring while the orb shows: a band of ground that keeps her face apart from the light.
--mt-kind-action--meeting-kind-actiondark.coral-deepThe action item icon (TasksIcon).
--mt-kind-note--meeting-kind-notedark.muted-foregroundThe note icon (DocumentIcon). Most of the feed, so it recedes.
--mt-kind-memory--meeting-kind-memorydark.waitingThe knowledge update icon (LightbulbIcon).
--mt-kind-answer--meeting-kind-answerdark.confirmThe answer icon (ChatIcon).
--mt-kind-summary--meeting-kind-summarydark.confirmThe summary icon (SparkleIcon).
--mt-pin-icon--meeting-pin-icondark.waitingThe confirmation pin’s question icon.
--mt-pin-hairline--meeting-pin-hairlinedark.waiting at 0.28The confirmation pin’s amber hairline. New: the waiting amber at 0.28.
--mt-edited-fill--meeting-edited-filldark.waiting-softThe Edited tag’s fill.
--mt-edited-ink--meeting-edited-inkdark.waitingThe Edited tag’s word and pencil.
--mt-removed-fill--meeting-removed-fillrn.dark.danger at 0.16The Removed tag’s fill. New: the dark danger at 0.16, the highest wash under which its word clears 4.5.
--mt-removed-ink--meeting-removed-inkrn.dark.dangerThe Removed tag’s word.
--mt-reading-fill--meeting-reading-fillrn.dark.coralWashThe Reading aloud tag’s fill: the app’s own coral wash.
--mt-reading-ink--meeting-reading-inkdark.coral-deepThe Reading aloud tag’s word and waveform.
--mt-owner-fill--meeting-owner-filldark.secondaryThe owner pill on an action item.
--mt-due--meeting-duedark.waitingThe due date and its clock.
--mt-confirmed--meeting-confirmeddark.confirmThe confirmed badge and the line under a confirmed member.
--mt-on-confirmed--meeting-on-confirmeddark.backgroundThe check on that badge.
--mt-live-dot--meeting-live-dotdark.coral-deepThe subtitle’s live dot, while she is taking notes.
--mt-initials--meeting-initialslight.foregroundInitials on an avatar: the brand ink, on every roster hue.
--mt-avatar-ring--meeting-avatar-ringdark.backgroundThe ring of ground between stacked avatars.
--mt-shadow--meeting-shadowrn.dark.shadowThe portrait shadows’ ink.
OrbValueReads
--meeting-orb-hue-1#E38273dark.primary
--meeting-orb-hue-2#F58FADstage.speakerHues.4
--meeting-orb-hue-3#FAB55Cstage.speakerHues.1
--meeting-orb-hue-4#8FB2DCdark.confirm
--meeting-orb-glow0 0 56px 14px rgba(227, 130, 115, 0.5), 0 0 120px 30px rgba(245, 143, 173, 0.22)meetingTile.orb.glow
--meeting-orb-size128pxbehind the 64px portrait

Geometry, and the reason for each number

Every distance on the tile is a meetingTile.layout token and carries its reason in the token file, printed here. The next change should argue with the reason rather than the number.

TokenValueWhy
frameWidth1280pxRecall's camera page is 1280x720, so the frame is exactly that and nothing is drawn outside it.
frameHeight720pxThe other half of the 16:9 frame Recall streams.
columnLeft33.33%The middle third starts at 427 px, inside the 280 px the tightest crop (the centre 720x720 square) removes from each side.
columnWidth33.33%Ends at 853 px, inside the 1000 px where that crop stops. Every crop Meet, Zoom and Teams apply keeps the whole band.
columnInset12pxKeeps the cards' edges off the band's own edges; unchanged from the 2026-09-29 stage-feed plan in cs-airmond-api.
headerHeight116pxFits the portrait and a two-line title, so the feed starts at the same height whether or not the title wraps.
portraitSize64pxLarge enough to read as her face at Meet's 3x3 grid size (about a fifth of this), small enough to sit beside the title rather than above it.
portraitTop26px26 px below the frame's top, as approved. At full level the orb reaches 72 px above the portrait's centre (at 58 px), so its top 14 px is clipped by the frame; the approved mockup draws it that way, and the light reads as rising off the top edge.
portraitRingWidth2pxThe resting ring, the stage's dusk hairline; a frame for the photo, not a mark anyone has to find.
portraitRingSpeakingWidth3pxWhile the orb shows, the ring becomes a 3 px band of ground colour, so her face stays separate from the light behind it.
headerTextLeft106pxFrom the portrait's left edge. The orb at full level reaches 104 px from that edge (its centre at 32 px plus 64 x 1.12), so the title never sits under the light.
headerTextTop30pxStarts the title 4 px below the portrait's top, so a two-line title and the subtitle sit beside the portrait rather than under it.
titleSize24pxSchibsted Grotesk 700: the meeting title, or her name until the meeting has one. The largest type in the column, because at grid size it is what identifies the tile.
titleLineHeight1.15Tight enough that two lines still fit the header.
titleMaxLines2A longer title is clamped rather than pushing the feed down.
subtitleSize15pxInstrument Sans; whether she is taking notes and how many people are in the call.
subtitleGap6pxBetween the title and the subtitle.
liveDotSize7pxShown only while she is taking notes; it pulses while she speaks.
pillPaddingY6pxThe status pill, above and below its label.
pillPaddingX12pxThe status pill, each side (the glyph side sits two closer, optically).
pillGap8pxBetween the pill's glyph and its words.
pillSpaceBelow12pxBetween the pill and the first card, so the pill reads as state rather than as the first item.
feedGap10pxBetween two cards or rows in the feed.
feedFadeFrom72%The feed fades out over its last 28 %, so a card cut by the frame's bottom reads as more below rather than as broken.
cardRadius16px (meta.radiusCard)The system's card radius, read from meta rather than restated.
cardPaddingX16pxInside a card, left and right.
cardPaddingTop14pxInside a card, above the kind row.
cardPaddingBottom15pxOne more than the top, which optically balances the descenders of the last line.
kindIconSize19pxThe app's icon for the kind, bare, with no tinted box behind it (2026-09-30).
kindGap8pxBetween the kind icon and its label.
labelSize15pxThe kind label in muted, 600; the card's text is the thing to read, the label only sorts it.
tagSize13pxThe Edited, Removed and Reading aloud tags, 600, right-aligned in the kind row.
tagIconSize13pxThe glyph inside a tag.
tagPaddingY3pxA tag, above and below its word.
tagPaddingX9pxA tag, each side.
tagGap5pxBetween a tag's glyph and its word.
cardTextSize21pxA note or an answer, read by people at grid size; never smaller than this.
cardTextLineHeight1.28Open enough for three lines at 21 px to stay distinct when scaled down.
cardTextWeight450A shade over regular, which keeps light-on-dark text from thinning at grid size.
cardTextGap7pxBetween the kind row and the text.
actionTextSize22pxAn action item asks someone to act, so its text is one step larger.
actionTextWeight600And semibold, for the same reason.
oldTextSize16pxThe words an edit replaced, struck through in muted for six seconds under the new text.
strikeWidth1.5pxThe strike through replaced or removed words.
metaSize15pxThe owner, due date, and who asked.
metaGap11pxBetween the text and its meta row.
ownerAvatarSize22pxThe owner's initials inside the owner pill.
ownerPaddingX11pxThe owner pill's trailing side; the avatar sits 3 px from the leading edge.
rowGap12pxBetween a row's avatars and its words.
pinTitleSize18pxThe confirmation pin's title, 600.
pinBodySize15pxThe confirmation pin's instruction.
rowTextSize19pxA join or confirmation row, which is lighter than a card.
rowSubSize15pxThe line under a confirmed member's name.
avatarSize36pxA person in a join row.
avatarOverlap10pxFolded joins stack their avatars, overlapping by this much.
avatarMax4At most four avatars in a folded row; the words say how many more.
namesShown3At most three first names in a folded row, then "and N others".
avatarRingWidth2.5pxA ring of ground colour between stacked avatars.
initialsSize13pxInitials on a 36 px avatar, 700.
confirmedBadgeSize18pxThe confirmed check on a member's avatar, bottom right.
confirmedCheckSize11pxThe check glyph inside it.
pillTextSize15pxThe status pill under the header: connecting voice, or muted.
pillIconSize17pxThe pill's glyph.
idleRingSize330pxBefore anyone joins, her portrait is the whole tile: a crisp circle whose ring is 330 px across, under half the 720 px square crop's height.
idleRingWidth3pxThe resting ring, drawn inside the 330 px.
idlePhotoInset7pxThe photo sits 7 px in from the ring's outer edge: the 3 px ring, then 4 px of dusk between ring and photo, so the ring reads as a frame. The photo is therefore 316 px across.
idleCentreX50%Centred across the frame.
idleCentreY42%Above centre, to leave room for her name and role under the circle and the orb.
idleNameOffset236pxFrom the circle's centre to the top of her name. The idle orb is 1.2 x 330 = 396 px and at full level scales by 1.12 to a 222 px radius, so 236 keeps her name 14 px clear of its rim while she speaks; the build fails if the orb ever reaches it. Her name sits at 538 px, and her role line ends well inside the 720 px frame.
idleNameSize42pxSchibsted Grotesk 700, her name under the circle.
idleRoleSize19pxWhose assistant she is, in muted.

Consuming it

Web: the --meeting-* variables in dist/css/airmond.css, in :root, scheme-independent like the --stage-* and --call-* sets. TypeScript: the meetingTile export from dist/ts/tokens.ts (tones, the ground gradient, the roster, the orb, the layout with each reason as a doc comment, and the fixed words). The consumer is cs-airmond-api/src/meetings/live-stage-page.ts, which builds the page the bot shows and, today, carries these values by hand.

The chat surface

The seven components a conversation with her is actually made of, every state, drawn in both schemes from the token file and from nothing else. This is the thread itself: the bar you type into, the two credential cards (one that keeps a secret and one that hands it straight to a live page and keeps nothing), the connector, the card that asks you to choose, her screen, and the page an OAuth round trip lands on.

Three rules, and all three have been broken at least once. NO GREEN: confirmation here is the confirm blue pair (confirm on confirmSoft) — the reference app this was drawn from uses a green tick and we do not. Coral is identity plus the ONE primary action per surface: the armed send disc is the only coral fill on this whole surface, and Save securely, Authorize and Reconnect are all the quiet fill. Every icon is a drawn SVG: the revision this section comes from exists because a text glyph rendered as an empty box.

The specimen palette

Each specimen carries one scheme's palette on itself as the --ch-* set, mapped once in site/chat.mjs from tokens/tokens.json. There is no hex anywhere in the section: change a token and every specimen below re-renders. The mapping is the whole surface contract, so it is printed rather than described.

VariableTokenWhat it draws
--ch-groundrn.<scheme>.groundThe thread behind the bar and the cards.
--ch-panelrn.<scheme>.groundAltThe recessed panel: the agent screen, and the option rows inside a card.
--ch-surfacern.<scheme>.surfaceThe card itself.
--ch-fieldrn.<scheme>.surfaceRaisedThe message bar’s pill — the one raised surface on the thread.
--ch-quietrn.<scheme>.sidebarAccentThe quiet fill: the + disc, every button that is not the primary action, the option keys.
--ch-primary-fillrn.<scheme>.primaryFillThe one FILLED control a card is allowed: CredentialHandoff’s Continue. Ink, not coral.
--ch-on-primaryrn.<scheme>.onPrimaryThe label on that fill.
--ch-inkrn.<scheme>.inkTitles, the typed message, every glyph on a quiet fill.
--ch-mutedrn.<scheme>.mutedSubtitles, placeholders, captions, footnotes. Quiet text is muted and never faint — see the note under the icon set.
--ch-coralrn.<scheme>.coralIdentity, and the ONE primary action per surface: the armed send disc, and nothing else here.
--ch-on-coralrn.<scheme>.onCoralThe glyph on coral. Ink, never white — the oldest measured claim in this system.
--ch-confirmrn.<scheme>.confirmConfirmation, the shield, the waveform, the chosen option. Never green.
--ch-confirm-softrn.<scheme>.confirmSoftThe wash under a confirm pill and under the connector’s logo tile.
--ch-warningrn.<scheme>.warningA refusal, in the subtitle slot of the card it belongs to.
--ch-linern.<scheme>.borderEvery hairline inside a specimen: the pill, the cards, the option separators.
--ch-coral-deep<scheme>.coral-deepThe live dot, the required marker on a labelled field, and the state label above a specimen. Coral-deep rather than coral: full coral misses the 3.0 non-text bar on the panel.
--ch-warning-soft<scheme>.waiting-softThe wash under the one refused pill (AuthComplete).
--ch-edge<scheme>.borderThe specimen’s own outer edge, so a light specimen on a dark page still looks like a card.

The icon set

Eleven symbols, drawn once into a <defs> block and referenced by <use>, every one of them stroked in currentColor so it takes the colour of whatever it sits on. Nothing in this section is an emoji or a text character. Two of them — Task and Knowledge — are drawn here but used by the reference densities inside a message bubble rather than by any card below; they are held in the same set so the thread cannot end up with two families of glyph.

Feather (light)

AttachRecordSendTaskKnowledgeStored securelyConfirmedDismissHer screenConnectorScan this

Low flame (dark)

AttachRecordSendTaskKnowledgeStored securelyConfirmedDismissHer screenConnectorScan this
Quiet text is muted, never faint. The app's faint tone measures 3.08:1 on the light field and 2.81:1 on the panel — under the 4.5 body bar and under even the 3.0 non-text minimum. So it carries no word and no mark in this section: every placeholder, caption, footnote and timestamp is muted (6.69:1 light, 4.87:1 dark). Both of those failures are recorded as must-fail claims in the token file, so the tone cannot quietly come back.

Two things on this page are the page talking, not the component. A state label above a specimen goes coral-deep when it is the state the section is arguing about — that is a caption, outside the component, and it is not the surface spending its one coral. And a specimen page runs more ambient loops than a product screen is allowed (the motion budget is at most two per screen; this page draws a live dot per scheme per state), because every state has to be visible at once here.

MessageBar

The rule: one pill, and every control inside it. The + is not a disc standing beside the bar, it is the same 36px circular control as the mic, inset 6px inside a 24px radius, so the bar reads as one object rather than two. The field is 17/23. The trailing slot does two jobs and never both at once: it is the mic while there is nothing to send, and it becomes the coral send the moment there is.

On the web the same bar is one step down (chat density, the standard since 2026-09-24): 42 tall with 30px discs, still 6px in from every side, a 21px radius and a 14/20 field, the size of the messages it sends. Both are drawn from density.composer.

Feather (light)

Resting

Message Ada

Typing · the trailing slot becomes the send

Go with the morning one please

Recording · the field becomes the waveform

0:07

Web · 42 tall, resting

Message Ada

Web · typing

Go with the morning one please

Low flame (dark)

Resting

Message Ada

Typing · the trailing slot becomes the send

Go with the morning one please

Recording · the field becomes the waveform

0:07

Web · 42 tall, resting

Message Ada

Web · typing

Go with the morning one please
PartColoursFeatherLow flame
Placeholdermuted on the field6.69:14.87:1
What you typedink on the field16.12:19.80:1
The + glyphink on the quiet fill14.03:19.24:1
The mic glyphthe ground on the ink disc15.41:112.61:1
The send glyphonCoral — ink, never white5.76:15.92:1
The waveformconfirm on the field (a non-text mark, so 3.0)6.74:15.92:1
The armed disc’s own plate against the field3.0 non-text — only the dark scheme clears it2.80:1 must stay under 34.77:1

The coral disc is found by its glyph, not by its edge. Coral on the white field measures 2.80:1, under the 3.0 a control boundary wants — the same property of this coral that makes onCoral ink rather than white. What identifies the send is the ink arrow on it at 5.76:1 and its fixed position at the end of the pill, so the claim is recorded as a must-fail rather than papered over with a deeper coral that would stop being the brand.

Recording keeps the + and arms the send. There is no use for a mic while a recording is already running, and the thing you want at that moment is to send it. The waveform and the elapsed time are drawn in confirm blue: the send disc stays the one coral fill on the surface.

SecureCapture

The rule: the ask, the field and the reassurance in one card — and a refusal never leaves it. A title, one line of why she needs the value, the field with a quiet Save securely beside it, and a shield footnote saying where the value goes. The button is the quiet fill, not coral: handing over a key is not the primary action of a conversation, it is a chore the card is asking you to finish.

Feather (light)

Empty

Fathom API key
She needs this once to search your meetings.
Paste your Fathom API keySave securely
Stored securely, never shown to Ada.

Filled

Fathom API key
She needs this once to search your meetings.
••••••••••••Save securely
Stored securely, never shown to Ada.

Saved

Fathom webhook secret
Saved securely and kept private.
Saved

Refused · the reason sits in the subtitle slot

Fathom API key
That key was refused. Check it was copied whole.
••••••••••••Save securely
Stored securely, never shown to Ada.

Low flame (dark)

Empty

Fathom API key
She needs this once to search your meetings.
Paste your Fathom API keySave securely
Stored securely, never shown to Ada.

Filled

Fathom API key
She needs this once to search your meetings.
••••••••••••Save securely
Stored securely, never shown to Ada.

Saved

Fathom webhook secret
Saved securely and kept private.
Saved

Refused · the reason sits in the subtitle slot

Fathom API key
That key was refused. Check it was copied whole.
••••••••••••Save securely
Stored securely, never shown to Ada.
PartColoursFeatherLow flame
Titleink on the card16.12:111.24:1
Subtitlemuted on the card6.69:15.58:1
The refusal, in the subtitle slotthe waiting amber on the card5.22:18.05:1
The fieldmuted placeholder, then ink dots, on the recessed ground6.40:16.27:1
Save securelyink on the quiet fill14.03:19.24:1
The shield footnotemuted text, confirm glyph6.74:16.79:1
Savedconfirm on confirm-soft — blue, never green5.78:14.92:1
A refusal is a subtitle, never a toast. The reason a key was rejected belongs directly above the field that holds the key, in the waiting amber, where it can be read twice and where the field it is about is still on screen. A toast puts the explanation somewhere the fix is not, and takes it away on a timer. Saved drops the field entirely: there is nothing left to type, and a filled field that can no longer be edited is a lie about what the card can still do.

CredentialHandoff

The rule: this one hands values to a page that is open right now, keeps nothing, and asks for everything it can already see at once. She is standing in a sign-in form on your behalf and it wants an email and a password; she takes an ordered list of fields in ONE card rather than a card per field, because asking twice interrupts you twice and leaves you waiting mid-flow while she sits on a half-filled page. The values go into the page and are not retained, so the done state says Submitted and never Saved — there is nothing saved to speak of, and a card that says Saved about a value nobody kept is the most expensive kind of wrong.

The subtitle always says two things: why she needs it, and what she does next. "Sign in to Windy so I can pull weather for your flights. I'll click Login after it fills." The first half is what makes handing over a password reasonable; the second is what stops you sitting there wondering whether to press anything. A card that carries only one of the two halves is not finished.

It looks like SecureCapture and it is a different component. They are kept apart because the promise each one makes is different, and the promise is the whole product here.
SecureCaptureCredentialHandoff
What happens to the valueStored for later use, so she can do this again next weekTyped into a live page now, and never retained
The reassurance“Stored securely, never shown to Ada.” — on the asking card“Typed straight into the page and never kept.” on the asking card, “Filled into the page. Secret values were never shown to Ada.” once it is done
The done stateSavedSubmitted
The fieldsOne, a placeholder onlyAn ORDERED LIST, each a label above the input with a required marker, plain or masked
The actionsOne quiet Save securelyThree, in descending weight: Continue, Open the screen, Dismiss
When it appearsOnce, when she needs a key she does not haveWhen a page she is driving asks — carrying every field she can already see

Feather (light)

Awaiting · both fields in one card

Windy sign-in
Sign in to Windy so I can pull weather for your flights. I’ll click Login after it fills.
Email Address*
cedric@airmond.com
Password*
••••••••••••
Continue Open the screen Dismiss
Typed straight into the page and never kept. Ada never sees the value.

Submitted

Windy sign-in
Filled into the page. Secret values were never shown to Ada.
Submitted

Refused

Windy sign-in
Windy refused that sign-in. Check the address, or open the screen and sign in yourself.
Email Address*
cedric@airmond.com
Password*
••••••••••••
Continue Open the screen Dismiss
Typed straight into the page and never kept. Ada never sees the value.

One field · the fallback, when a page reveals one she could not predict

Windy verification code
Windy asked for a code it never mentioned at sign-in. It’s in your inbox now — paste it and I’ll carry on.
Verification code*
••• from your inbox
Continue Open the screen Dismiss
Typed straight into the page and never kept. Ada never sees the value.

Low flame (dark)

Awaiting · both fields in one card

Windy sign-in
Sign in to Windy so I can pull weather for your flights. I’ll click Login after it fills.
Email Address*
cedric@airmond.com
Password*
••••••••••••
Continue Open the screen Dismiss
Typed straight into the page and never kept. Ada never sees the value.

Submitted

Windy sign-in
Filled into the page. Secret values were never shown to Ada.
Submitted

Refused

Windy sign-in
Windy refused that sign-in. Check the address, or open the screen and sign in yourself.
Email Address*
cedric@airmond.com
Password*
••••••••••••
Continue Open the screen Dismiss
Typed straight into the page and never kept. Ada never sees the value.

One field · the fallback, when a page reveals one she could not predict

Windy verification code
Windy asked for a code it never mentioned at sign-in. It’s in your inbox now — paste it and I’ll carry on.
Verification code*
••• from your inbox
Continue Open the screen Dismiss
Typed straight into the page and never kept. Ada never sees the value.
PartColoursFeatherLow flame
Titleink on the card16.12:111.24:1
What she will do nextmuted on the card6.69:15.58:1
The refusal, in the subtitle slotthe waiting amber on the card5.22:18.05:1
Field labelmuted on the card6.69:15.58:1
The required markercoral-deep on the card — an identity mark, not an action5.20:16.78:1
The masked valueink on the recessed ground15.41:112.61:1
Continue, the filled actiononPrimary on primaryFill — ink, never coral16.12:112.61:1
Open the screen, the secondaryink on the quiet fill14.03:19.24:1
Dismiss, plain textmuted on the card6.69:15.58:1
Submittedconfirm on confirm-soft — the same pill as Saved and On5.78:14.92:1

Three actions, three weights, and only one of them is filled. Continue is the ink fill (16.12:1 light, 12.61:1 dark) rather than coral: the armed send disc is this surface's single coral, and a password card is not where to spend it. Open the screen is the quiet fill and carries the screen glyph, because it is the same object as the AgentScreen panel above — it is the takeover escape hatch, the one control that says "you do it, here is the live browser". Dismiss is plain text with no plate at all: it is always available and never the thing to press.

The label is above the input, not inside it. A placeholder is the one piece of text that disappears exactly when it is needed — the moment you start typing — and this is a field you are being asked to fill from memory, under a card that has to be read carefully. The required marker is coral-deep on the card (5.20:1 light, 6.78:1 dark): identity, not an action, and the one place the coral family appears on a card.

One card, every field she can already see — and the single-field card is the fallback, not the default. A sign-in form that wants an email and a password is one ask, so it is one card: two interruptions to answer one question is the version that makes a member wait twice while she holds a half-filled page open. The one-field shape stays for the case that actually needs it — a page that reveals a field she could not predict, a login split across two screens, an unexpected second factor — and each finished card stays in the thread as Submitted, which is also the honest record of what was handed over and when.

The promise is made before the value is typed, not only after. The shield footnote sits on the asking card, where you are about to paste a live password, and the subtitle repeats it once the card is done. A component that explains itself only on the way out has asked for a password in silence.

ConnectorCard

The rule: one row, one verb. A 40px logo tile on the confirm wash, the connector's name, one line of what it buys you, and exactly one control at the end of the row. The verb is the state: Add offers it, Authorize is the sign-in round trip, connected stops offering a verb at all and shows a confirm-blue On pill instead, and an expired grant says Reconnect with the reason in the amber where the description was.

Feather (light)

Offer

Fathom
Search meetings, pull transcripts and summaries.
Add

Authorize

Fathom
One sign-in at Fathom and she can read it. You can revoke it there.
Authorize

Connected

Fathom
Connected · 13 Sep
On

Needs re-auth

Fathom
Access expired — she stopped reading it on Tuesday.
Reconnect

Low flame (dark)

Offer

Fathom
Search meetings, pull transcripts and summaries.
Add

Authorize

Fathom
One sign-in at Fathom and she can read it. You can revoke it there.
Authorize

Connected

Fathom
Connected · 13 Sep
On

Needs re-auth

Fathom
Access expired — she stopped reading it on Tuesday.
Reconnect
PartColoursFeatherLow flame
The logo tileconfirm glyph on the confirm wash5.78:14.92:1
Nameink on the card16.12:111.24:1
What it buys youmuted on the card6.69:15.58:1
Why it stopped workingthe waiting amber on the card5.22:18.05:1
Add · Authorize · Reconnectink on the quiet fill14.03:19.24:1
Onconfirm on confirm-soft5.78:14.92:1

Connected has no button, and that is the point. A working connector should not keep asking to be pressed; the only thing left to say is that it is on. The On pill is the same component as Saved and Connected below it, so one state reads identically wherever it appears.

ChoiceCard

The rule: the options are ONE group, separated by hairlines. Not three floating rows — a single bordered group at radius 12, each option a lettered key, a bold title line and an optional lighter description under it. The × in the header dismisses the question without answering it, which is a real answer and needs somewhere to go. Answered collapses the group to the option you picked, its key in the confirm wash and a confirm tick at the end of the row: the card stops asking.

Feather (light)

Open

How do you want to finish the publish?
I’ll wait for Diff or your DNS step — no finance changes either way.
A
I’ll do Vercel and DNS
I’ll import the repo and add the domain
B
Have Diff wire Vercel
Wait for Diff to try CLI or team access
C
I’m stuck — walk me through it

Answered

How do you want to finish the publish?
You chose B. She is waiting on Diff now.
B
Have Diff wire Vercel
Wait for Diff to try CLI or team access

Low flame (dark)

Open

How do you want to finish the publish?
I’ll wait for Diff or your DNS step — no finance changes either way.
A
I’ll do Vercel and DNS
I’ll import the repo and add the domain
B
Have Diff wire Vercel
Wait for Diff to try CLI or team access
C
I’m stuck — walk me through it

Answered

How do you want to finish the publish?
You chose B. She is waiting on Diff now.
B
Have Diff wire Vercel
Wait for Diff to try CLI or team access
PartColoursFeatherLow flame
Questionink on the card16.12:111.24:1
The line under itmuted on the card6.69:15.58:1
Option titleink on the recessed panel14.71:113.26:1
Option descriptionmuted on the recessed panel6.11:16.59:1
The key, unansweredmuted on the quiet fill5.82:14.59:1
The key and tick, answeredconfirm on confirm-soft5.78:14.92:1
Dismissmuted on the card (a drawn ×, never the letter)6.69:15.58:1

AgentScreen

The rule: it is a live view, not a picture of one — and the only question it answers is who has control. Some steps cannot be handed over as a form. A QR code on WhatsApp Web is the clearest case: there is no field to fill and no value to paste, so she shows you her screen and you scan it with your phone. That is what makes Open the screen on a CredentialHandoff card a destination rather than a label. The panel keeps 16:10 and radius 12 throughout; idle and working are thumbnails beside the thread, watching and handed over are the expanded forms, and the bar across the top is the part that changes.

Working carries a live dot that breathes at the ambient floor (--motion-ambient-min 4s, --motion-ease-ambient, static under prefers-reduced-motion) and names what she is doing. Watching says live and read-only out loud, because a screen you can see but not touch has to say so. Handed over changes the border and the bar to the waiting amber and adds the one control that gives it back.

Feather (light)

Idle · a thumbnail beside the thread

Ada’s screen

Working · the live mark, and what she is doing

Ada’s screen · signing into Windy

Watching · expanded, live, read-only

Live · read-only
You are watching. She is still driving.

Handed over · you are driving, she is waiting

You have the screen. She is waiting.
Ada’s screen · you have control
Hand back to Ada

Why it exists · the QR no form could ever carry

You have the screen. She is waiting.
WhatsApp Web is asking for a QR scan. Scan it with your phone, then hand the screen back.
Hand back to Ada

Low flame (dark)

Idle · a thumbnail beside the thread

Ada’s screen

Working · the live mark, and what she is doing

Ada’s screen · signing into Windy

Watching · expanded, live, read-only

Live · read-only
You are watching. She is still driving.

Handed over · you are driving, she is waiting

You have the screen. She is waiting.
Ada’s screen · you have control
Hand back to Ada

Why it exists · the QR no form could ever carry

You have the screen. She is waiting.
WhatsApp Web is asking for a QR scan. Scan it with your phone, then hand the screen back.
Hand back to Ada
PartColoursFeatherLow flame
The screen and QR glyphsmuted on the panel (a non-text mark, so 3.0)6.11:16.59:1
Captionmuted on the specimen ground6.40:16.27:1
The live dotcoral-deep on the panel (3.0 non-text)4.75:18.00:1
Full coral as that dot — why it is not usedasserted to stay under 3.0 in Feather2.55:1 must stay under 36.45:1
The watching barink on the quiet fill14.03:19.24:1
The live dot ON that barcoral-deep on the quiet fill (3.0 non-text) — the one place the live mark leaves the panel4.53:15.57:1
The handed-over barthe waiting amber on its own wash4.66:16.62:1
The handed-over borderthe waiting amber against the panel (3.0 non-text — it is the boundary that says control moved)4.76:19.50:1
Hand back to AdaonPrimary on primaryFill — the ink fill again, never coral16.12:112.61:1

Handed over is the waiting amber, and that is not decoration. Amber on this system means waiting on you, and a screen you are holding is exactly that: she is blocked until you give it back. It is the one state on this surface allowed to be louder than the accent, it carries a 2px border rather than the 1px hairline everything else uses (4.76:1 light, 9.50:1 dark), and it is the only place on the chat surface where a panel changes its edge. Control changing hands has to be unmistakable at a glance, or somebody types into a screen that is not listening.

Handing control back is a deliberate act, never a timeout. Hand back to Ada is a control the member presses; nothing reverts on a timer. A screen that silently takes itself back while somebody is mid-scan is worse than one that waits, because the member finds out by watching his own input go nowhere. The corollary: while the screen is handed over she does nothing, and the bar says so in her own words.

This panel is where Open the screen goes. The secondary action on a CredentialHandoff card carries the same screen glyph and lands here, which is why the two components share a glyph and why the handed-over state is drawn at all: an escape hatch that cannot show you what you are escaping to is a label.

The live dot is coral-deep, not coral. Full coral measures 2.55:1 on the Feather panel and misses the 3.0 a mark needs; coral-deep measures 4.75:1. This is the same swap the admin app icon makes for its tick, for the same reason — the coral family keeps the identity, the depth buys the contrast.

AuthComplete

The rule: a pill, a title, and one line of what happens next. This is the page a browser lands on at the end of an OAuth round trip, with nothing else on it, so it answers the only two questions anybody has there: did it work, and what do I do with this tab. Centred, and short enough to read without scrolling on a phone.

Feather (light)

Connected

Connected
Fathom is connected
You can close this tab — Ada already knows.

Refused

Not connected
Fathom didn’t connect
Nothing was saved. Tell Ada and she’ll send a fresh link.

Low flame (dark)

Connected

Connected
Fathom is connected
You can close this tab — Ada already knows.

Refused

Not connected
Fathom didn’t connect
Nothing was saved. Tell Ada and she’ll send a fresh link.
PartColoursFeatherLow flame
Connectedconfirm on confirm-soft — blue, never green5.78:14.92:1
Not connectedthe waiting amber on its own wash4.66:16.62:1
Titleink on the card16.12:111.24:1
What happens nextmuted on the card6.69:15.58:1

A refusal here is amber rather than the destructive red: nothing was destroyed and nothing was saved, and the next step is a fresh link rather than an apology. Red on this page would be the loudest thing in a flow whose worst outcome is "press it again".

Consuming it

Web: the --ch-* mapping printed at the top of this section, over the variables in dist/css/airmond.css. React Native: the same values are lightColors / darkColors in dist/ts/tokens.ts, which is where mb-airmond-app already reads ground, surface, ink, muted, coral, onCoral, confirm and confirmSoft — so a component here is a component there with no new token. The measured pairings above are asserted in tokens/tokens.json and re-run by build/contrast-test.mjs on every build, including the five this surface deliberately does not rely on.

Chat density

The chat at Grok's density is the standard since 2026-09-24. Cédric put app.airmond.com beside Grok's desktop app at 858 and 1283 on 2026-09-23 and approved its density, except for the emoji reactions; on 2026-09-24 he asked to keep it: “I do like the way you made it so that we can have more in one conversation now, so let's save that.” Every number below comes out of the density block in tokens/tokens.json, which records each one three times, for the web, the Mac and the native apps, and the apps mirror it.

Three rules. Web means every browser, at every width. The switch is Platform.OS, so a phone browser gets 14/20 as well; native does not move, because 14pt is too small at arm's length and nobody asked. The sidebar keeps its hero head on the desktop (2026-09-24: “on desktop, since we have space, we should still allow for a bit more room ... make the avatar larger again”): only its rows went compact. No emoji reactions: declined on 2026-09-23, so the hover rail stays Reply and More.

The web, at 858

The window Cédric tested, drawn at true size: a 300 sidebar with the 108 portrait and three captioned 48pt discs over rows 32 tall at 14; a 44 header with a 24 portrait and her name at 14/600; the thread at 14/20 in bubbles with a 10 corner and 12/8 insets, 5 between two from one sender and 16 when the sender changes; and a 42 composer with 30 discs. The column is the pane less 16 each side, 526 here, so no bubble grows past min(640, 0.78 × 526) = 410. The draft card under her last message (no Send on it: Airmond never sends from the member's mailbox) keeps its own 14 corner: the bubble's corner moved to 10 and the cards' did not.

Feather (light)

Ada

Can you move my Thursday flight? Anything before nine works.

Swiss had nothing before 07:40, so I took that one and released the 11:20.

Your car is booked for 05:50, which puts you at the gate with twenty minutes spare.

The Lisbon hotel held the room but moved you to the street side. I have asked for the courtyard back.

Perfect. Leave the room as it is if they say no.

Will do. I also drafted the reply to Ákos about Thursday:

Draft replyTo akos.kovacs@example.com · Re: Thursday in Lisbon
Ákos, Thursday works. I land at 09:20 and can be at your office by eleven.
Change itOpen in Gmail
Message Ada

Low flame (dark)

Ada

Can you move my Thursday flight? Anything before nine works.

Swiss had nothing before 07:40, so I took that one and released the 11:20.

Your car is booked for 05:50, which puts you at the gate with twenty minutes spare.

The Lisbon hotel held the room but moved you to the street side. I have asked for the courtyard back.

Perfect. Leave the room as it is if they say no.

Will do. I also drafted the reply to Ákos about Thursday:

Draft replyTo akos.kovacs@example.com · Re: Thursday in Lisbon
Ákos, Thursday works. I land at 09:20 and can be at your office by eleven.
Change itOpen in Gmail
Message Ada

The two windows stack rather than sit side by side for the reason the thread's widest pair does: two 858px windows do not fit across this page, and a specimen clipped at the edge of a scroller is a specimen half-seen. There is no call mark in this header because the sidebar already offers Call; a window without the sidebar (under 720) draws it.

Native and web, the same exchange

One conversation drawn twice in the same 393px column: the native phone's density on the left of each pair, the web's on the right. The words are the same; the web fits more of them in the frame, which is the whole of what was approved. Native keeps ONE gap of 12 whoever is speaking (2026-09-14: “we should not combine messages that follow each other”); the web puts the grouping back at 5 / 16, which Cédric approved with the rest.

Feather (light)

Native · 17/26, corner 18, one 12 gap, 50 header, 48 composer

Ada

Can you move my Thursday flight? Anything before nine works.

Swiss had nothing before 07:40, so I took that one and released the 11:20.

Your car is booked for 05:50, which puts you at the gate with twenty minutes spare.

The Lisbon hotel held the room but moved you to the street side. I have asked for the courtyard back.

Perfect. Leave the room as it is if they say no.

Message Ada

Web · 14/20, corner 10, 5 / 16, 44 header, 42 composer

Ada

Can you move my Thursday flight? Anything before nine works.

Swiss had nothing before 07:40, so I took that one and released the 11:20.

Your car is booked for 05:50, which puts you at the gate with twenty minutes spare.

The Lisbon hotel held the room but moved you to the street side. I have asked for the courtyard back.

Perfect. Leave the room as it is if they say no.

Message Ada

Low flame (dark)

Native · 17/26, corner 18, one 12 gap, 50 header, 48 composer

Ada

Can you move my Thursday flight? Anything before nine works.

Swiss had nothing before 07:40, so I took that one and released the 11:20.

Your car is booked for 05:50, which puts you at the gate with twenty minutes spare.

The Lisbon hotel held the room but moved you to the street side. I have asked for the courtyard back.

Perfect. Leave the room as it is if they say no.

Message Ada

Web · 14/20, corner 10, 5 / 16, 44 header, 42 composer

Ada

Can you move my Thursday flight? Anything before nine works.

Swiss had nothing before 07:40, so I took that one and released the 11:20.

Your car is booked for 05:50, which puts you at the gate with twenty minutes spare.

The Lisbon hotel held the room but moved you to the street side. I have asked for the courtyard back.

Perfect. Leave the room as it is if they say no.

Message Ada

Every number, on every platform

Generated from the block, so the table cannot fall behind it. A bold web value is one the web draws differently from native. none means that platform has no such number, usually because the system draws that part (the Mac's titlebar, its split-view sidebar). guide marks a number the build checks and this page draws but tokens.ts does not export, because the app reads it from somewhere else and a second copy would be a second thing to keep true. The Mac column is recorded, not drawn: the Mac is SwiftUI and its numbers live in ThreadLayout.Mac, ComposerLayout.Mac and MacWindowStyle; they are here so the three shells can be read against each other in one place.

PartTokenWebMacNativeWhat it sets
The thread
threadWeb
bodySize141417Message type. The thread has ONE size; hierarchy inside a bubble is weight and space.
bodyLineHeight202026Message line height. (The Mac sets 14pt with 3 of lineSpacing, which lands at 19.7.)
maxMeasure640640640The widest one bubble may be, before the share binds.
measureShare guide0.780.780.78A bubble's widest as a share of the thread column.
columnInset guide161614The pane's edge to the thread column, both sides. Native here is the phone's thread.horizontalInset; the app's own thread.columnInset (16) is the stage value, which is this row's web column.
maxColumnWidth guidenone1000900The widest the thread column gets. None on the web beside a sidebar: the column is the pane.
textBubbleHorizontalInset121214Text to a bubble's left and right edge.
textBubbleVerticalInset899Text to a bubble's top and bottom edge.
bubbleCornerRadius101418The bubble's corner, both speakers. Cards never read it.
cardCornerRadius guide14none14A card in the thread (a draft, a Slack message, a call, a connect or permission card). Cards keep 14 and never read the bubble's corner.
gapSameSender5512Between two consecutive messages from the same sender.
gapNewSender161612Where the sender changes.
The header
chatHeader / chatHeaderWeb
height guide444050The band: paddingVertical x 2 + avatarSize, which the build checks for web and native. The Mac's is the system's compact titlebar, about 40.
avatarSize242434Her portrait.
avatarSpacing1089Portrait to name.
paddingVertical10none8The band's own padding, the same above and below.
barMargin16none16The bar's horizontal margin, each side: the page gutter.
trailingActionWidth44none44One trailing action's tap target.
trailingClearance12none12The end of her name to the call mark, so a truncated name reads as truncated.
minimumNameWidth72none72The narrowest her name is squeezed to: about eight characters.
trailingActionMarginVertical-10none0How far the call mark's target overhangs the band. Negative pulls it in, so the target fills the band rather than growing it.
nameSize141417Her name's size. Native takes typography.bodyStrong, so its nameType is null.
nameLineHeight20none23Her name's line height.
nameWeight600600600Her name's weight: semibold everywhere.
The composer
composerBar / composerBarWeb
height424248The bar at rest.
control423048One control's square box, which holds the disc.
disc303036The circle drawn inside a control: height less 6 each side.
radius212124The pill's corner: half the resting height, and a literal, so a grown field becomes a rounded box rather than an ellipse.
fieldRest343040The text field's height with one line in it.
glyph161420The + glyph.
glyphSmall151418The mic and send glyphs, a step smaller.
textSize141417The typed text and the placeholder, the same size as the messages it sends. Native takes typography.body, so its text is null.
textLineHeight20none23Their line height.
The sidebar
rail / railWeb
width300232300Full rail width, padding included.
paddingVertical20none20The rail's own padding, top and bottom.
paddingHorizontal12none12The rail's own padding, each side.
rowGap2none2Between two nav rows.
rowPaddingVertical6none10One nav row's padding, above and below its label.
rowPaddingHorizontal10none12One nav row's padding, each side.
glyphGap10none12Glyph column to label.
rowRadius8none10The active capsule's corner.
glyphSlot18none24The fixed glyph column, so every label starts on one x.
glyphSize16none20The glyph drawn in that column.
labelSize141415Row label size.
labelLineHeight20none21Row label line height.
rowHeight guide323241One row, derived: rowPaddingVertical x 2 + labelLineHeight. The build checks it.
headGap10none10Portrait to name in the head.
headPaddingTop6none6Above the head, under the rail's own padding.
headPaddingBottom18none18Under the head's actions, before the first row.
headPortrait10840108Her portrait in the head, the door to About.
headActionDisc guide482448Call / Message / Record. 48 is layout.minTapTarget, which is what the app reads; Call is the one coral disc, the other two are surface.
headActionGlyph guide201220The glyph inside one of those discs.
headActionGap guide12none12Between two discs (spacing.md).
headActionsSpaceAbove guide16none16Her name to the row of discs (spacing.lg).
headCaptionSize guide12none12The word under each disc, in ink. The Mac's discs carry no caption.
headCaptionLineHeight guide15none15Its line height.
headNameSize guide181420Her name in the head: the heading step on each type ramp (typographyWide on the web).
headSubtitleSize guidenone11noneThe Mac's one-line hotkey subtitle under her name. The hero head has none.
footPaddingVertical8none10The pinned foot (Settings), above and below.
footPaddingHorizontal10none12The same foot, each side.
The breakpoint
layout.stageMinWindowWidth
stageMinWindowWidth720nonenoneThe browser width at which the sidebar appears and typographyWide takes over.

density.thread The thread on each platform, measured against Grok (14/20 type, 12/9 insets, a corner of about 10, 5 to 6 within a sender and 16 between, about 460 wide at a narrow window and 640 at a wide one). Two web numbers moved again on 2026-09-24: the vertical inset 9 to 8, because 8 + 20 + 8 lands Grok's measured 34 px one-line bubble within two, and the bubble corner 14 to 10; the Mac has not taken either yet (9 and 14). 640 at 14pt is about 80 characters, what Grok runs at 1283. The web's bubble is min(maxMeasure, measureShare x the thread column), and the column is the pane less columnInset each side with no cap once the sidebar is there (720 and up); below 720 a browser draws the phone's 14 inset and 900 cap. Mac caps its column at 1000. The native column's numbers are checked against the thread block, so the two cannot disagree.

density.chatHeader The band above the thread: her portrait, her name, the call mark. The web is 10 + a 24 portrait + 10 = 44, and the call mark's 44pt target is pulled in by a -10 margin each way rather than growing the band to 64. The Mac uses the system's compact titlebar, which is about 40 (macOS offers 52 or about 40, and 40 is what Grok's Mac app uses). The phone keeps native's ~50pt navigation bar, 8 + 34 + 8.

density.composer The composer's one bar: +, the field and the trailing slot inside one pill. The web is the same construction one step down: 42 boxes holding 30 circles, so the 6 inset survives on every side. The Mac keeps its square control exactly as tall as the field (30), which is the relationship its bottom-aligned row depends on. Coral appears here only on the armed send disc.

density.rail The sidebar. On the web the HERO HEAD is back since 2026-09-24 (Cédric: "on desktop, since we have space, we should still allow for a bit more room ... make the avatar larger again"): the 108 portrait, her name, and Call / Message / Record as captioned 48pt discs, on native's 6 / 10 / 18 spacing, on a 300 rail at every window from 720. Only the ROWS stay compact: 32 tall at 14 (6 + 20 + 6) with a 16 glyph in an 18 column, where native's are 41 at 15. The Mac kept the 2026-09-23 compact head (a 40 portrait, name 14/600, an 11pt one-line subtitle, 24pt Call and Record discs) on a 232 sidebar. The native iPad keeps its rail exactly as it was.

density.layout Where a browser grows the sidebar. 720, was 1000 (Cédric, 2026-09-23): at his 858 window the app dropped to the phone layout, a bottom tab bar under a 626 pane, where Grok beside it kept its sidebar. At 720 the 300 rail leaves a 420 pane whose widest bubble (286 measured: the hover rail's reserved slot binds at 388 - 90 = 298, before the 0.78 share) is still wider than a phone's (262 at 393). ONE switch for the sidebar AND the wide type ramp, because a window with a sidebar is a desktop window. The native iPad keeps its own 768 rule; the Mac always has its sidebar.

The build checks the block against itself and against the thread. Every native thread number must equal the thread block's (the MessageRun gaps included, which is how the old 4 / 12 was caught: neither app drew it); a header must be its padding twice plus its portrait; a composer's disc must sit 6 inside its bar and its corner be half the bar; a sidebar row must be its padding twice plus its line. A value that breaks one of those fails npm run build, not a review.

The colours did not move

The density pass changed sizes and nothing else: every fill is an existing token, coral is still only the Call disc and the armed send. Two things are new enough to measure, because the type came down a step and the sidebar grew a head again.

PartColoursFeatherLow flame
Message text at 14/20ink on her bubble, held to the same 7.0 the 17/26 thread was14.71:111.24:1
The header's name at 14/600ink on the header band (surface)16.12:111.24:1
The composer's 14px placeholdermuted on the field (groundAlt)6.11:16.59:1
The hero head's name and captionsink on the rail ground14.71:113.26:1
A sidebar row at rest, 14pxmuted on the rail ground6.11:16.59:1
The active rowink on the quiet capsule14.03:19.24:1
Message and Record glyphsink on the surface disc (3.0, a glyph)16.12:111.24:1
The Call glyphonCoral on coral: ink, never white5.76:15.92:1
The surface disc against the railnot relied on: the glyph and the caption find it1.10:1 must stay under 31.18:1 must stay under 3
The coral disc against the railidentity, not a boundary: in Feather the glyph and the caption find it2.55:1 must stay under 36.45:1
The header's bottom edgeborder over the page: texture under the band, not a mark anyone has to find1.16:1 must stay under 31.54:1 must stay under 3

Every pairing above that the surface relies on is a claim in tokens.json, re-run by build/contrast-test.mjs; the two discs are recorded as must-fails so nobody later credits a plate with doing the work its glyph does. The header's edge measures 1.16:1 and is texture by the rule on the thread's marks: the band is told from the thread by its fill and its contents, and nothing is lost if the line is not seen.

The thread

The thirteen components the conversation itself is made of, under the cards: the bubble both speakers use, the structured answer she writes inside one, the long message of his that folds, the run of messages from one speaker, the rail that appears beside a message, the timestamp, the day break, the unread divider, the quiet line that records something happening, code, the draft she wrote for you to review and open in your own mail, the pill that says how many messages you have not read, and the action card that steps back once its work is finished. Every specimen is drawn in both schemes, and every distance in them is read out of one block in tokens/tokens.json.

Four rules, and three of them are corrections. A mark that separates or encloses must clear 3.0 on the surface it ACTUALLY sits on: the section rule and the code plate's edge were border, which is toned for the page ground and measures 1.21:1 on a bubble; the quote's rule was full muted at 6.11:1. All three are --th-rule now: see the section below. There is no reaction button: the rail is reply and a three-dot menu, because Airmond is one member and one assistant, and a reaction to your own assistant has no reader. faint is unusable on this surface: it measures 2.81:1 on the bubble fill in Feather and 3.35:1 in Low flame, so every marker, timestamp, caption and footnote here is muted (6.11:1 and 5.58:1). The unread label is coral-deep, never coral: full coral on the thread ground measures 2.68:1, which fails the 4.5 text bar and the 3.0 non-text bar alike, against 4.97:1 and 7.61:1 for coral-deep. Every one of them is claimed in the token file and re-run by build/contrast-test.mjs.

The geometry, and the one place it lives

Everything below is drawn from the thread block in tokens/tokens.json. That block is the single source, and it is new: until 2026-09-14 these numbers existed twice, once in the React Native app and once in the iOS package, with nothing joining them, and the two had already drifted far enough that settling an argument about a disc and a glyph meant reading the approved artifact's own CSS. Both apps now mirror this block, which is why it is printed here in full rather than described. Sizes are px, which the Mac reads as points.

GroupTokenValueWhat it governs
measuremaxBubble640pxThe widest one bubble is allowed to be. Text stops growing here and wraps.
maxColumn900pxThe widest the thread’s own column is allowed to be, bubbles and centred lines alike.
bubblecornerRadius18pxThe bubble’s corner, both speakers.
borderWidth1pxEvery hairline in the thread: the bubble edge, the rule under a section, the unread rule, the plate around code.
insetHorizontal14pxText to the bubble’s left and right edge. Also the gap either side of the unread label, and half of it is the inline plate’s side inset.
insetVertical9pxText to the bubble’s top and bottom edge. Also how far the rail and the timestamp sit off the row’s baseline, so they line up with the last line of text.
bodysize17pxThe thread has ONE type size. Hierarchy inside a bubble is weight and space, never a second size.
lineHeight26pxThe line every block value is measured on top of.
blockparagraphGap8pxExtra space between two paragraphs, and between any two blocks that are not a heading.
listItemGap6pxExtra space between two items of one list.
headingSpaceAbove24pxExtra space above a section heading.
headingSpaceBelow12pxExtra space below a section heading. A horizontal rule takes the same value on both of its sides.
markerToText22pxA bullet’s left edge to its text, which is also where a wrapped line returns to. A blockquote is inset by the same amount.
hangingIndent28pxThe same distance for a NUMBERED list, wider because a number is wider than a dot.
markerDiameter5pxThe bullet itself, centred on the first line.
rungapWithinRun12pxBetween two consecutive messages from one speaker, on the native phone. The same number as the next row since 2026-09-14: native has one gap. The web and the Mac tighten it (density.thread.gapSameSender).
gapBetweenSpeakers12pxBetween the last message of one speaker and the first of the next, on the native phone (density.thread.gapNewSender on the web and the Mac).
railbubbleToFirstIcon12pxThe bubble’s edge to the first icon’s tap target. The timestamp keeps the same distance from the last icon.
iconPitch26pxCentre to centre between two rail icons.
iconSize15pxThe drawn glyph inside that tap target.
tapTarget22pxThe pointer hit box each glyph sits in. On touch the rail is not tapped at all, so this is not the 44pt touch minimum and is not trying to be.
revealMs120msHow long the rail takes to appear on hover. Collapses to nothing under reduced motion.
stampsize12pxThe timestamp, and the unread label.
tonemutedThe timestamp’s tone, resolved from the palette rather than hard-coded.
dayBreaksize12.5pxA centred line: the day break, and the activity line that copies it.
spaceAbove22pxAbove a centred line, and above the unread divider, which is the same furniture.
spaceBelow16pxBelow both of them.
tonemutedThe centred line’s tone, resolved from the palette.
clock24h24h, everywhere a time is written, including the timestamp.
unreadlabelNEWThe word in the break of the rule.
labelTonecoral-deepThat word’s tone, resolved from the palette. Coral-deep, never coral.
ruleToneborderThe hairline either side of it. The one mark in the thread allowed under 3.0, because its LABEL carries the reading at 4.97:1.
draftcornerRadius14pxThe draft card’s corner. Not the bubble’s: a card is a different object and says so.
headPadding16pxThe head’s padding on every side.
headGap12pxThe pen tile to the title block.
markSize44pxThe tile the pen sits on, in the head.
markRadius10pxThat tile’s corner.
markGlyph18pxThe pen drawn on it.
captionGap2pxThe title to the caption under it.
titleSize17px“Draft reply”: the app’s subheading step.
titleLineHeight23pxIts line height.
captionSize13pxWho and what: “To … · Re: …”. It wraps rather than cuts, because the address is the fact a member checks before opening.
captionLineHeight18pxIts line height.
bandPaddingHorizontal16pxThe band her text sits in, each side.
bandPaddingVertical12pxThe same band, above and below.
bandGap12pxHer text to “Read all”; and in the replaced strip, the arrow to its words.
textSize15pxHer text: the app’s callout step.
textLineHeight22pxIts line height.
footPadding16pxThe foot’s padding on every side.
footGap12pxBetween the quiet control and the primary.
quietPadding8px“Change it”: a word with this padding and no plate.
actionHeight38pxThe one primary’s height.
actionInsetHorizontal16pxIts side padding.
actionRadius14pxIts corner.
previewLines6pxLines of her text shown before “Read all”.
previewLinesReplaced3pxLines a REPLACED card shows: the record is the lighter of the two.
previewChars360pxCharacters shown before “Read all”, whatever the line count. The cut is made at a word, in the text, never by the renderer.
previewCrumb40pxA remainder this short is shown rather than hidden: a “Read all” that reveals one word is a control that lied about what it was hiding.
titleDraft replyThe card’s title.
changeLabelChange itThe quiet control: puts this message in the composer’s reply quote.
openLabelOpen inThe primary, followed by the mailbox’s client: “Open in Gmail”, “Open in Outlook”.
openFallbackLabelOpen the draftThe primary when the client is unknown.
readAllLabelRead allOpens the rest of her text in place.
showLessLabelShow lessCloses it again.
replacedLabelReplaced by the draft belowThe record a superseded card becomes, in place of its foot.
newMessagesheight28pxThe pill’s height.
cornerRadius14pxIts corner: exactly half the height, so it is a capsule.
edgeInset12pxHow far it floats in from the edge it anchors to, top or bottom.
insetLeading9pxThe arrow to the leading edge.
insetTrailing10pxThe dismiss hit box to the trailing edge.
gap9pxArrow to label, and label to dismiss. The same number both sides.
arrowSize14pxThe drawn arrow, up at the top anchor and down at the bottom.
dismissHit16pxThe ✕’s hit box.
dismissSize10pxThe ✕ drawn inside it.
labelSize13pxThe count and its word.
labelNumeralstabularTabular, so the count does not jitter going from 9 to 10 under the member’s eye.
labelnew messagesThe words after the count. A token, like the unread divider’s label, because both apps draw them.
labelSingularnew messageThe same words at a count of one. A pill reading “1 new messages” is a pill nobody read back.
longMessagecapLines14pxHow many lines of his body a folded message shows, counted in the body’s OWN line height (364px at 17/26, 280px at the web’s 14/20). Decided from the rendered height, never from a character count.
minHiddenLines3pxThe fewest lines a fold may hide. A message only just over the cap is drawn whole: a control that reveals one or two lines costs the space it saves.
fadeLines2.5pxHow much of the clip the fade covers, in lines. Transparent at the top, the bubble’s own fill at the bottom.
controlGap4pxThe clipped body to the control under it.
controlWeight600pxThe control’s weight. Its size is the body’s own and its colour is the body’s ink.
moreLabelShow moreThe control while folded, followed by a chevron pointing down.
lessLabelShow lessThe control once opened, the same chevron turned 180 degrees.
actionCardcornerRadius14pxThe card’s corner: the thread’s card radius, not the bubble’s.
padding12pxThe card’s padding on every side.
gap12pxBetween the card’s rows: head, brief, transcript or report, foot.
markSize36pxThe disc the kind’s glyph sits on (a phone on confirm-soft for a call).
markGlyph18pxThe glyph drawn on it.
briefRuleWidth2pxThe rule beside the brief, in the mark tone.
briefInset12pxThe brief’s text off that rule.
insetRadius10pxThe corner of the inset the transcript line or the report sits on.
insetPadding12pxThat inset’s padding.
insetGap2pxThe speaker’s name to the line under it.
pillInsetHorizontal8pxThe status pill’s side padding.
pillInsetVertical2pxIts top and bottom padding.
pillDot7pxThe dot in the pill. It breathes while the call is live and stops when it is not.
titleSize17pxThe title, the thing the card is about (the recipient on a call): bodyStrong.
titleLineHeight23pxIts line height.
detailSize15pxEvery explanatory line: the brief, the transcript line, the report. The callout step.
detailLineHeight22pxTheir line height.
labelSize13pxThe caption (the number), the speaker’s name and the pill’s word.
labelLineHeight18pxTheir line height.
titleToneinkThe title’s tone, live AND finished. It is what keeps a finished card scannable.
liveDetailToneinkThe transcript line while the card is live: ink, because it is news.
detailTonemutedEvery explanatory line once the card’s work is finished. Must clear 4.5 on the card and on its inset in both schemes, and is claimed.

thread.measure maxBubble caps one bubble; maxColumn caps the thread's own column. The app's layout.maxContentWidth is a DIFFERENT number read by 39 other screens and must not be conflated with either.

thread.block Spacing INSIDE a bubble holding a structured answer. Every value is the EXTRA space on top of the 26px line rather than a total, so a change to lineHeight carries through instead of being silently overridden.

thread.run THE NATIVE PHONE'S GAPS: ONE 12, NOT A PAIR. A run is consecutive messages from one speaker. Until 2026-09-24 this said 4 within a run and 12 between speakers, which neither app drew: the phone and the iPad collapsed the pair into one 12 on 2026-09-14 (Cédric: "we should not combine messages that follow each other"), and the Mac and the web both moved to 5 / 16 on 2026-09-23. The Mac agent found this block behind all three shells on 2026-09-23 (mb-airmond-ios #530). So this block now states what the phone draws, and the desktop pair lives in `density.thread.gapSameSender` / `gapNewSender`, where the build checks the native column against these two keys. A timestamp at rest belongs to the NEWEST message and to no other on every platform, whatever the gap: that is how a touch device shows the time without a hover to reveal it.

thread.rail The controls beside a message, revealed on hover on a pointer device and by long press on touch. Reply and a three-dot menu, and NO reaction button: decided 2026-09-14, because Airmond is one member and one assistant and a reaction to your own assistant has no reader.

thread.stamp tone is muted and NOT faint. faint measures 3.08:1 on this surface and 2.81:1 on the panel, so it fails the body-text bar and the non-text bar alike; the must-fail claims below are what say so if somebody reaches for it again.

thread.dayBreak Plain centred text carrying the day AND the time, with no pill and no rule around it. The time is the time of the FIRST message after the break, because the break introduces what follows rather than closing what came before.

thread.unread A hairline across the measure, broken for a centred label. Drawn only when at least one message sits ABOVE it: a rule at the very top of a thread labels the whole thread and says nothing.

thread.draft THE DRAFT SHE WROTE, shown in the thread: mb-airmond-app's DraftCard (src/features/drafts/draft-card.tsx, MAR-173 and MAR-174), mirrored here on 2026-09-24. ⛔ THERE IS NO SEND, AND THAT IS THE RULE, NOT A GAP: Airmond never sends from a member's mailbox (Cédric; the api's identity/never-send.ts, three layers including a load-time seal on the SMTP submission ports), so the card has no Send button and no Sent, Sending or Did-not-send state, and the library never draws one. It is review-and-edit: `Open in Gmail` (`Open in Outlook` for a Microsoft mailbox, `Open the draft` when the client is unknown) leaves the app for the draft in his own mail client, the one place it can go out from, and `Change it` puts this message in the composer's reply quote. Revisiting Send is Cédric's decision, not a pull request. THE HEAD: a pen on a `groundAlt` tile, the title `Draft reply`, and a caption naming who and what (`To akos@… · Re: Thursday in Lisbon`, never `Re: Re:`). HER TEXT sits in a band under a hairline, cut IN THE TEXT (not by the renderer) to `previewLines` lines or `previewChars` characters, whichever comes first, at a word, with a `Read all` in the working accent; a remainder under `previewCrumb` characters is shown rather than hidden. THE FOOT: a quiet `Change it` (muted text, no plate) and the ONE ink primary; no coral, no green, nothing faint. THE REPLACED RECORD (MAR-174): when she wrote a newer draft, this card keeps its head and her words (fewer lines, muted) and a `groundAlt` strip with a down arrow says `Replaced by the draft below` in place of the foot, both verbs gone; a used card stays, lighter than the one below it. THE COMPACT CARD: a draft with no stored text draws the head and the primary alone. The card's edge and its band hairlines are the `mark` tone per the rule above (the app still draws them in `border`; recorded, not copied).

thread.newMessages THE FLOATING PILL that says how many messages you have not read, centred in the thread column and `edgeInset` in from the edge it anchors to. ONE component, TWO anchors: unread BEHIND you gives a pill at the top with an up arrow that scrolls to the unread divider, and new messages arriving BELOW while you read history give a pill at the bottom with a down arrow. Same count either way, so the member never has to work out which of two components he is looking at. INK FILLED WITH GROUND TEXT: not confirm, which on this system means confirmed and succeeded and live and would be claiming something untrue about a message nobody has read; and not coral, which the thread has already spent on the composer's send disc. The ✕ dismisses the pill WITHOUT marking anything read: dismissing a reminder is not reading the messages, and a pill that quietly marked them read would destroy the one piece of state the member came back for. The label is TABULAR so it does not jitter as the count goes from 9 to 10 while he is looking at it.

thread.longMessage HIS LONG MESSAGE FOLDS (2026-09-26, mb-airmond-app #324, approved by Cédric: "Both look great."). Grok Bot's pattern: a text message OF THE MEMBER'S taller than `capLines` lines of body is clipped to exactly that many lines, its last `fadeLines` lines are covered by a gradient from transparent to THE BUBBLE'S OWN FILL (memberBubble, or a flavour's own member fill; no new colour, no grey, no hard line), and a `moreLabel` with a chevron sits bottom-left inside the bubble on the text's own left edge. One tap opens it in place and the control becomes `lessLabel` with the chevron turned 180 degrees; per message, in memory, never animated in height. ⛔ THE MEMBER'S OWN WORDS ONLY: never her answer (it is what he came to read, and folding it would fold a streamed answer under him the moment it landed), never a bubble with an attachment, a voice note, a call or a machine card, never a failed send (its retry wrapper is one accessibility element, so the control would be unreachable). ⛔ MEASURED IN RENDERED HEIGHT, NEVER IN CHARACTERS: the body's laid-out height against `capLines` x the body's own line height (364 at the phone's 17/26, 280 at the web's 14/20), and it folds only when at least `minHiddenLines` more lines would be hidden, because a control that reveals one or two lines costs the space it saves. The control is the body's INK at `controlWeight`, the body's own size, never muted and never onCoral (nothing quiet goes inside his bubble). On the web the clip is `overflow: clip`, not `hidden`, so find-in-page and a focused link cannot scroll it. The clipped text stays in the tree: a screen reader, copy and a reply quote act on the whole message.

thread.actionCard AN ACTION CARD IN THE THREAD, and what it does when its work is finished (2026-09-26, mb-airmond-app #319, variant B of three, from Grok Bot's dismissed action card; approved by Cédric: "Both look great."). CallCard is the first instance and the one drawn here: a neutral `surface` card inside the call's bubble, a phone on a `confirmSoft` disc, the recipient as the TITLE, the number as a caption, a status pill, the brief behind a rule, then the last transcript line (or the report, once it lands) on an inset, and a secondary `Follow call` / `View call`. THE RULE, for every action card: while it is live nothing changes; once its work is finished (a call ended or failed, or unavailable with its report in; generally approved, dismissed, done) its title stays `titleTone` and every explanatory LINE (brief, report, last transcript line, consequence text) takes `detailTone`. Labels (the speaker's name, the status pill) and controls keep their own tone. The finished card stops competing with the live messages while its title stays scannable. `detailTone` must clear the 4.5 text bar on EVERY surface it is drawn on, in both schemes: the card (`surface`) and its inset (`groundAlt` in Feather, `surfaceRaised` in Low flame). Claimed below; if one fails, the rule stops rather than ships. The card's edge, the brief's rule and the foot's hairline are the `mark` tone here per the 2026-09-14 rule (the app draws `border` / `borderStrong`; recorded, not copied).

The table is generated from the block, and the block is guarded in both directions. A key the token file loses throws at build time, and a key it gains that nothing here explains throws too, so this table cannot quietly fall behind the values the apps are mirroring. The notes above are the token file's own words, printed rather than paraphrased; where one of them quotes a number measured on a neighbouring surface (stamp's 3.08:1 is the chat card, which is not a thread fill), the measured table under each component below is the authority.

The specimen palette

Each specimen carries one scheme's palette on itself as the --th-* set, mapped once in site/thread.mjs. There is no hex in the section. Five of the tones the block names as strings (stamp.tone, dayBreak.tone, unread.labelTone, unread.ruleTone, mark.tone) are resolved through this table at build time rather than hard-coded, so changing one of those words in the token file actually changes what the page draws. The last two rows are derived rather than read: one ink at two strengths, as rgba, so they composite over whichever surface they are actually painted on.

VariableTokenWhat it draws
--th-groundrn.<scheme>.groundThe thread itself, behind every bubble, every centred line and the pill that floats over it.
--th-surfacern.<scheme>.surfaceThe one raised surface the thread has: the card a draft opens in.
--th-inkrn.<scheme>.inkAda’s words, the member’s words, a heading, an identifier, and the new-messages pill’s own fill.
--th-mutedrn.<scheme>.mutedEvery marker, timestamp, caption, quote and centred line, and the ink every separating mark is drawn from. Quiet text here is muted and never faint: see the corrections above.
--th-confirmrn.<scheme>.confirmA link inside a bubble, and Read all on a draft. The working accent, never coral.
--th-coralrn.<scheme>.coralDrawn once on this page, in the unread comparison, to show why the label is not coral.
--th-borderrn.<scheme>.borderThe bubble’s edge and the unread rule, and nothing else. It is the hairline token for the PAGE ground: on a bubble it measures 1.21:1, so no mark that has to be found may use it. See --th-rule.
--th-altrn.<scheme>.groundAltThe draft card’s pen tile, and the strip a replaced draft becomes.
--th-primary-fillrn.<scheme>.primaryFillThe one FILLED control the thread draws: Open in Gmail on a draft. Ink, not coral. There is no Send: Airmond never sends from a member’s mailbox.
--th-on-primaryrn.<scheme>.onPrimaryThe label on that fill.
--th-confirm-softrn.<scheme>.confirmSoftThe call’s own bubble, which an action card sits inside, and the disc under a call card’s phone. Nothing quiet is ever drawn directly on it: muted there measures 4.05:1 in Low flame.
--th-coral-deeprn.<scheme>.coralDeepThe unread label, and nothing else in the thread. Coral-deep rather than coral: full coral misses the text bar AND the non-text bar on this ground. Exported in the app set on 2026-09-14; before that it existed only as a web token, so the app could not reach it and the label shipped as full coral.
--th-bubble-her<scheme>.mutedAda’s bubble. The web token --muted is a BACKGROUND (#F1F5F8 / #1F2833) and has nothing to do with rn.<scheme>.muted, which is a foreground; the app set reaches the same two values as lightColors.groundAlt and darkColors.surface, and has no single name for the pair yet. That is a wart this table records rather than hides.
--th-bubble-me<scheme>.bubble-memberThe member’s own bubble, blue-soft in both schemes and never coral.
--th-edge<scheme>.borderThe specimen’s own outer edge, so a light specimen on a dark page still looks like a card.
--th-rulemuted at thread.mark.ruleOpacity (0.7)EVERY MARK IN THE THREAD THAT SEPARATES OR ENCLOSES: the rule under a section heading, the quote’s rule, the plate around code, and a draft card’s own edge. Was border for two of those and full muted for the third.
--th-platemuted at thread.mark.plateFillOpacity (0.14)The tint under inline code and under a fenced block. A tint, not a boundary: the edge above is what draws the plate.
--th-card-insetrn.light.groundAlt / rn.dark.surfaceRaisedThe inset inside an action card that holds the last transcript line or the report. The tightest surface the finished-card rule draws muted on.

The marks that separate, and the measurement that was wrong

The rule, and it applies to every component this kit gains: a mark that separates or encloses must clear the 3.0 non-text bar on the surface it ACTUALLY sits on. Not on the page the specimen was measured against. The thread had three marks doing one job and had got two of them invisible and the third six times louder than the other two, and the cause is a single habit: the kit measures a tone in the context its specimens live in, which is a page, while the product draws the same tone inside a bubble that already has an edge.

border is the hairline for the page ground and it clears 3.0 there. Inside a bubble the same token composites to #DBE0E4 and #363E48 and measures 1.21:1 and 1.38:1 against the fill. So a thread mark can never use border, no matter what the page does. All three marks are --th-rule now: muted at 0.7, which is the lowest 0.05 step that clears 3.0 in both schemes (3.18:1 and 3.50:1; the step below it, 0.65, measures 2.87:1 in Feather and misses). The same pair already ships on the Mac, which is where the self-consistent version existed first.

The markWasFeatherLow flameIs nowFeatherLow flame
The rule under a section headingborder1.21:1 must stay under 31.38:1 must stay under 3rule3.18:13.50:1
The rule beside a quotemuted6.11:15.58:1rule3.18:13.50:1
The plate around code, its edgeborder1.21:1 must stay under 31.38:1 must stay under 3rule3.18:13.50:1
The code plate, decided explicitly. A plate can be read by its FILL rather than by its edge, so an edge under 3.0 is defensible on a plate where a rule under 3.0 never is, and that defence does not hold here, because this fill measures 1.22:1 and 1.27:1 against the bubble. It is a tint, not a boundary. So the plate's edge takes the full rule strength like every other enclosing mark, and the fill is claimed as a must-fail rather than pretended to be doing something. The Mac draws that edge at muted 0.5, which measures 2.16:1 here; we take 0.7 instead, because with a fill this quiet a 0.5 edge leaves the plate drawn by nothing measurable at all. The fill moved with it, from the quiet wash to the same ink at 0.14, so the plate and its edge now come off ONE ink rather than two unrelated tokens.

The one mark allowed under 3.0, and why it is not an exception to the rule. The unread divider's rule measures 1.22:1 on the thread ground and stays exactly where it was. The rule above has a clause: unless the component draws something else at the body-text bar that carries the same meaning. The unread divider draws its label at 4.97:1, and that label is what tells you where you stopped reading. The section rule, the quote rule and the plate edge draw nothing else at all, which is why none of them may borrow the same argument. A hover mark is the other case: it appears and disappears under the pointer, and a change reads far below the ratio a static edge needs.

⛔ The one surface the rule tone does not clear, and it is not an exception either. On the member's own bubble it measures 3.07:1 and 2.79:1, which scrapes over the bar in Feather by 0.07 and misses it in Low flame. So his bubble carries no mark at all, which is not a new rule: the thread already keeps every caption out of it because muted there measures 4.05:1, and that rule now covers marks as well as words. His bubble holds his words and nothing else. Raising the opacity was the other option and it costs more than it buys: 0.80 clears his bubble at 3.15:1 and takes the card's hairline to 3.98:1 and 4.63:1 against the page ground, which is the wireframe the 2026-09-02 ruling removed. site/thread.mjs throws if a member row is given a mark, so this page cannot ship the combination it forbids, and the surface below is drawn so the reading is on screen rather than only in a claim.

Feather (light)

What the rule is about: a mark on HER fill, where it clears

Swiss had nothing before 07:40.


Your car is booked for 05:50.

His bubble, carrying no mark at all, which is the rule

Move my Thursday flight if Swiss has anything earlier.

Low flame (dark)

What the rule is about: a mark on HER fill, where it clears

Swiss had nothing before 07:40.


Your car is booked for 05:50.

His bubble, carrying no mark at all, which is the rule

Move my Thursday flight if Swiss has anything earlier.

The mark, by the fill it sits onFeatherLow flame
On Ada’s bubble, where every mark in the thread is drawn3.18:13.50:1
On the member’s bubble, the one surface it does not clear3.07:12.79:1 must stay under 3
Quiet text on the member’s bubble, the rule this extends5.74:14.05:1 must stay under 4.5

Bubble

The rule: one anatomy, two fills, two sides. Hers is the neutral wash on the left, the member's is the blue-soft fill on the right, and both hug their text and stop at measure.maxBubble (640px). There is no avatar, no name and no tail: a thread with one member and one assistant never has to answer which of nine people said this, and every pixel spent saying so is a pixel not spent on the words. Neither fill separates from the thread ground by luminance, which is deliberate and is why the hairline and the side carry the reading.

Feather (light)

One line, hers

Booked. You leave Zurich 07:40 on Thursday.

One line, the member’s

Can you move my Thursday flight?

Multi-paragraph, 8px between

Swiss had nothing before 07:40, so I took that one and released the 11:20.

Your car is booked for 05:50, which puts you at the gate with twenty minutes spare.

Low flame (dark)

One line, hers

Booked. You leave Zurich 07:40 on Thursday.

One line, the member’s

Can you move my Thursday flight?

Multi-paragraph, 8px between

Swiss had nothing before 07:40, so I took that one and released the 11:20.

Your car is booked for 05:50, which puts you at the gate with twenty minutes spare.

Feather (light)

At the cap (640px), hugging beside it, drawn at true width

Three things came in while you were flying. Windy wants its password again, so the weather line on tomorrow’s brief is a day old. The Lisbon hotel confirmed but moved you to a room facing the street, and I have asked for the courtyard side back. Nothing needs you tonight.

Good. Leave the room as it is.

Low flame (dark)

At the cap (640px), hugging beside it, drawn at true width

Three things came in while you were flying. Windy wants its password again, so the weather line on tomorrow’s brief is a day old. The Lisbon hotel confirmed but moved you to a room facing the street, and I have asked for the courtyard side back. Nothing needs you tonight.

Good. Leave the room as it is.

That last pair is drawn at measure.maxBubble, at true width, which is why it is the one pair on this page with its two schemes stacked rather than side by side: two 640px threads do not fit across this column, and capping the specimen to the column instead would have the container doing the work the token is supposed to do. Both schemes are still on screen together, which is what the rule is for.

PartColoursFeatherLow flame
Ada’s wordsink on the bubble fill14.71:111.24:1
The member’s wordsink on the member bubble13.82:18.14:1
Ada’s bubble against the threada non-relationship by design: the hairline and the side carry it1.05:1 must stay under 31.12:1 must stay under 3
The member’s bubble against the threadthe same, one step further from the ground1.11:1 must stay under 31.55:1 must stay under 3
Quiet text inside HER bubblemuted on the bubble fill6.11:15.58:1
Quiet text inside the MEMBER’S bubblemuted on the member bubble, which is why there is never any5.74:14.05:1 must stay under 4.5
faint, on either fillthe tone this surface does not use2.81:1 must stay under 4.53.35:1 must stay under 4.5

Nothing quiet ever goes inside the member's bubble. Muted on that fill measures 4.05:1 in Low flame, under the body bar, and the fix is not a new tone: his bubble holds his words and nothing else. Every caption the thread has (the timestamp, the day break, the activity line, the unread label) sits on the thread ground instead, where muted measures 6.40:1 and 6.27:1. The claim above is recorded as a must-fail so the rule survives somebody deciding a timestamp would look nice in there.

LongAnswer

The rule: a structured answer is one bubble, and the thread has one type size. Title, section heading, bullets, a numbered list, a link, a quote, a rule, inline code and a fenced block all live inside a single bubble, spaced by the block values and by nothing else. Hierarchy is weight and space: there is no second font size anywhere in the thread, because a bubble that starts growing headings turns a conversation into a document.

Every block value is the extra space on top of the 26px line rather than a total, so changing the line height carries through instead of being quietly overridden. A bullet's marker is 5px and its text starts 22px in; a numbered item gets 28px because a number is wider than a dot, and a wrapped line in both returns to the text edge rather than sliding under the marker.

Feather (light)

Thursday, the short version

The flight moved and the hotel held. Two things want you before Wednesday.

What I changed

  • Zurich to Lisbon is now LX318, 07:40, seat 4A.
  • The 11:20 is released, so nothing is double booked.
  • The car comes at 05:50 instead of 09:15.

What I need from you

  1. Say yes to the courtyard room and I will confirm it tonight.
  2. Windy wants its password again before the weather line is current.

The airline’s own page has the new boarding time: swiss.com booking LX318.

Your booking has been changed. New departure 07:40, boarding from 07:05, gate A62.


The key I hold for the weather feed is FATHOM_API_KEY, and it never leaves the vault.

every weekday 07:00
  brief: calendar, weather, flights

Low flame (dark)

Thursday, the short version

The flight moved and the hotel held. Two things want you before Wednesday.

What I changed

  • Zurich to Lisbon is now LX318, 07:40, seat 4A.
  • The 11:20 is released, so nothing is double booked.
  • The car comes at 05:50 instead of 09:15.

What I need from you

  1. Say yes to the courtyard room and I will confirm it tonight.
  2. Windy wants its password again before the weather line is current.

The airline’s own page has the new boarding time: swiss.com booking LX318.

Your booking has been changed. New departure 07:40, boarding from 07:05, gate A62.


The key I hold for the weather feed is FATHOM_API_KEY, and it never leaves the vault.

every weekday 07:00
  brief: calendar, weather, flights
PartColoursFeatherLow flame
Title, heading and bodyink on the bubble fill14.71:111.24:1
Bullets and list numbersmuted on the bubble fill (a mark, so 3.0, and it clears the text bar anyway)6.11:15.58:1
The quote’s wordsmuted on the bubble fill6.11:15.58:1
The rule under a section, and the quote’s rulerule on the bubble fill (a mark, so 3.0): the correction above3.18:13.50:1
border as either of those rulesthe tone they used to be: the page’s hairline, invisible on a bubble1.21:1 must stay under 31.38:1 must stay under 3
A linkconfirm on the bubble fill, the working accent and never coral6.15:16.79:1
Code, inline and fencedink on the tinted plate12.10:18.85:1
The plate’s EDGE against the bubblethe same rule, because a fill this quiet cannot be what draws the plate3.18:13.50:1
The plate’s FILL against the bubblea tint, not a boundary, and claimed as one1.22:1 must stay under 31.27:1 must stay under 3

The rule under a section takes the heading's lower space on both sides. It does the same job a heading does, which is to separate, so it borrows the same number rather than introducing one of its own. The same applies to the quote: it is inset by block.markerToText, the distance a bullet already uses, so the bubble has one left edge for indented things instead of two. Both of them take the same tone as well as the same spacing, which they did not until 2026-09-14: the section rule was border at 1.21:1 and the quote's was full muted at 6.11:1, two marks doing one job five times apart.

MessageRun

The rule: the gap depends on the platform, and this block is the native phone's. On the phone and the iPad every message sits 12px from the one before it, whoever sent it (2026-09-14, Cédric: “we should not combine messages that follow each other”), so the side and the fill are what say who is speaking. On the web and the Mac the grouping is back, approved with the rest of chat density on 2026-09-23: 5px between two from one sender and 16px when the sender changes, drawn in that section at the web's own type. Until 2026-09-24 this block said 4 and 12, which no app drew; the build now checks it against the density block's native column.

Feather (light)

Three from her, then the turn · one 12px gap on native

I moved Thursday to the 07:40.
Swiss had nothing earlier, so the 06:15 was never an option.
Your car is booked for 05:50 to make it.
Perfect, thank you.

Low flame (dark)

Three from her, then the turn · one 12px gap on native

I moved Thursday to the 07:40.
Swiss had nothing earlier, so the 06:15 was never an option.
Your car is booked for 05:50 to make it.
Perfect, thank you.

The timestamp does not key off a run. A time at rest belongs to the NEWEST message in the thread, plus whichever message the member has opened the menu on, so a touch device shows when the conversation last moved without a time under every bubble, which would be a log rather than a conversation.

HoverRail

The rule: reply and a menu, and nothing else. No reaction button. Airmond is one member and one assistant: a reaction on your own assistant's message has no reader, and a reaction on your own message has no sender. What is left is the two things a member actually does with a message, so the rail is 15px glyphs in 22px hit boxes at a 26px pitch, starting 12px off the bubble.

At rest it is invisible and its space is still reserved, so a bubble never moves under the pointer. It fades in over 120ms and collapses to nothing under prefers-reduced-motion. Both icons are drawn SVG symbols: a text glyph renders as an empty box, which is the defect this whole component family inherited its icon rule from.

Feather (light)

At rest, the space reserved

I moved Thursday to the 07:40.

Revealed on hover

I moved Thursday to the 07:40.

The same on the member’s side, mirrored

Perfect, thank you.

Low flame (dark)

At rest, the space reserved

I moved Thursday to the 07:40.

Revealed on hover

I moved Thursday to the 07:40.

The same on the member’s side, mirrored

Perfect, thank you.
PartColoursFeatherLow flame
The two glyphsmuted on the thread ground (a mark, so 3.0)6.40:16.27:1
faint as those glyphsit misses even the 3.0 mark bar in Feather; in Low flame it clears that and still misses the 4.5 a word needs2.94:1 must stay under 33.76:1 must stay under 4.5

22px is a pointer hit box, not a touch target. On touch this rail is not tapped at all: it is reached by long press on the bubble, which is a target the size of the message. So the 22px box is held to what a mouse needs, and the 44pt touch minimum is answered by the bubble rather than by shipping a rail nobody can hit.

Timestamp

The rule: one home, two ways of reaching it. The time sits after the rail, 12px past it, at 12px in muted, and it is the same place in both cases. On a pointer device it is revealed with the rail on hover. On touch there is no hover, so it is shown at rest on the newest message and on the one whose menu is open, and on no other, which is how a thread gets a time without every bubble carrying one.

Feather (light)

Pointer, revealed on hover beside the rail

Boarding from 07:05, gate A62.
18:41

Touch, at rest on the newest message

I moved Thursday to the 07:40.
Your car is booked for 05:50 to make it.
18:44

Low flame (dark)

Pointer, revealed on hover beside the rail

Boarding from 07:05, gate A62.
18:41

Touch, at rest on the newest message

I moved Thursday to the 07:40.
Your car is booked for 05:50 to make it.
18:44
PartColoursFeatherLow flame
The timemuted on the thread ground6.40:16.27:1
faint as the timethe original choice: under the 3.0 mark bar in Feather, under the 4.5 text bar in both2.94:1 must stay under 33.76:1 must stay under 4.5

24h everywhere, including here. The clock is a token rather than a locale guess, because the day break above a message and the time beside it have to agree, and they are drawn by two different components in two different apps.

DayBreak

The rule: plain centred text, and no furniture at all. No pill, no rule, no plate: 12.5px in muted, 22px above and 16px below. It carries the day and the time, and the time is the time of the first message after the break, because a break introduces what follows rather than closing what came before.

Four distances, one shape: today and yesterday by name, a weekday inside the last week, and a date beyond that. Nothing here ever says “2 days ago”: a relative phrase past yesterday makes the reader do arithmetic to find out whether they missed something.

Feather (light)

Today 09:14

Your car is confirmed for 05:50.

Yesterday 18:40

Lisbon held the room, facing the street for now.

Thursday 07:26

Boarding from 07:05, gate A62.

4 September 16:05

The Windy sign-in stopped working, so the weather line is stale.

Low flame (dark)

Today 09:14

Your car is confirmed for 05:50.

Yesterday 18:40

Lisbon held the room, facing the street for now.

Thursday 07:26

Boarding from 07:05, gate A62.

4 September 16:05

The Windy sign-in stopped working, so the weather line is stale.
PartColoursFeatherLow flame
The linemuted on the thread ground6.40:16.27:1
faint as that lineunder the 4.5 text bar in both schemes, and under the 3.0 non-text bar in Feather2.94:1 must stay under 33.76:1 must stay under 4.5

UnreadDivider

The rule: a hairline broken for a centred label, and it only appears when something sits above it. The label is NEW in coral-deep, inset from the rule by bubble.insetHorizontal either side, in the same vertical furniture a day break uses.

The case where it must NOT be drawn: a thread that is entirely unread. A rule at the very top, with nothing above it, labels the whole thread and says nothing. The divider exists to answer one question, which is “where did I stop reading”, and when the answer is “you have not started” the honest mark is no mark. So the rule is: draw it only when at least one read message sits above it, and never as the first thing in the thread.

Feather (light)

Where it appears: read above, new below

Lisbon held the room, facing the street for now.
Ask for the courtyard side.
NEW
Asked. They will answer tonight.
The flight moved to 07:40 as well.

Where it must not: the whole thread is new

Asked. They will answer tonight.
The flight moved to 07:40 as well.

Low flame (dark)

Where it appears: read above, new below

Lisbon held the room, facing the street for now.
Ask for the courtyard side.
NEW
Asked. They will answer tonight.
The flight moved to 07:40 as well.

Where it must not: the whole thread is new

Asked. They will answer tonight.
The flight moved to 07:40 as well.
PartColoursFeatherLow flame
The NEW labelcoral-deep on the thread ground, held to the text bar because it is a word a member reads4.97:17.61:1
Full coral as that labelunder the text bar AND the non-text bar in Feather, which is the whole reason for the swap2.68:1 must stay under 36.14:1
The rule itselfthe hairline, which cannot be what tells you and is not asked to be1.22:1 must stay under 31.36:1 must stay under 3

Coral-deep, in both schemes, because a role takes one token. Full coral clears the text bar on the dark ground (6.14:1) and fails it badly on the light one (2.68:1), so Feather alone forces the choice and Low flame follows it rather than running a second colour for the same word. This is the same substitution the AgentScreen's live dot and the admin icon's tick already make, for the same reason: the coral family keeps the identity, the depth buys the contrast.

The hairline is not load-bearing. It is one ink at one opacity, so it resolves to #E2E4E7 over the Feather ground (1.22:1) and #2F3540 over Low flame (1.36:1), which is a texture rather than a signal. The label is what carries the meaning, which is exactly why the label is held to the 4.5 body bar rather than the 3.0 a mark would get. Both numbers are claimed, so a future rule that tried to do the telling on its own would have to say so out loud.

ActivityLine

The rule: something that happened gets the day break's treatment, never a card. Same 12.5px, same muted, same space above and below. These are the thread's own events rather than messages, so they are not bubbles, they have no side, and they carry no control: a line that needs a button is a card and belongs with the cards above.

Feather (light)

Routine created

Every weekday at 07:00, unless you are flying.

Ada called you

You were in the air, so I left it there.

3 messages with Cash

He has the invoice and will confirm on Monday.

Low flame (dark)

Routine created

Every weekday at 07:00, unless you are flying.

Ada called you

You were in the air, so I left it there.

3 messages with Cash

He has the invoice and will confirm on Monday.
PartColoursFeatherLow flame
The linemuted on the thread ground, the same pairing as the day break6.40:16.27:1

The wording is the component. “Routine created”, “Ada called you”, “3 messages with Cash”: a fact in the fewest words that still say who did it, with no verb tense a member has to decode and no exclamation anywhere.

InlineCode

The rule: monospace on a tinted plate with a 1px edge, and the edge is what draws the plate. The fill is the mark ink at 0.14 and measures 1.22:1 against the bubble: a tint, not a boundary. That is exactly why the edge cannot be the quiet one. It is the only measurable thing there, so it takes the same 0.7 every other enclosing mark in the thread takes. Inline the plate takes half a bubble inset either side and no vertical padding at all, so a sentence with three identifiers in it still sits on the 26px line. A fenced block takes the bubble's own insets and scrolls sideways rather than wrapping, because a wrapped command is a command somebody will mistype.

Feather (light)

Inside prose

The key I hold for the weather feed is FATHOM_API_KEY, and it never leaves the vault.

As a standalone identifier

AIRMOND_WEBHOOK_SECRET

Fenced

every weekday 07:00
  brief: calendar, weather, flights

Low flame (dark)

Inside prose

The key I hold for the weather feed is FATHOM_API_KEY, and it never leaves the vault.

As a standalone identifier

AIRMOND_WEBHOOK_SECRET

Fenced

every weekday 07:00
  brief: calendar, weather, flights
PartColoursFeatherLow flame
The codeink on the tinted plate12.10:18.85:1
The plate’s EDGE against the bubblerule, held to the same 3.0 as every other enclosing mark3.18:13.50:1
The plate’s FILL against the bubblea tint, and claimed as one rather than credited with drawing anything1.22:1 must stay under 31.27:1 must stay under 3
The plate’s fill against the thread groundthe same, for a bubble that is only code1.22:1 must stay under 31.26:1 must stay under 3
border as that edgewhat it was until 2026-09-14, on the surface it was actually drawn on1.21:1 must stay under 31.38:1 must stay under 3

The plate and its edge come off ONE ink now. Until 2026-09-14 the fill was the quiet wash and the edge was the page's hairline: two unrelated tokens, neither chosen against the surface the plate actually sits on, and the edge measuring 1.21:1. Both are muted now, at 0.14 and 0.7, so the plate cannot end up tinted one way and outlined another.

DraftCard

The rule: she drafts, the member sends, and the card is the draft as it sits in his own mailbox. This is the app's DraftCard (MAR-173 and MAR-174), state for state. The head is a pen on a quiet tile, Draft reply, and a caption naming who and what (“To … · Re: …”, never “Re: Re:”), which wraps rather than cuts because the address is the fact a member checks before opening. Her text sits in a band under a hairline, cut to 6 lines or 360 characters at a word, with Read all in the working accent; a remainder under 40 characters is shown rather than hidden. The foot is a quiet Change it, which puts this message in the composer's reply quote, and the ONE ink primary, Open in Gmail (or Outlook), which leaves for the draft in his mail client. It takes the full 640px, because a card is a document and a bubble is a sentence.

⛔ There is no Send, and that is the rule, not a gap. Airmond never sends from a member's mailbox (Cédric; the api's identity/never-send.ts, three layers including a seal on the SMTP submission ports). So this card has no Send button and no Sent, Sending or Did-not-send state, and this library draws none: the place a draft goes out from is his own mail client, which is where the primary leads. Revisiting Send is Cédric's decision, not a pull request. The primary is ink, never coral: the thread spends its one coral on the composer's send disc.

Feather (light)

Live · Gmail, cut with Read all, at true 640px width

Draft reply
To akos.kovacs@example.com · Re: Thursday in Lisbon

Ákos, Thursday works. I land at 09:20 and can be at your office by eleven, so the morning is yours if you want it. If the afternoon is easier I can move the return flight instead: the 17:55 still has seats and the car can wait. Either way, send me the address of the new office and I will have the driver go straight there from the airport rather than to…

Read all
Change itOpen in Gmail

Low flame (dark)

Live · Gmail, cut with Read all, at true 640px width

Draft reply
To akos.kovacs@example.com · Re: Thursday in Lisbon

Ákos, Thursday works. I land at 09:20 and can be at your office by eleven, so the morning is yours if you want it. If the afternoon is easier I can move the return flight instead: the 17:55 still has seats and the car can wait. Either way, send me the address of the new office and I will have the driver go straight there from the airport rather than to…

Read all
Change itOpen in Gmail

That pair is drawn at measure.maxBubble, at true width, so it stacks its two schemes for the same reason the bubble at the cap does: two 640px cards do not fit across this column, and a specimen clipped at the edge of a scroller is a specimen half-seen.

Feather (light)

Expanded · the whole text, and Show less

Draft reply
To akos.kovacs@example.com · Re: Thursday in Lisbon

Ákos, Thursday works. I land at 09:20 and can be at your office by eleven, so the morning is yours if you want it. If the afternoon is easier I can move the return flight instead: the 17:55 still has seats and the car can wait. Either way, send me the address of the new office and I will have the driver go straight there from the airport rather than to the hotel first. Looking forward to it, Cédric

Show less
Change itOpen in Outlook

An Outlook mailbox · Open in Outlook

Draft reply
To akos.kovacs@example.com · Re: Thursday in Lisbon

Ákos, Thursday works. I land at 09:20 and will come straight to you, so eleven is safe. Send me the new office address and I will be there. Cédric

Change itOpen in Outlook

Compact · a draft with no stored text: the head and the door, no Change it

Draft reply
To akos.kovacs@example.com · Re: Thursday in Lisbon
Open the draft

Low flame (dark)

Expanded · the whole text, and Show less

Draft reply
To akos.kovacs@example.com · Re: Thursday in Lisbon

Ákos, Thursday works. I land at 09:20 and can be at your office by eleven, so the morning is yours if you want it. If the afternoon is easier I can move the return flight instead: the 17:55 still has seats and the car can wait. Either way, send me the address of the new office and I will have the driver go straight there from the airport rather than to the hotel first. Looking forward to it, Cédric

Show less
Change itOpen in Outlook

An Outlook mailbox · Open in Outlook

Draft reply
To akos.kovacs@example.com · Re: Thursday in Lisbon

Ákos, Thursday works. I land at 09:20 and will come straight to you, so eleven is safe. Send me the new office address and I will be there. Cédric

Change itOpen in Outlook

Compact · a draft with no stored text: the head and the door, no Change it

Draft reply
To akos.kovacs@example.com · Re: Thursday in Lisbon
Open the draft

The replaced record (MAR-174). When the member asks for a change and she writes a new draft, the earlier card stays as the thread's record of what she wrote: its head and her words, 3 lines of them and muted, and in place of the foot a quiet strip, Replaced by the draft below, with an arrow pointing at the newer card. Both verbs go: Open would lead to the draft he has been told is the old one, and Change it would rewrite what is already superseded. It must read LIGHTER than the live card below it, never wear the quiet button's face.

Feather (light)

The record, then the newer draft under it

Draft reply
To akos.kovacs@example.com · Re: Thursday in Lisbon

Ákos, Thursday works. I land at 09:20 and can be at your office by eleven, so the morning is yours if you want it. If the afternoon is easier I can move the return flight instead: the 17:55 still has seats and the car can wait…

Read all
Replaced by the draft below
Draft reply
To akos.kovacs@example.com · Re: Thursday in Lisbon

Ákos, Thursday works. I land at 09:20 and will come straight to you, so eleven is safe. Send me the new office address and I will be there. Cédric

Change itOpen in Gmail

Low flame (dark)

The record, then the newer draft under it

Draft reply
To akos.kovacs@example.com · Re: Thursday in Lisbon

Ákos, Thursday works. I land at 09:20 and can be at your office by eleven, so the morning is yours if you want it. If the afternoon is easier I can move the return flight instead: the 17:55 still has seats and the car can wait…

Read all
Replaced by the draft below
Draft reply
To akos.kovacs@example.com · Re: Thursday in Lisbon

Ákos, Thursday works. I land at 09:20 and will come straight to you, so eleven is safe. Send me the new office address and I will be there. Cédric

Change itOpen in Gmail
PartColoursFeatherLow flame
The card’s own edge, and the band hairlinesrule on the card, against the thread ground3.20:13.92:1
border as that edgewhat it would have been without the rule above (the app still draws border; recorded, not copied)1.16:1 must stay under 31.54:1 must stay under 3
The card against the thread grounda non-relationship, which is why the edge is the edge1.05:1 must stay under 31.12:1 must stay under 3
Title and her textink on the card16.12:111.24:1
The caption, To · Re:muted on the card6.69:15.58:1
The penmuted on the quiet tile (a glyph, so 3.0)6.11:16.59:1
Read allconfirm on the card: the working accent6.74:16.79:1
Change itmuted on the card: a word, no plate6.69:15.58:1
Open in Gmail, the one primaryon-primary on primary-fill. Ink, never coral16.12:112.61:1
The record’s textmuted on the card: lighter than the live card below6.69:15.58:1
Replaced by the draft belowmuted on the quiet strip6.11:16.59:1
The strip against the cardfound by its words and its arrow, never by its plate1.10:1 must stay under 31.18:1 must stay under 3

NewMessagesPill

The rule: one component, two anchors, and the arrow is the only difference. Unread behind you gives a pill at the top with an up arrow that scrolls to the unread divider; new messages arriving below while you read history give a pill at the bottom with a down arrow. The count is the same number either way, so the words are the same too. It floats 12px in from the edge it anchors to, centred in the thread column, and stands 28px tall at radius 14, which is exactly half its own height and therefore a capsule. It is content width rather than a fixed one: 9px leading, the arrow, 9px, the label, 9px, the ✕ on a 16px hit box, 10px trailing.

Ink filled with ground text. Not confirm, which on this system means confirmed and succeeded and live, and would be claiming something untrue about messages nobody has read. Not coral, which the thread has already spent on the composer's send disc. Ink is the only fill left that a floating object can carry without borrowing a meaning, and it clears everything it might float over by a wide margin.

The ✕ dismisses the pill and marks nothing read. Dismissing a reminder is not reading the messages, and a pill that quietly marked them read on the way out would destroy the one piece of state the member came back for. The label is tabular (newMessages.labelNumerals is tabular), so the count cannot jitter under the member's eye. Crossing from 9 to 10 costs exactly one digit width and never a reflow, and 10 to 11 costs nothing at all: the specimen below draws all three so the two same-width pills can be checked against each other rather than taken on trust.

Feather (light)

Unread behind you · the top anchor, an up arrow to the unread divider

9 new messages
Asked. They will answer tonight.
The flight moved to 07:40 as well.
Good. Leave the room as it is.
Done. Your car is booked for 05:50.

New arriving below · the bottom anchor, a down arrow to the newest

9 new messages
Asked. They will answer tonight.
The flight moved to 07:40 as well.
Good. Leave the room as it is.
Done. Your car is booked for 05:50.

Tabular · one digit width from 9 to 10, nothing at all from 10 to 11, and the singular at 1

1 new message
9 new messages
10 new messages
11 new messages

Low flame (dark)

Unread behind you · the top anchor, an up arrow to the unread divider

9 new messages
Asked. They will answer tonight.
The flight moved to 07:40 as well.
Good. Leave the room as it is.
Done. Your car is booked for 05:50.

New arriving below · the bottom anchor, a down arrow to the newest

9 new messages
Asked. They will answer tonight.
The flight moved to 07:40 as well.
Good. Leave the room as it is.
Done. Your car is booked for 05:50.

Tabular · one digit width from 9 to 10, nothing at all from 10 to 11, and the singular at 1

1 new message
9 new messages
10 new messages
11 new messages
PartColoursFeatherLow flame
The label and the two glyphsthe ground colour on the ink fill15.41:112.61:1
The pill itself against the threada floating object, so the 3.0 mark bar15.41:112.61:1
The same against a bubble scrolling under itthe other surface it floats over14.71:111.24:1
Coral as that fill: why it is not usedthe thread already spent its one coral, and the plate misses the mark bar in Feather anyway2.68:1 must stay under 36.14:1

There is no count on the tab bar to keep this in step with. The pill is the thread's own statement about the thread you are looking at, which is why it carries a number and not a dot: “you have not read nine of these” is a different sentence from “something happened here”, and only one of them is worth interrupting a read for.

LongMessage

The rule: his long message folds, hers never does. A text message of the member’s own that is taller than 14 lines of body is clipped to exactly 14 lines, the last 2.5 of them fade into the bubble’s own fill, and Show more with a chevron sits bottom-left inside the bubble, on the text’s own left edge. One tap opens it in place and the control becomes Show less, the same chevron turned over. It is Grok Bot’s pattern, asked for by Cédric on 2026-09-26 because a wall of his own text pushed the thread off the screen, and it shipped in the app as #324.

⛔ The member’s words only. Her answer is never folded, however long (the LongAnswer above is drawn whole): it is the thing he came to read, and folding it would cost a tap on every reply and fold a freshly streamed answer under him the moment it landed. Grok and Claude.ai do the same. Nor does a bubble fold that carries an attachment, a voice note, a call or a machine card, or a send that failed: its retry wrapper is one accessibility element, so a control inside it could not be reached by a screen reader.
Measured in rendered height, never in characters. The same 1,200 characters are nine lines at 1440 and thirty at 393, so the body reports its laid-out height and the fold is decided against 14 × the body’s own line height: 364px at the phone’s 17/26, 280px at the web’s 14/20. And only when at least 3 more lines would be hidden: a control that reveals one or two lines costs about the space it saves and makes him tap for nothing. The specimens below decide it the same way, from their own rendered height (resize the window and they re-measure); the last one is two lines over the cap, so it is drawn whole.

Feather (light)

Folded · 14 lines, the fade, Show more (try it)

Before Thursday, can you sort these:
move me to the 07:40 if Swiss has it
window seat, not the exit row
car at 05:50, not 06:15
tell Ákos I land at 09:20
ask him for the new office address
driver goes there first, not the hotel
courtyard room, not the street side
late checkout on Friday if they can
dinner Thursday for four, near him
nothing with a set menu
book it under my name
move the 14:00 with Hanna to Friday
keep the 17:00 with Josef
if the 17:55 home is full, take Saturday
put the Windy password back
so the morning brief has weather again
and remind me Wednesday about the charger
Thanks

Opened · the whole message, and Show less

Before Thursday, can you sort these:
move me to the 07:40 if Swiss has it
window seat, not the exit row
car at 05:50, not 06:15
tell Ákos I land at 09:20
ask him for the new office address
driver goes there first, not the hotel
courtyard room, not the street side
late checkout on Friday if they can
dinner Thursday for four, near him
nothing with a set menu
book it under my name
move the 14:00 with Hanna to Friday
keep the 17:00 with Josef
if the 17:55 home is full, take Saturday
put the Windy password back
so the morning brief has weather again
and remind me Wednesday about the charger
Thanks

16 lines · only 2 would hide, so it is drawn whole

Packing for Lisbon:
passport
the grey blazer
two white shirts
running kit
charger and the adapter
the Hanna notes
Ákos’s book back
sunglasses
the small umbrella
headphones
cash for the driver
reading glasses
the blue folder
toothbrush
that’s it

Low flame (dark)

Folded · 14 lines, the fade, Show more (try it)

Before Thursday, can you sort these:
move me to the 07:40 if Swiss has it
window seat, not the exit row
car at 05:50, not 06:15
tell Ákos I land at 09:20
ask him for the new office address
driver goes there first, not the hotel
courtyard room, not the street side
late checkout on Friday if they can
dinner Thursday for four, near him
nothing with a set menu
book it under my name
move the 14:00 with Hanna to Friday
keep the 17:00 with Josef
if the 17:55 home is full, take Saturday
put the Windy password back
so the morning brief has weather again
and remind me Wednesday about the charger
Thanks

Opened · the whole message, and Show less

Before Thursday, can you sort these:
move me to the 07:40 if Swiss has it
window seat, not the exit row
car at 05:50, not 06:15
tell Ákos I land at 09:20
ask him for the new office address
driver goes there first, not the hotel
courtyard room, not the street side
late checkout on Friday if they can
dinner Thursday for four, near him
nothing with a set menu
book it under my name
move the 14:00 with Hanna to Friday
keep the 17:00 with Josef
if the 17:55 home is full, take Saturday
put the Windy password back
so the morning brief has weather again
and remind me Wednesday about the charger
Thanks

16 lines · only 2 would hide, so it is drawn whole

Packing for Lisbon:
passport
the grey blazer
two white shirts
running kit
charger and the adapter
the Hanna notes
Ákos’s book back
sunglasses
the small umbrella
headphones
cash for the driver
reading glasses
the blue folder
toothbrush
that’s it

The fade is the bubble’s own fill, transparent at the top and opaque at the bottom over 65px: memberBubble, or a flavour’s own member fill wherever a flavour sets one. No new colour, no grey, no hard line: a fade in any other colour is a visible band across his words. The control is the body’s ink at 600, the body’s own size, never a quiet tone and never onCoral, because nothing quiet goes inside his bubble. It is a real button with aria-expanded and the clip it controls; opening it does not animate the height (instant, as on WhatsApp), only the chevron turns. The clipped text stays in the tree, so a screen reader, copy, a reply quote and the long-press menu all act on the whole message. On the web the clip is overflow: clip, not hidden: a hidden box is still a scroll container, and find-in-page or a focused link in the hidden lines would scroll the top of the message away under a fade that no longer lines up. The expansion is per message and in memory: it survives the list recycling a row, not a restart.

PartColoursFeatherLow flame
His wordsink on his bubble13.82:18.14:1
Show more / Show less and the chevronthe same ink at 600, on his bubble13.82:18.14:1
muted as that control: why it is inkmuted on his bubble, the reading that keeps every quiet thing out of it5.74:14.05:1 must stay under 4.5

A finished action card

The rule, for every action card in the thread: once its work is finished, the title stays in ink and every explanatory line steps back to muted. Finished means a call ended or failed, or went unavailable and its report has landed; for other cards, approved, dismissed or done. The explanatory lines are the brief, the report, the last transcript line and any consequence text. Labels keep their own tone (the speaker’s name stays confirm, the status pill says Ended in its own quiet pair), and so do the controls. While the card is live nothing changes: the transcript line is news and stays ink. The point is that a finished card stops competing with the live messages around it while its title stays scannable. It is Grok Bot’s dismissed “Action approved” card, picked by Cédric as variant B of three on 2026-09-26, and CallCard is its first instance (the app’s #319).

Feather (light)

Live · on the call

I’m calling the hotel now to ask about the cot.

Hotel Rigi
+41 44 123 45 67
Live

Ask whether the cabin has a cot for Saturday, and hold it if it does.

Hotel Rigi

Let me check the booking, one moment.

Follow call

Finished · the title stays, the details step back

I’m calling the hotel now to ask about the cot.

Hotel Rigi
+41 44 123 45 67
Ended

Ask whether the cabin has a cot for Saturday, and hold it if it does.

She picked up on the second ring. The cabin has a cot and she is holding it for Saturday; no deposit needed.

View call

Low flame (dark)

Live · on the call

I’m calling the hotel now to ask about the cot.

Hotel Rigi
+41 44 123 45 67
Live

Ask whether the cabin has a cot for Saturday, and hold it if it does.

Hotel Rigi

Let me check the booking, one moment.

Follow call

Finished · the title stays, the details step back

I’m calling the hotel now to ask about the cot.

Hotel Rigi
+41 44 123 45 67
Ended

Ask whether the cabin has a cot for Saturday, and hold it if it does.

She picked up on the second ring. The cabin has a cot and she is holding it for Saturday; no deposit needed.

View call

Stacked by scheme, so the live card and the finished one sit side by side in each scheme and can be compared line for line: the title is the same weight and tone in both, and the one line that changes is the one below the brief. The brief is muted in both states (it is the instruction she was given, not news). The card is drawn with the kit’s rule for its edge, its brief rule and its foot, where the app still draws border and borderStrong; recorded, not copied, the same as the draft card.

muted has to clear the 4.5 text bar on every surface it is drawn on, in both schemes, or the rule does not ship. Those surfaces are the card itself (surface) and the inset the report sits on (groundAlt in Feather, surfaceRaised in Low flame). All four clear, the tightest at 4.87:1, and all four are claims in tokens.json. The quiet lines stay ON the card, never out on the call’s own bubble, because muted on confirmSoft measures 4.05:1 in Low flame and fails; that is a must-fail claim too. A flavour that would take the finished card below 4.5 needs a token fix here before it shows call cards.
PartStateColoursFeatherLow flame
The title (the recipient)live and finishedink on the card16.12:111.24:1
The last transcript lineliveink on the inset14.71:19.80:1
The briefbothmuted on the card6.69:15.58:1
The reportfinishedmuted on the inset6.11:14.87:1
The speaker’s namelive (a label keeps its tone)confirm on the inset6.15:15.92:1
The numberbothmuted on the card6.69:15.58:1
The pill, Liveliveconfirm on confirm-soft5.78:14.92:1
The pill, Endedfinishedmuted on groundAlt6.11:16.59:1
muted out on the call’s bubblenevermuted on confirm-soft: why the quiet lines stay on the card5.74:14.05:1 must stay under 4.5
The card’s edge against the call’s bubblebothrule over the card, against confirm-soft (a mark, so 3.0)2.87:1 — fails 32.53:1 — fails 3
Open, and recorded rather than decided here: the card's own edge on the call's bubble. Every tone the finished-card rule is about clears. The one reading on this card that does not is older than this pass and is not a tone it changes: the card's rule edge, composited over the white card and read against the call's confirm-soft bubble, measures 2.87:1 and 2.53:1, under the 3.0 the 2026-09-14 mark rule asks of it, because that rule was measured against the thread ground and this card sits on a bubble (the card's fill against the bubble is 1.17:1 and 1.38:1). The app draws border there, which is lower still. Fixing it is a mark-rule decision (a stronger edge, or a card that does not sit inside a tinted bubble), so it is shown failing, loudly, instead of being quietly left out of the table.

Consuming it

Colours: the --th-* mapping printed above, over the variables in dist/css/airmond.css; the same values are lightColors / darkColors in dist/ts/tokens.ts, where the separating mark is the rule tone added on 2026-09-14. Geometry: the thread block in tokens/tokens.json, which both apps mirror and neither app may fork: the draft card and the pill are in it on the same terms as the other nine. Every pairing this surface RELIES ON is asserted in that file's claims array and re-run by build/contrast-test.mjs on every build, together with the ones it deliberately does not rely on, which are recorded as must-fails so a value drifting into being load-bearing would have to say so out loud. Not every cell above is a claim: a measured cell with no claim behind it is a reading, and the array is the shorter list of readings that are a contract.

Four controls a member could not find

Four fixes out of 84 captures of the Settings, Knowledge and Tasks screens: the card that had no edge, the secondary button that had no plate, the disabled recipe that halved the label and the fill in one step, and faint, which was carrying every line that says where a piece of information came from. All four are token-level, so they are ONE kit change and one app re-sync rather than eleven screen edits. Each is drawn before and after, side by side, in both schemes, with the measurement that decided it.

One principle, and all four fixes are it. A control or a container the member has to find must clear the 3.0 non-text bar on the surface it actually sits on, and a control's words must clear 4.5 whatever state it is in. Both halves do work here. The first is the rule the thread's marks are already held to (the marks that separate), and the clause that matters is the surface it actually sits on: border clears 3.0 on a page ground and draws nothing on a card, which is how a hairline came back as the wrong token twice. The second is the half a state can quietly break: a control that reads at 15.41:1 enabled and 3.10:1 disabled has not been dimmed, it has been deleted.

The specimen palette

Each specimen carries one scheme's palette on itself as the --co-* set, mapped once in site/controls.mjs from tokens/tokens.json. There is no hex anywhere in the section, including in the before states: border over a card, the groundAlt fill and the 0.5 dim are all recomputed on every build, so a retired recipe cannot come back through a number somebody typed. Every tone is read out of the app's own rn block, because that block is what mb-airmond-app mirrors and a tone the app cannot reach is a fix the app cannot ship.

VariableTokenWhat it draws
--co-groundrn.<scheme>.groundThe page behind every card on every screen, and the surface a control most often sits on.
--co-panelrn.<scheme>.groundAltThe recessed panel, and the fill the secondary button wore until 2026-09-14.
--co-surfacern.<scheme>.surfaceThe card itself.
--co-raisedrn.<scheme>.surfaceRaisedThe one raised surface: the message bar’s field, which is lifted off the card in Low flame and flush with it in Feather.
--co-quietrn.<scheme>.sidebarAccentThe quiet fill under a chip, an option key and a status pill.
--co-member-bubblern.<scheme>.memberBubbleThe member’s own bubble, which is where faint got its worst reading in the whole product.
--co-inkrn.<scheme>.inkTitles, a secondary control’s label, every glyph on a quiet fill.
--co-mutedrn.<scheme>.mutedCaptions, and every provenance line, date and timestamp in the product since faint was retired from member-facing text.
--co-faintrn.<scheme>.faintDrawn on this page ONLY to show what it measures. It carries no word anywhere in the product.
--co-coralrn.<scheme>.coralIdentity, and the one primary action a surface is allowed.
--co-on-coralrn.<scheme>.onCoralThe label on that coral. Ink, never white.
--co-primary-fillrn.<scheme>.primaryFillThe ink fill under the one FILLED control a card draws.
--co-on-primaryrn.<scheme>.onPrimaryThe label on that fill.
--co-disabled-fillrn.<scheme>.disabledFillThe plate under a disabled control, at FULL strength. New on 2026-09-14.
--co-on-disabledrn.<scheme>.onDisabledThe label on that plate, also at full strength, and held to the full body bar.
--co-linern.<scheme>.borderThe retired hairline, drawn on this page only to measure it. It is toned for a page ground and cannot draw a card.
--co-rulern.<scheme>.ruleThe tone every separating mark and every control outline is drawn in: the card’s hairline, the secondary’s outline, the outline a disabled control keeps.
--co-coral-deep<scheme>.coral-deepThe state label above a specimen when it is the state this section is arguing for. A caption on the page, outside the component.
--co-edge<scheme>.borderThe specimen’s own outer edge, so a light specimen on a dark page still looks like a card.

The icon set

Five symbols, drawn once into a <defs> block and referenced by <use>, each stroked in currentColor so it takes the colour of whatever it sits on. Nothing here is an emoji or a text character: the chevron on a card row and the clock on a timestamp are exactly the sizes at which a missing glyph renders as an empty box that nobody looks at twice.

Feather (light)

Opens a screenWhere it came fromWhen it was addedWhen it changedUnavailable until something else is done

Low flame (dark)

Opens a screenWhere it came fromWhen it was addedWhen it changedUnavailable until something else is done

1 · The card gets a hairline

The rule: a container the member has to find is a shape, and a shape needs an edge or a fill that measures. The card had neither. Its fill against the page ground measures 1.05:1 in Feather and 1.12:1 in Low flame, and the only other thing separating it was a shadow at 6%. That is a third of what the 3.0 non-text bar asks for, on every card on every screen.

The line that comes back is not the line that was removed. The one retired on 2026-09-02 was border, the hairline token for a page ground. Drawn on a card it composites to #E8E9EA and #363E48 and measures 1.16:1 and 1.54:1 against the ground it is meant to separate the card from: a wireframe that was also invisible, which is the worst of both. The line now is rule, the one tone this system draws every separating mark in, at layout.card.borderWidth: 1. It composites to #838E97 and #707C88 and measures 3.20:1 and 3.92:1.

Feather (light)

Before · the shadow alone

Knowledge
Three doors are open. Ada reads them when you ask her something.
Calendar
Manage doors

After · a 1px rule hairline

Knowledge
Three doors are open. Ada reads them when you ask her something.
Calendar
Manage doors

Low flame (dark)

Before · the shadow alone

Knowledge
Three doors are open. Ada reads them when you ask her something.
Calendar
Manage doors

After · a 1px rule hairline

Knowledge
Three doors are open. Ada reads them when you ask her something.
Calendar
Manage doors
PartColoursFeatherLow flame
The card’s FILL against the groundthe whole of what separated a card until 2026-09-141.05:1 must stay under 31.12:1 must stay under 3
border as the edgethe line removed on 2026-09-02, measured on the surface it was actually drawn on1.16:1 must stay under 31.54:1 must stay under 3
rule as the edgethe line that came back: one tone, one strength, every surface3.20:13.92:1
The titleink on the card16.12:111.24:1
The captionmuted on the card6.69:15.58:1
The row’s chevronmuted on the card (a non-text mark, so 3.0)6.69:15.58:1
⚠️ This one is a trial, and it is reversible in one value. Cédric, 2026-09-14: “I am not sure we need the hairline, but I am also not opposed. We can just try.” Set layout.card.borderWidth back to 0 and every card on every screen loses the line again, with no call-site change, because the width is a token the components read, not a style eleven screens repeat. If it reads heavy, the alternative is a stronger FILL rather than an edge, which is also one token (surface) and also no call-site change. What is not on the table is going back to a shape that measures 1.05:1 and calling the shadow an edge.

Both specimens are the same size. The unruled card carries a transparent border of the same width, so the pair differs by the line and by nothing else. Drawn the other way, the after card would be 2px bigger than the before card and the comparison would be about two boxes rather than about one hairline.

2 · secondary stops being an invisible button

The rule: a button is a plate and a label, and if the plate carries nothing the label is carrying a button on its own. secondary was a groundAlt FILL at 24 call sites. It measures 1.05:1 against the page ground and 1.10:1 on a card in Feather, 1.05:1 and 1.18:1 in Low flame. The label read perfectly (ink on that fill measures 14.71:1) and the shape around it was not there, so the control looked like a line of bold text that happened to be tappable.

It is an OUTLINE now: no fill at all, a 1px rule outline, an ink label. The outline measures 3.24:1 on the ground and 3.34:1 on a card in Feather, 3.78:1 and 3.50:1 in Low flame. A secondary appears in both places, so it has to be findable in both, which is why the pairing is claimed twice rather than once.

Feather (light)

Before · a groundAlt fill, on the ground

Save Manage doors

After · a rule outline, on the ground

Save Manage doors

Low flame (dark)

Before · a groundAlt fill, on the ground

Save Manage doors

After · a rule outline, on the ground

Save Manage doors

Feather (light)

Before · the same fill on a card

Connections
Calendar and email are connected. Two more are waiting on you.
Save Manage doors

After · the same outline on a card

Connections
Calendar and email are connected. Two more are waiting on you.
Save Manage doors

Low flame (dark)

Before · the same fill on a card

Connections
Calendar and email are connected. Two more are waiting on you.
Save Manage doors

After · the same outline on a card

Connections
Calendar and email are connected. Two more are waiting on you.
Save Manage doors
PartColoursFeatherLow flame
The OLD fill against the groundgroundAlt: the measurement that retired it1.05:1 must stay under 31.05:1 must stay under 3
The OLD fill on a cardthe same fill, one surface further in1.10:1 must stay under 31.18:1 must stay under 3
The old label, on that fillink on groundAlt: the half that was always fine, and was carrying the whole control14.71:113.26:1
The outline on the groundrule, a non-text mark, so 3.03.24:13.78:1
The outline on a cardthe same tone composited over a card instead3.34:13.50:1
The label on the groundink on the ground15.41:112.61:1
The label on a cardink on the card16.12:111.24:1
The PRIMARY beside itonPrimary on primaryFill: ink, never coral16.12:112.61:1
The primary’s plate against the groundwhat a filled control measures, for comparison15.41:112.61:1

The outline is the same tone and the same strength as the card's hairline, and that is the point. The card's edge and the secondary's outline composite to the same colour on the same surface, so a card holding a secondary button draws one mark at one weight instead of two marks a member has to tell apart. A filled secondary would also have worked arithmetically, and it was rejected: a fill deep enough to clear 3.0 against both the ground and a card is a fill deep enough to compete with the primary, and then the surface has two buttons that look equally pressable.

The primary did not move. It measures 15.41:1 against the ground in Feather and 12.61:1 in Low flame, which is twelve times what the old secondary managed and roughly four times what the new outline does. That gap is the hierarchy: one filled action, one outlined one, and the difference visible at a glance rather than only on inspection.

3 · The disabled recipe is a token pair, not an opacity

The rule: a disabled control should look unavailable, not absent. The old recipe was opacity: 0.5 on the whole group and nothing else, which halves the label and the fill together, so a control loses its words and its shape in one step. Worse, it takes the caption down with them, and the caption is the copy that says what enabling the control would cost. The member is left with a grey smear where the explanation was.

Every reading below is what that opacity actually produced, recomputed here rather than remembered. Two are worth saying out loud: a disabled SECONDARY's label resolves to #888E94 on a #F5F8FA plate and measures 3.10:1 in Feather, and a disabled CORAL primary's label resolves to #888E94 on a #FCB3AB plate and measures 1.92:1, a label all but gone into its own button.

The control, dimmed the old wayWhat the dim resolves toFeatherLow flame
A secondary, on the groundink on groundAlt, both dimmed over ground3.10:1 must stay under 4.54.25:1 must stay under 4.5
The same secondary on a cardink on groundAlt, both dimmed over surface3.04:1 must stay under 4.54.29:1 must stay under 4.5
A quiet buttonink on sidebarAccent, both dimmed over ground3.02:1 must stay under 4.53.59:1 must stay under 4.5
The ink primaryonPrimary on primaryFill, both dimmed over ground3.25:1 must stay under 4.54.16:1 must stay under 4.5
A CORAL primary, the worst caseonCoral on coral, both dimmed over ground1.92:1 must stay under 4.52.45:1 must stay under 4.5
The caption explaining the statemuted dimmed over ground, which does not dim with it2.20:1 must stay under 4.52.58:1 must stay under 4.5

The recipe now is a token pair at full strength: disabledFill under onDisabled, keeping the same rule outline an enabled secondary has. The label measures 5.82:1 in Feather and 4.59:1 in Low flame, which clears the full body bar rather than approaching it. The outline is not optional. The plate alone measures 1.10:1 and 1.37:1 against the ground, so the plate says unavailable and the outline is the only thing left saying this is still a control.

Feather (light)

Enabled, for comparison

Your number is verified, so Ada can call you now.
Call me now Schedule a call

Before · opacity 0.5 on the whole group

Calls are off until your number is verified. Ada will keep messaging either way.
Call me now Schedule a call

After · the pair at full strength, outline kept

Calls are off until your number is verified. Ada will keep messaging either way.
Call me now Schedule a call

Low flame (dark)

Enabled, for comparison

Your number is verified, so Ada can call you now.
Call me now Schedule a call

Before · opacity 0.5 on the whole group

Calls are off until your number is verified. Ada will keep messaging either way.
Call me now Schedule a call

After · the pair at full strength, outline kept

Calls are off until your number is verified. Ada will keep messaging either way.
Call me now Schedule a call
PartColoursFeatherLow flame
The disabled labelonDisabled on disabledFill, both at full strength5.82:14.59:1
The disabled PLATE against the grounda non-relationship, and the reason the outline stays1.10:1 must stay under 31.37:1 must stay under 3
The outline it keepsrule over the disabled plate, against the ground (3.0 non-text)3.38:14.16:1
The caption explaining the statemuted on the ground, at full strength6.40:16.27:1
The same caption under the old recipethe sentence the dim was quietly taking with it2.20:1 must stay under 4.52.58:1 must stay under 4.5
control.disabledOpacity is 1 on purpose. It is not a value anybody needs to multiply by; it is the retirement of opacity: 0.5 written down as a value an app reads rather than a rule somebody has to remember. A group that still dims itself is a group that has ignored the pair, and the token says so in the one place a component looks.

The before specimen dims today's controls, and the before table measures the recipe as it actually shipped. Those are two different questions and both are worth answering. The specimen changes exactly one variable, the opacity, so what is on screen is what a dim does rather than what a dim and a retired fill do together. The table goes the other way and measures the real pairs the app drew, the groundAlt secondary fix 2 has since retired included, because the point of a before state is to record what shipped rather than a tidier version of it.

The coral primary is the case that made this a token pair rather than a smaller opacity. Dimming a coral fill and an ink label by the same factor moves them towards each other, not apart: the label lands on 1.92:1 in Feather. No single opacity fixes that, because the two tones start on opposite sides of the ground the group is dissolving into. A disabled control therefore stops being a dimmed version of its enabled self and becomes its own pair, which also means a disabled coral primary and a disabled secondary look like the same thing, which they are.

4 · faint is retired from member-facing text

The rule: the line that says where information came from is read, not skimmed, so it takes the body bar like any other sentence. faint measures 3.08:1 on a card in Feather and 3.35:1 in Low flame, and it never clears the 4.5 body bar on any ground the product has. Its worst reading, 2.43:1, is inside the member's own bubble. What it carried is provenance, dates and timestamps: from your calendar, added 4 September, updated 18:41. The dim was landing on exactly the copy a member reads to decide whether to trust the rest of the card.

Every one of those readings is muted now. One exception, and it is not a tone change: muted inside the member's own bubble measures 5.74:1 and 4.05:1, so it misses the body bar in Low flame and his bubble takes the same answer in both schemes. A line that used to sit in there moves out onto the thread ground instead, where muted measures 6.40:1 and 6.27:1. It is the same rule the thread's marks landed on for the same fill.

Feather (light)

Before · faint on a card

Thursday in Lisbon
Zurich to Lisbon is LX318 at 07:40, seat 4A. The car comes at 05:50.
From your calendar · Google Workspace
Added 4 September
Updated 18:41

After · muted

Thursday in Lisbon
Zurich to Lisbon is LX318 at 07:40, seat 4A. The car comes at 05:50.
From your calendar · Google Workspace
Added 4 September
Updated 18:41

Low flame (dark)

Before · faint on a card

Thursday in Lisbon
Zurich to Lisbon is LX318 at 07:40, seat 4A. The car comes at 05:50.
From your calendar · Google Workspace
Added 4 September
Updated 18:41

After · muted

Thursday in Lisbon
Zurich to Lisbon is LX318 at 07:40, seat 4A. The car comes at 05:50.
From your calendar · Google Workspace
Added 4 September
Updated 18:41

Feather (light)

Before · faint inside the member’s own bubble, the worst reading in the set

Move my Thursday flight if Swiss has anything earlier.
From your calendar · Google Workspace
Added 4 September
Updated 18:41

After · the line moves out of the bubble, in muted

Move my Thursday flight if Swiss has anything earlier.
From your calendar · Google Workspace
Added 4 September
Updated 18:41

Low flame (dark)

Before · faint inside the member’s own bubble, the worst reading in the set

Move my Thursday flight if Swiss has anything earlier.
From your calendar · Google Workspace
Added 4 September
Updated 18:41

After · the line moves out of the bubble, in muted

Move my Thursday flight if Swiss has anything earlier.
From your calendar · Google Workspace
Added 4 September
Updated 18:41
GroundWhat sits on itfaint Featherfaint Low flamemuted Feathermuted Low flame
groundThe page behind every card, and the thread itself.2.94:1 must stay under 4.53.76:1 must stay under 4.56.40:16.27:1
groundAltThe recessed panel: a grouped list, a card’s option rows.2.81:1 must stay under 4.53.95:1 must stay under 4.56.11:16.59:1
surfaceA card. Every Settings, Knowledge and Tasks row sits on one.3.08:1 must stay under 4.53.35:1 must stay under 4.56.69:15.58:1
surfaceRaisedThe message bar’s field, raised off the card in Low flame and flush with it in Feather.3.08:1 must stay under 4.52.92:1 must stay under 4.56.69:14.87:1
sidebarAccentThe quiet fill under a chip, an option key and a status pill.2.68:1 must stay under 4.52.75:1 must stay under 4.55.82:14.59:1
memberBubbleThe member’s own bubble, and the worst reading in the set. The replacement does not go here: muted inside his bubble measures 5.74:1 and 4.05:1, so Low flame misses the body bar and one role takes one answer. A provenance line moves out to the ground, which is what the two muted cells on this row measure.2.64:1 must stay under 4.52.43:1 must stay under 4.56.40:16.27:1

Twelve readings, generated rather than typed. The rows come from an array of token keys, so the table measures whatever the token file currently holds: a change to faint that fixed one ground and broke another could not hide in a row somebody forgot to update. Every one of them is also a must-fail claim in tokens/tokens.json, which is what stops the tone quietly coming back for a timestamp.

faint is not deleted, and this is why it still has a token. It is legal as a non-text mark in Low flame on two of these grounds and illegal as a word on all twelve, which is precisely the trap: a tone that reads as fine in one scheme, in one role, and is wrong everywhere else. Keeping it named and claimed is cheaper than keeping the rule in somebody's head: the claims say where it may not go, and there is nothing left in the product that it does.

Consuming it

Colours: the --co-* mapping printed above, over the variables in dist/css/airmond.css; the same values are lightColors / darkColors in dist/ts/tokens.ts, where rule, disabledFill and onDisabled are the three keys this pass added. Chrome: layout.card carries the hairline's width and tone, and layout.control carries the secondary outline's width, the disabled outline's width and the disabledOpacity: 1 that retires the old recipe. That is the whole change. Nothing below the token file names a colour, so the four fixes reach eleven screens by re-syncing the kit, and rolling any one of them back is one value in one file. Every pairing this section RELIES ON is asserted in tokens/tokens.json and re-run by build/contrast-test.mjs on every build, and so are the before states it is arguing against, recorded as must-fails so a retired recipe cannot come back unannounced. Not every cell in the tables is a claim: a before state measured to make a point is a reading, and the array is the shorter list of readings that are a contract.

Landing-page recipes (2026-09-02)

What the airmond.com rebuild introduced and every other surface will want: the app, emails, decks, future landers. Each recipe is rendered from the tokens; the shipped implementation is cs-airmond-website/components/one.

The primary button, all six states

Coral --primary with ink type, 52px, radius 12, 17px semibold. Transform and opacity do the moving; colour and shadow change only on a state change. The nav pill is the same button in ink clothes (44px, radius 999) and is never coral.

Rest
Hover, 180ms
Press, 0.98 in 90ms
Focus ring
Loading, width reserved
Disabled, 60%
The nav pill
StateWhat changesMeasured
Rest--primary #FF6B5A, ink #18222B, radius 12, 52px, padding 0 26pxink on coral 5.76:1
Hover (fine pointer only)--coral-hover #FF7A6B, up 1px, shadow 0 8px 24px rgba(255,107,90,.35), 180ms --motion-ease-microink on coral-hover 6.33:1 (white 2.55:1 fails)
Pressscale 0.98 in 90ms ease-out; the release rides 260ms --motion-ease-press-out (back.out 1.6)
Focus3px --confirm #3E5D86 ring at 2px offset, on :focus-visible. The one universal ring: never coral on coralring on ground 6.44:1
LoadingThe label swaps to "Calling in a moment" with three dots in a grid cell that already had the room, so the button never changes width. aria-busy, cursor wait, NOT dimmed
Disabledopacity .6, no hover. A pre-hydration button is disabled but must not dim: a page at 60% for 300ms reads as broken

The phone-first sign-up card

The conversion component. Country prefix, number and Call me in one card, 640 wide, radius 18, --shadow-float. At rest it is four rows: the title, the number row, the button and one muted line. A valid number reveals the recap block above the button (label, the two doors, the address field, the consent line) and the trust block below it (the three chips, the price line, the lock line). The button stays the last control in both states, so the reveal reads as "and this, before you press".

Rest: title, number row, button, one line (219px at 640 wide)

Get your first call

+41 Your mobile number

Ada calls within a couple of minutes. 7 days free, no card.

Revealed: recap above the button, trust below it (444px)

Get your first call

+41 79 123 45 67
Where should the recap go?
Sign in with GoogleSign in with Microsoft Soon
or type your email
  • Swiss company
  • Data stored in the EU
  • No card for 7 days

7 days free, no card, then $999 a month. She asks for the rest on the call.

For Ada's calls only. Stored in Europe, never sold.Details

The closing band

The same card without its chrome on the ink ground (--foreground, radius --radius-band 24), the dark scheme's inputs (card #1F2833, border #2A3646, foreground #DCE0E5). Coral stays with the one primary action.

Ready when you are.

Leave your number. Ada calls within a couple of minutes. Seven days free, no card, then $999 a month.

+41 Your mobile number

Used for Ada's calls only. Stored in Europe, never sold.

The live chip

"Ada is on call": Feather at 92% (rgba(248,250,252,.92)), 32px, radius 999, 13px semibold ink, and an 8px --confirm dot that breathes (opacity .55 to 1 over 2.4s, sine). Use it wherever a live state is shown: a call, a recording, a meeting she is in. Over a photograph the 92% keeps the ink legible; on a plain ground it measures 15.41:1.

Ada at her desk, with the live chip in the corner
Ada is on call
On the ground
Ada is on call

Task status pills, three tones only

The green "Working on it" chip is retired. The app's older Tasks screenshots still show a fern chip; this recipe replaces it. Working means confirm blue, and nothing on any surface is green.
Waiting on the memberWaiting on you
She is workingWorking on it
Waiting on a third partyWaiting on the vendor
Neutral, on the muted panelBacklog
ToneColoursMeasured
Waiting on you (amber)--waiting on --waiting-soft4.66:1 light, 6.62:1 dark
Working (confirm blue)--confirm on --confirm-soft5.78:1 light, 4.92:1 dark
Waiting on a third party (muted)--muted-foreground on --muted6.11:1 light, 5.58:1 dark

22px tall, radius 999, 11px semibold, padding 0 9px. On the app's list screens the same three tones apply at the app's label size.

The screen vignette

How marketing draws the product: a muted panel (--muted, radius 12, 12px padding) holding white rows (--card, 1px --border, radius 10, 10px 12px padding), inside a tile (--card, radius --radius-card 16, 20px padding) with the 11px uppercase label in confirm blue and the 19px tile title. Never paste a raw screenshot with its chrome; draw the screen in the tokens.

Tasks

One list, hers to chase

Waiting on you · 1
Renew the Sling insuranceWaiting on you
Which excess do you want on the hull cover?
Working on it · 1
Book the Iceland flightsWorking on it
Waiting on us · 1
Cancel the unused subscriptionsWaiting on the vendor
Calls

She picks up, and she makes the calls

Ada
On a call with Praxis Dr. Keller
"Thursday at 9:30 then. Thank you, I will let him know."
She reports back on WhatsApp when she hangs up.

Trust chips and the flag pair

The chips are 22px, 12px medium, --muted-foreground on --muted, each with its 14px mark: the two flags at 2px corners and the card in the chip's own ink. The flag pair is the same two flags at 60px, always in this order, always with its fixed caption.

Under a primary action
  • Swiss company
  • Data stored in the EU
  • No card for 7 days
The flag pair
Made in Zürich · Data in the EU

Swiss cross: #FF0000 field, white cross, radius 3 of 32. EU: twelve #FFCC00 stars on #003399, 3:2, square corners. Both are inline SVG (the exact paths are in cs-airmond-website/components/one/Icons.tsx), never bitmaps.

The footer

Four columns, Airmond / Guides / Trust / Members, with 12px uppercase headings at .08em and 14px muted links each carrying its own 24px tap target. The Guides column is generated from the landing-page registry so it cannot drift. Under the columns, the mark line and the AI-disclosure sentence, which every Airmond surface carries verbatim.