openvibers/openvibe.design Commits
Motion: four choices, and Calm reduces instead of removing (Shared 4.183.0) (#236)
Changes
@@@ -234,6 +234,6 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by<!-- versions:start -->- openvibe-contracts: v0.384.0- openvibe-sdk: v0.133.0--- openvibe-shared: v4.180.0++- openvibe-shared: v4.183.0- openvibe-publishing: v1.7.0<!-- versions:end -->@@@ -10,12 +10,12 @@"domain": "openvibe.design","contracts": "openvibe-contracts v0.384.0","sdk": "openvibe-sdk v0.133.0",-- "shared": "openvibe-shared v4.180.0",++ "shared": "openvibe-shared v4.183.0","packages": ["openvibe-contracts v0.384.0","openvibe-publishing v1.7.0","openvibe-sdk v0.133.0",-- "openvibe-shared v4.180.0"++ "openvibe-shared v4.183.0"],"summary": "The service skeleton for openvibe.design: Network sign-in as OAuth client `design` (PKCE S256, httpOnly design_at/design_rt cookies) and a theme API proxy to OpenVibe.Network's /api/themes. The proxy serves the public catalogue (cached 5 minutes), one theme, the person's active theme and custom variables, saving a theme with source 'studio', its history and a restore by id, and a submission to the gallery. Writes need this site's own Origin; Network 4xx answers pass through and a Network failure is 502. The studio is designed: a gallery, previews on a Live channel, a Chat room and a shop's grid (with colour-vision views), a six-colour editor that derives the full token set, WCAG checks that gate Use, sharing to the gallery, and History with Restore. The Arena queues one prompt for three models through OpenVibe.AI (client credentials, ai.run.create), stores sanitized HTML with reported model/provider/cost/time/tokens, renders it only in sandboxed CSP-capped iframes, draws blind pairs, records one vote per person per round, and keeps Elo ratings per category plus median cost and time. The prompts, outputs and votes are an open JSONL dataset; rounds and votes follow ADR-033 export and deletion.","works": [@@@ -19,7 +19,7 @@"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0","openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.133.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.180.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.183.0","pg": "^8.23.1","subset-font": "^2.9.0","wawoff2": "^2.0.1"@@@ -1395,9 +1395,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.180.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.180.0",-- "integrity": "sha512-VZBOvI2D+ONBudakLG141Go1PaD0NkycldUxSYuQLzG3ZebewlRhGinzKI7Qj+RmrmFy2b4g7Wjb71XkDo2KCg==",++ "version": "4.183.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.183.0",++ "integrity": "sha512-vp9gg++Dhyj3h0cwvBjujM6dSFYoeXdh1UXdZ6OXP/NjG672Fs7uRANV4MjT467JtFDN7r7qLI3OJAwGQxOuQw==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -25,7 +25,7 @@"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0","openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.133.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.180.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.183.0","pg": "^8.23.1","subset-font": "^2.9.0","wawoff2": "^2.0.1"@@@ -156,7 +156,7 @@.br-play-stage{min-height:220px}.br-sizes{gap:18px 22px}}--@media (prefers-reduced-motion:reduce){.br-chip:not([data-fx-demo] *),.br-files a:not([data-fx-demo] *){transition:none}.br:not([data-fx-demo]) .br-files a:hover{transform:none}}++html[data-ov-motion=off] .br:not([data-fx-demo]) .br-files a:hover{transform:none}.br-h3-note{margin-left:6px;color:var(--text-muted);font-weight:600;font-size:12px}/* A phone: the motion files are a sideways rail (the owner, 10-31: short pages by rails and folds, never by hiding). */@@@ -124,7 +124,6 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex/* The home's motif: the theme orb (each dot a theme's accent); its dashed ring breathes, it never spins. */.ds-orb { position: relative; width: min(100%, 320px); aspect-ratio: 1; margin: 0 auto; }.ds-orb-ring { position: absolute; inset: 8%; border-radius: 50%; border: 1.5px dashed color-mix(in srgb, var(--room-accent) 40%, transparent); animation: ds-breathe 9s var(--ov-ease-in-out, ease-in-out) infinite alternate; }--@media (prefers-reduced-motion: reduce) { .ds-orb-ring { animation: none; } }.ds-orb-core { position: absolute; inset: 30%; border-radius: 50%; background: conic-gradient(from 90deg, var(--room-accent), #5b7cfa, #22c55e, #f59e0b, #ef4444, var(--room-accent)); filter: saturate(1.1); box-shadow: 0 20px 60px -20px var(--room-accent); }.ds-orb i { position: absolute; left: 50%; top: 50%; width: 22px; height: 22px; margin: -11px; border-radius: 50%; border: 3px solid var(--bg-primary, #0a0f1c); transform: rotate(calc(var(--i) * 60deg)) translateY(-136%) rotate(calc(var(--i) * -60deg)); }@supports (container-type: inline-size) { .ds-orb { container-type: inline-size; } .ds-orb i { transform: rotate(calc(var(--i) * 60deg)) translateY(-42cqw) rotate(calc(var(--i) * -60deg)); } }@@@ -142,8 +141,6 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex.ds-eps > div { padding: 12px 14px; border-radius: 14px; background: var(--bg-card, #0f1729); border: 1px solid var(--border, #1f2d47); }.ds-eps dt code { font: 600 .9rem/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: var(--room-strong); overflow-wrap: anywhere; }.ds-eps dd { margin: 6px 0 0; color: var(--text-secondary, #96a7c2); line-height: 1.5; }--@media (prefers-reduced-motion: reduce) { .ds-orb-ring { animation: none; } .ds-swatch, .ds-previews { transition: none; } }--html[data-ov-motion=reduced] .ds-orb-ring { animation: none; }/* ── The Design Arena (plan 2026-10-28 §5.2 v2) ──────────────────────────────────────────────────────────────Server-rendered placeholders for the main session's design. The generated page only ever appears inside an@@@ -249,7 +246,6 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef/* Forms */.ar-form select:focus-visible, .ar-form textarea:focus-visible { outline: 2px solid var(--room-strong); outline-offset: 1px; }.ar-form textarea::placeholder { color: var(--text-muted, #7386a3); }--@media (prefers-reduced-motion: reduce) { .ar-vote-side { transition: none; } }/* Design Studio A: a quiet prompt and canvas. */.dv-visually-hidden { position: absolute; clip-path: inset(50%); width: 1px; height: 1px; overflow: hidden; }.dv-create { max-width: 800px; margin: clamp(48px, 13vh, 150px) auto 100px; padding: 0 20px; }@@@ -308,7 +304,6 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef.dv-toolbar select { max-width: 102px; }.dv-toolbar form, .dv-toolbar label { gap: 4px; font-size: .72rem; }}--@media (prefers-reduced-motion: reduce) { .dv-frame { transition: none; } .dv-skeleton { animation: none; } }/* Part B: the working frame, the chat composer, versions and the click-edit panel. One toolbar stays the onlyfloating control bar; the composer and the version list are in the page flow, and the element panel opens only@@@ -363,7 +358,6 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef.dv-versions { margin-bottom: 104px; }.dv-element { left: 12px; right: 12px; bottom: calc(var(--ovnav-appbar-h, 56px) + 76px); width: auto; max-height: 56vh; }}--@media (prefers-reduced-motion: reduce) { .dv-pick { transition: none; } }/* ── /tokens: a theme as code (views/tokens.js) ── */.tk { max-width: 980px; }@@@ -669,7 +663,6 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef.ar-kb { scroll-snap-align: start; }.ar-who { text-align: left; }}--@media (prefers-reduced-motion: reduce) { .ar-kind { transition: none; } }/* /look: the icon styles side by side (server/views/look.js iconStyles): three small phone screens drawn with thereal glyphs. Each is a button that picks its style; the picked one wears the accent. */@@@ -696,7 +689,7 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef.lk-st-cap small{color:var(--text-muted);font-size:12px;line-height:1.35}.lk-style:has(input:checked) .lk-st-cap b{color:var(--accent)}@media (max-width:480px){.lk-styles{gap:8px}.lk-st-screen{padding:8px 7px 0;border-radius:14px}.lk-st-head{font-size:19px;gap:6px}.lk-st-lines>span+span{display:none}.lk-st-bar{margin:4px -7px 0;font-size:15px}.lk-st-tab{width:26px}.lk-st-cap small{font-size:11px}}--@media (prefers-reduced-motion:reduce){.lk-st-screen{transition:none}.lk-style:active .lk-st-screen{transform:none}}++html[data-ov-motion=off] .lk-style:active .lk-st-screen{transform:none}/* Publish on openvibe.space (open item D8), drawn like the Versions panel. */.dv-publish { max-width: 1320px; margin: 0 auto 12px; padding: 10px 14px; border: 1px solid var(--border); border-radius: 14px; background: var(--bg-card); }@@@ -744,7 +737,7 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef.ds-sys small { font-size: 13px; line-height: 1.45; }@media (max-width: 1000px) { .ds-system-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }@media (max-width: 520px) { .ds-system-grid { grid-template-columns: minmax(0, 1fr); } }--@media (prefers-reduced-motion: reduce) { .ds-sys { transition: none; } .ds-sys:hover { transform: none; } }++html[data-ov-motion=off] .ds-sys:hover{transform: none}/* ── The arena, easier to judge (owner 11-08: "make A and B clickable", "smoother between things") ──The drawing is the vote: a transparent button lies over each frame, so a tap picks it and a swipe scrolls the page@@@ -846,11 +839,7 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef.ar-wait.is-loaded { animation: none; background: #fff; }.ar-wait.is-loaded > .ar-frame { opacity: 1; }@keyframes ar-shimmer { to { background-position: -220% 0; } }--@media (prefers-reduced-motion: reduce) {-- .ar-pickable, .ar-pick-tag, .ar-arena #ar-pair { transition: none; }-- .ar-vote-pair[data-pick] .ar-pick-tag, .ar-wait, .ar-arena.is-loading #ar-kinds .ar-kind[aria-current] .ar-kind-i { animation: none; }-- .ar-vote-pair[data-pick] .ar-pickable { transform: none; }--}++html[data-ov-motion=off] .ar-vote-pair[data-pick] .ar-pickable{transform: none}/* The kinds rail on a phone: the icon beside the name, the line under both, no word broken mid-way */@media (max-width: 759px) {@@@ -35,9 +35,6 @@.ovm-shake.is-no, .ovm-shake.ovm-demo:hover, :hover > .ovm-shake.ovm-demo { animation: ovm-shake var(--ov-dur-4, .5s) var(--ov-ease-out, cubic-bezier(.36, .07, .19, .97)); }@keyframes ovm-shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-3px); } 40% { transform: translateX(3px); } 60% { transform: translateX(-2px); } 80% { transform: translateX(1px); } }--@media (prefers-reduced-motion: reduce) {-- .ovm-draw :is(path, circle, rect, ellipse, line, polyline) { animation: none; stroke-dashoffset: 0; }-- .ovm-pop, .ovm-nudge, .ovm-ring, .ovm-beat, .ovm-shake { animation: none !important; transition: none !important; transform: none !important; }--}--html[data-ov-motion="reduced"] .ovm-draw :is(path, circle, rect, ellipse, line, polyline) { animation: none; stroke-dashoffset: 0; }--html[data-ov-motion="reduced"] :is(.ovm-pop, .ovm-nudge, .ovm-ring, .ovm-beat, .ovm-shake) { animation: none !important; transition: none !important; transform: none !important; }++html[data-ov-motion=off] .ovm-draw :is(path, circle, rect, ellipse, line, polyline){stroke-dashoffset: 0}html[data-ov-motion=off] .ovm-pop,html[data-ov-motion=off] .ovm-nudge,html[data-ov-motion=off] .ovm-ring,html[data-ov-motion=off] .ovm-beat,html[data-ov-motion=off] .ovm-shake{transform: none !important}++html[data-ov-motion=off] .ovm-draw :is(path, circle, rect, ellipse, line, polyline){stroke-dashoffset: 0}++html[data-ov-motion=off] :is(.ovm-pop, .ovm-nudge, .ovm-ring, .ovm-beat, .ovm-shake){transform: none !important}@@@ -100,7 +100,6 @@.gp-more>.gp-use{padding:4px 16px 16px}.gp-files{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:16px}.gp-files>span{color:var(--text-muted);font-size:13px;font-weight:650;margin-right:4px}--@media (prefers-reduced-motion:reduce){.gp-more>summary::before{transition:none}}/* Get them all: one card per file. */.ic-packs{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px}@@@ -190,7 +189,7 @@.ic-tile{min-height:92px}}@media (max-width:420px){.gp-controls{grid-template-columns:minmax(0,1fr)}}--@media (prefers-reduced-motion:reduce){.ic-tile,.ic-packs a{transition:none}.ic-tile:hover{transform:none}}++html[data-ov-motion=off] .ic-tile:hover{transform:none}/* Make a kit: tick glyphs; the bar holds the name and the download and stays in reach at the bottom. */.kit-form{display:grid;gap:12px}@@@ -214,7 +213,6 @@.kit-bar .ic-btn{flex:none}.kit-bar .ic-btn:disabled{opacity:.5;cursor:default}@media (max-width:760px){.kit-grid{grid-template-columns:repeat(auto-fill,minmax(88px,1fr))}}--@media (prefers-reduced-motion:reduce){.kit-tile{transition:none}}/* /icons/migrate (L50 step 4): paste a page, see every icon with its twin. */.mg-form{display:grid;gap:10px;margin:0 0 8px}@@@ -258,7 +256,6 @@.ic-fold>summary span{color:var(--text-muted);font-weight:500;font-size:13px}.ic-fold>summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:14px}.ic-fold>.ic-grid{padding:4px 12px 12px}--@media (prefers-reduced-motion:reduce){.ic-fold>summary::before{transition:none}}/* A phone: denser tiles, so a screen holds five rows of four. */@media (max-width:520px){.ic .ic-grid{grid-template-columns:repeat(auto-fill,minmax(74px,1fr));gap:6px}@@@ -103,7 +103,6 @@.kt-pn { grid-template-columns: 1fr 1fr; }.kt-pn .kt-pn-all { grid-column: 1 / -1; grid-row: 2; }}--@media (prefers-reduced-motion: reduce) { .kt-tile { transition: none; } }/* ── /kit/layout: the fluid scale, the eight primitives (each in a box the reader can drag narrower) and the base ── */.kt-layout-card { display: grid; gap: 4px; margin: 18px 0 0; padding: 16px 18px; border: 1px solid color-mix(in srgb, var(--room-strong) 45%, var(--border, #26344d)); border-radius: 16px;@@@ -196,9 +196,8 @@@keyframes mxDot{0%,100%{transform:scale(1)}50%{transform:scale(1.28)}}@keyframes mxPulse{0%,100%{opacity:.4}50%{opacity:.95}}@keyframes mxPeek{0%,100%{transform:none}34%{transform:translateX(-68px)}58%{transform:translateX(6px)}76%{transform:translateX(-16px)}}--@media (prefers-reduced-motion:reduce){.mx-wrap.mx-live .mx-card,.mx-wrap.mx-live .mx-stage :is(.mx-s,.mx-f){transition:none!important}.mx-wrap.mx-live .mx-card:not(.is-in){opacity:1;transform:none}.mx-wrap.mx-live .mx-card:not(.is-in) .mx-stage :is(.mx-s,.mx-f){stroke-dashoffset:0;opacity:1}}--html[data-ov-motion=reduced] .mx-wrap .mx-card,html[data-ov-motion=reduced] .mx-wrap .mx-card *,html[data-ov-motion=reduced] .mx-ta{animation:none!important;transition:none!important}--html[data-ov-motion=reduced] .mx-wrap .mx-card:not(.is-in){opacity:1!important;transform:none!important}++html[data-ov-motion=off] .mx-wrap.mx-live .mx-card:not(.is-in){opacity:1;transform:none}html[data-ov-motion=off] .mx-wrap.mx-live .mx-card:not(.is-in) .mx-stage :is(.mx-s,.mx-f){stroke-dashoffset:0;opacity:1}++html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;transform:none!important}/* ── The menu buttons ───────────────────────────────────────────────────────────────────────────────────────────Drawn in currentColor with the accent (mb-a fills, mb-sa strokes); hovered, tapped or scrolled in (is-play) they@@@ -260,7 +259,6 @@ html[data-ov-motion=reduced] .mx-wrap .mx-card:not(.is-in){opacity:1!important;t@keyframes mbPop{0%,100%{transform:scale(1)}40%{transform:scale(.8)}75%{transform:scale(1.12)}}@keyframes mbTilt{0%,100%{transform:rotate(0)}25%{transform:rotate(-14deg)}55%{transform:rotate(9deg)}80%{transform:rotate(-4deg)}}@keyframes mbNod{0%,100%{transform:translateY(0)}30%{transform:translateY(2.4px)}60%{transform:translateY(-.6px)}80%{transform:translateY(1.2px)}}--html[data-ov-motion=reduced] .mb-btn *{animation:none!important}.mb-card .mx-stage{color:#e8edf3}.mb-big{width:auto;height:auto;padding:14px;border-radius:24px}.mb-big::before{inset:8px}@@@ -328,7 +326,6 @@ html[data-ov-motion=reduced] .mb-btn *{animation:none!important}.mx-nudge{position:fixed;left:50%;bottom:calc(16px + var(--ovnav-appbar-h,0px) + env(safe-area-inset-bottom,0px));transform:translateX(-50%);z-index:60;display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:8px;width:max-content;max-width:calc(100vw - 20px);padding:10px 12px;border-radius:16px;border:1px solid #8b5cf6;background:color-mix(in srgb,var(--mx-card) 95%,transparent);backdrop-filter:blur(10px);box-shadow:0 14px 34px rgba(0,0,0,.4);font-size:14px;animation:mxNudgeIn var(--ov-dur-3, .4s) var(--ov-ease-spring, cubic-bezier(.2,.9,.25,1.1))}.mx-nudge a.mx-btn{text-decoration:none;display:inline-grid;place-items:center}@keyframes mxNudgeIn{from{opacity:0;transform:translate(-50%,12px)}to{opacity:1;transform:translateX(-50%)}}--@media (prefers-reduced-motion:reduce){.mx-step.is-call,.mx-nudge{animation:none}}@media (max-width:640px){.mx-steps .mx-stepn{width:18px;height:18px;font-size:10.5px}.mx-steps .mx-steps-sep{display:none}}/* The menu buttons beside the chosen mark; on a phone the mock bar is just the mark and the button, to compare at a@@@ -414,7 +411,6 @@ html[data-ov-motion=reduced] .mb-btn *{animation:none!important}.openvibe-navbar .ovnav-orb .mx-navmenu svg{width:22px;height:22px}.openvibe-navbar .mx-nav-flash{animation:mxNavFlash 1.1s var(--ov-ease-out, ease-out)}@keyframes mxNavFlash{0%{box-shadow:0 0 0 0 color-mix(in srgb,#8b5cf6 70%,transparent);border-radius:12px}60%{box-shadow:0 0 0 10px transparent;border-radius:12px}100%{box-shadow:none}}--@media (prefers-reduced-motion:reduce){.openvibe-navbar .mx-nav-flash{animation:none}}/* The tally counts both families (a person can like both), and every vote sits in a fold under it. */.mx-both{margin:-2px 0 0;font-size:12.5px;color:var(--mx-muted);text-align:center}@@@ -94,5 +94,3 @@.mo-durs small{display:none}}/* Still under reduced motion, except while the person plays the demos on this page ([data-fx-demo], demo-motion-controls.js). */--@media (prefers-reduced-motion:reduce){.mo-race.is-run:not([data-fx-demo] *) i{transition:none}}--html[data-ov-motion=reduced] .mo-race.is-run:not([data-fx-demo] *) i{transition:none}@@@ -6,7 +6,7 @@.pgroup{padding:18px}.pcards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.pcard{position:relative;display:grid;gap:6px;min-height:94px;padding:14px 38px 14px 14px;border:1px solid var(--border);border-radius:14px;text-decoration:none;color:var(--text-primary);background:var(--bg-primary);transition:transform var(--ov-dur-2, 180ms) var(--ov-ease-out, cubic-bezier(.2,.7,.2,1)),border-color var(--ov-dur-2, 180ms)}.pcard:hover{transform:translateY(-2px);border-color:var(--accent)}.pcard small{font-size:12px;line-height:1.4;color:var(--text-muted)}.pcard b{position:absolute;right:14px;top:14px;color:var(--accent)}.dbtn,.pill-action,.opill,.tab-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:0 14px;border:1px solid var(--border);border-radius:999px;background:var(--bg-primary);color:var(--text-primary);font:inherit;font-size:13px;font-weight:650;cursor:pointer}.dbtn.primary,.pill-action.primary,.opill.primary{background:var(--accent);border-color:transparent;color:var(--on-accent)}.status-badge,.meta-chip{display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:6px 10px;background:color-mix(in srgb, var(--success) 16%, transparent);color:var(--success);font-size:12px;font-weight:700}.status-badge.warn{background:color-mix(in srgb, var(--warning) 16%, transparent);color:var(--warning)}.status-badge.bad{background:color-mix(in srgb, var(--danger) 16%, transparent);color:var(--danger)}.meta-chip{background:var(--bg-primary);border:1px solid var(--border);color:var(--text-secondary);font-weight:500}.demo-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.stream-box{display:grid;gap:7px;max-height:170px;overflow:auto;scroll-behavior:smooth;padding:2px 4px 2px 0}.stream-item,.attempt,.empty-demo,.progress-line,.outcome-line,.ship-line,.tab-panel{padding:12px;border-radius:12px;background:var(--bg-primary);border:1px solid var(--border)}.stream-item.enter{animation:row-in var(--ov-dur-2, 180ms) var(--ov-ease-out, cubic-bezier(.2,.7,.2,1)) both}@keyframes row-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}.demo-fold{width:100%;border:1px solid var(--border);border-radius:12px;background:var(--bg-primary);padding:12px;color:inherit;text-align:left}.demo-fold summary{min-height:44px;cursor:pointer;display:flex;align-items:center;font-weight:700}.fold-content{padding:8px 0 0;color:var(--text-secondary)}.progress-line{display:flex;align-items:center;gap:10px;min-height:44px}.progress-fold{display:none;padding:12px;color:var(--text-muted)}.progress-line.open+.progress-fold{display:block}.outcome-line{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.outcome-copy{flex:1;min-width:140px}.outcome-copy strong,.outcome-copy small{display:block}.outcome-copy small{color:var(--text-muted);margin-top:3px}.attempt-list{display:none;padding:8px 0 0 12px}.ship-line.open+.attempt-list{display:grid;gap:6px}.empty-demo{display:flex;align-items:center;justify-content:space-between;gap:10px}.demo-tabs{display:flex;gap:4px;border-bottom:1px solid var(--border)}.tab-button[aria-selected=true]{color:var(--accent);border-color:var(--accent)}.tab-panel{margin-top:10px;min-height:56px}.target-box{display:inline-grid;place-items:center;width:44px;height:44px;border:1px dashed var(--accent);border-radius:10px}.mark-demo{display:flex;align-items:center;gap:14px;font-size:26px;font-weight:800;letter-spacing:-.05em}.ov-mark{display:inline-grid;place-items:center;width:48px;height:48px;border-radius:16px;background:linear-gradient(145deg,var(--accent),var(--accent-light));color:var(--text-primary);font-size:23px;transition:transform var(--ov-dur-3, 360ms) var(--ov-ease-spring, cubic-bezier(.34,1.56,.64,1))}.ov-mark.loading{animation:mark-pulse 900ms var(--ov-ease-in-out, ease-in-out) infinite alternate}@keyframes mark-pulse{to{transform:rotate(12deg) scale(.88)}}.bottom-demo{position:relative;min-height:155px;padding:12px 12px 70px;border:1px solid var(--border);border-radius:14px;background:var(--bg-primary)}.bottom-nav{position:absolute;bottom:0;left:0;right:0;min-height:56px;padding:6px 12px max(6px,env(safe-area-inset-bottom));display:flex;justify-content:space-around;background:var(--bg-secondary);border-top:1px solid var(--border);border-radius:0 0 14px 14px}.site-switcher-demo{display:flex;align-items:center;gap:12px}.switcher-pop{display:none;position:absolute;z-index:2;top:55px;left:16px;padding:14px;border-radius:14px;background:var(--bg-secondary);border:1px solid var(--border);box-shadow:0 18px 50px color-mix(in srgb, var(--text-primary) 28%, transparent)}.site-switcher-demo.open .switcher-pop{display:grid;gap:8px}.site-switcher-demo{position:relative}.sample-nav{height:52px;padding:0 14px;display:flex;align-items:center;gap:16px;border:1px solid var(--border);border-radius:14px}.sample-nav span:last-child{margin-left:auto;color:var(--text-muted)}.demo-tabs button:focus-visible,.dbtn:focus-visible,.tab-button:focus-visible,.demo-fold summary:focus-visible{outline:3px solid var(--accent);outline-offset:2px}@media(max-width:620px){.pattern-page,.patterns-index{padding:24px 14px 100px}.pcards,.pcodegrid{grid-template-columns:1fr}.pnext{flex-direction:column}.pdemo-body{padding:14px}.pbug,.prule,.ptokens,.pnative{padding:16px}.pcodegrid pre{font-size:10px}}--@media(prefers-reduced-motion:reduce){.pcard,.ov-mark{transition:none}.pcard:hover{transform:none}.stream-item.enter{animation:none}.ov-mark.loading{animation:none}.demo-fold,.stream-box{scroll-behavior:auto}}++html[data-ov-motion=off] .pcard:hover{transform:none}html[data-ov-motion=off] .demo-fold,html[data-ov-motion=off] .stream-box{scroll-behavior:auto}/* Layout that holds (10-30): the demos for the seven rules. */.decor-demo{position:relative;height:170px;overflow:hidden;border-radius:14px;border:1px solid var(--border);background:var(--bg-primary)}@@@ -33,7 +33,6 @@.clear-toast{padding:10px 12px;border-radius:12px;border:1px solid var(--border);background:var(--bg-secondary);font-size:13px;font-weight:650;box-shadow:var(--shadow-lg)}.clear-pill{position:absolute;right:10px;bottom:10px;display:inline-flex;align-items:center;height:42px;padding:0 16px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);font-weight:750;font-size:13px}.clear-pill::before{content:'';width:8px;height:8px;margin-right:8px;border-radius:50%;background:var(--warning)}--@media(prefers-reduced-motion:reduce){.drag-chip,.clear-toasts{transition:none}}.watch-demo{position:relative;display:flex;flex-direction:column;height:240px;max-width:300px;margin:0 auto;border:1px solid var(--border);border-radius:14px;overflow:hidden;background:var(--bg-primary)}.watch-player{position:relative;flex:0 0 auto;aspect-ratio:16/9;background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 45%,#000),#000)}.watch-live{position:absolute;top:8px;left:8px;padding:2px 8px;border-radius:999px;background:#e11d48;color:#fff;font-size:.72rem;font-weight:800}@@@ -42,7 +41,6 @@.watch-demo .bottom-nav{transition:transform var(--ov-dur-base,260ms) ease}.watch-demo.is-watching .bottom-nav{transform:translateY(110%)}.watch-demo.is-watching .watch-chat{padding-bottom:8px}--@media (prefers-reduced-motion:reduce){.watch-demo .bottom-nav{transition:none}}/* 2026-11-01 patterns: labelled cards, ask with a button, own places, mixed words. */.lcards-demo{max-width:280px}.lcards-table{border-collapse:collapse;width:100%}@@@ -63,7 +61,6 @@.mixed-panel.light{background:#fff;--panel-text:#1a1a2e}.mixed-panel.dark{background:#0a0f1c;--panel-text:#e6edf7}.mixed-word{padding:2px 8px;border-radius:999px;font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:.04em;border:1px solid var(--hue);color:color-mix(in srgb,var(--hue) 50%,var(--panel-text))}.mixed-demo.broken .mixed-word{color:var(--hue)}--@media (prefers-reduced-motion: reduce){.places-node{transition:none}}/* 11-07 patterns: the menu, site cards, your things, status dots, the phone sheet, drop and tray, card or row, even grids, six places. */.menu-demo,.card-demo,.phone-demo,.drop-demo,.cr-demo,.even-demo,.six-demo,.things-bar{border:1px solid var(--border);border-radius:14px;background:var(--bg-primary)}@@@ -125,7 +122,6 @@.even-demo span{display:grid;place-items:center;height:44px;border-radius:10px;background:var(--bg-secondary);font-weight:800}.six-demo{flex-wrap:wrap;padding:8px 10px;font-size:13px}.six-demo strong{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:160px}.six-demo span{padding:4px 8px;border-radius:999px;color:var(--text-secondary)}.six-more{border:1px solid var(--border)}--@media (prefers-reduced-motion:reduce){.sheet-demo,.tray-demo li i{transition:none}.menu-demo.open .menu-panel{animation:none}}.status-word{font-weight:700;font-size:15px}/* Honest numbers (2026-11-09) */@@@ -16,11 +16,11 @@if (!buttons.length) return;const loader = window.OpenVibeThemeLoader;const media = window.matchMedia('(prefers-reduced-motion: reduce)');-- const calm = () => Boolean(loader && loader.display && loader.display.get().motion === 'reduced');++ const off = () => document.documentElement.getAttribute('data-ov-motion') === 'off';let playHere = false;// The demos use the shared motion tokens (openvibe-shared/ov-fx.js); Play reads them at click time, after-- // Calm's zero durations have been lifted for this page only.++ // Off's zero durations have been lifted for this page only (Calm keeps its own, calmer lengths).const tokenMs = (name, fallback) => {const raw = getComputedStyle(document.documentElement).getPropertyValue(name).trim();const value = parseFloat(raw);@@@ -36,10 +36,8 @@buttons[0].closest('.cr-demo').before(notice);const refresh = () => {-- notice.hidden = playHere || !(calm() || media.matches);-- notice.querySelector('span').textContent = calm()-- ? 'Animations are off: your Motion setting is Calm.'-- : 'Animations are off: your device asks for less motion.';++ notice.hidden = playHere || !off();++ notice.querySelector('span').textContent = 'Animations are off: your Motion setting is Off.';};notice.querySelector('[data-play-here]').addEventListener('click', () => {playHere = true;@@@ -18,8 +18,10 @@if (!anchor) return;const loader = window.OpenVibeThemeLoader;const media = window.matchMedia('(prefers-reduced-motion: reduce)');-- const calm = () => Boolean(loader && loader.display && loader.display.get().motion === 'reduced');-- const reduced = () => calm() || media.matches;++ // 'full' | 'calm' | 'off' (openvibe-shared 4.183.0): Calm plays every effect, smaller and slower; only Off stops them.++ const level = () => { const m = document.documentElement.getAttribute('data-ov-motion'); return m === 'off' ? 'off' : m === 'reduced' ? 'calm' : 'full'; };++ const chosen = () => (loader && loader.display ? loader.display.get().motion : 'auto');++ const reduced = () => level() !== 'full';const playing = () => page.hasAttribute('data-fx-demo');const notice = document.createElement('div');@@@ -29,7 +31,7 @@const words = document.createElement('p');const acts = document.createElement('div');const button = (label, kind) => { const b = document.createElement('button'); b.type = 'button'; b.className = `ovk-btn ovk-btn--${kind}`; b.textContent = label; acts.appendChild(b); return b; };-- const playBtn = button('Play them on this page', 'primary');++ const playBtn = button('Play them in full on this page', 'primary');const stopBtn = button('Stop', 'quiet');const calmBtn = button('Turn Calm off everywhere', 'quiet');notice.append(words, acts);@@@ -39,15 +41,22 @@const on = playing();notice.hidden = !(reduced() || on);notice.classList.toggle('is-playing', on);++ const lv = level(), mine = chosen();words.textContent = on-- ? 'Playing on this page only. Your setting has not changed.'-- : calm()-- ? 'Calm is on, so nothing on this page moves. That is the setting working.'-- : 'This device asks for less motion, so nothing on this page moves. That is the setting working.';++ ? 'Playing in full on this page only. Your setting has not changed.'++ : lv === 'off'++ ? 'Motion is off, so nothing on this page moves. That is the setting working.'++ : mine === 'reduced'++ ? 'Calm is on: every effect here plays, smaller and slower.'++ : 'This device asks for less motion, so you are seeing the calm versions: every effect plays, smaller and slower.';playBtn.hidden = on;stopBtn.hidden = !on;-- calmBtn.hidden = on || !calm();-- if (!on && reduced()) document.querySelectorAll('.br-motion-video').forEach((video) => video.pause());++ // One way out beside the notice, named for what it changes: Calm you chose, motion you turned off, or the++ // device's own setting (Full outranks it).++ calmBtn.hidden = on || lv === 'full';++ calmBtn.textContent = lv === 'off' ? 'Turn motion back on' : mine === 'reduced' ? 'Turn Calm off everywhere' : 'Always play in full';++ calmBtn.dataset.set = lv === 'off' || mine === 'reduced' ? 'auto' : 'full';++ if (!on && lv === 'off') document.querySelectorAll('.br-motion-video').forEach((video) => video.pause());}// The demos were wired while motion was reduced, so a reveal or a count-up skipped itself. Making each stage's@@@ -72,7 +81,7 @@}playBtn.addEventListener('click', play);stopBtn.addEventListener('click', stop);-- calmBtn.addEventListener('click', () => { if (loader && loader.display) loader.display.set({ motion: 'auto' }); refresh(); });++ calmBtn.addEventListener('click', () => { if (loader && loader.display) loader.display.set({ motion: calmBtn.dataset.set || 'auto' }); refresh(); });media.addEventListener('change', refresh);new MutationObserver(refresh).observe(document.documentElement, { attributes: true, attributeFilter: ['data-ov-motion'] });refresh();@@@ -49,13 +49,19 @@function check(name, value) {root.querySelectorAll(`input[name="${name}"]`).forEach((i) => { if (i.type === 'radio') i.checked = i.value === value; });}-- // The device's own setting wins over "On": say so, where the choice is made.++ // One line under Motion says what the chosen setting does, the device's own setting counted in.const motionNote = $('#lk-motion-note');let lessMotion = null;try { lessMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); lessMotion.addEventListener('change', () => showSettings()); } catch (e) { lessMotion = null; }function showSettings() {const d = L.display.get();-- if (motionNote) motionNote.hidden = !(lessMotion && lessMotion.matches);++ if (motionNote) {++ const asks = !!(lessMotion && lessMotion.matches);++ motionNote.textContent = d.motion === 'full' ? (asks ? 'Everything plays, though this device asks for less.' : 'Everything plays.')++ : d.motion === 'reduced' ? 'Every effect still plays, smaller and slower.'++ : d.motion === 'off' ? 'Nothing moves.'++ : asks ? 'Follows your device. It asks for less motion, so you get Calm: every effect, smaller and slower.' : 'Follows your device.';++ }check('scheme', L.scheme.get());check('text', d.text);check('motion', d.motion);@@@ -11,10 +11,8 @@function load(key, fallback) { try { var v = JSON.parse(localStorage.getItem(key) || 'null'); return v == null ? fallback : v; } catch (e) { return fallback; } }function store(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { /* a private window keeps it for the page only */ } }function all(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }-- function calm() {-- try { if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return true; } catch (e) { /* */ }-- return document.documentElement.getAttribute('data-ov-motion') === 'reduced';-- }++ // Only motion off keeps the page still; Calm plays it (openvibe-shared 4.183.0).++ function calm() { return document.documentElement.getAttribute('data-ov-motion') === 'off'; }function init() {var root = document.querySelector('[data-mx]');@@@ -50,7 +50,6 @@ footer b{color:var(--ink)}@keyframes ringBreathe{from{transform:scale(1);opacity:1}to{transform:scale(.9);opacity:.5}}.run{animation:run 1.15s linear infinite}@keyframes run{to{stroke-dashoffset:-151}}--@media (prefers-reduced-motion:reduce){.busy,.run{animation:none}}#scratch{position:absolute;width:0;height:0;overflow:hidden}</style>@@@ -84,7 +84,7 @@+ '@keyframes ovm-ring{0%,100%{transform:rotate(0)}15%{transform:rotate(14deg)}32%{transform:rotate(-12deg)}50%{transform:rotate(8deg)}68%{transform:rotate(-5deg)}84%{transform:rotate(2deg)}}'+ '@keyframes ovm-beat{0%,100%{transform:scale(1)}18%{transform:scale(1.2)}36%{transform:scale(.96)}54%{transform:scale(1.1)}72%{transform:scale(1)}}'+ '@keyframes ovm-shake{0%,100%{transform:translateX(0)}20%{transform:translateX(-3px)}40%{transform:translateX(3px)}60%{transform:translateX(-2px)}80%{transform:translateX(1px)}}'-- // Calm: the system's reduced motion or OpenVibe's (html[data-ov-motion="reduced"]) stops every move.++ // Still: only motion off (html[data-ov-motion="off"]) stops every move; Calm and a device that asks for less play them.+ ':host([data-calm]) svg,:host([data-calm]) svg *{animation:none!important;transition:none!important}'+ ':host([data-calm]) .l>*{stroke-dashoffset:0!important}:host([data-calm]) svg{transform:none!important}';var sheet = null;@@@ -92,7 +92,7 @@var live = new Set();var reduceMq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;-- function calm() { return !!(reduceMq && reduceMq.matches) || document.documentElement.getAttribute('data-ov-motion') === 'reduced'; }++ function calm() { return document.documentElement.getAttribute('data-ov-motion') === 'off'; }if (reduceMq && reduceMq.addEventListener) reduceMq.addEventListener('change', function () { live.forEach(function (el) { el._calm(); }); });function pref() {var p = document.documentElement.getAttribute('data-ov-icons');@@@ -1,7 +1,7 @@'use strict';/*** Icon motion (plan L50 step 7): six presets, each a single move when something happens. Nothing loops and nothing-- * spins; reduced motion (the system's, or OpenVibe's calm setting, html[data-ov-motion="reduced"]) stops them all.++ * spins; Calm plays them; only motion off (html[data-ov-motion="off"]) stops them all.* The CSS is public/css/icon-motion.css (served as /icons/pack/ov-icon-motion.css); <ov-icon motion="…"> carries the* same moves in its own shadow.*@@@ -35,6 +35,10 @@ const RULES = Object.freeze(['There are two sets: a site’s own mark (live, chat, quest: drawn in the page) and the glyphs (wrench, heart, camera: drawn as SVG). A name from one written as the other draws the fallback mark, a bare "V". A page’s checks fail when it asks for a mark that does not exist.','2026-11-10', 'Most of Quest’s icons were a "V": glyph names written as site marks, and the check looked at the wrong list.',[{ label: 'Icons', href: '/icons' }]),++ R('calm-reduces-off-removes', 'honest', 'Calm makes motion smaller and slower. Only Off removes it.',++ 'Motion has four settings: Automatic (follows the device), Full, Calm and Off. Under Calm, and on a device that asks for less motion, every effect still plays: a third of the distance, no overshoot, endless loops at 0.6 of their speed. A page never stops an effect because motion is reduced; a rule that stops one belongs to Off (html[data-ov-motion=off]). A distance of 24px or more is multiplied by --ov-move. Only what starts by itself and fills the view (a video, a carousel) still waits for a tap under Calm.',++ '2026-11-10', 'Reduced motion gave every animation no length at all, so Calm pages looked broken: the owner asked for motion reduced, not removed.',++ [{ label: 'Motion', href: '/motion' }, { label: 'Your look', href: '/look' }], ['openvibe-shared test: motion-setting']),R('count-what-it-says', 'honest', 'A number counts exactly what its label says.','A count next to a list equals the length of that list. "4 agents working now" must count agents at work, not the agent catalogue.','2026-11-01', 'A home page counted its catalogue as "working now".',@@@ -50,10 +50,11 @@ function lookPage({ themes = [], me = {} } = {}) {note: signedIn ? 'Each change applies at once and is saved to your account, so every OpenVibe site follows.' : 'Each change applies at once on this device. Sign in and it follows you to every OpenVibe site.',body: kit.seg({ name: 'scheme', label: 'Light or dark', options: [['light', 'Light'], ['dark', 'Dark'], ['auto', 'Automatic']], value: 'auto' })+ kit.seg({ name: 'text', label: 'Text size', options: [['100', 'Standard'], ['112', 'Larger'], ['125', 'Largest']], value: '100' })-- // "On" is what the device allows (theme-loader's 'auto'), so the line under it says when the device itself-- // asks for less motion: "Full" claimed motion that a phone set to reduce it never showed.-- + kit.seg({ name: 'motion', label: 'Motion', options: [['auto', 'On'], ['reduced', 'Calm']], value: 'auto' })-- + '<p class="lk-motion-note" id="lk-motion-note" hidden>This device asks for less motion, so OpenVibe keeps still on it whichever you choose.</p>'++ // Motion (openvibe-shared 4.183.0; the owner, 2026-11-10: "it should still have full motion but actually++ // just reduce it instead of getting rid of it"): Automatic follows the device, Full outranks it, Calm keeps++ // every effect smaller and slower, Off alone stops motion. The line under it says what the chosen one does.++ + kit.seg({ name: 'motion', label: 'Motion', options: [['auto', 'Automatic'], ['full', 'Full'], ['reduced', 'Calm'], ['off', 'Off']], value: 'auto' })++ + '<p class="lk-motion-note" id="lk-motion-note" aria-live="polite">Follows your device.</p>'// Icons (openvibe-shared 4.71.0, plan L56; the owner, 2026-11-01: "a setting to prefer a certain icon type…// configurable for users in most cases"): the sample under it is ordinary page markup, drawn by ov-glyphs.js, so// it changes as the choice does.@@@ -21,15 +21,21 @@ const read = (f) => fs.readFileSync(path.join(__dirname, '..', f), 'utf8');assert.ok(js.includes("removeAttribute('data-fx-demo')"), 'Stop takes it back');assert.ok(!/\.animate\(/.test(js), 'no stand-in animation: what plays is the shared effect itself');assert.ok(!js.includes("removeAttribute('data-ov-motion')"), 'the person\'s Motion setting is never touched to play a demo');-- assert.ok(js.includes('This device asks for less motion') && js.includes('Calm is on'), 'both reasons are said in words');++ assert.ok(js.includes('This device asks for less motion, so you are seeing the calm versions') && js.includes('Calm is on: every effect here plays, smaller and slower.') && js.includes('Motion is off, so nothing on this page moves.'), 'each reason is said in words: the device, Calm, Off');++ assert.ok(js.includes("'Always play in full'") && js.includes("'Turn Calm off everywhere'") && js.includes("'Turn motion back on'"), 'one way out beside the notice, named for what it changes');-- // The pages' own reduced-motion rules leave the scope alone.-- assert.ok(read('public/css/motion.css').includes('.mo-race.is-run:not([data-fx-demo] *) i{transition:none}'));-- assert.ok(read('public/css/brand.css').includes('.br-chip:not([data-fx-demo] *)'));++ // Calm reduces, only Off removes (openvibe-shared 4.183.0): no stylesheet here stops an effect under Calm or++ // because the device asked for less.++ for (const f of fs.readdirSync(path.join(__dirname, '..', 'public', 'css'))) {++ const css = read(`public/css/${f}`);++ assert.ok(!/prefers-reduced-motion:\s*reduce\)\s*\{[^@]*?(animation|transition)\s*:\s*none/.test(css), `${f}: nothing stops because the device asked for less`);++ assert.ok(!/data-ov-motion="?reduced"?\][^{}]*\{[^}]*(animation|transition)\s*:\s*none/.test(css), `${f}: nothing stops under Calm`);++ }// The shared code that honours the scope is the pinned one.const fx = fs.readFileSync(path.join(path.dirname(require.resolve('openvibe-shared/package.json')), 'ov-fx.js'), 'utf8');assert.ok(fx.includes('[data-fx-demo]'), 'openvibe-shared is 4.180.0 or newer (the demo scope)');++ assert.ok(fx.includes('html[data-ov-motion=off]{--ov-move:0;'), 'and 4.183.0 or newer (Calm reduces, only Off removes)');const pages = ['/motion', '/brand'];const answers = await Promise.all(pages.map((p) => t.get(p)));@@@ -38,12 +44,13 @@ const read = (f) => fs.readFileSync(path.join(__dirname, '..', f), 'utf8');assert.match(r.text, /\/js\/demo-motion-controls\.js\?v=[0-9a-f]{10}/, `${pages[i]} loads the controls`);});-- // Your look: the Motion choice says what it is, and has the line for a device that asks for less motion.++ // Your look: four Motion choices, and one line under them that says what the chosen one does.const look = await t.get('/look');-- assert.match(look.text, /name="motion" value="auto"[^>]*>\s*<span>On<\/span>/);-- assert.match(look.text, /name="motion" value="reduced"[^>]*>\s*<span>Calm<\/span>/);-- assert.match(look.text, /<p class="lk-motion-note" id="lk-motion-note" hidden>This device asks for less motion/);-- console.log('demo-motion: the real effects play in a demo scope; the reasons are said; Your look is honest about the device');++ for (const [value, label] of [['auto', 'Automatic'], ['full', 'Full'], ['reduced', 'Calm'], ['off', 'Off']]) assert.match(look.text, new RegExp(`name="motion" value="${value}"[^>]*>\\s*<span>${label}</span>`), `Motion offers ${label}`);++ assert.match(look.text, /<p class="lk-motion-note" id="lk-motion-note" aria-live="polite">Follows your device\.<\/p>/);++ const lookJs = read('public/js/look.js');++ assert.ok(lookJs.includes('Every effect still plays, smaller and slower.') && lookJs.includes('Nothing moves.') && lookJs.includes('It asks for less motion, so you get Calm'), 'the line says what each choice does, the device counted in');++ console.log('demo-motion: the real effects play in a demo scope; each reason is said; Calm stops nothing; Your look offers four choices and says what each does');} finally {await t.close();}@@@ -197,8 +197,9 @@ const jsonLd = (html) => [...html.matchAll(/<script type="application\/ld\+json"assert.equal(css.headers.get('access-control-allow-origin'), '*');for (const id of ['draw', 'pop', 'nudge', 'ring', 'beat', 'shake']) assert.ok(css.text.includes(`.ovm-${id}`), id);assert.ok(!/infinite|rotate\(360deg\)|turn\)/.test(css.text), 'nothing loops or spins');-- assert.match(css.text, /@media \(prefers-reduced-motion: reduce\)/);-- assert.match(css.text, /html\[data-ov-motion="reduced"\]/, 'OpenVibe\'s calm setting stops them too');++ assert.match(css.text, /html\[data-ov-motion=off\]/, 'only motion off stops an icon; Calm plays it');++ assert.doesNotMatch(css.text, /prefers-reduced-motion/);++ assert.doesNotMatch(css.text, /data-ov-motion="?reduced/, 'Calm plays them');const wc = (await t.get('/ov-icon.js')).text;for (const k of ['ovm-draw', 'ovm-ring', 'ovm-beat', 'ovm-shake']) assert.ok(wc.includes(`@keyframes ${k}{`) && css.text.includes(`@keyframes ${k} {`), `${k} in both`);assert.ok(!/infinite/.test(wc), 'the component loops nothing either');
@@ -234,6 +234,6 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by
@@ -234,6 +234,6 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.384.0
- openvibe-contracts: v0.384.0
- openvibe-sdk: v0.133.0
- openvibe-sdk: v0.133.0
-- openvibe-shared: v4.180.0
+- openvibe-shared: v4.183.0
- openvibe-publishing: v1.7.0
- openvibe-publishing: v1.7.0
<!-- versions:end -->
<!-- versions:end -->
@@ -10,12 +10,12 @@
@@ -10,12 +10,12 @@
"domain": "openvibe.design",
"domain": "openvibe.design",
"contracts": "openvibe-contracts v0.384.0",
"contracts": "openvibe-contracts v0.384.0",
"sdk": "openvibe-sdk v0.133.0",
"sdk": "openvibe-sdk v0.133.0",
- "shared": "openvibe-shared v4.180.0",
+ "shared": "openvibe-shared v4.183.0",
"packages": [
"packages": [
"openvibe-contracts v0.384.0",
"openvibe-contracts v0.384.0",
"openvibe-publishing v1.7.0",
"openvibe-publishing v1.7.0",
"openvibe-sdk v0.133.0",
"openvibe-sdk v0.133.0",
- "openvibe-shared v4.180.0"
+ "openvibe-shared v4.183.0"
],
],
"summary": "The service skeleton for openvibe.design: Network sign-in as OAuth client `design` (PKCE S256, httpOnly design_at/design_rt cookies) and a theme API proxy to OpenVibe.Network's /api/themes. The proxy serves the public catalogue (cached 5 minutes), one theme, the person's active theme and custom variables, saving a theme with source 'studio', its history and a restore by id, and a submission to the gallery. Writes need this site's own Origin; Network 4xx answers pass through and a Network failure is 502. The studio is designed: a gallery, previews on a Live channel, a Chat room and a shop's grid (with colour-vision views), a six-colour editor that derives the full token set, WCAG checks that gate Use, sharing to the gallery, and History with Restore. The Arena queues one prompt for three models through OpenVibe.AI (client credentials, ai.run.create), stores sanitized HTML with reported model/provider/cost/time/tokens, renders it only in sandboxed CSP-capped iframes, draws blind pairs, records one vote per person per round, and keeps Elo ratings per category plus median cost and time. The prompts, outputs and votes are an open JSONL dataset; rounds and votes follow ADR-033 export and deletion.",
"summary": "The service skeleton for openvibe.design: Network sign-in as OAuth client `design` (PKCE S256, httpOnly design_at/design_rt cookies) and a theme API proxy to OpenVibe.Network's /api/themes. The proxy serves the public catalogue (cached 5 minutes), one theme, the person's active theme and custom variables, saving a theme with source 'studio', its history and a restore by id, and a submission to the gallery. Writes need this site's own Origin; Network 4xx answers pass through and a Network failure is 502. The studio is designed: a gallery, previews on a Live channel, a Chat room and a shop's grid (with colour-vision views), a six-colour editor that derives the full token set, WCAG checks that gate Use, sharing to the gallery, and History with Restore. The Arena queues one prompt for three models through OpenVibe.AI (client credentials, ai.run.create), stores sanitized HTML with reported model/provider/cost/time/tokens, renders it only in sandboxed CSP-capped iframes, draws blind pairs, records one vote per person per round, and keeps Elo ratings per category plus median cost and time. The prompts, outputs and votes are an open JSONL dataset; rounds and votes follow ADR-033 export and deletion.",
"works": [
"works": [
@@ -19,7 +19,7 @@
@@ -19,7 +19,7 @@
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.133.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.133.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.180.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.183.0",
"pg": "^8.23.1",
"pg": "^8.23.1",
"subset-font": "^2.9.0",
"subset-font": "^2.9.0",
"wawoff2": "^2.0.1"
"wawoff2": "^2.0.1"
@@ -1395,9 +1395,9 @@
@@ -1395,9 +1395,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.180.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.180.0",
- "integrity": "sha512-VZBOvI2D+ONBudakLG141Go1PaD0NkycldUxSYuQLzG3ZebewlRhGinzKI7Qj+RmrmFy2b4g7Wjb71XkDo2KCg==",
+ "version": "4.183.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.183.0",
+ "integrity": "sha512-vp9gg++Dhyj3h0cwvBjujM6dSFYoeXdh1UXdZ6OXP/NjG672Fs7uRANV4MjT467JtFDN7r7qLI3OJAwGQxOuQw==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -25,7 +25,7 @@
@@ -25,7 +25,7 @@
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.133.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.133.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.180.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.183.0",
"pg": "^8.23.1",
"pg": "^8.23.1",
"subset-font": "^2.9.0",
"subset-font": "^2.9.0",
"wawoff2": "^2.0.1"
"wawoff2": "^2.0.1"
@@ -156,7 +156,7 @@
@@ -156,7 +156,7 @@
.br-play-stage{min-height:220px}
.br-play-stage{min-height:220px}
.br-sizes{gap:18px 22px}
.br-sizes{gap:18px 22px}
}
}
-@media (prefers-reduced-motion:reduce){.br-chip:not([data-fx-demo] *),.br-files a:not([data-fx-demo] *){transition:none}.br:not([data-fx-demo]) .br-files a:hover{transform:none}}
+html[data-ov-motion=off] .br:not([data-fx-demo]) .br-files a:hover{transform:none}
.br-h3-note{margin-left:6px;color:var(--text-muted);font-weight:600;font-size:12px}
.br-h3-note{margin-left:6px;color:var(--text-muted);font-weight:600;font-size:12px}
/* A phone: the motion files are a sideways rail (the owner, 10-31: short pages by rails and folds, never by hiding). */
/* A phone: the motion files are a sideways rail (the owner, 10-31: short pages by rails and folds, never by hiding). */
@@ -124,7 +124,6 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
@@ -124,7 +124,6 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
/* The home's motif: the theme orb (each dot a theme's accent); its dashed ring breathes, it never spins. */
/* The home's motif: the theme orb (each dot a theme's accent); its dashed ring breathes, it never spins. */
.ds-orb { position: relative; width: min(100%, 320px); aspect-ratio: 1; margin: 0 auto; }
.ds-orb { position: relative; width: min(100%, 320px); aspect-ratio: 1; margin: 0 auto; }
.ds-orb-ring { position: absolute; inset: 8%; border-radius: 50%; border: 1.5px dashed color-mix(in srgb, var(--room-accent) 40%, transparent); animation: ds-breathe 9s var(--ov-ease-in-out, ease-in-out) infinite alternate; }
.ds-orb-ring { position: absolute; inset: 8%; border-radius: 50%; border: 1.5px dashed color-mix(in srgb, var(--room-accent) 40%, transparent); animation: ds-breathe 9s var(--ov-ease-in-out, ease-in-out) infinite alternate; }
-@media (prefers-reduced-motion: reduce) { .ds-orb-ring { animation: none; } }
.ds-orb-core { position: absolute; inset: 30%; border-radius: 50%; background: conic-gradient(from 90deg, var(--room-accent), #5b7cfa, #22c55e, #f59e0b, #ef4444, var(--room-accent)); filter: saturate(1.1); box-shadow: 0 20px 60px -20px var(--room-accent); }
.ds-orb-core { position: absolute; inset: 30%; border-radius: 50%; background: conic-gradient(from 90deg, var(--room-accent), #5b7cfa, #22c55e, #f59e0b, #ef4444, var(--room-accent)); filter: saturate(1.1); box-shadow: 0 20px 60px -20px var(--room-accent); }
.ds-orb i { position: absolute; left: 50%; top: 50%; width: 22px; height: 22px; margin: -11px; border-radius: 50%; border: 3px solid var(--bg-primary, #0a0f1c); transform: rotate(calc(var(--i) * 60deg)) translateY(-136%) rotate(calc(var(--i) * -60deg)); }
.ds-orb i { position: absolute; left: 50%; top: 50%; width: 22px; height: 22px; margin: -11px; border-radius: 50%; border: 3px solid var(--bg-primary, #0a0f1c); transform: rotate(calc(var(--i) * 60deg)) translateY(-136%) rotate(calc(var(--i) * -60deg)); }
@supports (container-type: inline-size) { .ds-orb { container-type: inline-size; } .ds-orb i { transform: rotate(calc(var(--i) * 60deg)) translateY(-42cqw) rotate(calc(var(--i) * -60deg)); } }
@supports (container-type: inline-size) { .ds-orb { container-type: inline-size; } .ds-orb i { transform: rotate(calc(var(--i) * 60deg)) translateY(-42cqw) rotate(calc(var(--i) * -60deg)); } }
@@ -142,8 +141,6 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
@@ -142,8 +141,6 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
.ds-eps > div { padding: 12px 14px; border-radius: 14px; background: var(--bg-card, #0f1729); border: 1px solid var(--border, #1f2d47); }
.ds-eps > div { padding: 12px 14px; border-radius: 14px; background: var(--bg-card, #0f1729); border: 1px solid var(--border, #1f2d47); }
.ds-eps dt code { font: 600 .9rem/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: var(--room-strong); overflow-wrap: anywhere; }
.ds-eps dt code { font: 600 .9rem/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: var(--room-strong); overflow-wrap: anywhere; }
.ds-eps dd { margin: 6px 0 0; color: var(--text-secondary, #96a7c2); line-height: 1.5; }
.ds-eps dd { margin: 6px 0 0; color: var(--text-secondary, #96a7c2); line-height: 1.5; }
-@media (prefers-reduced-motion: reduce) { .ds-orb-ring { animation: none; } .ds-swatch, .ds-previews { transition: none; } }
-html[data-ov-motion=reduced] .ds-orb-ring { animation: none; }
/* ── The Design Arena (plan 2026-10-28 §5.2 v2) ──────────────────────────────────────────────────────────────
/* ── The Design Arena (plan 2026-10-28 §5.2 v2) ──────────────────────────────────────────────────────────────
Server-rendered placeholders for the main session's design. The generated page only ever appears inside an
Server-rendered placeholders for the main session's design. The generated page only ever appears inside an
@@ -249,7 +246,6 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef
@@ -249,7 +246,6 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef
/* Forms */
/* Forms */
.ar-form select:focus-visible, .ar-form textarea:focus-visible { outline: 2px solid var(--room-strong); outline-offset: 1px; }
.ar-form select:focus-visible, .ar-form textarea:focus-visible { outline: 2px solid var(--room-strong); outline-offset: 1px; }
.ar-form textarea::placeholder { color: var(--text-muted, #7386a3); }
.ar-form textarea::placeholder { color: var(--text-muted, #7386a3); }
-@media (prefers-reduced-motion: reduce) { .ar-vote-side { transition: none; } }
/* Design Studio A: a quiet prompt and canvas. */
/* Design Studio A: a quiet prompt and canvas. */
.dv-visually-hidden { position: absolute; clip-path: inset(50%); width: 1px; height: 1px; overflow: hidden; }
.dv-visually-hidden { position: absolute; clip-path: inset(50%); width: 1px; height: 1px; overflow: hidden; }
.dv-create { max-width: 800px; margin: clamp(48px, 13vh, 150px) auto 100px; padding: 0 20px; }
.dv-create { max-width: 800px; margin: clamp(48px, 13vh, 150px) auto 100px; padding: 0 20px; }
@@ -308,7 +304,6 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef
@@ -308,7 +304,6 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef
.dv-toolbar select { max-width: 102px; }
.dv-toolbar select { max-width: 102px; }
.dv-toolbar form, .dv-toolbar label { gap: 4px; font-size: .72rem; }
.dv-toolbar form, .dv-toolbar label { gap: 4px; font-size: .72rem; }
}
}
-@media (prefers-reduced-motion: reduce) { .dv-frame { transition: none; } .dv-skeleton { animation: none; } }
/* Part B: the working frame, the chat composer, versions and the click-edit panel. One toolbar stays the only
/* Part B: the working frame, the chat composer, versions and the click-edit panel. One toolbar stays the only
floating control bar; the composer and the version list are in the page flow, and the element panel opens only
floating control bar; the composer and the version list are in the page flow, and the element panel opens only
@@ -363,7 +358,6 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef
@@ -363,7 +358,6 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef
.dv-versions { margin-bottom: 104px; }
.dv-versions { margin-bottom: 104px; }
.dv-element { left: 12px; right: 12px; bottom: calc(var(--ovnav-appbar-h, 56px) + 76px); width: auto; max-height: 56vh; }
.dv-element { left: 12px; right: 12px; bottom: calc(var(--ovnav-appbar-h, 56px) + 76px); width: auto; max-height: 56vh; }
}
}
-@media (prefers-reduced-motion: reduce) { .dv-pick { transition: none; } }
/* ── /tokens: a theme as code (views/tokens.js) ── */
/* ── /tokens: a theme as code (views/tokens.js) ── */
.tk { max-width: 980px; }
.tk { max-width: 980px; }
@@ -669,7 +663,6 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef
@@ -669,7 +663,6 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef
.ar-kb { scroll-snap-align: start; }
.ar-kb { scroll-snap-align: start; }
.ar-who { text-align: left; }
.ar-who { text-align: left; }
}
}
-@media (prefers-reduced-motion: reduce) { .ar-kind { transition: none; } }
/* /look: the icon styles side by side (server/views/look.js iconStyles): three small phone screens drawn with the
/* /look: the icon styles side by side (server/views/look.js iconStyles): three small phone screens drawn with the
real glyphs. Each is a button that picks its style; the picked one wears the accent. */
real glyphs. Each is a button that picks its style; the picked one wears the accent. */
@@ -696,7 +689,7 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef
@@ -696,7 +689,7 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef
.lk-st-cap small{color:var(--text-muted);font-size:12px;line-height:1.35}
.lk-st-cap small{color:var(--text-muted);font-size:12px;line-height:1.35}
.lk-style:has(input:checked) .lk-st-cap b{color:var(--accent)}
.lk-style:has(input:checked) .lk-st-cap b{color:var(--accent)}
@media (max-width:480px){.lk-styles{gap:8px}.lk-st-screen{padding:8px 7px 0;border-radius:14px}.lk-st-head{font-size:19px;gap:6px}.lk-st-lines>span+span{display:none}.lk-st-bar{margin:4px -7px 0;font-size:15px}.lk-st-tab{width:26px}.lk-st-cap small{font-size:11px}}
@media (max-width:480px){.lk-styles{gap:8px}.lk-st-screen{padding:8px 7px 0;border-radius:14px}.lk-st-head{font-size:19px;gap:6px}.lk-st-lines>span+span{display:none}.lk-st-bar{margin:4px -7px 0;font-size:15px}.lk-st-tab{width:26px}.lk-st-cap small{font-size:11px}}
-@media (prefers-reduced-motion:reduce){.lk-st-screen{transition:none}.lk-style:active .lk-st-screen{transform:none}}
+html[data-ov-motion=off] .lk-style:active .lk-st-screen{transform:none}
/* Publish on openvibe.space (open item D8), drawn like the Versions panel. */
/* Publish on openvibe.space (open item D8), drawn like the Versions panel. */
.dv-publish { max-width: 1320px; margin: 0 auto 12px; padding: 10px 14px; border: 1px solid var(--border); border-radius: 14px; background: var(--bg-card); }
.dv-publish { max-width: 1320px; margin: 0 auto 12px; padding: 10px 14px; border: 1px solid var(--border); border-radius: 14px; background: var(--bg-card); }
@@ -744,7 +737,7 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef
@@ -744,7 +737,7 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef
.ds-sys small { font-size: 13px; line-height: 1.45; }
.ds-sys small { font-size: 13px; line-height: 1.45; }
@media (max-width: 1000px) { .ds-system-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 1000px) { .ds-system-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px) { .ds-system-grid { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 520px) { .ds-system-grid { grid-template-columns: minmax(0, 1fr); } }
-@media (prefers-reduced-motion: reduce) { .ds-sys { transition: none; } .ds-sys:hover { transform: none; } }
+html[data-ov-motion=off] .ds-sys:hover{transform: none}
/* ── The arena, easier to judge (owner 11-08: "make A and B clickable", "smoother between things") ──
/* ── The arena, easier to judge (owner 11-08: "make A and B clickable", "smoother between things") ──
The drawing is the vote: a transparent button lies over each frame, so a tap picks it and a swipe scrolls the page
The drawing is the vote: a transparent button lies over each frame, so a tap picks it and a swipe scrolls the page
@@ -846,11 +839,7 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef
@@ -846,11 +839,7 @@ html.ar-fit .ar-shot--desktop .ar-shot-frame, html.ar-fit .ar-shot--desktop::bef
.ar-wait.is-loaded { animation: none; background: #fff; }
.ar-wait.is-loaded { animation: none; background: #fff; }
.ar-wait.is-loaded > .ar-frame { opacity: 1; }
.ar-wait.is-loaded > .ar-frame { opacity: 1; }
@keyframes ar-shimmer { to { background-position: -220% 0; } }
@keyframes ar-shimmer { to { background-position: -220% 0; } }
-@media (prefers-reduced-motion: reduce) {
- .ar-pickable, .ar-pick-tag, .ar-arena #ar-pair { transition: none; }
- .ar-vote-pair[data-pick] .ar-pick-tag, .ar-wait, .ar-arena.is-loading #ar-kinds .ar-kind[aria-current] .ar-kind-i { animation: none; }
- .ar-vote-pair[data-pick] .ar-pickable { transform: none; }
-}
+html[data-ov-motion=off] .ar-vote-pair[data-pick] .ar-pickable{transform: none}
/* The kinds rail on a phone: the icon beside the name, the line under both, no word broken mid-way */
/* The kinds rail on a phone: the icon beside the name, the line under both, no word broken mid-way */
@media (max-width: 759px) {
@media (max-width: 759px) {
@@ -35,9 +35,6 @@
@@ -35,9 +35,6 @@
.ovm-shake.is-no, .ovm-shake.ovm-demo:hover, :hover > .ovm-shake.ovm-demo { animation: ovm-shake var(--ov-dur-4, .5s) var(--ov-ease-out, cubic-bezier(.36, .07, .19, .97)); }
.ovm-shake.is-no, .ovm-shake.ovm-demo:hover, :hover > .ovm-shake.ovm-demo { animation: ovm-shake var(--ov-dur-4, .5s) var(--ov-ease-out, cubic-bezier(.36, .07, .19, .97)); }
@keyframes ovm-shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-3px); } 40% { transform: translateX(3px); } 60% { transform: translateX(-2px); } 80% { transform: translateX(1px); } }
@keyframes ovm-shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-3px); } 40% { transform: translateX(3px); } 60% { transform: translateX(-2px); } 80% { transform: translateX(1px); } }
-@media (prefers-reduced-motion: reduce) {
- .ovm-draw :is(path, circle, rect, ellipse, line, polyline) { animation: none; stroke-dashoffset: 0; }
- .ovm-pop, .ovm-nudge, .ovm-ring, .ovm-beat, .ovm-shake { animation: none !important; transition: none !important; transform: none !important; }
-}
-html[data-ov-motion="reduced"] .ovm-draw :is(path, circle, rect, ellipse, line, polyline) { animation: none; stroke-dashoffset: 0; }
-html[data-ov-motion="reduced"] :is(.ovm-pop, .ovm-nudge, .ovm-ring, .ovm-beat, .ovm-shake) { animation: none !important; transition: none !important; transform: none !important; }
+html[data-ov-motion=off] .ovm-draw :is(path, circle, rect, ellipse, line, polyline){stroke-dashoffset: 0}html[data-ov-motion=off] .ovm-pop,html[data-ov-motion=off] .ovm-nudge,html[data-ov-motion=off] .ovm-ring,html[data-ov-motion=off] .ovm-beat,html[data-ov-motion=off] .ovm-shake{transform: none !important}
+html[data-ov-motion=off] .ovm-draw :is(path, circle, rect, ellipse, line, polyline){stroke-dashoffset: 0}
+html[data-ov-motion=off] :is(.ovm-pop, .ovm-nudge, .ovm-ring, .ovm-beat, .ovm-shake){transform: none !important}
@@ -100,7 +100,6 @@
@@ -100,7 +100,6 @@
.gp-more>.gp-use{padding:4px 16px 16px}
.gp-more>.gp-use{padding:4px 16px 16px}
.gp-files{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:16px}
.gp-files{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:16px}
.gp-files>span{color:var(--text-muted);font-size:13px;font-weight:650;margin-right:4px}
.gp-files>span{color:var(--text-muted);font-size:13px;font-weight:650;margin-right:4px}
-@media (prefers-reduced-motion:reduce){.gp-more>summary::before{transition:none}}
/* Get them all: one card per file. */
/* Get them all: one card per file. */
.ic-packs{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px}
.ic-packs{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px}
@@ -190,7 +189,7 @@
@@ -190,7 +189,7 @@
.ic-tile{min-height:92px}
.ic-tile{min-height:92px}
}
}
@media (max-width:420px){.gp-controls{grid-template-columns:minmax(0,1fr)}}
@media (max-width:420px){.gp-controls{grid-template-columns:minmax(0,1fr)}}
-@media (prefers-reduced-motion:reduce){.ic-tile,.ic-packs a{transition:none}.ic-tile:hover{transform:none}}
+html[data-ov-motion=off] .ic-tile:hover{transform:none}
/* Make a kit: tick glyphs; the bar holds the name and the download and stays in reach at the bottom. */
/* Make a kit: tick glyphs; the bar holds the name and the download and stays in reach at the bottom. */
.kit-form{display:grid;gap:12px}
.kit-form{display:grid;gap:12px}
@@ -214,7 +213,6 @@
@@ -214,7 +213,6 @@
.kit-bar .ic-btn{flex:none}
.kit-bar .ic-btn{flex:none}
.kit-bar .ic-btn:disabled{opacity:.5;cursor:default}
.kit-bar .ic-btn:disabled{opacity:.5;cursor:default}
@media (max-width:760px){.kit-grid{grid-template-columns:repeat(auto-fill,minmax(88px,1fr))}}
@media (max-width:760px){.kit-grid{grid-template-columns:repeat(auto-fill,minmax(88px,1fr))}}
-@media (prefers-reduced-motion:reduce){.kit-tile{transition:none}}
/* /icons/migrate (L50 step 4): paste a page, see every icon with its twin. */
/* /icons/migrate (L50 step 4): paste a page, see every icon with its twin. */
.mg-form{display:grid;gap:10px;margin:0 0 8px}
.mg-form{display:grid;gap:10px;margin:0 0 8px}
@@ -258,7 +256,6 @@
@@ -258,7 +256,6 @@
.ic-fold>summary span{color:var(--text-muted);font-weight:500;font-size:13px}
.ic-fold>summary span{color:var(--text-muted);font-weight:500;font-size:13px}
.ic-fold>summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:14px}
.ic-fold>summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:14px}
.ic-fold>.ic-grid{padding:4px 12px 12px}
.ic-fold>.ic-grid{padding:4px 12px 12px}
-@media (prefers-reduced-motion:reduce){.ic-fold>summary::before{transition:none}}
/* A phone: denser tiles, so a screen holds five rows of four. */
/* A phone: denser tiles, so a screen holds five rows of four. */
@media (max-width:520px){
@media (max-width:520px){
.ic .ic-grid{grid-template-columns:repeat(auto-fill,minmax(74px,1fr));gap:6px}
.ic .ic-grid{grid-template-columns:repeat(auto-fill,minmax(74px,1fr));gap:6px}
@@ -103,7 +103,6 @@
@@ -103,7 +103,6 @@
.kt-pn { grid-template-columns: 1fr 1fr; }
.kt-pn { grid-template-columns: 1fr 1fr; }
.kt-pn .kt-pn-all { grid-column: 1 / -1; grid-row: 2; }
.kt-pn .kt-pn-all { grid-column: 1 / -1; grid-row: 2; }
}
}
-@media (prefers-reduced-motion: reduce) { .kt-tile { transition: none; } }
/* ── /kit/layout: the fluid scale, the eight primitives (each in a box the reader can drag narrower) and the base ── */
/* ── /kit/layout: the fluid scale, the eight primitives (each in a box the reader can drag narrower) and the base ── */
.kt-layout-card { display: grid; gap: 4px; margin: 18px 0 0; padding: 16px 18px; border: 1px solid color-mix(in srgb, var(--room-strong) 45%, var(--border, #26344d)); border-radius: 16px;
.kt-layout-card { display: grid; gap: 4px; margin: 18px 0 0; padding: 16px 18px; border: 1px solid color-mix(in srgb, var(--room-strong) 45%, var(--border, #26344d)); border-radius: 16px;
@@ -196,9 +196,8 @@
@@ -196,9 +196,8 @@
@keyframes mxDot{0%,100%{transform:scale(1)}50%{transform:scale(1.28)}}
@keyframes mxDot{0%,100%{transform:scale(1)}50%{transform:scale(1.28)}}
@keyframes mxPulse{0%,100%{opacity:.4}50%{opacity:.95}}
@keyframes mxPulse{0%,100%{opacity:.4}50%{opacity:.95}}
@keyframes mxPeek{0%,100%{transform:none}34%{transform:translateX(-68px)}58%{transform:translateX(6px)}76%{transform:translateX(-16px)}}
@keyframes mxPeek{0%,100%{transform:none}34%{transform:translateX(-68px)}58%{transform:translateX(6px)}76%{transform:translateX(-16px)}}
-@media (prefers-reduced-motion:reduce){.mx-wrap.mx-live .mx-card,.mx-wrap.mx-live .mx-stage :is(.mx-s,.mx-f){transition:none!important}.mx-wrap.mx-live .mx-card:not(.is-in){opacity:1;transform:none}.mx-wrap.mx-live .mx-card:not(.is-in) .mx-stage :is(.mx-s,.mx-f){stroke-dashoffset:0;opacity:1}}
-html[data-ov-motion=reduced] .mx-wrap .mx-card,html[data-ov-motion=reduced] .mx-wrap .mx-card *,html[data-ov-motion=reduced] .mx-ta{animation:none!important;transition:none!important}
-html[data-ov-motion=reduced] .mx-wrap .mx-card:not(.is-in){opacity:1!important;transform:none!important}
+html[data-ov-motion=off] .mx-wrap.mx-live .mx-card:not(.is-in){opacity:1;transform:none}html[data-ov-motion=off] .mx-wrap.mx-live .mx-card:not(.is-in) .mx-stage :is(.mx-s,.mx-f){stroke-dashoffset:0;opacity:1}
+html[data-ov-motion=off] .mx-wrap .mx-card:not(.is-in){opacity:1!important;transform:none!important}
/* ── The menu buttons ───────────────────────────────────────────────────────────────────────────────────────────
/* ── The menu buttons ───────────────────────────────────────────────────────────────────────────────────────────
Drawn in currentColor with the accent (mb-a fills, mb-sa strokes); hovered, tapped or scrolled in (is-play) they
Drawn in currentColor with the accent (mb-a fills, mb-sa strokes); hovered, tapped or scrolled in (is-play) they
@@ -260,7 +259,6 @@ html[data-ov-motion=reduced] .mx-wrap .mx-card:not(.is-in){opacity:1!important;t
@@ -260,7 +259,6 @@ html[data-ov-motion=reduced] .mx-wrap .mx-card:not(.is-in){opacity:1!important;t
@keyframes mbPop{0%,100%{transform:scale(1)}40%{transform:scale(.8)}75%{transform:scale(1.12)}}
@keyframes mbPop{0%,100%{transform:scale(1)}40%{transform:scale(.8)}75%{transform:scale(1.12)}}
@keyframes mbTilt{0%,100%{transform:rotate(0)}25%{transform:rotate(-14deg)}55%{transform:rotate(9deg)}80%{transform:rotate(-4deg)}}
@keyframes mbTilt{0%,100%{transform:rotate(0)}25%{transform:rotate(-14deg)}55%{transform:rotate(9deg)}80%{transform:rotate(-4deg)}}
@keyframes mbNod{0%,100%{transform:translateY(0)}30%{transform:translateY(2.4px)}60%{transform:translateY(-.6px)}80%{transform:translateY(1.2px)}}
@keyframes mbNod{0%,100%{transform:translateY(0)}30%{transform:translateY(2.4px)}60%{transform:translateY(-.6px)}80%{transform:translateY(1.2px)}}
-html[data-ov-motion=reduced] .mb-btn *{animation:none!important}
.mb-card .mx-stage{color:#e8edf3}
.mb-card .mx-stage{color:#e8edf3}
.mb-big{width:auto;height:auto;padding:14px;border-radius:24px}
.mb-big{width:auto;height:auto;padding:14px;border-radius:24px}
.mb-big::before{inset:8px}
.mb-big::before{inset:8px}
@@ -328,7 +326,6 @@ html[data-ov-motion=reduced] .mb-btn *{animation:none!important}
@@ -328,7 +326,6 @@ html[data-ov-motion=reduced] .mb-btn *{animation:none!important}
.mx-nudge{position:fixed;left:50%;bottom:calc(16px + var(--ovnav-appbar-h,0px) + env(safe-area-inset-bottom,0px));transform:translateX(-50%);z-index:60;display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:8px;width:max-content;max-width:calc(100vw - 20px);padding:10px 12px;border-radius:16px;border:1px solid #8b5cf6;background:color-mix(in srgb,var(--mx-card) 95%,transparent);backdrop-filter:blur(10px);box-shadow:0 14px 34px rgba(0,0,0,.4);font-size:14px;animation:mxNudgeIn var(--ov-dur-3, .4s) var(--ov-ease-spring, cubic-bezier(.2,.9,.25,1.1))}
.mx-nudge{position:fixed;left:50%;bottom:calc(16px + var(--ovnav-appbar-h,0px) + env(safe-area-inset-bottom,0px));transform:translateX(-50%);z-index:60;display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:8px;width:max-content;max-width:calc(100vw - 20px);padding:10px 12px;border-radius:16px;border:1px solid #8b5cf6;background:color-mix(in srgb,var(--mx-card) 95%,transparent);backdrop-filter:blur(10px);box-shadow:0 14px 34px rgba(0,0,0,.4);font-size:14px;animation:mxNudgeIn var(--ov-dur-3, .4s) var(--ov-ease-spring, cubic-bezier(.2,.9,.25,1.1))}
.mx-nudge a.mx-btn{text-decoration:none;display:inline-grid;place-items:center}
.mx-nudge a.mx-btn{text-decoration:none;display:inline-grid;place-items:center}
@keyframes mxNudgeIn{from{opacity:0;transform:translate(-50%,12px)}to{opacity:1;transform:translateX(-50%)}}
@keyframes mxNudgeIn{from{opacity:0;transform:translate(-50%,12px)}to{opacity:1;transform:translateX(-50%)}}
-@media (prefers-reduced-motion:reduce){.mx-step.is-call,.mx-nudge{animation:none}}
@media (max-width:640px){.mx-steps .mx-stepn{width:18px;height:18px;font-size:10.5px}.mx-steps .mx-steps-sep{display:none}}
@media (max-width:640px){.mx-steps .mx-stepn{width:18px;height:18px;font-size:10.5px}.mx-steps .mx-steps-sep{display:none}}
/* The menu buttons beside the chosen mark; on a phone the mock bar is just the mark and the button, to compare at a
/* The menu buttons beside the chosen mark; on a phone the mock bar is just the mark and the button, to compare at a
@@ -414,7 +411,6 @@ html[data-ov-motion=reduced] .mb-btn *{animation:none!important}
@@ -414,7 +411,6 @@ html[data-ov-motion=reduced] .mb-btn *{animation:none!important}
.openvibe-navbar .ovnav-orb .mx-navmenu svg{width:22px;height:22px}
.openvibe-navbar .ovnav-orb .mx-navmenu svg{width:22px;height:22px}
.openvibe-navbar .mx-nav-flash{animation:mxNavFlash 1.1s var(--ov-ease-out, ease-out)}
.openvibe-navbar .mx-nav-flash{animation:mxNavFlash 1.1s var(--ov-ease-out, ease-out)}
@keyframes mxNavFlash{0%{box-shadow:0 0 0 0 color-mix(in srgb,#8b5cf6 70%,transparent);border-radius:12px}60%{box-shadow:0 0 0 10px transparent;border-radius:12px}100%{box-shadow:none}}
@keyframes mxNavFlash{0%{box-shadow:0 0 0 0 color-mix(in srgb,#8b5cf6 70%,transparent);border-radius:12px}60%{box-shadow:0 0 0 10px transparent;border-radius:12px}100%{box-shadow:none}}
-@media (prefers-reduced-motion:reduce){.openvibe-navbar .mx-nav-flash{animation:none}}
/* The tally counts both families (a person can like both), and every vote sits in a fold under it. */
/* The tally counts both families (a person can like both), and every vote sits in a fold under it. */
.mx-both{margin:-2px 0 0;font-size:12.5px;color:var(--mx-muted);text-align:center}
.mx-both{margin:-2px 0 0;font-size:12.5px;color:var(--mx-muted);text-align:center}
@@ -94,5 +94,3 @@
@@ -94,5 +94,3 @@
.mo-durs small{display:none}
.mo-durs small{display:none}
}
}
/* Still under reduced motion, except while the person plays the demos on this page ([data-fx-demo], demo-motion-controls.js). */
/* Still under reduced motion, except while the person plays the demos on this page ([data-fx-demo], demo-motion-controls.js). */
-@media (prefers-reduced-motion:reduce){.mo-race.is-run:not([data-fx-demo] *) i{transition:none}}
-html[data-ov-motion=reduced] .mo-race.is-run:not([data-fx-demo] *) i{transition:none}
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
.pgroup{padding:18px}.pcards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.pcard{position:relative;display:grid;gap:6px;min-height:94px;padding:14px 38px 14px 14px;border:1px solid var(--border);border-radius:14px;text-decoration:none;color:var(--text-primary);background:var(--bg-primary);transition:transform var(--ov-dur-2, 180ms) var(--ov-ease-out, cubic-bezier(.2,.7,.2,1)),border-color var(--ov-dur-2, 180ms)}.pcard:hover{transform:translateY(-2px);border-color:var(--accent)}.pcard small{font-size:12px;line-height:1.4;color:var(--text-muted)}.pcard b{position:absolute;right:14px;top:14px;color:var(--accent)}
.pgroup{padding:18px}.pcards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.pcard{position:relative;display:grid;gap:6px;min-height:94px;padding:14px 38px 14px 14px;border:1px solid var(--border);border-radius:14px;text-decoration:none;color:var(--text-primary);background:var(--bg-primary);transition:transform var(--ov-dur-2, 180ms) var(--ov-ease-out, cubic-bezier(.2,.7,.2,1)),border-color var(--ov-dur-2, 180ms)}.pcard:hover{transform:translateY(-2px);border-color:var(--accent)}.pcard small{font-size:12px;line-height:1.4;color:var(--text-muted)}.pcard b{position:absolute;right:14px;top:14px;color:var(--accent)}
.dbtn,.pill-action,.opill,.tab-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:0 14px;border:1px solid var(--border);border-radius:999px;background:var(--bg-primary);color:var(--text-primary);font:inherit;font-size:13px;font-weight:650;cursor:pointer}.dbtn.primary,.pill-action.primary,.opill.primary{background:var(--accent);border-color:transparent;color:var(--on-accent)}.status-badge,.meta-chip{display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:6px 10px;background:color-mix(in srgb, var(--success) 16%, transparent);color:var(--success);font-size:12px;font-weight:700}.status-badge.warn{background:color-mix(in srgb, var(--warning) 16%, transparent);color:var(--warning)}.status-badge.bad{background:color-mix(in srgb, var(--danger) 16%, transparent);color:var(--danger)}.meta-chip{background:var(--bg-primary);border:1px solid var(--border);color:var(--text-secondary);font-weight:500}.demo-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.stream-box{display:grid;gap:7px;max-height:170px;overflow:auto;scroll-behavior:smooth;padding:2px 4px 2px 0}.stream-item,.attempt,.empty-demo,.progress-line,.outcome-line,.ship-line,.tab-panel{padding:12px;border-radius:12px;background:var(--bg-primary);border:1px solid var(--border)}.stream-item.enter{animation:row-in var(--ov-dur-2, 180ms) var(--ov-ease-out, cubic-bezier(.2,.7,.2,1)) both}@keyframes row-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}.demo-fold{width:100%;border:1px solid var(--border);border-radius:12px;background:var(--bg-primary);padding:12px;color:inherit;text-align:left}.demo-fold summary{min-height:44px;cursor:pointer;display:flex;align-items:center;font-weight:700}.fold-content{padding:8px 0 0;color:var(--text-secondary)}.progress-line{display:flex;align-items:center;gap:10px;min-height:44px}.progress-fold{display:none;padding:12px;color:var(--text-muted)}.progress-line.open+.progress-fold{display:block}.outcome-line{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.outcome-copy{flex:1;min-width:140px}.outcome-copy strong,.outcome-copy small{display:block}.outcome-copy small{color:var(--text-muted);margin-top:3px}.attempt-list{display:none;padding:8px 0 0 12px}.ship-line.open+.attempt-list{display:grid;gap:6px}.empty-demo{display:flex;align-items:center;justify-content:space-between;gap:10px}.demo-tabs{display:flex;gap:4px;border-bottom:1px solid var(--border)}.tab-button[aria-selected=true]{color:var(--accent);border-color:var(--accent)}.tab-panel{margin-top:10px;min-height:56px}.target-box{display:inline-grid;place-items:center;width:44px;height:44px;border:1px dashed var(--accent);border-radius:10px}.mark-demo{display:flex;align-items:center;gap:14px;font-size:26px;font-weight:800;letter-spacing:-.05em}.ov-mark{display:inline-grid;place-items:center;width:48px;height:48px;border-radius:16px;background:linear-gradient(145deg,var(--accent),var(--accent-light));color:var(--text-primary);font-size:23px;transition:transform var(--ov-dur-3, 360ms) var(--ov-ease-spring, cubic-bezier(.34,1.56,.64,1))}.ov-mark.loading{animation:mark-pulse 900ms var(--ov-ease-in-out, ease-in-out) infinite alternate}@keyframes mark-pulse{to{transform:rotate(12deg) scale(.88)}}.bottom-demo{position:relative;min-height:155px;padding:12px 12px 70px;border:1px solid var(--border);border-radius:14px;background:var(--bg-primary)}.bottom-nav{position:absolute;bottom:0;left:0;right:0;min-height:56px;padding:6px 12px max(6px,env(safe-area-inset-bottom));display:flex;justify-content:space-around;background:var(--bg-secondary);border-top:1px solid var(--border);border-radius:0 0 14px 14px}.site-switcher-demo{display:flex;align-items:center;gap:12px}.switcher-pop{display:none;position:absolute;z-index:2;top:55px;left:16px;padding:14px;border-radius:14px;background:var(--bg-secondary);border:1px solid var(--border);box-shadow:0 18px 50px color-mix(in srgb, var(--text-primary) 28%, transparent)}.site-switcher-demo.open .switcher-pop{display:grid;gap:8px}.site-switcher-demo{position:relative}.sample-nav{height:52px;padding:0 14px;display:flex;align-items:center;gap:16px;border:1px solid var(--border);border-radius:14px}.sample-nav span:last-child{margin-left:auto;color:var(--text-muted)}.demo-tabs button:focus-visible,.dbtn:focus-visible,.tab-button:focus-visible,.demo-fold summary:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
.dbtn,.pill-action,.opill,.tab-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:0 14px;border:1px solid var(--border);border-radius:999px;background:var(--bg-primary);color:var(--text-primary);font:inherit;font-size:13px;font-weight:650;cursor:pointer}.dbtn.primary,.pill-action.primary,.opill.primary{background:var(--accent);border-color:transparent;color:var(--on-accent)}.status-badge,.meta-chip{display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:6px 10px;background:color-mix(in srgb, var(--success) 16%, transparent);color:var(--success);font-size:12px;font-weight:700}.status-badge.warn{background:color-mix(in srgb, var(--warning) 16%, transparent);color:var(--warning)}.status-badge.bad{background:color-mix(in srgb, var(--danger) 16%, transparent);color:var(--danger)}.meta-chip{background:var(--bg-primary);border:1px solid var(--border);color:var(--text-secondary);font-weight:500}.demo-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.stream-box{display:grid;gap:7px;max-height:170px;overflow:auto;scroll-behavior:smooth;padding:2px 4px 2px 0}.stream-item,.attempt,.empty-demo,.progress-line,.outcome-line,.ship-line,.tab-panel{padding:12px;border-radius:12px;background:var(--bg-primary);border:1px solid var(--border)}.stream-item.enter{animation:row-in var(--ov-dur-2, 180ms) var(--ov-ease-out, cubic-bezier(.2,.7,.2,1)) both}@keyframes row-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}.demo-fold{width:100%;border:1px solid var(--border);border-radius:12px;background:var(--bg-primary);padding:12px;color:inherit;text-align:left}.demo-fold summary{min-height:44px;cursor:pointer;display:flex;align-items:center;font-weight:700}.fold-content{padding:8px 0 0;color:var(--text-secondary)}.progress-line{display:flex;align-items:center;gap:10px;min-height:44px}.progress-fold{display:none;padding:12px;color:var(--text-muted)}.progress-line.open+.progress-fold{display:block}.outcome-line{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.outcome-copy{flex:1;min-width:140px}.outcome-copy strong,.outcome-copy small{display:block}.outcome-copy small{color:var(--text-muted);margin-top:3px}.attempt-list{display:none;padding:8px 0 0 12px}.ship-line.open+.attempt-list{display:grid;gap:6px}.empty-demo{display:flex;align-items:center;justify-content:space-between;gap:10px}.demo-tabs{display:flex;gap:4px;border-bottom:1px solid var(--border)}.tab-button[aria-selected=true]{color:var(--accent);border-color:var(--accent)}.tab-panel{margin-top:10px;min-height:56px}.target-box{display:inline-grid;place-items:center;width:44px;height:44px;border:1px dashed var(--accent);border-radius:10px}.mark-demo{display:flex;align-items:center;gap:14px;font-size:26px;font-weight:800;letter-spacing:-.05em}.ov-mark{display:inline-grid;place-items:center;width:48px;height:48px;border-radius:16px;background:linear-gradient(145deg,var(--accent),var(--accent-light));color:var(--text-primary);font-size:23px;transition:transform var(--ov-dur-3, 360ms) var(--ov-ease-spring, cubic-bezier(.34,1.56,.64,1))}.ov-mark.loading{animation:mark-pulse 900ms var(--ov-ease-in-out, ease-in-out) infinite alternate}@keyframes mark-pulse{to{transform:rotate(12deg) scale(.88)}}.bottom-demo{position:relative;min-height:155px;padding:12px 12px 70px;border:1px solid var(--border);border-radius:14px;background:var(--bg-primary)}.bottom-nav{position:absolute;bottom:0;left:0;right:0;min-height:56px;padding:6px 12px max(6px,env(safe-area-inset-bottom));display:flex;justify-content:space-around;background:var(--bg-secondary);border-top:1px solid var(--border);border-radius:0 0 14px 14px}.site-switcher-demo{display:flex;align-items:center;gap:12px}.switcher-pop{display:none;position:absolute;z-index:2;top:55px;left:16px;padding:14px;border-radius:14px;background:var(--bg-secondary);border:1px solid var(--border);box-shadow:0 18px 50px color-mix(in srgb, var(--text-primary) 28%, transparent)}.site-switcher-demo.open .switcher-pop{display:grid;gap:8px}.site-switcher-demo{position:relative}.sample-nav{height:52px;padding:0 14px;display:flex;align-items:center;gap:16px;border:1px solid var(--border);border-radius:14px}.sample-nav span:last-child{margin-left:auto;color:var(--text-muted)}.demo-tabs button:focus-visible,.dbtn:focus-visible,.tab-button:focus-visible,.demo-fold summary:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
@media(max-width:620px){.pattern-page,.patterns-index{padding:24px 14px 100px}.pcards,.pcodegrid{grid-template-columns:1fr}.pnext{flex-direction:column}.pdemo-body{padding:14px}.pbug,.prule,.ptokens,.pnative{padding:16px}.pcodegrid pre{font-size:10px}}
@media(max-width:620px){.pattern-page,.patterns-index{padding:24px 14px 100px}.pcards,.pcodegrid{grid-template-columns:1fr}.pnext{flex-direction:column}.pdemo-body{padding:14px}.pbug,.prule,.ptokens,.pnative{padding:16px}.pcodegrid pre{font-size:10px}}
-@media(prefers-reduced-motion:reduce){.pcard,.ov-mark{transition:none}.pcard:hover{transform:none}.stream-item.enter{animation:none}.ov-mark.loading{animation:none}.demo-fold,.stream-box{scroll-behavior:auto}}
+html[data-ov-motion=off] .pcard:hover{transform:none}html[data-ov-motion=off] .demo-fold,html[data-ov-motion=off] .stream-box{scroll-behavior:auto}
/* Layout that holds (10-30): the demos for the seven rules. */
/* Layout that holds (10-30): the demos for the seven rules. */
.decor-demo{position:relative;height:170px;overflow:hidden;border-radius:14px;border:1px solid var(--border);background:var(--bg-primary)}
.decor-demo{position:relative;height:170px;overflow:hidden;border-radius:14px;border:1px solid var(--border);background:var(--bg-primary)}
@@ -33,7 +33,6 @@
@@ -33,7 +33,6 @@
.clear-toast{padding:10px 12px;border-radius:12px;border:1px solid var(--border);background:var(--bg-secondary);font-size:13px;font-weight:650;box-shadow:var(--shadow-lg)}
.clear-toast{padding:10px 12px;border-radius:12px;border:1px solid var(--border);background:var(--bg-secondary);font-size:13px;font-weight:650;box-shadow:var(--shadow-lg)}
.clear-pill{position:absolute;right:10px;bottom:10px;display:inline-flex;align-items:center;height:42px;padding:0 16px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);font-weight:750;font-size:13px}
.clear-pill{position:absolute;right:10px;bottom:10px;display:inline-flex;align-items:center;height:42px;padding:0 16px;border-radius:999px;border:1px solid var(--border);background:var(--bg-secondary);font-weight:750;font-size:13px}
.clear-pill::before{content:'';width:8px;height:8px;margin-right:8px;border-radius:50%;background:var(--warning)}
.clear-pill::before{content:'';width:8px;height:8px;margin-right:8px;border-radius:50%;background:var(--warning)}
-@media(prefers-reduced-motion:reduce){.drag-chip,.clear-toasts{transition:none}}
.watch-demo{position:relative;display:flex;flex-direction:column;height:240px;max-width:300px;margin:0 auto;border:1px solid var(--border);border-radius:14px;overflow:hidden;background:var(--bg-primary)}
.watch-demo{position:relative;display:flex;flex-direction:column;height:240px;max-width:300px;margin:0 auto;border:1px solid var(--border);border-radius:14px;overflow:hidden;background:var(--bg-primary)}
.watch-player{position:relative;flex:0 0 auto;aspect-ratio:16/9;background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 45%,#000),#000)}
.watch-player{position:relative;flex:0 0 auto;aspect-ratio:16/9;background:linear-gradient(135deg,color-mix(in srgb,var(--accent) 45%,#000),#000)}
.watch-live{position:absolute;top:8px;left:8px;padding:2px 8px;border-radius:999px;background:#e11d48;color:#fff;font-size:.72rem;font-weight:800}
.watch-live{position:absolute;top:8px;left:8px;padding:2px 8px;border-radius:999px;background:#e11d48;color:#fff;font-size:.72rem;font-weight:800}
@@ -42,7 +41,6 @@
@@ -42,7 +41,6 @@
.watch-demo .bottom-nav{transition:transform var(--ov-dur-base,260ms) ease}
.watch-demo .bottom-nav{transition:transform var(--ov-dur-base,260ms) ease}
.watch-demo.is-watching .bottom-nav{transform:translateY(110%)}
.watch-demo.is-watching .bottom-nav{transform:translateY(110%)}
.watch-demo.is-watching .watch-chat{padding-bottom:8px}
.watch-demo.is-watching .watch-chat{padding-bottom:8px}
-@media (prefers-reduced-motion:reduce){.watch-demo .bottom-nav{transition:none}}
/* 2026-11-01 patterns: labelled cards, ask with a button, own places, mixed words. */
/* 2026-11-01 patterns: labelled cards, ask with a button, own places, mixed words. */
.lcards-demo{max-width:280px}.lcards-table{border-collapse:collapse;width:100%}
.lcards-demo{max-width:280px}.lcards-table{border-collapse:collapse;width:100%}
@@ -63,7 +61,6 @@
@@ -63,7 +61,6 @@
.mixed-panel.light{background:#fff;--panel-text:#1a1a2e}.mixed-panel.dark{background:#0a0f1c;--panel-text:#e6edf7}
.mixed-panel.light{background:#fff;--panel-text:#1a1a2e}.mixed-panel.dark{background:#0a0f1c;--panel-text:#e6edf7}
.mixed-word{padding:2px 8px;border-radius:999px;font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:.04em;border:1px solid var(--hue);color:color-mix(in srgb,var(--hue) 50%,var(--panel-text))}
.mixed-word{padding:2px 8px;border-radius:999px;font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:.04em;border:1px solid var(--hue);color:color-mix(in srgb,var(--hue) 50%,var(--panel-text))}
.mixed-demo.broken .mixed-word{color:var(--hue)}
.mixed-demo.broken .mixed-word{color:var(--hue)}
-@media (prefers-reduced-motion: reduce){.places-node{transition:none}}
/* 11-07 patterns: the menu, site cards, your things, status dots, the phone sheet, drop and tray, card or row, even grids, six places. */
/* 11-07 patterns: the menu, site cards, your things, status dots, the phone sheet, drop and tray, card or row, even grids, six places. */
.menu-demo,.card-demo,.phone-demo,.drop-demo,.cr-demo,.even-demo,.six-demo,.things-bar{border:1px solid var(--border);border-radius:14px;background:var(--bg-primary)}
.menu-demo,.card-demo,.phone-demo,.drop-demo,.cr-demo,.even-demo,.six-demo,.things-bar{border:1px solid var(--border);border-radius:14px;background:var(--bg-primary)}
@@ -125,7 +122,6 @@
@@ -125,7 +122,6 @@
.even-demo span{display:grid;place-items:center;height:44px;border-radius:10px;background:var(--bg-secondary);font-weight:800}
.even-demo span{display:grid;place-items:center;height:44px;border-radius:10px;background:var(--bg-secondary);font-weight:800}
.six-demo{flex-wrap:wrap;padding:8px 10px;font-size:13px}.six-demo strong{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:160px}
.six-demo{flex-wrap:wrap;padding:8px 10px;font-size:13px}.six-demo strong{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:160px}
.six-demo span{padding:4px 8px;border-radius:999px;color:var(--text-secondary)}.six-more{border:1px solid var(--border)}
.six-demo span{padding:4px 8px;border-radius:999px;color:var(--text-secondary)}.six-more{border:1px solid var(--border)}
-@media (prefers-reduced-motion:reduce){.sheet-demo,.tray-demo li i{transition:none}.menu-demo.open .menu-panel{animation:none}}
.status-word{font-weight:700;font-size:15px}
.status-word{font-weight:700;font-size:15px}
/* Honest numbers (2026-11-09) */
/* Honest numbers (2026-11-09) */
@@ -16,11 +16,11 @@
@@ -16,11 +16,11 @@
if (!buttons.length) return;
if (!buttons.length) return;
const loader = window.OpenVibeThemeLoader;
const loader = window.OpenVibeThemeLoader;
const media = window.matchMedia('(prefers-reduced-motion: reduce)');
const media = window.matchMedia('(prefers-reduced-motion: reduce)');
- const calm = () => Boolean(loader && loader.display && loader.display.get().motion === 'reduced');
+ const off = () => document.documentElement.getAttribute('data-ov-motion') === 'off';
let playHere = false;
let playHere = false;
// The demos use the shared motion tokens (openvibe-shared/ov-fx.js); Play reads them at click time, after
// The demos use the shared motion tokens (openvibe-shared/ov-fx.js); Play reads them at click time, after
- // Calm's zero durations have been lifted for this page only.
+ // Off's zero durations have been lifted for this page only (Calm keeps its own, calmer lengths).
const tokenMs = (name, fallback) => {
const tokenMs = (name, fallback) => {
const raw = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
const raw = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
const value = parseFloat(raw);
const value = parseFloat(raw);
@@ -36,10 +36,8 @@
@@ -36,10 +36,8 @@
buttons[0].closest('.cr-demo').before(notice);
buttons[0].closest('.cr-demo').before(notice);
const refresh = () => {
const refresh = () => {
- notice.hidden = playHere || !(calm() || media.matches);
- notice.querySelector('span').textContent = calm()
- ? 'Animations are off: your Motion setting is Calm.'
- : 'Animations are off: your device asks for less motion.';
+ notice.hidden = playHere || !off();
+ notice.querySelector('span').textContent = 'Animations are off: your Motion setting is Off.';
};
};
notice.querySelector('[data-play-here]').addEventListener('click', () => {
notice.querySelector('[data-play-here]').addEventListener('click', () => {
playHere = true;
playHere = true;
@@ -18,8 +18,10 @@
@@ -18,8 +18,10 @@
if (!anchor) return;
if (!anchor) return;
const loader = window.OpenVibeThemeLoader;
const loader = window.OpenVibeThemeLoader;
const media = window.matchMedia('(prefers-reduced-motion: reduce)');
const media = window.matchMedia('(prefers-reduced-motion: reduce)');
- const calm = () => Boolean(loader && loader.display && loader.display.get().motion === 'reduced');
- const reduced = () => calm() || media.matches;
+ // 'full' | 'calm' | 'off' (openvibe-shared 4.183.0): Calm plays every effect, smaller and slower; only Off stops them.
+ const level = () => { const m = document.documentElement.getAttribute('data-ov-motion'); return m === 'off' ? 'off' : m === 'reduced' ? 'calm' : 'full'; };
+ const chosen = () => (loader && loader.display ? loader.display.get().motion : 'auto');
+ const reduced = () => level() !== 'full';
const playing = () => page.hasAttribute('data-fx-demo');
const playing = () => page.hasAttribute('data-fx-demo');
const notice = document.createElement('div');
const notice = document.createElement('div');
@@ -29,7 +31,7 @@
@@ -29,7 +31,7 @@
const words = document.createElement('p');
const words = document.createElement('p');
const acts = document.createElement('div');
const acts = document.createElement('div');
const button = (label, kind) => { const b = document.createElement('button'); b.type = 'button'; b.className = `ovk-btn ovk-btn--${kind}`; b.textContent = label; acts.appendChild(b); return b; };
const button = (label, kind) => { const b = document.createElement('button'); b.type = 'button'; b.className = `ovk-btn ovk-btn--${kind}`; b.textContent = label; acts.appendChild(b); return b; };
- const playBtn = button('Play them on this page', 'primary');
+ const playBtn = button('Play them in full on this page', 'primary');
const stopBtn = button('Stop', 'quiet');
const stopBtn = button('Stop', 'quiet');
const calmBtn = button('Turn Calm off everywhere', 'quiet');
const calmBtn = button('Turn Calm off everywhere', 'quiet');
notice.append(words, acts);
notice.append(words, acts);
@@ -39,15 +41,22 @@
@@ -39,15 +41,22 @@
const on = playing();
const on = playing();
notice.hidden = !(reduced() || on);
notice.hidden = !(reduced() || on);
notice.classList.toggle('is-playing', on);
notice.classList.toggle('is-playing', on);
+ const lv = level(), mine = chosen();
words.textContent = on
words.textContent = on
- ? 'Playing on this page only. Your setting has not changed.'
- : calm()
- ? 'Calm is on, so nothing on this page moves. That is the setting working.'
- : 'This device asks for less motion, so nothing on this page moves. That is the setting working.';
+ ? 'Playing in full on this page only. Your setting has not changed.'
+ : lv === 'off'
+ ? 'Motion is off, so nothing on this page moves. That is the setting working.'
+ : mine === 'reduced'
+ ? 'Calm is on: every effect here plays, smaller and slower.'
+ : 'This device asks for less motion, so you are seeing the calm versions: every effect plays, smaller and slower.';
playBtn.hidden = on;
playBtn.hidden = on;
stopBtn.hidden = !on;
stopBtn.hidden = !on;
- calmBtn.hidden = on || !calm();
- if (!on && reduced()) document.querySelectorAll('.br-motion-video').forEach((video) => video.pause());
+ // One way out beside the notice, named for what it changes: Calm you chose, motion you turned off, or the
+ // device's own setting (Full outranks it).
+ calmBtn.hidden = on || lv === 'full';
+ calmBtn.textContent = lv === 'off' ? 'Turn motion back on' : mine === 'reduced' ? 'Turn Calm off everywhere' : 'Always play in full';
+ calmBtn.dataset.set = lv === 'off' || mine === 'reduced' ? 'auto' : 'full';
+ if (!on && lv === 'off') document.querySelectorAll('.br-motion-video').forEach((video) => video.pause());
}
}
// The demos were wired while motion was reduced, so a reveal or a count-up skipped itself. Making each stage's
// The demos were wired while motion was reduced, so a reveal or a count-up skipped itself. Making each stage's
@@ -72,7 +81,7 @@
@@ -72,7 +81,7 @@
}
}
playBtn.addEventListener('click', play);
playBtn.addEventListener('click', play);
stopBtn.addEventListener('click', stop);
stopBtn.addEventListener('click', stop);
- calmBtn.addEventListener('click', () => { if (loader && loader.display) loader.display.set({ motion: 'auto' }); refresh(); });
+ calmBtn.addEventListener('click', () => { if (loader && loader.display) loader.display.set({ motion: calmBtn.dataset.set || 'auto' }); refresh(); });
media.addEventListener('change', refresh);
media.addEventListener('change', refresh);
new MutationObserver(refresh).observe(document.documentElement, { attributes: true, attributeFilter: ['data-ov-motion'] });
new MutationObserver(refresh).observe(document.documentElement, { attributes: true, attributeFilter: ['data-ov-motion'] });
refresh();
refresh();
@@ -49,13 +49,19 @@
@@ -49,13 +49,19 @@
function check(name, value) {
function check(name, value) {
root.querySelectorAll(`input[name="${name}"]`).forEach((i) => { if (i.type === 'radio') i.checked = i.value === value; });
root.querySelectorAll(`input[name="${name}"]`).forEach((i) => { if (i.type === 'radio') i.checked = i.value === value; });
}
}
- // The device's own setting wins over "On": say so, where the choice is made.
+ // One line under Motion says what the chosen setting does, the device's own setting counted in.
const motionNote = $('#lk-motion-note');
const motionNote = $('#lk-motion-note');
let lessMotion = null;
let lessMotion = null;
try { lessMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); lessMotion.addEventListener('change', () => showSettings()); } catch (e) { lessMotion = null; }
try { lessMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); lessMotion.addEventListener('change', () => showSettings()); } catch (e) { lessMotion = null; }
function showSettings() {
function showSettings() {
const d = L.display.get();
const d = L.display.get();
- if (motionNote) motionNote.hidden = !(lessMotion && lessMotion.matches);
+ if (motionNote) {
+ const asks = !!(lessMotion && lessMotion.matches);
+ motionNote.textContent = d.motion === 'full' ? (asks ? 'Everything plays, though this device asks for less.' : 'Everything plays.')
+ : d.motion === 'reduced' ? 'Every effect still plays, smaller and slower.'
+ : d.motion === 'off' ? 'Nothing moves.'
+ : asks ? 'Follows your device. It asks for less motion, so you get Calm: every effect, smaller and slower.' : 'Follows your device.';
+ }
check('scheme', L.scheme.get());
check('scheme', L.scheme.get());
check('text', d.text);
check('text', d.text);
check('motion', d.motion);
check('motion', d.motion);
@@ -11,10 +11,8 @@
@@ -11,10 +11,8 @@
function load(key, fallback) { try { var v = JSON.parse(localStorage.getItem(key) || 'null'); return v == null ? fallback : v; } catch (e) { return fallback; } }
function load(key, fallback) { try { var v = JSON.parse(localStorage.getItem(key) || 'null'); return v == null ? fallback : v; } catch (e) { return fallback; } }
function store(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { /* a private window keeps it for the page only */ } }
function store(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { /* a private window keeps it for the page only */ } }
function all(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
function all(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
- function calm() {
- try { if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return true; } catch (e) { /* */ }
- return document.documentElement.getAttribute('data-ov-motion') === 'reduced';
- }
+ // Only motion off keeps the page still; Calm plays it (openvibe-shared 4.183.0).
+ function calm() { return document.documentElement.getAttribute('data-ov-motion') === 'off'; }
function init() {
function init() {
var root = document.querySelector('[data-mx]');
var root = document.querySelector('[data-mx]');
@@ -50,7 +50,6 @@ footer b{color:var(--ink)}
@@ -50,7 +50,6 @@ footer b{color:var(--ink)}
@keyframes ringBreathe{from{transform:scale(1);opacity:1}to{transform:scale(.9);opacity:.5}}
@keyframes ringBreathe{from{transform:scale(1);opacity:1}to{transform:scale(.9);opacity:.5}}
.run{animation:run 1.15s linear infinite}
.run{animation:run 1.15s linear infinite}
@keyframes run{to{stroke-dashoffset:-151}}
@keyframes run{to{stroke-dashoffset:-151}}
-@media (prefers-reduced-motion:reduce){.busy,.run{animation:none}}
#scratch{position:absolute;width:0;height:0;overflow:hidden}
#scratch{position:absolute;width:0;height:0;overflow:hidden}
</style>
</style>
@@ -84,7 +84,7 @@
@@ -84,7 +84,7 @@
+ '@keyframes ovm-ring{0%,100%{transform:rotate(0)}15%{transform:rotate(14deg)}32%{transform:rotate(-12deg)}50%{transform:rotate(8deg)}68%{transform:rotate(-5deg)}84%{transform:rotate(2deg)}}'
+ '@keyframes ovm-ring{0%,100%{transform:rotate(0)}15%{transform:rotate(14deg)}32%{transform:rotate(-12deg)}50%{transform:rotate(8deg)}68%{transform:rotate(-5deg)}84%{transform:rotate(2deg)}}'
+ '@keyframes ovm-beat{0%,100%{transform:scale(1)}18%{transform:scale(1.2)}36%{transform:scale(.96)}54%{transform:scale(1.1)}72%{transform:scale(1)}}'
+ '@keyframes ovm-beat{0%,100%{transform:scale(1)}18%{transform:scale(1.2)}36%{transform:scale(.96)}54%{transform:scale(1.1)}72%{transform:scale(1)}}'
+ '@keyframes ovm-shake{0%,100%{transform:translateX(0)}20%{transform:translateX(-3px)}40%{transform:translateX(3px)}60%{transform:translateX(-2px)}80%{transform:translateX(1px)}}'
+ '@keyframes ovm-shake{0%,100%{transform:translateX(0)}20%{transform:translateX(-3px)}40%{transform:translateX(3px)}60%{transform:translateX(-2px)}80%{transform:translateX(1px)}}'
- // Calm: the system's reduced motion or OpenVibe's (html[data-ov-motion="reduced"]) stops every move.
+ // Still: only motion off (html[data-ov-motion="off"]) stops every move; Calm and a device that asks for less play them.
+ ':host([data-calm]) svg,:host([data-calm]) svg *{animation:none!important;transition:none!important}'
+ ':host([data-calm]) svg,:host([data-calm]) svg *{animation:none!important;transition:none!important}'
+ ':host([data-calm]) .l>*{stroke-dashoffset:0!important}:host([data-calm]) svg{transform:none!important}';
+ ':host([data-calm]) .l>*{stroke-dashoffset:0!important}:host([data-calm]) svg{transform:none!important}';
var sheet = null;
var sheet = null;
@@ -92,7 +92,7 @@
@@ -92,7 +92,7 @@
var live = new Set();
var live = new Set();
var reduceMq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;
var reduceMq = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;
- function calm() { return !!(reduceMq && reduceMq.matches) || document.documentElement.getAttribute('data-ov-motion') === 'reduced'; }
+ function calm() { return document.documentElement.getAttribute('data-ov-motion') === 'off'; }
if (reduceMq && reduceMq.addEventListener) reduceMq.addEventListener('change', function () { live.forEach(function (el) { el._calm(); }); });
if (reduceMq && reduceMq.addEventListener) reduceMq.addEventListener('change', function () { live.forEach(function (el) { el._calm(); }); });
function pref() {
function pref() {
var p = document.documentElement.getAttribute('data-ov-icons');
var p = document.documentElement.getAttribute('data-ov-icons');
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
/**
/**
* Icon motion (plan L50 step 7): six presets, each a single move when something happens. Nothing loops and nothing
* Icon motion (plan L50 step 7): six presets, each a single move when something happens. Nothing loops and nothing
- * spins; reduced motion (the system's, or OpenVibe's calm setting, html[data-ov-motion="reduced"]) stops them all.
+ * spins; Calm plays them; only motion off (html[data-ov-motion="off"]) stops them all.
* The CSS is public/css/icon-motion.css (served as /icons/pack/ov-icon-motion.css); <ov-icon motion="…"> carries the
* The CSS is public/css/icon-motion.css (served as /icons/pack/ov-icon-motion.css); <ov-icon motion="…"> carries the
* same moves in its own shadow.
* same moves in its own shadow.
*
*
@@ -35,6 +35,10 @@ const RULES = Object.freeze([
@@ -35,6 +35,10 @@ const RULES = Object.freeze([
'There are two sets: a site’s own mark (live, chat, quest: drawn in the page) and the glyphs (wrench, heart, camera: drawn as SVG). A name from one written as the other draws the fallback mark, a bare "V". A page’s checks fail when it asks for a mark that does not exist.',
'There are two sets: a site’s own mark (live, chat, quest: drawn in the page) and the glyphs (wrench, heart, camera: drawn as SVG). A name from one written as the other draws the fallback mark, a bare "V". A page’s checks fail when it asks for a mark that does not exist.',
'2026-11-10', 'Most of Quest’s icons were a "V": glyph names written as site marks, and the check looked at the wrong list.',
'2026-11-10', 'Most of Quest’s icons were a "V": glyph names written as site marks, and the check looked at the wrong list.',
[{ label: 'Icons', href: '/icons' }]),
[{ label: 'Icons', href: '/icons' }]),
+ R('calm-reduces-off-removes', 'honest', 'Calm makes motion smaller and slower. Only Off removes it.',
+ 'Motion has four settings: Automatic (follows the device), Full, Calm and Off. Under Calm, and on a device that asks for less motion, every effect still plays: a third of the distance, no overshoot, endless loops at 0.6 of their speed. A page never stops an effect because motion is reduced; a rule that stops one belongs to Off (html[data-ov-motion=off]). A distance of 24px or more is multiplied by --ov-move. Only what starts by itself and fills the view (a video, a carousel) still waits for a tap under Calm.',
+ '2026-11-10', 'Reduced motion gave every animation no length at all, so Calm pages looked broken: the owner asked for motion reduced, not removed.',
+ [{ label: 'Motion', href: '/motion' }, { label: 'Your look', href: '/look' }], ['openvibe-shared test: motion-setting']),
R('count-what-it-says', 'honest', 'A number counts exactly what its label says.',
R('count-what-it-says', 'honest', 'A number counts exactly what its label says.',
'A count next to a list equals the length of that list. "4 agents working now" must count agents at work, not the agent catalogue.',
'A count next to a list equals the length of that list. "4 agents working now" must count agents at work, not the agent catalogue.',
'2026-11-01', 'A home page counted its catalogue as "working now".',
'2026-11-01', 'A home page counted its catalogue as "working now".',
@@ -50,10 +50,11 @@ function lookPage({ themes = [], me = {} } = {}) {
@@ -50,10 +50,11 @@ function lookPage({ themes = [], me = {} } = {}) {
note: signedIn ? 'Each change applies at once and is saved to your account, so every OpenVibe site follows.' : 'Each change applies at once on this device. Sign in and it follows you to every OpenVibe site.',
note: signedIn ? 'Each change applies at once and is saved to your account, so every OpenVibe site follows.' : 'Each change applies at once on this device. Sign in and it follows you to every OpenVibe site.',
body: kit.seg({ name: 'scheme', label: 'Light or dark', options: [['light', 'Light'], ['dark', 'Dark'], ['auto', 'Automatic']], value: 'auto' })
body: kit.seg({ name: 'scheme', label: 'Light or dark', options: [['light', 'Light'], ['dark', 'Dark'], ['auto', 'Automatic']], value: 'auto' })
+ kit.seg({ name: 'text', label: 'Text size', options: [['100', 'Standard'], ['112', 'Larger'], ['125', 'Largest']], value: '100' })
+ kit.seg({ name: 'text', label: 'Text size', options: [['100', 'Standard'], ['112', 'Larger'], ['125', 'Largest']], value: '100' })
- // "On" is what the device allows (theme-loader's 'auto'), so the line under it says when the device itself
- // asks for less motion: "Full" claimed motion that a phone set to reduce it never showed.
- + kit.seg({ name: 'motion', label: 'Motion', options: [['auto', 'On'], ['reduced', 'Calm']], value: 'auto' })
- + '<p class="lk-motion-note" id="lk-motion-note" hidden>This device asks for less motion, so OpenVibe keeps still on it whichever you choose.</p>'
+ // Motion (openvibe-shared 4.183.0; the owner, 2026-11-10: "it should still have full motion but actually
+ // just reduce it instead of getting rid of it"): Automatic follows the device, Full outranks it, Calm keeps
+ // every effect smaller and slower, Off alone stops motion. The line under it says what the chosen one does.
+ + kit.seg({ name: 'motion', label: 'Motion', options: [['auto', 'Automatic'], ['full', 'Full'], ['reduced', 'Calm'], ['off', 'Off']], value: 'auto' })
+ + '<p class="lk-motion-note" id="lk-motion-note" aria-live="polite">Follows your device.</p>'
// Icons (openvibe-shared 4.71.0, plan L56; the owner, 2026-11-01: "a setting to prefer a certain icon type…
// Icons (openvibe-shared 4.71.0, plan L56; the owner, 2026-11-01: "a setting to prefer a certain icon type…
// configurable for users in most cases"): the sample under it is ordinary page markup, drawn by ov-glyphs.js, so
// configurable for users in most cases"): the sample under it is ordinary page markup, drawn by ov-glyphs.js, so
// it changes as the choice does.
// it changes as the choice does.
@@ -21,15 +21,21 @@ const read = (f) => fs.readFileSync(path.join(__dirname, '..', f), 'utf8');
@@ -21,15 +21,21 @@ const read = (f) => fs.readFileSync(path.join(__dirname, '..', f), 'utf8');
assert.ok(js.includes("removeAttribute('data-fx-demo')"), 'Stop takes it back');
assert.ok(js.includes("removeAttribute('data-fx-demo')"), 'Stop takes it back');
assert.ok(!/\.animate\(/.test(js), 'no stand-in animation: what plays is the shared effect itself');
assert.ok(!/\.animate\(/.test(js), 'no stand-in animation: what plays is the shared effect itself');
assert.ok(!js.includes("removeAttribute('data-ov-motion')"), 'the person\'s Motion setting is never touched to play a demo');
assert.ok(!js.includes("removeAttribute('data-ov-motion')"), 'the person\'s Motion setting is never touched to play a demo');
- assert.ok(js.includes('This device asks for less motion') && js.includes('Calm is on'), 'both reasons are said in words');
+ assert.ok(js.includes('This device asks for less motion, so you are seeing the calm versions') && js.includes('Calm is on: every effect here plays, smaller and slower.') && js.includes('Motion is off, so nothing on this page moves.'), 'each reason is said in words: the device, Calm, Off');
+ assert.ok(js.includes("'Always play in full'") && js.includes("'Turn Calm off everywhere'") && js.includes("'Turn motion back on'"), 'one way out beside the notice, named for what it changes');
- // The pages' own reduced-motion rules leave the scope alone.
- assert.ok(read('public/css/motion.css').includes('.mo-race.is-run:not([data-fx-demo] *) i{transition:none}'));
- assert.ok(read('public/css/brand.css').includes('.br-chip:not([data-fx-demo] *)'));
+ // Calm reduces, only Off removes (openvibe-shared 4.183.0): no stylesheet here stops an effect under Calm or
+ // because the device asked for less.
+ for (const f of fs.readdirSync(path.join(__dirname, '..', 'public', 'css'))) {
+ const css = read(`public/css/${f}`);
+ assert.ok(!/prefers-reduced-motion:\s*reduce\)\s*\{[^@]*?(animation|transition)\s*:\s*none/.test(css), `${f}: nothing stops because the device asked for less`);
+ assert.ok(!/data-ov-motion="?reduced"?\][^{}]*\{[^}]*(animation|transition)\s*:\s*none/.test(css), `${f}: nothing stops under Calm`);
+ }
// The shared code that honours the scope is the pinned one.
// The shared code that honours the scope is the pinned one.
const fx = fs.readFileSync(path.join(path.dirname(require.resolve('openvibe-shared/package.json')), 'ov-fx.js'), 'utf8');
const fx = fs.readFileSync(path.join(path.dirname(require.resolve('openvibe-shared/package.json')), 'ov-fx.js'), 'utf8');
assert.ok(fx.includes('[data-fx-demo]'), 'openvibe-shared is 4.180.0 or newer (the demo scope)');
assert.ok(fx.includes('[data-fx-demo]'), 'openvibe-shared is 4.180.0 or newer (the demo scope)');
+ assert.ok(fx.includes('html[data-ov-motion=off]{--ov-move:0;'), 'and 4.183.0 or newer (Calm reduces, only Off removes)');
const pages = ['/motion', '/brand'];
const pages = ['/motion', '/brand'];
const answers = await Promise.all(pages.map((p) => t.get(p)));
const answers = await Promise.all(pages.map((p) => t.get(p)));
@@ -38,12 +44,13 @@ const read = (f) => fs.readFileSync(path.join(__dirname, '..', f), 'utf8');
@@ -38,12 +44,13 @@ const read = (f) => fs.readFileSync(path.join(__dirname, '..', f), 'utf8');
assert.match(r.text, /\/js\/demo-motion-controls\.js\?v=[0-9a-f]{10}/, `${pages[i]} loads the controls`);
assert.match(r.text, /\/js\/demo-motion-controls\.js\?v=[0-9a-f]{10}/, `${pages[i]} loads the controls`);
});
});
- // Your look: the Motion choice says what it is, and has the line for a device that asks for less motion.
+ // Your look: four Motion choices, and one line under them that says what the chosen one does.
const look = await t.get('/look');
const look = await t.get('/look');
- assert.match(look.text, /name="motion" value="auto"[^>]*>\s*<span>On<\/span>/);
- assert.match(look.text, /name="motion" value="reduced"[^>]*>\s*<span>Calm<\/span>/);
- assert.match(look.text, /<p class="lk-motion-note" id="lk-motion-note" hidden>This device asks for less motion/);
- console.log('demo-motion: the real effects play in a demo scope; the reasons are said; Your look is honest about the device');
+ for (const [value, label] of [['auto', 'Automatic'], ['full', 'Full'], ['reduced', 'Calm'], ['off', 'Off']]) assert.match(look.text, new RegExp(`name="motion" value="${value}"[^>]*>\\s*<span>${label}</span>`), `Motion offers ${label}`);
+ assert.match(look.text, /<p class="lk-motion-note" id="lk-motion-note" aria-live="polite">Follows your device\.<\/p>/);
+ const lookJs = read('public/js/look.js');
+ assert.ok(lookJs.includes('Every effect still plays, smaller and slower.') && lookJs.includes('Nothing moves.') && lookJs.includes('It asks for less motion, so you get Calm'), 'the line says what each choice does, the device counted in');
+ console.log('demo-motion: the real effects play in a demo scope; each reason is said; Calm stops nothing; Your look offers four choices and says what each does');
} finally {
} finally {
await t.close();
await t.close();
}
}
@@ -197,8 +197,9 @@ const jsonLd = (html) => [...html.matchAll(/<script type="application\/ld\+json"
@@ -197,8 +197,9 @@ const jsonLd = (html) => [...html.matchAll(/<script type="application\/ld\+json"
assert.equal(css.headers.get('access-control-allow-origin'), '*');
assert.equal(css.headers.get('access-control-allow-origin'), '*');
for (const id of ['draw', 'pop', 'nudge', 'ring', 'beat', 'shake']) assert.ok(css.text.includes(`.ovm-${id}`), id);
for (const id of ['draw', 'pop', 'nudge', 'ring', 'beat', 'shake']) assert.ok(css.text.includes(`.ovm-${id}`), id);
assert.ok(!/infinite|rotate\(360deg\)|turn\)/.test(css.text), 'nothing loops or spins');
assert.ok(!/infinite|rotate\(360deg\)|turn\)/.test(css.text), 'nothing loops or spins');
- assert.match(css.text, /@media \(prefers-reduced-motion: reduce\)/);
- assert.match(css.text, /html\[data-ov-motion="reduced"\]/, 'OpenVibe\'s calm setting stops them too');
+ assert.match(css.text, /html\[data-ov-motion=off\]/, 'only motion off stops an icon; Calm plays it');
+ assert.doesNotMatch(css.text, /prefers-reduced-motion/);
+ assert.doesNotMatch(css.text, /data-ov-motion="?reduced/, 'Calm plays them');
const wc = (await t.get('/ov-icon.js')).text;
const wc = (await t.get('/ov-icon.js')).text;
for (const k of ['ovm-draw', 'ovm-ring', 'ovm-beat', 'ovm-shake']) assert.ok(wc.includes(`@keyframes ${k}{`) && css.text.includes(`@keyframes ${k} {`), `${k} in both`);
for (const k of ['ovm-draw', 'ovm-ring', 'ovm-beat', 'ovm-shake']) assert.ok(wc.includes(`@keyframes ${k}{`) && css.text.includes(`@keyframes ${k} {`), `${k} in both`);
assert.ok(!/infinite/.test(wc), 'the component loops nothing either');
assert.ok(!/infinite/.test(wc), 'the component loops nothing either');