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 word wrapped around a ring, one character per slice, standing on a rim you can see all the way round.
Your edited version
<span> per character, all stacked in the same spot.rotateY(i × 360° / count) translateZ(radius) — the carousel formula again.backface-visibility: hidden hides the far side, where letters would otherwise show mirrored.2πr ≈ count × character width.--u: the radius is 52 of them, a character cell 36 × 64, so the ring is the same share of a gallery card, the editor and a recording canvas.CSS 3D •, are big enough to read on a gallery card; the thirteen and the twenty-four it had before were not.rotateX(-32deg) you look down on the ring, so the letter in front drops well below the ones at the sides and the word curves round instead of running across.::after circle of the same radius, laid flat with rotateX(90deg). The letters only show their near half, but the rim shows all of it, so the back of the ring is still there to see. It is drawn nearly as solid as the letters: it is what holds the ring’s full height in every pose, so even a paused ring, with only three letters in front, stands as tall as the whole ring.translateY lifts the ring by part of that, which puts it back in the middle of the box. The spin starts at -40deg, so a paused ring faces you with CSS.rotateY(i × 360° / n) translateZ(r)backface-visibility: hiddenmonospace for even spacinga rotateX(90deg) rim for the far side<div class="scene">
<div class="ring" style="--n:9" aria-label="CSS 3D •">
<span style="--i:0" aria-hidden="true">C</span>
<span style="--i:1" aria-hidden="true">S</span>
<span style="--i:2" aria-hidden="true">S</span>
<span style="--i:3" aria-hidden="true"> </span>
<span style="--i:4" aria-hidden="true">3</span>
<span style="--i:5" aria-hidden="true">D</span>
<span style="--i:6" aria-hidden="true"> </span>
<span style="--i:7" aria-hidden="true">•</span>
<span style="--i:8" aria-hidden="true"> </span>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the ring is the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.82vmin;
perspective: calc(800 * var(--u));
}
/* 52 units of radius against a 36-unit character cell: 2πr is a little more than the nine
characters need, so the band of text closes without the letters touching */
.ring {
--r: calc(52 * var(--u));
position: relative;
width: calc(36 * var(--u));
height: calc(64 * var(--u));
font: 800 calc(52 * var(--u))/calc(64 * var(--u)) 'JetBrains Mono', ui-monospace, monospace;
transform-style: preserve-3d;
animation: ring-spin 12s linear infinite;
}
.ring span {
position: absolute;
inset: 0;
text-align: center;
/* a teal of middle lightness, 3:1 or more on both stages (large text): #2ee6d6 was 1.4:1 on the
light one */
color: oklch(62% 0.13 185);
backface-visibility: hidden;
transform: rotateY(calc(var(--i) * 360deg / var(--n))) translateZ(var(--r));
}
/* the rim the letters stand on: a flat circle of the same radius, laid down with
rotateX(90deg) and centred on their feet. Unlike the letters it shows all the way round, so the
far half of the ring is still there to see */
.ring::after {
content: '';
position: absolute;
top: calc(100% - 52 * var(--u) - 14 * var(--u));
left: calc(50% - var(--r));
width: calc(2 * var(--r));
height: calc(2 * var(--r));
box-sizing: border-box;
border: calc(1.5 * var(--u)) solid color-mix(in srgb, #2ee6d6 80%, transparent);
border-radius: 50%;
transform: rotateX(90deg);
}
/* The tilt is what lets you see the rim as a ring rather than a line, and it drops the letter in
front below the ones at the sides, so the word reads as going round. At 32deg the rim stands
over 40vmin tall, so the ring is tall enough at any turn, paused included. The lift puts what is
drawn back in the middle; starting at -40deg puts CSS, not the bullet, in front of a paused
ring. */
@keyframes ring-spin {
from { transform: translateY(calc(-14 * var(--u))) rotateX(-32deg) rotateY(-40deg); }
to { transform: translateY(calc(-14 * var(--u))) rotateX(-32deg) rotateY(-400deg); }
}