openvibers/openvibe.shared Commits

openvibe-shared 4.230.0: OpenVibe's own controls on every site (#512)

OP OpenVibers committed 10 h ago · 606b5b5b61 · 8 files, +153 −10

- **OpenVibe's own controls on every site** (the owner, 11-12: "improve the selection boxes to use a more customized look integrated into openvibe.design and themed better with animations and effects for all inputs and buttons"). `ov-forms.css` now draws every checkbox and radio: a rounded box whose check pops in, and a ring whose dot grows. The mark is the box's own `::before` (a clip-path check, scaled), so no picture is fetched. Both lift their border on hover, squeeze on a press and glow with focus; the mixed state is a bar; disabled is faded. A switch (`role="switch"`) glows when on, its knob squeezes when pressed, and it has a focus ring. Buttons answer a press, and text fields, text areas and selects glow in the accent with focus. Everything is `:where()`, so a site's own rule wins; `data-ov-native` keeps the browser's control; motion follows the person (Calm slower, Off moves nothing).
- The bar loads `ov-forms.css` on any page with a text field, a text area or a button. The select kit's script still waits for a select, a box or a file input.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex ad0f62e..e3ca7e7 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,10 @@
# Changelog
++## 4.230.0 — 2026-11-12
++
++- **OpenVibe's own controls on every site** (the owner, 11-12: "improve the selection boxes to use a more customized look integrated into openvibe.design and themed better with animations and effects for all inputs and buttons"). `ov-forms.css` now draws every checkbox and radio: a rounded box whose check pops in, and a ring whose dot grows. The mark is the box's own `::before` (a clip-path check, scaled), so no picture is fetched. Both lift their border on hover, squeeze on a press and glow with focus; the mixed state is a bar; disabled is faded. A switch (`role="switch"`) glows when on, its knob squeezes when pressed, and it has a focus ring. Buttons answer a press, and text fields, text areas and selects glow in the accent with focus. Everything is `:where()`, so a site's own rule wins; `data-ov-native` keeps the browser's control; motion follows the person (Calm slower, Off moves nothing).
++- The bar loads `ov-forms.css` on any page with a text field, a text area or a button. The select kit's script still waits for a select, a box or a file input.
++
## 4.229.0 — 2026-11-12
- **Back brings you back to the menu** (the owner, 11-12: "fix the history back button so it properly brings you back to this GUI rather than the page behind it"). Leaving through a link in the menu marks where it was on its history entry (its place, its door, its scroll); Back to that page opens the menu again there, whether the page comes back from the browser's cache, loads again, or a site swaps it in place (Boost). It reuses that entry, so Forward still works, and closing it then leaves the page as it was. Only for a Back or Forward within half an hour.
ddiff --git a/README.md b/README.md
iindex 455195d..0d3dca9 100644
---- a/README.md
++++ b/README.md
@@@ -5,7 +5,7 @@
**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt
**Install and test:** `npm ci && npm test`
--**Status:** alpha, v4.229.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.230.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex e82da53..02491f6 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.229.0",
++ "version": "4.230.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
ddiff --git a/navbar.js b/navbar.js
iindex 196c1f0..2c90862 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -610,7 +610,7 @@
* serves its own pinned copy at /shared/ gets its own), else the network's copy.
*/
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
-- const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': 'c96584376da1', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': 'ef289c4550b1', 'dock.js': '9f15df28050c', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '0cef1f895965', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '3a88a5ba2565', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': 'b11405559831', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': '3fa6bb23b429', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': 'ae24086bd2be', 'soft.js': '98ab9b8080b1', 'ov-run.js': '91be6059adcf', 'ov-composer.js': 'ac7107f5d733' };
++ const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': 'c96584376da1', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '07e8f59d9e6d', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': 'ef289c4550b1', 'dock.js': '9f15df28050c', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '0cef1f895965', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '3a88a5ba2565', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': 'b11405559831', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': '3fa6bb23b429', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': 'ae24086bd2be', 'soft.js': '98ab9b8080b1', 'ov-run.js': '91be6059adcf', 'ov-composer.js': 'ac7107f5d733' };
const siblingV = (name) => `${sibling(name)}${SIBLING_REV[name] ? `?v=${SIBLING_REV[name]}` : ''}`;
// ov-tip.js loads at the first hover over a tooltip's owner.
@@@ -684,9 +684,17 @@
}
return false;
}
++ // 4.230.0: the look of every field and button is in ov-forms.css, so a page with any text field, text area or button
++ // gets the stylesheet once it settles; the select kit's script still waits for a select, a box or a file input.
++ function formsCss() {
++ if (document.documentElement.getAttribute('data-ov-forms') === 'off' || document.querySelector('link[href*="ov-forms.css"]')) return;
++ if (!document.querySelector(FORM_CONTROLS + ',input,textarea,button')) return;
++ try { const ln = document.createElement('link'); ln.rel = 'stylesheet'; ln.href = siblingV('ov-forms.css'); document.head.appendChild(ln); } catch { /* */ }
++ }
function loadForms() {
if (root.OVForms || document.getElementById('ov-forms-loader')) return true;
if (document.documentElement.getAttribute('data-ov-forms') === 'off') return true;
++ formsCss();
if (!formsWanted()) return false;
try {
if (!document.querySelector('link[href*="ov-forms.css"]')) {
ddiff --git a/ov-forms.css b/ov-forms.css
iindex ad291d8..9433996 100644
---- a/ov-forms.css
++++ b/ov-forms.css
@@@ -1,9 +1,11 @@
/* openvibe-shared/ov-forms.css — OpenVibe's own form controls, on every site (docs/forms.md). ov-forms.js draws the
* select (a button, its menu on a computer, its sheet on a phone); the rest is this file alone:
*
-- * every page checkboxes, radios, sliders and progress take the site's accent; a checkbox with role="switch" is a
-- * switch; a file input's button is a button; nothing here moves a site's layout (all of it is
-- * :where(), so a site's own rule always wins)
++ * every page checkboxes and radios are drawn (4.230.0: a box with a check that pops in, a ring with a dot; hover, a
++ * press, a focus glow), sliders and progress take the site's accent; a checkbox with role="switch" is
++ * a switch that glows when on and squeezes when pressed; buttons answer a press, text fields glow
++ * with focus; a file input's button is a button. All of it is :where(), so a site's own rule always
++ * wins; motion follows the person's setting (Calm slower, Off none)
* opted in .ov-field (a label, a control, a hint, an error) and .ov-input: the full field look;
* .ov-choices / .ov-choice: a choice drawn as a card (radio or checkbox inside)
* opted out data-ov-native on a control or any ancestor, or <html data-ov-forms="off">
@@@ -149,10 +151,61 @@ html.ov-sel-locked, html.ov-sel-locked body { overflow: hidden; overscroll-behav
.ov-sel-group:first-child { padding-top: 4px; }
.ov-sel-none { padding: 18px 12px; color: var(--ovf-muted); text-align: center; }
--/* ── Every page: the browser's own boxes, in the site's colours (nothing here changes a size) ─────────────────── */
++/* ── Every page: OpenVibe's own controls, in the site's colours ──────────────────────────────────────────────── */
:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"], input[type="radio"], input[type="range"], progress, meter):where(:not([data-ov-native], [data-ov-native] *)) {
accent-color: var(--accent, #3b82f6);
}
++/* 4.230.0 (the owner, 11-12: "improve the selection boxes … a more customized look … themed better with animations and
++ effects for all inputs and buttons"): a checkbox is a rounded box whose check pops in, a radio a ring whose dot
++ grows; both lift their border on hover, squeeze on a press and glow with focus. The mark is the box's own ::before
++ (clip-path check, scaled), so no picture is fetched and no page's image policy is involved. */
++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *)) {
++ -webkit-appearance: none; appearance: none; box-sizing: border-box; flex: none; vertical-align: middle;
++ display: inline-grid; place-content: center; width: 18px; height: 18px; cursor: pointer;
++ border: 1.5px solid color-mix(in srgb, var(--text-primary, CanvasText) 38%, transparent); border-radius: 6px;
++ background: var(--bg-input, color-mix(in srgb, var(--text-primary, CanvasText) 5%, transparent)); color: var(--accent, #3b82f6);
++ transition: background-color .16s ease, border-color .16s ease, box-shadow .2s ease, transform .12s ease;
++}
++:where(html:not([data-ov-forms="off"])) :where(input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *)) { border-radius: 50%; }
++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *))::before {
++ content: ""; width: 10px; height: 10px; transform: scale(0); transform-origin: 50% 60%;
++ box-shadow: inset 1em 1em var(--on-accent, #fff);
++ clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
++ transition: transform .18s cubic-bezier(.2, 1.5, .4, 1);
++}
++:where(html:not([data-ov-forms="off"])) :where(input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *))::before {
++ width: 8px; height: 8px; border-radius: 50%; clip-path: none; box-shadow: inset 1em 1em var(--accent, #3b82f6); transform-origin: 50% 50%;
++}
++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]):checked, input[type="checkbox"]:not([role="switch"]):indeterminate):where(:not([data-ov-native], [data-ov-native] *)) {
++ background: var(--accent, #3b82f6); border-color: var(--accent, #3b82f6);
++}
++:where(html:not([data-ov-forms="off"])) :where(input[type="radio"]:checked):where(:not([data-ov-native], [data-ov-native] *)) {
++ border-color: var(--accent, #3b82f6); border-width: 2px;
++}
++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]):checked, input[type="radio"]:checked):where(:not([data-ov-native], [data-ov-native] *))::before { transform: scale(1); }
++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]):indeterminate):where(:not([data-ov-native], [data-ov-native] *))::before {
++ transform: scale(1); height: 2.5px; border-radius: 2px; clip-path: none;
++}
++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)):hover {
++ border-color: color-mix(in srgb, var(--accent, #3b82f6) 70%, transparent);
++ box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent);
++}
++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"], input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)):active { transform: scale(.88); }
++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *)):focus-visible {
++ box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #3b82f6) 28%, transparent);
++}
++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *)):disabled { opacity: .5; cursor: not-allowed; }
++/* Buttons answer a press; text fields glow with focus (a site's own button or field rule wins over these). */
++:where(html:not([data-ov-forms="off"])) :where(button, input[type="button"], input[type="submit"], input[type="reset"], a.btn, .btn, [role="button"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)) {
++ transition: transform .12s ease, box-shadow .18s ease, background-color .16s ease, border-color .16s ease, color .16s ease;
++}
++:where(html:not([data-ov-forms="off"])) :where(button, input[type="button"], input[type="submit"], input[type="reset"], a.btn, .btn, [role="button"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)):active { transform: scale(.97); }
++:where(html:not([data-ov-forms="off"])) :where(input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"], [type="color"], [type="hidden"], [type="button"], [type="submit"], [type="reset"]), textarea, select):where(:not([data-ov-native], [data-ov-native] *)) {
++ transition: border-color .15s ease, box-shadow .2s ease, background-color .15s ease;
++}
++:where(html:not([data-ov-forms="off"])) :where(input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"], [type="color"], [type="hidden"], [type="button"], [type="submit"], [type="reset"]), textarea, select):where(:not([data-ov-native], [data-ov-native] *)):focus-visible {
++ outline: 2px solid var(--accent, #3b82f6); outline-offset: 0; box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent);
++}
:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"], input[type="radio"], input[type="range"]):where(:not([data-ov-native], [data-ov-native] *)):focus-visible {
outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px;
}
@@@ -167,7 +220,26 @@ html.ov-sel-locked, html.ov-sel-locked body { overflow: hidden; overscroll-behav
}
:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]:checked):where(:not([data-ov-native], [data-ov-native] *)) {
background-color: var(--accent, #3b82f6); background-position: calc(100% - 2px) 50%;
++ box-shadow: inset 0 0 0 1px color-mix(in srgb, #000 12%, transparent), 0 0 14px color-mix(in srgb, var(--accent, #3b82f6) 38%, transparent);
++}
++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)):hover {
++ background-color: color-mix(in srgb, CanvasText 34%, transparent);
++}
++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]:checked):where(:not([data-ov-native], [data-ov-native] *, :disabled)):hover {
++ background-color: color-mix(in srgb, var(--accent, #3b82f6) 88%, #fff);
++}
++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)):active {
++ transform: none; background-image: radial-gradient(ellipse 13px 8.5px, #fff 0 92%, transparent 100%); background-size: 26px 20px;
++}
++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]:checked):where(:not([data-ov-native], [data-ov-native] *, :disabled)):active { background-position: calc(100% - 2px) 50%; }
++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]):where(:not([data-ov-native], [data-ov-native] *)):focus-visible {
++ outline: none; box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #3b82f6) 30%, transparent);
}
++/* Motion follows the person: Calm takes its time, Off moves nothing (the state still changes). */
++html[data-ov-motion="calm"] :where(input[type="checkbox"], input[type="radio"], button, .btn)::before { transition-duration: .32s; }
++html[data-ov-motion="off"] :where(input[type="checkbox"], input[type="radio"], button, input[type="button"], input[type="submit"], .btn, [role="button"], input, textarea, select),
++html[data-ov-motion="off"] :where(input[type="checkbox"], input[type="radio"])::before { transition: none !important; animation: none !important; }
++html[data-ov-motion="off"] :where(input[type="checkbox"], input[type="radio"], button, .btn, [role="button"]):active { transform: none !important; }
:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]:disabled) { opacity: .5; cursor: not-allowed; }
:where(html:not([data-ov-forms="off"])) :where(input[type="file"]:not([data-ov-native]))::file-selector-button {
font: inherit; font-weight: 600; margin-inline-end: .75em; padding: .38em .85em; cursor: pointer;
ddiff --git a/package-lock.json b/package-lock.json
iindex 0fac9f5..3000d92 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-shared",
-- "version": "4.229.0",
++ "version": "4.230.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-shared",
-- "version": "4.229.0",
++ "version": "4.230.0",
"license": "MIT",
"dependencies": {
"clean-css": "^5.3.3",
ddiff --git a/package.json b/package.json
iindex c1d4c89..2be73a8 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.229.0",
++ "version": "4.230.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/test/ov-forms-controls-chrome.test.js b/test/ov-forms-controls-chrome.test.js
nnew file mode 100644
iindex 0000000..48ba6c0
---- /dev/null
++++ b/test/ov-forms-controls-chrome.test.js
@@@ -0,0 +1,58 @@
++'use strict';
++// OpenVibe's own controls (4.230.0; the owner, 11-12: "improve the selection boxes … a more customized look … themed
++// better with animations and effects for all inputs and buttons"), in a real headless Chrome with ov-forms.css alone:
++// a checkbox and a radio are drawn (no native look) and their mark scales in when checked; a switch stays a switch; a
++// site's own rule wins (:where), data-ov-native keeps the browser's box, motion Off removes the transitions, and the
++// mark needs no picture (no url() in the stylesheet, so no page's image policy is involved). Skipped without Chrome.
++const assert = require('node:assert/strict');
++const fs = require('node:fs');
++const os = require('node:os');
++const path = require('node:path');
++const http = require('node:http');
++const h = require('../browser-harness');
++if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) { console.log('ov-forms-controls-chrome: skipped (no Chrome; set CHROME_BIN)'); process.exit(0); }
++const root = path.join(__dirname, '..');
++const css = fs.readFileSync(path.join(root, 'ov-forms.css'), 'utf8');
++assert.ok(!/url\(/.test(css.replace(/\/\*[\s\S]*?\*\//g, '')), 'no url() in ov-forms.css: the marks are drawn, not fetched');
++
++const page = (motion) => `<!doctype html><html lang="en"${motion ? ` data-ov-motion="${motion}"` : ''}><head><meta charset="utf-8"><link rel="stylesheet" href="/ov-forms.css">
++<style>.mine { width: 30px; height: 30px; }</style></head><body>
++<input type="checkbox" id="on" checked><input type="checkbox" id="off"><input type="radio" id="r" checked><input type="checkbox" role="switch" id="sw" checked>
++<input type="checkbox" id="mine" class="mine"><span data-ov-native><input type="checkbox" id="native"></span><button id="b">B</button><input id="t"></body></html>`;
++const server = http.createServer((req, res) => {
++ const u = new URL(req.url, 'http://x');
++ if (u.pathname === '/ov-forms.css') { res.writeHead(200, { 'content-type': 'text/css' }); return res.end(css); }
++ res.writeHead(200, { 'content-type': 'text/html' }); res.end(page(u.searchParams.get('motion')));
++});
++(async () => {
++ await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
++ const base = `http://127.0.0.1:${server.address().port}`;
++ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-controls-'));
++ const browser = await h.launch({ tmpDir });
++ try {
++ const p = await h.openPage(browser, await browser.newContext(), { width: 390, height: 400 });
++ await p.goto(`${base}/`, { maxMs: 5000 });
++ const cs = (id, prop, pseudo = null) => p.evaluate(`getComputedStyle(document.getElementById(${JSON.stringify(id)})${pseudo ? `, ${JSON.stringify(pseudo)}` : ''}).${prop}`);
++ assert.equal(await cs('on', 'appearance'), 'none', 'a checkbox is drawn');
++ assert.equal(await cs('on', 'width'), '18px');
++ assert.equal(await cs('on', 'transform', '::before'), 'matrix(1, 0, 0, 1, 0, 0)', 'its check is shown when checked');
++ assert.equal(await cs('off', 'transform', '::before'), 'matrix(0, 0, 0, 0, 0, 0)', 'and scaled away when not');
++ assert.ok(/transform/.test(await cs('off', 'transitionProperty', '::before')), 'the check scales in');
++ assert.equal(await cs('r', 'appearance'), 'none', 'a radio is drawn');
++ assert.equal(await cs('r', 'borderTopLeftRadius'), '50%');
++ assert.equal(await cs('sw', 'width'), '42px', 'a switch stays a switch');
++ assert.equal(await cs('sw', 'transform', '::before'), 'none', 'a switch has no check mark');
++ assert.equal(await cs('mine', 'width'), '30px', 'a site\'s own rule wins');
++ assert.notEqual(await cs('native', 'appearance'), 'none', 'data-ov-native keeps the browser\'s box');
++ assert.ok(/transform/.test(await cs('b', 'transitionProperty')), 'a button answers a press');
++ await p.goto(`${base}/?motion=off`, { maxMs: 5000 });
++ assert.equal(await cs('off', 'transitionDuration', '::before'), '0s', 'motion Off: the check does not move');
++ assert.equal(await cs('b', 'transitionDuration'), '0s', 'motion Off: buttons do not move');
++ await p.close();
++ console.log('ov-forms-controls-chrome: drawn checkbox and radio, the check scales in, switch kept, site rules win, data-ov-native, no url(), motion Off');
++ } finally {
++ await browser.close();
++ server.close();
++ fs.rmSync(tmpDir, { recursive: true, force: true });
++ }
++})().catch((e) => { console.error(e); process.exit(1); });