openvibers/openvibe.live Commits
Motion: Calm reduces, only Off removes (Shared 4.183.0) (#755)
Changes
@@@ -184,5 +184,5 @@ Minimum `.env`: `BASE_URL`, `JWT_SECRET`, `OV_NETWORK_URL`, `OV_NETWORK_INTERNAL<!-- versions:start -->- openvibe-contracts: v0.384.0- openvibe-sdk: v0.125.0--- openvibe-shared: v4.179.0++- openvibe-shared: v4.183.0<!-- versions:end -->@@@ -7,7 +7,7 @@"domain": "openvibe.live (www, ingest, whip)","contracts": "openvibe-contracts v0.384.0","sdk": "openvibe-sdk v0.125.0",-- "shared": "openvibe-shared v4.179.0",++ "shared": "openvibe-shared v4.183.0","capabilities": ["live.chat_context.read","live.chat_effects.write",@@@ -26,7 +26,7 @@"onnxruntime-node": "^1.20.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.125.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.179.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.183.0","pg": "^8.23.1","sanitize-html": "^2.17.7","sdp-transform": "^3.0.0",@@@ -3342,9 +3342,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.179.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.179.0",-- "integrity": "sha512-Fjffjq3gLINhiXTqQJwsKQT/QbV4TYvsmz6GTp5hBJ8xA6KUQgu0wDgCQ3BRxCZ0ehmn0HGVAHUI+5m0LtPQCQ==",++ "version": "4.183.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.183.0",++ "integrity": "sha512-vp9gg++Dhyj3h0cwvBjujM6dSFYoeXdh1UXdZ6OXP/NjG672Fs7uRANV4MjT467JtFDN7r7qLI3OJAwGQxOuQw==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -45,7 +45,7 @@"onnxruntime-node": "^1.20.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.125.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.179.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.183.0","pg": "^8.23.1","sanitize-html": "^2.17.7","sdp-transform": "^3.0.0",@@@ -96,10 +96,7 @@.fine { font-size: 0.82rem; color: #8a8480; margin-top: 18px; }.logo { color: #a78bfa; font-weight: 800; text-decoration: none; }-- @media (prefers-reduced-motion: reduce) {-- .bg, .bg::after, .stamp, .card.shake, .rv, .msg, .msg.hot::before, .hurt.beat, .go { animation: none !important; }-- .rv, .msg { opacity: 1; transform: none; }-- }++ html[data-ov-motion=off] .rv,html[data-ov-motion=off] .msg{opacity: 1;transform: none}</style></head><body>@@@ -152,7 +149,7 @@ok = !!(res.ok && j.ok); name = j.name || null;} catch (e) { ok = false; }if (!ok) { btn.disabled = false; btn.innerHTML = 'Try again <span class="arrow">→</span>'; return; }-- var reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;++ var reduced = (document.documentElement.getAttribute('data-ov-motion') === 'off');if (name) document.getElementById('welcome-line').textContent = name + ', the ban is lifted. Taking you to the site…';if (!reduced) {var cx = window.innerWidth / 2, cy = window.innerHeight / 2;@@@ -284,7 +284,6 @@align-items: center;gap: 6px;}--@media (prefers-reduced-motion: reduce) { .bc-live-badge { animation: none; } }.bc-live-badge i {font-size: 0.6rem;@@@ -448,16 +448,12 @@/* Cosmetic name/hat effects only animate while their row is on screen (IntersectionObserver). */.chat-msg.has-fx:not(.fx-on) .chat-user, .chat-msg.has-fx:not(.fx-on) .chat-user *,.chat-msg.has-fx:not(.fx-on) .chat-hat { animation-play-state: paused !important; }--@media (prefers-reduced-motion: reduce) {-- .chat-msg .chat-user, .chat-msg .chat-hat, .chat-particle { animation: none !important; }--}.chat-loading-history { padding: 14px; color: var(--text-muted); text-align: center; font-size: 0.85rem; }/* New rows arrive with a short rise; history (rendered off-screen and swapped in) does not animate. */@keyframes chatRowIn { from { opacity: 0; transform: translateY(6px); } }.chat-msg.chat-msg-new { animation: chatRowIn var(--ov-dur-2, 180ms) var(--ov-ease-out, cubic-bezier(.2, .8, .3, 1)); }--@media (prefers-reduced-motion: reduce) { .chat-msg.chat-msg-new { animation: none; } }/* Friendly global chat: folded rows. */.chat-friendly-group { margin: 2px 0; }@@@ -180,7 +180,4 @@.dsc-toggle-text b { font-size: 0.83rem; }.dsc-toggle-text small { font-size: 0.68rem; }}--@media (prefers-reduced-motion: reduce) {-- .dsc-toggle-count, .dsc-toggle-chev, .dsc-toggle { animation: none !important; transition: none !important; }--}.rs-lite .dsc-toggle-count { animation: none !important; }@@@ -243,10 +243,7 @@}.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::before,.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::after { animation-play-state: paused !important; }--@media (prefers-reduced-motion: reduce) {-- .name-fx-vegas, .name-fx-vegas::before, .name-fx-vegas::after { animation: none !important; }-- .name-fx-vegas { filter: drop-shadow(0 0 3px rgba(255, 207, 63, .9)); }--}++html[data-ov-motion=off] .name-fx-vegas{filter: drop-shadow(0 0 3px rgba(255, 207, 63, .9))}/* 2fat Bass Drop (2026-10-23, one of a kind: 2fat, who brings the old classic bangers back): the name thumps on the beat,a big drop then a smaller one, in club lights, with an equalizer dancing before it and a speaker after it. Only itsone holder wears it (fx_twofat is never in the catalog). The equalizer and the speaker are pseudo-elements, so they@@@ -308,10 +305,7 @@}.chat-msg.has-fx:not(.fx-on) .name-fx-twofat::before,.chat-msg.has-fx:not(.fx-on) .name-fx-twofat::after { animation-play-state: paused !important; }--@media (prefers-reduced-motion: reduce) {-- .name-fx-twofat, .name-fx-twofat::before, .name-fx-twofat::after { animation: none !important; }-- .name-fx-twofat { filter: drop-shadow(0 0 3px rgba(255, 47, 214, .9)); }--}++html[data-ov-motion=off] .name-fx-twofat{filter: drop-shadow(0 0 3px rgba(255, 47, 214, .9))}/* ══════════════════════════════════════════════════════════════@@@ -68,7 +68,6 @@.agent-overlay-dot.is-failed { background: #ef4444; }.agent-overlay-dot.is-skipped { background: #94a3b8; }@keyframes agent-overlay-pulse { 50% { opacity: 0.45; } }--@media (prefers-reduced-motion: reduce) { .agent-overlay-dot.is-started { animation: none; } }.agent-overlay-ticks { display: flex; gap: 3px; align-items: center; margin-top: 3px; min-height: 10px; }.agent-overlay-ticks[hidden] { display: none; }.agent-overlay-tick { width: 12px; height: 8px; padding: 0; border: 0; border-radius: 2px; background: #4b5563; cursor: pointer; opacity: 0.85; }@@@ -535,7 +535,6 @@#bc-stats-overlay .bc-stat > i { width: 16px; text-align: center; font-size: 0.72rem; color: rgba(255, 255, 255, 0.55); }#bc-stats-overlay .bc-stat-label { color: rgba(255, 255, 255, 0.66); min-width: 0; }#bc-stats-overlay .bc-stat-v { font-weight: 700; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }--@media (prefers-reduced-motion: reduce) { #bc-stats-overlay.bc-stats-overlay { animation: none; } }.bc-status-good { background: var(--success); color: #fff; }.bc-status-warn { background: var(--warning); color: #000; }@@@ -2505,7 +2504,6 @@.bc-ws-quality[open] .bc-sum-chev { transform: rotate(180deg); color: var(--accent); }.bc-ws-quality[open] > summary { border-bottom: 1px solid var(--border); }.bc-ws-quality .bc-ws-quality-inner { margin: 0; padding: 12px; }--@media (prefers-reduced-motion: reduce) { .bc-switch-thumb, .bc-sum-chev { transition: none; } }/* Wide panels: details on the left, how you stream on the right, so fields are not a thousand pixels wide. */.bc-ws-main { container-type: inline-size; container-name: bcws; }@@@ -2522,7 +2520,6 @@.bc-live-badge { animation: none; }.bc-live-badge i { animation: pulse-live 1.5s infinite; }.bc-rs-control .bc-restream-detail { flex-basis: 100%; min-width: 0; white-space: normal; overflow-wrap: anywhere; }--@media (prefers-reduced-motion: reduce) { .bc-live-badge i { animation: none; } }/* On a phone's full page the live controls are a grid of tiles (icon over a short label), not a column of thirteenfull-width bars; ending the stream keeps its own full-width row. */@media (max-width: 768px) {@@@ -2688,7 +2685,6 @@ body.bc-studio-on .toast-container { z-index: 1500; }/* Chat's own reconnecting line sits with the chat; the site-wide pill would say it twice (and read as the stream). */body.bc-studio-on .ov-conn-pill { display: none !important; }body.bc-studio-on #bc-chat-messages .chat-conn-status { display: block !important; width: fit-content; background: rgba(180, 83, 9, 0.85); font-style: normal; }--@media (prefers-reduced-motion: reduce) { .bcs, .bcs-state-spin { animation: none; } }.bcs-end { height: 34px; padding: 0 16px; border: 0; border-radius: 999px; background: #e11d48; color: #fff; font-size: 0.9rem; font-weight: 800; }.bcs-zoom {position: absolute; top: 38%; left: 50%; transform: translate(-50%, -50%); padding: 8px 16px; border-radius: 999px;@@@ -2800,7 +2796,6 @@ body.bc-studio-on #bc-chat-messages .chat-conn-status { display: block !importan.bcs-switch-k { position: absolute; top: 3px; left: 3px; width: 24px; height: 24px; border-radius: 50%; background: #fff; transition: transform var(--ov-dur-2, 0.15s); }.bcs-switch[aria-checked="true"] { background: #16a34a; border-color: transparent; }.bcs-switch[aria-checked="true"] .bcs-switch-k { transform: translateX(20px); }--@media (prefers-reduced-motion: reduce) { .bcs-switch-k { transition: none; } }.bcs-confirm {width: 100%; max-width: 360px; padding: 20px; border-radius: 18px; text-align: center;background: var(--bg-secondary, #15151c); color: var(--text-primary, #f4f4f5); box-shadow: 0 20px 60px rgba(0,0,0,0.5);@@@ -2827,10 +2822,6 @@ body.bc-studio-on #bc-chat-messages .chat-conn-status { display: block !importan.bc-live-pill-dot { width: 8px; height: 8px; border-radius: 50%; background: #fff; animation: bcLivePillPulse 1.6s var(--ov-ease-in-out, ease-in-out) infinite; }@keyframes bcLivePillPulse { 50% { opacity: 0.35; } }.bc-live-pill:focus-visible { outline: 3px solid #93c5fd; outline-offset: 2px; }--@media (prefers-reduced-motion: reduce) { .bc-live-pill-dot { animation: none; } }--@media (prefers-reduced-motion: reduce) {-- .bcs-live-dot, .bcs-sheet, .bcs-sheet-wrap { animation: none; }--}/* Setting up on a phone keeps the app's bottom bar, with Go Live lit, like every other page (the owner, 2026-11-01:"the buttons at the bottom don't show like our mobile buttons"). Only the live studio (body.bc-studio-on, above)@@@ -2911,10 +2902,7 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--}.bc-clip-name.is-held .bc-clip-name-timer { display: none; }@keyframes bc-clip-name-time { to { transform: scaleX(0); } }--@media (prefers-reduced-motion: reduce) {-- .bc-clip-name { animation: none; }-- .bc-clip-name-timer { animation: none; display: none; }--}++html[data-ov-motion=off] .bc-clip-name-timer{display: none}/* ── The camera chooser before going live (broadcast-devices.js _bcRenderCameraChooser) ──Phones: Front | Back, and a chip per camera when a side has several. Computers: a card per camera. */@@@ -2970,7 +2958,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--.bc-cam-side:focus-visible, .bc-cam-lens:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }.bc-cam-sides:has(> :nth-child(3)) .bc-cam-side { padding: 0 11px; }.bc-cam-sides:has(> :nth-child(3)) .bc-cam-side i { display: none; } /* Front · Back · USB: words only */--@media (prefers-reduced-motion: reduce) { .bc-cam-side, .bc-cam-lens, .bc-setup-row { transition: none; } }.bc-setup-note { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 4px 12px; margin: 8px 2px 0; font-size: 0.74rem; color: var(--text-muted); }.bc-setup-link { font: inherit; font-weight: 600; color: var(--accent); background: none; border: 0; padding: 6px 0; cursor: pointer; }.bc-setup-link:hover { text-decoration: underline; }@@@ -3011,7 +2998,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--.gl-device-sheet-head button { width: 44px; height: 44px; border: 0; border-radius: 50%; background: var(--bg-primary); color: var(--text-primary); }}@media (max-width: 760px) { .gl-preview { min-height: min(46vh, 390px); max-height: min(58vh, 520px); } .gl-pv-chips { bottom: 62px; } }--@media (prefers-reduced-motion: reduce) { .gl-allow-mark, .bcs-golive[data-mode="live"], .bcs-golive[data-mode="connecting"] { animation: none; } .bcs-golive-btn .fa-spin { animation-iteration-count: 1; } }.bc-ws-golive-section .bc-perm-hint { flex: 1 1 200px; display: flex; align-items: flex-start; gap: 8px; margin: 0; font-size: 0.84rem; }.bc-ws-golive-section .bc-perm-hint i { color: var(--accent); margin-top: 3px; }.bc-ws-golive-section #bc-perm-btn { flex: 1 0 auto; justify-content: center; min-height: 44px; border-radius: 12px; font-weight: 650; }@@@ -3136,7 +3122,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--.bcs-golive:not([hidden]) { animation: bcs-dock-in var(--ov-dur-2, 0.2s) var(--ov-ease-out, ease-out); }.bcs-golive-t { margin-left: 8px; }@keyframes bcs-dock-in { from { opacity: 0; transform: translateY(8px); } }--@media (prefers-reduced-motion: reduce) { .bcs-golive:not([hidden]), .bc-live-card-dot { animation: none; } }/* RobotStreamer, at every width: its state and Stop on one line, the details under them in full (on a phone the Stopbutton stood between them as a tall tile and the details ran off the right; on a computer the state sat at the far@@@ -3203,7 +3188,7 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--.bcs-gl-burst { position: absolute; left: 22px; top: 21px; width: 0; height: 0; pointer-events: none; }.bcs-gl-burst i { position: absolute; width: 5px; height: 5px; margin: -2.5px; border-radius: 50%; background: hsl(var(--a), 90%, 66%); opacity: 0; animation: bcs-gl-spark .9s var(--ov-ease-out, cubic-bezier(.2,.8,.3,1)) var(--d) forwards; }@keyframes bcs-gl-spark { 0% { opacity: 1; transform: rotate(var(--a)) translateX(0); } 100% { opacity: 0; transform: rotate(var(--a)) translateX(48px) scale(.4); } }--@media (prefers-reduced-motion: reduce) { .bcs-livecard, .bcs-gl-dot::after, .bcs-gl-burst i { animation: none !important; } .bcs-gl-burst { display: none; } }++html[data-ov-motion=off] .bcs-gl-burst{display: none}/* A phone: the link on its own line, whole, and Copy the width of the card (an easy tap while holding the phone). */@media (max-width: 480px) {.bcs-livecard .bcs-gl-link { flex-wrap: wrap; padding: 10px; gap: 10px; }@@@ -3314,7 +3299,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--.gl-loading span { width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted); animation: gl-breathe 1.2s var(--ov-ease-in-out, ease-in-out) infinite; }.gl-loading span:nth-child(2) { animation-delay: 0.2s; }.gl-loading span:nth-child(3) { animation-delay: 0.4s; }--@media (prefers-reduced-motion: reduce) { .gl-wait-dot, .gl-loading span { animation: none; } .gl-slot, .gl-chip, .gl-title { transition: none; } }/* Video quality is a fold too: the same row as the others (its older rule had its own padding). */.gl-fold.bc-ws-quality > summary { padding: 8px 14px; min-height: 52px; font-size: 0.9rem; }.gl-fold.bc-ws-quality > summary > i:first-child { width: 18px; }@@@ -3375,7 +3359,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--.gl-pv-off:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }.gl-pv-off[aria-pressed="true"] i::before { content: "\f06e"; }@media (pointer: coarse) { .gl-pv-off { width: 44px; height: 44px; } }--@media (prefers-reduced-motion: reduce) { .gl-preview video, .gl-pv-lvl i { transition: none; } }/* Test privately: the quiet button under Go live; while recording, a red dot and the seconds left; then the test playsback over the preview with Done. */@@@ -3398,7 +3381,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--background: var(--accent-strong, var(--accent)); color: var(--on-accent-strong, var(--on-accent, #fff)); }.gl-test-done:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }@media (pointer: coarse) { .gl-test-done { min-height: 44px; } }--@media (prefers-reduced-motion: reduce) { .gl-test-btn.is-rec i { animation: none; } }/* Labels (L1) in the go-live card's fold: six toggles; the AI's suggestions dashed, to confirm. */.gl-labels-why { margin: 0 0 10px; font-size: .82rem; line-height: 1.45; color: var(--text-secondary); }@@@ -3415,7 +3397,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--.gl-label--adult.is-on { color: var(--warning-text, var(--warning)); border-color: color-mix(in srgb, var(--warning-text, var(--warning)) 55%, var(--border)); background: color-mix(in srgb, var(--warning-text, var(--warning)) 10%, transparent); }.gl-label--ai { border-style: dashed; color: var(--accent); }@media (pointer: coarse) { .gl-label { min-height: 44px; } }--@media (prefers-reduced-motion: reduce) { .gl-label { transition: none; } }/* One "Turn on camera" (11-09 review): on a phone the Go live dock at the thumb carries it, so the card keeps the wordsand the other ways, not a second identical button. */@@@ -1453,7 +1453,6 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:.ch-chat-reopen[hidden] { display: none; }/* Below 1181px the chat is an overlay or a bottom sheet with its own open/close button, so no hide/show here. */@media (max-width: 1180px) { .ch-chat-collapse, .ch-chat-reopen { display: none !important; } }--@media (prefers-reduced-motion: reduce) { #page-channel.chat-revealing .stream-layout > .chat-sidebar { animation: none; } }/* ── Resizing the chat (public/js/channel-chat-resize.js) ─────One grip per chat: on its left edge beside the stream (width), on its top edge as a bottom sheet (height). */@@@ -1515,7 +1514,6 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:.ch-about-card-title { font-size: 0.88rem; font-weight: 650; line-height: 1.3; color: var(--text-primary);display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }.ch-about-card-meta { font-size: 0.75rem; color: var(--text-muted); }--@media (prefers-reduced-motion: reduce) { .ch-about-card-thumb, .ch-about-more i { transition: none; } }/* The channel page ends where its content ends: a full-viewport minimum left a screen of empty space above the footerwhenever the open tab was short. */@@@ -1610,9 +1608,6 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:}.player-shape-glow.is-on { opacity: 0.8; }#page-channel .video-container:fullscreen { aspect-ratio: auto; max-height: none; width: 100%; height: 100%; }--@media (prefers-reduced-motion: reduce) {-- #page-channel .video-container { transition: none; }--}/* ── A phone on its side: the stream fills the screen (2026-10-25) ───────────────Landscape, at most 540px tall, on a touch screen. Every rule above sizes the player for a portrait phone (half the@@@ -1772,8 +1767,6 @@ html:has(.openvibe-navbar.ovnav-hide) #page-channel .ch-tabs-wrap { top: 0; padd/* The phone's gutter is the container's 16 px plus the fold's own 8 px. */#page-channel .ch-tabs-wrap { margin-inline: -24px; padding-inline: 24px; }}--@media (prefers-reduced-motion: reduce) { #page-channel .ch-tabs-wrap { transition: none; } }--html[data-ov-motion="reduced"] #page-channel .ch-tabs-wrap { transition: none; }#page-channel .ch-tabs-wrap #ch-tabs { margin: 0; scroll-padding-inline: 14px; }#page-channel .ch-tabs-arrow { display: none; position: absolute; top: 50%; transform: translateY(-50%); z-index: 2; width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-card); color: var(--text-primary); cursor: pointer; }#page-channel .ch-tabs-arrow[data-dir="prev"] { left: 0; }@@@ -1810,8 +1803,6 @@ html[data-ov-motion="reduced"] #page-channel .ch-tabs-wrap { transition: none; }@media (max-width: 760px) { #page-channel .ch-tab-panel :is(.stream-grid, .pastes-grid) { grid-auto-columns: minmax(220px, 78%); } }#page-channel .ch-rail-skeleton { min-height: 150px; border-radius: 12px; background: linear-gradient(110deg, var(--bg-card) 25%, var(--bg-hover) 50%, var(--bg-card) 75%); background-size: 200% 100%; animation: ch-shimmer 1.5s linear infinite; }@keyframes ch-shimmer { to { background-position-x: -200%; } }--@media (prefers-reduced-motion: reduce) { #page-channel .ch-rail-skeleton { animation: none; } }--html[data-ov-motion="reduced"] #page-channel .ch-rail-skeleton { animation: none; }.ch-list-page { background: var(--bg-primary); color: var(--text-primary); }.ch-list-content { max-width: 960px; margin: 0 auto; padding: clamp(20px, 4vw, 48px); }.ch-list-content > p:first-child a { color: var(--accent); text-decoration: none; }@@@ -1840,7 +1831,7 @@ html[data-ov-motion="reduced"] #page-channel .ch-rail-skeleton { animation: none#page-channel .ch-tab-panel .stream-grid > * { scroll-snap-align: start; min-width: 0; }#page-channel .ch-tab-panel .stream-grid > p.muted { grid-column: auto; }}--@media (prefers-reduced-motion: reduce) { #page-channel .ch-tab-panel { scroll-margin-top: calc(var(--ovnav-h, 56px) + 56px); } }++html[data-ov-motion=off] #page-channel .ch-tab-panel{scroll-margin-top: calc(var(--ovnav-h, 56px) + 56px)}/* Shared channel identity, chips, actions, and More menu. */#page-channel :is(.ch-info-bar-text, .ch-offline-header-info) { min-width: 0; }@@@ -1969,7 +1960,6 @@ html[data-ov-motion="reduced"] #page-channel .ch-rail-skeleton { animation: nonebody:has(#page-channel.active.ch-is-live) .ch-chat-reopen .fa-comments { color: inherit; }}@keyframes chChatRise { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: none; } }--@media (prefers-reduced-motion: reduce) { #page-channel.chat-revealing .stream-layout > .chat-sidebar { animation: none !important; } }/* Offline, with when (ask 57, 11-09): a quiet state line and the last stream one tap away (the header says "Last live" too). */.ch-offline-state { display: inline-flex; align-items: center; gap: 8px; margin: 8px 0 0; padding: 6px 14px; border-radius: 999px; font-size: .9rem; font-weight: 600;@@@ -1985,7 +1975,6 @@ html[data-ov-motion="reduced"] #page-channel .ch-rail-skeleton { animation: none.ch-last-text { min-width: 0; display: grid; gap: 2px; }.ch-last-text b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .95rem; color: var(--text-primary); }.ch-last-text small { font-size: .78rem; color: var(--text-muted); }--@media (prefers-reduced-motion: reduce) { .ch-last-stream { transition: none; } }/* ── The player's bar, 11-09 (the owner: "the buttons and the icons… better sized and centred… better tags"). One buttonsize (40 px, 44 on a touch screen) with the icon centred at 18 px; LIVE is a red pill with a breathing dot (a grey@@@ -2000,7 +1989,6 @@ html[data-ov-motion="reduced"] #page-channel .ch-rail-skeleton { animation: none.video-controls .dvr-live-btn .live-dot { width: 7px; height: 7px; background: #fff; box-shadow: 0 0 0 0 rgba(255, 255, 255, .6); animation: vc-live-breathe 1.8s ease-in-out infinite; }.video-controls .dvr-live-btn.dvr-behind { background: rgba(255, 255, 255, .16) !important; box-shadow: none; }@keyframes vc-live-breathe { 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, .55); } 50% { box-shadow: 0 0 0 4px rgba(255, 255, 255, 0); } }--html[data-ov-motion=reduced] .video-controls .dvr-live-btn .live-dot { animation-duration: 3.6s; }.video-controls .viewer-badge, .video-controls .uptime-badge { height: 28px; min-height: 28px; padding: 0 10px; border-radius: 999px; gap: 6px; font-size: 12.5px !important; font-weight: 600;background: rgba(255, 255, 255, .12); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); color: #fff; }.video-controls .viewer-badge i, .video-controls .uptime-badge i { font-size: 12px; color: rgba(255, 255, 255, .75); }@@@ -360,7 +360,6 @@.vc-participant-row.speaking .vc-avatar-mini { box-shadow: 0 0 0 2px rgba(74, 222, 128, 0.7); }.vc-connected-panel.is-open { animation: vcPanelIn var(--ov-dur-2, 220ms) var(--ov-ease-out, cubic-bezier(.2, .9, .3, 1)); }@keyframes vcPanelIn { from { opacity: 0; transform: translateY(6px); } }--@media (prefers-reduced-motion: reduce) { .vc-channel-item.is-entering, .vc-channel-item.is-leaving, .vc-connected-panel.is-open { animation: none; } }/* Without JavaScript (the server inlines this fragment for a direct visit): what needs it, and where to go instead. */.chat-noscript { max-width: 720px; margin: 24px auto; padding: 0 16px; line-height: 1.6; }@@@ -498,11 +498,6 @@ a.cf-recap-band:hover .cf-recap-sub { text-decoration: underline; text-underline.cf-chips::-webkit-scrollbar { display: none; }.cf-chip { flex: none; min-height: 34px; padding: 0 12px; font-size: 0.84rem; }}--@media (prefers-reduced-motion: reduce) {-- .cf-card { animation: none; }-- .cf-skel-media::after, .cf-skel-line::after { animation: none; }-- .cf-media img.cf-thumb, .cf-play { transition: none; }--}/* A card with no picture shows who made it: their face, round, over a blur of the same picture. */.cf-thumb--who { overflow: hidden; }@@@ -7,7 +7,6 @@.pc-status { font-size: 0.92rem; display: flex; align-items: center; gap: 8px; min-height: 22px; }.pc-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; display: inline-block; }.pc-dot-on { background: #53fc18; box-shadow: 0 0 6px rgba(83,252,24,0.6); animation: pcDotPulse 2.2s var(--ov-ease-in-out, ease-in-out) infinite; }--@media (prefers-reduced-motion: reduce) { .pc-dot-on { animation: none; } }.pc-kind { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; padding: 1px 7px; border-radius: 6px; margin-left: 4px; vertical-align: middle; }.pc-kind-app { background: rgba(83,252,24,0.16); color: #6ee34a; }.pc-kind-test { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent-light, #e1b77d); }@@@ -88,10 +88,6 @@.hf-panel { padding: 14px 14px 14px; }.hf-overview { display: none; }}--@media (prefers-reduced-motion: reduce) {-- .hf, .hf-live i { animation: none; }-- .hf-media video, .hf-media canvas, .hf-media img.hf-frame, .hf-now { transition: none; }--}/* Touch pointers: the unmute pill answers a finger at 44 px (2026-11-05 phone sweep: 35.6 px tall). It sits on thevideo overlay, so it takes the height itself rather than a hit area over the frame's own link. */@@@ -108,7 +108,6 @@.sg-live-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #ef4444; margin-right: 2px; vertical-align: middle; animation: sg-pulse 1.6s var(--ov-ease-in-out, ease-in-out) infinite; }@keyframes sg-pulse { 50% { opacity: .35; } }.streamer-group-stream.is-live .streamer-group-stream-thumb { box-shadow: 0 0 0 2px #ef4444; border-radius: 8px; }--@media (prefers-reduced-motion: reduce) { .sg-live-dot { animation: none; } }.streamer-group-stream-title { font-size: 0.88rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.streamer-group-stream-time { font-size: 0.75rem; }.hero-rotate {@@@ -133,9 +132,6 @@transform: scale(1.07); filter: none; opacity: 1;box-shadow: 0 26px 60px -10px rgba(0, 0, 0, 0.85), 0 0 0 1px var(--accent) inset;}--@media (prefers-reduced-motion: reduce) {-- .hero-bg-layer { transition: opacity var(--ov-dur-4, 0.6s) var(--ov-ease-out, ease); }--}.hero-quip {height: 2.8em; margin: 2px auto 18px; max-width: 640px; overflow: hidden;display: grid; place-items: center; text-align: center; text-wrap: balance;@@@ -296,12 +292,7 @@.hero-stat-label { white-space: normal; line-height: 1.1; }.hero-float { border-radius: 10px; }}--@media (prefers-reduced-motion: reduce) {-- .hero-float { animation: heroFloatIn var(--ov-dur-1, 0.01s) forwards; opacity: 0.5; }-- .hero-float::after { animation: none; display: none; }-- .hero-collage { transition: none; transform: none; }-- .hero-quip { transition: none; }--}++html[data-ov-motion=off] .hero-float{opacity: 0.5}html[data-ov-motion=off] .hero-float::after{display: none}html[data-ov-motion=off] .hero-collage{transform: none}.stream-card-ago {margin-left: auto;font-size: 0.75rem;@@@ -372,9 +363,7 @@.home-cta-features { grid-template-columns: 1fr; }.cta-hero { font-size: 1.7rem; }}--@media (prefers-reduced-motion: reduce) {-- .cta-hero-grad { -webkit-text-fill-color: var(--accent); color: var(--accent); }--}++html[data-ov-motion=off] .cta-hero-grad{-webkit-text-fill-color: var(--accent);color: var(--accent)}.home-pastes-list {display: grid;grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));@@@ -476,10 +465,6 @@.stream-card-uptime, .stream-card-vcount { display: inline-flex; align-items: center; white-space: nowrap; }.stream-card-appear { animation: streamCardAppear var(--ov-dur-4, 0.6s) var(--ov-ease-out, ease); }.stream-card-leave { opacity: 0; transform: scale(0.94); transition: opacity var(--ov-dur-3, 0.4s) var(--ov-ease-out, ease), transform var(--ov-dur-3, 0.4s) var(--ov-ease-out, ease); pointer-events: none; }--@media (prefers-reduced-motion: reduce) {-- .stream-card-appear { animation: none; }-- .stream-card-leave { transition: none; }--}.hero-aurora::before, .hero-aurora::after {content: ''; position: absolute; pointer-events: none; border-radius: 50%;mix-blend-mode: lighten; filter: blur(44px);@@@ -569,9 +554,6 @@@media (max-width: 640px) {.hero-stats { min-height: 182px; }}--@media (prefers-reduced-motion: reduce) {-- .hero-stats.is-swapping { transition: none; }--}.hero-rotate-wrap { display: inline; }.hero-tagline { text-wrap: balance; overflow-wrap: break-word; }@media (max-width: 760px) {@@@ -647,7 +629,6 @@.home-load-more:active { transform: translateY(0) scale(0.99); }.home-load-more:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }.home-load-more:hover i { transform: translateY(2px); }--@media (prefers-reduced-motion: reduce) { .home-load-more, .home-load-more i { transition: none; } }.hero-stat:hover { border-color: color-mix(in srgb, var(--accent) 38%, transparent); }.hero-stat--clickable:hover { transform: translateY(-1px); }.hero-stat--live > i { color: var(--live-red-text, var(--live-red)); background: color-mix(in srgb, var(--live-red) 15%, transparent); border-color: color-mix(in srgb, var(--live-red) 34%, transparent); }@@@ -748,7 +729,6 @@animation-delay: calc(var(--ov-enter-i, 0) * 45ms);will-change: transform, opacity;}--@media (prefers-reduced-motion: reduce) { .ov-enter { animation: none !important; } }.cta-btn-short { display: none; }.cta-method { display: flex; flex-direction: column; }@@@ -826,11 +806,7 @@.cta-section-label { white-space: nowrap; }}--@media (prefers-reduced-motion: reduce) {-- .cta-method:hover::before, .home-cta-feature:hover::after { animation: none; opacity: 0; }-- .home-cta-banner.about-opening * { animation: none !important; }-- .cta-method:hover .cta-method-icon, .home-cta-feature:hover > i:first-child { transform: none; }--}++html[data-ov-motion=off] .cta-method:hover::before,html[data-ov-motion=off] .home-cta-feature:hover::after{opacity: 0}html[data-ov-motion=off] .cta-method:hover .cta-method-icon,html[data-ov-motion=off] .home-cta-feature:hover > i:first-child{transform: none}/* ── from i18n-star.css ── *//* ═══ Star of OpenVibe (decoration and compact grid: css/i18n-star.css) ═══@@@ -893,9 +869,7 @@.home-star-section { margin-bottom: 34px; }.star-live-card { max-width: 420px; width: 100%; margin: 0 auto; }}--@media (prefers-reduced-motion: reduce) {-- .star-petals { display: none; }--}++html[data-ov-motion=off] .star-petals{display: none}.rs-lite .star-card-border::before { animation: none; background: linear-gradient(135deg, var(--star-gold), #f472b6, var(--accent)); inset: 0; }.rs-lite .star-avatar-glow, .rs-lite .star-petals { display: none; }.rs-lite .star-avatar-ring { animation: none; }@@@ -1044,12 +1018,6 @@position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;}}--@media (prefers-reduced-motion: reduce) {-- .star-more.is-entering { animation: none; }-- .star-card.is-swapping { transition: none; }-- .star-card.is-compact .star-avatar-ring { animation: none; }-- .star-act, .star-act > i { transition: none; }--}/* The pitch banner: what kind of place this is, before the headline says it. */.cta-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin: 0 0 14px; }@@@ -1256,7 +1224,6 @@.hero-stat-all-sites > i:last-child { font-size: 0.7rem; color: var(--text-muted); transition: transform var(--ov-dur-2, 0.18s); }.hero-stat-all-sites:hover, .hero-stat-all-sites:focus-visible { border-color: color-mix(in srgb, var(--accent) 60%, transparent); background: color-mix(in srgb, var(--accent) 16%, transparent); outline: none; }.hero-stat-all-sites:hover > i:last-child { transform: translateX(3px); }--@media (prefers-reduced-motion: reduce) { .hero-stat-all-sites, .hero-stat-all-sites > i { transition: none; } }/* Recently Online: a stream that has a recording opens its replay (openvibe.video, the chat in time beside it), andits picture says so with a play mark (2026-10-27). */@@@ -1272,7 +1239,6 @@}.streamer-group-stream.has-replay:hover .sg-play i, .streamer-group-stream.has-replay:focus-visible .sg-play i { transform: scale(1.08); background: var(--accent); border-color: transparent; }.sg-replay { color: var(--accent-light, var(--accent)); font-weight: 700; }--@media (prefers-reduced-motion: reduce) { .sg-play i { transition: none; } }/* Touch screens (2026-10-29, tap-target audit): every control on the home reaches 44 px where it has room, and aninvisible hit area (::after) where growing it would move the layout. Nothing changes on a mouse. */@@@ -1337,7 +1303,6 @@}.home-rail-tail { display: none; }@keyframes homeRailPulse { from { opacity: 1; } to { opacity: .55; } }--@media (prefers-reduced-motion: reduce) { .home-rail-skel { animation: none; } }/* 2026-10-31, the phone home, shorter again: the featured stream's card leaves the Live Now grid on a phone (thefeatured block above already shows it), and when it was the only stream there the grid's header goes too; the@@@ -1517,12 +1482,7 @@/* Light themes: the surface follows the page, the aurora softens. */@media (prefers-color-scheme: light) { :root:not([data-theme]) .ovnet::before { opacity: .35; } }[data-theme-mode="light"] .ovnet::before, html.light .ovnet::before { opacity: .35; }--@media (prefers-reduced-motion: reduce) {-- .ovnet::before, .ovnet-sky i, .ovnet-dot { animation: none; }-- .ovnet-sky i { opacity: .45; }-- .ovnet.is-armed .ovnet-site { opacity: 1; --lift: 0px; }-- .ovnet-site, .ovnet-jump a, .ovnet-cta { transition: none; }--}++html[data-ov-motion=off] .ovnet-sky i{opacity: .45}html[data-ov-motion=off] .ovnet.is-armed .ovnet-site{opacity: 1;--lift: 0px}/* The menu hint carries the menu's own orb (11-09: "Jump between sites… from the OV mark" pointed at the wrong button). */.ovnet-switch .ovnet-orb { flex: none; color: var(--accent-light, #60a5fa); }@@@ -238,7 +238,6 @@ a.rd-use:hover b { color: var(--accent); }.rd-more > summary::before { content: ''; width: 8px; height: 8px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: translateY(-2px) rotate(-45deg); transition: transform var(--ov-dur-2, .18s) var(--ov-ease-out, ease); }.rd-more[open] > summary::before { transform: translateY(-4px) rotate(45deg); }.rd-more > .rd-topic-grid { padding: 4px 12px 14px; }--@media (prefers-reduced-motion: reduce) { .rd-more > summary::before { transition: none; } }/* ── The bar: search and filters (public/js/rundown.js) ──────────────────────────────────Sticky under the navbar on every Rundown page. The phone gets the search box and one@@@ -544,7 +543,6 @@ body.rd-sheet-open { overflow: hidden; }#rundown-root .rd-take .rd-compact-top .rd-chip { position: relative; min-height: 28px; }#rundown-root .rd-take .rd-compact-top .rd-chip::after { content: ''; position: absolute; inset: -8px; }}--@media (prefers-reduced-motion: reduce) { .rd-bar, #rundown-root .rd-react-btn, #rundown-root .rd-skeleton span { transition: none; animation: none; } }/* ── The day, at the top of the front page (rundown.js _rdDayDraw): the day in short when the AI summary is on, thentoday's moments as a row to swipe, in time order, each hung from one line by its dot. ── */@@@ -123,9 +123,7 @@.sn-tile:last-child:nth-child(odd) { grid-column: 1 / -1; }.sn-swipe-hint { display: inline; }}--@media (prefers-reduced-motion: reduce) {-- .sn-overlay, .sn-dialog, .sn-bar, .sn-marks .sn-line { animation: none !important; stroke-dashoffset: 0; }--}++html[data-ov-motion=off] .sn-overlay,html[data-ov-motion=off] .sn-dialog,html[data-ov-motion=off] .sn-bar,html[data-ov-motion=off] .sn-marks .sn-line{stroke-dashoffset: 0}@media (forced-colors: active) {.sn-bar, .sn-dot { fill: CanvasText; }.sn-line { stroke: CanvasText; }@@@ -27,7 +27,6 @@.ovp-out { font-size: 0.68rem; opacity: 0.55; }.ovp-n { margin-left: auto; padding-left: 12px; font-weight: 700; }.ovp-n.is-live { font-size: 0.72rem; font-weight: 700; color: #fca5a5; text-transform: uppercase; letter-spacing: 0.05em; }--@media (prefers-reduced-motion: reduce) { .ovp { animation: none; } }/* A section under the places (the live studio's restreams): each with a state dot, and a way to manage them. */.ovp-extra { margin-top: 4px; padding-top: 4px; border-top: 1px solid color-mix(in srgb, var(--text-primary, #fff) 10%, transparent); }.ovp-extra[hidden] { display: none; }@@@ -1,7 +1,7 @@/* ═══════════════════════════════════════════════════════════════HOME FX — decorative effects layer for the home page (public/js/home-fx.js)-- Additive only; everything degrades to the plain page without JS or under-- prefers-reduced-motion.++ Additive only; everything degrades to the plain page without JS or with++ motion off (Calm plays it, smaller and slower).═══════════════════════════════════════════════════════════════ */#page-home .hero { position: relative; }.hfx-sparks { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; opacity: 0.9; }@@@ -162,11 +162,7 @@#page-home .section-header h2 .live-dot { animation: hfxLive 1.4s var(--ov-ease-in-out, ease-in-out) infinite; }@keyframes hfxLive { 0%,100% { transform: scale(1); filter: drop-shadow(0 0 0 rgba(231,76,60,0)); } 50% { transform: scale(1.25); filter: drop-shadow(0 0 8px rgba(231,76,60,0.9)); } }--@media (prefers-reduced-motion: reduce) {-- .hfx-sparks, .hfx-spot { display: none; }-- .hfx-shimmer, .hfx-glow, .hb::before, .hb::after, .hb-ico, #page-home .section-header h2 .live-dot { animation: none !important; }-- .hfx-reveal { opacity: 1; transform: none; transition: none; }--}++html[data-ov-motion=off] .hfx-sparks,html[data-ov-motion=off] .hfx-spot{display: none}html[data-ov-motion=off] .hfx-reveal{opacity: 1;transform: none}.rs-lite .hfx-sparks, .rs-lite .hfx-spot { display: none; }.rs-lite .hfx-shimmer, .rs-lite .hfx-glow, .rs-lite .hb::after, .rs-lite #page-home .section-header h2 .live-dot { animation: none !important; text-shadow: none; }@@@ -239,7 +235,6 @@@media (max-width: 520px) { .hb-label small { display: none; } #page-home .hero-primary .hb-label small { display: inline; } }/* The very narrowest phones: Go live alone, so the label never squeezes. */@media (max-width: 340px) { #page-home .hero-primary .hb-label small { display: none; } }--@media (prefers-reduced-motion: reduce) { .hb-vibe, .hb-vibe .hb-ico::after { animation: none !important; } }/* Touch devices get no hover, so the one button on the row that has something extra to say needsto advertise it. A small pulsing dot marks it as tappable-for-more; it is pointer-events:none@@@ -267,7 +262,6 @@100% { transform: scale(3.3); opacity: 0; }}}--@media (hover: none) and (prefers-reduced-motion: reduce) { .hb[data-ovtip]::after { animation: none; } }/* ═══════════════════════════════════════════════════════════════════════════@@@ -143,7 +143,7 @@overflow-x: auto; scrollbar-width: none; margin-left: -14px; margin-right: -14px; padding: 0 14px 2px; }.ovg-join:not(.is-open) .ovg-join-perks::-webkit-scrollbar { display: none; }}--@media (prefers-reduced-motion: reduce) { .tour-wire, .tour-node-dot, .tour-core-ring, .tour-feat, .tour-core::before, .tour-core b, .tour-core-logo { animation: none; } .tour-feat { opacity: 1; transform: none; } }++html[data-ov-motion=off] .tour-feat{opacity: 1;transform: none}.rs-lite .tour::before, .rs-lite .tour-core-ring { display: none; }/* Phones keep the glow but not the repainting gradients. */.rs-lite .tour-core b { animation: none; -webkit-text-fill-color: var(--text-primary); background: none; }@@@ -342,9 +342,6 @@.ovg-join-expand:hover i, .ovg-join.is-open .ovg-join-expand i { animation: none !important; }@keyframes joinNudgeSweep { 0%, 62% { transform: translateX(0); } 100% { transform: translateX(420%); } }@keyframes joinNudge { 0%, 74%, 100% { transform: translateY(0); } 82% { transform: translateY(2.5px); } 90% { transform: translateY(0); } }--@media (prefers-reduced-motion: reduce) {-- .ovg-join-expand::after, .ovg-join-expand i { animation: none !important; }--}.tour-quest-slot.fx-live .ovg-join-perks li { animation: joinPerk var(--ov-dur-4, 0.6s) var(--ov-ease-out, cubic-bezier(.2, 1, .3, 1)) both;animation-delay: calc(var(--i) * 85ms + 100ms); }@keyframes joinPerk { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }@@@ -374,12 +371,6 @@.ovg-join-cta .btn { width: 100%; justify-content: center; }.ovg-join-tour { width: 100%; text-align: center; }}--@media (prefers-reduced-motion: reduce) {-- .ovg-quest-wave, .ovg-quest-bub, .ovg-quest-sheen, .ovg-quest-ico,-- .ovg-join-glow, .ovg-join-mark, .ovg-join-go::after { animation: none !important; }-- .ovg-quest-liquid { transition: none; }-- .ovg-join-perks li { animation: none !important; }--}/* ═══════════════════════════════════════════════════════════════The launch button — what the quest slot becomes once the setup list is finished, and what the@@@ -482,10 +473,6 @@.ovg-launch-ico { width: 34px; height: 34px; }.ovg-launch-text small { font-size: 0.7rem; }}--@media (prefers-reduced-motion: reduce) {-- .ovg-launch-aura, .ovg-firstlive-ring, .ovg-launch-sheen, .ovg-firstlive-sheen { animation: none !important; }-- .ovg-launch, .ovg-firstlive { transition: none; }--}.rs-lite .ovg-launch-aura, .rs-lite .ovg-firstlive-ring { animation: none !important; }/* ── Where the wires meet the hub ────────────────────────────────────────────@@@ -109,12 +109,7 @@}@keyframes starSheen { 0% { background-position: 120% 0; } 100% { background-position: -120% 0; } }--@media (prefers-reduced-motion: reduce) {-- .chat-translation, .chat-translate-hint, .ch-captions, .ch-caption, .ch-captions-dot, .ch-about-bio-translated,-- .star-card-border, .star-card-border::before, .star-petals span, .star-avatar-ring, .star-avatar-glow, .star-live-pill,-- .star-kicker i, .star-btn-watch::after { animation: none !important; }-- .chat-translation, .ch-caption, .star-card-border { opacity: 1; transform: none; }--}++html[data-ov-motion=off] .chat-translation,html[data-ov-motion=off] .ch-caption,html[data-ov-motion=off] .star-card-border{opacity: 1;transform: none}/* ── Chat: "translate to my language" hover button (sits left of the reply button) ── */.chat-msg-hoverable .chat-translate-btn {@@@ -199,9 +194,6 @@.star-card.is-compact { padding: 13px 13px 14px; gap: 9px 11px; }/* On a phone the three actions stack two-then-one cleanly rather than at random. */}--@media (prefers-reduced-motion: reduce) {-- .star-expand, .star-expand i, .star-card.is-compact .star-avatar { transition: none; }--}/* ── Chat: "Translate chat" switch (shows once a line in another language arrives; chat.js _autoTrChip) ──A compact pill on the right of the tools row, the height of the other tool buttons. The tools row's generic@@@ -82,7 +82,7 @@ code { background: rgba(255,255,255,.08); padding: 2px 6px; border-radius: 6px;animation: mp-pulse 1.4s var(--ov-ease-out, ease-out) infinite; /* a ring that pulses out: nothing spins */}@keyframes mp-pulse { from { box-shadow: 0 0 0 0 color-mix(in srgb, var(--mp-accent) 55%, transparent); } to { box-shadow: 0 0 0 12px transparent; } }--@media (prefers-reduced-motion: reduce) { .mp-spinner { animation: none; opacity: .7; } } /* a still ring while it loads */++html[data-ov-motion=off] .mp-spinner{opacity: .7} /* a still ring while it loads *//* ── Custom player controls ──────────────────────────────── */.mp-controls {@@@ -67,7 +67,6 @@overflow: hidden;animation: msgSlideUp var(--ov-dur-2, 0.2s) var(--ov-ease-out, ease-out);}--@media (prefers-reduced-motion: reduce) { .messenger-panel { animation: none; } }.messenger-panel.open {display: flex;}@@@ -110,7 +110,6 @@ body.ovg-lock { overflow: hidden; }.ovg-kv { grid-template-columns: 1fr; } .ovg-kv span { font-size: 0.7rem; }.ovg-foot { padding: 10px 14px calc(14px + env(safe-area-inset-bottom, 0px)); }}--@media (prefers-reduced-motion: reduce) { .ovg-card, .ovg-panel, .ovg-pick, .ovg-box, .ovg-sum-row, .ovg-big, .ovg-radar i, .ovg-live-badge { animation: none !important; } }/* ── Engine extras: clickable steps, hub, rings, spotlight, mini-journey forms ── */.ovg-steps { grid-template-columns: repeat(var(--n, 4), 1fr); }@@@ -156,11 +156,8 @@ html { -webkit-tap-highlight-color: transparent; }:where(a, button, summary, [role="button"], [tabindex]:not([tabindex="-1"]), input[type="checkbox"], input[type="radio"], input[type="range"]):focus-visible {outline: 2px solid var(--accent-light, var(--accent)); outline-offset: 2px;}--@media (prefers-reduced-motion: reduce) {-- /* No movement: the press is a brief dim instead. */-- :root { --press-scale: 1; --press-scale-card: 1; }-- :where(button, [role="button"], .btn, .ov-press, a.badge, .hb, .section-action, .digest-chip, .digest-recap, a.stream-card, a.moment-card, a.pulse-card, a.home-paste-card, a.streamer-group-stream, .ov-press-card):not(:disabled):active { opacity: 0.82; }--}++/* Motion off: no movement, the press is a brief dim instead. */++html[data-ov-motion=off]{--press-scale: 1;--press-scale-card: 1}html[data-ov-motion=off] :where(button, [role="button"], .btn, .ov-press, a.badge, .hb, .section-action, .digest-chip, .digest-recap, a.stream-card, a.moment-card, a.pulse-card, a.home-paste-card, a.streamer-group-stream, .ov-press-card):not(:disabled):active{opacity: 0.82}/* ═══════════════════════════════════════════════════════════════NAVBAR@@@ -1092,9 +1089,7 @@ html[data-theme-mode="light"] .hero-bg { --hero-veil: rgba(255, 255, 255, 0.62);from { transform: scale(1.06) translate(-1.4%, 1%); }to { transform: scale(1.16) translate(1.8%, -1.8%); }}--@media (prefers-reduced-motion: reduce) {-- .hero-bg-layer.active { animation: none; transform: scale(1.08); }--}++html[data-ov-motion=off] .hero-bg-layer.active{transform: scale(1.08)}@keyframes heroFloatIn { to { opacity: var(--op, 0.6); } }/* 3D drift: translate in x/y + slow z-tilt so cards feel like they float in space. */@@@ -1195,7 +1190,7 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }.ov-rail-bar > span { position: absolute; inset: 0 auto 0 0; border-radius: inherit; background: var(--rail-accent, var(--accent)); }/* Touch screens swipe; buttons there would only cover the cards. */@media (hover: none), (pointer: coarse) { .ov-rail-btn { display: none; } }--@media (prefers-reduced-motion: reduce) { .ov-rail-track { scroll-behavior: auto; } }++html[data-ov-motion=off] .ov-rail-track{scroll-behavior: auto}/* ── AI Moments showcase row ── */.moments-row { display: flex; gap: 12px; overflow-x: auto; margin-bottom: 22px; }@@@ -1258,9 +1253,6 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }itself off the chip now and the labels are short words, so both fit.) */.hero-stat-num { font-size: 1.05rem; }}--@media (prefers-reduced-motion: reduce) {-- .hero-stat--live > i { animation: none; }--}/* ═══════════════════════════════════════════════════════════════STREAM CARD EXTRAS (duration, ago badge)@@@ -1303,7 +1295,6 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }transition: max-height var(--ov-dur-4, 0.6s) var(--ov-ease-out, cubic-bezier(0.22, 1, 0.36, 1)), opacity var(--ov-dur-3, 0.4s) var(--ov-ease-out, ease), margin var(--ov-dur-3, 0.4s) var(--ov-ease-out, ease);}/* Toggle label swaps with collapsed state. */--@media (prefers-reduced-motion: reduce) { .home-about-body { transition: opacity var(--ov-dur-3, 0.3s) var(--ov-ease-out, ease); } }/* Animated glowing border sweep */@property --cta-angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }@@@ -1433,12 +1424,6 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }.cta-btn-glow { position: relative; isolation: isolate; box-shadow: 0 8px 22px color-mix(in srgb, var(--accent-dark) 35%, transparent); }@keyframes ctaBtnGlow { 0%,100% { opacity: 0; } 50% { opacity: 1; } }/* Respect reduced-motion */--@media (prefers-reduced-motion: reduce) {-- .home-cta-banner::before, .cta-ambient, .cta-embers span, .cta-live-dot, .cta-hero-grad,-- .cta-btn-glow, .home-cta-feature {-- animation: none !important;-- }--}/* ═══════════════════════════════════════════════════════════════FOUNDER STORY — Timeline Layout@@@ -2127,7 +2112,6 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }.muted-hint { z-index: 21; border: 0; cursor: pointer; -webkit-tap-highlight-color: transparent; }.muted-hint:hover, .muted-hint:focus-visible { background: var(--accent-strong, var(--accent, #3b82f6)); color: var(--on-accent-strong, #fff); }@keyframes unmute-pop { from { opacity: 0; transform: translateY(-4px) scale(.96); } }--@media (prefers-reduced-motion: reduce) { .unmute-pill { animation: none; } }/* ── DVR Progress Bar (Live Stream Seeking) ─────────────────── */.dvr-progress-wrap {@@@ -5398,10 +5382,6 @@ body.theme-light .btn-outline:hover {.call-participant-tile.is-leaving { animation: callTileOut var(--ov-dur-2, 240ms) ease-in forwards; pointer-events: none; }@keyframes callTileIn { from { opacity: 0; transform: scale(0.86); } }@keyframes callTileOut { to { opacity: 0; transform: scale(0.86); } }--@media (prefers-reduced-motion: reduce) {-- .call-participant-tile.is-entering, .call-participant-tile.is-leaving, .call-participant-tile.is-connecting .call-avatar, .vc-connected-badge i { animation: none !important; }-- .call-avatar::after { transition: none; }--}/* Video (camera on) */.call-video {@@@ -7284,7 +7264,7 @@ textarea.paste-textarea {/* Phones, scrolling down: the floating buttons step aside (draggable-fab.js) and come back on the way up. */body .floating-chat-fab, body .mobile-chat-toggle { transition: transform var(--ov-dur-2, 0.2s), opacity var(--ov-dur-2, 0.2s), background var(--ov-dur-2, 0.15s); }body.fabs-tucked .floating-chat-fab, body.fabs-tucked .mobile-chat-toggle { opacity: 0; transform: scale(0.6); pointer-events: none; }--@media (prefers-reduced-motion: reduce) { body.fabs-tucked .floating-chat-fab, body.fabs-tucked .mobile-chat-toggle { transform: none; } }++html[data-ov-motion=off] body.fabs-tucked .floating-chat-fab,html[data-ov-motion=off] body.fabs-tucked .mobile-chat-toggle{transform: none}/* ── Floating Chat Widget ─────────────────────────────────────── */.floating-chat-fab {@@@ -9016,9 +8996,6 @@ body.fabs-tucked .floating-chat-fab, body.fabs-tucked .mobile-chat-toggle { opac/* On a light page the shimmer's pale gold sank into the white; a deep amber keeps the name readable through it. */html[data-theme-mode="light"] .hero h1 { background-image: linear-gradient(90deg, var(--accent, var(--accent)), #b7791f, var(--accent, var(--accent))); }@keyframes heroShimmer { to { background-position: 200% center; } }--@media (prefers-reduced-motion: reduce) {-- .hero-aurora, .hero-aurora::before, .hero-aurora::after, .hero h1 { animation: none; }--}/* Stream description on live cards (collapsible) */@@@ -9038,7 +9015,6 @@ html[data-theme-mode="light"] .hero h1 { background-image: linear-gradient(90deg0%,100% { box-shadow: 0 8px 24px rgba(0,0,0,.5), 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }50% { box-shadow: 0 8px 24px rgba(0,0,0,.5), 0 0 0 12px transparent; }}--@media (prefers-reduced-motion: reduce) { .cp-bonus-chest { animation: none; } }/* Live-counting uptime tooltip */.uptime-tooltip {@@@ -9143,7 +9119,6 @@ html[data-theme-mode="light"] .hero h1 { background-image: linear-gradient(90degposition: static; height: auto; background: none; padding: 4px 0 6px;}.cgw-more.expanded i { animation: none; }--@media (prefers-reduced-motion: reduce) { .cgw-more i { animation: none; } }.cgw-goal {position: relative; height: 42px; border-radius: 9px; overflow: hidden; cursor: pointer;background: var(--bg-card); border: 1px solid var(--border);@@@ -9569,7 +9544,7 @@ label.staff-inline-toggle.aiv-field { flex-direction:row; align-items:center; }@keyframes ovSpark3 { 0% { opacity: 0; transform: translate(0,10px) scale(0.6); } 10% { opacity: 1; } 60% { opacity: 0; transform: translate(3px,16px) scale(1.2); } 100% { opacity: 0; } }@media (max-width: 600px) { .brand-mark { width: 26px; height: 26px; } .brand-mark svg { width: 25px; height: 25px; } .brand-mark--hero svg { width: 100%; height: 100%; } }.rs-lite .ov-orbit, .rs-lite .ov-spark { filter: none; }--@media (prefers-reduced-motion: reduce) { .brand-mark svg, .ov-glow, .ov-comet, .ov-v, .ov-dot, .ov-spark { animation: none !important; } .ov-orbit, .ov-spark { display: none; } .ov-comet { stroke-dasharray: none; opacity: 0.9; } }++html[data-ov-motion=off] .ov-orbit,html[data-ov-motion=off] .ov-spark{display: none}html[data-ov-motion=off] .ov-comet{stroke-dasharray: none;opacity: 0.9}@media (max-width: 600px) { .brand-mark { width: 26px; height: 26px; } .brand-mark svg { width: 25px; height: 25px; } }.ov-mark--ph { opacity: 0.45; color: var(--text-muted); }@@@ -9596,10 +9571,6 @@ label.staff-inline-toggle.aiv-field { flex-direction:row; align-items:center; }Only the element that is arriving animates, so the one leaving doesn't flash on its way out. */.hero-stat-full.is-entering, .hero-stat-strip.is-entering { animation: heroStatsOpen var(--ov-dur-3, 0.42s) var(--ov-ease-out, cubic-bezier(.2, 1, .3, 1)); }@keyframes heroStatsOpen { from { opacity: 0; transform: translateY(-7px); } to { opacity: 1; transform: none; } }--@media (prefers-reduced-motion: reduce) {-- .hero-stat-full.is-entering, .hero-stat-strip.is-entering { animation: none; }-- .hero-stat-more i { transition: none; }--}/* ── Hero polish ─────────────────────────────────────────────────────────────The rotating tagline is a single unbreakable line, so on a narrow screen a long phrase ran off@@@ -9852,9 +9823,6 @@ label.staff-inline-toggle.aiv-field { flex-direction:row; align-items:center; }#page-home.page { padding-bottom: 10px; }/* Route changes (the bar, the loading line, the fade-in) are openvibe-shared web-runtime enter() + route-transition.js. */--@media (prefers-reduced-motion: reduce) {-- .brand-mark.is-spin svg, .brand-mark.is-settling svg { animation: none !important; }--}/* ═══════════════════════════════════════════════════════════════Off-screen sections animate nothing.@@@ -9941,10 +9909,7 @@ label.staff-inline-toggle.aiv-field { flex-direction:row; align-items:center; }@keyframes ovConnPulse { 50% { transform: scale(0.55); opacity: 0.5; } }.chat-conn-status { font-style: italic; opacity: 0.85; }.chat-conn-status[data-state="connected"] { color: var(--success-text, var(--success)); }--@media (prefers-reduced-motion: reduce) {-- .ov-conn-pill { transition: opacity var(--motion-fast) linear; transform: translate3d(-50%, 0, 0); }-- .ov-conn-pill .ov-conn-dot { animation: none !important; }--}++html[data-ov-motion=off] .ov-conn-pill{transform: translate3d(-50%, 0, 0)}body.ov-panel-open #floating-chat-fab { opacity: 0; pointer-events: none; transform: scale(.85); transition: opacity var(--ov-dur-2, .15s), transform var(--ov-dur-2, .15s); }@@@ -60,7 +60,7 @@// page's effects script (which only loads on the home route).(function () {try {-- if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;++ if ((document.documentElement.getAttribute('data-ov-motion') === 'off')) return;if (matchMedia('(pointer: coarse)').matches || matchMedia('(max-width: 820px)').matches || (navigator.deviceMemory && navigator.deviceMemory <= 4)) {document.documentElement.classList.add('rs-lite');}@@@ -86,7 +86,6 @@.ovsk-fade{animation:ovskFade var(--ov-dur-2, .25s) var(--ov-ease-out, ease) forwards}@keyframes ovskFade{to{opacity:0}}@media (max-width:760px){#stream-grid-live .ovsk-card:nth-of-type(n+3){display:none}}-- @media (prefers-reduced-motion:reduce){.ovsk::after,.ovsk-bar::after{animation:none}}/* A route's own markup is fetched on first visit; until it lands the page shows its outline. */.ov-route-skel{display:grid;gap:14px;padding:24px 16px;max-width:1200px;margin:0 auto}.ov-route-skel span{display:block;border-radius:14px;background:color-mix(in srgb,var(--text-muted,#8b93ad) 10%,transparent);animation:ovRouteSkel 1.4s var(--ov-ease-in-out, ease-in-out) infinite alternate}@@@ -98,7 +97,6 @@.ov-route-error>i{font-size:1.4rem;color:var(--danger-text, var(--danger,#ef4444))}.ov-route-error>div{flex:1;min-width:180px;display:grid;gap:2px}.ov-route-error span{color:var(--text-muted,#8b93ad);font-size:.9rem}-- @media (prefers-reduced-motion:reduce){.ov-route-skel span{animation:none}}/* Keep the cosmetics modal out of the page before its stylesheet lands. */.cosmetics-modal-overlay:not(.active){display:none}</style>@@@ -2193,7 +2193,7 @@ function switchChannelTab(tab, btn, jump) {setTimeout(_queueChannelSectionSpy, reduce ? 200 : 1450);}function _channelReduceMotion() {-- return document.documentElement.dataset.ovMotion === 'reduced' || !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);++ return document.documentElement.dataset.ovMotion === 'off';}let _chJumpUntil = 0;function _markChannelJump(tab, fromTap = false) {@@@ -126,7 +126,7 @@ function ovPutCards(container, html, append) {added.forEach(el => el.classList.add('hfx-reveal', 'is-in'));let reduce = false;-- try { reduce = matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { /* */ }++ try { reduce = (document.documentElement.getAttribute('data-ov-motion') === 'off'); } catch { /* */ }if (reduce) return;added.forEach((el, i) => {el.classList.add('ov-enter');@@@ -209,7 +209,7 @@ function startHeroQuips(quips) {*/function animateHeightChange(wrap, mutate, opts = {}) {const { duration = 420, entering = null } = opts;-- const reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;++ const reduced = window.matchMedia && (document.documentElement.getAttribute('data-ov-motion') === 'off');if (!wrap || reduced) { try { mutate(); } catch { /* */ } return; }const h0 = wrap.offsetHeight;try { mutate(); } catch { /* */ }@@@ -508,7 +508,7 @@ function _renderHeroStats(stats) {* nothing is clipped once it settles and the board can still grow if a number gets longer.*/let swapping = 0;-- const reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;++ const reduced = window.matchMedia && (document.documentElement.getAttribute('data-ov-motion') === 'off');const swap = () => {const toOpen = full.hidden;if (reduced) {@@@ -752,7 +752,7 @@ function renderHeroBackground(media, moments) {layers[0].classList.remove('active'); void layers[0].offsetWidth; layers[0].classList.add('active');} else layers[0].classList.add('active');layers[1].classList.remove('active');-- if (frames.length < 2 || (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches && frames.length < 2)) return;++ if (frames.length < 2 || (window.matchMedia && (document.documentElement.getAttribute('data-ov-motion') === 'off') && frames.length < 2)) return;let idx = 0;_heroBgTimer = setInterval(() => {if (document.hidden) return;@@@ -777,7 +777,7 @@ function renderHeroCollage(media) {if (!wrap || !Array.isArray(media) || !media.length) return;if (_heroCollageTimer) { clearInterval(_heroCollageTimer); _heroCollageTimer = null; }wrap.innerHTML = '';-- const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;++ const reduce = window.matchMedia && (document.documentElement.getAttribute('data-ov-motion') === 'off');const w = window.innerWidth || 1200;const count = Math.min(media.length, w < 640 ? 5 : w < 1024 ? 8 : 12);const positions = _heroCollagePositions(count);@@@ -824,7 +824,7 @@ function _heroParallaxInit() {const hero = document.querySelector('#page-home .hero');const collage = document.getElementById('hero-collage');if (!hero || !collage) return;-- if (window.matchMedia && (window.matchMedia('(prefers-reduced-motion: reduce)').matches || window.matchMedia('(pointer: coarse)').matches)) return;++ if (window.matchMedia && ((document.documentElement.getAttribute('data-ov-motion') === 'off') || window.matchMedia('(pointer: coarse)').matches)) return;_heroParallaxBound = true;// Parallax is a plain 2D shift of the whole collage, written straight to its transform. It used// to write --tilt-x/--tilt-y custom properties; custom properties inherit, so every pointer move@@@ -980,7 +980,7 @@ function _initHomeAbout() {*/function _aboutPlayOpening(banner) {let reduce = false;-- try { reduce = matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { /* */ }++ try { reduce = (document.documentElement.getAttribute('data-ov-motion') === 'off'); } catch { /* */ }if (reduce) return;const items = banner.querySelectorAll('.home-about-body .cta-section-label, .home-about-body .cta-method, .home-about-body .home-cta-feature, .home-about-body .home-cta-buttons, .home-about-body .cta-money-lede');items.forEach((el, i) => el.style.setProperty('--about-i', String(Math.min(i, 16))));@@@ -1197,7 +1197,7 @@ function _ovnetCount(root) {const n = Number(root.querySelector('.ovnet-groups')?.dataset.sites || el?.dataset.count || 0);if (!el || !n) return;el.dataset.count = String(n);-- if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) { el.textContent = String(n); return; }++ if (document.documentElement.getAttribute('data-ov-motion') === 'off') { el.textContent = String(n); return; }const clock = window.performance || Date;const t0 = clock.now(), dur = 900;const step = (t) => {@@@ -1281,7 +1281,7 @@ function initHomeNetwork() {_ovnetCount(root);_ovnetIcons();};-- if ('IntersectionObserver' in window && !(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches)) {++ if ('IntersectionObserver' in window && !(window.matchMedia && (document.documentElement.getAttribute('data-ov-motion') === 'off'))) {root.classList.add('is-armed');const io = new IntersectionObserver((entries) => {if (entries.some((en) => en.isIntersecting)) { io.disconnect(); requestAnimationFrame(wake); }@@@ -1372,7 +1372,7 @@ function ovWakeBrandMark() {try { marks = document.querySelectorAll('.brand-mark'); } catch { return; }if (!marks || !marks.length) return;let reduce = false;-- try { reduce = matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { /* */ }++ try { reduce = (document.documentElement.getAttribute('data-ov-motion') === 'off'); } catch { /* */ }if (reduce) {// CSS stops the loops under reduced motion but cannot reach SMIL — the gradient sweep on the// V would otherwise run for the whole session. Hold everything still, once.@@@ -1762,7 +1762,7 @@ function _ensureGoalCycleScheduler() {}, GOAL_CYCLE_INTERVAL_MS);}function _playGoalCycle(el) {-- if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;++ if (window.matchMedia && (document.documentElement.getAttribute('data-ov-motion') === 'off')) return;const track = el && el.querySelector('.cgw-track-cycle');if (!track || track.children.length < 2) return;const n = parseInt(el.dataset.goalCount || '0', 10);@@@ -1050,7 +1050,7 @@ function _glOpenFold(id) {const d = document.getElementById(id);if (!d) return;d.open = true;-- d.scrollIntoView({ block: 'start', behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' });++ d.scrollIntoView({ block: 'start', behavior: (document.documentElement.getAttribute('data-ov-motion') === 'off') ? 'auto' : 'smooth' });}function _glCopyLink() {@@@ -2834,7 +2834,7 @@ function _chatEvCss() {@container (max-width:240px){.chat-ev{grid-template-columns:auto minmax(0,1fr)}.chat-ev-acts{grid-column:2;justify-self:start}}.chat-ev--here{grid-template-columns:auto minmax(0,1fr);padding:6px 10px}.chat-ev--here .chat-ev-icon{width:22px;height:22px}.chat-ev--here .chat-ev-icon::before{width:8px;height:8px}@container (max-width:360px){@media (pointer:coarse){.chat-ev-cta{width:44px}}}--@media (prefers-reduced-motion:reduce){.chat-ev.chat-msg-new{animation:none}.chat-ev--live .chat-ev-icon::before{animation:none}}`;++`;document.head.appendChild(st);}function _chatEvTime(at) {@@@ -390,7 +390,7 @@const ratios = new Map();let active = null;const still = (() => {-- try { if (matchMedia('(prefers-reduced-motion: reduce)').matches) return true; } catch { /* */ }++ try { if (/^(reduced|off)$/.test(document.documentElement.getAttribute('data-ov-motion') || '')) return true; } catch { /* */ }try { if (navigator.connection && navigator.connection.saveData) return true; } catch { /* */ }return false;})();@@@ -107,7 +107,7 @@localStorage.setItem(KEY, JSON.stringify([...openSet]));} catch { /* */ }-- const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;++ const reduce = (document.documentElement.getAttribute('data-ov-motion') === 'off');if (reduce) { body.hidden = !opening; busy = false; return; }// Animate the real height rather than a max-height guess: the board's height varies by@@@ -7,12 +7,12 @@* - scroll reveal: section headers, cards and grids slide/fade in as they enter the viewport* - count-up: hero stat numbers tick up from 0 when first painted* - card tilt: stream cards tilt slightly toward the pointer-- * Everything is disabled under prefers-reduced-motion, paused when the tab is hidden, and++ * Everything is disabled with motion off (Calm plays it), paused when the tab is hidden, and* skips the canvas on coarse-pointer / narrow screens to keep phones cool.*/(function () {'use strict';-- const REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;++ const REDUCED = window.matchMedia && (document.documentElement.getAttribute('data-ov-motion') === 'off');const COARSE = window.matchMedia && window.matchMedia('(pointer: coarse)').matches;if (REDUCED) return;// Phones, coarse pointers and low-memory devices run in "lite" mode: no particle canvas, no@@@ -131,7 +131,7 @@svg.setAttribute('viewBox', `0 0 ${R.width} ${R.height}`);svg.setAttribute('width', R.width); svg.setAttribute('height', R.height);const vertical = getComputedStyle(stage).getPropertyValue('--tour-vertical').trim() === '1';-- const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches || document.documentElement.classList.contains('rs-lite');++ const reduce = (document.documentElement.getAttribute('data-ov-motion') === 'off') || document.documentElement.classList.contains('rs-lite');const logo = stage.querySelector('.tour-core-logo') || stage.querySelector('[data-node="core"]');if (!logo) return;@@@ -55,7 +55,6 @@.ovd-bar { display: block; width: 2.5px; border-radius: 1px; background: currentColor; height: 11px; }.ovd-btn[data-n="1"] .ovd-bar { width: 9px; }.ovd-btn[data-n="2"] .ovd-bar { width: 5px; }--@media (prefers-reduced-motion: reduce) { .ovd-btn { transition: none; } }/* The grid honours the chosen count; without a choice it keeps its own auto-fit behaviour. */[data-ovd-cols] { grid-template-columns: repeat(var(--ovd-cols), minmax(0, 1fr)) !important; }`;@@@ -33,7 +33,7 @@const KEY = 'ov-mini-player';const st = { box: null, video: null, home: null, title: '', href: '', user: '', styleEl: null, chat: null, x: 0, y: 0, w: 0, corner: 'br', idle: 0, offRo: null };-- const reduced = () => { try { return matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { return false; } };++ const reduced = () => { try { return (document.documentElement.getAttribute('data-ov-motion') === 'off'); } catch { return false; } };const phone = () => { try { return matchMedia('(max-width: 759px)').matches; } catch { return false; } };const fine = () => { try { return matchMedia('(hover: hover) and (pointer: fine)').matches; } catch { return false; } };@@@ -106,7 +106,6 @@.ov-mp-chat-h .ov-mp-b{background:transparent;color:inherit;width:30px;height:30px}.ov-mp-chat-h .ov-mp-b:hover{background:rgba(127,127,127,.18)}.ov-mp-chat iframe{flex:1;width:100%;border:0;background:transparent}--@media (prefers-reduced-motion:reduce){.ov-mini-player.is-settling,.ov-mp-chat{transition:none}}`;(document.head || document.documentElement).appendChild(st.styleEl);}@@@ -36,7 +36,7 @@.ovnum.is-down .ovnum-digit.changed{animation:ovnumDown .62s ease-out}@keyframes ovnumUp{0%{filter:none}35%{filter:brightness(1.55)}100%{filter:none}}@keyframes ovnumDown{0%{filter:none}35%{filter:brightness(.72)}100%{filter:none}}--@media (prefers-reduced-motion:reduce){.ovnum-track{transition:none}.ovnum .ovnum-digit.changed{animation:none}}`;++`;function ensure() {if (document.getElementById('ovnum-css')) return;@@@ -13,7 +13,7 @@if (typeof window === 'undefined' || window.OVRail) return;let seq = 0;-- const reducedMotion = () => !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);++ const reducedMotion = () => !!(window.matchMedia && (document.documentElement.getAttribute('data-ov-motion') === 'off'));function button(dir, track, label) {const b = document.createElement('button');@@@ -39,7 +39,7 @@.ovsk-panel { padding: 14px; display: grid; gap: 10px; }.ovsk-fade { animation: ovskFade .25s ease forwards; }@keyframes ovskFade { to { opacity: 0; } }--@media (prefers-reduced-motion: reduce) { .ovsk::after, .ovsk-bar::after { animation: none; } }`;++`;function ensure() {if (document.getElementById('ovsk-css')) return;@@@ -235,7 +235,6 @@'.ov-freeze-notice button svg{width:16px;height:16px}','@keyframes ov-freeze-in{from{opacity:0;transform:translate(-50%,-6px) scale(.96)}}','@keyframes ov-freeze-turn{to{transform:rotate(360deg)}}',-- 'html[data-ov-motion=reduced] .ov-freeze-notice::before{animation-duration:1.8s}',].join('');(document.head || document.documentElement).appendChild(styleEl);}@@@ -58,7 +58,8 @@/** The blurred fill beside a picture narrower than its box. */function glow(box, source) {-- const reduce = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;++ // A slow colour fade moves nothing: Calm keeps the glow, only motion off drops it.++ const reduce = !!(document.documentElement && document.documentElement.getAttribute('data-ov-motion') === 'off');const saver = navigator.connection && navigator.connection.saveData;const c = document.createElement('canvas');const ctx = !reduce && !saver && c.getContext && c.getContext('2d');@@@ -146,7 +146,6 @@ button{font:inherit}box-shadow:0 0 12px var(--red);animation:pulse 1.6s infinite}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}--@media (prefers-reduced-motion:reduce){.hero-side .live-pill .pulse{animation:none}}.hero-side span.tag{display:block;margin-top:.8vh;font-size:.78vw;color:var(--muted);font-weight:850;@@@ -33,7 +33,7 @@'.hln-x{position:absolute;top:5px;right:7px;color:#7a7a86;font-size:14px;line-height:1;padding:2px;cursor:pointer;background:none;border:none}' +'.hln-x:hover{color:#fff}' +'@keyframes hlnPulse{0%,100%{opacity:1}50%{opacity:.3}}' +-- '@media (prefers-reduced-motion:reduce){.hln-card{transition:opacity .3s}}';++ '';(document.head || document.documentElement).appendChild(s);}@@@ -43,7 +43,7 @@ body[data-pos=bottom] .al { animation-name: alInUp; }.al-msg:empty { display: none; }.al-status { position: fixed; left: 1vw; bottom: 1vw; font-size: .9vw; color: rgba(255,255,255,.55); text-shadow: 0 1px 2px #000; }.al-status:empty { display: none; }--@media (prefers-reduced-motion: reduce) { .al, .al.is-out { animation: none; } .al::after { display: none; } }++html[data-ov-motion=off] .al::after{display: none}</style><script src="/shared/theme-loader.js"></script><!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->@@@ -264,10 +264,7 @@ body.no-shadow .msg { text-shadow: none; }}.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::before,.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::after { animation-play-state: paused !important; }--@media (prefers-reduced-motion: reduce) {-- .name-fx-vegas, .name-fx-vegas::before, .name-fx-vegas::after { animation: none !important; }-- .name-fx-vegas { filter: drop-shadow(0 0 3px rgba(255, 207, 63, .9)); }--}++html[data-ov-motion=off] .name-fx-vegas{filter: drop-shadow(0 0 3px rgba(255, 207, 63, .9))}/* 2fat Bass Drop (2026-10-23, one of a kind: 2fat, who brings the old classic bangers back): the name thumps on the beat,a big drop then a smaller one, in club lights, with an equalizer dancing before it and a speaker after it. Only itsone holder wears it (fx_twofat is never in the catalog). The equalizer and the speaker are pseudo-elements, so they@@@ -329,10 +326,7 @@ body.no-shadow .msg { text-shadow: none; }}.chat-msg.has-fx:not(.fx-on) .name-fx-twofat::before,.chat-msg.has-fx:not(.fx-on) .name-fx-twofat::after { animation-play-state: paused !important; }--@media (prefers-reduced-motion: reduce) {-- .name-fx-twofat, .name-fx-twofat::before, .name-fx-twofat::after { animation: none !important; }-- .name-fx-twofat { filter: drop-shadow(0 0 3px rgba(255, 47, 214, .9)); }--}++html[data-ov-motion=off] .name-fx-twofat{filter: drop-shadow(0 0 3px rgba(255, 47, 214, .9))}.name-fx-void {color: #9900ff !important;@@@ -56,7 +56,7 @@ body::after { content: ''; position: absolute; inset: 0; z-index: -1; pointer-ev.sc > :nth-child(4) { animation-delay: 1.2s; }.sc > :nth-child(5) { animation-delay: 1.35s; }@keyframes scIn { to { opacity: 1; transform: none; } }--@media (prefers-reduced-motion: reduce) { .sc-light { animation: none; } .sc > * { animation: none; opacity: 1; transform: none; } }++html[data-ov-motion=off] .sc > *{opacity: 1;transform: none}</style><script src="/shared/theme-loader.js"></script></head>@@@ -47,5 +47,5 @@ assert.ok(!/innerHTML\s*=\s*[^;]*ev\.(name|title)/.test(card), 'the name and titassert.ok(/who\.textContent = ev\.name/.test(card) && /title\.textContent = `“\$\{ev\.title\}”`/.test(card), 'they are text nodes');assert.ok(/showTime = !!opts\.isGlobal \|\| !!chatSettings\.showTimestamps/.test(card), 'time: global always, elsewhere the viewer\'s setting');assert.ok(/_CHAT_EV_FOLD_MS/.test(card) && /went live again/.test(card), 'the same person within 15 minutes updates the card');--assert.ok(/prefers-reduced-motion:reduce/.test(src.slice(src.indexOf('function _chatEvCss('), src.indexOf('function _chatEvTime('))), 'motion stops for people who ask');++assert.ok(!/prefers-reduced-motion/.test(src.slice(src.indexOf('function _chatEvCss('), src.indexOf('function _chatEvTime('))), 'Calm plays the cards (openvibe-shared 4.183.0); only motion off stops them, by the loader\'s one rule');console.log('chat event cards: ok');@@@ -44,11 +44,11 @@ const cosmetics = require('../server/monetization/cosmetics');const css = fs.readFileSync(path.join(__dirname, '..', file), 'utf8');assert.ok(css.includes('.name-fx-vegas {') && css.includes('@keyframes nameFxVegasReel') && css.includes('@keyframes nameFxVegasJackpot'), `${file} draws the effect`);assert.ok(css.includes('.px-vegas .chat-particle'), `${file} colours the Vegas particles`);-- assert.ok(css.includes('prefers-reduced-motion: reduce') && /\.name-fx-vegas::before, \.name-fx-vegas::after \{ animation: none/.test(css), `${file} stops the reels for reduced motion`);-- // 2fat's set (2026-10-23): the bass drop, its equalizer and its particles, stopped for reduced motion too.++ assert.ok(!css.includes('prefers-reduced-motion') && css.includes('html[data-ov-motion=off] .name-fx-vegas{filter:'), `${file}: Calm plays the reels; with motion off the glow stays`);++ // 2fat's set (2026-10-23): the bass drop, its equalizer and its particles; at rest on its glow with motion off.assert.ok(css.includes('.name-fx-twofat {') && css.includes('@keyframes nameFxTwofatThump') && css.includes('@keyframes nameFxTwofatEq'), `${file} draws 2fat's effect`);assert.ok(css.includes('.px-twofat .chat-particle'), `${file} colours 2fat's particles`);-- assert.ok(/\.name-fx-twofat, \.name-fx-twofat::before, \.name-fx-twofat::after \{ animation: none/.test(css), `${file} stops 2fat's effect for reduced motion`);++ assert.ok(css.includes('html[data-ov-motion=off] .name-fx-twofat{filter:'), `${file}: 2fat's effect rests on its glow with motion off`);}// 2fat's set is one of a kind like Vegas, and fits Inventory's kinds.const T = cosmetics.COSMETICS;@@@ -105,7 +105,7 @@ function page(overflowX) {// control's column clamp never squeezes a rail's cards.assert.match(css, /\.home-rail-tail \{ display: none; \}/);assert.match(css, /#home-catchup \.stream-grid > \.home-rail-tail, #home-catchup \.home-pastes-list > \.home-rail-tail \{ display: flex; \}/);-- assert.match(css, /@media \(prefers-reduced-motion: reduce\) \{ \.home-rail-skel \{ animation: none; \} \}/);++ assert.doesNotMatch(css, /\.home-rail-skel \{ animation: none/, 'the placeholder shimmers under Calm too (slower, by the loader)');assert.match(css, /#home-catchup \.stream-grid\[data-ovd-cols\], #home-catchup \.home-pastes-list\[data-ovd-cols\] \{ grid-template-columns: none !important; \}/);// Recent Pastes rides the same rail on a phone (it was two cards a row with "Load 2 more", 2026-11-02).assert.match(css, /#home-catchup \.stream-grid, #home-catchup \.home-pastes-list \{\n\s+display: grid; grid-auto-flow: column;/);@@@ -65,6 +65,9 @@ function openHome({ views = 0 } = {}) {});if (views) store.set('ov_home_intro_seen', String(views));window.matchMedia = () => ({ matches: true, addEventListener() { }, removeEventListener() { } });++ // Motion off: the page writes its numbers at once (the frame stub below passes no time to count with). Since++ // openvibe-shared 4.183.0 a device that asks for less motion still counts up; only Off does not.++ window.document.documentElement.setAttribute('data-ov-motion', 'off');window.requestAnimationFrame = (f) => setTimeout(f, 0);window.cancelAnimationFrame = () => { };window.scrollTo = () => { };@@@ -20,7 +20,7 @@ assert.ok(re.test('/obs/alerts/goosely') && !re.test('/obs/alerts/<x>') && !re.t// ── The page, statically ──assert.ok(!/innerHTML/.test(script), 'a message is only ever text');assert.match(SRC, /background: transparent !important/, 'a transparent ground over the stream');--assert.match(SRC, /@media \(prefers-reduced-motion: reduce\)/);++assert.doesNotMatch(SRC, /prefers-reduced-motion/, 'an alert plays whatever the streamer\'s own device asks');assert.match(script, /m\.type === 'overlay-alert'/, 'only overlay-alert messages');assert.match(script, /type: 'join', streamId: streamId, channel: user/, 'joins the channel\'s room, live or not');@@@ -26,7 +26,7 @@ assert.match(SRC, /\/\^\[a-z0-9-\]\{1,40\}\$\/\.test\(q\.get\('theme'\)/, 'the tassert.ok(!/innerHTML/.test(SRC), 'a query value is only ever text');assert.match(SRC, /\(q\.get\('msg'\) \|\| ''\)\.slice\(0, 140\)/, 'a message of your own, short');assert.match(SRC, /setAttribute\('data-state', 'busy'\)/, 'on Be right back the mark breathes');--assert.match(SRC, /@media \(prefers-reduced-motion: reduce\) \{ \.sc-light \{ animation: none; \}/, 'still under reduced motion');++assert.doesNotMatch(SRC, /prefers-reduced-motion/, 'a scene plays whatever the streamer\'s own device asks: the audience is who sees it');assert.ok(!/spin|rotate\(/.test(SRC.replace(/<!--[\s\S]*?-->/g, '')), 'nothing spins');// ── The dashboard: a Scenes card in the Chat tab, a URL for each scene in the theme picked ──const frag = fs.readFileSync(path.join(__dirname, '..', 'public/fragments/dashboard.html'), 'utf8');@@@ -36,4 +36,4 @@ assert.match(dash, /chatai: +\['dash-card-vibe', 'dash-aibot-card', 'dash-card-oassert.match(dash, /\['', 'obs', 'scene', kind, encodeURIComponent\(currentUser\.username\)\]\.join\('\/'\)/, 'each scene\'s address');assert.match(dash, /if \(theme && theme !== 'vibe'\) q\.set\('theme', theme\)/, 'the theme picked rides along');assert.match(dash, /_call\('updateDashScenes'\)/, 'drawn when the tab opens');--console.log('obs scenes: three scenes by route, the loader and the mark, any theme by name, the mark arriving, text only, still under reduced motion');++console.log('obs scenes: three scenes by route, the loader and the mark, any theme by name, the mark arriving, text only, playing whatever the device asks');@@@ -38,11 +38,11 @@ function element(tag, props = {}) {}, props);}--function load({ store = {}, reduce = false } = {}) {++function load({ store = {}, reduce = false, motion = null } = {}) {const observers = [];const ctx = {window: {}, console, Math, JSON, Number, Object, setInterval: () => 1, clearInterval() {},-- document: { hidden: false, createElement: (t) => element(t) },++ document: { hidden: false, createElement: (t) => element(t), documentElement: { getAttribute: (k) => (k === 'data-ov-motion' ? motion : null) } },navigator: {},matchMedia: () => ({ matches: reduce }),localStorage: { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); } },@@@ -123,12 +123,17 @@ const near = (a, b) => Math.abs(a - b) < 0.001;assert.ok(near(ratio(box), 4 / 3), 'the canvas drives the shape while it is the picture');}--// ── The glow is not made for reduced motion; the page wiring ──++// ── The glow is not made with motion off (Calm and a device that asks for less keep it: a slow colour fade moves++// nothing); the page wiring ──{-- const { api } = load({ reduce: true });++ const { api } = load({ motion: 'off' });const box = element('div');api.watch(element('video'), box, { key: 'x' });-- assert.strictEqual(box.children.length, 0, 'no glow canvas with reduced motion');++ assert.strictEqual(box.children.length, 0, 'no glow canvas with motion off');++ const calm = load({ reduce: true, motion: 'reduced' });++ const calmBox = element('div');++ calm.api.watch(element('video'), calmBox, { key: 'x' });++ assert.ok(calmBox.children.length > 0, 'Calm keeps the glow');const withGlow = element('div');load().api.watch(element('video'), withGlow, { key: 'y' });assert.strictEqual(withGlow.children[0] && withGlow.children[0].className, 'player-shape-glow');
@@ -184,5 +184,5 @@ Minimum `.env`: `BASE_URL`, `JWT_SECRET`, `OV_NETWORK_URL`, `OV_NETWORK_INTERNAL
@@ -184,5 +184,5 @@ Minimum `.env`: `BASE_URL`, `JWT_SECRET`, `OV_NETWORK_URL`, `OV_NETWORK_INTERNAL
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.384.0
- openvibe-contracts: v0.384.0
- openvibe-sdk: v0.125.0
- openvibe-sdk: v0.125.0
-- openvibe-shared: v4.179.0
+- openvibe-shared: v4.183.0
<!-- versions:end -->
<!-- versions:end -->
@@ -7,7 +7,7 @@
@@ -7,7 +7,7 @@
"domain": "openvibe.live (www, ingest, whip)",
"domain": "openvibe.live (www, ingest, whip)",
"contracts": "openvibe-contracts v0.384.0",
"contracts": "openvibe-contracts v0.384.0",
"sdk": "openvibe-sdk v0.125.0",
"sdk": "openvibe-sdk v0.125.0",
- "shared": "openvibe-shared v4.179.0",
+ "shared": "openvibe-shared v4.183.0",
"capabilities": [
"capabilities": [
"live.chat_context.read",
"live.chat_context.read",
"live.chat_effects.write",
"live.chat_effects.write",
@@ -26,7 +26,7 @@
@@ -26,7 +26,7 @@
"onnxruntime-node": "^1.20.0",
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.125.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.125.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.179.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.183.0",
"pg": "^8.23.1",
"pg": "^8.23.1",
"sanitize-html": "^2.17.7",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
"sdp-transform": "^3.0.0",
@@ -3342,9 +3342,9 @@
@@ -3342,9 +3342,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.179.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.179.0",
- "integrity": "sha512-Fjffjq3gLINhiXTqQJwsKQT/QbV4TYvsmz6GTp5hBJ8xA6KUQgu0wDgCQ3BRxCZ0ehmn0HGVAHUI+5m0LtPQCQ==",
+ "version": "4.183.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.183.0",
+ "integrity": "sha512-vp9gg++Dhyj3h0cwvBjujM6dSFYoeXdh1UXdZ6OXP/NjG672Fs7uRANV4MjT467JtFDN7r7qLI3OJAwGQxOuQw==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -45,7 +45,7 @@
@@ -45,7 +45,7 @@
"onnxruntime-node": "^1.20.0",
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.384.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.125.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.125.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.179.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.183.0",
"pg": "^8.23.1",
"pg": "^8.23.1",
"sanitize-html": "^2.17.7",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
"sdp-transform": "^3.0.0",
@@ -96,10 +96,7 @@
@@ -96,10 +96,7 @@
.fine { font-size: 0.82rem; color: #8a8480; margin-top: 18px; }
.fine { font-size: 0.82rem; color: #8a8480; margin-top: 18px; }
.logo { color: #a78bfa; font-weight: 800; text-decoration: none; }
.logo { color: #a78bfa; font-weight: 800; text-decoration: none; }
- @media (prefers-reduced-motion: reduce) {
- .bg, .bg::after, .stamp, .card.shake, .rv, .msg, .msg.hot::before, .hurt.beat, .go { animation: none !important; }
- .rv, .msg { opacity: 1; transform: none; }
- }
+ html[data-ov-motion=off] .rv,html[data-ov-motion=off] .msg{opacity: 1;transform: none}
</style>
</style>
</head>
</head>
<body>
<body>
@@ -152,7 +149,7 @@
@@ -152,7 +149,7 @@
ok = !!(res.ok && j.ok); name = j.name || null;
ok = !!(res.ok && j.ok); name = j.name || null;
} catch (e) { ok = false; }
} catch (e) { ok = false; }
if (!ok) { btn.disabled = false; btn.innerHTML = 'Try again <span class="arrow">→</span>'; return; }
if (!ok) { btn.disabled = false; btn.innerHTML = 'Try again <span class="arrow">→</span>'; return; }
- var reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
+ var reduced = (document.documentElement.getAttribute('data-ov-motion') === 'off');
if (name) document.getElementById('welcome-line').textContent = name + ', the ban is lifted. Taking you to the site…';
if (name) document.getElementById('welcome-line').textContent = name + ', the ban is lifted. Taking you to the site…';
if (!reduced) {
if (!reduced) {
var cx = window.innerWidth / 2, cy = window.innerHeight / 2;
var cx = window.innerWidth / 2, cy = window.innerHeight / 2;
@@ -284,7 +284,6 @@
@@ -284,7 +284,6 @@
align-items: center;
align-items: center;
gap: 6px;
gap: 6px;
}
}
-@media (prefers-reduced-motion: reduce) { .bc-live-badge { animation: none; } }
.bc-live-badge i {
.bc-live-badge i {
font-size: 0.6rem;
font-size: 0.6rem;
@@ -448,16 +448,12 @@
@@ -448,16 +448,12 @@
/* Cosmetic name/hat effects only animate while their row is on screen (IntersectionObserver). */
/* Cosmetic name/hat effects only animate while their row is on screen (IntersectionObserver). */
.chat-msg.has-fx:not(.fx-on) .chat-user, .chat-msg.has-fx:not(.fx-on) .chat-user *,
.chat-msg.has-fx:not(.fx-on) .chat-user, .chat-msg.has-fx:not(.fx-on) .chat-user *,
.chat-msg.has-fx:not(.fx-on) .chat-hat { animation-play-state: paused !important; }
.chat-msg.has-fx:not(.fx-on) .chat-hat { animation-play-state: paused !important; }
-@media (prefers-reduced-motion: reduce) {
- .chat-msg .chat-user, .chat-msg .chat-hat, .chat-particle { animation: none !important; }
-}
.chat-loading-history { padding: 14px; color: var(--text-muted); text-align: center; font-size: 0.85rem; }
.chat-loading-history { padding: 14px; color: var(--text-muted); text-align: center; font-size: 0.85rem; }
/* New rows arrive with a short rise; history (rendered off-screen and swapped in) does not animate. */
/* New rows arrive with a short rise; history (rendered off-screen and swapped in) does not animate. */
@keyframes chatRowIn { from { opacity: 0; transform: translateY(6px); } }
@keyframes chatRowIn { from { opacity: 0; transform: translateY(6px); } }
.chat-msg.chat-msg-new { animation: chatRowIn var(--ov-dur-2, 180ms) var(--ov-ease-out, cubic-bezier(.2, .8, .3, 1)); }
.chat-msg.chat-msg-new { animation: chatRowIn var(--ov-dur-2, 180ms) var(--ov-ease-out, cubic-bezier(.2, .8, .3, 1)); }
-@media (prefers-reduced-motion: reduce) { .chat-msg.chat-msg-new { animation: none; } }
/* Friendly global chat: folded rows. */
/* Friendly global chat: folded rows. */
.chat-friendly-group { margin: 2px 0; }
.chat-friendly-group { margin: 2px 0; }
@@ -180,7 +180,4 @@
@@ -180,7 +180,4 @@
.dsc-toggle-text b { font-size: 0.83rem; }
.dsc-toggle-text b { font-size: 0.83rem; }
.dsc-toggle-text small { font-size: 0.68rem; }
.dsc-toggle-text small { font-size: 0.68rem; }
}
}
-@media (prefers-reduced-motion: reduce) {
- .dsc-toggle-count, .dsc-toggle-chev, .dsc-toggle { animation: none !important; transition: none !important; }
-}
.rs-lite .dsc-toggle-count { animation: none !important; }
.rs-lite .dsc-toggle-count { animation: none !important; }
@@ -243,10 +243,7 @@
@@ -243,10 +243,7 @@
}
}
.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::before,
.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::before,
.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::after { animation-play-state: paused !important; }
.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::after { animation-play-state: paused !important; }
-@media (prefers-reduced-motion: reduce) {
- .name-fx-vegas, .name-fx-vegas::before, .name-fx-vegas::after { animation: none !important; }
- .name-fx-vegas { filter: drop-shadow(0 0 3px rgba(255, 207, 63, .9)); }
-}
+html[data-ov-motion=off] .name-fx-vegas{filter: drop-shadow(0 0 3px rgba(255, 207, 63, .9))}
/* 2fat Bass Drop (2026-10-23, one of a kind: 2fat, who brings the old classic bangers back): the name thumps on the beat,
/* 2fat Bass Drop (2026-10-23, one of a kind: 2fat, who brings the old classic bangers back): the name thumps on the beat,
a big drop then a smaller one, in club lights, with an equalizer dancing before it and a speaker after it. Only its
a big drop then a smaller one, in club lights, with an equalizer dancing before it and a speaker after it. Only its
one holder wears it (fx_twofat is never in the catalog). The equalizer and the speaker are pseudo-elements, so they
one holder wears it (fx_twofat is never in the catalog). The equalizer and the speaker are pseudo-elements, so they
@@ -308,10 +305,7 @@
@@ -308,10 +305,7 @@
}
}
.chat-msg.has-fx:not(.fx-on) .name-fx-twofat::before,
.chat-msg.has-fx:not(.fx-on) .name-fx-twofat::before,
.chat-msg.has-fx:not(.fx-on) .name-fx-twofat::after { animation-play-state: paused !important; }
.chat-msg.has-fx:not(.fx-on) .name-fx-twofat::after { animation-play-state: paused !important; }
-@media (prefers-reduced-motion: reduce) {
- .name-fx-twofat, .name-fx-twofat::before, .name-fx-twofat::after { animation: none !important; }
- .name-fx-twofat { filter: drop-shadow(0 0 3px rgba(255, 47, 214, .9)); }
-}
+html[data-ov-motion=off] .name-fx-twofat{filter: drop-shadow(0 0 3px rgba(255, 47, 214, .9))}
/* ══════════════════════════════════════════════════════════════
/* ══════════════════════════════════════════════════════════════
@@ -68,7 +68,6 @@
@@ -68,7 +68,6 @@
.agent-overlay-dot.is-failed { background: #ef4444; }
.agent-overlay-dot.is-failed { background: #ef4444; }
.agent-overlay-dot.is-skipped { background: #94a3b8; }
.agent-overlay-dot.is-skipped { background: #94a3b8; }
@keyframes agent-overlay-pulse { 50% { opacity: 0.45; } }
@keyframes agent-overlay-pulse { 50% { opacity: 0.45; } }
-@media (prefers-reduced-motion: reduce) { .agent-overlay-dot.is-started { animation: none; } }
.agent-overlay-ticks { display: flex; gap: 3px; align-items: center; margin-top: 3px; min-height: 10px; }
.agent-overlay-ticks { display: flex; gap: 3px; align-items: center; margin-top: 3px; min-height: 10px; }
.agent-overlay-ticks[hidden] { display: none; }
.agent-overlay-ticks[hidden] { display: none; }
.agent-overlay-tick { width: 12px; height: 8px; padding: 0; border: 0; border-radius: 2px; background: #4b5563; cursor: pointer; opacity: 0.85; }
.agent-overlay-tick { width: 12px; height: 8px; padding: 0; border: 0; border-radius: 2px; background: #4b5563; cursor: pointer; opacity: 0.85; }
@@ -535,7 +535,6 @@
@@ -535,7 +535,6 @@
#bc-stats-overlay .bc-stat > i { width: 16px; text-align: center; font-size: 0.72rem; color: rgba(255, 255, 255, 0.55); }
#bc-stats-overlay .bc-stat > i { width: 16px; text-align: center; font-size: 0.72rem; color: rgba(255, 255, 255, 0.55); }
#bc-stats-overlay .bc-stat-label { color: rgba(255, 255, 255, 0.66); min-width: 0; }
#bc-stats-overlay .bc-stat-label { color: rgba(255, 255, 255, 0.66); min-width: 0; }
#bc-stats-overlay .bc-stat-v { font-weight: 700; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
#bc-stats-overlay .bc-stat-v { font-weight: 700; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
-@media (prefers-reduced-motion: reduce) { #bc-stats-overlay.bc-stats-overlay { animation: none; } }
.bc-status-good { background: var(--success); color: #fff; }
.bc-status-good { background: var(--success); color: #fff; }
.bc-status-warn { background: var(--warning); color: #000; }
.bc-status-warn { background: var(--warning); color: #000; }
@@ -2505,7 +2504,6 @@
@@ -2505,7 +2504,6 @@
.bc-ws-quality[open] .bc-sum-chev { transform: rotate(180deg); color: var(--accent); }
.bc-ws-quality[open] .bc-sum-chev { transform: rotate(180deg); color: var(--accent); }
.bc-ws-quality[open] > summary { border-bottom: 1px solid var(--border); }
.bc-ws-quality[open] > summary { border-bottom: 1px solid var(--border); }
.bc-ws-quality .bc-ws-quality-inner { margin: 0; padding: 12px; }
.bc-ws-quality .bc-ws-quality-inner { margin: 0; padding: 12px; }
-@media (prefers-reduced-motion: reduce) { .bc-switch-thumb, .bc-sum-chev { transition: none; } }
/* Wide panels: details on the left, how you stream on the right, so fields are not a thousand pixels wide. */
/* Wide panels: details on the left, how you stream on the right, so fields are not a thousand pixels wide. */
.bc-ws-main { container-type: inline-size; container-name: bcws; }
.bc-ws-main { container-type: inline-size; container-name: bcws; }
@@ -2522,7 +2520,6 @@
@@ -2522,7 +2520,6 @@
.bc-live-badge { animation: none; }
.bc-live-badge { animation: none; }
.bc-live-badge i { animation: pulse-live 1.5s infinite; }
.bc-live-badge i { animation: pulse-live 1.5s infinite; }
.bc-rs-control .bc-restream-detail { flex-basis: 100%; min-width: 0; white-space: normal; overflow-wrap: anywhere; }
.bc-rs-control .bc-restream-detail { flex-basis: 100%; min-width: 0; white-space: normal; overflow-wrap: anywhere; }
-@media (prefers-reduced-motion: reduce) { .bc-live-badge i { animation: none; } }
/* On a phone's full page the live controls are a grid of tiles (icon over a short label), not a column of thirteen
/* On a phone's full page the live controls are a grid of tiles (icon over a short label), not a column of thirteen
full-width bars; ending the stream keeps its own full-width row. */
full-width bars; ending the stream keeps its own full-width row. */
@media (max-width: 768px) {
@media (max-width: 768px) {
@@ -2688,7 +2685,6 @@ body.bc-studio-on .toast-container { z-index: 1500; }
@@ -2688,7 +2685,6 @@ body.bc-studio-on .toast-container { z-index: 1500; }
/* Chat's own reconnecting line sits with the chat; the site-wide pill would say it twice (and read as the stream). */
/* Chat's own reconnecting line sits with the chat; the site-wide pill would say it twice (and read as the stream). */
body.bc-studio-on .ov-conn-pill { display: none !important; }
body.bc-studio-on .ov-conn-pill { display: none !important; }
body.bc-studio-on #bc-chat-messages .chat-conn-status { display: block !important; width: fit-content; background: rgba(180, 83, 9, 0.85); font-style: normal; }
body.bc-studio-on #bc-chat-messages .chat-conn-status { display: block !important; width: fit-content; background: rgba(180, 83, 9, 0.85); font-style: normal; }
-@media (prefers-reduced-motion: reduce) { .bcs, .bcs-state-spin { animation: none; } }
.bcs-end { height: 34px; padding: 0 16px; border: 0; border-radius: 999px; background: #e11d48; color: #fff; font-size: 0.9rem; font-weight: 800; }
.bcs-end { height: 34px; padding: 0 16px; border: 0; border-radius: 999px; background: #e11d48; color: #fff; font-size: 0.9rem; font-weight: 800; }
.bcs-zoom {
.bcs-zoom {
position: absolute; top: 38%; left: 50%; transform: translate(-50%, -50%); padding: 8px 16px; border-radius: 999px;
position: absolute; top: 38%; left: 50%; transform: translate(-50%, -50%); padding: 8px 16px; border-radius: 999px;
@@ -2800,7 +2796,6 @@ body.bc-studio-on #bc-chat-messages .chat-conn-status { display: block !importan
@@ -2800,7 +2796,6 @@ body.bc-studio-on #bc-chat-messages .chat-conn-status { display: block !importan
.bcs-switch-k { position: absolute; top: 3px; left: 3px; width: 24px; height: 24px; border-radius: 50%; background: #fff; transition: transform var(--ov-dur-2, 0.15s); }
.bcs-switch-k { position: absolute; top: 3px; left: 3px; width: 24px; height: 24px; border-radius: 50%; background: #fff; transition: transform var(--ov-dur-2, 0.15s); }
.bcs-switch[aria-checked="true"] { background: #16a34a; border-color: transparent; }
.bcs-switch[aria-checked="true"] { background: #16a34a; border-color: transparent; }
.bcs-switch[aria-checked="true"] .bcs-switch-k { transform: translateX(20px); }
.bcs-switch[aria-checked="true"] .bcs-switch-k { transform: translateX(20px); }
-@media (prefers-reduced-motion: reduce) { .bcs-switch-k { transition: none; } }
.bcs-confirm {
.bcs-confirm {
width: 100%; max-width: 360px; padding: 20px; border-radius: 18px; text-align: center;
width: 100%; max-width: 360px; padding: 20px; border-radius: 18px; text-align: center;
background: var(--bg-secondary, #15151c); color: var(--text-primary, #f4f4f5); box-shadow: 0 20px 60px rgba(0,0,0,0.5);
background: var(--bg-secondary, #15151c); color: var(--text-primary, #f4f4f5); box-shadow: 0 20px 60px rgba(0,0,0,0.5);
@@ -2827,10 +2822,6 @@ body.bc-studio-on #bc-chat-messages .chat-conn-status { display: block !importan
@@ -2827,10 +2822,6 @@ body.bc-studio-on #bc-chat-messages .chat-conn-status { display: block !importan
.bc-live-pill-dot { width: 8px; height: 8px; border-radius: 50%; background: #fff; animation: bcLivePillPulse 1.6s var(--ov-ease-in-out, ease-in-out) infinite; }
.bc-live-pill-dot { width: 8px; height: 8px; border-radius: 50%; background: #fff; animation: bcLivePillPulse 1.6s var(--ov-ease-in-out, ease-in-out) infinite; }
@keyframes bcLivePillPulse { 50% { opacity: 0.35; } }
@keyframes bcLivePillPulse { 50% { opacity: 0.35; } }
.bc-live-pill:focus-visible { outline: 3px solid #93c5fd; outline-offset: 2px; }
.bc-live-pill:focus-visible { outline: 3px solid #93c5fd; outline-offset: 2px; }
-@media (prefers-reduced-motion: reduce) { .bc-live-pill-dot { animation: none; } }
-@media (prefers-reduced-motion: reduce) {
- .bcs-live-dot, .bcs-sheet, .bcs-sheet-wrap { animation: none; }
-}
/* Setting up on a phone keeps the app's bottom bar, with Go Live lit, like every other page (the owner, 2026-11-01:
/* Setting up on a phone keeps the app's bottom bar, with Go Live lit, like every other page (the owner, 2026-11-01:
"the buttons at the bottom don't show like our mobile buttons"). Only the live studio (body.bc-studio-on, above)
"the buttons at the bottom don't show like our mobile buttons"). Only the live studio (body.bc-studio-on, above)
@@ -2911,10 +2902,7 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
@@ -2911,10 +2902,7 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
}
}
.bc-clip-name.is-held .bc-clip-name-timer { display: none; }
.bc-clip-name.is-held .bc-clip-name-timer { display: none; }
@keyframes bc-clip-name-time { to { transform: scaleX(0); } }
@keyframes bc-clip-name-time { to { transform: scaleX(0); } }
-@media (prefers-reduced-motion: reduce) {
- .bc-clip-name { animation: none; }
- .bc-clip-name-timer { animation: none; display: none; }
-}
+html[data-ov-motion=off] .bc-clip-name-timer{display: none}
/* ── The camera chooser before going live (broadcast-devices.js _bcRenderCameraChooser) ──
/* ── The camera chooser before going live (broadcast-devices.js _bcRenderCameraChooser) ──
Phones: Front | Back, and a chip per camera when a side has several. Computers: a card per camera. */
Phones: Front | Back, and a chip per camera when a side has several. Computers: a card per camera. */
@@ -2970,7 +2958,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
@@ -2970,7 +2958,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
.bc-cam-side:focus-visible, .bc-cam-lens:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.bc-cam-side:focus-visible, .bc-cam-lens:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.bc-cam-sides:has(> :nth-child(3)) .bc-cam-side { padding: 0 11px; }
.bc-cam-sides:has(> :nth-child(3)) .bc-cam-side { padding: 0 11px; }
.bc-cam-sides:has(> :nth-child(3)) .bc-cam-side i { display: none; } /* Front · Back · USB: words only */
.bc-cam-sides:has(> :nth-child(3)) .bc-cam-side i { display: none; } /* Front · Back · USB: words only */
-@media (prefers-reduced-motion: reduce) { .bc-cam-side, .bc-cam-lens, .bc-setup-row { transition: none; } }
.bc-setup-note { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 4px 12px; margin: 8px 2px 0; font-size: 0.74rem; color: var(--text-muted); }
.bc-setup-note { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 4px 12px; margin: 8px 2px 0; font-size: 0.74rem; color: var(--text-muted); }
.bc-setup-link { font: inherit; font-weight: 600; color: var(--accent); background: none; border: 0; padding: 6px 0; cursor: pointer; }
.bc-setup-link { font: inherit; font-weight: 600; color: var(--accent); background: none; border: 0; padding: 6px 0; cursor: pointer; }
.bc-setup-link:hover { text-decoration: underline; }
.bc-setup-link:hover { text-decoration: underline; }
@@ -3011,7 +2998,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
@@ -3011,7 +2998,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
.gl-device-sheet-head button { width: 44px; height: 44px; border: 0; border-radius: 50%; background: var(--bg-primary); color: var(--text-primary); }
.gl-device-sheet-head button { width: 44px; height: 44px; border: 0; border-radius: 50%; background: var(--bg-primary); color: var(--text-primary); }
}
}
@media (max-width: 760px) { .gl-preview { min-height: min(46vh, 390px); max-height: min(58vh, 520px); } .gl-pv-chips { bottom: 62px; } }
@media (max-width: 760px) { .gl-preview { min-height: min(46vh, 390px); max-height: min(58vh, 520px); } .gl-pv-chips { bottom: 62px; } }
-@media (prefers-reduced-motion: reduce) { .gl-allow-mark, .bcs-golive[data-mode="live"], .bcs-golive[data-mode="connecting"] { animation: none; } .bcs-golive-btn .fa-spin { animation-iteration-count: 1; } }
.bc-ws-golive-section .bc-perm-hint { flex: 1 1 200px; display: flex; align-items: flex-start; gap: 8px; margin: 0; font-size: 0.84rem; }
.bc-ws-golive-section .bc-perm-hint { flex: 1 1 200px; display: flex; align-items: flex-start; gap: 8px; margin: 0; font-size: 0.84rem; }
.bc-ws-golive-section .bc-perm-hint i { color: var(--accent); margin-top: 3px; }
.bc-ws-golive-section .bc-perm-hint i { color: var(--accent); margin-top: 3px; }
.bc-ws-golive-section #bc-perm-btn { flex: 1 0 auto; justify-content: center; min-height: 44px; border-radius: 12px; font-weight: 650; }
.bc-ws-golive-section #bc-perm-btn { flex: 1 0 auto; justify-content: center; min-height: 44px; border-radius: 12px; font-weight: 650; }
@@ -3136,7 +3122,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
@@ -3136,7 +3122,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
.bcs-golive:not([hidden]) { animation: bcs-dock-in var(--ov-dur-2, 0.2s) var(--ov-ease-out, ease-out); }
.bcs-golive:not([hidden]) { animation: bcs-dock-in var(--ov-dur-2, 0.2s) var(--ov-ease-out, ease-out); }
.bcs-golive-t { margin-left: 8px; }
.bcs-golive-t { margin-left: 8px; }
@keyframes bcs-dock-in { from { opacity: 0; transform: translateY(8px); } }
@keyframes bcs-dock-in { from { opacity: 0; transform: translateY(8px); } }
-@media (prefers-reduced-motion: reduce) { .bcs-golive:not([hidden]), .bc-live-card-dot { animation: none; } }
/* RobotStreamer, at every width: its state and Stop on one line, the details under them in full (on a phone the Stop
/* RobotStreamer, at every width: its state and Stop on one line, the details under them in full (on a phone the Stop
button stood between them as a tall tile and the details ran off the right; on a computer the state sat at the far
button stood between them as a tall tile and the details ran off the right; on a computer the state sat at the far
@@ -3203,7 +3188,7 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
@@ -3203,7 +3188,7 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
.bcs-gl-burst { position: absolute; left: 22px; top: 21px; width: 0; height: 0; pointer-events: none; }
.bcs-gl-burst { position: absolute; left: 22px; top: 21px; width: 0; height: 0; pointer-events: none; }
.bcs-gl-burst i { position: absolute; width: 5px; height: 5px; margin: -2.5px; border-radius: 50%; background: hsl(var(--a), 90%, 66%); opacity: 0; animation: bcs-gl-spark .9s var(--ov-ease-out, cubic-bezier(.2,.8,.3,1)) var(--d) forwards; }
.bcs-gl-burst i { position: absolute; width: 5px; height: 5px; margin: -2.5px; border-radius: 50%; background: hsl(var(--a), 90%, 66%); opacity: 0; animation: bcs-gl-spark .9s var(--ov-ease-out, cubic-bezier(.2,.8,.3,1)) var(--d) forwards; }
@keyframes bcs-gl-spark { 0% { opacity: 1; transform: rotate(var(--a)) translateX(0); } 100% { opacity: 0; transform: rotate(var(--a)) translateX(48px) scale(.4); } }
@keyframes bcs-gl-spark { 0% { opacity: 1; transform: rotate(var(--a)) translateX(0); } 100% { opacity: 0; transform: rotate(var(--a)) translateX(48px) scale(.4); } }
-@media (prefers-reduced-motion: reduce) { .bcs-livecard, .bcs-gl-dot::after, .bcs-gl-burst i { animation: none !important; } .bcs-gl-burst { display: none; } }
+html[data-ov-motion=off] .bcs-gl-burst{display: none}
/* A phone: the link on its own line, whole, and Copy the width of the card (an easy tap while holding the phone). */
/* A phone: the link on its own line, whole, and Copy the width of the card (an easy tap while holding the phone). */
@media (max-width: 480px) {
@media (max-width: 480px) {
.bcs-livecard .bcs-gl-link { flex-wrap: wrap; padding: 10px; gap: 10px; }
.bcs-livecard .bcs-gl-link { flex-wrap: wrap; padding: 10px; gap: 10px; }
@@ -3314,7 +3299,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
@@ -3314,7 +3299,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
.gl-loading span { width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted); animation: gl-breathe 1.2s var(--ov-ease-in-out, ease-in-out) infinite; }
.gl-loading span { width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted); animation: gl-breathe 1.2s var(--ov-ease-in-out, ease-in-out) infinite; }
.gl-loading span:nth-child(2) { animation-delay: 0.2s; }
.gl-loading span:nth-child(2) { animation-delay: 0.2s; }
.gl-loading span:nth-child(3) { animation-delay: 0.4s; }
.gl-loading span:nth-child(3) { animation-delay: 0.4s; }
-@media (prefers-reduced-motion: reduce) { .gl-wait-dot, .gl-loading span { animation: none; } .gl-slot, .gl-chip, .gl-title { transition: none; } }
/* Video quality is a fold too: the same row as the others (its older rule had its own padding). */
/* Video quality is a fold too: the same row as the others (its older rule had its own padding). */
.gl-fold.bc-ws-quality > summary { padding: 8px 14px; min-height: 52px; font-size: 0.9rem; }
.gl-fold.bc-ws-quality > summary { padding: 8px 14px; min-height: 52px; font-size: 0.9rem; }
.gl-fold.bc-ws-quality > summary > i:first-child { width: 18px; }
.gl-fold.bc-ws-quality > summary > i:first-child { width: 18px; }
@@ -3375,7 +3359,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
@@ -3375,7 +3359,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
.gl-pv-off:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.gl-pv-off:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.gl-pv-off[aria-pressed="true"] i::before { content: "\f06e"; }
.gl-pv-off[aria-pressed="true"] i::before { content: "\f06e"; }
@media (pointer: coarse) { .gl-pv-off { width: 44px; height: 44px; } }
@media (pointer: coarse) { .gl-pv-off { width: 44px; height: 44px; } }
-@media (prefers-reduced-motion: reduce) { .gl-preview video, .gl-pv-lvl i { transition: none; } }
/* Test privately: the quiet button under Go live; while recording, a red dot and the seconds left; then the test plays
/* Test privately: the quiet button under Go live; while recording, a red dot and the seconds left; then the test plays
back over the preview with Done. */
back over the preview with Done. */
@@ -3398,7 +3381,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
@@ -3398,7 +3381,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
background: var(--accent-strong, var(--accent)); color: var(--on-accent-strong, var(--on-accent, #fff)); }
background: var(--accent-strong, var(--accent)); color: var(--on-accent-strong, var(--on-accent, #fff)); }
.gl-test-done:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.gl-test-done:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (pointer: coarse) { .gl-test-done { min-height: 44px; } }
@media (pointer: coarse) { .gl-test-done { min-height: 44px; } }
-@media (prefers-reduced-motion: reduce) { .gl-test-btn.is-rec i { animation: none; } }
/* Labels (L1) in the go-live card's fold: six toggles; the AI's suggestions dashed, to confirm. */
/* Labels (L1) in the go-live card's fold: six toggles; the AI's suggestions dashed, to confirm. */
.gl-labels-why { margin: 0 0 10px; font-size: .82rem; line-height: 1.45; color: var(--text-secondary); }
.gl-labels-why { margin: 0 0 10px; font-size: .82rem; line-height: 1.45; color: var(--text-secondary); }
@@ -3415,7 +3397,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
@@ -3415,7 +3397,6 @@ body.bc-golive-dock-on #page-broadcast { padding-bottom: calc(104px + max(var(--
.gl-label--adult.is-on { color: var(--warning-text, var(--warning)); border-color: color-mix(in srgb, var(--warning-text, var(--warning)) 55%, var(--border)); background: color-mix(in srgb, var(--warning-text, var(--warning)) 10%, transparent); }
.gl-label--adult.is-on { color: var(--warning-text, var(--warning)); border-color: color-mix(in srgb, var(--warning-text, var(--warning)) 55%, var(--border)); background: color-mix(in srgb, var(--warning-text, var(--warning)) 10%, transparent); }
.gl-label--ai { border-style: dashed; color: var(--accent); }
.gl-label--ai { border-style: dashed; color: var(--accent); }
@media (pointer: coarse) { .gl-label { min-height: 44px; } }
@media (pointer: coarse) { .gl-label { min-height: 44px; } }
-@media (prefers-reduced-motion: reduce) { .gl-label { transition: none; } }
/* One "Turn on camera" (11-09 review): on a phone the Go live dock at the thumb carries it, so the card keeps the words
/* One "Turn on camera" (11-09 review): on a phone the Go live dock at the thumb carries it, so the card keeps the words
and the other ways, not a second identical button. */
and the other ways, not a second identical button. */
@@ -1453,7 +1453,6 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:
@@ -1453,7 +1453,6 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:
.ch-chat-reopen[hidden] { display: none; }
.ch-chat-reopen[hidden] { display: none; }
/* Below 1181px the chat is an overlay or a bottom sheet with its own open/close button, so no hide/show here. */
/* Below 1181px the chat is an overlay or a bottom sheet with its own open/close button, so no hide/show here. */
@media (max-width: 1180px) { .ch-chat-collapse, .ch-chat-reopen { display: none !important; } }
@media (max-width: 1180px) { .ch-chat-collapse, .ch-chat-reopen { display: none !important; } }
-@media (prefers-reduced-motion: reduce) { #page-channel.chat-revealing .stream-layout > .chat-sidebar { animation: none; } }
/* ── Resizing the chat (public/js/channel-chat-resize.js) ─────
/* ── Resizing the chat (public/js/channel-chat-resize.js) ─────
One grip per chat: on its left edge beside the stream (width), on its top edge as a bottom sheet (height). */
One grip per chat: on its left edge beside the stream (width), on its top edge as a bottom sheet (height). */
@@ -1515,7 +1514,6 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:
@@ -1515,7 +1514,6 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:
.ch-about-card-title { font-size: 0.88rem; font-weight: 650; line-height: 1.3; color: var(--text-primary);
.ch-about-card-title { font-size: 0.88rem; font-weight: 650; line-height: 1.3; color: var(--text-primary);
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.ch-about-card-meta { font-size: 0.75rem; color: var(--text-muted); }
.ch-about-card-meta { font-size: 0.75rem; color: var(--text-muted); }
-@media (prefers-reduced-motion: reduce) { .ch-about-card-thumb, .ch-about-more i { transition: none; } }
/* The channel page ends where its content ends: a full-viewport minimum left a screen of empty space above the footer
/* The channel page ends where its content ends: a full-viewport minimum left a screen of empty space above the footer
whenever the open tab was short. */
whenever the open tab was short. */
@@ -1610,9 +1608,6 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:
@@ -1610,9 +1608,6 @@ button.viewer-badge:focus-visible { outline: 2px solid #93c5fd; outline-offset:
}
}
.player-shape-glow.is-on { opacity: 0.8; }
.player-shape-glow.is-on { opacity: 0.8; }
#page-channel .video-container:fullscreen { aspect-ratio: auto; max-height: none; width: 100%; height: 100%; }
#page-channel .video-container:fullscreen { aspect-ratio: auto; max-height: none; width: 100%; height: 100%; }
-@media (prefers-reduced-motion: reduce) {
- #page-channel .video-container { transition: none; }
-}
/* ── A phone on its side: the stream fills the screen (2026-10-25) ───────────────
/* ── A phone on its side: the stream fills the screen (2026-10-25) ───────────────
Landscape, at most 540px tall, on a touch screen. Every rule above sizes the player for a portrait phone (half the
Landscape, at most 540px tall, on a touch screen. Every rule above sizes the player for a portrait phone (half the
@@ -1772,8 +1767,6 @@ html:has(.openvibe-navbar.ovnav-hide) #page-channel .ch-tabs-wrap { top: 0; padd
@@ -1772,8 +1767,6 @@ html:has(.openvibe-navbar.ovnav-hide) #page-channel .ch-tabs-wrap { top: 0; padd
/* The phone's gutter is the container's 16 px plus the fold's own 8 px. */
/* The phone's gutter is the container's 16 px plus the fold's own 8 px. */
#page-channel .ch-tabs-wrap { margin-inline: -24px; padding-inline: 24px; }
#page-channel .ch-tabs-wrap { margin-inline: -24px; padding-inline: 24px; }
}
}
-@media (prefers-reduced-motion: reduce) { #page-channel .ch-tabs-wrap { transition: none; } }
-html[data-ov-motion="reduced"] #page-channel .ch-tabs-wrap { transition: none; }
#page-channel .ch-tabs-wrap #ch-tabs { margin: 0; scroll-padding-inline: 14px; }
#page-channel .ch-tabs-wrap #ch-tabs { margin: 0; scroll-padding-inline: 14px; }
#page-channel .ch-tabs-arrow { display: none; position: absolute; top: 50%; transform: translateY(-50%); z-index: 2; width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-card); color: var(--text-primary); cursor: pointer; }
#page-channel .ch-tabs-arrow { display: none; position: absolute; top: 50%; transform: translateY(-50%); z-index: 2; width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-card); color: var(--text-primary); cursor: pointer; }
#page-channel .ch-tabs-arrow[data-dir="prev"] { left: 0; }
#page-channel .ch-tabs-arrow[data-dir="prev"] { left: 0; }
@@ -1810,8 +1803,6 @@ html[data-ov-motion="reduced"] #page-channel .ch-tabs-wrap { transition: none; }
@@ -1810,8 +1803,6 @@ html[data-ov-motion="reduced"] #page-channel .ch-tabs-wrap { transition: none; }
@media (max-width: 760px) { #page-channel .ch-tab-panel :is(.stream-grid, .pastes-grid) { grid-auto-columns: minmax(220px, 78%); } }
@media (max-width: 760px) { #page-channel .ch-tab-panel :is(.stream-grid, .pastes-grid) { grid-auto-columns: minmax(220px, 78%); } }
#page-channel .ch-rail-skeleton { min-height: 150px; border-radius: 12px; background: linear-gradient(110deg, var(--bg-card) 25%, var(--bg-hover) 50%, var(--bg-card) 75%); background-size: 200% 100%; animation: ch-shimmer 1.5s linear infinite; }
#page-channel .ch-rail-skeleton { min-height: 150px; border-radius: 12px; background: linear-gradient(110deg, var(--bg-card) 25%, var(--bg-hover) 50%, var(--bg-card) 75%); background-size: 200% 100%; animation: ch-shimmer 1.5s linear infinite; }
@keyframes ch-shimmer { to { background-position-x: -200%; } }
@keyframes ch-shimmer { to { background-position-x: -200%; } }
-@media (prefers-reduced-motion: reduce) { #page-channel .ch-rail-skeleton { animation: none; } }
-html[data-ov-motion="reduced"] #page-channel .ch-rail-skeleton { animation: none; }
.ch-list-page { background: var(--bg-primary); color: var(--text-primary); }
.ch-list-page { background: var(--bg-primary); color: var(--text-primary); }
.ch-list-content { max-width: 960px; margin: 0 auto; padding: clamp(20px, 4vw, 48px); }
.ch-list-content { max-width: 960px; margin: 0 auto; padding: clamp(20px, 4vw, 48px); }
.ch-list-content > p:first-child a { color: var(--accent); text-decoration: none; }
.ch-list-content > p:first-child a { color: var(--accent); text-decoration: none; }
@@ -1840,7 +1831,7 @@ html[data-ov-motion="reduced"] #page-channel .ch-rail-skeleton { animation: none
@@ -1840,7 +1831,7 @@ html[data-ov-motion="reduced"] #page-channel .ch-rail-skeleton { animation: none
#page-channel .ch-tab-panel .stream-grid > * { scroll-snap-align: start; min-width: 0; }
#page-channel .ch-tab-panel .stream-grid > * { scroll-snap-align: start; min-width: 0; }
#page-channel .ch-tab-panel .stream-grid > p.muted { grid-column: auto; }
#page-channel .ch-tab-panel .stream-grid > p.muted { grid-column: auto; }
}
}
-@media (prefers-reduced-motion: reduce) { #page-channel .ch-tab-panel { scroll-margin-top: calc(var(--ovnav-h, 56px) + 56px); } }
+html[data-ov-motion=off] #page-channel .ch-tab-panel{scroll-margin-top: calc(var(--ovnav-h, 56px) + 56px)}
/* Shared channel identity, chips, actions, and More menu. */
/* Shared channel identity, chips, actions, and More menu. */
#page-channel :is(.ch-info-bar-text, .ch-offline-header-info) { min-width: 0; }
#page-channel :is(.ch-info-bar-text, .ch-offline-header-info) { min-width: 0; }
@@ -1969,7 +1960,6 @@ html[data-ov-motion="reduced"] #page-channel .ch-rail-skeleton { animation: none
@@ -1969,7 +1960,6 @@ html[data-ov-motion="reduced"] #page-channel .ch-rail-skeleton { animation: none
body:has(#page-channel.active.ch-is-live) .ch-chat-reopen .fa-comments { color: inherit; }
body:has(#page-channel.active.ch-is-live) .ch-chat-reopen .fa-comments { color: inherit; }
}
}
@keyframes chChatRise { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: none; } }
@keyframes chChatRise { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: none; } }
-@media (prefers-reduced-motion: reduce) { #page-channel.chat-revealing .stream-layout > .chat-sidebar { animation: none !important; } }
/* Offline, with when (ask 57, 11-09): a quiet state line and the last stream one tap away (the header says "Last live" too). */
/* Offline, with when (ask 57, 11-09): a quiet state line and the last stream one tap away (the header says "Last live" too). */
.ch-offline-state { display: inline-flex; align-items: center; gap: 8px; margin: 8px 0 0; padding: 6px 14px; border-radius: 999px; font-size: .9rem; font-weight: 600;
.ch-offline-state { display: inline-flex; align-items: center; gap: 8px; margin: 8px 0 0; padding: 6px 14px; border-radius: 999px; font-size: .9rem; font-weight: 600;
@@ -1985,7 +1975,6 @@ html[data-ov-motion="reduced"] #page-channel .ch-rail-skeleton { animation: none
@@ -1985,7 +1975,6 @@ html[data-ov-motion="reduced"] #page-channel .ch-rail-skeleton { animation: none
.ch-last-text { min-width: 0; display: grid; gap: 2px; }
.ch-last-text { min-width: 0; display: grid; gap: 2px; }
.ch-last-text b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .95rem; color: var(--text-primary); }
.ch-last-text b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .95rem; color: var(--text-primary); }
.ch-last-text small { font-size: .78rem; color: var(--text-muted); }
.ch-last-text small { font-size: .78rem; color: var(--text-muted); }
-@media (prefers-reduced-motion: reduce) { .ch-last-stream { transition: none; } }
/* ── The player's bar, 11-09 (the owner: "the buttons and the icons… better sized and centred… better tags"). One button
/* ── The player's bar, 11-09 (the owner: "the buttons and the icons… better sized and centred… better tags"). One button
size (40 px, 44 on a touch screen) with the icon centred at 18 px; LIVE is a red pill with a breathing dot (a grey
size (40 px, 44 on a touch screen) with the icon centred at 18 px; LIVE is a red pill with a breathing dot (a grey
@@ -2000,7 +1989,6 @@ html[data-ov-motion="reduced"] #page-channel .ch-rail-skeleton { animation: none
@@ -2000,7 +1989,6 @@ html[data-ov-motion="reduced"] #page-channel .ch-rail-skeleton { animation: none
.video-controls .dvr-live-btn .live-dot { width: 7px; height: 7px; background: #fff; box-shadow: 0 0 0 0 rgba(255, 255, 255, .6); animation: vc-live-breathe 1.8s ease-in-out infinite; }
.video-controls .dvr-live-btn .live-dot { width: 7px; height: 7px; background: #fff; box-shadow: 0 0 0 0 rgba(255, 255, 255, .6); animation: vc-live-breathe 1.8s ease-in-out infinite; }
.video-controls .dvr-live-btn.dvr-behind { background: rgba(255, 255, 255, .16) !important; box-shadow: none; }
.video-controls .dvr-live-btn.dvr-behind { background: rgba(255, 255, 255, .16) !important; box-shadow: none; }
@keyframes vc-live-breathe { 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, .55); } 50% { box-shadow: 0 0 0 4px rgba(255, 255, 255, 0); } }
@keyframes vc-live-breathe { 0%, 100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, .55); } 50% { box-shadow: 0 0 0 4px rgba(255, 255, 255, 0); } }
-html[data-ov-motion=reduced] .video-controls .dvr-live-btn .live-dot { animation-duration: 3.6s; }
.video-controls .viewer-badge, .video-controls .uptime-badge { height: 28px; min-height: 28px; padding: 0 10px; border-radius: 999px; gap: 6px; font-size: 12.5px !important; font-weight: 600;
.video-controls .viewer-badge, .video-controls .uptime-badge { height: 28px; min-height: 28px; padding: 0 10px; border-radius: 999px; gap: 6px; font-size: 12.5px !important; font-weight: 600;
background: rgba(255, 255, 255, .12); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); color: #fff; }
background: rgba(255, 255, 255, .12); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); color: #fff; }
.video-controls .viewer-badge i, .video-controls .uptime-badge i { font-size: 12px; color: rgba(255, 255, 255, .75); }
.video-controls .viewer-badge i, .video-controls .uptime-badge i { font-size: 12px; color: rgba(255, 255, 255, .75); }
@@ -360,7 +360,6 @@
@@ -360,7 +360,6 @@
.vc-participant-row.speaking .vc-avatar-mini { box-shadow: 0 0 0 2px rgba(74, 222, 128, 0.7); }
.vc-participant-row.speaking .vc-avatar-mini { box-shadow: 0 0 0 2px rgba(74, 222, 128, 0.7); }
.vc-connected-panel.is-open { animation: vcPanelIn var(--ov-dur-2, 220ms) var(--ov-ease-out, cubic-bezier(.2, .9, .3, 1)); }
.vc-connected-panel.is-open { animation: vcPanelIn var(--ov-dur-2, 220ms) var(--ov-ease-out, cubic-bezier(.2, .9, .3, 1)); }
@keyframes vcPanelIn { from { opacity: 0; transform: translateY(6px); } }
@keyframes vcPanelIn { from { opacity: 0; transform: translateY(6px); } }
-@media (prefers-reduced-motion: reduce) { .vc-channel-item.is-entering, .vc-channel-item.is-leaving, .vc-connected-panel.is-open { animation: none; } }
/* Without JavaScript (the server inlines this fragment for a direct visit): what needs it, and where to go instead. */
/* Without JavaScript (the server inlines this fragment for a direct visit): what needs it, and where to go instead. */
.chat-noscript { max-width: 720px; margin: 24px auto; padding: 0 16px; line-height: 1.6; }
.chat-noscript { max-width: 720px; margin: 24px auto; padding: 0 16px; line-height: 1.6; }
@@ -498,11 +498,6 @@ a.cf-recap-band:hover .cf-recap-sub { text-decoration: underline; text-underline
@@ -498,11 +498,6 @@ a.cf-recap-band:hover .cf-recap-sub { text-decoration: underline; text-underline
.cf-chips::-webkit-scrollbar { display: none; }
.cf-chips::-webkit-scrollbar { display: none; }
.cf-chip { flex: none; min-height: 34px; padding: 0 12px; font-size: 0.84rem; }
.cf-chip { flex: none; min-height: 34px; padding: 0 12px; font-size: 0.84rem; }
}
}
-@media (prefers-reduced-motion: reduce) {
- .cf-card { animation: none; }
- .cf-skel-media::after, .cf-skel-line::after { animation: none; }
- .cf-media img.cf-thumb, .cf-play { transition: none; }
-}
/* A card with no picture shows who made it: their face, round, over a blur of the same picture. */
/* A card with no picture shows who made it: their face, round, over a blur of the same picture. */
.cf-thumb--who { overflow: hidden; }
.cf-thumb--who { overflow: hidden; }
@@ -7,7 +7,6 @@
@@ -7,7 +7,6 @@
.pc-status { font-size: 0.92rem; display: flex; align-items: center; gap: 8px; min-height: 22px; }
.pc-status { font-size: 0.92rem; display: flex; align-items: center; gap: 8px; min-height: 22px; }
.pc-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; display: inline-block; }
.pc-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; display: inline-block; }
.pc-dot-on { background: #53fc18; box-shadow: 0 0 6px rgba(83,252,24,0.6); animation: pcDotPulse 2.2s var(--ov-ease-in-out, ease-in-out) infinite; }
.pc-dot-on { background: #53fc18; box-shadow: 0 0 6px rgba(83,252,24,0.6); animation: pcDotPulse 2.2s var(--ov-ease-in-out, ease-in-out) infinite; }
-@media (prefers-reduced-motion: reduce) { .pc-dot-on { animation: none; } }
.pc-kind { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; padding: 1px 7px; border-radius: 6px; margin-left: 4px; vertical-align: middle; }
.pc-kind { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; padding: 1px 7px; border-radius: 6px; margin-left: 4px; vertical-align: middle; }
.pc-kind-app { background: rgba(83,252,24,0.16); color: #6ee34a; }
.pc-kind-app { background: rgba(83,252,24,0.16); color: #6ee34a; }
.pc-kind-test { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent-light, #e1b77d); }
.pc-kind-test { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent-light, #e1b77d); }
@@ -88,10 +88,6 @@
@@ -88,10 +88,6 @@
.hf-panel { padding: 14px 14px 14px; }
.hf-panel { padding: 14px 14px 14px; }
.hf-overview { display: none; }
.hf-overview { display: none; }
}
}
-@media (prefers-reduced-motion: reduce) {
- .hf, .hf-live i { animation: none; }
- .hf-media video, .hf-media canvas, .hf-media img.hf-frame, .hf-now { transition: none; }
-}
/* Touch pointers: the unmute pill answers a finger at 44 px (2026-11-05 phone sweep: 35.6 px tall). It sits on the
/* Touch pointers: the unmute pill answers a finger at 44 px (2026-11-05 phone sweep: 35.6 px tall). It sits on the
video overlay, so it takes the height itself rather than a hit area over the frame's own link. */
video overlay, so it takes the height itself rather than a hit area over the frame's own link. */
@@ -108,7 +108,6 @@
@@ -108,7 +108,6 @@
.sg-live-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #ef4444; margin-right: 2px; vertical-align: middle; animation: sg-pulse 1.6s var(--ov-ease-in-out, ease-in-out) infinite; }
.sg-live-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #ef4444; margin-right: 2px; vertical-align: middle; animation: sg-pulse 1.6s var(--ov-ease-in-out, ease-in-out) infinite; }
@keyframes sg-pulse { 50% { opacity: .35; } }
@keyframes sg-pulse { 50% { opacity: .35; } }
.streamer-group-stream.is-live .streamer-group-stream-thumb { box-shadow: 0 0 0 2px #ef4444; border-radius: 8px; }
.streamer-group-stream.is-live .streamer-group-stream-thumb { box-shadow: 0 0 0 2px #ef4444; border-radius: 8px; }
-@media (prefers-reduced-motion: reduce) { .sg-live-dot { animation: none; } }
.streamer-group-stream-title { font-size: 0.88rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.streamer-group-stream-title { font-size: 0.88rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.streamer-group-stream-time { font-size: 0.75rem; }
.streamer-group-stream-time { font-size: 0.75rem; }
.hero-rotate {
.hero-rotate {
@@ -133,9 +132,6 @@
@@ -133,9 +132,6 @@
transform: scale(1.07); filter: none; opacity: 1;
transform: scale(1.07); filter: none; opacity: 1;
box-shadow: 0 26px 60px -10px rgba(0, 0, 0, 0.85), 0 0 0 1px var(--accent) inset;
box-shadow: 0 26px 60px -10px rgba(0, 0, 0, 0.85), 0 0 0 1px var(--accent) inset;
}
}
-@media (prefers-reduced-motion: reduce) {
- .hero-bg-layer { transition: opacity var(--ov-dur-4, 0.6s) var(--ov-ease-out, ease); }
-}
.hero-quip {
.hero-quip {
height: 2.8em; margin: 2px auto 18px; max-width: 640px; overflow: hidden;
height: 2.8em; margin: 2px auto 18px; max-width: 640px; overflow: hidden;
display: grid; place-items: center; text-align: center; text-wrap: balance;
display: grid; place-items: center; text-align: center; text-wrap: balance;
@@ -296,12 +292,7 @@
@@ -296,12 +292,7 @@
.hero-stat-label { white-space: normal; line-height: 1.1; }
.hero-stat-label { white-space: normal; line-height: 1.1; }
.hero-float { border-radius: 10px; }
.hero-float { border-radius: 10px; }
}
}
-@media (prefers-reduced-motion: reduce) {
- .hero-float { animation: heroFloatIn var(--ov-dur-1, 0.01s) forwards; opacity: 0.5; }
- .hero-float::after { animation: none; display: none; }
- .hero-collage { transition: none; transform: none; }
- .hero-quip { transition: none; }
-}
+html[data-ov-motion=off] .hero-float{opacity: 0.5}html[data-ov-motion=off] .hero-float::after{display: none}html[data-ov-motion=off] .hero-collage{transform: none}
.stream-card-ago {
.stream-card-ago {
margin-left: auto;
margin-left: auto;
font-size: 0.75rem;
font-size: 0.75rem;
@@ -372,9 +363,7 @@
@@ -372,9 +363,7 @@
.home-cta-features { grid-template-columns: 1fr; }
.home-cta-features { grid-template-columns: 1fr; }
.cta-hero { font-size: 1.7rem; }
.cta-hero { font-size: 1.7rem; }
}
}
-@media (prefers-reduced-motion: reduce) {
- .cta-hero-grad { -webkit-text-fill-color: var(--accent); color: var(--accent); }
-}
+html[data-ov-motion=off] .cta-hero-grad{-webkit-text-fill-color: var(--accent);color: var(--accent)}
.home-pastes-list {
.home-pastes-list {
display: grid;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
@@ -476,10 +465,6 @@
@@ -476,10 +465,6 @@
.stream-card-uptime, .stream-card-vcount { display: inline-flex; align-items: center; white-space: nowrap; }
.stream-card-uptime, .stream-card-vcount { display: inline-flex; align-items: center; white-space: nowrap; }
.stream-card-appear { animation: streamCardAppear var(--ov-dur-4, 0.6s) var(--ov-ease-out, ease); }
.stream-card-appear { animation: streamCardAppear var(--ov-dur-4, 0.6s) var(--ov-ease-out, ease); }
.stream-card-leave { opacity: 0; transform: scale(0.94); transition: opacity var(--ov-dur-3, 0.4s) var(--ov-ease-out, ease), transform var(--ov-dur-3, 0.4s) var(--ov-ease-out, ease); pointer-events: none; }
.stream-card-leave { opacity: 0; transform: scale(0.94); transition: opacity var(--ov-dur-3, 0.4s) var(--ov-ease-out, ease), transform var(--ov-dur-3, 0.4s) var(--ov-ease-out, ease); pointer-events: none; }
-@media (prefers-reduced-motion: reduce) {
- .stream-card-appear { animation: none; }
- .stream-card-leave { transition: none; }
-}
.hero-aurora::before, .hero-aurora::after {
.hero-aurora::before, .hero-aurora::after {
content: ''; position: absolute; pointer-events: none; border-radius: 50%;
content: ''; position: absolute; pointer-events: none; border-radius: 50%;
mix-blend-mode: lighten; filter: blur(44px);
mix-blend-mode: lighten; filter: blur(44px);
@@ -569,9 +554,6 @@
@@ -569,9 +554,6 @@
@media (max-width: 640px) {
@media (max-width: 640px) {
.hero-stats { min-height: 182px; }
.hero-stats { min-height: 182px; }
}
}
-@media (prefers-reduced-motion: reduce) {
- .hero-stats.is-swapping { transition: none; }
-}
.hero-rotate-wrap { display: inline; }
.hero-rotate-wrap { display: inline; }
.hero-tagline { text-wrap: balance; overflow-wrap: break-word; }
.hero-tagline { text-wrap: balance; overflow-wrap: break-word; }
@media (max-width: 760px) {
@media (max-width: 760px) {
@@ -647,7 +629,6 @@
@@ -647,7 +629,6 @@
.home-load-more:active { transform: translateY(0) scale(0.99); }
.home-load-more:active { transform: translateY(0) scale(0.99); }
.home-load-more:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.home-load-more:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.home-load-more:hover i { transform: translateY(2px); }
.home-load-more:hover i { transform: translateY(2px); }
-@media (prefers-reduced-motion: reduce) { .home-load-more, .home-load-more i { transition: none; } }
.hero-stat:hover { border-color: color-mix(in srgb, var(--accent) 38%, transparent); }
.hero-stat:hover { border-color: color-mix(in srgb, var(--accent) 38%, transparent); }
.hero-stat--clickable:hover { transform: translateY(-1px); }
.hero-stat--clickable:hover { transform: translateY(-1px); }
.hero-stat--live > i { color: var(--live-red-text, 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--live > i { color: var(--live-red-text, var(--live-red)); background: color-mix(in srgb, var(--live-red) 15%, transparent); border-color: color-mix(in srgb, var(--live-red) 34%, transparent); }
@@ -748,7 +729,6 @@
@@ -748,7 +729,6 @@
animation-delay: calc(var(--ov-enter-i, 0) * 45ms);
animation-delay: calc(var(--ov-enter-i, 0) * 45ms);
will-change: transform, opacity;
will-change: transform, opacity;
}
}
-@media (prefers-reduced-motion: reduce) { .ov-enter { animation: none !important; } }
.cta-btn-short { display: none; }
.cta-btn-short { display: none; }
.cta-method { display: flex; flex-direction: column; }
.cta-method { display: flex; flex-direction: column; }
@@ -826,11 +806,7 @@
@@ -826,11 +806,7 @@
.cta-section-label { white-space: nowrap; }
.cta-section-label { white-space: nowrap; }
}
}
-@media (prefers-reduced-motion: reduce) {
- .cta-method:hover::before, .home-cta-feature:hover::after { animation: none; opacity: 0; }
- .home-cta-banner.about-opening * { animation: none !important; }
- .cta-method:hover .cta-method-icon, .home-cta-feature:hover > i:first-child { transform: none; }
-}
+html[data-ov-motion=off] .cta-method:hover::before,html[data-ov-motion=off] .home-cta-feature:hover::after{opacity: 0}html[data-ov-motion=off] .cta-method:hover .cta-method-icon,html[data-ov-motion=off] .home-cta-feature:hover > i:first-child{transform: none}
/* ── from i18n-star.css ── */
/* ── from i18n-star.css ── */
/* ═══ Star of OpenVibe (decoration and compact grid: css/i18n-star.css) ═══
/* ═══ Star of OpenVibe (decoration and compact grid: css/i18n-star.css) ═══
@@ -893,9 +869,7 @@
@@ -893,9 +869,7 @@
.home-star-section { margin-bottom: 34px; }
.home-star-section { margin-bottom: 34px; }
.star-live-card { max-width: 420px; width: 100%; margin: 0 auto; }
.star-live-card { max-width: 420px; width: 100%; margin: 0 auto; }
}
}
-@media (prefers-reduced-motion: reduce) {
- .star-petals { display: none; }
-}
+html[data-ov-motion=off] .star-petals{display: none}
.rs-lite .star-card-border::before { animation: none; background: linear-gradient(135deg, var(--star-gold), #f472b6, var(--accent)); inset: 0; }
.rs-lite .star-card-border::before { animation: none; background: linear-gradient(135deg, var(--star-gold), #f472b6, var(--accent)); inset: 0; }
.rs-lite .star-avatar-glow, .rs-lite .star-petals { display: none; }
.rs-lite .star-avatar-glow, .rs-lite .star-petals { display: none; }
.rs-lite .star-avatar-ring { animation: none; }
.rs-lite .star-avatar-ring { animation: none; }
@@ -1044,12 +1018,6 @@
@@ -1044,12 +1018,6 @@
position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
}
}
}
-@media (prefers-reduced-motion: reduce) {
- .star-more.is-entering { animation: none; }
- .star-card.is-swapping { transition: none; }
- .star-card.is-compact .star-avatar-ring { animation: none; }
- .star-act, .star-act > i { transition: none; }
-}
/* The pitch banner: what kind of place this is, before the headline says it. */
/* The pitch banner: what kind of place this is, before the headline says it. */
.cta-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin: 0 0 14px; }
.cta-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin: 0 0 14px; }
@@ -1256,7 +1224,6 @@
@@ -1256,7 +1224,6 @@
.hero-stat-all-sites > i:last-child { font-size: 0.7rem; color: var(--text-muted); transition: transform var(--ov-dur-2, 0.18s); }
.hero-stat-all-sites > i:last-child { font-size: 0.7rem; color: var(--text-muted); transition: transform var(--ov-dur-2, 0.18s); }
.hero-stat-all-sites:hover, .hero-stat-all-sites:focus-visible { border-color: color-mix(in srgb, var(--accent) 60%, transparent); background: color-mix(in srgb, var(--accent) 16%, transparent); outline: none; }
.hero-stat-all-sites:hover, .hero-stat-all-sites:focus-visible { border-color: color-mix(in srgb, var(--accent) 60%, transparent); background: color-mix(in srgb, var(--accent) 16%, transparent); outline: none; }
.hero-stat-all-sites:hover > i:last-child { transform: translateX(3px); }
.hero-stat-all-sites:hover > i:last-child { transform: translateX(3px); }
-@media (prefers-reduced-motion: reduce) { .hero-stat-all-sites, .hero-stat-all-sites > i { transition: none; } }
/* Recently Online: a stream that has a recording opens its replay (openvibe.video, the chat in time beside it), and
/* Recently Online: a stream that has a recording opens its replay (openvibe.video, the chat in time beside it), and
its picture says so with a play mark (2026-10-27). */
its picture says so with a play mark (2026-10-27). */
@@ -1272,7 +1239,6 @@
@@ -1272,7 +1239,6 @@
}
}
.streamer-group-stream.has-replay:hover .sg-play i, .streamer-group-stream.has-replay:focus-visible .sg-play i { transform: scale(1.08); background: var(--accent); border-color: transparent; }
.streamer-group-stream.has-replay:hover .sg-play i, .streamer-group-stream.has-replay:focus-visible .sg-play i { transform: scale(1.08); background: var(--accent); border-color: transparent; }
.sg-replay { color: var(--accent-light, var(--accent)); font-weight: 700; }
.sg-replay { color: var(--accent-light, var(--accent)); font-weight: 700; }
-@media (prefers-reduced-motion: reduce) { .sg-play i { transition: none; } }
/* Touch screens (2026-10-29, tap-target audit): every control on the home reaches 44 px where it has room, and an
/* Touch screens (2026-10-29, tap-target audit): every control on the home reaches 44 px where it has room, and an
invisible hit area (::after) where growing it would move the layout. Nothing changes on a mouse. */
invisible hit area (::after) where growing it would move the layout. Nothing changes on a mouse. */
@@ -1337,7 +1303,6 @@
@@ -1337,7 +1303,6 @@
}
}
.home-rail-tail { display: none; }
.home-rail-tail { display: none; }
@keyframes homeRailPulse { from { opacity: 1; } to { opacity: .55; } }
@keyframes homeRailPulse { from { opacity: 1; } to { opacity: .55; } }
-@media (prefers-reduced-motion: reduce) { .home-rail-skel { animation: none; } }
/* 2026-10-31, the phone home, shorter again: the featured stream's card leaves the Live Now grid on a phone (the
/* 2026-10-31, the phone home, shorter again: the featured stream's card leaves the Live Now grid on a phone (the
featured block above already shows it), and when it was the only stream there the grid's header goes too; the
featured block above already shows it), and when it was the only stream there the grid's header goes too; the
@@ -1517,12 +1482,7 @@
@@ -1517,12 +1482,7 @@
/* Light themes: the surface follows the page, the aurora softens. */
/* Light themes: the surface follows the page, the aurora softens. */
@media (prefers-color-scheme: light) { :root:not([data-theme]) .ovnet::before { opacity: .35; } }
@media (prefers-color-scheme: light) { :root:not([data-theme]) .ovnet::before { opacity: .35; } }
[data-theme-mode="light"] .ovnet::before, html.light .ovnet::before { opacity: .35; }
[data-theme-mode="light"] .ovnet::before, html.light .ovnet::before { opacity: .35; }
-@media (prefers-reduced-motion: reduce) {
- .ovnet::before, .ovnet-sky i, .ovnet-dot { animation: none; }
- .ovnet-sky i { opacity: .45; }
- .ovnet.is-armed .ovnet-site { opacity: 1; --lift: 0px; }
- .ovnet-site, .ovnet-jump a, .ovnet-cta { transition: none; }
-}
+html[data-ov-motion=off] .ovnet-sky i{opacity: .45}html[data-ov-motion=off] .ovnet.is-armed .ovnet-site{opacity: 1;--lift: 0px}
/* The menu hint carries the menu's own orb (11-09: "Jump between sites… from the OV mark" pointed at the wrong button). */
/* The menu hint carries the menu's own orb (11-09: "Jump between sites… from the OV mark" pointed at the wrong button). */
.ovnet-switch .ovnet-orb { flex: none; color: var(--accent-light, #60a5fa); }
.ovnet-switch .ovnet-orb { flex: none; color: var(--accent-light, #60a5fa); }
@@ -238,7 +238,6 @@ a.rd-use:hover b { color: var(--accent); }
@@ -238,7 +238,6 @@ a.rd-use:hover b { color: var(--accent); }
.rd-more > summary::before { content: ''; width: 8px; height: 8px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: translateY(-2px) rotate(-45deg); transition: transform var(--ov-dur-2, .18s) var(--ov-ease-out, ease); }
.rd-more > summary::before { content: ''; width: 8px; height: 8px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: translateY(-2px) rotate(-45deg); transition: transform var(--ov-dur-2, .18s) var(--ov-ease-out, ease); }
.rd-more[open] > summary::before { transform: translateY(-4px) rotate(45deg); }
.rd-more[open] > summary::before { transform: translateY(-4px) rotate(45deg); }
.rd-more > .rd-topic-grid { padding: 4px 12px 14px; }
.rd-more > .rd-topic-grid { padding: 4px 12px 14px; }
-@media (prefers-reduced-motion: reduce) { .rd-more > summary::before { transition: none; } }
/* ── The bar: search and filters (public/js/rundown.js) ──────────────────────────────────
/* ── The bar: search and filters (public/js/rundown.js) ──────────────────────────────────
Sticky under the navbar on every Rundown page. The phone gets the search box and one
Sticky under the navbar on every Rundown page. The phone gets the search box and one
@@ -544,7 +543,6 @@ body.rd-sheet-open { overflow: hidden; }
@@ -544,7 +543,6 @@ body.rd-sheet-open { overflow: hidden; }
#rundown-root .rd-take .rd-compact-top .rd-chip { position: relative; min-height: 28px; }
#rundown-root .rd-take .rd-compact-top .rd-chip { position: relative; min-height: 28px; }
#rundown-root .rd-take .rd-compact-top .rd-chip::after { content: ''; position: absolute; inset: -8px; }
#rundown-root .rd-take .rd-compact-top .rd-chip::after { content: ''; position: absolute; inset: -8px; }
}
}
-@media (prefers-reduced-motion: reduce) { .rd-bar, #rundown-root .rd-react-btn, #rundown-root .rd-skeleton span { transition: none; animation: none; } }
/* ── The day, at the top of the front page (rundown.js _rdDayDraw): the day in short when the AI summary is on, then
/* ── The day, at the top of the front page (rundown.js _rdDayDraw): the day in short when the AI summary is on, then
today's moments as a row to swipe, in time order, each hung from one line by its dot. ── */
today's moments as a row to swipe, in time order, each hung from one line by its dot. ── */
@@ -123,9 +123,7 @@
@@ -123,9 +123,7 @@
.sn-tile:last-child:nth-child(odd) { grid-column: 1 / -1; }
.sn-tile:last-child:nth-child(odd) { grid-column: 1 / -1; }
.sn-swipe-hint { display: inline; }
.sn-swipe-hint { display: inline; }
}
}
-@media (prefers-reduced-motion: reduce) {
- .sn-overlay, .sn-dialog, .sn-bar, .sn-marks .sn-line { animation: none !important; stroke-dashoffset: 0; }
-}
+html[data-ov-motion=off] .sn-overlay,html[data-ov-motion=off] .sn-dialog,html[data-ov-motion=off] .sn-bar,html[data-ov-motion=off] .sn-marks .sn-line{stroke-dashoffset: 0}
@media (forced-colors: active) {
@media (forced-colors: active) {
.sn-bar, .sn-dot { fill: CanvasText; }
.sn-bar, .sn-dot { fill: CanvasText; }
.sn-line { stroke: CanvasText; }
.sn-line { stroke: CanvasText; }
@@ -27,7 +27,6 @@
@@ -27,7 +27,6 @@
.ovp-out { font-size: 0.68rem; opacity: 0.55; }
.ovp-out { font-size: 0.68rem; opacity: 0.55; }
.ovp-n { margin-left: auto; padding-left: 12px; font-weight: 700; }
.ovp-n { margin-left: auto; padding-left: 12px; font-weight: 700; }
.ovp-n.is-live { font-size: 0.72rem; font-weight: 700; color: #fca5a5; text-transform: uppercase; letter-spacing: 0.05em; }
.ovp-n.is-live { font-size: 0.72rem; font-weight: 700; color: #fca5a5; text-transform: uppercase; letter-spacing: 0.05em; }
-@media (prefers-reduced-motion: reduce) { .ovp { animation: none; } }
/* A section under the places (the live studio's restreams): each with a state dot, and a way to manage them. */
/* A section under the places (the live studio's restreams): each with a state dot, and a way to manage them. */
.ovp-extra { margin-top: 4px; padding-top: 4px; border-top: 1px solid color-mix(in srgb, var(--text-primary, #fff) 10%, transparent); }
.ovp-extra { margin-top: 4px; padding-top: 4px; border-top: 1px solid color-mix(in srgb, var(--text-primary, #fff) 10%, transparent); }
.ovp-extra[hidden] { display: none; }
.ovp-extra[hidden] { display: none; }
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
/* ═══════════════════════════════════════════════════════════════
/* ═══════════════════════════════════════════════════════════════
HOME FX — decorative effects layer for the home page (public/js/home-fx.js)
HOME FX — decorative effects layer for the home page (public/js/home-fx.js)
- Additive only; everything degrades to the plain page without JS or under
- prefers-reduced-motion.
+ Additive only; everything degrades to the plain page without JS or with
+ motion off (Calm plays it, smaller and slower).
═══════════════════════════════════════════════════════════════ */
═══════════════════════════════════════════════════════════════ */
#page-home .hero { position: relative; }
#page-home .hero { position: relative; }
.hfx-sparks { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; opacity: 0.9; }
.hfx-sparks { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; opacity: 0.9; }
@@ -162,11 +162,7 @@
@@ -162,11 +162,7 @@
#page-home .section-header h2 .live-dot { animation: hfxLive 1.4s var(--ov-ease-in-out, ease-in-out) infinite; }
#page-home .section-header h2 .live-dot { animation: hfxLive 1.4s var(--ov-ease-in-out, ease-in-out) infinite; }
@keyframes hfxLive { 0%,100% { transform: scale(1); filter: drop-shadow(0 0 0 rgba(231,76,60,0)); } 50% { transform: scale(1.25); filter: drop-shadow(0 0 8px rgba(231,76,60,0.9)); } }
@keyframes hfxLive { 0%,100% { transform: scale(1); filter: drop-shadow(0 0 0 rgba(231,76,60,0)); } 50% { transform: scale(1.25); filter: drop-shadow(0 0 8px rgba(231,76,60,0.9)); } }
-@media (prefers-reduced-motion: reduce) {
- .hfx-sparks, .hfx-spot { display: none; }
- .hfx-shimmer, .hfx-glow, .hb::before, .hb::after, .hb-ico, #page-home .section-header h2 .live-dot { animation: none !important; }
- .hfx-reveal { opacity: 1; transform: none; transition: none; }
-}
+html[data-ov-motion=off] .hfx-sparks,html[data-ov-motion=off] .hfx-spot{display: none}html[data-ov-motion=off] .hfx-reveal{opacity: 1;transform: none}
.rs-lite .hfx-sparks, .rs-lite .hfx-spot { display: none; }
.rs-lite .hfx-sparks, .rs-lite .hfx-spot { display: none; }
.rs-lite .hfx-shimmer, .rs-lite .hfx-glow, .rs-lite .hb::after, .rs-lite #page-home .section-header h2 .live-dot { animation: none !important; text-shadow: none; }
.rs-lite .hfx-shimmer, .rs-lite .hfx-glow, .rs-lite .hb::after, .rs-lite #page-home .section-header h2 .live-dot { animation: none !important; text-shadow: none; }
@@ -239,7 +235,6 @@
@@ -239,7 +235,6 @@
@media (max-width: 520px) { .hb-label small { display: none; } #page-home .hero-primary .hb-label small { display: inline; } }
@media (max-width: 520px) { .hb-label small { display: none; } #page-home .hero-primary .hb-label small { display: inline; } }
/* The very narrowest phones: Go live alone, so the label never squeezes. */
/* The very narrowest phones: Go live alone, so the label never squeezes. */
@media (max-width: 340px) { #page-home .hero-primary .hb-label small { display: none; } }
@media (max-width: 340px) { #page-home .hero-primary .hb-label small { display: none; } }
-@media (prefers-reduced-motion: reduce) { .hb-vibe, .hb-vibe .hb-ico::after { animation: none !important; } }
/* Touch devices get no hover, so the one button on the row that has something extra to say needs
/* Touch devices get no hover, so the one button on the row that has something extra to say needs
to advertise it. A small pulsing dot marks it as tappable-for-more; it is pointer-events:none
to advertise it. A small pulsing dot marks it as tappable-for-more; it is pointer-events:none
@@ -267,7 +262,6 @@
@@ -267,7 +262,6 @@
100% { transform: scale(3.3); opacity: 0; }
100% { transform: scale(3.3); opacity: 0; }
}
}
}
}
-@media (hover: none) and (prefers-reduced-motion: reduce) { .hb[data-ovtip]::after { animation: none; } }
/* ═══════════════════════════════════════════════════════════════════════════
/* ═══════════════════════════════════════════════════════════════════════════
@@ -143,7 +143,7 @@
@@ -143,7 +143,7 @@
overflow-x: auto; scrollbar-width: none; margin-left: -14px; margin-right: -14px; padding: 0 14px 2px; }
overflow-x: auto; scrollbar-width: none; margin-left: -14px; margin-right: -14px; padding: 0 14px 2px; }
.ovg-join:not(.is-open) .ovg-join-perks::-webkit-scrollbar { display: none; }
.ovg-join:not(.is-open) .ovg-join-perks::-webkit-scrollbar { display: none; }
}
}
-@media (prefers-reduced-motion: reduce) { .tour-wire, .tour-node-dot, .tour-core-ring, .tour-feat, .tour-core::before, .tour-core b, .tour-core-logo { animation: none; } .tour-feat { opacity: 1; transform: none; } }
+html[data-ov-motion=off] .tour-feat{opacity: 1;transform: none}
.rs-lite .tour::before, .rs-lite .tour-core-ring { display: none; }
.rs-lite .tour::before, .rs-lite .tour-core-ring { display: none; }
/* Phones keep the glow but not the repainting gradients. */
/* Phones keep the glow but not the repainting gradients. */
.rs-lite .tour-core b { animation: none; -webkit-text-fill-color: var(--text-primary); background: none; }
.rs-lite .tour-core b { animation: none; -webkit-text-fill-color: var(--text-primary); background: none; }
@@ -342,9 +342,6 @@
@@ -342,9 +342,6 @@
.ovg-join-expand:hover i, .ovg-join.is-open .ovg-join-expand i { animation: none !important; }
.ovg-join-expand:hover i, .ovg-join.is-open .ovg-join-expand i { animation: none !important; }
@keyframes joinNudgeSweep { 0%, 62% { transform: translateX(0); } 100% { transform: translateX(420%); } }
@keyframes joinNudgeSweep { 0%, 62% { transform: translateX(0); } 100% { transform: translateX(420%); } }
@keyframes joinNudge { 0%, 74%, 100% { transform: translateY(0); } 82% { transform: translateY(2.5px); } 90% { transform: translateY(0); } }
@keyframes joinNudge { 0%, 74%, 100% { transform: translateY(0); } 82% { transform: translateY(2.5px); } 90% { transform: translateY(0); } }
-@media (prefers-reduced-motion: reduce) {
- .ovg-join-expand::after, .ovg-join-expand i { animation: none !important; }
-}
.tour-quest-slot.fx-live .ovg-join-perks li { animation: joinPerk var(--ov-dur-4, 0.6s) var(--ov-ease-out, cubic-bezier(.2, 1, .3, 1)) both;
.tour-quest-slot.fx-live .ovg-join-perks li { animation: joinPerk var(--ov-dur-4, 0.6s) var(--ov-ease-out, cubic-bezier(.2, 1, .3, 1)) both;
animation-delay: calc(var(--i) * 85ms + 100ms); }
animation-delay: calc(var(--i) * 85ms + 100ms); }
@keyframes joinPerk { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes joinPerk { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@@ -374,12 +371,6 @@
@@ -374,12 +371,6 @@
.ovg-join-cta .btn { width: 100%; justify-content: center; }
.ovg-join-cta .btn { width: 100%; justify-content: center; }
.ovg-join-tour { width: 100%; text-align: center; }
.ovg-join-tour { width: 100%; text-align: center; }
}
}
-@media (prefers-reduced-motion: reduce) {
- .ovg-quest-wave, .ovg-quest-bub, .ovg-quest-sheen, .ovg-quest-ico,
- .ovg-join-glow, .ovg-join-mark, .ovg-join-go::after { animation: none !important; }
- .ovg-quest-liquid { transition: none; }
- .ovg-join-perks li { animation: none !important; }
-}
/* ═══════════════════════════════════════════════════════════════
/* ═══════════════════════════════════════════════════════════════
The launch button — what the quest slot becomes once the setup list is finished, and what the
The launch button — what the quest slot becomes once the setup list is finished, and what the
@@ -482,10 +473,6 @@
@@ -482,10 +473,6 @@
.ovg-launch-ico { width: 34px; height: 34px; }
.ovg-launch-ico { width: 34px; height: 34px; }
.ovg-launch-text small { font-size: 0.7rem; }
.ovg-launch-text small { font-size: 0.7rem; }
}
}
-@media (prefers-reduced-motion: reduce) {
- .ovg-launch-aura, .ovg-firstlive-ring, .ovg-launch-sheen, .ovg-firstlive-sheen { animation: none !important; }
- .ovg-launch, .ovg-firstlive { transition: none; }
-}
.rs-lite .ovg-launch-aura, .rs-lite .ovg-firstlive-ring { animation: none !important; }
.rs-lite .ovg-launch-aura, .rs-lite .ovg-firstlive-ring { animation: none !important; }
/* ── Where the wires meet the hub ────────────────────────────────────────────
/* ── Where the wires meet the hub ────────────────────────────────────────────
@@ -109,12 +109,7 @@
@@ -109,12 +109,7 @@
}
}
@keyframes starSheen { 0% { background-position: 120% 0; } 100% { background-position: -120% 0; } }
@keyframes starSheen { 0% { background-position: 120% 0; } 100% { background-position: -120% 0; } }
-@media (prefers-reduced-motion: reduce) {
- .chat-translation, .chat-translate-hint, .ch-captions, .ch-caption, .ch-captions-dot, .ch-about-bio-translated,
- .star-card-border, .star-card-border::before, .star-petals span, .star-avatar-ring, .star-avatar-glow, .star-live-pill,
- .star-kicker i, .star-btn-watch::after { animation: none !important; }
- .chat-translation, .ch-caption, .star-card-border { opacity: 1; transform: none; }
-}
+html[data-ov-motion=off] .chat-translation,html[data-ov-motion=off] .ch-caption,html[data-ov-motion=off] .star-card-border{opacity: 1;transform: none}
/* ── Chat: "translate to my language" hover button (sits left of the reply button) ── */
/* ── Chat: "translate to my language" hover button (sits left of the reply button) ── */
.chat-msg-hoverable .chat-translate-btn {
.chat-msg-hoverable .chat-translate-btn {
@@ -199,9 +194,6 @@
@@ -199,9 +194,6 @@
.star-card.is-compact { padding: 13px 13px 14px; gap: 9px 11px; }
.star-card.is-compact { padding: 13px 13px 14px; gap: 9px 11px; }
/* On a phone the three actions stack two-then-one cleanly rather than at random. */
/* On a phone the three actions stack two-then-one cleanly rather than at random. */
}
}
-@media (prefers-reduced-motion: reduce) {
- .star-expand, .star-expand i, .star-card.is-compact .star-avatar { transition: none; }
-}
/* ── Chat: "Translate chat" switch (shows once a line in another language arrives; chat.js _autoTrChip) ──
/* ── Chat: "Translate chat" switch (shows once a line in another language arrives; chat.js _autoTrChip) ──
A compact pill on the right of the tools row, the height of the other tool buttons. The tools row's generic
A compact pill on the right of the tools row, the height of the other tool buttons. The tools row's generic
@@ -82,7 +82,7 @@ code { background: rgba(255,255,255,.08); padding: 2px 6px; border-radius: 6px;
@@ -82,7 +82,7 @@ code { background: rgba(255,255,255,.08); padding: 2px 6px; border-radius: 6px;
animation: mp-pulse 1.4s var(--ov-ease-out, ease-out) infinite; /* a ring that pulses out: nothing spins */
animation: mp-pulse 1.4s var(--ov-ease-out, ease-out) infinite; /* a ring that pulses out: nothing spins */
}
}
@keyframes mp-pulse { from { box-shadow: 0 0 0 0 color-mix(in srgb, var(--mp-accent) 55%, transparent); } to { box-shadow: 0 0 0 12px transparent; } }
@keyframes mp-pulse { from { box-shadow: 0 0 0 0 color-mix(in srgb, var(--mp-accent) 55%, transparent); } to { box-shadow: 0 0 0 12px transparent; } }
-@media (prefers-reduced-motion: reduce) { .mp-spinner { animation: none; opacity: .7; } } /* a still ring while it loads */
+html[data-ov-motion=off] .mp-spinner{opacity: .7} /* a still ring while it loads */
/* ── Custom player controls ──────────────────────────────── */
/* ── Custom player controls ──────────────────────────────── */
.mp-controls {
.mp-controls {
@@ -67,7 +67,6 @@
@@ -67,7 +67,6 @@
overflow: hidden;
overflow: hidden;
animation: msgSlideUp var(--ov-dur-2, 0.2s) var(--ov-ease-out, ease-out);
animation: msgSlideUp var(--ov-dur-2, 0.2s) var(--ov-ease-out, ease-out);
}
}
-@media (prefers-reduced-motion: reduce) { .messenger-panel { animation: none; } }
.messenger-panel.open {
.messenger-panel.open {
display: flex;
display: flex;
}
}
@@ -110,7 +110,6 @@ body.ovg-lock { overflow: hidden; }
@@ -110,7 +110,6 @@ body.ovg-lock { overflow: hidden; }
.ovg-kv { grid-template-columns: 1fr; } .ovg-kv span { font-size: 0.7rem; }
.ovg-kv { grid-template-columns: 1fr; } .ovg-kv span { font-size: 0.7rem; }
.ovg-foot { padding: 10px 14px calc(14px + env(safe-area-inset-bottom, 0px)); }
.ovg-foot { padding: 10px 14px calc(14px + env(safe-area-inset-bottom, 0px)); }
}
}
-@media (prefers-reduced-motion: reduce) { .ovg-card, .ovg-panel, .ovg-pick, .ovg-box, .ovg-sum-row, .ovg-big, .ovg-radar i, .ovg-live-badge { animation: none !important; } }
/* ── Engine extras: clickable steps, hub, rings, spotlight, mini-journey forms ── */
/* ── Engine extras: clickable steps, hub, rings, spotlight, mini-journey forms ── */
.ovg-steps { grid-template-columns: repeat(var(--n, 4), 1fr); }
.ovg-steps { grid-template-columns: repeat(var(--n, 4), 1fr); }
@@ -156,11 +156,8 @@ html { -webkit-tap-highlight-color: transparent; }
@@ -156,11 +156,8 @@ html { -webkit-tap-highlight-color: transparent; }
:where(a, button, summary, [role="button"], [tabindex]:not([tabindex="-1"]), input[type="checkbox"], input[type="radio"], input[type="range"]):focus-visible {
:where(a, button, summary, [role="button"], [tabindex]:not([tabindex="-1"]), input[type="checkbox"], input[type="radio"], input[type="range"]):focus-visible {
outline: 2px solid var(--accent-light, var(--accent)); outline-offset: 2px;
outline: 2px solid var(--accent-light, var(--accent)); outline-offset: 2px;
}
}
-@media (prefers-reduced-motion: reduce) {
- /* No movement: the press is a brief dim instead. */
- :root { --press-scale: 1; --press-scale-card: 1; }
- :where(button, [role="button"], .btn, .ov-press, a.badge, .hb, .section-action, .digest-chip, .digest-recap, a.stream-card, a.moment-card, a.pulse-card, a.home-paste-card, a.streamer-group-stream, .ov-press-card):not(:disabled):active { opacity: 0.82; }
-}
+/* Motion off: no movement, the press is a brief dim instead. */
+html[data-ov-motion=off]{--press-scale: 1;--press-scale-card: 1}html[data-ov-motion=off] :where(button, [role="button"], .btn, .ov-press, a.badge, .hb, .section-action, .digest-chip, .digest-recap, a.stream-card, a.moment-card, a.pulse-card, a.home-paste-card, a.streamer-group-stream, .ov-press-card):not(:disabled):active{opacity: 0.82}
/* ═══════════════════════════════════════════════════════════════
/* ═══════════════════════════════════════════════════════════════
NAVBAR
NAVBAR
@@ -1092,9 +1089,7 @@ html[data-theme-mode="light"] .hero-bg { --hero-veil: rgba(255, 255, 255, 0.62);
@@ -1092,9 +1089,7 @@ html[data-theme-mode="light"] .hero-bg { --hero-veil: rgba(255, 255, 255, 0.62);
from { transform: scale(1.06) translate(-1.4%, 1%); }
from { transform: scale(1.06) translate(-1.4%, 1%); }
to { transform: scale(1.16) translate(1.8%, -1.8%); }
to { transform: scale(1.16) translate(1.8%, -1.8%); }
}
}
-@media (prefers-reduced-motion: reduce) {
- .hero-bg-layer.active { animation: none; transform: scale(1.08); }
-}
+html[data-ov-motion=off] .hero-bg-layer.active{transform: scale(1.08)}
@keyframes heroFloatIn { to { opacity: var(--op, 0.6); } }
@keyframes heroFloatIn { to { opacity: var(--op, 0.6); } }
/* 3D drift: translate in x/y + slow z-tilt so cards feel like they float in space. */
/* 3D drift: translate in x/y + slow z-tilt so cards feel like they float in space. */
@@ -1195,7 +1190,7 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }
@@ -1195,7 +1190,7 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }
.ov-rail-bar > span { position: absolute; inset: 0 auto 0 0; border-radius: inherit; background: var(--rail-accent, var(--accent)); }
.ov-rail-bar > span { position: absolute; inset: 0 auto 0 0; border-radius: inherit; background: var(--rail-accent, var(--accent)); }
/* Touch screens swipe; buttons there would only cover the cards. */
/* Touch screens swipe; buttons there would only cover the cards. */
@media (hover: none), (pointer: coarse) { .ov-rail-btn { display: none; } }
@media (hover: none), (pointer: coarse) { .ov-rail-btn { display: none; } }
-@media (prefers-reduced-motion: reduce) { .ov-rail-track { scroll-behavior: auto; } }
+html[data-ov-motion=off] .ov-rail-track{scroll-behavior: auto}
/* ── AI Moments showcase row ── */
/* ── AI Moments showcase row ── */
.moments-row { display: flex; gap: 12px; overflow-x: auto; margin-bottom: 22px; }
.moments-row { display: flex; gap: 12px; overflow-x: auto; margin-bottom: 22px; }
@@ -1258,9 +1253,6 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }
@@ -1258,9 +1253,6 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }
itself off the chip now and the labels are short words, so both fit.) */
itself off the chip now and the labels are short words, so both fit.) */
.hero-stat-num { font-size: 1.05rem; }
.hero-stat-num { font-size: 1.05rem; }
}
}
-@media (prefers-reduced-motion: reduce) {
- .hero-stat--live > i { animation: none; }
-}
/* ═══════════════════════════════════════════════════════════════
/* ═══════════════════════════════════════════════════════════════
STREAM CARD EXTRAS (duration, ago badge)
STREAM CARD EXTRAS (duration, ago badge)
@@ -1303,7 +1295,6 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }
@@ -1303,7 +1295,6 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }
transition: max-height var(--ov-dur-4, 0.6s) var(--ov-ease-out, cubic-bezier(0.22, 1, 0.36, 1)), opacity var(--ov-dur-3, 0.4s) var(--ov-ease-out, ease), margin var(--ov-dur-3, 0.4s) var(--ov-ease-out, ease);
transition: max-height var(--ov-dur-4, 0.6s) var(--ov-ease-out, cubic-bezier(0.22, 1, 0.36, 1)), opacity var(--ov-dur-3, 0.4s) var(--ov-ease-out, ease), margin var(--ov-dur-3, 0.4s) var(--ov-ease-out, ease);
}
}
/* Toggle label swaps with collapsed state. */
/* Toggle label swaps with collapsed state. */
-@media (prefers-reduced-motion: reduce) { .home-about-body { transition: opacity var(--ov-dur-3, 0.3s) var(--ov-ease-out, ease); } }
/* Animated glowing border sweep */
/* Animated glowing border sweep */
@property --cta-angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
@property --cta-angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
@@ -1433,12 +1424,6 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }
@@ -1433,12 +1424,6 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }
.cta-btn-glow { position: relative; isolation: isolate; box-shadow: 0 8px 22px color-mix(in srgb, var(--accent-dark) 35%, transparent); }
.cta-btn-glow { position: relative; isolation: isolate; box-shadow: 0 8px 22px color-mix(in srgb, var(--accent-dark) 35%, transparent); }
@keyframes ctaBtnGlow { 0%,100% { opacity: 0; } 50% { opacity: 1; } }
@keyframes ctaBtnGlow { 0%,100% { opacity: 0; } 50% { opacity: 1; } }
/* Respect reduced-motion */
/* Respect reduced-motion */
-@media (prefers-reduced-motion: reduce) {
- .home-cta-banner::before, .cta-ambient, .cta-embers span, .cta-live-dot, .cta-hero-grad,
- .cta-btn-glow, .home-cta-feature {
- animation: none !important;
- }
-}
/* ═══════════════════════════════════════════════════════════════
/* ═══════════════════════════════════════════════════════════════
FOUNDER STORY — Timeline Layout
FOUNDER STORY — Timeline Layout
@@ -2127,7 +2112,6 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }
@@ -2127,7 +2112,6 @@ a.pulse-card:hover { border-color: var(--accent); transform: translateY(-2px); }
.muted-hint { z-index: 21; border: 0; cursor: pointer; -webkit-tap-highlight-color: transparent; }
.muted-hint { z-index: 21; border: 0; cursor: pointer; -webkit-tap-highlight-color: transparent; }
.muted-hint:hover, .muted-hint:focus-visible { background: var(--accent-strong, var(--accent, #3b82f6)); color: var(--on-accent-strong, #fff); }
.muted-hint:hover, .muted-hint:focus-visible { background: var(--accent-strong, var(--accent, #3b82f6)); color: var(--on-accent-strong, #fff); }
@keyframes unmute-pop { from { opacity: 0; transform: translateY(-4px) scale(.96); } }
@keyframes unmute-pop { from { opacity: 0; transform: translateY(-4px) scale(.96); } }
-@media (prefers-reduced-motion: reduce) { .unmute-pill { animation: none; } }
/* ── DVR Progress Bar (Live Stream Seeking) ─────────────────── */
/* ── DVR Progress Bar (Live Stream Seeking) ─────────────────── */
.dvr-progress-wrap {
.dvr-progress-wrap {
@@ -5398,10 +5382,6 @@ body.theme-light .btn-outline:hover {
@@ -5398,10 +5382,6 @@ body.theme-light .btn-outline:hover {
.call-participant-tile.is-leaving { animation: callTileOut var(--ov-dur-2, 240ms) ease-in forwards; pointer-events: none; }
.call-participant-tile.is-leaving { animation: callTileOut var(--ov-dur-2, 240ms) ease-in forwards; pointer-events: none; }
@keyframes callTileIn { from { opacity: 0; transform: scale(0.86); } }
@keyframes callTileIn { from { opacity: 0; transform: scale(0.86); } }
@keyframes callTileOut { to { opacity: 0; transform: scale(0.86); } }
@keyframes callTileOut { to { opacity: 0; transform: scale(0.86); } }
-@media (prefers-reduced-motion: reduce) {
- .call-participant-tile.is-entering, .call-participant-tile.is-leaving, .call-participant-tile.is-connecting .call-avatar, .vc-connected-badge i { animation: none !important; }
- .call-avatar::after { transition: none; }
-}
/* Video (camera on) */
/* Video (camera on) */
.call-video {
.call-video {
@@ -7284,7 +7264,7 @@ textarea.paste-textarea {
@@ -7284,7 +7264,7 @@ textarea.paste-textarea {
/* Phones, scrolling down: the floating buttons step aside (draggable-fab.js) and come back on the way up. */
/* Phones, scrolling down: the floating buttons step aside (draggable-fab.js) and come back on the way up. */
body .floating-chat-fab, body .mobile-chat-toggle { transition: transform var(--ov-dur-2, 0.2s), opacity var(--ov-dur-2, 0.2s), background var(--ov-dur-2, 0.15s); }
body .floating-chat-fab, body .mobile-chat-toggle { transition: transform var(--ov-dur-2, 0.2s), opacity var(--ov-dur-2, 0.2s), background var(--ov-dur-2, 0.15s); }
body.fabs-tucked .floating-chat-fab, body.fabs-tucked .mobile-chat-toggle { opacity: 0; transform: scale(0.6); pointer-events: none; }
body.fabs-tucked .floating-chat-fab, body.fabs-tucked .mobile-chat-toggle { opacity: 0; transform: scale(0.6); pointer-events: none; }
-@media (prefers-reduced-motion: reduce) { body.fabs-tucked .floating-chat-fab, body.fabs-tucked .mobile-chat-toggle { transform: none; } }
+html[data-ov-motion=off] body.fabs-tucked .floating-chat-fab,html[data-ov-motion=off] body.fabs-tucked .mobile-chat-toggle{transform: none}
/* ── Floating Chat Widget ─────────────────────────────────────── */
/* ── Floating Chat Widget ─────────────────────────────────────── */
.floating-chat-fab {
.floating-chat-fab {
@@ -9016,9 +8996,6 @@ body.fabs-tucked .floating-chat-fab, body.fabs-tucked .mobile-chat-toggle { opac
@@ -9016,9 +8996,6 @@ body.fabs-tucked .floating-chat-fab, body.fabs-tucked .mobile-chat-toggle { opac
/* On a light page the shimmer's pale gold sank into the white; a deep amber keeps the name readable through it. */
/* On a light page the shimmer's pale gold sank into the white; a deep amber keeps the name readable through it. */
html[data-theme-mode="light"] .hero h1 { background-image: linear-gradient(90deg, var(--accent, var(--accent)), #b7791f, var(--accent, var(--accent))); }
html[data-theme-mode="light"] .hero h1 { background-image: linear-gradient(90deg, var(--accent, var(--accent)), #b7791f, var(--accent, var(--accent))); }
@keyframes heroShimmer { to { background-position: 200% center; } }
@keyframes heroShimmer { to { background-position: 200% center; } }
-@media (prefers-reduced-motion: reduce) {
- .hero-aurora, .hero-aurora::before, .hero-aurora::after, .hero h1 { animation: none; }
-}
/* Stream description on live cards (collapsible) */
/* Stream description on live cards (collapsible) */
@@ -9038,7 +9015,6 @@ html[data-theme-mode="light"] .hero h1 { background-image: linear-gradient(90deg
@@ -9038,7 +9015,6 @@ html[data-theme-mode="light"] .hero h1 { background-image: linear-gradient(90deg
0%,100% { box-shadow: 0 8px 24px rgba(0,0,0,.5), 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }
0%,100% { box-shadow: 0 8px 24px rgba(0,0,0,.5), 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }
50% { box-shadow: 0 8px 24px rgba(0,0,0,.5), 0 0 0 12px transparent; }
50% { box-shadow: 0 8px 24px rgba(0,0,0,.5), 0 0 0 12px transparent; }
}
}
-@media (prefers-reduced-motion: reduce) { .cp-bonus-chest { animation: none; } }
/* Live-counting uptime tooltip */
/* Live-counting uptime tooltip */
.uptime-tooltip {
.uptime-tooltip {
@@ -9143,7 +9119,6 @@ html[data-theme-mode="light"] .hero h1 { background-image: linear-gradient(90deg
@@ -9143,7 +9119,6 @@ html[data-theme-mode="light"] .hero h1 { background-image: linear-gradient(90deg
position: static; height: auto; background: none; padding: 4px 0 6px;
position: static; height: auto; background: none; padding: 4px 0 6px;
}
}
.cgw-more.expanded i { animation: none; }
.cgw-more.expanded i { animation: none; }
-@media (prefers-reduced-motion: reduce) { .cgw-more i { animation: none; } }
.cgw-goal {
.cgw-goal {
position: relative; height: 42px; border-radius: 9px; overflow: hidden; cursor: pointer;
position: relative; height: 42px; border-radius: 9px; overflow: hidden; cursor: pointer;
background: var(--bg-card); border: 1px solid var(--border);
background: var(--bg-card); border: 1px solid var(--border);
@@ -9569,7 +9544,7 @@ label.staff-inline-toggle.aiv-field { flex-direction:row; align-items:center; }
@@ -9569,7 +9544,7 @@ label.staff-inline-toggle.aiv-field { flex-direction:row; align-items:center; }
@keyframes ovSpark3 { 0% { opacity: 0; transform: translate(0,10px) scale(0.6); } 10% { opacity: 1; } 60% { opacity: 0; transform: translate(3px,16px) scale(1.2); } 100% { opacity: 0; } }
@keyframes ovSpark3 { 0% { opacity: 0; transform: translate(0,10px) scale(0.6); } 10% { opacity: 1; } 60% { opacity: 0; transform: translate(3px,16px) scale(1.2); } 100% { opacity: 0; } }
@media (max-width: 600px) { .brand-mark { width: 26px; height: 26px; } .brand-mark svg { width: 25px; height: 25px; } .brand-mark--hero svg { width: 100%; height: 100%; } }
@media (max-width: 600px) { .brand-mark { width: 26px; height: 26px; } .brand-mark svg { width: 25px; height: 25px; } .brand-mark--hero svg { width: 100%; height: 100%; } }
.rs-lite .ov-orbit, .rs-lite .ov-spark { filter: none; }
.rs-lite .ov-orbit, .rs-lite .ov-spark { filter: none; }
-@media (prefers-reduced-motion: reduce) { .brand-mark svg, .ov-glow, .ov-comet, .ov-v, .ov-dot, .ov-spark { animation: none !important; } .ov-orbit, .ov-spark { display: none; } .ov-comet { stroke-dasharray: none; opacity: 0.9; } }
+html[data-ov-motion=off] .ov-orbit,html[data-ov-motion=off] .ov-spark{display: none}html[data-ov-motion=off] .ov-comet{stroke-dasharray: none;opacity: 0.9}
@media (max-width: 600px) { .brand-mark { width: 26px; height: 26px; } .brand-mark svg { width: 25px; height: 25px; } }
@media (max-width: 600px) { .brand-mark { width: 26px; height: 26px; } .brand-mark svg { width: 25px; height: 25px; } }
.ov-mark--ph { opacity: 0.45; color: var(--text-muted); }
.ov-mark--ph { opacity: 0.45; color: var(--text-muted); }
@@ -9596,10 +9571,6 @@ label.staff-inline-toggle.aiv-field { flex-direction:row; align-items:center; }
@@ -9596,10 +9571,6 @@ label.staff-inline-toggle.aiv-field { flex-direction:row; align-items:center; }
Only the element that is arriving animates, so the one leaving doesn't flash on its way out. */
Only the element that is arriving animates, so the one leaving doesn't flash on its way out. */
.hero-stat-full.is-entering, .hero-stat-strip.is-entering { animation: heroStatsOpen var(--ov-dur-3, 0.42s) var(--ov-ease-out, cubic-bezier(.2, 1, .3, 1)); }
.hero-stat-full.is-entering, .hero-stat-strip.is-entering { animation: heroStatsOpen var(--ov-dur-3, 0.42s) var(--ov-ease-out, cubic-bezier(.2, 1, .3, 1)); }
@keyframes heroStatsOpen { from { opacity: 0; transform: translateY(-7px); } to { opacity: 1; transform: none; } }
@keyframes heroStatsOpen { from { opacity: 0; transform: translateY(-7px); } to { opacity: 1; transform: none; } }
-@media (prefers-reduced-motion: reduce) {
- .hero-stat-full.is-entering, .hero-stat-strip.is-entering { animation: none; }
- .hero-stat-more i { transition: none; }
-}
/* ── Hero polish ─────────────────────────────────────────────────────────────
/* ── Hero polish ─────────────────────────────────────────────────────────────
The rotating tagline is a single unbreakable line, so on a narrow screen a long phrase ran off
The rotating tagline is a single unbreakable line, so on a narrow screen a long phrase ran off
@@ -9852,9 +9823,6 @@ label.staff-inline-toggle.aiv-field { flex-direction:row; align-items:center; }
@@ -9852,9 +9823,6 @@ label.staff-inline-toggle.aiv-field { flex-direction:row; align-items:center; }
#page-home.page { padding-bottom: 10px; }
#page-home.page { padding-bottom: 10px; }
/* Route changes (the bar, the loading line, the fade-in) are openvibe-shared web-runtime enter() + route-transition.js. */
/* Route changes (the bar, the loading line, the fade-in) are openvibe-shared web-runtime enter() + route-transition.js. */
-@media (prefers-reduced-motion: reduce) {
- .brand-mark.is-spin svg, .brand-mark.is-settling svg { animation: none !important; }
-}
/* ═══════════════════════════════════════════════════════════════
/* ═══════════════════════════════════════════════════════════════
Off-screen sections animate nothing.
Off-screen sections animate nothing.
@@ -9941,10 +9909,7 @@ label.staff-inline-toggle.aiv-field { flex-direction:row; align-items:center; }
@@ -9941,10 +9909,7 @@ label.staff-inline-toggle.aiv-field { flex-direction:row; align-items:center; }
@keyframes ovConnPulse { 50% { transform: scale(0.55); opacity: 0.5; } }
@keyframes ovConnPulse { 50% { transform: scale(0.55); opacity: 0.5; } }
.chat-conn-status { font-style: italic; opacity: 0.85; }
.chat-conn-status { font-style: italic; opacity: 0.85; }
.chat-conn-status[data-state="connected"] { color: var(--success-text, var(--success)); }
.chat-conn-status[data-state="connected"] { color: var(--success-text, var(--success)); }
-@media (prefers-reduced-motion: reduce) {
- .ov-conn-pill { transition: opacity var(--motion-fast) linear; transform: translate3d(-50%, 0, 0); }
- .ov-conn-pill .ov-conn-dot { animation: none !important; }
-}
+html[data-ov-motion=off] .ov-conn-pill{transform: translate3d(-50%, 0, 0)}
body.ov-panel-open #floating-chat-fab { opacity: 0; pointer-events: none; transform: scale(.85); transition: opacity var(--ov-dur-2, .15s), transform var(--ov-dur-2, .15s); }
body.ov-panel-open #floating-chat-fab { opacity: 0; pointer-events: none; transform: scale(.85); transition: opacity var(--ov-dur-2, .15s), transform var(--ov-dur-2, .15s); }
@@ -60,7 +60,7 @@
@@ -60,7 +60,7 @@
// page's effects script (which only loads on the home route).
// page's effects script (which only loads on the home route).
(function () {
(function () {
try {
try {
- if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
+ if ((document.documentElement.getAttribute('data-ov-motion') === 'off')) return;
if (matchMedia('(pointer: coarse)').matches || matchMedia('(max-width: 820px)').matches || (navigator.deviceMemory && navigator.deviceMemory <= 4)) {
if (matchMedia('(pointer: coarse)').matches || matchMedia('(max-width: 820px)').matches || (navigator.deviceMemory && navigator.deviceMemory <= 4)) {
document.documentElement.classList.add('rs-lite');
document.documentElement.classList.add('rs-lite');
}
}
@@ -86,7 +86,6 @@
@@ -86,7 +86,6 @@
.ovsk-fade{animation:ovskFade var(--ov-dur-2, .25s) var(--ov-ease-out, ease) forwards}
.ovsk-fade{animation:ovskFade var(--ov-dur-2, .25s) var(--ov-ease-out, ease) forwards}
@keyframes ovskFade{to{opacity:0}}
@keyframes ovskFade{to{opacity:0}}
@media (max-width:760px){#stream-grid-live .ovsk-card:nth-of-type(n+3){display:none}}
@media (max-width:760px){#stream-grid-live .ovsk-card:nth-of-type(n+3){display:none}}
- @media (prefers-reduced-motion:reduce){.ovsk::after,.ovsk-bar::after{animation:none}}
/* A route's own markup is fetched on first visit; until it lands the page shows its outline. */
/* A route's own markup is fetched on first visit; until it lands the page shows its outline. */
.ov-route-skel{display:grid;gap:14px;padding:24px 16px;max-width:1200px;margin:0 auto}
.ov-route-skel{display:grid;gap:14px;padding:24px 16px;max-width:1200px;margin:0 auto}
.ov-route-skel span{display:block;border-radius:14px;background:color-mix(in srgb,var(--text-muted,#8b93ad) 10%,transparent);animation:ovRouteSkel 1.4s var(--ov-ease-in-out, ease-in-out) infinite alternate}
.ov-route-skel span{display:block;border-radius:14px;background:color-mix(in srgb,var(--text-muted,#8b93ad) 10%,transparent);animation:ovRouteSkel 1.4s var(--ov-ease-in-out, ease-in-out) infinite alternate}
@@ -98,7 +97,6 @@
@@ -98,7 +97,6 @@
.ov-route-error>i{font-size:1.4rem;color:var(--danger-text, var(--danger,#ef4444))}
.ov-route-error>i{font-size:1.4rem;color:var(--danger-text, var(--danger,#ef4444))}
.ov-route-error>div{flex:1;min-width:180px;display:grid;gap:2px}
.ov-route-error>div{flex:1;min-width:180px;display:grid;gap:2px}
.ov-route-error span{color:var(--text-muted,#8b93ad);font-size:.9rem}
.ov-route-error span{color:var(--text-muted,#8b93ad);font-size:.9rem}
- @media (prefers-reduced-motion:reduce){.ov-route-skel span{animation:none}}
/* Keep the cosmetics modal out of the page before its stylesheet lands. */
/* Keep the cosmetics modal out of the page before its stylesheet lands. */
.cosmetics-modal-overlay:not(.active){display:none}
.cosmetics-modal-overlay:not(.active){display:none}
</style>
</style>
@@ -2193,7 +2193,7 @@ function switchChannelTab(tab, btn, jump) {
@@ -2193,7 +2193,7 @@ function switchChannelTab(tab, btn, jump) {
setTimeout(_queueChannelSectionSpy, reduce ? 200 : 1450);
setTimeout(_queueChannelSectionSpy, reduce ? 200 : 1450);
}
}
function _channelReduceMotion() {
function _channelReduceMotion() {
- return document.documentElement.dataset.ovMotion === 'reduced' || !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
+ return document.documentElement.dataset.ovMotion === 'off';
}
}
let _chJumpUntil = 0;
let _chJumpUntil = 0;
function _markChannelJump(tab, fromTap = false) {
function _markChannelJump(tab, fromTap = false) {
@@ -126,7 +126,7 @@ function ovPutCards(container, html, append) {
@@ -126,7 +126,7 @@ function ovPutCards(container, html, append) {
added.forEach(el => el.classList.add('hfx-reveal', 'is-in'));
added.forEach(el => el.classList.add('hfx-reveal', 'is-in'));
let reduce = false;
let reduce = false;
- try { reduce = matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { /* */ }
+ try { reduce = (document.documentElement.getAttribute('data-ov-motion') === 'off'); } catch { /* */ }
if (reduce) return;
if (reduce) return;
added.forEach((el, i) => {
added.forEach((el, i) => {
el.classList.add('ov-enter');
el.classList.add('ov-enter');
@@ -209,7 +209,7 @@ function startHeroQuips(quips) {
@@ -209,7 +209,7 @@ function startHeroQuips(quips) {
*/
*/
function animateHeightChange(wrap, mutate, opts = {}) {
function animateHeightChange(wrap, mutate, opts = {}) {
const { duration = 420, entering = null } = opts;
const { duration = 420, entering = null } = opts;
- const reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ const reduced = window.matchMedia && (document.documentElement.getAttribute('data-ov-motion') === 'off');
if (!wrap || reduced) { try { mutate(); } catch { /* */ } return; }
if (!wrap || reduced) { try { mutate(); } catch { /* */ } return; }
const h0 = wrap.offsetHeight;
const h0 = wrap.offsetHeight;
try { mutate(); } catch { /* */ }
try { mutate(); } catch { /* */ }
@@ -508,7 +508,7 @@ function _renderHeroStats(stats) {
@@ -508,7 +508,7 @@ function _renderHeroStats(stats) {
* nothing is clipped once it settles and the board can still grow if a number gets longer.
* nothing is clipped once it settles and the board can still grow if a number gets longer.
*/
*/
let swapping = 0;
let swapping = 0;
- const reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ const reduced = window.matchMedia && (document.documentElement.getAttribute('data-ov-motion') === 'off');
const swap = () => {
const swap = () => {
const toOpen = full.hidden;
const toOpen = full.hidden;
if (reduced) {
if (reduced) {
@@ -752,7 +752,7 @@ function renderHeroBackground(media, moments) {
@@ -752,7 +752,7 @@ function renderHeroBackground(media, moments) {
layers[0].classList.remove('active'); void layers[0].offsetWidth; layers[0].classList.add('active');
layers[0].classList.remove('active'); void layers[0].offsetWidth; layers[0].classList.add('active');
} else layers[0].classList.add('active');
} else layers[0].classList.add('active');
layers[1].classList.remove('active');
layers[1].classList.remove('active');
- if (frames.length < 2 || (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches && frames.length < 2)) return;
+ if (frames.length < 2 || (window.matchMedia && (document.documentElement.getAttribute('data-ov-motion') === 'off') && frames.length < 2)) return;
let idx = 0;
let idx = 0;
_heroBgTimer = setInterval(() => {
_heroBgTimer = setInterval(() => {
if (document.hidden) return;
if (document.hidden) return;
@@ -777,7 +777,7 @@ function renderHeroCollage(media) {
@@ -777,7 +777,7 @@ function renderHeroCollage(media) {
if (!wrap || !Array.isArray(media) || !media.length) return;
if (!wrap || !Array.isArray(media) || !media.length) return;
if (_heroCollageTimer) { clearInterval(_heroCollageTimer); _heroCollageTimer = null; }
if (_heroCollageTimer) { clearInterval(_heroCollageTimer); _heroCollageTimer = null; }
wrap.innerHTML = '';
wrap.innerHTML = '';
- const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ const reduce = window.matchMedia && (document.documentElement.getAttribute('data-ov-motion') === 'off');
const w = window.innerWidth || 1200;
const w = window.innerWidth || 1200;
const count = Math.min(media.length, w < 640 ? 5 : w < 1024 ? 8 : 12);
const count = Math.min(media.length, w < 640 ? 5 : w < 1024 ? 8 : 12);
const positions = _heroCollagePositions(count);
const positions = _heroCollagePositions(count);
@@ -824,7 +824,7 @@ function _heroParallaxInit() {
@@ -824,7 +824,7 @@ function _heroParallaxInit() {
const hero = document.querySelector('#page-home .hero');
const hero = document.querySelector('#page-home .hero');
const collage = document.getElementById('hero-collage');
const collage = document.getElementById('hero-collage');
if (!hero || !collage) return;
if (!hero || !collage) return;
- if (window.matchMedia && (window.matchMedia('(prefers-reduced-motion: reduce)').matches || window.matchMedia('(pointer: coarse)').matches)) return;
+ if (window.matchMedia && ((document.documentElement.getAttribute('data-ov-motion') === 'off') || window.matchMedia('(pointer: coarse)').matches)) return;
_heroParallaxBound = true;
_heroParallaxBound = true;
// Parallax is a plain 2D shift of the whole collage, written straight to its transform. It used
// Parallax is a plain 2D shift of the whole collage, written straight to its transform. It used
// to write --tilt-x/--tilt-y custom properties; custom properties inherit, so every pointer move
// to write --tilt-x/--tilt-y custom properties; custom properties inherit, so every pointer move
@@ -980,7 +980,7 @@ function _initHomeAbout() {
@@ -980,7 +980,7 @@ function _initHomeAbout() {
*/
*/
function _aboutPlayOpening(banner) {
function _aboutPlayOpening(banner) {
let reduce = false;
let reduce = false;
- try { reduce = matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { /* */ }
+ try { reduce = (document.documentElement.getAttribute('data-ov-motion') === 'off'); } catch { /* */ }
if (reduce) return;
if (reduce) return;
const items = banner.querySelectorAll('.home-about-body .cta-section-label, .home-about-body .cta-method, .home-about-body .home-cta-feature, .home-about-body .home-cta-buttons, .home-about-body .cta-money-lede');
const items = banner.querySelectorAll('.home-about-body .cta-section-label, .home-about-body .cta-method, .home-about-body .home-cta-feature, .home-about-body .home-cta-buttons, .home-about-body .cta-money-lede');
items.forEach((el, i) => el.style.setProperty('--about-i', String(Math.min(i, 16))));
items.forEach((el, i) => el.style.setProperty('--about-i', String(Math.min(i, 16))));
@@ -1197,7 +1197,7 @@ function _ovnetCount(root) {
@@ -1197,7 +1197,7 @@ function _ovnetCount(root) {
const n = Number(root.querySelector('.ovnet-groups')?.dataset.sites || el?.dataset.count || 0);
const n = Number(root.querySelector('.ovnet-groups')?.dataset.sites || el?.dataset.count || 0);
if (!el || !n) return;
if (!el || !n) return;
el.dataset.count = String(n);
el.dataset.count = String(n);
- if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) { el.textContent = String(n); return; }
+ if (document.documentElement.getAttribute('data-ov-motion') === 'off') { el.textContent = String(n); return; }
const clock = window.performance || Date;
const clock = window.performance || Date;
const t0 = clock.now(), dur = 900;
const t0 = clock.now(), dur = 900;
const step = (t) => {
const step = (t) => {
@@ -1281,7 +1281,7 @@ function initHomeNetwork() {
@@ -1281,7 +1281,7 @@ function initHomeNetwork() {
_ovnetCount(root);
_ovnetCount(root);
_ovnetIcons();
_ovnetIcons();
};
};
- if ('IntersectionObserver' in window && !(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches)) {
+ if ('IntersectionObserver' in window && !(window.matchMedia && (document.documentElement.getAttribute('data-ov-motion') === 'off'))) {
root.classList.add('is-armed');
root.classList.add('is-armed');
const io = new IntersectionObserver((entries) => {
const io = new IntersectionObserver((entries) => {
if (entries.some((en) => en.isIntersecting)) { io.disconnect(); requestAnimationFrame(wake); }
if (entries.some((en) => en.isIntersecting)) { io.disconnect(); requestAnimationFrame(wake); }
@@ -1372,7 +1372,7 @@ function ovWakeBrandMark() {
@@ -1372,7 +1372,7 @@ function ovWakeBrandMark() {
try { marks = document.querySelectorAll('.brand-mark'); } catch { return; }
try { marks = document.querySelectorAll('.brand-mark'); } catch { return; }
if (!marks || !marks.length) return;
if (!marks || !marks.length) return;
let reduce = false;
let reduce = false;
- try { reduce = matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { /* */ }
+ try { reduce = (document.documentElement.getAttribute('data-ov-motion') === 'off'); } catch { /* */ }
if (reduce) {
if (reduce) {
// CSS stops the loops under reduced motion but cannot reach SMIL — the gradient sweep on the
// CSS stops the loops under reduced motion but cannot reach SMIL — the gradient sweep on the
// V would otherwise run for the whole session. Hold everything still, once.
// V would otherwise run for the whole session. Hold everything still, once.
@@ -1762,7 +1762,7 @@ function _ensureGoalCycleScheduler() {
@@ -1762,7 +1762,7 @@ function _ensureGoalCycleScheduler() {
}, GOAL_CYCLE_INTERVAL_MS);
}, GOAL_CYCLE_INTERVAL_MS);
}
}
function _playGoalCycle(el) {
function _playGoalCycle(el) {
- if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
+ if (window.matchMedia && (document.documentElement.getAttribute('data-ov-motion') === 'off')) return;
const track = el && el.querySelector('.cgw-track-cycle');
const track = el && el.querySelector('.cgw-track-cycle');
if (!track || track.children.length < 2) return;
if (!track || track.children.length < 2) return;
const n = parseInt(el.dataset.goalCount || '0', 10);
const n = parseInt(el.dataset.goalCount || '0', 10);
@@ -1050,7 +1050,7 @@ function _glOpenFold(id) {
@@ -1050,7 +1050,7 @@ function _glOpenFold(id) {
const d = document.getElementById(id);
const d = document.getElementById(id);
if (!d) return;
if (!d) return;
d.open = true;
d.open = true;
- d.scrollIntoView({ block: 'start', behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' });
+ d.scrollIntoView({ block: 'start', behavior: (document.documentElement.getAttribute('data-ov-motion') === 'off') ? 'auto' : 'smooth' });
}
}
function _glCopyLink() {
function _glCopyLink() {
@@ -2834,7 +2834,7 @@ function _chatEvCss() {
@@ -2834,7 +2834,7 @@ function _chatEvCss() {
@container (max-width:240px){.chat-ev{grid-template-columns:auto minmax(0,1fr)}.chat-ev-acts{grid-column:2;justify-self:start}}
@container (max-width:240px){.chat-ev{grid-template-columns:auto minmax(0,1fr)}.chat-ev-acts{grid-column:2;justify-self:start}}
.chat-ev--here{grid-template-columns:auto minmax(0,1fr);padding:6px 10px}.chat-ev--here .chat-ev-icon{width:22px;height:22px}.chat-ev--here .chat-ev-icon::before{width:8px;height:8px}
.chat-ev--here{grid-template-columns:auto minmax(0,1fr);padding:6px 10px}.chat-ev--here .chat-ev-icon{width:22px;height:22px}.chat-ev--here .chat-ev-icon::before{width:8px;height:8px}
@container (max-width:360px){@media (pointer:coarse){.chat-ev-cta{width:44px}}}
@container (max-width:360px){@media (pointer:coarse){.chat-ev-cta{width:44px}}}
-@media (prefers-reduced-motion:reduce){.chat-ev.chat-msg-new{animation:none}.chat-ev--live .chat-ev-icon::before{animation:none}}`;
+`;
document.head.appendChild(st);
document.head.appendChild(st);
}
}
function _chatEvTime(at) {
function _chatEvTime(at) {
@@ -390,7 +390,7 @@
@@ -390,7 +390,7 @@
const ratios = new Map();
const ratios = new Map();
let active = null;
let active = null;
const still = (() => {
const still = (() => {
- try { if (matchMedia('(prefers-reduced-motion: reduce)').matches) return true; } catch { /* */ }
+ try { if (/^(reduced|off)$/.test(document.documentElement.getAttribute('data-ov-motion') || '')) return true; } catch { /* */ }
try { if (navigator.connection && navigator.connection.saveData) return true; } catch { /* */ }
try { if (navigator.connection && navigator.connection.saveData) return true; } catch { /* */ }
return false;
return false;
})();
})();
@@ -107,7 +107,7 @@
@@ -107,7 +107,7 @@
localStorage.setItem(KEY, JSON.stringify([...openSet]));
localStorage.setItem(KEY, JSON.stringify([...openSet]));
} catch { /* */ }
} catch { /* */ }
- const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
+ const reduce = (document.documentElement.getAttribute('data-ov-motion') === 'off');
if (reduce) { body.hidden = !opening; busy = false; return; }
if (reduce) { body.hidden = !opening; busy = false; return; }
// Animate the real height rather than a max-height guess: the board's height varies by
// Animate the real height rather than a max-height guess: the board's height varies by
@@ -7,12 +7,12 @@
@@ -7,12 +7,12 @@
* - scroll reveal: section headers, cards and grids slide/fade in as they enter the viewport
* - scroll reveal: section headers, cards and grids slide/fade in as they enter the viewport
* - count-up: hero stat numbers tick up from 0 when first painted
* - count-up: hero stat numbers tick up from 0 when first painted
* - card tilt: stream cards tilt slightly toward the pointer
* - card tilt: stream cards tilt slightly toward the pointer
- * Everything is disabled under prefers-reduced-motion, paused when the tab is hidden, and
+ * Everything is disabled with motion off (Calm plays it), paused when the tab is hidden, and
* skips the canvas on coarse-pointer / narrow screens to keep phones cool.
* skips the canvas on coarse-pointer / narrow screens to keep phones cool.
*/
*/
(function () {
(function () {
'use strict';
'use strict';
- const REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ const REDUCED = window.matchMedia && (document.documentElement.getAttribute('data-ov-motion') === 'off');
const COARSE = window.matchMedia && window.matchMedia('(pointer: coarse)').matches;
const COARSE = window.matchMedia && window.matchMedia('(pointer: coarse)').matches;
if (REDUCED) return;
if (REDUCED) return;
// Phones, coarse pointers and low-memory devices run in "lite" mode: no particle canvas, no
// Phones, coarse pointers and low-memory devices run in "lite" mode: no particle canvas, no
@@ -131,7 +131,7 @@
@@ -131,7 +131,7 @@
svg.setAttribute('viewBox', `0 0 ${R.width} ${R.height}`);
svg.setAttribute('viewBox', `0 0 ${R.width} ${R.height}`);
svg.setAttribute('width', R.width); svg.setAttribute('height', R.height);
svg.setAttribute('width', R.width); svg.setAttribute('height', R.height);
const vertical = getComputedStyle(stage).getPropertyValue('--tour-vertical').trim() === '1';
const vertical = getComputedStyle(stage).getPropertyValue('--tour-vertical').trim() === '1';
- const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches || document.documentElement.classList.contains('rs-lite');
+ const reduce = (document.documentElement.getAttribute('data-ov-motion') === 'off') || document.documentElement.classList.contains('rs-lite');
const logo = stage.querySelector('.tour-core-logo') || stage.querySelector('[data-node="core"]');
const logo = stage.querySelector('.tour-core-logo') || stage.querySelector('[data-node="core"]');
if (!logo) return;
if (!logo) return;
@@ -55,7 +55,6 @@
@@ -55,7 +55,6 @@
.ovd-bar { display: block; width: 2.5px; border-radius: 1px; background: currentColor; height: 11px; }
.ovd-bar { display: block; width: 2.5px; border-radius: 1px; background: currentColor; height: 11px; }
.ovd-btn[data-n="1"] .ovd-bar { width: 9px; }
.ovd-btn[data-n="1"] .ovd-bar { width: 9px; }
.ovd-btn[data-n="2"] .ovd-bar { width: 5px; }
.ovd-btn[data-n="2"] .ovd-bar { width: 5px; }
-@media (prefers-reduced-motion: reduce) { .ovd-btn { transition: none; } }
/* The grid honours the chosen count; without a choice it keeps its own auto-fit behaviour. */
/* The grid honours the chosen count; without a choice it keeps its own auto-fit behaviour. */
[data-ovd-cols] { grid-template-columns: repeat(var(--ovd-cols), minmax(0, 1fr)) !important; }
[data-ovd-cols] { grid-template-columns: repeat(var(--ovd-cols), minmax(0, 1fr)) !important; }
`;
`;
@@ -33,7 +33,7 @@
@@ -33,7 +33,7 @@
const KEY = 'ov-mini-player';
const KEY = 'ov-mini-player';
const st = { box: null, video: null, home: null, title: '', href: '', user: '', styleEl: null, chat: null, x: 0, y: 0, w: 0, corner: 'br', idle: 0, offRo: null };
const st = { box: null, video: null, home: null, title: '', href: '', user: '', styleEl: null, chat: null, x: 0, y: 0, w: 0, corner: 'br', idle: 0, offRo: null };
- const reduced = () => { try { return matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { return false; } };
+ const reduced = () => { try { return (document.documentElement.getAttribute('data-ov-motion') === 'off'); } catch { return false; } };
const phone = () => { try { return matchMedia('(max-width: 759px)').matches; } catch { return false; } };
const phone = () => { try { return matchMedia('(max-width: 759px)').matches; } catch { return false; } };
const fine = () => { try { return matchMedia('(hover: hover) and (pointer: fine)').matches; } catch { return false; } };
const fine = () => { try { return matchMedia('(hover: hover) and (pointer: fine)').matches; } catch { return false; } };
@@ -106,7 +106,6 @@
@@ -106,7 +106,6 @@
.ov-mp-chat-h .ov-mp-b{background:transparent;color:inherit;width:30px;height:30px}
.ov-mp-chat-h .ov-mp-b{background:transparent;color:inherit;width:30px;height:30px}
.ov-mp-chat-h .ov-mp-b:hover{background:rgba(127,127,127,.18)}
.ov-mp-chat-h .ov-mp-b:hover{background:rgba(127,127,127,.18)}
.ov-mp-chat iframe{flex:1;width:100%;border:0;background:transparent}
.ov-mp-chat iframe{flex:1;width:100%;border:0;background:transparent}
-@media (prefers-reduced-motion:reduce){.ov-mini-player.is-settling,.ov-mp-chat{transition:none}}
`;
`;
(document.head || document.documentElement).appendChild(st.styleEl);
(document.head || document.documentElement).appendChild(st.styleEl);
}
}
@@ -36,7 +36,7 @@
@@ -36,7 +36,7 @@
.ovnum.is-down .ovnum-digit.changed{animation:ovnumDown .62s ease-out}
.ovnum.is-down .ovnum-digit.changed{animation:ovnumDown .62s ease-out}
@keyframes ovnumUp{0%{filter:none}35%{filter:brightness(1.55)}100%{filter:none}}
@keyframes ovnumUp{0%{filter:none}35%{filter:brightness(1.55)}100%{filter:none}}
@keyframes ovnumDown{0%{filter:none}35%{filter:brightness(.72)}100%{filter:none}}
@keyframes ovnumDown{0%{filter:none}35%{filter:brightness(.72)}100%{filter:none}}
-@media (prefers-reduced-motion:reduce){.ovnum-track{transition:none}.ovnum .ovnum-digit.changed{animation:none}}`;
+`;
function ensure() {
function ensure() {
if (document.getElementById('ovnum-css')) return;
if (document.getElementById('ovnum-css')) return;
@@ -13,7 +13,7 @@
@@ -13,7 +13,7 @@
if (typeof window === 'undefined' || window.OVRail) return;
if (typeof window === 'undefined' || window.OVRail) return;
let seq = 0;
let seq = 0;
- const reducedMotion = () => !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
+ const reducedMotion = () => !!(window.matchMedia && (document.documentElement.getAttribute('data-ov-motion') === 'off'));
function button(dir, track, label) {
function button(dir, track, label) {
const b = document.createElement('button');
const b = document.createElement('button');
@@ -39,7 +39,7 @@
@@ -39,7 +39,7 @@
.ovsk-panel { padding: 14px; display: grid; gap: 10px; }
.ovsk-panel { padding: 14px; display: grid; gap: 10px; }
.ovsk-fade { animation: ovskFade .25s ease forwards; }
.ovsk-fade { animation: ovskFade .25s ease forwards; }
@keyframes ovskFade { to { opacity: 0; } }
@keyframes ovskFade { to { opacity: 0; } }
-@media (prefers-reduced-motion: reduce) { .ovsk::after, .ovsk-bar::after { animation: none; } }`;
+`;
function ensure() {
function ensure() {
if (document.getElementById('ovsk-css')) return;
if (document.getElementById('ovsk-css')) return;
@@ -235,7 +235,6 @@
@@ -235,7 +235,6 @@
'.ov-freeze-notice button svg{width:16px;height:16px}',
'.ov-freeze-notice button svg{width:16px;height:16px}',
'@keyframes ov-freeze-in{from{opacity:0;transform:translate(-50%,-6px) scale(.96)}}',
'@keyframes ov-freeze-in{from{opacity:0;transform:translate(-50%,-6px) scale(.96)}}',
'@keyframes ov-freeze-turn{to{transform:rotate(360deg)}}',
'@keyframes ov-freeze-turn{to{transform:rotate(360deg)}}',
- 'html[data-ov-motion=reduced] .ov-freeze-notice::before{animation-duration:1.8s}',
].join('');
].join('');
(document.head || document.documentElement).appendChild(styleEl);
(document.head || document.documentElement).appendChild(styleEl);
}
}
@@ -58,7 +58,8 @@
@@ -58,7 +58,8 @@
/** The blurred fill beside a picture narrower than its box. */
/** The blurred fill beside a picture narrower than its box. */
function glow(box, source) {
function glow(box, source) {
- const reduce = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
+ // A slow colour fade moves nothing: Calm keeps the glow, only motion off drops it.
+ const reduce = !!(document.documentElement && document.documentElement.getAttribute('data-ov-motion') === 'off');
const saver = navigator.connection && navigator.connection.saveData;
const saver = navigator.connection && navigator.connection.saveData;
const c = document.createElement('canvas');
const c = document.createElement('canvas');
const ctx = !reduce && !saver && c.getContext && c.getContext('2d');
const ctx = !reduce && !saver && c.getContext && c.getContext('2d');
@@ -146,7 +146,6 @@ button{font:inherit}
@@ -146,7 +146,6 @@ button{font:inherit}
box-shadow:0 0 12px var(--red);animation:pulse 1.6s infinite
box-shadow:0 0 12px var(--red);animation:pulse 1.6s infinite
}
}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
-@media (prefers-reduced-motion:reduce){.hero-side .live-pill .pulse{animation:none}}
.hero-side span.tag{
.hero-side span.tag{
display:block;margin-top:.8vh;
display:block;margin-top:.8vh;
font-size:.78vw;color:var(--muted);font-weight:850;
font-size:.78vw;color:var(--muted);font-weight:850;
@@ -33,7 +33,7 @@
@@ -33,7 +33,7 @@
'.hln-x{position:absolute;top:5px;right:7px;color:#7a7a86;font-size:14px;line-height:1;padding:2px;cursor:pointer;background:none;border:none}' +
'.hln-x{position:absolute;top:5px;right:7px;color:#7a7a86;font-size:14px;line-height:1;padding:2px;cursor:pointer;background:none;border:none}' +
'.hln-x:hover{color:#fff}' +
'.hln-x:hover{color:#fff}' +
'@keyframes hlnPulse{0%,100%{opacity:1}50%{opacity:.3}}' +
'@keyframes hlnPulse{0%,100%{opacity:1}50%{opacity:.3}}' +
- '@media (prefers-reduced-motion:reduce){.hln-card{transition:opacity .3s}}';
+ '';
(document.head || document.documentElement).appendChild(s);
(document.head || document.documentElement).appendChild(s);
}
}
@@ -43,7 +43,7 @@ body[data-pos=bottom] .al { animation-name: alInUp; }
@@ -43,7 +43,7 @@ body[data-pos=bottom] .al { animation-name: alInUp; }
.al-msg:empty { display: none; }
.al-msg:empty { display: none; }
.al-status { position: fixed; left: 1vw; bottom: 1vw; font-size: .9vw; color: rgba(255,255,255,.55); text-shadow: 0 1px 2px #000; }
.al-status { position: fixed; left: 1vw; bottom: 1vw; font-size: .9vw; color: rgba(255,255,255,.55); text-shadow: 0 1px 2px #000; }
.al-status:empty { display: none; }
.al-status:empty { display: none; }
-@media (prefers-reduced-motion: reduce) { .al, .al.is-out { animation: none; } .al::after { display: none; } }
+html[data-ov-motion=off] .al::after{display: none}
</style>
</style>
<script src="/shared/theme-loader.js"></script>
<script src="/shared/theme-loader.js"></script>
<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->
<!-- The OpenVibe glyphs draw every fa- icon here (no Font Awesome since 2026-11-01). -->
@@ -264,10 +264,7 @@ body.no-shadow .msg { text-shadow: none; }
@@ -264,10 +264,7 @@ body.no-shadow .msg { text-shadow: none; }
}
}
.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::before,
.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::before,
.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::after { animation-play-state: paused !important; }
.chat-msg.has-fx:not(.fx-on) .name-fx-vegas::after { animation-play-state: paused !important; }
-@media (prefers-reduced-motion: reduce) {
- .name-fx-vegas, .name-fx-vegas::before, .name-fx-vegas::after { animation: none !important; }
- .name-fx-vegas { filter: drop-shadow(0 0 3px rgba(255, 207, 63, .9)); }
-}
+html[data-ov-motion=off] .name-fx-vegas{filter: drop-shadow(0 0 3px rgba(255, 207, 63, .9))}
/* 2fat Bass Drop (2026-10-23, one of a kind: 2fat, who brings the old classic bangers back): the name thumps on the beat,
/* 2fat Bass Drop (2026-10-23, one of a kind: 2fat, who brings the old classic bangers back): the name thumps on the beat,
a big drop then a smaller one, in club lights, with an equalizer dancing before it and a speaker after it. Only its
a big drop then a smaller one, in club lights, with an equalizer dancing before it and a speaker after it. Only its
one holder wears it (fx_twofat is never in the catalog). The equalizer and the speaker are pseudo-elements, so they
one holder wears it (fx_twofat is never in the catalog). The equalizer and the speaker are pseudo-elements, so they
@@ -329,10 +326,7 @@ body.no-shadow .msg { text-shadow: none; }
@@ -329,10 +326,7 @@ body.no-shadow .msg { text-shadow: none; }
}
}
.chat-msg.has-fx:not(.fx-on) .name-fx-twofat::before,
.chat-msg.has-fx:not(.fx-on) .name-fx-twofat::before,
.chat-msg.has-fx:not(.fx-on) .name-fx-twofat::after { animation-play-state: paused !important; }
.chat-msg.has-fx:not(.fx-on) .name-fx-twofat::after { animation-play-state: paused !important; }
-@media (prefers-reduced-motion: reduce) {
- .name-fx-twofat, .name-fx-twofat::before, .name-fx-twofat::after { animation: none !important; }
- .name-fx-twofat { filter: drop-shadow(0 0 3px rgba(255, 47, 214, .9)); }
-}
+html[data-ov-motion=off] .name-fx-twofat{filter: drop-shadow(0 0 3px rgba(255, 47, 214, .9))}
.name-fx-void {
.name-fx-void {
color: #9900ff !important;
color: #9900ff !important;
@@ -56,7 +56,7 @@ body::after { content: ''; position: absolute; inset: 0; z-index: -1; pointer-ev
@@ -56,7 +56,7 @@ body::after { content: ''; position: absolute; inset: 0; z-index: -1; pointer-ev
.sc > :nth-child(4) { animation-delay: 1.2s; }
.sc > :nth-child(4) { animation-delay: 1.2s; }
.sc > :nth-child(5) { animation-delay: 1.35s; }
.sc > :nth-child(5) { animation-delay: 1.35s; }
@keyframes scIn { to { opacity: 1; transform: none; } }
@keyframes scIn { to { opacity: 1; transform: none; } }
-@media (prefers-reduced-motion: reduce) { .sc-light { animation: none; } .sc > * { animation: none; opacity: 1; transform: none; } }
+html[data-ov-motion=off] .sc > *{opacity: 1;transform: none}
</style>
</style>
<script src="/shared/theme-loader.js"></script>
<script src="/shared/theme-loader.js"></script>
</head>
</head>
@@ -47,5 +47,5 @@ assert.ok(!/innerHTML\s*=\s*[^;]*ev\.(name|title)/.test(card), 'the name and tit
@@ -47,5 +47,5 @@ assert.ok(!/innerHTML\s*=\s*[^;]*ev\.(name|title)/.test(card), 'the name and tit
assert.ok(/who\.textContent = ev\.name/.test(card) && /title\.textContent = `“\$\{ev\.title\}”`/.test(card), 'they are text nodes');
assert.ok(/who\.textContent = ev\.name/.test(card) && /title\.textContent = `“\$\{ev\.title\}”`/.test(card), 'they are text nodes');
assert.ok(/showTime = !!opts\.isGlobal \|\| !!chatSettings\.showTimestamps/.test(card), 'time: global always, elsewhere the viewer\'s setting');
assert.ok(/showTime = !!opts\.isGlobal \|\| !!chatSettings\.showTimestamps/.test(card), 'time: global always, elsewhere the viewer\'s setting');
assert.ok(/_CHAT_EV_FOLD_MS/.test(card) && /went live again/.test(card), 'the same person within 15 minutes updates the card');
assert.ok(/_CHAT_EV_FOLD_MS/.test(card) && /went live again/.test(card), 'the same person within 15 minutes updates the card');
-assert.ok(/prefers-reduced-motion:reduce/.test(src.slice(src.indexOf('function _chatEvCss('), src.indexOf('function _chatEvTime('))), 'motion stops for people who ask');
+assert.ok(!/prefers-reduced-motion/.test(src.slice(src.indexOf('function _chatEvCss('), src.indexOf('function _chatEvTime('))), 'Calm plays the cards (openvibe-shared 4.183.0); only motion off stops them, by the loader\'s one rule');
console.log('chat event cards: ok');
console.log('chat event cards: ok');
@@ -44,11 +44,11 @@ const cosmetics = require('../server/monetization/cosmetics');
@@ -44,11 +44,11 @@ const cosmetics = require('../server/monetization/cosmetics');
const css = fs.readFileSync(path.join(__dirname, '..', file), 'utf8');
const css = fs.readFileSync(path.join(__dirname, '..', file), 'utf8');
assert.ok(css.includes('.name-fx-vegas {') && css.includes('@keyframes nameFxVegasReel') && css.includes('@keyframes nameFxVegasJackpot'), `${file} draws the effect`);
assert.ok(css.includes('.name-fx-vegas {') && css.includes('@keyframes nameFxVegasReel') && css.includes('@keyframes nameFxVegasJackpot'), `${file} draws the effect`);
assert.ok(css.includes('.px-vegas .chat-particle'), `${file} colours the Vegas particles`);
assert.ok(css.includes('.px-vegas .chat-particle'), `${file} colours the Vegas particles`);
- assert.ok(css.includes('prefers-reduced-motion: reduce') && /\.name-fx-vegas::before, \.name-fx-vegas::after \{ animation: none/.test(css), `${file} stops the reels for reduced motion`);
- // 2fat's set (2026-10-23): the bass drop, its equalizer and its particles, stopped for reduced motion too.
+ assert.ok(!css.includes('prefers-reduced-motion') && css.includes('html[data-ov-motion=off] .name-fx-vegas{filter:'), `${file}: Calm plays the reels; with motion off the glow stays`);
+ // 2fat's set (2026-10-23): the bass drop, its equalizer and its particles; at rest on its glow with motion off.
assert.ok(css.includes('.name-fx-twofat {') && css.includes('@keyframes nameFxTwofatThump') && css.includes('@keyframes nameFxTwofatEq'), `${file} draws 2fat's effect`);
assert.ok(css.includes('.name-fx-twofat {') && css.includes('@keyframes nameFxTwofatThump') && css.includes('@keyframes nameFxTwofatEq'), `${file} draws 2fat's effect`);
assert.ok(css.includes('.px-twofat .chat-particle'), `${file} colours 2fat's particles`);
assert.ok(css.includes('.px-twofat .chat-particle'), `${file} colours 2fat's particles`);
- assert.ok(/\.name-fx-twofat, \.name-fx-twofat::before, \.name-fx-twofat::after \{ animation: none/.test(css), `${file} stops 2fat's effect for reduced motion`);
+ assert.ok(css.includes('html[data-ov-motion=off] .name-fx-twofat{filter:'), `${file}: 2fat's effect rests on its glow with motion off`);
}
}
// 2fat's set is one of a kind like Vegas, and fits Inventory's kinds.
// 2fat's set is one of a kind like Vegas, and fits Inventory's kinds.
const T = cosmetics.COSMETICS;
const T = cosmetics.COSMETICS;
@@ -105,7 +105,7 @@ function page(overflowX) {
@@ -105,7 +105,7 @@ function page(overflowX) {
// control's column clamp never squeezes a rail's cards.
// control's column clamp never squeezes a rail's cards.
assert.match(css, /\.home-rail-tail \{ display: none; \}/);
assert.match(css, /\.home-rail-tail \{ display: none; \}/);
assert.match(css, /#home-catchup \.stream-grid > \.home-rail-tail, #home-catchup \.home-pastes-list > \.home-rail-tail \{ display: flex; \}/);
assert.match(css, /#home-catchup \.stream-grid > \.home-rail-tail, #home-catchup \.home-pastes-list > \.home-rail-tail \{ display: flex; \}/);
- assert.match(css, /@media \(prefers-reduced-motion: reduce\) \{ \.home-rail-skel \{ animation: none; \} \}/);
+ assert.doesNotMatch(css, /\.home-rail-skel \{ animation: none/, 'the placeholder shimmers under Calm too (slower, by the loader)');
assert.match(css, /#home-catchup \.stream-grid\[data-ovd-cols\], #home-catchup \.home-pastes-list\[data-ovd-cols\] \{ grid-template-columns: none !important; \}/);
assert.match(css, /#home-catchup \.stream-grid\[data-ovd-cols\], #home-catchup \.home-pastes-list\[data-ovd-cols\] \{ grid-template-columns: none !important; \}/);
// Recent Pastes rides the same rail on a phone (it was two cards a row with "Load 2 more", 2026-11-02).
// Recent Pastes rides the same rail on a phone (it was two cards a row with "Load 2 more", 2026-11-02).
assert.match(css, /#home-catchup \.stream-grid, #home-catchup \.home-pastes-list \{\n\s+display: grid; grid-auto-flow: column;/);
assert.match(css, /#home-catchup \.stream-grid, #home-catchup \.home-pastes-list \{\n\s+display: grid; grid-auto-flow: column;/);
@@ -65,6 +65,9 @@ function openHome({ views = 0 } = {}) {
@@ -65,6 +65,9 @@ function openHome({ views = 0 } = {}) {
});
});
if (views) store.set('ov_home_intro_seen', String(views));
if (views) store.set('ov_home_intro_seen', String(views));
window.matchMedia = () => ({ matches: true, addEventListener() { }, removeEventListener() { } });
window.matchMedia = () => ({ matches: true, addEventListener() { }, removeEventListener() { } });
+ // Motion off: the page writes its numbers at once (the frame stub below passes no time to count with). Since
+ // openvibe-shared 4.183.0 a device that asks for less motion still counts up; only Off does not.
+ window.document.documentElement.setAttribute('data-ov-motion', 'off');
window.requestAnimationFrame = (f) => setTimeout(f, 0);
window.requestAnimationFrame = (f) => setTimeout(f, 0);
window.cancelAnimationFrame = () => { };
window.cancelAnimationFrame = () => { };
window.scrollTo = () => { };
window.scrollTo = () => { };
@@ -20,7 +20,7 @@ assert.ok(re.test('/obs/alerts/goosely') && !re.test('/obs/alerts/<x>') && !re.t
@@ -20,7 +20,7 @@ assert.ok(re.test('/obs/alerts/goosely') && !re.test('/obs/alerts/<x>') && !re.t
// ── The page, statically ──
// ── The page, statically ──
assert.ok(!/innerHTML/.test(script), 'a message is only ever text');
assert.ok(!/innerHTML/.test(script), 'a message is only ever text');
assert.match(SRC, /background: transparent !important/, 'a transparent ground over the stream');
assert.match(SRC, /background: transparent !important/, 'a transparent ground over the stream');
-assert.match(SRC, /@media \(prefers-reduced-motion: reduce\)/);
+assert.doesNotMatch(SRC, /prefers-reduced-motion/, 'an alert plays whatever the streamer\'s own device asks');
assert.match(script, /m\.type === 'overlay-alert'/, 'only overlay-alert messages');
assert.match(script, /m\.type === 'overlay-alert'/, 'only overlay-alert messages');
assert.match(script, /type: 'join', streamId: streamId, channel: user/, 'joins the channel\'s room, live or not');
assert.match(script, /type: 'join', streamId: streamId, channel: user/, 'joins the channel\'s room, live or not');
@@ -26,7 +26,7 @@ assert.match(SRC, /\/\^\[a-z0-9-\]\{1,40\}\$\/\.test\(q\.get\('theme'\)/, 'the t
@@ -26,7 +26,7 @@ assert.match(SRC, /\/\^\[a-z0-9-\]\{1,40\}\$\/\.test\(q\.get\('theme'\)/, 'the t
assert.ok(!/innerHTML/.test(SRC), 'a query value is only ever text');
assert.ok(!/innerHTML/.test(SRC), 'a query value is only ever text');
assert.match(SRC, /\(q\.get\('msg'\) \|\| ''\)\.slice\(0, 140\)/, 'a message of your own, short');
assert.match(SRC, /\(q\.get\('msg'\) \|\| ''\)\.slice\(0, 140\)/, 'a message of your own, short');
assert.match(SRC, /setAttribute\('data-state', 'busy'\)/, 'on Be right back the mark breathes');
assert.match(SRC, /setAttribute\('data-state', 'busy'\)/, 'on Be right back the mark breathes');
-assert.match(SRC, /@media \(prefers-reduced-motion: reduce\) \{ \.sc-light \{ animation: none; \}/, 'still under reduced motion');
+assert.doesNotMatch(SRC, /prefers-reduced-motion/, 'a scene plays whatever the streamer\'s own device asks: the audience is who sees it');
assert.ok(!/spin|rotate\(/.test(SRC.replace(/<!--[\s\S]*?-->/g, '')), 'nothing spins');
assert.ok(!/spin|rotate\(/.test(SRC.replace(/<!--[\s\S]*?-->/g, '')), 'nothing spins');
// ── The dashboard: a Scenes card in the Chat tab, a URL for each scene in the theme picked ──
// ── The dashboard: a Scenes card in the Chat tab, a URL for each scene in the theme picked ──
const frag = fs.readFileSync(path.join(__dirname, '..', 'public/fragments/dashboard.html'), 'utf8');
const frag = fs.readFileSync(path.join(__dirname, '..', 'public/fragments/dashboard.html'), 'utf8');
@@ -36,4 +36,4 @@ assert.match(dash, /chatai: +\['dash-card-vibe', 'dash-aibot-card', 'dash-card-o
@@ -36,4 +36,4 @@ assert.match(dash, /chatai: +\['dash-card-vibe', 'dash-aibot-card', 'dash-card-o
assert.match(dash, /\['', 'obs', 'scene', kind, encodeURIComponent\(currentUser\.username\)\]\.join\('\/'\)/, 'each scene\'s address');
assert.match(dash, /\['', 'obs', 'scene', kind, encodeURIComponent\(currentUser\.username\)\]\.join\('\/'\)/, 'each scene\'s address');
assert.match(dash, /if \(theme && theme !== 'vibe'\) q\.set\('theme', theme\)/, 'the theme picked rides along');
assert.match(dash, /if \(theme && theme !== 'vibe'\) q\.set\('theme', theme\)/, 'the theme picked rides along');
assert.match(dash, /_call\('updateDashScenes'\)/, 'drawn when the tab opens');
assert.match(dash, /_call\('updateDashScenes'\)/, 'drawn when the tab opens');
-console.log('obs scenes: three scenes by route, the loader and the mark, any theme by name, the mark arriving, text only, still under reduced motion');
+console.log('obs scenes: three scenes by route, the loader and the mark, any theme by name, the mark arriving, text only, playing whatever the device asks');
@@ -38,11 +38,11 @@ function element(tag, props = {}) {
@@ -38,11 +38,11 @@ function element(tag, props = {}) {
}, props);
}, props);
}
}
-function load({ store = {}, reduce = false } = {}) {
+function load({ store = {}, reduce = false, motion = null } = {}) {
const observers = [];
const observers = [];
const ctx = {
const ctx = {
window: {}, console, Math, JSON, Number, Object, setInterval: () => 1, clearInterval() {},
window: {}, console, Math, JSON, Number, Object, setInterval: () => 1, clearInterval() {},
- document: { hidden: false, createElement: (t) => element(t) },
+ document: { hidden: false, createElement: (t) => element(t), documentElement: { getAttribute: (k) => (k === 'data-ov-motion' ? motion : null) } },
navigator: {},
navigator: {},
matchMedia: () => ({ matches: reduce }),
matchMedia: () => ({ matches: reduce }),
localStorage: { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); } },
localStorage: { getItem: (k) => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); } },
@@ -123,12 +123,17 @@ const near = (a, b) => Math.abs(a - b) < 0.001;
@@ -123,12 +123,17 @@ const near = (a, b) => Math.abs(a - b) < 0.001;
assert.ok(near(ratio(box), 4 / 3), 'the canvas drives the shape while it is the picture');
assert.ok(near(ratio(box), 4 / 3), 'the canvas drives the shape while it is the picture');
}
}
-// ── The glow is not made for reduced motion; the page wiring ──
+// ── The glow is not made with motion off (Calm and a device that asks for less keep it: a slow colour fade moves
+// nothing); the page wiring ──
{
{
- const { api } = load({ reduce: true });
+ const { api } = load({ motion: 'off' });
const box = element('div');
const box = element('div');
api.watch(element('video'), box, { key: 'x' });
api.watch(element('video'), box, { key: 'x' });
- assert.strictEqual(box.children.length, 0, 'no glow canvas with reduced motion');
+ assert.strictEqual(box.children.length, 0, 'no glow canvas with motion off');
+ const calm = load({ reduce: true, motion: 'reduced' });
+ const calmBox = element('div');
+ calm.api.watch(element('video'), calmBox, { key: 'x' });
+ assert.ok(calmBox.children.length > 0, 'Calm keeps the glow');
const withGlow = element('div');
const withGlow = element('div');
load().api.watch(element('video'), withGlow, { key: 'y' });
load().api.watch(element('video'), withGlow, { key: 'y' });
assert.strictEqual(withGlow.children[0] && withGlow.children[0].className, 'player-shape-glow');
assert.strictEqual(withGlow.children[0] && withGlow.children[0].className, 'player-shape-glow');