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.
Four words on the four long sides of a prism that turns a quarter at a time, pausing on each. After 360° it is back where it started, so the loop has no seam.
Your edited version
n is turned n quarter turns backwards around X, then pushed out with translateZ by half the prism’s depth — the same "turn, then push" recipe as a basic cube, just on one axis.translateZ(calc(-18 * var(--u)))) so the face currently in front sits exactly at z = 0 and stays crisp. --u is the one base unit every length here is a multiple of.cubic-bezier — a snappy, springy turn rather than a constant spin.backface-visibility: hidden stops a word from showing mirrored through the box while it turns.360deg looks exactly like 0deg, so the loop has no seam.√2 × 36 units tall, half of that above the resting face and half below. That corner, not the face, is what the band has to hold.rotateX(n × -90deg) translateZ(h / 2)hold + turn keyframesbackface-visibility: hiddenper-keyframe easing<div class="scene">
<div class="wordcube">
<span>We make</span>
<span class="prism">
<span style="--i:0;--c:#8b6cff">design</span>
<span style="--i:1;--c:#2ee6d6">code</span>
<span style="--i:2;--c:#ff4d9d">motion</span>
<span style="--i:3;--c:#ffb547">brands</span>
</span>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the line is the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.83vmin;
perspective: calc(800 * var(--u));
}
/* The lead-in sits over the prism, not beside it: side by side the line is five times wider
than it is tall, so at the 92vmin width limit it stands 18vmin high, well under the 40vmin
floor. Stacked, it is the shape of the headline it is imitating anyway. */
.wordcube {
display: grid;
justify-items: center;
gap: calc(9 * var(--u));
/* no colour of its own: the lead-in is written on the stage, so it takes the stage's ink */
font-size: calc(21 * var(--u));
font-weight: 800;
white-space: nowrap;
transform-style: preserve-3d;
/* measured by what it paints, the turning prism reaches further below the line than the lead-in
does above it, so the drawing hangs below the layout box; this lift centres it */
transform: translateY(calc(-5 * var(--u)));
}
.prism {
position: relative;
display: inline-block;
width: calc(104 * var(--u));
height: calc(36 * var(--u)); /* face height = prism depth: its cross-section is a square */
transform-style: preserve-3d;
animation: cube-turn 9s infinite;
}
.prism span {
position: absolute;
inset: 0;
display: flex;
align-items: center;
padding-left: calc(13 * var(--u));
border-radius: calc(8 * var(--u));
background: color-mix(in srgb, var(--c) 26%, transparent);
/* the edge is an inner line, not a border: turning, a face passes side-on, where a hairline
border breaks up; the inner glow is the soft shade that survives */
box-shadow:
inset 0 0 0 calc(1 * var(--u)) color-mix(in srgb, var(--c) 80%, transparent),
inset 0 0 calc(18 * var(--u)) color-mix(in srgb, var(--c) 32%, transparent);
/* the face is see-through, so the word is really on the stage: its colour leans towards the
stage's ink, lighter on a dark stage and darker on a light one, so it reads on both */
color: color-mix(in srgb, var(--c) 55%, currentColor);
backface-visibility: hidden;
transform: rotateX(calc(var(--i) * -90deg)) translateZ(calc(18 * var(--u)));
}
@keyframes cube-turn {
0%, 19% {
transform: translateZ(calc(-18 * var(--u))) rotateX(0deg);
animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
}
25%, 44% {
transform: translateZ(calc(-18 * var(--u))) rotateX(90deg);
animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
}
50%, 69% {
transform: translateZ(calc(-18 * var(--u))) rotateX(180deg);
animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
}
75%, 94% {
transform: translateZ(calc(-18 * var(--u))) rotateX(270deg);
animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
}
100% {
transform: translateZ(calc(-18 * var(--u))) rotateX(360deg);
}
}