Two frontends sharing one WebSocket-backed state on the server: - Player: fullscreen poster background with a small frame (30% of the screen) that drifts diagonally and reflects off the edges. Two stacked YouTube players hand over with a 5s equal-power crossfade so there is never silence between tracks. - Queue: add by URL, drag to reorder, delete, click to jump. Shows title and length per item, highlights what is playing, and tracks remaining vs. total playtime. No API key is required — titles come from YouTube's oEmbed endpoint and durations from a hidden muted probe player that cues each new video just to read its length. The fade is driven by both requestAnimationFrame and the interval tick: a hidden or throttled tab stops serving frames entirely, which would otherwise strand a handover half-faded with two tracks audible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tg15tLAuT6NrCE1ZxKVgDB
157 lines
2.7 KiB
CSS
157 lines
2.7 KiB
CSS
:root {
|
|
--accent: #ff4fc3;
|
|
--ink: #14060f;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
margin: 0;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
background: var(--ink);
|
|
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
cursor: none;
|
|
}
|
|
|
|
/* --- background ------------------------------------------------------- */
|
|
|
|
#backgrounds {
|
|
position: fixed;
|
|
inset: 0;
|
|
}
|
|
|
|
#backgrounds img {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
opacity: 0;
|
|
transition: opacity 900ms ease;
|
|
/* Knock the poster back so the video frame stays the focal point — the
|
|
posters are pale and read far brighter on a projector than on a desk. */
|
|
filter: brightness(0.45) saturate(1.15);
|
|
}
|
|
|
|
#backgrounds img.active {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* --- the moving frame -------------------------------------------------- */
|
|
|
|
#frameMover {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
will-change: transform;
|
|
}
|
|
|
|
#frameSkin {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
border-radius: 10px;
|
|
border: 2px solid rgba(255, 255, 255, 0.85);
|
|
box-shadow:
|
|
0 0 0 4px rgba(255, 79, 195, 0.55),
|
|
0 24px 70px rgba(0, 0, 0, 0.7);
|
|
background: #000;
|
|
}
|
|
|
|
.deck {
|
|
position: absolute;
|
|
inset: 0;
|
|
opacity: 0;
|
|
/* Opacity is driven frame-by-frame during a crossfade, so no CSS transition
|
|
here — it would fight the JS ramp. */
|
|
}
|
|
|
|
.deck iframe {
|
|
width: 100%;
|
|
height: 100%;
|
|
border: 0;
|
|
/* The iframe would otherwise swallow nothing useful, but keeping it inert
|
|
avoids stray clicks pausing playback. */
|
|
pointer-events: none;
|
|
}
|
|
|
|
#probe {
|
|
position: fixed;
|
|
left: -400px;
|
|
top: -400px;
|
|
width: 320px;
|
|
height: 180px;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* --- start gate -------------------------------------------------------- */
|
|
|
|
#gate {
|
|
position: fixed;
|
|
inset: 0;
|
|
display: grid;
|
|
place-items: center;
|
|
background: rgba(20, 6, 15, 0.92);
|
|
backdrop-filter: blur(6px);
|
|
z-index: 10;
|
|
cursor: pointer;
|
|
transition: opacity 400ms ease;
|
|
}
|
|
|
|
#gate.hidden {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.gate-inner {
|
|
text-align: center;
|
|
color: #fff;
|
|
}
|
|
|
|
.gate-inner h1 {
|
|
font-size: clamp(2rem, 7vw, 5rem);
|
|
margin: 0 0 0.3em;
|
|
letter-spacing: -0.03em;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.gate-inner p {
|
|
margin: 0 0 2em;
|
|
opacity: 0.7;
|
|
font-size: 1.05rem;
|
|
}
|
|
|
|
#start {
|
|
font: inherit;
|
|
font-size: 1.3rem;
|
|
padding: 0.7em 3em;
|
|
border: 0;
|
|
border-radius: 999px;
|
|
background: var(--accent);
|
|
color: var(--ink);
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
}
|
|
|
|
#start:disabled {
|
|
opacity: 0.35;
|
|
cursor: default;
|
|
}
|
|
|
|
#status {
|
|
position: fixed;
|
|
bottom: 12px;
|
|
right: 16px;
|
|
z-index: 20;
|
|
color: var(--accent);
|
|
font-size: 0.8rem;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|