openvibers/openvibe.shared Commits

Shape and elevation are tokens, and a look has a shape (4.81.0) (#234)

OP OpenVibers committed 12 days ago · 399f3d6248 · 7 files, +122 −32

--ov-radius-1..3 and --ov-elev-1..3 in kit.css, defaulting to the kit's own corners and shadows; every kit corner
and shadow reads them. Paper and the quiet looks are square and flat, Neon Tokyo and Vapor round and glowing in
their accent. room.css carries the same tokens and the personalities at zero specificity.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 2ea0bf1..d1be49f 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,20 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
breaking change to any exported module, browser global or served file name is a new major.
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
++## 4.81.0 — 2026-11-01
++
++- **Shape and elevation are tokens, and a look has a shape** (plan L45 step 2: "Paper is quiet and square, Neon glows
++ and bounces").
++ - `--ov-radius-1..3` (small things, cards, sheets) and `--ov-elev-1..3` (on its surface, a bar over scrolled content,
++ a menu above the page) are in `kit.css`. Their defaults are the kit's own corners and shadows, so nothing moves.
++ - Every kit corner and shadow now reads them: `--ovk-radius` is `--ov-radius-2`, and a 12 px corner is
++ `radius-1 + 2px`. Only pills and circles stay literal.
++ - The quiet looks are square, with hairlines where shadows were: Paper, Solarized Light, Nord, Nord Light, Matrix,
++ Hacker and High Contrast.
++ - Neon Tokyo and Vapor are round, and their raised things glow in the accent.
++ - A site's own CSS can use the same tokens to follow the look: `border-radius: var(--ov-radius-2, 14px)`.
++ - `room.css` carries the same tokens, and the personalities at zero specificity.
++
## 4.80.0 — 2026-11-01
- **33 more glyphs have a solid drawing for small sizes** (plan L50; the owner, 2026-11-01: "really tiny icons… a
ddiff --git a/README.md b/README.md
iindex 90dbd92..6a03f53 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
--**Status:** alpha, v4.80.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.81.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 2695011..e9426ef 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.80.0",
++ "version": "4.81.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/kit.css b/kit.css
iindex 9a6d54f..99c63dc 100644
---- a/kit.css
++++ b/kit.css
@@@ -25,14 +25,38 @@
--ovk-warn: var(--warning, #f59e0b);
--ovk-bad: var(--danger, var(--error, #ef4444));
--ovk-live: var(--live-red, #ef4444);
-- --ovk-radius: 14px;
-- --ovk-radius-sm: 10px;
++ /* Shape and elevation (4.81.0, plan L45 step 2): the kit's corners and shadows as tokens a look can set, so a theme
++ * has a shape as well as colours. The defaults are the kit's own corners and shadows; the personalities follow. */
++ --ov-radius-1: 10px; /* chips, inputs, rows, small buttons */
++ --ov-radius-2: 14px; /* cards, sections, panels */
++ --ov-radius-3: 20px; /* sheets, dialogs, big surfaces */
++ --ov-elev-1: 0 1px 3px rgba(0, 0, 0, .35); /* a knob, a chip that sits on its surface */
++ --ov-elev-2: 0 10px 24px -18px rgba(0, 0, 0, .7); /* a bar that has scrolled content under it */
++ --ov-elev-3: 0 14px 32px rgba(0, 0, 0, .4); /* a menu or popover above the page */
++ --ovk-radius: var(--ov-radius-2);
++ --ovk-radius-sm: var(--ov-radius-1);
--ovk-tap: 44px;
--ovk-top: var(--ovnav-real-h, 52px); /* the navbar's height: sticky things stop under it */
--ovk-bottom: var(--ovnav-appbar-h, 0px); /* the app bar's height on phones (navbar.js sets it) */
--ovk-font: var(--ov-font, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
}
++/* Shape personalities (plan L45: "Paper is quiet and square, Neon glows and bounces"). A quiet look has square corners
++ * and lies flat, with a hairline where a shadow would be; a loud one is round and its raised things glow in its accent.
++ * The ids are Network's theme catalog (as ov-fx.js's motion feels); any other look keeps the defaults above. */
++:is([data-theme=paper], [data-theme=solarized-light], [data-theme=nord], [data-theme=nord-light], [data-theme=matrix], [data-theme=hacker], [data-theme=high-contrast]) {
++ --ov-radius-1: 4px; --ov-radius-2: 6px; --ov-radius-3: 10px;
++ --ov-elev-1: 0 0 0 1px var(--ovk-line);
++ --ov-elev-2: 0 1px 0 var(--ovk-line);
++ --ov-elev-3: 0 0 0 1px var(--ovk-line), 0 8px 18px -12px rgba(0, 0, 0, .35);
++}
++:is([data-theme=neon-tokyo], [data-theme=vapor]) {
++ --ov-radius-1: 14px; --ov-radius-2: 20px; --ov-radius-3: 28px;
++ --ov-elev-1: 0 0 8px color-mix(in srgb, var(--ovk-accent) 40%, transparent);
++ --ov-elev-2: 0 10px 28px -14px color-mix(in srgb, var(--ovk-accent) 55%, transparent);
++ --ov-elev-3: 0 0 0 1px color-mix(in srgb, var(--ovk-accent) 40%, transparent), 0 18px 44px -14px color-mix(in srgb, var(--ovk-accent) 55%, transparent);
++}
++
/* Heading and paragraph rules double their class (.ovk-section.ovk-section > h2): a site's own ".site h2" has the same
* specificity as ".ovk-section > h2" and loads later, so it used to restyle the kit's headings (Report, 2026-10-29). */
@@@ -45,7 +69,7 @@
.ovk-page--wide { max-width: 1280px; }
/* ── Buttons ── */
--.ovk-btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; box-sizing: border-box; min-height: var(--ovk-tap); padding: 10px 18px; border-radius: 12px; border: 1px solid transparent; font: inherit; font-weight: 700; font-size: .95rem; line-height: 1.2; text-decoration: none; cursor: pointer; transition: filter .12s, border-color .12s, background .12s; }
++.ovk-btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; box-sizing: border-box; min-height: var(--ovk-tap); padding: 10px 18px; border-radius: calc(var(--ov-radius-1) + 2px); border: 1px solid transparent; font: inherit; font-weight: 700; font-size: .95rem; line-height: 1.2; text-decoration: none; cursor: pointer; transition: filter .12s, border-color .12s, background .12s; }
.ovk-btn--primary { background: var(--ovk-fill); color: var(--ovk-on-fill); }
.ovk-btn--primary:hover { filter: brightness(1.08); }
.ovk-btn--quiet { background: var(--ovk-panel); color: var(--ovk-text); border-color: var(--ovk-line); }
@@@ -56,7 +80,7 @@
.ovk-btn--small { min-height: 36px; padding: 6px 13px; border-radius: 999px; font-size: .88rem; }
.ovk-btn:focus-visible, .ovk-icon-btn:focus-visible, .ovk-go:focus-visible { outline: 2px solid var(--ovk-accent); outline-offset: 2px; }
.ovk-btn[disabled] { opacity: .55; cursor: default; filter: none; }
--.ovk-icon-btn { display: inline-grid; place-items: center; box-sizing: border-box; width: 40px; height: 40px; flex: none; padding: 0; border-radius: 12px; border: 1px solid transparent; background: transparent; color: var(--ovk-muted); font: inherit; cursor: pointer; text-decoration: none; list-style: none; }
++.ovk-icon-btn { display: inline-grid; place-items: center; box-sizing: border-box; width: 40px; height: 40px; flex: none; padding: 0; border-radius: calc(var(--ov-radius-1) + 2px); border: 1px solid transparent; background: transparent; color: var(--ovk-muted); font: inherit; cursor: pointer; text-decoration: none; list-style: none; }
.ovk-icon-btn::-webkit-details-marker { display: none; }
.ovk-icon-btn:hover { color: var(--ovk-text); background: var(--ovk-panel-2); border-color: var(--ovk-line); }
.ovk-icon-btn[hidden] { display: none; }
@@@ -74,7 +98,7 @@
/* ── The head: back, title and one line, icon actions; its tabs under it; sticky under the navbar ── */
.ovk-head { position: sticky; top: var(--ovk-top); z-index: 40; margin: 0 -16px 12px; padding: 6px 16px 0; background: color-mix(in srgb, var(--ovk-bg) 88%, transparent); -webkit-backdrop-filter: saturate(1.4) blur(14px); backdrop-filter: saturate(1.4) blur(14px); border-bottom: 1px solid transparent; transition: border-color .2s, box-shadow .2s; }
--.ovk-scrolled .ovk-head { border-bottom-color: var(--ovk-line); box-shadow: 0 10px 24px -18px rgba(0, 0, 0, .7); }
++.ovk-scrolled .ovk-head { border-bottom-color: var(--ovk-line); box-shadow: var(--ov-elev-2); }
.ovk-head--static { position: static; background: transparent; -webkit-backdrop-filter: none; backdrop-filter: none; }
.ovk-head-row { display: flex; align-items: center; gap: 6px; min-height: 48px; padding-bottom: 6px; }
.ovk-head-row > .ovk-back { margin-left: -8px; }
@@@ -115,7 +139,7 @@
.ovk-row--unread .ovk-row-sub { color: var(--ovk-text); }
.ovk-row-trail { display: flex; align-items: center; gap: 2px; flex: none; }
.ovk-row-trail form { display: flex; align-items: center; gap: 2px; margin: 0; }
--.ovk-row-trail select { max-width: 9.5em; padding: 5px 6px; border-radius: 8px; border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font: inherit; font-size: .86rem; }
++.ovk-row-trail select { max-width: 9.5em; padding: 5px 6px; border-radius: max(0px, calc(var(--ov-radius-1) - 2px)); border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font: inherit; font-size: .86rem; }
.ovk-mark { --ovk-mark: 40px; flex: none; display: inline-grid; place-items: center; width: var(--ovk-mark); height: var(--ovk-mark); box-sizing: border-box; border-radius: 50%; object-fit: cover; background: hsl(var(--ovk-hue, 215) 45% 38%); color: #fff; font-size: calc(var(--ovk-mark) * .34); font-weight: 800; letter-spacing: .02em; line-height: 1; }
.ovk-mark--square { border-radius: 28%; }
.ovk-empty-line { margin: 0 0 14px; padding: 14px; border: 1px dashed var(--ovk-line); border-radius: var(--ovk-radius); color: var(--ovk-muted); }
@@@ -140,9 +164,9 @@
.ovk-fold--danger > summary { color: var(--ovk-bad); }
.ovk-more { position: relative; display: inline-block; }
.ovk-more > summary { list-style: none; }
--.ovk-pop { position: absolute; right: 0; top: calc(100% + 4px); z-index: 60; min-width: 210px; padding: 6px; box-sizing: border-box; border-radius: 12px; border: 1px solid var(--ovk-line); background: var(--ovk-panel); box-shadow: 0 14px 32px rgba(0, 0, 0, .4); }
++.ovk-pop { position: absolute; right: 0; top: calc(100% + 4px); z-index: 60; min-width: 210px; padding: 6px; box-sizing: border-box; border-radius: calc(var(--ov-radius-1) + 2px); border: 1px solid var(--ovk-line); background: var(--ovk-panel); box-shadow: var(--ov-elev-3); }
.ovk-pop form { margin: 0; }
--.ovk-pop-item { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 40px; box-sizing: border-box; padding: 8px 10px; border: 0; border-radius: 8px; background: transparent; color: var(--ovk-text); font: inherit; font-weight: 600; text-align: left; text-decoration: none; cursor: pointer; }
++.ovk-pop-item { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 40px; box-sizing: border-box; padding: 8px 10px; border: 0; border-radius: max(0px, calc(var(--ov-radius-1) - 2px)); background: transparent; color: var(--ovk-text); font: inherit; font-weight: 600; text-align: left; text-decoration: none; cursor: pointer; }
.ovk-pop-item:hover, .ovk-pop-item:focus-visible { background: var(--ovk-panel-2); outline: none; }
.ovk-pop-item--danger { color: var(--ovk-bad); }
@@@ -152,17 +176,17 @@
div.ovk-set-row { cursor: default; }
.ovk-set-text { flex: 1; min-width: 0; display: grid; gap: 2px; }
.ovk-set-text small { color: var(--ovk-muted); font-size: .8rem; }
--.ovk-set-row select { flex: none; max-width: 52%; margin: 0; padding: 7px 8px; border-radius: 10px; border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font: inherit; font-size: .9rem; }
++.ovk-set-row select { flex: none; max-width: 52%; margin: 0; padding: 7px 8px; border-radius: var(--ov-radius-1); border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font: inherit; font-size: .9rem; }
.ovk-set-range input[type=range] { flex: 0 1 45%; min-width: 120px; margin: 0; accent-color: var(--ovk-fill); }
.ovk-set-chips { flex-wrap: wrap; }
.ovk-switch { appearance: none; -webkit-appearance: none; flex: none; position: relative; width: 44px; height: 26px; margin: 0; padding: 0; border-radius: 999px; border: 0; background: var(--ovk-line); cursor: pointer; transition: background .15s; }
--.ovk-switch::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, .35); transition: transform .15s; }
++.ovk-switch::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: #fff; box-shadow: var(--ov-elev-1); transition: transform .15s; }
.ovk-switch:checked { background: var(--ovk-fill); }
.ovk-switch:checked::after { transform: translateX(18px); }
.ovk-switch:focus-visible { outline: 2px solid var(--ovk-accent); outline-offset: 2px; }
--.ovk-seg { display: inline-flex; flex: none; flex-wrap: wrap; max-width: 100%; gap: 2px; padding: 3px; border-radius: 10px; background: var(--ovk-panel-2); }
++.ovk-seg { display: inline-flex; flex: none; flex-wrap: wrap; max-width: 100%; gap: 2px; padding: 3px; border-radius: var(--ov-radius-1); background: var(--ovk-panel-2); }
.ovk-seg input, .ovk-chip input { position: absolute; opacity: 0; pointer-events: none; }
--.ovk-seg span { display: block; padding: 6px 12px; border-radius: 8px; color: var(--ovk-muted); font-size: .86rem; font-weight: 650; cursor: pointer; }
++.ovk-seg span { display: block; padding: 6px 12px; border-radius: max(0px, calc(var(--ov-radius-1) - 2px)); color: var(--ovk-muted); font-size: .86rem; font-weight: 650; cursor: pointer; }
.ovk-seg input:checked + span { background: var(--ovk-fill); color: var(--ovk-on-fill); }
.ovk-seg input:focus-visible + span, .ovk-chip input:focus-visible + span { outline: 2px solid var(--ovk-accent); outline-offset: 1px; }
.ovk-chips { display: flex; flex-wrap: wrap; gap: 6px; }
@@@ -173,7 +197,7 @@ div.ovk-set-row { cursor: default; }
.ovk-form { display: grid; gap: 12px; margin: 0; }
.ovk-form label { display: grid; gap: 5px; color: var(--ovk-muted); font-size: .9rem; }
.ovk-form label.ovk-check { display: flex; align-items: flex-start; gap: 10px; color: var(--ovk-text); font-size: .95rem; }
--.ovk-form input:not([type=checkbox]):not([type=radio]):not([type=range]), .ovk-form select, .ovk-form textarea { width: 100%; box-sizing: border-box; min-height: var(--ovk-tap); padding: 10px 12px; border-radius: 10px; border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font: inherit; font-size: 1rem; }
++.ovk-form input:not([type=checkbox]):not([type=radio]):not([type=range]), .ovk-form select, .ovk-form textarea { width: 100%; box-sizing: border-box; min-height: var(--ovk-tap); padding: 10px 12px; border-radius: var(--ov-radius-1); border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font: inherit; font-size: 1rem; }
.ovk-form textarea { min-height: 96px; resize: vertical; }
.ovk-form input:focus-visible, .ovk-form select:focus-visible, .ovk-form textarea:focus-visible { outline: 2px solid var(--ovk-accent); outline-offset: 0; border-color: transparent; }
.ovk-form.ovk-form > p { margin: 0; }
@@@ -211,7 +235,7 @@ div.ovk-set-row { cursor: default; }
/* ── Numbers, empty states, notices, code, sheets ── */
.ovk-stats { position: relative; display: flex; gap: 8px; margin: 12px -16px; padding: 0 16px; overflow-x: auto; scrollbar-width: none; }
.ovk-stats::-webkit-scrollbar { display: none; }
--.ovk-stats > div { display: flex; flex-direction: column-reverse; flex: 0 0 auto; min-width: 104px; box-sizing: border-box; padding: 8px 14px; border: 1px solid var(--ovk-line); border-radius: 12px; background: var(--ovk-panel); }
++.ovk-stats > div { display: flex; flex-direction: column-reverse; flex: 0 0 auto; min-width: 104px; box-sizing: border-box; padding: 8px 14px; border: 1px solid var(--ovk-line); border-radius: calc(var(--ov-radius-1) + 2px); background: var(--ovk-panel); }
.ovk-stats dt { margin: 0; color: var(--ovk-muted); font-size: .76rem; line-height: 1.25; }
.ovk-stats dt a { color: inherit; text-decoration: none; }
/* A number that is a link: the whole tile is the target (the label's link stretched over it), never an underline. */
@@@ -225,7 +249,7 @@ div.ovk-set-row { cursor: default; }
.ovk-empty > .ovk-i { width: 28px; height: 28px; }
.ovk-empty.ovk-empty p { margin: 0; max-width: 46ch; }
.ovk-empty .ovk-empty-title { color: var(--ovk-text); font-weight: 750; }
--.ovk-notice { display: flex; align-items: flex-start; gap: 10px; margin: 0 0 12px; padding: 10px 14px; border-radius: 12px; border: 1px solid var(--ovk-line); background: var(--ovk-panel); color: var(--ovk-text); font-size: .92rem; }
++.ovk-notice { display: flex; align-items: flex-start; gap: 10px; margin: 0 0 12px; padding: 10px 14px; border-radius: calc(var(--ov-radius-1) + 2px); border: 1px solid var(--ovk-line); background: var(--ovk-panel); color: var(--ovk-text); font-size: .92rem; }
.ovk-notice > .ovk-i { margin-top: 2px; }
.ovk-notice--ok { border-color: color-mix(in srgb, var(--ovk-ok) 45%, transparent); }
.ovk-notice--ok > .ovk-i { color: var(--ovk-ok); }
@@@ -235,8 +259,8 @@ div.ovk-set-row { cursor: default; }
.ovk-notice--info > .ovk-i { color: var(--ovk-accent); }
.ovk-snip { position: relative; }
.ovk-snip > .ovk-icon-btn { position: absolute; top: 6px; right: 6px; z-index: 1; width: 34px; height: 34px; background: var(--ovk-panel); border-color: var(--ovk-line); }
--.ovk-snip > pre { max-height: 50vh; margin: 0 0 10px; padding: 12px 48px 12px 12px; overflow: auto; border-radius: 10px; border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font-size: .84rem; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }
--.ovk-sheet { width: min(560px, 100vw); max-width: 100vw; max-height: 85dvh; margin: auto; padding: 0; box-sizing: border-box; border: 1px solid var(--ovk-line); border-radius: 18px; background: var(--ovk-panel); color: var(--ovk-text); overflow: hidden; }
++.ovk-snip > pre { max-height: 50vh; margin: 0 0 10px; padding: 12px 48px 12px 12px; overflow: auto; border-radius: var(--ov-radius-1); border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font-size: .84rem; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }
++.ovk-sheet { width: min(560px, 100vw); max-width: 100vw; max-height: 85dvh; margin: auto; padding: 0; box-sizing: border-box; border: 1px solid var(--ovk-line); border-radius: calc(var(--ov-radius-3) - 2px); background: var(--ovk-panel); color: var(--ovk-text); overflow: hidden; }
.ovk-sheet::backdrop { background: rgba(0, 0, 0, .55); }
.ovk-sheet[open] { display: flex; flex-direction: column; }
.ovk-sheet-head { display: flex; align-items: center; gap: 8px; padding: 8px 8px 8px 16px; border-bottom: 1px solid var(--ovk-line); }
@@@ -246,7 +270,7 @@ div.ovk-set-row { cursor: default; }
/* ── Phones (< 600 px): bottom sheets, full-width forms ── */
@media (max-width: 599px) {
-- .ovk-sheet { margin: auto 0 0; width: 100vw; border-radius: 18px 18px 0 0; border-bottom: 0; }
++ .ovk-sheet { margin: auto 0 0; width: 100vw; border-radius: calc(var(--ov-radius-3) - 2px) calc(var(--ov-radius-3) - 2px) 0 0; border-bottom: 0; }
.ovk-row > a, .ovk-row--static { padding-left: 12px; }
.ovk-head-tools { gap: 0; }
}
@@@ -281,16 +305,16 @@ div.ovk-set-row { cursor: default; }
.ovk-with-sidebar-main { min-width: 0; }
.ovk-sidebar { position: sticky; top: var(--navbar-h, 52px); height: calc(100dvh - var(--navbar-h, 52px)); box-sizing: border-box; display: flex; flex-direction: column; gap: 8px; min-width: 0; padding: 12px 10px 10px; border-right: 1px solid var(--ovk-line); background: var(--ovk-panel); color: var(--ovk-text); overflow: hidden; }
.ovk-sidebar-top { display: flex; align-items: center; gap: 6px; }
--.ovk-sidebar-action { flex: 1; display: flex; align-items: center; gap: 10px; min-height: 42px; padding: 0 12px; border-radius: 12px; border: 1px solid var(--ovk-line); background: var(--ovk-bg); color: var(--ovk-text); font-weight: 650; text-decoration: none; transition: border-color .18s, background .18s; }
++.ovk-sidebar-action { flex: 1; display: flex; align-items: center; gap: 10px; min-height: 42px; padding: 0 12px; border-radius: calc(var(--ov-radius-1) + 2px); border: 1px solid var(--ovk-line); background: var(--ovk-bg); color: var(--ovk-text); font-weight: 650; text-decoration: none; transition: border-color .18s, background .18s; }
.ovk-sidebar-action:hover { border-color: color-mix(in srgb, var(--ovk-accent) 55%, var(--ovk-line)); }
.ovk-sidebar-action .ovk-i { width: 18px; height: 18px; color: var(--ovk-accent); }
.ovk-sidebar-close, .ovk-sidebar-open { display: none; }
.ovk-sidebar-find { position: relative; display: flex; align-items: center; margin: 0; }
.ovk-sidebar-find .ovk-i { position: absolute; left: 11px; width: 16px; height: 16px; color: var(--ovk-muted); pointer-events: none; }
--.ovk-sidebar-find input { width: 100%; min-height: 38px; box-sizing: border-box; padding: 0 12px 0 34px; border-radius: 10px; border: 1px solid transparent; background: var(--ovk-input); color: var(--ovk-text); font: inherit; font-size: 14px; }
++.ovk-sidebar-find input { width: 100%; min-height: 38px; box-sizing: border-box; padding: 0 12px 0 34px; border-radius: var(--ov-radius-1); border: 1px solid transparent; background: var(--ovk-input); color: var(--ovk-text); font: inherit; font-size: 14px; }
.ovk-sidebar-find input:focus { outline: none; border-color: var(--ovk-accent); }
.ovk-sidebar-nav { display: grid; gap: 1px; padding-bottom: 8px; border-bottom: 1px solid var(--ovk-line); }
--.ovk-sidebar-link { display: flex; align-items: center; gap: 11px; min-height: 38px; padding: 0 10px; border-radius: 10px; color: var(--ovk-muted); font-size: 14.5px; font-weight: 600; text-decoration: none; transition: background .15s, color .15s; }
++.ovk-sidebar-link { display: flex; align-items: center; gap: 11px; min-height: 38px; padding: 0 10px; border-radius: var(--ov-radius-1); color: var(--ovk-muted); font-size: 14.5px; font-weight: 600; text-decoration: none; transition: background .15s, color .15s; }
.ovk-sidebar-link .ovk-i { width: 19px; height: 19px; flex: none; }
.ovk-sidebar-link span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovk-sidebar-link:hover { background: var(--ovk-panel-2); color: var(--ovk-text); }
@@@ -305,7 +329,7 @@ div.ovk-set-row { cursor: default; }
@keyframes ovk-sidebar-breathe { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .45; transform: scale(.75); } }
.ovk-sidebar-secs { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; margin: 0 -4px; padding: 0 4px 8px; }
.ovk-sidebar-h { margin: 12px 10px 4px; font-size: 11.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--ovk-muted); }
--.ovk-sidebar-item { display: flex; align-items: center; gap: 9px; min-height: 36px; padding: 4px 10px; border-radius: 10px; color: var(--ovk-text); font-size: 14px; text-decoration: none; }
++.ovk-sidebar-item { display: flex; align-items: center; gap: 9px; min-height: 36px; padding: 4px 10px; border-radius: var(--ov-radius-1); color: var(--ovk-text); font-size: 14px; text-decoration: none; }
.ovk-sidebar-item:hover { background: var(--ovk-panel-2); }
.ovk-sidebar-item.is-here { background: var(--ovk-panel-2); font-weight: 600; }
.ovk-sidebar-t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@@ -336,25 +360,25 @@ div.ovk-set-row { cursor: default; }
/* ── Placeholders shaped like the content (4.70.0, kit.skeleton; plan L52) ── A soft light passes over grey shapes the size of
what is coming; nothing spins; still under reduced motion and Calm. A list paints these in its first HTML and fills them in
place. */
--.ovk-skel { display: block; border-radius: 8px; background: color-mix(in srgb, var(--text-primary, #e8edf3) 7%, transparent); background-image: linear-gradient(100deg, transparent 30%, color-mix(in srgb, var(--text-primary, #e8edf3) 9%, transparent) 50%, transparent 70%); background-size: 220% 100%; background-repeat: no-repeat; animation: ovkShimmer 1.6s ease-in-out infinite; }
++.ovk-skel { display: block; border-radius: max(0px, calc(var(--ov-radius-1) - 2px)); background: color-mix(in srgb, var(--text-primary, #e8edf3) 7%, transparent); background-image: linear-gradient(100deg, transparent 30%, color-mix(in srgb, var(--text-primary, #e8edf3) 9%, transparent) 50%, transparent 70%); background-size: 220% 100%; background-repeat: no-repeat; animation: ovkShimmer 1.6s ease-in-out infinite; }
@keyframes ovkShimmer { from { background-position: 120% 0; } to { background-position: -120% 0; } }
.ovk-skels { display: grid; gap: 12px; }
.ovk-skels-card { grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr)); }
.ovk-skels-tile { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); }
.ovk-skels-chip { display: flex; flex-wrap: wrap; gap: 8px; }
.ovk-skel-card { display: grid; gap: 8px; }
--.ovk-skel-thumb { aspect-ratio: 16 / 9; border-radius: 12px; }
++.ovk-skel-thumb { aspect-ratio: 16 / 9; border-radius: calc(var(--ov-radius-1) + 2px); }
.ovk-skel-l1 { height: 13px; width: 86%; }
.ovk-skel-l2 { height: 11px; width: 58%; }
.ovk-skel-row { display: flex; align-items: center; gap: 12px; }
.ovk-skel-avatar { flex: none; width: 40px; height: 40px; border-radius: 50%; }
.ovk-skel-lines { flex: 1; display: grid; gap: 7px; min-width: 0; }
--.ovk-skel-tile { aspect-ratio: 1; border-radius: 14px; }
++.ovk-skel-tile { aspect-ratio: 1; border-radius: var(--ov-radius-2); }
.ovk-skel-chip { width: 84px; height: 30px; border-radius: 999px; }
.ovk-skel-line { height: 12px; }
.ovk-skel-chat { display: flex; align-items: center; gap: 8px; }
.ovk-skel-dot { flex: none; width: 22px; height: 22px; border-radius: 50%; }
--.ovk-skel-stat { display: grid; gap: 6px; padding: 12px; border-radius: 14px; border: 1px solid var(--border, rgba(255,255,255,.08)); }
++.ovk-skel-stat { display: grid; gap: 6px; padding: 12px; border-radius: var(--ov-radius-2); border: 1px solid var(--border, rgba(255,255,255,.08)); }
.ovk-skel-num { height: 22px; width: 48%; }
@media (prefers-reduced-motion: reduce) { .ovk-skel { animation: none; } }
html[data-ov-motion=reduced] .ovk-skel { animation: none; }
ddiff --git a/package.json b/package.json
iindex d4a75a0..7bdcec6 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.80.0",
++ "version": "4.81.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/room.css b/room.css
iindex 46d10e0..3e733aa 100644
---- a/room.css
++++ b/room.css
@@@ -22,16 +22,39 @@
--ovk-warn: var(--warning, #f59e0b);
--ovk-bad: var(--danger, var(--error, #ef4444));
--ovk-live: var(--live-red, #ef4444);
-- --ovk-radius: 14px;
-- --ovk-radius-sm: 10px;
++ /* Shape and elevation (4.81.0, plan L45 step 2): the kit's corners and shadows as tokens a look can set, so a theme
++ * has a shape as well as colours. The defaults are the kit's own corners and shadows; the personalities follow. */
++ --ov-radius-1: 10px; /* chips, inputs, rows, small buttons */
++ --ov-radius-2: 14px; /* cards, sections, panels */
++ --ov-radius-3: 20px; /* sheets, dialogs, big surfaces */
++ --ov-elev-1: 0 1px 3px rgba(0, 0, 0, .35); /* a knob, a chip that sits on its surface */
++ --ov-elev-2: 0 10px 24px -18px rgba(0, 0, 0, .7); /* a bar that has scrolled content under it */
++ --ov-elev-3: 0 14px 32px rgba(0, 0, 0, .4); /* a menu or popover above the page */
++ --ovk-radius: var(--ov-radius-2);
++ --ovk-radius-sm: var(--ov-radius-1);
--ovk-tap: 44px;
--ovk-top: var(--ovnav-real-h, 52px); /* the navbar's height: sticky things stop under it */
--ovk-bottom: var(--ovnav-appbar-h, 0px); /* the app bar's height on phones (navbar.js sets it) */
--ovk-font: var(--ov-font, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
}
++/* Shape personalities, as in kit.css but at zero specificity (plan L45: "Paper is quiet and square, Neon glows and bounces"). A quiet look has square corners
++ * and lies flat, with a hairline where a shadow would be; a loud one is round and its raised things glow in its accent.
++ * The ids are Network's theme catalog (as ov-fx.js's motion feels); any other look keeps the defaults above. */
++:where([data-theme=paper], [data-theme=solarized-light], [data-theme=nord], [data-theme=nord-light], [data-theme=matrix], [data-theme=hacker], [data-theme=high-contrast]) {
++ --ov-radius-1: 4px; --ov-radius-2: 6px; --ov-radius-3: 10px;
++ --ov-elev-1: 0 0 0 1px var(--ovk-line);
++ --ov-elev-2: 0 1px 0 var(--ovk-line);
++ --ov-elev-3: 0 0 0 1px var(--ovk-line), 0 8px 18px -12px rgba(0, 0, 0, .35);
++}
++:where([data-theme=neon-tokyo], [data-theme=vapor]) {
++ --ov-radius-1: 14px; --ov-radius-2: 20px; --ov-radius-3: 28px;
++ --ov-elev-1: 0 0 8px color-mix(in srgb, var(--ovk-accent) 40%, transparent);
++ --ov-elev-2: 0 10px 28px -14px color-mix(in srgb, var(--ovk-accent) 55%, transparent);
++ --ov-elev-3: 0 0 0 1px color-mix(in srgb, var(--ovk-accent) 40%, transparent), 0 18px 44px -14px color-mix(in srgb, var(--ovk-accent) 55%, transparent);
++}
.ovk-stats { position: relative; display: flex; gap: 8px; margin: 12px -16px; padding: 0 16px; overflow-x: auto; scrollbar-width: none; }
.ovk-stats::-webkit-scrollbar { display: none; }
--.ovk-stats > div { display: flex; flex-direction: column-reverse; flex: 0 0 auto; min-width: 104px; box-sizing: border-box; padding: 8px 14px; border: 1px solid var(--ovk-line); border-radius: 12px; background: var(--ovk-panel); }
++.ovk-stats > div { display: flex; flex-direction: column-reverse; flex: 0 0 auto; min-width: 104px; box-sizing: border-box; padding: 8px 14px; border: 1px solid var(--ovk-line); border-radius: calc(var(--ov-radius-1) + 2px); background: var(--ovk-panel); }
.ovk-stats dt { margin: 0; color: var(--ovk-muted); font-size: .76rem; line-height: 1.25; }
.ovk-stats dt a { color: inherit; text-decoration: none; }
.ovk-stat--link { position: relative; transition: border-color .15s; }
ddiff --git a/test/kit-shape.test.js b/test/kit-shape.test.js
nnew file mode 100644
iindex 0000000..0f32d26
---- /dev/null
++++ b/test/kit-shape.test.js
@@@ -0,0 +1,29 @@
++'use strict';
++/**
++ * Shape and elevation tokens (4.81.0, plan L45 step 2): the kit's corners and shadows are --ov-radius-1..3 and
++ * --ov-elev-1..3, with the kit's own values as defaults, so a look can give a theme a shape. Paper and the quiet looks
++ * are square and flat, Neon round and glowing. room.css carries the same tokens and, at zero specificity, the same
++ * personalities (it can load without kit.css).
++ */
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++
++const kit = fs.readFileSync(path.join(__dirname, '..', 'kit.css'), 'utf8');
++const room = fs.readFileSync(path.join(__dirname, '..', 'room.css'), 'utf8');
++
++assert.ok(kit.includes('--ov-radius-1: 10px;'), 'radius-1 defaults to the kit\'s own 10px');
++assert.ok(kit.includes('--ov-radius-2: 14px;'), 'radius-2 defaults to the kit\'s own 14px');
++assert.ok(kit.includes('--ov-radius-3: 20px;'), 'radius-3 is 20px');
++assert.match(kit, /--ov-elev-1: [^;]+;/);
++assert.match(kit, /--ov-elev-2: [^;]+;/);
++assert.match(kit, /--ov-elev-3: [^;]+;/);
++assert.ok(kit.includes('--ovk-radius: var(--ov-radius-2);') && kit.includes('--ovk-radius-sm: var(--ov-radius-1);'), 'the kit\'s own radius names follow the tokens');
++assert.doesNotMatch(kit, /border-radius: (8|10|12|14|18|20)px/, 'no corner of the kit is a literal any more (pills and circles stay)');
++assert.ok(kit.includes('box-shadow: var(--ov-elev-3);') && kit.includes('box-shadow: var(--ov-elev-2);') && kit.includes('box-shadow: var(--ov-elev-1);'), 'the kit\'s shadows are the elevation tokens');
++assert.match(kit, /:is\(\[data-theme=paper\][^{]*\) \{\s*--ov-radius-1: 4px; --ov-radius-2: 6px; --ov-radius-3: 10px;/, 'Paper is square');
++assert.match(kit, /:is\(\[data-theme=neon-tokyo\][^{]*\) \{\s*--ov-radius-1: 14px; --ov-radius-2: 20px; --ov-radius-3: 28px;/, 'Neon is round');
++assert.match(kit, /--ov-elev-3: 0 0 0 1px color-mix\(in srgb, var\(--ovk-accent\)/, 'and Neon\'s raised things glow in its accent');
++assert.match(room, /:where\(\[data-theme=paper\][^{]*\) \{\s*--ov-radius-1: 4px;/, 'room.css: the same personalities at zero specificity');
++assert.match(room, /:where\(\[data-theme=neon-tokyo\][^{]*\) \{\s*--ov-radius-1: 14px;/);
++console.log('kit-shape: tokens, the kit on them, and the personalities');