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 watch with thickness, strap stubs and the real time. Once a second JS writes three angles and two integers; CSS turns the hands and prints the digits with counters.
Your edited version
--h, --m, --s as angles: not while the frame is paused (<html data-paused>), and not the second hand when the visitor asks for reduced motion. Each hand stands on the centre (bottom: 50%), turns about its foot and reads its own variable.counter-reset: hh var(--hh) mm var(--mm) turns them into counters and content: counter(hh, decimal-leading-zero) prints 09:05.±5. The straps are hinged on the case edge with transform-origin and tilted away with rotateX.--u, tied to the canvas, so the case, the dial and the digits are the same share of a gallery card, the editor and a recording canvas.setInterval → --h / --m / --s anglesdigits via counter-reset: var(--hh)straps hinged on the case edgerounded slabs for thickness<div class="scene">
<div class="watch">
<div class="body">
<i class="strap top"></i><i class="strap bottom"></i>
<i class="slab" style="--i:0"></i><i class="slab" style="--i:1"></i><i class="slab" style="--i:2"></i>
<i class="slab mid" style="--i:3"></i>
<i class="slab" style="--i:4"></i><i class="slab" style="--i:5"></i><i class="slab" style="--i:6"></i>
<i class="crown"></i>
<div class="back"></div>
<div class="face">
<div class="dial"><i class="hand h"></i><i class="hand m"></i><i class="hand s"></i></div>
<div class="time"></div>
<small>live time</small>
</div>
</div>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the watch is the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.34vmin;
perspective: calc(800 * var(--u));
}
.watch {
--metal: #5a607f;
--band: #6550ba;
transform-style: preserve-3d;
animation: sway 7s ease-in-out infinite alternate;
}
/* 88 × 104, 10 thick, 24 corners */
.body {
position: relative;
width: calc(88 * var(--u));
height: calc(104 * var(--u));
transform-style: preserve-3d;
}
/* hinged on the back edge of the case, then tilted away round an invisible wrist */
.strap {
position: absolute;
left: calc(16 * var(--u));
width: calc(56 * var(--u));
height: calc(44 * var(--u));
background:
linear-gradient(90deg, rgb(0 0 0 / 0.35), transparent 22% 78%, rgb(0 0 0 / 0.35)),
var(--band);
}
.strap.top {
bottom: 100%;
border-radius: calc(10 * var(--u)) calc(10 * var(--u)) 0 0;
transform-origin: 50% 100%;
transform: translateZ(calc(-4 * var(--u))) rotateX(38deg);
}
.strap.bottom {
top: 100%;
border-radius: 0 0 calc(10 * var(--u)) calc(10 * var(--u));
background:
radial-gradient(circle, rgb(0 0 0 / 0.45) 0 calc(2 * var(--u)), transparent calc(2.5 * var(--u))) 50% calc(8 * var(--u)) / 100% calc(10 * var(--u)) repeat-y,
linear-gradient(90deg, rgb(0 0 0 / 0.35), transparent 22% 78%, rgb(0 0 0 / 0.35)),
var(--band);
transform-origin: 50% 0;
transform: translateZ(calc(-4 * var(--u))) rotateX(-38deg);
}
/* rounded slabs fill the case, corners included */
.slab {
position: absolute;
inset: calc(0.5 * var(--u));
border-radius: calc(24 * var(--u));
background: var(--metal);
transform: translateZ(calc((var(--i) - 3) * 1.5 * var(--u)));
}
.slab.mid {
background: color-mix(in srgb, #eceefb 45%, var(--metal)); /* a polished band */
}
/* ribbed crown plate, plus its end face turned side-on */
.crown {
position: absolute;
top: calc(30 * var(--u));
left: calc(100% - 3 * var(--u));
width: calc(7 * var(--u));
height: calc(20 * var(--u));
border-radius: calc(2 * var(--u));
background: repeating-linear-gradient(var(--metal) 0 calc(1.5 * var(--u)), #a3a7bd calc(1.5 * var(--u)) calc(3 * var(--u)));
transform-style: preserve-3d;
transform: translateZ(calc(0.75 * var(--u)));
}
.crown::before {
content: '';
position: absolute;
top: 0;
left: calc(100% - 4 * var(--u));
width: calc(8 * var(--u));
height: 100%;
border-radius: calc(2 * var(--u));
background: inherit;
transform: rotateY(90deg);
}
.back,
.face {
position: absolute;
inset: 0;
box-sizing: border-box;
border-radius: calc(24 * var(--u));
backface-visibility: hidden;
}
.back {
background:
radial-gradient(circle, #05060c 0 calc(10 * var(--u)), #1e8c85 calc(11 * var(--u)) calc(13 * var(--u)), transparent calc(14 * var(--u))),
radial-gradient(circle, #868ba4 0 calc(30 * var(--u)), var(--metal) calc(31 * var(--u)));
transform: rotateY(180deg) translateZ(calc(5 * var(--u)));
}
/* black glass, the border is the bezel */
.face {
display: flex;
flex-direction: column;
align-items: center;
padding: calc(9 * var(--u)) calc(6 * var(--u)) calc(6 * var(--u));
overflow: hidden;
border: calc(5 * var(--u)) solid #05060c;
background: radial-gradient(circle at 50% 30%, #231c42, #05060c 70%);
color: #fff;
font-family: Inter, system-ui, sans-serif;
transform: translateZ(calc(5 * var(--u)));
}
.face::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(125deg, rgb(255 255 255 / 0.16), transparent 38%);
}
.face small {
margin-top: calc(1 * var(--u));
color: #2ee6d6;
font-size: calc(7 * var(--u));
font-weight: 700;
letter-spacing: calc(1.2 * var(--u));
text-transform: uppercase;
}
/* 12 ticks from a repeating conic gradient, a dark disc on top, an activity arc behind */
.dial {
position: relative;
flex: none;
width: calc(50 * var(--u));
height: calc(50 * var(--u));
border-radius: 50%;
background:
radial-gradient(circle, #0b0d18 0 calc(19 * var(--u)), transparent calc(19.5 * var(--u))),
repeating-conic-gradient(from -1.5deg, rgb(255 255 255 / 0.85) 0 3deg, transparent 3deg 30deg),
conic-gradient(#ff4d9d 0 68%, rgb(255 77 157 / 0.25) 0);
box-shadow: inset 0 0 0 calc(1 * var(--u)) rgb(255 255 255 / 0.12);
}
.dial::after {
content: '';
position: absolute;
top: calc(50% - 2.5 * var(--u));
left: calc(50% - 2.5 * var(--u));
width: calc(5 * var(--u));
height: calc(5 * var(--u));
border-radius: 50%;
background: #2ee6d6;
}
/* stands on the centre, turns about its foot */
.hand {
position: absolute;
bottom: 50%;
border-radius: calc(2 * var(--u));
background: #fff;
transform-origin: 50% 100%;
}
.hand.h { left: calc(50% - 1.5 * var(--u)); width: calc(3 * var(--u)); height: calc(12 * var(--u)); transform: rotate(var(--h, 300deg)); }
.hand.m { left: calc(50% - 1 * var(--u)); width: calc(2 * var(--u)); height: calc(18 * var(--u)); transform: rotate(var(--m, 54deg)); }
.hand.s { left: calc(50% - 0.5 * var(--u)); width: calc(1 * var(--u)); height: calc(21 * var(--u)); background: #2ee6d6; transform: rotate(var(--s, 180deg)); }
/* two integers from JS become counters; decimal-leading-zero pads them */
.time {
margin-top: calc(5 * var(--u));
counter-reset: hh var(--hh, 10) mm var(--mm, 9);
font: 700 calc(15 * var(--u))/1 'JetBrains Mono', ui-monospace, Consolas, monospace;
letter-spacing: calc(-0.5 * var(--u));
}
.time::before {
content: counter(hh, decimal-leading-zero) ':' counter(mm, decimal-leading-zero);
}
@keyframes sway {
from { transform: rotateX(10deg) rotateY(-30deg); }
to { transform: rotateX(6deg) rotateY(30deg); }
}
const watch = document.querySelector('.watch');
// The site pauses a model by marking the frame's <html data-paused>: then the hands stand still,
// and catch up when it is un-paused. With reduced motion the second hand stays where it is.
const calm = matchMedia('(prefers-reduced-motion: reduce)');
function tick() {
if (document.documentElement.hasAttribute('data-paused')) return;
const d = new Date();
// seconds since midnight: the angles only grow, so no hand sweeps backwards at 59 → 0
const t = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
if (!calm.matches || !watch.style.getPropertyValue('--s')) watch.style.setProperty('--s', t * 6 + 'deg');
watch.style.setProperty('--m', (t / 10).toFixed(1) + 'deg');
watch.style.setProperty('--h', (t / 120).toFixed(2) + 'deg');
// integers for the digital readout (CSS counters)
watch.style.setProperty('--hh', d.getHours());
watch.style.setProperty('--mm', d.getMinutes());
}
tick();
setInterval(tick, 1000);