An error or warning alert was role="alert", an assertive live region — but the
usual alert is part of the page as it renders, which some screen readers then
announce over the page title, and a Livewire morph re-announces. It is now
role="status" whatever its colour, with an `assertive` prop for a notice put on
screen in answer to something the person just did. Its 40px dismiss button takes
the shared touch-target. Plan step 18, actions.md ACT-18, ACT-36.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 19, containment.md C-27 and C-28. `.inner-body` was 24px, a
value not on M3's 0/4/8/12/16/20/28/32/48 scale; it is 28px, the
extra-large corner of the dialog and bottom-sheet surface it stands in
for. `.header a` is title-large at weight 400, which its own section
comment and the `h2` under it already said.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
M3 wants two visual indicators per state and names the pair for a text
field: the error colours and a trailing error icon. The field now draws
one, labelled "Error", unless the caller already trails it or the field
is xs. A read-only field loses its dashed outline: M3 keeps "the same
visual style as an editable field", and the native readonly attribute is
what tells a screen reader. The data-readonly hook stays.
Plan step 20, findings IN-18 and IN-19.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
M3 says to avoid an icon in a snackbar and gives the supporting text no fourth
colour, so the state glyph and the 80% description are gone: a type now only
picks the announcement role. The 40px action and close buttons take the shared
touch-target, Escape dismisses a snackbar that holds the focus, and the host
publishes --material-snackbar-height on <html> so a `fab` button sits above the
snackbar instead of under it. Plan step 18, actions.md ACT-17, ACT-18, ACT-20,
ACT-34, ACT-35.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 19, containment.md C-24. `interpolate-size: allow-keywords`
was set and nothing transitioned, so the content snapped open. A new
resources/css/components/collapse.css transitions `block-size` on
`::details-content` over the fast spatial spring, with
`content-visibility` `allow-discrete` beside it so the section stays
rendered while it closes. The `<details>` carries `data-collapse` for
the rule to find.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
NavigationBarTokens names one label font for both icon positions, label-medium;
the horizontal layout had taken label-large from the rail's item, a different
component with a token of its own. And the item's state layer washed in
on-surface over a secondary-container pill, where the only state-layer tokens M3
states for a navigation item — the rail's, which the rail already follows — are
on-secondary-container for all six states.
Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-08, N-19).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Density was applied to every fine-pointer device with no way out, which
M3 forbids outright: "don't apply density by default; offer an explicit
density opt-in instead, keeping opt-out targets at >= 48x48 CSS px". A
row is now 52px and `dense` is a prop that tightens it to 36. The row
divider also takes outline-variant itself rather than 60% of it, since M3
keeps opacity for state layers and disabled.
Plan step 20, findings IN-16 and IN-30.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 19, containment.md C-10. The action row was right-aligned with
24px above it; the side-sheet specs table puts bottom actions on the
left in a 72dp row with 16dp above and 24dp below. A dialog's actions
stay trailing-aligned — M3 specifies the two differently.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
M3 asks a vertical toolbar for a 24dp margin from the window edge where a
horizontal one keeps 16, and its standard-toolbar colour list ends "Standard
button (Primary)" — the vibrant row of the same list was already honoured, so
the two were being read inconsistently. Both rules are unlayered, because an
icon button's ink is a utility.
Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-12, N-13).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 19, containment.md C-19 and C-09. A modal sheet opened at 90%
of the viewport where M3 caps the initial position at 50% and lets the
person pull it up; `height` now defaults to `50dvh` and whatever is
passed is held under a ceiling of the screen less M3's 72dp top margin.
The drag handle is `on-surface-variant` undiluted, as
SheetBottomTokens' DockedDragHandleColor and the component's own header
comment already said.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
The centred headline was absolutely positioned 56px in from each edge — room for
exactly one of M3's two trailing icon buttons, so a long title ellipsised behind
them. It is now the middle column of a three-column row and shrinks against
whatever is really beside it. The search container follows M3's curve, filling
the row to 312dp and then only half of what is left, so the showcase drops the
hand-written max-width it had been working around it with.
Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-09, N-11).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Its trigger holds two aria-hidden icons and wrote aria-label only when a caller
passed `label`, so an unnamed button was the default; `label` now falls back to
"Toggle menu" and is always written. The list had no ceiling, so six items ran
off a landscape phone — it now scrolls under the FAB, which stays put — and its
items rose into place but vanished on close, because the popover took its
display away before they could move. Plan step 18, actions.md ACT-21, ACT-32,
ACT-33.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 19, containment.md C-13, C-23 and the directly actionable card
of C-13/C-18. A card had one elevation and never changed it, and its
hover replaced the elevated card's shadow with the state layer; it now
names its variant in `data-card` and moves a step on hover (elevated
1 to 2, filled and outlined 0 to 1) with the layer over it. The 12 to 16
corner morph on hover is gone: M3 gives a card one shape and lists shape
morph for buttons, FABs and list items only. New `actionable` prop for
M3's directly actionable card — the card is the one tab stop, with
`role="button"` (or `link`), its `title` as the name, and Enter or Space
reaching the opener from resources/js/list-rows.js.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Both were 40px or less, which is M3's state-layer size, not its 48px
target: the paginator's steps and page numbers keep their 40px circle and
now catch presses over 48, and the sort button — a title-small line and a
16px arrow, about 20px tall — does the same, since the cell's padding
belongs to the cell and not to the button inside it.
Plan step 20, findings IN-15 and IN-17.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Five or more sections wrapped onto a 3- or 4-column grid, which left the tab
bar's divider under the last row only and stranded every upper row's active
indicator against nothing. From five they are now what M3 prescribes and its
accessibility page defends: a scrollable tab bar, each tab as wide as its label,
offset 52dp. Four or fewer still share the row. The 20px icons take the 20 cut.
Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-16).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
M3's chip keyboard table makes a set one tab stop that the arrows move
through; ten filter chips cost ten Tab presses today. The set now keeps a
roving tabindex — re-applied whenever a Livewire morph rewrites the chips
— and walks left, right, Home and End, following the writing direction.
The input chip's remove button also grows to a full 48x48 target, where
it was 34px wide.
Plan step 20, findings IN-13 and IN-14.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
M3 is explicit twice over — never disable a FAB, remove it instead — and the
prop painted the full disabled treatment with nothing warning against it. It is
gone; hiding the FAB is the documented answer. The extended FAB's icon-to-label
gap goes to M3's 16dp at md and 20dp at lg, the small one takes the baseline's
80dp minimum width, and the root gains a `data-fab` hook for a place that draws
a nested FAB its own way. Plan step 18, actions.md ACT-08, ACT-16, ACT-31.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
A scrollable set now starts 52dp in, so it reads as scrollable; a primary
indicator is inset 2dp at each side while a secondary one still spans the tab;
a link marked aria-current takes the active label colour the indicator already
followed; and the focus ring is 2px outside the tab, as everywhere else in the
package, paid for with 5px of room around the bar that the state layer and the
indicator reach back into.
Plan: docs/plans/material-3-alignment.md, step 21 (navigation N-10, N-18,
N-20, N-21).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 19, containment.md C-14, C-15 and C-26. The leading and
trailing gap was 12px where ListItem.kt's LeadingContentEndPadding and
TrailingContentStartPadding are 16dp; a three-line item was
middle-aligned and padded 10px where M3 top-aligns an 88dp item and
Compose pads it 12; and a segmented list's icons stayed at the baseline
24dp instead of ListTokens' expressive 20dp, which the item now reads
from its list with `@aware` and draws from the 20 optical cut.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Items were 44px tall with the baseline menu's 12px sides, the separator had half
its 8dp padding, a disabled item was skipped by the keyboard entirely, a chosen
item was told by colour and shape alone, and the list simply faded in place. Now:
48px rows with 16px either side, an 8px separator, disabled items focusable but
never activatable, a trailing check on a selected item, and an enter-exit that
scales the list out of the corner nearest its trigger — the scale on the spatial
spring, the fade on the effects one. Plan step 18, actions.md ACT-11, ACT-12,
ACT-13, ACT-26, ACT-27, ACT-28.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 19, containment.md C-08, C-21, C-22 and C-16. The segmented
fill was declared unlayered, so it beat every state layer in
@layer components and a segmented row lit up for nothing: it now sits in
the layer, `:where()`d to weigh nothing, ahead of the state rules. Its
tone is `surface`, which is ListTokens' ItemSegmentedContainerColor, and
a keyboard-focused row morphs to the large corner like a pressed one.
`<x-list dividers>` draws ListTokens' 16dp-inset rule instead of a
full-bleed `divide-y`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
CheckboxTokens' icon size is 18px, the same as the box it fills; the tick
and the dash were 16. Every icon these components draw at 20px or under —
the tick, the switch's check and cross, the chip's leading, trailing,
remove and filter-check icons, the date picker's menu arrows and the sort
arrow — now asks for the optical size 20 cut, which is drawn for that
size rather than scaled down from 24.
Plan step 20, finding IN-12 (and core C16's optical sizes).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
M3 asks a FAB's icon and a default (non-toggle) icon button's icon to be filled,
never outlined; the toggle half of that rule was already right, so only the
`$selected === null` case changed. Every glyph these components draw at 20px now
passes `optical="20"` for the cut M3 draws at that size — button xs/sm, group,
menu item, the alert, snackbar and FAB-menu close buttons, the stat's icon.
Plan step 18, actions.md ACT-14, ACT-15.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 19, containment.md C-06, C-17 and C-20. The whole box scrolled,
so a long dialog scrolled its headline and its buttons away; the header,
the body and the action row are now separate rows of the flex column,
each carrying the 24dp padding the box used to hold, and only the body
scrolls. `separator` draws the divider under the header and above the
actions, where it now stays put. The full-screen header is M3's 56dp
(was 64) and its action bar 56 (was ~72). New `alert` prop for M3's "on
web, basic dialogs should have the alert dialog role", and a `subtitle`
is always the dialog's `aria-describedby`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Every panel was visible until Alpine booted — four role="tabpanel" regions at
once for a screen reader, and aria-controls pointing at ids that did not exist
yet. The panel now carries its id, its aria-labelledby and, unless it is the
chosen one, display:none from the server. A slot renders before the component
around it, so the panel reads <x-tabs>'s attributes off the component stack and
repeats its id and initial-tab lines; both files say so.
Plan: docs/plans/material-3-alignment.md, step 14 (navigation N-05).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
The 24-hour clock has no period selector beside the hour and minute
boxes, so M3 widens them to 114px; AM/PM takes its own
tertiary-container rather than borrowing the boxes' primary-container;
a disabled period button uses M3's 12% disabled container, not 10%; and
the pair is a radio group, which is what M3's labelling table asks for,
so the arrows move between AM and PM.
Plan step 20, findings IN-10, IN-11, IN-26 and IN-27.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
ButtonXSmallTokens puts 16dp either side of an xs label, where the button, the
<x-group> segment and --group-pad all wrote 12px; the variants table calls 16dp
the Expressive value twice over. A standard button group also dropped onto a
second line on a narrow window, which M3 forbids — and the press expansion only
ever reaches a neighbour on the same line. Plan step 18, actions.md ACT-09,
ACT-10.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
`mode="picker"` hand-rolled an outlined segmented button, point for point the
design M3 Expressive deprecates, and its own arrow keys neither wrapped nor
handled Up/Down. Both rows are now <x-group>: native radios in a fieldset, so the
browser supplies the radiogroup, both axes, the wrap and the roving tab stop, and
the store is bound through an x-model accessor — a contrast row still marks the
resolved level but writes through setContrast(). A new `label` prop legends the
row. Plan: docs/plans/material-3-alignment.md, step 14 (navigation N-04, N-17).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Four components animated background-color and color on ease-spatial-fast, whose
fast variant peaks at 1.094 — the one thing tokens/motion.css says a colour must
never do. Tailwind cannot emit two springs on one element, so a small unlayered
file gives shape and size the spatial spring and colour and elevation the
effects one in a single property list. Plan step 18, actions.md ACT-19.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 12, containment.md C-05. Only the parallax half of M3's rule
was honoured: the mask was still written every frame, so items kept
growing and shrinking between the keylines. Under reduced motion the
inset, the shift and the label fade are now all zero, so every item
stays at the strategy's large size and the keylines only decide where
the row snaps. `--material-carousel-pin` went with it: with no mask
there is nothing to pin the content to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Expressive deprecates the divided style, so the results lose their
divider; the full-screen layout takes its own surface-container-low, one
step from the docked one; the docked view opens over a scrim, as M3's
variants table says it does; and the bar is bounded at M3's 720px, grows
to that width while focused, and rests with the 24px leading and trailing
padding the specs table gives an unfocused bar. --search-width sets the
resting width for M3's 360px bar.
Plan step 20, findings IN-07, IN-08, IN-09, IN-20 and IN-21.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
It borrowed the connected group's corner variables, so its inner corners were
wrong at four of five sizes and shrank under the finger where SplitButton*Tokens
grows them; its chevron took the icon-button table's size at every step, sat dead
centre, and turned over on the expressive spring. groups.css now carries
--split-inner, --split-icon and --split-nudge per size, and the rotation uses
M3's standard scheme. Plan step 18, actions.md ACT-06, ACT-07, ACT-29, ACT-30.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 12, containment.md C-04. `with-close-button` defaulted to
false, so a drawer could render with no exit at all once
`close-on-escape` and the scrim were off. It now defaults to true, and
`:with-close-button="false"` is ignored where nothing else closes the
sheet: Escape off, the scrim off, or a pane, which has neither.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 12, containment.md C-03. `selected` was colour alone on a
`listitem`, which cannot carry a selected state at all. `<x-list
selectable>` (or `selection="single|multi"`) now makes the container a
`role="listbox"` and each item an `option` with `aria-selected`; a plain
list keeps `role="list"` and marks a selected item `aria-current`. A
selected option draws a trailing check as M3's second cue, which
`icon-right` replaces. The item reads its list's mode with `@aware`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
An <x-group> segment is a plain label, 32px or 40px tall, with nothing reaching
past it, and nothing gave a connected <x-button-group> its 48dp minimum width
either — both numbers M3 names and tells you not to reduce. The segment now
carries the shared `touch-target` utility and a 48px floor, and groups.css puts
the floor on the buttons a caller nests in a connected group. <x-button> drops
its hand-rolled ::after for the same utility. Plan step 11, actions.md ACT-05.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
The md handle is 52px, not the 44 the MDC tokens gave it; the value
indicator is M3's 44x48 label container, not Flutter's 32px pill; and a
stop indicator, a tick and the inset icon now take the "on" colour of the
part of the track they sit on, which M3's nine colour roles spell out and
the library had inverted. Space held with an arrow moves by the same
large interval as PageUp, M3's "Space & Arrows" row.
Plan step 20, findings IN-04, IN-05, IN-06 and IN-24.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
The popover was fit-content with overflow visible, so a long menu ran past the
edge of the top layer, where the page's own scrolling cannot reach it. It now
caps at 18rem — less on a short window — and scrolls, as M3's menu behaviour
asks, and the keyboard brings the item it moves to into view. Plan step 11,
actions.md ACT-04.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
M3 puts a FAB nested in another component at level 0, and the documented pattern
puts one in the rail's header, so every documented use sat at elevation 3. The
rule is unlayered: the FAB's shadow is a utility and a layer would lose to it.
Plan: docs/plans/material-3-alignment.md, step 14 (navigation N-03).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
M3's accessibility page forbids it outright: an actioned snackbar has to wait
for the person to read it and reach its action. An entry with an action and no
timeout of its own is now untimed, the close button it already draws being the
way out; a timeout written out still wins. Plan step 11, actions.md ACT-03.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 12, containment.md C-02. `pointer-events-none` blocked the
pointer but not Tab and Enter: the item's `<a href>` stayed in the tab
order and navigated, and nothing announced it disabled. A disabled item
now renders its title as text, is no longer a `data-list-row`, and
carries `aria-disabled="true"`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
A FAB nested in another component has to be restyled from that component's
stylesheet, and its root carried no hook. Added here rather than with the rule
that needs it, so the actions stream's own use of the hook merges cleanly.
Plan: docs/plans/material-3-alignment.md, step 14 (navigation N-03).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
The region carrying aria-live was created together with its message, inside the
x-if that draws the snackbar, so a screen reader had nothing to notice changing.
It moves to the permanent host, which is polite and atomic as M3 asks; a type
now only picks the region's role, and the explicit aria-live keeps an error
polite too. Plan step 11, actions.md ACT-02.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
Plan step 12, containment.md C-01. The handle button was 32x4px: the
padding that makes M3's 48dp hit target sat on the wrapper, which is not
the control. `touch-target` on the button and 22px above and below it —
SheetDefaults.kt's DragHandleVerticalPadding — make the pressed area
48x48 while the handle is still drawn 32x4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
`place="bottom"` ignored --material-bottom-bar, so a floating toolbar overlapped
the shell's navigation bar and a docked one landed squarely on it. Both now clear
it — max(), not a sum, since that height already swallows the bottom safe area —
and the docs say what M3 says: never show a docked toolbar and a navigation bar
together. Plan: docs/plans/material-3-alignment.md, step 14 (navigation N-02).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
The loading indicator inside a filled or tonal button was painted in
text-primary on a primary container — 1:1 contrast. M3 asks an indicator
embedded in another component to take that component's label colour, so the
button now passes text-current, which loading.blade.php reads as a caller
colour and leaves alone. Plan step 11, actions.md ACT-01.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
A selection control with a label is pressed anywhere along its row, but
one without — a table's "select all", a row's tick — was only its 18, 20
or 32px box, because the 40px state layer is a ::before that catches no
pointer. The box now wears the shared touch-target utility, which M3 asks
for on all three (checkbox, radio and switch specs: target size 48dp).
Plan step 13, finding IN-03.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
The theme toggle and the account-menu avatar are drawn 40px and had no target
extension; both now carry `touch-target`, and the avatar's clipping moves from
the button (which would have cut the pseudo-target off) to the <img>. The rail's
hand-rolled `after:` target becomes the same utility.
Plan: docs/plans/material-3-alignment.md, step 14 (navigation N-01).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
The hover declaration sat on .field-box while the disabled edge is
inherited from .field, and a direct declaration beats an inherited one
whatever the selectors weigh — so a disabled outlined field lit a
full-opacity on-surface outline. It is now scoped past a disabled control
and, like every other state layer here, kept inside @media (hover: hover)
so a tap does not leave it stuck. M3 gives a disabled control no state.
Plan step 13, finding IN-02.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
M3's search accessibility page asks that results be announced when they
appear and read as a list. The bar now wraps its input in a
role="combobox" that carries aria-expanded and aria-controls — ARIA gives
a bare textbox neither — the results container is a role="list", and a
polite live region counts the results whenever the view's DOM settles.
Plan step 13, finding IN-01.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
The page names the class this window is in, lights the five cards through the
compiled variants themselves, and tabulates what each class changes: navigation,
panes, dialogs and sheets, margins. Plan step 28, in part.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9