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.
The extrusion is a stack of text-shadows whose direction follows your pointer.
Your edited version
calc(var(--dx) * N * var(--u)).--u, the one base unit every length here is a multiple of.--dx / --dy, and the shadow swings away from it. It writes plain numbers, never lengths, so the CSS stays in charge of the scale.wide in its gallery entry.text-shadow with calc(var() × n)works without JS: default directionpointer → custom properties<div class="scene" tabindex="0" role="img" aria-label="LIGHT SHADOW, a headline whose long shadow falls away from the pointer">
<h1 class="lit">LIGHT SHADOW</h1>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the headline is the same share of
a card, the editor, a full screen and a recording canvas */
--u: 0.145vmin;
display: grid;
place-items: center;
width: 100%; /* the whole canvas is the light's field, so the pointer is read across it */
height: 100%;
}
/* One line, as a headline is: LIGHT SHADOW draws many times wider than it is tall, so it is sized
by its width (it is marked 'wide'): the unit puts it near the width the gallery's other wide
models have */
.lit {
--dx: 0.7; /* direction — overwritten from JS */
--dy: 0.7;
margin: 0;
font: 900 calc(72 * var(--u))/1.2 Inter, system-ui;
text-align: center;
white-space: nowrap;
transform: translateY(calc(-4 * var(--u)));
color: #fff;
/* the long shadow in a deep amber, so the white face stands 3:1 or more off it (large text) where
the shadow shows at its edges; from a bright #e6a340 it was 2.2:1 */
text-shadow:
calc(var(--dx) * 1 * var(--u)) calc(var(--dy) * 1 * var(--u)) 0 #b07424,
calc(var(--dx) * 2 * var(--u)) calc(var(--dy) * 2 * var(--u)) 0 #a56d22,
calc(var(--dx) * 3 * var(--u)) calc(var(--dy) * 3 * var(--u)) 0 #9a6620,
calc(var(--dx) * 4 * var(--u)) calc(var(--dy) * 4 * var(--u)) 0 #905f1d,
calc(var(--dx) * 5 * var(--u)) calc(var(--dy) * 5 * var(--u)) 0 #85581b,
calc(var(--dx) * 6 * var(--u)) calc(var(--dy) * 6 * var(--u)) 0 #7a5019,
calc(var(--dx) * 7 * var(--u)) calc(var(--dy) * 7 * var(--u)) 0 #6f4917,
calc(var(--dx) * 8 * var(--u)) calc(var(--dy) * 8 * var(--u)) 0 #654214,
calc(var(--dx) * 9 * var(--u)) calc(var(--dy) * 9 * var(--u)) 0 #5a3b12,
calc(var(--dx) * 10 * var(--u)) calc(var(--dy) * 10 * var(--u)) 0 #4f3410,
calc(var(--dx) * 22 * var(--u)) calc(var(--dy) * 22 * var(--u)) calc(20 * var(--u)) rgb(0 0 0 / 0.5);
}
/* the keyboard: Tab to it and the shadow falls as it would for a pointer near the top right, even
with the pointer resting on the canvas (!important beats the direction the script writes inline) */
.scene:focus-visible .lit { --dx: -0.8 !important; --dy: 0.4 !important; }
const scene = document.querySelector('.scene');
const text = document.querySelector('.lit');
scene.addEventListener('pointermove', (e) => {
const r = scene.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width - 0.5;
const y = (e.clientY - r.top) / r.height - 0.5;
// the pointer is the light, so the shadow falls the other way
text.style.setProperty('--dx', -x * 2);
text.style.setProperty('--dy', -y * 2);
});