Phone mockup
Pure CSSA phone with real thickness: screen, back, four thin walls and a stack of rounded slabs that fills the corners. It turns to show both sides.
Twenty flat strips form the cylinder. Every strip shows the same wide label, shifted by its own index, so the artwork wraps around without a seam.
Your edited version
--u, tied to the canvas, so the can is the same share of a gallery card, the editor and a recording canvas. The numbers below are those units.rotateY(i × 18°) translateZ(r), with r = (width / 2) / tan(180° / 20) ≈ 34.7 units for 11-unit strips.background-size = one lap, 220 units) and shifts it left by its own index: background-position: i × −11 units. Neighbours line up, so the artwork wraps without a seam.animation-delay of i × −(8s / 20) starts strip i already at its place on the way round.rotateX(±90deg). The white streak is outside the spinning element, so it stays where the lamp is.rotateY(i × 18°) translateZ(radius)background-position: i × −strip widthper-strip shading via animation-delaydiscs laid flat with rotateX(90deg)<div class="scene">
<div class="can">
<div class="spin">
<i style="--i:0"></i><i style="--i:1"></i><i style="--i:2"></i><i style="--i:3"></i><i style="--i:4"></i>
<i style="--i:5"></i><i style="--i:6"></i><i style="--i:7"></i><i style="--i:8"></i><i style="--i:9"></i>
<i style="--i:10"></i><i style="--i:11"></i><i style="--i:12"></i><i style="--i:13"></i><i style="--i:14"></i>
<i style="--i:15"></i><i style="--i:16"></i><i style="--i:17"></i><i style="--i:18"></i><i style="--i:19"></i>
<b class="top"></b><b class="bottom"></b>
</div>
<span class="streak"></span>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the can is the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.45vmin;
perspective: calc(800 * var(--u));
}
.can {
position: relative;
transform-style: preserve-3d;
transform: rotateX(-16deg) rotateZ(-9deg);
}
.spin {
position: relative;
width: calc(70 * var(--u));
height: calc(118 * var(--u));
transform-style: preserve-3d;
animation: spin 8s linear infinite;
}
.spin > i {
position: absolute;
top: 0;
/* 0.75 units wider than its 11-unit slice on each side, so no seams show. The can is tilted, so
the seams run at a slant: with less overlap they show as rows of dark dots. */
left: calc(50% - 6.25 * var(--u));
width: calc(12.5 * var(--u));
height: 100%;
backface-visibility: hidden;
/* the whole label on every strip, one lap (220 units) long */
background-image:
linear-gradient(#e6e9f5 0 3%, #9aa1bd 5%, transparent 5% 95%, #9aa1bd 95%, #e6e9f5 97%),
radial-gradient(circle at calc(55 * var(--u)) 50%, #ff4d9d 0 calc(8 * var(--u)), #fff calc(8.5 * var(--u)) calc(15 * var(--u)), transparent calc(15.5 * var(--u))),
radial-gradient(circle at calc(165 * var(--u)) 50%, #2ee6d6 0 calc(8 * var(--u)), #fff calc(8.5 * var(--u)) calc(15 * var(--u)), transparent calc(15.5 * var(--u))),
linear-gradient(100deg, transparent 0 18%, rgb(255 255 255 / 0.28) 18% 24%, transparent 24% 68%, rgb(255 255 255 / 0.28) 68% 74%, transparent 74%),
linear-gradient(90deg, #8b6cff, #ff4d9d 50%, #8b6cff); /* same colour at both ends: no seam */
background-size: calc(220 * var(--u)) 100%;
/* strip i shows slice i */
background-position: calc(var(--i) * -11 * var(--u) + 0.75 * var(--u)) 0;
transform: rotateY(calc(var(--i) * 18deg)) translateZ(calc(34.7 * var(--u)));
}
/* fixed lighting: each strip darkens and brightens once per lap, started i/20 of the way round */
.spin > i::after {
content: '';
position: absolute;
inset: 0;
background: #05060f;
animation: shade 8s linear infinite;
animation-delay: calc(var(--i) * -0.4s);
}
.top,
.bottom {
position: absolute;
left: 0;
width: calc(70 * var(--u));
height: calc(70 * var(--u));
border-radius: 50%;
}
/* centred on the top edge, then laid flat */
.top {
top: calc(-35 * var(--u));
background:
radial-gradient(ellipse calc(9 * var(--u)) calc(13 * var(--u)) at 50% 68%, #3a3f5c 0 95%, transparent 100%),
radial-gradient(circle, #cfd3e6 0 52%, #8f96b3 54% 60%, #eef0fa 62% 86%, #a3a9c4 88%);
transform: rotateX(90deg);
}
.bottom {
top: calc(100% - 35 * var(--u));
background: radial-gradient(circle, #6d7391 0 50%, #a3a9c4 52%);
transform: rotateX(-90deg);
}
/* a specular streak just in front of the surface; it does not spin */
.streak {
position: absolute;
top: 9%;
left: calc(50% - 19 * var(--u));
width: calc(8 * var(--u));
height: 82%;
border-radius: calc(4 * var(--u));
background: linear-gradient(90deg, transparent, rgb(255 255 255 / 0.55), transparent);
transform: translateZ(calc(35.7 * var(--u)));
pointer-events: none;
}
@keyframes spin {
to { transform: rotateY(360deg); }
}
/* 0% = facing you. Brightest near 330° (lamp front-left); 25–75% faces away and is hidden */
@keyframes shade {
0% { opacity: 0.08; }
6.25% { opacity: 0.23; }
12.5% { opacity: 0.44; }
25%, 50% { opacity: 0.62; }
75% { opacity: 0.34; }
87.5% { opacity: 0.04; }
91.5% { opacity: 0; }
100% { opacity: 0.08; }
}