From 33e4e523ca53f53e47f78836dc02b6c498384f25 Mon Sep 17 00:00:00 2001 From: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 06:40:17 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(style):=20honour=20prefers-r?= =?UTF-8?q?educed-motion=20for=20in-page=20smooth=20scrolling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> --- scripts/style-contract.test.mjs | 39 +++++++++++++++++++++++++++++++++ style.css | 3 ++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/scripts/style-contract.test.mjs b/scripts/style-contract.test.mjs index be6a9c9..691292a 100644 --- a/scripts/style-contract.test.mjs +++ b/scripts/style-contract.test.mjs @@ -691,6 +691,45 @@ test("style.css: self-hosted @font-face files are committed and every committed assert.deepEqual(unreferencedFontFiles(fonts, referencedAssets), []); }); +test("style.css: every scroll-behavior: smooth is inside @media (prefers-reduced-motion: no-preference)", () => { + const css = readFileSync(stylesheets.get("style.css").file, "utf8"); + const clean = stripComments(css); + const errors = []; + const mediaStack = []; + let braceDepth = 0; + for (let i = 0; i < clean.length; i++) { + if (clean[i] === "{") { + const atMediaIdx = clean.lastIndexOf("@media", i); + const lastCloseIdx = clean.lastIndexOf("}", i); + if (atMediaIdx !== -1 && atMediaIdx > lastCloseIdx) { + const prelude = clean.substring(atMediaIdx, i); + mediaStack.push(prelude); + } else { + mediaStack.push(null); + } + braceDepth++; + } else if (clean[i] === "}") { + braceDepth--; + if (mediaStack.length > 0) mediaStack.pop(); + } + if (clean.startsWith("scroll-behavior", i)) { + const colonIdx = clean.indexOf(":", i); + const declEnd = clean.indexOf(";", colonIdx); + if (colonIdx !== -1 && declEnd !== -1) { + const decl = clean.substring(colonIdx + 1, declEnd).trim(); + if (decl === "smooth") { + const hasNoPreference = mediaStack.some((m) => m && /prefers-reduced-motion\s*:\s*no-preference/.test(m)); + if (!hasNoPreference) { + const line = clean.substring(0, i).split("\n").length; + errors.push(`scroll-behavior: smooth at line ${line} is not inside @media (prefers-reduced-motion: no-preference)`); + } + } + } + } + } + assert.deepEqual(errors, []); +}); + test("index.html: the carousel and ACMM state classes are the ones the stub-DOM tests exercise", () => { const index = contentPages.find((p) => p.rel === "index.html"); const toggled = scriptToggledClasses(index.html); diff --git a/style.css b/style.css index b8a038d..8248ec5 100644 --- a/style.css +++ b/style.css @@ -89,7 +89,8 @@ /* ---------- base ---------- */ *, *::before, *::after { box-sizing: border-box; } -html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; } +html { -webkit-text-size-adjust: 100%; } +@media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } } body { margin: 0; background: var(--bg);