openvibers/openvibe.tools Commits
Motion: Calm reduces, only Off removes (Shared 4.184.0) (#273)
Changes
@@@ -591,4 +591,4 @@ a:hover { color: var(--accent-hover); }/* The page is the shared tool frame: it starts under the navbar, not behind it. */.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }--@media (prefers-reduced-motion: reduce) { .option-group, .result-panel, .toast { animation: none; } .spinner { animation: none; opacity: .7; } }++html[data-ov-motion=off] .spinner{opacity: .7}@@@ -481,4 +481,4 @@ body {/* The page is the shared tool frame: it starts under the navbar, not behind it. */.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }--@media (prefers-reduced-motion: reduce) { .spinner { animation: none; opacity: .7; } }++html[data-ov-motion=off] .spinner{opacity: .7}@@@ -247,4 +247,4 @@ body { min-height:100%; font-family:var(--font); background:var(--bg); color:var/* The page is the shared tool frame: it starts under the navbar, not behind it. */.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }--@media (prefers-reduced-motion: reduce) { .spinner { animation: none; opacity: .7; } }++html[data-ov-motion=off] .spinner{opacity: .7}@@@ -235,6 +235,5 @@});</script><script src="https://openvibe.live/live-notify.js" async></script>-- <style>@media (prefers-reduced-motion: reduce) { .showcase-tool, .example-btn, .stat-card { animation: none; } }</style></body></html>@@@ -141,7 +141,6 @@ select{appearance:none;background-image:linear-gradient(45deg,transparent 50%,va.facts span{display:block;font-size:.7rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--c-tx3)}.dl{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px}@media (max-width:900px){.studio{grid-template-columns:minmax(0,1fr)}.preview{position:static;order:-1}.stage{min-height:220px}.stage .art,.stage .art svg{max-height:260px}}--@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}/* ── Hub ── */.smart{padding:18px;border-radius:calc(var(--r) + 4px);background:linear-gradient(135deg,color-mix(in srgb,var(--c-ac) 14%,var(--c-card)),var(--c-card));border:1px solid var(--c-brd2);display:grid;grid-template-columns:minmax(0,1fr) 220px;gap:18px;align-items:center;margin-bottom:22px}@@@ -549,6 +549,5 @@ function sample(){const t=T[ACT];if(t?.p){$('in1').value=t.p;if(t.p2&&$('in2'))$function toast(msg){const t=$('toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),2000)}</script><script src="https://openvibe.live/live-notify.js" async></script>-- <style>@media (prefers-reduced-motion: reduce) { .bg-glow::before, .bg-glow::after { animation: none; } }</style></body></html>@@@ -235,6 +235,5 @@});</script><script src="https://openvibe.live/live-notify.js" async></script>-- <style>@media (prefers-reduced-motion: reduce) { .showcase-tool, .example-btn, .stat-card { animation: none; } }</style></body></html>@@@ -172,12 +172,12 @@})();// The headline's phrases are links already in the page; this shows them in turn, once through, then rests on the--// first. The button pauses, or plays it again. Reduced motion keeps the first. Never an aria-live region.++// first. The button pauses, or plays it again. Motion off keeps the first; Calm rotates as usual. Never an aria-live region.(function () {const el = document.getElementById('tool-headline');const items = el ? Array.from(el.querySelectorAll('.hm-ph')) : [];if (items.length < 2) return;-- const motion = window.matchMedia('(prefers-reduced-motion: reduce)');++ const still = () => document.documentElement.getAttribute('data-ov-motion') === 'off';const control = document.getElementById('headline-motion');let i = 0, timer = 0, paused = false, rested = false;const show = (n) => {@@@ -190,8 +190,8 @@};const update = () => {clearInterval(timer); timer = 0;-- if (control) control.hidden = motion.matches;-- if (motion.matches) { show(0); return; }++ if (control) control.hidden = still();++ if (still()) { show(0); return; }if (!paused && !rested && !document.hidden) timer = setInterval(() => {if (el.matches(':hover') || el.contains(document.activeElement)) return;const next = (i + 1) % items.length;@@@ -203,7 +203,7 @@control.setAttribute('aria-label', say); control.title = say; control.classList.toggle('is-paused', paused || rested);};if (control) control.addEventListener('click', () => { if (rested) rested = paused = false; else paused = !paused; update(); });-- motion.addEventListener('change', update);++ window.addEventListener('ov:display', update);document.addEventListener('visibilitychange', update);update();})();@@@ -1428,6 +1428,5 @@}</script><script src="https://openvibe.live/live-notify.js" async></script>-- <style>@media (prefers-reduced-motion: reduce) { .bg-glow::before, .bg-glow::after, .toast, .showcase-tool, .stat-card { animation: none; } }</style></body></html>@@@ -127,7 +127,7 @@ details{border-bottom:1px solid var(--border,rgba(255,255,255,.08));padding:12pxtransition:opacity var(--ov-dur-3,.32s) var(--ov-ease-out,ease-out),transform var(--ov-dur-3,.32s) var(--ov-ease-out,ease-out),visibility 0s linear var(--ov-dur-3,.32s)}.hm-ph.is-on{opacity:1;visibility:visible;transform:none;transition-delay:0s}.hm-ph.is-out{transform:translateY(-.35em)}.hm-ph:hover,.hm-ph:focus-visible{text-decoration:underline;text-decoration-thickness:.06em;text-underline-offset:.12em}--@media (prefers-reduced-motion:reduce){.hm-ph{transition:none;transform:none}}++html[data-ov-motion=off] .hm-ph{transform:none}/* The strip of tools drifts, so its ends fade: a chip cut by a hard edge looked broken. */.quick-strip{-webkit-mask-image:linear-gradient(90deg,transparent,#000 22px,#000 calc(100% - 22px),transparent);mask-image:linear-gradient(90deg,transparent,#000 22px,#000 calc(100% - 22px),transparent);padding-inline:14px;margin-inline:-14px;max-width:calc(100% + 28px)}/* The headline's pause: a small round icon beside the words, not a line of its own. */@@@ -222,7 +222,6 @@ background:color-mix(in srgb,var(--text-primary,#e8ecf3) 8%,transparent);color:v/* The home on phones (2026-10-29): the examples are one row that scrolls sideways (all four still there), and four of the mostopened tools show (the families below and the A to Z list hold the rest). */@media (max-width:599px){.start-grid{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:16px;scrollbar-width:none;gap:10px;margin:0 -16px;padding:0 16px 4px}.start-grid::-webkit-scrollbar{display:none}.start-grid>*{flex:0 0 84%;min-width:0;scroll-snap-align:start}section[aria-labelledby="top-h"] .grid>:nth-child(n+5){display:none}}--@media (prefers-reduced-motion:reduce){.tool{transition:none}}${icons.CSS}/* Touch targets on phones (fleet audit, 2026-10-29): touch pointers only. */@media (pointer: coarse) {@@@ -32,10 +32,11 @@ const site = require('../server/pages/site');assert.strictEqual(new Set(links.map((m) => m[2])).size, links.length, 'each phrase is a different tool');assert.ok(!home.includes('data-phrases='), 'the phrases are links in the page, not a data attribute');-- // The swap is CSS on shared timing, still under reduced motion; the script goes once through, then rests.++ // The swap is CSS on shared timing, played under Calm and held only with motion off; the script goes once through, then rests.assert.match(home, /\.hm-rot\{display:grid\}\.hm-ph\{grid-area:1\/1;/, 'the phrases share one cell, so nothing below moves');-- assert.match(home, /@media \(prefers-reduced-motion:reduce\)\{\.hm-ph\{transition:none;transform:none\}\}/);++ assert.match(home, /html\[data-ov-motion=off\] \.hm-ph\{transform:none\}/);const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'site.js'), 'utf8');++ assert.ok(js.includes("getAttribute('data-ov-motion') === 'off'"), 'only motion off holds the first phrase');assert.ok(js.includes('if (next === 0) { rested = true;'), 'once through, then it rests');assert.ok(js.includes("el.matches(':hover') || el.contains(document.activeElement)"), 'it never changes under a pointer or the focus');console.log(`home-headline: ${links.length} phrases, each a link to its tool; one shows; once through, then still`);@@@ -484,4 +484,4 @@ body {/* The page is the shared tool frame: it starts under the navbar, not behind it. */.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }--@media (prefers-reduced-motion: reduce) { .spinner { animation: none; opacity: .7; } }++html[data-ov-motion=off] .spinner{opacity: .7}@@@ -574,4 +574,4 @@ html, body { height: auto; min-height: 100%; overflow: auto; }/* The page is the shared tool frame: it starts under the navbar, not behind it. */#app { padding-top: calc(var(--ovk-nav-h, 52px) + 8px); }.ovk-page { padding-top: 12px; }--@media (prefers-reduced-motion: reduce) { .splash-content, .splash-icon, #splash-screen.fade-out, .welcome-icon, .user-marker, #detail-panel, .modal-content, .sp-item:not(.done):not(.error) { animation: none; } .search-progress .spinner { animation: none; opacity: .7; } }++html[data-ov-motion=off] .search-progress .spinner{opacity: .7}@@@ -912,4 +912,3 @@ textarea.large {/* The page is the shared tool frame: its head starts under the navbar, not behind it. */.ovk-tool-head { margin-top: calc(var(--ovk-nav-h, 52px) + 12px); }--@media (prefers-reduced-motion: reduce) { .live-dot, .output-flash, .toast { animation: none; } }@@@ -422,4 +422,4 @@ a:hover { text-decoration: underline; }/* The page is the shared tool frame: it starts under the navbar, not behind it. */.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }--@media (prefers-reduced-motion: reduce) { .spinner, .progress-bar.indeterminate { animation: none; opacity: .7; } }++html[data-ov-motion=off] .spinner,html[data-ov-motion=off] .progress-bar.indeterminate{opacity: .7}
@@ -591,4 +591,4 @@ a:hover { color: var(--accent-hover); }
@@ -591,4 +591,4 @@ a:hover { color: var(--accent-hover); }
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }
.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }
-@media (prefers-reduced-motion: reduce) { .option-group, .result-panel, .toast { animation: none; } .spinner { animation: none; opacity: .7; } }
+html[data-ov-motion=off] .spinner{opacity: .7}
@@ -481,4 +481,4 @@ body {
@@ -481,4 +481,4 @@ body {
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }
.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }
-@media (prefers-reduced-motion: reduce) { .spinner { animation: none; opacity: .7; } }
+html[data-ov-motion=off] .spinner{opacity: .7}
@@ -247,4 +247,4 @@ body { min-height:100%; font-family:var(--font); background:var(--bg); color:var
@@ -247,4 +247,4 @@ body { min-height:100%; font-family:var(--font); background:var(--bg); color:var
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }
.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }
-@media (prefers-reduced-motion: reduce) { .spinner { animation: none; opacity: .7; } }
+html[data-ov-motion=off] .spinner{opacity: .7}
@@ -235,6 +235,5 @@
@@ -235,6 +235,5 @@
});
});
</script>
</script>
<script src="https://openvibe.live/live-notify.js" async></script>
<script src="https://openvibe.live/live-notify.js" async></script>
- <style>@media (prefers-reduced-motion: reduce) { .showcase-tool, .example-btn, .stat-card { animation: none; } }</style>
</body>
</body>
</html>
</html>
@@ -141,7 +141,6 @@ select{appearance:none;background-image:linear-gradient(45deg,transparent 50%,va
@@ -141,7 +141,6 @@ select{appearance:none;background-image:linear-gradient(45deg,transparent 50%,va
.facts span{display:block;font-size:.7rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--c-tx3)}
.facts span{display:block;font-size:.7rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--c-tx3)}
.dl{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px}
.dl{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px}
@media (max-width:900px){.studio{grid-template-columns:minmax(0,1fr)}.preview{position:static;order:-1}.stage{min-height:220px}.stage .art,.stage .art svg{max-height:260px}}
@media (max-width:900px){.studio{grid-template-columns:minmax(0,1fr)}.preview{position:static;order:-1}.stage{min-height:220px}.stage .art,.stage .art svg{max-height:260px}}
-@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
/* ── Hub ── */
/* ── Hub ── */
.smart{padding:18px;border-radius:calc(var(--r) + 4px);background:linear-gradient(135deg,color-mix(in srgb,var(--c-ac) 14%,var(--c-card)),var(--c-card));border:1px solid var(--c-brd2);display:grid;grid-template-columns:minmax(0,1fr) 220px;gap:18px;align-items:center;margin-bottom:22px}
.smart{padding:18px;border-radius:calc(var(--r) + 4px);background:linear-gradient(135deg,color-mix(in srgb,var(--c-ac) 14%,var(--c-card)),var(--c-card));border:1px solid var(--c-brd2);display:grid;grid-template-columns:minmax(0,1fr) 220px;gap:18px;align-items:center;margin-bottom:22px}
@@ -549,6 +549,5 @@ function sample(){const t=T[ACT];if(t?.p){$('in1').value=t.p;if(t.p2&&$('in2'))$
@@ -549,6 +549,5 @@ function sample(){const t=T[ACT];if(t?.p){$('in1').value=t.p;if(t.p2&&$('in2'))$
function toast(msg){const t=$('toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),2000)}
function toast(msg){const t=$('toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),2000)}
</script>
</script>
<script src="https://openvibe.live/live-notify.js" async></script>
<script src="https://openvibe.live/live-notify.js" async></script>
- <style>@media (prefers-reduced-motion: reduce) { .bg-glow::before, .bg-glow::after { animation: none; } }</style>
</body>
</body>
</html>
</html>
@@ -235,6 +235,5 @@
@@ -235,6 +235,5 @@
});
});
</script>
</script>
<script src="https://openvibe.live/live-notify.js" async></script>
<script src="https://openvibe.live/live-notify.js" async></script>
- <style>@media (prefers-reduced-motion: reduce) { .showcase-tool, .example-btn, .stat-card { animation: none; } }</style>
</body>
</body>
</html>
</html>
@@ -172,12 +172,12 @@
@@ -172,12 +172,12 @@
})();
})();
// The headline's phrases are links already in the page; this shows them in turn, once through, then rests on the
// The headline's phrases are links already in the page; this shows them in turn, once through, then rests on the
-// first. The button pauses, or plays it again. Reduced motion keeps the first. Never an aria-live region.
+// first. The button pauses, or plays it again. Motion off keeps the first; Calm rotates as usual. Never an aria-live region.
(function () {
(function () {
const el = document.getElementById('tool-headline');
const el = document.getElementById('tool-headline');
const items = el ? Array.from(el.querySelectorAll('.hm-ph')) : [];
const items = el ? Array.from(el.querySelectorAll('.hm-ph')) : [];
if (items.length < 2) return;
if (items.length < 2) return;
- const motion = window.matchMedia('(prefers-reduced-motion: reduce)');
+ const still = () => document.documentElement.getAttribute('data-ov-motion') === 'off';
const control = document.getElementById('headline-motion');
const control = document.getElementById('headline-motion');
let i = 0, timer = 0, paused = false, rested = false;
let i = 0, timer = 0, paused = false, rested = false;
const show = (n) => {
const show = (n) => {
@@ -190,8 +190,8 @@
@@ -190,8 +190,8 @@
};
};
const update = () => {
const update = () => {
clearInterval(timer); timer = 0;
clearInterval(timer); timer = 0;
- if (control) control.hidden = motion.matches;
- if (motion.matches) { show(0); return; }
+ if (control) control.hidden = still();
+ if (still()) { show(0); return; }
if (!paused && !rested && !document.hidden) timer = setInterval(() => {
if (!paused && !rested && !document.hidden) timer = setInterval(() => {
if (el.matches(':hover') || el.contains(document.activeElement)) return;
if (el.matches(':hover') || el.contains(document.activeElement)) return;
const next = (i + 1) % items.length;
const next = (i + 1) % items.length;
@@ -203,7 +203,7 @@
@@ -203,7 +203,7 @@
control.setAttribute('aria-label', say); control.title = say; control.classList.toggle('is-paused', paused || rested);
control.setAttribute('aria-label', say); control.title = say; control.classList.toggle('is-paused', paused || rested);
};
};
if (control) control.addEventListener('click', () => { if (rested) rested = paused = false; else paused = !paused; update(); });
if (control) control.addEventListener('click', () => { if (rested) rested = paused = false; else paused = !paused; update(); });
- motion.addEventListener('change', update);
+ window.addEventListener('ov:display', update);
document.addEventListener('visibilitychange', update);
document.addEventListener('visibilitychange', update);
update();
update();
})();
})();
@@ -1428,6 +1428,5 @@
@@ -1428,6 +1428,5 @@
}
}
</script>
</script>
<script src="https://openvibe.live/live-notify.js" async></script>
<script src="https://openvibe.live/live-notify.js" async></script>
- <style>@media (prefers-reduced-motion: reduce) { .bg-glow::before, .bg-glow::after, .toast, .showcase-tool, .stat-card { animation: none; } }</style>
</body>
</body>
</html>
</html>
@@ -127,7 +127,7 @@ details{border-bottom:1px solid var(--border,rgba(255,255,255,.08));padding:12px
@@ -127,7 +127,7 @@ details{border-bottom:1px solid var(--border,rgba(255,255,255,.08));padding:12px
transition:opacity var(--ov-dur-3,.32s) var(--ov-ease-out,ease-out),transform var(--ov-dur-3,.32s) var(--ov-ease-out,ease-out),visibility 0s linear var(--ov-dur-3,.32s)}
transition:opacity var(--ov-dur-3,.32s) var(--ov-ease-out,ease-out),transform var(--ov-dur-3,.32s) var(--ov-ease-out,ease-out),visibility 0s linear var(--ov-dur-3,.32s)}
.hm-ph.is-on{opacity:1;visibility:visible;transform:none;transition-delay:0s}.hm-ph.is-out{transform:translateY(-.35em)}
.hm-ph.is-on{opacity:1;visibility:visible;transform:none;transition-delay:0s}.hm-ph.is-out{transform:translateY(-.35em)}
.hm-ph:hover,.hm-ph:focus-visible{text-decoration:underline;text-decoration-thickness:.06em;text-underline-offset:.12em}
.hm-ph:hover,.hm-ph:focus-visible{text-decoration:underline;text-decoration-thickness:.06em;text-underline-offset:.12em}
-@media (prefers-reduced-motion:reduce){.hm-ph{transition:none;transform:none}}
+html[data-ov-motion=off] .hm-ph{transform:none}
/* The strip of tools drifts, so its ends fade: a chip cut by a hard edge looked broken. */
/* The strip of tools drifts, so its ends fade: a chip cut by a hard edge looked broken. */
.quick-strip{-webkit-mask-image:linear-gradient(90deg,transparent,#000 22px,#000 calc(100% - 22px),transparent);mask-image:linear-gradient(90deg,transparent,#000 22px,#000 calc(100% - 22px),transparent);padding-inline:14px;margin-inline:-14px;max-width:calc(100% + 28px)}
.quick-strip{-webkit-mask-image:linear-gradient(90deg,transparent,#000 22px,#000 calc(100% - 22px),transparent);mask-image:linear-gradient(90deg,transparent,#000 22px,#000 calc(100% - 22px),transparent);padding-inline:14px;margin-inline:-14px;max-width:calc(100% + 28px)}
/* The headline's pause: a small round icon beside the words, not a line of its own. */
/* The headline's pause: a small round icon beside the words, not a line of its own. */
@@ -222,7 +222,6 @@ background:color-mix(in srgb,var(--text-primary,#e8ecf3) 8%,transparent);color:v
@@ -222,7 +222,6 @@ background:color-mix(in srgb,var(--text-primary,#e8ecf3) 8%,transparent);color:v
/* The home on phones (2026-10-29): the examples are one row that scrolls sideways (all four still there), and four of the most
/* The home on phones (2026-10-29): the examples are one row that scrolls sideways (all four still there), and four of the most
opened tools show (the families below and the A to Z list hold the rest). */
opened tools show (the families below and the A to Z list hold the rest). */
@media (max-width:599px){.start-grid{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:16px;scrollbar-width:none;gap:10px;margin:0 -16px;padding:0 16px 4px}.start-grid::-webkit-scrollbar{display:none}.start-grid>*{flex:0 0 84%;min-width:0;scroll-snap-align:start}section[aria-labelledby="top-h"] .grid>:nth-child(n+5){display:none}}
@media (max-width:599px){.start-grid{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:16px;scrollbar-width:none;gap:10px;margin:0 -16px;padding:0 16px 4px}.start-grid::-webkit-scrollbar{display:none}.start-grid>*{flex:0 0 84%;min-width:0;scroll-snap-align:start}section[aria-labelledby="top-h"] .grid>:nth-child(n+5){display:none}}
-@media (prefers-reduced-motion:reduce){.tool{transition:none}}
${icons.CSS}
${icons.CSS}
/* Touch targets on phones (fleet audit, 2026-10-29): touch pointers only. */
/* Touch targets on phones (fleet audit, 2026-10-29): touch pointers only. */
@media (pointer: coarse) {
@media (pointer: coarse) {
@@ -32,10 +32,11 @@ const site = require('../server/pages/site');
@@ -32,10 +32,11 @@ const site = require('../server/pages/site');
assert.strictEqual(new Set(links.map((m) => m[2])).size, links.length, 'each phrase is a different tool');
assert.strictEqual(new Set(links.map((m) => m[2])).size, links.length, 'each phrase is a different tool');
assert.ok(!home.includes('data-phrases='), 'the phrases are links in the page, not a data attribute');
assert.ok(!home.includes('data-phrases='), 'the phrases are links in the page, not a data attribute');
- // The swap is CSS on shared timing, still under reduced motion; the script goes once through, then rests.
+ // The swap is CSS on shared timing, played under Calm and held only with motion off; the script goes once through, then rests.
assert.match(home, /\.hm-rot\{display:grid\}\.hm-ph\{grid-area:1\/1;/, 'the phrases share one cell, so nothing below moves');
assert.match(home, /\.hm-rot\{display:grid\}\.hm-ph\{grid-area:1\/1;/, 'the phrases share one cell, so nothing below moves');
- assert.match(home, /@media \(prefers-reduced-motion:reduce\)\{\.hm-ph\{transition:none;transform:none\}\}/);
+ assert.match(home, /html\[data-ov-motion=off\] \.hm-ph\{transform:none\}/);
const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'site.js'), 'utf8');
const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'site.js'), 'utf8');
+ assert.ok(js.includes("getAttribute('data-ov-motion') === 'off'"), 'only motion off holds the first phrase');
assert.ok(js.includes('if (next === 0) { rested = true;'), 'once through, then it rests');
assert.ok(js.includes('if (next === 0) { rested = true;'), 'once through, then it rests');
assert.ok(js.includes("el.matches(':hover') || el.contains(document.activeElement)"), 'it never changes under a pointer or the focus');
assert.ok(js.includes("el.matches(':hover') || el.contains(document.activeElement)"), 'it never changes under a pointer or the focus');
console.log(`home-headline: ${links.length} phrases, each a link to its tool; one shows; once through, then still`);
console.log(`home-headline: ${links.length} phrases, each a link to its tool; one shows; once through, then still`);
@@ -484,4 +484,4 @@ body {
@@ -484,4 +484,4 @@ body {
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }
.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }
-@media (prefers-reduced-motion: reduce) { .spinner { animation: none; opacity: .7; } }
+html[data-ov-motion=off] .spinner{opacity: .7}
@@ -574,4 +574,4 @@ html, body { height: auto; min-height: 100%; overflow: auto; }
@@ -574,4 +574,4 @@ html, body { height: auto; min-height: 100%; overflow: auto; }
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
#app { padding-top: calc(var(--ovk-nav-h, 52px) + 8px); }
#app { padding-top: calc(var(--ovk-nav-h, 52px) + 8px); }
.ovk-page { padding-top: 12px; }
.ovk-page { padding-top: 12px; }
-@media (prefers-reduced-motion: reduce) { .splash-content, .splash-icon, #splash-screen.fade-out, .welcome-icon, .user-marker, #detail-panel, .modal-content, .sp-item:not(.done):not(.error) { animation: none; } .search-progress .spinner { animation: none; opacity: .7; } }
+html[data-ov-motion=off] .search-progress .spinner{opacity: .7}
@@ -912,4 +912,3 @@ textarea.large {
@@ -912,4 +912,3 @@ textarea.large {
/* The page is the shared tool frame: its head starts under the navbar, not behind it. */
/* The page is the shared tool frame: its head starts under the navbar, not behind it. */
.ovk-tool-head { margin-top: calc(var(--ovk-nav-h, 52px) + 12px); }
.ovk-tool-head { margin-top: calc(var(--ovk-nav-h, 52px) + 12px); }
-@media (prefers-reduced-motion: reduce) { .live-dot, .output-flash, .toast { animation: none; } }
@@ -422,4 +422,4 @@ a:hover { text-decoration: underline; }
@@ -422,4 +422,4 @@ a:hover { text-decoration: underline; }
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }
.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }
-@media (prefers-reduced-motion: reduce) { .spinner, .progress-bar.indeterminate { animation: none; opacity: .7; } }
+html[data-ov-motion=off] .spinner,html[data-ov-motion=off] .progress-bar.indeterminate{opacity: .7}