The candidate baseline from round 1 is now decided. This sheet is no longer a chooser between variants — it is the acceptance record for the locked createAppTheme(overrides) baseline, and the object later theme changes get re-checked against. The MUI 7.3.11 raw-default comparison stays throughout so the delta is still visible. Every contrast ratio shown is computed by an inline script at render time, not hand-typed. Static HTML/CSS, light mode only, no app code.
#FAFAFA page, three opaque ink tiers, status split into text and fill roles, 40px controls (44px hit area on touch), 3/8px radii, no resting shadow.stale has no precedent in the estate and is unvalidated in a running screen.Covers: the token table; ink/border/surface with the page-background strip; the divider/controlBorder split; status split into text and fill roles; the settled type scale, control height, radius and shadow rule; the data-series ramp and its greyscale check; a render-time contrast script; and the 14 agreed component specimens, each beside its MUI default.
Does not cover: dark mode (out of scope for v1); components outside the agreed list; the createAppTheme() implementation and its override API; an audit of the 14 apps against this baseline; and the series-4/5 hue proximity itself, which is unchanged.
Explicit: the MUI column is a CSS transcription of MUI v7's documented defaults, not a live mount, and it renders in a system-sans stand-in rather than Roboto. Judge size, weight, radius, colour and shadow from it — not glyph shapes.
Baseline columns use DM Sans 400/500/600, base64-embedded from @fontsource/dm-sans@5.2.8 in this estate's node_modules — no network fetch. MUI columns use a system-sans stand-in, not Roboto.
MUI's default text is translucent (rgba(0,0,0,0.87)) and shifts with whatever sits behind it. The candidate uses three fixed opaque tiers. Muted was darkened twice: #6A7178 → #6A7178 → #6A7178.
divider vs. controlBorderWCAG treats the two differently, so one token cannot serve both. divider is decorative and exempt from 1.4.11; it stays pale. controlBorder is a control's boundary and needs ≥3:1 against its surface — the inherited input.border never cleared it. Derived from the same ink hue at higher opacity.
Same four states, MUI default beside the candidate, every boundary computed live. MUI's rest default also fails 3:1; its hover/focus/error states clear it. A rest-state gap in the library, not a deviation from it.
Checkbox boxes and outlined Button/Chip edges use controlBorder too — the same rest-state fix wherever the sub-3:1 input.border was the outline.
One hex cannot be both text and fill: #1E8E3E measures 4.20:1 as text on white and — the formula being symmetric — the same 4.20:1 as a fill under white text. Each status now carries a text tone and a fill tone with its own contrastText. The text tones are checked against their own Alert tint, not against white — a tint lowers contrast, and checking the wrong surface hid a real failure: warning measured 4.10:1 on #FBF0DC, now #976100 at 4.62:1.
MUI's own palette.contrastThreshold defaults to 3, not WCAG's 4.5 — its auto-computed contrastText can legally ship combinations that fail AA normal text. No fill+contrastText row is rendered for this column: the point being made is the token design, not a second MUI demo.
No estate precedent. Three prototypes invented a stale concept, all in amber: cockpit #9a7b33, fitness-monitor reused --status-warn. #5B6B7D is new — included on the operator's decision, because reusing amber would collide with warning, which is the reason the token exists. Unvalidated in a running screen; the first consuming app validates it.
Six colours chosen to avoid every status hue (no green, amber, red or blue-grey), so a series is never mistaken for a status. Muted and cool-to-neutral, unchanged on the operator's verdict. No second channel: dash patterns, marker shapes and pattern fills were all removed on review — how a chart separates series is DS-4's question.
Colour removed. Series 4 and 5 remain near-identical greys (109 vs 106), and nothing else distinguishes them since the marker glyphs were dropped.
Desaturated greys (live, sRGB 0–255): 92 · 123 · 129 · 109 · 106 · 119. Series 4 and 5 differ by ~3/255, below reliable perception; 2/3/6 sit inside 10 points. With nothing but hue carrying identity, a six-series chart does not satisfy WCAG 1.4.1.
Same six series, solid strokes, no per-series pattern or marker — removed on operator review (2026-08-09). Series 4 and 5 cross around March/May, which is where the close-luminance pair matters most; in the desaturated pane they are not reliably separable.
The same six series as bar fills — flat colour, no pattern. In the desaturated pane the close pairs are not separable.
With the second channel gone, the desaturated panes are not reliably readable — series 4 and 5 in particular. Surface contrast is unaffected (desaturation preserves relative luminance); this section is about series-to-series distinguishability under 1.4.1, not text contrast.
MUI's shadows[1] is a visible three-layer drop shadow — right for 2014 Material, wrong for a flat dashboard. Round 1's near-invisible shadow only moved the judgment to how invisible is enough. The rule: Paper and Card at rest have zero shadow (elevation: 0, variant: 'outlined'); weight exists only on transient surfaces — Dialog, Drawer, Menu, Tooltip. Binary and checkable in review.
MUI's h6 is 20px/500; the corpus runs 15–16px card titles and 13px tile titles. In the decided scale h4 (20px) is the screen title — a visual variant only. Semantically it stays <h1>: in MUI, component="h1" variant="h4". Treating the variant as the element produces a broken heading hierarchy and h6 (16px) the card title; h1–h3 stay usable rather than decorative.
MUI: medium OutlinedInput ~56px, small ~40px. Decided: 40px (36px in round 1), consistently across Button/Select/TextField/Chip. 34–36px is a recorded defect against the 44px touch guideline in an approved envelope; 40px sits on MUI's small, and @media (pointer: coarse) bumps to 44px — simulated here. Radius settled at 3px control / 8px card.
Decided baseline throughout, no variants left to pick. Buttons: textTransform: none, no elevation on contained — all 7 sampled app themes override MUI's uppercase independently, so this replaces 7 repeated overrides.
| App | Staging | CI |
|---|---|---|
| hram | Success · 4h | 2 failing |
| jg-ferien | Success · 5d | OK |
| App | Staging | CI |
|---|---|---|
| hram | Success · 4h | 2 failing |
| jg-ferien | Success · 5d | OK |
| App | Staging | CI |
|---|---|---|
| hram | Success · 4h | 2 failing |
| jg-ferien | Success · 5d | OK |
| App | Staging | CI |
|---|---|---|
| hram | Success · 4h | 2 failing |
| jg-ferien | Success · 5d | OK |
A literal phone frame, not a resized window — at the decided control height, with the table→card behaviour below the usual breakpoint.
Below the corpus breakpoint, each row becomes a card — same fields, stacked instead of columned. Cards are border-only at rest, per the shadow rule above.
Release candidate, not yet a locked baseline. The motion tokens are declared but not demonstrated — a static specimen cannot show timing, and this document renders without JS in the environments it has been opened in so far. Reviewed 2026-08-09 by a second reader; the warning/tint contrast failure, the chart-token contradiction, the touch and focus rules and this document's own legibility were corrected in response. This document is what a later theme change gets re-checked against. Below: what is verified here at render time, and what still has to be confirmed elsewhere.
contrastText pair clears 4.5:1. Success and warning needed a fill hue distinct from their text tone; critical and stale reuse one hex, which clears in both directions.input.border (1.59 rest / 2.72 hover) never cleared 3:1. Split into divider (exempt) and controlBorder (all states ≥3:1), applied to TextField, Select, Checkbox and outlined Button/Chip.#FAFAFA is decided (operator, 2026-08-09) after three warm candidates were rejected. The four-value strip is kept as provenance.stale has no estate precedent and has not been seen in a running screen — a decision, not a validation. The first consuming app tests it.createAppTheme() implementation, and per-app conformance.Four values side by side: pure white, the chosen neutral, the original cool value, and the smallest perceptible warm shift. Each carries a real white card, so the question is whether the card still reads as a card. Path: #F5F7F8 → #F7F6F3 → #FCFBF9 → #FAFAFA.
Card on #FFFFFF
body text inside a white surface
Body text directly on the page
Muted caption directly on the page
0, 0, 0 · cast (R−B): 0Card on #FAFAFA
body text inside a white surface
Body text directly on the page
Muted caption directly on the page
5, 5, 5 · cast (R−B): 0Card on #F5F7F8
body text inside a white surface
Body text directly on the page
Muted caption directly on the page
10, 8, 7 · cast (R−B): −3Card on #FAFAF7
body text inside a white surface
Body text directly on the page
Muted caption directly on the page
5, 5, 8 · cast (R−B): +3#FAFAFA is the chosen bg.page (operator, 2026-08-09). All three warm candidates read as too warm: against a blue-cast value, warmer meant neutral, and the need was a lightness step off white, not a hue. #FAFAFA is MUI's grey[50]. hram, fitness-monitor and innoservice ship pure white today and change visibly on adoption; jg-ferien and spesix override for identity.
#6A7178 cleared 4.5:1 only on pure white (4.549) and failed all three page candidates (4.399 / 4.324 / 4.247). Darkened along the same hue to #6A7178: 4.946 on white, 4.616 on the darkest candidate — against a 4.6 target, not a nudge over 4.5. Used everywhere ink.muted appears.
Every decided value, once. A work order references this table by token name rather than retyping values; if a number here and a number in a WO disagree, this table is the one to fix. Contrast badges are computed live.
| Token | Value | Role | Computed contrast | Note |
|---|---|---|---|---|
| Ink & surface | ||||
ink.primary | #212529 | body text, headings | unchanged | |
ink.secondary | #5B6670 | subtitle2, secondary text | unchanged | |
ink.muted | #6A7178 | captions, meta text | was #6A7178 (4.549 on white, 4.247 on the darkest page candidate). Darkened to clear 4.6:1 on all of them. | |
divider | rgba(33,37,41,.10) | decorative separators, card outline | decorative — exempt from WCAG 1.4.11. Split off from the single border token this round. | |
controlBorder (rest) | rgba(33,37,41,.50) | TextField/Select rest, Checkbox box, outlined Button/Chip | new — supersedes input.border (0.23 alpha), which never cleared 3:1. Same ink hue, higher opacity. | |
controlBorder-hover | rgba(33,37,41,.65) | control outline on hover | supersedes input.border-hover (rgba(33,37,41,.45), 2.72:1, also sub-3:1) | |
control.autofill | surface inset shadow + -webkit-text-fill-color = ink.primary | autofilled input/select background and text | not a contrast pair — a suppression | MUI has no autofill styling, and the browsers disagree: the same field rendered yellow in Chromium, dark grey in Opera, dark blue in Firefox (operator, 2026-08-09). :-webkit-autofill alone misses Firefox — the standard :autofill is needed too. Not demonstrated here: this sheet's form controls are inert, so no native chrome pollutes the comparisons. The gap is real for the apps. |
controlBorder-focus | derived from the app's primary, auto-darkened until it clears 3:1 against the surface (MUI getContrastRatio/darken). No fixed hex — the baseline defines no primary. Ratios for the four real estate accents → | control outline on focus | hram #468AB2 spesix #1B8FD8 fm #0F62FE survey #432CA1 |
new — reuses the existing primary hue rather than introducing another colour |
controlBorder-error | #BF3227 | control outline in error state | new — reuses the existing critical hue | |
bg.page | #FAFAFA | app canvas / page background | — | neutral, no cast — MUI grey[50]. Fourth in the chain #F5F7F8 → #F7F6F3 → #FCFBF9 → #FAFAFA. |
bg.surface | #FFFFFF | Paper/Card/Table/Dialog | — | unchanged |
| Status — success | ||||
success.text | #35794A | Alert text/icon, table cell text | was #1E8E3E (4.20:1, FAIL). Replaced with jg-ferien's validated tone. | |
success.fill / .fillText | #1B8038 / #FFFFFF | solid chip/badge fill | new — #1E8E3E fails as a fill too (4.20:1 either direction, by symmetry). Darkened until fill+white clears 4.5:1. | |
success.bg | #E5F4E9 | Alert background tint | — | unchanged; pairs with success.text, contrast only improves vs. pure white |
| Status — warning | ||||
warning.text | #976100 | Alert text/icon, table cell text | was #A66A00 (4.484:1, short by 0.016). Darkened minimally along the same muted brown-amber hue — never yellow. | |
warning.fill / .fillText | #C08A2C / #212529 | solid chip/badge fill | jg-ferien's validated precedent, dark ink as contrastText — a mid-tone amber fails under white text. | |
warning.bg | #FBF0DC | Alert background tint | — | unchanged |
| Status — critical | ||||
critical.text | #BF3227 | Alert text/icon, table cell text | kept — already passes | |
critical.fill / .fillText | #BF3227 / #FFFFFF | solid chip/badge fill | one hex, both roles: it clears 4.5:1 on white, so by symmetry white-on-it clears too. No second hue needed. | |
critical.bg | #FBEAE8 | Alert background tint | — | unchanged |
| Status — stale | ||||
stale.text | #5B6B7D | Alert text/icon, table cell text | kept — already passes | |
stale.fill / .fillText | #5B6B7D / #FFFFFF | solid chip/badge fill | same hex, same symmetry argument as critical | |
stale.bg | #EAEDF1 | Alert background tint | — | unchanged. No precedent elsewhere in the estate — see the provenance note below. |
| Typography | ||||
h1 | 32px / 600 | rare, top-level marketing-adjacent heading | — | was 38px |
h2 | 28px / 600 | section heading | — | was 30px |
h3 | 24px / 600 | subsection heading | — | was 25px |
h4 | 20px / 600 | screen title (visual variant; the element stays <h1>) — matches the estate's real 19-22px screen titles | — | was 21px |
h5 | 18px / 600 | panel / dialog heading | — | unchanged |
h6 | 16px / 600 | card title | — | unchanged |
subtitle1 | 15px / 600 | emphasised secondary line | — | was 14px |
subtitle2 | 13px / 500 | secondary tile title | — | unchanged |
body1 | 14px / 400 | primary body copy | — | unchanged |
body2 | 13px / 400 | secondary body copy, table cells | — | unchanged |
button | 14px / 500, no uppercase | Button label | — | unchanged |
caption | 12px / 400 | captions | — | unchanged |
overline | 11px / 600, uppercase | overline labels | — | unchanged |
| Density & control height | ||||
control.height | 40px 44px @ (pointer: coarse) | Button/Select/TextField height | — | was 36px. 34–36px is a recorded defect against the 44px touch guideline; 40px sits on MUI's small, 44px activates under a coarse pointer. |
table.cellPadding | 10px 16px | TableCell | — | was 8px 12px, scaled proportionally with control height |
chip.height / .radius / .fontSize | 32px / 16px / 13px | Chip | — | was 28px / 14px / 12.5px, same proportional scaling |
| Radius | ||||
radius.control | 3px | Button/Select/TextField/Chip corner | — | settled — no A/B variant left |
radius.card | 8px | Paper/Card/Dialog corner | — | settled — no A/B variant left |
| Shadow | ||||
shadow.rest | none | Paper/Card at rest — elevation:0, variant:'outlined' is the default | — | binary rule now: rest state never has a shadow, full stop |
shadow.overlay | 0 8px 24px rgba(20,26,31,.16), 0 2px 8px rgba(20,26,31,.08) | Dialog, Drawer, Menu, Tooltip only | — | unchanged value; scope narrowed to transient surfaces only |
| Data-series ramp (categorical) | ||||
series.1…series.6 | #3D5A99 · #3E80B8 · #2E8F8A · #7A5FA8 · #9C4F86 · #8A7355 | charts, categorical fills | see the ramp section below | hues unchanged per the operator's verdict. The series-4/5 greyscale collision is not fixed — the second channel that covered it was removed; see DS-4. |
series.marker | circle · square · triangle · diamond · cross · plus | line chart point markers, series 1→6 in order | — | opt-in, off by default — DS-13. Not applied in the charts above; a multi-series chart satisfies WCAG 1.4.1 only with the overlay switched on. |
series.dasharray | none · 8,4 · 2,3 · 10,3,2,3 · 14,4 · 8,3,2,3,2,3 | line stroke pattern, series 1→6 in order | — | opt-in, off by default — DS-13. Not applied in the charts above; a multi-series chart satisfies WCAG 1.4.1 only with the overlay switched on. |
series.pattern | solid · diagonal hatch · dots · cross-hatch · h-lines · v-lines | bar/area fill pattern, series 1→6 in order | — | opt-in, off by default — DS-13. Not applied in the charts above; a multi-series chart satisfies WCAG 1.4.1 only with the overlay switched on. |
motion.fast | 120ms | hover, focus, border and colour state on controls | not a contrast pair | below ~100ms no transition is perceived; above ~150ms a control feels sluggish |
motion.base | 180ms | expand/collapse, reveal, list-item enter | — | — |
motion.overlay | 220ms in / 180ms out | Dialog, Drawer, Menu, Tooltip | — | asymmetric on purpose, following MUI's own 225/195: an entrance may be followed, an exit should get out of the way |
motion.chart | 300ms | chart entry only | — | MUI's standard: 300 is kept for exactly this case, where a line needs time to draw. Charts that poll or re-render on a timer get skipAnimation (cockpit's status board, hram's CampaignMonitorPanel) — X-Charts 8.28.2 offers no separate enter/update control, so those two lose entry animation too. A library limit, not a design choice. |
motion.easing | enter easeOut · exit easeIn · state easeInOut | which of MUI's curves applies where | — | curves are MUI's own, unchanged — only the assignment is decided here |
motion.reduced | all durations → 0.01ms | prefers-reduced-motion: reduce |
— | 0.01ms, not 0 — at exactly 0 some browsers never fire transitionend, and MUI's Grow/Fade wait for it, so the component hangs. Kept as courtesy, not compliance: reduced-motion is not itself a WCAG criterion (2.2.2 is A for auto-playing motion over 5s, 2.3.3 is AAA). |
| Spacing, gutters, buttons (already settled, unchanged) | ||||
spacing.unit | 8px | layout rhythm multiplier | — | unchanged |
container.gutter | 24px (32px ≥1200px) | Container | — | unchanged |
button.textTransform | none | Button label casing | — | unchanged |
button.elevation | none (contained) | Button contained variant | — | unchanged, and now consistent with the shadow.rest rule above |