openvibers/openvibe.live Commits

Join panel: perks collapse to a chip row, sentences on request

OP OpenVibers committed 16 Sept 2026 · ca8bdaa2db · 3 files, +57 −10

Six perks with a sentence each made the signed-out panel about 600px tall in the middle of the
front page, and most of that height was explanation nobody needs before they are interested.
Collapsed, the perks are a wrapped row of labelled chips — the whole pitch still readable at a
glance — and the sentences are one tap away. The panel is 329px collapsed, 410px open, and the
call to action never moves or hides. The choice is remembered.

The collapse is purely presentational: every sentence stays in the document, so crawlers and
screen readers see the full pitch either way.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMxJm1dB8rdsaMSgNjVa82

Changes

ddiff --git a/public/css/home-tour.css b/public/css/home-tour.css
iindex f211b772..444aa85c 100644
---- a/public/css/home-tour.css
++++ b/public/css/home-tour.css
@@@ -207,18 +207,46 @@
@keyframes joinMark { 0%, 100% { transform: translateY(0) rotate(0); } 50% { transform: translateY(-3px) rotate(-7deg); } }
.ovg-join-head h3 { margin: 0; font-size: 1.02rem; font-weight: 900; letter-spacing: -0.015em; }
.ovg-join-head p { margin: 3px 0 0; font-size: 0.82rem; color: var(--text-muted); }
--.ovg-join-perks { position: relative; list-style: none; margin: 14px 0 0; padding: 0;
-- display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 10px 18px; }
--/* Visible by default — the entrance is a bonus, never the reason anything is readable. */
--.ovg-join-perks li { display: flex; gap: 10px; align-items: flex-start; }
++/* Collapsed: a wrapped row of labelled chips — the whole pitch at a glance, one line tall per
++ two or three perks. Expanded: the same list with its sentences, in columns. The collapse is
++ purely presentational, so the text is always in the document for crawlers and screen readers. */
++.ovg-join-perks { position: relative; list-style: none; margin: 13px 0 0; padding: 0;
++ display: flex; flex-wrap: wrap; gap: 7px; }
++.ovg-join-perks li { display: flex; gap: 8px; align-items: center;
++ padding: 6px 11px 6px 7px; border-radius: 999px;
++ background: color-mix(in srgb, var(--accent) 9%, transparent);
++ border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
++ transition: background 0.2s, border-color 0.2s; }
++.ovg-join-perks li:hover { background: color-mix(in srgb, var(--accent) 15%, transparent); border-color: color-mix(in srgb, var(--accent) 38%, transparent); }
++.ovg-join-perks small { display: none; }
++.ovg-join .ovg-join-ico { width: 22px; height: 22px; border-radius: 7px; font-size: 0.66rem; }
++.ovg-join-perks b { font-size: 0.8rem; white-space: nowrap; }
++
++/* Expanded */
++.ovg-join.is-open .ovg-join-perks { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 10px 18px; }
++.ovg-join.is-open .ovg-join-perks li { display: flex; align-items: flex-start; gap: 10px;
++ padding: 0; border: 0; border-radius: 0; background: none; }
++.ovg-join.is-open .ovg-join-perks li:hover { background: none; }
++.ovg-join.is-open .ovg-join-perks small { display: block; }
++.ovg-join.is-open .ovg-join-ico { width: 28px; height: 28px; border-radius: 9px; font-size: 0.76rem; }
++.ovg-join.is-open .ovg-join-perks b { white-space: normal; font-size: 0.86rem; }
++
++.ovg-join-expand { display: inline-flex; align-items: center; gap: 7px; margin-top: 11px;
++ padding: 5px 2px; background: none; border: 0; cursor: pointer; font: inherit;
++ font-size: 0.76rem; font-weight: 700; color: color-mix(in srgb, var(--accent) 78%, var(--text-muted));
++ transition: color 0.2s; }
++.ovg-join-expand:hover { color: var(--accent); }
++.ovg-join-expand i { font-size: 0.66rem; transition: transform 0.28s cubic-bezier(.2, 1, .3, 1); }
++.ovg-join.is-open .ovg-join-expand i { transform: rotate(180deg); }
.tour-quest-slot.fx-live .ovg-join-perks li { animation: joinPerk 0.6s cubic-bezier(.2, 1, .3, 1) both;
animation-delay: calc(var(--i) * 85ms + 100ms); }
@keyframes joinPerk { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.ovg-join-ico { width: 28px; height: 28px; flex: none; border-radius: 9px; display: grid; place-items: center; font-size: 0.76rem;
color: var(--accent); background: color-mix(in srgb, var(--accent) 15%, transparent);
border: 1px solid color-mix(in srgb, var(--accent) 32%, transparent); }
--.ovg-join-perks b { display: block; font-size: 0.86rem; font-weight: 800; }
--.ovg-join-perks small { display: block; margin-top: 1px; font-size: 0.76rem; line-height: 1.45; color: var(--text-muted); }
++.ovg-join-perks b { font-weight: 800; }
++.ovg-join.is-open .ovg-join-perks b { display: block; }
++.ovg-join.is-open .ovg-join-perks small { margin-top: 1px; font-size: 0.76rem; line-height: 1.45; color: var(--text-muted); }
.ovg-join-cta { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 16px; }
.ovg-join-go { position: relative; overflow: hidden; }
.ovg-join-go::after { content: ''; position: absolute; top: 0; bottom: 0; left: -45%; width: 35%;
ddiff --git a/public/index.html b/public/index.html
iindex 025c1d35..f9b3dad0 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -90,7 +90,7 @@
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="stylesheet" href="/css/style.css?v=169">
<link rel="stylesheet" href="/css/home-fx.css?v=10">
-- <link rel="stylesheet" href="/css/home-tour.css?v=4">
++ <link rel="stylesheet" href="/css/home-tour.css?v=6">
<link rel="stylesheet" href="/css/i18n-star.css?v=5">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/css/chat-shared.css?v=1" media="print" onload="this.media='all';this.onload=null">
@@@ -5215,7 +5215,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<script src="/js/home-fx.js?v=8" defer></script>
<script src="/js/home-tour.js?v=6" defer></script>
<script src="/js/ov-guide.js?v=1" defer></script>
--<script src="/js/guides-live.js?v=4" defer></script>
++<script src="/js/guides-live.js?v=5" defer></script>
<!-- ── OpenVibe shared libs (notifications, account sync, themes) ── -->
<script src="/shared/theme-loader.js?v=1" defer></script>
ddiff --git a/public/js/guides-live.js b/public/js/guides-live.js
iindex 009f566f..15fa2c8b 100644
---- a/public/js/guides-live.js
++++ b/public/js/guides-live.js
@@@ -349,8 +349,14 @@
}
function renderJoin(el) {
++ // Six perks with a sentence each is a tall block to put in the middle of a front page, and
++ // most of that height is explanation nobody needs before they are interested. Collapsed,
++ // the perks are a single wrapped row of labels — still the whole pitch, at a glance — and
++ // the sentences are one tap away for anyone who wants them. The call to action never
++ // moves or hides.
++ const open = (() => { try { return localStorage.getItem('ov_join_open') === '1'; } catch { return false; } })();
el.innerHTML = `
-- <div class="ovg-join">
++ <div class="ovg-join${open ? ' is-open' : ''}">
<span class="ovg-join-glow" aria-hidden="true"></span>
<div class="ovg-join-head">
<span class="ovg-join-mark" aria-hidden="true"><i class="fa-solid fa-circle-nodes"></i></span>
@@@ -359,9 +365,12 @@
<p>Free to make, works everywhere on the network, and takes about ten seconds.</p>
</div>
</div>
-- <ul class="ovg-join-perks">
++ <ul class="ovg-join-perks" id="ovg-join-perks">
${PERKS.map(([ico, title, why], i) => `<li style="--i:${i}"><span class="ovg-join-ico"><i class="fa-solid ${ico}"></i></span><span><b>${title}</b><small>${why}</small></span></li>`).join('')}
</ul>
++ <button type="button" class="ovg-join-expand" aria-expanded="${open}" aria-controls="ovg-join-perks">
++ <i class="fa-solid fa-chevron-down"></i><span></span>
++ </button>
<div class="ovg-join-cta">
<a class="btn btn-primary btn-lg ovg-join-go" href="/api/auth/sso/login"><i class="fa-solid fa-user-plus"></i> Create your free account</a>
<a class="btn btn-outline ovg-join-in" href="/api/auth/sso/login"><i class="fa-solid fa-right-to-bracket"></i> I already have one</a>
@@@ -369,6 +378,16 @@
</div>
</div>`;
el.querySelector('.ovg-join-tour').addEventListener('click', () => openSiteTour());
++ const panel = el.querySelector('.ovg-join');
++ const exp = el.querySelector('.ovg-join-expand');
++ const label = () => { exp.querySelector('span').textContent = panel.classList.contains('is-open') ? 'Less detail' : 'What each of these means'; };
++ label();
++ exp.addEventListener('click', () => {
++ panel.classList.toggle('is-open');
++ exp.setAttribute('aria-expanded', String(panel.classList.contains('is-open')));
++ label();
++ try { localStorage.setItem('ov_join_open', panel.classList.contains('is-open') ? '1' : '0'); } catch { /* */ }
++ });
}
window.setupNextUp = async function (el) {