From 632080637b00fff3944707ba16d79b94c4328508 Mon Sep 17 00:00:00 2001
From: flessan <132740827+flessan@users.noreply.github.com>
Date: Thu, 8 Oct 2026 05:24:43 +0000
Subject: [PATCH 1/5] Redesign the GDIPS web app with a Material You 3 design
system
Introduce main/assets/m3/, a self-contained Material 3 layer (tonal palettes
generated from a seed colour, design tokens, components, page patterns, a
runtime dynamic-colour generator and shared UI behaviour), and rebuild every
page on top of it without touching application logic.
Design system
- palettes.css: full light/dark tonal palettes with the M3 role mapping
(primary, secondary, tertiary, neutral, neutral-variant, error + containers,
inverse and fixed roles), generated by gen_palette.py through CIELAB with
gamut-clamped chroma; six named seeds selectable with .
- tokens.css: type scale, shape, elevation, motion, spacing and z-index tokens,
plus responsive visibility helpers and the Pusab display face.
- components.css: buttons, icon buttons, chips, cards, lists, menus, dialogs,
bottom sheets, scrims, tabs, segmented buttons, switches, sliders, text
fields, tables, expansion panels, snackbars, tooltips, progress and loading
indicators with hover/focus/pressed/disabled/loading/error states.
- color.js: dynamic colour engine (seed generation, light/dark/system themes,
the seed picker, theme-colour meta sync, legacy theme-key bridge).
- ui.js: drawer, menu, dialog, tab, segmented, chip, copy, range and reveal
behaviours, snackbars and scroll locking.
Pages
- Boot menu, team, download, rating system, about, tools, 404, WhatsApp
redirect, feedback app and its terms/FAQ/contact pages, and the demon list,
pemon list, impossible list, about and leaderboard pages all rebuilt as M3
surfaces: app bars, navigation drawer/tabs, cards, data tables, dialogues
and responsive layouts in light and dark themes.
- The demon list pages now share one stylesheet and one application script;
the duplicated inline scripts and styles, the duplicate PapaParse include and
the dead per-page theme code are gone. Every id, class and data attribute the
list application consumes is preserved, so filtering, search, sorting,
favourites, progress, achievements, ratings, reviews, statistics, comparison,
recommendations, export and CSV loading behave exactly as before.
Verified with a headless DOM pass over all pages: no runtime errors, the list
app renders and filters data, the feedback app renders/searches/filters and
opens its detail dialog, theme switching and the dynamic-colour picker work,
and no internal links or asset references are broken.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
---
main/assets/m3/color.js | 404 ++
main/assets/m3/components.css | 1746 ++++++
main/assets/m3/gen_palette.py | 242 +
main/assets/m3/pages.css | 464 ++
main/assets/m3/palettes.css | 960 +++
main/assets/m3/site.css | 448 ++
main/assets/m3/tokens.css | 487 ++
main/assets/m3/ui.js | 583 ++
main/demonlist/best.html | 486 +-
main/demonlist/impossiblelist.html | 3944 ++----------
main/demonlist/index.html | 6372 ++-----------------
main/demonlist/info.html | 412 +-
main/demonlist/pemonlist.html | 6372 ++-----------------
main/demonlist/script.js | 7982 +++++++++++-------------
main/demonlist/styles.css | 3778 ++++-------
main/error.html | 197 +-
main/feedback/contact.html | 446 +-
main/feedback/faq.html | 260 +-
main/feedback/index.html | 930 ++-
main/feedback/terms.html | 427 +-
main/pages/en/about/index.html | 377 +-
main/pages/en/download/index.html | 469 +-
main/pages/en/main/index.html | 477 +-
main/pages/en/rating-system/index.html | 649 +-
main/pages/id/about/index.html | 377 +-
main/pages/id/download/index.html | 467 +-
main/pages/id/rate.html | 647 +-
main/pages/id/rating-system/index.html | 647 +-
main/pages/id/utama/index.html | 482 +-
main/pages/index.html | 676 +-
main/tools.html | 399 +-
main/wa.html | 118 +-
32 files changed, 16319 insertions(+), 26406 deletions(-)
create mode 100644 main/assets/m3/color.js
create mode 100644 main/assets/m3/components.css
create mode 100644 main/assets/m3/gen_palette.py
create mode 100644 main/assets/m3/pages.css
create mode 100644 main/assets/m3/palettes.css
create mode 100644 main/assets/m3/site.css
create mode 100644 main/assets/m3/tokens.css
create mode 100644 main/assets/m3/ui.js
diff --git a/main/assets/m3/color.js b/main/assets/m3/color.js
new file mode 100644
index 0000000..c655f0e
--- /dev/null
+++ b/main/assets/m3/color.js
@@ -0,0 +1,404 @@
+/* ============================================================================
+ GDIPS · Material You 3 — Dynamic colour runtime
+ ----------------------------------------------------------------------------
+ Generates a full Material 3 role set from any seed colour at runtime, so the
+ whole site can re-theme itself (Material You "dynamic colour") without a
+ build step. Tonal palettes use CIELAB tone mapping with sRGB gamut
+ clamping — the same algorithm used to pre-generate assets/m3/palettes.css.
+
+ Public API
+ M3Color.applySeed(hex) apply an arbitrary seed colour
+ M3Color.seed() current seed (#RRGGBB)
+ M3Color.setTheme('light'|'dark'|'system')
+ M3Color.theme() resolved theme
+ M3Color.presets [{ id, label, seed }]
+ M3Color.mountPicker(el) render an accessible seed-colour picker
+ ========================================================================== */
+
+(function (global) {
+ 'use strict';
+
+ var STORAGE = { theme: 'gdips:theme', seed: 'gdips:seed' };
+ var LEGACY_THEME_KEY = 'theme'; // older GDIPS scripts store the theme here
+ var TONES = [0, 4, 5, 6, 10, 12, 17, 20, 22, 24, 25, 30, 35, 40, 50, 60, 70, 80, 87, 90, 92, 94, 95, 96, 98, 99, 100];
+
+ var PRESETS = [
+ { id: 'brand', label: 'GDIPS Merah', seed: '#C8102E' },
+ { id: 'indigo', label: 'Indigo', seed: '#6366F1' },
+ { id: 'violet', label: 'Violet', seed: '#8B5CF6' },
+ { id: 'azure', label: 'Azure', seed: '#2563EB' },
+ { id: 'emerald', label: 'Emerald', seed: '#0F9D58' },
+ { id: 'amber', label: 'Amber', seed: '#F59E0B' }
+ ];
+
+ /* ── Colour maths ───────────────────────────────────────────────────── */
+
+ function srgbToLin(c) {
+ c /= 255;
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
+ }
+
+ function linToSrgb(c) {
+ c = Math.min(1, Math.max(0, c));
+ var v = c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
+ return Math.round(Math.min(255, Math.max(0, v * 255)));
+ }
+
+ function hexToRgb(hex) {
+ hex = String(hex).trim().replace('#', '');
+ if (hex.length === 3) hex = hex.split('').map(function (c) { return c + c; }).join('');
+ var n = parseInt(hex, 16);
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
+ }
+
+ function rgbToHex(rgb) {
+ return '#' + rgb.map(function (c) {
+ var s = c.toString(16).toUpperCase();
+ return s.length === 1 ? '0' + s : s;
+ }).join('');
+ }
+
+ var WHITE = [0.95047, 1.0, 1.08883];
+
+ function rgbToLab(rgb) {
+ var r = srgbToLin(rgb[0]), g = srgbToLin(rgb[1]), b = srgbToLin(rgb[2]);
+ var x = (r * 0.4124564 + g * 0.3575761 + b * 0.1804375) / WHITE[0];
+ var y = (r * 0.2126729 + g * 0.7151522 + b * 0.0721750) / WHITE[1];
+ var z = (r * 0.0193339 + g * 0.1191920 + b * 0.9503041) / WHITE[2];
+ function f(t) { return t > 216 / 24389 ? Math.cbrt(t) : (841 / 108) * t + 4 / 29; }
+ var fx = f(x), fy = f(y), fz = f(z);
+ return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
+ }
+
+ function labToRgb(lab) {
+ var fy = (lab[0] + 16) / 116;
+ var fx = fy + lab[1] / 500;
+ var fz = fy - lab[2] / 200;
+ function fi(t) { var t3 = t * t * t; return t3 > 216 / 24389 ? t3 : (t - 4 / 29) * (108 / 841); }
+ var x = fi(fx) * WHITE[0], y = fi(fy) * WHITE[1], z = fi(fz) * WHITE[2];
+ return [
+ linToSrgb(x * 3.2404542 + y * -1.5371385 + z * -0.4985314),
+ linToSrgb(x * -0.9692660 + y * 1.8760108 + z * 0.0415560),
+ linToSrgb(x * 0.0556434 + y * -0.2040259 + z * 1.0572252)
+ ];
+ }
+
+ function inGamut(lab) {
+ var fy = (lab[0] + 16) / 116;
+ var fx = fy + lab[1] / 500;
+ var fz = fy - lab[2] / 200;
+ function fi(t) { var t3 = t * t * t; return t3 > 216 / 24389 ? t3 : (t - 4 / 29) * (108 / 841); }
+ var x = fi(fx), y = fi(fy), z = fi(fz);
+ return x >= -1e-4 && x <= 1.0001 && y >= -1e-4 && y <= 1.0001 && z >= -1e-4 && z <= 1.0001;
+ }
+
+ /** Hex for target CIELAB lightness, chroma reduced until inside sRGB. */
+ function toneToHex(L, a, b) {
+ if (L >= 100) return '#FFFFFF';
+ if (L <= 0) return '#000000';
+ var lab = [L, a, b];
+ if (inGamut(lab)) return rgbToHex(labToRgb(lab));
+ var lo = 0, hi = 1, best = L > 50 ? [255, 255, 255] : [0, 0, 0];
+ for (var i = 0; i < 20; i++) {
+ var mid = (lo + hi) / 2;
+ lab = [L, a * mid, b * mid];
+ if (inGamut(lab)) { best = labToRgb(lab); lo = mid; } else { hi = mid; }
+ }
+ return rgbToHex(best);
+ }
+
+ function tonalPalette(seed, chromaScale, hueOverride) {
+ var lab = rgbToLab(hexToRgb(seed));
+ var hue = Math.atan2(lab[2], lab[1]);
+ var chroma = Math.hypot(lab[1], lab[2]) * (chromaScale === undefined ? 1 : chromaScale);
+ if (hueOverride !== undefined) hue = hueOverride;
+ var out = {};
+ for (var i = 0; i < TONES.length; i++) {
+ var t = TONES[i];
+ out[t] = toneToHex(t, chroma * Math.cos(hue), chroma * Math.sin(hue));
+ }
+ return out;
+ }
+
+ var ROLE_MAP_LIGHT = {
+ primary: ['primary', 40], 'on-primary': ['primary', 100],
+ 'primary-container': ['primary', 90], 'on-primary-container': ['primary', 10],
+ 'primary-fixed': ['primary', 90], 'primary-fixed-dim': ['primary', 80],
+ 'on-primary-fixed': ['primary', 10], 'on-primary-fixed-variant': ['primary', 30],
+ secondary: ['secondary', 40], 'on-secondary': ['secondary', 100],
+ 'secondary-container': ['secondary', 90], 'on-secondary-container': ['secondary', 10],
+ tertiary: ['tertiary', 40], 'on-tertiary': ['tertiary', 100],
+ 'tertiary-container': ['tertiary', 90], 'on-tertiary-container': ['tertiary', 10],
+ error: ['error', 40], 'on-error': ['error', 100],
+ 'error-container': ['error', 90], 'on-error-container': ['error', 10],
+ background: ['neutral', 98], 'on-background': ['neutral', 10],
+ surface: ['neutral', 98], 'on-surface': ['neutral', 10],
+ 'surface-variant': ['neutral-variant', 90], 'on-surface-variant': ['neutral-variant', 30],
+ 'surface-dim': ['neutral', 87], 'surface-bright': ['neutral', 98],
+ 'surface-container-lowest': ['neutral', 100], 'surface-container-low': ['neutral', 96],
+ 'surface-container': ['neutral', 94], 'surface-container-high': ['neutral', 92],
+ 'surface-container-highest': ['neutral', 90],
+ outline: ['neutral-variant', 50], 'outline-variant': ['neutral-variant', 80],
+ 'inverse-surface': ['neutral', 20], 'inverse-on-surface': ['neutral', 95],
+ 'inverse-primary': ['primary', 80],
+ scrim: ['neutral', 0], shadow: ['neutral', 0], 'surface-tint': ['primary', 40]
+ };
+
+ var DARK_OVERRIDES = {
+ primary: ['primary', 80], 'on-primary': ['primary', 20],
+ 'primary-container': ['primary', 30], 'on-primary-container': ['primary', 90],
+ secondary: ['secondary', 80], 'on-secondary': ['secondary', 20],
+ 'secondary-container': ['secondary', 30], 'on-secondary-container': ['secondary', 90],
+ tertiary: ['tertiary', 80], 'on-tertiary': ['tertiary', 20],
+ 'tertiary-container': ['tertiary', 30], 'on-tertiary-container': ['tertiary', 90],
+ error: ['error', 80], 'on-error': ['error', 20],
+ 'error-container': ['error', 30], 'on-error-container': ['error', 90],
+ background: ['neutral', 6], 'on-background': ['neutral', 90],
+ surface: ['neutral', 6], 'on-surface': ['neutral', 90],
+ 'surface-variant': ['neutral-variant', 30], 'on-surface-variant': ['neutral-variant', 80],
+ 'surface-dim': ['neutral', 6], 'surface-bright': ['neutral', 24],
+ 'surface-container-lowest': ['neutral', 4], 'surface-container-low': ['neutral', 10],
+ 'surface-container': ['neutral', 12], 'surface-container-high': ['neutral', 17],
+ 'surface-container-highest': ['neutral', 22],
+ outline: ['neutral-variant', 60], 'outline-variant': ['neutral-variant', 30],
+ 'inverse-surface': ['neutral', 90], 'inverse-on-surface': ['neutral', 20],
+ 'inverse-primary': ['primary', 40], 'surface-tint': ['primary', 80]
+ };
+
+ function roleMap(dark) {
+ var map = {};
+ for (var k in ROLE_MAP_LIGHT) map[k] = ROLE_MAP_LIGHT[k];
+ if (dark) for (var d in DARK_OVERRIDES) map[d] = DARK_OVERRIDES[d];
+ return map;
+ }
+
+ /** Build every M3 colour role for a seed colour. */
+ function buildScheme(seed, dark) {
+ var primary = tonalPalette(seed, 1);
+ var secondary = tonalPalette(seed, 0.34);
+ var seedLab = rgbToLab(hexToRgb(seed));
+ var tertiary = tonalPalette(seed, 0.72, Math.atan2(seedLab[2], seedLab[1]) + Math.PI / 3);
+ var error = tonalPalette('#B3261E');
+ var hue = Math.atan2(seedLab[2], seedLab[1]);
+ var neutral = tonalPalette(seed, 0);
+ var neutralVariant = tonalPalette(seed, 0);
+ TONES.forEach(function (t) {
+ neutral[t] = toneToHex(t, 3.2 * Math.cos(hue), 3.2 * Math.sin(hue));
+ neutralVariant[t] = toneToHex(t, 9 * Math.cos(hue), 9 * Math.sin(hue));
+ });
+ var maps = { primary: primary, secondary: secondary, tertiary: tertiary, error: error, neutral: neutral, 'neutral-variant': neutralVariant };
+ var map = roleMap(dark);
+ var out = {};
+ Object.keys(map).forEach(function (role) {
+ var ref = map[role];
+ var tone = ref[1];
+ if (tone === 100) out['--m3-' + role] = '#FFFFFF';
+ else if (tone === 0) out['--m3-' + role] = '#000000';
+ else out['--m3-' + role] = maps[ref[0]][tone];
+ });
+ return out;
+ }
+
+ /* ── State ──────────────────────────────────────────────────────────── */
+
+ var media = global.matchMedia ? global.matchMedia('(prefers-color-scheme: dark)') : null;
+ var root = document.documentElement;
+
+ function read(key) {
+ try { return global.localStorage.getItem(key); } catch (e) { return null; }
+ }
+
+ function write(key, value) {
+ try { value === null ? global.localStorage.removeItem(key) : global.localStorage.setItem(key, value); } catch (e) { /* private mode */ }
+ }
+
+ function isHex(value) {
+ return typeof value === 'string' && /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value.trim());
+ }
+
+ /* The data-seed attribute doubles as a *named* palette selector
+ (e.g. data-seed="indigo" is delivered by palettes.css) and as an inline hex
+ seed. Only hex values are generated at runtime; named seeds stay in CSS. */
+ var attrSeed = root.getAttribute('data-seed');
+ var state = {
+ theme: read(STORAGE.theme) || read(LEGACY_THEME_KEY) || 'system',
+ seed: read(STORAGE.seed) || (isHex(attrSeed) ? attrSeed.toUpperCase() : null)
+ };
+
+ function resolvedTheme() {
+ if (state.theme === 'system') return media && media.matches ? 'dark' : 'light';
+ return state.theme;
+ }
+
+ function applyVars(vars) {
+ Object.keys(vars).forEach(function (k) { root.style.setProperty(k, vars[k]); });
+ }
+
+ function syncMetaTheme() {
+ var meta = document.querySelector('meta[name="theme-color"]');
+ var color = getComputedStyle(document.body || root).getPropertyValue('--m3-surface').trim();
+ if (!meta) {
+ meta = document.createElement('meta');
+ meta.setAttribute('name', 'theme-color');
+ document.head.appendChild(meta);
+ }
+ if (color) meta.setAttribute('content', color);
+ }
+
+ var appliedVars = [];
+
+ function clearVars() {
+ appliedVars.forEach(function (key) { root.style.removeProperty(key); });
+ appliedVars = [];
+ }
+
+ function paint() {
+ var theme = resolvedTheme();
+ root.setAttribute('data-theme', theme);
+ if (state.seed) {
+ var vars = buildScheme(state.seed, theme === 'dark');
+ clearVars();
+ appliedVars = Object.keys(vars);
+ applyVars(vars);
+ } else {
+ clearVars(); // fall back to the palette declared in palettes.css
+ }
+ syncMetaTheme();
+ }
+
+ function announce(type, detail) {
+ root.dispatchEvent(new CustomEvent('m3color:' + type, { detail: detail }));
+ }
+
+ var api = {
+ presets: PRESETS,
+
+ seed: function () { return state.seed; },
+ theme: function () { return resolvedTheme(); },
+ preference: function () { return state.theme; },
+
+ /** Apply an arbitrary seed colour (#RGB / #RRGGBB). */
+ applySeed: function (seed, options) {
+ var hex = String(seed || '').trim();
+ if (!isHex(hex)) return false;
+ if (hex.charAt(0) !== '#') hex = '#' + hex;
+ state.seed = hex.toUpperCase();
+ if (!options || options.persist !== false) write(STORAGE.seed, state.seed);
+ paint();
+ announce('seed', { seed: state.seed });
+ return true;
+ },
+
+ /** Drop the custom seed and return to the page's declared palette. */
+ resetSeed: function () {
+ state.seed = isHex(attrSeed) ? attrSeed.toUpperCase() : null;
+ write(STORAGE.seed, null);
+ paint();
+ announce('seed', { seed: state.seed });
+ },
+
+ /** light · dark · system */
+ setTheme: function (mode) {
+ state.theme = ['light', 'dark', 'system'].indexOf(mode) === -1 ? 'system' : mode;
+ write(STORAGE.theme, state.theme);
+ write(LEGACY_THEME_KEY, state.theme);
+ paint();
+ announce('theme', { theme: resolvedTheme(), preference: state.theme });
+ },
+
+ toggleTheme: function () {
+ api.setTheme(resolvedTheme() === 'dark' ? 'light' : 'dark');
+ return resolvedTheme();
+ },
+
+ /** Read a generated role, e.g. M3Color.role('primary') */
+ role: function (name) {
+ return getComputedStyle(root).getPropertyValue('--m3-' + name).trim();
+ },
+
+ /** Render an accessible dynamic-colour picker inside `el`. */
+ mountPicker: function (el) {
+ if (!el) return;
+ el.innerHTML = '';
+ el.classList.add('m3-color-picker');
+
+ var row = document.createElement('div');
+ row.className = 'm3-chip-set';
+ row.setAttribute('role', 'radiogroup');
+ row.setAttribute('aria-label', 'Dynamic colour');
+
+ PRESETS.forEach(function (preset) {
+ var chip = document.createElement('button');
+ chip.type = 'button';
+ chip.className = 'm3-chip m3-chip--accent';
+ chip.dataset.seed = preset.seed;
+ chip.dataset.id = preset.seed === '#C8102E' ? '' : preset.id;
+ chip.setAttribute('role', 'radio');
+ chip.setAttribute('aria-label', preset.label);
+ chip.innerHTML = '' + preset.label + '';
+ chip.addEventListener('click', function () { api.applySeed(preset.seed); mark(); });
+ row.appendChild(chip);
+ });
+
+ var custom = document.createElement('label');
+ custom.className = 'm3-chip m3-chip--sm';
+ custom.style.cursor = 'pointer';
+ custom.innerHTML = 'Custom' +
+ '';
+ custom.querySelector('input').addEventListener('input', function (e) {
+ api.applySeed(e.target.value);
+ mark();
+ });
+ row.appendChild(custom);
+
+ el.appendChild(row);
+
+ function mark() {
+ var chips = row.querySelectorAll('.m3-chip[data-seed]');
+ var named = root.getAttribute('data-seed') || '';
+ Array.prototype.forEach.call(chips, function (chip) {
+ var active = state.seed
+ ? chip.dataset.seed.toLowerCase() === state.seed.toLowerCase()
+ : chip.dataset.id === named;
+ chip.setAttribute('aria-checked', String(active));
+ chip.classList.toggle('is-selected', active);
+ });
+ }
+
+ mark();
+ root.addEventListener('m3color:seed', mark);
+ return el;
+ }
+ };
+
+ /* Pages that manage their own toggle (the demon list) simply set
+ ; adopt those changes so both systems stay in sync. */
+ if (global.MutationObserver) {
+ new MutationObserver(function () {
+ var value = root.getAttribute('data-theme');
+ if (value !== 'light' && value !== 'dark') return;
+ if (value === resolvedTheme() && state.theme !== 'system') return;
+ state.theme = value;
+ write(STORAGE.theme, value);
+ write(LEGACY_THEME_KEY, value);
+ paint();
+ }).observe(root, { attributes: true, attributeFilter: ['data-theme'] });
+ }
+
+ if (media) {
+ var onSystem = function () { if (state.theme === 'system') paint(); };
+ media.addEventListener ? media.addEventListener('change', onSystem) : media.addListener(onSystem);
+ }
+
+ root.addEventListener('m3color:theme', function () { syncMetaTheme(); });
+
+ paint();
+ global.M3Color = api;
+
+ // Upgrade every declarative picker on the page.
+ document.addEventListener('DOMContentLoaded', function () {
+ document.querySelectorAll('[data-m3-color-picker]').forEach(function (el) { api.mountPicker(el); });
+ });
+})(window);
diff --git a/main/assets/m3/components.css b/main/assets/m3/components.css
new file mode 100644
index 0000000..bdc9909
--- /dev/null
+++ b/main/assets/m3/components.css
@@ -0,0 +1,1746 @@
+/* ============================================================================
+ GDIPS · Material You 3 — Components
+ ----------------------------------------------------------------------------
+ A hand-written, dependency-free Material 3 component layer. Everything is
+ driven by the tokens in tokens.css + palettes.css, so all components adapt to
+ the active dynamic colour scheme and theme automatically.
+
+ Contents
+ 01 State layer 08 Checkbox / Radio / Switch
+ 02 Buttons 09 Slider
+ 03 Icon buttons / FAB 10 Lists
+ 04 Segmented buttons 11 Menus
+ 05 Cards 12 Dialogs & sheets
+ 06 Chips 13 Snackbar / tooltip / banner
+ 07 Text fields & selects 14 Tabs / nav bar / rail / drawer / app bar
+ 15 Tables, progress, badges, misc
+ ========================================================================== */
+
+/* ============================================================================
+ 01 · State layer
+ A reusable overlay that gives every interactive surface M3 hover / focus /
+ pressed states without hard-coding colours.
+ ========================================================================== */
+
+.m3-state-layer {
+ position: relative;
+ isolation: isolate;
+ overflow: hidden;
+}
+
+.m3-state-layer::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: currentColor;
+ opacity: 0;
+ pointer-events: none;
+ border-radius: inherit;
+ transition: opacity var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+@media (hover: hover) {
+ .m3-state-layer:hover::after { opacity: var(--m3-state-hover); }
+}
+
+.m3-state-layer:focus-visible::after { opacity: var(--m3-state-focus); }
+.m3-state-layer:active::after { opacity: var(--m3-state-pressed); }
+.m3-state-layer[aria-disabled='true']::after,
+.m3-state-layer:disabled::after { opacity: 0; }
+
+/* Ripple -------------------------------------------------------------------- */
+.m3-ripple {
+ position: absolute;
+ border-radius: var(--m3-shape-full);
+ transform: scale(0);
+ background: currentColor;
+ opacity: 0.24;
+ pointer-events: none;
+ animation: m3-ripple-in 550ms var(--m3-ease-standard-decel) forwards;
+}
+
+@keyframes m3-ripple-in {
+ to { transform: scale(1); opacity: 0; }
+}
+
+/* ============================================================================
+ 02 · Buttons
+ Variants: filled (default) · tonal · elevated · outlined · text
+ ========================================================================== */
+
+.m3-button {
+ --m3-button-height: 40px;
+ --m3-button-pad: 24px;
+ --m3-button-gap: 8px;
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--m3-button-gap);
+ min-height: var(--m3-button-height);
+ padding: 0 var(--m3-button-pad);
+ border: 1px solid transparent;
+ border-radius: var(--m3-shape-full);
+ font-family: var(--m3-font-plain);
+ font-size: var(--m3-label-large-size);
+ line-height: var(--m3-label-large-line);
+ font-weight: var(--m3-label-large-weight);
+ letter-spacing: var(--m3-tracking-label);
+ text-decoration: none;
+ white-space: nowrap;
+ cursor: pointer;
+ user-select: none;
+ isolation: isolate;
+ overflow: hidden;
+ background: var(--m3-primary);
+ color: var(--m3-on-primary);
+ box-shadow: var(--m3-elevation-0);
+ transition:
+ background-color var(--m3-duration-short) var(--m3-ease-standard),
+ color var(--m3-duration-short) var(--m3-ease-standard),
+ box-shadow var(--m3-duration-short-2) var(--m3-ease-standard),
+ border-radius var(--m3-duration-short-2) var(--m3-ease-emphasized),
+ transform var(--m3-duration-short-2) var(--m3-ease-emphasized);
+}
+
+.m3-button::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: currentColor;
+ opacity: 0;
+ transition: opacity var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+@media (hover: hover) {
+ .m3-button:hover::after { opacity: var(--m3-state-hover); }
+}
+
+.m3-button:focus-visible::after { opacity: var(--m3-state-focus); }
+.m3-button:active::after { opacity: var(--m3-state-pressed); }
+.m3-button:active { transform: scale(0.97); }
+
+.m3-button > * { position: relative; z-index: 1; }
+.m3-button i,
+.m3-button svg { font-size: 1.125em; }
+
+.m3-button--tonal {
+ background: var(--m3-secondary-container);
+ color: var(--m3-on-secondary-container);
+}
+
+.m3-button--elevated {
+ background: var(--m3-surface-container-low);
+ color: var(--m3-primary);
+ box-shadow: var(--m3-elevation-1);
+}
+
+@media (hover: hover) {
+ .m3-button--elevated:hover { box-shadow: var(--m3-elevation-2); }
+}
+
+.m3-button--outlined {
+ background: transparent;
+ color: var(--m3-primary);
+ border-color: var(--m3-outline-variant);
+}
+
+.m3-button--text {
+ background: transparent;
+ color: var(--m3-primary);
+ padding-inline: 12px;
+}
+
+.m3-button--danger {
+ background: var(--m3-error);
+ color: var(--m3-on-error);
+}
+
+.m3-button--danger-text {
+ background: transparent;
+ color: var(--m3-error);
+}
+
+.m3-button--full { width: 100%; }
+.m3-button--lg { --m3-button-height: 48px; --m3-button-pad: 28px; font-size: var(--m3-title-small-size); }
+.m3-button--sm { --m3-button-height: 32px; --m3-button-pad: 16px; font-size: var(--m3-label-medium-size); }
+
+.m3-button:disabled,
+.m3-button[aria-disabled='true'] {
+ background: color-mix(in srgb, var(--m3-on-surface) 12%, transparent);
+ color: color-mix(in srgb, var(--m3-on-surface) 38%, transparent);
+ box-shadow: none;
+ border-color: transparent;
+ transform: none;
+ pointer-events: none;
+}
+
+.m3-button.is-loading {
+ color: transparent;
+ pointer-events: none;
+}
+
+.m3-button.is-loading::before {
+ content: '';
+ position: absolute;
+ inset-inline: 0;
+ margin-inline: auto;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ border: 2px solid currentColor;
+ border-top-color: transparent;
+ color: var(--m3-on-primary);
+ animation: m3-spin 900ms linear infinite;
+}
+
+.m3-button--tonal.is-loading::before,
+.m3-button--outlined.is-loading::before,
+.m3-button--text.is-loading::before,
+.m3-button--elevated.is-loading::before { color: var(--m3-primary); }
+
+.m3-button--outlined.is-loading::before,
+.m3-button--text.is-loading::before { color: var(--m3-primary); }
+
+.m3-button-group { display: inline-flex; gap: 8px; flex-wrap: wrap; }
+
+@keyframes m3-spin {
+ to { transform: rotate(360deg); }
+}
+
+/* ============================================================================
+ 03 · Icon buttons & FABs
+ ========================================================================== */
+
+.m3-icon-button {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ padding: 0;
+ border: 1px solid transparent;
+ border-radius: var(--m3-shape-full);
+ background: transparent;
+ color: var(--m3-on-surface-variant);
+ font-size: 1.15rem;
+ cursor: pointer;
+ isolation: isolate;
+ overflow: hidden;
+ text-decoration: none;
+ transition:
+ background-color var(--m3-duration-short) var(--m3-ease-standard),
+ color var(--m3-duration-short) var(--m3-ease-standard),
+ transform var(--m3-duration-short-2) var(--m3-ease-emphasized);
+}
+
+.m3-icon-button::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: currentColor;
+ opacity: 0;
+ transition: opacity var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+@media (hover: hover) { .m3-icon-button:hover::after { opacity: var(--m3-state-hover); } }
+.m3-icon-button:focus-visible::after { opacity: var(--m3-state-focus); }
+.m3-icon-button:active::after { opacity: var(--m3-state-pressed); }
+.m3-icon-button:active { transform: scale(0.92); }
+.m3-icon-button > * { position: relative; z-index: 1; }
+
+.m3-icon-button--filled {
+ background: var(--m3-primary);
+ color: var(--m3-on-primary);
+}
+
+.m3-icon-button--tonal {
+ background: var(--m3-secondary-container);
+ color: var(--m3-on-secondary-container);
+}
+
+.m3-icon-button--outlined { border-color: var(--m3-outline-variant); }
+.m3-icon-button--selected { background: var(--m3-primary); color: var(--m3-on-primary); }
+.m3-icon-button--lg { width: 48px; height: 48px; font-size: 1.35rem; }
+.m3-icon-button--sm { width: 32px; height: 32px; font-size: 1rem; }
+
+.m3-icon-button:disabled {
+ color: color-mix(in srgb, var(--m3-on-surface) 38%, transparent);
+ pointer-events: none;
+}
+
+/* FAB ----------------------------------------------------------------------- */
+.m3-fab {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ height: 56px;
+ min-width: 56px;
+ padding-inline: 16px;
+ border: none;
+ border-radius: var(--m3-shape-lg);
+ background: var(--m3-primary-container);
+ color: var(--m3-on-primary-container);
+ font-size: 1.5rem;
+ box-shadow: var(--m3-elevation-3);
+ cursor: pointer;
+ isolation: isolate;
+ overflow: hidden;
+ transition:
+ box-shadow var(--m3-duration-short-2) var(--m3-ease-standard),
+ transform var(--m3-duration-short-2) var(--m3-ease-emphasized),
+ border-radius var(--m3-duration-short-2) var(--m3-ease-emphasized);
+}
+
+.m3-fab::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: currentColor;
+ opacity: 0;
+ transition: opacity var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+@media (hover: hover) {
+ .m3-fab:hover::after { opacity: var(--m3-state-hover); }
+ .m3-fab:hover { box-shadow: var(--m3-elevation-4); }
+}
+
+.m3-fab:active { transform: scale(0.96); }
+.m3-fab:active::after { opacity: var(--m3-state-pressed); }
+.m3-fab > * { position: relative; z-index: 1; }
+.m3-fab--extended {
+ width: auto;
+ padding-inline: 20px;
+ font-family: var(--m3-font-plain);
+ font-size: var(--m3-label-large-size);
+ font-weight: var(--m3-label-large-weight);
+ letter-spacing: var(--m3-tracking-label);
+}
+.m3-fab--sm { height: 40px; min-width: 40px; font-size: 1.15rem; border-radius: var(--m3-shape-md); }
+.m3-fab--lg { height: 96px; min-width: 96px; font-size: 2.25rem; border-radius: var(--m3-shape-xl); }
+.m3-fab--fixed { position: fixed; inset-block-end: 24px; inset-inline-end: 24px; z-index: var(--m3-z-appbar); }
+
+/* ============================================================================
+ 04 · Segmented buttons
+ ========================================================================== */
+
+.m3-segmented {
+ display: inline-flex;
+ align-items: stretch;
+ border: 1px solid var(--m3-outline);
+ border-radius: var(--m3-shape-full);
+ overflow: hidden;
+ background: transparent;
+ max-width: 100%;
+}
+
+.m3-segmented__item {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ flex: 1 1 auto;
+ min-height: 40px;
+ padding-inline: 16px;
+ border: none;
+ border-inline-start: 1px solid var(--m3-outline);
+ background: transparent;
+ color: var(--m3-on-surface);
+ font-size: var(--m3-label-large-size);
+ font-weight: var(--m3-label-large-weight);
+ letter-spacing: var(--m3-tracking-label);
+ white-space: nowrap;
+ cursor: pointer;
+ isolation: isolate;
+ overflow: hidden;
+ transition: background-color var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+.m3-segmented__item:first-child { border-inline-start: none; }
+
+.m3-segmented__item::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: currentColor;
+ opacity: 0;
+ transition: opacity var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+@media (hover: hover) { .m3-segmented__item:hover::after { opacity: var(--m3-state-hover); } }
+.m3-segmented__item:focus-visible::after { opacity: var(--m3-state-focus); }
+.m3-segmented__item:active::after { opacity: var(--m3-state-pressed); }
+.m3-segmented__item[aria-selected='true'],
+.m3-segmented__item.is-selected {
+ background: var(--m3-secondary-container);
+ color: var(--m3-on-secondary-container);
+}
+.m3-segmented__item > * { position: relative; z-index: 1; }
+
+/* ============================================================================
+ 05 · Cards
+ ========================================================================== */
+
+.m3-card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ border-radius: var(--m3-shape-md);
+ background: var(--m3-surface-container-low);
+ color: var(--m3-on-surface);
+ overflow: hidden;
+ transition:
+ box-shadow var(--m3-duration-short-2) var(--m3-ease-standard),
+ background-color var(--m3-duration-short) var(--m3-ease-standard),
+ transform var(--m3-duration-short-2) var(--m3-ease-emphasized);
+}
+
+.m3-card--elevated { background: var(--m3-surface-container-low); box-shadow: var(--m3-elevation-1); }
+.m3-card--filled { background: var(--m3-surface-container-highest); }
+.m3-card--outlined { background: var(--m3-surface); border: 1px solid var(--m3-outline-variant); }
+
+@media (hover: hover) {
+ .m3-card--elevated:hover,
+ .m3-card--interactive:hover { box-shadow: var(--m3-elevation-2); }
+ .m3-card--interactive:hover { transform: translateY(-2px); }
+}
+
+.m3-card--interactive { cursor: pointer; }
+.m3-card--interactive:active { transform: translateY(0) scale(0.995); }
+
+.m3-card__media {
+ position: relative;
+ aspect-ratio: 16 / 9;
+ background: var(--m3-surface-container-highest);
+ overflow: hidden;
+}
+
+.m3-card__media img,
+.m3-card__media iframe {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.m3-card__content {
+ display: flex;
+ flex-direction: column;
+ gap: var(--m3-space-2);
+ padding: var(--m3-space-4);
+}
+
+.m3-card__title { font-size: var(--m3-title-medium-size); font-weight: var(--m3-title-medium-weight); letter-spacing: var(--m3-tracking-title); }
+.m3-card__subtitle { color: var(--m3-on-surface-variant); font-size: var(--m3-body-medium-size); }
+.m3-card__text { color: var(--m3-on-surface-variant); font-size: var(--m3-body-medium-size); }
+.m3-card__actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--m3-space-2);
+ padding: var(--m3-space-4);
+ padding-top: 0;
+ margin-top: auto;
+}
+.m3-card__actions--end { justify-content: flex-end; }
+
+.m3-card--accent::before {
+ content: '';
+ position: absolute;
+ inset-block-start: 0;
+ inset-inline: 0;
+ height: 4px;
+ background: linear-gradient(90deg, var(--m3-primary), var(--m3-tertiary));
+}
+
+/* ============================================================================
+ 06 · Chips
+ ========================================================================== */
+
+.m3-chip-set { display: flex; flex-wrap: wrap; gap: 8px; }
+
+.m3-chip {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ height: 32px;
+ padding-inline: 12px;
+ border: 1px solid var(--m3-outline-variant);
+ border-radius: var(--m3-shape-sm);
+ background: transparent;
+ color: var(--m3-on-surface-variant);
+ font-size: var(--m3-label-large-size);
+ font-weight: var(--m3-label-large-weight);
+ letter-spacing: var(--m3-tracking-label);
+ cursor: pointer;
+ text-decoration: none;
+ isolation: isolate;
+ overflow: hidden;
+ transition: background-color var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+.m3-chip::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: currentColor;
+ opacity: 0;
+ transition: opacity var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+@media (hover: hover) { .m3-chip:hover::after { opacity: var(--m3-state-hover); } }
+.m3-chip:focus-visible::after { opacity: var(--m3-state-focus); }
+.m3-chip:active::after { opacity: var(--m3-state-pressed); }
+.m3-chip > * { position: relative; z-index: 1; }
+.m3-chip--selected,
+.m3-chip[aria-pressed='true'],
+.m3-chip[aria-selected='true'] {
+ background: var(--m3-secondary-container);
+ color: var(--m3-on-secondary-container);
+ border-color: transparent;
+}
+.m3-chip--assist { border-color: var(--m3-outline); }
+.m3-chip--sm { height: 26px; padding-inline: 10px; font-size: var(--m3-label-medium-size); }
+.m3-chip--accent {
+ background: var(--m3-primary-container);
+ color: var(--m3-on-primary-container);
+ border-color: transparent;
+}
+
+/* ============================================================================
+ 07 · Text fields & selects
+ ========================================================================== */
+
+.m3-field {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 0;
+}
+
+.m3-field__box {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-height: 56px;
+ padding-inline: 16px;
+ border-radius: var(--m3-shape-xs) var(--m3-shape-xs) 0 0;
+ background: var(--m3-surface-container-highest);
+ border-bottom: 1px solid var(--m3-on-surface-variant);
+ transition:
+ background-color var(--m3-duration-short) var(--m3-ease-standard),
+ border-color var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+.m3-field__box::after {
+ content: '';
+ position: absolute;
+ inset-block-end: -1px;
+ inset-inline: 0;
+ height: 2px;
+ background: var(--m3-primary);
+ transform: scaleX(0);
+ transition: transform var(--m3-duration-short-2) var(--m3-ease-emphasized-decel);
+}
+
+.m3-field__box:hover::after { transform: scaleX(1); opacity: 0.4; }
+.m3-field__box:focus-within::after { transform: scaleX(1); opacity: 1; }
+
+.m3-field__input {
+ flex: 1 1 auto;
+ width: 100%;
+ min-width: 0;
+ padding: 20px 0 6px;
+ border: none;
+ background: transparent;
+ color: var(--m3-on-surface);
+ font-size: var(--m3-body-large-size);
+ outline: none;
+}
+
+.m3-field__label {
+ position: absolute;
+ inset-inline-start: 16px;
+ inset-block-start: 50%;
+ translate: 0 -50%;
+ color: var(--m3-on-surface-variant);
+ font-size: var(--m3-body-large-size);
+ pointer-events: none;
+ transform-origin: left top;
+ transition:
+ transform var(--m3-duration-short-2) var(--m3-ease-emphasized-decel),
+ color var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+.m3-field__box:focus-within .m3-field__label,
+.m3-field__input:not(:placeholder-shown) + .m3-field__label,
+.m3-field__input:focus + .m3-field__label,
+.m3-field--filled .m3-field__label,
+.m3-field__box.has-value .m3-field__label,
+.m3-field__select.has-value ~ .m3-field__label {
+ transform: translateY(-14px) scale(0.78);
+}
+
+.m3-field__icon { color: var(--m3-on-surface-variant); font-size: 1.05rem; }
+.m3-field__suffix { color: var(--m3-on-surface-variant); font-size: var(--m3-body-medium-size); white-space: nowrap; }
+.m3-field__support {
+ padding-inline: 16px;
+ color: var(--m3-on-surface-variant);
+ font-size: var(--m3-body-small-size);
+ line-height: var(--m3-body-small-line);
+}
+
+.m3-field__input::placeholder { color: transparent; }
+
+textarea.m3-field__input {
+ padding-top: 22px;
+ resize: vertical;
+ min-height: 96px;
+ line-height: 1.5;
+}
+
+select.m3-field__input {
+ appearance: none;
+ padding-inline-end: 24px;
+ cursor: pointer;
+}
+
+.m3-field__chevron {
+ position: absolute;
+ inset-inline-end: 16px;
+ color: var(--m3-on-surface-variant);
+ pointer-events: none;
+}
+
+/* Outlined variant ---------------------------------------------------------- */
+.m3-field--outlined .m3-field__box {
+ border: 1px solid var(--m3-outline);
+ border-radius: var(--m3-shape-xs);
+ background: transparent;
+ padding-block: 0;
+}
+
+.m3-field--outlined .m3-field__box::after { display: none; }
+.m3-field--outlined .m3-field__box:hover { border-color: var(--m3-on-surface); }
+.m3-field--outlined .m3-field__box:focus-within {
+ border-color: var(--m3-primary);
+ box-shadow: inset 0 0 0 1px var(--m3-primary);
+}
+.m3-field--outlined .m3-field__input { padding-block: 16px; }
+.m3-field--outlined .m3-field__label {
+ inset-block-start: 0;
+ translate: 0 -50%;
+ padding-inline: 4px;
+ background: var(--m3-surface);
+ border-radius: 2px;
+ font-size: var(--m3-body-medium-size);
+}
+
+/* Error state --------------------------------------------------------------- */
+.m3-field--error .m3-field__box { border-color: var(--m3-error); }
+.m3-field--error .m3-field__box::after { background: var(--m3-error); opacity: 1; }
+.m3-field--error .m3-field__support,
+.m3-field--error .m3-field__label,
+.m3-field--error .m3-field__icon { color: var(--m3-error); }
+
+.m3-field--disabled { opacity: 0.38; pointer-events: none; }
+
+/* Search bar (M3 "search bar" surface) -------------------------------------- */
+.m3-search-bar {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ height: 56px;
+ padding-inline: 16px;
+ border: 1px solid transparent;
+ border-radius: var(--m3-shape-full);
+ background: var(--m3-surface-container-high);
+ color: var(--m3-on-surface-variant);
+ box-shadow: var(--m3-elevation-1);
+ transition: box-shadow var(--m3-duration-short-2) var(--m3-ease-standard), background-color var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+.m3-search-bar:focus-within {
+ background: var(--m3-surface-container-highest);
+ box-shadow: var(--m3-elevation-2);
+}
+
+.m3-search-bar input {
+ flex: 1 1 auto;
+ min-width: 0;
+ border: none;
+ background: transparent;
+ outline: none;
+ color: var(--m3-on-surface);
+ font-size: var(--m3-body-large-size);
+}
+
+.m3-search-bar input::placeholder { color: var(--m3-on-surface-variant); }
+
+/* ============================================================================
+ 08 · Checkbox / Radio / Switch
+ ========================================================================== */
+
+.m3-check {
+ display: inline-flex;
+ align-items: center;
+ gap: 12px;
+ min-height: 40px;
+ cursor: pointer;
+ font-size: var(--m3-body-medium-size);
+ color: var(--m3-on-surface);
+}
+
+.m3-check input {
+ appearance: none;
+ flex: 0 0 auto;
+ width: 18px;
+ height: 18px;
+ margin: 0;
+ border: 2px solid var(--m3-on-surface-variant);
+ border-radius: 2px;
+ background: transparent;
+ cursor: pointer;
+ position: relative;
+ transition: background-color var(--m3-duration-short) var(--m3-ease-standard), border-color var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+.m3-check input::after {
+ content: '';
+ position: absolute;
+ inset: -14px;
+ border-radius: 50%;
+ background: var(--m3-primary);
+ opacity: 0;
+ transition: opacity var(--m3-duration-short) var(--m3-ease-standard);
+ z-index: -1;
+}
+
+@media (hover: hover) { .m3-check:hover input::after { opacity: var(--m3-state-hover); } }
+.m3-check input:focus-visible { outline: 2px solid var(--m3-primary); outline-offset: 3px; }
+.m3-check input:checked {
+ background: var(--m3-primary);
+ border-color: var(--m3-primary);
+}
+.m3-check input:checked::before {
+ content: '';
+ position: absolute;
+ inset-inline-start: 4.5px;
+ inset-block-start: 1px;
+ width: 5px;
+ height: 9px;
+ border: solid var(--m3-on-primary);
+ border-width: 0 2px 2px 0;
+ transform: rotate(45deg);
+}
+.m3-check input:disabled { opacity: 0.38; pointer-events: none; }
+
+.m3-check--radio input { border-radius: 50%; }
+.m3-check--radio input:checked { background: transparent; border-color: var(--m3-primary); }
+.m3-check--radio input:checked::before {
+ inset: 3px;
+ width: auto;
+ height: auto;
+ border: none;
+ border-radius: 50%;
+ background: var(--m3-primary);
+ transform: none;
+}
+
+.m3-switch {
+ display: inline-flex;
+ align-items: center;
+ gap: 12px;
+ cursor: pointer;
+ font-size: var(--m3-body-medium-size);
+}
+
+.m3-switch input {
+ appearance: none;
+ position: relative;
+ flex: 0 0 auto;
+ width: 52px;
+ height: 32px;
+ margin: 0;
+ border: 2px solid var(--m3-outline);
+ border-radius: var(--m3-shape-full);
+ background: var(--m3-surface-container-highest);
+ cursor: pointer;
+ transition: background-color var(--m3-duration-short-2) var(--m3-ease-standard), border-color var(--m3-duration-short-2) var(--m3-ease-standard);
+}
+
+.m3-switch input::before {
+ content: '';
+ position: absolute;
+ inset-block-start: 50%;
+ inset-inline-start: 5px;
+ translate: 0 -50%;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background: var(--m3-outline);
+ transition: width var(--m3-duration-short-2) var(--m3-ease-emphasized), height var(--m3-duration-short-2) var(--m3-ease-emphasized), inset-inline-start var(--m3-duration-short-2) var(--m3-ease-emphasized), background-color var(--m3-duration-short-2) var(--m3-ease-standard);
+}
+
+.m3-switch input:checked {
+ background: var(--m3-primary);
+ border-color: var(--m3-primary);
+}
+
+.m3-switch input:checked::before {
+ inset-inline-start: 24px;
+ width: 24px;
+ height: 24px;
+ background: var(--m3-on-primary);
+}
+
+@media (hover: hover) {
+ .m3-switch:hover input::before { width: 20px; height: 20px; }
+ .m3-switch:hover input:checked::before { inset-inline-start: 20px; width: 28px; height: 28px; }
+}
+
+.m3-switch input:focus-visible { outline: 2px solid var(--m3-primary); outline-offset: 3px; }
+.m3-switch input:disabled { opacity: 0.38; pointer-events: none; }
+
+/* ============================================================================
+ 09 · Slider
+ ========================================================================== */
+
+.m3-slider {
+ --m3-slider-fill: 50%;
+ appearance: none;
+ width: 100%;
+ height: 32px;
+ background: transparent;
+ cursor: pointer;
+}
+
+.m3-slider::-webkit-slider-runnable-track {
+ height: 16px;
+ border-radius: var(--m3-shape-full);
+ background:
+ linear-gradient(var(--m3-primary), var(--m3-primary)) 0 / var(--m3-slider-fill) 100% no-repeat,
+ var(--m3-surface-container-highest);
+}
+
+.m3-slider::-moz-range-track {
+ height: 16px;
+ border-radius: var(--m3-shape-full);
+ background: var(--m3-surface-container-highest);
+}
+
+.m3-slider::-moz-range-progress {
+ height: 16px;
+ border-radius: var(--m3-shape-full);
+ background: var(--m3-primary);
+}
+
+.m3-slider::-webkit-slider-thumb {
+ appearance: none;
+ width: 6px;
+ height: 24px;
+ margin-top: -4px;
+ border-radius: var(--m3-shape-full);
+ background: var(--m3-primary);
+ border: 2px solid var(--m3-primary);
+ box-shadow: 0 0 0 0 color-mix(in srgb, var(--m3-primary) 16%, transparent);
+ transition: box-shadow var(--m3-duration-short) var(--m3-ease-standard), height var(--m3-duration-short) var(--m3-ease-emphasized);
+}
+
+.m3-slider::-moz-range-thumb {
+ width: 6px;
+ height: 24px;
+ border-radius: var(--m3-shape-full);
+ background: var(--m3-primary);
+ border: none;
+}
+
+.m3-slider:hover::-webkit-slider-thumb { box-shadow: 0 0 0 6px color-mix(in srgb, var(--m3-primary) 12%, transparent); }
+.m3-slider:active::-webkit-slider-thumb { height: 28px; margin-top: -6px; }
+.m3-slider:focus-visible { outline: none; }
+.m3-slider:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 10px color-mix(in srgb, var(--m3-primary) 16%, transparent); }
+
+/* ============================================================================
+ 10 · Lists
+ ========================================================================== */
+
+.m3-list {
+ display: flex;
+ flex-direction: column;
+ padding: var(--m3-space-2) 0;
+ list-style: none;
+ margin: 0;
+}
+
+.m3-list-item {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: var(--m3-space-4);
+ min-height: 56px;
+ padding: var(--m3-space-2) var(--m3-space-4);
+ border-radius: var(--m3-shape-xs);
+ color: var(--m3-on-surface);
+ text-decoration: none;
+ isolation: isolate;
+ overflow: hidden;
+ transition: background-color var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+.m3-list-item::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: currentColor;
+ opacity: 0;
+ transition: opacity var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+@media (hover: hover) { .m3-list-item:hover::after { opacity: var(--m3-state-hover); } }
+.m3-list-item:focus-visible::after { opacity: var(--m3-state-focus); }
+.m3-list-item:active::after { opacity: var(--m3-state-pressed); }
+.m3-list-item > * { position: relative; z-index: 1; }
+.m3-list-item:hover { text-decoration: none; }
+
+.m3-list-item__leading {
+ display: grid;
+ place-items: center;
+ flex: 0 0 auto;
+ width: 40px;
+ height: 40px;
+ border-radius: var(--m3-shape-full);
+ background: var(--m3-surface-container-highest);
+ color: var(--m3-on-surface-variant);
+ font-size: 1.05rem;
+}
+
+.m3-list-item__text { display: flex; flex-direction: column; min-width: 0; }
+.m3-list-item__headline { font-size: var(--m3-body-large-size); line-height: 1.35; }
+.m3-list-item__supporting { color: var(--m3-on-surface-variant); font-size: var(--m3-body-medium-size); }
+.m3-list-item__trailing { margin-inline-start: auto; color: var(--m3-on-surface-variant); font-size: var(--m3-label-medium-size); }
+
+.m3-divider { height: 1px; border: 0; background: var(--m3-outline-variant); margin: 0; }
+.m3-divider--inset { margin-inline: var(--m3-space-4); }
+
+/* ============================================================================
+ 11 · Menus
+ ========================================================================== */
+
+.m3-menu-anchor { position: relative; display: inline-flex; }
+
+.m3-menu {
+ position: absolute;
+ z-index: var(--m3-z-menu);
+ min-width: 200px;
+ max-width: min(320px, calc(100vw - 32px));
+ max-height: 60vh;
+ overflow-y: auto;
+ padding: var(--m3-space-2);
+ border-radius: var(--m3-shape-xs);
+ background: var(--m3-surface-container);
+ color: var(--m3-on-surface);
+ box-shadow: var(--m3-elevation-2);
+ opacity: 0;
+ transform: scale(0.9) translateY(-6px);
+ transform-origin: top left;
+ visibility: hidden;
+ transition:
+ opacity var(--m3-duration-short) var(--m3-ease-standard),
+ transform var(--m3-duration-short-2) var(--m3-ease-emphasized-decel),
+ visibility var(--m3-duration-short);
+}
+
+.m3-menu--open,
+.m3-menu.is-open {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ visibility: visible;
+}
+
+.m3-menu--end { inset-inline-end: 0; transform-origin: top right; }
+.m3-menu--bottom { top: 100%; margin-block-start: 4px; }
+
+.m3-menu-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ min-height: 48px;
+ padding: var(--m3-space-2) var(--m3-space-3);
+ border: none;
+ border-radius: var(--m3-shape-xs);
+ background: transparent;
+ color: inherit;
+ font-size: var(--m3-body-large-size);
+ text-align: start;
+ text-decoration: none;
+ cursor: pointer;
+ transition: background-color var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+@media (hover: hover) { .m3-menu-item:hover { background: color-mix(in srgb, var(--m3-on-surface) 8%, transparent); text-decoration: none; } }
+.m3-menu-item:focus-visible { background: color-mix(in srgb, var(--m3-on-surface) 10%, transparent); }
+.m3-menu-item[aria-checked='true'],
+.m3-menu-item.is-selected { background: var(--m3-secondary-container); color: var(--m3-on-secondary-container); }
+.m3-menu-item i:first-child { width: 22px; text-align: center; color: var(--m3-on-surface-variant); }
+.m3-menu-item[aria-checked='true'] i:first-child,
+.m3-menu-item.is-selected i:first-child { color: inherit; }
+.m3-menu-item__meta { margin-inline-start: auto; color: var(--m3-on-surface-variant); font-size: var(--m3-label-medium-size); }
+
+/* ============================================================================
+ 12 · Dialogs, bottom sheets, scrims
+ ========================================================================== */
+
+.m3-scrim {
+ position: fixed;
+ inset: 0;
+ z-index: var(--m3-z-drawer);
+ background: color-mix(in srgb, var(--m3-scrim) 42%, transparent);
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity var(--m3-duration-medium) var(--m3-ease-standard), visibility var(--m3-duration-medium);
+}
+
+.m3-scrim.is-open { opacity: 1; visibility: visible; }
+
+.m3-dialog {
+ position: fixed;
+ inset: 0;
+ z-index: var(--m3-z-dialog);
+ display: grid;
+ place-items: center;
+ padding: var(--m3-space-4);
+ background: color-mix(in srgb, var(--m3-scrim) 45%, transparent);
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity var(--m3-duration-medium) var(--m3-ease-standard), visibility var(--m3-duration-medium);
+}
+
+.m3-dialog.is-open,
+.m3-dialog[open] { opacity: 1; visibility: visible; }
+
+.m3-dialog__surface {
+ width: min(560px, 100%);
+ max-height: min(88vh, 880px);
+ display: flex;
+ flex-direction: column;
+ border-radius: var(--m3-shape-xl);
+ background: var(--m3-surface-container-high);
+ color: var(--m3-on-surface);
+ box-shadow: var(--m3-elevation-3);
+ transform: scale(0.9);
+ opacity: 0;
+ transition:
+ transform var(--m3-duration-medium) var(--m3-ease-emphasized-decel),
+ opacity var(--m3-duration-medium) var(--m3-ease-standard);
+}
+
+.m3-dialog.is-open .m3-dialog__surface { transform: scale(1); opacity: 1; }
+.m3-dialog--wide .m3-dialog__surface { width: min(880px, 100%); }
+.m3-dialog--full .m3-dialog__surface { width: min(1100px, 100%); height: min(88vh, 900px); }
+
+.m3-dialog__head {
+ display: flex;
+ align-items: center;
+ gap: var(--m3-space-4);
+ padding: var(--m3-space-6) var(--m3-space-6) var(--m3-space-4);
+}
+
+.m3-dialog__head--centered { flex-direction: column; text-align: center; padding-inline: var(--m3-space-6); }
+
+.m3-dialog__icon {
+ display: grid;
+ place-items: center;
+ color: var(--m3-secondary);
+ font-size: 1.75rem;
+}
+
+.m3-dialog__title {
+ font-size: var(--m3-headline-small-size);
+ line-height: var(--m3-headline-small-line);
+ font-weight: 500;
+ letter-spacing: var(--m3-tracking-headline);
+}
+
+.m3-dialog__body {
+ flex: 1 1 auto;
+ overflow-y: auto;
+ padding: 0 var(--m3-space-6) var(--m3-space-4);
+ color: var(--m3-on-surface-variant);
+ font-size: var(--m3-body-medium-size);
+ line-height: var(--m3-body-medium-line);
+}
+
+.m3-dialog__actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--m3-space-2);
+ padding: var(--m3-space-4) var(--m3-space-6) var(--m3-space-6);
+ flex-wrap: wrap;
+}
+
+/* Bottom sheet -------------------------------------------------------------- */
+.m3-sheet {
+ position: fixed;
+ inset-inline: 0;
+ inset-block-end: 0;
+ z-index: var(--m3-z-dialog);
+ display: flex;
+ flex-direction: column;
+ max-height: 90dvh;
+ padding: var(--m3-space-3) var(--m3-space-4) calc(var(--m3-space-6) + env(safe-area-inset-bottom));
+ border-radius: var(--m3-shape-xl) var(--m3-shape-xl) 0 0;
+ background: var(--m3-surface-container-low);
+ color: var(--m3-on-surface);
+ box-shadow: var(--m3-elevation-2);
+ transform: translateY(100%);
+ visibility: hidden;
+ transition: transform var(--m3-duration-medium) var(--m3-ease-emphasized-decel), visibility var(--m3-duration-medium);
+}
+
+.m3-sheet.is-open { transform: translateY(0); visibility: visible; }
+
+.m3-sheet__handle {
+ width: 32px;
+ height: 4px;
+ margin: 0 auto var(--m3-space-4);
+ border-radius: var(--m3-shape-full);
+ background: var(--m3-outline-variant);
+}
+
+/* ============================================================================
+ 13 · Snackbar, tooltip, banner
+ ========================================================================== */
+
+.m3-snackbar-region {
+ position: fixed;
+ z-index: var(--m3-z-snackbar);
+ inset-block-end: calc(var(--m3-space-5) + env(safe-area-inset-bottom));
+ inset-inline: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--m3-space-2);
+ padding-inline: var(--m3-space-4);
+ pointer-events: none;
+}
+
+.m3-snackbar {
+ display: flex;
+ align-items: center;
+ gap: var(--m3-space-3);
+ width: min(560px, 100%);
+ min-height: 48px;
+ padding: var(--m3-space-3) var(--m3-space-4);
+ border-radius: var(--m3-shape-xs);
+ background: var(--m3-inverse-surface);
+ color: var(--m3-inverse-on-surface);
+ font-size: var(--m3-body-medium-size);
+ box-shadow: var(--m3-elevation-3);
+ pointer-events: auto;
+ animation: m3-snackbar-in var(--m3-duration-medium) var(--m3-ease-emphasized-decel);
+}
+
+.m3-snackbar.is-leaving { animation: m3-snackbar-out var(--m3-duration-short-2) var(--m3-ease-standard-accel) forwards; }
+.m3-snackbar__action {
+ margin-inline-start: auto;
+ padding: 6px 10px;
+ border: none;
+ border-radius: var(--m3-shape-full);
+ background: transparent;
+ color: var(--m3-inverse-primary);
+ font-weight: var(--m3-label-large-weight);
+ font-size: var(--m3-label-large-size);
+ cursor: pointer;
+}
+
+.m3-snackbar__action:hover { background: color-mix(in srgb, var(--m3-inverse-primary) 16%, transparent); }
+.m3-snackbar--error { background: var(--m3-error-container); color: var(--m3-on-error-container); }
+
+@keyframes m3-snackbar-in {
+ from { opacity: 0; transform: translateY(16px) scale(0.96); }
+ to { opacity: 1; transform: translateY(0) scale(1); }
+}
+
+@keyframes m3-snackbar-out {
+ to { opacity: 0; transform: translateY(12px) scale(0.96); }
+}
+
+.m3-tooltip {
+ position: relative;
+}
+
+.m3-tooltip::after {
+ content: attr(data-tooltip);
+ position: absolute;
+ inset-block-end: calc(100% + 6px);
+ inset-inline-start: 50%;
+ translate: -50% 0;
+ z-index: var(--m3-z-tooltip);
+ padding: 4px 8px;
+ border-radius: var(--m3-shape-xs);
+ background: var(--m3-inverse-surface);
+ color: var(--m3-inverse-on-surface);
+ font-size: var(--m3-body-small-size);
+ white-space: nowrap;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+@media (hover: hover) {
+ .m3-tooltip:hover::after,
+ .m3-tooltip:focus-visible::after { opacity: 1; }
+}
+
+.m3-banner {
+ display: flex;
+ align-items: center;
+ gap: var(--m3-space-4);
+ padding: var(--m3-space-3) var(--m3-space-4);
+ border-radius: var(--m3-shape-md);
+ background: var(--m3-surface-container-high);
+ color: var(--m3-on-surface);
+ font-size: var(--m3-body-medium-size);
+}
+
+.m3-banner--info { background: var(--m3-secondary-container); color: var(--m3-on-secondary-container); }
+.m3-banner--warn { background: var(--m3-tertiary-container); color: var(--m3-on-tertiary-container); }
+.m3-banner--error { background: var(--m3-error-container); color: var(--m3-on-error-container); }
+
+/* ============================================================================
+ 14 · Tabs, navigation bar / rail / drawer, app bar
+ ========================================================================== */
+
+.m3-tabs {
+ position: relative;
+ display: flex;
+ align-items: stretch;
+ gap: var(--m3-space-2);
+ overflow-x: auto;
+ scrollbar-width: none;
+ border-block-end: 1px solid var(--m3-outline-variant);
+}
+
+.m3-tabs::-webkit-scrollbar { display: none; }
+
+.m3-tab {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ flex: 1 1 auto;
+ min-width: max-content;
+ min-height: 48px;
+ padding: 0 var(--m3-space-4);
+ border: none;
+ background: transparent;
+ color: var(--m3-on-surface-variant);
+ font-size: var(--m3-title-small-size);
+ font-weight: var(--m3-title-small-weight);
+ letter-spacing: var(--m3-tracking-title);
+ text-decoration: none;
+ cursor: pointer;
+ transition: color var(--m3-duration-short) var(--m3-ease-standard), background-color var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+.m3-tab:hover { color: var(--m3-on-surface); text-decoration: none; }
+.m3-tab[aria-selected='true'],
+.m3-tab.is-active {
+ color: var(--m3-primary);
+}
+
+.m3-tab[aria-selected='true']::after,
+.m3-tab.is-active::after {
+ content: '';
+ position: absolute;
+ inset-block-end: 0;
+ inset-inline: var(--m3-space-4);
+ height: 3px;
+ border-radius: var(--m3-shape-full) var(--m3-shape-full) 0 0;
+ background: var(--m3-primary);
+}
+
+.m3-tab-panel[hidden] { display: none; }
+.m3-tab-panel { animation: m3-fade-in var(--m3-duration-medium) var(--m3-ease-standard-decel); }
+
+@keyframes m3-fade-in {
+ from { opacity: 0; transform: translateY(8px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* Navigation bar (mobile) --------------------------------------------------- */
+.m3-nav-bar {
+ position: fixed;
+ inset-block-end: 0;
+ inset-inline: 0;
+ z-index: var(--m3-z-appbar);
+ display: flex;
+ justify-content: space-around;
+ align-items: stretch;
+ height: calc(80px + env(safe-area-inset-bottom));
+ padding-block-end: env(safe-area-inset-bottom);
+ background: var(--m3-surface-container);
+ box-shadow: var(--m3-elevation-2);
+}
+
+.m3-nav-bar__item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ flex: 1 1 0;
+ max-width: 120px;
+ padding: var(--m3-space-2) 0;
+ color: var(--m3-on-surface-variant);
+ font-size: var(--m3-label-medium-size);
+ font-weight: var(--m3-label-medium-weight);
+ text-decoration: none;
+ transition: color var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+.m3-nav-bar__item:hover { text-decoration: none; }
+
+.m3-nav-bar__icon {
+ display: grid;
+ place-items: center;
+ width: 64px;
+ height: 32px;
+ border-radius: var(--m3-shape-full);
+ font-size: 1.15rem;
+ transition: background-color var(--m3-duration-short-2) var(--m3-ease-emphasized), color var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+.m3-nav-bar__item[aria-current='page'] { color: var(--m3-on-secondary-container); }
+.m3-nav-bar__item[aria-current='page'] .m3-nav-bar__icon {
+ background: var(--m3-secondary-container);
+ color: var(--m3-on-secondary-container);
+}
+
+/* Navigation drawer --------------------------------------------------------- */
+.m3-drawer {
+ position: fixed;
+ inset-block: 0;
+ inset-inline-start: 0;
+ z-index: calc(var(--m3-z-drawer) + 1);
+ width: min(320px, 88vw);
+ display: flex;
+ flex-direction: column;
+ gap: var(--m3-space-2);
+ padding: var(--m3-space-3) var(--m3-space-3) calc(var(--m3-space-4) + env(safe-area-inset-bottom));
+ background: var(--m3-surface-container-low);
+ color: var(--m3-on-surface);
+ box-shadow: var(--m3-elevation-1);
+ border-start-end-radius: var(--m3-shape-lg);
+ border-end-end-radius: var(--m3-shape-lg);
+ overflow-y: auto;
+ transform: translateX(-105%);
+ visibility: hidden;
+ transition:
+ transform var(--m3-duration-medium) var(--m3-ease-emphasized-decel),
+ visibility var(--m3-duration-medium);
+}
+
+.m3-drawer.is-open,
+.m3-drawer.active,
+.m3-drawer[data-open='true'] {
+ transform: translateX(0);
+ visibility: visible;
+}
+
+.m3-drawer__head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--m3-space-3);
+ padding: var(--m3-space-2) var(--m3-space-2) var(--m3-space-4);
+}
+
+.m3-drawer__brand {
+ display: flex;
+ align-items: center;
+ gap: var(--m3-space-3);
+ font-size: var(--m3-title-large-size);
+ font-weight: 600;
+ color: var(--m3-on-surface);
+}
+
+.m3-drawer__brand img {
+ width: 40px;
+ height: 40px;
+ border-radius: var(--m3-shape-md);
+ object-fit: cover;
+}
+
+.m3-drawer__label {
+ padding: var(--m3-space-4) var(--m3-space-4) var(--m3-space-2);
+ color: var(--m3-on-surface-variant);
+ font-size: var(--m3-label-large-size);
+ font-weight: var(--m3-label-large-weight);
+ letter-spacing: var(--m3-tracking-label);
+ text-transform: uppercase;
+}
+
+.m3-drawer__item {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: var(--m3-space-3);
+ min-height: 56px;
+ padding: var(--m3-space-2) var(--m3-space-4);
+ border-radius: var(--m3-shape-full);
+ color: var(--m3-on-surface-variant);
+ font-size: var(--m3-body-large-size);
+ font-weight: 500;
+ text-decoration: none;
+ isolation: isolate;
+ overflow: hidden;
+ transition: background-color var(--m3-duration-short-2) var(--m3-ease-standard), color var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+.m3-drawer__item::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: currentColor;
+ opacity: 0;
+ transition: opacity var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+@media (hover: hover) { .m3-drawer__item:hover::after { opacity: var(--m3-state-hover); } }
+.m3-drawer__item:focus-visible::after { opacity: var(--m3-state-focus); }
+.m3-drawer__item:active::after { opacity: var(--m3-state-pressed); }
+.m3-drawer__item > * { position: relative; z-index: 1; }
+.m3-drawer__item:hover { text-decoration: none; color: var(--m3-on-surface); }
+
+.m3-drawer__item[aria-current='page'],
+.m3-drawer__item.active,
+.m3-drawer__item.is-active {
+ background: var(--m3-secondary-container);
+ color: var(--m3-on-secondary-container);
+ font-weight: 600;
+}
+
+.m3-drawer__item i { width: 22px; text-align: center; }
+.m3-drawer__footer { margin-top: auto; padding: var(--m3-space-4); color: var(--m3-on-surface-variant); font-size: var(--m3-body-small-size); }
+
+/* Top app bar --------------------------------------------------------------- */
+.m3-app-bar {
+ position: sticky;
+ inset-block-start: 0;
+ z-index: var(--m3-z-appbar);
+ background: var(--m3-surface);
+ color: var(--m3-on-surface);
+ transition: background-color var(--m3-duration-short-2) var(--m3-ease-standard), box-shadow var(--m3-duration-short-2) var(--m3-ease-standard);
+}
+
+.m3-app-bar.is-scrolled {
+ background: var(--m3-surface-container);
+ box-shadow: var(--m3-elevation-2);
+}
+
+.m3-app-bar__row {
+ display: flex;
+ align-items: center;
+ gap: var(--m3-space-2);
+ min-height: var(--m3-appbar-height);
+ padding-inline: var(--m3-space-2);
+}
+
+.m3-app-bar__title {
+ display: flex;
+ align-items: center;
+ gap: var(--m3-space-2);
+ margin-inline-start: var(--m3-space-2);
+ font-family: var(--m3-font-brand);
+ font-size: var(--m3-title-large-size);
+ font-weight: 600;
+ letter-spacing: var(--m3-tracking-title);
+ color: inherit;
+ text-decoration: none;
+}
+
+.m3-app-bar__title:hover { text-decoration: none; color: inherit; }
+.m3-app-bar__title img { width: 36px; height: 36px; border-radius: var(--m3-shape-sm); object-fit: cover; }
+.m3-app-bar__actions { display: flex; align-items: center; gap: 4px; margin-inline-start: auto; }
+
+.m3-app-bar--large { display: flex; flex-direction: column; }
+.m3-app-bar--large .m3-app-bar__headline {
+ padding: var(--m3-space-2) var(--m3-space-6) var(--m3-space-6);
+ font-size: var(--m3-headline-medium-size);
+ line-height: var(--m3-headline-medium-line);
+}
+
+/* ============================================================================
+ 15 · Tables, progress, badges & misc
+ ========================================================================== */
+
+.m3-table-wrap {
+ width: 100%;
+ overflow-x: auto;
+ border-radius: var(--m3-shape-md);
+ border: 1px solid var(--m3-outline-variant);
+ background: var(--m3-surface);
+ -webkit-overflow-scrolling: touch;
+}
+
+.m3-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: var(--m3-body-medium-size);
+}
+
+.m3-table caption {
+ caption-side: top;
+ padding: var(--m3-space-4);
+ text-align: start;
+ color: var(--m3-on-surface-variant);
+ font-size: var(--m3-body-small-size);
+}
+
+.m3-table th,
+.m3-table td {
+ padding: var(--m3-space-3) var(--m3-space-4);
+ text-align: start;
+ border-block-end: 1px solid var(--m3-outline-variant);
+ vertical-align: middle;
+}
+
+.m3-table thead th {
+ position: sticky;
+ inset-block-start: 0;
+ z-index: 1;
+ background: var(--m3-surface-container);
+ color: var(--m3-on-surface-variant);
+ font-size: var(--m3-label-large-size);
+ font-weight: var(--m3-label-large-weight);
+ letter-spacing: var(--m3-tracking-label);
+ white-space: nowrap;
+}
+
+.m3-table tbody tr { transition: background-color var(--m3-duration-short) var(--m3-ease-standard); }
+@media (hover: hover) { .m3-table tbody tr:hover { background: color-mix(in srgb, var(--m3-on-surface) 6%, transparent); } }
+.m3-table tbody tr:last-child td { border-block-end: none; }
+.m3-table__num { font-variant-numeric: tabular-nums; font-family: var(--m3-font-mono); }
+.m3-table__actions { width: 1%; white-space: nowrap; text-align: end; }
+
+.m3-progress-linear {
+ position: relative;
+ height: 4px;
+ border-radius: var(--m3-shape-full);
+ background: var(--m3-surface-container-highest);
+ overflow: hidden;
+}
+
+.m3-progress-linear__bar {
+ height: 100%;
+ border-radius: inherit;
+ background: var(--m3-primary);
+ transition: width var(--m3-duration-long) var(--m3-ease-emphasized-decel);
+}
+
+.m3-progress-linear--indeterminate::after {
+ content: '';
+ position: absolute;
+ inset-block: 0;
+ inset-inline-start: -40%;
+ width: 40%;
+ border-radius: inherit;
+ background: var(--m3-primary);
+ animation: m3-indeterminate 1.6s var(--m3-ease-standard) infinite;
+}
+
+@keyframes m3-indeterminate {
+ 0% { inset-inline-start: -40%; }
+ 100% { inset-inline-start: 100%; }
+}
+
+.m3-circular {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ border: 3px solid var(--m3-surface-container-highest);
+ border-top-color: var(--m3-primary);
+ animation: m3-spin 900ms linear infinite;
+}
+
+.m3-circular--sm { width: 18px; height: 18px; border-width: 2px; }
+
+.m3-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 10px;
+ border-radius: var(--m3-shape-sm);
+ background: var(--m3-secondary-container);
+ color: var(--m3-on-secondary-container);
+ font-size: var(--m3-label-medium-size);
+ font-weight: var(--m3-label-medium-weight);
+ letter-spacing: var(--m3-tracking-label);
+}
+
+.m3-badge--primary { background: var(--m3-primary-container); color: var(--m3-on-primary-container); }
+.m3-badge--tertiary { background: var(--m3-tertiary-container); color: var(--m3-on-tertiary-container); }
+.m3-badge--error { background: var(--m3-error-container); color: var(--m3-on-error-container); }
+.m3-badge--outline { background: transparent; border: 1px solid var(--m3-outline-variant); color: var(--m3-on-surface-variant); }
+.m3-badge--dot {
+ width: 8px;
+ height: 8px;
+ padding: 0;
+ border-radius: 50%;
+ background: var(--m3-error);
+}
+
+.m3-avatar {
+ display: grid;
+ place-items: center;
+ width: 48px;
+ height: 48px;
+ border-radius: var(--m3-shape-full);
+ background: var(--m3-primary-container);
+ color: var(--m3-on-primary-container);
+ object-fit: cover;
+ font-weight: 600;
+ overflow: hidden;
+}
+
+.m3-avatar--sm { width: 32px; height: 32px; font-size: var(--m3-body-small-size); }
+.m3-avatar--lg { width: 88px; height: 88px; font-size: 2rem; }
+
+.m3-skeleton {
+ position: relative;
+ overflow: hidden;
+ border-radius: var(--m3-shape-sm);
+ background: var(--m3-surface-container-high);
+}
+
+.m3-skeleton::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--m3-on-surface) 8%, transparent), transparent);
+ transform: translateX(-100%);
+ animation: m3-shimmer 1.4s var(--m3-ease-standard) infinite;
+}
+
+@keyframes m3-shimmer {
+ to { transform: translateX(100%); }
+}
+
+.m3-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--m3-space-3);
+ padding: var(--m3-space-12) var(--m3-space-4);
+ text-align: center;
+ color: var(--m3-on-surface-variant);
+}
+
+.m3-empty i,
+.m3-empty svg { font-size: 2.5rem; color: var(--m3-outline); }
+.m3-empty h3 { color: var(--m3-on-surface); }
+
+.m3-expansion {
+ border-radius: var(--m3-shape-md);
+ background: var(--m3-surface-container-low);
+ overflow: hidden;
+}
+
+.m3-expansion + .m3-expansion { margin-block-start: var(--m3-space-3); }
+
+.m3-expansion__summary {
+ display: flex;
+ align-items: center;
+ gap: var(--m3-space-3);
+ padding: var(--m3-space-4);
+ cursor: pointer;
+ list-style: none;
+ font-size: var(--m3-title-medium-size);
+ font-weight: var(--m3-title-medium-weight);
+ color: var(--m3-on-surface);
+}
+
+.m3-expansion__summary::-webkit-details-marker { display: none; }
+.m3-expansion__summary::after {
+ content: '\f107';
+ font-family: 'Font Awesome 6 Free';
+ font-weight: 900;
+ margin-inline-start: auto;
+ color: var(--m3-on-surface-variant);
+ transition: transform var(--m3-duration-medium) var(--m3-ease-emphasized);
+}
+
+.m3-expansion[open] > .m3-expansion__summary::after { transform: rotate(180deg); }
+.m3-expansion__content { padding: 0 var(--m3-space-4) var(--m3-space-4); color: var(--m3-on-surface-variant); }
+
+.m3-stepper { display: flex; align-items: center; gap: var(--m3-space-2); }
+.m3-stepper__btn {
+ display: grid;
+ place-items: center;
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ border: 1px solid var(--m3-outline);
+ background: transparent;
+ color: var(--m3-on-surface-variant);
+ cursor: pointer;
+}
+.m3-stepper__value { min-width: 3ch; text-align: center; font-variant-numeric: tabular-nums; font-family: var(--m3-font-mono); }
+
+.m3-pagination {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ flex-wrap: wrap;
+}
+
+.m3-page-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 40px;
+ height: 40px;
+ padding-inline: 12px;
+ border: none;
+ border-radius: var(--m3-shape-full);
+ background: transparent;
+ color: var(--m3-on-surface-variant);
+ font-size: var(--m3-label-large-size);
+ font-weight: var(--m3-label-large-weight);
+ cursor: pointer;
+ transition: background-color var(--m3-duration-short) var(--m3-ease-standard), color var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+@media (hover: hover) { .m3-page-btn:hover:not(:disabled) { background: color-mix(in srgb, var(--m3-on-surface) 8%, transparent); color: var(--m3-on-surface); } }
+.m3-page-btn[aria-current='page'],
+.m3-page-btn.is-active { background: var(--m3-primary); color: var(--m3-on-primary); }
+.m3-page-btn:disabled { opacity: 0.38; cursor: not-allowed; }
+
+.m3-label-float { color: var(--m3-on-surface-variant); font-size: var(--m3-label-large-size); font-weight: var(--m3-label-large-weight); letter-spacing: var(--m3-tracking-label); }
+
+/* Motion utilities ---------------------------------------------------------- */
+.m3-animate-in { animation: m3-fade-in var(--m3-duration-medium) var(--m3-ease-emphasized-decel) both; }
+.m3-press { transition: transform var(--m3-duration-short-2) var(--m3-ease-emphasized); }
+.m3-press:active { transform: scale(0.96); }
+
+@media (prefers-reduced-motion: reduce) {
+ .m3-dialog__surface,
+ .m3-drawer,
+ .m3-sheet { transform: none !important; }
+}
+
+/* ── Dynamic colour picker (rendered by assets/m3/color.js) ─────────────── */
+
+.m3-color-picker { display: flex; flex-direction: column; gap: var(--m3-space-2); }
+.m3-color-picker .m3-chip { gap: 10px; }
+.m3-color-picker input[type='color'] { border-radius: var(--m3-shape-full); }
diff --git a/main/assets/m3/gen_palette.py b/main/assets/m3/gen_palette.py
new file mode 100644
index 0000000..bb030b7
--- /dev/null
+++ b/main/assets/m3/gen_palette.py
@@ -0,0 +1,242 @@
+"""Generate Material You 3 tonal palettes from seed colors (offline, one-shot).
+Mirrors the runtime generator in main/assets/m3/color.js (CIELAB-based tonal palettes)."""
+import math
+
+def srgb_to_lin(c):
+ c = c / 255.0
+ return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
+
+def lin_to_srgb(c):
+ c = max(0.0, min(1.0, c))
+ v = 12.92 * c if c <= 0.0031308 else 1.055 * (c ** (1 / 2.4)) - 0.055
+ return round(max(0, min(255, v * 255)))
+
+def hex_to_rgb(h):
+ h = h.lstrip('#')
+ return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
+
+def rgb_to_hex(rgb):
+ return '#%02X%02X%02X' % tuple(rgb)
+
+# sRGB (D65) -> XYZ -> Lab
+def rgb_to_lab(rgb):
+ r, g, b = [srgb_to_lin(c) for c in rgb]
+ x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375
+ y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750
+ z = r * 0.0193339 + g * 0.1191920 + b * 0.9503041
+ xn, yn, zn = 0.95047, 1.0, 1.08883
+ def f(t):
+ return t ** (1/3) if t > 216/24389 else (841/108) * t + 4/29
+ fx, fy, fz = f(x/xn), f(y/yn), f(z/zn)
+ return (116*fy - 16, 500*(fx-fy), 200*(fy-fz))
+
+def lab_to_rgb(lab):
+ L, a, bb = lab
+ fy = (L + 16) / 116
+ fx = fy + a / 500
+ fz = fy - bb / 200
+ def f_inv(t):
+ return t ** 3 if t ** 3 > 216/24389 else (t - 4/29) * (108/841)
+ x = f_inv(fx) * 0.95047
+ y = f_inv(fy) * 1.0
+ z = f_inv(fz) * 1.08883
+ r = x * 3.2404542 + y * -1.5371385 + z * -0.4985314
+ g = x * -0.9692660 + y * 1.8760108 + z * 0.0415560
+ bl = x * 0.0556434 + y * -0.2040259 + z * 1.0572252
+ return (lin_to_srgb(r), lin_to_srgb(g), lin_to_srgb(bl))
+
+def in_gamut(rgb):
+ r, g, b = [srgb_to_lin(c) for c in rgb]
+ x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375
+ y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750
+ z = r * 0.0193339 + g * 0.1191920 + b * 0.9503041
+ xn, yn, zn = 0.95047, 1.0, 1.08883
+ x, y, z = x/xn, y/yn, z/zn
+ return (-1e-4 <= x <= 1.0001) and (-1e-4 <= y <= 1.0001) and (-1e-4 <= z <= 1.0001)
+
+def tone_to_hex(L, a, b):
+ """Return hex for target lightness L keeping (a,b) chroma, clamped into sRGB gamut."""
+ aa, bb = a, b
+ rgb = lab_to_rgb((L, aa, bb))
+ if in_gamut(rgb):
+ return rgb_to_hex(rgb)
+ # binary search chroma reduction
+ lo, hi = 0.0, 1.0
+ best = (255, 255, 255) if L > 50 else (0, 0, 0)
+ for _ in range(24):
+ mid = (lo + hi) / 2
+ rgb = lab_to_rgb((L, aa * mid, bb * mid))
+ if in_gamut(rgb):
+ best = rgb
+ lo = mid
+ else:
+ hi = mid
+ return rgb_to_hex(best)
+
+TONES = [0, 4, 5, 6, 10, 12, 17, 20, 22, 24, 25, 30, 35, 40, 50, 60, 70, 80, 87, 90, 92, 94, 95, 96, 98, 99, 100]
+
+def palette(seed_hex, chroma_scale=1.0, forced_hue=None):
+ L, a, b = rgb_to_lab(hex_to_rgb(seed_hex))
+ hue = math.atan2(b, a)
+ chroma = math.hypot(a, b) * chroma_scale
+ if forced_hue is not None:
+ hue = math.radians(forced_hue)
+ out = {}
+ for t in TONES:
+ out[t] = tone_to_hex(t, chroma * math.cos(hue), chroma * math.sin(hue))
+ return out, math.degrees(hue), chroma
+
+LIGHT = dict(
+ primary=40, on_primary=100, primary_container=90, on_primary_container=10,
+ primary_fixed=90, primary_fixed_dim=80, on_primary_fixed=10, on_primary_fixed_variant=30,
+ secondary=40, on_secondary=100, secondary_container=90, on_secondary_container=10,
+ tertiary=40, on_tertiary=100, tertiary_container=90, on_tertiary_container=10,
+ error=40, on_error=100, error_container=90, on_error_container=10,
+ background=98, on_background=10,
+ surface=98, on_surface=10, surface_variant=90, on_surface_variant=30,
+ surface_dim=87, surface_bright=98,
+ surface_container_lowest=100, surface_container_low=96, surface_container=94,
+ surface_container_high=92, surface_container_highest=90,
+ outline=50, outline_variant=80,
+ inverse_surface=20, inverse_on_surface=95, inverse_primary=80,
+ scrim=0, shadow=0, surface_tint=40,
+)
+DARK = dict(
+ primary=80, on_primary=20, primary_container=30, on_primary_container=90,
+ primary_fixed=90, primary_fixed_dim=80, on_primary_fixed=10, on_primary_fixed_variant=30,
+ secondary=80, on_secondary=20, secondary_container=30, on_secondary_container=90,
+ tertiary=80, on_tertiary=20, tertiary_container=30, on_tertiary_container=90,
+ error=80, on_error=20, error_container=30, on_error_container=90,
+ background=6, on_background=90,
+ surface=6, on_surface=90, surface_variant=30, on_surface_variant=80,
+ surface_dim=6, surface_bright=24,
+ surface_container_lowest=4, surface_container_low=10, surface_container=12,
+ surface_container_high=17, surface_container_highest=22,
+ outline=60, outline_variant=30,
+ inverse_surface=90, inverse_on_surface=20, inverse_primary=40,
+ scrim=0, shadow=0, surface_tint=80,
+)
+
+ERROR_SEED = '#B3261E' # M3 fixed error palette
+NEUTRAL_CHROMA = 3.2
+NV_CHROMA = 9.0
+
+def build(seed_hex, tertiary_hue_shift=60):
+ p, hue, chroma = palette(seed_hex)
+ # secondary = desaturated version of the seed hue
+ sec, _, _ = palette(seed_hex, chroma_scale=0.34)
+ ter, _, _ = palette(seed_hex, chroma_scale=0.72, forced_hue=hue + tertiary_hue_shift)
+ n_err, _, _ = palette(ERROR_SEED)
+ neu = {}
+ nv = {}
+ for t in TONES:
+ L, a, b = rgb_to_lab(hex_to_rgb(p[t]))
+ neu[t] = tone_to_hex(t, NEUTRAL_CHROMA * math.cos(math.radians(hue)), NEUTRAL_CHROMA * math.sin(math.radians(hue)))
+ nv[t] = tone_to_hex(t, NV_CHROMA * math.cos(math.radians(hue)), NV_CHROMA * math.sin(math.radians(hue)))
+ return dict(primary=p, secondary=sec, tertiary=ter, error=n_err, neutral=neu, neutral_variant=nv)
+
+# role -> (palette key, tone) -- explicit Material 3 role mapping
+ROLE_MAP_LIGHT = {
+ 'primary': ('primary', 40), 'on_primary': ('primary', 100),
+ 'primary_container': ('primary', 90), 'on_primary_container': ('primary', 10),
+ 'primary_fixed': ('primary', 90), 'primary_fixed_dim': ('primary', 80),
+ 'on_primary_fixed': ('primary', 10), 'on_primary_fixed_variant': ('primary', 30),
+ 'secondary': ('secondary', 40), 'on_secondary': ('secondary', 100),
+ 'secondary_container': ('secondary', 90), 'on_secondary_container': ('secondary', 10),
+ 'tertiary': ('tertiary', 40), 'on_tertiary': ('tertiary', 100),
+ 'tertiary_container': ('tertiary', 90), 'on_tertiary_container': ('tertiary', 10),
+ 'error': ('error', 40), 'on_error': ('error', 100),
+ 'error_container': ('error', 90), 'on_error_container': ('error', 10),
+ 'background': ('neutral', 98), 'on_background': ('neutral', 10),
+ 'surface': ('neutral', 98), 'on_surface': ('neutral', 10),
+ 'surface_variant': ('neutral_variant', 90), 'on_surface_variant': ('neutral_variant', 30),
+ 'surface_dim': ('neutral', 87), 'surface_bright': ('neutral', 98),
+ 'surface_container_lowest': ('neutral', 100), 'surface_container_low': ('neutral', 96),
+ 'surface_container': ('neutral', 94), 'surface_container_high': ('neutral', 92),
+ 'surface_container_highest': ('neutral', 90),
+ 'outline': ('neutral_variant', 50), 'outline_variant': ('neutral_variant', 80),
+ 'inverse_surface': ('neutral', 20), 'inverse_on_surface': ('neutral', 95),
+ 'inverse_primary': ('primary', 80),
+ 'scrim': ('neutral', 0), 'shadow': ('neutral', 0), 'surface_tint': ('primary', 40),
+}
+ROLE_MAP_DARK = dict(ROLE_MAP_LIGHT)
+ROLE_MAP_DARK.update({
+ 'primary': ('primary', 80), 'on_primary': ('primary', 20),
+ 'primary_container': ('primary', 30), 'on_primary_container': ('primary', 90),
+ 'secondary': ('secondary', 80), 'on_secondary': ('secondary', 20),
+ 'secondary_container': ('secondary', 30), 'on_secondary_container': ('secondary', 90),
+ 'tertiary': ('tertiary', 80), 'on_tertiary': ('tertiary', 20),
+ 'tertiary_container': ('tertiary', 30), 'on_tertiary_container': ('tertiary', 90),
+ 'error': ('error', 80), 'on_error': ('error', 20),
+ 'error_container': ('error', 30), 'on_error_container': ('error', 90),
+ 'background': ('neutral', 6), 'on_background': ('neutral', 90),
+ 'surface': ('neutral', 6), 'on_surface': ('neutral', 90),
+ 'surface_variant': ('neutral_variant', 30), 'on_surface_variant': ('neutral_variant', 80),
+ 'surface_dim': ('neutral', 6), 'surface_bright': ('neutral', 24),
+ 'surface_container_lowest': ('neutral', 4), 'surface_container_low': ('neutral', 10),
+ 'surface_container': ('neutral', 12), 'surface_container_high': ('neutral', 17),
+ 'surface_container_highest': ('neutral', 22),
+ 'outline': ('neutral_variant', 60), 'outline_variant': ('neutral_variant', 30),
+ 'inverse_surface': ('neutral', 90), 'inverse_on_surface': ('neutral', 20),
+ 'inverse_primary': ('primary', 40), 'surface_tint': ('primary', 80),
+})
+
+def _tone(maps, key, tone):
+ if tone == 100:
+ return '#FFFFFF'
+ if tone == 0:
+ return '#000000'
+ return maps[key][tone]
+
+def roles(scheme, maps):
+ return {role: _tone(maps, *ref) for role, ref in scheme.items()}
+
+def css_block(selector, scheme, maps, indent=' '):
+ r = roles(scheme, maps)
+ lines = [f'{selector} {{']
+ for k in scheme.keys():
+ lines.append(f'{indent}--m3-{k.replace("_", "-")}: {r[k]};')
+ lines.append('}')
+ return '\n'.join(lines)
+
+SEEDS = {
+ '': '#C8102E', # GDIPS red (default / brand)
+ 'indigo': '#6366F1',
+ 'violet': '#8B5CF6',
+ 'crimson': '#DC2626',
+ 'emerald': '#0F9D58',
+ 'amber': '#F59E0B',
+ 'azure': '#2563EB',
+}
+
+blocks = []
+# default seed: light + dark (+ system dark fallback for no-JS)
+maps_default = build(SEEDS[''])
+blocks.append('/* ── Default dynamic palette: GDIPS red ───────────────────────────── */')
+blocks.append(css_block(':root', ROLE_MAP_LIGHT, maps_default))
+blocks.append(css_block(':root[data-theme="dark"]', ROLE_MAP_DARK, maps_default))
+blocks.append('@media (prefers-color-scheme: dark) {\n ' +
+ css_block(':root:not([data-theme="light"])', ROLE_MAP_DARK, maps_default).replace('\n', '\n ') + '\n}')
+
+for name, seed in SEEDS.items():
+ if not name:
+ continue
+ m = build(seed)
+ blocks.append(f'/* ── Seed: {name} ({seed}) ───────────────────────────────────────── */')
+ blocks.append(css_block(f':root[data-seed="{name}"]', ROLE_MAP_LIGHT, m))
+ blocks.append(css_block(f':root[data-seed="{name}"][data-theme="dark"]', ROLE_MAP_DARK, m))
+ blocks.append('@media (prefers-color-scheme: dark) {\n ' +
+ css_block(f':root[data-seed="{name}"]:not([data-theme="light"])', ROLE_MAP_DARK, m).replace('\n', '\n ') + '\n}')
+
+header = '''/* ============================================================================
+ GDIPS · Material You 3 — Dynamic color palettes
+ ----------------------------------------------------------------------------
+ Material 3 tonal palettes generated from seed colours (CIELAB tone mapping,
+ gamut-clamped). The default seed is the GDIPS brand red; alternate seeds are
+ selected with and the runtime generator in
+ assets/m3/color.js can produce a palette from *any* user-chosen seed.
+ ========================================================================== */
+'''
+open('main/assets/m3/palettes.css', 'w').write(header + '\n' + '\n\n'.join(blocks) + '\n')
+print('wrote palettes.css', len(header + '\n'.join(blocks)))
+print('sample light primary:', roles(ROLE_MAP_LIGHT, maps_default)['primary'], roles(ROLE_MAP_LIGHT, maps_default)['on_primary'])
diff --git a/main/assets/m3/pages.css b/main/assets/m3/pages.css
new file mode 100644
index 0000000..ffad140
--- /dev/null
+++ b/main/assets/m3/pages.css
@@ -0,0 +1,464 @@
+/* ============================================================================
+ GDIPS · Material You 3 — Page level styles
+ ----------------------------------------------------------------------------
+ Layouts that are specific to individual GDIPS pages (entry bootloader,
+ download, rating standards, tools, feedback, 404, redirect). Everything here
+ is built from the shared tokens/components.
+ ========================================================================== */
+
+/* ============================================================================
+ Entry / bootloader page
+ ========================================================================== */
+
+.gd-boot-page {
+ overflow: hidden;
+ height: 100dvh;
+ overscroll-behavior: none;
+}
+
+.gd-boot {
+ position: relative;
+ display: grid;
+ place-items: center;
+ min-height: 100dvh;
+ padding-block: var(--m3-space-6);
+ background: var(--m3-surface);
+ isolation: isolate;
+}
+
+.gd-boot__glow {
+ position: absolute;
+ inset: 0;
+ overflow: hidden;
+ pointer-events: none;
+ z-index: -1;
+}
+
+.gd-boot__orb {
+ position: absolute;
+ display: block;
+ border-radius: 50%;
+ filter: blur(60px);
+ opacity: 0.55;
+ animation: gd-drift 18s var(--m3-ease-standard) infinite alternate;
+}
+
+.gd-boot__orb--primary {
+ width: clamp(240px, 45vw, 520px);
+ aspect-ratio: 1;
+ inset-block-start: -12%;
+ inset-inline-start: -8%;
+ background: var(--m3-primary-container);
+}
+
+.gd-boot__orb--tertiary {
+ width: clamp(200px, 38vw, 440px);
+ aspect-ratio: 1;
+ inset-block-end: -14%;
+ inset-inline-end: -6%;
+ background: var(--m3-tertiary-container);
+ animation-delay: -6s;
+}
+
+.gd-boot__grid {
+ position: absolute;
+ inset: 0;
+ background-image:
+ linear-gradient(color-mix(in srgb, var(--m3-outline-variant) 45%, transparent) 1px, transparent 1px),
+ linear-gradient(90deg, color-mix(in srgb, var(--m3-outline-variant) 45%, transparent) 1px, transparent 1px);
+ background-size: 56px 56px;
+ mask-image: radial-gradient(circle at 50% 45%, #000 0%, transparent 78%);
+ -webkit-mask-image: radial-gradient(circle at 50% 45%, #000 0%, transparent 78%);
+ opacity: 0.6;
+}
+
+@keyframes gd-drift {
+ from { transform: translate3d(0, 0, 0) scale(1); }
+ to { transform: translate3d(4%, 6%, 0) scale(1.12); }
+}
+
+.gd-boot__inner {
+ display: flex;
+ flex-direction: column;
+ gap: var(--m3-space-5);
+ width: 100%;
+ max-width: 640px;
+}
+
+.gd-boot__brand {
+ margin: 0;
+ font-size: clamp(1.5rem, 4vw, 2rem);
+}
+
+.gd-boot__brand-sub {
+ display: block;
+ font-size: var(--m3-label-medium-size);
+ font-weight: 600;
+ letter-spacing: var(--m3-tracking-label);
+ text-transform: uppercase;
+ color: var(--m3-on-surface-variant);
+}
+
+.gd-boot__card {
+ padding: clamp(20px, 4vw, 32px);
+ background: color-mix(in srgb, var(--m3-surface-container-low) 88%, transparent);
+ backdrop-filter: blur(14px);
+ border-radius: var(--m3-shape-xl);
+}
+
+.gd-boot__card-head {
+ display: flex;
+ flex-direction: column;
+ gap: var(--m3-space-2);
+ margin-block-end: var(--m3-space-5);
+}
+
+.gd-boot__menu {
+ display: flex;
+ flex-direction: column;
+ gap: var(--m3-space-2);
+ transition: opacity var(--m3-duration-medium) var(--m3-ease-standard), transform var(--m3-duration-medium) var(--m3-ease-emphasized);
+}
+
+.gd-boot__menu.is-hidden {
+ opacity: 0;
+ transform: scale(0.98);
+ pointer-events: none;
+}
+
+.gd-boot__item {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: var(--m3-space-3);
+ padding: var(--m3-space-3) var(--m3-space-4);
+ border: 1px solid transparent;
+ border-radius: var(--m3-shape-lg);
+ color: var(--m3-on-surface);
+ text-decoration: none;
+ isolation: isolate;
+ overflow: hidden;
+ transition:
+ background-color var(--m3-duration-short-2) var(--m3-ease-standard),
+ border-color var(--m3-duration-short-2) var(--m3-ease-standard),
+ transform var(--m3-duration-short-2) var(--m3-ease-emphasized);
+}
+
+.gd-boot__item:hover {
+ background: var(--m3-surface-container-high);
+ text-decoration: none;
+ transform: translateX(4px);
+}
+
+.gd-boot__item.is-active {
+ background: var(--m3-secondary-container);
+ color: var(--m3-on-secondary-container);
+ border-color: color-mix(in srgb, var(--m3-primary) 45%, transparent);
+}
+
+.gd-boot__indicator {
+ display: grid;
+ place-items: center;
+ width: 24px;
+ color: var(--m3-primary);
+ opacity: 0;
+ transform: translateX(-6px);
+ transition: opacity var(--m3-duration-short-2) var(--m3-ease-standard), transform var(--m3-duration-short-2) var(--m3-ease-emphasized);
+}
+
+.gd-boot__item.is-active .gd-boot__indicator { opacity: 1; transform: none; }
+
+.gd-boot__item-icon {
+ display: grid;
+ place-items: center;
+ width: 44px;
+ height: 44px;
+ flex: 0 0 auto;
+ border-radius: var(--m3-shape-full);
+ background: var(--m3-surface-container-highest);
+ color: var(--m3-on-surface-variant);
+ font-size: 1.1rem;
+}
+
+.gd-boot__item.is-active .gd-boot__item-icon {
+ background: var(--m3-primary);
+ color: var(--m3-on-primary);
+}
+
+.gd-boot__item-text { display: flex; flex-direction: column; min-width: 0; }
+.gd-boot__item-title { font-size: var(--m3-title-medium-size); font-weight: 600; }
+.gd-boot__item-sub { color: inherit; opacity: 0.75; font-size: var(--m3-body-small-size); }
+
+.gd-boot__item-key {
+ margin-inline-start: auto;
+ display: grid;
+ place-items: center;
+ width: 28px;
+ height: 28px;
+ border-radius: var(--m3-shape-sm);
+ background: color-mix(in srgb, currentColor 10%, transparent);
+ font-size: var(--m3-label-medium-size);
+}
+
+.gd-boot__foot {
+ display: flex;
+ align-items: center;
+ gap: var(--m3-space-4);
+ flex-wrap: wrap;
+ margin-block-start: var(--m3-space-5);
+ padding-block-start: var(--m3-space-4);
+ border-block-start: 1px solid var(--m3-outline-variant);
+}
+
+.gd-boot__theme { margin-inline-start: auto; }
+
+.gd-boot__output {
+ position: fixed;
+ inset: 0;
+ display: grid;
+ place-items: center;
+ background: var(--m3-surface);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity var(--m3-duration-medium) var(--m3-ease-standard);
+ z-index: 5;
+}
+
+.gd-boot__output.is-visible { opacity: 1; }
+
+.gd-boot__output-card {
+ display: flex;
+ flex-direction: column;
+ gap: var(--m3-space-4);
+ width: min(420px, 86vw);
+ padding: var(--m3-space-6);
+ border-radius: var(--m3-shape-xl);
+ background: var(--m3-surface-container);
+ box-shadow: var(--m3-elevation-2);
+}
+
+.gd-boot__output-text {
+ font-size: clamp(0.95rem, 2.4vw, 1.15rem);
+ color: var(--m3-primary);
+ min-height: 1.6em;
+}
+
+.gd-boot__output-text::after {
+ content: '';
+ display: inline-block;
+ width: 9px;
+ height: 1.15em;
+ margin-inline-start: 6px;
+ vertical-align: -0.15em;
+ background: currentColor;
+ animation: gd-blink 1s step-end infinite;
+}
+
+.gd-boot__output-text.is-loading::after { animation: gd-dots 1.4s steps(4, end) infinite content; }
+.gd-boot__output-text.is-loading::before { content: ''; }
+
+@keyframes gd-blink {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0; }
+}
+
+/* ============================================================================
+ Shared content page pieces
+ ========================================================================== */
+
+.gd-page-head {
+ display: flex;
+ flex-direction: column;
+ gap: var(--m3-space-3);
+ padding-block: clamp(24px, 5vw, 48px) var(--m3-space-6);
+}
+
+.gd-page-head__title { font-size: clamp(1.75rem, 4.2vw, 2.75rem); font-weight: 600; }
+.gd-page-head__lead { color: var(--m3-on-surface-variant); max-width: 68ch; }
+
+.gd-card-icon {
+ display: grid;
+ place-items: center;
+ width: 48px;
+ height: 48px;
+ border-radius: var(--m3-shape-md);
+ background: var(--m3-primary-container);
+ color: var(--m3-on-primary-container);
+ font-size: 1.2rem;
+}
+
+.gd-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--m3-space-4);
+ color: var(--m3-on-surface-variant);
+ font-size: var(--m3-body-small-size);
+}
+
+.gd-meta span { display: inline-flex; align-items: center; gap: 6px; }
+
+/* Download cards ------------------------------------------------------------ */
+
+.gd-download {
+ display: flex;
+ flex-direction: column;
+ gap: var(--m3-space-3);
+ padding: var(--m3-space-5);
+ height: 100%;
+}
+
+.gd-download__head { display: flex; align-items: center; gap: var(--m3-space-3); }
+.gd-download__title { font-size: var(--m3-title-large-size); font-weight: 600; }
+.gd-download__size { color: var(--m3-on-surface-variant); font-size: var(--m3-body-small-size); }
+.gd-download__actions { display: flex; gap: var(--m3-space-2); margin-block-start: auto; padding-block-start: var(--m3-space-3); flex-wrap: wrap; }
+
+/* Rating standards ---------------------------------------------------------- */
+
+.gd-rating-card { padding: var(--m3-space-5); display: flex; flex-direction: column; gap: var(--m3-space-4); height: 100%; }
+.gd-rating-card__tier { display: flex; align-items: center; justify-content: space-between; gap: var(--m3-space-3); }
+.gd-rating-card__name { font-size: var(--m3-title-large-size); font-weight: 600; }
+.gd-rating-card__meters { display: flex; flex-direction: column; gap: var(--m3-space-3); }
+
+/* Tools grid ---------------------------------------------------------------- */
+
+.gd-tool {
+ display: flex;
+ flex-direction: column;
+ gap: var(--m3-space-3);
+ padding: var(--m3-space-5);
+ height: 100%;
+}
+
+.gd-tool__head { display: flex; align-items: center; gap: var(--m3-space-3); }
+.gd-tool__title { font-size: var(--m3-title-medium-size); font-weight: 600; }
+.gd-tool__actions { margin-block-start: auto; padding-block-start: var(--m3-space-2); }
+
+/* Error page ---------------------------------------------------------------- */
+
+.gd-error {
+ display: grid;
+ place-items: center;
+ min-height: 100dvh;
+ padding: var(--m3-space-6);
+ text-align: center;
+ background:
+ radial-gradient(circle at 50% 0%, color-mix(in srgb, var(--m3-error-container) 55%, transparent), transparent 60%),
+ var(--m3-surface);
+}
+
+.gd-error__inner { max-width: 620px; display: flex; flex-direction: column; align-items: center; gap: var(--m3-space-4); }
+.gd-error__code {
+ font-family: var(--m3-font-display);
+ font-size: clamp(4rem, 18vw, 9rem);
+ line-height: 1;
+ color: var(--m3-error);
+ animation: gd-bob 3s var(--m3-ease-standard) infinite;
+}
+.gd-error__actions { display: flex; flex-wrap: wrap; gap: var(--m3-space-3); justify-content: center; margin-block-start: var(--m3-space-2); }
+
+@keyframes gd-bob {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-10px); }
+}
+
+/* Redirect / interstitial page --------------------------------------------- */
+
+.gd-redirect {
+ display: grid;
+ place-items: center;
+ min-height: 100dvh;
+ padding: var(--m3-space-6);
+ text-align: center;
+ background: var(--m3-surface);
+}
+
+.gd-redirect__card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--m3-space-4);
+ width: min(460px, 100%);
+ padding: clamp(24px, 5vw, 40px);
+ border-radius: var(--m3-shape-xl);
+ background: var(--m3-surface-container-low);
+ box-shadow: var(--m3-elevation-2);
+}
+
+.gd-redirect__logo {
+ width: 96px;
+ height: 96px;
+ border-radius: var(--m3-shape-lg);
+ object-fit: cover;
+ animation: gd-bob 2.6s var(--m3-ease-standard) infinite;
+}
+
+.gd-redirect__count {
+ font-family: var(--m3-font-mono);
+ font-size: 2.5rem;
+ font-weight: 600;
+ color: var(--m3-primary);
+ font-variant-numeric: tabular-nums;
+}
+
+.gd-redirect__ring {
+ --m3-meter-value: 0%;
+ width: 100%;
+}
+
+/* ============================================================================
+ Feedback pages
+ ========================================================================== */
+
+.gd-feedback-grid {
+ display: grid;
+ gap: var(--m3-space-5);
+ align-items: start;
+}
+
+@media (min-width: 1024px) {
+ .gd-feedback-grid { grid-template-columns: minmax(0, 1fr) 340px; }
+}
+
+.gd-feedback-table td:nth-child(1) { white-space: nowrap; color: var(--m3-on-surface-variant); font-size: var(--m3-body-small-size); }
+.gd-feedback-table .gd-feedback-text { max-width: 46ch; }
+.gd-feedback-actions { display: flex; gap: var(--m3-space-2); flex-wrap: wrap; }
+
+.gd-faq-list { display: flex; flex-direction: column; gap: var(--m3-space-3); }
+
+.gd-tabs-wrap {
+ position: sticky;
+ inset-block-start: var(--m3-appbar-height);
+ z-index: 2;
+ background: var(--m3-surface);
+ padding-block: var(--m3-space-2);
+}
+
+.gd-legal section + section { margin-block-start: var(--m3-space-8); }
+
+/* Contact page -------------------------------------------------------------- */
+
+.gd-contact-list { display: grid; gap: var(--m3-space-4); }
+@media (min-width: 720px) { .gd-contact-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
+
+.gd-contact-card {
+ display: flex;
+ gap: var(--m3-space-4);
+ padding: var(--m3-space-5);
+ border-radius: var(--m3-shape-lg);
+ background: var(--m3-surface-container-low);
+ border: 1px solid var(--m3-outline-variant);
+ transition: transform var(--m3-duration-short-2) var(--m3-ease-emphasized), box-shadow var(--m3-duration-short-2) var(--m3-ease-standard);
+}
+
+@media (hover: hover) {
+ .gd-contact-card:hover { transform: translateY(-3px); box-shadow: var(--m3-elevation-2); }
+}
+
+.gd-forms-grid { display: grid; gap: var(--m3-space-4); }
+@media (min-width: 720px) { .gd-forms-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
+
+/* Pulse used when a prediction result updates (kept from the original design) */
+@keyframes pulse {
+ 0%, 100% { transform: scale(1); }
+ 50% { transform: scale(1.04); }
+}
diff --git a/main/assets/m3/palettes.css b/main/assets/m3/palettes.css
new file mode 100644
index 0000000..7ceff1a
--- /dev/null
+++ b/main/assets/m3/palettes.css
@@ -0,0 +1,960 @@
+/* ============================================================================
+ GDIPS · Material You 3 — Dynamic color palettes
+ ----------------------------------------------------------------------------
+ Material 3 tonal palettes generated from seed colours (CIELAB tone mapping,
+ gamut-clamped). The default seed is the GDIPS brand red; alternate seeds are
+ selected with and the runtime generator in
+ assets/m3/color.js can produce a palette from *any* user-chosen seed.
+ ========================================================================== */
+
+/* ── Default dynamic palette: GDIPS red ───────────────────────────── */
+
+:root {
+ --m3-primary: #C00029;
+ --m3-on-primary: #FFFFFF;
+ --m3-primary-container: #FFA9A2;
+ --m3-on-primary-container: #640000;
+ --m3-primary-fixed: #FFA9A2;
+ --m3-primary-fixed-dim: #FF8C88;
+ --m3-on-primary-fixed: #640000;
+ --m3-on-primary-fixed-variant: #A00014;
+ --m3-secondary: #864F4C;
+ --m3-on-secondary: #FFFFFF;
+ --m3-secondary-container: #FFD3CC;
+ --m3-on-secondary-container: #390B08;
+ --m3-tertiary: #745C00;
+ --m3-on-tertiary: #FFFFFF;
+ --m3-tertiary-container: #FFDE79;
+ --m3-on-tertiary-container: #321800;
+ --m3-error: #B4271F;
+ --m3-on-error: #FFFFFF;
+ --m3-error-container: #FFB598;
+ --m3-on-error-container: #5B0000;
+ --m3-background: #FFF7F6;
+ --m3-on-background: #201A1A;
+ --m3-surface: #FFF7F6;
+ --m3-on-surface: #201A1A;
+ --m3-surface-variant: #F5DDDB;
+ --m3-on-surface-variant: #554240;
+ --m3-surface-dim: #E1D8D7;
+ --m3-surface-bright: #FFF7F6;
+ --m3-surface-container-lowest: #FFFFFF;
+ --m3-surface-container-low: #FAF2F1;
+ --m3-surface-container: #F5ECEB;
+ --m3-surface-container-high: #EFE6E5;
+ --m3-surface-container-highest: #E9E0E0;
+ --m3-outline: #877270;
+ --m3-outline-variant: #D9C1BF;
+ --m3-inverse-surface: #352F2E;
+ --m3-inverse-on-surface: #F8EFEE;
+ --m3-inverse-primary: #FF8C88;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #C00029;
+}
+
+:root[data-theme="dark"] {
+ --m3-primary: #FF8C88;
+ --m3-on-primary: #810000;
+ --m3-primary-container: #A00014;
+ --m3-on-primary-container: #FFA9A2;
+ --m3-primary-fixed: #FFA9A2;
+ --m3-primary-fixed-dim: #FF8C88;
+ --m3-on-primary-fixed: #640000;
+ --m3-on-primary-fixed-variant: #A00014;
+ --m3-secondary: #F7B7B1;
+ --m3-on-secondary: #522120;
+ --m3-secondary-container: #6B3835;
+ --m3-on-secondary-container: #FFD3CC;
+ --m3-tertiary: #E7C35F;
+ --m3-on-tertiary: #432E00;
+ --m3-tertiary-container: #5A4400;
+ --m3-on-tertiary-container: #FFDE79;
+ --m3-error: #FF987E;
+ --m3-on-error: #770000;
+ --m3-error-container: #950008;
+ --m3-on-error-container: #FFB598;
+ --m3-background: #181211;
+ --m3-on-background: #E9E0E0;
+ --m3-surface: #181211;
+ --m3-on-surface: #E9E0E0;
+ --m3-surface-variant: #554240;
+ --m3-on-surface-variant: #D9C1BF;
+ --m3-surface-dim: #181211;
+ --m3-surface-bright: #3E3837;
+ --m3-surface-container-lowest: #140C0B;
+ --m3-surface-container-low: #201A1A;
+ --m3-surface-container: #241E1D;
+ --m3-surface-container-high: #2F2828;
+ --m3-surface-container-highest: #3A3332;
+ --m3-outline: #A28C89;
+ --m3-outline-variant: #554240;
+ --m3-inverse-surface: #E9E0E0;
+ --m3-inverse-on-surface: #352F2E;
+ --m3-inverse-primary: #C00029;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #FF8C88;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root:not([data-theme="light"]) {
+ --m3-primary: #FF8C88;
+ --m3-on-primary: #810000;
+ --m3-primary-container: #A00014;
+ --m3-on-primary-container: #FFA9A2;
+ --m3-primary-fixed: #FFA9A2;
+ --m3-primary-fixed-dim: #FF8C88;
+ --m3-on-primary-fixed: #640000;
+ --m3-on-primary-fixed-variant: #A00014;
+ --m3-secondary: #F7B7B1;
+ --m3-on-secondary: #522120;
+ --m3-secondary-container: #6B3835;
+ --m3-on-secondary-container: #FFD3CC;
+ --m3-tertiary: #E7C35F;
+ --m3-on-tertiary: #432E00;
+ --m3-tertiary-container: #5A4400;
+ --m3-on-tertiary-container: #FFDE79;
+ --m3-error: #FF987E;
+ --m3-on-error: #770000;
+ --m3-error-container: #950008;
+ --m3-on-error-container: #FFB598;
+ --m3-background: #181211;
+ --m3-on-background: #E9E0E0;
+ --m3-surface: #181211;
+ --m3-on-surface: #E9E0E0;
+ --m3-surface-variant: #554240;
+ --m3-on-surface-variant: #D9C1BF;
+ --m3-surface-dim: #181211;
+ --m3-surface-bright: #3E3837;
+ --m3-surface-container-lowest: #140C0B;
+ --m3-surface-container-low: #201A1A;
+ --m3-surface-container: #241E1D;
+ --m3-surface-container-high: #2F2828;
+ --m3-surface-container-highest: #3A3332;
+ --m3-outline: #A28C89;
+ --m3-outline-variant: #554240;
+ --m3-inverse-surface: #E9E0E0;
+ --m3-inverse-on-surface: #352F2E;
+ --m3-inverse-primary: #C00029;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #FF8C88;
+ }
+}
+
+/* ── Seed: indigo (#6366F1) ───────────────────────────────────────── */
+
+:root[data-seed="indigo"] {
+ --m3-primary: #414ED4;
+ --m3-on-primary: #FFFFFF;
+ --m3-primary-container: #DDCFFF;
+ --m3-on-primary-container: #000E81;
+ --m3-primary-fixed: #DDCFFF;
+ --m3-primary-fixed-dim: #BFB4FF;
+ --m3-on-primary-fixed: #000E81;
+ --m3-on-primary-fixed-variant: #0D37B8;
+ --m3-secondary: #5F5985;
+ --m3-on-secondary: #FFFFFF;
+ --m3-secondary-container: #E5DCFF;
+ --m3-on-secondary-container: #18173C;
+ --m3-tertiary: #AE2562;
+ --m3-on-tertiary: #FFFFFF;
+ --m3-tertiary-container: #FFB5E7;
+ --m3-on-tertiary-container: #58001F;
+ --m3-error: #B4271F;
+ --m3-on-error: #FFFFFF;
+ --m3-error-container: #FFB598;
+ --m3-on-error-container: #5B0000;
+ --m3-background: #FAF8FF;
+ --m3-on-background: #1C1B1F;
+ --m3-surface: #FAF8FF;
+ --m3-on-surface: #1C1B1F;
+ --m3-surface-variant: #E4E0F1;
+ --m3-on-surface-variant: #484553;
+ --m3-surface-dim: #DAD9DF;
+ --m3-surface-bright: #FAF8FF;
+ --m3-surface-container-lowest: #FFFFFF;
+ --m3-surface-container-low: #F4F3F9;
+ --m3-surface-container: #EEEDF3;
+ --m3-surface-container-high: #E9E7ED;
+ --m3-surface-container-highest: #E3E2E8;
+ --m3-outline: #787584;
+ --m3-outline-variant: #C8C4D5;
+ --m3-inverse-surface: #313034;
+ --m3-inverse-on-surface: #F1F0F6;
+ --m3-inverse-primary: #BFB4FF;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #414ED4;
+}
+
+:root[data-seed="indigo"][data-theme="dark"] {
+ --m3-primary: #BFB4FF;
+ --m3-on-primary: #00219C;
+ --m3-primary-container: #0D37B8;
+ --m3-on-primary-container: #DDCFFF;
+ --m3-primary-fixed: #DDCFFF;
+ --m3-primary-fixed-dim: #BFB4FF;
+ --m3-on-primary-fixed: #000E81;
+ --m3-on-primary-fixed-variant: #0D37B8;
+ --m3-secondary: #C9C0F3;
+ --m3-on-secondary: #2F2B53;
+ --m3-secondary-container: #47416C;
+ --m3-on-secondary-container: #E5DCFF;
+ --m3-tertiary: #FF98CB;
+ --m3-on-tertiary: #740034;
+ --m3-tertiary-container: #91004B;
+ --m3-on-tertiary-container: #FFB5E7;
+ --m3-error: #FF987E;
+ --m3-on-error: #770000;
+ --m3-error-container: #950008;
+ --m3-on-error-container: #FFB598;
+ --m3-background: #141317;
+ --m3-on-background: #E3E2E8;
+ --m3-surface: #141317;
+ --m3-on-surface: #E3E2E8;
+ --m3-surface-variant: #484553;
+ --m3-on-surface-variant: #C8C4D5;
+ --m3-surface-dim: #141317;
+ --m3-surface-bright: #39383D;
+ --m3-surface-container-lowest: #0F0D13;
+ --m3-surface-container-low: #1C1B1F;
+ --m3-surface-container: #201F23;
+ --m3-surface-container-high: #2A292E;
+ --m3-surface-container-highest: #353439;
+ --m3-outline: #928F9E;
+ --m3-outline-variant: #484553;
+ --m3-inverse-surface: #E3E2E8;
+ --m3-inverse-on-surface: #313034;
+ --m3-inverse-primary: #414ED4;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #BFB4FF;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root[data-seed="indigo"]:not([data-theme="light"]) {
+ --m3-primary: #BFB4FF;
+ --m3-on-primary: #00219C;
+ --m3-primary-container: #0D37B8;
+ --m3-on-primary-container: #DDCFFF;
+ --m3-primary-fixed: #DDCFFF;
+ --m3-primary-fixed-dim: #BFB4FF;
+ --m3-on-primary-fixed: #000E81;
+ --m3-on-primary-fixed-variant: #0D37B8;
+ --m3-secondary: #C9C0F3;
+ --m3-on-secondary: #2F2B53;
+ --m3-secondary-container: #47416C;
+ --m3-on-secondary-container: #E5DCFF;
+ --m3-tertiary: #FF98CB;
+ --m3-on-tertiary: #740034;
+ --m3-tertiary-container: #91004B;
+ --m3-on-tertiary-container: #FFB5E7;
+ --m3-error: #FF987E;
+ --m3-on-error: #770000;
+ --m3-error-container: #950008;
+ --m3-on-error-container: #FFB598;
+ --m3-background: #141317;
+ --m3-on-background: #E3E2E8;
+ --m3-surface: #141317;
+ --m3-on-surface: #E3E2E8;
+ --m3-surface-variant: #484553;
+ --m3-on-surface-variant: #C8C4D5;
+ --m3-surface-dim: #141317;
+ --m3-surface-bright: #39383D;
+ --m3-surface-container-lowest: #0F0D13;
+ --m3-surface-container-low: #1C1B1F;
+ --m3-surface-container: #201F23;
+ --m3-surface-container-high: #2A292E;
+ --m3-surface-container-highest: #353439;
+ --m3-outline: #928F9E;
+ --m3-outline-variant: #484553;
+ --m3-inverse-surface: #E3E2E8;
+ --m3-inverse-on-surface: #313034;
+ --m3-inverse-primary: #414ED4;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #BFB4FF;
+ }
+}
+
+/* ── Seed: violet (#8B5CF6) ───────────────────────────────────────── */
+
+:root[data-seed="violet"] {
+ --m3-primary: #683FD4;
+ --m3-on-primary: #FFFFFF;
+ --m3-primary-container: #FEC3FF;
+ --m3-on-primary-container: #000082;
+ --m3-primary-fixed: #FEC3FF;
+ --m3-primary-fixed-dim: #E0A7FF;
+ --m3-on-primary-fixed: #000082;
+ --m3-on-primary-fixed-variant: #4925B8;
+ --m3-secondary: #685686;
+ --m3-on-secondary: #FFFFFF;
+ --m3-secondary-container: #F0D9FF;
+ --m3-on-secondary-container: #21133C;
+ --m3-tertiary: #B71455;
+ --m3-on-tertiary: #FFFFFF;
+ --m3-tertiary-container: #FFAED6;
+ --m3-on-tertiary-container: #5E0014;
+ --m3-error: #B4271F;
+ --m3-on-error: #FFFFFF;
+ --m3-error-container: #FFB598;
+ --m3-on-error-container: #5B0000;
+ --m3-background: #FBF8FE;
+ --m3-on-background: #1D1B1F;
+ --m3-surface: #FBF8FE;
+ --m3-on-surface: #1D1B1F;
+ --m3-surface-variant: #E7E0F0;
+ --m3-on-surface-variant: #4A4452;
+ --m3-surface-dim: #DBD9DF;
+ --m3-surface-bright: #FBF8FE;
+ --m3-surface-container-lowest: #FFFFFF;
+ --m3-surface-container-low: #F5F2F8;
+ --m3-surface-container: #EFEDF3;
+ --m3-surface-container-high: #EAE7ED;
+ --m3-surface-container-highest: #E4E1E7;
+ --m3-outline: #7B7483;
+ --m3-outline-variant: #CBC4D4;
+ --m3-inverse-surface: #313034;
+ --m3-inverse-on-surface: #F2F0F6;
+ --m3-inverse-primary: #E0A7FF;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #683FD4;
+}
+
+:root[data-seed="violet"][data-theme="dark"] {
+ --m3-primary: #E0A7FF;
+ --m3-on-primary: #25099C;
+ --m3-primary-container: #4925B8;
+ --m3-on-primary-container: #FEC3FF;
+ --m3-primary-fixed: #FEC3FF;
+ --m3-primary-fixed-dim: #E0A7FF;
+ --m3-on-primary-fixed: #000082;
+ --m3-on-primary-fixed-variant: #4925B8;
+ --m3-secondary: #D3BDF3;
+ --m3-on-secondary: #372854;
+ --m3-secondary-container: #4F3E6C;
+ --m3-on-secondary-container: #F0D9FF;
+ --m3-tertiary: #FF92BB;
+ --m3-on-tertiary: #7B0028;
+ --m3-tertiary-container: #99003E;
+ --m3-on-tertiary-container: #FFAED6;
+ --m3-error: #FF987E;
+ --m3-on-error: #770000;
+ --m3-error-container: #950008;
+ --m3-on-error-container: #FFB598;
+ --m3-background: #151217;
+ --m3-on-background: #E4E1E7;
+ --m3-surface: #151217;
+ --m3-on-surface: #E4E1E7;
+ --m3-surface-variant: #4A4452;
+ --m3-on-surface-variant: #CBC4D4;
+ --m3-surface-dim: #151217;
+ --m3-surface-bright: #3A383D;
+ --m3-surface-container-lowest: #100D13;
+ --m3-surface-container-low: #1D1B1F;
+ --m3-surface-container: #211F23;
+ --m3-surface-container-high: #2B292E;
+ --m3-surface-container-highest: #363438;
+ --m3-outline: #948E9D;
+ --m3-outline-variant: #4A4452;
+ --m3-inverse-surface: #E4E1E7;
+ --m3-inverse-on-surface: #313034;
+ --m3-inverse-primary: #683FD4;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #E0A7FF;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root[data-seed="violet"]:not([data-theme="light"]) {
+ --m3-primary: #E0A7FF;
+ --m3-on-primary: #25099C;
+ --m3-primary-container: #4925B8;
+ --m3-on-primary-container: #FEC3FF;
+ --m3-primary-fixed: #FEC3FF;
+ --m3-primary-fixed-dim: #E0A7FF;
+ --m3-on-primary-fixed: #000082;
+ --m3-on-primary-fixed-variant: #4925B8;
+ --m3-secondary: #D3BDF3;
+ --m3-on-secondary: #372854;
+ --m3-secondary-container: #4F3E6C;
+ --m3-on-secondary-container: #F0D9FF;
+ --m3-tertiary: #FF92BB;
+ --m3-on-tertiary: #7B0028;
+ --m3-tertiary-container: #99003E;
+ --m3-on-tertiary-container: #FFAED6;
+ --m3-error: #FF987E;
+ --m3-on-error: #770000;
+ --m3-error-container: #950008;
+ --m3-on-error-container: #FFB598;
+ --m3-background: #151217;
+ --m3-on-background: #E4E1E7;
+ --m3-surface: #151217;
+ --m3-on-surface: #E4E1E7;
+ --m3-surface-variant: #4A4452;
+ --m3-on-surface-variant: #CBC4D4;
+ --m3-surface-dim: #151217;
+ --m3-surface-bright: #3A383D;
+ --m3-surface-container-lowest: #100D13;
+ --m3-surface-container-low: #1D1B1F;
+ --m3-surface-container: #211F23;
+ --m3-surface-container-high: #2B292E;
+ --m3-surface-container-highest: #363438;
+ --m3-outline: #948E9D;
+ --m3-outline-variant: #4A4452;
+ --m3-inverse-surface: #E4E1E7;
+ --m3-inverse-on-surface: #313034;
+ --m3-inverse-primary: #683FD4;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #E0A7FF;
+ }
+}
+
+/* ── Seed: crimson (#DC2626) ───────────────────────────────────────── */
+
+:root[data-seed="crimson"] {
+ --m3-primary: #C20014;
+ --m3-on-primary: #FFFFFF;
+ --m3-primary-container: #FFA78D;
+ --m3-on-primary-container: #670000;
+ --m3-primary-fixed: #FFA78D;
+ --m3-primary-fixed-dim: #FF8A73;
+ --m3-on-primary-fixed: #670000;
+ --m3-on-primary-fixed-variant: #A20000;
+ --m3-secondary: #884F45;
+ --m3-on-secondary: #FFFFFF;
+ --m3-secondary-container: #FFD2C5;
+ --m3-on-secondary-container: #3A0A00;
+ --m3-tertiary: #6B5F00;
+ --m3-on-tertiary: #FFFFFF;
+ --m3-tertiary-container: #FBE26F;
+ --m3-on-tertiary-container: #2E1B00;
+ --m3-error: #B4271F;
+ --m3-on-error: #FFFFFF;
+ --m3-error-container: #FFB598;
+ --m3-on-error-container: #5B0000;
+ --m3-background: #FFF7F6;
+ --m3-on-background: #201A19;
+ --m3-surface: #FFF7F6;
+ --m3-on-surface: #201A19;
+ --m3-surface-variant: #F5DDD9;
+ --m3-on-surface-variant: #55423F;
+ --m3-surface-dim: #E1D8D6;
+ --m3-surface-bright: #FFF7F6;
+ --m3-surface-container-lowest: #FFFFFF;
+ --m3-surface-container-low: #FAF2F0;
+ --m3-surface-container: #F5ECEA;
+ --m3-surface-container-high: #EFE6E5;
+ --m3-surface-container-highest: #E9E1DF;
+ --m3-outline: #87726E;
+ --m3-outline-variant: #D8C1BD;
+ --m3-inverse-surface: #352F2E;
+ --m3-inverse-on-surface: #F7EFED;
+ --m3-inverse-primary: #FF8A73;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #C20014;
+}
+
+:root[data-seed="crimson"][data-theme="dark"] {
+ --m3-primary: #FF8A73;
+ --m3-on-primary: #830000;
+ --m3-primary-container: #A20000;
+ --m3-on-primary-container: #FFA78D;
+ --m3-primary-fixed: #FFA78D;
+ --m3-primary-fixed-dim: #FF8A73;
+ --m3-on-primary-fixed: #670000;
+ --m3-on-primary-fixed-variant: #A20000;
+ --m3-secondary: #FAB6AA;
+ --m3-on-secondary: #53211A;
+ --m3-secondary-container: #6D372F;
+ --m3-on-secondary-container: #FFD2C5;
+ --m3-tertiary: #DEC654;
+ --m3-on-tertiary: #3C3100;
+ --m3-tertiary-container: #524700;
+ --m3-on-tertiary-container: #FBE26F;
+ --m3-error: #FF987E;
+ --m3-on-error: #770000;
+ --m3-error-container: #950008;
+ --m3-on-error-container: #FFB598;
+ --m3-background: #181210;
+ --m3-on-background: #E9E1DF;
+ --m3-surface: #181210;
+ --m3-on-surface: #E9E1DF;
+ --m3-surface-variant: #55423F;
+ --m3-on-surface-variant: #D8C1BD;
+ --m3-surface-dim: #181210;
+ --m3-surface-bright: #3E3836;
+ --m3-surface-container-lowest: #140C0A;
+ --m3-surface-container-low: #201A19;
+ --m3-surface-container: #241E1D;
+ --m3-surface-container-high: #2F2927;
+ --m3-surface-container-highest: #3A3332;
+ --m3-outline: #A18C88;
+ --m3-outline-variant: #55423F;
+ --m3-inverse-surface: #E9E1DF;
+ --m3-inverse-on-surface: #352F2E;
+ --m3-inverse-primary: #C20014;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #FF8A73;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root[data-seed="crimson"]:not([data-theme="light"]) {
+ --m3-primary: #FF8A73;
+ --m3-on-primary: #830000;
+ --m3-primary-container: #A20000;
+ --m3-on-primary-container: #FFA78D;
+ --m3-primary-fixed: #FFA78D;
+ --m3-primary-fixed-dim: #FF8A73;
+ --m3-on-primary-fixed: #670000;
+ --m3-on-primary-fixed-variant: #A20000;
+ --m3-secondary: #FAB6AA;
+ --m3-on-secondary: #53211A;
+ --m3-secondary-container: #6D372F;
+ --m3-on-secondary-container: #FFD2C5;
+ --m3-tertiary: #DEC654;
+ --m3-on-tertiary: #3C3100;
+ --m3-tertiary-container: #524700;
+ --m3-on-tertiary-container: #FBE26F;
+ --m3-error: #FF987E;
+ --m3-on-error: #770000;
+ --m3-error-container: #950008;
+ --m3-on-error-container: #FFB598;
+ --m3-background: #181210;
+ --m3-on-background: #E9E1DF;
+ --m3-surface: #181210;
+ --m3-on-surface: #E9E1DF;
+ --m3-surface-variant: #55423F;
+ --m3-on-surface-variant: #D8C1BD;
+ --m3-surface-dim: #181210;
+ --m3-surface-bright: #3E3836;
+ --m3-surface-container-lowest: #140C0A;
+ --m3-surface-container-low: #201A19;
+ --m3-surface-container: #241E1D;
+ --m3-surface-container-high: #2F2927;
+ --m3-surface-container-highest: #3A3332;
+ --m3-outline: #A18C88;
+ --m3-outline-variant: #55423F;
+ --m3-inverse-surface: #E9E1DF;
+ --m3-inverse-on-surface: #352F2E;
+ --m3-inverse-primary: #C20014;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #FF8A73;
+ }
+}
+
+/* ── Seed: emerald (#0F9D58) ───────────────────────────────────────── */
+
+:root[data-seed="emerald"] {
+ --m3-primary: #007030;
+ --m3-on-primary: #FFFFFF;
+ --m3-primary-container: #82FAAD;
+ --m3-on-primary-container: #002900;
+ --m3-primary-fixed: #82FAAD;
+ --m3-primary-fixed-dim: #64DD93;
+ --m3-on-primary-fixed: #002900;
+ --m3-on-primary-fixed-variant: #005619;
+ --m3-secondary: #45664F;
+ --m3-on-secondary: #FFFFFF;
+ --m3-secondary-container: #C6EBD1;
+ --m3-on-secondary-container: #01210D;
+ --m3-tertiary: #006E81;
+ --m3-on-tertiary: #FFFFFF;
+ --m3-tertiary-container: #59F6FF;
+ --m3-on-tertiary-container: #002839;
+ --m3-error: #B4271F;
+ --m3-on-error: #FFFFFF;
+ --m3-error-container: #FFB598;
+ --m3-on-error-container: #5B0000;
+ --m3-background: #F5FBF6;
+ --m3-on-background: #181D19;
+ --m3-surface: #F5FBF6;
+ --m3-on-surface: #181D19;
+ --m3-surface-variant: #D6E7DA;
+ --m3-on-surface-variant: #3C4A40;
+ --m3-surface-dim: #D5DBD7;
+ --m3-surface-bright: #F5FBF6;
+ --m3-surface-container-lowest: #FFFFFF;
+ --m3-surface-container-low: #EFF5F0;
+ --m3-surface-container: #E9EFEB;
+ --m3-surface-container-high: #E4EAE5;
+ --m3-surface-container-highest: #DEE4DF;
+ --m3-outline: #6C7B70;
+ --m3-outline-variant: #BACBBE;
+ --m3-inverse-surface: #2D312E;
+ --m3-inverse-on-surface: #ECF2EE;
+ --m3-inverse-primary: #64DD93;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #007030;
+}
+
+:root[data-seed="emerald"][data-theme="dark"] {
+ --m3-primary: #64DD93;
+ --m3-on-primary: #003E01;
+ --m3-primary-container: #005619;
+ --m3-on-primary-container: #82FAAD;
+ --m3-primary-fixed: #82FAAD;
+ --m3-primary-fixed-dim: #64DD93;
+ --m3-on-primary-fixed: #002900;
+ --m3-on-primary-fixed-variant: #005619;
+ --m3-secondary: #ABCFB5;
+ --m3-on-secondary: #173723;
+ --m3-secondary-container: #2E4D38;
+ --m3-on-secondary-container: #C6EBD1;
+ --m3-tertiary: #2ADAEE;
+ --m3-on-tertiary: #003D50;
+ --m3-tertiary-container: #005568;
+ --m3-on-tertiary-container: #59F6FF;
+ --m3-error: #FF987E;
+ --m3-on-error: #770000;
+ --m3-error-container: #950008;
+ --m3-on-error-container: #FFB598;
+ --m3-background: #0F1511;
+ --m3-on-background: #DEE4DF;
+ --m3-surface: #0F1511;
+ --m3-on-surface: #DEE4DF;
+ --m3-surface-variant: #3C4A40;
+ --m3-on-surface-variant: #BACBBE;
+ --m3-surface-dim: #0F1511;
+ --m3-surface-bright: #363A37;
+ --m3-surface-container-lowest: #09100B;
+ --m3-surface-container-low: #181D19;
+ --m3-surface-container: #1C211D;
+ --m3-surface-container-high: #262B28;
+ --m3-surface-container-highest: #313632;
+ --m3-outline: #859589;
+ --m3-outline-variant: #3C4A40;
+ --m3-inverse-surface: #DEE4DF;
+ --m3-inverse-on-surface: #2D312E;
+ --m3-inverse-primary: #007030;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #64DD93;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root[data-seed="emerald"]:not([data-theme="light"]) {
+ --m3-primary: #64DD93;
+ --m3-on-primary: #003E01;
+ --m3-primary-container: #005619;
+ --m3-on-primary-container: #82FAAD;
+ --m3-primary-fixed: #82FAAD;
+ --m3-primary-fixed-dim: #64DD93;
+ --m3-on-primary-fixed: #002900;
+ --m3-on-primary-fixed-variant: #005619;
+ --m3-secondary: #ABCFB5;
+ --m3-on-secondary: #173723;
+ --m3-secondary-container: #2E4D38;
+ --m3-on-secondary-container: #C6EBD1;
+ --m3-tertiary: #2ADAEE;
+ --m3-on-tertiary: #003D50;
+ --m3-tertiary-container: #005568;
+ --m3-on-tertiary-container: #59F6FF;
+ --m3-error: #FF987E;
+ --m3-on-error: #770000;
+ --m3-error-container: #950008;
+ --m3-on-error-container: #FFB598;
+ --m3-background: #0F1511;
+ --m3-on-background: #DEE4DF;
+ --m3-surface: #0F1511;
+ --m3-on-surface: #DEE4DF;
+ --m3-surface-variant: #3C4A40;
+ --m3-on-surface-variant: #BACBBE;
+ --m3-surface-dim: #0F1511;
+ --m3-surface-bright: #363A37;
+ --m3-surface-container-lowest: #09100B;
+ --m3-surface-container-low: #181D19;
+ --m3-surface-container: #1C211D;
+ --m3-surface-container-high: #262B28;
+ --m3-surface-container-highest: #313632;
+ --m3-outline: #859589;
+ --m3-outline-variant: #3C4A40;
+ --m3-inverse-surface: #DEE4DF;
+ --m3-inverse-on-surface: #2D312E;
+ --m3-inverse-primary: #007030;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #64DD93;
+ }
+}
+
+/* ── Seed: amber (#F59E0B) ───────────────────────────────────────── */
+
+:root[data-seed="amber"] {
+ --m3-primary: #924D00;
+ --m3-on-primary: #FFFFFF;
+ --m3-primary-container: #FFCF4B;
+ --m3-on-primary-container: #480400;
+ --m3-primary-fixed: #FFCF4B;
+ --m3-primary-fixed-dim: #FFB32C;
+ --m3-on-primary-fixed: #480400;
+ --m3-on-primary-fixed-variant: #763500;
+ --m3-secondary: #775935;
+ --m3-on-secondary: #FFFFFF;
+ --m3-secondary-container: #FFDCB2;
+ --m3-on-secondary-container: #2F1600;
+ --m3-tertiary: #296C0E;
+ --m3-on-tertiary: #FFFFFF;
+ --m3-tertiary-container: #B1F490;
+ --m3-on-tertiary-container: #002600;
+ --m3-error: #B4271F;
+ --m3-on-error: #FFFFFF;
+ --m3-error-container: #FFB598;
+ --m3-on-error-container: #5B0000;
+ --m3-background: #FEF8F3;
+ --m3-on-background: #1E1B17;
+ --m3-surface: #FEF8F3;
+ --m3-on-surface: #1E1B17;
+ --m3-surface-variant: #EFE0D2;
+ --m3-on-surface-variant: #504539;
+ --m3-surface-dim: #DED9D4;
+ --m3-surface-bright: #FEF8F3;
+ --m3-surface-container-lowest: #FFFFFF;
+ --m3-surface-container-low: #F8F3EE;
+ --m3-surface-container: #F2EDE8;
+ --m3-surface-container-high: #EDE7E2;
+ --m3-surface-container-highest: #E7E2DD;
+ --m3-outline: #817569;
+ --m3-outline-variant: #D2C4B7;
+ --m3-inverse-surface: #34302C;
+ --m3-inverse-on-surface: #F5F0EB;
+ --m3-inverse-primary: #FFB32C;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #924D00;
+}
+
+:root[data-seed="amber"][data-theme="dark"] {
+ --m3-primary: #FFB32C;
+ --m3-on-primary: #5D1E00;
+ --m3-primary-container: #763500;
+ --m3-on-primary-container: #FFCF4B;
+ --m3-primary-fixed: #FFCF4B;
+ --m3-primary-fixed-dim: #FFB32C;
+ --m3-on-primary-fixed: #480400;
+ --m3-on-primary-fixed-variant: #763500;
+ --m3-secondary: #E6C097;
+ --m3-on-secondary: #442B08;
+ --m3-secondary-container: #5D411E;
+ --m3-on-secondary-container: #FFDCB2;
+ --m3-tertiary: #96D876;
+ --m3-on-tertiary: #003B00;
+ --m3-tertiary-container: #045300;
+ --m3-on-tertiary-container: #B1F490;
+ --m3-error: #FF987E;
+ --m3-on-error: #770000;
+ --m3-error-container: #950008;
+ --m3-on-error-container: #FFB598;
+ --m3-background: #17130E;
+ --m3-on-background: #E7E2DD;
+ --m3-surface: #17130E;
+ --m3-on-surface: #E7E2DD;
+ --m3-surface-variant: #504539;
+ --m3-on-surface-variant: #D2C4B7;
+ --m3-surface-dim: #17130E;
+ --m3-surface-bright: #3D3835;
+ --m3-surface-container-lowest: #120D07;
+ --m3-surface-container-low: #1E1B17;
+ --m3-surface-container: #231F1B;
+ --m3-surface-container-high: #2D2926;
+ --m3-surface-container-highest: #383430;
+ --m3-outline: #9C8E82;
+ --m3-outline-variant: #504539;
+ --m3-inverse-surface: #E7E2DD;
+ --m3-inverse-on-surface: #34302C;
+ --m3-inverse-primary: #924D00;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #FFB32C;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root[data-seed="amber"]:not([data-theme="light"]) {
+ --m3-primary: #FFB32C;
+ --m3-on-primary: #5D1E00;
+ --m3-primary-container: #763500;
+ --m3-on-primary-container: #FFCF4B;
+ --m3-primary-fixed: #FFCF4B;
+ --m3-primary-fixed-dim: #FFB32C;
+ --m3-on-primary-fixed: #480400;
+ --m3-on-primary-fixed-variant: #763500;
+ --m3-secondary: #E6C097;
+ --m3-on-secondary: #442B08;
+ --m3-secondary-container: #5D411E;
+ --m3-on-secondary-container: #FFDCB2;
+ --m3-tertiary: #96D876;
+ --m3-on-tertiary: #003B00;
+ --m3-tertiary-container: #045300;
+ --m3-on-tertiary-container: #B1F490;
+ --m3-error: #FF987E;
+ --m3-on-error: #770000;
+ --m3-error-container: #950008;
+ --m3-on-error-container: #FFB598;
+ --m3-background: #17130E;
+ --m3-on-background: #E7E2DD;
+ --m3-surface: #17130E;
+ --m3-on-surface: #E7E2DD;
+ --m3-surface-variant: #504539;
+ --m3-on-surface-variant: #D2C4B7;
+ --m3-surface-dim: #17130E;
+ --m3-surface-bright: #3D3835;
+ --m3-surface-container-lowest: #120D07;
+ --m3-surface-container-low: #1E1B17;
+ --m3-surface-container: #231F1B;
+ --m3-surface-container-high: #2D2926;
+ --m3-surface-container-highest: #383430;
+ --m3-outline: #9C8E82;
+ --m3-outline-variant: #504539;
+ --m3-inverse-surface: #E7E2DD;
+ --m3-inverse-on-surface: #34302C;
+ --m3-inverse-primary: #924D00;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #FFB32C;
+ }
+}
+
+/* ── Seed: azure (#2563EB) ───────────────────────────────────────── */
+
+:root[data-seed="azure"] {
+ --m3-primary: #0055D9;
+ --m3-on-primary: #FFFFFF;
+ --m3-primary-container: #C2D6FF;
+ --m3-on-primary-container: #001886;
+ --m3-primary-fixed: #C2D6FF;
+ --m3-primary-fixed-dim: #A3BAFF;
+ --m3-on-primary-fixed: #001886;
+ --m3-on-primary-fixed-variant: #003EBD;
+ --m3-secondary: #595B87;
+ --m3-on-secondary: #FFFFFF;
+ --m3-secondary-container: #DEDEFF;
+ --m3-on-secondary-container: #0F193E;
+ --m3-tertiary: #AB286C;
+ --m3-on-tertiary: #FFFFFF;
+ --m3-tertiary-container: #FFB6F2;
+ --m3-on-tertiary-container: #560027;
+ --m3-error: #B4271F;
+ --m3-on-error: #FFFFFF;
+ --m3-error-container: #FFB598;
+ --m3-on-error-container: #5B0000;
+ --m3-background: #F9F9FF;
+ --m3-on-background: #1B1B1F;
+ --m3-surface: #F9F9FF;
+ --m3-on-surface: #1B1B1F;
+ --m3-surface-variant: #E2E1F2;
+ --m3-on-surface-variant: #464553;
+ --m3-surface-dim: #DAD9DF;
+ --m3-surface-bright: #F9F9FF;
+ --m3-surface-container-lowest: #FFFFFF;
+ --m3-surface-container-low: #F3F3F9;
+ --m3-surface-container: #EEEDF3;
+ --m3-surface-container-high: #E8E7EE;
+ --m3-surface-container-highest: #E2E2E8;
+ --m3-outline: #767685;
+ --m3-outline-variant: #C6C5D6;
+ --m3-inverse-surface: #303035;
+ --m3-inverse-on-surface: #F0F0F6;
+ --m3-inverse-primary: #A3BAFF;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #0055D9;
+}
+
+:root[data-seed="azure"][data-theme="dark"] {
+ --m3-primary: #A3BAFF;
+ --m3-on-primary: #002AA1;
+ --m3-primary-container: #003EBD;
+ --m3-on-primary-container: #C2D6FF;
+ --m3-primary-fixed: #C2D6FF;
+ --m3-primary-fixed-dim: #A3BAFF;
+ --m3-on-primary-fixed: #001886;
+ --m3-on-primary-fixed-variant: #003EBD;
+ --m3-secondary: #C2C2F5;
+ --m3-on-secondary: #282D55;
+ --m3-secondary-container: #40436E;
+ --m3-on-secondary-container: #DEDEFF;
+ --m3-tertiary: #FF9AD6;
+ --m3-on-tertiary: #72003D;
+ --m3-tertiary-container: #8E0054;
+ --m3-on-tertiary-container: #FFB6F2;
+ --m3-error: #FF987E;
+ --m3-on-error: #770000;
+ --m3-error-container: #950008;
+ --m3-on-error-container: #FFB598;
+ --m3-background: #131317;
+ --m3-on-background: #E2E2E8;
+ --m3-surface: #131317;
+ --m3-on-surface: #E2E2E8;
+ --m3-surface-variant: #464553;
+ --m3-on-surface-variant: #C6C5D6;
+ --m3-surface-dim: #131317;
+ --m3-surface-bright: #39393D;
+ --m3-surface-container-lowest: #0E0E13;
+ --m3-surface-container-low: #1B1B1F;
+ --m3-surface-container: #1F1F24;
+ --m3-surface-container-high: #2A292E;
+ --m3-surface-container-highest: #343439;
+ --m3-outline: #908F9F;
+ --m3-outline-variant: #464553;
+ --m3-inverse-surface: #E2E2E8;
+ --m3-inverse-on-surface: #303035;
+ --m3-inverse-primary: #0055D9;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #A3BAFF;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root[data-seed="azure"]:not([data-theme="light"]) {
+ --m3-primary: #A3BAFF;
+ --m3-on-primary: #002AA1;
+ --m3-primary-container: #003EBD;
+ --m3-on-primary-container: #C2D6FF;
+ --m3-primary-fixed: #C2D6FF;
+ --m3-primary-fixed-dim: #A3BAFF;
+ --m3-on-primary-fixed: #001886;
+ --m3-on-primary-fixed-variant: #003EBD;
+ --m3-secondary: #C2C2F5;
+ --m3-on-secondary: #282D55;
+ --m3-secondary-container: #40436E;
+ --m3-on-secondary-container: #DEDEFF;
+ --m3-tertiary: #FF9AD6;
+ --m3-on-tertiary: #72003D;
+ --m3-tertiary-container: #8E0054;
+ --m3-on-tertiary-container: #FFB6F2;
+ --m3-error: #FF987E;
+ --m3-on-error: #770000;
+ --m3-error-container: #950008;
+ --m3-on-error-container: #FFB598;
+ --m3-background: #131317;
+ --m3-on-background: #E2E2E8;
+ --m3-surface: #131317;
+ --m3-on-surface: #E2E2E8;
+ --m3-surface-variant: #464553;
+ --m3-on-surface-variant: #C6C5D6;
+ --m3-surface-dim: #131317;
+ --m3-surface-bright: #39393D;
+ --m3-surface-container-lowest: #0E0E13;
+ --m3-surface-container-low: #1B1B1F;
+ --m3-surface-container: #1F1F24;
+ --m3-surface-container-high: #2A292E;
+ --m3-surface-container-highest: #343439;
+ --m3-outline: #908F9F;
+ --m3-outline-variant: #464553;
+ --m3-inverse-surface: #E2E2E8;
+ --m3-inverse-on-surface: #303035;
+ --m3-inverse-primary: #0055D9;
+ --m3-scrim: #000000;
+ --m3-shadow: #000000;
+ --m3-surface-tint: #A3BAFF;
+ }
+}
diff --git a/main/assets/m3/site.css b/main/assets/m3/site.css
new file mode 100644
index 0000000..aef20df
--- /dev/null
+++ b/main/assets/m3/site.css
@@ -0,0 +1,448 @@
+/* ============================================================================
+ GDIPS · Material You 3 — Site shell
+ ----------------------------------------------------------------------------
+ Layout + shared page furniture (app bar, drawer, hero, footer, section
+ headers, team/credit cards, download cards, rating tables) built on top of
+ tokens.css and components.css.
+ ========================================================================== */
+
+/* ── Page shell ─────────────────────────────────────────────────────────── */
+
+.m3-page {
+ display: flex;
+ flex-direction: column;
+ min-height: 100dvh;
+ background: var(--m3-surface);
+}
+
+.m3-main { flex: 1 1 auto; }
+
+.m3-main--offset {
+ padding-block-end: calc(88px + env(safe-area-inset-bottom));
+}
+
+@media (min-width: 900px) {
+ .m3-main--offset { padding-block-end: 0; }
+}
+
+/* ── App bar shortcuts ──────────────────────────────────────────────────── */
+
+.m3-app-bar__brand-mark {
+ display: grid;
+ place-items: center;
+ width: 40px;
+ height: 40px;
+ border-radius: var(--m3-shape-md);
+ background: var(--m3-primary-container);
+ color: var(--m3-on-primary-container);
+ font-size: 1.15rem;
+}
+
+.m3-app-bar__brand-mark img {
+ width: 100%;
+ height: 100%;
+ border-radius: inherit;
+ object-fit: cover;
+}
+
+.m3-theme-toggle .m3-theme-toggle__dark { display: none; }
+:root[data-theme='dark'] .m3-theme-toggle .m3-theme-toggle__dark { display: inline-block; }
+:root[data-theme='dark'] .m3-theme-toggle .m3-theme-toggle__light { display: none; }
+
+@media (prefers-color-scheme: dark) {
+ :root:not([data-theme='light']) .m3-theme-toggle .m3-theme-toggle__dark { display: inline-block; }
+ :root:not([data-theme='light']) .m3-theme-toggle .m3-theme-toggle__light { display: none; }
+}
+
+/* ── Hero ───────────────────────────────────────────────────────────────── */
+
+.m3-hero {
+ position: relative;
+ overflow: hidden;
+ padding-block: clamp(32px, 7vw, 72px);
+ background:
+ radial-gradient(circle at 12% 18%, color-mix(in srgb, var(--m3-primary-container) 70%, transparent) 0%, transparent 45%),
+ radial-gradient(circle at 88% 8%, color-mix(in srgb, var(--m3-tertiary-container) 45%, transparent) 0%, transparent 42%),
+ var(--m3-surface);
+ isolation: isolate;
+}
+
+.m3-hero__grid {
+ display: grid;
+ gap: clamp(24px, 5vw, 48px);
+ align-items: center;
+}
+
+@media (min-width: 900px) {
+ .m3-hero__grid { grid-template-columns: 1.1fr 0.9fr; }
+}
+
+.m3-hero__eyebrow {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 14px;
+ border-radius: var(--m3-shape-full);
+ background: var(--m3-secondary-container);
+ color: var(--m3-on-secondary-container);
+ font-size: var(--m3-label-large-size);
+ font-weight: var(--m3-label-large-weight);
+ letter-spacing: var(--m3-tracking-label);
+}
+
+.m3-hero__title {
+ margin-block: var(--m3-space-4) var(--m3-space-3);
+ font-size: clamp(2rem, 5.2vw, 3.5rem);
+ line-height: 1.08;
+ letter-spacing: var(--m3-tracking-display);
+ font-weight: 600;
+}
+
+.m3-hero__title em {
+ font-style: normal;
+ background: linear-gradient(100deg, var(--m3-primary), var(--m3-tertiary));
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+}
+
+.m3-hero__lead {
+ max-width: 60ch;
+ color: var(--m3-on-surface-variant);
+ font-size: clamp(1rem, 1.4vw, 1.125rem);
+ line-height: 1.6;
+}
+
+.m3-hero__actions { margin-block-start: var(--m3-space-6); }
+
+.m3-hero__visual {
+ position: relative;
+ display: grid;
+ place-items: center;
+ min-height: 240px;
+}
+
+.m3-hero__orb {
+ position: relative;
+ display: grid;
+ place-items: center;
+ width: clamp(180px, 40vw, 300px);
+ aspect-ratio: 1;
+ border-radius: var(--m3-shape-full);
+ background: linear-gradient(145deg, var(--m3-primary-container), var(--m3-tertiary-container));
+ box-shadow: var(--m3-elevation-3);
+ animation: m3-float 7s var(--m3-ease-standard) infinite;
+}
+
+.m3-hero__orb img {
+ width: 62%;
+ height: 62%;
+ border-radius: var(--m3-shape-full);
+ object-fit: cover;
+ border: 6px solid color-mix(in srgb, var(--m3-surface) 65%, transparent);
+}
+
+.m3-hero__badge {
+ position: absolute;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 16px;
+ border-radius: var(--m3-shape-full);
+ background: var(--m3-surface-container-high);
+ color: var(--m3-on-surface);
+ box-shadow: var(--m3-elevation-2);
+ font-size: var(--m3-label-large-size);
+ font-weight: var(--m3-label-large-weight);
+ white-space: nowrap;
+}
+
+.m3-hero__badge--tl { inset-block-start: 4%; inset-inline-start: 0; }
+.m3-hero__badge--br { inset-block-end: 6%; inset-inline-end: 0; }
+
+@keyframes m3-float {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-14px); }
+}
+
+/* ── Section header ─────────────────────────────────────────────────────── */
+
+.m3-section-head {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: var(--m3-space-4);
+ margin-block-end: var(--m3-space-6);
+}
+
+.m3-section-head__title { font-size: clamp(1.5rem, 3vw, 2rem); font-weight: 600; }
+.m3-section-head__sub { color: var(--m3-on-surface-variant); margin-block-start: 6px; max-width: 65ch; }
+.m3-section-head--centered { flex-direction: column; align-items: center; text-align: center; }
+
+/* ── Feature / stat tiles ───────────────────────────────────────────────── */
+
+.m3-tile {
+ display: flex;
+ flex-direction: column;
+ gap: var(--m3-space-3);
+ padding: var(--m3-space-5);
+ border-radius: var(--m3-shape-lg);
+ background: var(--m3-surface-container-low);
+ border: 1px solid var(--m3-outline-variant);
+ transition: transform var(--m3-duration-medium) var(--m3-ease-emphasized), box-shadow var(--m3-duration-medium) var(--m3-ease-standard), border-color var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+@media (hover: hover) {
+ .m3-tile:hover {
+ transform: translateY(-4px);
+ box-shadow: var(--m3-elevation-2);
+ border-color: transparent;
+ }
+}
+
+.m3-tile__icon {
+ display: grid;
+ place-items: center;
+ width: 48px;
+ height: 48px;
+ border-radius: var(--m3-shape-md);
+ background: var(--m3-primary-container);
+ color: var(--m3-on-primary-container);
+ font-size: 1.25rem;
+}
+
+.m3-tile__icon--secondary { background: var(--m3-secondary-container); color: var(--m3-on-secondary-container); }
+.m3-tile__icon--tertiary { background: var(--m3-tertiary-container); color: var(--m3-on-tertiary-container); }
+.m3-tile__title { font-size: var(--m3-title-medium-size); font-weight: var(--m3-title-medium-weight); }
+.m3-tile__text { color: var(--m3-on-surface-variant); font-size: var(--m3-body-medium-size); }
+
+.m3-stat {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ padding: var(--m3-space-4);
+ border-radius: var(--m3-shape-md);
+ background: var(--m3-surface-container);
+}
+
+.m3-stat__value {
+ font-size: clamp(1.5rem, 3vw, 2rem);
+ font-weight: 600;
+ line-height: 1.15;
+ font-variant-numeric: tabular-nums;
+}
+
+.m3-stat__label { color: var(--m3-on-surface-variant); font-size: var(--m3-label-large-size); font-weight: var(--m3-label-large-weight); letter-spacing: var(--m3-tracking-label); }
+
+/* ── Person / credit cards (team, contributors) ─────────────────────────── */
+
+.m3-person {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--m3-space-3);
+ padding: var(--m3-space-6) var(--m3-space-5);
+ border-radius: var(--m3-shape-xl);
+ background: var(--m3-surface-container-low);
+ border: 1px solid var(--m3-outline-variant);
+ text-align: center;
+ height: 100%;
+ transition: transform var(--m3-duration-medium) var(--m3-ease-emphasized), box-shadow var(--m3-duration-medium) var(--m3-ease-standard);
+}
+
+@media (hover: hover) {
+ .m3-person:hover { transform: translateY(-4px); box-shadow: var(--m3-elevation-2); }
+}
+
+.m3-person__avatar {
+ width: 96px;
+ height: 96px;
+ border-radius: var(--m3-shape-full);
+ object-fit: cover;
+ border: 4px solid var(--m3-surface-container-high);
+ background: var(--m3-surface-container-high);
+}
+
+.m3-person__role {
+ padding: 4px 12px;
+ border-radius: var(--m3-shape-full);
+ background: var(--m3-primary-container);
+ color: var(--m3-on-primary-container);
+ font-size: var(--m3-label-medium-size);
+ font-weight: var(--m3-label-medium-weight);
+ letter-spacing: var(--m3-tracking-label);
+}
+
+.m3-person__name { font-size: var(--m3-title-large-size); font-weight: 600; }
+.m3-person__bio { color: var(--m3-on-surface-variant); font-size: var(--m3-body-medium-size); }
+.m3-person__actions { margin-block-start: auto; padding-block-start: var(--m3-space-2); }
+
+/* ── Footer ─────────────────────────────────────────────────────────────── */
+
+.m3-footer {
+ margin-block-start: clamp(32px, 6vw, 64px);
+ padding-block: var(--m3-space-8) calc(var(--m3-space-8) + env(safe-area-inset-bottom));
+ background: var(--m3-surface-container);
+ color: var(--m3-on-surface-variant);
+ font-size: var(--m3-body-medium-size);
+}
+
+.m3-footer__grid {
+ display: grid;
+ gap: var(--m3-space-8);
+}
+
+@media (min-width: 720px) {
+ .m3-footer__grid { grid-template-columns: 1.4fr 1fr 1fr; }
+}
+
+.m3-footer__brand { display: flex; align-items: center; gap: var(--m3-space-3); color: var(--m3-on-surface); font-size: var(--m3-title-medium-size); font-weight: 600; }
+.m3-footer__brand img { width: 40px; height: 40px; border-radius: var(--m3-shape-sm); }
+.m3-footer__heading { color: var(--m3-on-surface); font-size: var(--m3-label-large-size); font-weight: var(--m3-label-large-weight); letter-spacing: var(--m3-tracking-label); text-transform: uppercase; margin-block-end: var(--m3-space-3); }
+.m3-footer__links { display: flex; flex-direction: column; gap: var(--m3-space-2); }
+.m3-footer__links a { color: var(--m3-on-surface-variant); text-decoration: none; width: fit-content; }
+.m3-footer__links a:hover { color: var(--m3-primary); text-decoration: underline; }
+.m3-footer__bottom {
+ margin-block-start: var(--m3-space-8);
+ padding-block-start: var(--m3-space-4);
+ border-block-start: 1px solid var(--m3-outline-variant);
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--m3-space-3);
+ justify-content: space-between;
+ align-items: center;
+ font-size: var(--m3-body-small-size);
+}
+
+/* ── Rich text / legal pages ────────────────────────────────────────────── */
+
+.m3-prose {
+ max-width: 74ch;
+ color: var(--m3-on-surface-variant);
+ font-size: var(--m3-body-large-size);
+ line-height: 1.7;
+}
+
+.m3-prose > * + * { margin-block-start: var(--m3-space-4); }
+.m3-prose h2 { color: var(--m3-on-surface); margin-block-start: var(--m3-space-8); }
+.m3-prose h3 { color: var(--m3-on-surface); margin-block-start: var(--m3-space-6); }
+.m3-prose strong { color: var(--m3-on-surface); }
+.m3-prose ul,
+.m3-prose ol { padding-inline-start: 1.5em; display: flex; flex-direction: column; gap: 8px; }
+.m3-prose a { text-decoration: underline; text-underline-offset: 2px; }
+
+/* ── Progress / rating meters (rating-system page) ──────────────────────── */
+
+.m3-meter { display: flex; flex-direction: column; gap: 6px; }
+.m3-meter__head { display: flex; justify-content: space-between; font-size: var(--m3-label-large-size); font-weight: var(--m3-label-large-weight); color: var(--m3-on-surface-variant); }
+.m3-meter__track { height: 8px; border-radius: var(--m3-shape-full); background: var(--m3-surface-container-highest); overflow: hidden; }
+.m3-meter__fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--m3-primary), var(--m3-tertiary)); width: var(--m3-meter-value, 0%); transition: width var(--m3-duration-long) var(--m3-ease-emphasized-decel); }
+
+.m3-rating-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 14px;
+ border-radius: var(--m3-shape-full);
+ background: var(--m3-surface-container-highest);
+ color: var(--m3-on-surface);
+ font-size: var(--m3-label-large-size);
+ font-weight: var(--m3-label-large-weight);
+}
+
+.m3-rating-chip i { color: var(--m3-tertiary); }
+
+/* Difficulty accents reused by the demon list cards */
+:root {
+ --m3-diff-easy: #2e7d32;
+ --m3-diff-medium: #ef6c00;
+ --m3-diff-hard: #c62828;
+ --m3-diff-insane: #6a1b9a;
+ --m3-diff-extreme: #1565c0;
+ --m3-diff-impossible: #37474f;
+}
+
+:root[data-theme='dark'] {
+ --m3-diff-easy: #81c995;
+ --m3-diff-medium: #ffb74d;
+ --m3-diff-hard: #f28b82;
+ --m3-diff-insane: #d7aefb;
+ --m3-diff-extreme: #8ab4f8;
+ --m3-diff-impossible: #cfd8dc;
+}
+
+/* ── Utility: two-column split used by several pages ────────────────────── */
+
+.m3-split {
+ display: grid;
+ gap: clamp(20px, 4vw, 40px);
+ align-items: start;
+}
+
+@media (min-width: 1024px) {
+ .m3-split { grid-template-columns: minmax(0, 1fr) minmax(280px, 360px); }
+ .m3-split--reverse { grid-template-columns: minmax(280px, 360px) minmax(0, 1fr); }
+}
+
+.m3-aside {
+ display: flex;
+ flex-direction: column;
+ gap: var(--m3-space-4);
+ position: sticky;
+ inset-block-start: calc(var(--m3-appbar-height) + 16px);
+}
+
+.m3-panel {
+ padding: var(--m3-space-5);
+ border-radius: var(--m3-shape-lg);
+ background: var(--m3-surface-container-low);
+ border: 1px solid var(--m3-outline-variant);
+}
+
+.m3-panel--filled { background: var(--m3-surface-container-high); border-color: transparent; }
+
+/* ── Print ──────────────────────────────────────────────────────────────── */
+
+@media print {
+ .m3-app-bar,
+ .m3-drawer,
+ .m3-nav-bar,
+ .m3-footer,
+ .m3-scrim,
+ .m3-fab { display: none !important; }
+
+ body { background: #fff; color: #000; }
+}
+
+/* ── Scroll reveal ──────────────────────────────────────────────────────── */
+
+.m3-reveal {
+ opacity: 0;
+ transform: translateY(18px);
+ transition:
+ opacity var(--m3-duration-long) var(--m3-ease-emphasized-decel),
+ transform var(--m3-duration-long) var(--m3-ease-emphasized-decel);
+ will-change: opacity, transform;
+}
+
+.m3-reveal.is-visible { opacity: 1; transform: none; }
+
+@media (prefers-reduced-motion: reduce) {
+ .m3-reveal { opacity: 1; transform: none; }
+}
+
+/* ── Small shared helpers used across the redesigned pages ─────────────────── */
+
+.m3-meta {
+ color: var(--m3-on-surface-variant);
+ font-size: var(--m3-body-small-size);
+}
+
+.m3-drawer__meta {
+ color: var(--m3-on-surface-variant);
+ font-size: 0.75rem;
+ line-height: 1;
+}
+
+.gd-year { font-variant-numeric: tabular-nums; }
diff --git a/main/assets/m3/tokens.css b/main/assets/m3/tokens.css
new file mode 100644
index 0000000..fb26229
--- /dev/null
+++ b/main/assets/m3/tokens.css
@@ -0,0 +1,487 @@
+/* ============================================================================
+ GDIPS · Material You 3 — Design tokens
+ ----------------------------------------------------------------------------
+ Non-colour tokens (type scale, shape, elevation, motion, spacing) shared by
+ every page. Colour roles live in palettes.css and are generated from a seed
+ colour, so themes are fully dynamic.
+ ========================================================================== */
+
+/* Local display face used for GDIPS "game style" headlines (404, bootloader) */
+@font-face {
+ font-family: 'Pusab';
+ src: url('../Pusab.ttf') format('truetype');
+ font-display: swap;
+ font-weight: 400;
+}
+
+:root {
+ /* ── Typography ─────────────────────────────────────────────────────────
+ Material 3 expressive scale. Roboto Flex is the variable M3 face; the
+ Grotesk stack keeps the "GDIPS" personality in display sizes. */
+ --m3-font-brand: 'Roboto Flex', 'Google Sans', 'Roboto', 'Segoe UI', system-ui, sans-serif;
+ --m3-font-plain: 'Roboto Flex', 'Roboto', 'Segoe UI', system-ui, -apple-system, sans-serif;
+ --m3-font-mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', 'Cascadia Code', Consolas, monospace;
+ --m3-font-display: 'Pusab', 'Roboto Flex', 'Roboto', system-ui, sans-serif;
+
+ --m3-display-large-size: 3.5625rem; /* 57 */
+ --m3-display-medium-size: 2.8125rem; /* 45 */
+ --m3-display-small-size: 2.25rem; /* 36 */
+ --m3-headline-large-size: 2rem; /* 32 */
+ --m3-headline-medium-size: 1.75rem; /* 28 */
+ --m3-headline-small-size: 1.5rem; /* 24 */
+ --m3-title-large-size: 1.375rem; /* 22 */
+ --m3-title-medium-size: 1rem; /* 16 */
+ --m3-title-small-size: 0.875rem; /* 14 */
+ --m3-body-large-size: 1rem; /* 16 */
+ --m3-body-medium-size: 0.875rem; /* 14 */
+ --m3-body-small-size: 0.75rem; /* 12 */
+ --m3-label-large-size: 0.875rem; /* 14 */
+ --m3-label-medium-size: 0.75rem; /* 12 */
+ --m3-label-small-size: 0.6875rem; /* 11 */
+
+ --m3-display-large-line: 4rem;
+ --m3-display-medium-line: 3.25rem;
+ --m3-display-small-line: 2.75rem;
+ --m3-headline-large-line: 2.5rem;
+ --m3-headline-medium-line: 2.25rem;
+ --m3-headline-small-line: 2rem;
+ --m3-title-large-line: 1.75rem;
+ --m3-title-medium-line: 1.5rem;
+ --m3-title-small-line: 1.25rem;
+ --m3-body-large-line: 1.5rem;
+ --m3-body-medium-line: 1.25rem;
+ --m3-body-small-line: 1rem;
+ --m3-label-large-line: 1.25rem;
+ --m3-label-medium-line: 1rem;
+ --m3-label-small-line: 1rem;
+
+ --m3-display-large-weight: 500;
+ --m3-display-small-weight: 500;
+ --m3-headline-weight: 500;
+ --m3-title-large-weight: 500;
+ --m3-title-medium-weight: 600;
+ --m3-title-small-weight: 600;
+ --m3-body-weight: 400;
+ --m3-label-large-weight: 600;
+ --m3-label-medium-weight: 600;
+
+ --m3-tracking-display: -0.02em;
+ --m3-tracking-headline: -0.012em;
+ --m3-tracking-title: 0.006em;
+ --m3-tracking-body: 0.015em;
+ --m3-tracking-label: 0.035em;
+
+ /* ── Shape (corner radii) ───────────────────────────────────────────── */
+ --m3-shape-none: 0;
+ --m3-shape-xs: 4px;
+ --m3-shape-sm: 8px;
+ --m3-shape-md: 12px;
+ --m3-shape-lg: 16px;
+ --m3-shape-xl: 28px;
+ --m3-shape-2xl: 32px;
+ --m3-shape-full: 999px;
+
+ /* ── Elevation (M3 key + ambient shadows) ───────────────────────────── */
+ --m3-elevation-0: none;
+ --m3-elevation-1: 0 1px 2px 0 rgb(0 0 0 / 0.30), 0 1px 3px 1px rgb(0 0 0 / 0.15);
+ --m3-elevation-2: 0 1px 2px 0 rgb(0 0 0 / 0.30), 0 2px 6px 2px rgb(0 0 0 / 0.15);
+ --m3-elevation-3: 0 1px 3px 0 rgb(0 0 0 / 0.30), 0 4px 8px 3px rgb(0 0 0 / 0.15);
+ --m3-elevation-4: 0 2px 3px 0 rgb(0 0 0 / 0.30), 0 6px 10px 4px rgb(0 0 0 / 0.15);
+ --m3-elevation-5: 0 4px 4px 0 rgb(0 0 0 / 0.30), 0 8px 12px 6px rgb(0 0 0 / 0.15);
+
+ /* ── Spacing (4dp grid) ─────────────────────────────────────────────── */
+ --m3-space-1: 4px;
+ --m3-space-2: 8px;
+ --m3-space-3: 12px;
+ --m3-space-4: 16px;
+ --m3-space-5: 20px;
+ --m3-space-6: 24px;
+ --m3-space-7: 28px;
+ --m3-space-8: 32px;
+ --m3-space-10: 40px;
+ --m3-space-12: 48px;
+ --m3-space-16: 64px;
+ --m3-space-20: 80px;
+
+ --m3-page-gutter: clamp(16px, 4vw, 32px);
+ --m3-content-max: 1200px;
+ --m3-content-wide: 1440px;
+ --m3-appbar-height: 64px;
+
+ /* ── Motion ─────────────────────────────────────────────────────────── */
+ --m3-ease-standard: cubic-bezier(0.2, 0, 0, 1);
+ --m3-ease-standard-decel: cubic-bezier(0.05, 0.7, 0.1, 1);
+ --m3-ease-standard-accel: cubic-bezier(0.3, 0, 0.8, 0.15);
+ --m3-ease-emphasized: cubic-bezier(0.2, 0, 0, 1);
+ --m3-ease-emphasized-decel: cubic-bezier(0.05, 0.7, 0.1, 1);
+ --m3-ease-emphasized-accel: cubic-bezier(0.3, 0, 0.8, 0.15);
+ --m3-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
+ --m3-duration-short: 150ms;
+ --m3-duration-short-2: 200ms;
+ --m3-duration-medium: 300ms;
+ --m3-duration-long: 500ms;
+ --m3-duration-extra-long: 700ms;
+
+ /* ── State layers (M3 interaction opacities) ────────────────────────── */
+ --m3-state-hover: 0.08;
+ --m3-state-focus: 0.10;
+ --m3-state-pressed: 0.10;
+ --m3-state-drag: 0.16;
+ --m3-state-disabled-content: 0.38;
+ --m3-state-disabled-container: 0.12;
+
+ /* ── Misc ───────────────────────────────────────────────────────────── */
+ --m3-focus-ring: 3px;
+ --m3-z-drawer: 1000;
+ --m3-z-appbar: 900;
+ --m3-z-dialog: 1200;
+ --m3-z-menu: 1100;
+ --m3-z-snackbar: 1300;
+ --m3-z-tooltip: 1400;
+
+ color-scheme: light;
+}
+
+:root[data-theme='dark'] {
+ color-scheme: dark;
+ --m3-elevation-1: 0 1px 3px 1px rgb(0 0 0 / 0.55), 0 1px 2px 0 rgb(0 0 0 / 0.7);
+ --m3-elevation-2: 0 2px 6px 2px rgb(0 0 0 / 0.55), 0 1px 2px 0 rgb(0 0 0 / 0.7);
+ --m3-elevation-3: 0 4px 8px 3px rgb(0 0 0 / 0.55), 0 1px 3px 0 rgb(0 0 0 / 0.7);
+ --m3-elevation-4: 0 6px 10px 4px rgb(0 0 0 / 0.55), 0 2px 3px 0 rgb(0 0 0 / 0.7);
+ --m3-elevation-5: 0 8px 12px 6px rgb(0 0 0 / 0.55), 0 4px 4px 0 rgb(0 0 0 / 0.7);
+}
+
+/* ============================================================================
+ Base / reset
+ ========================================================================== */
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html {
+ -webkit-text-size-adjust: 100%;
+ text-size-adjust: 100%;
+ scroll-behavior: smooth;
+ scroll-padding-top: calc(var(--m3-appbar-height) + 16px);
+}
+
+body {
+ margin: 0;
+ min-height: 100dvh;
+ font-family: var(--m3-font-plain);
+ font-size: var(--m3-body-large-size);
+ line-height: var(--m3-body-large-line);
+ letter-spacing: var(--m3-tracking-body);
+ font-weight: var(--m3-body-weight);
+ color: var(--m3-on-surface);
+ background-color: var(--m3-surface);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-rendering: optimizeLegibility;
+ font-variant-ligatures: common-ligatures;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+figure,
+blockquote,
+dl,
+dd {
+ margin: 0;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: var(--m3-font-brand);
+ font-weight: var(--m3-headline-weight);
+ letter-spacing: var(--m3-tracking-headline);
+ line-height: 1.25;
+ text-wrap: balance;
+}
+
+h1 { font-size: var(--m3-headline-large-size); }
+h2 { font-size: var(--m3-headline-medium-size); }
+h3 { font-size: var(--m3-title-large-size); font-weight: var(--m3-title-large-weight); }
+h4 { font-size: var(--m3-title-medium-size); font-weight: var(--m3-title-medium-weight); }
+h5 { font-size: var(--m3-title-small-size); }
+
+p { text-wrap: pretty; }
+
+ul,
+ol {
+ margin: 0;
+ padding-inline-start: 1.35em;
+}
+
+img,
+svg,
+video,
+canvas,
+iframe {
+ max-width: 100%;
+ display: block;
+}
+
+img { height: auto; }
+
+a {
+ color: var(--m3-primary);
+ text-decoration: none;
+ border-radius: var(--m3-shape-xs);
+ transition: color var(--m3-duration-short) var(--m3-ease-standard);
+}
+
+a:hover { color: var(--m3-primary); text-decoration: underline; }
+a:visited { color: var(--m3-tertiary); }
+
+button,
+input,
+select,
+textarea {
+ font: inherit;
+ letter-spacing: inherit;
+ color: inherit;
+}
+
+button { cursor: pointer; }
+button:disabled { cursor: not-allowed; }
+
+hr {
+ border: 0;
+ border-top: 1px solid var(--m3-outline-variant);
+ margin-block: var(--m3-space-5);
+}
+
+code,
+kbd,
+samp,
+pre {
+ font-family: var(--m3-font-mono);
+ font-size: 0.9em;
+}
+
+kbd {
+ display: inline-block;
+ padding: 0.1em 0.45em;
+ border-radius: var(--m3-shape-xs);
+ background: var(--m3-surface-container-high);
+ border: 1px solid var(--m3-outline-variant);
+ border-bottom-width: 2px;
+ color: var(--m3-on-surface-variant);
+ font-size: var(--m3-label-medium-size);
+ line-height: 1.5;
+}
+
+strong,
+b { font-weight: 650; }
+
+small { font-size: var(--m3-body-small-size); }
+
+/* ── Focus ──────────────────────────────────────────────────────────────── */
+:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible,
+.m3-focus-ring:focus-visible {
+ outline: var(--m3-focus-ring) solid var(--m3-secondary);
+ outline-offset: 2px;
+}
+
+:focus:not(:focus-visible) { outline: none; }
+
+/* ── Selection & scrollbars ─────────────────────────────────────────────── */
+::selection {
+ background: var(--m3-primary);
+ color: var(--m3-on-primary);
+}
+
+* {
+ scrollbar-color: var(--m3-outline) transparent;
+ scrollbar-width: thin;
+}
+
+::-webkit-scrollbar { width: 10px; height: 10px; }
+::-webkit-scrollbar-track { background: transparent; }
+::-webkit-scrollbar-thumb {
+ background: var(--m3-outline-variant);
+ border-radius: var(--m3-shape-full);
+ border: 3px solid transparent;
+ background-clip: content-box;
+}
+::-webkit-scrollbar-thumb:hover { background-clip: content-box; background-color: var(--m3-outline); }
+
+/* ============================================================================
+ Typography utilities
+ ========================================================================== */
+
+.m3-display-large,
+.m3-display-medium,
+.m3-display-small,
+.m3-headline-large,
+.m3-headline-medium,
+.m3-headline-small,
+.m3-title-large,
+.m3-title-medium,
+.m3-title-small,
+.m3-body-large,
+.m3-body-medium,
+.m3-body-small,
+.m3-label-large,
+.m3-label-medium,
+.m3-label-small {
+ font-family: var(--m3-font-brand);
+ margin: 0;
+}
+
+.m3-display-large { font-size: var(--m3-display-large-size); line-height: var(--m3-display-large-line); font-weight: var(--m3-display-large-weight); letter-spacing: var(--m3-tracking-display); }
+.m3-display-medium { font-size: var(--m3-display-medium-size); line-height: var(--m3-display-medium-line); font-weight: var(--m3-display-large-weight); letter-spacing: var(--m3-tracking-display); }
+.m3-display-small { font-size: var(--m3-display-small-size); line-height: var(--m3-display-small-line); font-weight: var(--m3-display-small-weight); letter-spacing: var(--m3-tracking-display); }
+.m3-headline-large { font-size: var(--m3-headline-large-size); line-height: var(--m3-headline-large-line); letter-spacing: var(--m3-tracking-headline); }
+.m3-headline-medium { font-size: var(--m3-headline-medium-size); line-height: var(--m3-headline-medium-line); letter-spacing: var(--m3-tracking-headline); }
+.m3-headline-small { font-size: var(--m3-headline-small-size); line-height: var(--m3-headline-small-line); letter-spacing: var(--m3-tracking-headline); }
+.m3-title-large { font-size: var(--m3-title-large-size); line-height: var(--m3-title-large-line); font-weight: var(--m3-title-large-weight); letter-spacing: var(--m3-tracking-title); }
+.m3-title-medium { font-size: var(--m3-title-medium-size); line-height: var(--m3-title-medium-line); font-weight: var(--m3-title-medium-weight); letter-spacing: var(--m3-tracking-title); }
+.m3-title-small { font-size: var(--m3-title-small-size); line-height: var(--m3-title-small-line); font-weight: var(--m3-title-small-weight); letter-spacing: var(--m3-tracking-title); }
+
+.m3-body-large { font-family: var(--m3-font-plain); font-size: var(--m3-body-large-size); line-height: var(--m3-body-large-line); letter-spacing: var(--m3-tracking-body); }
+.m3-body-medium { font-family: var(--m3-font-plain); font-size: var(--m3-body-medium-size); line-height: var(--m3-body-medium-line); letter-spacing: var(--m3-tracking-body); }
+.m3-body-small { font-family: var(--m3-font-plain); font-size: var(--m3-body-small-size); line-height: var(--m3-body-small-line); letter-spacing: var(--m3-tracking-body); }
+.m3-label-large { font-family: var(--m3-font-plain); font-size: var(--m3-label-large-size); line-height: var(--m3-label-large-line); font-weight: var(--m3-label-large-weight); letter-spacing: var(--m3-tracking-label); }
+.m3-label-medium { font-family: var(--m3-font-plain); font-size: var(--m3-label-medium-size); line-height: var(--m3-label-medium-line); font-weight: var(--m3-label-medium-weight); letter-spacing: var(--m3-tracking-label); }
+.m3-label-small { font-family: var(--m3-font-plain); font-size: var(--m3-label-small-size); line-height: var(--m3-label-small-line); font-weight: var(--m3-label-medium-weight); letter-spacing: var(--m3-tracking-label); }
+
+.m3-mono { font-family: var(--m3-font-mono); font-variant-numeric: tabular-nums; }
+.m3-display-font { font-family: var(--m3-font-display); letter-spacing: 0.01em; }
+.m3-text-primary { color: var(--m3-primary); }
+.m3-text-muted { color: var(--m3-on-surface-variant); }
+.m3-text-error { color: var(--m3-error); }
+.m3-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.m3-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
+.m3-clamp-3 { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
+
+/* ============================================================================
+ Layout helpers
+ ========================================================================== */
+
+.m3-container {
+ width: 100%;
+ max-width: var(--m3-content-max);
+ margin-inline: auto;
+ padding-inline: var(--m3-page-gutter);
+}
+
+.m3-container--wide { max-width: var(--m3-content-wide); }
+
+.m3-section { padding-block: clamp(24px, 6vw, 56px); }
+
+.m3-stack { display: flex; flex-direction: column; gap: var(--m3-space-4); }
+.m3-row { display: flex; flex-wrap: wrap; gap: var(--m3-space-3); align-items: center; }
+.m3-spacer { flex: 1 1 auto; }
+
+.m3-grid { display: grid; gap: var(--m3-space-5); }
+.m3-grid--auto { grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); }
+.m3-grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); }
+.m3-grid--3 { grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); }
+
+.m3-shape-none { border-radius: 0; }
+.m3-shape-xs { border-radius: var(--m3-shape-xs); }
+.m3-shape-sm { border-radius: var(--m3-shape-sm); }
+.m3-shape-md { border-radius: var(--m3-shape-md); }
+.m3-shape-lg { border-radius: var(--m3-shape-lg); }
+.m3-shape-xl { border-radius: var(--m3-shape-xl); }
+.m3-shape-full { border-radius: var(--m3-shape-full); }
+
+.m3-elevation-0 { box-shadow: var(--m3-elevation-0); }
+.m3-elevation-1 { box-shadow: var(--m3-elevation-1); }
+.m3-elevation-2 { box-shadow: var(--m3-elevation-2); }
+.m3-elevation-3 { box-shadow: var(--m3-elevation-3); }
+.m3-elevation-4 { box-shadow: var(--m3-elevation-4); }
+.m3-elevation-5 { box-shadow: var(--m3-elevation-5); }
+
+.m3-hidden { display: none !important; }
+.m3-visually-hidden,
+.sr-only {
+ position: absolute !important;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ clip-path: inset(50%);
+ white-space: nowrap;
+ border: 0;
+}
+
+.m3-skip-link {
+ position: absolute;
+ inset-inline-start: var(--m3-space-4);
+ top: -80px;
+ z-index: calc(var(--m3-z-snackbar) + 1);
+ padding: 12px 20px;
+ border-radius: var(--m3-shape-full);
+ background: var(--m3-primary);
+ color: var(--m3-on-primary);
+ font-weight: 650;
+ box-shadow: var(--m3-elevation-3);
+ transition: top var(--m3-duration-short-2) var(--m3-ease-emphasized-decel);
+}
+
+.m3-skip-link:focus { top: var(--m3-space-4); text-decoration: none; }
+
+/* ============================================================================
+ Motion
+ ========================================================================== */
+
+@media (prefers-reduced-motion: reduce) {
+ html { scroll-behavior: auto; }
+
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.001ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.001ms !important;
+ scroll-behavior: auto !important;
+ }
+}
+
+@media (prefers-contrast: more) {
+ :root {
+ --m3-outline-variant: var(--m3-outline);
+ }
+
+ .m3-card,
+ .m3-button,
+ .m3-text-field,
+ .m3-navigation-drawer {
+ border-color: currentColor;
+ }
+}
+
+/* ── Responsive visibility helpers ──────────────────────────────────────── */
+
+@media (max-width: 599px) {
+ .m3-hide-sm { display: none !important; }
+}
+
+@media (min-width: 600px) and (max-width: 899px) {
+ .m3-hide-md { display: none !important; }
+}
+
+@media (max-width: 899px) {
+ .m3-hide-below-md { display: none !important; }
+}
+
+@media (min-width: 900px) {
+ .m3-show-below-md { display: none !important; }
+}
diff --git a/main/assets/m3/ui.js b/main/assets/m3/ui.js
new file mode 100644
index 0000000..1d2fbda
--- /dev/null
+++ b/main/assets/m3/ui.js
@@ -0,0 +1,583 @@
+/* ============================================================================
+ GDIPS · Material You 3 — Interaction layer
+ ----------------------------------------------------------------------------
+ Progressive enhancement for the M3 components: ripples, app-bar elevation,
+ navigation drawers + scrims, menus, dialogs (with focus trap), tabs,
+ segmented buttons, text-field state, sliders, snackbars, copy-to-clipboard
+ and scroll reveals. Every behaviour is declarative — pages only add data
+ attributes, no per-page scripting required.
+
+ Public API
+ M3UI.snackbar(message, opts) transient message with optional action
+ M3UI.openDialog(elOrId) / close() dialog helpers
+ M3UI.openDrawer(elOrId, open) drawer helpers
+ M3UI.setLoading(el, bool) button loading state
+ M3UI.copy(text) clipboard helper
+ ========================================================================== */
+
+(function (global) {
+ 'use strict';
+
+ var REDUCED = global.matchMedia && global.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ var FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
+
+ function $(sel, scope) { return (scope || document).querySelector(sel); }
+ function $$(sel, scope) { return Array.prototype.slice.call((scope || document).querySelectorAll(sel)); }
+ function byId(id) { return typeof id === 'string' ? document.getElementById(id.replace(/^#/, '')) : id; }
+
+ /* ── Scroll lock ────────────────────────────────────────────────────── */
+
+ var lockCount = 0;
+ function lockScroll() {
+ if (lockCount++ === 0) {
+ var gap = global.innerWidth - document.documentElement.clientWidth;
+ document.body.style.overflow = 'hidden';
+ if (gap > 0) document.body.style.paddingInlineEnd = gap + 'px';
+ }
+ }
+ function unlockScroll() {
+ if (lockCount > 0 && --lockCount === 0) {
+ document.body.style.overflow = '';
+ document.body.style.paddingInlineEnd = '';
+ }
+ }
+
+ /* ── Ripple ─────────────────────────────────────────────────────────── */
+
+ var RIPPLE_SELECTOR = [
+ '.m3-button', '.m3-icon-button', '.m3-fab', '.m3-chip', '.m3-tab',
+ '.m3-list-item', '.m3-menu-item', '.m3-page-btn', '.m3-nav-bar__item',
+ '.m3-drawer__item', '.m3-segmented__item', '.m3-card--interactive',
+ '.sidebar-link', '.card-action-btn', '.view-btn', '.action-btn', '.filter-option'
+ ].join(',');
+
+ function attachRipple(el) {
+ if (REDUCED || el.dataset.m3Ripple === 'off') return;
+ el.addEventListener('pointerdown', function (event) {
+ var rect = el.getBoundingClientRect();
+ var size = Math.max(rect.width, rect.height) * 2;
+ var ripple = document.createElement('span');
+ ripple.className = 'm3-ripple';
+ ripple.style.width = ripple.style.height = size + 'px';
+ ripple.style.left = (event.clientX - rect.left - size / 2) + 'px';
+ ripple.style.top = (event.clientY - rect.top - size / 2) + 'px';
+ if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
+ el.appendChild(ripple);
+ setTimeout(function () { ripple.remove(); }, 600);
+ });
+ }
+
+ /* ── App bar elevation on scroll ────────────────────────────────────── */
+
+ function setupAppBars(scope) {
+ $$('.m3-app-bar', scope).forEach(function (bar) {
+ if (bar.dataset.m3Bound) return;
+ bar.dataset.m3Bound = '1';
+ var sentinel = document.createElement('div');
+ sentinel.setAttribute('aria-hidden', 'true');
+ sentinel.style.cssText = 'position:absolute;top:0;left:0;height:1px;width:1px;';
+ bar.parentNode.insertBefore(sentinel, bar);
+ if ('IntersectionObserver' in global) {
+ new IntersectionObserver(function (entries) {
+ bar.classList.toggle('is-scrolled', !entries[0].isIntersecting);
+ }, { threshold: 0 }).observe(sentinel);
+ }
+ });
+ }
+
+ /* ── Drawers ────────────────────────────────────────────────────────── */
+
+ function setDrawer(drawer, open) {
+ if (!drawer) return;
+ var wasOpen = drawer.classList.contains('is-open') || drawer.classList.contains('active');
+ drawer.classList.toggle('is-open', open);
+ drawer.classList.toggle('active', open);
+ drawer.setAttribute('aria-hidden', open ? 'false' : 'true');
+ var scrim = $('.m3-scrim[data-m3-scrim-for="#' + drawer.id + '"]') || $('.m3-scrim.is-paired');
+ if (scrim) scrim.classList.toggle('is-open', open);
+ if (open && !wasOpen) {
+ lockScroll();
+ var focusable = $(FOCUSABLE, drawer);
+ if (focusable) setTimeout(function () { focusable.focus(); }, 220);
+ } else if (!open && wasOpen) {
+ unlockScroll();
+ }
+ if (!open) {
+ var toggle = $('[data-m3-drawer-toggle="#' + drawer.id + '"]');
+ if (toggle) toggle.focus();
+ }
+ }
+
+ function setupDrawers(scope) {
+ $$('[data-m3-drawer-toggle]', scope).forEach(function (toggle) {
+ if (toggle.dataset.m3Bound) return;
+ toggle.dataset.m3Bound = '1';
+ toggle.addEventListener('click', function (event) {
+ event.preventDefault();
+ var drawer = byId(toggle.getAttribute('data-m3-drawer-toggle'));
+ if (!drawer) return;
+ setDrawer(drawer, !(drawer.classList.contains('is-open') || drawer.classList.contains('active')));
+ });
+ });
+
+ $$('[data-m3-drawer-close]', scope).forEach(function (btn) {
+ if (btn.dataset.m3Bound) return;
+ btn.dataset.m3Bound = '1';
+ btn.addEventListener('click', function () { setDrawer(btn.closest('.m3-drawer, .sidebar'), false); });
+ });
+
+ // Bridge for drawers whose open/close state is owned by legacy scripts
+ // (the demon list toggles `.active` itself — we only mirror it).
+ $$('[data-m3-scrim-for]', scope).forEach(function (scrim) {
+ var drawer = byId(scrim.getAttribute('data-m3-scrim-for'));
+ if (!drawer || scrim.dataset.m3Bound) return;
+ scrim.dataset.m3Bound = '1';
+ var sync = function () {
+ var open = drawer.classList.contains('is-open') || drawer.classList.contains('active');
+ scrim.classList.toggle('is-open', open);
+ if (open) { lockScroll(); } else { unlockScroll(); }
+ };
+ new MutationObserver(sync).observe(drawer, { attributes: true, attributeFilter: ['class'] });
+ scrim.addEventListener('click', function () {
+ drawer.classList.remove('is-open', 'active');
+ if (typeof global.DemonListApp !== 'undefined') { /* legacy app keeps its own state */ }
+ setDrawer(drawer, false);
+ });
+ sync();
+ });
+ }
+
+ /* ── Menus ──────────────────────────────────────────────────────────── */
+
+ function closeMenus(except) {
+ $$('.m3-menu.is-open').forEach(function (menu) {
+ if (menu === except) return;
+ menu.classList.remove('is-open');
+ var trigger = $('[data-m3-menu-trigger="#' + menu.id + '"]');
+ if (trigger) trigger.setAttribute('aria-expanded', 'false');
+ });
+ }
+
+ function setupMenus(scope) {
+ $$('[data-m3-menu-trigger]', scope).forEach(function (trigger) {
+ if (trigger.dataset.m3Bound) return;
+ trigger.dataset.m3Bound = '1';
+ trigger.setAttribute('aria-haspopup', 'true');
+ trigger.setAttribute('aria-expanded', 'false');
+ trigger.addEventListener('click', function (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ var menu = byId(trigger.getAttribute('data-m3-menu-trigger'));
+ if (!menu) return;
+ var willOpen = !menu.classList.contains('is-open');
+ closeMenus(menu);
+ menu.classList.toggle('is-open', willOpen);
+ trigger.setAttribute('aria-expanded', String(willOpen));
+ });
+ });
+
+ $$('.m3-menu', scope).forEach(function (menu) {
+ if (menu.dataset.m3Bound) return;
+ menu.dataset.m3Bound = '1';
+ menu.addEventListener('click', function (event) {
+ if (event.target.closest('[data-m3-menu-keep]')) return;
+ if (event.target.closest('.m3-menu-item')) closeMenus();
+ });
+ });
+ }
+
+ /* ── Dialogs ────────────────────────────────────────────────────────── */
+
+ var openDialogs = [];
+
+ function openDialog(target) {
+ var dialog = byId(target);
+ if (!dialog) return;
+ dialog.classList.add('is-open');
+ dialog.setAttribute('aria-hidden', 'false');
+ openDialogs.push(dialog);
+ lockScroll();
+ var focusable = $(FOCUSABLE, dialog);
+ if (focusable) setTimeout(function () { focusable.focus(); }, 180);
+ dialog.dispatchEvent(new CustomEvent('m3dialog:open', { bubbles: true }));
+ }
+
+ function closeDialog(target) {
+ var dialog = target ? byId(target) : openDialogs[openDialogs.length - 1];
+ if (!dialog) return;
+ dialog.classList.remove('is-open');
+ dialog.setAttribute('aria-hidden', 'true');
+ openDialogs = openDialogs.filter(function (d) { return d !== dialog; });
+ unlockScroll();
+ var trigger = $('[data-m3-dialog-open="#' + dialog.id + '"]');
+ if (trigger) trigger.focus();
+ dialog.dispatchEvent(new CustomEvent('m3dialog:close', { bubbles: true }));
+ }
+
+ function trapFocus(dialog, event) {
+ var items = $$(FOCUSABLE, dialog).filter(function (el) { return el.offsetParent !== null || el === document.activeElement; });
+ if (!items.length) return;
+ var first = items[0];
+ var last = items[items.length - 1];
+ if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
+ else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
+ }
+
+ function setupDialogs(scope) {
+ $$('[data-m3-dialog-open]', scope).forEach(function (trigger) {
+ if (trigger.dataset.m3Bound) return;
+ trigger.dataset.m3Bound = '1';
+ trigger.addEventListener('click', function (event) {
+ event.preventDefault();
+ openDialog(trigger.getAttribute('data-m3-dialog-open'));
+ });
+ });
+
+ $$('.m3-dialog', scope).forEach(function (dialog) {
+ if (dialog.dataset.m3Bound) return;
+ dialog.dataset.m3Bound = '1';
+ dialog.setAttribute('role', 'dialog');
+ dialog.setAttribute('aria-modal', 'true');
+ dialog.setAttribute('aria-hidden', 'true');
+ dialog.addEventListener('click', function (event) {
+ if (event.target === dialog) closeDialog(dialog);
+ });
+ });
+
+ document.addEventListener('click', function (event) {
+ var closer = event.target.closest('[data-m3-dialog-close]');
+ if (closer) {
+ event.preventDefault();
+ closeDialog(closer.closest('.m3-dialog'));
+ }
+ });
+
+ document.addEventListener('keydown', function (event) {
+ if (event.key === 'Escape') {
+ if (openDialogs.length) { closeDialog(); return; }
+ if ($('.m3-menu.is-open')) { closeMenus(); return; }
+ var drawer = $('.m3-scrim.is-open');
+ if (drawer) {
+ var target = byId(drawer.getAttribute('data-m3-scrim-for'));
+ if (target) setDrawer(target, false);
+ }
+ }
+ if (event.key === 'Tab' && openDialogs.length) trapFocus(openDialogs[openDialogs.length - 1], event);
+ });
+
+ document.addEventListener('click', function (event) {
+ if (!event.target.closest('.m3-menu') && !event.target.closest('[data-m3-menu-trigger]')) closeMenus();
+ });
+ }
+
+ /* ── Snackbar ───────────────────────────────────────────────────────── */
+
+ function region() {
+ var el = $('.m3-snackbar-region');
+ if (!el) {
+ el = document.createElement('div');
+ el.className = 'm3-snackbar-region';
+ el.setAttribute('role', 'status');
+ el.setAttribute('aria-live', 'polite');
+ document.body.appendChild(el);
+ }
+ return el;
+ }
+
+ function snackbar(message, options) {
+ var opts = options || {};
+ var bar = document.createElement('div');
+ bar.className = 'm3-snackbar' + (opts.variant === 'error' ? ' m3-snackbar--error' : '');
+ var icon = opts.variant === 'error' ? 'fa-circle-exclamation' : (opts.icon || null);
+ bar.innerHTML = (icon ? '' : '') +
+ '' + message + '';
+ if (opts.actionLabel) {
+ var action = document.createElement('button');
+ action.type = 'button';
+ action.className = 'm3-snackbar__action';
+ action.textContent = opts.actionLabel;
+ action.addEventListener('click', function () {
+ if (typeof opts.onAction === 'function') opts.onAction();
+ dismiss();
+ });
+ bar.appendChild(action);
+ }
+ region().appendChild(bar);
+ var timer = setTimeout(dismiss, opts.duration || 4000);
+ function dismiss() {
+ clearTimeout(timer);
+ bar.classList.add('is-leaving');
+ setTimeout(function () { bar.remove(); }, 200);
+ }
+ return dismiss;
+ }
+
+ /* ── Tabs ───────────────────────────────────────────────────────────── */
+
+ function activateTab(tab) {
+ var group = tab.closest('[data-m3-tabs]');
+ if (!group) return;
+ var tabs = $$('[role="tab"]', group);
+ tabs.forEach(function (t) {
+ var selected = t === tab;
+ t.setAttribute('aria-selected', String(selected));
+ t.classList.toggle('is-active', selected);
+ t.tabIndex = selected ? 0 : -1;
+ var panelId = t.getAttribute('aria-controls') || (t.getAttribute('href') || '').replace(/^.*#/, '');
+ var panel = panelId ? byId(panelId) : null;
+ if (panel) {
+ panel.hidden = !selected;
+ panel.classList.toggle('is-active', selected);
+ }
+ });
+ }
+
+ function setupTabs(scope) {
+ $$('[data-m3-tabs]', scope).forEach(function (group) {
+ if (group.dataset.m3Bound) return;
+ group.dataset.m3Bound = '1';
+ group.setAttribute('role', 'tablist');
+ var tabs = $$('[role="tab"]', group);
+ tabs.forEach(function (tab, index) {
+ if (!tab.hasAttribute('aria-controls')) {
+ var href = (tab.getAttribute('href') || '').replace(/^.*#/, '');
+ if (href) tab.setAttribute('aria-controls', href);
+ }
+ tab.addEventListener('click', function (event) {
+ event.preventDefault();
+ activateTab(tab);
+ if (global.history && tab.getAttribute('href') && tab.getAttribute('href').charAt(0) === '#') {
+ global.history.replaceState(null, '', tab.getAttribute('href'));
+ }
+ });
+ tab.addEventListener('keydown', function (event) {
+ var step = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0;
+ if (!step) return;
+ event.preventDefault();
+ var next = tabs[(index + step + tabs.length) % tabs.length];
+ next.focus();
+ activateTab(next);
+ });
+ });
+ var initial = tabs.filter(function (t) { return t.getAttribute('aria-selected') === 'true' || t.classList.contains('is-active'); })[0] || tabs[0];
+ if (initial) activateTab(initial);
+ });
+ }
+
+ /* ── Segmented buttons & chips ──────────────────────────────────────── */
+
+ function setupSegmented(scope) {
+ $$('[data-m3-segmented]', scope).forEach(function (group) {
+ if (group.dataset.m3Bound) return;
+ group.dataset.m3Bound = '1';
+ group.setAttribute('role', 'tablist');
+ var items = $$('.m3-segmented__item', group);
+ items.forEach(function (item) {
+ item.setAttribute('role', 'tab');
+ item.setAttribute('aria-selected', String(item.classList.contains('is-selected')));
+ item.addEventListener('click', function () {
+ items.forEach(function (other) {
+ other.classList.toggle('is-selected', other === item);
+ other.setAttribute('aria-selected', String(other === item));
+ });
+ group.dispatchEvent(new CustomEvent('m3segment:change', { detail: { value: item.dataset.value, item: item }, bubbles: true }));
+ });
+ });
+ });
+
+ $$('[data-m3-chip-toggle]', scope).forEach(function (chip) {
+ if (chip.dataset.m3Bound) return;
+ chip.dataset.m3Bound = '1';
+ chip.addEventListener('click', function () {
+ var pressed = chip.getAttribute('aria-pressed') === 'true';
+ chip.setAttribute('aria-pressed', String(!pressed));
+ chip.classList.toggle('m3-chip--selected', !pressed);
+ });
+ });
+ }
+
+ /* ── Fields, sliders, switches ──────────────────────────────────────── */
+
+ function setupFields(scope) {
+ $$('.m3-field__box', scope).forEach(function (box) {
+ var field = box.closest('.m3-field');
+ var input = $('.m3-field__input', box);
+ if (!input || box.dataset.m3Bound) return;
+ box.dataset.m3Bound = '1';
+ var sync = function () {
+ box.classList.toggle('has-value', !!input.value);
+ if (field) field.classList.toggle('m3-field--filled', !!input.value);
+ };
+ input.addEventListener('input', sync);
+ input.addEventListener('change', sync);
+ input.addEventListener('blur', function () {
+ if (field && field.dataset.m3Validate === 'required' && !input.value) {
+ field.classList.add('m3-field--error');
+ }
+ });
+ sync();
+ });
+
+ var syncRange = function (input) {
+ var min = parseFloat(input.min || 0);
+ var max = parseFloat(input.max || 100);
+ var value = parseFloat(input.value);
+ var pct = ((value - min) / (max - min)) * 100;
+ input.style.setProperty('--m3-slider-fill', pct + '%');
+ };
+
+ $$('.m3-slider', scope).forEach(function (input) {
+ if (input.dataset.m3Bound) return;
+ input.dataset.m3Bound = '1';
+ input.addEventListener('input', function () { syncRange(input); });
+ syncRange(input);
+ });
+
+ $$('[data-m3-range-output]', scope).forEach(function (input) {
+ var output = byId(input.getAttribute('data-m3-range-output'));
+ if (!output) return;
+ var update = function () { output.textContent = input.value; };
+ input.addEventListener('input', update);
+ update();
+ });
+ }
+
+ /* ── Theme toggles & copy buttons ───────────────────────────────────── */
+
+ function setupThemeToggles(scope) {
+ $$('.m3-theme-toggle', scope).forEach(function (btn) {
+ if (btn.dataset.m3Bound) return;
+ btn.dataset.m3Bound = '1';
+ btn.addEventListener('click', function (event) {
+ event.preventDefault();
+ if (global.M3Color) {
+ var theme = global.M3Color.toggleTheme();
+ btn.setAttribute('aria-label', theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme');
+ }
+ });
+ });
+ }
+
+ function copy(text) {
+ if (navigator.clipboard && global.isSecureContext) return navigator.clipboard.writeText(text);
+ return new Promise(function (resolve, reject) {
+ try {
+ var area = document.createElement('textarea');
+ area.value = text;
+ area.setAttribute('readonly', '');
+ area.style.cssText = 'position:absolute;left:-9999px;';
+ document.body.appendChild(area);
+ area.select();
+ document.execCommand('copy');
+ area.remove();
+ resolve();
+ } catch (error) { reject(error); }
+ });
+ }
+
+ function setupCopy(scope) {
+ $$('[data-m3-copy]', scope).forEach(function (btn) {
+ if (btn.dataset.m3Bound) return;
+ btn.dataset.m3Bound = '1';
+ btn.addEventListener('click', function (event) {
+ event.preventDefault();
+ var source = btn.getAttribute('data-m3-copy');
+ var field = source ? $(source) : null;
+ var value = field ? (field.value || field.textContent) : source;
+ copy(value.trim()).then(function () {
+ snackbar(btn.getAttribute('data-m3-copy-message') || 'Copied to clipboard');
+ }).catch(function () {
+ snackbar('Could not copy — copy manually: ' + value, { variant: 'error' });
+ });
+ });
+ });
+ }
+
+ /* ── Reveal on scroll ───────────────────────────────────────────────── */
+
+ function setupReveals(scope) {
+ var items = $$('.m3-reveal', scope);
+ if (!items.length) return;
+ if (REDUCED || !('IntersectionObserver' in global)) {
+ items.forEach(function (el) { el.classList.add('is-visible'); });
+ return;
+ }
+ var observer = new IntersectionObserver(function (entries) {
+ entries.forEach(function (entry) {
+ if (!entry.isIntersecting) return;
+ var delay = parseInt(entry.target.getAttribute('data-m3-reveal-delay') || '0', 10);
+ setTimeout(function () { entry.target.classList.add('is-visible'); }, delay);
+ observer.unobserve(entry.target);
+ });
+ }, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
+ items.forEach(function (el) { observer.observe(el); });
+ }
+
+ /* ── Loading state ──────────────────────────────────────────────────── */
+
+ function setLoading(el, loading, label) {
+ if (!el) return;
+ var target = el.classList.contains('m3-button') ? el : el.closest('.m3-button') || el;
+ target.classList.toggle('is-loading', !!loading);
+ target.setAttribute('aria-busy', String(!!loading));
+ if (loading) {
+ target.dataset.m3Label = target.dataset.m3Label || target.textContent.trim();
+ if (!target.querySelector('.sr-only')) {
+ var sr = document.createElement('span');
+ sr.className = 'sr-only';
+ sr.textContent = label || 'Loading';
+ target.appendChild(sr);
+ }
+ } else {
+ var sr = target.querySelector('.sr-only.sr-only--auto');
+ if (sr) sr.remove();
+ }
+ target.disabled = !!loading;
+ }
+
+ /* ── Boot ───────────────────────────────────────────────────────────── */
+
+ function init(scope) {
+ scope = scope || document;
+ $$(RIPPLE_SELECTOR, scope).forEach(attachRipple);
+ setupAppBars(scope);
+ setupDrawers(scope);
+ setupMenus(scope);
+ setupDialogs(scope);
+ setupTabs(scope);
+ setupSegmented(scope);
+ setupFields(scope);
+ setupThemeToggles(scope);
+ setupCopy(scope);
+ setupReveals(scope);
+ }
+
+ global.M3UI = {
+ init: init,
+ snackbar: snackbar,
+ openDialog: openDialog,
+ closeDialog: closeDialog,
+ openDrawer: function (target, open) { setDrawer(byId(target), open); },
+ setLoading: setLoading,
+ copy: copy,
+ lockScroll: lockScroll,
+ unlockScroll: unlockScroll
+ };
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', function () { init(); });
+ } else {
+ init();
+ }
+
+ // Re-scan when pages inject markup dynamically (e.g. the demon list grids).
+ global.addEventListener('m3ui:refresh', function () { init(); });
+
+ // Legacy bridge: elements that toggle drawers via plain classes.
+ document.addEventListener('click', function (event) {
+ var toggle = event.target.closest('[data-m3-drawer-legacy]');
+ if (!toggle) return;
+ var drawer = byId(toggle.getAttribute('data-m3-drawer-legacy'));
+ if (drawer) setTimeout(function () { setDrawer(drawer, drawer.classList.contains('active') || drawer.classList.contains('is-open')); }, 0);
+ });
+})(window);
diff --git a/main/demonlist/best.html b/main/demonlist/best.html
index 01c6071..7df1fa9 100644
--- a/main/demonlist/best.html
+++ b/main/demonlist/best.html
@@ -1,398 +1,128 @@
-
+
Leaderboard - GDIPS Demonlist
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
-
-