openvibers/openvibe.shared player.css

player.css

main 17.1 KB · 251 lines · css

1/*
2 * OpenVibe player (player.js): the network's one look for <video>. Colours follow the site's theme (--accent, the
3 * live red); everything else is the player's own, so it looks the same on every OpenVibe site.
4 */
5.ov-player {
6 --ovp-accent: var(--accent, #3b82f6);
7 --ovp-fg: #fff;
8 --ovp-muted: rgba(255, 255, 255, 0.72);
9 --ovp-track: rgba(255, 255, 255, 0.24);
10 --ovp-buffer: rgba(255, 255, 255, 0.38);
11 position: relative;
12 display: block;
13 width: 100%;
14 height: 100%;
15 background: var(--ovp-bg, #000);
16 color: var(--ovp-fg);
17 border-radius: inherit;
18 overflow: hidden;
19 font: 600 13px/1 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
20 -webkit-tap-highlight-color: transparent;
21 outline: none;
22 isolation: isolate;
23}
24.ov-player:focus-visible { box-shadow: 0 0 0 2px var(--ovp-accent) inset; }
25.ov-player .ov-player-video { position: relative; z-index: 1; display: block; width: 100%; height: 100%; object-fit: contain; background: transparent; opacity: 0; transition: opacity 0.35s ease; }
26/* Loading in layers (player.js): the average colour (--ovp-bg), a tiny preview blurred, the poster, then the first frame. */
27.ov-player.has-frame .ov-player-video, .ov-player:not(.has-thumb):not(.has-poster) .ov-player-video { opacity: 1; }
28.ov-player-placeholder, .ov-player-poster { position: absolute; inset: 0; z-index: 0; background: center / contain no-repeat; pointer-events: none; }
29.ov-player-placeholder { background-size: cover; filter: blur(18px) saturate(1.2); transform: scale(1.12); opacity: 0.9; }
30.ov-player-poster { opacity: 0; transition: opacity 0.35s ease; }
31.ov-player.has-poster .ov-player-poster { opacity: 1; }
32.ov-player.has-frame .ov-player-placeholder, .ov-player.has-poster .ov-player-placeholder { opacity: 0; transition: opacity 0.5s ease; }
33/* An audio-only recording (no picture track; plan R4, 2026-11-01): the streamer's face over a soft blur of it, never a
34 black box. The video element stays (it plays the sound) but shows nothing; the controls sit above as always. A sound
35 file has no picture size, so the player keeps the 16:9 a video would have (the browser's default is 150 px tall). */
36.ov-player-audio { position: absolute; inset: 0; z-index: 1; display: none; place-items: center; overflow: hidden; background: var(--ovp-bg, #0b1220); pointer-events: none; }
37.ov-player.is-audio .ov-player-audio { display: grid; }
38.ov-player.is-audio { aspect-ratio: 16 / 9; }
39.ov-player.is-audio .ov-player-video { position: absolute; inset: 0; height: 100%; opacity: 0 !important; }
40.ov-player-audio-bg { position: absolute; inset: -15%; width: 130%; height: 130%; object-fit: cover; filter: blur(28px) saturate(1.15); opacity: .5; }
41.ov-player-audio-face { position: relative; width: clamp(72px, 22%, 168px); aspect-ratio: 1; border-radius: 50%; object-fit: cover; box-shadow: 0 0 0 4px rgb(255 255 255 / .12), 0 12px 32px rgb(0 0 0 / .4); }
42.ov-player-audio-glyph { position: relative; display: grid; place-items: center; width: clamp(64px, 16%, 112px); aspect-ratio: 1; border-radius: 50%; background: rgb(255 255 255 / .1); color: #fff; }
43.ov-player-audio-glyph svg { width: 44%; height: 44%; }
44.ov-player-audio-tag { position: absolute; top: 12px; right: 12px; padding: 4px 10px; border-radius: 999px; background: rgb(0 0 0 / .55); color: #fff; font: 600 12px/1.4 system-ui, -apple-system, 'Segoe UI', sans-serif; letter-spacing: .02em; }
45
46/* Ambient glow (data-ov-ambient): the picture, blurred, behind and around the player. */
47.ov-player-host { position: relative; isolation: isolate; }
48.ov-player-host > .ov-player { z-index: 1; }
49.ov-player-ambient { position: absolute; inset: -4% -3%; z-index: 0; pointer-events: none; opacity: 0; transition: opacity 0.8s ease; transform: translateZ(0); }
50.ov-player-ambient.is-on { opacity: 0.62; }
51:root[data-theme-mode="light"] .ov-player-ambient.is-on { opacity: 0.38; }
52.ov-player-ambient canvas { position: absolute; inset: 0; width: 100%; height: 100%; filter: blur(48px) saturate(1.5); opacity: 0; transition: opacity 1.1s ease; border-radius: 24px; }
53.ov-player-ambient canvas.is-front { opacity: 1; }
54.ov-player.is-idle { cursor: none; }
55
56/* The bar: a soft gradient from the bottom; hidden while playing and idle. */
57.ov-player-bar {
58 position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
59 padding: 44px 10px 6px;
60 background: linear-gradient(to top, rgba(0, 0, 0, 0.88) 0%, rgba(0, 0, 0, 0.62) 42%, rgba(0, 0, 0, 0.22) 75%, transparent 100%);
61 transition: opacity 0.2s ease, transform 0.2s ease;
62 pointer-events: none; /* the gradient is backdrop: only the controls take the pointer */
63}
64.ov-player-bar > * { pointer-events: auto; }
65.ov-player.is-idle .ov-player-bar, .ov-player.is-idle .ov-player-bar > * { opacity: 0; pointer-events: none; }
66.ov-player.is-idle .ov-player-bar { transform: translateY(6px); }
67
68/* Scrubber: buffered, played and a thumb in the theme's accent; the time under the pointer above it. */
69.ov-player-scrub { position: relative; height: 16px; display: flex; align-items: center; cursor: pointer; touch-action: none; outline: none; }
70.ov-player-track { position: relative; width: 100%; height: 4px; border-radius: 2px; background: var(--ovp-track); transition: height 0.12s; }
71.ov-player-scrub:hover .ov-player-track, .ov-player.is-scrubbing .ov-player-track, .ov-player-scrub:focus-visible .ov-player-track { height: 6px; }
72.ov-player-buffered, .ov-player-played { position: absolute; left: 0; top: 0; bottom: 0; border-radius: inherit; width: 0; }
73.ov-player-buffered { background: var(--ovp-buffer); }
74.ov-player-played { background: var(--ovp-accent); }
75.ov-player-played::after {
76 content: ''; position: absolute; right: -6px; top: 50%;
77 width: 12px; height: 12px; border-radius: 50%;
78 background: var(--ovp-accent); border: 2px solid #fff; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
79 transform: translateY(-50%) scale(0); transition: transform 0.12s;
80}
81.ov-player-scrub:hover .ov-player-played::after, .ov-player.is-scrubbing .ov-player-played::after,
82.ov-player-scrub:focus-visible .ov-player-played::after { transform: translateY(-50%) scale(1); }
83.ov-player-scrub:focus-visible .ov-player-track { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); }
84.ov-player-hover {
85 position: absolute; bottom: 18px; transform: translateX(-50%);
86 padding: 4px 7px; border-radius: 6px; background: rgba(0, 0, 0, 0.85);
87 font-size: 12px; font-variant-numeric: tabular-nums; white-space: nowrap;
88 opacity: 0; pointer-events: none; transition: opacity 0.1s;
89}
90.ov-player-scrub:hover .ov-player-hover, .ov-player.is-scrubbing .ov-player-hover { opacity: 1; }
91
92/* The row of controls. */
93.ov-player-row { display: flex; align-items: center; gap: 2px; margin-top: 2px; }
94.ov-player-spacer { flex: 1; }
95.ov-player-btn {
96 display: inline-flex; align-items: center; justify-content: center;
97 min-width: 38px; height: 38px; padding: 0 6px;
98 background: none; border: 0; border-radius: 8px;
99 color: var(--ovp-fg); font: inherit; cursor: pointer;
100 opacity: 0.92; transition: background 0.15s, opacity 0.15s;
101}
102.ov-player-btn:hover { opacity: 1; background: rgba(255, 255, 255, 0.14); }
103.ov-player-btn:active { transform: scale(0.9); }
104.ov-player-btn { transition: background 0.15s, opacity 0.15s, transform 0.08s ease; }
105.ov-player-btn svg { width: 24px; height: 24px; }
106.ov-player-btn svg { filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6)); }
107.ov-player-btn:focus-visible { outline: 2px solid var(--ovp-accent); outline-offset: 1px; opacity: 1; }
108.ov-player-btn[hidden] { display: none; }
109.ov-player-cc.is-on { color: var(--ovp-accent); }
110.ov-player-cc.is-on svg { filter: drop-shadow(0 0 4px color-mix(in srgb, var(--ovp-accent) 60%, transparent)); }
111/* Audio description: the two letters are the icon; on, it carries the same accent as captions. */
112.ov-player-ad { min-width: 40px; font: 700 12.5px/1 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; letter-spacing: 0.02em; }
113.ov-player-ad.is-on { color: var(--ovp-accent); background: color-mix(in srgb, var(--ovp-accent) 18%, transparent); }
114.ov-player-speed { font-size: 12.5px; font-variant-numeric: tabular-nums; min-width: 44px; }
115.ov-player-time { padding: 0 8px; color: rgba(255, 255, 255, 0.86); font-size: 12.5px; font-variant-numeric: tabular-nums; white-space: nowrap; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7); }
116
117/* Volume: the slider opens beside the button on hover or focus. */
118.ov-player-volume { display: flex; align-items: center; }
119.ov-player-volume-slider {
120 -webkit-appearance: none; appearance: none;
121 width: 0; height: 22px; margin: 0; padding: 0; opacity: 0;
122 background: transparent; cursor: pointer;
123 transition: width 0.2s ease, opacity 0.2s ease, margin 0.2s ease;
124 --ovp-fill: 100%;
125}
126.ov-player-volume:hover .ov-player-volume-slider, .ov-player-volume-slider:focus-visible, .ov-player-volume:focus-within .ov-player-volume-slider {
127 width: 76px; opacity: 1; margin: 0 6px 0 2px;
128}
129.ov-player-volume-slider::-webkit-slider-runnable-track {
130 height: 4px; border-radius: 2px;
131 background: linear-gradient(to right, #fff var(--ovp-fill), var(--ovp-track) var(--ovp-fill));
132}
133.ov-player-volume-slider::-moz-range-track { height: 4px; border-radius: 2px; background: var(--ovp-track); }
134.ov-player-volume-slider::-moz-range-progress { height: 4px; border-radius: 2px; background: #fff; }
135.ov-player-volume-slider::-webkit-slider-thumb {
136 -webkit-appearance: none; appearance: none; margin-top: -4px;
137 width: 12px; height: 12px; border-radius: 50%; background: #fff; border: 0;
138}
139.ov-player-volume-slider::-moz-range-thumb { width: 12px; height: 12px; border-radius: 50%; background: #fff; border: 0; }
140
141/* Playing muted (autoplay): one tap brings the sound in. */
142.ov-player-unmute {
143 position: absolute; left: 12px; top: 12px; z-index: 5;
144 display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px 8px 10px;
145 border: 0; border-radius: 999px; background: rgba(0, 0, 0, 0.66); color: #fff;
146 font: 650 13px/1 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; cursor: pointer;
147 -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
148 animation: ov-player-pop 0.3s ease-out both;
149}
150.ov-player-unmute svg { width: 18px; height: 18px; }
151.ov-player-unmute:hover, .ov-player-unmute:focus-visible { background: var(--ovp-accent); outline: none; }
152.ov-player-unmute[hidden] { display: none; }
153@keyframes ov-player-pop { from { opacity: 0; transform: translateY(-4px) scale(0.96); } }
154
155/* A seek or a volume level, shown for a moment (double tap, keys). */
156.ov-player-flash {
157 position: absolute; left: 50%; top: 50%; z-index: 4; transform: translate(-50%, -50%) scale(0.9);
158 display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 14px 16px; min-width: 76px;
159 border-radius: 18px; background: rgba(0, 0, 0, 0.55); color: #fff; font-size: 13px; font-variant-numeric: tabular-nums;
160 opacity: 0; pointer-events: none; transition: opacity 0.18s ease, transform 0.18s ease;
161}
162.ov-player-flash.is-left { left: 22%; }
163.ov-player-flash.is-right { left: 78%; }
164.ov-player-flash.is-on { opacity: 1; transform: translate(-50%, -50%) scale(1); }
165.ov-player-flash svg { width: 30px; height: 30px; }
166
167/* Paused before the first play, or ended: one large button in the middle. */
168.ov-player-big {
169 position: absolute; left: 50%; top: 50%; z-index: 4; /* above the bar on small players */
170 width: 68px; height: 68px; border-radius: 50%;
171 transform: translate(-50%, -50%);
172 background: color-mix(in srgb, var(--ovp-accent) 88%, transparent);
173 box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45), 0 0 0 6px color-mix(in srgb, var(--ovp-accent) 22%, transparent);
174 opacity: 1;
175 transition: transform 0.15s ease, background 0.15s ease, opacity 0.2s ease;
176}
177.ov-player-big:active { transform: translate(-50%, -50%) scale(0.94); }
178.ov-player-big svg { width: 32px; height: 32px; }
179.ov-player-big:hover { background: var(--ovp-accent); transform: translate(-50%, -50%) scale(1.06); }
180.ov-player:not(.is-start):not(.is-ended) .ov-player-big { opacity: 0; pointer-events: none; }
181
182/* Buffering: three dots hopping in turn over the picture (nothing spins). */
183.ov-player-spinner { position: relative; display: inline-block; width: 8px; height: 8px; margin: 0 14px; border: 0; border-radius: 50%; background: #fff; animation: ov-player-hop 1s ease-in-out .15s infinite; }
184.ov-player-spinner::before, .ov-player-spinner::after { content: ''; position: absolute; top: 0; width: 8px; height: 8px; border-radius: 50%; background: inherit; animation: ov-player-hop 1s ease-in-out infinite; }
185.ov-player-spinner::before { left: -14px; } .ov-player-spinner::after { left: 14px; animation-delay: .3s; }
186@keyframes ov-player-hop { 0%, 80%, 100% { transform: translateY(0); opacity: .45; } 40% { transform: translateY(-5px); opacity: 1; } }
187.ov-player-spinner { position: absolute; left: 50%; top: 50%; z-index: 2; margin: -4px 0 0 -4px; display: none; filter: drop-shadow(0 1px 6px rgba(0, 0, 0, .5)); }
188.ov-player.is-waiting:not(.is-paused) .ov-player-spinner { display: block; }
189html[data-ov-motion=off] .ov-player-spinner,html[data-ov-motion=off] .ov-player-spinner::before,html[data-ov-motion=off] .ov-player-spinner::after{opacity: .8}
190/* Touch screens: the slider shows, finger-sized, wherever the page can set the volume (.can-volume: Android). An iPhone
191 leaves volume to its own buttons, so there the mute button stays alone. */
192@media (hover: none) {
193 .ov-player-volume-slider { display: none; }
194 .ov-player.can-volume .ov-player-volume-slider { display: block; width: 84px; height: 36px; opacity: 1; margin: 0 8px 0 0; }
195 .ov-player.can-volume .ov-player-volume-slider::-webkit-slider-thumb { width: 18px; height: 18px; margin-top: -7px; }
196 .ov-player.can-volume .ov-player-volume-slider::-moz-range-thumb { width: 18px; height: 18px; }
197}
198@media (hover: none) and (max-width: 360px) { .ov-player.can-volume .ov-player-volume-slider { width: 64px; } }
199/* No sound track: no slider, a dimmed speaker that says so when tapped. */
200.ov-player.is-silent .ov-player-volume-slider { display: none; }
201.ov-player.is-silent .ov-player-mute { opacity: 0.55; }
202
203.ov-player-message:empty { display: none; }
204.ov-player-message {
205 position: absolute; left: 50%; top: 50%; z-index: 2; transform: translate(-50%, -50%);
206 padding: 10px 14px; border-radius: 10px; background: rgba(0, 0, 0, 0.8); font-weight: 500;
207}
208
209/* Captions sit above the bar while it shows. */
210.ov-player .ov-player-video::cue { background: rgba(0, 0, 0, 0.72); color: #fff; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
211
212/* An audio description cue: shown over the picture, in its own band above the controls, while it is spoken. */
213.ov-player-ad-line {
214 position: absolute; left: 50%; bottom: 70px; z-index: 3; transform: translateX(-50%);
215 max-width: min(86%, 720px); padding: 6px 12px; border-radius: 8px;
216 background: rgba(0, 0, 0, 0.72); color: #fff; text-align: center;
217 font: 500 13px/1.35 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
218 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7); pointer-events: none;
219 opacity: 0; transition: opacity 0.15s ease;
220}
221.ov-player-ad-line:not(:empty) { opacity: 1; }
222@media (max-width: 520px) { .ov-player-ad-line { bottom: 58px; font-size: 12px; } }
223
224/* Small players and phones: fewer, larger targets. */
225@media (max-width: 520px) {
226 .ov-player-speed, .ov-player-pip { display: none; }
227 .ov-player-btn { min-width: 40px; height: 40px; }
228 .ov-player-unmute { left: 8px; top: 8px; padding: 7px 12px 7px 9px; }
229 .ov-player-time { padding: 0 4px; font-size: 12px; }
230 .ov-player-big { width: 60px; height: 60px; }
231}
232
233/* One control bar across OpenVibe (11-09, with Live's: the owner asked for buttons "better sized and centred"): 40 px
234 square buttons (44 on a touch screen) with the icon centred at 22 px; readouts as glass chips. */
235.ov-player-btn { width: 40px; min-width: 40px; height: 40px; padding: 0; border-radius: 12px; display: inline-grid; place-items: center; }
236.ov-player-btn svg { width: 22px; height: 22px; display: block; }
237@media (pointer: coarse) { .ov-player-btn { width: 44px; min-width: 44px; height: 44px; } }
238.ov-player-time { height: 28px; display: inline-flex; align-items: center; padding: 0 10px; border-radius: 999px; background: rgba(255, 255, 255, .12); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); font-size: 12.5px; font-weight: 600; font-variant-numeric: tabular-nums; }
239
240/* Phones (ask 288): one row that never runs off the screen. Last in the file, because the bar's sizing block above
241 would otherwise win over the phone rules: speed and picture-in-picture step out, the volume slider is shorter, under
242 380 px the mute button stands alone (a phone's own buttons set the volume) and under 360 px the time shows the
243 position alone (the scrubber has the length). Measured at 390 px: play, mute, slider, time, captions, fullscreen fit.
244@media (max-width: 520px) {
245 .ov-player-speed, .ov-player-pip { display: none; }
246 .ov-player-time { padding: 0 8px; font-size: 12px; }
247 .ov-player.can-volume .ov-player-volume-slider { width: 64px; margin-right: 2px; }
248}
249@media (max-width: 380px) { .ov-player.can-volume .ov-player-volume-slider { display: none; } }
250@media (max-width: 360px) { .ov-player-dur { display: none; } }
251