From f2de737a6b02fc054ea1c6c8a4e86c8f4e037fa9 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Thu, 30 Apr 2026 00:00:40 +0200 Subject: [PATCH] fix(ux): light-mode contrast, reduced-motion, ARIA state, mobile hit-target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four UX blockers from a multi-persona review of the new light mode and 3D projects cube. Each fix is small, isolated, and ships ahead of the overdoing post going live (which would land more eyes on this surface). 1. Bump `--text-faint` in light tokens from `#6b7280` → `#586273`. The previous value computed to ~4.4:1 on cream `#f4efe4`, failing WCAG AA 4.5:1 for body text. The new value clears AA. This was the smallest text on the page (`.face-tool__desc`, `.face-edge`, `.cube-hint`, `.cube-detail__panel p`) — and the whole "summer / sunlight visibility" thesis hinged on it being legible. 2. cube.js: honor prefers-reduced-motion in openFace / closeFace. Previously the global flag killed the auto-spin but the click-to-open path still ran the 600ms rotate + 300ms zoom-out + 700ms detail-show sequence. Users with vestibular disorders got the full motion anyway. Now both functions short-circuit when reduceMotion is set: snap the rotation, skip the scale animation, open the detail panel synchronously. 3. theme-toggle.js: sync aria-pressed and aria-label to the rendered theme. aria-pressed="true" when light is active (the non-default state). aria-label describes the action a click would take, which is what assistive tech announces. Runs once at load (after the bootstrapper has set data-theme) and again on every click. 4. projects.html mobile media query: `.face-btn { min-height: 44px; padding: 0.6rem 0.9rem; font-size: 0.875rem; }`. The previous ~28px-tall buttons were under iOS's 44×44 hit-target guideline. Co-Authored-By: Claude Opus 4.7 (1M context) --- sass/_variables.scss | 2 +- static/cube.js | 26 +++++++++++++++++++++++--- static/theme-toggle.js | 15 +++++++++++++++ templates/projects.html | 13 +++++-------- 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/sass/_variables.scss b/sass/_variables.scss index 2f2d788..ca98966 100644 --- a/sass/_variables.scss +++ b/sass/_variables.scss @@ -54,7 +54,7 @@ --border-subtle: #d9d1bd; --text: #1a2235; // deep navy — high AA on cream --text-dim: #44516a; - --text-faint: #6b7280; + --text-faint: #586273; // bumped from #6b7280 (4.4:1) to clear AA 4.5:1 on cream — sunlight-legibility fix --accent: #3a5fce; // darkened accent for AA on cream --accent-hover: #2747ad; --accent-glow: rgba(58, 95, 206, 0.10); diff --git a/static/cube.js b/static/cube.js index 1929cfe..06551ee 100644 --- a/static/cube.js +++ b/static/cube.js @@ -155,6 +155,15 @@ }); } + if (reduceMotion) { + // Honor prefers-reduced-motion: snap to target rotation, no zoom/rotate animations. + cube.style.transition = 'none'; + scene.style.transition = 'none'; + apply(); + if (detailPanel) detailPanel.classList.add('cube-detail--open'); + return; + } + // Animate: rotate the cube cube.style.transition = 'transform 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; apply(); @@ -177,12 +186,23 @@ // Hide detail if (detailPanel) detailPanel.classList.remove('cube-detail--open'); - // Zoom back in - scene.style.transform = 'scale(1)'; - // Return to isometric rotX = -30; rotY = 35; + + if (reduceMotion) { + cube.style.transition = 'none'; + scene.style.transition = 'none'; + scene.style.transform = 'scale(1)'; + apply(); + autoSpin = true; + snaps.forEach(function (b) { b.classList.remove('face-btn--active'); }); + return; + } + + // Zoom back in + scene.style.transform = 'scale(1)'; + cube.style.transition = 'transform 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; apply(); diff --git a/static/theme-toggle.js b/static/theme-toggle.js index 1b70fb8..0b84e38 100644 --- a/static/theme-toggle.js +++ b/static/theme-toggle.js @@ -27,9 +27,24 @@ return 'dark'; } + // Sync ARIA state to the rendered theme. aria-pressed="true" when light + // is active (the non-default state); aria-label describes what the click + // would *do*, which is what screen readers announce. + function syncAria() { + var theme = currentTheme(); + btn.setAttribute('aria-pressed', theme === 'light' ? 'true' : 'false'); + btn.setAttribute( + 'aria-label', + theme === 'light' ? 'Switch to dark theme' : 'Switch to light theme' + ); + } + + syncAria(); + btn.addEventListener('click', function () { var next = currentTheme() === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', next); try { localStorage.setItem(STORAGE_KEY, next); } catch (e) { /* private mode */ } + syncAria(); }); })(); diff --git a/templates/projects.html b/templates/projects.html index 76690a1..e823281 100644 --- a/templates/projects.html +++ b/templates/projects.html @@ -91,10 +91,6 @@

{{ section.title }}

Lean 4 scheduling theory - - witness - MC/DC on Wasm IR -
@@ -219,14 +215,13 @@

Build — the pipeline

@@ -532,6 +527,8 @@

Agent — AI-native development

.cube__face--bottom { transform: rotateX(-90deg) translateZ(135px); } .cube-wrap { padding: 2.5rem 0 2rem; } .face-component__name { font-size: 1.1rem; } + /* Hit-target fix — iOS guideline is 44×44 minimum for touchable controls. */ + .face-btn { min-height: 44px; padding: 0.6rem 0.9rem; font-size: 0.875rem; } } @media (prefers-reduced-motion: reduce) {