Project card

A project at a glance. The details are one hover away.

Due Oct 24

    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;