: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; } /* Built and removed by the wash animation. Sits between the poster and the stills, and blends rather than covers, so the poster stays legible through whatever colour is passing over it. The gradient itself is written per frame from JS. */ #wash { position: fixed; inset: 0; z-index: 0; pointer-events: none; /* Takes the poster's luminance and only replaces its hue, so the wash reads as a colour change rather than as a sheet of colour laid over the top. */ mix-blend-mode: color; } /* --- frozen screenshots ------------------------------------------------ */ #stills { position: fixed; inset: 0; pointer-events: none; z-index: 1; } #stills img { position: absolute; top: 0; left: 0; object-fit: cover; opacity: 0; /* Matches #frameSkin, so a still reads as a frozen copy of the frame. */ 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); transition: opacity 500ms ease; will-change: transform; } #stills img.visible { opacity: 1; } /* The trail ramps opacity frame-by-frame as a still ages, so the CSS transition above would only fight it. */ #stills img.trail { transition: none; } /* --- the moving frame -------------------------------------------------- */ #frameMover { position: fixed; top: 0; left: 0; /* Above the stills, so the live video always wins if anything ever overlaps. */ z-index: 2; 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; } /* Lift the frame while the drag animation is holding it, the way a dragged window separates from the desktop. */ #frameMover.dragging #frameSkin { box-shadow: 0 0 0 4px rgba(255, 79, 195, 0.75), 0 40px 100px rgba(0, 0, 0, 0.85); } /* The wash animation hands the frame the colour currently passing behind it. */ #frameMover.washing #frameSkin { box-shadow: 0 0 0 4px var(--wash, rgba(255, 79, 195, 0.55)), 0 24px 70px rgba(0, 0, 0, 0.7); } /* --- the drawn pointer -------------------------------------------------- */ /* Built and removed by the drag animation; nothing else on the page has one. */ #pointer { position: fixed; top: 0; left: 0; z-index: 3; pointer-events: none; will-change: transform; filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.6)); } #pointer svg { display: block; transition: transform 90ms ease; } /* The click itself: the arrow dips, so a grab is visible even mid-pause. */ #pointer.down svg { transform: scale(0.82); } .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; } /* The screen-capture stream's sink. Has to be in the document to keep getting frames (see capture.js), but must not be visible or catch clicks. */ #captureSink { position: fixed; left: 0; top: 0; width: 1px; height: 1px; opacity: 0.01; pointer-events: none; z-index: -1; } #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; }