Plan step 33. Each token file now opens with the material layer
statement and wraps its declarations in material.tokens (the font face
in material.base), so an application's unlayered scheme outranks the
package default whatever the selectors, and the foundation can import
the files with no layer() on the import.
The type-* and state utilities move to tokens/utilities.css, still
imported by material.css, so no file the foundation imports carries a
Tailwind directive. [x-cloak] moves beside the [hidden] rule in
foundation/hidden.css, outside every layer; material.css imports it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
The three state colours are DynamicColors on their own tonal palette now, built
exactly as Google builds error/on-error/error-container/on-error-container in both
specs, so the scheme's contrast level, dark tones, spec version and platform reach
them as they reach every other role; --harmonize (and a profile's harmonize) pulls
each source towards the seed, off by default. material:scheme also writes M3's
medium (0.5) and high (1.0) levels for both themes and every profile, keyed on
data-contrast and never on a media query, and --contrast now moves the standard
block alone. Scheme::load() takes the level; the mail theme stays on standard.
Plan: docs/plans/material-3-alignment.md, steps 6 and 7 (core C1, C2, C15).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
bin/springs.mjs is the sampler behind motion.css: damping ratio and stiffness in,
the 49-point linear() and the settle time out. Run with --settle it reproduces
the six curves the file carried byte for byte, so only the window changed.
Each spring is now sampled over the duration M3 publishes for the web (spatial
350/500/650 ms, effects 150/200/300 ms) instead of stopping at its own settle
time, so the *-duration tokens read Google's numbers. Sampling stays in real
time — point i is the spring at duration × i / 48 — so the bounce lands at the
same millisecond as before; a longer duration just holds the tail flat at 1, and
a shorter one pins the last point, at most 0.3% of the travel in one frame. Each
token's comment records damping, stiffness, settle time and sampled duration.
M3's second motion scheme is here too: [data-motion="standard"] swaps the three
spatial springs (damping 0.9, 0.2% overshoot instead of 9%) and their durations;
the effects springs sample identically in both schemes, so they are shared. The
reduced-motion block names both selectors, so it still zeroes every duration.
Plan: docs/plans/material-3-alignment.md step 5, findings core C8, C9.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Tracking now comes from androidx's TypeScaleTokens.kt as sp/16 rem, so Display
Large is -0.0125rem, Title Medium and Body Medium 0.0125rem where material-web's
pre-Expressive numbers stood. The emphasized set gets its own
--md-sys-typescale-emphasized-<style>-tracking, which the emphasized utilities
read: M3 widens four of them and takes Display Large back to zero. Every regular
utility sets `font-variation-settings: normal`, because the property inherits and
body copy inside an emphasized heading is meant to read as itself.
bin/check-font.mjs (fontkit) prints the packaged woff2's fvar axes, so a
re-subset cannot silently drop the ROND axis the emphasized styles depend on;
`npm run check:font` runs it and tests/Feature/FontTest.php runs it through the
configured node binary, skipping when node cannot run (PHP cannot open a woff2
without Brotli).
Plan: docs/plans/material-3-alignment.md step 4, findings core C5, C6, C7.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Components read the device's safe-area insets straight from env(), which a
browser test cannot fake and an application drawing its own status strip
cannot extend. Every inset the package reads, in its CSS and its views, is
now var(--material-safe-top|bottom|left|right, env(safe-area-inset-…)):
unchanged while the variables are unset.
The app shell's --material-bottom-bar also adds
var(--material-bottom-extra, 0px), so an application that docks something on
the phone's navigation bar (an offline banner) sets its height once and the
snackbar, a fab button and the page's bottom padding clear it.
A guard test fails on any env(safe-area-inset-*) outside such a variable, and
AppShellTest's assertion on the bar height's class follows the new value.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2
Colour, shape, type, elevation and motion as tokens and Tailwind
utilities; `php artisan material:scheme`, which generates an app's colour
roles with Google's material-color-utilities (spec 2025); the theme head
script with light, dark and system and its Alpine store; Google Sans Flex;
every Material Symbol (4,135, outlined and filled) drawn by <x-icon>
without blade-icons; all 35 M3 Expressive shapes, ported from androidx, as
<x-shape>; the x-figure directive; the Toasts concern; DesignGuard for
applications' tests; and a showcase with every token, both themes side by
side and an icon search.
Colour utilities are `@theme inline`, so a section with its own
data-theme repaints; without it they resolve once on :root.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy