openvibers/openvibe.live Commits

Home hero: compact responsive stats strip, no ALPHA badge, blue theme colour, widget history, hamburger only on small screens

OP OpenVibers committed 18 Sept 2026 · 399ee943d6 · 7 files, +140 −56

- hero stats strip v3: 9 compact chips in one row on wide screens, 3 horizontal rows on
  tablets, 3 tight columns on phones, landscape-phone variant; reserved heights match each
  layout; "shipped" receipt and strip get real breathing room; hero spacing evened out
- stat tooltip no longer pops on a touch tap (the tap opens the stats view)
- the floating Global Chat widget loads history on pages without a chat pane (home, VODs…):
  it used to open empty until someone typed, because only the main pane fetched history
- navbar: the hamburger is shown only ≤820px (a size-only rule had made it display on every
  desktop navbar; between 820 and 1024 the full link row fits and now stays)
- ALPHA badge removed from the brand; theme-color/manifest and the last hard-coded violet
  accents follow the blue Vibe default / theme tokens

Changes

ddiff --git a/public/css/features/home.css b/public/css/features/home.css
iindex 6b3639cc..d289bce8 100644
---- a/public/css/features/home.css
++++ b/public/css/features/home.css
@@@ -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; }
ddiff --git a/public/css/style.css b/public/css/style.css
iindex bdf9c05b..1510edc9 100644
---- a/public/css/style.css
++++ b/public/css/style.css
@@@ -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 srg
font-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 rather
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
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.
ddiff --git a/public/index.html b/public/index.html
iindex 9d1f42e3..d121b0c3 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -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 &amp; 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 &amp; themes</span>
ddiff --git a/public/js/app-home.js b/public/js/app-home.js
iindex e2fb340b..d23496f4 100644
---- a/public/js/app-home.js
++++ b/public/js/app-home.js
@@@ -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);
ddiff --git a/public/js/app.js b/public/js/app.js
iindex 7fda82d2..10e3ba06 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -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>
ddiff --git a/public/js/chat.js b/public/js/chat.js
iindex 9e3acd61..cfffaacc 100644
---- a/public/js/chat.js
++++ b/public/js/chat.js
@@@ -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 bottom
const msgs = document.getElementById('fcw-messages');
ddiff --git a/public/manifest.webmanifest b/public/manifest.webmanifest
iindex 87ca01f9..777193f4 100644
---- a/public/manifest.webmanifest
++++ b/public/manifest.webmanifest
@@@ -6,7 +6,7 @@
"scope": "/",
"display": "standalone",
"background_color": "#1a1a24",
-- "theme_color": "#8b5cf6",
++ "theme_color": "#3b82f6",
"icons": [
{
"src": "/assets/logo-72.png",