{
  "_comment": "Airmond design tokens — THE single source. dist/css/airmond.css and dist/ts/tokens.ts are generated from this file by build/build-tokens.mjs; never edit dist by hand. NO GREEN: the fern family (the banned list lives in build/color.mjs) is removed for good; confirmation is the blue pair. Every pairing claimed here is re-measured by build/contrast-test.mjs.",
  "meta": {
    "name": "Airmond",
    "schemeNames": { "light": "Feather", "dark": "Low flame (Dusk)" },
    "radius": "0.625rem",
    "radiusCard": "16px",
    "radiusBand": "24px",
    "shadowFloat": "0 18px 50px rgba(24, 34, 43, 0.16)",
    "fonts": {
      "display": "'Schibsted Grotesk', ui-sans-serif, system-ui, sans-serif",
      "body": "'Instrument Sans', ui-sans-serif, system-ui, sans-serif"
    }
  },
  "light": {
    "background": "#F8FAFC",
    "foreground": "#18222B",
    "card": "#FFFFFF",
    "card-foreground": "#18222B",
    "popover": "#FFFFFF",
    "popover-foreground": "#18222B",
    "primary": "#FF6B5A",
    "primary-foreground": "#18222B",
    "secondary": "#EAF0F4",
    "secondary-foreground": "#18222B",
    "muted": "#F1F5F8",
    "muted-foreground": "#4E5E6B",
    "accent": "#EAF0F4",
    "accent-foreground": "#18222B",
    "destructive": "oklch(0.577 0.245 27.325)",
    "destructive-foreground": "#FFFFFF",
    "border": "#E2E9EF",
    "input": "#E2E9EF",
    "rule": "rgba(78, 94, 107, 0.7)",
    "disabled-fill": "#EAF0F4",
    "on-disabled": "#4E5E6B",
    "ring": "#FF6B5A",
    "confirm": "#3E5D86",
    "confirm-soft": "#E8EEF7",
    "waiting": "#A9560B",
    "waiting-soft": "#FBF1DE",
    "bubble-member": "#E8EEF7",
    "coral-deep": "#C63A28",
    "coral-soft": "#FFF2EF",
    "coral-hover": "#FF7A6B",
    "sidebar": "#F1F5F8",
    "sidebar-foreground": "#18222B",
    "sidebar-primary": "#FF6B5A",
    "sidebar-primary-foreground": "#18222B",
    "sidebar-accent": "#EAF0F4",
    "sidebar-accent-foreground": "#18222B",
    "sidebar-border": "#E2E9EF",
    "sidebar-ring": "#FF6B5A"
  },
  "dark": {
    "background": "#171E2A",
    "foreground": "#DCE0E5",
    "card": "#1F2833",
    "card-foreground": "#DCE0E5",
    "popover": "#1F2833",
    "popover-foreground": "#DCE0E5",
    "primary": "#E38273",
    "primary-foreground": "#18222B",
    "secondary": "#2A3646",
    "secondary-foreground": "#DCE0E5",
    "muted": "#1F2833",
    "muted-foreground": "#93A0AD",
    "accent": "#2A3646",
    "accent-foreground": "#DCE0E5",
    "destructive": "oklch(0.704 0.191 22.216)",
    "destructive-foreground": "#FFFFFF",
    "border": "#2A3646",
    "input": "#2A3646",
    "rule": "rgba(147, 160, 173, 0.7)",
    "disabled-fill": "#2A3646",
    "on-disabled": "#93A0AD",
    "ring": "#E38273",
    "confirm": "#8FB2DC",
    "confirm-soft": "#2E3E58",
    "waiting": "#D9BC6A",
    "waiting-soft": "#3A3524",
    "bubble-member": "#2E3E58",
    "coral-deep": "#FF9081",
    "coral-soft": "#3A282A",
    "coral-hover": "#E98F80",
    "sidebar": "#131926",
    "sidebar-foreground": "#DCE0E5",
    "sidebar-primary": "#E38273",
    "sidebar-primary-foreground": "#18222B",
    "sidebar-accent": "#2A3646",
    "sidebar-accent-foreground": "#DCE0E5",
    "sidebar-border": "#2A3646",
    "sidebar-ring": "#E38273"
  },
  "rn": {
    "brand": {
      "coral": "#FF6B5A",
      "confirm": "#3E5D86",
      "confirmOnDark": "#8FB2DC"
    },
    "light": {
      "ground": "#F8FAFC",
      "groundAlt": "#F1F5F8",
      "surface": "#FFFFFF",
      "surfaceRaised": "#FFFFFF",
      "sidebar": "#F1F5F8",
      "sidebarAccent": "#EAF0F4",
      "ink": "#18222B",
      "muted": "#4E5E6B",
      "faint": "#8595A2",
      "coral": "#FF6B5A",
      "coralHover": "#FF7A6B",
      "coralDeep": "#C63A28",
      "coralWash": "rgba(255, 107, 90, 0.16)",
      "memberBubble": "#E8EEF7",
      "onCoral": "#18222B",
      "primaryFill": "#18222B",
      "onPrimary": "#FFFFFF",
      "disabledFill": "#EAF0F4",
      "onDisabled": "#4E5E6B",
      "confirm": "#3E5D86",
      "confirmDeep": "#34517A",
      "confirmSoft": "#E8EEF7",
      "border": "rgba(24, 34, 43, 0.10)",
      "borderStrong": "rgba(24, 34, 43, 0.22)",
      "rule": "rgba(78, 94, 107, 0.7)",
      "danger": "#B3261E",
      "warning": "#A9560B",
      "scrim": "rgba(24, 34, 43, 0.42)",
      "shadow": "#18222B"
    },
    "dark": {
      "ground": "#171E2A",
      "groundAlt": "#131926",
      "surface": "#1F2833",
      "surfaceRaised": "#273240",
      "sidebar": "#131926",
      "sidebarAccent": "#2A3646",
      "ink": "#DCE0E5",
      "muted": "#93A0AD",
      "faint": "#6C7987",
      "coral": "#E38273",
      "coralHover": "#E98F80",
      "coralDeep": "#FF9081",
      "coralWash": "rgba(227, 130, 115, 0.20)",
      "memberBubble": "#2E3E58",
      "onCoral": "#18222B",
      "primaryFill": "#DCE0E5",
      "onPrimary": "#171E2A",
      "disabledFill": "#2A3646",
      "onDisabled": "#93A0AD",
      "confirm": "#8FB2DC",
      "confirmDeep": "#3E5D86",
      "confirmSoft": "#2E3E58",
      "border": "rgba(220, 224, 229, 0.12)",
      "borderStrong": "rgba(220, 224, 229, 0.24)",
      "rule": "rgba(147, 160, 173, 0.7)",
      "danger": "#F2857C",
      "warning": "#D9BC6A",
      "scrim": "rgba(0, 0, 0, 0.62)",
      "shadow": "#000000"
    }
  },
  "stage": {
    "_comment": "THE RECORDING STAGE — the one Airmond surface that paints its own world edge to edge, and the one that does NOT follow light/dark mode. The phone is lying flat on a table and the question (\"can she hear us?\") is being asked by people two metres away, so a state whose colour depended on a setting nobody in the room can see would be a state two people could describe differently. She is listening: the DUSK ground. She has gone off the record: the WHOLE SCREEN repaints AMBER, which is the feature, not decoration (red already means \"recording\" everywhere and grey reads as \"disabled\", i.e. the recording died). THE DUSK PAIR IS NOT REPEATED HERE: the listening ground IS the dark scheme, so build/build-tokens.mjs reads dark.background (#171E2A) and dark.foreground (#DCE0E5) straight out of it and the two can never drift. Everything below is what the stage ADDS. Ported from cs-airmond-ios AirmondKit (RecordingStagePalette, in Recording/RecordingStageCopy.swift) and, until this file, hand-maintained in exactly one place: mb-airmond-app/src/features/recording/stage-palette.ts. That is why two bugs shipped unseen — see stopOnDusk and speakerHues below, and the stage: claims at the foot of this file.",
    "offRecordGround": "#E8A317",
    "offRecordInk": "#18222B",
    "_secondaryOpacityComment": "Secondary — captions, the tap hint, the privacy note, the honesty caveat, the count — is the ground's own ink at reduced strength. TWO NUMBERS, NOT ONE, and that is the point: the amber ink has to survive a much lighter ground than the dusk ink does, so a single shared opacity cannot serve both. At the old shared 0.68 the amber composite measured 3.91:1 and failed AA while the dusk composite sat comfortably at 6.51:1. 0.75 is the LOWEST step that clears 4.5 on the amber (4.58:1; 0.74 measures 4.44 and misses), chosen deliberately over a rounder 0.8 so a caption stays visibly quieter than the headline it sits under — the point of a secondary tone is that it recedes.",
    "duskSecondaryOpacity": 0.68,
    "offRecordSecondaryOpacity": 0.75,
    "fillOpacity": 0.1,
    "duskHairlineOpacity": 0.3,
    "offRecordHairlineOpacity": 0.28,
    "cardHairlineOpacity": 0.22,
    "chipFillOpacity": 0.1,
    "chipFillSpeakingOpacity": 0.28,
    "_stopComment": "Stop is systemRed, as the native app resolved it in each appearance: cs-airmond-ios/App/Sources/Screens/RecordingStageView.swift:847 fills the button with SwiftUI's Color.red, which is #FF3B30 in a light appearance and #FF453A in a dark one. ⚠️ THE SHIPPED RN APP USES NEITHER. stage-palette.ts drifted to a muted #DC2626 on dusk and a near-black brick #8C1D12 on amber, and nobody noticed until Cédric said the button looked faded. Note WHY that drift is easy to make: white type measures 4.83:1 on #DC2626 and 9.13:1 on #8C1D12, but only 3.41:1 and 3.55:1 on systemRed — so the drift reads as somebody fixing a contrast failure and quietly trading away the one colour a room already reads as Stop. DECIDED 2026-09-04 (Cédric): systemRed STAYS on both grounds and the LABEL changes instead — white is replaced by the brand ink #18222B, which measures 4.73:1 on the dusk red and 4.55:1 on the amber red, against white's 3.41 and 3.55. This is not a new rule, it is the one this system already applies to coral: ink on coral is 5.76:1 where white is 2.80:1, which is why onCoral is ink. The 'white on Stop must FAIL' claims below record why the label is ink, exactly as the coral pair does. What an ink label CANNOT fix is the amber-side FILL, which does not separate from the amber ground by luminance at all — see its max claim.",
    "stopOnDusk": "#FF453A",
    "stopOnOffRecord": "#FF3B30",
    "onStop": "#18222B",
    "_liveDotComment": "The pulsing dot in the status pill while she is on the record — recording-red, deliberately not the stage foreground, because \"recording\" is red everywhere else in this app. It has no amber counterpart: off the record the pill swaps the dot for a pause glyph in the stage ink, which is the honest thing to draw when nothing is being captured. Currently a literal in mb-airmond-app/src/features/recording/components/status-pill.tsx.",
    "liveDot": "#E8574A",
    "_speakerHuesComment": "The roster palette, indexed by speaker. Five hues that must stay apart from each other AND from the ground, held once so the iOS chip and the Mac chip cannot colour the same person differently (they each held a private copy until AirmondKit took it over). Used as a NON-TEXT mark: a 10pt filled dot, a capsule fill at chipFillOpacity/chipFillSpeakingOpacity and a capsule border. ⚠️ THEY WERE CHOSEN AGAINST THE DUSK GROUND ONLY. Against the amber every one of them lands between 1.01:1 and 1.26:1 — the claims below record it rather than hiding it. ⚠️ SPEAKER 4 WAS A GREEN AND IS NOT ANY MORE. It was a mint the no-green guard never saw, because that guard is a LITERAL HEX BLOCKLIST — it matches the retired fern values and nothing else, so a mint nine channel steps off one of them walks straight past it. A hue-range check would be the durable fix; this comment is the record that the gap exists. (Do not paste a banned hex into this file to illustrate the point: the guard is a plain substring scan over the whole file, so NAMING a banned green in prose fails the build exactly as declaring one would.) Replaced 2026-09-04 (Cédric) with #EC8DEC, an orchid at hue 300°. The wheel it had to fit is small: green is banned outright, red 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°). #EC8DEC sits at its midpoint, 40° clear of one 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 the dusk ground, inside the band the other four already occupy (7.10–9.42) so no one chip shouts. HONEST COST: the old roster's tightest pair was 51° apart and the new one's is 40°, because dropping green gives up the only hue in a 169° arc and five chips must now share what is left.",
    "speakerHues": ["#5CB8F0", "#FAB55C", "#B899F5", "#EC8DEC", "#F58FAD"]
  },
  "call": {
    "_comment": "THE CALL \u2014 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. \u26d4 THE CALL GROUND IS NOT REPEATED HERE, for the same reason the stage's dusk is not: it IS the dusk, so build/build-tokens.mjs reads dark.background and dark.foreground straight out of the dark scheme and the two can never drift. Everything below is what the CALL adds. DECIDED 2026-09-19 (C\u00e9dric): Airmond paints ONE ground, in the member's own scheme, and exactly one surface is allowed a world of its own and only while it is live \u2014 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 not that surface. It is a phone held by one person, and it is the SAME MOMENT as the listening stage, so it is the same ground. The two are told apart by what is ON them \u2014 a portrait and a handset against a speaker roster and a capture feed \u2014 and never by two different blacks. Until now this lived only in mb-airmond-app/src/features/voice/components/call-ground.tsx, which is the whole problem: the call is the one Airmond surface with no entry in the component library, and it is the one that invented a ground.",
    "_retiredComment": "WHAT RETIRED, AND THE ARGUMENT THAT WAS MADE FOR IT, so nobody re-derives it. The call painted #000000 with the dusty coral breathed into the top at 0.34, falling to 0.09 at a quarter and to 0 at a half \u2014 an ember rather than a wash, compositing to about #4D2C27 at the top, 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 it rests on: ground \"is the app's page colour and the stage's dusk; painting the call in it would make the two screens siblings at a glance, which is the one thing they must not be\". That premise is exactly what the decision reverses. \u26d4 DO NOT REINTRODUCE A CALL GROUND HEX. A second immersive ground is a second thing to keep true, and this one had nothing upstream of it: the file's own docblock said \"Pure black rather than a token because there is no token for it and there should not be\", which is how a surface leaves the system while everybody is agreeing with it.",
    "_endFillComment": "The End disc is not a value of its own: build/build-tokens.mjs reads stage.stopOnDusk (systemRed in the dark appearance) so the control that ends a live conversation cannot come to differ from the control that ends a recording. The app had already resolved it this way \u2014 call-controls.tsx sets END_FILL to the dusk palette's destructive rather than to the theme's danger \u2014 so this key records that, it does not decide it. Against the call ground it measures 4.91:1, clear of the 3.0 a control must make to be findable.",
    "_onEndGlyphComment": "\u26a0\ufe0f WHITE HERE, WHERE THE STAGE INSISTS ON INK ON THE SAME RED, and the difference is TEXT versus GLYPH rather than a lapse. \"Keep the red, move the label\" (C\u00e9dric, 2026-09-04) was measured for a WORD on the Stop button, read from two metres away, against WCAG's 4.5 text bar \u2014 which white on systemRed 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 clears it. A dark glyph there would read as a disabled control. Both claims are recorded below, the 3.0 it must clear and the 4.5 it must FAIL, so the pair cannot quietly collapse into one rule.",
    "onEndGlyph": "#FFFFFF",
    "_controlWashComment": "The quiet disc behind an unarmed control (mute, speaker): the ground's own ink at 0.18, native's .white.opacity(0.18) read in this screen's ink. \u26a0\ufe0f THIS FILL HAS NEVER MADE A SHAPE, ON EITHER GROUND. It composites to #3A414C on the dusk and measures 1.63:1 against it; on the retired black it was #2E2E2E at 1.55:1. Both are about half of the 3.0 that a container the member has to find is meant to clear, so this is not a cost of the move \u2014 the move improved it by 0.08 and left it failing. No opacity fixes it either: 0.30 still reaches only 2.33:1, by which point the disc has stopped being quiet. So the wash is not what identifies the control; controlOutlineTone is. The failure is recorded as a must-fail claim rather than relaxed, so it cannot drift back into being load-bearing.",
    "controlWashOpacity": 0.18,
    "_controlOutlineComment": "The line that makes an unarmed control a shape: rule, the one tone this system draws every separating mark in, at control.card.borderWidth. Over the call ground it composites to #6E7986 and measures 3.78:1. This is the same fix, in the same token, that the card got on 2026-09-14 once its fill was found to measure 1.10:1 with nothing but a shadow beside it \u2014 a mark that separates must clear 3.0 on the surface it ACTUALLY sits on. \u26a0\ufe0f THE APP DRAWS IT, since mb-airmond-app #241 (2026-09-19): call-controls.tsx takes this tone 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, which is the likelier regression.",
    "controlOutlineTone": "rule"
  },
  "meetingTile": {
    "_comment": "ADA'S MEETING TILE: the 1280x720 camera page the Recall bot shows as her video in Meet, Zoom and Teams. Decided 2026-09-30 and the speaking orb approved 2026-10-01 (Cédric, option C of five); the decisions and their reasons are DESIGN.md's \"2026-09-30, the meeting tile\" section, and this block is their values. ⛔ IT DOES NOT FOLLOW LIGHT OR DARK, for the recording stage's reason and not a new one: the people looking at this page are everyone else in the call, and none of them can see the member's setting, so the tile always paints the DUSK ground and dusk cards. Next to camera feeds in a video grid, white Feather cards read as a screen share and pull the eye off the people. (The tile it replaces painted a forest ground, which also broke the no-green rule.) ⛔ NOTHING HERE REPEATS A VALUE THE SYSTEM ALREADY HOLDS. `tones` names the key each role reads, as a path into this file; build/meeting-tile.mjs resolves every path for both the generator and the guide and throws on one that does not resolve, so a renamed key fails the build instead of painting nothing. The new decisions are few and all visible: two washes (an existing hue at a new opacity), the orb, the portrait shadows, and the layout. ⛔ SPEAKING IS AN ORB BEHIND HER PORTRAIT, NEVER A BORDER ROUND THE FRAME. Meet, Zoom and Teams crop the tile (worst case the centre 720x720 square, which cuts 280 px off each side), so a frame border is invisible on at least two sides for a large share of viewers; the orb lives inside the band every crop keeps. ⛔ EVERYTHING ELSE SITS IN THE MIDDLE THIRD once anyone has joined, for the same reason: the third runs from 427 to 853 px, inside the 280 to 1000 px the tightest crop keeps. The consumer is cs-airmond-api src/meetings/live-stage-page.ts, which carries these values by hand today; see the README.",
    "_tonesComment": "Role to token. A string is a path into this file (`stage.speakerHues.4` indexes the array). {\"of\", \"at\"} is that tone at an opacity, where `at` is a number or a path to one. The two entries whose `at` is a literal number are the only new colour decisions in this block: the confirmation pin's hairline (the waiting amber at 0.28, as approved) and the Removed tag's wash (the dark danger at 0.16: the approved 0.18 measured 4.40:1 and missed the text bar, and 0.16 is the highest wash that clears it, at 4.57:1; decided 2026-10-01).",
    "tones": {
      "ground": "dark.background",
      "groundCentre": "dark.card",
      "groundEdge": "dark.sidebar",
      "card": "dark.card",
      "ink": "dark.foreground",
      "muted": "dark.muted-foreground",
      "hairline": "rn.dark.border",
      "raised": "dark.secondary",
      "portraitRing": { "of": "dark.foreground", "at": "stage.duskHairlineOpacity" },
      "portraitRingSpeaking": "dark.background",
      "kindAction": "dark.coral-deep",
      "kindNote": "dark.muted-foreground",
      "kindMemory": "dark.waiting",
      "kindAnswer": "dark.confirm",
      "kindSummary": "dark.confirm",
      "pinIcon": "dark.waiting",
      "pinHairline": { "of": "dark.waiting", "at": 0.28 },
      "editedFill": "dark.waiting-soft",
      "editedInk": "dark.waiting",
      "removedFill": { "of": "rn.dark.danger", "at": 0.16 },
      "removedInk": "rn.dark.danger",
      "readingFill": "rn.dark.coralWash",
      "readingInk": "dark.coral-deep",
      "ownerFill": "dark.secondary",
      "due": "dark.waiting",
      "confirmed": "dark.confirm",
      "onConfirmed": "dark.background",
      "liveDot": "dark.coral-deep",
      "initials": "light.foreground",
      "avatarRing": "dark.background",
      "shadow": "rn.dark.shadow"
    },
    "_speakerHuesComment": "Avatars take the recording stage's five roster hues, by reference, with the brand ink for initials (measured on every hue below). One roster, so a person the stage colours one way is not coloured another way on the tile.",
    "speakerHues": "stage.speakerHues",
    "_groundComment": "The ground is the dusk with a soft lift behind the column: a radial from dark.card at the centre through dark.background to dark.sidebar at the edge. Three existing tones; the shape and the middle stop are the only new numbers. Text is measured against dark.card, the gradient's LIGHTEST point, so every claim below holds anywhere on the ground.",
    "groundShape": "ellipse 70% 80% at 50% 40%",
    "groundMidStop": 58,
    "_shadowsComment": "The two portrait shadows, in rn.dark.shadow at an opacity. They lift the portrait off the lift in the ground; they are not marks and carry no claim.",
    "shadows": {
      "portrait": { "y": 8, "blur": 22, "at": 0.4 },
      "idle": { "y": 24, "blur": 60, "at": 0.45 }
    },
    "orb": {
      "_comment": "THE LIQUID ORB: she is speaking. Two conic layers of four brand hues, screen-blended behind her 64 px portrait, their edges morphing and the gradient turning, so the shape is always moving while she talks. Its SIZE follows her output level (the PCM amplitude the page is already playing), scaled from scaleBase at silence to scaleBase + scaleRange at full level, eased over levelMs so it breathes rather than jitters. It fades in and out over fadeMs. Chosen 2026-10-01 over a halo, ripples, a waveform and an aurora because it is the one that still reads at Meet's 3x3 grid size; the portrait's ring turns to the ground colour while it shows, so her face stays separate from the light. Animates transform, opacity and border-radius only, with no full-frame blur, so Recall's headless Chrome draws it at full frame rate. Under prefers-reduced-motion the shape stops morphing and turning, the level is held at reducedLevel, and the fade takes motion.reduced. livePulseMs is the subtitle's live dot, which pulses while she speaks and holds still under reduced motion. Before anyone joins, the same orb sits behind the idle portrait at idleSizeRatio of the idle ring's outer diameter (1.2 x 330 px = 396 px), and her name sits below the orb's rim at full level (see layout.idleNameOffset; the build checks it). The hues are the brand coral and three of the roster (pink, amber, blue): no green, and nothing a viewer could take for a recording light.",
      "hues": ["dark.primary", "stage.speakerHues.4", "stage.speakerHues.1", "dark.confirm"],
      "size": 128,
      "innerInset": 6,
      "innerOpacity": 0.75,
      "glow": [
        { "blur": 56, "spread": 14, "of": "dark.primary", "at": 0.5 },
        { "blur": 120, "spread": 30, "of": "stage.speakerHues.4", "at": 0.22 }
      ],
      "blend": "screen",
      "scaleBase": 0.86,
      "scaleRange": 0.26,
      "levelMs": 130,
      "fadeMs": 450,
      "fadeEase": "cubic-bezier(0.16, 1, 0.3, 1)",
      "morphMs": 5000,
      "spinMs": 7000,
      "innerMorphMs": 3600,
      "innerSpinMs": 5000,
      "shapes": [
        "46% 54% 58% 42% / 44% 50% 50% 56%",
        "58% 42% 40% 60% / 55% 40% 60% 45%",
        "40% 60% 55% 45% / 42% 58% 42% 58%"
      ],
      "idleSizeRatio": 1.2,
      "reducedLevel": 0.5,
      "livePulseMs": 1200
    },
    "layout": {
      "_comment": "Every distance on the tile, in the frame's own pixels (the page is drawn at 1:1 and the bot's browser never scales it). Each entry carries the reason it has the value it has, so the next change argues with the reason rather than the number.",
      "frameWidth": { "value": 1280, "unit": "px", "why": "Recall's camera page is 1280x720, so the frame is exactly that and nothing is drawn outside it." },
      "frameHeight": { "value": 720, "unit": "px", "why": "The other half of the 16:9 frame Recall streams." },
      "columnLeft": { "value": 33.33, "unit": "%", "why": "The middle third starts at 427 px, inside the 280 px the tightest crop (the centre 720x720 square) removes from each side." },
      "columnWidth": { "value": 33.33, "unit": "%", "why": "Ends at 853 px, inside the 1000 px where that crop stops. Every crop Meet, Zoom and Teams apply keeps the whole band." },
      "columnInset": { "value": 12, "unit": "px", "why": "Keeps the cards' edges off the band's own edges; unchanged from the 2026-09-29 stage-feed plan in cs-airmond-api." },
      "headerHeight": { "value": 116, "unit": "px", "why": "Fits the portrait and a two-line title, so the feed starts at the same height whether or not the title wraps." },
      "portraitSize": { "value": 64, "unit": "px", "why": "Large 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." },
      "portraitTop": { "value": 26, "unit": "px", "why": "26 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." },
      "portraitRingWidth": { "value": 2, "unit": "px", "why": "The resting ring, the stage's dusk hairline; a frame for the photo, not a mark anyone has to find." },
      "portraitRingSpeakingWidth": { "value": 3, "unit": "px", "why": "While the orb shows, the ring becomes a 3 px band of ground colour, so her face stays separate from the light behind it." },
      "headerTextLeft": { "value": 106, "unit": "px", "why": "From 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." },
      "headerTextTop": { "value": 30, "unit": "px", "why": "Starts 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." },
      "titleSize": { "value": 24, "unit": "px", "why": "Schibsted 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." },
      "titleLineHeight": { "value": 1.15, "unit": "", "why": "Tight enough that two lines still fit the header." },
      "titleMaxLines": { "value": 2, "unit": "", "why": "A longer title is clamped rather than pushing the feed down." },
      "subtitleSize": { "value": 15, "unit": "px", "why": "Instrument Sans; whether she is taking notes and how many people are in the call." },
      "subtitleGap": { "value": 6, "unit": "px", "why": "Between the title and the subtitle." },
      "liveDotSize": { "value": 7, "unit": "px", "why": "Shown only while she is taking notes; it pulses while she speaks." },
      "pillPaddingY": { "value": 6, "unit": "px", "why": "The status pill, above and below its label." },
      "pillPaddingX": { "value": 12, "unit": "px", "why": "The status pill, each side (the glyph side sits two closer, optically)." },
      "pillGap": { "value": 8, "unit": "px", "why": "Between the pill's glyph and its words." },
      "pillSpaceBelow": { "value": 12, "unit": "px", "why": "Between the pill and the first card, so the pill reads as state rather than as the first item." },
      "feedGap": { "value": 10, "unit": "px", "why": "Between two cards or rows in the feed." },
      "feedFadeFrom": { "value": 72, "unit": "%", "why": "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." },
      "cardRadius": { "value": "meta.radiusCard", "unit": "px", "why": "The system's card radius, read from meta rather than restated." },
      "cardPaddingX": { "value": 16, "unit": "px", "why": "Inside a card, left and right." },
      "cardPaddingTop": { "value": 14, "unit": "px", "why": "Inside a card, above the kind row." },
      "cardPaddingBottom": { "value": 15, "unit": "px", "why": "One more than the top, which optically balances the descenders of the last line." },
      "kindIconSize": { "value": 19, "unit": "px", "why": "The app's icon for the kind, bare, with no tinted box behind it (2026-09-30)." },
      "kindGap": { "value": 8, "unit": "px", "why": "Between the kind icon and its label." },
      "labelSize": { "value": 15, "unit": "px", "why": "The kind label in muted, 600; the card's text is the thing to read, the label only sorts it." },
      "tagSize": { "value": 13, "unit": "px", "why": "The Edited, Removed and Reading aloud tags, 600, right-aligned in the kind row." },
      "tagIconSize": { "value": 13, "unit": "px", "why": "The glyph inside a tag." },
      "tagPaddingY": { "value": 3, "unit": "px", "why": "A tag, above and below its word." },
      "tagPaddingX": { "value": 9, "unit": "px", "why": "A tag, each side." },
      "tagGap": { "value": 5, "unit": "px", "why": "Between a tag's glyph and its word." },
      "cardTextSize": { "value": 21, "unit": "px", "why": "A note or an answer, read by people at grid size; never smaller than this." },
      "cardTextLineHeight": { "value": 1.28, "unit": "", "why": "Open enough for three lines at 21 px to stay distinct when scaled down." },
      "cardTextWeight": { "value": 450, "unit": "", "why": "A shade over regular, which keeps light-on-dark text from thinning at grid size." },
      "cardTextGap": { "value": 7, "unit": "px", "why": "Between the kind row and the text." },
      "actionTextSize": { "value": 22, "unit": "px", "why": "An action item asks someone to act, so its text is one step larger." },
      "actionTextWeight": { "value": 600, "unit": "", "why": "And semibold, for the same reason." },
      "oldTextSize": { "value": 16, "unit": "px", "why": "The words an edit replaced, struck through in muted for six seconds under the new text." },
      "strikeWidth": { "value": 1.5, "unit": "px", "why": "The strike through replaced or removed words." },
      "metaSize": { "value": 15, "unit": "px", "why": "The owner, due date, and who asked." },
      "metaGap": { "value": 11, "unit": "px", "why": "Between the text and its meta row." },
      "ownerAvatarSize": { "value": 22, "unit": "px", "why": "The owner's initials inside the owner pill." },
      "ownerPaddingX": { "value": 11, "unit": "px", "why": "The owner pill's trailing side; the avatar sits 3 px from the leading edge." },
      "rowGap": { "value": 12, "unit": "px", "why": "Between a row's avatars and its words." },
      "pinTitleSize": { "value": 18, "unit": "px", "why": "The confirmation pin's title, 600." },
      "pinBodySize": { "value": 15, "unit": "px", "why": "The confirmation pin's instruction." },
      "rowTextSize": { "value": 19, "unit": "px", "why": "A join or confirmation row, which is lighter than a card." },
      "rowSubSize": { "value": 15, "unit": "px", "why": "The line under a confirmed member's name." },
      "avatarSize": { "value": 36, "unit": "px", "why": "A person in a join row." },
      "avatarOverlap": { "value": 10, "unit": "px", "why": "Folded joins stack their avatars, overlapping by this much." },
      "avatarMax": { "value": 4, "unit": "", "why": "At most four avatars in a folded row; the words say how many more." },
      "namesShown": { "value": 3, "unit": "", "why": "At most three first names in a folded row, then \"and N others\"." },
      "avatarRingWidth": { "value": 2.5, "unit": "px", "why": "A ring of ground colour between stacked avatars." },
      "initialsSize": { "value": 13, "unit": "px", "why": "Initials on a 36 px avatar, 700." },
      "confirmedBadgeSize": { "value": 18, "unit": "px", "why": "The confirmed check on a member's avatar, bottom right." },
      "confirmedCheckSize": { "value": 11, "unit": "px", "why": "The check glyph inside it." },
      "pillTextSize": { "value": 15, "unit": "px", "why": "The status pill under the header: connecting voice, or muted." },
      "pillIconSize": { "value": 17, "unit": "px", "why": "The pill's glyph." },
      "idleRingSize": { "value": 330, "unit": "px", "why": "Before 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." },
      "idleRingWidth": { "value": 3, "unit": "px", "why": "The resting ring, drawn inside the 330 px." },
      "idlePhotoInset": { "value": 7, "unit": "px", "why": "The 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." },
      "idleCentreX": { "value": 50, "unit": "%", "why": "Centred across the frame." },
      "idleCentreY": { "value": 42, "unit": "%", "why": "Above centre, to leave room for her name and role under the circle and the orb." },
      "idleNameOffset": { "value": 236, "unit": "px", "why": "From 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." },
      "idleNameSize": { "value": 42, "unit": "px", "why": "Schibsted Grotesk 700, her name under the circle." },
      "idleRoleSize": { "value": 19, "unit": "px", "why": "Whose assistant she is, in muted." }
    },
    "copy": {
      "_comment": "The tile's fixed words, as tokens, for the same reason the thread's labels are: two renderers drawing the same state should not be able to say it two ways. {name}, {first} and {assistant} are filled in by the page.",
      "kindAction": "Action item",
      "kindNote": "Note",
      "kindMemory": "Knowledge update",
      "kindAnswer": "Answer",
      "kindSummary": "Summary",
      "edited": "Edited",
      "removed": "Removed",
      "readingAloud": "Reading aloud",
      "askedBy": "Asked by {name}",
      "atRequest": "At {name}’s request",
      "confirmed": "Confirmed · {assistant} works for {first}",
      "pinTitle": "Member confirmation needed",
      "pinBody": "Open the Airmond app and confirm which participant is you.",
      "hearing": "Connecting voice…",
      "mutedLabel": "Muted",
      "mutedDetail": "· still taking notes",
      "takingNotes": "taking notes",
      "notTakingNotes": "not taking notes",
      "people": "{n} people",
      "joined": "joined",
      "andOthers": "and {n} others",
      "andOneOther": "and 1 other"
    }
  },
  "motion": {
    "_comment": "The marketing-web motion vocabulary, lifted from the 2026-09-02 airmond.com rebuild (cs-airmond-website components/one/motion). Durations and eases only; the budget (reveals once, at most two ambient loops, transform/opacity only, no scroll-jacking) and the reduced-motion rule (everything collapses to the 200 ms fade, video shows its poster, no loops) are documented on brandkit.airmond.com/motion.html. The `gsap` field is the same curve by its GSAP name, for the surfaces that animate with it.",
    "arrive": { "duration": "700ms", "ease": "cubic-bezier(0.22, 1, 0.36, 1)", "gsap": "power3.out", "use": "anything entering the page" },
    "land": { "duration": "600ms", "ease": "cubic-bezier(0.16, 1, 0.3, 1)", "gsap": "expo.out", "use": "elements that settle: cards, pills, check marks" },
    "micro": { "duration": "180ms", "ease": "cubic-bezier(0.25, 0.46, 0.45, 0.94)", "gsap": "power2.out", "use": "hover, focus and press feedback" },
    "press-in": { "duration": "90ms", "ease": "ease-out", "gsap": "power1.out", "use": "the press itself: scale 0.98" },
    "press-out": { "duration": "260ms", "ease": "cubic-bezier(0.34, 1.56, 0.64, 1)", "gsap": "back.out(1.6)", "use": "the release: a small spring back to 1" },
    "ambient": { "min": "4s", "max": "8s", "ease": "cubic-bezier(0.37, 0, 0.63, 1)", "gsap": "sine.inOut", "use": "the one or two things allowed to breathe: a waveform, a portrait row" },
    "scrub-damping": "0.6",
    "reduced": { "duration": "200ms", "ease": "ease", "use": "under prefers-reduced-motion every state change is this opacity fade, with no travel" }
  },
  "marketing": {
    "_comment": "Marketing-web steps above the app scales (typography / typographyWide in dist/ts/tokens.ts), from the 2026-09-02 landing rebuild. Sizes in px; lineHeight is a ratio; phone is the size under 1100px. The label is the tile eyebrow in confirm blue and is a MARKETING device: the app's list screens never carry an uppercase eyebrow above a card (see the typography page).",
    "type": {
      "h1": { "size": 56, "phone": 40, "lineHeight": 1.04, "weight": 700, "letterSpacing": "-0.025em", "face": "display" },
      "h2": { "size": 42, "phone": 30, "lineHeight": 1.1, "weight": 700, "letterSpacing": "-0.02em", "face": "display" },
      "tileTitle": { "size": 19, "phone": 19, "lineHeight": 1.25, "weight": 700, "letterSpacing": "-0.01em", "face": "display" },
      "label": { "size": 11, "phone": 11, "lineHeight": 1.2, "weight": 600, "letterSpacing": "0.08em", "face": "body", "transform": "uppercase", "color": "confirm" }
    },
    "rhythm": {
      "sectionGap": "112px",
      "sectionGapPhone": "80px",
      "contentWidth": "1120px",
      "canvas": "1440px",
      "gutterPhone": "16px"
    }
  },
  "mark": {
    "_comment": "THE TONE EVERY MARK THAT SEPARATES OR ENCLOSES IS DRAWN IN, on every surface this system has, and the correction that forced it into the token file. Until 2026-09-14 the section rule and the code plate's edge were `border`, which is the hairline token for the PAGE ground: inside a bubble it composites to #DBE0E4 and #363E48 and measures 1.21:1 and 1.38:1 against the fill, i.e. nothing. The quote rule was full `muted` at 6.11:1. Three marks doing one job, two of them invisible and the third six times louder than the other two. The root cause is that 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: so `border` clears 3.0 on the page ground and cannot clear it on a bubble, and a thread mark can therefore never use `border` no matter what the page does. THE RULE, which 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, unless the component draws something else at the body-text bar that carries the same meaning (the unread divider is the one case: its rule is texture at 1.22:1 and its LABEL carries the reading at 4.97:1). tone is `muted` because that is already the thread's one quiet tone; 0.7 is the LOWEST 0.05 step that clears 3.0 in BOTH schemes (3.18:1 light and 3.50:1 dark; 0.65 measures 2.87 light and misses), chosen the same way the recording stage chose its amber secondary. The same pair ships on the Mac, which is where the self-consistent version already existed. IT IS A TOP-LEVEL BLOCK AND NOT PART OF `thread`: the same ink draws a card's hairline, a secondary control's outline and a disabled control's outline, so a rule that lived inside the thread block was a rule the rest of the kit had to re-derive by hand, and site/controls.mjs was already doing exactly that. `tone` is an `rn` KEY rather than a --th-* name, because the app mirrors the `rn` block and nothing else: a mark ink the app cannot reach is not a mark. ⛔ ONE SURFACE IS EXEMPT AND IT IS NOT AN EXCEPTION TO THE RULE: the member's own bubble, where this tone measures 3.07:1 and 2.79:1. Nothing quiet goes inside his bubble anyway (muted there measures 4.05:1 in Low flame and is already claimed as a must-fail), and that rule now covers marks as well as words: his bubble holds his words and nothing else. Raising the opacity to 0.8 would clear it at 3.15:1, and would also take the card's hairline to 3.98:1 and 4.63:1 against the ground, which is the wireframe the 2026-09-02 ruling removed. site/thread.mjs throws if a member row is asked to carry a mark, so the guide cannot ship the combination it forbids.",
    "tone": "muted",
    "ruleOpacity": 0.7,
    "plateFillOpacity": 0.14,
    "_plateComment": "THE CODE PLATE, DECIDED EXPLICITLY. A plate can be read by its FILL rather than its edge, so an edge under 3.0 is defensible there 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 not asked to clear anything (claimed as a must-fail). The Mac's plate edge is muted at 0.5, which measures 2.16:1 and 2.47:1 on the bubble; we take 0.7 instead, because with a 1.22:1 fill a 0.5 edge leaves the plate drawn by nothing measurable at all."
  },
  "thread": {
    "_comment": "THE THREAD'S GEOMETRY ON THE NATIVE PHONE, and the one place it lives. (Since 2026-09-24 the web and the Mac draw the thread denser; their numbers are the `density` block below, and the build fails if that block's native column disagrees with this one.) Until 2026-09-14 these numbers existed TWICE, in mb-airmond-app/src/theme/tokens.ts and in cs-airmond-ios/AirmondKit/Sources/AirmondKit/Models/ThreadLayout.swift, with no shared source. The two had already drifted: the mic disc and the send glyph differed between the apps until 2026-09-13 and the only way to settle it was to read the approved artifact's own CSS. Both apps now mirror this block. Sizes are px, which the Mac reads as points. Measured off the reference captures of 13 and 14 September 2026 and rescaled from their 20px line height to this one.",
    "measure": {
      "_comment": "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.",
      "maxBubble": 640,
      "maxColumn": 900
    },
    "bubble": { "cornerRadius": 18, "borderWidth": 1, "insetHorizontal": 14, "insetVertical": 9 },
    "body": { "size": 17, "lineHeight": 26 },
    "block": {
      "_comment": "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.",
      "paragraphGap": 8,
      "listItemGap": 6,
      "headingSpaceAbove": 24,
      "headingSpaceBelow": 12,
      "markerToText": 22,
      "hangingIndent": 28,
      "markerDiameter": 5
    },
    "run": {
      "_comment": "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.",
      "gapWithinRun": 12,
      "gapBetweenSpeakers": 12
    },
    "rail": {
      "_comment": "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.",
      "bubbleToFirstIcon": 12,
      "iconPitch": 26,
      "iconSize": 15,
      "tapTarget": 22,
      "revealMs": 120
    },
    "stamp": {
      "_comment": "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.",
      "size": 12,
      "tone": "muted"
    },
    "dayBreak": {
      "_comment": "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.",
      "size": 12.5,
      "spaceAbove": 22,
      "spaceBelow": 16,
      "tone": "muted",
      "clock": "24h"
    },
    "unread": {
      "_comment": "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.",
      "label": "NEW",
      "labelTone": "coral-deep",
      "ruleTone": "border",
      "_labelToneComment": "coral-deep, NOT coral. Full coral measures 2.68:1 on the light ground, which fails the 4.5 body-text bar and fails even the 3.0 non-text bar; coral-deep measures 4.97:1 light and 7.61:1 dark. The label is a word a member reads, so it takes the text bar. Both are claimed below."
    },
    "draft": {
      "_comment": "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).",
      "cornerRadius": 14,
      "headPadding": 16,
      "headGap": 12,
      "markSize": 44,
      "markRadius": 10,
      "markGlyph": 18,
      "captionGap": 2,
      "titleSize": 17,
      "titleLineHeight": 23,
      "captionSize": 13,
      "captionLineHeight": 18,
      "bandPaddingHorizontal": 16,
      "bandPaddingVertical": 12,
      "bandGap": 12,
      "textSize": 15,
      "textLineHeight": 22,
      "footPadding": 16,
      "footGap": 12,
      "quietPadding": 8,
      "actionHeight": 38,
      "actionInsetHorizontal": 16,
      "actionRadius": 14,
      "previewLines": 6,
      "previewLinesReplaced": 3,
      "previewChars": 360,
      "previewCrumb": 40,
      "title": "Draft reply",
      "changeLabel": "Change it",
      "openLabel": "Open in",
      "openFallbackLabel": "Open the draft",
      "readAllLabel": "Read all",
      "showLessLabel": "Show less",
      "replacedLabel": "Replaced by the draft below"
    },
    "newMessages": {
      "_comment": "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.",
      "height": 28,
      "cornerRadius": 14,
      "edgeInset": 12,
      "insetLeading": 9,
      "insetTrailing": 10,
      "gap": 9,
      "arrowSize": 14,
      "dismissHit": 16,
      "dismissSize": 10,
      "labelSize": 13,
      "labelNumerals": "tabular",
      "label": "new messages",
      "labelSingular": "new message",
      "_labelComment": "Both apps draw the same words, so the words are a token, exactly as the unread divider's label is. TWO of them, because a pill that reads \"1 new messages\" is a pill somebody wrote without reading it: the count crosses 1 every time a thread goes quiet, which is the most common state this component has."
    },
    "longMessage": {
      "_comment": "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.",
      "capLines": 14,
      "minHiddenLines": 3,
      "fadeLines": 2.5,
      "controlGap": 4,
      "controlWeight": 600,
      "moreLabel": "Show more",
      "lessLabel": "Show less"
    },
    "actionCard": {
      "_comment": "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).",
      "cornerRadius": 14,
      "padding": 12,
      "gap": 12,
      "markSize": 36,
      "markGlyph": 18,
      "briefRuleWidth": 2,
      "briefInset": 12,
      "insetRadius": 10,
      "insetPadding": 12,
      "insetGap": 2,
      "pillInsetHorizontal": 8,
      "pillInsetVertical": 2,
      "pillDot": 7,
      "titleSize": 17,
      "titleLineHeight": 23,
      "detailSize": 15,
      "detailLineHeight": 22,
      "labelSize": 13,
      "labelLineHeight": 18,
      "titleTone": "ink",
      "liveDetailTone": "ink",
      "detailTone": "muted"
    }
  },
  "control": {
    "_comment": "CARD AND CONTROL CHROME, moved out of build/build-tokens.mjs on 2026-09-14 so it lives where every other value does. It was a literal in the generator, which meant the style guide could not READ the card's border width and drew its own 1px instead: a page asserting that the hairline is reversible in one value while itself proving nothing of the kind. site/controls.mjs draws the specimen at card.borderWidth now, so setting that to 0 takes the line off the guide and off every card in the app in the same edit.",
    "card": {
      "_comment": "1 until 2026-09-02, 0 until 2026-09-14, 1 again since. The 2026-09-02 ruling removed a hairline that was `border` and measured 1.16:1 against the ground, a wireframe that was also invisible, and left the card separated by a 6% shadow at 1.05:1 light and 1.12:1 dark, which is a third of what a shape needs. The line that came back is `rule`, at 3.20:1 and 3.92:1. ⚠️ THIS IS A TRIAL (Cédric, 2026-09-14: \"I am not sure we need the hairline, but I am also not opposed. We can just try.\") and it is reversible in ONE value: set borderWidth to 0, with no call-site change anywhere. 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.",
      "borderWidth": 1,
      "borderTone": "rule",
      "elevation": 1
    },
    "secondaryBorderWidth": 1,
    "disabledOpacity": 1,
    "disabledBorderWidth": 1,
    "_disabledComment": "disabledOpacity is ONE on purpose. 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 opacity halves the label and the fill together, and a control that reads at 15.41:1 enabled and 3.10:1 disabled has not been dimmed, it has been deleted. A disabled control is `disabledFill` + `onDisabled` at full strength, and it KEEPS its outline (disabledBorderWidth), because the plate alone measures 1.10:1 against the ground: the plate says unavailable and the outline says it is still a control."
  },
  "density": {
    "_comment": "THE CHAT AT GROK'S DENSITY, and the standard since 2026-09-24. Cédric compared app.airmond.com at 858 and 1283 beside Grok's desktop app on 2026-09-23 and approved its density (\"let's make those updates, except for the emojis\"); on 2026-09-24 he asked for it to be kept as the standard (\"I do like the way you made it so that we can have more in one conversation now, so let's save that\"). It shipped first in the apps (mb-airmond-app #280, #285, #286; mb-airmond-ios #530) and this block brings the kit back upstream of them. EVERY KEY IS A ROW OF THREE: `web` (app.airmond.com at every window width, touch browsers included, the switch being Platform.OS), `mac` (the native Mac app, read from AirmondKit's ThreadLayout.Mac / ComposerLayout.Mac and MacWindowStyle) and `native` (the React Native app on iOS and Android, phone and iPad, whose thread is the `thread` block above; it did NOT move, because 14pt is too small at arm's length and nobody asked. The native iOS phone's own ThreadLayout numbers were not part of this pass either). `null` means that platform has no such number, usually because the system draws it. `doc` is what the number governs; the generator prints it into dist/ts/tokens.ts and the guide prints it in its table, so the two cannot describe one value differently. THE KIT IS UPSTREAM: change a number here and re-sync the apps, never the other way round. The generator emits `threadWeb`, `railWeb`, `chatHeaderWeb`, `composerBarWeb` and `layoutWeb` in exactly the shape mb-airmond-app declares them, and `rail`, `chatHeader` and `composerBar` from the native column; a key listed in `_guideOnly` is drawn on the guide and checked by the build but not emitted, because the app reads that number from somewhere else and a second copy would be a second thing to keep true. ⛔ Emoji reactions were part of the same proposal and Cédric declined them on 2026-09-23: the hover rail stays Reply + More, and `thread.rail` still says why.",
    "_sources": {
      "web": "mb-airmond-app src/theme/tokens.ts on main after #286 (threadWeb, railWeb, chatHeaderWeb, composerBarWeb, layoutWeb, layout.stageMinWindowWidth), and its DESIGN.md entries of 2026-09-23 and 2026-09-24",
      "mac": "mb-airmond-ios main after #530: AirmondKit ThreadLayout.Mac and ComposerLayout.Mac, MacApp MacWindowStyle and MacWindowRootView (sidebar ideal width)",
      "native": "the `thread` block above (the build checks it) and mb-airmond-app's native `thread`, `chatHeader`, `composerBar` and `rail`"
    },
    "thread": {
      "_comment": "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.",
      "_guideOnly": ["measureShare", "columnInset", "maxColumnWidth", "cardCornerRadius"],
      "bodySize": { "web": 14, "mac": 14, "native": 17, "doc": "Message type. The thread has ONE size; hierarchy inside a bubble is weight and space." },
      "bodyLineHeight": { "web": 20, "mac": 20, "native": 26, "doc": "Message line height. (The Mac sets 14pt with 3 of lineSpacing, which lands at 19.7.)" },
      "maxMeasure": { "web": 640, "mac": 640, "native": 640, "doc": "The widest one bubble may be, before the share binds." },
      "measureShare": { "web": 0.78, "mac": 0.78, "native": 0.78, "doc": "A bubble's widest as a share of the thread column." },
      "columnInset": { "web": 16, "mac": 16, "native": 14, "doc": "The 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": { "web": null, "mac": 1000, "native": 900, "doc": "The widest the thread column gets. None on the web beside a sidebar: the column is the pane." },
      "textBubbleHorizontalInset": { "web": 12, "mac": 12, "native": 14, "doc": "Text to a bubble's left and right edge." },
      "textBubbleVerticalInset": { "web": 8, "mac": 9, "native": 9, "doc": "Text to a bubble's top and bottom edge." },
      "bubbleCornerRadius": { "web": 10, "mac": 14, "native": 18, "doc": "The bubble's corner, both speakers. Cards never read it." },
      "cardCornerRadius": { "web": 14, "mac": null, "native": 14, "doc": "A 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." },
      "gapSameSender": { "web": 5, "mac": 5, "native": 12, "doc": "Between two consecutive messages from the same sender." },
      "gapNewSender": { "web": 16, "mac": 16, "native": 12, "doc": "Where the sender changes." }
    },
    "chatHeader": {
      "_comment": "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.",
      "_guideOnly": ["height"],
      "height": { "web": 44, "mac": 40, "native": 50, "doc": "The band: paddingVertical x 2 + avatarSize, which the build checks for web and native. The Mac's is the system's compact titlebar, about 40." },
      "avatarSize": { "web": 24, "mac": 24, "native": 34, "doc": "Her portrait." },
      "avatarSpacing": { "web": 10, "mac": 8, "native": 9, "doc": "Portrait to name." },
      "paddingVertical": { "web": 10, "mac": null, "native": 8, "doc": "The band's own padding, the same above and below." },
      "barMargin": { "web": 16, "mac": null, "native": 16, "doc": "The bar's horizontal margin, each side: the page gutter." },
      "trailingActionWidth": { "web": 44, "mac": null, "native": 44, "doc": "One trailing action's tap target." },
      "trailingClearance": { "web": 12, "mac": null, "native": 12, "doc": "The end of her name to the call mark, so a truncated name reads as truncated." },
      "minimumNameWidth": { "web": 72, "mac": null, "native": 72, "doc": "The narrowest her name is squeezed to: about eight characters." },
      "trailingActionMarginVertical": { "web": -10, "mac": null, "native": 0, "doc": "How far the call mark's target overhangs the band. Negative pulls it in, so the target fills the band rather than growing it." },
      "nameSize": { "web": 14, "mac": 14, "native": 17, "doc": "Her name's size. Native takes `typography.bodyStrong`, so its `nameType` is null." },
      "nameLineHeight": { "web": 20, "mac": null, "native": 23, "doc": "Her name's line height." },
      "nameWeight": { "web": 600, "mac": 600, "native": 600, "doc": "Her name's weight: semibold everywhere." }
    },
    "composer": {
      "_comment": "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.",
      "height": { "web": 42, "mac": 42, "native": 48, "doc": "The bar at rest." },
      "control": { "web": 42, "mac": 30, "native": 48, "doc": "One control's square box, which holds the disc." },
      "disc": { "web": 30, "mac": 30, "native": 36, "doc": "The circle drawn inside a control: height less 6 each side." },
      "radius": { "web": 21, "mac": 21, "native": 24, "doc": "The pill's corner: half the resting height, and a literal, so a grown field becomes a rounded box rather than an ellipse." },
      "fieldRest": { "web": 34, "mac": 30, "native": 40, "doc": "The text field's height with one line in it." },
      "glyph": { "web": 16, "mac": 14, "native": 20, "doc": "The `+` glyph." },
      "glyphSmall": { "web": 15, "mac": 14, "native": 18, "doc": "The mic and send glyphs, a step smaller." },
      "textSize": { "web": 14, "mac": 14, "native": 17, "doc": "The typed text and the placeholder, the same size as the messages it sends. Native takes `typography.body`, so its `text` is null." },
      "textLineHeight": { "web": 20, "mac": null, "native": 23, "doc": "Their line height." }
    },
    "rail": {
      "_comment": "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.",
      "_guideOnly": ["rowHeight", "headActionDisc", "headActionGlyph", "headActionGap", "headActionsSpaceAbove", "headCaptionSize", "headCaptionLineHeight", "headNameSize", "headSubtitleSize"],
      "width": { "web": 300, "mac": 232, "native": 300, "doc": "Full rail width, padding included." },
      "paddingVertical": { "web": 20, "mac": null, "native": 20, "doc": "The rail's own padding, top and bottom." },
      "paddingHorizontal": { "web": 12, "mac": null, "native": 12, "doc": "The rail's own padding, each side." },
      "rowGap": { "web": 2, "mac": null, "native": 2, "doc": "Between two nav rows." },
      "rowPaddingVertical": { "web": 6, "mac": null, "native": 10, "doc": "One nav row's padding, above and below its label." },
      "rowPaddingHorizontal": { "web": 10, "mac": null, "native": 12, "doc": "One nav row's padding, each side." },
      "glyphGap": { "web": 10, "mac": null, "native": 12, "doc": "Glyph column to label." },
      "rowRadius": { "web": 8, "mac": null, "native": 10, "doc": "The active capsule's corner." },
      "glyphSlot": { "web": 18, "mac": null, "native": 24, "doc": "The fixed glyph column, so every label starts on one x." },
      "glyphSize": { "web": 16, "mac": null, "native": 20, "doc": "The glyph drawn in that column." },
      "labelSize": { "web": 14, "mac": 14, "native": 15, "doc": "Row label size." },
      "labelLineHeight": { "web": 20, "mac": null, "native": 21, "doc": "Row label line height." },
      "rowHeight": { "web": 32, "mac": 32, "native": 41, "doc": "One row, derived: rowPaddingVertical x 2 + labelLineHeight. The build checks it." },
      "headGap": { "web": 10, "mac": null, "native": 10, "doc": "Portrait to name in the head." },
      "headPaddingTop": { "web": 6, "mac": null, "native": 6, "doc": "Above the head, under the rail's own padding." },
      "headPaddingBottom": { "web": 18, "mac": null, "native": 18, "doc": "Under the head's actions, before the first row." },
      "headPortrait": { "web": 108, "mac": 40, "native": 108, "doc": "Her portrait in the head, the door to About." },
      "headActionDisc": { "web": 48, "mac": 24, "native": 48, "doc": "Call / Message / Record. 48 is `layout.minTapTarget`, which is what the app reads; Call is the one coral disc, the other two are `surface`." },
      "headActionGlyph": { "web": 20, "mac": 12, "native": 20, "doc": "The glyph inside one of those discs." },
      "headActionGap": { "web": 12, "mac": null, "native": 12, "doc": "Between two discs (`spacing.md`)." },
      "headActionsSpaceAbove": { "web": 16, "mac": null, "native": 16, "doc": "Her name to the row of discs (`spacing.lg`)." },
      "headCaptionSize": { "web": 12, "mac": null, "native": 12, "doc": "The word under each disc, in ink. The Mac's discs carry no caption." },
      "headCaptionLineHeight": { "web": 15, "mac": null, "native": 15, "doc": "Its line height." },
      "headNameSize": { "web": 18, "mac": 14, "native": 20, "doc": "Her name in the head: the `heading` step on each type ramp (typographyWide on the web)." },
      "headSubtitleSize": { "web": null, "mac": 11, "native": null, "doc": "The Mac's one-line hotkey subtitle under her name. The hero head has none." },
      "footPaddingVertical": { "web": 8, "mac": null, "native": 10, "doc": "The pinned foot (Settings), above and below." },
      "footPaddingHorizontal": { "web": 10, "mac": null, "native": 12, "doc": "The same foot, each side." }
    },
    "layout": {
      "_comment": "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.",
      "stageMinWindowWidth": { "web": 720, "mac": null, "native": null, "doc": "The browser width at which the sidebar appears and `typographyWide` takes over." }
    }
  },
  "claims": [
    { "name": "light: ink on coral (primary button text)", "fg": "#18222B", "bg": "#FF6B5A", "min": 4.5 },
    { "name": "light: white on coral must FAIL (that is why ink, never white)", "fg": "#FFFFFF", "bg": "#FF6B5A", "max": 3.0 },
    { "name": "light: coral-deep as text on white", "fg": "#C63A28", "bg": "#FFFFFF", "min": 4.5 },
    { "name": "light: coral-deep as text on ground", "fg": "#C63A28", "bg": "#F8FAFC", "min": 4.5 },
    { "name": "light: coral-deep on coral-soft wash", "fg": "#C63A28", "bg": "#FFF2EF", "min": 4.5 },
    { "name": "light: confirm on ground", "fg": "#3E5D86", "bg": "#F8FAFC", "min": 4.5 },
    { "name": "light: confirm on card", "fg": "#3E5D86", "bg": "#FFFFFF", "min": 4.5 },
    { "name": "light: confirm on confirm-soft", "fg": "#3E5D86", "bg": "#E8EEF7", "min": 4.5 },
    { "name": "light: white on confirm (filled confirm control)", "fg": "#FFFFFF", "bg": "#3E5D86", "min": 4.5 },
    { "name": "light: white on confirmDeep (RN pressed fill)", "fg": "#FFFFFF", "bg": "#34517A", "min": 4.5 },
    { "name": "light: waiting on ground", "fg": "#A9560B", "bg": "#F8FAFC", "min": 4.5 },
    { "name": "light: waiting on waiting-soft", "fg": "#A9560B", "bg": "#FBF1DE", "min": 4.5 },
    { "name": "light: foreground on background. Also the NEW-MESSAGES PILL's plate against the thread it floats over: ink on ground is the pill's fill read the other way round, and a floating object has to be findable against whatever scrolls under it, which is the argument for a filled pill rather than an outlined one", "fg": "#18222B", "bg": "#F8FAFC", "min": 7.0 },
    { "name": "light: muted-foreground on background", "fg": "#4E5E6B", "bg": "#F8FAFC", "min": 4.5 },
    { "name": "light: muted-foreground on muted", "fg": "#4E5E6B", "bg": "#F1F5F8", "min": 4.5 },
    { "name": "light: foreground on secondary wash", "fg": "#18222B", "bg": "#EAF0F4", "min": 4.5 },
    { "name": "light: foreground on member bubble", "fg": "#18222B", "bg": "#E8EEF7", "min": 4.5 },
    { "name": "dark: ink on dusty coral (primary button text)", "fg": "#18222B", "bg": "#E38273", "min": 4.5 },
    { "name": "dark: white on dusty coral must FAIL", "fg": "#FFFFFF", "bg": "#E38273", "max": 3.0 },
    { "name": "dark: foreground on background. Also the new-messages pill's plate against the dark thread, the same pairing read the other way round", "fg": "#DCE0E5", "bg": "#171E2A", "min": 7.0 },
    { "name": "dark: foreground on card", "fg": "#DCE0E5", "bg": "#1F2833", "min": 7.0 },
    { "name": "dark: muted-foreground on background", "fg": "#93A0AD", "bg": "#171E2A", "min": 4.5 },
    { "name": "dark: confirm on card (the DERIVED value)", "fg": "#8FB2DC", "bg": "#1F2833", "min": 4.5 },
    { "name": "dark: confirm on background", "fg": "#8FB2DC", "bg": "#171E2A", "min": 4.5 },
    { "name": "dark: confirm on confirm-soft", "fg": "#8FB2DC", "bg": "#2E3E58", "min": 4.5 },
    { "name": "dark: white on confirmDeep (RN filled confirm)", "fg": "#FFFFFF", "bg": "#3E5D86", "min": 4.5 },
    { "name": "dark: waiting gold on card", "fg": "#D9BC6A", "bg": "#1F2833", "min": 4.5 },
    { "name": "dark: waiting gold on background", "fg": "#D9BC6A", "bg": "#171E2A", "min": 4.5 },
    { "name": "dark: waiting on waiting-soft", "fg": "#D9BC6A", "bg": "#3A3524", "min": 4.5 },
    { "name": "dark: coral-deep (coralOnDark) on background", "fg": "#FF9081", "bg": "#171E2A", "min": 4.5 },
    { "name": "dark: coral-deep on coral-soft wash", "fg": "#FF9081", "bg": "#3A282A", "min": 4.5 },
    { "name": "dark: foreground on member bubble", "fg": "#DCE0E5", "bg": "#2E3E58", "min": 4.5 },
    { "name": "dark: foreground on secondary", "fg": "#DCE0E5", "bg": "#2A3646", "min": 4.5 },
    { "name": "dark: dusty coral UI mark on background (3.0 non-text)", "fg": "#E38273", "bg": "#171E2A", "min": 3.0 },
    { "name": "icon main: ink A on coral plate", "fg": "#16211E", "bg": "#FF6B5A", "min": 4.5 },
    { "name": "icon beta: dusty-coral A on dusk plate (3.0 graphical)", "fg": "#E38273", "bg": "#171E2A", "min": 3.0 },
    { "name": "icon admin: ink A on feather-wash plate", "fg": "#18222B", "bg": "#EAF0F4", "min": 4.5 },
    { "name": "icon admin: coral-deep tick on feather-wash (3.0 graphical)", "fg": "#C63A28", "bg": "#EAF0F4", "min": 3.0 },
    { "name": "light: rail label on the active row (sidebarAccent)", "fg": "#18222B", "bg": "#EAF0F4", "min": 4.5 },
    { "name": "dark: rail label on the active row (sidebarAccent)", "fg": "#DCE0E5", "bg": "#2A3646", "min": 4.5 },
    { "name": "light: rail label at rest, muted on the rail ground", "fg": "#4E5E6B", "bg": "#F1F5F8", "min": 4.5 },
    { "name": "dark: rail label at rest, muted on the rail ground", "fg": "#93A0AD", "bg": "#131926", "min": 4.5 },
    { "name": "light: ink on coral-hover (primary button, hovered)", "fg": "#18222B", "bg": "#FF7A6B", "min": 4.5 },
    { "name": "light: white on coral-hover must FAIL (hover keeps ink type)", "fg": "#FFFFFF", "bg": "#FF7A6B", "max": 3.0 },
    { "name": "dark: ink on coral-hover (primary button, hovered)", "fg": "#18222B", "bg": "#E98F80", "min": 4.5 },
    { "name": "dark: white on coral-hover must FAIL", "fg": "#FFFFFF", "bg": "#E98F80", "max": 3.0 },
    { "name": "light: confirm focus ring on ground (3.0 non-text; the ring sits at 2px offset, on the page, never on the coral)", "fg": "#3E5D86", "bg": "#F8FAFC", "min": 3.0 },
    { "name": "light: confirm focus ring on card (3.0 non-text)", "fg": "#3E5D86", "bg": "#FFFFFF", "min": 3.0 },
    { "name": "dark: confirm focus ring on ground (3.0 non-text)", "fg": "#8FB2DC", "bg": "#171E2A", "min": 3.0 },
    { "name": "light: live-chip dot, confirm on the Feather chip ground (3.0 non-text)", "fg": "#3E5D86", "bg": "#F8FAFC", "min": 3.0 },
    { "name": "light: status pill, muted on muted (waiting on a third party)", "fg": "#4E5E6B", "bg": "#F1F5F8", "min": 4.5 },
    { "name": "dark: status pill, muted on muted (waiting on a third party)", "fg": "#93A0AD", "bg": "#1F2833", "min": 4.5 },
    { "name": "light: tile label, confirm on card (the 11px uppercase eyebrow)", "fg": "#3E5D86", "bg": "#FFFFFF", "min": 4.5 },
    { "name": "light: trust chip, muted on muted", "fg": "#4E5E6B", "bg": "#F1F5F8", "min": 4.5 },
    { "name": "light: white on ink: the nav pill, the closing band, and `Open in Gmail` on a DraftCard, which is the ink fill and never coral because the thread spends its one coral on the composer's send disc (there is no Send: Airmond never sends from a member's mailbox)", "fg": "#FFFFFF", "bg": "#18222B", "min": 7.0 },
    { "name": "stage: ink on the off-the-record amber", "fg": "#18222B", "bg": "#E8A317", "min": 4.5 },
    { "name": "stage: secondary caption on the amber (the stage ink at 0.75, resolving to #4C4226)", "fg": "#4C4226", "bg": "#E8A317", "min": 4.5 },
    { "name": "stage: dusk ink on the dusk ground (the dark scheme's own pair, asserted again because the stage reads it edge to edge, across a table)", "fg": "#DCE0E5", "bg": "#171E2A", "min": 7.0 },
    { "name": "stage: secondary caption on the dusk (the dusk ink at 0.68, resolving to #9DA2A9 — the dusk opacity is unchanged; only the amber needed more ink)", "fg": "#9DA2A9", "bg": "#171E2A", "min": 4.5 },
    { "name": "call: primary ink on the call ground \u2014 the dusk pair, read out of the dark scheme rather than repeated", "fg": "#DCE0E5", "bg": "#171E2A", "min": 7.0 },
    { "name": "call: the End disc against the call ground (3.0 non-text: a control must be findable). It is stage.stopOnDusk, read from there so the two Ends cannot drift", "fg": "#FF453A", "bg": "#171E2A", "min": 3.0 },
    { "name": "call: the white End GLYPH on the End disc \u2014 a 24pt handset, so the bar is 3.0 and not 4.5", "fg": "#FFFFFF", "bg": "#FF453A", "min": 3.0 },
    { "name": "call: white on the End disc must FAIL the 4.5 TEXT bar \u2014 the record of why the Stop button's word is ink while this glyph is white, and why the two rules are not in conflict", "fg": "#FFFFFF", "bg": "#FF453A", "max": 4.5 },
    { "name": "call: the quiet control wash CANNOT make a shape (ink at 0.18 composites to #3A414C, 1.63:1) \u2014 it never did on the retired black either (#2E2E2E, 1.55:1), and 0.30 reaches only 2.33:1. The disc is found by its outline, never by its fill", "fg": "#3A414C", "bg": "#171E2A", "max": 3.0 },
    { "name": "call: the control OUTLINE against the call ground (rule at control.card.borderWidth composites to #6E7986) \u2014 this is what makes an unarmed control a shape", "fg": "#6E7986", "bg": "#171E2A", "min": 3.0 },
    { "name": "stage: ink on Stop, dusk (systemRed #FF453A kept, the LABEL changed — 17pt semibold is body text, so 4.5)", "fg": "#18222B", "bg": "#FF453A", "min": 4.5 },
    { "name": "stage: ink on Stop, off the record (systemRed #FF3B30)", "fg": "#18222B", "bg": "#FF3B30", "min": 4.5 },
    { "name": "stage: white on Stop must FAIL, dusk (that is why the label is ink, never white — the same reason onCoral is ink)", "fg": "#FFFFFF", "bg": "#FF453A", "max": 4.5 },
    { "name": "stage: white on Stop must FAIL, off the record", "fg": "#FFFFFF", "bg": "#FF3B30", "max": 4.5 },
    { "name": "stage: the Stop fill itself against the dusk ground (3.0 non-text: a control must be findable)", "fg": "#FF453A", "bg": "#171E2A", "min": 3.0 },
    { "name": "stage: the Stop fill CANNOT separate from the amber ground by luminance (measures 1.64:1) — so the button is identified by its ink LABEL, never by its edge. Recorded as a must-fail because no red that reads on dusk can also clear 3.0 on amber: the two windows are disjoint (>=3 on dusk needs L>=0.138, >=3 on amber needs L<=0.111)", "fg": "#FF3B30", "bg": "#E8A317", "max": 3.0 },
    { "name": "stage: the live dot on the dusk ground (3.0 non-text; there is no amber counterpart — the pill swaps to a pause glyph)", "fg": "#E8574A", "bg": "#171E2A", "min": 3.0 },
    { "name": "stage: kind chip, confirm blue on its own dusk wash (confirm at 0.18, resolving to #2D394A)", "fg": "#8FB2DC", "bg": "#2D394A", "min": 4.5 },
    { "name": "stage: kind chip, stage ink on its own amber wash (ink at 0.12, resolving to #CF9419)", "fg": "#18222B", "bg": "#CF9419", "min": 4.5 },
    { "name": "stage: speaker 1 mark on the dusk ground (3.0 non-text)", "fg": "#5CB8F0", "bg": "#171E2A", "min": 3.0 },
    { "name": "stage: speaker 1 mark on the amber ground must FAIL 3.0 — NO roster hue can be a legible mark on the amber, and this is arithmetic rather than a bad pick: clearing 3.0 on the dusk needs a luminance of at least 0.138 and clearing 3.0 on the amber needs at most 0.111, so the two windows do not overlap for ANY colour. Recorded as a must-fail so the file states the real relationship; off the record the chip is therefore identified by the NAME and the capsule outline, and speaking is signalled by weight and border, never by hue alone", "fg": "#5CB8F0", "bg": "#E8A317", "max": 3.0 },
    { "name": "stage: speaker 2 mark on the dusk ground (3.0 non-text)", "fg": "#FAB55C", "bg": "#171E2A", "min": 3.0 },
    { "name": "stage: speaker 2 mark on the amber ground must FAIL 3.0 — NO roster hue can be a legible mark on the amber, and this is arithmetic rather than a bad pick: clearing 3.0 on the dusk needs a luminance of at least 0.138 and clearing 3.0 on the amber needs at most 0.111, so the two windows do not overlap for ANY colour. Recorded as a must-fail so the file states the real relationship; off the record the chip is therefore identified by the NAME and the capsule outline, and speaking is signalled by weight and border, never by hue alone", "fg": "#FAB55C", "bg": "#E8A317", "max": 3.0 },
    { "name": "stage: speaker 3 mark on the dusk ground (3.0 non-text)", "fg": "#B899F5", "bg": "#171E2A", "min": 3.0 },
    { "name": "stage: speaker 3 mark on the amber ground must FAIL 3.0 — NO roster hue can be a legible mark on the amber, and this is arithmetic rather than a bad pick: clearing 3.0 on the dusk needs a luminance of at least 0.138 and clearing 3.0 on the amber needs at most 0.111, so the two windows do not overlap for ANY colour. Recorded as a must-fail so the file states the real relationship; off the record the chip is therefore identified by the NAME and the capsule outline, and speaking is signalled by weight and border, never by hue alone", "fg": "#B899F5", "bg": "#E8A317", "max": 3.0 },
    { "name": "stage: speaker 4 mark on the dusk ground (3.0 non-text)", "fg": "#EC8DEC", "bg": "#171E2A", "min": 3.0 },
    { "name": "stage: speaker 4 mark on the amber ground must FAIL 3.0 — NO roster hue can be a legible mark on the amber, and this is arithmetic rather than a bad pick: clearing 3.0 on the dusk needs a luminance of at least 0.138 and clearing 3.0 on the amber needs at most 0.111, so the two windows do not overlap for ANY colour. Recorded as a must-fail so the file states the real relationship; off the record the chip is therefore identified by the NAME and the capsule outline, and speaking is signalled by weight and border, never by hue alone", "fg": "#EC8DEC", "bg": "#E8A317", "max": 3.0 },
    { "name": "stage: speaker 5 mark on the dusk ground (3.0 non-text)", "fg": "#F58FAD", "bg": "#171E2A", "min": 3.0 },
    { "name": "stage: speaker 5 mark on the amber ground must FAIL 3.0 — NO roster hue can be a legible mark on the amber, and this is arithmetic rather than a bad pick: clearing 3.0 on the dusk needs a luminance of at least 0.138 and clearing 3.0 on the amber needs at most 0.111, so the two windows do not overlap for ANY colour. Recorded as a must-fail so the file states the real relationship; off the record the chip is therefore identified by the NAME and the capsule outline, and speaking is signalled by weight and border, never by hue alone", "fg": "#F58FAD", "bg": "#E8A317", "max": 3.0 },
    { "name": "light: the chat surface's quiet text, muted on the card and on the message-bar field — the card's subtitle line and the field's placeholder. This is the tone that REPLACED faint across the chat section: faint misses the body-text bar on this very surface (3.08:1, claimed below), so every placeholder, caption and timestamp here is muted instead", "fg": "#4E5E6B", "bg": "#FFFFFF", "min": 4.5 },
    { "name": "dark: the same quiet chat text on surfaceRaised, which IS the dark message bar's field — the dark field is raised off the card rather than flush with it, so this is a different measurement from the card pairing and has to be claimed on its own", "fg": "#93A0AD", "bg": "#273240", "min": 4.5 },
    { "name": "dark: chat card titles and the typed message on the raised field, held to the same 7.0 bar as the light side", "fg": "#DCE0E5", "bg": "#273240", "min": 7.0 },
    { "name": "light: a refusal reason sits in the chat card's SUBTITLE slot, in the waiting amber — beside the field that caused it and never in a toast, so it has to hold as body text on the card rather than merely be findable. (The dark counterpart is the existing 'dark: waiting gold on card'.)", "fg": "#A9560B", "bg": "#FFFFFF", "min": 4.5 },
    { "name": "light: the AgentScreen's live dot is CORAL-DEEP, not coral (3.0 non-text) — full coral measures only 2.55:1 on that panel and misses the non-text bar, which is the same reason the admin app icon's tick is coral-deep rather than coral", "fg": "#C63A28", "bg": "#F1F5F8", "min": 3.0 },
    { "name": "dark: the same AgentScreen live dot on the dark panel, dark coral-deep on groundAlt (3.0 non-text)", "fg": "#FF9081", "bg": "#131926", "min": 3.0 },
    { "name": "light: faint must FAIL the body-text bar on the chat card and the message-bar field (3.08:1) — faint never carries a word on this surface, which is exactly why every placeholder, caption and timestamp in the chat section uses muted instead", "fg": "#8595A2", "bg": "#FFFFFF", "max": 4.5 },
    { "name": "light: faint must FAIL even the NON-TEXT minimum on the AgentScreen panel (2.81:1), so it is not usable as a mark there either — the chat surface has no legitimate use for faint at all, and this claim is what says so if somebody reaches for it", "fg": "#8595A2", "bg": "#F1F5F8", "max": 3.0 },
    { "name": "light: the quiet disc (the + control) and the quiet button have essentially no plate contrast against the field (1.15:1), BY DESIGN — they are identified by their GLYPH and their label instead (ink on the quiet fill measures 14.03:1), which is why every icon in this section is a drawn SVG and never a text glyph", "fg": "#EAF0F4", "bg": "#FFFFFF", "max": 3.0 },
    { "name": "dark: the quiet disc and the quiet button have the same non-relationship with the dark field (1.06:1) — read by their glyph and their label, never by their edge", "fg": "#2A3646", "bg": "#273240", "max": 3.0 },
    { "name": "dark: CredentialHandoff's required marker, dark coral-deep on the card — the one place the coral family appears on a card, and it marks a required FIELD rather than an action, so it is held to the body-text bar. (The light counterpart is the existing 'light: coral-deep as text on white'.)", "fg": "#FF9081", "bg": "#1F2833", "min": 4.5 },
    { "name": "light: AgentScreen handed over — the waiting amber as the panel's 2px BORDER (3.0 non-text). This is the only place on the chat surface where a panel changes its edge, because control moving from her to the member has to be unmistakable at a glance: amber is the system's waiting-on-you tone and she is blocked until it comes back", "fg": "#A9560B", "bg": "#F1F5F8", "min": 3.0 },
    { "name": "dark: the same handed-over border on the dark panel (3.0 non-text)", "fg": "#D9BC6A", "bg": "#131926", "min": 3.0 },
    { "name": "light: full coral as the AgentScreen live dot must FAIL the 3.0 non-text bar on the panel (2.55:1) — this is the claim that MAKES the coral-deep substitution safe: without it the page could go on printing the comparison while the value moved. The dot is coral-deep instead, claimed just above", "fg": "#FF6B5A", "bg": "#F1F5F8", "max": 3.0 },
    { "name": "light: a ChoiceCard option title, ink on the recessed panel the option rows sit on — the option rows are the panel tone rather than the card, so this is its own pairing and it carries the member's actual choices, hence 7.0", "fg": "#18222B", "bg": "#F1F5F8", "min": 7.0 },
    { "name": "dark: the same ChoiceCard option title on the dark panel", "fg": "#DCE0E5", "bg": "#131926", "min": 7.0 },
    { "name": "light: a ChoiceCard option KEY (the lettered badge), muted on the quiet fill — the badge is how an option is referred to out loud (“go with B”), so it is text and held to 4.5", "fg": "#4E5E6B", "bg": "#EAF0F4", "min": 4.5 },
    { "name": "dark: the same ChoiceCard option key on the dark quiet fill — 4.59:1, the tightest pairing on the chat surface and the one to watch if either token moves", "fg": "#93A0AD", "bg": "#2A3646", "min": 4.5 },
    { "name": "dark: the MessageBar recording waveform, confirm blue on the raised field (3.0 non-text). The waveform is confirm rather than coral on purpose: the armed send disc is the bar's one coral", "fg": "#8FB2DC", "bg": "#273240", "min": 3.0 },
    { "name": "dark: the armed coral send disc's plate against the DARK field (3.0 non-text). Unlike the light scheme, this one clears — which is why the light claim is a must-fail and this one is not, and why the ink glyph has to carry the control in both", "fg": "#E38273", "bg": "#273240", "min": 3.0 },
    { "name": "dark: full dusty coral on the dark panel (3.0 non-text) — the dark half of the live-dot comparison. It clears on the dark panel, so only the light scheme forces the coral-deep substitution; the page prints both numbers side by side", "fg": "#E38273", "bg": "#131926", "min": 3.0 },
    { "name": "dark: the AgentScreen watching bar's live dot — dark coral-deep on the quiet fill (3.0 non-text). The bar is the one place the live mark sits on the quiet fill instead of on the panel, so it is its own pairing. (The light counterpart is the existing 'icon admin: coral-deep tick on feather-wash', the same two values.)", "fg": "#FF9081", "bg": "#2A3646", "min": 3.0 },

    { "name": "light: the thread, Ada's words on her bubble fill, and the new-messages pill's plate against a bubble scrolling under it (the same two values, read the other way round). The bubble is unbroken body text a member reads for minutes at a time, so the thread holds it to 7.0 rather than the 4.5 the palette page claims for the same tones elsewhere", "fg": "#18222B", "bg": "#F1F5F8", "min": 7.0 },
    { "name": "dark: the thread, Ada's words on her bubble fill, and the pill's plate against a dark bubble under it", "fg": "#DCE0E5", "bg": "#1F2833", "min": 7.0 },
    { "name": "light: the thread, the member's own words on the member bubble, held to the same 7.0 as hers; the palette page claims this pair at 4.5 for a chip, and a bubble is not a chip", "fg": "#18222B", "bg": "#E8EEF7", "min": 7.0 },
    { "name": "dark: the thread, the member's own words on the member bubble", "fg": "#DCE0E5", "bg": "#2E3E58", "min": 7.0 },
    { "name": "light: the thread's quiet tone, muted on the bubble fill. Every marker, list number, quote and footnote inside a bubble is this tone, because faint fails the same pairing (claimed below)", "fg": "#4E5E6B", "bg": "#F1F5F8", "min": 4.5 },
    { "name": "dark: the thread's quiet tone, muted on the bubble fill", "fg": "#93A0AD", "bg": "#1F2833", "min": 4.5 },
    { "name": "light: faint must FAIL the body-text bar on the thread's bubble fill (2.81:1). This is the correction the thread section exists to record: faint carries no marker, timestamp, caption or footnote anywhere on this surface, and muted carries all four", "fg": "#8595A2", "bg": "#F1F5F8", "max": 4.5 },
    { "name": "dark: faint must FAIL the body-text bar on the thread's bubble fill (3.35:1). It clears the 3.0 non-text bar in Low flame, which is exactly the trap: a tone that is legal as a mark in one scheme and illegal as a word in both", "fg": "#6C7987", "bg": "#1F2833", "max": 4.5 },
    { "name": "light: faint must FAIL even the NON-TEXT minimum on the thread ground (2.94:1), where the timestamp, the day break, the activity line and the rail glyphs all sit; so it is not usable as a mark out there either", "fg": "#8595A2", "bg": "#F8FAFC", "max": 3.0 },
    { "name": "dark: faint must FAIL the body-text bar on the thread ground (3.76:1)", "fg": "#6C7987", "bg": "#171E2A", "max": 4.5 },
    { "name": "light: every caption the thread has sits on the thread ground, not inside a bubble: muted there (6.40:1) carries the timestamp, the day break, the activity line and the rail glyphs", "fg": "#4E5E6B", "bg": "#F8FAFC", "min": 4.5 },
    { "name": "dark: the same captions on the dark thread ground", "fg": "#93A0AD", "bg": "#171E2A", "min": 4.5 },
    { "name": "dark: nothing quiet may go INSIDE the member's bubble; muted there measures 4.05:1 and must fail the body bar. This is why every caption in the thread sits on the ground instead, and the claim is what stops a timestamp being moved into his bubble because it looked tidier", "fg": "#93A0AD", "bg": "#2E3E58", "max": 4.5 },
    { "name": "light: the unread divider's label, coral-deep on the thread ground (4.97:1). It is a word a member reads, so it takes the 4.5 body bar and not the 3.0 a mark would get", "fg": "#C63A28", "bg": "#F8FAFC", "min": 4.5 },
    { "name": "dark: the unread divider's label, coral-deep on the thread ground (7.61:1)", "fg": "#FF9081", "bg": "#171E2A", "min": 4.5 },
    { "name": "light: full coral as the unread label must FAIL (2.68:1). It misses the 4.5 text bar and the 3.0 non-text bar alike, which is the whole reason the label is coral-deep; without this claim the page could go on printing the comparison while the value moved", "fg": "#FF6B5A", "bg": "#F8FAFC", "max": 3.0 },
    { "name": "dark: full dusty coral CLEARS the body bar on the dark thread ground (6.14:1). Only Feather forces the coral-deep substitution, and Low flame follows it anyway because one role takes one token", "fg": "#E38273", "bg": "#171E2A", "min": 4.5 },
    { "name": "light: a link inside a bubble, confirm on the bubble fill. Links are the working accent, never coral", "fg": "#3E5D86", "bg": "#F1F5F8", "min": 4.5 },
    { "name": "dark: a link inside a bubble, confirm on the bubble fill", "fg": "#8FB2DC", "bg": "#1F2833", "min": 4.5 },
    { "name": "light: code in the thread, ink on the tinted plate, inline and fenced alike. The plate is now the `mark` tone at thread.mark.plateFillOpacity rather than the quiet fill, so that the plate and its edge come off ONE ink instead of two unrelated tokens", "fg": "#18222B", "bg": "#DAE0E4", "min": 4.5 },
    { "name": "dark: code in the thread, ink on the tinted plate", "fg": "#DCE0E5", "bg": "#2F3944", "min": 4.5 },
    { "name": "light: the code plate's FILL has essentially no contrast against the bubble it sits in (1.22:1) and is not asked to. It is a TINT, not a boundary, and that is precisely why the plate's EDGE cannot be the exception to the 3.0 mark rule: with a fill this quiet, an edge under 3.0 would leave the plate drawn by nothing measurable at all", "fg": "#DAE0E4", "bg": "#F1F5F8", "max": 3.0 },
    { "name": "dark: the code plate's fill against the dark bubble fill (1.27:1), the same tint", "fg": "#2F3944", "bg": "#1F2833", "max": 3.0 },
    { "name": "light: the code plate's fill against the thread ground (1.22:1), for a bubble that is nothing but code", "fg": "#E0E4E8", "bg": "#F8FAFC", "max": 3.0 },
    { "name": "dark: the code plate's fill against the dark thread ground (1.26:1)", "fg": "#28303C", "bg": "#171E2A", "max": 3.0 },
    { "name": "light: Ada's bubble does not separate from the thread ground by luminance (1.05:1) and is not asked to; the 1px hairline and the side it sits on are what make it a bubble. Recorded so nobody deepens the fill to fix a number that was never load-bearing", "fg": "#F1F5F8", "bg": "#F8FAFC", "max": 3.0 },
    { "name": "dark: Ada's bubble against the dark thread ground (1.12:1), the same by design", "fg": "#1F2833", "bg": "#171E2A", "max": 3.0 },
    { "name": "light: the member's bubble against the thread ground (1.11:1); one step further from the ground than hers, still not a boundary anything relies on", "fg": "#E8EEF7", "bg": "#F8FAFC", "max": 3.0 },
    { "name": "dark: the member's bubble against the dark thread ground (1.55:1)", "fg": "#2E3E58", "bg": "#171E2A", "max": 3.0 },
    { "name": "light: the unread divider's RULE cannot be what tells you. The hairline is the ink at 0.10, resolving to #E2E4E7 over the thread ground (1.22:1), so it is texture; the LABEL carries the meaning, which is why the label is held to the body bar above", "fg": "#E2E4E7", "bg": "#F8FAFC", "max": 3.0 },
    { "name": "dark: the same unread rule in Low flame, the ink at 0.12 resolving to #2F3540 over the dark ground (1.36:1)", "fg": "#2F3540", "bg": "#171E2A", "max": 3.0 },

    { "name": "rn light: coralDeep on the app's own ground, which is the pairing mb-airmond-app actually reads (lightColors.coralDeep on lightColors.ground, 4.97:1). Same two hexes as the web claim above and claimed again on purpose: the app mirrors the rn block and NOTHING else, so until 2026-09-14 this tone was not reachable from the app at all and the unread label shipped as full coral at 2.68:1", "fg": "#C63A28", "bg": "#F8FAFC", "min": 4.5 },
    { "name": "rn dark: coralDeep on the app's own dark ground (darkColors.coralDeep on darkColors.ground, 7.61:1). In Low flame the deep coral is LIGHTER than coral rather than darker: deep is relative to the ground it has to carry a word on", "fg": "#FF9081", "bg": "#171E2A", "min": 4.5 },

    { "name": "light: the thread's SECTION RULE, the `mark` tone (muted at 0.7, resolving to #7F8B95) on Ada's bubble fill (3.18:1). Claimed on the BUBBLE and never on the page ground, because the ground is the context the kit's specimens live in and the bubble is the surface the product draws on", "fg": "#7F8B95", "bg": "#F1F5F8", "min": 3.0 },
    { "name": "dark: the thread's section rule on the dark bubble fill (3.50:1)", "fg": "#707C88", "bg": "#1F2833", "min": 3.0 },
    { "name": "light: the thread's QUOTE RULE, the same `mark` tone on the same fill. Claimed separately from the section rule although the pairing is identical: they are two components, and a tone that moved for one of them would have to be argued for both", "fg": "#7F8B95", "bg": "#F1F5F8", "min": 3.0 },
    { "name": "dark: the thread's quote rule on the dark bubble fill", "fg": "#707C88", "bg": "#1F2833", "min": 3.0 },
    { "name": "light: the CODE PLATE'S EDGE, held to the same 3.0 as the two rules rather than to the Mac's 0.5 (which measures 2.16:1 here). Decided explicitly: a plate may be read by its fill instead of its edge, but this fill measures 1.22:1, so the edge is the only measurable thing drawing the plate", "fg": "#7F8B95", "bg": "#F1F5F8", "min": 3.0 },
    { "name": "dark: the code plate's edge on the dark bubble fill", "fg": "#707C88", "bg": "#1F2833", "min": 3.0 },
    { "name": "light: `border` INSIDE A BUBBLE must FAIL the 3.0 mark bar (1.21:1), the ink at 0.10 resolves to #DBE0E4 on the bubble fill. This is the claim the whole correction hangs on: `border` is the hairline token for the PAGE GROUND, it clears 3.0 nowhere near a bubble, and so no thread mark may ever use it no matter what the page around it does", "fg": "#DBE0E4", "bg": "#F1F5F8", "max": 3.0 },
    { "name": "dark: `border` inside a dark bubble must FAIL the 3.0 mark bar (1.38:1), the same token, the same non-relationship, in the other scheme", "fg": "#363E48", "bg": "#1F2833", "max": 3.0 },

    { "name": "light: THE CARD'S HAIRLINE, the `rule` tone drawn on the card, measured against the page ground it separates the card from (3.20:1). The same tone and the same 3.0 bar as the thread's marks: one rule, one tone, every surface", "fg": "#838E97", "bg": "#F8FAFC", "min": 3.0 },
    { "name": "dark: the card's hairline against the dark ground (3.92:1)", "fg": "#707C88", "bg": "#171E2A", "min": 3.0 },
    { "name": "light: the card's FILL against the ground must FAIL (1.05:1), a shadow at 6% is all that separated a card from the page until 2026-09-14, on every card on every screen, which is a third of what a shape needs. This claim is why the hairline came back", "fg": "#FFFFFF", "bg": "#F8FAFC", "max": 3.0 },
    { "name": "dark: the card's fill against the dark ground (1.12:1), in Low flame the lift is tone rather than shadow, and it is still not a boundary", "fg": "#1F2833", "bg": "#171E2A", "max": 3.0 },
    { "name": "light: `border` as the card's hairline must FAIL (1.16:1), the ink at 0.10 over a white card. This is the version that would have shipped if the card had simply reused the hairline token, and it is why the card's edge is the `rule` tone instead", "fg": "#E8E9EA", "bg": "#F8FAFC", "max": 3.0 },
    { "name": "dark: `border` as the card's hairline must FAIL (1.54:1)", "fg": "#363E48", "bg": "#171E2A", "max": 3.0 },

    { "name": "light: THE SECONDARY CONTROL'S OUTLINE on the page ground (3.24:1). `secondary` was a near-invisible FILL at 24 call sites; it is an outline in the `rule` tone now, because a button a member cannot find is not a button", "fg": "#818D97", "bg": "#F8FAFC", "min": 3.0 },
    { "name": "dark: the secondary control's outline on the dark ground (3.78:1)", "fg": "#6E7986", "bg": "#171E2A", "min": 3.0 },
    { "name": "light: the same outline where the control sits ON A CARD rather than on the ground (3.34:1), a secondary button appears in both places and has to be findable in both", "fg": "#838E97", "bg": "#FFFFFF", "min": 3.0 },
    { "name": "dark: the secondary control's outline on a dark card (3.50:1)", "fg": "#707C88", "bg": "#1F2833", "min": 3.0 },
    { "name": "light: the OLD secondary fill (`groundAlt`) against the page ground must FAIL (1.05:1), this is the measurement that retired the fill. The label carried the whole control and the plate carried nothing", "fg": "#F1F5F8", "bg": "#F8FAFC", "max": 3.0 },
    { "name": "dark: the old secondary fill against the dark ground (1.05:1), identical, in the scheme where a fill was the only thing that could have worked", "fg": "#131926", "bg": "#171E2A", "max": 3.0 },
    { "name": "light: the old secondary fill on a CARD must FAIL (1.10:1)", "fg": "#F1F5F8", "bg": "#FFFFFF", "max": 3.0 },
    { "name": "dark: the old secondary fill on a dark card must FAIL (1.18:1)", "fg": "#131926", "bg": "#1F2833", "max": 3.0 },

    { "name": "light: A DISABLED CONTROL'S LABEL, `onDisabled` on `disabledFill` (5.82:1). A disabled control should look UNAVAILABLE, not ABSENT: the label is held to the full 4.5 body bar because the dim is exactly what hides the copy explaining what enabling it would cost", "fg": "#4E5E6B", "bg": "#EAF0F4", "min": 4.5 },
    { "name": "dark: a disabled control's label, `onDisabled` on `disabledFill` (4.59:1), the tightest pairing in the token set and the one to watch if either value moves", "fg": "#93A0AD", "bg": "#2A3646", "min": 4.5 },
    { "name": "light: the disabled PLATE against the ground must FAIL (1.10:1), which is why a disabled control keeps the same `rule` outline an enabled secondary has: the plate says unavailable and the outline says it is still a control", "fg": "#EAF0F4", "bg": "#F8FAFC", "max": 3.0 },
    { "name": "dark: the disabled plate against the dark ground (1.37:1)", "fg": "#2A3646", "bg": "#171E2A", "max": 3.0 },
    { "name": "light: THE OLD DISABLED RECIPE, `opacity: 0.5` on the whole group, must FAIL the body bar. On a secondary the ink label resolves to #888E94 and the plate to #F5F8FA, measuring 3.10:1: the dim halves the label and the fill together, so the control loses its words and its shape in one step", "fg": "#888E94", "bg": "#F5F8FA", "max": 4.5 },
    { "name": "dark: the old disabled recipe on a dark secondary (4.25:1), it misses the bar in Low flame too, by less, which is exactly the kind of near-miss a single opacity produces and a token pair cannot", "fg": "#7A7F88", "bg": "#151C28", "max": 4.5 },
    { "name": "light: the old disabled recipe on a CORAL primary is the worst case (1.92:1): the ink label goes to #888E94 and the coral plate to #FCB3AB, and the label all but disappears into its own button", "fg": "#888E94", "bg": "#FCB3AB", "max": 4.5 },
    { "name": "dark: the old disabled recipe on a dark coral primary (2.45:1)", "fg": "#18202B", "bg": "#7D504F", "max": 4.5 },

    { "name": "light: `faint` must FAIL the body bar on the quiet fill (2.68:1). faint is RETIRED from member-facing text: it carries provenance, dates and timestamps, which is the line that says where information came from, and it measured 2.64 to 3.08 in Feather on every ground the app has. Those readings become `muted`", "fg": "#8595A2", "bg": "#EAF0F4", "max": 4.5 },
    { "name": "light: `faint` on the member's own bubble is the worst light reading (2.64:1)", "fg": "#8595A2", "bg": "#E8EEF7", "max": 4.5 },
    { "name": "dark: `faint` must FAIL the body bar on the recessed ground (3.95:1), the best reading faint gets anywhere in Low flame, and it still misses", "fg": "#6C7987", "bg": "#131926", "max": 4.5 },
    { "name": "dark: `faint` on the raised surface (2.92:1), which is the dark message bar's own field", "fg": "#6C7987", "bg": "#273240", "max": 4.5 },
    { "name": "dark: `faint` on the quiet fill (2.75:1)", "fg": "#6C7987", "bg": "#2A3646", "max": 4.5 },
    { "name": "dark: `faint` on the member's own bubble is the worst reading in the whole set (2.43:1)", "fg": "#6C7987", "bg": "#2E3E58", "max": 4.5 },

    { "name": "light: THE DRAFTCARD'S EDGE, the `rule` tone on the card against the thread ground (3.20:1). A card in the thread is the first component drawn AFTER the mark rule was written down, so it takes the rule tone rather than `border` without anybody having to catch it in review", "fg": "#838E97", "bg": "#F8FAFC", "min": 3.0 },
    { "name": "dark: the DraftCard's edge against the dark thread ground (3.92:1)", "fg": "#707C88", "bg": "#171E2A", "min": 3.0 },

    { "name": "light: THE DRAFT CARD'S caption (To · Re:), muted on the card (6.69:1): the address IS the fact a member checks before opening", "fg": "#4E5E6B", "bg": "#FFFFFF", "min": 4.5 },
    { "name": "dark: the draft card's caption on the dark card (5.58:1)", "fg": "#93A0AD", "bg": "#1F2833", "min": 4.5 },
    { "name": "light: `Open in Gmail`, the draft card's ONE primary, on-primary on the ink fill (15.41:1). Ink, never coral; and there is no Send, because Airmond never sends from a member's mailbox", "fg": "#F8FAFC", "bg": "#18222B", "min": 7.0 },
    { "name": "dark: `Open in Gmail` on the dark ink fill (12.61:1)", "fg": "#171E2A", "bg": "#DCE0E5", "min": 7.0 },
    { "name": "light: `Read all`, confirm on the card (6.74:1): it opens the rest of her text in place, and links on this system are the working accent", "fg": "#3E5D86", "bg": "#FFFFFF", "min": 4.5 },
    { "name": "dark: `Read all` on the dark card (6.79:1)", "fg": "#8FB2DC", "bg": "#1F2833", "min": 4.5 },
    { "name": "light: `Change it`, the quiet control, muted on the card (6.69:1): a word with no plate, so the text bar", "fg": "#4E5E6B", "bg": "#FFFFFF", "min": 4.5 },
    { "name": "light: the REPLACED record's strip label, muted on groundAlt (6.11:1). The record reads lighter than the live card below it and still clears the text bar", "fg": "#4E5E6B", "bg": "#F1F5F8", "min": 4.5 },
    { "name": "dark: the replaced record's strip label on the dark groundAlt (6.59:1)", "fg": "#93A0AD", "bg": "#131926", "min": 4.5 },
    { "name": "light: the replaced record's strip against the card must FAIL 3.0 (1.10:1): it is found by its words and its arrow, never by its plate", "fg": "#F1F5F8", "bg": "#FFFFFF", "max": 3.0 },
    { "name": "light: the pen on the draft card's head tile, muted on groundAlt (3.0 non-text; 6.11:1)", "fg": "#4E5E6B", "bg": "#F1F5F8", "min": 3.0 },
    { "name": "dark: the pen on the dark head tile (6.59:1)", "fg": "#93A0AD", "bg": "#131926", "min": 3.0 },

    { "name": "light: THE NEW-MESSAGES PILL, the ground colour on the ink fill (15.41:1). Ink and not confirm, which would mean confirmed and succeeded and live about messages nobody has read; ink and not coral, which the thread has already spent", "fg": "#F8FAFC", "bg": "#18222B", "min": 4.5 },
    { "name": "dark: the new-messages pill's label on its ink fill (12.61:1)", "fg": "#171E2A", "bg": "#DCE0E5", "min": 4.5 },

    { "name": "light: THE MARK TONE ON THE MEMBER'S OWN BUBBLE scrapes over the bar at 3.07:1, by 0.07. Claimed at 3.0 so the margin is on the record and the next token move cannot quietly spend it. Low flame is what actually decides the rule: see the must-fail below", "fg": "#7C8995", "bg": "#E8EEF7", "min": 3.0 },
    { "name": "dark: the mark tone on the member's own bubble MISSES the bar at 2.79:1, and it is the one surface in the system that does. 0.21 under the bar the whole pass is named after, and no specimen drew it, which is why the guard is in code rather than in a sentence. Recorded as a must-fail rather than fixed by raising the opacity: 0.8 would clear it at 3.15:1 and would also take the card's hairline to 3.98:1 and 4.63:1 against the ground, which is the wireframe the 2026-09-02 ruling removed. His bubble holds his words and nothing else instead, the same rule that already keeps every caption out of it, applied in BOTH schemes because one role takes one answer", "fg": "#758394", "bg": "#2E3E58", "max": 3.0 },
    { "name": "light: A DISABLED CONTROL'S OUTLINE against the ground (3.38:1). The section calls this outline not optional, because the disabled plate alone measures 1.10:1: the plate says unavailable and the outline says it is still a control. It was the one load-bearing mark in that argument with no claim behind it", "fg": "#7D8A94", "bg": "#F8FAFC", "min": 3.0 },
    { "name": "dark: a disabled control's outline against the dark ground (4.16:1)", "fg": "#74808E", "bg": "#171E2A", "min": 3.0 },
    { "name": "light: the mark tone on the QUIET FILL (3.08:1), which is the tightest surface it clears anywhere: a mark drawn on a status pill, a hover mark or a disabled plate. Claimed because the margin is 0.08 and the next token move is what will spend it", "fg": "#7D8A94", "bg": "#EAF0F4", "min": 3.0 },
    { "name": "dark: the mark tone on the dark quiet fill (3.05:1), tighter still", "fg": "#74808E", "bg": "#2A3646", "min": 3.0 },
    { "name": "light: ink on a white card, the pairing every card title and every field value on the DraftCard rests on (16.12:1). It was drawn on the guide and measured there without ever being claimed, which is the kind of gap that only shows up when somebody counts", "fg": "#18222B", "bg": "#FFFFFF", "min": 7.0 },
    { "name": "light: CHAT DENSITY (2026-09-24), the sidebar hero's name, ink on the rail ground (14.71:1)", "fg": "#18222B", "bg": "#F1F5F8", "min": 7.0 },
    { "name": "dark: the sidebar hero's name on the dark rail ground (13.26:1)", "fg": "#DCE0E5", "bg": "#131926", "min": 7.0 },
    { "name": "light: the hero's Call / Message / Record captions at 12/15, ink on the rail ground (14.71:1). A word under a disc, so the text bar", "fg": "#18222B", "bg": "#F1F5F8", "min": 4.5 },
    { "name": "dark: the hero's captions on the dark rail ground (13.26:1)", "fg": "#DCE0E5", "bg": "#131926", "min": 4.5 },
    { "name": "light: a sidebar row's 14px label at rest, muted on the rail ground (6.11:1): the compact row came down a size and kept the tone", "fg": "#4E5E6B", "bg": "#F1F5F8", "min": 4.5 },
    { "name": "dark: a sidebar row's 14px label at rest on the dark rail ground (6.59:1)", "fg": "#93A0AD", "bg": "#131926", "min": 4.5 },
    { "name": "light: the Message and Record glyphs, ink on the surface disc (3.0 non-text; 16.12:1)", "fg": "#18222B", "bg": "#FFFFFF", "min": 3.0 },
    { "name": "dark: the Message and Record glyphs on the dark surface disc (11.24:1)", "fg": "#DCE0E5", "bg": "#1F2833", "min": 3.0 },
    { "name": "light: the surface disc against the rail ground must FAIL 3.0 (1.10:1). The disc is found by its glyph and its caption, never by its plate, which is the hero's native design and is recorded so nobody credits the plate", "fg": "#FFFFFF", "bg": "#F1F5F8", "max": 3.0 },
    { "name": "dark: the surface disc against the dark rail ground must FAIL 3.0 (1.18:1)", "fg": "#1F2833", "bg": "#131926", "max": 3.0 },
    { "name": "light: the coral Call disc against the rail ground must FAIL 3.0 (2.55:1). The disc is found by its ink glyph (5.76:1, claimed as ink on coral) and its caption; the plate is identity, not a boundary", "fg": "#FF6B5A", "bg": "#F1F5F8", "max": 3.0 },
    { "name": "dark: the dusty coral Call disc against the dark rail ground clears 3.0 (6.45:1)", "fg": "#E38273", "bg": "#131926", "min": 3.0 },
    { "name": "light: the 44 chat header's name at 14/600, ink on the header band (surface; 16.12:1)", "fg": "#18222B", "bg": "#FFFFFF", "min": 7.0 },
    { "name": "dark: the chat header's name on the dark band (11.24:1)", "fg": "#DCE0E5", "bg": "#1F2833", "min": 7.0 },
    { "name": "light: the 42 composer's 14px placeholder, muted on the field (groundAlt; 6.11:1). The size came down a step and the bar did not move", "fg": "#4E5E6B", "bg": "#F1F5F8", "min": 4.5 },
    { "name": "dark: the composer's placeholder on the dark field (groundAlt; 6.59:1)", "fg": "#93A0AD", "bg": "#131926", "min": 4.5 },
    { "name": "light: the web thread's 14/20 message text, ink on HER bubble (the muted wash; 14.71:1). Smaller type keeps the 7.0 bar the 17/26 thread was held to", "fg": "#18222B", "bg": "#F1F5F8", "min": 7.0 },
    { "name": "dark: the web thread's 14/20 message text on her dark bubble (11.24:1)", "fg": "#DCE0E5", "bg": "#1F2833", "min": 7.0 },
    { "name": "light: LongMessage's Show more / Show less, the body's ink at 600 on HIS bubble (memberBubble; 13.82:1). The control is his bubble's ink, never muted and never onCoral", "fg": "#18222B", "bg": "#E8EEF7", "min": 4.5 },
    { "name": "dark: LongMessage's control, ink on his dark bubble (8.14:1)", "fg": "#DCE0E5", "bg": "#2E3E58", "min": 4.5 },
    { "name": "light: a FINISHED action card's detail lines, muted on the card (surface; 6.69:1)", "fg": "#4E5E6B", "bg": "#FFFFFF", "min": 4.5 },
    { "name": "light: a finished card's report, muted on the card's inset (groundAlt; 6.11:1)", "fg": "#4E5E6B", "bg": "#F1F5F8", "min": 4.5 },
    { "name": "dark: a finished card's detail lines, muted on the dark card (surface; 5.58:1)", "fg": "#93A0AD", "bg": "#1F2833", "min": 4.5 },
    { "name": "dark: a finished card's report, muted on the dark inset (surfaceRaised; 4.87:1). The tightest surface the finished rule draws on, and it clears", "fg": "#93A0AD", "bg": "#273240", "min": 4.5 },
    { "name": "light: an action card's speaker label, confirm on the inset (6.15:1). Labels keep their own tone when the card finishes", "fg": "#3E5D86", "bg": "#F1F5F8", "min": 4.5 },
    { "name": "dark: an action card's speaker label, confirm on the dark inset (surfaceRaised; 5.92:1)", "fg": "#8FB2DC", "bg": "#273240", "min": 4.5 },
    { "name": "dark: muted on the call's blue bubble (confirmSoft) must FAIL 4.5 (4.05:1). This is why a finished card's quiet lines stay ON the card and never move out onto the bubble", "fg": "#93A0AD", "bg": "#2E3E58", "max": 4.5 },
    { "name": "meeting tile: ink on a dusk card, which is also the ground's lightest point (the radial's centre is dark.card), so it covers the title and every card's text", "fg": "#DCE0E5", "bg": "#1F2833", "min": 4.5 },
    { "name": "meeting tile: muted on a dusk card and on the ground's lightest point: the subtitle, kind labels, meta rows, struck words, and the note icon (3.0 would do for the icon)", "fg": "#93A0AD", "bg": "#1F2833", "min": 4.5 },
    { "name": "meeting tile: muted on the raised fill (dark.secondary): the detail half of the status pill. The thinnest pass on the tile (4.59:1); a darker pill would fail it", "fg": "#93A0AD", "bg": "#2A3646", "min": 4.5 },
    { "name": "meeting tile: ink on the raised fill: the owner pill's name and the status pill's label", "fg": "#DCE0E5", "bg": "#2A3646", "min": 4.5 },
    { "name": "meeting tile: the Edited tag, waiting amber on waiting-soft", "fg": "#D9BC6A", "bg": "#3A3524", "min": 4.5 },
    { "name": "meeting tile: waiting amber on a dusk card: the due date as text, and the knowledge-update and confirmation-pin icons", "fg": "#D9BC6A", "bg": "#1F2833", "min": 4.5 },
    { "name": "meeting tile: the Reading aloud tag, coral-deep on the coral wash (rn.dark.coralWash) composited over the card", "fg": "#FF9081", "bg": "#463A40", "min": 4.5 },
    { "name": "meeting tile: confirm blue on a dusk card and on the ground's lightest point: the line under a confirmed member, and the answer and summary icons", "fg": "#8FB2DC", "bg": "#1F2833", "min": 4.5 },
    { "name": "meeting tile: coral-deep on a dusk card (3.0 non-text): the action icon (coral is reserved for the one kind that asks someone to act), and the live dot on the ground's lightest point. Never set as text on the tile except inside its own wash", "fg": "#FF9081", "bg": "#1F2833", "min": 3.0 },
    { "name": "meeting tile: the confirmed badge (confirm blue) against the dusk ground (3.0 non-text)", "fg": "#8FB2DC", "bg": "#171E2A", "min": 3.0 },
    { "name": "meeting tile: the ground-coloured check on the confirmed badge (3.0 non-text glyph)", "fg": "#171E2A", "bg": "#8FB2DC", "min": 3.0 },
    { "name": "meeting tile: ink initials on speaker hue 1", "fg": "#18222B", "bg": "#5CB8F0", "min": 4.5 },
    { "name": "meeting tile: ink initials on speaker hue 2", "fg": "#18222B", "bg": "#FAB55C", "min": 4.5 },
    { "name": "meeting tile: ink initials on speaker hue 3", "fg": "#18222B", "bg": "#B899F5", "min": 4.5 },
    { "name": "meeting tile: ink initials on speaker hue 4", "fg": "#18222B", "bg": "#EC8DEC", "min": 4.5 },
    { "name": "meeting tile: ink initials on speaker hue 5", "fg": "#18222B", "bg": "#F58FAD", "min": 4.5 },
    { "name": "meeting tile: the Removed tag, rn.dark.danger on its own wash at 0.16 composited over the card (13 px type, so 4.5). The mockup's 0.18 measured 4.40:1 and missed the bar; 0.16 is the highest wash that clears it (decided 2026-10-01)", "fg": "#F2857C", "bg": "#41373F", "min": 4.5 },
    { "name": "meeting tile: RECORDED, NOT DECIDED. The card's 12% hairline (rn.dark.border over the card) does not clear the 3.0 mark bar (1.38:1), and the card's fill does not separate from the ground either (1.12:1, the next claim). The approved card is found by its text and its kind icon, which clear 4.5 and 3.0; whether a feed card on a video tile is a mark the mark rule covers is a question for the next pass, not this one", "fg": "#363E48", "bg": "#1F2833", "max": 3.0 },
    { "name": "meeting tile: the dusk card's fill against the dusk ground does not separate by luminance (1.12:1) and is not asked to; recorded so nobody darkens the ground to fix a number that was never load-bearing", "fg": "#1F2833", "bg": "#171E2A", "max": 3.0 },
    { "name": "meeting tile: the confirmation pin's amber hairline (waiting at 0.28 over the card) is a tint, not a boundary (1.86:1); the pin is found by its amber question icon and its 18 px title", "fg": "#535142", "bg": "#1F2833", "max": 3.0 },
    { "name": "meeting tile: the portrait's resting ring (the dusk ink at stage.duskHairlineOpacity over the ground) frames the photo and is not a mark (2.33:1); the face is what identifies her", "fg": "#525862", "bg": "#171E2A", "max": 3.0 }
  ]
}
