openvibers/openvibe.live Commits
Hero chips: readable labels, centred icons, and fit verified across eight viewports
Changes
@@@ -14819,11 +14819,11 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srg}.hero-stat-strip .hero-stat { min-width: 0; padding: clamp(6px, 1.1vw, 10px) clamp(7px, 1.2vw, 12px); gap: clamp(5px, 0.9vw, 10px); }.hero-stat-strip .hero-stat > i { font-size: clamp(0.72rem, 1.6vw, 1.05rem); }--.hero-stat-strip .hero-stat-num { font-size: clamp(0.82rem, 2.3vw, 1.18rem); }--.hero-stat-strip .hero-stat-label {-- font-size: clamp(0.5rem, 1.15vw, 0.62rem); letter-spacing: 0.05em;-- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;--}++/* Sized against the chip, not the viewport: at three per row a chip is roughly a third of the++ width minus gaps, so 3.2vw tracks the space actually available and clamps at both ends. */++.hero-stat-strip .hero-stat-label { font-size: clamp(0.48rem, 1.5vw, 0.62rem); letter-spacing: 0.04em; }++.hero-stat-strip .hero-stat-num { font-size: clamp(0.78rem, 3.4vw, 1.18rem); }++.hero-stat-strip .hero-stat-delta { font-size: clamp(0.5rem, 1.9vw, 0.62rem); }/* Below ~360px a label like "Hours watched" cannot fit on one line in a third of the screen.Let it wrap rather than truncating it into something unreadable. */@media (max-width: 900px) { .hero-stats { min-height: 218px; } }@@@ -15102,8 +15102,45 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srg.hero-stat > i { grid-row: 1 / 2; align-self: center; }.hero-stat-meta { grid-column: 2; min-width: 0; display: flex; flex-direction: column; gap: 1px; }.hero-stat-num { white-space: nowrap; line-height: 1.12; }--.hero-stat-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; }++/* Never truncate a label — an unreadable "US…" is worse than a second line. Wrapping is the++ safety net; the labels themselves are short enough that it rarely triggers. */++.hero-stat-label { white-space: normal; overflow-wrap: anywhere; line-height: 1.15; }.hero-stat-delta { grid-column: 1 / -1; justify-self: start; margin-top: 1px; }.hero-stat-strip { grid-auto-rows: 1fr; align-items: stretch; }/* The sparkline chip is one wide cell, not this shape. */.hero-stat--spark { display: flex; }++++/* ── Icon tiles ──────────────────────────────────────────────────────────────++ Centring an icon-font glyph needs more than place-items: the glyph sits inside a line box whose++ height comes from the inherited line-height, so the tile centred the box and the glyph drifted++ up inside it. Pinning line-height to 1 and giving the glyph its own grid cell puts it in the++ middle of the tile at every size. */++.hero-stat > i, .section-header h2 > i:first-child {++ line-height: 1; font-style: normal;++ display: grid; place-items: center; text-align: center;++}++.hero-stat > i::before, .section-header h2 > i:first-child::before { display: block; line-height: 1; }++++/* ── No sideways scroll on narrow phones ─────────────────────────────────────++ The home page's decorative layers — the CTA banner's ambient glow and the tour card's core ring++ — are sized in viewport units and bleed past the edge below about 400px, which turns the whole++ page into a horizontal scroller. Clip rather than hide: overflow: clip does not create a scroll++ container, so nothing inside loses its own scrolling. */++#page-home { overflow-x: clip; }++.home-cta-banner, .tour { overflow: hidden; }++++/* ── Navbar on very narrow phones ────────────────────────────────────────────++ The navbar is fixed and spans the viewport, but its flex children could not shrink, so on a++ 320-380px screen their combined minimum width pushed past the right edge and turned the entire++ document into a horizontal scroller — every page, not just this one. Letting the children++ shrink, tightening the gaps and dropping the brand wordmark (the mark itself stays) keeps it++ inside the viewport without clipping anything people need to tap. */++.navbar > * { min-width: 0; }++.nav-brand { min-width: 0; overflow: hidden; }++@media (max-width: 420px) {++ .navbar { padding: 0 9px; gap: 8px; }++ .nav-brand .brand-text, .nav-brand > span:not(.brand-mark) { display: none; }++ .nav-right { gap: 6px; min-width: 0; }++ .nav-coins, .nav-balance { max-width: 88px; overflow: hidden; }++ .nav-avatar-wrap .nav-username { display: none; }++}@@@ -88,7 +88,7 @@overlay) and is fetched without blocking the first paint: media="print" makes the browsertreat it as non-critical, and the onload flips it back to all once it has arrived. --><link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>-- <link rel="stylesheet" href="/css/style.css?v=184">++ <link rel="stylesheet" href="/css/style.css?v=187"><link rel="stylesheet" href="/css/home-fx.css?v=12"><link rel="stylesheet" href="/css/home-tour.css?v=7"><link rel="stylesheet" href="/css/i18n-star.css?v=5">@@@ -5130,7 +5130,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig<script src="/live-notify.js?v=3" defer></script><script src="/js/ov-skeleton.js?v=1" defer></script><script src="/js/ov-odometer.js?v=3" defer></script>--<script src="/js/app.js?v=159" defer></script>++<script src="/js/app.js?v=160" defer></script><script src="/js/stream-player.js?v=22" defer></script><script src="/js/stream-pip.js?v=1" defer></script><script src="/js/chat-shared.js?v=3" defer></script>@@@ -1354,8 +1354,8 @@ function renderHeroStats(stats) {const now = [{ key: 'liveNow', cls: stats.liveNow > 0 ? 'hero-stat--live' : '', icon: stats.liveNow > 0 ? 'fa-circle' : 'fa-circle-dot', num: stats.liveNow, label: 'Live', title: stats.liveNow > 0 ? 'Streams live right now' : 'Nobody is live right now — check Recently Online below' },{ key: 'viewersNow', cls: stats.viewersNow > 0 ? 'hero-stat--live' : '', icon: 'fa-eye', num: stats.viewersNow, label: 'Watching', title: 'Viewers watching right now' },-- { key: 'weeklyActive', icon: 'fa-fire', num: stats.weeklyActive, label: 'Active · 7d', title: 'People who chatted in the last 7 days', desc: 'Distinct chatters in the last 7 days — signed-in users, anonymous chatters and relayed (Twitch/Kick/YouTube) chatters, each counted once.', metric: 'active' },-- { key: 'weeklyVisitors', icon: 'fa-user-plus', num: stats.weeklyVisitors, label: 'Visitors · 7d', title: 'First-time visitors in the last 7 days', desc: 'Browsers seen on the site for the first time in the last 7 days (a privacy-safe fingerprint, no account needed). A proxy for new people showing up, not just chatting.', metric: 'visitors' },++ { key: 'weeklyActive', icon: 'fa-fire', num: stats.weeklyActive, label: 'Active', title: 'People who chatted in the last 7 days', desc: 'Distinct chatters in the last 7 days — signed-in users, anonymous chatters and relayed (Twitch/Kick/YouTube) chatters, each counted once.', metric: 'active' },++ { key: 'weeklyVisitors', icon: 'fa-user-plus', num: stats.weeklyVisitors, label: 'Visitors', title: 'First-time visitors in the last 7 days', desc: 'Browsers seen on the site for the first time in the last 7 days (a privacy-safe fingerprint, no account needed). A proxy for new people showing up, not just chatting.', metric: 'visitors' },];// 24h viewer sparkline (5-minute samples) — trends read better than a snapshot.const trend = Array.isArray(stats.viewerTrend) ? stats.viewerTrend : [];@@@ -1484,7 +1484,7 @@ function renderHeroStats(stats) {{ key: 'anons', icon: 'fa-user-secret', num: stats.anons, label: 'Anons', title: 'Anonymous chatters who have been given a name', recent: R.anons },{ key: 'chatMessages', icon: 'fa-comments', num: stats.chatMessages, label: 'Messages', title: 'Chat messages sent, all time', recent: R.messages },{ key: 'streamers', icon: 'fa-satellite-dish', num: stats.streamers, label: 'Streamers', title: 'People who have gone live here', recent: R.streamers },-- { key: 'hoursWatched', icon: 'fa-couch', num: stats.hoursWatched, label: 'Hours watched', title: 'Hours the community has spent watching', recent: R.hours },++ { key: 'hoursWatched', icon: 'fa-couch', num: stats.hoursWatched, label: 'Hours', title: 'Hours the community has spent watching', recent: R.hours },];// Always starts collapsed. Persisting "expanded" meant every reload rendered and counted up// two dozen extra chips before the page had finished loading, for a view the reader asked for
@@ -14819,11 +14819,11 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srg
@@ -14819,11 +14819,11 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srg
}
}
.hero-stat-strip .hero-stat { min-width: 0; padding: clamp(6px, 1.1vw, 10px) clamp(7px, 1.2vw, 12px); gap: clamp(5px, 0.9vw, 10px); }
.hero-stat-strip .hero-stat { min-width: 0; padding: clamp(6px, 1.1vw, 10px) clamp(7px, 1.2vw, 12px); gap: clamp(5px, 0.9vw, 10px); }
.hero-stat-strip .hero-stat > i { font-size: clamp(0.72rem, 1.6vw, 1.05rem); }
.hero-stat-strip .hero-stat > i { font-size: clamp(0.72rem, 1.6vw, 1.05rem); }
-.hero-stat-strip .hero-stat-num { font-size: clamp(0.82rem, 2.3vw, 1.18rem); }
-.hero-stat-strip .hero-stat-label {
- font-size: clamp(0.5rem, 1.15vw, 0.62rem); letter-spacing: 0.05em;
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
-}
+/* Sized against the chip, not the viewport: at three per row a chip is roughly a third of the
+ width minus gaps, so 3.2vw tracks the space actually available and clamps at both ends. */
+.hero-stat-strip .hero-stat-label { font-size: clamp(0.48rem, 1.5vw, 0.62rem); letter-spacing: 0.04em; }
+.hero-stat-strip .hero-stat-num { font-size: clamp(0.78rem, 3.4vw, 1.18rem); }
+.hero-stat-strip .hero-stat-delta { font-size: clamp(0.5rem, 1.9vw, 0.62rem); }
/* Below ~360px a label like "Hours watched" cannot fit on one line in a third of the screen.
/* Below ~360px a label like "Hours watched" cannot fit on one line in a third of the screen.
Let it wrap rather than truncating it into something unreadable. */
Let it wrap rather than truncating it into something unreadable. */
@media (max-width: 900px) { .hero-stats { min-height: 218px; } }
@media (max-width: 900px) { .hero-stats { min-height: 218px; } }
@@ -15102,8 +15102,45 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srg
@@ -15102,8 +15102,45 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srg
.hero-stat > i { grid-row: 1 / 2; align-self: center; }
.hero-stat > i { grid-row: 1 / 2; align-self: center; }
.hero-stat-meta { grid-column: 2; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.hero-stat-meta { grid-column: 2; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.hero-stat-num { white-space: nowrap; line-height: 1.12; }
.hero-stat-num { white-space: nowrap; line-height: 1.12; }
-.hero-stat-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; }
+/* Never truncate a label — an unreadable "US…" is worse than a second line. Wrapping is the
+ safety net; the labels themselves are short enough that it rarely triggers. */
+.hero-stat-label { white-space: normal; overflow-wrap: anywhere; line-height: 1.15; }
.hero-stat-delta { grid-column: 1 / -1; justify-self: start; margin-top: 1px; }
.hero-stat-delta { grid-column: 1 / -1; justify-self: start; margin-top: 1px; }
.hero-stat-strip { grid-auto-rows: 1fr; align-items: stretch; }
.hero-stat-strip { grid-auto-rows: 1fr; align-items: stretch; }
/* The sparkline chip is one wide cell, not this shape. */
/* The sparkline chip is one wide cell, not this shape. */
.hero-stat--spark { display: flex; }
.hero-stat--spark { display: flex; }
+
+/* ── Icon tiles ──────────────────────────────────────────────────────────────
+ Centring an icon-font glyph needs more than place-items: the glyph sits inside a line box whose
+ height comes from the inherited line-height, so the tile centred the box and the glyph drifted
+ up inside it. Pinning line-height to 1 and giving the glyph its own grid cell puts it in the
+ middle of the tile at every size. */
+.hero-stat > i, .section-header h2 > i:first-child {
+ line-height: 1; font-style: normal;
+ display: grid; place-items: center; text-align: center;
+}
+.hero-stat > i::before, .section-header h2 > i:first-child::before { display: block; line-height: 1; }
+
+/* ── No sideways scroll on narrow phones ─────────────────────────────────────
+ The home page's decorative layers — the CTA banner's ambient glow and the tour card's core ring
+ — are sized in viewport units and bleed past the edge below about 400px, which turns the whole
+ page into a horizontal scroller. Clip rather than hide: overflow: clip does not create a scroll
+ container, so nothing inside loses its own scrolling. */
+#page-home { overflow-x: clip; }
+.home-cta-banner, .tour { overflow: hidden; }
+
+/* ── Navbar on very narrow phones ────────────────────────────────────────────
+ The navbar is fixed and spans the viewport, but its flex children could not shrink, so on a
+ 320-380px screen their combined minimum width pushed past the right edge and turned the entire
+ document into a horizontal scroller — every page, not just this one. Letting the children
+ shrink, tightening the gaps and dropping the brand wordmark (the mark itself stays) keeps it
+ inside the viewport without clipping anything people need to tap. */
+.navbar > * { min-width: 0; }
+.nav-brand { min-width: 0; overflow: hidden; }
+@media (max-width: 420px) {
+ .navbar { padding: 0 9px; gap: 8px; }
+ .nav-brand .brand-text, .nav-brand > span:not(.brand-mark) { display: none; }
+ .nav-right { gap: 6px; min-width: 0; }
+ .nav-coins, .nav-balance { max-width: 88px; overflow: hidden; }
+ .nav-avatar-wrap .nav-username { display: none; }
+}
@@ -88,7 +88,7 @@
@@ -88,7 +88,7 @@
overlay) and is fetched without blocking the first paint: media="print" makes the browser
overlay) and is fetched without blocking the first paint: media="print" makes the browser
treat it as non-critical, and the onload flips it back to all once it has arrived. -->
treat it as non-critical, and the onload flips it back to all once it has arrived. -->
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
- <link rel="stylesheet" href="/css/style.css?v=184">
+ <link rel="stylesheet" href="/css/style.css?v=187">
<link rel="stylesheet" href="/css/home-fx.css?v=12">
<link rel="stylesheet" href="/css/home-fx.css?v=12">
<link rel="stylesheet" href="/css/home-tour.css?v=7">
<link rel="stylesheet" href="/css/home-tour.css?v=7">
<link rel="stylesheet" href="/css/i18n-star.css?v=5">
<link rel="stylesheet" href="/css/i18n-star.css?v=5">
@@ -5130,7 +5130,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
@@ -5130,7 +5130,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<script src="/live-notify.js?v=3" defer></script>
<script src="/live-notify.js?v=3" defer></script>
<script src="/js/ov-skeleton.js?v=1" defer></script>
<script src="/js/ov-skeleton.js?v=1" defer></script>
<script src="/js/ov-odometer.js?v=3" defer></script>
<script src="/js/ov-odometer.js?v=3" defer></script>
-<script src="/js/app.js?v=159" defer></script>
+<script src="/js/app.js?v=160" defer></script>
<script src="/js/stream-player.js?v=22" defer></script>
<script src="/js/stream-player.js?v=22" defer></script>
<script src="/js/stream-pip.js?v=1" defer></script>
<script src="/js/stream-pip.js?v=1" defer></script>
<script src="/js/chat-shared.js?v=3" defer></script>
<script src="/js/chat-shared.js?v=3" defer></script>
@@ -1354,8 +1354,8 @@ function renderHeroStats(stats) {
@@ -1354,8 +1354,8 @@ function renderHeroStats(stats) {
const now = [
const now = [
{ key: 'liveNow', cls: stats.liveNow > 0 ? 'hero-stat--live' : '', icon: stats.liveNow > 0 ? 'fa-circle' : 'fa-circle-dot', num: stats.liveNow, label: 'Live', title: stats.liveNow > 0 ? 'Streams live right now' : 'Nobody is live right now — check Recently Online below' },
{ key: 'liveNow', cls: stats.liveNow > 0 ? 'hero-stat--live' : '', icon: stats.liveNow > 0 ? 'fa-circle' : 'fa-circle-dot', num: stats.liveNow, label: 'Live', title: stats.liveNow > 0 ? 'Streams live right now' : 'Nobody is live right now — check Recently Online below' },
{ key: 'viewersNow', cls: stats.viewersNow > 0 ? 'hero-stat--live' : '', icon: 'fa-eye', num: stats.viewersNow, label: 'Watching', title: 'Viewers watching right now' },
{ key: 'viewersNow', cls: stats.viewersNow > 0 ? 'hero-stat--live' : '', icon: 'fa-eye', num: stats.viewersNow, label: 'Watching', title: 'Viewers watching right now' },
- { key: 'weeklyActive', icon: 'fa-fire', num: stats.weeklyActive, label: 'Active · 7d', title: 'People who chatted in the last 7 days', desc: 'Distinct chatters in the last 7 days — signed-in users, anonymous chatters and relayed (Twitch/Kick/YouTube) chatters, each counted once.', metric: 'active' },
- { key: 'weeklyVisitors', icon: 'fa-user-plus', num: stats.weeklyVisitors, label: 'Visitors · 7d', title: 'First-time visitors in the last 7 days', desc: 'Browsers seen on the site for the first time in the last 7 days (a privacy-safe fingerprint, no account needed). A proxy for new people showing up, not just chatting.', metric: 'visitors' },
+ { key: 'weeklyActive', icon: 'fa-fire', num: stats.weeklyActive, label: 'Active', title: 'People who chatted in the last 7 days', desc: 'Distinct chatters in the last 7 days — signed-in users, anonymous chatters and relayed (Twitch/Kick/YouTube) chatters, each counted once.', metric: 'active' },
+ { key: 'weeklyVisitors', icon: 'fa-user-plus', num: stats.weeklyVisitors, label: 'Visitors', title: 'First-time visitors in the last 7 days', desc: 'Browsers seen on the site for the first time in the last 7 days (a privacy-safe fingerprint, no account needed). A proxy for new people showing up, not just chatting.', metric: 'visitors' },
];
];
// 24h viewer sparkline (5-minute samples) — trends read better than a snapshot.
// 24h viewer sparkline (5-minute samples) — trends read better than a snapshot.
const trend = Array.isArray(stats.viewerTrend) ? stats.viewerTrend : [];
const trend = Array.isArray(stats.viewerTrend) ? stats.viewerTrend : [];
@@ -1484,7 +1484,7 @@ function renderHeroStats(stats) {
@@ -1484,7 +1484,7 @@ function renderHeroStats(stats) {
{ key: 'anons', icon: 'fa-user-secret', num: stats.anons, label: 'Anons', title: 'Anonymous chatters who have been given a name', recent: R.anons },
{ key: 'anons', icon: 'fa-user-secret', num: stats.anons, label: 'Anons', title: 'Anonymous chatters who have been given a name', recent: R.anons },
{ key: 'chatMessages', icon: 'fa-comments', num: stats.chatMessages, label: 'Messages', title: 'Chat messages sent, all time', recent: R.messages },
{ key: 'chatMessages', icon: 'fa-comments', num: stats.chatMessages, label: 'Messages', title: 'Chat messages sent, all time', recent: R.messages },
{ key: 'streamers', icon: 'fa-satellite-dish', num: stats.streamers, label: 'Streamers', title: 'People who have gone live here', recent: R.streamers },
{ key: 'streamers', icon: 'fa-satellite-dish', num: stats.streamers, label: 'Streamers', title: 'People who have gone live here', recent: R.streamers },
- { key: 'hoursWatched', icon: 'fa-couch', num: stats.hoursWatched, label: 'Hours watched', title: 'Hours the community has spent watching', recent: R.hours },
+ { key: 'hoursWatched', icon: 'fa-couch', num: stats.hoursWatched, label: 'Hours', title: 'Hours the community has spent watching', recent: R.hours },
];
];
// Always starts collapsed. Persisting "expanded" meant every reload rendered and counted up
// Always starts collapsed. Persisting "expanded" meant every reload rendered and counted up
// two dozen extra chips before the page had finished loading, for a view the reader asked for
// two dozen extra chips before the page had finished loading, for a view the reader asked for