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 + + + - - - + + + + + + + + + - -
-
-
-
+
-
-

Top Players Leaderboard

+
+
+

Top Players Leaderboard

+

Ranked by weighted points across every verified GDIPS completion

+
-
- - +
-
-
- - - - - - - - - - - - - - -
#PlayerPointsDemonsCPLast Record
Loading...
+
+ +
+ + + + + + + + + + + + + + + +
GDIPS demon list leaderboard
#PlayerPointsDemonsCPLast Record
Loading…
-
+ + +