openvibers/openvibe.community Commits

The home on phones: the roadmap and the pulse are chips, the questions a fold (#191)

OP OpenVibers committed 14 days ago · 0dda3bb697 · 6 files, +228 −7

The home page is one or two screens on a phone. The roadmap and the pulse, two
shelves of the same kind, become one pick block: a row of chips picks the
shelf a phone shows, and both stay in the page for crawlers. The questions
become a fold, closed on a phone and open where there is room. A wider screen
shows both shelves side by side and no chips, as before.

The html and css budgets rise to the measured sizes (41.9 KB and 109.0 KB).

Tested with the site's suite (home-short.test.js covers the chips, the CSS
guard and the fold).

Changes

ddiff --git a/public/css/community.css b/public/css/community.css
iindex 31a4f60..f710d65 100644
---- a/public/css/community.css
++++ b/public/css/community.css
@@@ -390,3 +390,55 @@ button.rx:hover { border-color: var(--accent); }
.cm-faq summary { min-height: 44px; box-sizing: border-box; cursor: pointer; padding: 12px 16px; font-weight: 700; }
.cm-faq details[open] summary { border-bottom: 1px solid var(--border); }
.cm-faq p { margin: 0; padding: 12px 16px 14px; color: var(--text-secondary); line-height: 1.6; }
++
++/* The phone home (2026-10-30, the owner's short-pages rule): the roadmap and the pulse are chips, one shelf at a
++ time, and the questions are a fold. Both only work under 640 px, so a wider screen shows both shelves and no chips. */
++/* Pick (the owner's short-pages rule, 2026-10-30): on a phone the front page's blocks of the same kind are one at a
++ time, picked with a row of chips. Radio inputs and :has(), no script: the chips and the one-pane rule live under
++ 640 px and only where :has() is understood, so a wider screen, or an older browser, shows every pane and no chips.
++ Every pane stays in the page for crawlers. */
++.pick-head { display: none; }
++.pick-row { display: none; }
++@supports selector(:has(*)) { @media (max-width: 640px) {
++ .pick-bar { margin-top: 30px; }
++ .pick-head { display: block; }
++ .pick-head h2 { margin: 0 0 4px; font: 700 clamp(20px, 2.4vw, 28px)/1.2 var(--ov-font, 'Inter', system-ui, sans-serif); letter-spacing: -.02em; }
++ .pick-row { display: flex; gap: 8px; overflow-x: auto; margin: 10px -16px 0; padding: 2px 16px 4px; scrollbar-width: none; overscroll-behavior-x: contain; }
++ .pick-row::-webkit-scrollbar { display: none; }
++ .pick-row input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
++ .pick-row label { display: inline-flex; align-items: center; flex: none; min-height: 44px; padding: 0 15px; border-radius: 999px; border: 1px solid var(--border, #1f2d47); color: var(--text-secondary, #96a7c2); font-size: .9rem; font-weight: 650; white-space: nowrap; cursor: pointer; }
++ .pick-row input:checked + label { color: var(--text-primary, #e6edf7); border-color: var(--accent, #3b82f6); background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); }
++ .pick-row input:focus-visible + label { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
++ .pick .pick-pane { display: none; }
++ /* A link that points at a pane opens that pane on a phone too. */
++ .pick .pick-pane:target { display: block !important; }
++ .pick:has(.pick-pane:target) .pick-pane:not(:target) { display: none !important; }
++ /* The chip names the pane, so the pane's own heading gives way to it; the link to more stays. */
++ .pick .pick-pane > h2, .pick .pick-pane > h3 { display: none; }
++} }
++
++/* The fold (the owner's short-pages rule): closed on a phone, and where there is room it looks like the section it
++ replaces: a heading and its content, with no card around it. */
++.hm-fold-block { margin-top: 32px; }
++.hm-fold-block > summary { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 44px; cursor: pointer; list-style: none; }
++.hm-fold-block > summary::-webkit-details-marker { display: none; }
++.hm-fold-block > summary h2 { margin: 0; font: 700 clamp(19px, 2.2vw, 26px)/1.2 var(--ov-font, 'Inter', system-ui, sans-serif); }
++.hm-fold-block > summary span { color: var(--text-muted, #7386a3); font-weight: 600; font-size: .86rem; white-space: nowrap; }
++.hm-fold-block > summary:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
++@media (min-width: 641px) {
++ .hm-fold-block { border: 0; background: none; border-radius: 0; margin: 56px auto 0; padding: 0; }
++ .hm-fold-block > summary { display: block; min-height: 0; }
++ .hm-fold-block > summary h2 { margin: 0 0 6px; font: 700 clamp(22px, 2.6vw, 30px)/1.2 var(--ov-font, 'Inter', system-ui, sans-serif); }
++ .hm-fold-block > summary span { display: none; }
++ .hm-fold-block .hm-sec, .hm-fold-block .sc-sec { margin: 0; max-width: none; padding: 0; }
++}
++@media (max-width: 640px) { .hm-fold-block { margin-top: 24px; } .hm-fold-block > summary h2 { font-size: 1.05rem; } }
++/* A fold that sits inside the section it replaces adds no space of its own. */
++:where(.sc-sec, .cm-sec, .hm-sec, .section) > .hm-fold-block { margin: 0; }
++/* The chip names the shelf, so the shelf's own heading gives way to it; the link to the whole list stays. */
++@supports selector(:has(*)) { @media (max-width: 640px) {
++ .pick .pick-pane .ovk-rail-head h2 { display: none; }
++ .pick .pick-pane .ovk-rail-head { justify-content: flex-end; }
++} }
++/* The fold holds the questions, so its list gets the room the card gives it. */
++.hm-fold-block .cm-faq { margin: 0; padding: 0 16px 16px; }
ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex 0365b35..c0b903e 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -141,6 +141,7 @@ function renderPage(o) {
appIcon.headTags({ site: 'community', iconBase: '/assets' }),
...feeds.map((f) => `<link rel="alternate" type="application/rss+xml" title="${escapeHtml(f.title)}" href="${escapeHtml(f.href)}">`),
...(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`),
++ ...(o.inlineStyles || []).map((css) => `<style>${css}</style>`),
`<link rel="stylesheet" href="${asset('css/community.css')}">`,
'<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">',
`<script src="${asset('js/community.js')}" defer></script>`,
ddiff --git a/server/render/pages.js b/server/render/pages.js
iindex fc8130b..06499d2 100644
---- a/server/render/pages.js
++++ b/server/render/pages.js
@@@ -14,6 +14,7 @@ const showcase = require('openvibe-shared/showcase');
const kit = require('openvibe-shared/kit');
const { highlight, escapeHtml: esc, languageLabel, extensionFor, LANGUAGES } = require('./highlight');
const { renderMarkdown } = require('./markdown');
++const { pickBlock, pickRules, foldBlock } = require('./short');
// Languages read as prose: their lines wrap by default (the Wrap button still toggles), so a phone shows the whole text.
const PROSE = new Set(['markdown', 'plaintext', 'text']);
@@@ -247,10 +248,31 @@ const { url } = await res.json(); // "/p/<slug>"` })}`,
}
function homeFaq() {
-- return `<section class="cm-sec" id="questions" aria-labelledby="h-faq">
-- <h2 id="h-faq" class="cm-h">Questions</h2>
-- <div class="cm-faq">${HOME_FAQ.map((f) => `<details><summary>${esc(f.q)}</summary><p>${esc(f.a)}</p></details>`).join('')}</div>
--</section>`;
++ // The questions are a fold on a phone (the owner's short-pages rule, 2026-10-30): they stay in the page for the
++ // crawlers, and the script closes them where the reader has room for more.
++ return `<section class="cm-sec" id="questions" aria-labelledby="h-faq">${foldBlock({
++ id: 'faq-fold',
++ head: '<h2 id="h-faq" class="cm-h">Questions</h2>',
++ html: `<div class="cm-faq">${HOME_FAQ.map((f) => `<details><summary>${esc(f.q)}</summary><p>${esc(f.a)}</p></details>`).join('')}</div>`,
++ })}</section>`;
++}
++
++/**
++ * The roadmap and the pulse, one at a time on a phone (the owner's short-pages rule, 2026-10-30): the chips pick one
++ * of the two shelves, and both stay in the page for the crawlers. On a wide screen they stay side by side.
++ */
++function pickWhatIsHappening(roadmap, activity) {
++ const panes = [
++ { id: 'shelf-roadmap', label: 'Roadmap', html: roadmapShelf(roadmap) },
++ { id: 'shelf-pulse', label: 'Across OpenVibe', html: pulseShelf(activity) },
++ ];
++ return pickBlock({ key: 'happening', label: 'What is happening', head: 'What is happening', panes, bodyClass: 'cm-pair' });
++}
++
++/** The rules that show the picked shelf of the home's pick block: one per shelf, written for this page. */
++function happeningRules() {
++ const ids = ['shelf-roadmap', 'shelf-pulse'];
++ return `@supports selector(:has(*)){@media (max-width:640px){${pickRules('happening', ids)}}}`;
}
function homePage({ latest, fromStreams = [], languages, user, spaces = [], roadmap = [], activity = [], counts = {} }) {
@@@ -268,7 +290,7 @@ function homePage({ latest, fromStreams = [], languages, user, spaces = [], road
${latestShelf(latest, languages)}
${streamsShelf(fromStreams)}
${forumShelf(spaces)}
--<div class="cm-pair">${roadmapShelf(roadmap)}${pulseShelf(activity)}</div>
++${pickWhatIsHappening(roadmap, activity)}
<div class="cm-sec">${apiFold()}</div>
${homeFaq()}
<section class="section">${frame.shipped({ service: 'community', title: 'Recently shipped on OpenVibe.Community' })}</section>`;
@@@ -285,6 +307,8 @@ ${homeFaq()}
],
// The kit's controls (kit.css) and its cards and rails (room.css, after it); kit-web.js comes with community.js.
styles: [showcase.STYLESHEET, 'kit.css', 'room.css'],
++ // The rule that shows the picked shelf: one per shelf, so the CSS is written for this page.
++ inlineStyles: [happeningRules()],
bodyClass: 'cm-home',
body,
});
ddiff --git a/server/render/short.js b/server/render/short.js
nnew file mode 100644
iindex 0000000..d201b02
---- /dev/null
++++ b/server/render/short.js
@@@ -0,0 +1,86 @@
++'use strict';
++/**
++ * The home on a phone (the owner's short-pages rule, 2026-10-30): one or two screens, app-like.
++ *
++ * pickBlock() several blocks of the same kind, one at a time on a phone, picked with a row of chips. Radio
++ * inputs and :has(), no script: every block stays in the page for crawlers, and a wider screen (or a
++ * browser without :has()) shows them all and no chips.
++ * foldBlock() a part a reader rarely needs, closed on a phone and open where there is room. The page's own markup
++ * keeps it open, so a browser without the script shows it as before; the script closes it on a narrow
++ * screen and remembers the reader's choice.
++ *
++ * The CSS for both lives in the site's own stylesheet; the rule that shows the picked block is written for the
++ * page, because there is one per block.
++ */
++const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
++
++/**
++ * A pick block. The panes are the page's own blocks; each one keeps its heading on a wide screen, and on a phone the
++ * heading gives way to the chip that names it.
++ *
++ * pickBlock({ key, label, head, panes }) head: the heading of the block, or '' when the block already has one.
++ * panes: [{ id, label, html }]
++ */
++function pickBlock({ key, label, head = '', panes, bodyClass = '' }) {
++ // A pane with nothing to show is left out, so the chips and the rules that show them match the page.
++ panes = (panes || []).filter((p) => p.html);
++ if (!panes.length) return '';
++ const chips = panes.map((p, i) => `<input type="radio" name="pick-${esc(key)}" id="pk-${esc(key)}-${esc(p.id)}" value="${esc(p.id)}"${i === 0 ? ' checked' : ''}><label for="pk-${esc(key)}-${esc(p.id)}">${esc(p.label)}</label>`).join('');
++ // The pane carries the id the page already links to (an anchor in the hero, for instance), so a link to it still
++ // opens that pane on a phone (the CSS shows the pane a link points at).
++ const body = panes.map((p) => `<div class="pick-pane" id="${esc(p.id)}">${p.html}</div>`).join('');
++ const bar = `${head ? `<div class="pick-head"><h2>${esc(head)}</h2></div>` : ''}<div class="pick-row" role="radiogroup" aria-label="${esc(label)}">${chips}</div>`;
++ return `<div class="pick" id="pick-${esc(key)}"><div class="pick-bar">${bar}</div><div class="pick-body${bodyClass ? ' ' + esc(bodyClass) : ''}">${body}</div></div>`;
++}
++
++/** The rule that shows the picked block of a pick block: one per block, so the CSS is written for the page. */
++function pickRule(key, id) {
++ return `.pick:has(#pk-${key}-${id}:checked) #${id}{display:block}`;
++}
++
++/** The rules for every block of a pick block, in the order they appear. */
++function pickRules(key, ids) {
++ return ids.map((id) => pickRule(key, id)).join('');
++}
++
++/**
++ * A fold. The head is the heading the block had; the html is what it holds.
++ *
++ * foldBlock({ id, head, html })
++ */
++function foldBlock({ id, head, html }) {
++ return `<details class="hm-fold hm-fold-block" id="${esc(id)}" open><summary>${head}<span>tap to open</span></summary>${html}</details>${foldScript(id)}`;
++}
++
++/** The script a fold carries: closed on a phone unless the reader opened it before, and it remembers that. */
++function foldScript(id) {
++ return `<script>(function(){var d=document.getElementById(${JSON.stringify(id)});if(!d)return;var k='ov_fold_'+${JSON.stringify(id)};var s=null;try{s=localStorage.getItem(k)}catch(e){}if(s==='open'){d.open=true;return}if(s==='closed'){d.open=false;return}var t=0;if(window.matchMedia&&window.matchMedia('(max-width:640px)').matches){t=Date.now();d.open=false}d.addEventListener('toggle',function(){if(Date.now()-t<300)return;try{localStorage.setItem(k,d.open?'open':'closed')}catch(e){}})})();</script>`;
++}
++
++/**
++ * A section of the page's own markup as a fold. The section stays where it was; its own heading becomes the fold's
++ * heading, and the fold is closed on a phone (the script) and open where there is room.
++ *
++ * foldSection(html, needle, id) needle: a mark of the part to fold (a class, for instance)
++ */
++function foldSection(html, needle, id) {
++ const at = html.indexOf(needle);
++ if (at < 0) return html;
++ const start = html.lastIndexOf('<section', at);
++ const close = html.indexOf('</section>', at);
++ if (start < 0 || close < 0) return html;
++ const openEnd = html.indexOf('>', start) + 1;
++ const openTag = html.slice(start, openEnd);
++ let inner = html.slice(openEnd, close);
++ const h2 = /<h2[^>]*>[\s\S]*?<\/h2>/.exec(inner);
++ if (!h2) return html;
++ const head = h2[0];
++ inner = inner.slice(h2.index + head.length);
++ // The lede under the heading goes with it: the fold's heading says enough on a phone.
++ const lede = /<p class="sc-lede"[^>]*>[\s\S]*?<\/p>/.exec(inner);
++ if (lede) inner = inner.slice(0, lede.index) + inner.slice(lede.index + lede[0].length);
++ const fold = `<details class="hm-fold hm-fold-block" id="${esc(id)}" open><summary>${head}<span>tap to open</span></summary>${inner.trim()}</details>${foldScript(id)}`;
++ return html.slice(0, start) + openTag + fold + html.slice(close);
++}
++
++module.exports = { pickBlock, pickRule, pickRules, foldBlock, foldScript, foldSection };
ddiff --git a/test/home-short.test.js b/test/home-short.test.js
nnew file mode 100644
iindex 0000000..1e56cbc
---- /dev/null
++++ b/test/home-short.test.js
@@@ -0,0 +1,58 @@
++'use strict';
++/**
++ * The home on a phone (the owner's short-pages rule, 2026-10-30): the roadmap and the pulse are a pick block, a row
++ * of chips picks one of the two shelves, and the questions are a fold. Radio inputs and :has(), no script for the
++ * chips; every shelf stays in the page for crawlers; a wider screen shows both shelves and no chips.
++ */
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const { boot, check, done } = require('./helpers/app');
++
++(async () => {
++ const t = await boot();
++ try {
++ // A roadmap item and a pulse item, so the home has something to show in its pick block.
++ await t.db.prepare("INSERT INTO threads (space_id, slug, title, status, last_activity_at) VALUES (4, 'test-item', 'A test item', 'in_progress', ov_now())").run();
++ await t.db.prepare("INSERT INTO pulse_items (source_service, source_type, source_id, title, url, origin, visibility, occurred_at) VALUES ('network', 'notice', 'n1', 'Something happened', 'https://openvibe.network/', 'system', 'public', ov_now())").run();
++
++ await check('the roadmap and the pulse are chips: one per shelf, the first picked, every shelf in the page', async () => {
++ const r = await t.get('/');
++ assert.strictEqual(r.status, 200, r.text);
++ const page = r.text;
++ assert.ok(page.includes('<div class="pick" id="pick-happening">'), 'the pick block is on the home');
++ const block = page.slice(page.indexOf('<div class="pick" id="pick-happening">'));
++ const chips = [...block.matchAll(/<input type="radio" name="pick-happening" id="pk-happening-([a-z-]+)" value="\1"( checked)?><label for="pk-happening-\1">([^<]+)<\/label>/g)];
++ assert.deepStrictEqual(chips.map((m) => m[1]), ['shelf-roadmap', 'shelf-pulse'], 'a chip per shelf');
++ assert.deepStrictEqual(chips.filter((m) => m[2]).map((m) => m[1]), ['shelf-roadmap'], 'the first shelf is picked');
++ const panes = [...block.matchAll(/<div class="pick-pane" id="([a-z-]+)">/g)].map((m) => m[1]);
++ assert.deepStrictEqual(panes, ['shelf-roadmap', 'shelf-pulse'], 'every shelf is in the page, one per chip');
++ // Nothing was removed: both shelves are still there.
++ for (const title of ['On the roadmap', 'A test item', 'Across OpenVibe', 'Recently shipped on OpenVibe.Community']) {
++ assert.ok(page.includes(title), `${title} is on the page`);
++ }
++ });
++
++ await check('the CSS guard: the chips are hidden and both shelves show until 640 px and where :has() is understood', async () => {
++ const r = await t.get('/');
++ assert.match(r.text, /<style>@supports selector\(:has\(\*\)\)\{@media \(max-width:640px\)\{\.pick:has\(#pk-happening-shelf-roadmap:checked\) #shelf-roadmap\{display:block\}/, 'the rule that shows the picked shelf');
++ const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'community.css'), 'utf8');
++ assert.match(css, /\.pick-row \{ display: none; \}/, 'the chips are hidden by default');
++ assert.match(css, /@supports selector\(:has\(\*\)\) \{ @media \(max-width: 640px\) \{/, 'the phone rules are under :has() and 640 px');
++ assert.match(css, /\.pick-row label \{[^}]*min-height: 44px/, 'a chip is a 44 px target');
++ assert.match(css, /\.pick \.pick-pane \{ display: none; \}/, 'the shelves are hidden on a phone');
++ });
++
++ await check('the questions are a fold, open in the page and closed on a phone', async () => {
++ const r = await t.get('/');
++ assert.ok(r.text.includes('<details class="hm-fold hm-fold-block" id="faq-fold" open>'), 'the questions are a fold, open in the page');
++ assert.ok(r.text.includes('document.getElementById("faq-fold")'), 'the fold has the script that closes it on a phone');
++ // The questions stay in the page.
++ assert.ok(r.text.includes('id="questions"'));
++ assert.match(r.text, /<div class="cm-faq">/);
++ });
++ } finally {
++ await t.close();
++ }
++ done();
++})().catch((e) => { console.error(e); process.exit(1); });
ddiff --git a/test/perf-budget.test.js b/test/perf-budget.test.js
iindex 8adbc29..70d10f9 100644
---- a/test/perf-budget.test.js
++++ b/test/perf-budget.test.js
@@@ -10,7 +10,7 @@ const { spawn } = require('child_process');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const BUDGETS = {
-- htmlRawKB: 41, // 39.4 the short home (2026-10-29: rails, a launcher and a fold in place of three grids, What lives here, Part of OpenVibe and the closing call), lowered back from 42; 41.6 with the agent layer's two discovery <link> tags (Shared 4.34.0, 2026-10-29); was 41 at 41.0
++ htmlRawKB: 42, // 41.9 the phone home (2026-10-30: the roadmap and the pulse become chips and the questions a fold); 39.4 the short home (2026-10-29: rails, a launcher and a fold in place of three grids, What lives here, Part of OpenVibe and the closing call), lowered back from 42; 41.6 with the agent layer's two discovery <link> tags (Shared 4.34.0, 2026-10-29); was 41 at 41.0
// 36.4 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); measured 31.7 on 10-22: the home gained the forum's places, the roadmap, Pulse, real counts and six
// questions with their FAQPage JSON-LD; was 26 at 22.0
htmlBrotliKB: 10, // 8.6 with the Part of OpenVibe section (Shared 4.13, 2026-10-26); 7.3 on 10-22 (the same shelves); was 6.5 at 5.7
@@@ -18,7 +18,7 @@ const BUDGETS = {
jsRawKB: 336, // 335.4 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 332.4 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 329.8 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 322 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 314 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 306.5 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 300.7 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 286.6 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 276.8 with shared 4.11.0's seasonal navbar; 267.5 with openvibe-shared 4.9.0 (status text twins in every theme, 2026-10-23); 251.3 (was 208.8 before the shell's web-runtime.js)
jsBrotliKB: 84, // 83.1 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 82.3 with openvibe-shared 4.52.0: the Dock's loader and its Chats tab (2026-10-30); 81.7 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 80.2 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 78.1 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 74.4 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 71.7 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 69.1 Shared 4.12.0: the light/dark switch, the seasonal mark and the theme pairs; 65.5 with shared 4.11.0's seasonal navbar; 59.9 (was 48.9)
cssFiles: 5, // 5 the short home: + kit.css (numbers, lists, chips, the fold) and room.css (cards and rails), the shared kit's look; 3
-- cssRawKB: 106, // 104.8 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 102.6 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 97.3 the short home (2026-10-29): kit.css 25.6 KB + room.css 15.7 KB, while community.css sheds the old shelves' rules and the page 10 of its 15 phone screens; 54.9 openvibe-shared 4.26.1 (hero visual); 47.2 with Shared 4.13.0 (2026-10-26); 40.6 on 10-22 (the home shelves' styles in community.css); was 40 at 36.5 with the showcase home
++ cssRawKB: 110, // 109.0 the phone home (2026-10-30: the chips and the fold rules); 104.8 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 102.6 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 97.3 the short home (2026-10-29): kit.css 25.6 KB + room.css 15.7 KB, while community.css sheds the old shelves' rules and the page 10 of its 15 phone screens; 54.9 openvibe-shared 4.26.1 (hero visual); 47.2 with Shared 4.13.0 (2026-10-26); 40.6 on 10-22 (the home shelves' styles in community.css); was 40 at 36.5 with the showcase home
cssBrotliKB: 23, // 22.1 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 21.7 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 20.4 the short home (kit.css + room.css, as above; was 11.5 once Kit 2 had its own files in openvibe-shared v4.37.0); 9.5 with Shared 4.13.0 (2026-10-26); 7.7 // raised 11 -> 12 for the openvibe-shared Kit 2 pin (kit.css grew)
externalFiles: 2, // 1
};