diff --git a/resources/css/tokens/state.css b/resources/css/tokens/state.css
index a853ee40..39f33e9b 100644
--- a/resources/css/tokens/state.css
+++ b/resources/css/tokens/state.css
@@ -1,15 +1,34 @@
/*
* Interaction states, as M3 draws them.
*
- * `state-layer` puts M3's state layer on an element: its content colour laid over it at 8%
- * on hover, 10% on focus and press. Hover only where the pointer can hover, because a touch
- * screen keeps the last hover after a tap. The layer is a ::before, so the element becomes
- * `position: relative` and `isolation: isolate`; never pass `static` or a z-index to it.
+ * The opacities are M3's state tokens (md.sys.state.*): the content colour laid over an
+ * element at 8% on hover, 10% on focus and press, 16% while dragged; disabled content at 38%
+ * and a disabled container at 12%.
*
- * `focus-ring` is M3's focus indicator — 3px in secondary, 2px out — drawn only for keyboard
- * focus. `link` is a link in running text: underlined in the colour of the words around it.
+ * `state-layer` puts M3's state layer on an element. Hover only where the pointer can hover,
+ * because a touch screen keeps the last hover after a tap; `data-dragged` on the element draws
+ * the dragged layer. The layer is a ::before, so the element becomes `position: relative` and
+ * `isolation: isolate`; never pass `static` or a z-index to it.
+ *
+ * `touch-target` extends an element's pointer target to M3's 48×48px minimum without changing
+ * what is drawn: a ::after centred on it, at least 48px each way. It is `::after`-based, so a
+ * component that already draws with ::after wraps its control in an element carrying it.
+ *
+ * `focus-ring` is M3's focus indicator — 3px in secondary, 2px out, drawn only for keyboard
+ * focus. The site gives no thickness or offset; the values are @material/web's focus ring
+ * tokens (Apache-2.0, © Google LLC). `link` is a link in running text: underlined in the
+ * colour of the words around it.
*/
+:root {
+ --md-sys-state-hover-state-layer-opacity: 0.08;
+ --md-sys-state-focus-state-layer-opacity: 0.1;
+ --md-sys-state-pressed-state-layer-opacity: 0.1;
+ --md-sys-state-dragged-state-layer-opacity: 0.16;
+ --md-sys-state-disabled-content-opacity: 0.38;
+ --md-sys-state-disabled-container-opacity: 0.12;
+}
+
@utility state-layer {
position: relative;
isolation: isolate;
@@ -28,13 +47,20 @@
@media (hover: hover) {
&:hover::before {
- opacity: 0.08;
+ opacity: var(--md-sys-state-hover-state-layer-opacity);
}
}
- &:focus-visible::before,
+ &:focus-visible::before {
+ opacity: var(--md-sys-state-focus-state-layer-opacity);
+ }
+
&:active::before {
- opacity: 0.1;
+ opacity: var(--md-sys-state-pressed-state-layer-opacity);
+ }
+
+ &[data-dragged]::before {
+ opacity: var(--md-sys-state-dragged-state-layer-opacity);
}
&:disabled::before,
@@ -43,6 +69,22 @@
}
}
+@utility touch-target {
+ position: relative;
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 100%;
+ height: 100%;
+ min-width: 3rem;
+ min-height: 3rem;
+ transform: translate(-50%, -50%);
+ }
+}
+
@utility focus-ring {
outline: none;
diff --git a/resources/css/tokens/theme.css b/resources/css/tokens/theme.css
index ecd3b4e3..a4f16a43 100644
--- a/resources/css/tokens/theme.css
+++ b/resources/css/tokens/theme.css
@@ -22,6 +22,40 @@
--font-sans: var(--md-ref-typeface-brand);
}
+/*
+ * Breakpoints are M3's window size classes, and only those: compact is below `medium`, then
+ * medium 600, expanded 840, large 1200 and extra-large 1600 (`medium:`, `expanded:`, `large:`,
+ * `extra-large:`, and `max-medium:` … for "below"). Tailwind's sm/md/lg/xl/2xl are cleared —
+ * a `sm:` compiles to nothing — because a layout decision keyed on 640px means nothing in M3.
+ * resources/js/breakpoints.js carries the same four numbers for scripts.
+ */
+@theme {
+ --breakpoint-*: initial;
+ --breakpoint-medium: 600px;
+ --breakpoint-expanded: 840px;
+ --breakpoint-large: 1200px;
+ --breakpoint-extra-large: 1600px;
+}
+
+/*
+ * Only M3's scales compile. Tailwind's default radius, shadow, text-size, weight, leading,
+ * tracking and easing utilities are cleared like its palette, so `rounded-lg`, `text-sm`,
+ * `font-medium`, `shadow-md` or `ease-in-out` compile to nothing; the M3 sets are declared
+ * below (`rounded-corner-*`, `shadow-elevation-*`, `type-*`, `ease-spatial-*`…). The 4px
+ * spacing scale stays: it is M3's spacing grid.
+ */
+@theme {
+ --radius-*: initial;
+ --shadow-*: initial;
+ --inset-shadow-*: initial;
+ --drop-shadow-*: initial;
+ --text-*: initial;
+ --font-weight-*: initial;
+ --leading-*: initial;
+ --tracking-*: initial;
+ --ease-*: initial;
+}
+
/* The colour roles, as generated by `php artisan material:scheme`. */
@theme inline {
--color-background: var(--md-sys-color-background);
@@ -94,16 +128,18 @@
}
/*
- * Ink and lines by what they are, rather than by an opacity number that means nothing out of
- * context: body copy, metadata, decoration; a structural line, a line around chrome, a divider.
+ * Ink and lines by what they are: body copy, metadata, decoration; a structural line, a line
+ * around chrome, a divider. Each is an M3 role, never an opacity: M3 marks emphasis with
+ * on-surface-variant and outline, and reserves 38% for disabled content, so an ink dimmed by
+ * opacity reads as disabled and loses its contrast guarantee.
*/
@theme inline {
--color-body: var(--md-sys-color-on-surface-variant);
- --color-meta: color-mix(in oklab, var(--md-sys-color-on-surface) 60%, transparent);
- --color-quiet: color-mix(in oklab, var(--md-sys-color-on-surface) 38%, transparent);
+ --color-meta: var(--md-sys-color-on-surface-variant);
+ --color-quiet: var(--md-sys-color-outline);
--color-structure: var(--md-sys-color-outline-variant);
- --color-chrome: color-mix(in oklab, var(--md-sys-color-outline-variant) 60%, transparent);
- --color-divider: color-mix(in oklab, var(--md-sys-color-outline-variant) 40%, transparent);
+ --color-chrome: var(--md-sys-color-outline-variant);
+ --color-divider: var(--md-sys-color-outline-variant);
}
@theme {
diff --git a/resources/js/breakpoints.js b/resources/js/breakpoints.js
new file mode 100644
index 00000000..82d80168
--- /dev/null
+++ b/resources/js/breakpoints.js
@@ -0,0 +1,28 @@
+/**
+ * M3's window size classes, for scripts that ask the viewport width.
+ *
+ * The same four numbers as the `medium:`, `expanded:`, `large:` and `extra-large:` variants in
+ * tokens/theme.css; compact is everything below `medium`. `from('expanded')` and `upTo('medium')`
+ * return MediaQueryLists on the range syntax Tailwind compiles, so a script and a stylesheet
+ * never disagree at the boundary pixel.
+ */
+export const breakpoints = Object.freeze({
+ medium: 600,
+ expanded: 840,
+ large: 1200,
+ 'extra-large': 1600,
+})
+
+const width = (name) => {
+ if (!(name in breakpoints)) {
+ throw new Error(`Unknown breakpoint "${name}". Use one of: ${Object.keys(breakpoints).join(', ')}.`)
+ }
+
+ return breakpoints[name]
+}
+
+/** Matches at and above the class's lower edge, like `medium:`. */
+export const from = (name) => window.matchMedia(`(width >= ${width(name)}px)`)
+
+/** Matches below the class's lower edge, like `max-medium:`. */
+export const upTo = (name) => window.matchMedia(`(width < ${width(name)}px)`)
diff --git a/resources/views/components/scheme-picker.blade.php b/resources/views/components/scheme-picker.blade.php
index 1c9172c6..87094a69 100644
--- a/resources/views/components/scheme-picker.blade.php
+++ b/resources/views/components/scheme-picker.blade.php
@@ -53,7 +53,7 @@
@foreach (['primary', 'secondary', 'tertiary'] as $role)
@endforeach
diff --git a/resources/views/components/slider.blade.php b/resources/views/components/slider.blade.php
index c06c8934..5db369b9 100644
--- a/resources/views/components/slider.blade.php
+++ b/resources/views/components/slider.blade.php
@@ -299,7 +299,7 @@
data-tick="{{ $format($mark['fraction']) }}"
@if ($mark['hidden']) hidden @endif
@if ($mark['active']) data-active @endif
- @class(['absolute top-1/2 size-1 -translate-1/2 rounded-full', $tickInk[$color], 'group-has-disabled/slider:bg-on-surface/38 group-has-disabled/slider:data-active:bg-on-surface/12'])
+ @class(['absolute top-1/2 size-1 -translate-1/2 rounded-corner-full', $tickInk[$color], 'group-has-disabled/slider:bg-on-surface/38 group-has-disabled/slider:data-active:bg-on-surface/12'])
style="left: {{ $calc($mark['at']) }}"
>
@endforeach
@@ -308,7 +308,7 @@
@endforeach
@@ -329,7 +329,7 @@
@foreach ($thumbs as $index => $thumb)