Split-flap board
CSS + JSA departure board: each character is four half-height leaves, and a change drops the old top half and lands the new bottom half. JS only swaps the letters and restarts the CSS animation.
A red and a cyan copy of one word, blended with screen. The pointer sets how far apart they sit and turns the card, like old red/cyan cinema glasses.
Your edited version
translate() and blended with mix-blend-mode: screen — like old red/cyan 3D glasses, where the overlap reads near-white and the fringes stay coloured.rotateY / rotateX); it is deliberately not preserve-3d, because a blend mode only combines elements painted into the same flat surface.--sx, --sy, --ry, --rx); every animated value in the CSS just reads one of them. The shifts are plain numbers, not lengths: CSS multiplies them by --u, the one base unit every length here is a multiple of, so the card is the same share of a gallery card, the editor and a recording canvas.ease-out (.is-live); once it leaves, the slower springy transition takes over for the way back to rest.mix-blend-mode: screen on the leavespointer → --sx / --ry / --rxflat card, no preserve-3d around a blendfast transition live, springy on release<div class="scene" tabindex="0" role="img" aria-label="STEREO in red and cyan layers that shift apart with the pointer">
<div class="anaglyph">
<div class="word">
<span>STEREO</span>
<span aria-hidden="true">STEREO</span>
</div>
<small>move your pointer</small>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the card is the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.41vmin;
perspective: calc(800 * var(--u));
}
.anaglyph {
box-sizing: content-box; /* its padding and edge are round the words */
display: grid;
place-items: center;
gap: calc(2 * var(--u));
width: calc(214 * var(--u));
padding: calc(24 * var(--u)) 0 calc(16 * var(--u));
border: calc(1 * var(--u)) solid #3a4070;
border-radius: calc(16 * var(--u));
background: radial-gradient(circle at 50% 30%, #171b38, #07080f 75%);
box-shadow: 0 calc(22 * var(--u)) calc(34 * var(--u)) calc(-22 * var(--u)) rgb(0 0 0 / 0.85);
/* the card turns as ONE flat plane in the stage's perspective; not preserve-3d, because the
screen blend below only works among elements painted into the same flat surface */
transform: rotateY(var(--ry, -14deg)) rotateX(var(--rx, 6deg));
transition: transform 0.7s cubic-bezier(0.3, 1.3, 0.5, 1);
}
.word {
position: relative;
font-size: calc(43 * var(--u));
font-weight: 900;
line-height: 1;
letter-spacing: 0.02em;
user-select: none;
}
/* two copies of the word, one per eye, pushed apart horizontally; screen adds their light,
so where red and cyan overlap you get near-white and the fringes stay coloured */
.word span {
display: block;
color: #ff1744;
mix-blend-mode: screen;
/* --sx / --sy are plain numbers from JS; the unit is the model's, like every other length */
transform: translate(calc(var(--sx, 3) * -1 * var(--u)), calc(var(--sy, 0) * -1 * var(--u)));
transition: transform 0.7s cubic-bezier(0.3, 1.3, 0.5, 1);
}
.word span + span {
position: absolute;
inset: 0;
color: #00e5ff;
transform: translate(calc(var(--sx, 3) * var(--u)), calc(var(--sy, 0) * var(--u)));
}
.anaglyph small {
color: #949bc0;
font-size: calc(9.5 * var(--u));
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
}
/* while the pointer is over the stage, follow it almost directly; the slow springy transition
above is only used on the way back */
.anaglyph.is-live,
.anaglyph.is-live span {
transition-duration: 0.12s;
transition-timing-function: ease-out;
}
/* the keyboard: Tab to it and it leans as it would for a pointer near the top right, even with
the pointer resting on the canvas (!important beats the pose the script writes inline) */
.scene:focus-visible .anaglyph { --rx: -4deg !important; --ry: 12deg !important; --sx: -4 !important; --sy: 1 !important; }
var stage = document.querySelector('.scene');
var card = document.querySelector('.anaglyph');
function clamp(v, min, max) {
return Math.min(max, Math.max(min, v));
}
function move(e) {
var r = stage.getBoundingClientRect();
var x = clamp((e.clientX - r.left) / r.width - 0.5, -0.5, 0.5);
var y = clamp((e.clientY - r.top) / r.height - 0.5, -0.5, 0.5);
// plain numbers for the shift: CSS multiplies them by --u, so it scales with the canvas
card.style.setProperty('--sx', (x * 16).toFixed(2));
card.style.setProperty('--sy', (y * 5).toFixed(2));
card.style.setProperty('--ry', (x * 44).toFixed(1) + 'deg');
card.style.setProperty('--rx', (-y * 30).toFixed(1) + 'deg');
card.classList.add('is-live');
}
function leave() {
card.classList.remove('is-live');
['--sx', '--sy', '--ry', '--rx'].forEach(function (p) {
card.style.removeProperty(p);
});
}
stage.addEventListener('pointermove', move);
stage.addEventListener('pointerdown', move);
stage.addEventListener('pointerleave', leave);
stage.addEventListener('pointercancel', leave);