HTML 13 lines
<div class="scene">
<div class="fan">
<div class="fan-slot" tabindex="0">
<div class="fan-card" style="--c:#8b6cff"><i></i><b>Design</b><span></span><span></span></div>
</div>
<div class="fan-slot" tabindex="0">
<div class="fan-card" style="--c:#ff4d9d"><i></i><b>Build</b><span></span><span></span></div>
</div>
<div class="fan-slot" tabindex="0">
<div class="fan-card" style="--c:#2ee6d6"><i></i><b>Ship</b><span></span><span></span></div>
</div>
</div>
</div>
CSS 117 lines
.scene {
/* one base unit: every length below is a multiple of it, so the fan is the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.35vmin;
perspective: calc(800 * var(--u));
}
.fan {
display: flex;
width: calc(156 * var(--u));
height: calc(132 * var(--u));
transform-style: preserve-3d;
/* coplanar with its slots: only the slots may catch the pointer */
pointer-events: none;
}
.fan-slot {
position: relative;
flex: 1;
outline: none;
cursor: pointer;
pointer-events: auto;
transform-style: preserve-3d;
}
/* rest pose: each card a little nearer than the one before so they never z-fight */
.fan-slot:nth-child(1) { --r: -8deg; --z: 0; }
.fan-slot:nth-child(2) { --r: 0deg; --z: calc(12 * var(--u)); }
.fan-slot:nth-child(3) { --r: 8deg; --z: calc(24 * var(--u)); }
.fan-card {
position: absolute;
top: calc(6 * var(--u));
left: calc(50% - 42 * var(--u));
display: grid;
align-content: start;
gap: calc(6 * var(--u));
/* 84 × 120 outside, padding and border included: centred in its slot, 6 units clear top and bottom */
box-sizing: border-box;
width: calc(84 * var(--u));
height: calc(120 * var(--u));
padding: calc(8 * var(--u));
border: calc(1 * var(--u)) solid color-mix(in srgb, var(--c) 70%, transparent);
border-radius: calc(11 * var(--u));
background: linear-gradient(
160deg,
color-mix(in srgb, var(--c) 46%, #141830),
color-mix(in srgb, var(--c) 12%, #141830)
);
box-shadow: 0 calc(12 * var(--u)) calc(18 * var(--u)) calc(-12 * var(--u)) #000;
color: #eceefb;
/* the cards not pointed at dim by brightness, not opacity: see-through, on the light stage their
words were 1.6:1; darkened, they stay on their own dark card on either stage */
filter: brightness(var(--o, 1));
pointer-events: none;
transform-origin: 50% 100%;
/* one transform, fed by variables: every state below only ever changes numbers */
transform: translate3d(var(--x, 0), var(--y, 0), var(--z)) rotateY(var(--ry, 0deg)) rotateZ(var(--r));
transition:
transform 0.45s cubic-bezier(0.3, 1.3, 0.5, 1),
filter 0.3s;
}
.fan-card i {
display: block;
height: calc(42 * var(--u));
border-radius: calc(6 * var(--u));
background:
radial-gradient(circle at 70% 30%, rgb(255 255 255 / 0.55), transparent 45%),
linear-gradient(135deg, var(--c), color-mix(in srgb, var(--c) 40%, #000));
}
.fan-card b {
font-size: calc(11 * var(--u));
line-height: 1;
}
.fan-card span {
display: block;
height: calc(4 * var(--u));
border-radius: calc(2 * var(--u));
background: color-mix(in srgb, #eceefb 28%, transparent);
}
.fan-card span:last-child {
width: 60%;
}
/* something is active: every card dims... */
.fan:hover .fan-slot,
.fan:focus-within .fan-slot {
--o: 0.7;
}
/* ...cards AFTER the active one lean away to the right... */
.fan-slot:hover ~ .fan-slot,
.fan-slot:focus-visible ~ .fan-slot {
--x: calc(16 * var(--u));
--ry: 26deg;
}
/* ...cards BEFORE it lean away to the left (:has() looks forward for a hovered sibling)... */
.fan-slot:has(~ .fan-slot:hover),
.fan-slot:has(~ .fan-slot:focus-visible) {
--x: calc(-16 * var(--u));
--ry: -26deg;
}
/* ...and the active one (this rule is last, so it wins) squares up and comes toward you */
.fan .fan-slot:is(:hover, :focus-visible) {
--o: 1;
--x: 0;
--y: calc(-6 * var(--u));
--z: calc(80 * var(--u));
--r: 0deg;
--ry: 0deg;
}