/* The CRT's light spills onto the page behind the character. */
.character-stage{--head-x:50%;--head-y:36%;--screen-tint:#a4b3ff;--screen-glow:0}
.character-stage::before{content:'';position:absolute;left:0;top:0;width:min(82%,620px);aspect-ratio:1;translate:calc(var(--head-x) - 50%) calc(var(--head-y) - 50%);will-change:translate,opacity;border-radius:50%;background:radial-gradient(closest-side,color-mix(in srgb,var(--screen-tint) 46%,transparent),color-mix(in srgb,var(--screen-tint) 14%,transparent) 52%,transparent);opacity:calc(var(--screen-glow) * .5);pointer-events:none}
.character-stage canvas{position:relative;z-index:1}
.journey-character-dock .character-stage::before,.music-visual .character-stage::before{opacity:calc(var(--screen-glow) * .8)}
.character-stage.is-paused::before{opacity:.4}
@media(prefers-reduced-motion:reduce){.character-stage::before{transition:none}}
/* Without WebGL (or JavaScript) the stage shows a still render of the character instead of a placeholder card. */
.character-stage .scene-fallback{display:none;inset:0;border-radius:0;background:url('/assets/character-still.webp') 50% 0/auto 100% no-repeat}
.character-stage .scene-fallback>*{display:none}
.character-stage.is-unavailable .scene-fallback{display:block}
/* Zooming into the screen: the stage covers the window and the camera flies into the glass. */
.character-stage.is-zooming{position:fixed;inset:0;z-index:5;overflow:visible;pointer-events:none;cursor:auto;-webkit-mask-image:none;mask-image:none}
/* During the zoom the stage covers the window over the plain paper hero, so the torso's soft fade is a paper-coloured
   overlay moved by a transform (a per-frame mask would re-rasterise the whole window on every scroll step). */
.character-stage.is-zooming::after{content:'';position:absolute;left:0;right:0;top:0;height:200vh;z-index:2;pointer-events:none;background:linear-gradient(to bottom,transparent 0,var(--paper) 5%);transform-origin:0 0;transform:translateY(var(--fade-a,86vh)) scaleY(var(--fade-k,1));will-change:transform}
.character-stage.is-unavailable.is-zooming::after{display:none}
/* Touch screens: the zoom stage matches the broadcast's full-screen height, so browser bars never resize it. */
@media(pointer:coarse){.character-stage.is-zooming{bottom:auto;height:100lvh}}
.character-stage.is-zooming::before{opacity:calc(var(--screen-glow) * .5 * (1 - var(--zoom,0)))}
/* Without WebGL the zoom plays back as frames rendered from the real scene. */
.still-zoom{display:none;position:absolute;inset:0;overflow:hidden;pointer-events:none}
.character-stage .still-zoom canvas{position:absolute;left:0;top:0;opacity:1;transition:none;z-index:auto}
.character-stage.is-unavailable.is-zooming{-webkit-mask-image:none;mask-image:none}
.character-stage.is-unavailable.is-zooming .scene-fallback{display:none}
.character-stage.is-unavailable.is-zooming .still-zoom{display:block}
/* Without WebGL: posed renders of the rig, with the live screen drawn into each render's glass, in one canvas. */
.sprite-character{position:absolute;pointer-events:none}
.character-stage .sprite-character canvas{position:absolute;left:0;top:0;width:100%;height:100%;opacity:1;transition:none;z-index:auto}
.character-stage.has-sprite .scene-fallback{display:none}
.character-stage.is-zooming .sprite-character{-webkit-mask-image:linear-gradient(to bottom,#000 86%,transparent);mask-image:linear-gradient(to bottom,#000 86%,transparent)}

/* Low-power (no WebGL, CPU compositing): keep the look, drop what forces a repaint of large areas each frame. */
.low-power .channel-controls{-webkit-backdrop-filter:none;backdrop-filter:none;background:#e9e9eef2}
.low-power .journey-character-dock .channel-controls,.low-power .music-visual .channel-controls{background:#181e30f5}
.low-power .character-stage.is-zooming::before{display:none}
