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.
A paper cup that is wider at the top: twenty trapezoid strips lean out from the base circle. The sleeve and its logo wrap around without a seam, the lid is a stack of discs and the steam keeps rising.
Your edited version
rotateY(i × 18deg) translateZ(27 units)), hinged on their bottom edge, and lean out by atan((36 − 27) / 100) ≈ 5.14° so the rim is wider than the base.clip-path: 11.4 units wide at the rim, 8.6 units at the base (both are 2 · r · tan(9°)), each drawn 1.2 units wider so neighbours overlap and no seam shows. Its height is the slant length, √(100² + 9²).background-position). The lap is measured at the sleeve's middle height, where the strips are 9.86 units wide, so the logo wraps round without a seam.animation-delay.--u, tied to the canvas, so the cup is the same share of a gallery card, the editor and a recording canvas.frustum: strips hinged at the base, rotateX(−lean)trapezoids with clip-pathone lap-long label, background-position by indexsteam from the sip slot, counter-turned to face you<div class="scene">
<div class="coffee">
<div class="spin">
<i class="shadow"></i>
<i class="strip" style="--i:0"></i>
<i class="strip" style="--i:1"></i>
<i class="strip" style="--i:2"></i>
<i class="strip" style="--i:3"></i>
<i class="strip" style="--i:4"></i>
<i class="strip" style="--i:5"></i>
<i class="strip" style="--i:6"></i>
<i class="strip" style="--i:7"></i>
<i class="strip" style="--i:8"></i>
<i class="strip" style="--i:9"></i>
<i class="strip" style="--i:10"></i>
<i class="strip" style="--i:11"></i>
<i class="strip" style="--i:12"></i>
<i class="strip" style="--i:13"></i>
<i class="strip" style="--i:14"></i>
<i class="strip" style="--i:15"></i>
<i class="strip" style="--i:16"></i>
<i class="strip" style="--i:17"></i>
<i class="strip" style="--i:18"></i>
<i class="strip" style="--i:19"></i>
<i class="lid" style="--y:-1.5"></i><i class="lid" style="--y:0"></i><i class="lid" style="--y:1.5"></i><i class="lid" style="--y:3"></i>
<i class="lid lid-top" style="--y:4.5"></i>
<i class="lid spout" style="--y:6.5"></i>
<i class="vent"><i class="face"><i class="steam" style="--x:-2;--d:0"></i><i class="steam" style="--x:3;--d:1"></i></i></i>
</div>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the cup is the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.36vmin;
perspective: calc(800 * var(--u));
}
/* a 0-wide column, 100 units tall: the cup's axis */
.coffee {
--paper: #f4f1ea;
--paper-dark: #d9d3c6;
--kraft: #c1834a;
--violet: #8b6cff;
--lid: #22232e;
position: relative;
width: 0;
height: calc(100 * var(--u));
transform-style: preserve-3d;
/* the lid rises a little above this column: move it down by that much, and the solid cup is
centred. The steam is faint and rises well above it, so it is left out of that sum: the eye
places the cup by its body */
translate: 0 calc(3 * var(--u));
transform: rotateX(-20deg);
}
.spin {
position: absolute;
inset: 0;
transform-style: preserve-3d;
animation: spin 12s linear infinite;
}
.shadow {
position: absolute;
left: calc(-60 * var(--u));
top: calc(70 * var(--u));
width: calc(120 * var(--u));
height: calc(60 * var(--u));
border-radius: 50%;
background: radial-gradient(closest-side, rgb(0 0 0 / 0.45), transparent);
transform: rotateX(90deg);
}
/* 20 trapezoids hinged on the base circle (r 27 units), leaning out towards the rim (r 36 units) */
.strip {
position: absolute;
left: calc(-6.3 * var(--u));
bottom: 0;
width: calc(12.6 * var(--u)); /* 11.4 units at the rim, plus a little overlap */
height: calc(100.4 * var(--u)); /* the slant length */
transform-origin: 50% 100%;
transform: rotateY(calc(var(--i) * 18deg)) translateZ(calc(27 * var(--u))) rotateX(-5.14deg);
clip-path: polygon(0 0, 100% 0, calc(50% + 4.9 * var(--u)) 100%, calc(50% - 4.9 * var(--u)) 100%);
backface-visibility: hidden;
/* a soft shade under the lid: the lid's discs are nearly side-on, and their edge against
bare white paper steps and reads as a dashed line; against the shade it reads as one edge */
box-shadow: inset 0 calc(5 * var(--u)) calc(3 * var(--u)) calc(-2 * var(--u)) rgb(20 20 30 / 0.8);
/* one 197 units canvas = one lap round the sleeve's middle; each strip shows its own slice */
background-image:
radial-gradient(ellipse calc(1.2 * var(--u)) calc(5 * var(--u)) at calc(49.3 * var(--u)) calc(53 * var(--u)), #f0d9b8 0 60%, transparent 90%),
radial-gradient(ellipse calc(4.5 * var(--u)) calc(6.5 * var(--u)) at calc(49.3 * var(--u)) calc(53 * var(--u)), #fff 0 20%, #6b3e22 30% 92%, transparent),
radial-gradient(circle calc(11 * var(--u)) at calc(49.3 * var(--u)) calc(53 * var(--u)), var(--violet) 0 86%, #fff 87% 93%, var(--violet) 94% 100%, transparent 100%),
radial-gradient(ellipse calc(1.2 * var(--u)) calc(5 * var(--u)) at calc(148 * var(--u)) calc(53 * var(--u)), #f0d9b8 0 60%, transparent 90%),
radial-gradient(ellipse calc(4.5 * var(--u)) calc(6.5 * var(--u)) at calc(148 * var(--u)) calc(53 * var(--u)), #fff 0 20%, #6b3e22 30% 92%, transparent),
radial-gradient(circle calc(11 * var(--u)) at calc(148 * var(--u)) calc(53 * var(--u)), var(--violet) 0 86%, #fff 87% 93%, var(--violet) 94% 100%, transparent 100%),
linear-gradient(transparent calc(34 * var(--u)), var(--kraft) calc(34 * var(--u)) calc(36 * var(--u)), #d8b08c calc(36 * var(--u)) calc(37 * var(--u)), var(--kraft) calc(37 * var(--u)) calc(70 * var(--u)), #d8b08c calc(70 * var(--u)) calc(71 * var(--u)), var(--kraft) calc(71 * var(--u)) calc(73 * var(--u)), transparent calc(73 * var(--u))),
linear-gradient(var(--paper-dark) 0 calc(1 * var(--u)), var(--paper) calc(3 * var(--u)) calc(12 * var(--u)), var(--violet) calc(12 * var(--u)) calc(14 * var(--u)), var(--paper) calc(14 * var(--u)) calc(100% - 3 * var(--u)), var(--paper-dark));
background-size: calc(197.3 * var(--u)) 100%;
background-position: calc(1.07 * var(--u) - var(--i) * 9.864 * var(--u)) 0;
}
/* lighting that stays put: one lap of shade, each strip started i/20 of the way round */
.strip::after {
content: '';
position: absolute;
inset: 0;
background: #06070f;
animation: shade 12s linear infinite;
animation-delay: calc(var(--i) * -0.6s);
}
/* discs centred on the rim, lifted by --y, laid flat */
.lid {
--r: calc(38.4 * var(--u));
position: absolute;
left: calc(var(--r) * -1);
top: calc(var(--r) * -1);
width: calc(var(--r) * 2);
height: calc(var(--r) * 2);
border-radius: 50%;
background: radial-gradient(circle, var(--lid) 0 80%, #646570 94%, var(--lid));
transform: translateY(calc(var(--y) * -1 * var(--u))) rotateX(90deg);
}
.lid-top {
--r: calc(35.4 * var(--u));
background:
radial-gradient(circle, transparent 0 70%, rgb(255 255 255 / 0.12) 72% 74%, transparent 76%),
radial-gradient(circle at 35% 30%, #4e4f58, var(--lid) 60%);
}
.spout {
--r: calc(13 * var(--u));
background:
radial-gradient(ellipse calc(7 * var(--u)) calc(2.5 * var(--u)) at 50% 70%, #05060a 0 90%, transparent), /* the sip slot */
radial-gradient(circle at 40% 30%, #6a6b74, var(--lid) 70%);
transform: translateY(calc(var(--y) * -1 * var(--u))) translateZ(calc(19 * var(--u))) rotateX(90deg);
}
/* steam rises from the sip slot: placed there inside .spin, so it goes round with the cup... */
.vent {
position: absolute;
top: calc(-7 * var(--u));
left: 0;
transform-style: preserve-3d;
transform: translateZ(calc(24 * var(--u)));
}
/* ...and turned back the other way, so the wisps always face you */
.face {
position: absolute;
transform-style: preserve-3d;
animation: spin 12s linear infinite reverse;
}
/* two arcs make an S */
.steam {
position: absolute;
top: calc(-30 * var(--u));
left: calc(-5 * var(--u));
width: calc(10 * var(--u));
height: calc(30 * var(--u));
opacity: 0;
animation: steam 3.6s linear infinite;
animation-delay: calc(var(--d) * -1.8s);
}
.steam::before,
.steam::after {
content: '';
position: absolute;
left: 0;
box-sizing: border-box;
width: 100%;
height: 52%;
border: calc(2.5 * var(--u)) solid transparent;
border-radius: 50%;
}
.steam::before { top: 0; border-left-color: rgb(236 238 251 / 0.5); }
.steam::after { bottom: 0; left: -70%; border-right-color: rgb(236 238 251 / 0.5); }
@keyframes spin {
to { transform: rotateY(360deg); }
}
/* 0% = facing you; between 25% and 75% the strip faces away */
@keyframes shade {
0% { opacity: 0.06; }
12.5% { opacity: 0.3; }
25%, 50% { opacity: 0.5; }
75% { opacity: 0.3; }
88% { opacity: 0; }
100% { opacity: 0.06; }
}
@keyframes steam {
0% { opacity: 0; transform: translate(calc(var(--x) * 1 * var(--u)), calc(10 * var(--u))) scale(0.6); }
30% { opacity: 0.9; }
60% { transform: translate(calc(var(--x) * 1 * var(--u) + 4 * var(--u)), calc(-12 * var(--u))) scale(0.9, 1.1); }
100% { opacity: 0; transform: translate(calc(var(--x) * 1 * var(--u) - 2 * var(--u)), calc(-34 * var(--u))) scale(1.1, 1.3); }
}