From 74f51bdd43aa7092c615062cec8c14c061ff1638 Mon Sep 17 00:00:00 2001 From: implexrr <84114379+implexrr@users.noreply.github.com> Date: Thu, 12 Nov 2026 01:43:10 -0500 Subject: [PATCH 1/2] Keep Little Sprout animated until scans finish --- scripts/preview-browser.mjs | 9 +++++++++ src/styles.css | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/scripts/preview-browser.mjs b/scripts/preview-browser.mjs index d8bdb68..539b416 100644 --- a/scripts/preview-browser.mjs +++ b/scripts/preview-browser.mjs @@ -315,6 +315,15 @@ try { assert.equal(await frog.evaluate(node => getComputedStyle(node).animationName), 'none'); assert.equal(await sprout.getAttribute('aria-hidden'), 'true'); assert.equal(await sprout.evaluate(node => getComputedStyle(node).animationName), 'landing-sprite-frames'); + const sproutFrames = await sprout.evaluate(node => { + const animation = node.getAnimations()[0]; + animation.pause(); + return [5400, 6000].map(time => { + animation.currentTime = time; + return getComputedStyle(node).backgroundPositionX; + }); + }); + assert.notEqual(sproutFrames[0], sproutFrames[1], 'Little Sprout must keep moving past two cycles'); await page.emulateMedia({reducedMotion:'reduce'}); assert.equal(await dots.evaluate(node => getComputedStyle(node).animationName), 'none'); for (const sprite of [frog, sprout]) { diff --git a/src/styles.css b/src/styles.css index 5c1b13e..1a33184 100644 --- a/src/styles.css +++ b/src/styles.css @@ -107,9 +107,9 @@ html{scrollbar-gutter:stable} 66.666%,100%{clip-path:inset(0)} } @media (prefers-reduced-motion:no-preference){ - /* Both sprite strips contain 30 frames. End on the resting frame. */ + /* Both sprite strips contain 30 frames. */ .landing-frog-celebrate{animation:landing-sprite-frames 1.5s steps(29,end)} - .scan-sprout{animation:landing-sprite-frames 2.4s steps(29,end) 2} + .scan-sprout{animation:landing-sprite-frames 2.4s steps(29,end) infinite} } @keyframes landing-sprite-frames{to{background-position:100% 0}} .hero h1{font:400 clamp(44px,5.2vw,64px)/1.04 Georgia,serif;letter-spacing:-3px;margin:0} From fd3805b144e7083c0e432fd052ded27ae64ae441 Mon Sep 17 00:00:00 2001 From: implexrr <84114379+implexrr@users.noreply.github.com> Date: Thu, 12 Nov 2026 02:11:28 -0500 Subject: [PATCH 2/2] Add a Warm Cream theme with smooth switching --- scripts/preview-browser.mjs | 67 ++++++++++++++++++++++++++++++++-- src/App.tsx | 2 + src/components/ThemeToggle.tsx | 44 ++++++++++++++++++++++ src/styles.css | 63 ++++++++++++++++++++++++++------ 4 files changed, 161 insertions(+), 15 deletions(-) create mode 100644 src/components/ThemeToggle.tsx diff --git a/scripts/preview-browser.mjs b/scripts/preview-browser.mjs index 539b416..7685e57 100644 --- a/scripts/preview-browser.mjs +++ b/scripts/preview-browser.mjs @@ -154,6 +154,53 @@ try { await route.fulfill(asset ? { body: asset.body, contentType: asset.type } : { status: 404, body: '' }); }); await page.goto('http://kindpath-ui.invalid/'); + const lightMode = page.getByRole('button', {name:'Light mode',exact:true}); + const darkMode = page.getByRole('button', {name:'Dark mode',exact:true}); + const supportsThemeTransition = await page.evaluate(() => { + if (!document.startViewTransition) return false; + const start = document.startViewTransition.bind(document); + window.themeTransitionCount = 0; + document.startViewTransition = update => { + const transition = start(update); + window.themeTransitionCount++; + window.themeTransitionFinished = transition.finished; + return transition; + }; + return true; + }); + const themeTransitionCount = () => page.evaluate(() => window.themeTransitionCount ?? 0); + const switchTheme = async (theme, key = 'Enter') => { + await (theme === 'light' ? lightMode : darkMode).press(key); + await page.waitForFunction(expected => document.documentElement.dataset.theme === expected, theme); + await page.evaluate(() => window.themeTransitionFinished); + }; + const rootTheme = () => page.evaluate(() => ({ + name: document.documentElement.dataset.theme, + background: getComputedStyle(document.documentElement).backgroundColor, + colourScheme: getComputedStyle(document.documentElement).colorScheme, + })); + await lightMode.waitFor(); + assert.deepEqual(await rootTheme(), {name:'dark',background:'rgb(29, 39, 35)',colourScheme:'dark'}); + const themeButton = await lightMode.boundingBox(); + await switchTheme('light'); + assert.deepEqual(await rootTheme(), {name:'light',background:'rgb(250, 248, 239)',colourScheme:'light'}); + assert.deepEqual(await darkMode.boundingBox(), themeButton); + assert.equal(await page.locator('.url-field button').evaluate(node => getComputedStyle(node).backgroundColor), 'rgb(23, 62, 56)'); + const comparisonLabels = page.locator('.landing-comparison-label'); + const panelStyles = () => page.locator('.landing-comparison-panes').evaluate(node => ({ + background: getComputedStyle(node).backgroundColor, + label: getComputedStyle(node.querySelector('.landing-comparison-label')).color, + })); + assert.deepEqual(await panelStyles(), {background:'rgba(0, 0, 0, 0)',label:'rgb(23, 62, 56)'}); + const transitions = supportsThemeTransition ? 1 : 0; + assert.equal(await themeTransitionCount(), transitions); + await page.emulateMedia({reducedMotion:'reduce'}); + await switchTheme('dark', 'Space'); + assert.equal(await themeTransitionCount(), transitions); + assert.deepEqual(await panelStyles(), {background:'rgba(0, 0, 0, 0)',label:'rgb(226, 240, 227)'}); + assert.equal(await comparisonLabels.count(), 2); + await page.emulateMedia({reducedMotion:'no-preference'}); + assert.equal((await rootTheme()).name, 'dark'); const websiteEntry=page.getByLabel('Your website URL'); assert.equal(await websiteEntry.getAttribute('type'),'text'); assert.equal(await websiteEntry.getAttribute('inputmode'),'url'); @@ -366,15 +413,19 @@ try { await page.emulateMedia({reducedMotion:'no-preference'}); assert.equal(await page.locator('.results .manual-card').getByText(/axe-core/).count(), 0); const reportSupportFollows = async content => { - const bottom = await content.evaluate(node => node.getBoundingClientRect().bottom); - const top = await content.evaluate(node => node.getBoundingClientRect().top); - assert.ok(await page.locator('.results .report-exports').evaluate(node => node.getBoundingClientRect().bottom) <= top); + const bottom = await content.evaluate(node => node.getBoundingClientRect().bottom + scrollY); + const top = await content.evaluate(node => node.getBoundingClientRect().top + scrollY); + assert.ok(await page.locator('.results .report-exports').evaluate(node => node.getBoundingClientRect().bottom + scrollY) <= top); for (const block of [ page.locator('.results').getByText('Automated results for the final scanned page only. Manual review is still required.', {exact:true}), page.locator('.results .scan-coverage'), - ]) assert.ok(await block.evaluate(node => node.getBoundingClientRect().top) >= bottom); + ]) assert.ok(await block.evaluate(node => node.getBoundingClientRect().top + scrollY) >= bottom); }; await reportSupportFollows(page.locator('.results .findings-layout')); + await switchTheme('light'); + assert.equal(await page.locator('.finding').first().evaluate(node => getComputedStyle(node).backgroundColor), 'rgb(255, 254, 250)'); + assert.equal(await page.locator('.report-exports').evaluate(node => getComputedStyle(node).backgroundColor), 'rgb(232, 237, 223)'); + await switchTheme('dark'); assert.equal(await page.getByText('Automated page scan', { exact: true }).count(), 1); const coverageNotice=page.locator('.results').getByRole('region',{name:'Scan coverage',exact:true}); @@ -551,6 +602,14 @@ try { assert.equal(await page.getByRole('status').filter({hasText: /KindPath received a .* screen/}).count(), 0); assert.equal(await page.locator('.vision-controls').first().evaluate(node => getComputedStyle(node).backgroundColor), 'rgb(41, 58, 48)'); const imageFrames=page.locator('iframe'); + const lightThemeSources = await imageFrames.evaluateAll(frames => frames.map(frame => frame.getAttribute('srcdoc'))); + await page.setViewportSize({width:320,height:844}); + await switchTheme('light'); + assert.equal(await page.locator('.vision-controls').first().evaluate(node => getComputedStyle(node).backgroundColor), 'rgb(232, 237, 223)'); + assert.deepEqual(await imageFrames.evaluateAll(frames => frames.map(frame => frame.getAttribute('srcdoc'))), lightThemeSources); + assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)); + await switchTheme('dark'); + await page.setViewportSize({width:1280,height:900}); for(let i=0;i<2;i++){ const frame=page.frameLocator('iframe').nth(i); await frame.locator('img').evaluate(img=>img.decode()); diff --git a/src/App.tsx b/src/App.tsx index 9fa41f1..c773fc3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import type { Report } from '../shared/types'; import { normalizeWebsiteEntry } from './components/websiteEntry'; import { scanFailureFeedback } from './components/scanFeedback'; import type { ScanFailureFeedback } from './components/scanFeedback'; +import { ThemeToggle } from './components/ThemeToggle'; import { WebsiteForm } from './components/WebsiteForm'; import { ReportExports } from './components/ReportExports'; import { ScanCoverageNotice } from './components/ScanCoverageNotice'; @@ -137,6 +138,7 @@ export function App() { Skip to content
KindPath +
diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..e4cb702 --- /dev/null +++ b/src/components/ThemeToggle.tsx @@ -0,0 +1,44 @@ +import { useLayoutEffect, useState } from 'react'; +import { flushSync } from 'react-dom'; + +export function ThemeToggle() { + const [light, setLight] = useState(false); + useLayoutEffect(() => { + const root = document.documentElement; + const theme = light ? 'light' : 'dark'; + if (root.dataset.theme === theme) return; + + root.dataset.themeSwitching = ''; + root.dataset.theme = theme; + // Let the new palette paint before restoring hover and press transitions. + let frame = requestAnimationFrame(() => { + frame = requestAnimationFrame(() => { delete root.dataset.themeSwitching; }); + }); + return () => { + cancelAnimationFrame(frame); + delete root.dataset.themeSwitching; + }; + }, [light]); + + function toggleTheme() { + const update = () => setLight(value => !value); + if (!document.startViewTransition || window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + update(); + return; + } + + // Capture the old and new palettes as complete frames so controls stay readable. + const transition = document.startViewTransition(() => flushSync(update)); + void transition.ready.catch(() => {}); // An overlapping switch can skip the previous transition. + } + + return ; +} diff --git a/src/styles.css b/src/styles.css index 1a33184..4d72374 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,5 +1,5 @@ html{scrollbar-gutter:stable} -/* App chrome uses the forest and sage palette. Preview artwork retains its own colours. */ +/* The palettes apply to app chrome. Preview artwork retains its own colours. */ :root{ font-family:Arial,Helvetica,sans-serif; font-synthesis:none; @@ -17,6 +17,8 @@ html{scrollbar-gutter:stable} --control-hover:#304239; --control-pressed:#202e28; --green:#173e38; + --on-action:var(--green); + --focus:var(--action); --error:#ffb9b0; --notice-background:#362e23; --notice-line:#776345; @@ -24,11 +26,42 @@ html{scrollbar-gutter:stable} color:var(--text); background:var(--background); } -@media screen{:root{color-scheme:dark}} -*{box-sizing:border-box}body{margin:0}button,input,select{font:inherit}a{color:inherit}button,a,input,select,summary{touch-action:manipulation}button{cursor:pointer}button:disabled{cursor:wait;opacity:.65}:focus-visible{outline:3px solid var(--action);outline-offset:5px}.skip-link{position:absolute;top:-60px;left:20px;background:var(--action);color:var(--green);padding:12px;z-index:10}.skip-link:focus{top:12px}.filter-defs{position:absolute}.site-header,main,footer{max-width:1440px;margin-inline:auto}.brand{font-size:24px;font-weight:750;text-decoration:none;display:flex;align-items:center;gap:10px;letter-spacing:-1px}.eyebrow{font-size:10px;font-weight:700;letter-spacing:1.6px;display:block}h1{font-family:Georgia,serif;font-size:clamp(48px,5.1vw,76px);font-weight:400;line-height:1.08;letter-spacing:-3px;margin:27px 0}h1 em{font-weight:400;color:var(--sage)}form label,.preview-controls label{display:block;font-size:12px;font-weight:700;margin-bottom:10px}.form-note{font-size:11px;color:var(--muted);margin:11px 0}.error{color:var(--error);font-size:13px}.comparison-slice{position:absolute;inset:0 0 0 52%;background:#829d5730;backdrop-filter:blur(1px) saturate(.2);border-left:2px solid white;pointer-events:none}.step{font-size:9px;letter-spacing:1.6px;color:#5c6b5c}.results{scroll-margin-top:24px;padding:65px 6%}.section-heading{display:flex;justify-content:space-between;align-items:center;gap:20px}.section-heading h2{font:400 35px/1.2 Georgia,serif;letter-spacing:-1px;margin:12px 0}.report-url{color:var(--muted);font-size:12px;overflow-wrap:anywhere}.demo-badge{border:1px solid var(--control-line);border-radius:30px;padding:8px 12px;white-space:nowrap;font-size:11px}.notice{padding:14px 18px;border:1px solid var(--notice-line);background:var(--notice-background);font-size:12px;line-height:1.7;color:var(--notice-text);border-radius:5px;margin:22px 0}.findings-layout{display:grid;grid-template-columns:2fr 1fr;gap:44px}.finding-intro h3{font-size:17px;margin:0 0 8px}.finding-intro p{font-size:12px;color:var(--muted);margin-bottom:25px}.finding{display:flex;gap:20px;background:var(--surface);border:1px solid var(--line);border-radius:7px;padding:24px;margin:14px 0}.severity{font-size:9px;text-transform:uppercase;letter-spacing:1px;border-radius:4px;padding:4px 7px;display:inline-block}.serious{background:#4e362b;color:#ffd5bd}.moderate{background:#463d26;color:#f1d9aa}.finding h3{font-size:16px;line-height:1.5;margin:10px 0 6px}.finding p{color:var(--muted);font-size:13px;line-height:1.7}.finding .location{font-size:11px;color:var(--muted)}summary{font-size:12px;cursor:pointer;font-weight:bold}.manual-card{align-self:start;background:var(--panel);padding:32px;border-radius:7px}.manual-card>span{font-size:30px}.manual-card h3{font:400 29px/1.25 Georgia,serif;margin:15px 0}.manual-card p,.manual-card li{color:var(--muted);font-size:13px;line-height:1.8}.manual-card h4{font-size:12px;margin:27px 0 12px}.manual-card ul{padding-left:17px}.manual-card li{margin:10px 0}.preview-controls{display:flex;gap:30px;align-items:end;padding:20px;background:var(--panel);border-radius:6px}.preview-controls select{padding:10px;border:1px solid var(--control-line);border-radius:4px;background:var(--surface);color:var(--text);max-width:100%}.strength{flex:1}.strength label{display:flex;justify-content:space-between;gap:20px}.strength input{width:100%;accent-color:var(--action);height:35px}.secondary{border:1px solid;border-radius:7px;padding:12px;font-size:12px}.effect-description,.preview-caption{font-size:12px;color:var(--muted);line-height:1.8}.comparison{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:28px}.comparison h3{font-size:12px;font-weight:400;display:flex;align-items:center;gap:8px}.status-dot{display:inline-block;border:1px solid var(--action);border-radius:50%;width:7px;height:7px}.status-dot.altered{background:var(--action)}.field-mask{position:absolute;inset:0;pointer-events:none}@media(min-width:1400px){}@media(max-width:1000px){.preview-controls{flex-wrap:wrap}.findings-layout{gap:24px}}@media(max-width:700px){h1{font-size:59px}.findings-layout,.comparison{grid-template-columns:1fr}.section-heading{align-items:flex-start;flex-direction:column}.section-heading h2{font-size:30px}.results{padding-top:45px}.preview-controls{gap:20px}.preview-controls>div{width:100%}.strength{flex:auto}.finding{padding:19px;gap:14px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;transition:none!important}} +:root[data-theme=light]{ + --background:#faf8ef; + --text:#173e38; + --muted:#4e6257; + --sage:#526b4d; + --line:#d5ded0; + --control-line:#768777; + --panel:#e8eddf; + --surface:#fffefa; + --action:#173e38; + --action-hover:#245448; + --action-pressed:#12322d; + --on-action:#fffefa; + --control-hover:#dce7d6; + --control-pressed:#cbdcc8; + --focus:#286456; + --error:#8f2b25; + --notice-background:#f6edd8; + --notice-line:#997f52; + --notice-text:#604719; +} +@media screen{ + :root{color-scheme:dark} + :root[data-theme=light]{color-scheme:light} +} +@media (prefers-reduced-motion:no-preference){ + ::view-transition{pointer-events:none} + ::view-transition-group(root),::view-transition-old(root),::view-transition-new(root){ + animation-duration:180ms; + animation-timing-function:ease-in-out; + } +} +*{box-sizing:border-box}body{margin:0}button,input,select{font:inherit}a{color:inherit}button,a,input,select,summary{touch-action:manipulation}button{cursor:pointer}button:disabled{cursor:wait;opacity:.65}:focus-visible{outline:3px solid var(--focus);outline-offset:5px}.skip-link{position:absolute;top:-60px;left:20px;background:var(--action);color:var(--on-action);padding:12px;z-index:10}.skip-link:focus{top:12px}.filter-defs{position:absolute}.site-header,main,footer{max-width:1440px;margin-inline:auto}.brand{font-size:24px;font-weight:750;text-decoration:none;display:flex;align-items:center;gap:10px;letter-spacing:-1px}.eyebrow{font-size:10px;font-weight:700;letter-spacing:1.6px;display:block}h1{font-family:Georgia,serif;font-size:clamp(48px,5.1vw,76px);font-weight:400;line-height:1.08;letter-spacing:-3px;margin:27px 0}h1 em{font-weight:400;color:var(--sage)}form label,.preview-controls label{display:block;font-size:12px;font-weight:700;margin-bottom:10px}.form-note{font-size:11px;color:var(--muted);margin:11px 0}.error{color:var(--error);font-size:13px}.comparison-slice{position:absolute;inset:0 0 0 52%;background:#829d5730;backdrop-filter:blur(1px) saturate(.2);border-left:2px solid white;pointer-events:none}.step{font-size:9px;letter-spacing:1.6px;color:#5c6b5c}.results{scroll-margin-top:24px;padding:65px 6%}.section-heading{display:flex;justify-content:space-between;align-items:center;gap:20px}.section-heading h2{font:400 35px/1.2 Georgia,serif;letter-spacing:-1px;margin:12px 0}.report-url{color:var(--muted);font-size:12px;overflow-wrap:anywhere}.demo-badge{border:1px solid var(--control-line);border-radius:30px;padding:8px 12px;white-space:nowrap;font-size:11px}.notice{padding:14px 18px;border:1px solid var(--notice-line);background:var(--notice-background);font-size:12px;line-height:1.7;color:var(--notice-text);border-radius:5px;margin:22px 0}.findings-layout{display:grid;grid-template-columns:2fr 1fr;gap:44px}.finding-intro h3{font-size:17px;margin:0 0 8px}.finding-intro p{font-size:12px;color:var(--muted);margin-bottom:25px}.finding{display:flex;gap:20px;background:var(--surface);border:1px solid var(--line);border-radius:7px;padding:24px;margin:14px 0}.severity{font-size:9px;text-transform:uppercase;letter-spacing:1px;border-radius:4px;padding:4px 7px;display:inline-block}.serious{background:#4e362b;color:#ffd5bd}.moderate{background:#463d26;color:#f1d9aa}.finding h3{font-size:16px;line-height:1.5;margin:10px 0 6px}.finding p{color:var(--muted);font-size:13px;line-height:1.7}.finding .location{font-size:11px;color:var(--muted)}summary{font-size:12px;cursor:pointer;font-weight:bold}.manual-card{align-self:start;background:var(--panel);padding:32px;border-radius:7px}.manual-card>span{font-size:30px}.manual-card h3{font:400 29px/1.25 Georgia,serif;margin:15px 0}.manual-card p,.manual-card li{color:var(--muted);font-size:13px;line-height:1.8}.manual-card h4{font-size:12px;margin:27px 0 12px}.manual-card ul{padding-left:17px}.manual-card li{margin:10px 0}.preview-controls{display:flex;gap:30px;align-items:end;padding:20px;background:var(--panel);border-radius:6px}.preview-controls select{padding:10px;border:1px solid var(--control-line);border-radius:4px;background:var(--surface);color:var(--text);max-width:100%}.strength{flex:1}.strength label{display:flex;justify-content:space-between;gap:20px}.strength input{width:100%;accent-color:var(--action);height:35px}.secondary{border:1px solid;border-radius:7px;padding:12px;font-size:12px}.effect-description,.preview-caption{font-size:12px;color:var(--muted);line-height:1.8}.comparison{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:28px}.comparison h3{font-size:12px;font-weight:400;display:flex;align-items:center;gap:8px}.status-dot{display:inline-block;border:1px solid var(--action);border-radius:50%;width:7px;height:7px}.status-dot.altered{background:var(--action)}.field-mask{position:absolute;inset:0;pointer-events:none}@media(min-width:1400px){}@media(max-width:1000px){.preview-controls{flex-wrap:wrap}.findings-layout{gap:24px}}@media(max-width:700px){h1{font-size:59px}.findings-layout,.comparison{grid-template-columns:1fr}.section-heading{align-items:flex-start;flex-direction:column}.section-heading h2{font-size:30px}.results{padding-top:45px}.preview-controls{gap:20px}.preview-controls>div{width:100%}.strength{flex:auto}.finding{padding:19px;gap:14px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;transition:none!important}} /* Fixture snapshots retain their own styles inside opaque, script-free frames. */ -.snapshot-status{font-size:12px;color:var(--muted);min-height:20px;margin-bottom:14px}.snapshot-placeholder{border:1px dashed var(--control-line);background:var(--panel);border-radius:7px;padding:36px 24px;text-align:center}.snapshot-placeholder h3{font:400 25px/1.3 Georgia,serif;margin:0 0 12px}.snapshot-placeholder p{font-size:13px;line-height:1.8;color:var(--muted)}.snapshot-window{position:relative;border:1px solid var(--line);border-radius:6px;overflow:hidden;background:var(--panel);min-width:0}.snapshot-frame{display:block;width:100%;height:560px;border:0;background:#f5f2e8;color-scheme:light}.snapshot-window:focus-within{outline:3px solid var(--action);outline-offset:3px}.comparison>div{min-width:0}@media(max-width:700px){.snapshot-frame{height:500px}} +.snapshot-status{font-size:12px;color:var(--muted);min-height:20px;margin-bottom:14px}.snapshot-placeholder{border:1px dashed var(--control-line);background:var(--panel);border-radius:7px;padding:36px 24px;text-align:center}.snapshot-placeholder h3{font:400 25px/1.3 Georgia,serif;margin:0 0 12px}.snapshot-placeholder p{font-size:13px;line-height:1.8;color:var(--muted)}.snapshot-window{position:relative;border:1px solid var(--line);border-radius:6px;overflow:hidden;background:var(--panel);min-width:0}.snapshot-frame{display:block;width:100%;height:560px;border:0;background:#f5f2e8;color-scheme:light}.snapshot-window:focus-within{outline:3px solid var(--focus);outline-offset:3px}.comparison>div{min-width:0}@media(max-width:700px){.snapshot-frame{height:500px}} /* Vision settings are native controls; effects stay outside the isolated snapshot document. */ .vision-controls{background:var(--panel);border:1px solid var(--line);border-radius:7px;padding:24px;margin-top:12px}.vision-note,.colour-choice p,.effect-control p{font-size:12px;line-height:1.7;color:var(--muted)}.intensity-presets{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.intensity-presets{margin-top:22px}.intensity-presets>span{font-size:12px;font-weight:bold;margin-right:5px}.vision-note{margin:10px 0 18px}.colour-choice{display:grid;grid-template-columns:auto minmax(150px,230px) 1fr;align-items:center;gap:14px;margin-bottom:20px}.colour-choice label,.effect-label label{font-size:12px;font-weight:bold}.colour-choice p{margin:0}.effect-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.effect-control{background:var(--surface);border:1px solid var(--line);border-radius:5px;padding:16px;min-width:0}.effect-label{display:flex;justify-content:space-between;gap:14px}.effect-label output{font-size:12px;color:var(--action)}.effect-control input{width:100%;accent-color:var(--action);height:32px;margin:8px 0} @@ -39,6 +72,12 @@ html{scrollbar-gutter:stable} /* Findings keep each element's location next to its own repair guidance. */ .findings-main{min-width:0}.severity-counts{display:flex;flex-wrap:wrap;gap:10px;list-style:none;padding:0;margin:20px 0}.severity-counts li{padding:8px 12px;border:1px solid var(--line);border-radius:4px;font-size:12px;background:var(--panel)}.severity-counts strong{margin-left:8px}.finding-filter{display:flex;align-items:center;flex-wrap:wrap;gap:12px}.finding-filter label{font-size:13px;font-weight:bold}.finding-status{font-size:12px;color:var(--muted);margin:14px 0}.finding h4,.review-finding h4{font-size:17px;line-height:1.5;margin:12px 0 8px}.finding>div:last-child{min-width:0}.findings-empty{padding:20px;border:1px dashed var(--control-line);border-radius:5px;line-height:1.8;font-size:14px}.element-guidance ol{padding-left:22px}.element-guidance li{padding:4px 0 12px}.element-target strong{display:block}.element-target code{display:block;white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px;margin-top:6px}.element-summary{white-space:pre-wrap;overflow-wrap:anywhere}.finding p,.review-finding p{overflow-wrap:anywhere}.review-findings{margin-top:32px;border-top:1px solid var(--line);padding-top:20px}.review-findings>p,.review-finding p{font-size:13px;line-height:1.8;color:var(--muted)}.review-finding{padding:20px;border:1px solid var(--line);border-radius:5px;margin-top:16px;min-width:0}.review-label{font-size:11px;font-weight:bold;letter-spacing:.5px;color:var(--notice-text)}.review-finding summary{font-size:12px;cursor:pointer;text-decoration:underline}.severity.critical{background:#522e2d;color:#ffd2ce}.severity.minor{background:#314537;color:#d7eccb}.severity.unknown{background:#39433e;color:#e2f0e3} +:root[data-theme=light] .severity.critical{background:#f6dad7;color:#7c2722} +:root[data-theme=light] .severity.serious{background:#f2ded3;color:#6e3524} +:root[data-theme=light] .severity.moderate{background:#f6edd8;color:#604719} +:root[data-theme=light] .severity.minor{background:#dce7d6;color:#365540} +:root[data-theme=light] .severity.unknown{background:#e6e9e2;color:#405147} + .print-report{display:none}.report-exports{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:20px;border:1px solid var(--line);border-radius:6px;padding:20px;margin:22px 0;background:var(--panel)}.report-exports h3{font-size:16px;margin:0 0 8px}.report-exports p{font-size:12px;line-height:1.7;color:var(--muted);margin:4px 0}.report-export-actions{display:flex;gap:12px;flex-wrap:wrap}.report-export-actions a{display:inline-block;text-decoration:none}.report-exports>p[role=alert]{color:var(--error);width:100%} @media print{ @@ -78,7 +117,7 @@ html{scrollbar-gutter:stable} .results:focus{outline:none} .results:focus-visible #report-title{ text-decoration:underline; - text-decoration-color:var(--action); + text-decoration-color:var(--focus); text-decoration-thickness:2px; text-underline-offset:.18em; } @@ -89,7 +128,9 @@ html{scrollbar-gutter:stable} #root > .site-header,#root > main,#root > footer{width:100%;flex-shrink:0} #root > main{flex-grow:1} } -.site-header{height:64px;padding-inline:6%;display:flex;align-items:center;border-bottom:1px solid var(--line)} +.site-header{height:64px;padding-inline:6%;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--line)} +.theme-toggle{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-width:116px;min-height:44px;padding:8px 12px;white-space:nowrap} +.theme-toggle svg{flex:none} .hero{display:grid;grid-template-columns:minmax(0,560px);justify-content:center;align-items:center;padding:40px 6%} .hero-heading{display:flex;align-items:center;gap:12px;margin-bottom:20px} .hero .hero-heading h1{margin:0} @@ -116,7 +157,7 @@ html{scrollbar-gutter:stable} .hero-copy>p{max-width:55ch;margin:0 0 15px;color:var(--muted);line-height:1.6} .hero form label{font-size:13px;margin-bottom:9px} .url-field{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:6px;border:1px solid var(--muted);border-radius:12px;padding:5px;background:var(--surface)} -.url-field:has(input:focus){border-color:var(--action);box-shadow:0 0 0 1px var(--action)} +.url-field:has(input:focus){border-color:var(--focus);box-shadow:0 0 0 1px var(--focus)} .url-field input{width:100%;min-width:0;padding:12px;border:0;border-radius:7px;background:transparent;color:var(--text);font-size:16px;outline:none} .url-field input::placeholder{color:var(--muted);opacity:1} .url-field button{min-height:44px;min-width:154px;padding:12px;white-space:nowrap;border:0;border-radius:7px;font-size:14px;font-weight:700} @@ -128,7 +169,7 @@ html{scrollbar-gutter:stable} .hero .form-note{font-size:12px;line-height:1.6;margin:10px 0 0;color:var(--muted)} .landing-comparison{min-width:0;margin:0;background:var(--panel);border:1px solid var(--line);padding:16px;border-radius:12px} -.landing-comparison-panes{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;padding:10px;border-radius:8px;background:#e8eddf;color:var(--green)} +.landing-comparison-panes{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;padding:10px;border-radius:8px;background:transparent;color:var(--text)} .landing-comparison-label{font-size:12px;font-weight:700;margin-bottom:8px} .landing-comparison-panes>div{min-width:0} .landing-comparison svg{display:block;width:100%;height:auto;border-radius:6px;border:1px solid #cdd5c6} @@ -188,7 +229,7 @@ footer a{display:inline-flex;align-items:center;min-height:24px;margin-inline-st --button-hover:var(--action-hover); --button-pressed:var(--action-pressed); --button-line:var(--action); - color:var(--green); + color:var(--on-action); } .secondary[aria-pressed=true]{--button-hover:var(--action)} .secondary,.url-field button{ @@ -262,7 +303,7 @@ footer a{display:inline-flex;align-items:center;min-height:24px;margin-inline-st } .view-switch button span{margin-inline-start:4px} :is(.secondary,.url-field button,.view-switch button,.results select):focus-visible{ - outline:2px solid var(--action); + outline:2px solid var(--focus); outline-offset:3px; } @media (hover:hover){ @@ -279,7 +320,7 @@ footer a{display:inline-flex;align-items:center;min-height:24px;margin-inline-st } .view-switch button[aria-pressed]:active{background:var(--control-pressed)} @media (prefers-reduced-motion:no-preference){ - :is(.secondary,.url-field button,.view-switch button,.results select){ + :root:not([data-theme-switching]) :is(.secondary,.url-field button,.view-switch button,.results select){ transition:background-color 120ms ease,border-color 120ms ease,box-shadow 120ms ease; } }