openvibers/openvibe.live Commits

Offline discover board → full-width dashboard grid with item caps; hero buttons toned down + polished; animated navbar brand mark (nodes, live edges, packets)

OP OpenVibers committed 15 Sept 2026 · da514807fb · 5 files, +75 −35

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

Changes

ddiff --git a/public/css/context-cards.css b/public/css/context-cards.css
iindex c4db149b..ac3eb3ec 100644
---- a/public/css/context-cards.css
++++ b/public/css/context-cards.css
@@@ -53,17 +53,20 @@
.cc-top small { color: var(--text-muted); }
@media (max-width: 720px) { .cc-main { grid-template-columns: 1fr; } .cc-title { white-space: normal; } }
--/* ── Offline channel: discover board ── */
--.dsc { width: 100%; max-width: 1040px; margin: 22px auto 6px; padding: 0 clamp(8px, 2vw, 18px); box-sizing: border-box; text-align: left; }
--.dsc-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 14px; margin-bottom: 14px; }
++/* ── Offline channel: discover board — a full-width dashboard grid ── */
++#ch-discover { width: 100%; }
++.dsc { width: 100%; max-width: none; margin: 18px 0 0; padding: 0 clamp(4px, 1.5vw, 12px); box-sizing: border-box; text-align: left;
++ display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(280px, 1fr); grid-template-areas: "head head" "live side" "clips side" "people people"; gap: 14px 18px; align-items: start; }
++.dsc-head { grid-area: head; display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 14px; margin: 0; }
.dsc-title { font-size: 1rem; font-weight: 900; color: var(--text-primary); }
.dsc-title i { color: var(--accent); margin-right: 6px; }
.dsc-sub { font-size: 0.8rem; color: var(--text-muted); }
--.dsc-sec { margin-bottom: 18px; }
--.dsc-sec h4 { margin: 0 0 8px; font-size: 0.8rem; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-secondary); display: flex; align-items: center; gap: 6px; }
++.dsc-sec { margin: 0; min-width: 0; }
++.dsc-sec--live { grid-area: live; } .dsc-sec--clips { grid-area: clips; } .dsc-sec--side { grid-area: side; display: grid; gap: 12px; align-content: start; } .dsc-sec--people { grid-area: people; }
++.dsc-sec h4 { margin: 0 0 8px; font-size: 0.76rem; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-secondary); display: flex; align-items: center; gap: 6px; }
.dsc-sec h4 i { color: var(--accent); }
.dsc-sec h4 .muted { text-transform: none; letter-spacing: 0; font-weight: 600; }
--.dsc-live { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 10px; }
++.dsc-live { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 10px; }
.dsc-live-card { display: block; border-radius: 12px; overflow: hidden; background: var(--bg-secondary); border: 1px solid color-mix(in srgb, #e0245e 40%, var(--border)); text-decoration: none; color: var(--text-primary); transition: transform 0.15s, border-color 0.15s; }
.dsc-live-card:hover { transform: translateY(-2px); border-color: #e0245e; }
.dsc-live-thumb { position: relative; display: block; aspect-ratio: 16 / 9; background: var(--bg-tertiary); }
@@@ -73,30 +76,30 @@
.dsc-live-viewers { position: absolute; right: 8px; bottom: 8px; font-size: 0.68rem; font-weight: 700; padding: 2px 7px; border-radius: 5px; background: rgba(0,0,0,0.7); color: #fff; }
.dsc-live-body { display: flex; align-items: center; gap: 8px; padding: 8px 10px; }
.dsc-live-body > span { display: grid; min-width: 0; }
--.dsc-live-body b { font-size: 0.88rem; } .dsc-live-body small { color: var(--text-muted); font-size: 0.74rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
--.dsc-clips { grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); }
--.dsc-two { display: grid; grid-template-columns: minmax(240px, 1fr) 1.4fr; gap: 12px; }
--.dsc-star { position: relative; display: grid; grid-template-columns: auto 1fr; gap: 12px; align-items: center; padding: 16px 14px 14px; border-radius: 14px; text-decoration: none; color: var(--text-primary);
++.dsc-live-body b { font-size: 0.86rem; } .dsc-live-body small { color: var(--text-muted); font-size: 0.72rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
++.dsc-clips { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
++.dsc-star { position: relative; display: grid; grid-template-columns: auto 1fr; gap: 12px; align-items: center; padding: 16px 14px 12px; border-radius: 14px; text-decoration: none; color: var(--text-primary);
background: linear-gradient(135deg, rgba(251,191,36,0.12), rgba(244,114,182,0.08)); border: 1px solid rgba(251,191,36,0.4); }
.dsc-star:hover { border-color: #fbbf24; }
.dsc-star-badge { position: absolute; top: -10px; left: 12px; font-size: 0.64rem; font-weight: 900; letter-spacing: 0.06em; padding: 2px 8px; border-radius: 999px; background: linear-gradient(135deg, #fde68a, #fbbf24); color: #1c1002; }
.dsc-star-body { display: grid; gap: 2px; min-width: 0; }
--.dsc-star-body b { font-size: 1rem; }
--.dsc-star-head { font-weight: 700; font-size: 0.86rem; color: #fde68a; }
--.dsc-star-body small { color: var(--text-secondary); font-size: 0.76rem; line-height: 1.4; }
++.dsc-star-body b { font-size: 0.98rem; }
++.dsc-star-head { font-weight: 700; font-size: 0.84rem; color: #fde68a; }
++.dsc-star-body small { color: var(--text-secondary); font-size: 0.74rem; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.dsc-recaps { display: grid; gap: 6px; align-content: start; }
.dsc-recaps-head { font-size: 0.74rem; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-secondary); margin-bottom: 2px; }
.dsc-recaps-head i { color: var(--accent); margin-right: 4px; }
.dsc-recap { display: grid; grid-template-columns: 28px 1fr; gap: 10px; align-items: center; padding: 7px 10px; border-radius: 10px; background: var(--bg-secondary); border: 1px solid var(--border); text-decoration: none; color: var(--text-primary); }
.dsc-recap:hover { border-color: var(--accent); }
.dsc-recap-body { display: grid; min-width: 0; }
--.dsc-recap-body b { font-size: 0.84rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
--.dsc-recap-body small { font-size: 0.7rem; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
++.dsc-recap-body b { font-size: 0.82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
++.dsc-recap-body small { font-size: 0.68rem; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dsc-people { display: flex; flex-wrap: wrap; gap: 8px; }
--.dsc-person { display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: 10px; align-items: center; padding: 8px 12px 8px 8px; border-radius: 999px; background: var(--bg-secondary); border: 1px solid var(--border); text-decoration: none; color: var(--text-primary); min-width: 200px; }
++.dsc-person { display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: 10px; align-items: center; padding: 6px 12px 6px 6px; border-radius: 999px; background: var(--bg-secondary); border: 1px solid var(--border); text-decoration: none; color: var(--text-primary); }
.dsc-person:hover { border-color: var(--accent); }
.dsc-person .cc-avatar { grid-row: 1 / 3; }
--.dsc-person b { font-size: 0.86rem; } .dsc-person small { font-size: 0.7rem; color: var(--text-muted); }
--.dsc-person em { grid-column: 2; font-style: normal; font-size: 0.64rem; color: var(--accent); font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }
++.dsc-person b { font-size: 0.84rem; } .dsc-person small { font-size: 0.68rem; color: var(--text-muted); }
++.dsc-person em { grid-column: 2; font-style: normal; font-size: 0.62rem; color: var(--accent); font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }
.dsc-live-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #e0245e; margin-right: 3px; animation: ccMark 1.4s ease-in-out infinite; }
--@media (max-width: 760px) { .dsc-two { grid-template-columns: 1fr; } .dsc-person { min-width: 0; flex: 1 1 46%; } }
++@media (max-width: 980px) { .dsc { grid-template-columns: 1fr; grid-template-areas: "head" "live" "clips" "side" "people"; } }
++@media (max-width: 640px) { .dsc-live { grid-template-columns: 1fr 1fr; } .dsc-clips { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); } }
ddiff --git a/public/css/home-fx.css b/public/css/home-fx.css
iindex 01962550..2c26903a 100644
---- a/public/css/home-fx.css
++++ b/public/css/home-fx.css
@@@ -13,14 +13,14 @@
/* ── Hero buttons: icon tile + label, brand colour per destination, one clean hover ── */
#page-home .hero-cta { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; }
.hb {
-- --c: var(--accent); position: relative; display: inline-flex; align-items: center; gap: 10px; padding: 7px 16px 7px 7px; border-radius: 999px;
++ --c: var(--accent); position: relative; display: inline-flex; align-items: center; gap: 10px; padding: 6px 18px 6px 6px; border-radius: 999px; min-height: 46px; letter-spacing: 0.01em;
font: inherit; font-weight: 700; font-size: 0.95rem; color: var(--text-primary); text-decoration: none; cursor: pointer; isolation: isolate;
background: color-mix(in srgb, var(--bg-card) 78%, transparent); border: 1px solid color-mix(in srgb, var(--c) 28%, var(--border));
box-shadow: 0 6px 18px rgba(0,0,0,0.22); transition: transform 0.22s cubic-bezier(.2,1.3,.3,1), border-color 0.22s, box-shadow 0.22s, color 0.22s;
}
.hb::before { content: ''; position: absolute; inset: 0; border-radius: inherit; z-index: -1; background: linear-gradient(120deg, color-mix(in srgb, var(--c) 22%, transparent), transparent 60%); opacity: 0; transition: opacity 0.25s; }
.hb::after { content: ''; position: absolute; inset: -1px; border-radius: inherit; z-index: -2; padding: 1px; background: conic-gradient(from var(--a, 0deg), transparent 0 70%, var(--c) 85%, transparent 100%); -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask-composite: exclude; opacity: 0; transition: opacity 0.25s; }
--.hb-ico { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; font-size: 0.95rem; color: var(--c); background: color-mix(in srgb, var(--c) 14%, transparent); border: 1px solid color-mix(in srgb, var(--c) 35%, transparent); transition: transform 0.35s cubic-bezier(.2,1.6,.3,1), background 0.25s, color 0.25s, box-shadow 0.25s; }
++.hb-ico { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; font-size: 0.92rem; color: var(--c); background: color-mix(in srgb, var(--c) 12%, var(--bg-tertiary)); border: 1px solid color-mix(in srgb, var(--c) 32%, transparent); box-shadow: 0 2px 6px rgba(0,0,0,0.35) inset; transition: transform 0.35s cubic-bezier(.2,1.6,.3,1), background 0.25s, color 0.25s, box-shadow 0.25s; }
.hb:hover, .hb:focus-visible { transform: translateY(-2px); border-color: color-mix(in srgb, var(--c) 70%, transparent); box-shadow: 0 12px 30px rgba(0,0,0,0.3), 0 0 24px color-mix(in srgb, var(--c) 30%, transparent); outline: none; }
.hb:hover::before, .hb:focus-visible::before { opacity: 1; }
.hb:hover::after { opacity: 1; animation: hbOrbit 1.6s linear infinite; }
@@@ -28,10 +28,13 @@
.hb:active { transform: translateY(0) scale(0.98); }
@property --a { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
@keyframes hbOrbit { to { --a: 360deg; } }
--.hb-stream { --c: var(--accent); color: #fff; background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 65%, #22d3ee)); border-color: transparent; box-shadow: 0 10px 28px color-mix(in srgb, var(--accent) 40%, transparent); }
--.hb-stream .hb-ico { background: rgba(255,255,255,0.18); border-color: rgba(255,255,255,0.35); color: #fff; }
--.hb-stream:hover .hb-ico { background: #fff; color: var(--accent); }
--.hb-stream::before { background: linear-gradient(120deg, rgba(255,255,255,0.18), transparent 60%); }
++.hb-stream { --c: var(--accent); color: #fff; border-color: color-mix(in srgb, var(--accent) 55%, transparent);
++ background: linear-gradient(160deg, color-mix(in srgb, var(--accent) 78%, #000) 0%, color-mix(in srgb, var(--accent) 55%, #000) 100%);
++ box-shadow: 0 8px 22px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.05) inset, 0 0 22px color-mix(in srgb, var(--accent) 22%, transparent); }
++.hb-stream::before { background: linear-gradient(180deg, rgba(255,255,255,0.14), transparent 55%); opacity: 1; }
++.hb-stream .hb-ico { background: rgba(255,255,255,0.14); border-color: rgba(255,255,255,0.28); color: #fff; }
++.hb-stream:hover { box-shadow: 0 14px 32px rgba(0,0,0,0.4), 0 0 30px color-mix(in srgb, var(--accent) 40%, transparent); }
++.hb-stream:hover .hb-ico { background: #fff; color: color-mix(in srgb, var(--accent) 70%, #000); box-shadow: 0 0 16px rgba(255,255,255,0.45); }
.hb-tools { --c: #2dd4bf; } .hb-game { --c: #4ade80; } .hb-github { --c: #e5e7eb; } .hb-discord { --c: #5865f2; } .hb-vibe { --c: #f472b6; }
@media (max-width: 560px) { .hb { padding: 6px 13px 6px 6px; font-size: 0.88rem; } .hb-ico { width: 30px; height: 30px; font-size: 0.85rem; } }
@keyframes hfxPulse { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); } 70% { box-shadow: 0 0 0 14px color-mix(in srgb, var(--accent) 0%, transparent); } 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%, transparent); } }
ddiff --git a/public/css/style.css b/public/css/style.css
iindex 367ee596..7ba5e3aa 100644
---- a/public/css/style.css
++++ b/public/css/style.css
@@@ -14741,3 +14741,27 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srg
/* Arena "your arena" strip: the portrait is a div, so the generic `> div` flex rule was stretching it */
.arena-me-left > .arena-portrait { flex: 0 0 64px !important; width: 64px !important; height: 64px !important; display: block; }
.arena-me-left > .arena-portrait-initial { display: flex; }
++
++/* ── Animated brand mark (navbar) — three nodes, live edges, packets doing laps ── */
++.brand-mark { display: inline-grid; place-items: center; width: 32px; height: 32px; color: var(--accent); flex: none; }
++.brand-mark svg { overflow: visible; transform-origin: 50% 55%; animation: bmFloat 4.5s ease-in-out infinite; }
++.bm-glow { fill: currentColor; opacity: 0.12; transform-box: fill-box; transform-origin: center; animation: bmGlow 3s ease-in-out infinite; }
++.bm-edges { fill: none; stroke: url(#bm-grad); stroke-width: 2.2; stroke-linejoin: round; stroke-dasharray: 6 4; animation: bmDash 1.6s linear infinite; opacity: 0.9; }
++.bm-node { fill: currentColor; stroke: var(--bg-primary, #0f1117); stroke-width: 1.6; transform-box: fill-box; transform-origin: center; animation: bmPulse 2.6s ease-in-out infinite; }
++.bm-node--2 { animation-delay: 0.87s; } .bm-node--3 { animation-delay: 1.73s; }
++.bm-core { fill: #fff; opacity: 0.85; transform-box: fill-box; transform-origin: center; animation: bmCore 2.6s ease-in-out infinite; }
++.bm-pk { fill: #fff; filter: drop-shadow(0 0 3px currentColor); }
++.bm-pk--2 { opacity: 0.7; }
++.nav-brand:hover .brand-mark svg { animation: bmSpin 0.9s cubic-bezier(.2,1.5,.3,1) 1; }
++.nav-brand:hover .bm-glow { opacity: 0.3; }
++.nav-brand:hover .bm-edges { stroke-dasharray: none; animation: none; }
++@keyframes bmFloat { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-1px) rotate(3deg); } }
++@keyframes bmGlow { 0%, 100% { opacity: 0.1; transform: scale(0.9); } 50% { opacity: 0.22; transform: scale(1.08); } }
++@keyframes bmDash { to { stroke-dashoffset: -20; } }
++@keyframes bmPulse { 0%, 70%, 100% { transform: scale(1); } 15% { transform: scale(1.45); } 30% { transform: scale(1); } }
++@keyframes bmCore { 0%, 100% { transform: scale(1); opacity: 0.85; } 50% { transform: scale(1.4); opacity: 1; } }
++@keyframes bmSpin { from { transform: rotate(0deg) scale(1); } 40% { transform: rotate(200deg) scale(1.2); } to { transform: rotate(360deg) scale(1); } }
++@media (prefers-reduced-motion: reduce) { .brand-mark svg, .bm-glow, .bm-edges, .bm-node, .bm-core { animation: none !important; } .bm-pk { display: none; } }
++.rs-lite .brand-mark svg, .rs-lite .bm-glow, .rs-lite .bm-node, .rs-lite .bm-core { animation: none; }
++.rs-lite .bm-pk { filter: none; }
++@media (max-width: 600px) { .brand-mark { width: 26px; height: 26px; } .brand-mark svg { width: 25px; height: 25px; } }
ddiff --git a/public/index.html b/public/index.html
iindex 8e0e02f6..d6e0a7cb 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -62,17 +62,17 @@
</script>
<!-- Stylesheets -->
-- <link rel="stylesheet" href="/css/style.css?v=159">
++ <link rel="stylesheet" href="/css/style.css?v=160">
<link rel="stylesheet" href="/css/chat-shared.css?v=1">
<link rel="stylesheet" href="/css/broadcast.css?v=11">
<link rel="stylesheet" href="/css/cosmetics.css?v=2">
<link rel="stylesheet" href="/css/messenger.css?v=1">
-- <link rel="stylesheet" href="/css/home-fx.css?v=5">
++ <link rel="stylesheet" href="/css/home-fx.css?v=6">
<link rel="stylesheet" href="/css/home-tour.css?v=2">
<link rel="stylesheet" href="/css/broadcast-guide.css?v=1">
<link rel="stylesheet" href="/css/i18n-star.css?v=5">
<link rel="stylesheet" href="/css/recap.css?v=1">
-- <link rel="stylesheet" href="/css/context-cards.css?v=1">
++ <link rel="stylesheet" href="/css/context-cards.css?v=2">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- JSON-LD Structured Data -->
@@@ -116,7 +116,17 @@
<!-- ═══════════════════════════════════════════════════════════ -->
<nav class="navbar">
<a class="nav-brand" href="/" onclick="return handleLinkClick(event, '/')" data-brand-tooltip="This platform is in early alpha — features may be incomplete, buggy, or change without notice. Data will likely be reset multiple times during the alpha phase. Thanks for testing!">
-- <i class="fa-solid fa-circle-nodes"></i>
++ <span class="brand-mark" aria-hidden="true"><svg viewBox="0 0 40 40" width="30" height="30">
++ <defs><linearGradient id="bm-grad" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="currentColor"/><stop offset="1" stop-color="currentColor" stop-opacity="0.55"/></linearGradient></defs>
++ <circle class="bm-glow" cx="20" cy="22" r="17"/>
++ <path id="bm-path" class="bm-edges" d="M20,7 L34,31 L6,31 Z"/>
++ <circle class="bm-node bm-node--1" cx="20" cy="7" r="4.2"/>
++ <circle class="bm-node bm-node--2" cx="34" cy="31" r="4.2"/>
++ <circle class="bm-node bm-node--3" cx="6" cy="31" r="4.2"/>
++ <circle class="bm-core" cx="20" cy="23" r="2.6"/>
++ <circle class="bm-pk" r="1.9"><animateMotion dur="2.6s" repeatCount="indefinite" rotate="auto"><mpath href="#bm-path"/></animateMotion></circle>
++ <circle class="bm-pk bm-pk--2" r="1.4"><animateMotion dur="2.6s" begin="1.3s" repeatCount="indefinite"><mpath href="#bm-path"/></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>
@@@ -5116,7 +5126,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<script src="/js/ai-viewers.js?v=2" defer></script>
<script src="/js/arena.js?v=18" defer></script>
<script src="/js/recap.js?v=1" defer></script>
--<script src="/js/context-cards.js?v=1" defer></script>
++<script src="/js/context-cards.js?v=2" defer></script>
<script src="/js/powerchat.js?v=10" defer></script>
<script src="/js/pastes.js?v=15" defer></script>
<script src="/js/draggable-fab.js?v=2" defer></script>
ddiff --git a/public/js/context-cards.js b/public/js/context-cards.js
iindex 381ec1b9..f52825b2 100644
---- a/public/js/context-cards.js
++++ b/public/js/context-cards.js
@@@ -137,14 +137,14 @@
const name = d.channel ? d.channel.display_name : 'this channel';
const sections = [];
if ((d.live || []).length) {
-- sections.push(`<section class="dsc-sec"><h4><i class="fa-solid fa-circle live-dot"></i> Live right now</h4><div class="dsc-live">${d.live.map(s => `
++ sections.push(`<section class="dsc-sec dsc-sec--live"><h4><i class="fa-solid fa-circle live-dot"></i> Live right now</h4><div class="dsc-live">${d.live.slice(0, 3).map(s => `
<a class="dsc-live-card" ${go(`/@${s.username}`)}>
<span class="dsc-live-thumb">${s.thumbnail_url ? `<img src="${esc(s.thumbnail_url)}" alt="" loading="lazy">` : '<span class="cc-clip-ph"><i class="fa-solid fa-tower-broadcast"></i></span>'}<span class="dsc-live-pill">LIVE</span><span class="dsc-live-viewers"><i class="fa-solid fa-eye"></i> ${n(s.viewer_count)}</span></span>
<span class="dsc-live-body">${av(s, 26)}<span><b>${esc(s.display_name || s.username)}</b><small>${esc(s.title || '')}</small></span></span>
</a>`).join('')}</div></section>`);
}
if ((d.clips || []).length) {
-- sections.push(`<section class="dsc-sec"><h4><i class="fa-solid fa-fire"></i> ${d.clips[0].fresh ? 'Hot clips this week' : 'Most-watched clips'} <span class="muted">across OpenVibe</span></h4><div class="cc-clips dsc-clips">${d.clips.map(c => clipTile(c)).join('')}</div></section>`);
++ sections.push(`<section class="dsc-sec dsc-sec--clips"><h4><i class="fa-solid fa-fire"></i> ${d.clips[0].fresh ? 'Hot clips this week' : 'Most-watched clips'} <span class="muted">across OpenVibe</span></h4><div class="cc-clips dsc-clips">${d.clips.slice(0, 6).map(c => clipTile(c)).join('')}</div></section>`);
}
if (d.star || (d.recaps || []).length) {
const star = d.star ? `<a class="dsc-star" ${go(`/@${d.star.username}`)}>
@@@ -152,15 +152,15 @@
${av(d.star, 56)}
<span class="dsc-star-body"><b>${esc(d.star.display_name)}${d.star.live ? ' <span class="dsc-live-pill">LIVE</span>' : ''}</b>${d.star.headline ? `<span class="dsc-star-head">${esc(d.star.headline)}</span>` : ''}${d.star.reason ? `<small>${esc(d.star.reason)}</small>` : ''}</span>
</a>` : '';
-- const recaps = (d.recaps || []).length ? `<div class="dsc-recaps"><div class="dsc-recaps-head"><i class="fa-solid fa-clipboard-list"></i> Fresh after-show reports</div>${d.recaps.slice(0, 5).map(r => `
++ const recaps = (d.recaps || []).length ? `<div class="dsc-recaps"><div class="dsc-recaps-head"><i class="fa-solid fa-clipboard-list"></i> Fresh after-show reports</div>${d.recaps.slice(0, 4).map(r => `
<a class="dsc-recap" ${go(`/recap/${r.stream_id}`)}>
<span class="cc-grade" style="--g:${GRADE[r.grade] || GRADE.B}">${esc(r.grade)}</span>
<span class="dsc-recap-body"><b>${esc(r.headline || r.title)}</b><small>${esc(r.display_name)} · ${esc(ago(r.ended_at))} · ${durWords(r.duration_seconds)} · peak ${n(r.peak_viewers)}${r.chat_messages ? ` · ${n(r.chat_messages)} chat lines` : ''}</small></span>
</a>`).join('')}</div>` : '';
-- sections.push(`<section class="dsc-sec dsc-two">${star}${recaps}</section>`);
++ sections.push(`<section class="dsc-sec dsc-sec--side">${star}${recaps}</section>`);
}
if ((d.similar || []).length) {
-- sections.push(`<section class="dsc-sec"><h4><i class="fa-solid fa-people-group"></i> Streamers to check out</h4><div class="dsc-people">${d.similar.map(p => `
++ sections.push(`<section class="dsc-sec dsc-sec--people"><h4><i class="fa-solid fa-people-group"></i> Streamers to check out</h4><div class="dsc-people">${d.similar.slice(0, 6).map(p => `
<a class="dsc-person" ${go(`/@${p.username}`)}>${av(p, 44)}<b>${esc(p.display_name || p.username)}</b><small>${p.live ? '<span class="dsc-live-dot"></span> live now' : (p.last_live_at ? `live ${esc(ago(p.last_live_at))}` : '')}${p.followers ? ` · ${n(p.followers)} follower${p.followers === 1 ? '' : 's'}` : ''}</small>${p.same_category && p.category ? `<em>${esc(p.category)}</em>` : ''}</a>`).join('')}</div></section>`);
}
if (!sections.length) { host.innerHTML = ''; return; }