openvibers/openvibe.live Commits
Home hero: compact responsive stats strip, no ALPHA badge, blue theme colour, widget history, hamburger only on small screens
Changes
@@@ -993,3 +993,80 @@ body.egg-rainbow { animation: eggRainbow 2.6s linear; }}.cta-chip i { color: var(--accent-light); }.cta-hero { text-shadow: 0 6px 30px color-mix(in srgb, var(--accent) 35%, transparent); }++++/* ═══ Hero stats strip v3 — compact, no dead space, honest per viewport ═══++ The headline strip used to be three wide cards per row with the number in one corner, the++ label under it and the delta pill on a third line: ~80px tall cards that were mostly air.++ Now: one 9-across row on wide screens (icon, number, label, short delta), three horizontal++ rows on tablets (icon · number+label · delta right), three tight columns on phones. The++ reserved min-height matches each layout so nothing jumps while the numbers load. */++.hero-latest { margin-top: 18px; }++.hero-stats { margin: 16px 0 10px; gap: 10px; min-height: 108px; }++.hero-stat-strip {++ display: grid; grid-template-columns: repeat(9, minmax(0, 1fr)); grid-auto-rows: auto;++ gap: 8px; max-width: 1240px; margin: 0 auto; width: 100%; padding: 0;++}++.hero-stat-strip .hero-stat {++ display: grid; grid-template-columns: auto minmax(0, 1fr); grid-template-rows: auto auto;++ column-gap: 8px; row-gap: 3px; align-items: center; align-content: center;++ padding: 8px 10px; border-radius: 12px; min-height: 0;++ background: color-mix(in srgb, var(--bg-secondary) 74%, transparent);++ border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);++}++.hero-stat-strip .hero-stat > i {++ grid-row: 1 / 3; grid-column: 1; width: 28px; height: 28px; display: grid; place-items: center;++ border-radius: 9px; font-size: 0.74rem; color: var(--accent);++ background: color-mix(in srgb, var(--accent) 13%, transparent);++ border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);++}++.hero-stat-strip .hero-stat--live > i { color: var(--live-red); background: color-mix(in srgb, var(--live-red) 15%, transparent); border-color: color-mix(in srgb, var(--live-red) 34%, transparent); }++.hero-stat-strip .hero-stat-meta { grid-column: 2; grid-row: 1; display: flex; flex-direction: column; gap: 1px; min-width: 0; }++.hero-stat-strip .hero-stat-num { font-size: 1.06rem; line-height: 1.05; letter-spacing: -0.02em; }++.hero-stat-strip .hero-stat-label { font-size: 0.56rem; letter-spacing: 0.06em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.1; }++.hero-stat-strip .hero-stat-sub { display: none; }++.hero-stat-strip .hero-stat-delta {++ grid-column: 2; grid-row: 2; justify-self: start; margin: 0; max-width: 100%;++ font-size: 0.55rem; padding: 1px 6px 1px 5px; overflow: hidden; text-overflow: ellipsis;++}++/* "vs 24h" / "7d" suffixes live in the tooltip; the pill keeps the number and the arrow. */++.hero-stat-strip .hero-stat-delta em { display: none; }++.hero-stat-strip .hero-stat-trend { margin-left: 4px; padding-left: 4px; }++.hero-stat-strip .hero-stat-chart-hint { font-size: 0.5rem; margin-left: 2px; }++/* Tablets and small laptops: three horizontal rows — icon · number + label · delta on the right. */++@media (max-width: 1279px) {++ .hero-stats { min-height: 176px; }++ .hero-stat-strip { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; max-width: 960px; }++ .hero-stat-strip .hero-stat { grid-template-columns: auto minmax(0, 1fr) auto; grid-template-rows: auto; padding: 8px 12px; }++ .hero-stat-strip .hero-stat > i { grid-row: 1; }++ .hero-stat-strip .hero-stat-meta { grid-row: 1; flex-direction: row; align-items: baseline; gap: 7px; }++ .hero-stat-strip .hero-stat-num { font-size: 1.12rem; }++ .hero-stat-strip .hero-stat-label { font-size: 0.6rem; }++ .hero-stat-strip .hero-stat-delta { grid-column: 3; grid-row: 1; justify-self: end; }++ .hero-stat-strip .hero-stat-delta em { display: inline; }++}++/* Phones: three tight columns, number over label, short pill. */++@media (max-width: 699px) {++ .hero-stats { min-height: 214px; margin-top: 14px; }++ .hero-stat-strip { gap: 6px; }++ .hero-stat-strip .hero-stat { grid-template-columns: auto minmax(0, 1fr); grid-template-rows: auto auto; padding: 7px 8px; column-gap: 6px; row-gap: 2px; border-radius: 10px; }++ .hero-stat-strip .hero-stat > i { grid-row: 1 / 3; width: 24px; height: 24px; font-size: 0.66rem; border-radius: 7px; }++ .hero-stat-strip .hero-stat-meta { grid-row: 1; flex-direction: column; gap: 1px; }++ .hero-stat-strip .hero-stat-num { font-size: clamp(0.9rem, 4.2vw, 1.05rem); }++ .hero-stat-strip .hero-stat-label { font-size: 0.5rem; }++ .hero-stat-strip .hero-stat-delta { grid-column: 2; grid-row: 2; justify-self: start; font-size: 0.5rem; padding: 1px 5px; }++ .hero-stat-strip .hero-stat-delta em { display: none; }++}++@media (max-width: 359px) {++ .hero-stat-strip .hero-stat > i { display: none; }++ .hero-stat-strip .hero-stat { grid-template-columns: minmax(0, 1fr); }++ .hero-stat-strip .hero-stat-meta, .hero-stat-strip .hero-stat-delta { grid-column: 1; }++}++/* Short landscape phones: keep the hero above the fold. */++@media (max-height: 520px) and (orientation: landscape) {++ .hero-stats { min-height: 0; margin-top: 10px; }++ .hero-stat-strip .hero-stat { padding: 5px 8px; }++ .hero-stat-strip .hero-stat > i { width: 22px; height: 22px; }++}++/* Hero rhythm: one consistent vertical beat between title → tagline → buttons → links → receipt → stats. */++.hero-slogan-timer { margin: -4px 0 16px; }++.hero-philosophy-links { margin-top: 18px; }@@@ -935,20 +935,7 @@ main#app { position: relative; z-index: 1; }.brand-text { color: var(--text-primary); }.brand-text strong { color: var(--accent); }--/* Alpha badge — sits above "Streamer" */--.alpha-badge {-- font-size: 0.45rem; font-weight: 800; letter-spacing: 0.08em;-- padding: 1px 4px; border-radius: 3px;-- background: linear-gradient(135deg, #ff6b6b, #ee5a24, #ff6b6b);-- background-size: 200% 200%;-- color: #fff; line-height: 1;-- text-transform: uppercase;-- animation: alpha-pulse 2.5s ease-in-out infinite;-- box-shadow: 0 0 6px rgba(238, 90, 36, 0.35);-- margin-bottom: -1px;--}--@keyframes alpha-pulse {-- 0%, 100% { background-position: 0% 50%; box-shadow: 0 0 8px rgba(238, 90, 36, 0.4); }++50% { background-position: 100% 50%; box-shadow: 0 0 14px rgba(238, 90, 36, 0.7); }}@@@ -4933,16 +4920,21 @@ textarea.form-input { resize: vertical; min-height: 60px; }}/* 1024px: Hamburger menu + hide nav links (catches Steam Deck & small laptops) */++/* Phones and small tablets: the hamburger replaces the link row. Between 820 and 1024px the++ links fit (or scroll), so showing a menu button there only hid navigation that had room. */++@media (max-width: 820px) {++ .nav-links { display: none; }++ .nav-links-wrap { display: contents; }++ .nav-links-wrap .nav-scroll-btn { display: none !important; }++ .nav-hamburger { display: inline-flex; }++}@media (max-width: 1024px) {.navbar { gap: 8px; padding: 0 10px; }.nav-brand { font-size: 1.1rem; }.nav-brand i { font-size: 1.2rem; }-- /* Hide inline nav links — hamburger takes over */-- .nav-links { display: none; }-- .nav-links-wrap { display: contents; }-- .nav-links-wrap .nav-scroll-btn { display: none !important; }-- .nav-hamburger { display: flex; }++ /* (nav links stay visible here — the row scrolls when it overflows; the hamburger only++ takes over below 820px, see the block right after this one.) *//* ── Mobile nav menu ──────────────────────────────────────────────────────This was a full-bleed sheet pinned to both edges of the screen, with the items centred@@@ -5106,7 +5098,6 @@ textarea.form-input { resize: vertical; min-height: 60px; }/* 900px: Compact currency, hide brand text (keep "OpenVibe" on mobile) */@media (max-width: 900px) {-- .brand-text-wrap .alpha-badge { display: none; }.brand-text-wrap .brand-streamer { display: none; }.brand-text-wrap { flex-direction: row; align-items: center; }.brand-text { font-size: 1rem; font-weight: 700; }@@@ -10820,7 +10811,7 @@ label.staff-inline-toggle.aiv-field { flex-direction:row; align-items:center; }.arena-topic-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 12px; }.arena-topic { display: flex; flex-direction: column; gap: 8px; padding: 14px; border-radius: 16px; background: var(--bg-card); border: 1px solid var(--border); position: relative; }.arena-topic-debate { border-left: 4px solid #60a5fa; }--.arena-topic-phrase { border-left: 4px solid #a78bfa; }++.arena-topic-phrase { border-left: 4px solid var(--accent-light, var(--accent)); }.arena-topic-bounty { border-left: 4px solid #f5c542; }.arena-topic-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 0.75rem; color: var(--text-muted); }.arena-kind { display: inline-flex; align-items: center; gap: 5px; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; font-size: 0.68rem; color: var(--accent-light); }@@@ -11530,7 +11521,9 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srgfont-size: clamp(0.75rem, 1.8vw, 0.9rem); font-weight: 650;overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;}--.nav-hamburger { height: 34px; width: 34px; display: inline-flex; align-items: center; justify-content: center; flex: none; }++/* Size only — whether it shows is decided by the ≤820px block (it was display:inline-flex here,++ which put a menu button on every desktop navbar next to the full link row). */++.nav-hamburger { height: 34px; width: 34px; align-items: center; justify-content: center; flex: none; }/* The menu itself: anchored to the avatar, never wider than the screen, and scrollable ratherthan running off the bottom on a short screen or a landscape phone. */@@@ -11689,28 +11682,7 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srganimation-play-state: paused !important;}--/* The ALPHA badge is in the navbar, so it is on screen for the whole session — and it animated-- background-position and box-shadow together, the two most expensive things to animate. The-- gradient now slides on a transform inside a clipped pseudo-element and the glow is an opacity-- fade on a second one, so the same effect composites instead of repainting. */--.alpha-badge { position: relative; isolation: isolate; overflow: hidden; animation: none !important; }--.alpha-badge > * { position: relative; z-index: 2; }--.alpha-badge::before {-- content: ''; position: absolute; top: 0; left: -50%; width: 200%; bottom: 0; z-index: 0;-- background: linear-gradient(135deg, #ff6b6b, #ee5a24, #ff6b6b, #ee5a24);-- background-size: 50% 100%;-- animation: alphaSlide 2.5s ease-in-out infinite;--}--.alpha-badge::after {-- content: ''; position: absolute; inset: -3px; z-index: -1; border-radius: inherit;-- box-shadow: 0 0 14px rgba(238, 90, 36, 0.7);-- animation: alphaGlow 2.5s ease-in-out infinite;--}--@keyframes alphaSlide { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(-25%); } }--@keyframes alphaGlow { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } }--@media (prefers-reduced-motion: reduce) {-- .alpha-badge::before, .alpha-badge::after { animation: none !important; }--}++/* ═══════════════════════════════════════════════════════════════The account menu.@@@ -8,7 +8,7 @@<meta name="description" content="Open source live streaming platform, run by its community. Restream to Twitch, YouTube, Kick, and more. VODs, clips, chat moderation, voice channels, emotes, analytics, and 200+ community tools."><meta name="keywords" content="live streaming, restream, streaming platform, twitch, youtube, open source, openvibelive"><meta name="author" content="OpenVibe">-- <meta name="theme-color" content="#8b5cf6">++ <meta name="theme-color" content="#3b82f6"><!-- Open Graph / Facebook --><meta property="og:type" content="website">@@@ -183,7 +183,6 @@<circle class="ov-orbit ov-orbit--tail" r="1.4"><animateMotion dur="3.6s" begin="0.14s" repeatCount="indefinite"><mpath href="#ovn-ring"/></animateMotion></circle></svg></span><span class="brand-text-wrap">-- <span class="alpha-badge">ALPHA</span><span class="brand-text"><span class="brand-openvibe">OpenVibe</span><strong class="brand-streamer">.Live</strong></span></span></a>@@@ -653,7 +652,7 @@<i class="fa-solid fa-arrow-right network-card-arrow"></i></a><a href="https://openvibe.games" class="network-card" target="_blank" rel="noopener">-- <div class="network-card-icon" style="--card-accent:#8b5cf6"><i class="fa-solid fa-hat-wizard"></i></div>++ <div class="network-card-icon" style="--card-accent:var(--accent, #3b82f6)"><i class="fa-solid fa-hat-wizard"></i></div><div class="network-card-info"><strong>Scraplandia</strong><span>Browser RPG & community games</span>@@@ -757,7 +756,7 @@<i class="fa-solid fa-arrow-right network-card-arrow"></i></a><a href="https://openvibe.network" class="network-card" target="_blank" rel="noopener">-- <div class="network-card-icon" style="--card-accent:#8b5cf6"><i class="fa-solid fa-user-circle"></i></div>++ <div class="network-card-icon" style="--card-accent:var(--accent, #3b82f6)"><i class="fa-solid fa-user-circle"></i></div><div class="network-card-info"><strong>My Account</strong><span>Profile, settings & themes</span>@@@ -403,9 +403,9 @@ function _renderHeroStats(stats) {const HEADLINE = [{ key: 'liveNow', metric: 'liveNow', icon: stats.liveNow > 0 ? 'fa-circle' : 'fa-circle-dot', cls: stats.liveNow > 0 ? 'hero-stat--live' : '', num: stats.liveNow, label: 'Live', title: 'Streams live right now', deltaHTML: _baselineDeltaHTML(stats.liveNow, CC.liveAvg24h, CC.livePeak24h) },{ key: 'viewersNow', metric: 'viewersNow', icon: 'fa-eye', cls: stats.viewersNow > 0 ? 'hero-stat--live' : '', num: stats.viewersNow, label: 'Watching', title: 'Viewers watching right now', deltaHTML: _baselineDeltaHTML(stats.viewersNow, CC.viewersAvg24h, CC.viewersPeak24h) },-- { key: 'weeklyActive', metric: 'active', icon: 'fa-fire', num: stats.weeklyActive, label: 'Active · 7d', title: 'People who chatted in the last 7 days', recent: { w: stats.weeklyActive, pw: stats.prevWeeklyActive } },++ { key: 'weeklyActive', metric: 'active', icon: 'fa-fire', num: stats.weeklyActive, label: 'Active', title: 'People who chatted in the last 7 days', recent: { w: stats.weeklyActive, pw: stats.prevWeeklyActive } },{ key: 'users', metric: 'users', icon: 'fa-user-group', num: stats.users, label: 'Users', title: 'Accounts on OpenVibe.Live', recent: R.users },-- { key: 'weeklyVisitors', metric: 'visitors', icon: 'fa-user-plus', num: stats.weeklyVisitors, label: 'Visitors · 7d', title: 'First-time visitors in the last 7 days', recent: { w: stats.weeklyVisitors, pw: stats.prevWeeklyVisitors } },++ { key: 'weeklyVisitors', metric: 'visitors', icon: 'fa-user-plus', num: stats.weeklyVisitors, label: 'Visitors', title: 'First-time visitors in the last 7 days', recent: { w: stats.weeklyVisitors, pw: stats.prevWeeklyVisitors } },{ key: 'anons', metric: 'anons', icon: 'fa-user-secret', num: stats.anons, label: 'Anons', title: 'Anonymous chatters who have been given a name', recent: R.anons },{ key: 'chatMessages', metric: 'messages', icon: 'fa-comments', num: stats.chatMessages, label: 'Messages', title: 'Chat messages sent, all time', recent: R.messages },{ key: 'streamers', metric: 'streamers', icon: 'fa-satellite-dish', num: stats.streamers, label: 'Streamers', title: 'People who have gone live here', recent: R.streamers },@@@ -612,8 +612,11 @@ function _heroBindInteractions(wrap) {chip.addEventListener('mouseenter', () => _heroTipShow(chip));chip.addEventListener('mouseleave', _heroTipHide);}-- chip.addEventListener('focus', () => _heroTipShow(chip));++ // Keyboard focus gets the tip; a tap on a touch screen focuses too, but the tap already++ // opens the stats view, so the tip only got in the way there.++ chip.addEventListener('focus', () => { if (fine || chip.matches(':focus-visible')) _heroTipShow(chip); });chip.addEventListener('blur', _heroTipHide);++ chip.addEventListener('touchstart', _heroTipHide, { passive: true });if (chip.dataset.metric) {const open = () => { _heroTipHide(); if (typeof openStatsNerds === 'function') openStatsNerds(chip); };chip.addEventListener('click', open);@@@ -517,14 +517,14 @@ function showModal(id) {login: `<h3><i class="fa-solid fa-right-to-bracket"></i> Sign In</h3><p style="color:var(--text-muted);margin-bottom:16px">Sign in with your OpenVibe account to continue.</p>-- <a href="/api/auth/sso/login" class="btn btn-lg" style="width:100%;display:flex;align-items:center;justify-content:center;gap:8px;background:linear-gradient(135deg,#8b5cf6,#6d28d9);color:#fff;text-decoration:none;border:none;cursor:pointer">++ <a href="/api/auth/sso/login" class="btn btn-lg" style="width:100%;display:flex;align-items:center;justify-content:center;gap:8px;background:linear-gradient(135deg,var(--accent),#6d28d9);color:#fff;text-decoration:none;border:none;cursor:pointer"><i class="fa-solid fa-network-wired"></i> Sign in with OpenVibe</a><p style="text-align:center;margin-top:12px;font-size:12px;color:var(--text-muted)">Don't have an account? One will be created when you sign in.</p>`,register: `<h3><i class="fa-solid fa-user-plus"></i> Sign Up</h3><p style="color:var(--text-muted);margin-bottom:16px">Create your account on the OpenVibe.</p>-- <a href="/api/auth/sso/login" class="btn btn-lg" style="width:100%;display:flex;align-items:center;justify-content:center;gap:8px;background:linear-gradient(135deg,#8b5cf6,#6d28d9);color:#fff;text-decoration:none;border:none;cursor:pointer">++ <a href="/api/auth/sso/login" class="btn btn-lg" style="width:100%;display:flex;align-items:center;justify-content:center;gap:8px;background:linear-gradient(135deg,var(--accent),#6d28d9);color:#fff;text-decoration:none;border:none;cursor:pointer"><i class="fa-solid fa-network-wired"></i> Sign in with OpenVibe</a><p style="text-align:center;margin-top:12px;font-size:12px;color:var(--text-muted)">Registration is handled on openvibe.network</p>`,@@@ -2492,7 +2492,7 @@ function addConfigButtonModal() {</div><div class="form-group" style="margin:0"><label style="font-size:0.85rem">Border Color</label>-- <input type="text" id="modal-cfgbtn-border" class="form-input form-input-sm" placeholder="#8b5cf6 or var(--accent)">++ <input type="text" id="modal-cfgbtn-border" class="form-input form-input-sm" placeholder="var(--accent) or var(--accent)"></div></div></details>@@@ -2233,10 +2233,41 @@ function _renderHistoryPayload(p) {else _renderChatHistoryData(p.data, p.globalMsgs || []);}++/**++ * Pages without a chat pane (home, VODs, pastes…) only have the floating widget. History used to++ * be fetched exclusively through the main pane's hydrate, so the widget opened empty there and++ * stayed empty until someone typed. Fill it directly: cached copy first, then the network.++ */++let _fcwHydratedAt = 0, _fcwHydrating = false;++async function hydrateWidgetOnly() {++ const container = document.getElementById('fcw-messages');++ if (!container || _fcwHydrating) return;++ if (container.querySelector('.chat-msg') && Date.now() - _fcwHydratedAt < 10 * 60 * 1000) return;++ _fcwHydrating = true;++ const feed = (payload) => {++ const rows = (payload && payload.data && payload.data.messages) || [];++ if (!rows.length) return 0;++ const prev = _loadingHistory; _loadingHistory = true;++ try { rows.slice(-200).forEach((m) => _fcwAddMessage(m)); } finally { _loadingHistory = prev; }++ _fcwScrollToBottom();++ return rows.length;++ };++ try {++ if (!container.querySelector('.chat-msg')) { const cached = _chatCacheRead('global'); if (cached) feed(cached); }++ try { if (typeof loadEmotes === 'function') await loadEmotes(); } catch { /* plain text is fine */ }++ const payload = await _fetchHistoryPayload(null);++ feed(payload);++ _fcwHydratedAt = Date.now();++ try { _chatCacheWrite('global', payload); } catch { /* */ }++ } catch (err) {++ console.warn('[chat] widget history failed:', err && err.message);++ } finally { _fcwHydrating = false; }++}++async function hydrateActiveChatHistory(streamId, { clear = false } = {}) {const chatEl = getChatEl();const { messages } = chatEl;-- if (!messages) return;++ if (!messages) { if (!streamId) hydrateWidgetOnly(); return; }const gen = ++_chatHydrateGen;const room = _chatRoomKey(streamId);_watchChatPin(messages);@@@ -6984,6 +7015,8 @@ function fcwToggle() {if (!chatWs || chatWs.readyState !== WebSocket.OPEN) {initChat(null);}++ // The widget is its own chat surface on pages without a pane: give it the history.++ hydrateWidgetOnly();// Scroll to bottomconst msgs = document.getElementById('fcw-messages');@@@ -6,7 +6,7 @@"scope": "/","display": "standalone","background_color": "#1a1a24",-- "theme_color": "#8b5cf6",++ "theme_color": "#3b82f6","icons": [{"src": "/assets/logo-72.png",
@@ -993,3 +993,80 @@ body.egg-rainbow { animation: eggRainbow 2.6s linear; }
@@ -993,3 +993,80 @@ body.egg-rainbow { animation: eggRainbow 2.6s linear; }
}
}
.cta-chip i { color: var(--accent-light); }
.cta-chip i { color: var(--accent-light); }
.cta-hero { text-shadow: 0 6px 30px color-mix(in srgb, var(--accent) 35%, transparent); }
.cta-hero { text-shadow: 0 6px 30px color-mix(in srgb, var(--accent) 35%, transparent); }
+
+/* ═══ Hero stats strip v3 — compact, no dead space, honest per viewport ═══
+ The headline strip used to be three wide cards per row with the number in one corner, the
+ label under it and the delta pill on a third line: ~80px tall cards that were mostly air.
+ Now: one 9-across row on wide screens (icon, number, label, short delta), three horizontal
+ rows on tablets (icon · number+label · delta right), three tight columns on phones. The
+ reserved min-height matches each layout so nothing jumps while the numbers load. */
+.hero-latest { margin-top: 18px; }
+.hero-stats { margin: 16px 0 10px; gap: 10px; min-height: 108px; }
+.hero-stat-strip {
+ display: grid; grid-template-columns: repeat(9, minmax(0, 1fr)); grid-auto-rows: auto;
+ gap: 8px; max-width: 1240px; margin: 0 auto; width: 100%; padding: 0;
+}
+.hero-stat-strip .hero-stat {
+ display: grid; grid-template-columns: auto minmax(0, 1fr); grid-template-rows: auto auto;
+ column-gap: 8px; row-gap: 3px; align-items: center; align-content: center;
+ padding: 8px 10px; border-radius: 12px; min-height: 0;
+ background: color-mix(in srgb, var(--bg-secondary) 74%, transparent);
+ border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
+}
+.hero-stat-strip .hero-stat > i {
+ grid-row: 1 / 3; grid-column: 1; width: 28px; height: 28px; display: grid; place-items: center;
+ border-radius: 9px; font-size: 0.74rem; color: var(--accent);
+ background: color-mix(in srgb, var(--accent) 13%, transparent);
+ border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
+}
+.hero-stat-strip .hero-stat--live > i { color: var(--live-red); background: color-mix(in srgb, var(--live-red) 15%, transparent); border-color: color-mix(in srgb, var(--live-red) 34%, transparent); }
+.hero-stat-strip .hero-stat-meta { grid-column: 2; grid-row: 1; display: flex; flex-direction: column; gap: 1px; min-width: 0; }
+.hero-stat-strip .hero-stat-num { font-size: 1.06rem; line-height: 1.05; letter-spacing: -0.02em; }
+.hero-stat-strip .hero-stat-label { font-size: 0.56rem; letter-spacing: 0.06em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.1; }
+.hero-stat-strip .hero-stat-sub { display: none; }
+.hero-stat-strip .hero-stat-delta {
+ grid-column: 2; grid-row: 2; justify-self: start; margin: 0; max-width: 100%;
+ font-size: 0.55rem; padding: 1px 6px 1px 5px; overflow: hidden; text-overflow: ellipsis;
+}
+/* "vs 24h" / "7d" suffixes live in the tooltip; the pill keeps the number and the arrow. */
+.hero-stat-strip .hero-stat-delta em { display: none; }
+.hero-stat-strip .hero-stat-trend { margin-left: 4px; padding-left: 4px; }
+.hero-stat-strip .hero-stat-chart-hint { font-size: 0.5rem; margin-left: 2px; }
+/* Tablets and small laptops: three horizontal rows — icon · number + label · delta on the right. */
+@media (max-width: 1279px) {
+ .hero-stats { min-height: 176px; }
+ .hero-stat-strip { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; max-width: 960px; }
+ .hero-stat-strip .hero-stat { grid-template-columns: auto minmax(0, 1fr) auto; grid-template-rows: auto; padding: 8px 12px; }
+ .hero-stat-strip .hero-stat > i { grid-row: 1; }
+ .hero-stat-strip .hero-stat-meta { grid-row: 1; flex-direction: row; align-items: baseline; gap: 7px; }
+ .hero-stat-strip .hero-stat-num { font-size: 1.12rem; }
+ .hero-stat-strip .hero-stat-label { font-size: 0.6rem; }
+ .hero-stat-strip .hero-stat-delta { grid-column: 3; grid-row: 1; justify-self: end; }
+ .hero-stat-strip .hero-stat-delta em { display: inline; }
+}
+/* Phones: three tight columns, number over label, short pill. */
+@media (max-width: 699px) {
+ .hero-stats { min-height: 214px; margin-top: 14px; }
+ .hero-stat-strip { gap: 6px; }
+ .hero-stat-strip .hero-stat { grid-template-columns: auto minmax(0, 1fr); grid-template-rows: auto auto; padding: 7px 8px; column-gap: 6px; row-gap: 2px; border-radius: 10px; }
+ .hero-stat-strip .hero-stat > i { grid-row: 1 / 3; width: 24px; height: 24px; font-size: 0.66rem; border-radius: 7px; }
+ .hero-stat-strip .hero-stat-meta { grid-row: 1; flex-direction: column; gap: 1px; }
+ .hero-stat-strip .hero-stat-num { font-size: clamp(0.9rem, 4.2vw, 1.05rem); }
+ .hero-stat-strip .hero-stat-label { font-size: 0.5rem; }
+ .hero-stat-strip .hero-stat-delta { grid-column: 2; grid-row: 2; justify-self: start; font-size: 0.5rem; padding: 1px 5px; }
+ .hero-stat-strip .hero-stat-delta em { display: none; }
+}
+@media (max-width: 359px) {
+ .hero-stat-strip .hero-stat > i { display: none; }
+ .hero-stat-strip .hero-stat { grid-template-columns: minmax(0, 1fr); }
+ .hero-stat-strip .hero-stat-meta, .hero-stat-strip .hero-stat-delta { grid-column: 1; }
+}
+/* Short landscape phones: keep the hero above the fold. */
+@media (max-height: 520px) and (orientation: landscape) {
+ .hero-stats { min-height: 0; margin-top: 10px; }
+ .hero-stat-strip .hero-stat { padding: 5px 8px; }
+ .hero-stat-strip .hero-stat > i { width: 22px; height: 22px; }
+}
+/* Hero rhythm: one consistent vertical beat between title → tagline → buttons → links → receipt → stats. */
+.hero-slogan-timer { margin: -4px 0 16px; }
+.hero-philosophy-links { margin-top: 18px; }
@@ -935,20 +935,7 @@ main#app { position: relative; z-index: 1; }
@@ -935,20 +935,7 @@ main#app { position: relative; z-index: 1; }
.brand-text { color: var(--text-primary); }
.brand-text { color: var(--text-primary); }
.brand-text strong { color: var(--accent); }
.brand-text strong { color: var(--accent); }
-/* Alpha badge — sits above "Streamer" */
-.alpha-badge {
- font-size: 0.45rem; font-weight: 800; letter-spacing: 0.08em;
- padding: 1px 4px; border-radius: 3px;
- background: linear-gradient(135deg, #ff6b6b, #ee5a24, #ff6b6b);
- background-size: 200% 200%;
- color: #fff; line-height: 1;
- text-transform: uppercase;
- animation: alpha-pulse 2.5s ease-in-out infinite;
- box-shadow: 0 0 6px rgba(238, 90, 36, 0.35);
- margin-bottom: -1px;
-}
-@keyframes alpha-pulse {
- 0%, 100% { background-position: 0% 50%; box-shadow: 0 0 8px rgba(238, 90, 36, 0.4); }
+
50% { background-position: 100% 50%; box-shadow: 0 0 14px rgba(238, 90, 36, 0.7); }
50% { background-position: 100% 50%; box-shadow: 0 0 14px rgba(238, 90, 36, 0.7); }
}
}
@@ -4933,16 +4920,21 @@ textarea.form-input { resize: vertical; min-height: 60px; }
@@ -4933,16 +4920,21 @@ textarea.form-input { resize: vertical; min-height: 60px; }
}
}
/* 1024px: Hamburger menu + hide nav links (catches Steam Deck & small laptops) */
/* 1024px: Hamburger menu + hide nav links (catches Steam Deck & small laptops) */
+/* Phones and small tablets: the hamburger replaces the link row. Between 820 and 1024px the
+ links fit (or scroll), so showing a menu button there only hid navigation that had room. */
+@media (max-width: 820px) {
+ .nav-links { display: none; }
+ .nav-links-wrap { display: contents; }
+ .nav-links-wrap .nav-scroll-btn { display: none !important; }
+ .nav-hamburger { display: inline-flex; }
+}
@media (max-width: 1024px) {
@media (max-width: 1024px) {
.navbar { gap: 8px; padding: 0 10px; }
.navbar { gap: 8px; padding: 0 10px; }
.nav-brand { font-size: 1.1rem; }
.nav-brand { font-size: 1.1rem; }
.nav-brand i { font-size: 1.2rem; }
.nav-brand i { font-size: 1.2rem; }
- /* Hide inline nav links — hamburger takes over */
- .nav-links { display: none; }
- .nav-links-wrap { display: contents; }
- .nav-links-wrap .nav-scroll-btn { display: none !important; }
- .nav-hamburger { display: flex; }
+ /* (nav links stay visible here — the row scrolls when it overflows; the hamburger only
+ takes over below 820px, see the block right after this one.) */
/* ── Mobile nav menu ──────────────────────────────────────────────────────
/* ── Mobile nav menu ──────────────────────────────────────────────────────
This was a full-bleed sheet pinned to both edges of the screen, with the items centred
This was a full-bleed sheet pinned to both edges of the screen, with the items centred
@@ -5106,7 +5098,6 @@ textarea.form-input { resize: vertical; min-height: 60px; }
@@ -5106,7 +5098,6 @@ textarea.form-input { resize: vertical; min-height: 60px; }
/* 900px: Compact currency, hide brand text (keep "OpenVibe" on mobile) */
/* 900px: Compact currency, hide brand text (keep "OpenVibe" on mobile) */
@media (max-width: 900px) {
@media (max-width: 900px) {
- .brand-text-wrap .alpha-badge { display: none; }
.brand-text-wrap .brand-streamer { display: none; }
.brand-text-wrap .brand-streamer { display: none; }
.brand-text-wrap { flex-direction: row; align-items: center; }
.brand-text-wrap { flex-direction: row; align-items: center; }
.brand-text { font-size: 1rem; font-weight: 700; }
.brand-text { font-size: 1rem; font-weight: 700; }
@@ -10820,7 +10811,7 @@ label.staff-inline-toggle.aiv-field { flex-direction:row; align-items:center; }
@@ -10820,7 +10811,7 @@ label.staff-inline-toggle.aiv-field { flex-direction:row; align-items:center; }
.arena-topic-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 12px; }
.arena-topic-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 12px; }
.arena-topic { display: flex; flex-direction: column; gap: 8px; padding: 14px; border-radius: 16px; background: var(--bg-card); border: 1px solid var(--border); position: relative; }
.arena-topic { display: flex; flex-direction: column; gap: 8px; padding: 14px; border-radius: 16px; background: var(--bg-card); border: 1px solid var(--border); position: relative; }
.arena-topic-debate { border-left: 4px solid #60a5fa; }
.arena-topic-debate { border-left: 4px solid #60a5fa; }
-.arena-topic-phrase { border-left: 4px solid #a78bfa; }
+.arena-topic-phrase { border-left: 4px solid var(--accent-light, var(--accent)); }
.arena-topic-bounty { border-left: 4px solid #f5c542; }
.arena-topic-bounty { border-left: 4px solid #f5c542; }
.arena-topic-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 0.75rem; color: var(--text-muted); }
.arena-topic-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 0.75rem; color: var(--text-muted); }
.arena-kind { display: inline-flex; align-items: center; gap: 5px; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; font-size: 0.68rem; color: var(--accent-light); }
.arena-kind { display: inline-flex; align-items: center; gap: 5px; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; font-size: 0.68rem; color: var(--accent-light); }
@@ -11530,7 +11521,9 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srg
@@ -11530,7 +11521,9 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srg
font-size: clamp(0.75rem, 1.8vw, 0.9rem); font-weight: 650;
font-size: clamp(0.75rem, 1.8vw, 0.9rem); font-weight: 650;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
}
-.nav-hamburger { height: 34px; width: 34px; display: inline-flex; align-items: center; justify-content: center; flex: none; }
+/* Size only — whether it shows is decided by the ≤820px block (it was display:inline-flex here,
+ which put a menu button on every desktop navbar next to the full link row). */
+.nav-hamburger { height: 34px; width: 34px; align-items: center; justify-content: center; flex: none; }
/* The menu itself: anchored to the avatar, never wider than the screen, and scrollable rather
/* The menu itself: anchored to the avatar, never wider than the screen, and scrollable rather
than running off the bottom on a short screen or a landscape phone. */
than running off the bottom on a short screen or a landscape phone. */
@@ -11689,28 +11682,7 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srg
@@ -11689,28 +11682,7 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srg
animation-play-state: paused !important;
animation-play-state: paused !important;
}
}
-/* The ALPHA badge is in the navbar, so it is on screen for the whole session — and it animated
- background-position and box-shadow together, the two most expensive things to animate. The
- gradient now slides on a transform inside a clipped pseudo-element and the glow is an opacity
- fade on a second one, so the same effect composites instead of repainting. */
-.alpha-badge { position: relative; isolation: isolate; overflow: hidden; animation: none !important; }
-.alpha-badge > * { position: relative; z-index: 2; }
-.alpha-badge::before {
- content: ''; position: absolute; top: 0; left: -50%; width: 200%; bottom: 0; z-index: 0;
- background: linear-gradient(135deg, #ff6b6b, #ee5a24, #ff6b6b, #ee5a24);
- background-size: 50% 100%;
- animation: alphaSlide 2.5s ease-in-out infinite;
-}
-.alpha-badge::after {
- content: ''; position: absolute; inset: -3px; z-index: -1; border-radius: inherit;
- box-shadow: 0 0 14px rgba(238, 90, 36, 0.7);
- animation: alphaGlow 2.5s ease-in-out infinite;
-}
-@keyframes alphaSlide { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(-25%); } }
-@keyframes alphaGlow { 0%, 100% { opacity: 0.45; } 50% { opacity: 1; } }
-@media (prefers-reduced-motion: reduce) {
- .alpha-badge::before, .alpha-badge::after { animation: none !important; }
-}
+
/* ═══════════════════════════════════════════════════════════════
/* ═══════════════════════════════════════════════════════════════
The account menu.
The account menu.
@@ -8,7 +8,7 @@
@@ -8,7 +8,7 @@
<meta name="description" content="Open source live streaming platform, run by its community. Restream to Twitch, YouTube, Kick, and more. VODs, clips, chat moderation, voice channels, emotes, analytics, and 200+ community tools.">
<meta name="description" content="Open source live streaming platform, run by its community. Restream to Twitch, YouTube, Kick, and more. VODs, clips, chat moderation, voice channels, emotes, analytics, and 200+ community tools.">
<meta name="keywords" content="live streaming, restream, streaming platform, twitch, youtube, open source, openvibelive">
<meta name="keywords" content="live streaming, restream, streaming platform, twitch, youtube, open source, openvibelive">
<meta name="author" content="OpenVibe">
<meta name="author" content="OpenVibe">
- <meta name="theme-color" content="#8b5cf6">
+ <meta name="theme-color" content="#3b82f6">
<!-- Open Graph / Facebook -->
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:type" content="website">
@@ -183,7 +183,6 @@
@@ -183,7 +183,6 @@
<circle class="ov-orbit ov-orbit--tail" r="1.4"><animateMotion dur="3.6s" begin="0.14s" repeatCount="indefinite"><mpath href="#ovn-ring"/></animateMotion></circle>
<circle class="ov-orbit ov-orbit--tail" r="1.4"><animateMotion dur="3.6s" begin="0.14s" repeatCount="indefinite"><mpath href="#ovn-ring"/></animateMotion></circle>
</svg></span>
</svg></span>
<span class="brand-text-wrap">
<span class="brand-text-wrap">
- <span class="alpha-badge">ALPHA</span>
<span class="brand-text"><span class="brand-openvibe">OpenVibe</span><strong class="brand-streamer">.Live</strong></span>
<span class="brand-text"><span class="brand-openvibe">OpenVibe</span><strong class="brand-streamer">.Live</strong></span>
</span>
</span>
</a>
</a>
@@ -653,7 +652,7 @@
@@ -653,7 +652,7 @@
<i class="fa-solid fa-arrow-right network-card-arrow"></i>
<i class="fa-solid fa-arrow-right network-card-arrow"></i>
</a>
</a>
<a href="https://openvibe.games" class="network-card" target="_blank" rel="noopener">
<a href="https://openvibe.games" class="network-card" target="_blank" rel="noopener">
- <div class="network-card-icon" style="--card-accent:#8b5cf6"><i class="fa-solid fa-hat-wizard"></i></div>
+ <div class="network-card-icon" style="--card-accent:var(--accent, #3b82f6)"><i class="fa-solid fa-hat-wizard"></i></div>
<div class="network-card-info">
<div class="network-card-info">
<strong>Scraplandia</strong>
<strong>Scraplandia</strong>
<span>Browser RPG & community games</span>
<span>Browser RPG & community games</span>
@@ -757,7 +756,7 @@
@@ -757,7 +756,7 @@
<i class="fa-solid fa-arrow-right network-card-arrow"></i>
<i class="fa-solid fa-arrow-right network-card-arrow"></i>
</a>
</a>
<a href="https://openvibe.network" class="network-card" target="_blank" rel="noopener">
<a href="https://openvibe.network" class="network-card" target="_blank" rel="noopener">
- <div class="network-card-icon" style="--card-accent:#8b5cf6"><i class="fa-solid fa-user-circle"></i></div>
+ <div class="network-card-icon" style="--card-accent:var(--accent, #3b82f6)"><i class="fa-solid fa-user-circle"></i></div>
<div class="network-card-info">
<div class="network-card-info">
<strong>My Account</strong>
<strong>My Account</strong>
<span>Profile, settings & themes</span>
<span>Profile, settings & themes</span>
@@ -403,9 +403,9 @@ function _renderHeroStats(stats) {
@@ -403,9 +403,9 @@ function _renderHeroStats(stats) {
const HEADLINE = [
const HEADLINE = [
{ key: 'liveNow', metric: 'liveNow', icon: stats.liveNow > 0 ? 'fa-circle' : 'fa-circle-dot', cls: stats.liveNow > 0 ? 'hero-stat--live' : '', num: stats.liveNow, label: 'Live', title: 'Streams live right now', deltaHTML: _baselineDeltaHTML(stats.liveNow, CC.liveAvg24h, CC.livePeak24h) },
{ key: 'liveNow', metric: 'liveNow', icon: stats.liveNow > 0 ? 'fa-circle' : 'fa-circle-dot', cls: stats.liveNow > 0 ? 'hero-stat--live' : '', num: stats.liveNow, label: 'Live', title: 'Streams live right now', deltaHTML: _baselineDeltaHTML(stats.liveNow, CC.liveAvg24h, CC.livePeak24h) },
{ key: 'viewersNow', metric: 'viewersNow', icon: 'fa-eye', cls: stats.viewersNow > 0 ? 'hero-stat--live' : '', num: stats.viewersNow, label: 'Watching', title: 'Viewers watching right now', deltaHTML: _baselineDeltaHTML(stats.viewersNow, CC.viewersAvg24h, CC.viewersPeak24h) },
{ key: 'viewersNow', metric: 'viewersNow', icon: 'fa-eye', cls: stats.viewersNow > 0 ? 'hero-stat--live' : '', num: stats.viewersNow, label: 'Watching', title: 'Viewers watching right now', deltaHTML: _baselineDeltaHTML(stats.viewersNow, CC.viewersAvg24h, CC.viewersPeak24h) },
- { key: 'weeklyActive', metric: 'active', icon: 'fa-fire', num: stats.weeklyActive, label: 'Active · 7d', title: 'People who chatted in the last 7 days', recent: { w: stats.weeklyActive, pw: stats.prevWeeklyActive } },
+ { key: 'weeklyActive', metric: 'active', icon: 'fa-fire', num: stats.weeklyActive, label: 'Active', title: 'People who chatted in the last 7 days', recent: { w: stats.weeklyActive, pw: stats.prevWeeklyActive } },
{ key: 'users', metric: 'users', icon: 'fa-user-group', num: stats.users, label: 'Users', title: 'Accounts on OpenVibe.Live', recent: R.users },
{ key: 'users', metric: 'users', icon: 'fa-user-group', num: stats.users, label: 'Users', title: 'Accounts on OpenVibe.Live', recent: R.users },
- { key: 'weeklyVisitors', metric: 'visitors', icon: 'fa-user-plus', num: stats.weeklyVisitors, label: 'Visitors · 7d', title: 'First-time visitors in the last 7 days', recent: { w: stats.weeklyVisitors, pw: stats.prevWeeklyVisitors } },
+ { key: 'weeklyVisitors', metric: 'visitors', icon: 'fa-user-plus', num: stats.weeklyVisitors, label: 'Visitors', title: 'First-time visitors in the last 7 days', recent: { w: stats.weeklyVisitors, pw: stats.prevWeeklyVisitors } },
{ key: 'anons', metric: 'anons', icon: 'fa-user-secret', num: stats.anons, label: 'Anons', title: 'Anonymous chatters who have been given a name', recent: R.anons },
{ key: 'anons', metric: 'anons', icon: 'fa-user-secret', num: stats.anons, label: 'Anons', title: 'Anonymous chatters who have been given a name', recent: R.anons },
{ key: 'chatMessages', metric: 'messages', icon: 'fa-comments', num: stats.chatMessages, label: 'Messages', title: 'Chat messages sent, all time', recent: R.messages },
{ key: 'chatMessages', metric: 'messages', icon: 'fa-comments', num: stats.chatMessages, label: 'Messages', title: 'Chat messages sent, all time', recent: R.messages },
{ key: 'streamers', metric: 'streamers', icon: 'fa-satellite-dish', num: stats.streamers, label: 'Streamers', title: 'People who have gone live here', recent: R.streamers },
{ key: 'streamers', metric: 'streamers', icon: 'fa-satellite-dish', num: stats.streamers, label: 'Streamers', title: 'People who have gone live here', recent: R.streamers },
@@ -612,8 +612,11 @@ function _heroBindInteractions(wrap) {
@@ -612,8 +612,11 @@ function _heroBindInteractions(wrap) {
chip.addEventListener('mouseenter', () => _heroTipShow(chip));
chip.addEventListener('mouseenter', () => _heroTipShow(chip));
chip.addEventListener('mouseleave', _heroTipHide);
chip.addEventListener('mouseleave', _heroTipHide);
}
}
- chip.addEventListener('focus', () => _heroTipShow(chip));
+ // Keyboard focus gets the tip; a tap on a touch screen focuses too, but the tap already
+ // opens the stats view, so the tip only got in the way there.
+ chip.addEventListener('focus', () => { if (fine || chip.matches(':focus-visible')) _heroTipShow(chip); });
chip.addEventListener('blur', _heroTipHide);
chip.addEventListener('blur', _heroTipHide);
+ chip.addEventListener('touchstart', _heroTipHide, { passive: true });
if (chip.dataset.metric) {
if (chip.dataset.metric) {
const open = () => { _heroTipHide(); if (typeof openStatsNerds === 'function') openStatsNerds(chip); };
const open = () => { _heroTipHide(); if (typeof openStatsNerds === 'function') openStatsNerds(chip); };
chip.addEventListener('click', open);
chip.addEventListener('click', open);
@@ -517,14 +517,14 @@ function showModal(id) {
@@ -517,14 +517,14 @@ function showModal(id) {
login: `
login: `
<h3><i class="fa-solid fa-right-to-bracket"></i> Sign In</h3>
<h3><i class="fa-solid fa-right-to-bracket"></i> Sign In</h3>
<p style="color:var(--text-muted);margin-bottom:16px">Sign in with your OpenVibe account to continue.</p>
<p style="color:var(--text-muted);margin-bottom:16px">Sign in with your OpenVibe account to continue.</p>
- <a href="/api/auth/sso/login" class="btn btn-lg" style="width:100%;display:flex;align-items:center;justify-content:center;gap:8px;background:linear-gradient(135deg,#8b5cf6,#6d28d9);color:#fff;text-decoration:none;border:none;cursor:pointer">
+ <a href="/api/auth/sso/login" class="btn btn-lg" style="width:100%;display:flex;align-items:center;justify-content:center;gap:8px;background:linear-gradient(135deg,var(--accent),#6d28d9);color:#fff;text-decoration:none;border:none;cursor:pointer">
<i class="fa-solid fa-network-wired"></i> Sign in with OpenVibe
<i class="fa-solid fa-network-wired"></i> Sign in with OpenVibe
</a>
</a>
<p style="text-align:center;margin-top:12px;font-size:12px;color:var(--text-muted)">Don't have an account? One will be created when you sign in.</p>`,
<p style="text-align:center;margin-top:12px;font-size:12px;color:var(--text-muted)">Don't have an account? One will be created when you sign in.</p>`,
register: `
register: `
<h3><i class="fa-solid fa-user-plus"></i> Sign Up</h3>
<h3><i class="fa-solid fa-user-plus"></i> Sign Up</h3>
<p style="color:var(--text-muted);margin-bottom:16px">Create your account on the OpenVibe.</p>
<p style="color:var(--text-muted);margin-bottom:16px">Create your account on the OpenVibe.</p>
- <a href="/api/auth/sso/login" class="btn btn-lg" style="width:100%;display:flex;align-items:center;justify-content:center;gap:8px;background:linear-gradient(135deg,#8b5cf6,#6d28d9);color:#fff;text-decoration:none;border:none;cursor:pointer">
+ <a href="/api/auth/sso/login" class="btn btn-lg" style="width:100%;display:flex;align-items:center;justify-content:center;gap:8px;background:linear-gradient(135deg,var(--accent),#6d28d9);color:#fff;text-decoration:none;border:none;cursor:pointer">
<i class="fa-solid fa-network-wired"></i> Sign in with OpenVibe
<i class="fa-solid fa-network-wired"></i> Sign in with OpenVibe
</a>
</a>
<p style="text-align:center;margin-top:12px;font-size:12px;color:var(--text-muted)">Registration is handled on openvibe.network</p>`,
<p style="text-align:center;margin-top:12px;font-size:12px;color:var(--text-muted)">Registration is handled on openvibe.network</p>`,
@@ -2492,7 +2492,7 @@ function addConfigButtonModal() {
@@ -2492,7 +2492,7 @@ function addConfigButtonModal() {
</div>
</div>
<div class="form-group" style="margin:0">
<div class="form-group" style="margin:0">
<label style="font-size:0.85rem">Border Color</label>
<label style="font-size:0.85rem">Border Color</label>
- <input type="text" id="modal-cfgbtn-border" class="form-input form-input-sm" placeholder="#8b5cf6 or var(--accent)">
+ <input type="text" id="modal-cfgbtn-border" class="form-input form-input-sm" placeholder="var(--accent) or var(--accent)">
</div>
</div>
</div>
</div>
</details>
</details>
@@ -2233,10 +2233,41 @@ function _renderHistoryPayload(p) {
@@ -2233,10 +2233,41 @@ function _renderHistoryPayload(p) {
else _renderChatHistoryData(p.data, p.globalMsgs || []);
else _renderChatHistoryData(p.data, p.globalMsgs || []);
}
}
+/**
+ * Pages without a chat pane (home, VODs, pastes…) only have the floating widget. History used to
+ * be fetched exclusively through the main pane's hydrate, so the widget opened empty there and
+ * stayed empty until someone typed. Fill it directly: cached copy first, then the network.
+ */
+let _fcwHydratedAt = 0, _fcwHydrating = false;
+async function hydrateWidgetOnly() {
+ const container = document.getElementById('fcw-messages');
+ if (!container || _fcwHydrating) return;
+ if (container.querySelector('.chat-msg') && Date.now() - _fcwHydratedAt < 10 * 60 * 1000) return;
+ _fcwHydrating = true;
+ const feed = (payload) => {
+ const rows = (payload && payload.data && payload.data.messages) || [];
+ if (!rows.length) return 0;
+ const prev = _loadingHistory; _loadingHistory = true;
+ try { rows.slice(-200).forEach((m) => _fcwAddMessage(m)); } finally { _loadingHistory = prev; }
+ _fcwScrollToBottom();
+ return rows.length;
+ };
+ try {
+ if (!container.querySelector('.chat-msg')) { const cached = _chatCacheRead('global'); if (cached) feed(cached); }
+ try { if (typeof loadEmotes === 'function') await loadEmotes(); } catch { /* plain text is fine */ }
+ const payload = await _fetchHistoryPayload(null);
+ feed(payload);
+ _fcwHydratedAt = Date.now();
+ try { _chatCacheWrite('global', payload); } catch { /* */ }
+ } catch (err) {
+ console.warn('[chat] widget history failed:', err && err.message);
+ } finally { _fcwHydrating = false; }
+}
+
async function hydrateActiveChatHistory(streamId, { clear = false } = {}) {
async function hydrateActiveChatHistory(streamId, { clear = false } = {}) {
const chatEl = getChatEl();
const chatEl = getChatEl();
const { messages } = chatEl;
const { messages } = chatEl;
- if (!messages) return;
+ if (!messages) { if (!streamId) hydrateWidgetOnly(); return; }
const gen = ++_chatHydrateGen;
const gen = ++_chatHydrateGen;
const room = _chatRoomKey(streamId);
const room = _chatRoomKey(streamId);
_watchChatPin(messages);
_watchChatPin(messages);
@@ -6984,6 +7015,8 @@ function fcwToggle() {
@@ -6984,6 +7015,8 @@ function fcwToggle() {
if (!chatWs || chatWs.readyState !== WebSocket.OPEN) {
if (!chatWs || chatWs.readyState !== WebSocket.OPEN) {
initChat(null);
initChat(null);
}
}
+ // The widget is its own chat surface on pages without a pane: give it the history.
+ hydrateWidgetOnly();
// Scroll to bottom
// Scroll to bottom
const msgs = document.getElementById('fcw-messages');
const msgs = document.getElementById('fcw-messages');
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
"scope": "/",
"scope": "/",
"display": "standalone",
"display": "standalone",
"background_color": "#1a1a24",
"background_color": "#1a1a24",
- "theme_color": "#8b5cf6",
+ "theme_color": "#3b82f6",
"icons": [
"icons": [
{
{
"src": "/assets/logo-72.png",
"src": "/assets/logo-72.png",