Project card
A project at a glance. The details are one hover away.
Hover the card or tap its title. Check off a task, hover a face, change the status. Tab works too; Esc steps back.
Sample project and people. An interaction study, nothing is saved.
Key CSS & implementation tricks
.reveal {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 380ms cubic-bezier(.22, 1, .36, 1);
}
.card[data-open="true"] .reveal { grid-template-rows: 1fr; }
.reveal-inner { min-height: 0; overflow: hidden; }
/* Stagger only on the way in; closing starts at once. */
.card[data-open="true"] .task {
transition-delay: calc(var(--i) * 26ms + 70ms);
}<!-- one circle per segment, offset by i × 20 -->
<circle cx="12" cy="12" r="9" pathLength="100"
stroke-dashoffset="-23" class="seg" />
.ring svg { rotate: -90deg; }
.ring-fill .seg {
stroke-dasharray: 0 100;
transition: stroke-dasharray 460ms cubic-bezier(.65, 0, .35, 1);
}
.ring-fill .seg[data-on] { stroke-dasharray: 14 86; }.swap-window { display: inline-grid; overflow: hidden; }
.swap-window > * { grid-area: 1 / 1; }
// Read the visible pose first, then cancel and leave from it.
const seen = getComputedStyle(previous);
const from = { transform: seen.transform, opacity: seen.opacity };
previous.getAnimations().forEach(a => a.cancel());
previous.animate([from, {
transform: `translateY(${-70 * dir}%)`, opacity: 0
}], { duration: 200, easing });card.addEventListener('pointerenter', e => {
if (e.pointerType === 'mouse') { hovered = true; sync(); }
});
card.addEventListener('focusin', e => {
keyboardInside = e.target.matches(':focus-visible'); sync();
});
const open = pinned || hovered || picker || keyboardInside;
details.inert = !open;