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.
Every letter sits on its own cube. The cubes roll forward a quarter turn one after another, so CUBE becomes ROLL and back; after four turns each cube is where it started, so the loop has no seam.
Your edited version
rotateX(n × -90deg) translateZ(s / 2): front, bottom, back and top carry the letters of CUBE, ROLL, CUBE, ROLL.animation-delay: calc(var(--i) * 0.13s) starts each cube a moment after the one before it, and the turn runs along the word like a wave.--u, tied to the canvas, so the word is the same share of a gallery card, the editor and a recording canvas.backface-visibility: hidden keeps letters from showing mirrored through the cube, and a square plate behind each rounded face hides the gaps at the corners.face n: rotateX(n × -90deg) translateZ(s / 2)hold + turn keyframes to 360deganimation-delay from --ibackface-visibility: hidden<div class="scene">
<div class="word" role="img" aria-label="CUBE, rolling over to ROLL">
<span class="cube" style="--i:0" aria-hidden="true"><i>C</i><i>R</i><i>C</i><i>R</i><b></b><b></b></span>
<span class="cube" style="--i:1" aria-hidden="true"><i>U</i><i>O</i><i>U</i><i>O</i><b></b><b></b></span>
<span class="cube" style="--i:2" aria-hidden="true"><i>B</i><i>L</i><i>B</i><i>L</i><b></b><b></b></span>
<span class="cube" style="--i:3" aria-hidden="true"><i>E</i><i>L</i><i>E</i><i>L</i><b></b><b></b></span>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the word is the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.5vmin;
perspective: calc(800 * var(--u));
}
/* two by two, CU over BE: four cubes in a row draw four and a half times wider than they are
tall, so at the 92vmin width limit the word stands well under the 40vmin floor */
.word {
display: grid;
grid-template-columns: repeat(2, auto);
gap: calc(18 * var(--u)) calc(9 * var(--u)); /* more between the rows: seen from above, a row shows its top */
transform-style: preserve-3d;
transform: rotateX(-16deg) rotateY(-18deg); /* a fixed view from above and to the right */
}
.cube {
--s: calc(44 * var(--u));
position: relative;
width: var(--s);
height: var(--s);
transform-style: preserve-3d;
/* --i staggers the cubes so the turn runs along the word */
animation: roll 8s calc(var(--i) * 0.13s) infinite both;
}
.cube i,
.cube b {
position: absolute;
inset: 0;
border-radius: calc(8 * var(--u));
transform-style: preserve-3d;
}
/* letter faces: first word violet, second word pink */
.cube i {
--c: #8b6cff;
display: grid;
place-items: center;
/* an inner line plus a soft shade instead of a hard border: squeezed side-on, a 1-unit line
breaks up, the shade survives */
box-shadow:
inset 0 0 0 calc(1 * var(--u)) color-mix(in srgb, var(--c), #fff 35%),
inset 0 0 calc(8 * var(--u)) color-mix(in srgb, color-mix(in srgb, var(--c), #fff 35%) 35%, transparent);
background:
linear-gradient(150deg, rgb(255 255 255 / 0.3), transparent 45%),
linear-gradient(color-mix(in srgb, var(--c), #fff 8%), color-mix(in srgb, var(--c), #000 22%));
color: #fff;
font: 900 calc(26 * var(--u))/1 Inter, system-ui, sans-serif;
font-style: normal;
text-shadow: 0 calc(2 * var(--u)) 0 color-mix(in srgb, var(--c), #000 45%);
backface-visibility: hidden; /* never a mirrored letter */
}
.cube i:nth-child(even) {
--c: #ff4d9d;
}
/* face n: n quarter turns back, then out by half a side */
.cube i:nth-child(1) { transform: rotateX(0deg) translateZ(calc(var(--s) / 2)); }
.cube i:nth-child(2) { transform: rotateX(-90deg) translateZ(calc(var(--s) / 2)); }
.cube i:nth-child(3) { transform: rotateX(-180deg) translateZ(calc(var(--s) / 2)); }
.cube i:nth-child(4) { transform: rotateX(-270deg) translateZ(calc(var(--s) / 2)); }
/* the two ends */
.cube b {
box-shadow:
inset 0 0 0 calc(1 * var(--u)) color-mix(in srgb, #8b6cff, #fff 10%),
inset 0 0 calc(8 * var(--u)) color-mix(in srgb, color-mix(in srgb, #8b6cff, #fff 10%) 35%, transparent);
background: linear-gradient(color-mix(in srgb, #8b6cff, #000 30%), color-mix(in srgb, #8b6cff, #000 50%));
}
.cube b:nth-of-type(1) { transform: rotateY(-90deg) translateZ(calc(var(--s) / 2)); }
.cube b:nth-of-type(2) { transform: rotateY(90deg) translateZ(calc(var(--s) / 2)); }
/* rounded faces leave holes at the corners: a square plate behind each one plugs them */
.cube i::before,
.cube b::before {
content: '';
position: absolute;
/* 1 unit short of the edge: full size, it would touch the next face and show there as a dotted
seam; much smaller leaves a channel along each edge you can see into */
inset: calc(1 * var(--u));
background: color-mix(in srgb, #8b6cff, #000 50%);
transform: translateZ(calc(-5 * var(--u)));
}
.cube i:nth-child(even)::before {
background: color-mix(in srgb, #ff4d9d, #000 50%);
}
/* hold, quarter turn, hold... 360deg = 0deg, so the loop is seamless */
@keyframes roll {
0%, 17% {
transform: rotateX(0deg);
animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
}
25%, 42% {
transform: rotateX(90deg);
animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
}
50%, 67% {
transform: rotateX(180deg);
animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
}
75%, 92% {
transform: rotateX(270deg);
animation-timing-function: cubic-bezier(0.6, -0.3, 0.3, 1.3);
}
100% {
transform: rotateX(360deg);
}
}