openvibers/openvibe.live Commits
Home: 'One stream. Everywhere. Every toy.' — the restream pipeline + feature tour as its own section (no promo)
Changes
nnew file mode 100644@@@ -0,0 +1,55 @@++/* ═══════════════════════════════════════════════════════════════++ HOME TOUR — "One stream. Everywhere. Every toy." (public/js/home-tour.js)++ Transform/opacity animations only, so it stays smooth on phones.++ ═══════════════════════════════════════════════════════════════ */++.home-tour-mount { max-width: 860px; margin: 22px auto 0; }++.tour {++ position: relative; border-radius: 20px; padding: 20px 22px 18px; text-align: center; color: var(--text-primary);++ background: linear-gradient(160deg, color-mix(in srgb, var(--accent) 12%, var(--bg-card)), var(--bg-card));++ border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border)); box-shadow: 0 20px 50px rgba(0,0,0,0.35);++}++.tour-head h2 { margin: 0 0 6px; font-size: clamp(1.2rem, 2.6vw, 1.6rem); font-weight: 900; }++.tour-head h2 i { color: var(--accent-light); }++.tour-head p { margin: 0 auto 14px; max-width: 640px; color: var(--text-secondary); font-size: 0.92rem; line-height: 1.5; }++.tour-flow { display: grid; gap: 6px; justify-items: center; }++.tour-src, .tour-out { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }++.tour-node {++ display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px; border-radius: 999px; font-weight: 700; font-size: 0.8rem;++ color: var(--text-primary); background: color-mix(in srgb, var(--bg-tertiary) 80%, transparent); border: 1px solid var(--border-light);++}++.tour-twitch { border-color: #9146ff; background: rgba(145,70,255,0.22); color: #fff; }++.tour-yt { border-color: #ff0000; background: rgba(255,0,0,0.18); color: #fff; }++.tour-kick { border-color: #53fc18; background: rgba(83,252,24,0.14); color: #d9ffcc; }++.tour-rs { border-color: #7dd3fc; background: rgba(125,211,252,0.14); color: #fff; }++.tour-hub {++ display: grid; justify-items: center; gap: 2px; padding: 10px 18px; border-radius: 14px;++ background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 45%, transparent), color-mix(in srgb, var(--accent-dark) 25%, transparent));++ border: 1px solid color-mix(in srgb, var(--accent-light) 75%, transparent); color: #fff;++}++.tour-hub b { font-size: 1rem; }++.tour-hub small { color: #e9d5ff; font-size: 0.74rem; font-weight: 600; }++.tour-wire { position: relative; width: 4px; height: 26px; border-radius: 2px; background: color-mix(in srgb, var(--text-primary) 18%, transparent); }++.tour-fan { width: min(70%, 420px); height: 26px; background: none; }++.tour-fan::before { content: ''; position: absolute; left: 0; right: 0; top: 50%; height: 3px; background: repeating-linear-gradient(90deg, color-mix(in srgb, var(--text-primary) 45%, transparent) 0 8px, transparent 8px 16px); animation: tourDash 0.6s linear infinite; }++.tour-fan::after { content: ''; position: absolute; left: 50%; top: 0; width: 4px; height: 50%; background: color-mix(in srgb, var(--text-primary) 18%, transparent); transform: translateX(-50%); }++@keyframes tourDash { to { background-position: 20px 0; } }++.tour-packet { position: absolute; left: 50%; top: -6px; width: 10px; height: 10px; margin-left: -5px; border-radius: 50%; background: #fde68a; animation: tourDown 1.2s linear infinite; }++.tour-packet:nth-child(2) { animation-delay: 0.6s; }++.tour-fan .tour-packet { top: 50%; margin-top: -5px; animation: tourFan 1.4s ease-in-out infinite; }++.tour-fan .tour-packet:nth-child(2) { animation-name: tourFanL; animation-delay: 0.45s; }++.tour-fan .tour-packet:nth-child(3) { animation-name: tourFanR; animation-delay: 0.9s; }++@keyframes tourDown { 0% { transform: translateY(0); opacity: 0; } 15% { opacity: 1; } 100% { transform: translateY(26px); opacity: 0; } }++@keyframes tourFan { 0% { transform: translate(0, -14px) scale(0.6); opacity: 0; } 30% { opacity: 1; } 100% { transform: translate(0, 12px) scale(1); opacity: 0; } }++@keyframes tourFanL { 0% { transform: translate(0, -14px) scale(0.6); opacity: 0; } 30% { opacity: 1; } 100% { transform: translate(-160px, 12px) scale(1); opacity: 0; } }++@keyframes tourFanR { 0% { transform: translate(0, -14px) scale(0.6); opacity: 0; } 30% { opacity: 1; } 100% { transform: translate(160px, 12px) scale(1); opacity: 0; } }++.tour-features { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin: 14px 0 0; }++.tour-feat {++ display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; font-size: 0.78rem; font-weight: 700; color: var(--text-primary);++ background: rgba(34,197,94,0.12); border: 1px solid rgba(34,197,94,0.45);++ animation: tourPop 0.5s cubic-bezier(.2,1.5,.3,1) both; animation-delay: calc(0.2s + var(--i, 0) * 70ms);++}++.tour-feat i { color: #86efac; }++@keyframes tourPop { from { opacity: 0; transform: translateY(8px) scale(0.85); } to { opacity: 1; transform: none; } }++.tour-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-top: 16px; }++@media (max-width: 720px) { .tour { padding: 16px 12px 14px; } .tour-fan { width: 90%; } .tour-node { font-size: 0.74rem; padding: 4px 9px; } }++@media (prefers-reduced-motion: reduce) { .tour-fan::before, .tour-packet, .tour-feat { animation: none; } .tour-feat { opacity: 1; transform: none; } }@@@ -68,6 +68,7 @@<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=4">++ <link rel="stylesheet" href="/css/home-tour.css?v=1"><link rel="stylesheet" href="/css/i18n-star.css?v=3"><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">@@@ -248,6 +249,8 @@<i class="fa-solid fa-wand-magic-sparkles"></i> Vibe Coders</a></div>++ <!-- One stream, everywhere — restream pipeline + feature tour (public/js/home-tour.js) -->++ <div class="home-tour-mount" id="home-tour-mount"></div><!-- Newest shipped commit — built in the open, receipts included. --><a class="hero-latest" id="hero-latest" href="/updates" onclick="return handleLinkClick(event, '/updates')" style="display:none"></a><div class="hero-philosophy-links">@@@ -5120,6 +5123,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig<script src="/js/draggable-fab.js?v=2" defer></script><script src="/js/messenger.js?v=2" defer></script><script src="/js/home-fx.js?v=2" defer></script>++<script src="/js/home-tour.js?v=1" defer></script><!-- ── OpenVibe shared libs (notifications, account sync, themes) ── --><script src="/shared/theme-loader.js" defer></script>nnew file mode 100644@@@ -0,0 +1,54 @@++/**++ * home-tour.js — "One stream. Everywhere. Every toy." — the animated feature tour on the home page.++ *++ * Static content, no API: the restream pipeline (your cam or robot → OpenVibe.Live → Twitch,++ * YouTube, Kick, RobotStreamer, any RTMP) with packets flowing along it, and the feature chips++ * that only exist here. Mounted into #home-tour-mount under the hero CTA. Transform-only++ * animations, so it stays smooth in mobile lite mode.++ */++(function () {++ 'use strict';++ function mount() {++ const el = document.getElementById('home-tour-mount');++ if (!el || el.dataset.mounted) return;++ el.dataset.mounted = '1';++ const go = (href) => `href="${href}" onclick="return handleLinkClick(event, '${href}')"`;++ el.innerHTML = `++ <div class="tour">++ <div class="tour-head">++ <h2><i class="fa-solid fa-satellite-dish"></i> One stream. Everywhere. Every toy.</h2>++ <p>Go live here once and mirror it to every platform at the same time — while your chat, emotes, sound commands, robot controls, VODs and clips all live in one place.</p>++ </div>++ <div class="tour-flow" aria-label="Stream from OpenVibe.Live and restream everywhere">++ <div class="tour-src"><span class="tour-node"><i class="fa-solid fa-video"></i> your cam</span><span class="tour-node"><i class="fa-solid fa-robot"></i> your robot</span><span class="tour-node"><i class="fa-solid fa-desktop"></i> OBS / RTMP</span></div>++ <div class="tour-wire" aria-hidden="true"><span class="tour-packet"></span><span class="tour-packet"></span></div>++ <div class="tour-hub"><b><i class="fa-solid fa-circle-nodes"></i> OpenVibe.Live</b><small>WebRTC <1s latency · emotes · sound commands · robot controls · VODs · clips · AI moments</small></div>++ <div class="tour-wire tour-fan" aria-hidden="true"><span class="tour-packet"></span><span class="tour-packet"></span><span class="tour-packet"></span></div>++ <div class="tour-out">++ <span class="tour-node tour-twitch"><i class="fa-brands fa-twitch"></i> Twitch</span>++ <span class="tour-node tour-yt"><i class="fa-brands fa-youtube"></i> YouTube</span>++ <span class="tour-node tour-kick"><i class="fa-solid fa-bolt"></i> Kick</span>++ <span class="tour-node tour-rs"><i class="fa-solid fa-robot"></i> RobotStreamer</span>++ <span class="tour-node"><i class="fa-solid fa-tower-broadcast"></i> any RTMP</span>++ </div>++ </div>++ <div class="tour-features">++ <span class="tour-feat" style="--i:0"><i class="fa-solid fa-gauge-high"></i> Sub-second WebRTC latency</span>++ <span class="tour-feat" style="--i:1"><i class="fa-solid fa-face-grin-squint-tears"></i> 7TV / BTTV / FFZ + custom emotes</span>++ <span class="tour-feat" style="--i:2"><i class="fa-solid fa-volume-high"></i> Sound commands & soundboard</span>++ <span class="tour-feat" style="--i:3"><i class="fa-solid fa-gamepad"></i> Robot & hardware controls</span>++ <span class="tour-feat" style="--i:4"><i class="fa-solid fa-satellite-dish"></i> Free restream, all at once</span>++ <span class="tour-feat" style="--i:5"><i class="fa-solid fa-scissors"></i> VODs, clips, AI moments</span>++ <span class="tour-feat" style="--i:6"><i class="fa-solid fa-comments"></i> One chat from every platform</span>++ <span class="tour-feat" style="--i:7"><i class="fa-solid fa-language"></i> Auto-translated chat</span>++ <span class="tour-feat" style="--i:8"><i class="fa-solid fa-microphone-lines"></i> The Arena: mic-judged beefs</span>++ <span class="tour-feat" style="--i:9"><i class="fa-brands fa-github"></i> 100% open source</span>++ </div>++ <div class="tour-actions">++ <a class="btn btn-primary btn-lg" ${go('/broadcast')}><i class="fa-solid fa-tower-broadcast"></i> Go live</a>++ <a class="btn btn-outline btn-lg" ${go('/docs/broadcasting')}><i class="fa-solid fa-book"></i> Restream setup</a>++ </div>++ </div>`;++ }++ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mount); else mount();++})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
+/* ═══════════════════════════════════════════════════════════════
+ HOME TOUR — "One stream. Everywhere. Every toy." (public/js/home-tour.js)
+ Transform/opacity animations only, so it stays smooth on phones.
+ ═══════════════════════════════════════════════════════════════ */
+.home-tour-mount { max-width: 860px; margin: 22px auto 0; }
+.tour {
+ position: relative; border-radius: 20px; padding: 20px 22px 18px; text-align: center; color: var(--text-primary);
+ background: linear-gradient(160deg, color-mix(in srgb, var(--accent) 12%, var(--bg-card)), var(--bg-card));
+ border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border)); box-shadow: 0 20px 50px rgba(0,0,0,0.35);
+}
+.tour-head h2 { margin: 0 0 6px; font-size: clamp(1.2rem, 2.6vw, 1.6rem); font-weight: 900; }
+.tour-head h2 i { color: var(--accent-light); }
+.tour-head p { margin: 0 auto 14px; max-width: 640px; color: var(--text-secondary); font-size: 0.92rem; line-height: 1.5; }
+.tour-flow { display: grid; gap: 6px; justify-items: center; }
+.tour-src, .tour-out { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
+.tour-node {
+ display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px; border-radius: 999px; font-weight: 700; font-size: 0.8rem;
+ color: var(--text-primary); background: color-mix(in srgb, var(--bg-tertiary) 80%, transparent); border: 1px solid var(--border-light);
+}
+.tour-twitch { border-color: #9146ff; background: rgba(145,70,255,0.22); color: #fff; }
+.tour-yt { border-color: #ff0000; background: rgba(255,0,0,0.18); color: #fff; }
+.tour-kick { border-color: #53fc18; background: rgba(83,252,24,0.14); color: #d9ffcc; }
+.tour-rs { border-color: #7dd3fc; background: rgba(125,211,252,0.14); color: #fff; }
+.tour-hub {
+ display: grid; justify-items: center; gap: 2px; padding: 10px 18px; border-radius: 14px;
+ background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 45%, transparent), color-mix(in srgb, var(--accent-dark) 25%, transparent));
+ border: 1px solid color-mix(in srgb, var(--accent-light) 75%, transparent); color: #fff;
+}
+.tour-hub b { font-size: 1rem; }
+.tour-hub small { color: #e9d5ff; font-size: 0.74rem; font-weight: 600; }
+.tour-wire { position: relative; width: 4px; height: 26px; border-radius: 2px; background: color-mix(in srgb, var(--text-primary) 18%, transparent); }
+.tour-fan { width: min(70%, 420px); height: 26px; background: none; }
+.tour-fan::before { content: ''; position: absolute; left: 0; right: 0; top: 50%; height: 3px; background: repeating-linear-gradient(90deg, color-mix(in srgb, var(--text-primary) 45%, transparent) 0 8px, transparent 8px 16px); animation: tourDash 0.6s linear infinite; }
+.tour-fan::after { content: ''; position: absolute; left: 50%; top: 0; width: 4px; height: 50%; background: color-mix(in srgb, var(--text-primary) 18%, transparent); transform: translateX(-50%); }
+@keyframes tourDash { to { background-position: 20px 0; } }
+.tour-packet { position: absolute; left: 50%; top: -6px; width: 10px; height: 10px; margin-left: -5px; border-radius: 50%; background: #fde68a; animation: tourDown 1.2s linear infinite; }
+.tour-packet:nth-child(2) { animation-delay: 0.6s; }
+.tour-fan .tour-packet { top: 50%; margin-top: -5px; animation: tourFan 1.4s ease-in-out infinite; }
+.tour-fan .tour-packet:nth-child(2) { animation-name: tourFanL; animation-delay: 0.45s; }
+.tour-fan .tour-packet:nth-child(3) { animation-name: tourFanR; animation-delay: 0.9s; }
+@keyframes tourDown { 0% { transform: translateY(0); opacity: 0; } 15% { opacity: 1; } 100% { transform: translateY(26px); opacity: 0; } }
+@keyframes tourFan { 0% { transform: translate(0, -14px) scale(0.6); opacity: 0; } 30% { opacity: 1; } 100% { transform: translate(0, 12px) scale(1); opacity: 0; } }
+@keyframes tourFanL { 0% { transform: translate(0, -14px) scale(0.6); opacity: 0; } 30% { opacity: 1; } 100% { transform: translate(-160px, 12px) scale(1); opacity: 0; } }
+@keyframes tourFanR { 0% { transform: translate(0, -14px) scale(0.6); opacity: 0; } 30% { opacity: 1; } 100% { transform: translate(160px, 12px) scale(1); opacity: 0; } }
+.tour-features { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin: 14px 0 0; }
+.tour-feat {
+ display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; font-size: 0.78rem; font-weight: 700; color: var(--text-primary);
+ background: rgba(34,197,94,0.12); border: 1px solid rgba(34,197,94,0.45);
+ animation: tourPop 0.5s cubic-bezier(.2,1.5,.3,1) both; animation-delay: calc(0.2s + var(--i, 0) * 70ms);
+}
+.tour-feat i { color: #86efac; }
+@keyframes tourPop { from { opacity: 0; transform: translateY(8px) scale(0.85); } to { opacity: 1; transform: none; } }
+.tour-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-top: 16px; }
+@media (max-width: 720px) { .tour { padding: 16px 12px 14px; } .tour-fan { width: 90%; } .tour-node { font-size: 0.74rem; padding: 4px 9px; } }
+@media (prefers-reduced-motion: reduce) { .tour-fan::before, .tour-packet, .tour-feat { animation: none; } .tour-feat { opacity: 1; transform: none; } }
@@ -68,6 +68,7 @@
@@ -68,6 +68,7 @@
<link rel="stylesheet" href="/css/cosmetics.css?v=2">
<link rel="stylesheet" href="/css/cosmetics.css?v=2">
<link rel="stylesheet" href="/css/messenger.css?v=1">
<link rel="stylesheet" href="/css/messenger.css?v=1">
<link rel="stylesheet" href="/css/home-fx.css?v=4">
<link rel="stylesheet" href="/css/home-fx.css?v=4">
+ <link rel="stylesheet" href="/css/home-tour.css?v=1">
<link rel="stylesheet" href="/css/i18n-star.css?v=3">
<link rel="stylesheet" href="/css/i18n-star.css?v=3">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
@@ -248,6 +249,8 @@
@@ -248,6 +249,8 @@
<i class="fa-solid fa-wand-magic-sparkles"></i> Vibe Coders
<i class="fa-solid fa-wand-magic-sparkles"></i> Vibe Coders
</a>
</a>
</div>
</div>
+ <!-- One stream, everywhere — restream pipeline + feature tour (public/js/home-tour.js) -->
+ <div class="home-tour-mount" id="home-tour-mount"></div>
<!-- Newest shipped commit — built in the open, receipts included. -->
<!-- Newest shipped commit — built in the open, receipts included. -->
<a class="hero-latest" id="hero-latest" href="/updates" onclick="return handleLinkClick(event, '/updates')" style="display:none"></a>
<a class="hero-latest" id="hero-latest" href="/updates" onclick="return handleLinkClick(event, '/updates')" style="display:none"></a>
<div class="hero-philosophy-links">
<div class="hero-philosophy-links">
@@ -5120,6 +5123,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
@@ -5120,6 +5123,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<script src="/js/draggable-fab.js?v=2" defer></script>
<script src="/js/draggable-fab.js?v=2" defer></script>
<script src="/js/messenger.js?v=2" defer></script>
<script src="/js/messenger.js?v=2" defer></script>
<script src="/js/home-fx.js?v=2" defer></script>
<script src="/js/home-fx.js?v=2" defer></script>
+<script src="/js/home-tour.js?v=1" defer></script>
<!-- ── OpenVibe shared libs (notifications, account sync, themes) ── -->
<!-- ── OpenVibe shared libs (notifications, account sync, themes) ── -->
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
new file mode 100644
new file mode 100644
@@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
+/**
+ * home-tour.js — "One stream. Everywhere. Every toy." — the animated feature tour on the home page.
+ *
+ * Static content, no API: the restream pipeline (your cam or robot → OpenVibe.Live → Twitch,
+ * YouTube, Kick, RobotStreamer, any RTMP) with packets flowing along it, and the feature chips
+ * that only exist here. Mounted into #home-tour-mount under the hero CTA. Transform-only
+ * animations, so it stays smooth in mobile lite mode.
+ */
+(function () {
+ 'use strict';
+ function mount() {
+ const el = document.getElementById('home-tour-mount');
+ if (!el || el.dataset.mounted) return;
+ el.dataset.mounted = '1';
+ const go = (href) => `href="${href}" onclick="return handleLinkClick(event, '${href}')"`;
+ el.innerHTML = `
+ <div class="tour">
+ <div class="tour-head">
+ <h2><i class="fa-solid fa-satellite-dish"></i> One stream. Everywhere. Every toy.</h2>
+ <p>Go live here once and mirror it to every platform at the same time — while your chat, emotes, sound commands, robot controls, VODs and clips all live in one place.</p>
+ </div>
+ <div class="tour-flow" aria-label="Stream from OpenVibe.Live and restream everywhere">
+ <div class="tour-src"><span class="tour-node"><i class="fa-solid fa-video"></i> your cam</span><span class="tour-node"><i class="fa-solid fa-robot"></i> your robot</span><span class="tour-node"><i class="fa-solid fa-desktop"></i> OBS / RTMP</span></div>
+ <div class="tour-wire" aria-hidden="true"><span class="tour-packet"></span><span class="tour-packet"></span></div>
+ <div class="tour-hub"><b><i class="fa-solid fa-circle-nodes"></i> OpenVibe.Live</b><small>WebRTC <1s latency · emotes · sound commands · robot controls · VODs · clips · AI moments</small></div>
+ <div class="tour-wire tour-fan" aria-hidden="true"><span class="tour-packet"></span><span class="tour-packet"></span><span class="tour-packet"></span></div>
+ <div class="tour-out">
+ <span class="tour-node tour-twitch"><i class="fa-brands fa-twitch"></i> Twitch</span>
+ <span class="tour-node tour-yt"><i class="fa-brands fa-youtube"></i> YouTube</span>
+ <span class="tour-node tour-kick"><i class="fa-solid fa-bolt"></i> Kick</span>
+ <span class="tour-node tour-rs"><i class="fa-solid fa-robot"></i> RobotStreamer</span>
+ <span class="tour-node"><i class="fa-solid fa-tower-broadcast"></i> any RTMP</span>
+ </div>
+ </div>
+ <div class="tour-features">
+ <span class="tour-feat" style="--i:0"><i class="fa-solid fa-gauge-high"></i> Sub-second WebRTC latency</span>
+ <span class="tour-feat" style="--i:1"><i class="fa-solid fa-face-grin-squint-tears"></i> 7TV / BTTV / FFZ + custom emotes</span>
+ <span class="tour-feat" style="--i:2"><i class="fa-solid fa-volume-high"></i> Sound commands & soundboard</span>
+ <span class="tour-feat" style="--i:3"><i class="fa-solid fa-gamepad"></i> Robot & hardware controls</span>
+ <span class="tour-feat" style="--i:4"><i class="fa-solid fa-satellite-dish"></i> Free restream, all at once</span>
+ <span class="tour-feat" style="--i:5"><i class="fa-solid fa-scissors"></i> VODs, clips, AI moments</span>
+ <span class="tour-feat" style="--i:6"><i class="fa-solid fa-comments"></i> One chat from every platform</span>
+ <span class="tour-feat" style="--i:7"><i class="fa-solid fa-language"></i> Auto-translated chat</span>
+ <span class="tour-feat" style="--i:8"><i class="fa-solid fa-microphone-lines"></i> The Arena: mic-judged beefs</span>
+ <span class="tour-feat" style="--i:9"><i class="fa-brands fa-github"></i> 100% open source</span>
+ </div>
+ <div class="tour-actions">
+ <a class="btn btn-primary btn-lg" ${go('/broadcast')}><i class="fa-solid fa-tower-broadcast"></i> Go live</a>
+ <a class="btn btn-outline btn-lg" ${go('/docs/broadcasting')}><i class="fa-solid fa-book"></i> Restream setup</a>
+ </div>
+ </div>`;
+ }
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mount); else mount();
+})();