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 medal hanging from a neck ribbon: it sways from the neck and twists on its ring, with a real metal edge and a shine that sweeps each time it faces you.
Your edited version
transform-origin: 50% 0 on a wrapper and a small rotateZ swing, alternate.rotateY(-50deg) and rotateY(50deg), never edge-on, while the bail stays flat: as a child of the medal it runs the opposite twist, which cancels it, so it never swings through the ribbon.--u, tied to the canvas, so the medal is the same share of a gallery card, the editor and a recording canvas.bail looped behind a folded tabpendulum from transform-origin: topedge from stacked discsshine synced to the twist<div class="scene">
<div class="badge">
<div class="swing">
<i class="strap"></i><i class="strap"></i>
<div class="medal">
<i class="bail"></i>
<u style="--i:0"></u><u style="--i:1"></u><u style="--i:2"></u><u style="--i:3"></u><u style="--i:4"></u>
<div class="face"><b></b><small>WINNER</small><em></em></div>
</div>
<i class="tab"></i>
</div>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the medal is the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.31vmin;
perspective: calc(800 * var(--u));
}
.badge {
--gold: #ffd28f;
--gold-deep: #aa7519;
position: relative;
width: calc(120 * var(--u));
height: calc(196 * var(--u));
transform-style: preserve-3d;
}
/* sways from the top, like something hanging from a neck */
.swing {
position: absolute;
inset: 0;
transform-style: preserve-3d;
transform-origin: 50% 0;
animation: sway 2.5s ease-in-out infinite alternate;
}
/* two straps pinned at the tab, splayed toward the neck, slightly behind everything */
.strap {
position: absolute;
top: 0;
left: calc(50% - 12 * var(--u));
width: calc(24 * var(--u));
height: calc(92 * var(--u));
background:
linear-gradient(90deg, rgb(0 0 0 / 0.28), transparent 30% 70%, rgb(0 0 0 / 0.28)),
linear-gradient(90deg, #8b6cff 0 30%, #fff 30% 36%, #ff4d9d 36% 64%, #fff 64% 70%, #8b6cff 70%);
transform-origin: 50% 100%;
transform: translateX(calc(-7 * var(--u))) translateZ(calc(-3 * var(--u))) rotate(-20deg);
}
.strap + .strap {
transform: translateX(calc(7 * var(--u))) translateZ(calc(-2 * var(--u))) rotate(20deg);
}
/* the folded end of the ribbon, in front of the bail so the bail looks looped through it */
.tab {
position: absolute;
top: calc(80 * var(--u));
left: calc(50% - 19 * var(--u));
width: calc(38 * var(--u));
height: calc(14 * var(--u));
border-radius: calc(3 * var(--u));
background:
linear-gradient(rgb(255 255 255 / 0.25), transparent 40%, rgb(0 0 0 / 0.3)),
linear-gradient(90deg, #8b6cff, #ff4d9d 50%, #8b6cff);
box-shadow: 0 calc(2 * var(--u)) calc(4 * var(--u)) rgb(0 0 0 / 0.35);
transform: translateZ(calc(2 * var(--u)));
}
.medal {
position: absolute;
top: calc(100 * var(--u));
left: calc(50% - 48 * var(--u));
width: calc(96 * var(--u));
height: calc(96 * var(--u));
transform-style: preserve-3d;
animation: twist 5s ease-in-out infinite alternate;
}
/* the edge: five discs 1.2 units apart */
.medal u {
position: absolute;
inset: calc(0.5 * var(--u));
border-radius: 50%;
background: linear-gradient(90deg, var(--gold-deep), var(--gold) 50%, var(--gold-deep));
transform: translateZ(calc((var(--i) - 2) * 1.2 * var(--u)));
}
/* the bail hangs flat from the tab while the medal turns on it: as the medal's child it runs
the opposite twist with the same timing, which cancels it (turning with the medal would swing
it through the tab and show a hard cut) */
.bail {
position: absolute;
top: calc(-16 * var(--u));
left: calc(50% - 9 * var(--u));
width: calc(18 * var(--u));
height: calc(20 * var(--u));
box-sizing: border-box;
border: calc(3 * var(--u)) solid var(--gold);
border-radius: 50%;
box-shadow: inset 0 0 0 calc(1 * var(--u)) var(--gold-deep), 0 0 0 calc(1 * var(--u)) var(--gold-deep);
animation: untwist 5s ease-in-out infinite alternate;
}
.face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
align-content: center;
gap: calc(2 * var(--u));
overflow: hidden; /* fine: the face itself is flat */
border-radius: 50%;
background:
radial-gradient(circle at 32% 26%, rgb(255 255 255 / 0.55), transparent 42%),
conic-gradient(from 20deg, var(--gold), var(--gold-deep), var(--gold), #c89445, var(--gold));
box-shadow:
inset 0 0 0 calc(4 * var(--u)) #ffe3b8,
inset 0 0 0 calc(6 * var(--u)) var(--gold-deep),
inset 0 0 0 calc(12 * var(--u)) rgb(170 117 25 / 0.25);
color: #5a3a10;
font-family: Inter, system-ui, sans-serif;
transform: translateZ(calc(3 * var(--u)));
}
.face b {
width: calc(40 * var(--u));
height: calc(40 * var(--u));
clip-path: polygon(50% 0, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
background: linear-gradient(160deg, #fff8de, var(--gold) 45%, var(--gold-deep));
}
.face small {
font-size: calc(8 * var(--u));
font-weight: 800;
letter-spacing: calc(2 * var(--u));
}
/* one sweep per twist, centred on the moment the face looks straight at you */
.face em {
position: absolute;
inset: 0;
background: linear-gradient(115deg, transparent 38%, rgb(255 255 255 / 0.7) 50%, transparent 62%);
transform: translateX(-120%);
animation: shine 5s linear infinite;
}
@keyframes sway {
from { transform: rotateZ(-3deg); }
to { transform: rotateZ(3deg); }
}
@keyframes twist {
from { transform: rotateY(-50deg); }
to { transform: rotateY(50deg); }
}
@keyframes untwist {
from { transform: rotateY(50deg); }
to { transform: rotateY(-50deg); }
}
@keyframes shine {
0%, 35% { transform: translateX(-120%); }
65%, 100% { transform: translateX(120%); }
}