openvibers/openvibe.shared Commits
4.37.0: Kit 2 gets its own files (kit-data.css, kit-data-web.js); kit.css and kit-web.js weigh what they did before (#175)
Changes
@@@ -6,6 +6,16 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).## Unreleased++## 4.37.0 — 2026-10-29++++- **Kit 2 has its own files: `kit-data.css` and `kit-data-web.js`.** Its look and behaviour moved out of `kit.css` and++ `kit-web.js`, which weigh what they did before Kit 2 again (kit.css 44 → 25.6 KB, kit-web.js 20 → 12.4 KB). A page that++ draws steps, a table, a chart, a ranking, a meter, a drop zone or a receipt adds `kit-data.css` after `kit.css`, and++ `kit-data-web.js` for chart readouts and drop zones. Why: Media's front page needed only cards and rails, and the++ 44 KB kit.css took its stylesheets from 28 KB to 63 KB; every site in the pin round would have paid the same. The++ builders (`kit.js`) are unchanged. Breaking only for a page that used Kit 2 on 4.36.x without the new files (Report,++ Vision, Sources and Codes, updated alongside).++## 4.36.2 — 2026-10-29- **kit-web.js on boosted sites:** a page reached through boost.js (a swap of `#main`, no page load) now gets the kit's@@@ -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.36.2 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.37.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer 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` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.36.2",++ "version": "4.37.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)",@@@ -254,6 +254,17 @@ built under the same rules as the rest of the kit: plain HTML that works withouttokens, and phone first. `node scripts/kit-gallery.js --shots <dir> --kit2` draws every piece at 320, 390, 768 and 1280px wide, light and dark.++Kit 2 is two files of its own, served at `/shared/` like the rest: **`kit-data.css`** (load it after `kit.css`) and++**`kit-data-web.js`** (the chart readout, scatter labels and drop zones; steps, tables, rankings, meters and receipts need++no script). A page that draws none of them loads neither, so `kit.css` and `kit-web.js` weigh what they did before++Kit 2 (split in 4.37.0).++++```html++<link rel="stylesheet" href="${ovServe.url('kit.css')}">++<link rel="stylesheet" href="${ovServe.url('kit-data.css')}">++<script src="${ovServe.url('kit-data-web.js')}" defer></script>++```++| Builder | What it is for | On a phone || --- | --- | --- || `steps({ items, current })` | A flow of a few steps: opt in, verify, choose, live. Done steps link back; later ones never link ahead. | Dots joined by a line, and "Step 2 of 4 · Verify the domain" |@@@ -278,7 +289,7 @@ How the charts are drawn, so they read on a phone: the lines and areas are an SV(`preserveAspectRatio="none"`, and the stroke keeps its width with `vector-effect`), while every tick label, legend,point and bar is real HTML placed by percent. Text stays its real size from 320 px to 1280 px, points and bars arefocusable and say their values, ticks fall on round numbers (1, 2 or 5 × 10ⁿ) and on round moments (hours, days, weeks,--months), and a table with the same numbers sits behind each chart for screen readers. kit-web.js adds the readout on++months), and a table with the same numbers sits behind each chart for screen readers. kit-data-web.js adds the readout onlines and areas (under the pointer, a tap, or ← → Home End) and hides a scatter label that would sit on one alreadyshown, best points first. Colours: `--ovk-c1` is the theme's accent, then `--ovk-c2` … `--ovk-c5` (cyan, amber,violet, green), with deeper tones in light themes.@@@ -42,6 +42,7 @@ const BROWSER = Object.freeze(['appbar.js', // the phone app bar; navbar.js loads it only for a site that sets appBar'kit.js', // the OpenVibe Kit's builders in the page (for pages that draw themselves; with kit-web.js)'kit-web.js', // the OpenVibe Kit's behaviour (copy, menus, Enter to send, confirms, sheets): all a server-rendered site loads++ 'kit-data-web.js', // Kit 2's behaviour: chart readouts, scatter labels, drop zones (4.37.0; with kit-data.css)'search.js', // search everything from the navbar's magnifier, / or Ctrl+K; navbar.js loads it on first use'loading.js', // the one loading look: a poster-backed overlay with status and progress, and skeleton rows'agent-session.js', // a coding agent's session as people read it: steps grouped, live output, diffs, helpers, hand-offs@@@ -58,6 +59,7 @@ const STYLES = Object.freeze(['player.css', // player.js's controls'kit.css', // the OpenVibe Kit's look (kit.js / openvibe-shared/kit build the markup; docs/kit.md)'room.css', // kit.card, kit.rail and a room's accent (4.29.1; load after kit.css where cards, rails or sc.room() are)++ 'kit-data.css', // Kit 2's look: steps, tables, charts, rankings, meters, drop zones, receipts (4.37.0; after kit.css)'agent-session.css', // agent-session.js's look (4.32.0; the script links it from beside itself unless the page does)]);const served = (name) => BROWSER.includes(String(name)) || STYLES.includes(String(name));nnew file mode 100644@@@ -0,0 +1,132 @@++/**++ * openvibe-shared/kit-data-web.js — Kit 2's behaviour in the page (split from kit-web.js in 4.37.0):++ *++ * charts .ovk-chart-plot[data-ovk-chart]: the values under the pointer, a tap, or ← → Home End++ * scatter labels a label that would cover another, or cross the right edge, is hidden or moved++ * drop zones [data-ovk-drop]: drag and drop, paste, the camera on phones, the file's name and picture++ *++ * It runs by itself when it loads, again on a boosted page swap (ov:boost:load), and for markup added later through++ * OpenVibeKitData.enhance(container). Idempotent and safe on a page that uses none of it. Load it on a page that draws++ * kit.chart or kit.drop (kit.steps, table, rank, meter and receipt need no script).++ */++(function (root) {++ 'use strict';++ function enhance(rootEl) {++ if (typeof document === 'undefined') return;++ const scope = rootEl || document;++ const all = (sel) => Array.prototype.slice.call(scope.querySelectorAll ? scope.querySelectorAll(sel) : []);++ const once = (el, key) => { if (el.dataset[key]) return false; el.dataset[key] = '1'; return true; };++ // Charts (Kit 2): a rule, a dot per series and the values under the pointer or the arrow keys, on lines and areas.++ all('.ovk-chart-plot[data-ovk-chart]').forEach((plot) => {++ if (!once(plot, 'ovkChart')) return;++ let data; try { data = JSON.parse(plot.getAttribute('data-ovk-chart')); } catch (e) { return; }++ if (!data || !data.x || !data.x.length) return;++ const rule = plot.querySelector('.ovk-chart-rule'), tip = plot.querySelector('.ovk-chart-tip');++ if (!rule || !tip) return;++ let at = -1;++ const show = (i) => {++ at = Math.max(0, Math.min(data.x.length - 1, i));++ const [px, label] = data.x[at];++ rule.style.left = `${px}%`; rule.hidden = false;++ rule.innerHTML = data.s.map((s) => (s.v[at] ? `<i class="ovk-chart-s${s.c}" style="bottom:${s.v[at][0]}%"></i>` : '')).join('');++ tip.textContent = '';++ const b = document.createElement('b'); b.textContent = label; tip.appendChild(b);++ data.s.forEach((s) => { if (!s.v[at]) return; const line = document.createElement('span'); line.className = `ovk-chart-s${s.c}`; line.innerHTML = '<i></i>'; line.appendChild(document.createTextNode(`${s.n ? `${s.n} ` : ''}${s.v[at][1]}`)); tip.appendChild(line); });++ tip.hidden = false;++ const w = plot.clientWidth, tw = tip.offsetWidth, x = (px / 100) * w;++ tip.style.left = `${Math.max(0, Math.min(w - tw, x + 12 + tw > w ? x - tw - 12 : x + 12))}px`;++ };++ const hide = () => { rule.hidden = true; tip.hidden = true; };++ const nearest = (clientX) => {++ const r = plot.getBoundingClientRect(); const p = ((clientX - r.left) / r.width) * 100;++ let best = 0; for (let i = 1; i < data.x.length; i++) if (Math.abs(data.x[i][0] - p) < Math.abs(data.x[best][0] - p)) best = i;++ return best;++ };++ plot.addEventListener('pointermove', (e) => show(nearest(e.clientX)));++ plot.addEventListener('pointerdown', (e) => show(nearest(e.clientX)));++ plot.addEventListener('pointerleave', hide);++ plot.addEventListener('blur', hide);++ plot.addEventListener('focus', () => show(at < 0 ? data.x.length - 1 : at));++ plot.addEventListener('keydown', (e) => {++ const step = { ArrowLeft: -1, ArrowRight: 1, Home: -1e9, End: 1e9 }[e.key];++ if (step == null) { if (e.key === 'Escape') hide(); return; }++ e.preventDefault(); show((at < 0 ? data.x.length - 1 : at) + step);++ });++ });++ // Scatter labels (Kit 2): a label that would sit on one already shown is hidden (best points first); its dot still says it.++ all('.ovk-chart--scatter .ovk-chart-plot').forEach((plot) => {++ const names = Array.prototype.slice.call(plot.querySelectorAll('.ovk-chart-name'));++ if (names.length < 2) return;++ const place = () => {++ names.forEach((n) => { n.classList.remove('is-hidden'); n.parentNode.classList.remove('is-flip'); });++ // A label that would run past the plot's right edge goes to the left of its dot.++ const edge = plot.getBoundingClientRect().right + 4;++ names.forEach((n) => { if (!n.parentNode.classList.contains('is-right') && n.getBoundingClientRect().right > edge) n.parentNode.classList.add('is-flip'); });++ const order = names.slice().sort((a, b) => (b.parentNode.classList.contains('is-best') - a.parentNode.classList.contains('is-best')) || (parseFloat(b.parentNode.style.bottom) - parseFloat(a.parentNode.style.bottom)));++ const kept = [];++ order.forEach((n) => {++ const r = n.getBoundingClientRect();++ const hit = kept.some((k) => r.left < k.right + 4 && r.right + 4 > k.left && r.top < k.bottom + 2 && r.bottom + 2 > k.top);++ if (hit) n.classList.add('is-hidden'); else kept.push(r);++ });++ };++ if (!once(plot, 'ovkLabels')) return;++ place();++ if (typeof ResizeObserver !== 'undefined') new ResizeObserver(place).observe(plot);++ });++ // Drop zones (Kit 2): drag and drop, paste, the camera on phones, the chosen file's name and picture.++ all('[data-ovk-drop]').forEach((box) => {++ if (!once(box, 'ovkDrop')) return;++ const input = box.querySelector('.ovk-drop-input'), zone = box.querySelector('.ovk-drop-zone'), name = box.querySelector('.ovk-drop-name');++ if (!input || !zone) return;++ box.classList.add('is-js');++ let url = null;++ const shown = () => {++ const files = Array.prototype.slice.call(input.files || []);++ box.classList.toggle('has-file', files.length > 0);++ if (!name) return;++ name.textContent = '';++ if (url) { URL.revokeObjectURL(url); url = null; }++ if (!files.length) return;++ if (files.length === 1 && /^image\//.test(files[0].type) && window.URL) {++ const img = document.createElement('img'); url = URL.createObjectURL(files[0]); img.src = url; img.alt = ''; name.appendChild(img);++ }++ name.appendChild(document.createTextNode(files.length === 1 ? files[0].name : `${files.length} files`));++ };++ const take = (list) => {++ if (!list || !list.length || typeof DataTransfer === 'undefined') return;++ const dt = new DataTransfer();++ Array.prototype.slice.call(list, 0, input.multiple ? list.length : 1).forEach((f) => dt.items.add(f));++ input.files = dt.files;++ input.dispatchEvent(new Event('change', { bubbles: true }));++ };++ input.addEventListener('change', () => {++ shown();++ if (box.hasAttribute('data-ovk-auto') && input.files && input.files.length && input.form) (input.form.requestSubmit ? input.form.requestSubmit() : input.form.submit());++ });++ ['dragenter', 'dragover'].forEach((t) => zone.addEventListener(t, (e) => { e.preventDefault(); box.classList.add('is-over'); }));++ ['dragleave', 'dragend'].forEach((t) => zone.addEventListener(t, () => box.classList.remove('is-over')));++ zone.addEventListener('drop', (e) => { e.preventDefault(); box.classList.remove('is-over'); take(e.dataTransfer && e.dataTransfer.files); });++ // Paste: only while this is the page's one drop zone, and never into a field someone is typing in.++ document.addEventListener('paste', (e) => {++ if (document.querySelectorAll('[data-ovk-drop]').length !== 1) return;++ const t = e.target; if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;++ const files = e.clipboardData && e.clipboardData.files; if (files && files.length) { e.preventDefault(); take(files); }++ });++ const cam = box.querySelector('.ovk-drop-cam'), camInput = box.querySelector('[data-ovk-drop-cam]');++ if (cam && camInput && window.matchMedia && matchMedia('(pointer: coarse)').matches) {++ cam.hidden = false;++ camInput.addEventListener('change', () => { take(camInput.files); camInput.value = ''; });++ }++ });++ }++++ const api = { enhance };++ root.OpenVibeKitData = api;++ if (typeof module === 'object' && module.exports) { module.exports = api; return; }++ if (typeof document !== 'undefined') {++ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => enhance(document));++ else enhance(document);++ document.addEventListener('ov:boost:load', () => enhance(document));++ }++})(typeof globalThis !== 'undefined' ? globalThis : this);nnew file mode 100644@@@ -0,0 +1,175 @@++/* openvibe-shared/kit-data.css — Kit 2's look: steps, tables, charts, rankings, meters, drop zones and receipts++ * (kit.steps/table/chart/rank/meter/drop/receipt; docs/kit.md "Kit 2"). Load it after kit.css, whose tokens it uses,++ * on a page that draws any of them; kit-data-web.js adds their behaviour. Split from kit.css in 4.37.0 so a page that++ * uses none of them does not carry them (kit.css weighed 25 KB before Kit 2, 44 KB with it). */++++:root {++ --ovk-c1: var(--ovk-accent); --ovk-c2: #22d3ee; --ovk-c3: #f59e0b; --ovk-c4: #a78bfa; --ovk-c5: #34d399;++}++:root[data-theme-mode="light"] { --ovk-c2: #0e7490; --ovk-c3: #b45309; --ovk-c4: #6d28d9; --ovk-c5: #047857; }++.ovk-chart-s1 { --ovk-c: var(--ovk-c1); } .ovk-chart-s2 { --ovk-c: var(--ovk-c2); } .ovk-chart-s3 { --ovk-c: var(--ovk-c3); }++.ovk-chart-s4 { --ovk-c: var(--ovk-c4); } .ovk-chart-s5 { --ovk-c: var(--ovk-c5); }++++/* Steps: dots joined by a line; a phone says "Step 2 of 4 · Verify", wider screens name every step */++.ovk-steps { margin: 0 0 16px; color: var(--ovk-text); }++.ovk-steps-now { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; margin: 0 0 10px; font-size: .92rem; }++.ovk-steps-now span { color: var(--ovk-muted); font-size: .8rem; font-weight: 700; letter-spacing: .02em; }++.ovk-steps ol { display: flex; margin: 0; padding: 0; list-style: none; }++.ovk-steps li { position: relative; flex: 1 1 0; min-width: 0; display: flex; justify-content: center; }++/* The line between two dots stops short of both, so it never crosses a dot (or shows through a see-through panel). */++.ovk-steps li + li::before { content: ''; position: absolute; top: 21px; right: calc(50% + 20px); width: calc(100% - 40px); height: 2px; border-radius: 2px; background: var(--ovk-line); }++.ovk-steps li.is-done + li::before { background: var(--ovk-fill); }++.ovk-steps li > a, .ovk-steps-item { position: relative; display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: var(--ovk-tap); min-height: var(--ovk-tap); padding: 8px 4px 0; color: inherit; text-decoration: none; text-align: center; }++.ovk-steps-n { display: grid; place-items: center; width: 28px; height: 28px; box-sizing: border-box; border-radius: 50%; border: 2px solid var(--ovk-line); background: var(--ovk-panel); color: var(--ovk-muted); font-size: .8rem; font-weight: 800; font-variant-numeric: tabular-nums; }++.ovk-steps-n .ovk-i { width: 15px; height: 15px; stroke-width: 3; }++.ovk-steps .is-done .ovk-steps-n { border-color: var(--ovk-fill); background: var(--ovk-fill); color: var(--ovk-on-fill); }++.ovk-steps .is-current .ovk-steps-n { border-color: var(--ovk-accent); background: color-mix(in srgb, var(--ovk-accent) 16%, var(--ovk-panel)); color: var(--ovk-accent); box-shadow: 0 0 0 4px color-mix(in srgb, var(--ovk-accent) 14%, transparent); }++.ovk-steps li > a:hover .ovk-steps-n { filter: brightness(1.1); }++.ovk-steps li > a:focus-visible { outline: 2px solid var(--ovk-accent); outline-offset: 2px; border-radius: 12px; }++.ovk-steps-l { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }++.ovk-steps-l small { display: block; color: var(--ovk-muted); font-size: .76rem; font-weight: 500; }++@media (min-width: 600px) {++ .ovk-steps-now { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }++ .ovk-steps-l { position: static !important; width: auto; height: auto; overflow: visible; clip: auto; white-space: normal; font-size: .86rem; font-weight: 600; line-height: 1.3; }++ .ovk-steps .is-next .ovk-steps-l { color: var(--ovk-muted); }++ .ovk-steps .is-current .ovk-steps-l { color: var(--ovk-text); font-weight: 800; }++}++++/* Tables: a real table on wide screens; under 600 px each row is a card that names its cells */++.ovk-table-wrap { margin: 0 0 14px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow-x: auto; color: var(--ovk-text); }++.ovk-table-wrap:focus-visible { outline: 2px solid var(--ovk-accent); outline-offset: 2px; }++.ovk-table { width: 100%; border-collapse: collapse; font-size: .92rem; line-height: 1.4; }++.ovk-table caption { padding: 12px 14px 6px; color: var(--ovk-muted); font-size: .74rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; text-align: left; }++.ovk-table thead th { padding: 10px 14px; color: var(--ovk-muted); font-size: .72rem; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; text-align: left; white-space: nowrap; border-bottom: 1px solid var(--ovk-line); }++.ovk-table td, .ovk-table tbody th { padding: 11px 14px; text-align: left; vertical-align: top; border-top: 1px solid var(--ovk-line); overflow-wrap: anywhere; }++.ovk-table tbody tr:first-child > * { border-top: 0; }++.ovk-table tbody th { font-weight: 700; }++.ovk-table .is-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }++.ovk-table a { color: var(--ovk-accent); text-decoration: none; }++.ovk-table a:hover { text-decoration: underline; }++.ovk-table tbody tr:hover > * { background: color-mix(in srgb, var(--ovk-panel-2) 55%, transparent); }++.ovk-table-empty { margin: 0 0 14px; padding: 18px 16px; border: 1px dashed var(--ovk-line); border-radius: var(--ovk-radius); color: var(--ovk-muted); text-align: center; }++@media (max-width: 599px) {++ .ovk-table--stack { overflow: visible; }++ .ovk-table--stack .ovk-table, .ovk-table--stack tbody, .ovk-table--stack tr, .ovk-table--stack td, .ovk-table--stack tbody th { display: block; }++ .ovk-table--stack caption { display: block; }++ .ovk-table--stack thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }++ .ovk-table--stack tbody tr { padding: 12px 14px; border-top: 1px solid var(--ovk-line); }++ .ovk-table--stack tbody tr:first-child { border-top: 0; }++ .ovk-table--stack tbody tr:hover > * { background: none; }++ .ovk-table--stack td > *, .ovk-table--stack td { min-width: 0; }++ .ovk-table--stack td, .ovk-table--stack tbody th { display: flex; justify-content: space-between; align-items: baseline; gap: 14px; padding: 3px 0; border: 0; text-align: right; }++ .ovk-table--stack td::before { content: attr(data-label); flex: none; max-width: 50%; color: var(--ovk-muted); font-size: .8rem; text-align: left; }++ .ovk-table--stack .is-primary { display: block; padding: 0 0 4px; font-size: 1rem; font-weight: 800; text-align: left; }++ .ovk-table--stack .is-primary::before { content: none; }++ .ovk-table--stack .is-hide-sm { display: none; }++}++++/* Charts: the lines are an SVG stretched to the box; every label is real text at its real size */++.ovk-chart { overflow: clip; margin: 0 0 14px; padding: 14px 14px 10px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); color: var(--ovk-text); font-family: var(--ovk-font); }++.ovk-chart figcaption { display: grid; gap: 2px; margin: 0 0 8px; }++.ovk-chart figcaption strong { font-size: 1rem; font-weight: 800; }++.ovk-chart figcaption span { color: var(--ovk-muted); font-size: .84rem; }++.ovk-chart-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 0 0 10px; padding: 0; list-style: none; font-size: .84rem; color: var(--ovk-muted); }++.ovk-chart-legend li { display: inline-flex; align-items: center; gap: 6px; }++.ovk-chart-legend i { width: 14px; height: 3px; border-radius: 2px; background: var(--ovk-c); }++.ovk-chart-legend b { color: var(--ovk-text); font-weight: 800; font-variant-numeric: tabular-nums; }++.ovk-chart-yname { display: block; margin: 0 0 6px; color: var(--ovk-muted); font-size: .74rem; font-weight: 700; }++.ovk-chart-xname { margin: 2px 0 0; color: var(--ovk-muted); font-size: .74rem; font-weight: 700; text-align: right; }++.ovk-chart-plot { position: relative; height: var(--ovk-chart-h, 220px); margin: 8px 6px 26px var(--ovk-chart-g, 44px); outline: none; touch-action: pan-y; }++.ovk-chart-plot:focus-visible { box-shadow: 0 0 0 2px var(--ovk-accent); border-radius: 4px; }++.ovk-chart-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }++.ovk-chart-grid { position: absolute; left: 0; right: 0; height: 0; border-top: 1px solid color-mix(in srgb, var(--ovk-line) 70%, transparent); }++.ovk-chart-yt { position: absolute; right: calc(100% + 8px); transform: translateY(50%); color: var(--ovk-muted); font-size: .72rem; font-variant-numeric: tabular-nums; white-space: nowrap; line-height: 1; }++.ovk-chart-xt { position: absolute; top: calc(100% + 8px); transform: translateX(-50%); color: var(--ovk-muted); font-size: .72rem; white-space: nowrap; line-height: 1; }++.ovk-chart-line { fill: none; stroke: var(--ovk-c, var(--ovk-c1)); stroke-width: 2.25; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }++.ovk-chart-area { fill: color-mix(in srgb, var(--ovk-c, var(--ovk-c1)) 16%, transparent); stroke: none; }++.ovk-chart-front { fill: none; stroke: var(--ovk-c3); stroke-width: 1.5; stroke-dasharray: 5 4; vector-effect: non-scaling-stroke; }++.ovk-chart-rule { position: absolute; top: 0; bottom: 0; width: 1px; background: color-mix(in srgb, var(--ovk-text) 35%, transparent); pointer-events: none; }++.ovk-chart-rule[hidden], .ovk-chart-tip[hidden] { display: none; }++.ovk-chart-rule i { position: absolute; left: -5px; width: 10px; height: 10px; box-sizing: border-box; border-radius: 50%; border: 2px solid var(--ovk-panel); background: var(--ovk-c); transform: translateY(50%); }++.ovk-chart-tip, .ovk-chart-pop { position: absolute; z-index: 3; min-width: 96px; padding: 7px 10px; border: 1px solid var(--ovk-line); border-radius: 10px; background: var(--ovk-panel-2); color: var(--ovk-text); font-size: .8rem; line-height: 1.35; font-variant-numeric: tabular-nums; white-space: nowrap; box-shadow: 0 8px 24px rgba(0, 0, 0, .28); pointer-events: none; }++.ovk-chart-tip { top: 0; }++.ovk-chart-tip b, .ovk-chart-pop b { display: block; color: var(--ovk-muted); font-size: .74rem; font-weight: 700; }++.ovk-chart-tip span { display: flex; align-items: center; gap: 6px; }++.ovk-chart-tip span i { width: 8px; height: 8px; border-radius: 50%; background: var(--ovk-c); }++.ovk-chart-pop { display: none; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%); }++.ovk-chart-dot { position: absolute; z-index: 1; width: 12px; height: 12px; margin: 0 0 -6px -6px; box-sizing: border-box; border-radius: 50%; border: 2px solid var(--ovk-panel); background: color-mix(in srgb, var(--ovk-c1) 60%, var(--ovk-muted)); color: var(--ovk-text); outline: none; text-decoration: none; }++.ovk-chart-dot::after { content: ''; position: absolute; inset: -12px; }++.ovk-chart-dot.is-best { background: var(--ovk-c1); width: 14px; height: 14px; margin: 0 0 -7px -7px; }++.ovk-chart-dot:hover, .ovk-chart-dot:focus-visible { z-index: 4; box-shadow: 0 0 0 3px color-mix(in srgb, var(--ovk-accent) 40%, transparent); }++.ovk-chart-dot:hover .ovk-chart-pop, .ovk-chart-dot:focus-visible .ovk-chart-pop, .ovk-chart-bar:hover .ovk-chart-pop, .ovk-chart-bar:focus-visible .ovk-chart-pop { display: block; }++.ovk-chart-dot.is-right .ovk-chart-pop { left: auto; right: -6px; transform: none; }++.ovk-chart-name.is-hidden { visibility: hidden; }++.ovk-chart-name { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--ovk-text); font-size: .74rem; font-weight: 600; white-space: nowrap; pointer-events: none; text-shadow: 0 0 3px var(--ovk-panel), 0 0 3px var(--ovk-panel); }++.ovk-chart-dot.is-right .ovk-chart-name, .ovk-chart-dot.is-flip .ovk-chart-name { left: auto; right: 14px; }++.ovk-chart-bars { position: absolute; inset: 0; display: flex; align-items: flex-end; gap: clamp(1px, calc(12% / var(--n, 10)), 6px); }++.ovk-chart-bar { position: relative; flex: 1 1 0; min-width: 2px; height: var(--h, 0%); border-radius: 3px 3px 0 0; background: var(--ovk-c1); outline: none; }++.ovk-chart-bar:hover, .ovk-chart-bar:focus-visible { filter: brightness(1.15); box-shadow: 0 0 0 2px color-mix(in srgb, var(--ovk-accent) 45%, transparent); }++.ovk-chart-bar.is-none { background: transparent; }++.ovk-chart-blab { position: absolute; top: calc(100% + 8px); left: 50%; transform: translateX(-50%); color: var(--ovk-muted); font-size: .72rem; white-space: nowrap; line-height: 1; }++.ovk-chart-bar .ovk-chart-pop { bottom: calc(100% + 6px); }++++/* Rankings: place, name, value ± interval, change, and a bar in proportion */++.ovk-rank { margin: 0 0 14px; padding: 0; list-style: none; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow: hidden; color: var(--ovk-text); }++.ovk-rank-row { position: relative; display: grid; grid-template-columns: 2.1em auto minmax(0, 1fr) auto auto; align-items: center; column-gap: 10px; row-gap: 6px; padding: 11px 14px 10px; border-top: 1px solid var(--ovk-line); }++.ovk-rank-row:first-child { border-top: 0; }++.ovk-rank-n { grid-column: 1; color: var(--ovk-muted); font-size: .95rem; font-weight: 800; font-variant-numeric: tabular-nums; text-align: center; }++.ovk-rank-row.is-top .ovk-rank-n { color: var(--ovk-accent); }++.ovk-rank-row > .ovk-mark { grid-column: 2; --ovk-mark: 34px; }++.ovk-rank-main { grid-column: 3; min-width: 0; display: grid; gap: 1px; }++.ovk-rank-name { display: flex; align-items: center; gap: 6px; min-width: 0; }++.ovk-rank-name > strong, .ovk-rank-link { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 700; color: var(--ovk-text); text-decoration: none; }++.ovk-rank-link::after { content: ''; position: absolute; inset: 0; }++.ovk-rank-row:has(.ovk-rank-link):hover { background: var(--ovk-panel-2); }++.ovk-rank-link:focus-visible { outline: none; }++.ovk-rank-row:has(.ovk-rank-link:focus-visible) { box-shadow: inset 0 0 0 2px var(--ovk-accent); }++.ovk-rank-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ovk-muted); font-size: .8rem; }++.ovk-rank-val { grid-column: 4; display: grid; justify-items: end; line-height: 1.2; }++.ovk-rank-val b { font-size: 1rem; font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; }++.ovk-rank-val b small { color: var(--ovk-muted); font-size: .76rem; font-weight: 600; }++.ovk-rank-val > small { color: var(--ovk-muted); font-size: .74rem; font-variant-numeric: tabular-nums; white-space: nowrap; }++.ovk-rank-delta { grid-column: 5; display: inline-flex; align-items: center; gap: 1px; min-width: 2.6em; justify-content: flex-end; font-size: .78rem; font-weight: 800; font-variant-numeric: tabular-nums; }++.ovk-rank-delta .ovk-i { width: 13px; height: 13px; stroke-width: 3; }++.ovk-rank-delta.is-up { color: var(--ovk-ok); } .ovk-rank-delta.is-down { color: var(--ovk-bad); } .ovk-rank-delta.is-new { color: var(--ovk-accent); text-transform: uppercase; font-size: .68rem; letter-spacing: .04em; }++.ovk-rank-bar { grid-column: 3 / -1; grid-row: 2; height: 4px; border-radius: 999px; background: color-mix(in srgb, var(--ovk-line) 60%, transparent); overflow: hidden; }++.ovk-rank-bar::after { content: ''; display: block; height: 100%; width: calc(var(--v, 0) * 100%); border-radius: inherit; background: color-mix(in srgb, var(--ovk-c1) 55%, transparent); }++.ovk-rank-row.is-top .ovk-rank-bar::after { background: var(--ovk-c1); }++++/* Meters */++.ovk-meter { display: block; height: 8px; min-width: 56px; border-radius: 999px; background: color-mix(in srgb, var(--ovk-line) 70%, transparent); overflow: hidden; }++.ovk-meter > span { display: block; height: 100%; width: calc(var(--v, 0) * 100%); border-radius: inherit; background: var(--ovk-fill); }++.ovk-meter--ok > span { background: var(--ovk-ok); } .ovk-meter--warn > span { background: var(--ovk-warn); } .ovk-meter--bad > span { background: var(--ovk-bad); }++++/* Drop zones: a real file input; drag, paste and the camera arrive with kit-web.js */++.ovk-drop { margin: 0 0 14px; color: var(--ovk-text); }++.ovk-drop-input { display: block; width: 100%; margin: 0 0 8px; color: var(--ovk-muted); font: inherit; }++.ovk-drop.is-js .ovk-drop-input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; overflow: hidden; }++.ovk-drop-zone { display: grid; justify-items: center; gap: 6px; padding: 28px 16px; border: 2px dashed var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); text-align: center; cursor: pointer; transition: border-color .12s, background .12s; }++.ovk-drop-zone > .ovk-i { width: 30px; height: 30px; color: var(--ovk-accent); }++.ovk-drop-zone strong { font-size: 1rem; font-weight: 800; }++.ovk-drop-zone small { color: var(--ovk-muted); font-size: .84rem; }++.ovk-drop-zone:hover, .ovk-drop.is-over .ovk-drop-zone { border-color: var(--ovk-accent); background: color-mix(in srgb, var(--ovk-accent) 8%, var(--ovk-panel)); }++.ovk-drop-input:focus-visible + .ovk-drop-zone { outline: 2px solid var(--ovk-accent); outline-offset: 2px; }++.ovk-drop-name:empty { display: none; }++.ovk-drop-name { display: grid; justify-items: center; gap: 6px; margin-top: 4px; font-size: .86rem; font-weight: 600; word-break: break-all; }++.ovk-drop-name img { max-width: 100%; max-height: 160px; border-radius: 10px; object-fit: contain; }++.ovk-drop.has-file .ovk-drop-zone { border-style: solid; border-color: color-mix(in srgb, var(--ovk-accent) 55%, var(--ovk-line)); }++.ovk-drop-cam { margin-top: 8px; }++.ovk-drop-cam[hidden] { display: none; }++++/* Receipts: the result first, then one quiet line of what it took */++.ovk-receipt { margin: 0 0 14px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow: hidden; color: var(--ovk-text); }++.ovk-receipt--ok { border-color: color-mix(in srgb, var(--ovk-ok) 45%, var(--ovk-line)); }++.ovk-receipt--bad { border-color: color-mix(in srgb, var(--ovk-bad) 45%, var(--ovk-line)); }++.ovk-receipt-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 8px 0 14px; min-height: 44px; }++.ovk-receipt-head.ovk-receipt-head h2 { margin: 0; font-size: 1rem; font-weight: 800; }++.ovk-receipt-acts { display: inline-flex; gap: 2px; }++.ovk-receipt-body { padding: 10px 14px 14px; line-height: 1.5; overflow-wrap: anywhere; }++.ovk-receipt-body > :first-child { margin-top: 0; } .ovk-receipt-body > :last-child { margin-bottom: 0; }++.ovk-receipt-facts { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 0; padding: 10px 14px; border-top: 1px solid var(--ovk-line); background: color-mix(in srgb, var(--ovk-panel-2) 45%, transparent); font-size: .8rem; }++.ovk-receipt-facts > div { display: inline-flex; align-items: center; gap: 5px; }++.ovk-receipt-facts dt { color: var(--ovk-muted); }++.ovk-receipt-facts dd { display: inline-flex; align-items: center; gap: 6px; margin: 0; font-weight: 700; font-variant-numeric: tabular-nums; }++.ovk-receipt-facts .ovk-meter { width: 56px; height: 6px; }++@media (prefers-reduced-motion: reduce) { .ovk-drop-zone { transition: none; } }@@@ -15,8 +15,6 @@* share [data-ovk-share]: the system share sheet, else the link copied (the menu item says "Link copied")* card previews .ovk-card video[data-src]: a muted loop while the card is hovered or focused (pointers only, never* under reduced motion; the file loads only then)-- * charts .ovk-chart-plot[data-ovk-chart]: the values under the pointer, a tap, or ← → Home End (Kit 2)-- * drop zones [data-ovk-drop]: drag and drop, paste, the camera on phones, the file's name and picture (Kit 2)** Idempotent and safe on a page that uses none of it. docs/kit.md describes the pieces.*/@@@ -159,109 +157,6 @@}, () => {});});});-- // Charts (Kit 2): a rule, a dot per series and the values under the pointer or the arrow keys, on lines and areas.-- all('.ovk-chart-plot[data-ovk-chart]').forEach((plot) => {-- if (!once(plot, 'ovkChart')) return;-- let data; try { data = JSON.parse(plot.getAttribute('data-ovk-chart')); } catch (e) { return; }-- if (!data || !data.x || !data.x.length) return;-- const rule = plot.querySelector('.ovk-chart-rule'), tip = plot.querySelector('.ovk-chart-tip');-- if (!rule || !tip) return;-- let at = -1;-- const show = (i) => {-- at = Math.max(0, Math.min(data.x.length - 1, i));-- const [px, label] = data.x[at];-- rule.style.left = `${px}%`; rule.hidden = false;-- rule.innerHTML = data.s.map((s) => (s.v[at] ? `<i class="ovk-chart-s${s.c}" style="bottom:${s.v[at][0]}%"></i>` : '')).join('');-- tip.textContent = '';-- const b = document.createElement('b'); b.textContent = label; tip.appendChild(b);-- data.s.forEach((s) => { if (!s.v[at]) return; const line = document.createElement('span'); line.className = `ovk-chart-s${s.c}`; line.innerHTML = '<i></i>'; line.appendChild(document.createTextNode(`${s.n ? `${s.n} ` : ''}${s.v[at][1]}`)); tip.appendChild(line); });-- tip.hidden = false;-- const w = plot.clientWidth, tw = tip.offsetWidth, x = (px / 100) * w;-- tip.style.left = `${Math.max(0, Math.min(w - tw, x + 12 + tw > w ? x - tw - 12 : x + 12))}px`;-- };-- const hide = () => { rule.hidden = true; tip.hidden = true; };-- const nearest = (clientX) => {-- const r = plot.getBoundingClientRect(); const p = ((clientX - r.left) / r.width) * 100;-- let best = 0; for (let i = 1; i < data.x.length; i++) if (Math.abs(data.x[i][0] - p) < Math.abs(data.x[best][0] - p)) best = i;-- return best;-- };-- plot.addEventListener('pointermove', (e) => show(nearest(e.clientX)));-- plot.addEventListener('pointerdown', (e) => show(nearest(e.clientX)));-- plot.addEventListener('pointerleave', hide);-- plot.addEventListener('blur', hide);-- plot.addEventListener('focus', () => show(at < 0 ? data.x.length - 1 : at));-- plot.addEventListener('keydown', (e) => {-- const step = { ArrowLeft: -1, ArrowRight: 1, Home: -1e9, End: 1e9 }[e.key];-- if (step == null) { if (e.key === 'Escape') hide(); return; }-- e.preventDefault(); show((at < 0 ? data.x.length - 1 : at) + step);-- });-- });-- // Scatter labels (Kit 2): a label that would sit on one already shown is hidden (best points first); its dot still says it.-- all('.ovk-chart--scatter .ovk-chart-plot').forEach((plot) => {-- const names = Array.prototype.slice.call(plot.querySelectorAll('.ovk-chart-name'));-- if (names.length < 2) return;-- const place = () => {-- names.forEach((n) => { n.classList.remove('is-hidden'); n.parentNode.classList.remove('is-flip'); });-- // A label that would run past the plot's right edge goes to the left of its dot.-- const edge = plot.getBoundingClientRect().right + 4;-- names.forEach((n) => { if (!n.parentNode.classList.contains('is-right') && n.getBoundingClientRect().right > edge) n.parentNode.classList.add('is-flip'); });-- const order = names.slice().sort((a, b) => (b.parentNode.classList.contains('is-best') - a.parentNode.classList.contains('is-best')) || (parseFloat(b.parentNode.style.bottom) - parseFloat(a.parentNode.style.bottom)));-- const kept = [];-- order.forEach((n) => {-- const r = n.getBoundingClientRect();-- const hit = kept.some((k) => r.left < k.right + 4 && r.right + 4 > k.left && r.top < k.bottom + 2 && r.bottom + 2 > k.top);-- if (hit) n.classList.add('is-hidden'); else kept.push(r);-- });-- };-- if (!once(plot, 'ovkLabels')) return;-- place();-- if (typeof ResizeObserver !== 'undefined') new ResizeObserver(place).observe(plot);-- });-- // Drop zones (Kit 2): drag and drop, paste, the camera on phones, the chosen file's name and picture.-- all('[data-ovk-drop]').forEach((box) => {-- if (!once(box, 'ovkDrop')) return;-- const input = box.querySelector('.ovk-drop-input'), zone = box.querySelector('.ovk-drop-zone'), name = box.querySelector('.ovk-drop-name');-- if (!input || !zone) return;-- box.classList.add('is-js');-- let url = null;-- const shown = () => {-- const files = Array.prototype.slice.call(input.files || []);-- box.classList.toggle('has-file', files.length > 0);-- if (!name) return;-- name.textContent = '';-- if (url) { URL.revokeObjectURL(url); url = null; }-- if (!files.length) return;-- if (files.length === 1 && /^image\//.test(files[0].type) && window.URL) {-- const img = document.createElement('img'); url = URL.createObjectURL(files[0]); img.src = url; img.alt = ''; name.appendChild(img);-- }-- name.appendChild(document.createTextNode(files.length === 1 ? files[0].name : `${files.length} files`));-- };-- const take = (list) => {-- if (!list || !list.length || typeof DataTransfer === 'undefined') return;-- const dt = new DataTransfer();-- Array.prototype.slice.call(list, 0, input.multiple ? list.length : 1).forEach((f) => dt.items.add(f));-- input.files = dt.files;-- input.dispatchEvent(new Event('change', { bubbles: true }));-- };-- input.addEventListener('change', () => {-- shown();-- if (box.hasAttribute('data-ovk-auto') && input.files && input.files.length && input.form) (input.form.requestSubmit ? input.form.requestSubmit() : input.form.submit());-- });-- ['dragenter', 'dragover'].forEach((t) => zone.addEventListener(t, (e) => { e.preventDefault(); box.classList.add('is-over'); }));-- ['dragleave', 'dragend'].forEach((t) => zone.addEventListener(t, () => box.classList.remove('is-over')));-- zone.addEventListener('drop', (e) => { e.preventDefault(); box.classList.remove('is-over'); take(e.dataTransfer && e.dataTransfer.files); });-- // Paste: only while this is the page's one drop zone, and never into a field someone is typing in.-- document.addEventListener('paste', (e) => {-- if (document.querySelectorAll('[data-ovk-drop]').length !== 1) return;-- const t = e.target; if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;-- const files = e.clipboardData && e.clipboardData.files; if (files && files.length) { e.preventDefault(); take(files); }-- });-- const cam = box.querySelector('.ovk-drop-cam'), camInput = box.querySelector('[data-ovk-drop-cam]');-- if (cam && camInput && window.matchMedia && matchMedia('(pointer: coarse)').matches) {-- cam.hidden = false;-- camInput.addEventListener('change', () => { take(camInput.files); camInput.value = ''; });-- }-- });// Card previews: a muted loop while hovered or focused, on computers only, never under reduced motion.if (fine) all('.ovk-card').forEach((c) => {const v = c.querySelector('video.ovk-card-video[data-src]');@@@ -256,175 +256,3 @@ div.ovk-set-row { cursor: default; }}@media (prefers-reduced-motion: reduce) { .ovk-switch, .ovk-switch::after, .ovk-fold > summary > .ovk-i, .ovk-head, .ovk-btn { transition: none; } }@media print { .ovk-head, .ovk-dock, .ovk-save { position: static; background: none; -webkit-backdrop-filter: none; backdrop-filter: none; } }----/* ── Kit 2 (4.36.0): steps, tables, charts, rankings, meters, drop zones, receipts ── */--:root {-- --ovk-c1: var(--ovk-accent); --ovk-c2: #22d3ee; --ovk-c3: #f59e0b; --ovk-c4: #a78bfa; --ovk-c5: #34d399;--}--:root[data-theme-mode="light"] { --ovk-c2: #0e7490; --ovk-c3: #b45309; --ovk-c4: #6d28d9; --ovk-c5: #047857; }--.ovk-chart-s1 { --ovk-c: var(--ovk-c1); } .ovk-chart-s2 { --ovk-c: var(--ovk-c2); } .ovk-chart-s3 { --ovk-c: var(--ovk-c3); }--.ovk-chart-s4 { --ovk-c: var(--ovk-c4); } .ovk-chart-s5 { --ovk-c: var(--ovk-c5); }----/* Steps: dots joined by a line; a phone says "Step 2 of 4 · Verify", wider screens name every step */--.ovk-steps { margin: 0 0 16px; color: var(--ovk-text); }--.ovk-steps-now { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; margin: 0 0 10px; font-size: .92rem; }--.ovk-steps-now span { color: var(--ovk-muted); font-size: .8rem; font-weight: 700; letter-spacing: .02em; }--.ovk-steps ol { display: flex; margin: 0; padding: 0; list-style: none; }--.ovk-steps li { position: relative; flex: 1 1 0; min-width: 0; display: flex; justify-content: center; }--/* The line between two dots stops short of both, so it never crosses a dot (or shows through a see-through panel). */--.ovk-steps li + li::before { content: ''; position: absolute; top: 21px; right: calc(50% + 20px); width: calc(100% - 40px); height: 2px; border-radius: 2px; background: var(--ovk-line); }--.ovk-steps li.is-done + li::before { background: var(--ovk-fill); }--.ovk-steps li > a, .ovk-steps-item { position: relative; display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: var(--ovk-tap); min-height: var(--ovk-tap); padding: 8px 4px 0; color: inherit; text-decoration: none; text-align: center; }--.ovk-steps-n { display: grid; place-items: center; width: 28px; height: 28px; box-sizing: border-box; border-radius: 50%; border: 2px solid var(--ovk-line); background: var(--ovk-panel); color: var(--ovk-muted); font-size: .8rem; font-weight: 800; font-variant-numeric: tabular-nums; }--.ovk-steps-n .ovk-i { width: 15px; height: 15px; stroke-width: 3; }--.ovk-steps .is-done .ovk-steps-n { border-color: var(--ovk-fill); background: var(--ovk-fill); color: var(--ovk-on-fill); }--.ovk-steps .is-current .ovk-steps-n { border-color: var(--ovk-accent); background: color-mix(in srgb, var(--ovk-accent) 16%, var(--ovk-panel)); color: var(--ovk-accent); box-shadow: 0 0 0 4px color-mix(in srgb, var(--ovk-accent) 14%, transparent); }--.ovk-steps li > a:hover .ovk-steps-n { filter: brightness(1.1); }--.ovk-steps li > a:focus-visible { outline: 2px solid var(--ovk-accent); outline-offset: 2px; border-radius: 12px; }--.ovk-steps-l { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }--.ovk-steps-l small { display: block; color: var(--ovk-muted); font-size: .76rem; font-weight: 500; }--@media (min-width: 600px) {-- .ovk-steps-now { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }-- .ovk-steps-l { position: static !important; width: auto; height: auto; overflow: visible; clip: auto; white-space: normal; font-size: .86rem; font-weight: 600; line-height: 1.3; }-- .ovk-steps .is-next .ovk-steps-l { color: var(--ovk-muted); }-- .ovk-steps .is-current .ovk-steps-l { color: var(--ovk-text); font-weight: 800; }--}----/* Tables: a real table on wide screens; under 600 px each row is a card that names its cells */--.ovk-table-wrap { margin: 0 0 14px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow-x: auto; color: var(--ovk-text); }--.ovk-table-wrap:focus-visible { outline: 2px solid var(--ovk-accent); outline-offset: 2px; }--.ovk-table { width: 100%; border-collapse: collapse; font-size: .92rem; line-height: 1.4; }--.ovk-table caption { padding: 12px 14px 6px; color: var(--ovk-muted); font-size: .74rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; text-align: left; }--.ovk-table thead th { padding: 10px 14px; color: var(--ovk-muted); font-size: .72rem; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; text-align: left; white-space: nowrap; border-bottom: 1px solid var(--ovk-line); }--.ovk-table td, .ovk-table tbody th { padding: 11px 14px; text-align: left; vertical-align: top; border-top: 1px solid var(--ovk-line); overflow-wrap: anywhere; }--.ovk-table tbody tr:first-child > * { border-top: 0; }--.ovk-table tbody th { font-weight: 700; }--.ovk-table .is-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }--.ovk-table a { color: var(--ovk-accent); text-decoration: none; }--.ovk-table a:hover { text-decoration: underline; }--.ovk-table tbody tr:hover > * { background: color-mix(in srgb, var(--ovk-panel-2) 55%, transparent); }--.ovk-table-empty { margin: 0 0 14px; padding: 18px 16px; border: 1px dashed var(--ovk-line); border-radius: var(--ovk-radius); color: var(--ovk-muted); text-align: center; }--@media (max-width: 599px) {-- .ovk-table--stack { overflow: visible; }-- .ovk-table--stack .ovk-table, .ovk-table--stack tbody, .ovk-table--stack tr, .ovk-table--stack td, .ovk-table--stack tbody th { display: block; }-- .ovk-table--stack caption { display: block; }-- .ovk-table--stack thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }-- .ovk-table--stack tbody tr { padding: 12px 14px; border-top: 1px solid var(--ovk-line); }-- .ovk-table--stack tbody tr:first-child { border-top: 0; }-- .ovk-table--stack tbody tr:hover > * { background: none; }-- .ovk-table--stack td > *, .ovk-table--stack td { min-width: 0; }-- .ovk-table--stack td, .ovk-table--stack tbody th { display: flex; justify-content: space-between; align-items: baseline; gap: 14px; padding: 3px 0; border: 0; text-align: right; }-- .ovk-table--stack td::before { content: attr(data-label); flex: none; max-width: 50%; color: var(--ovk-muted); font-size: .8rem; text-align: left; }-- .ovk-table--stack .is-primary { display: block; padding: 0 0 4px; font-size: 1rem; font-weight: 800; text-align: left; }-- .ovk-table--stack .is-primary::before { content: none; }-- .ovk-table--stack .is-hide-sm { display: none; }--}----/* Charts: the lines are an SVG stretched to the box; every label is real text at its real size */--.ovk-chart { overflow: clip; margin: 0 0 14px; padding: 14px 14px 10px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); color: var(--ovk-text); font-family: var(--ovk-font); }--.ovk-chart figcaption { display: grid; gap: 2px; margin: 0 0 8px; }--.ovk-chart figcaption strong { font-size: 1rem; font-weight: 800; }--.ovk-chart figcaption span { color: var(--ovk-muted); font-size: .84rem; }--.ovk-chart-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 0 0 10px; padding: 0; list-style: none; font-size: .84rem; color: var(--ovk-muted); }--.ovk-chart-legend li { display: inline-flex; align-items: center; gap: 6px; }--.ovk-chart-legend i { width: 14px; height: 3px; border-radius: 2px; background: var(--ovk-c); }--.ovk-chart-legend b { color: var(--ovk-text); font-weight: 800; font-variant-numeric: tabular-nums; }--.ovk-chart-yname { display: block; margin: 0 0 6px; color: var(--ovk-muted); font-size: .74rem; font-weight: 700; }--.ovk-chart-xname { margin: 2px 0 0; color: var(--ovk-muted); font-size: .74rem; font-weight: 700; text-align: right; }--.ovk-chart-plot { position: relative; height: var(--ovk-chart-h, 220px); margin: 8px 6px 26px var(--ovk-chart-g, 44px); outline: none; touch-action: pan-y; }--.ovk-chart-plot:focus-visible { box-shadow: 0 0 0 2px var(--ovk-accent); border-radius: 4px; }--.ovk-chart-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }--.ovk-chart-grid { position: absolute; left: 0; right: 0; height: 0; border-top: 1px solid color-mix(in srgb, var(--ovk-line) 70%, transparent); }--.ovk-chart-yt { position: absolute; right: calc(100% + 8px); transform: translateY(50%); color: var(--ovk-muted); font-size: .72rem; font-variant-numeric: tabular-nums; white-space: nowrap; line-height: 1; }--.ovk-chart-xt { position: absolute; top: calc(100% + 8px); transform: translateX(-50%); color: var(--ovk-muted); font-size: .72rem; white-space: nowrap; line-height: 1; }--.ovk-chart-line { fill: none; stroke: var(--ovk-c, var(--ovk-c1)); stroke-width: 2.25; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }--.ovk-chart-area { fill: color-mix(in srgb, var(--ovk-c, var(--ovk-c1)) 16%, transparent); stroke: none; }--.ovk-chart-front { fill: none; stroke: var(--ovk-c3); stroke-width: 1.5; stroke-dasharray: 5 4; vector-effect: non-scaling-stroke; }--.ovk-chart-rule { position: absolute; top: 0; bottom: 0; width: 1px; background: color-mix(in srgb, var(--ovk-text) 35%, transparent); pointer-events: none; }--.ovk-chart-rule[hidden], .ovk-chart-tip[hidden] { display: none; }--.ovk-chart-rule i { position: absolute; left: -5px; width: 10px; height: 10px; box-sizing: border-box; border-radius: 50%; border: 2px solid var(--ovk-panel); background: var(--ovk-c); transform: translateY(50%); }--.ovk-chart-tip, .ovk-chart-pop { position: absolute; z-index: 3; min-width: 96px; padding: 7px 10px; border: 1px solid var(--ovk-line); border-radius: 10px; background: var(--ovk-panel-2); color: var(--ovk-text); font-size: .8rem; line-height: 1.35; font-variant-numeric: tabular-nums; white-space: nowrap; box-shadow: 0 8px 24px rgba(0, 0, 0, .28); pointer-events: none; }--.ovk-chart-tip { top: 0; }--.ovk-chart-tip b, .ovk-chart-pop b { display: block; color: var(--ovk-muted); font-size: .74rem; font-weight: 700; }--.ovk-chart-tip span { display: flex; align-items: center; gap: 6px; }--.ovk-chart-tip span i { width: 8px; height: 8px; border-radius: 50%; background: var(--ovk-c); }--.ovk-chart-pop { display: none; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%); }--.ovk-chart-dot { position: absolute; z-index: 1; width: 12px; height: 12px; margin: 0 0 -6px -6px; box-sizing: border-box; border-radius: 50%; border: 2px solid var(--ovk-panel); background: color-mix(in srgb, var(--ovk-c1) 60%, var(--ovk-muted)); color: var(--ovk-text); outline: none; text-decoration: none; }--.ovk-chart-dot::after { content: ''; position: absolute; inset: -12px; }--.ovk-chart-dot.is-best { background: var(--ovk-c1); width: 14px; height: 14px; margin: 0 0 -7px -7px; }--.ovk-chart-dot:hover, .ovk-chart-dot:focus-visible { z-index: 4; box-shadow: 0 0 0 3px color-mix(in srgb, var(--ovk-accent) 40%, transparent); }--.ovk-chart-dot:hover .ovk-chart-pop, .ovk-chart-dot:focus-visible .ovk-chart-pop, .ovk-chart-bar:hover .ovk-chart-pop, .ovk-chart-bar:focus-visible .ovk-chart-pop { display: block; }--.ovk-chart-dot.is-right .ovk-chart-pop { left: auto; right: -6px; transform: none; }--.ovk-chart-name.is-hidden { visibility: hidden; }--.ovk-chart-name { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--ovk-text); font-size: .74rem; font-weight: 600; white-space: nowrap; pointer-events: none; text-shadow: 0 0 3px var(--ovk-panel), 0 0 3px var(--ovk-panel); }--.ovk-chart-dot.is-right .ovk-chart-name, .ovk-chart-dot.is-flip .ovk-chart-name { left: auto; right: 14px; }--.ovk-chart-bars { position: absolute; inset: 0; display: flex; align-items: flex-end; gap: clamp(1px, calc(12% / var(--n, 10)), 6px); }--.ovk-chart-bar { position: relative; flex: 1 1 0; min-width: 2px; height: var(--h, 0%); border-radius: 3px 3px 0 0; background: var(--ovk-c1); outline: none; }--.ovk-chart-bar:hover, .ovk-chart-bar:focus-visible { filter: brightness(1.15); box-shadow: 0 0 0 2px color-mix(in srgb, var(--ovk-accent) 45%, transparent); }--.ovk-chart-bar.is-none { background: transparent; }--.ovk-chart-blab { position: absolute; top: calc(100% + 8px); left: 50%; transform: translateX(-50%); color: var(--ovk-muted); font-size: .72rem; white-space: nowrap; line-height: 1; }--.ovk-chart-bar .ovk-chart-pop { bottom: calc(100% + 6px); }----/* Rankings: place, name, value ± interval, change, and a bar in proportion */--.ovk-rank { margin: 0 0 14px; padding: 0; list-style: none; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow: hidden; color: var(--ovk-text); }--.ovk-rank-row { position: relative; display: grid; grid-template-columns: 2.1em auto minmax(0, 1fr) auto auto; align-items: center; column-gap: 10px; row-gap: 6px; padding: 11px 14px 10px; border-top: 1px solid var(--ovk-line); }--.ovk-rank-row:first-child { border-top: 0; }--.ovk-rank-n { grid-column: 1; color: var(--ovk-muted); font-size: .95rem; font-weight: 800; font-variant-numeric: tabular-nums; text-align: center; }--.ovk-rank-row.is-top .ovk-rank-n { color: var(--ovk-accent); }--.ovk-rank-row > .ovk-mark { grid-column: 2; --ovk-mark: 34px; }--.ovk-rank-main { grid-column: 3; min-width: 0; display: grid; gap: 1px; }--.ovk-rank-name { display: flex; align-items: center; gap: 6px; min-width: 0; }--.ovk-rank-name > strong, .ovk-rank-link { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 700; color: var(--ovk-text); text-decoration: none; }--.ovk-rank-link::after { content: ''; position: absolute; inset: 0; }--.ovk-rank-row:has(.ovk-rank-link):hover { background: var(--ovk-panel-2); }--.ovk-rank-link:focus-visible { outline: none; }--.ovk-rank-row:has(.ovk-rank-link:focus-visible) { box-shadow: inset 0 0 0 2px var(--ovk-accent); }--.ovk-rank-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ovk-muted); font-size: .8rem; }--.ovk-rank-val { grid-column: 4; display: grid; justify-items: end; line-height: 1.2; }--.ovk-rank-val b { font-size: 1rem; font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; }--.ovk-rank-val b small { color: var(--ovk-muted); font-size: .76rem; font-weight: 600; }--.ovk-rank-val > small { color: var(--ovk-muted); font-size: .74rem; font-variant-numeric: tabular-nums; white-space: nowrap; }--.ovk-rank-delta { grid-column: 5; display: inline-flex; align-items: center; gap: 1px; min-width: 2.6em; justify-content: flex-end; font-size: .78rem; font-weight: 800; font-variant-numeric: tabular-nums; }--.ovk-rank-delta .ovk-i { width: 13px; height: 13px; stroke-width: 3; }--.ovk-rank-delta.is-up { color: var(--ovk-ok); } .ovk-rank-delta.is-down { color: var(--ovk-bad); } .ovk-rank-delta.is-new { color: var(--ovk-accent); text-transform: uppercase; font-size: .68rem; letter-spacing: .04em; }--.ovk-rank-bar { grid-column: 3 / -1; grid-row: 2; height: 4px; border-radius: 999px; background: color-mix(in srgb, var(--ovk-line) 60%, transparent); overflow: hidden; }--.ovk-rank-bar::after { content: ''; display: block; height: 100%; width: calc(var(--v, 0) * 100%); border-radius: inherit; background: color-mix(in srgb, var(--ovk-c1) 55%, transparent); }--.ovk-rank-row.is-top .ovk-rank-bar::after { background: var(--ovk-c1); }----/* Meters */--.ovk-meter { display: block; height: 8px; min-width: 56px; border-radius: 999px; background: color-mix(in srgb, var(--ovk-line) 70%, transparent); overflow: hidden; }--.ovk-meter > span { display: block; height: 100%; width: calc(var(--v, 0) * 100%); border-radius: inherit; background: var(--ovk-fill); }--.ovk-meter--ok > span { background: var(--ovk-ok); } .ovk-meter--warn > span { background: var(--ovk-warn); } .ovk-meter--bad > span { background: var(--ovk-bad); }----/* Drop zones: a real file input; drag, paste and the camera arrive with kit-web.js */--.ovk-drop { margin: 0 0 14px; color: var(--ovk-text); }--.ovk-drop-input { display: block; width: 100%; margin: 0 0 8px; color: var(--ovk-muted); font: inherit; }--.ovk-drop.is-js .ovk-drop-input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; overflow: hidden; }--.ovk-drop-zone { display: grid; justify-items: center; gap: 6px; padding: 28px 16px; border: 2px dashed var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); text-align: center; cursor: pointer; transition: border-color .12s, background .12s; }--.ovk-drop-zone > .ovk-i { width: 30px; height: 30px; color: var(--ovk-accent); }--.ovk-drop-zone strong { font-size: 1rem; font-weight: 800; }--.ovk-drop-zone small { color: var(--ovk-muted); font-size: .84rem; }--.ovk-drop-zone:hover, .ovk-drop.is-over .ovk-drop-zone { border-color: var(--ovk-accent); background: color-mix(in srgb, var(--ovk-accent) 8%, var(--ovk-panel)); }--.ovk-drop-input:focus-visible + .ovk-drop-zone { outline: 2px solid var(--ovk-accent); outline-offset: 2px; }--.ovk-drop-name:empty { display: none; }--.ovk-drop-name { display: grid; justify-items: center; gap: 6px; margin-top: 4px; font-size: .86rem; font-weight: 600; word-break: break-all; }--.ovk-drop-name img { max-width: 100%; max-height: 160px; border-radius: 10px; object-fit: contain; }--.ovk-drop.has-file .ovk-drop-zone { border-style: solid; border-color: color-mix(in srgb, var(--ovk-accent) 55%, var(--ovk-line)); }--.ovk-drop-cam { margin-top: 8px; }--.ovk-drop-cam[hidden] { display: none; }----/* Receipts: the result first, then one quiet line of what it took */--.ovk-receipt { margin: 0 0 14px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow: hidden; color: var(--ovk-text); }--.ovk-receipt--ok { border-color: color-mix(in srgb, var(--ovk-ok) 45%, var(--ovk-line)); }--.ovk-receipt--bad { border-color: color-mix(in srgb, var(--ovk-bad) 45%, var(--ovk-line)); }--.ovk-receipt-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 8px 0 14px; min-height: 44px; }--.ovk-receipt-head.ovk-receipt-head h2 { margin: 0; font-size: 1rem; font-weight: 800; }--.ovk-receipt-acts { display: inline-flex; gap: 2px; }--.ovk-receipt-body { padding: 10px 14px 14px; line-height: 1.5; overflow-wrap: anywhere; }--.ovk-receipt-body > :first-child { margin-top: 0; } .ovk-receipt-body > :last-child { margin-bottom: 0; }--.ovk-receipt-facts { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 0; padding: 10px 14px; border-top: 1px solid var(--ovk-line); background: color-mix(in srgb, var(--ovk-panel-2) 45%, transparent); font-size: .8rem; }--.ovk-receipt-facts > div { display: inline-flex; align-items: center; gap: 5px; }--.ovk-receipt-facts dt { color: var(--ovk-muted); }--.ovk-receipt-facts dd { display: inline-flex; align-items: center; gap: 6px; margin: 0; font-weight: 700; font-variant-numeric: tabular-nums; }--.ovk-receipt-facts .ovk-meter { width: 56px; height: 6px; }--@media (prefers-reduced-motion: reduce) { .ovk-drop-zone { transition: none; } }@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "4.36.2",++ "version": "4.37.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "4.36.2",++ "version": "4.37.0","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.36.2",++ "version": "4.37.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -62,6 +62,7 @@"./showcase": "./showcase.js","./kit": "./kit.js","./kit-web": "./kit-web.js",++ "./kit-data-web": "./kit-data-web.js","./appbar": "./appbar.js","./search": "./search.js","./agent-session": "./agent-session.js",@@@ -51,12 +51,14 @@ function page(body, title) {<title>${title}</title><script src="theme-loader.js"></script><link rel="stylesheet" href="kit.css">++<link rel="stylesheet" href="kit-data.css"><style>body { margin: 0; background: var(--bg-primary, #0b0f17); color: var(--text-primary, #e8ecf3); font-family: 'Inter', system-ui, sans-serif; }</style></head><body><div id="navbar-mount"></div><main class="ovk-page">${body}</main><script src="navbar.js"></script><script src="kit-web.js"></script>++<script src="kit-data-web.js"></script><script>OpenVibeNavbar.init({ service: 'chat', user: null, links: [{ label: 'Vibe', href: '/vibe', icon: 'fa-wand-magic-sparkles' }, { label: 'Chat', href: '/', icon: 'fa-comments' },{ label: 'Rooms', href: '/rooms', icon: 'fa-people-group' }, { label: 'Settings', href: '/kit', icon: 'fa-gear' },@@@ -103,7 +103,13 @@ const sc = require('../showcase');assert.ok(rcss.includes(':root[data-theme-mode="light"] .ov-room[style*="--room-light"]'), 'light themes take the light accent');assert.ok(rcss.includes('.ovk-rail-btn { display: none; }'), 'phones swipe rails');assert.ok(!/ovk-card|ovk-rail|ov-room/.test(kcss), 'kit.css stays the controls only: cards, rails and accents are room.css (4.29.1)');++ const dcss = fs.readFileSync(path.join(__dirname, '..', 'kit-data.css'), 'utf8');++ const kweb = fs.readFileSync(path.join(__dirname, '..', 'kit-web.js'), 'utf8');++ assert.ok(!/ovk-chart|ovk-table|ovk-steps|ovk-rank|ovk-drop|ovk-receipt|ovk-meter/.test(kcss), 'kit.css stays the controls: Kit 2 is kit-data.css (4.37.0)');++ assert.ok(['.ovk-chart', '.ovk-table', '.ovk-steps', '.ovk-rank', '.ovk-drop', '.ovk-receipt', '.ovk-meter'].every((c) => dcss.includes(c)), 'kit-data.css carries every Kit 2 piece');++ assert.ok(!/data-ovk-chart|data-ovk-drop/.test(kweb), 'kit-web.js carries no Kit 2 behaviour (kit-data-web.js does)');const files = require('../files');++ for (const f of ['kit-data.css', 'kit-data-web.js']) assert.ok(files.isBrowserFile(f), `${f} is served at /shared/`);for (const f of ['motion.js', 'rooms.js', 'room.css']) assert.ok(files.isBrowserFile(f), `${f} is served at /shared/`);const scss = fs.readFileSync(path.join(__dirname, '..', 'showcase.css'), 'utf8');assert.ok(rcss.includes('.sc-room-hero{') && rcss.includes('html.ov-motion [data-ov-reveal]'), 'sc.room()\'s look is in room.css (4.29.2)');
@@ -6,6 +6,16 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
@@ -6,6 +6,16 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## Unreleased
## Unreleased
+## 4.37.0 — 2026-10-29
+
+- **Kit 2 has its own files: `kit-data.css` and `kit-data-web.js`.** Its look and behaviour moved out of `kit.css` and
+ `kit-web.js`, which weigh what they did before Kit 2 again (kit.css 44 → 25.6 KB, kit-web.js 20 → 12.4 KB). A page that
+ draws steps, a table, a chart, a ranking, a meter, a drop zone or a receipt adds `kit-data.css` after `kit.css`, and
+ `kit-data-web.js` for chart readouts and drop zones. Why: Media's front page needed only cards and rails, and the
+ 44 KB kit.css took its stylesheets from 28 KB to 63 KB; every site in the pin round would have paid the same. The
+ builders (`kit.js`) are unchanged. Breaking only for a page that used Kit 2 on 4.36.x without the new files (Report,
+ Vision, Sources and Codes, updated alongside).
+
## 4.36.2 — 2026-10-29
## 4.36.2 — 2026-10-29
- **kit-web.js on boosted sites:** a page reached through boost.js (a swap of `#main`, no page load) now gets the kit's
- **kit-web.js on boosted sites:** a page reached through boost.js (a swap of `#main`, no page load) now gets the kit's
@@ -2,7 +2,7 @@
@@ -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.
> 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.36.2 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.37.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
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
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.36.2",
+ "version": "4.37.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"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)",
"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)",
@@ -254,6 +254,17 @@ built under the same rules as the rest of the kit: plain HTML that works without
@@ -254,6 +254,17 @@ built under the same rules as the rest of the kit: plain HTML that works without
tokens, and phone first. `node scripts/kit-gallery.js --shots <dir> --kit2` draws every piece at 320, 390, 768 and 1280
tokens, and phone first. `node scripts/kit-gallery.js --shots <dir> --kit2` draws every piece at 320, 390, 768 and 1280
px wide, light and dark.
px wide, light and dark.
+Kit 2 is two files of its own, served at `/shared/` like the rest: **`kit-data.css`** (load it after `kit.css`) and
+**`kit-data-web.js`** (the chart readout, scatter labels and drop zones; steps, tables, rankings, meters and receipts need
+no script). A page that draws none of them loads neither, so `kit.css` and `kit-web.js` weigh what they did before
+Kit 2 (split in 4.37.0).
+
+```html
+<link rel="stylesheet" href="${ovServe.url('kit.css')}">
+<link rel="stylesheet" href="${ovServe.url('kit-data.css')}">
+<script src="${ovServe.url('kit-data-web.js')}" defer></script>
+```
+
| Builder | What it is for | On a phone |
| Builder | What it is for | On a phone |
| --- | --- | --- |
| --- | --- | --- |
| `steps({ items, current })` | A flow of a few steps: opt in, verify, choose, live. Done steps link back; later ones never link ahead. | Dots joined by a line, and "Step 2 of 4 · Verify the domain" |
| `steps({ items, current })` | A flow of a few steps: opt in, verify, choose, live. Done steps link back; later ones never link ahead. | Dots joined by a line, and "Step 2 of 4 · Verify the domain" |
@@ -278,7 +289,7 @@ How the charts are drawn, so they read on a phone: the lines and areas are an SV
@@ -278,7 +289,7 @@ How the charts are drawn, so they read on a phone: the lines and areas are an SV
(`preserveAspectRatio="none"`, and the stroke keeps its width with `vector-effect`), while every tick label, legend,
(`preserveAspectRatio="none"`, and the stroke keeps its width with `vector-effect`), while every tick label, legend,
point and bar is real HTML placed by percent. Text stays its real size from 320 px to 1280 px, points and bars are
point and bar is real HTML placed by percent. Text stays its real size from 320 px to 1280 px, points and bars are
focusable and say their values, ticks fall on round numbers (1, 2 or 5 × 10ⁿ) and on round moments (hours, days, weeks,
focusable and say their values, ticks fall on round numbers (1, 2 or 5 × 10ⁿ) and on round moments (hours, days, weeks,
-months), and a table with the same numbers sits behind each chart for screen readers. kit-web.js adds the readout on
+months), and a table with the same numbers sits behind each chart for screen readers. kit-data-web.js adds the readout on
lines and areas (under the pointer, a tap, or ← → Home End) and hides a scatter label that would sit on one already
lines and areas (under the pointer, a tap, or ← → Home End) and hides a scatter label that would sit on one already
shown, best points first. Colours: `--ovk-c1` is the theme's accent, then `--ovk-c2` … `--ovk-c5` (cyan, amber,
shown, best points first. Colours: `--ovk-c1` is the theme's accent, then `--ovk-c2` … `--ovk-c5` (cyan, amber,
violet, green), with deeper tones in light themes.
violet, green), with deeper tones in light themes.
@@ -42,6 +42,7 @@ const BROWSER = Object.freeze([
@@ -42,6 +42,7 @@ const BROWSER = Object.freeze([
'appbar.js', // the phone app bar; navbar.js loads it only for a site that sets appBar
'appbar.js', // the phone app bar; navbar.js loads it only for a site that sets appBar
'kit.js', // the OpenVibe Kit's builders in the page (for pages that draw themselves; with kit-web.js)
'kit.js', // the OpenVibe Kit's builders in the page (for pages that draw themselves; with kit-web.js)
'kit-web.js', // the OpenVibe Kit's behaviour (copy, menus, Enter to send, confirms, sheets): all a server-rendered site loads
'kit-web.js', // the OpenVibe Kit's behaviour (copy, menus, Enter to send, confirms, sheets): all a server-rendered site loads
+ 'kit-data-web.js', // Kit 2's behaviour: chart readouts, scatter labels, drop zones (4.37.0; with kit-data.css)
'search.js', // search everything from the navbar's magnifier, / or Ctrl+K; navbar.js loads it on first use
'search.js', // search everything from the navbar's magnifier, / or Ctrl+K; navbar.js loads it on first use
'loading.js', // the one loading look: a poster-backed overlay with status and progress, and skeleton rows
'loading.js', // the one loading look: a poster-backed overlay with status and progress, and skeleton rows
'agent-session.js', // a coding agent's session as people read it: steps grouped, live output, diffs, helpers, hand-offs
'agent-session.js', // a coding agent's session as people read it: steps grouped, live output, diffs, helpers, hand-offs
@@ -58,6 +59,7 @@ const STYLES = Object.freeze([
@@ -58,6 +59,7 @@ const STYLES = Object.freeze([
'player.css', // player.js's controls
'player.css', // player.js's controls
'kit.css', // the OpenVibe Kit's look (kit.js / openvibe-shared/kit build the markup; docs/kit.md)
'kit.css', // the OpenVibe Kit's look (kit.js / openvibe-shared/kit build the markup; docs/kit.md)
'room.css', // kit.card, kit.rail and a room's accent (4.29.1; load after kit.css where cards, rails or sc.room() are)
'room.css', // kit.card, kit.rail and a room's accent (4.29.1; load after kit.css where cards, rails or sc.room() are)
+ 'kit-data.css', // Kit 2's look: steps, tables, charts, rankings, meters, drop zones, receipts (4.37.0; after kit.css)
'agent-session.css', // agent-session.js's look (4.32.0; the script links it from beside itself unless the page does)
'agent-session.css', // agent-session.js's look (4.32.0; the script links it from beside itself unless the page does)
]);
]);
const served = (name) => BROWSER.includes(String(name)) || STYLES.includes(String(name));
const served = (name) => BROWSER.includes(String(name)) || STYLES.includes(String(name));
new file mode 100644
new file mode 100644
@@ -0,0 +1,132 @@
@@ -0,0 +1,132 @@
+/**
+ * openvibe-shared/kit-data-web.js — Kit 2's behaviour in the page (split from kit-web.js in 4.37.0):
+ *
+ * charts .ovk-chart-plot[data-ovk-chart]: the values under the pointer, a tap, or ← → Home End
+ * scatter labels a label that would cover another, or cross the right edge, is hidden or moved
+ * drop zones [data-ovk-drop]: drag and drop, paste, the camera on phones, the file's name and picture
+ *
+ * It runs by itself when it loads, again on a boosted page swap (ov:boost:load), and for markup added later through
+ * OpenVibeKitData.enhance(container). Idempotent and safe on a page that uses none of it. Load it on a page that draws
+ * kit.chart or kit.drop (kit.steps, table, rank, meter and receipt need no script).
+ */
+(function (root) {
+ 'use strict';
+ function enhance(rootEl) {
+ if (typeof document === 'undefined') return;
+ const scope = rootEl || document;
+ const all = (sel) => Array.prototype.slice.call(scope.querySelectorAll ? scope.querySelectorAll(sel) : []);
+ const once = (el, key) => { if (el.dataset[key]) return false; el.dataset[key] = '1'; return true; };
+ // Charts (Kit 2): a rule, a dot per series and the values under the pointer or the arrow keys, on lines and areas.
+ all('.ovk-chart-plot[data-ovk-chart]').forEach((plot) => {
+ if (!once(plot, 'ovkChart')) return;
+ let data; try { data = JSON.parse(plot.getAttribute('data-ovk-chart')); } catch (e) { return; }
+ if (!data || !data.x || !data.x.length) return;
+ const rule = plot.querySelector('.ovk-chart-rule'), tip = plot.querySelector('.ovk-chart-tip');
+ if (!rule || !tip) return;
+ let at = -1;
+ const show = (i) => {
+ at = Math.max(0, Math.min(data.x.length - 1, i));
+ const [px, label] = data.x[at];
+ rule.style.left = `${px}%`; rule.hidden = false;
+ rule.innerHTML = data.s.map((s) => (s.v[at] ? `<i class="ovk-chart-s${s.c}" style="bottom:${s.v[at][0]}%"></i>` : '')).join('');
+ tip.textContent = '';
+ const b = document.createElement('b'); b.textContent = label; tip.appendChild(b);
+ data.s.forEach((s) => { if (!s.v[at]) return; const line = document.createElement('span'); line.className = `ovk-chart-s${s.c}`; line.innerHTML = '<i></i>'; line.appendChild(document.createTextNode(`${s.n ? `${s.n} ` : ''}${s.v[at][1]}`)); tip.appendChild(line); });
+ tip.hidden = false;
+ const w = plot.clientWidth, tw = tip.offsetWidth, x = (px / 100) * w;
+ tip.style.left = `${Math.max(0, Math.min(w - tw, x + 12 + tw > w ? x - tw - 12 : x + 12))}px`;
+ };
+ const hide = () => { rule.hidden = true; tip.hidden = true; };
+ const nearest = (clientX) => {
+ const r = plot.getBoundingClientRect(); const p = ((clientX - r.left) / r.width) * 100;
+ let best = 0; for (let i = 1; i < data.x.length; i++) if (Math.abs(data.x[i][0] - p) < Math.abs(data.x[best][0] - p)) best = i;
+ return best;
+ };
+ plot.addEventListener('pointermove', (e) => show(nearest(e.clientX)));
+ plot.addEventListener('pointerdown', (e) => show(nearest(e.clientX)));
+ plot.addEventListener('pointerleave', hide);
+ plot.addEventListener('blur', hide);
+ plot.addEventListener('focus', () => show(at < 0 ? data.x.length - 1 : at));
+ plot.addEventListener('keydown', (e) => {
+ const step = { ArrowLeft: -1, ArrowRight: 1, Home: -1e9, End: 1e9 }[e.key];
+ if (step == null) { if (e.key === 'Escape') hide(); return; }
+ e.preventDefault(); show((at < 0 ? data.x.length - 1 : at) + step);
+ });
+ });
+ // Scatter labels (Kit 2): a label that would sit on one already shown is hidden (best points first); its dot still says it.
+ all('.ovk-chart--scatter .ovk-chart-plot').forEach((plot) => {
+ const names = Array.prototype.slice.call(plot.querySelectorAll('.ovk-chart-name'));
+ if (names.length < 2) return;
+ const place = () => {
+ names.forEach((n) => { n.classList.remove('is-hidden'); n.parentNode.classList.remove('is-flip'); });
+ // A label that would run past the plot's right edge goes to the left of its dot.
+ const edge = plot.getBoundingClientRect().right + 4;
+ names.forEach((n) => { if (!n.parentNode.classList.contains('is-right') && n.getBoundingClientRect().right > edge) n.parentNode.classList.add('is-flip'); });
+ const order = names.slice().sort((a, b) => (b.parentNode.classList.contains('is-best') - a.parentNode.classList.contains('is-best')) || (parseFloat(b.parentNode.style.bottom) - parseFloat(a.parentNode.style.bottom)));
+ const kept = [];
+ order.forEach((n) => {
+ const r = n.getBoundingClientRect();
+ const hit = kept.some((k) => r.left < k.right + 4 && r.right + 4 > k.left && r.top < k.bottom + 2 && r.bottom + 2 > k.top);
+ if (hit) n.classList.add('is-hidden'); else kept.push(r);
+ });
+ };
+ if (!once(plot, 'ovkLabels')) return;
+ place();
+ if (typeof ResizeObserver !== 'undefined') new ResizeObserver(place).observe(plot);
+ });
+ // Drop zones (Kit 2): drag and drop, paste, the camera on phones, the chosen file's name and picture.
+ all('[data-ovk-drop]').forEach((box) => {
+ if (!once(box, 'ovkDrop')) return;
+ const input = box.querySelector('.ovk-drop-input'), zone = box.querySelector('.ovk-drop-zone'), name = box.querySelector('.ovk-drop-name');
+ if (!input || !zone) return;
+ box.classList.add('is-js');
+ let url = null;
+ const shown = () => {
+ const files = Array.prototype.slice.call(input.files || []);
+ box.classList.toggle('has-file', files.length > 0);
+ if (!name) return;
+ name.textContent = '';
+ if (url) { URL.revokeObjectURL(url); url = null; }
+ if (!files.length) return;
+ if (files.length === 1 && /^image\//.test(files[0].type) && window.URL) {
+ const img = document.createElement('img'); url = URL.createObjectURL(files[0]); img.src = url; img.alt = ''; name.appendChild(img);
+ }
+ name.appendChild(document.createTextNode(files.length === 1 ? files[0].name : `${files.length} files`));
+ };
+ const take = (list) => {
+ if (!list || !list.length || typeof DataTransfer === 'undefined') return;
+ const dt = new DataTransfer();
+ Array.prototype.slice.call(list, 0, input.multiple ? list.length : 1).forEach((f) => dt.items.add(f));
+ input.files = dt.files;
+ input.dispatchEvent(new Event('change', { bubbles: true }));
+ };
+ input.addEventListener('change', () => {
+ shown();
+ if (box.hasAttribute('data-ovk-auto') && input.files && input.files.length && input.form) (input.form.requestSubmit ? input.form.requestSubmit() : input.form.submit());
+ });
+ ['dragenter', 'dragover'].forEach((t) => zone.addEventListener(t, (e) => { e.preventDefault(); box.classList.add('is-over'); }));
+ ['dragleave', 'dragend'].forEach((t) => zone.addEventListener(t, () => box.classList.remove('is-over')));
+ zone.addEventListener('drop', (e) => { e.preventDefault(); box.classList.remove('is-over'); take(e.dataTransfer && e.dataTransfer.files); });
+ // Paste: only while this is the page's one drop zone, and never into a field someone is typing in.
+ document.addEventListener('paste', (e) => {
+ if (document.querySelectorAll('[data-ovk-drop]').length !== 1) return;
+ const t = e.target; if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;
+ const files = e.clipboardData && e.clipboardData.files; if (files && files.length) { e.preventDefault(); take(files); }
+ });
+ const cam = box.querySelector('.ovk-drop-cam'), camInput = box.querySelector('[data-ovk-drop-cam]');
+ if (cam && camInput && window.matchMedia && matchMedia('(pointer: coarse)').matches) {
+ cam.hidden = false;
+ camInput.addEventListener('change', () => { take(camInput.files); camInput.value = ''; });
+ }
+ });
+ }
+
+ const api = { enhance };
+ root.OpenVibeKitData = api;
+ if (typeof module === 'object' && module.exports) { module.exports = api; return; }
+ if (typeof document !== 'undefined') {
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => enhance(document));
+ else enhance(document);
+ document.addEventListener('ov:boost:load', () => enhance(document));
+ }
+})(typeof globalThis !== 'undefined' ? globalThis : this);
new file mode 100644
new file mode 100644
@@ -0,0 +1,175 @@
@@ -0,0 +1,175 @@
+/* openvibe-shared/kit-data.css — Kit 2's look: steps, tables, charts, rankings, meters, drop zones and receipts
+ * (kit.steps/table/chart/rank/meter/drop/receipt; docs/kit.md "Kit 2"). Load it after kit.css, whose tokens it uses,
+ * on a page that draws any of them; kit-data-web.js adds their behaviour. Split from kit.css in 4.37.0 so a page that
+ * uses none of them does not carry them (kit.css weighed 25 KB before Kit 2, 44 KB with it). */
+
+:root {
+ --ovk-c1: var(--ovk-accent); --ovk-c2: #22d3ee; --ovk-c3: #f59e0b; --ovk-c4: #a78bfa; --ovk-c5: #34d399;
+}
+:root[data-theme-mode="light"] { --ovk-c2: #0e7490; --ovk-c3: #b45309; --ovk-c4: #6d28d9; --ovk-c5: #047857; }
+.ovk-chart-s1 { --ovk-c: var(--ovk-c1); } .ovk-chart-s2 { --ovk-c: var(--ovk-c2); } .ovk-chart-s3 { --ovk-c: var(--ovk-c3); }
+.ovk-chart-s4 { --ovk-c: var(--ovk-c4); } .ovk-chart-s5 { --ovk-c: var(--ovk-c5); }
+
+/* Steps: dots joined by a line; a phone says "Step 2 of 4 · Verify", wider screens name every step */
+.ovk-steps { margin: 0 0 16px; color: var(--ovk-text); }
+.ovk-steps-now { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; margin: 0 0 10px; font-size: .92rem; }
+.ovk-steps-now span { color: var(--ovk-muted); font-size: .8rem; font-weight: 700; letter-spacing: .02em; }
+.ovk-steps ol { display: flex; margin: 0; padding: 0; list-style: none; }
+.ovk-steps li { position: relative; flex: 1 1 0; min-width: 0; display: flex; justify-content: center; }
+/* The line between two dots stops short of both, so it never crosses a dot (or shows through a see-through panel). */
+.ovk-steps li + li::before { content: ''; position: absolute; top: 21px; right: calc(50% + 20px); width: calc(100% - 40px); height: 2px; border-radius: 2px; background: var(--ovk-line); }
+.ovk-steps li.is-done + li::before { background: var(--ovk-fill); }
+.ovk-steps li > a, .ovk-steps-item { position: relative; display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: var(--ovk-tap); min-height: var(--ovk-tap); padding: 8px 4px 0; color: inherit; text-decoration: none; text-align: center; }
+.ovk-steps-n { display: grid; place-items: center; width: 28px; height: 28px; box-sizing: border-box; border-radius: 50%; border: 2px solid var(--ovk-line); background: var(--ovk-panel); color: var(--ovk-muted); font-size: .8rem; font-weight: 800; font-variant-numeric: tabular-nums; }
+.ovk-steps-n .ovk-i { width: 15px; height: 15px; stroke-width: 3; }
+.ovk-steps .is-done .ovk-steps-n { border-color: var(--ovk-fill); background: var(--ovk-fill); color: var(--ovk-on-fill); }
+.ovk-steps .is-current .ovk-steps-n { border-color: var(--ovk-accent); background: color-mix(in srgb, var(--ovk-accent) 16%, var(--ovk-panel)); color: var(--ovk-accent); box-shadow: 0 0 0 4px color-mix(in srgb, var(--ovk-accent) 14%, transparent); }
+.ovk-steps li > a:hover .ovk-steps-n { filter: brightness(1.1); }
+.ovk-steps li > a:focus-visible { outline: 2px solid var(--ovk-accent); outline-offset: 2px; border-radius: 12px; }
+.ovk-steps-l { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
+.ovk-steps-l small { display: block; color: var(--ovk-muted); font-size: .76rem; font-weight: 500; }
+@media (min-width: 600px) {
+ .ovk-steps-now { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
+ .ovk-steps-l { position: static !important; width: auto; height: auto; overflow: visible; clip: auto; white-space: normal; font-size: .86rem; font-weight: 600; line-height: 1.3; }
+ .ovk-steps .is-next .ovk-steps-l { color: var(--ovk-muted); }
+ .ovk-steps .is-current .ovk-steps-l { color: var(--ovk-text); font-weight: 800; }
+}
+
+/* Tables: a real table on wide screens; under 600 px each row is a card that names its cells */
+.ovk-table-wrap { margin: 0 0 14px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow-x: auto; color: var(--ovk-text); }
+.ovk-table-wrap:focus-visible { outline: 2px solid var(--ovk-accent); outline-offset: 2px; }
+.ovk-table { width: 100%; border-collapse: collapse; font-size: .92rem; line-height: 1.4; }
+.ovk-table caption { padding: 12px 14px 6px; color: var(--ovk-muted); font-size: .74rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; text-align: left; }
+.ovk-table thead th { padding: 10px 14px; color: var(--ovk-muted); font-size: .72rem; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; text-align: left; white-space: nowrap; border-bottom: 1px solid var(--ovk-line); }
+.ovk-table td, .ovk-table tbody th { padding: 11px 14px; text-align: left; vertical-align: top; border-top: 1px solid var(--ovk-line); overflow-wrap: anywhere; }
+.ovk-table tbody tr:first-child > * { border-top: 0; }
+.ovk-table tbody th { font-weight: 700; }
+.ovk-table .is-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
+.ovk-table a { color: var(--ovk-accent); text-decoration: none; }
+.ovk-table a:hover { text-decoration: underline; }
+.ovk-table tbody tr:hover > * { background: color-mix(in srgb, var(--ovk-panel-2) 55%, transparent); }
+.ovk-table-empty { margin: 0 0 14px; padding: 18px 16px; border: 1px dashed var(--ovk-line); border-radius: var(--ovk-radius); color: var(--ovk-muted); text-align: center; }
+@media (max-width: 599px) {
+ .ovk-table--stack { overflow: visible; }
+ .ovk-table--stack .ovk-table, .ovk-table--stack tbody, .ovk-table--stack tr, .ovk-table--stack td, .ovk-table--stack tbody th { display: block; }
+ .ovk-table--stack caption { display: block; }
+ .ovk-table--stack thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
+ .ovk-table--stack tbody tr { padding: 12px 14px; border-top: 1px solid var(--ovk-line); }
+ .ovk-table--stack tbody tr:first-child { border-top: 0; }
+ .ovk-table--stack tbody tr:hover > * { background: none; }
+ .ovk-table--stack td > *, .ovk-table--stack td { min-width: 0; }
+ .ovk-table--stack td, .ovk-table--stack tbody th { display: flex; justify-content: space-between; align-items: baseline; gap: 14px; padding: 3px 0; border: 0; text-align: right; }
+ .ovk-table--stack td::before { content: attr(data-label); flex: none; max-width: 50%; color: var(--ovk-muted); font-size: .8rem; text-align: left; }
+ .ovk-table--stack .is-primary { display: block; padding: 0 0 4px; font-size: 1rem; font-weight: 800; text-align: left; }
+ .ovk-table--stack .is-primary::before { content: none; }
+ .ovk-table--stack .is-hide-sm { display: none; }
+}
+
+/* Charts: the lines are an SVG stretched to the box; every label is real text at its real size */
+.ovk-chart { overflow: clip; margin: 0 0 14px; padding: 14px 14px 10px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); color: var(--ovk-text); font-family: var(--ovk-font); }
+.ovk-chart figcaption { display: grid; gap: 2px; margin: 0 0 8px; }
+.ovk-chart figcaption strong { font-size: 1rem; font-weight: 800; }
+.ovk-chart figcaption span { color: var(--ovk-muted); font-size: .84rem; }
+.ovk-chart-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 0 0 10px; padding: 0; list-style: none; font-size: .84rem; color: var(--ovk-muted); }
+.ovk-chart-legend li { display: inline-flex; align-items: center; gap: 6px; }
+.ovk-chart-legend i { width: 14px; height: 3px; border-radius: 2px; background: var(--ovk-c); }
+.ovk-chart-legend b { color: var(--ovk-text); font-weight: 800; font-variant-numeric: tabular-nums; }
+.ovk-chart-yname { display: block; margin: 0 0 6px; color: var(--ovk-muted); font-size: .74rem; font-weight: 700; }
+.ovk-chart-xname { margin: 2px 0 0; color: var(--ovk-muted); font-size: .74rem; font-weight: 700; text-align: right; }
+.ovk-chart-plot { position: relative; height: var(--ovk-chart-h, 220px); margin: 8px 6px 26px var(--ovk-chart-g, 44px); outline: none; touch-action: pan-y; }
+.ovk-chart-plot:focus-visible { box-shadow: 0 0 0 2px var(--ovk-accent); border-radius: 4px; }
+.ovk-chart-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
+.ovk-chart-grid { position: absolute; left: 0; right: 0; height: 0; border-top: 1px solid color-mix(in srgb, var(--ovk-line) 70%, transparent); }
+.ovk-chart-yt { position: absolute; right: calc(100% + 8px); transform: translateY(50%); color: var(--ovk-muted); font-size: .72rem; font-variant-numeric: tabular-nums; white-space: nowrap; line-height: 1; }
+.ovk-chart-xt { position: absolute; top: calc(100% + 8px); transform: translateX(-50%); color: var(--ovk-muted); font-size: .72rem; white-space: nowrap; line-height: 1; }
+.ovk-chart-line { fill: none; stroke: var(--ovk-c, var(--ovk-c1)); stroke-width: 2.25; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
+.ovk-chart-area { fill: color-mix(in srgb, var(--ovk-c, var(--ovk-c1)) 16%, transparent); stroke: none; }
+.ovk-chart-front { fill: none; stroke: var(--ovk-c3); stroke-width: 1.5; stroke-dasharray: 5 4; vector-effect: non-scaling-stroke; }
+.ovk-chart-rule { position: absolute; top: 0; bottom: 0; width: 1px; background: color-mix(in srgb, var(--ovk-text) 35%, transparent); pointer-events: none; }
+.ovk-chart-rule[hidden], .ovk-chart-tip[hidden] { display: none; }
+.ovk-chart-rule i { position: absolute; left: -5px; width: 10px; height: 10px; box-sizing: border-box; border-radius: 50%; border: 2px solid var(--ovk-panel); background: var(--ovk-c); transform: translateY(50%); }
+.ovk-chart-tip, .ovk-chart-pop { position: absolute; z-index: 3; min-width: 96px; padding: 7px 10px; border: 1px solid var(--ovk-line); border-radius: 10px; background: var(--ovk-panel-2); color: var(--ovk-text); font-size: .8rem; line-height: 1.35; font-variant-numeric: tabular-nums; white-space: nowrap; box-shadow: 0 8px 24px rgba(0, 0, 0, .28); pointer-events: none; }
+.ovk-chart-tip { top: 0; }
+.ovk-chart-tip b, .ovk-chart-pop b { display: block; color: var(--ovk-muted); font-size: .74rem; font-weight: 700; }
+.ovk-chart-tip span { display: flex; align-items: center; gap: 6px; }
+.ovk-chart-tip span i { width: 8px; height: 8px; border-radius: 50%; background: var(--ovk-c); }
+.ovk-chart-pop { display: none; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%); }
+.ovk-chart-dot { position: absolute; z-index: 1; width: 12px; height: 12px; margin: 0 0 -6px -6px; box-sizing: border-box; border-radius: 50%; border: 2px solid var(--ovk-panel); background: color-mix(in srgb, var(--ovk-c1) 60%, var(--ovk-muted)); color: var(--ovk-text); outline: none; text-decoration: none; }
+.ovk-chart-dot::after { content: ''; position: absolute; inset: -12px; }
+.ovk-chart-dot.is-best { background: var(--ovk-c1); width: 14px; height: 14px; margin: 0 0 -7px -7px; }
+.ovk-chart-dot:hover, .ovk-chart-dot:focus-visible { z-index: 4; box-shadow: 0 0 0 3px color-mix(in srgb, var(--ovk-accent) 40%, transparent); }
+.ovk-chart-dot:hover .ovk-chart-pop, .ovk-chart-dot:focus-visible .ovk-chart-pop, .ovk-chart-bar:hover .ovk-chart-pop, .ovk-chart-bar:focus-visible .ovk-chart-pop { display: block; }
+.ovk-chart-dot.is-right .ovk-chart-pop { left: auto; right: -6px; transform: none; }
+.ovk-chart-name.is-hidden { visibility: hidden; }
+.ovk-chart-name { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--ovk-text); font-size: .74rem; font-weight: 600; white-space: nowrap; pointer-events: none; text-shadow: 0 0 3px var(--ovk-panel), 0 0 3px var(--ovk-panel); }
+.ovk-chart-dot.is-right .ovk-chart-name, .ovk-chart-dot.is-flip .ovk-chart-name { left: auto; right: 14px; }
+.ovk-chart-bars { position: absolute; inset: 0; display: flex; align-items: flex-end; gap: clamp(1px, calc(12% / var(--n, 10)), 6px); }
+.ovk-chart-bar { position: relative; flex: 1 1 0; min-width: 2px; height: var(--h, 0%); border-radius: 3px 3px 0 0; background: var(--ovk-c1); outline: none; }
+.ovk-chart-bar:hover, .ovk-chart-bar:focus-visible { filter: brightness(1.15); box-shadow: 0 0 0 2px color-mix(in srgb, var(--ovk-accent) 45%, transparent); }
+.ovk-chart-bar.is-none { background: transparent; }
+.ovk-chart-blab { position: absolute; top: calc(100% + 8px); left: 50%; transform: translateX(-50%); color: var(--ovk-muted); font-size: .72rem; white-space: nowrap; line-height: 1; }
+.ovk-chart-bar .ovk-chart-pop { bottom: calc(100% + 6px); }
+
+/* Rankings: place, name, value ± interval, change, and a bar in proportion */
+.ovk-rank { margin: 0 0 14px; padding: 0; list-style: none; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow: hidden; color: var(--ovk-text); }
+.ovk-rank-row { position: relative; display: grid; grid-template-columns: 2.1em auto minmax(0, 1fr) auto auto; align-items: center; column-gap: 10px; row-gap: 6px; padding: 11px 14px 10px; border-top: 1px solid var(--ovk-line); }
+.ovk-rank-row:first-child { border-top: 0; }
+.ovk-rank-n { grid-column: 1; color: var(--ovk-muted); font-size: .95rem; font-weight: 800; font-variant-numeric: tabular-nums; text-align: center; }
+.ovk-rank-row.is-top .ovk-rank-n { color: var(--ovk-accent); }
+.ovk-rank-row > .ovk-mark { grid-column: 2; --ovk-mark: 34px; }
+.ovk-rank-main { grid-column: 3; min-width: 0; display: grid; gap: 1px; }
+.ovk-rank-name { display: flex; align-items: center; gap: 6px; min-width: 0; }
+.ovk-rank-name > strong, .ovk-rank-link { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 700; color: var(--ovk-text); text-decoration: none; }
+.ovk-rank-link::after { content: ''; position: absolute; inset: 0; }
+.ovk-rank-row:has(.ovk-rank-link):hover { background: var(--ovk-panel-2); }
+.ovk-rank-link:focus-visible { outline: none; }
+.ovk-rank-row:has(.ovk-rank-link:focus-visible) { box-shadow: inset 0 0 0 2px var(--ovk-accent); }
+.ovk-rank-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ovk-muted); font-size: .8rem; }
+.ovk-rank-val { grid-column: 4; display: grid; justify-items: end; line-height: 1.2; }
+.ovk-rank-val b { font-size: 1rem; font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; }
+.ovk-rank-val b small { color: var(--ovk-muted); font-size: .76rem; font-weight: 600; }
+.ovk-rank-val > small { color: var(--ovk-muted); font-size: .74rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
+.ovk-rank-delta { grid-column: 5; display: inline-flex; align-items: center; gap: 1px; min-width: 2.6em; justify-content: flex-end; font-size: .78rem; font-weight: 800; font-variant-numeric: tabular-nums; }
+.ovk-rank-delta .ovk-i { width: 13px; height: 13px; stroke-width: 3; }
+.ovk-rank-delta.is-up { color: var(--ovk-ok); } .ovk-rank-delta.is-down { color: var(--ovk-bad); } .ovk-rank-delta.is-new { color: var(--ovk-accent); text-transform: uppercase; font-size: .68rem; letter-spacing: .04em; }
+.ovk-rank-bar { grid-column: 3 / -1; grid-row: 2; height: 4px; border-radius: 999px; background: color-mix(in srgb, var(--ovk-line) 60%, transparent); overflow: hidden; }
+.ovk-rank-bar::after { content: ''; display: block; height: 100%; width: calc(var(--v, 0) * 100%); border-radius: inherit; background: color-mix(in srgb, var(--ovk-c1) 55%, transparent); }
+.ovk-rank-row.is-top .ovk-rank-bar::after { background: var(--ovk-c1); }
+
+/* Meters */
+.ovk-meter { display: block; height: 8px; min-width: 56px; border-radius: 999px; background: color-mix(in srgb, var(--ovk-line) 70%, transparent); overflow: hidden; }
+.ovk-meter > span { display: block; height: 100%; width: calc(var(--v, 0) * 100%); border-radius: inherit; background: var(--ovk-fill); }
+.ovk-meter--ok > span { background: var(--ovk-ok); } .ovk-meter--warn > span { background: var(--ovk-warn); } .ovk-meter--bad > span { background: var(--ovk-bad); }
+
+/* Drop zones: a real file input; drag, paste and the camera arrive with kit-web.js */
+.ovk-drop { margin: 0 0 14px; color: var(--ovk-text); }
+.ovk-drop-input { display: block; width: 100%; margin: 0 0 8px; color: var(--ovk-muted); font: inherit; }
+.ovk-drop.is-js .ovk-drop-input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; overflow: hidden; }
+.ovk-drop-zone { display: grid; justify-items: center; gap: 6px; padding: 28px 16px; border: 2px dashed var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); text-align: center; cursor: pointer; transition: border-color .12s, background .12s; }
+.ovk-drop-zone > .ovk-i { width: 30px; height: 30px; color: var(--ovk-accent); }
+.ovk-drop-zone strong { font-size: 1rem; font-weight: 800; }
+.ovk-drop-zone small { color: var(--ovk-muted); font-size: .84rem; }
+.ovk-drop-zone:hover, .ovk-drop.is-over .ovk-drop-zone { border-color: var(--ovk-accent); background: color-mix(in srgb, var(--ovk-accent) 8%, var(--ovk-panel)); }
+.ovk-drop-input:focus-visible + .ovk-drop-zone { outline: 2px solid var(--ovk-accent); outline-offset: 2px; }
+.ovk-drop-name:empty { display: none; }
+.ovk-drop-name { display: grid; justify-items: center; gap: 6px; margin-top: 4px; font-size: .86rem; font-weight: 600; word-break: break-all; }
+.ovk-drop-name img { max-width: 100%; max-height: 160px; border-radius: 10px; object-fit: contain; }
+.ovk-drop.has-file .ovk-drop-zone { border-style: solid; border-color: color-mix(in srgb, var(--ovk-accent) 55%, var(--ovk-line)); }
+.ovk-drop-cam { margin-top: 8px; }
+.ovk-drop-cam[hidden] { display: none; }
+
+/* Receipts: the result first, then one quiet line of what it took */
+.ovk-receipt { margin: 0 0 14px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow: hidden; color: var(--ovk-text); }
+.ovk-receipt--ok { border-color: color-mix(in srgb, var(--ovk-ok) 45%, var(--ovk-line)); }
+.ovk-receipt--bad { border-color: color-mix(in srgb, var(--ovk-bad) 45%, var(--ovk-line)); }
+.ovk-receipt-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 8px 0 14px; min-height: 44px; }
+.ovk-receipt-head.ovk-receipt-head h2 { margin: 0; font-size: 1rem; font-weight: 800; }
+.ovk-receipt-acts { display: inline-flex; gap: 2px; }
+.ovk-receipt-body { padding: 10px 14px 14px; line-height: 1.5; overflow-wrap: anywhere; }
+.ovk-receipt-body > :first-child { margin-top: 0; } .ovk-receipt-body > :last-child { margin-bottom: 0; }
+.ovk-receipt-facts { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 0; padding: 10px 14px; border-top: 1px solid var(--ovk-line); background: color-mix(in srgb, var(--ovk-panel-2) 45%, transparent); font-size: .8rem; }
+.ovk-receipt-facts > div { display: inline-flex; align-items: center; gap: 5px; }
+.ovk-receipt-facts dt { color: var(--ovk-muted); }
+.ovk-receipt-facts dd { display: inline-flex; align-items: center; gap: 6px; margin: 0; font-weight: 700; font-variant-numeric: tabular-nums; }
+.ovk-receipt-facts .ovk-meter { width: 56px; height: 6px; }
+@media (prefers-reduced-motion: reduce) { .ovk-drop-zone { transition: none; } }
@@ -15,8 +15,6 @@
@@ -15,8 +15,6 @@
* share [data-ovk-share]: the system share sheet, else the link copied (the menu item says "Link copied")
* share [data-ovk-share]: the system share sheet, else the link copied (the menu item says "Link copied")
* card previews .ovk-card video[data-src]: a muted loop while the card is hovered or focused (pointers only, never
* card previews .ovk-card video[data-src]: a muted loop while the card is hovered or focused (pointers only, never
* under reduced motion; the file loads only then)
* under reduced motion; the file loads only then)
- * charts .ovk-chart-plot[data-ovk-chart]: the values under the pointer, a tap, or ← → Home End (Kit 2)
- * drop zones [data-ovk-drop]: drag and drop, paste, the camera on phones, the file's name and picture (Kit 2)
*
*
* Idempotent and safe on a page that uses none of it. docs/kit.md describes the pieces.
* Idempotent and safe on a page that uses none of it. docs/kit.md describes the pieces.
*/
*/
@@ -159,109 +157,6 @@
@@ -159,109 +157,6 @@
}, () => {});
}, () => {});
});
});
});
});
- // Charts (Kit 2): a rule, a dot per series and the values under the pointer or the arrow keys, on lines and areas.
- all('.ovk-chart-plot[data-ovk-chart]').forEach((plot) => {
- if (!once(plot, 'ovkChart')) return;
- let data; try { data = JSON.parse(plot.getAttribute('data-ovk-chart')); } catch (e) { return; }
- if (!data || !data.x || !data.x.length) return;
- const rule = plot.querySelector('.ovk-chart-rule'), tip = plot.querySelector('.ovk-chart-tip');
- if (!rule || !tip) return;
- let at = -1;
- const show = (i) => {
- at = Math.max(0, Math.min(data.x.length - 1, i));
- const [px, label] = data.x[at];
- rule.style.left = `${px}%`; rule.hidden = false;
- rule.innerHTML = data.s.map((s) => (s.v[at] ? `<i class="ovk-chart-s${s.c}" style="bottom:${s.v[at][0]}%"></i>` : '')).join('');
- tip.textContent = '';
- const b = document.createElement('b'); b.textContent = label; tip.appendChild(b);
- data.s.forEach((s) => { if (!s.v[at]) return; const line = document.createElement('span'); line.className = `ovk-chart-s${s.c}`; line.innerHTML = '<i></i>'; line.appendChild(document.createTextNode(`${s.n ? `${s.n} ` : ''}${s.v[at][1]}`)); tip.appendChild(line); });
- tip.hidden = false;
- const w = plot.clientWidth, tw = tip.offsetWidth, x = (px / 100) * w;
- tip.style.left = `${Math.max(0, Math.min(w - tw, x + 12 + tw > w ? x - tw - 12 : x + 12))}px`;
- };
- const hide = () => { rule.hidden = true; tip.hidden = true; };
- const nearest = (clientX) => {
- const r = plot.getBoundingClientRect(); const p = ((clientX - r.left) / r.width) * 100;
- let best = 0; for (let i = 1; i < data.x.length; i++) if (Math.abs(data.x[i][0] - p) < Math.abs(data.x[best][0] - p)) best = i;
- return best;
- };
- plot.addEventListener('pointermove', (e) => show(nearest(e.clientX)));
- plot.addEventListener('pointerdown', (e) => show(nearest(e.clientX)));
- plot.addEventListener('pointerleave', hide);
- plot.addEventListener('blur', hide);
- plot.addEventListener('focus', () => show(at < 0 ? data.x.length - 1 : at));
- plot.addEventListener('keydown', (e) => {
- const step = { ArrowLeft: -1, ArrowRight: 1, Home: -1e9, End: 1e9 }[e.key];
- if (step == null) { if (e.key === 'Escape') hide(); return; }
- e.preventDefault(); show((at < 0 ? data.x.length - 1 : at) + step);
- });
- });
- // Scatter labels (Kit 2): a label that would sit on one already shown is hidden (best points first); its dot still says it.
- all('.ovk-chart--scatter .ovk-chart-plot').forEach((plot) => {
- const names = Array.prototype.slice.call(plot.querySelectorAll('.ovk-chart-name'));
- if (names.length < 2) return;
- const place = () => {
- names.forEach((n) => { n.classList.remove('is-hidden'); n.parentNode.classList.remove('is-flip'); });
- // A label that would run past the plot's right edge goes to the left of its dot.
- const edge = plot.getBoundingClientRect().right + 4;
- names.forEach((n) => { if (!n.parentNode.classList.contains('is-right') && n.getBoundingClientRect().right > edge) n.parentNode.classList.add('is-flip'); });
- const order = names.slice().sort((a, b) => (b.parentNode.classList.contains('is-best') - a.parentNode.classList.contains('is-best')) || (parseFloat(b.parentNode.style.bottom) - parseFloat(a.parentNode.style.bottom)));
- const kept = [];
- order.forEach((n) => {
- const r = n.getBoundingClientRect();
- const hit = kept.some((k) => r.left < k.right + 4 && r.right + 4 > k.left && r.top < k.bottom + 2 && r.bottom + 2 > k.top);
- if (hit) n.classList.add('is-hidden'); else kept.push(r);
- });
- };
- if (!once(plot, 'ovkLabels')) return;
- place();
- if (typeof ResizeObserver !== 'undefined') new ResizeObserver(place).observe(plot);
- });
- // Drop zones (Kit 2): drag and drop, paste, the camera on phones, the chosen file's name and picture.
- all('[data-ovk-drop]').forEach((box) => {
- if (!once(box, 'ovkDrop')) return;
- const input = box.querySelector('.ovk-drop-input'), zone = box.querySelector('.ovk-drop-zone'), name = box.querySelector('.ovk-drop-name');
- if (!input || !zone) return;
- box.classList.add('is-js');
- let url = null;
- const shown = () => {
- const files = Array.prototype.slice.call(input.files || []);
- box.classList.toggle('has-file', files.length > 0);
- if (!name) return;
- name.textContent = '';
- if (url) { URL.revokeObjectURL(url); url = null; }
- if (!files.length) return;
- if (files.length === 1 && /^image\//.test(files[0].type) && window.URL) {
- const img = document.createElement('img'); url = URL.createObjectURL(files[0]); img.src = url; img.alt = ''; name.appendChild(img);
- }
- name.appendChild(document.createTextNode(files.length === 1 ? files[0].name : `${files.length} files`));
- };
- const take = (list) => {
- if (!list || !list.length || typeof DataTransfer === 'undefined') return;
- const dt = new DataTransfer();
- Array.prototype.slice.call(list, 0, input.multiple ? list.length : 1).forEach((f) => dt.items.add(f));
- input.files = dt.files;
- input.dispatchEvent(new Event('change', { bubbles: true }));
- };
- input.addEventListener('change', () => {
- shown();
- if (box.hasAttribute('data-ovk-auto') && input.files && input.files.length && input.form) (input.form.requestSubmit ? input.form.requestSubmit() : input.form.submit());
- });
- ['dragenter', 'dragover'].forEach((t) => zone.addEventListener(t, (e) => { e.preventDefault(); box.classList.add('is-over'); }));
- ['dragleave', 'dragend'].forEach((t) => zone.addEventListener(t, () => box.classList.remove('is-over')));
- zone.addEventListener('drop', (e) => { e.preventDefault(); box.classList.remove('is-over'); take(e.dataTransfer && e.dataTransfer.files); });
- // Paste: only while this is the page's one drop zone, and never into a field someone is typing in.
- document.addEventListener('paste', (e) => {
- if (document.querySelectorAll('[data-ovk-drop]').length !== 1) return;
- const t = e.target; if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;
- const files = e.clipboardData && e.clipboardData.files; if (files && files.length) { e.preventDefault(); take(files); }
- });
- const cam = box.querySelector('.ovk-drop-cam'), camInput = box.querySelector('[data-ovk-drop-cam]');
- if (cam && camInput && window.matchMedia && matchMedia('(pointer: coarse)').matches) {
- cam.hidden = false;
- camInput.addEventListener('change', () => { take(camInput.files); camInput.value = ''; });
- }
- });
// Card previews: a muted loop while hovered or focused, on computers only, never under reduced motion.
// Card previews: a muted loop while hovered or focused, on computers only, never under reduced motion.
if (fine) all('.ovk-card').forEach((c) => {
if (fine) all('.ovk-card').forEach((c) => {
const v = c.querySelector('video.ovk-card-video[data-src]');
const v = c.querySelector('video.ovk-card-video[data-src]');
@@ -256,175 +256,3 @@ div.ovk-set-row { cursor: default; }
@@ -256,175 +256,3 @@ div.ovk-set-row { cursor: default; }
}
}
@media (prefers-reduced-motion: reduce) { .ovk-switch, .ovk-switch::after, .ovk-fold > summary > .ovk-i, .ovk-head, .ovk-btn { transition: none; } }
@media (prefers-reduced-motion: reduce) { .ovk-switch, .ovk-switch::after, .ovk-fold > summary > .ovk-i, .ovk-head, .ovk-btn { transition: none; } }
@media print { .ovk-head, .ovk-dock, .ovk-save { position: static; background: none; -webkit-backdrop-filter: none; backdrop-filter: none; } }
@media print { .ovk-head, .ovk-dock, .ovk-save { position: static; background: none; -webkit-backdrop-filter: none; backdrop-filter: none; } }
-
-/* ── Kit 2 (4.36.0): steps, tables, charts, rankings, meters, drop zones, receipts ── */
-:root {
- --ovk-c1: var(--ovk-accent); --ovk-c2: #22d3ee; --ovk-c3: #f59e0b; --ovk-c4: #a78bfa; --ovk-c5: #34d399;
-}
-:root[data-theme-mode="light"] { --ovk-c2: #0e7490; --ovk-c3: #b45309; --ovk-c4: #6d28d9; --ovk-c5: #047857; }
-.ovk-chart-s1 { --ovk-c: var(--ovk-c1); } .ovk-chart-s2 { --ovk-c: var(--ovk-c2); } .ovk-chart-s3 { --ovk-c: var(--ovk-c3); }
-.ovk-chart-s4 { --ovk-c: var(--ovk-c4); } .ovk-chart-s5 { --ovk-c: var(--ovk-c5); }
-
-/* Steps: dots joined by a line; a phone says "Step 2 of 4 · Verify", wider screens name every step */
-.ovk-steps { margin: 0 0 16px; color: var(--ovk-text); }
-.ovk-steps-now { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; margin: 0 0 10px; font-size: .92rem; }
-.ovk-steps-now span { color: var(--ovk-muted); font-size: .8rem; font-weight: 700; letter-spacing: .02em; }
-.ovk-steps ol { display: flex; margin: 0; padding: 0; list-style: none; }
-.ovk-steps li { position: relative; flex: 1 1 0; min-width: 0; display: flex; justify-content: center; }
-/* The line between two dots stops short of both, so it never crosses a dot (or shows through a see-through panel). */
-.ovk-steps li + li::before { content: ''; position: absolute; top: 21px; right: calc(50% + 20px); width: calc(100% - 40px); height: 2px; border-radius: 2px; background: var(--ovk-line); }
-.ovk-steps li.is-done + li::before { background: var(--ovk-fill); }
-.ovk-steps li > a, .ovk-steps-item { position: relative; display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: var(--ovk-tap); min-height: var(--ovk-tap); padding: 8px 4px 0; color: inherit; text-decoration: none; text-align: center; }
-.ovk-steps-n { display: grid; place-items: center; width: 28px; height: 28px; box-sizing: border-box; border-radius: 50%; border: 2px solid var(--ovk-line); background: var(--ovk-panel); color: var(--ovk-muted); font-size: .8rem; font-weight: 800; font-variant-numeric: tabular-nums; }
-.ovk-steps-n .ovk-i { width: 15px; height: 15px; stroke-width: 3; }
-.ovk-steps .is-done .ovk-steps-n { border-color: var(--ovk-fill); background: var(--ovk-fill); color: var(--ovk-on-fill); }
-.ovk-steps .is-current .ovk-steps-n { border-color: var(--ovk-accent); background: color-mix(in srgb, var(--ovk-accent) 16%, var(--ovk-panel)); color: var(--ovk-accent); box-shadow: 0 0 0 4px color-mix(in srgb, var(--ovk-accent) 14%, transparent); }
-.ovk-steps li > a:hover .ovk-steps-n { filter: brightness(1.1); }
-.ovk-steps li > a:focus-visible { outline: 2px solid var(--ovk-accent); outline-offset: 2px; border-radius: 12px; }
-.ovk-steps-l { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
-.ovk-steps-l small { display: block; color: var(--ovk-muted); font-size: .76rem; font-weight: 500; }
-@media (min-width: 600px) {
- .ovk-steps-now { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
- .ovk-steps-l { position: static !important; width: auto; height: auto; overflow: visible; clip: auto; white-space: normal; font-size: .86rem; font-weight: 600; line-height: 1.3; }
- .ovk-steps .is-next .ovk-steps-l { color: var(--ovk-muted); }
- .ovk-steps .is-current .ovk-steps-l { color: var(--ovk-text); font-weight: 800; }
-}
-
-/* Tables: a real table on wide screens; under 600 px each row is a card that names its cells */
-.ovk-table-wrap { margin: 0 0 14px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow-x: auto; color: var(--ovk-text); }
-.ovk-table-wrap:focus-visible { outline: 2px solid var(--ovk-accent); outline-offset: 2px; }
-.ovk-table { width: 100%; border-collapse: collapse; font-size: .92rem; line-height: 1.4; }
-.ovk-table caption { padding: 12px 14px 6px; color: var(--ovk-muted); font-size: .74rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; text-align: left; }
-.ovk-table thead th { padding: 10px 14px; color: var(--ovk-muted); font-size: .72rem; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; text-align: left; white-space: nowrap; border-bottom: 1px solid var(--ovk-line); }
-.ovk-table td, .ovk-table tbody th { padding: 11px 14px; text-align: left; vertical-align: top; border-top: 1px solid var(--ovk-line); overflow-wrap: anywhere; }
-.ovk-table tbody tr:first-child > * { border-top: 0; }
-.ovk-table tbody th { font-weight: 700; }
-.ovk-table .is-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
-.ovk-table a { color: var(--ovk-accent); text-decoration: none; }
-.ovk-table a:hover { text-decoration: underline; }
-.ovk-table tbody tr:hover > * { background: color-mix(in srgb, var(--ovk-panel-2) 55%, transparent); }
-.ovk-table-empty { margin: 0 0 14px; padding: 18px 16px; border: 1px dashed var(--ovk-line); border-radius: var(--ovk-radius); color: var(--ovk-muted); text-align: center; }
-@media (max-width: 599px) {
- .ovk-table--stack { overflow: visible; }
- .ovk-table--stack .ovk-table, .ovk-table--stack tbody, .ovk-table--stack tr, .ovk-table--stack td, .ovk-table--stack tbody th { display: block; }
- .ovk-table--stack caption { display: block; }
- .ovk-table--stack thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
- .ovk-table--stack tbody tr { padding: 12px 14px; border-top: 1px solid var(--ovk-line); }
- .ovk-table--stack tbody tr:first-child { border-top: 0; }
- .ovk-table--stack tbody tr:hover > * { background: none; }
- .ovk-table--stack td > *, .ovk-table--stack td { min-width: 0; }
- .ovk-table--stack td, .ovk-table--stack tbody th { display: flex; justify-content: space-between; align-items: baseline; gap: 14px; padding: 3px 0; border: 0; text-align: right; }
- .ovk-table--stack td::before { content: attr(data-label); flex: none; max-width: 50%; color: var(--ovk-muted); font-size: .8rem; text-align: left; }
- .ovk-table--stack .is-primary { display: block; padding: 0 0 4px; font-size: 1rem; font-weight: 800; text-align: left; }
- .ovk-table--stack .is-primary::before { content: none; }
- .ovk-table--stack .is-hide-sm { display: none; }
-}
-
-/* Charts: the lines are an SVG stretched to the box; every label is real text at its real size */
-.ovk-chart { overflow: clip; margin: 0 0 14px; padding: 14px 14px 10px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); color: var(--ovk-text); font-family: var(--ovk-font); }
-.ovk-chart figcaption { display: grid; gap: 2px; margin: 0 0 8px; }
-.ovk-chart figcaption strong { font-size: 1rem; font-weight: 800; }
-.ovk-chart figcaption span { color: var(--ovk-muted); font-size: .84rem; }
-.ovk-chart-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 0 0 10px; padding: 0; list-style: none; font-size: .84rem; color: var(--ovk-muted); }
-.ovk-chart-legend li { display: inline-flex; align-items: center; gap: 6px; }
-.ovk-chart-legend i { width: 14px; height: 3px; border-radius: 2px; background: var(--ovk-c); }
-.ovk-chart-legend b { color: var(--ovk-text); font-weight: 800; font-variant-numeric: tabular-nums; }
-.ovk-chart-yname { display: block; margin: 0 0 6px; color: var(--ovk-muted); font-size: .74rem; font-weight: 700; }
-.ovk-chart-xname { margin: 2px 0 0; color: var(--ovk-muted); font-size: .74rem; font-weight: 700; text-align: right; }
-.ovk-chart-plot { position: relative; height: var(--ovk-chart-h, 220px); margin: 8px 6px 26px var(--ovk-chart-g, 44px); outline: none; touch-action: pan-y; }
-.ovk-chart-plot:focus-visible { box-shadow: 0 0 0 2px var(--ovk-accent); border-radius: 4px; }
-.ovk-chart-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
-.ovk-chart-grid { position: absolute; left: 0; right: 0; height: 0; border-top: 1px solid color-mix(in srgb, var(--ovk-line) 70%, transparent); }
-.ovk-chart-yt { position: absolute; right: calc(100% + 8px); transform: translateY(50%); color: var(--ovk-muted); font-size: .72rem; font-variant-numeric: tabular-nums; white-space: nowrap; line-height: 1; }
-.ovk-chart-xt { position: absolute; top: calc(100% + 8px); transform: translateX(-50%); color: var(--ovk-muted); font-size: .72rem; white-space: nowrap; line-height: 1; }
-.ovk-chart-line { fill: none; stroke: var(--ovk-c, var(--ovk-c1)); stroke-width: 2.25; stroke-linejoin: round; stroke-linecap: round; vector-effect: non-scaling-stroke; }
-.ovk-chart-area { fill: color-mix(in srgb, var(--ovk-c, var(--ovk-c1)) 16%, transparent); stroke: none; }
-.ovk-chart-front { fill: none; stroke: var(--ovk-c3); stroke-width: 1.5; stroke-dasharray: 5 4; vector-effect: non-scaling-stroke; }
-.ovk-chart-rule { position: absolute; top: 0; bottom: 0; width: 1px; background: color-mix(in srgb, var(--ovk-text) 35%, transparent); pointer-events: none; }
-.ovk-chart-rule[hidden], .ovk-chart-tip[hidden] { display: none; }
-.ovk-chart-rule i { position: absolute; left: -5px; width: 10px; height: 10px; box-sizing: border-box; border-radius: 50%; border: 2px solid var(--ovk-panel); background: var(--ovk-c); transform: translateY(50%); }
-.ovk-chart-tip, .ovk-chart-pop { position: absolute; z-index: 3; min-width: 96px; padding: 7px 10px; border: 1px solid var(--ovk-line); border-radius: 10px; background: var(--ovk-panel-2); color: var(--ovk-text); font-size: .8rem; line-height: 1.35; font-variant-numeric: tabular-nums; white-space: nowrap; box-shadow: 0 8px 24px rgba(0, 0, 0, .28); pointer-events: none; }
-.ovk-chart-tip { top: 0; }
-.ovk-chart-tip b, .ovk-chart-pop b { display: block; color: var(--ovk-muted); font-size: .74rem; font-weight: 700; }
-.ovk-chart-tip span { display: flex; align-items: center; gap: 6px; }
-.ovk-chart-tip span i { width: 8px; height: 8px; border-radius: 50%; background: var(--ovk-c); }
-.ovk-chart-pop { display: none; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%); }
-.ovk-chart-dot { position: absolute; z-index: 1; width: 12px; height: 12px; margin: 0 0 -6px -6px; box-sizing: border-box; border-radius: 50%; border: 2px solid var(--ovk-panel); background: color-mix(in srgb, var(--ovk-c1) 60%, var(--ovk-muted)); color: var(--ovk-text); outline: none; text-decoration: none; }
-.ovk-chart-dot::after { content: ''; position: absolute; inset: -12px; }
-.ovk-chart-dot.is-best { background: var(--ovk-c1); width: 14px; height: 14px; margin: 0 0 -7px -7px; }
-.ovk-chart-dot:hover, .ovk-chart-dot:focus-visible { z-index: 4; box-shadow: 0 0 0 3px color-mix(in srgb, var(--ovk-accent) 40%, transparent); }
-.ovk-chart-dot:hover .ovk-chart-pop, .ovk-chart-dot:focus-visible .ovk-chart-pop, .ovk-chart-bar:hover .ovk-chart-pop, .ovk-chart-bar:focus-visible .ovk-chart-pop { display: block; }
-.ovk-chart-dot.is-right .ovk-chart-pop { left: auto; right: -6px; transform: none; }
-.ovk-chart-name.is-hidden { visibility: hidden; }
-.ovk-chart-name { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--ovk-text); font-size: .74rem; font-weight: 600; white-space: nowrap; pointer-events: none; text-shadow: 0 0 3px var(--ovk-panel), 0 0 3px var(--ovk-panel); }
-.ovk-chart-dot.is-right .ovk-chart-name, .ovk-chart-dot.is-flip .ovk-chart-name { left: auto; right: 14px; }
-.ovk-chart-bars { position: absolute; inset: 0; display: flex; align-items: flex-end; gap: clamp(1px, calc(12% / var(--n, 10)), 6px); }
-.ovk-chart-bar { position: relative; flex: 1 1 0; min-width: 2px; height: var(--h, 0%); border-radius: 3px 3px 0 0; background: var(--ovk-c1); outline: none; }
-.ovk-chart-bar:hover, .ovk-chart-bar:focus-visible { filter: brightness(1.15); box-shadow: 0 0 0 2px color-mix(in srgb, var(--ovk-accent) 45%, transparent); }
-.ovk-chart-bar.is-none { background: transparent; }
-.ovk-chart-blab { position: absolute; top: calc(100% + 8px); left: 50%; transform: translateX(-50%); color: var(--ovk-muted); font-size: .72rem; white-space: nowrap; line-height: 1; }
-.ovk-chart-bar .ovk-chart-pop { bottom: calc(100% + 6px); }
-
-/* Rankings: place, name, value ± interval, change, and a bar in proportion */
-.ovk-rank { margin: 0 0 14px; padding: 0; list-style: none; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow: hidden; color: var(--ovk-text); }
-.ovk-rank-row { position: relative; display: grid; grid-template-columns: 2.1em auto minmax(0, 1fr) auto auto; align-items: center; column-gap: 10px; row-gap: 6px; padding: 11px 14px 10px; border-top: 1px solid var(--ovk-line); }
-.ovk-rank-row:first-child { border-top: 0; }
-.ovk-rank-n { grid-column: 1; color: var(--ovk-muted); font-size: .95rem; font-weight: 800; font-variant-numeric: tabular-nums; text-align: center; }
-.ovk-rank-row.is-top .ovk-rank-n { color: var(--ovk-accent); }
-.ovk-rank-row > .ovk-mark { grid-column: 2; --ovk-mark: 34px; }
-.ovk-rank-main { grid-column: 3; min-width: 0; display: grid; gap: 1px; }
-.ovk-rank-name { display: flex; align-items: center; gap: 6px; min-width: 0; }
-.ovk-rank-name > strong, .ovk-rank-link { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 700; color: var(--ovk-text); text-decoration: none; }
-.ovk-rank-link::after { content: ''; position: absolute; inset: 0; }
-.ovk-rank-row:has(.ovk-rank-link):hover { background: var(--ovk-panel-2); }
-.ovk-rank-link:focus-visible { outline: none; }
-.ovk-rank-row:has(.ovk-rank-link:focus-visible) { box-shadow: inset 0 0 0 2px var(--ovk-accent); }
-.ovk-rank-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ovk-muted); font-size: .8rem; }
-.ovk-rank-val { grid-column: 4; display: grid; justify-items: end; line-height: 1.2; }
-.ovk-rank-val b { font-size: 1rem; font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; }
-.ovk-rank-val b small { color: var(--ovk-muted); font-size: .76rem; font-weight: 600; }
-.ovk-rank-val > small { color: var(--ovk-muted); font-size: .74rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
-.ovk-rank-delta { grid-column: 5; display: inline-flex; align-items: center; gap: 1px; min-width: 2.6em; justify-content: flex-end; font-size: .78rem; font-weight: 800; font-variant-numeric: tabular-nums; }
-.ovk-rank-delta .ovk-i { width: 13px; height: 13px; stroke-width: 3; }
-.ovk-rank-delta.is-up { color: var(--ovk-ok); } .ovk-rank-delta.is-down { color: var(--ovk-bad); } .ovk-rank-delta.is-new { color: var(--ovk-accent); text-transform: uppercase; font-size: .68rem; letter-spacing: .04em; }
-.ovk-rank-bar { grid-column: 3 / -1; grid-row: 2; height: 4px; border-radius: 999px; background: color-mix(in srgb, var(--ovk-line) 60%, transparent); overflow: hidden; }
-.ovk-rank-bar::after { content: ''; display: block; height: 100%; width: calc(var(--v, 0) * 100%); border-radius: inherit; background: color-mix(in srgb, var(--ovk-c1) 55%, transparent); }
-.ovk-rank-row.is-top .ovk-rank-bar::after { background: var(--ovk-c1); }
-
-/* Meters */
-.ovk-meter { display: block; height: 8px; min-width: 56px; border-radius: 999px; background: color-mix(in srgb, var(--ovk-line) 70%, transparent); overflow: hidden; }
-.ovk-meter > span { display: block; height: 100%; width: calc(var(--v, 0) * 100%); border-radius: inherit; background: var(--ovk-fill); }
-.ovk-meter--ok > span { background: var(--ovk-ok); } .ovk-meter--warn > span { background: var(--ovk-warn); } .ovk-meter--bad > span { background: var(--ovk-bad); }
-
-/* Drop zones: a real file input; drag, paste and the camera arrive with kit-web.js */
-.ovk-drop { margin: 0 0 14px; color: var(--ovk-text); }
-.ovk-drop-input { display: block; width: 100%; margin: 0 0 8px; color: var(--ovk-muted); font: inherit; }
-.ovk-drop.is-js .ovk-drop-input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; overflow: hidden; }
-.ovk-drop-zone { display: grid; justify-items: center; gap: 6px; padding: 28px 16px; border: 2px dashed var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); text-align: center; cursor: pointer; transition: border-color .12s, background .12s; }
-.ovk-drop-zone > .ovk-i { width: 30px; height: 30px; color: var(--ovk-accent); }
-.ovk-drop-zone strong { font-size: 1rem; font-weight: 800; }
-.ovk-drop-zone small { color: var(--ovk-muted); font-size: .84rem; }
-.ovk-drop-zone:hover, .ovk-drop.is-over .ovk-drop-zone { border-color: var(--ovk-accent); background: color-mix(in srgb, var(--ovk-accent) 8%, var(--ovk-panel)); }
-.ovk-drop-input:focus-visible + .ovk-drop-zone { outline: 2px solid var(--ovk-accent); outline-offset: 2px; }
-.ovk-drop-name:empty { display: none; }
-.ovk-drop-name { display: grid; justify-items: center; gap: 6px; margin-top: 4px; font-size: .86rem; font-weight: 600; word-break: break-all; }
-.ovk-drop-name img { max-width: 100%; max-height: 160px; border-radius: 10px; object-fit: contain; }
-.ovk-drop.has-file .ovk-drop-zone { border-style: solid; border-color: color-mix(in srgb, var(--ovk-accent) 55%, var(--ovk-line)); }
-.ovk-drop-cam { margin-top: 8px; }
-.ovk-drop-cam[hidden] { display: none; }
-
-/* Receipts: the result first, then one quiet line of what it took */
-.ovk-receipt { margin: 0 0 14px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow: hidden; color: var(--ovk-text); }
-.ovk-receipt--ok { border-color: color-mix(in srgb, var(--ovk-ok) 45%, var(--ovk-line)); }
-.ovk-receipt--bad { border-color: color-mix(in srgb, var(--ovk-bad) 45%, var(--ovk-line)); }
-.ovk-receipt-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 8px 0 14px; min-height: 44px; }
-.ovk-receipt-head.ovk-receipt-head h2 { margin: 0; font-size: 1rem; font-weight: 800; }
-.ovk-receipt-acts { display: inline-flex; gap: 2px; }
-.ovk-receipt-body { padding: 10px 14px 14px; line-height: 1.5; overflow-wrap: anywhere; }
-.ovk-receipt-body > :first-child { margin-top: 0; } .ovk-receipt-body > :last-child { margin-bottom: 0; }
-.ovk-receipt-facts { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: 0; padding: 10px 14px; border-top: 1px solid var(--ovk-line); background: color-mix(in srgb, var(--ovk-panel-2) 45%, transparent); font-size: .8rem; }
-.ovk-receipt-facts > div { display: inline-flex; align-items: center; gap: 5px; }
-.ovk-receipt-facts dt { color: var(--ovk-muted); }
-.ovk-receipt-facts dd { display: inline-flex; align-items: center; gap: 6px; margin: 0; font-weight: 700; font-variant-numeric: tabular-nums; }
-.ovk-receipt-facts .ovk-meter { width: 56px; height: 6px; }
-@media (prefers-reduced-motion: reduce) { .ovk-drop-zone { transition: none; } }
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.36.2",
+ "version": "4.37.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.36.2",
+ "version": "4.37.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.36.2",
+ "version": "4.37.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -62,6 +62,7 @@
@@ -62,6 +62,7 @@
"./showcase": "./showcase.js",
"./showcase": "./showcase.js",
"./kit": "./kit.js",
"./kit": "./kit.js",
"./kit-web": "./kit-web.js",
"./kit-web": "./kit-web.js",
+ "./kit-data-web": "./kit-data-web.js",
"./appbar": "./appbar.js",
"./appbar": "./appbar.js",
"./search": "./search.js",
"./search": "./search.js",
"./agent-session": "./agent-session.js",
"./agent-session": "./agent-session.js",
@@ -51,12 +51,14 @@ function page(body, title) {
@@ -51,12 +51,14 @@ function page(body, title) {
<title>${title}</title>
<title>${title}</title>
<script src="theme-loader.js"></script>
<script src="theme-loader.js"></script>
<link rel="stylesheet" href="kit.css">
<link rel="stylesheet" href="kit.css">
+<link rel="stylesheet" href="kit-data.css">
<style>body { margin: 0; background: var(--bg-primary, #0b0f17); color: var(--text-primary, #e8ecf3); font-family: 'Inter', system-ui, sans-serif; }</style>
<style>body { margin: 0; background: var(--bg-primary, #0b0f17); color: var(--text-primary, #e8ecf3); font-family: 'Inter', system-ui, sans-serif; }</style>
</head><body>
</head><body>
<div id="navbar-mount"></div>
<div id="navbar-mount"></div>
<main class="ovk-page">${body}</main>
<main class="ovk-page">${body}</main>
<script src="navbar.js"></script>
<script src="navbar.js"></script>
<script src="kit-web.js"></script>
<script src="kit-web.js"></script>
+<script src="kit-data-web.js"></script>
<script>OpenVibeNavbar.init({ service: 'chat', user: null, links: [
<script>OpenVibeNavbar.init({ service: 'chat', user: null, links: [
{ label: 'Vibe', href: '/vibe', icon: 'fa-wand-magic-sparkles' }, { label: 'Chat', href: '/', icon: 'fa-comments' },
{ label: 'Vibe', href: '/vibe', icon: 'fa-wand-magic-sparkles' }, { label: 'Chat', href: '/', icon: 'fa-comments' },
{ label: 'Rooms', href: '/rooms', icon: 'fa-people-group' }, { label: 'Settings', href: '/kit', icon: 'fa-gear' },
{ label: 'Rooms', href: '/rooms', icon: 'fa-people-group' }, { label: 'Settings', href: '/kit', icon: 'fa-gear' },
@@ -103,7 +103,13 @@ const sc = require('../showcase');
@@ -103,7 +103,13 @@ const sc = require('../showcase');
assert.ok(rcss.includes(':root[data-theme-mode="light"] .ov-room[style*="--room-light"]'), 'light themes take the light accent');
assert.ok(rcss.includes(':root[data-theme-mode="light"] .ov-room[style*="--room-light"]'), 'light themes take the light accent');
assert.ok(rcss.includes('.ovk-rail-btn { display: none; }'), 'phones swipe rails');
assert.ok(rcss.includes('.ovk-rail-btn { display: none; }'), 'phones swipe rails');
assert.ok(!/ovk-card|ovk-rail|ov-room/.test(kcss), 'kit.css stays the controls only: cards, rails and accents are room.css (4.29.1)');
assert.ok(!/ovk-card|ovk-rail|ov-room/.test(kcss), 'kit.css stays the controls only: cards, rails and accents are room.css (4.29.1)');
+ const dcss = fs.readFileSync(path.join(__dirname, '..', 'kit-data.css'), 'utf8');
+ const kweb = fs.readFileSync(path.join(__dirname, '..', 'kit-web.js'), 'utf8');
+ assert.ok(!/ovk-chart|ovk-table|ovk-steps|ovk-rank|ovk-drop|ovk-receipt|ovk-meter/.test(kcss), 'kit.css stays the controls: Kit 2 is kit-data.css (4.37.0)');
+ assert.ok(['.ovk-chart', '.ovk-table', '.ovk-steps', '.ovk-rank', '.ovk-drop', '.ovk-receipt', '.ovk-meter'].every((c) => dcss.includes(c)), 'kit-data.css carries every Kit 2 piece');
+ assert.ok(!/data-ovk-chart|data-ovk-drop/.test(kweb), 'kit-web.js carries no Kit 2 behaviour (kit-data-web.js does)');
const files = require('../files');
const files = require('../files');
+ for (const f of ['kit-data.css', 'kit-data-web.js']) assert.ok(files.isBrowserFile(f), `${f} is served at /shared/`);
for (const f of ['motion.js', 'rooms.js', 'room.css']) assert.ok(files.isBrowserFile(f), `${f} is served at /shared/`);
for (const f of ['motion.js', 'rooms.js', 'room.css']) assert.ok(files.isBrowserFile(f), `${f} is served at /shared/`);
const scss = fs.readFileSync(path.join(__dirname, '..', 'showcase.css'), 'utf8');
const scss = fs.readFileSync(path.join(__dirname, '..', 'showcase.css'), 'utf8');
assert.ok(rcss.includes('.sc-room-hero{') && rcss.includes('html.ov-motion [data-ov-reveal]'), 'sc.room()\'s look is in room.css (4.29.2)');
assert.ok(rcss.includes('.sc-room-hero{') && rcss.includes('html.ov-motion [data-ov-reveal]'), 'sc.room()\'s look is in room.css (4.29.2)');