Files
Andreas Reinhold / reiniandClaude Fable 5.1 651a513d1e Plan the Material 3 alignment, with the audits and Google's references
Every foundations, styles and components page of m3.material.io (238, from the
sitemap) extracted into docs/reference/m3, five audit reports with 142 findings in
docs/audits/m3-alignment, and the 2.0.0 plan in docs/plans/material-3-alignment.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 03:53:40 +02:00

32 KiB
Raw Permalink Blame History

Audit: core (tokens, scheme, theme, icons, shapes, guard, showcase foundations, AI docs)

Ground truth: docs/reference/m3/styles.md, docs/reference/m3/foundations.md, raw pages under m3/raw/. Library paths are relative to /Users/surtic86/Privat/Development/livewire-material.

Summary

The core is in good shape: every 2025-spec colour role (the *-dim roles and the fixed roles included) is generated and mapped, the legacy surface-tint/surface-variant are correctly left out, the corner scale is M3 Expressive's ten steps, the five shadow levels are Google's web geometry, the six springs carry the exact Compose damping/stiffness constants, the state layer uses M3's 8/10/10 % and hover is pointer-only, the typescale sizes/line heights/weights match, and theme switching honours both the OS and the visitor's choice. The 35 shapes and the full Symbols set are ported from Google's sources.

The gaps are around the edges of the system rather than in it. (1) The three custom state colours (success/warning/info) are built with the 2021 customColor recipe, so they ignore the --contrast level and the 2025 spec's tone rules that error follows. (2) Only the standard contrast level exists; M3 (May 2025) makes standard/medium/high part of the colour system. (3) The semantic ink utilities (text-meta 60 %, text-quiet 38 %, border-divider 40 %) are M2-style opacity emphasis, which M3 replaced with on-surface-variant/outline-variant, and 38 % collides with M3's disabled opacity. (4) Tailwind's own radius, shadow, type-size, weight, easing and duration utilities still compile, so an application (or an AI agent) can silently leave the M3 scale; only colours were cleared. (5) There are no tokens or variants for M3's five breakpoints, no dragged state, and no shared touch-target utility. (6) Type tracking follows the older material-web numbers rather than Compose's, and the emphasized styles reuse baseline tracking. (7) The AI docs list the utilities but explain none of M3's rules for choosing between them; that is the "core concept" document the user asked for.

Counts: must-fix 1 · should-fix 12 · nice-to-have 11.

Findings

C1 · scheme command · custom colours ignore the contrast level and the 2025 spec

  • Severity: must-fix
  • M3 says: "Custom components support contrast levels simply by using Material color roles… the role's resolved value changes per contrast level automatically" (reference-styles §Color/Contrast levels; site styles/color/roles). Aug 2024: on-container roles became "more colourful while remaining accessible"; the 2025 spec that the library asks for (--spec=2025) applies those curves to error. define-new-colors: custom colours should behave like the built-in roles.
  • Library does: resources/node/scheme.mjs (tail) builds success/warning/info with material-color-utilities' customColor() (pe(...), blend:false): fixed tones 40/100/90/10 in light and 80/20/30/90 in dark, no contrast curve, no spec awareness. So php artisan material:scheme --contrast=1 raises every M3 role but leaves the three state colours at standard contrast, and their on-*-container pairs are tone 10/90 while on-error-container (2025) is #6e0523-style tone ~25. Visible in resources/css/tokens/scheme.css:66-77 vs :62-65.
  • Fix: in bin/scheme.mjs (source of the bundle), define the three colours as DynamicColors on their own TonalPalette exactly as color_spec_2025.js defines error/onError/errorContainer/onErrorContainer (same ContrastCurves and ToneDeltaPairs), evaluated against the same DynamicScheme (so contrastLevel, isDark, specVersion, platform all apply). Rebuild resources/node/scheme.mjs; regenerate tokens/scheme.css, scheme.json, the workbench scheme. Add a SchemeCommandTest case: contrast 1 changes success.
  • Effort: M
  • Breaks API? no (role names unchanged; values change)

C2 · scheme · only the standard contrast level is generated

  • Severity: should-fix
  • M3 says: "Three levels of contrast: standard, medium (3:1 minimum), high (7:1)… contrast settings apply automatically in both light and dark theme" (reference-styles §Color; styles/color/roles, "What's new May 2025"). Accessibility principle "Honor individuals — build in customizable features" (reference-foundations §Accessibility).
  • Library does: SchemeCommand (src/Console/SchemeCommand.php:24) takes --contrast once and writes a single light and dark block; nothing at runtime can raise contrast; theme.js has no contrast state; no @media (prefers-contrast: more).
  • Fix: have material:scheme also emit [data-contrast="medium"] (contrast 0.5) and [data-contrast="high"] (1.0) blocks for light and dark (and per profile), plus @media (prefers-contrast: more) { :root:not([data-contrast]) {…} } as the OS default; add contrast to $store.theme (resources/js/theme.js) and to the head script's attribute set (theme-script.blade.php, kept across wire:navigate like data-theme); <x-theme-toggle mode="picker"> or a new <x-contrast-picker> exposes it. Document in SKILL.md §Theme.
  • Effort: M
  • Breaks API? no (opt-in)

C3 · theme.css · M2-style opacity inks and lines instead of M3 roles

  • Severity: should-fix
  • M3 says: "Default typography color is on surface (or on surface variant as a strong alternative)"; dividers use outline variant, not a lighter mix; "Always apply color roles, never static hex values"; 0.38 is the disabled opacity (reference-foundations §States values; reference-styles §Color rules, §Typography accessibility).
  • Library does: resources/css/tokens/theme.css:99-106 defines --color-meta = on-surface 60 %, --color-quiet = on-surface 38 %, --color-chrome = outline-variant 60 %, --color-divider = outline-variant 40 % via color-mix(). text-quiet therefore looks exactly like disabled text (text-on-surface/38, used 21× in components), and border-divider is lighter than M3's divider. None of these utilities is used by a package component (grep: only the showcase swatches and one border-divider/divide-divider), so they exist for ReStride's templates.
  • Fix: keep the names, repoint them to roles: --color-body → on-surface-variant (already), --color-meta → on-surface-variant, --color-quiet → outline, --color-structure → outline-variant (already), --color-chrome → outline-variant, --color-divider → outline-variant. Say in SKILL.md that emphasis is a role, not an opacity, and that 38 % means disabled. If ReStride needs the lighter divider, it overrides the variable in its own @theme.
  • Effort: S
  • Breaks API? no (names stay; rendered colour changes in ReStride)

C4 · theme.css · Tailwind's default radius, shadow, type-size, weight, easing and duration utilities still compile

  • Severity: should-fix
  • M3 says: "Do use design tokens instead of hardcoded values" (reference-foundations §Design tokens); the corner scale, elevation levels, typescale and motion tokens are the only values (reference-styles §Shape, §Elevation, §Typography rule "avoid changing type size", §Motion).
  • Library does: theme.css:19-25 clears --color-* only. rounded-lg (Tailwind: 8 px, M3 "large" is 16 px), rounded-xl (12 px vs 28 px), shadow-md, text-sm/text-lg, font-bold, leading-*, tracking-*, ease-in-out, duration-300 all still compile with Tailwind's values. The package's own views are clean (only rounded-full ×4 and rounded-none ×1, both fine), but consuming apps and AI agents are not stopped. DesignGuard does not look for them.
  • Fix: two steps. (a) Now, non-breaking: DesignGuard gains a "value outside the M3 scale" check for rounded-(xs|sm|md|lg|xl|2xl|3xl|4xl), shadow-(2xs|xs|sm|md|lg|xl|2xl), text-(xs|sm|base|lg|xl|\dxl), font-(thin|…|black), leading-*, tracking-*, ease-(in|out|in-out), duration-\d+, blur-*? (no), each with the M3 utility to use instead. (b) In the next major: @theme { --radius-*: initial; --shadow-*: initial; --ease-*: initial; --text-*: initial; --font-weight-*: initial; --leading-*: initial; --tracking-*: initial; } before re-declaring the M3 ones, mirroring what was done for colour (keep --radius-full/rounded-none behaviour by declaring --radius-full: 9999px).
  • Effort: S (guard) + S (theme, but a major-version change)
  • Breaks API? (a) no · (b) yes for applications using Tailwind defaults

C5 · type.css · tracking follows material-web, not Compose; emphasized styles reuse baseline tracking

  • Severity: should-fix
  • M3 says (reference-styles §Typography, TypeScaleTokens.kt): Display Large 0.2 sp; Title Medium 0.2 sp; Body Medium 0.2 sp; emphasized: Display Large 0, Title Medium 0.15, Body Large 0.15, Body Medium 0.25 (others unchanged).
  • Library does: resources/css/tokens/type.css:21 display-lg tracking 0.015625 rem (0.25 px, material-web); :39 title-md 0.009375 rem (0.15 px, M3: 0.2 sp = 0.0125 rem); :48 body-md 0.015625 rem (0.25 px, M3: 0.2 sp); every type-emphasized-* utility (:101 onward) uses the baseline -tracking variable, so emphasized display-lg keeps 0.25 px instead of 0, emphasized body-lg keeps 0.5 px instead of 0.15.
  • Fix: add --md-sys-typescale-emphasized-*-tracking variables with Compose's values and use them in the emphasized utilities; correct the three baseline values to Compose's (sp/16 rem). Sub-pixel, but it is what the Expressive token file says and the header comment cites material-web, which is pre-Expressive.
  • Effort: S
  • Breaks API? no

C6 · type.css · regular styles do not reset font-variation-settings

  • Severity: nice-to-have
  • M3 says: emphasized styles are applied deliberately, per element (reference-styles §Typography rules).
  • Library does: type-emphasized-* sets font-variation-settings: "ROND" 100 (type.css:107), an inherited property; type-* utilities set none, so type-body-md text inside an element carrying type-emphasized-title-md (a card slot under an emphasized wrapper) renders fully rounded.
  • Fix: every regular utility sets font-variation-settings: normal (or "ROND" 0).
  • Effort: S
  • Breaks API? no

C7 · font · variable axes of the subset are unverified here

  • Severity: nice-to-have (verification)
  • M3 says: brand/plain typefaces are the product's choice; Roboto is the default (reference-styles §Typography fonts). Google Sans Flex is Google's Expressive typeface (the site's shape page: "M3 shapes and Google Sans Flex share roundness attributes"), so the choice is aligned.
  • Library does: font.css claims weight 400700 and ROND 0100 are kept. I could not open the woff2 (no fontTools on this machine).
  • Fix: add a test (or a bin/ check) that reads the fvar table and asserts the wght and ROND axes exist, so a future re-subset cannot silently drop the axis the emphasized styles depend on.
  • Effort: S
  • Breaks API? no

C8 · motion.css · spring durations differ from the web equivalents Google publishes

  • Severity: should-fix (with a counter-argument)
  • M3 says (styles/motion/overview/specs, reference-styles §Motion "Web curve equivalents"): expressive spatial fast/default/slow = 350/500/650 ms, effects = 150/200/300 ms, each with a cubic-bezier; "springs aren't natively supported on the web, so Web should use these approximated curves with matching duration".
  • Library does: motion.css:33-52 samples the real springs into linear() (more faithful than Google's cubic-bezier) but stops at the settle time: 360/440/600 ms and 150/240/330 ms. Default spatial is 12 % quicker than Google's web number, slow effects 10 % slower.
  • Fix: either (a) sample each spring over Google's published web duration (the curve reaches 1 earlier and holds, so the bounce timing is unchanged) so the *-duration tokens read 350/500/650/150/200/300, or (b) keep the settle times and record why in the header. Also add the six published cubic-bezier curves as --md-sys-motion-spring-*-bezier fallbacks for @supports not (animation-timing-function: linear(0,1)) (Safari < 17.2 is below the floor, so this is documentation more than need). (a) is "what Google says".
  • Effort: S
  • Breaks API? no

C9 · motion.css · only the Expressive motion scheme exists

  • Severity: nice-to-have
  • M3 says: two preset schemes, Expressive (default) and Standard ("minimal bounce, for utilitarian products"), swappable product-wide (reference-styles §Motion principles; StandardMotionTokens.kt: fast spatial 0.9/1400, default 0.9/700, slow 0.9/300; effects identical).
  • Library does: six springs, Expressive only.
  • Fix: sample the three Standard spatial springs too and let [data-motion="standard"] on <html> (or a config flag rendered by the theme script) swap the --md-sys-motion-spatial-* variables. Effects are the same in both schemes.
  • Effort: S
  • Breaks API? no

C10 · state.css · no dragged state, no disabled tokens, no shared touch target

  • Severity: should-fix
  • M3 says: state layer opacities hover 0.08, focus 0.10, pressed 0.10, dragged 0.16; disabled content 0.38; "the size of state layers is 40dp while the interactive target size is 48dp"; target spacing 8 dp (reference-foundations §States, §Accessibility values).
  • Library does: state.css:26-40 covers hover/focus/active; nothing for dragged (cards, chips, list items and the slider are the components M3 gives a dragged state); no --md-sys-state-* custom properties, so components hard-code /38, /12, /10 (21, 10 and 4 occurrences); the 48 px target is built ad hoc in button.blade.php:167 with an after: pseudo-element and nowhere shared.
  • Fix: in state.css declare --md-sys-state-hover-opacity: 0.08, focus 0.10, pressed 0.10, dragged 0.16, disabled-content 0.38, disabled-container 0.12 and use them in state-layer (add &[data-dragged]::before { opacity: var(--md-sys-state-dragged-opacity) }); add a touch-target utility (the after: pseudo-element from the button, min 48×48) so small icon buttons, chips, checkboxes, radios and switch share one implementation.
  • Effort: S
  • Breaks API? no

C11 · state.css · focus ring spec has no source on the M3 site

  • Severity: nice-to-have (documentation)
  • M3 says: only "a ring-like keyboard focus indicator" (reference-foundations §States; the site defines no thickness/offset/colour — see its "Cross-cutting gaps" 1).
  • Library does: focus-ring = 3 px secondary, offset 2 px (state.css:47-53), which is material-web's md-focus-ring default (3 px, outward offset 2 px, secondary).
  • Fix: none to the code; cite material-web in the header so nobody "corrects" it to the site's silence.
  • Effort: S
  • Breaks API? no

C12 · core · no tokens or variants for M3's breakpoints

  • Severity: should-fix
  • M3 says: five breakpoints — compact < 600, medium 600839, expanded 8401199, large 12001599, extra-large ≥ 1600 dp — with the navigation component, pane count and dialog/menu choice per breakpoint (reference-foundations §Layout breakpoints tables).
  • Library does: nothing in the tokens; components switch on Tailwind's sm 640 / md 768 / lg 1024 / xl 1280 (the navigation audit maps each use). An application cannot write "at the medium breakpoint" at all.
  • Fix: theme.css adds --breakpoint-medium: 600px; --breakpoint-expanded: 840px; --breakpoint-large: 1200px; --breakpoint-extra-large: 1600px (Tailwind 4 turns these into medium:, expanded:, large:, extra-large: variants; max-medium: etc. come for free), keeps sm/md/lg/xl for now, and the components migrate to the M3 variants in their own findings. Add pane tokens as custom properties: --md-sys-layout-pane-fixed: 360px / 412px, --md-sys-layout-side-sheet-max: 400px, margins 16 px compact / 24 px otherwise (older M3 layout page values; the current site gives no numeric margin table — mark as such).
  • Effort: S (tokens) — the component migration is counted in the other audits
  • Breaks API? no

C13 · DesignGuard · no check for values outside the M3 scale, hex colours, or white/black

  • Severity: should-fix
  • M3 says: "Always apply color roles, never static hex values or raw tonal-palette values" (reference-styles §Color rules).
  • Library does: src/Testing/DesignGuard.php:26-33 flags the Tailwind palette and daisyUI colours; it does not flag arbitrary values (bg-[#1d7afc], text-[rgb(…)]), bg-white/text-black (re-added in theme.css:21-22 and not M3 roles; the M3 white is surface-container-lowest in light), or any of the non-token utilities in C4.
  • Fix: add patterns for (bg|text|border|…)-\[#, -\[rgb, -\[hsl, -\[oklch, and an opt-in forbidAbsolutes() for white/black; add the C4 list with a hint per match ("rounded-lgrounded-corner-lg").
  • Effort: S
  • Breaks API? no

C14 · theme-toggle · the picker is drawn as M3's deprecated segmented button

  • Severity: should-fix
  • M3 says: segmented buttons are "M3 only", replaced in M3 Expressive by the connected button group (reference-components-a §Segmented buttons / §Button groups; the plan doc lists segmented button among the six deprecated components).
  • Library does: resources/views/components/theme-toggle.blade.php:27-49 (mode="picker") draws an outlined 40 px group with border-outline and aria-checked fills — the segmented-button anatomy — while <x-group> (the connected button group) already exists.
  • Fix: render the picker with <x-livewire-material::group> (three wire:model-less radio options bound to $store.theme), or reuse its classes; keep role="radiogroup" and the arrow-key behaviour.
  • Effort: S
  • Breaks API? no (data-theme-option hooks can stay)

C15 · scheme · harmonisation of the custom colours is not offered

  • Severity: nice-to-have
  • M3 says: "you can choose to harmonize your static colors to the scheme's primary color… while retaining the semantic meaning"; "colors can stay completely static and forgo harmonization if their values are tied to literal sources such as brand colors" (styles/color/advanced/define-new-colors).
  • Library does: customColor(..., blend:false) always (scheme.mjs tail); no option.
  • Fix: --harmonize flag on material:scheme (and harmonize per profile) passed as blend:true; default off, as today. Do together with C1.
  • Effort: S
  • Breaks API? no

C16 · icons · one weight/grade/optical size for every context

  • Severity: nice-to-have
  • M3 says: grade 25 "for a light icon on dark background"; optical size 20 for 20 dp icons in dense/desktop layouts; 40/48 for display pairing (reference-styles §Icons axes table).
  • Library does: Material Symbols Rounded 400/0/24 only (bin/fetch-symbols, icon.blade.php header); buttons xs/sm draw 20 px icons from the 24-opsz outlines (button.blade.php:147-149), so strokes are ~17 % thinner than a true opsz-20 glyph; dark theme keeps grade 0.
  • Fix: none that is cheap — each extra cut is another 4,135 SVGs (~2.6 MB). Record the trade-off in the icon header and SKILL.md; if ever needed, ship opsz20 for the outlined set only and let <x-icon size="20"> pick it.
  • Effort: L
  • Breaks API? no

C17 · scheme · background/on-background are emitted, surface-tint/surface-variant are not

  • Severity: nice-to-have (informational, aligned)
  • M3 says: background/on-background are legacy roles with the same values as surface/on-surface; surface-variant was superseded by the surface-container family (Feb 2023); surface tint is deprecated (reference-styles §Color roles table).
  • Library does: emits and maps background/on-background (scheme.css:16-17, theme.css:29-30), omits the two deprecated ones. Correct. Consider marking bg-background as "same as bg-surface; prefer surface" in SKILL.md so agents do not treat them as two surfaces.
  • Effort: S
  • Breaks API? no

C18 · scheme · the package default is the 2025 rendering of the baseline seed, not the site's baseline table

  • Severity: nice-to-have (informational)
  • M3 says: the baseline palette table (PaletteTokens.kt, site styles/color/static/baseline) is the 2021 spec: primary 40 = #6750A4, on-primary = white.
  • Library does: tokens/scheme.css is #6750a4 tonal-spot through the 2025 spec (primary #655789, on-primary #fdf7ff). Both are Google's; the header says so. Nothing to fix; the showcase colour page could say "spec 2025" so a reader comparing with the site's swatches is not puzzled.
  • Effort: S
  • Breaks API? no

C19 · elevation · no expression of "which component rests at which level"

  • Severity: nice-to-have (documentation)
  • M3 says: level 3 = dialogs, FAB, pickers, search; level 2 = menus, scrolled app bar, navigation bar, rich tooltip, toolbar; level 1 = elevated button/card/chip, modal sheets; level 0 = everything else; +1 level on hover (reference-styles §Elevation tokens table); scrim 32 %.
  • Library does: shadow tokens only (elevation.css); the showcase shows five boxes with no mapping; components pick levels individually (correctly where I looked: menu 2, FAB 3, elevated button 1→2 on hover, bg-scrim/32 ×4).
  • Fix: put the table in the new guideline/skill and in the showcase elevation section, with M3's rule "tonal separation first, shadows for floating things and interaction".
  • Effort: S
  • Breaks API? no

C20 · showcase · the foundation pages show tokens but not M3's rules for them

  • Severity: nice-to-have
  • M3 says: (as in C19, plus) surface container hierarchy by emphasis; outline vs outline-variant; type roles and when emphasized is used; spatial vs effects; corner scale per component class; optical roundness (outer padding = inner).
  • Library does: showcase/sections/{colour,type,shape,elevation,motion,icons}.blade.php list every utility with one sentence each; the colour page groups "Ink and lines" as if they were roles (see C3).
  • Fix: one short "when to use" paragraph per section, drawn from the same guideline text (C24), so the showcase and the AI docs cannot drift.
  • Effort: S
  • Breaks API? no

C21 · theme script · data-theme only, no prefers-color-scheme in CSS — aligned

  • Severity: nice-to-have (no change)
  • M3 says: light/dark is a user preference; honour the OS and the person's choice (reference-foundations §Accessibility principles; §Color "built-in dark theme").
  • Library does: resolves system before first paint, follows OS changes while system, keeps the attributes across wire:navigate, declares color-scheme per block. Correct.
  • Fix: none. (C2 adds contrast to the same mechanism.)

C22 · reduced motion · stricter than the site, aligned with its intent

  • Severity: nice-to-have (no change)
  • M3 says: no global reduced-motion rule on styles/motion; component pages ask for parallax/expansion to be removed under reduced motion (reference-styles §Motion accessibility).
  • Library does: zeroes every duration token (motion.css:66-77); carousel, progress, tabs and figure check the media query themselves. Correct; the header's "anything that animates without them is a bug" is a good rule to repeat in the guideline.

C23 · icons · accessibility of <x-icon> — aligned

  • Severity: nice-to-have (no change)
  • M3 says: decorative icons hidden; meaningful icons labelled; don't put the role in the label (reference-foundations §Accessibility labelling).
  • Library does: aria-hidden by default, role="img" + aria-label when label is given, focusable="false" (icon.blade.php:27-33). Correct.

C24 · AI docs · the M3 core concepts are not written down anywhere an agent reads

  • Severity: should-fix (this is the user's explicit request)
  • M3 says: see the list under "Concepts missing from the AI docs" below.
  • Library does: resources/boost/guidelines/core.blade.php (6 bullets: what the package is, no maryUI, theme script, showcase, error pages/mail) and SKILL.md §Tokens/§Theme/§Conventions list the utilities and three rules (elevation is for floating things; pair easing with duration; dark: follows data-theme). Nothing says what a role, a container level, a corner size, a type role or a spring is for, nor any layout/accessibility rule.
  • Fix: a second always-on guideline resources/boost/guidelines/material-3.blade.php (short: the rules that change what an agent writes) and a fuller resources/boost/skills/material-3-design/SKILL.md (the concept reference with the tables), both generated from one source or tested against each other; Boost 2.8 loads every file under resources/boost/guidelines and every skill directory (verified in laravel/boost/src/Install/GuidelineComposer.php:320-351, SkillComposer.php:127-141). Add the drift test.
  • Effort: M
  • Breaks API? no

Deliberate deviations

Where Deviation Reason given Holds up?
shape.css corner-full = 9999px instead of material-web's 50cqmin container units need a declared container Yes. Compose's CircleShape is 50 % of the smaller dimension = a stadium; 9999px gives the same on rectangles, 50% would not.
elevation.css color-mix() instead of hsl(from …) browser floor Yes; same result.
motion.css springs sampled to linear() rather than Google's cubic-bezier approximations closer to the physics Yes for the curve; the durations differ from Google's published web numbers (C8).
motion.css reduced motion zeroes durations one place instead of per component Stricter than the site; consistent with "Honor individuals".
theme.css colour blocks are @theme inline nested data-theme sections Sound; verified by a browser test per the plan doc.
theme.css white/black re-added absolutes M3 has no such roles; white is surface-container-lowest (light). Harmless, but the guard should at least be able to flag them (C13).
theme.css --color-meta/quiet/divider/chrome opacity mixes ReStride's templates Does not hold against M3's text (C3): M3 dropped opacity-based emphasis with M3; 38 % is disabled.
type.css Google Sans Flex for brand and plain, ROND 100 on emphasized Expressive's typeface Holds: the site names Google Sans Flex on the shape page and leaves the typeface to the product; ROND is that font's own axis, not an M3 token.
scheme surface-tint/surface-variant omitted superseded/deprecated Holds (reference-styles §Color roles).
scheme success/warning/info via customColor, unharmonised "harmonisation off" Off is allowed; the 2021 recipe and contrast-blindness are not (C1, C15).
icon one Symbols cut, 400/0/24, SVG not font any name works, no runtime download Holds for weight; grade/optical size are lost (C16).
theme-script OS never read by CSS the script resolves system Holds.

Aligned (keep as is)

  • All 2025-spec colour roles generated and mapped, *-dim and fixed roles included; scheme.json and Scheme.php fill missing roles from the default; every pair (x / on-x) comes from Google's algorithm.
  • Corner scale: none 0, xs 4, sm 8, md 12, lg 16, lg-increased 20, xl 28, xl-increased 32, xxl 48, full — exactly M3 Expressive's ten steps, same names.
  • Elevation: five levels with material-web's shadow geometry; tonal separation preferred; scrim at 32 % where used.
  • Motion: spatial 0.6/800, 0.8/380, 0.8/200; effects 1.0/3800, 1.0/1600, 1.0/800 — the Compose Expressive constants; legacy easing tokens present; reduced motion handled centrally and in the four scripted components.
  • Typescale: all 15 sizes, line heights and weights match TypeScaleTokens.kt, emphasized set present (weights right).
  • State layer: 8/10/10 %, content colour, hover only on (hover: hover), disabled removes it; focus ring only on :focus-visible.
  • Theme: data-theme resolved before paint, OS followed while system, choice persisted, legacy keys adopted, attributes survive wire:navigate, color-scheme declared, dark: variant on data-theme.
  • Shapes: the 35 Expressive shapes ported from androidx, all fill the same box, decorative and hidden from AT.
  • Symbols: Google's own 24 px Rounded files (not the 48-opsz npm cut), outlined and filled, currentColor, hidden unless labelled.
  • Safe areas: --material-safe-* = M3's safety region.
  • DesignGuard already enforces "roles, not palette" and "no maryUI/daisyUI".

Missing (M3 things the core does not offer at all)

  • Contrast levels medium/high (C2).
  • Breakpoint tokens/variants and pane/margin tokens (C12).
  • Dragged state, disabled tokens, a shared touch target (C10).
  • Standard motion scheme (C9).
  • Harmonisation option for custom colours (C15).
  • An M3 concept guideline for agents (C24).

Breakpoint map (core only)

Item Library M3 breakpoint Gap
tokens none; Tailwind sm 640 / md 768 / lg 1024 / xl 1280 / 2xl 1536 compact <600 / medium 600 / expanded 840 / large 1200 / extra-large 1600 no M3 breakpoint is expressible; nearest Tailwind values are 4088 px off (see navigation audit for each component)

Concepts missing from the AI docs (input for the "Material 3 core concept" guideline)

Each line: what M3 says → whether the package docs say it today.

Colour

  • Roles are "paint-by-number" slots; a colour is always a role, never a hex or palette tone → docs say "every colour class names an M3 role" (yes) but not why or the hex rule.
  • Pair x with on-x only; a container is a fill, "on" is for text/icons on it; don't mix pairs (primary + secondary-container + on-surface) → no.
  • Primary = high-emphasis actions; secondary = less prominent, tonal fills; tertiary = complementary accent; error = static semantic → no.
  • Surface = page; surface-container-lowest…highest = emphasis hierarchy, not elevation; navigation on surface-container; same mapping for a region across breakpoints → no.
  • outline for boundaries that must read (text fields), outline-variant for dividers and cards; never outline on dividers → no.
  • Fixed/dim roles: "if you aren't sure, you probably shouldn't"; never where contrast matters → no.
  • Inverse roles are for the snackbar/inverse surfaces only → no.
  • success/warning/info are the package's custom roles, built like error; semantic colours are never dynamic-coloured away → no.
  • Low emphasis is on-surface-variant, 38 % means disabled; links are primary and underlined → no (the opposite: text-quiet is documented as decoration).
  • Contrast targets 4.5:1 small text, 3:1 large text/graphics, 3:1 clustered elements; disabled exempt → no.
  • Three contrast levels exist → no.

Elevation

  • Tonal separation first; shadows for floating elements and interaction only; the level table per component; +1 on hover; scrim 32 % → partly ("for what floats over content").

Shape

  • Which corner for what: full for buttons/chips/FAB-menu, xs 4 for text fields/snackbar, sm 8 for chips, md 12 for cards, lg 16 for FAB, xl 28 for dialogs/sheets/menus in Expressive, xxl 48 for large containers → no.
  • Optical roundness: inner radius = outer radius padding → no.
  • Shapes are decoration, never semantic; use sparingly; press morph is the interaction cue → no.

Typography

  • Role purposes: display (big, short), headline (short, high emphasis), title (medium, short), body (paragraphs), label (inside components; buttons use label-lg) → no.
  • Emphasized styles are opt-in, for selection, primary actions, headlines, badges — not decoration → no.
  • 4060 characters per line; tabular numbers where values change; don't change sizes when customising → no (only "never assemble text-* by hand").

Motion

  • Spatial springs for position/size/shape (they overshoot), effects for colour/opacity (never overshoot); fast for small elements, default for most, slow for large → partly (the pairing rule, one sentence).
  • Enter = decelerate, permanent exit = accelerate, temporary exit = emphasized; exits shorter than enters → no.
  • Reduced motion: everything through the tokens goes instant; parallax/expansion removed → no.

States

  • Six states; two visual indicators per state; hover 8 % / focus 10 % / press 10 % / drag 16 %; disabled 38 % content, 12 % container, no hover; state layer takes the content colour → no (utilities only).
  • 48 × 48 target, 8 dp between targets, never below 48 by default, density is opt-in → no.

Layout

  • Five breakpoints and what changes at each (navigation bar → collapsed rail → expanded rail; 1 → 2 → 3 panes; bottom sheet → menu; full-screen dialog → basic dialog) → no.
  • Scaffold: bars, rails, panes; safety regions; pane widths 360/412, side sheet ≤ 400 → partly (safe areas documented).
  • RTL: leading/trailing, mirror directional icons, use logical utilities (ps-, ms-, start-) → no.

Accessibility

  • Landmarks (one main/banner/contentinfo; label repeated navs; no role in the label); headings in order, one H1; dialog focus in/out; labels for icon-only controls; decorative images hidden; keyboard shortcuts need a modifier → no.

Icons

  • Filled = active/selected; keep one weight per group; 24 default, 20 in dense UI; label complex icons below 20; 48 target → partly (filled documented).