Solar system
Pure CSSOrbits are rings on a tilted plane, each spinning at its own speed. Every planet undoes both rotations, so it always faces the camera.
A striped balloon bobbing and turning slowly on its ropes, with a woven basket and clouds drifting past in front of it and behind it. Each of its twelve gores is six flat panels, each tilted to follow the balloon’s outline, so the silhouette is round from every side as the stripes go by.
Your edited version
rotateY(k × 30deg), and each gore is six flat panels following the outline down from the crown ring to the mouth. A panel is moved to the middle of its band with translate3d(0, y, r) and tilted with rotateX by the outline’s angle there, so its edges land exactly on its neighbours’ and the silhouette is round from every side.2 r tan 15°, which differs at its top and bottom, so each is a trapezoid cut with clip-path. A clip flattens anything 3D inside an element, so only leaves are clipped: the panels are the ::before and ::after of the gore and of two plain <i> carriers inside it. Panels facing away are hidden with backface-visibility.translateZ(54 units) and shrunk by (800 − 54) ÷ 800 about the point the perspective looks from, it lands exactly on the balloon, so the stripes turn under a light that stays put.repeating-linear-gradients) round a floor, turning with the balloon. It turns once every 40 s and bobs on a separate wrapper, so the two motions add up without either knowing of the other; the bob (10 s there and back), the burner and the clouds (20 and 40 s) all divide the turn, so the whole scene repeats exactly once a turn.opacity, so their loops have no seam and they never reach the edge of the canvas. Every length is a multiple of one base unit, --u.12 gores × 6 panels, each turned rotateY(k × 30°)panel: translate3d to its band, rotateX to the outlinetrapezoid panels: clip-path on the leaves onlya light that does not turn: a plane in front, scaled to land on it<div class="scene">
<div class="sky">
<i class="cloud far" style="--y:18;--z:-160;--d:40s;--t:-20s"></i>
<i class="cloud" style="--y:66;--z:-90;--d:40s;--t:-10s"></i>
<div class="flight">
<div class="balloon">
<b class="crown"></b>
<b class="gore" style="--k:0;--c:#8b6cff"><i></i><i></i></b>
<b class="gore" style="--k:1;--c:#2ee6d6"><i></i><i></i></b>
<b class="gore" style="--k:2;--c:#ff4d9d"><i></i><i></i></b>
<b class="gore" style="--k:3;--c:#ffb547"><i></i><i></i></b>
<b class="gore" style="--k:4;--c:#8b6cff"><i></i><i></i></b>
<b class="gore" style="--k:5;--c:#2ee6d6"><i></i><i></i></b>
<b class="gore" style="--k:6;--c:#ff4d9d"><i></i><i></i></b>
<b class="gore" style="--k:7;--c:#ffb547"><i></i><i></i></b>
<b class="gore" style="--k:8;--c:#8b6cff"><i></i><i></i></b>
<b class="gore" style="--k:9;--c:#2ee6d6"><i></i><i></i></b>
<b class="gore" style="--k:10;--c:#ff4d9d"><i></i><i></i></b>
<b class="gore" style="--k:11;--c:#ffb547"><i></i><i></i></b>
<b class="rope" style="--k:0"></b>
<b class="rope" style="--k:1"></b>
<b class="rope" style="--k:2"></b>
<b class="rope" style="--k:3"></b>
<b class="burner"></b>
<div class="basket"><i></i><i></i><i></i><i></i></div>
</div>
<i class="shade"></i>
</div>
<i class="cloud near" style="--y:120;--z:70;--d:20s;--t:-14s"></i>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the balloon is the same share of a
gallery card, the editor, a full screen and a recording canvas */
--u: 0.42vmin;
display: grid;
place-items: center;
perspective: calc(800 * var(--u));
}
.scene * {
box-sizing: border-box;
}
/* the sky: the balloon and the clouds share one 3D space, so a cloud can pass in front */
.sky {
position: relative;
width: calc(110 * var(--u));
height: calc(144 * var(--u));
transform-style: preserve-3d;
/* the low cloud hangs under the basket: lifted this much, balloon and clouds together sit in
the middle */
transform: translateY(calc(-7 * var(--u)));
}
/* the bob, on its own wrapper, so it adds to the turn without either knowing of the other */
.flight {
position: absolute;
inset: 0;
transform-style: preserve-3d;
animation: bob 5s ease-in-out infinite alternate;
}
@keyframes bob {
from { transform: translateY(calc(3 * var(--u))); }
to { transform: translateY(calc(-5 * var(--u))); }
}
/* the balloon's axis: a zero-size point at the crown, in the middle of the sky. Everything that
turns is placed from it */
.balloon {
position: absolute;
left: 50%;
top: 0;
transform-style: preserve-3d;
animation: turn 40s linear infinite;
}
@keyframes turn {
from { transform: rotateY(0deg); }
to { transform: rotateY(360deg); }
}
/* the crown: a flat disc closing the top of the gores, laid level */
.crown {
position: absolute;
left: calc(-23.5 * var(--u));
top: calc(-18.5 * var(--u));
width: calc(47 * var(--u));
height: calc(47 * var(--u));
border-radius: 50%;
background: conic-gradient(#8b6cff 0deg 90deg, #2ee6d6 90deg 180deg, #ff4d9d 180deg 270deg, #ffb547 270deg 360deg);
transform: rotateX(90deg);
}
/* a gore: a carrier at the axis, turned to its place round the ring, with two more carriers in
it (its <i>s, untransformed). None draws anything: the six panels are their pseudo-elements,
because a panel is clipped and a clip flattens anything 3D inside it */
.gore,
.gore i {
position: absolute;
transform-style: preserve-3d;
}
.gore {
transform: rotateY(calc(var(--k) * 30deg));
}
.gore::before,
.gore::after,
.gore i::before,
.gore i::after {
content: '';
position: absolute;
backface-visibility: hidden; /* the far side's panels are not drawn through the near side */
}
/* the shoulder, out from the crown ring */
.gore::before {
left: calc(-10.85 * var(--u));
top: calc(-11.5 * var(--u));
width: calc(21.7 * var(--u));
height: calc(23 * var(--u));
background: linear-gradient(rgb(255 255 255 / 0.30), rgb(255 255 255 / 0.20)), var(--c);
clip-path: polygon(20.99% 0, 79.01% 0, 100% 100%, 0% 100%);
transform: translate3d(0, calc(11.5 * var(--u)), calc(30.5 * var(--u))) rotateX(52.59deg);
}
/* the upper slope */
.gore::after {
left: calc(-13.26 * var(--u));
top: calc(-10.42 * var(--u));
width: calc(26.52 * var(--u));
height: calc(20.84 * var(--u));
background: linear-gradient(rgb(255 255 255 / 0.20), rgb(255 255 255 / 0.09)), var(--c);
clip-path: polygon(9.09% 0, 90.91% 0, 100% 100%, 0% 100%);
transform: translate3d(0, calc(26.5 * var(--u)), calc(43.5 * var(--u))) rotateX(27.9deg);
}
/* the last of the upper half, almost upright */
.gore i:first-child::before {
left: calc(-13.79 * var(--u));
top: calc(-9.36 * var(--u));
width: calc(27.59 * var(--u));
height: calc(18.72 * var(--u));
background: linear-gradient(rgb(255 255 255 / 0.09), rgb(255 255 255 / 0.00)), var(--c);
clip-path: polygon(1.94% 0, 98.06% 0, 100% 100%, 0% 100%);
transform: translate3d(0, calc(43.5 * var(--u)), calc(49 * var(--u))) rotateX(6.71deg);
}
/* under the equator, its top leaning out (a negative tilt) */
.gore i:first-child::after {
left: calc(-13.79 * var(--u));
top: calc(-11.24 * var(--u));
width: calc(27.59 * var(--u));
height: calc(22.48 * var(--u));
background: linear-gradient(rgb(255 255 255 / 0.00), rgb(10 8 30 / 0.10)), var(--c);
clip-path: polygon(0% 0, 100% 0, 94.17% 100%, 5.83% 100%);
transform: translate3d(0, calc(62 * var(--u)), calc(47 * var(--u))) rotateX(-16.7deg);
}
/* the taper */
.gore i + i::before {
left: calc(-12.19 * var(--u));
top: calc(-11.5 * var(--u));
width: calc(24.38 * var(--u));
height: calc(23 * var(--u));
background: linear-gradient(rgb(10 8 30 / 0.10), rgb(10 8 30 / 0.22)), var(--c);
clip-path: polygon(0% 0, 100% 0, 85.71% 100%, 14.29% 100%);
transform: translate3d(0, calc(80.5 * var(--u)), calc(37.5 * var(--u))) rotateX(-37.41deg);
}
/* the skirt, narrowing to the mouth */
.gore i + i::after {
left: calc(-8.71 * var(--u));
top: calc(-10.4 * var(--u));
width: calc(17.41 * var(--u));
height: calc(20.81 * var(--u));
background: linear-gradient(rgb(10 8 30 / 0.22), rgb(10 8 30 / 0.34)), var(--c);
clip-path: polygon(0% 0, 100% 0, 76.92% 100%, 23.08% 100%);
transform: translate3d(0, calc(95 * var(--u)), calc(23.5 * var(--u))) rotateX(-51.34deg);
}
/* the light, which does not turn: a sheen up and to the left and shade round the edge, on a
plane just in front of the envelope, cut to its outline. It is pushed toward you and shrunk by
the same ratio about the point the perspective looks from, so it lands exactly on the balloon */
.shade {
position: absolute;
inset: 0;
background:
radial-gradient(30% 22% at 38% 22%, rgb(255 255 255 / 0.34), transparent),
radial-gradient(48% 40% at 47% 37%, transparent 52%, rgb(12 8 40 / 0.42));
clip-path: polygon(70% 3.47%, 85.45% 12.5%, 93.64% 24.31%, 95.45% 36.11%, 90% 50%, 78.18% 61.81%, 64.55% 70.14%, 35.45% 70.14%, 21.82% 61.81%, 10% 50%, 4.55% 36.11%, 6.36% 24.31%, 14.55% 12.5%, 30% 3.47%);
transform-origin: 50% 50%;
transform: translateZ(calc(54 * var(--u))) scale(0.93);
}
/* a rope: from the skirt down to a corner of the basket, at 45° between the gores. A second
line crossed through it (::before), so it never vanishes edge-on */
.rope {
position: absolute;
left: calc(-0.5 * var(--u));
top: calc(-14.01 * var(--u));
width: calc(1 * var(--u));
height: calc(28.01 * var(--u));
background: #9d7f5c;
transform-style: preserve-3d;
transform: rotateY(calc(45deg + var(--k) * 90deg)) translate3d(0, calc(114 * var(--u)), calc(15.28 * var(--u))) rotateX(1.14deg);
}
.rope::before {
content: '';
position: absolute;
inset: 0;
background: inherit;
transform: rotateY(90deg);
}
/* the burner's flame, under the mouth: two crossed teardrops, flickering */
.burner {
position: absolute;
left: calc(-4 * var(--u));
top: calc(104 * var(--u));
width: calc(8 * var(--u));
height: calc(12 * var(--u));
transform-style: preserve-3d;
transform-origin: 50% 100%;
animation: flare 0.5s ease-in-out infinite alternate;
}
.burner::before,
.burner::after {
content: '';
position: absolute;
inset: 0;
border-radius: 50% 50% 50% 50% / 70% 70% 30% 30%;
background: radial-gradient(60% 50% at 50% 72%, #fffbe8, #ffd36b 40%, #ff8a3d 70%, rgb(255 77 157 / 0.6));
}
.burner::after {
transform: rotateY(90deg);
}
@keyframes flare {
from { transform: scale(0.9, 0.8); }
to { transform: scale(1, 1.1); } /* scale only: opacity on it would flatten its crossed plane */
}
/* the basket: a zero-size point under the axis; four woven walls stand round it */
.basket {
position: absolute;
top: calc(128 * var(--u));
transform-style: preserve-3d;
}
.basket i {
position: absolute;
left: calc(-11 * var(--u));
top: 0;
width: calc(22 * var(--u));
height: calc(16 * var(--u));
border-top: calc(2.4 * var(--u)) solid #5e3a1f;
border-radius: 0 0 calc(2 * var(--u)) calc(2 * var(--u));
/* the weave: light strands across, darker ones up, over the wicker's own brown */
background:
repeating-linear-gradient(90deg, rgb(70 40 18 / 0.45) 0 calc(0.8 * var(--u)), transparent calc(0.8 * var(--u)) calc(3.6 * var(--u))),
repeating-linear-gradient(rgb(255 225 170 / 0.35) 0 calc(1.4 * var(--u)), transparent calc(1.4 * var(--u)) calc(2.8 * var(--u))),
linear-gradient(#c08a4e, #8a5a2e);
transform: rotateY(calc(var(--k, 0) * 90deg)) translateZ(calc(11 * var(--u)));
}
.basket i:nth-child(2) { --k: 1; }
.basket i:nth-child(3) { --k: 2; }
.basket i:nth-child(4) { --k: 3; }
/* the floor, seen inside from above and under from below */
.basket::before {
content: '';
position: absolute;
left: calc(-11 * var(--u));
top: calc(5 * var(--u));
width: calc(22 * var(--u));
height: calc(22 * var(--u));
background: #4a2c16;
transform: rotateX(90deg);
}
/* a cloud: a long base and two puffs, one gradient across all three (sized to the whole cloud and
shifted by each piece's top), so it reads as one body: bright above, lilac in its belly, which
keeps it visible on a light stage. It drifts across at its own depth and fades at both ends */
.cloud,
.cloud::before,
.cloud::after {
background: linear-gradient(#ffffff 22%, #dfe1f7 60%, #a9afe0) 0 var(--gy, calc(-14 * var(--u))) / 100% calc(30 * var(--u));
}
/* the element is the base, the bottom 16 of the cloud's 30 units */
.cloud {
position: absolute;
left: calc(50% - calc(30 * var(--u)));
top: calc((var(--y) + 14) * var(--u));
width: calc(60 * var(--u));
height: calc(16 * var(--u));
border-radius: 999px;
opacity: 0;
animation: drift var(--d) linear var(--t) infinite;
}
/* the two puffs on its back, their feet on its bottom edge */
.cloud::before,
.cloud::after {
content: '';
position: absolute;
bottom: 0;
border-radius: 50%;
}
.cloud::before {
--gy: calc(-6 * var(--u));
left: calc(7 * var(--u));
width: calc(26 * var(--u));
height: calc(24 * var(--u));
}
.cloud::after {
--gy: calc(0 * var(--u));
left: calc(25 * var(--u));
width: calc(28 * var(--u));
height: calc(30 * var(--u));
}
.cloud.far { --s: 0.8; }
.cloud.near { --s: 0.9; }
@keyframes drift {
0% { opacity: 0; transform: translate3d(calc(62 * var(--u)), 0, calc(var(--z) * var(--u))) scale(var(--s, 1)); }
18% { opacity: 0.94; }
82% { opacity: 0.94; }
100% { opacity: 0; transform: translate3d(calc(-62 * var(--u)), 0, calc(var(--z) * var(--u))) scale(var(--s, 1)); }
}