diff --git a/src/client/components/EventRow.tsx b/src/client/components/EventRow.tsx index 0d98186..d5115f8 100644 --- a/src/client/components/EventRow.tsx +++ b/src/client/components/EventRow.tsx @@ -28,78 +28,93 @@ export function EventRow({ row, completed, onToggle, onOpen }: EventRowProps) { return (
  • - {/* Game identity: a hue stripe, never an urgency colour. */} - - -
    -
    - - - - {countdown} - -
    - -
    - -
    -
    - + {/* The whole row opens the event. A single full-bleed target gives a + generous tap area on mobile and one unambiguous hover region — the + content above it is pointer-transparent so clicks fall through. */} + +
    + {/* Game identity rail. Grows on hover — the cheapest possible signal + that this row is live under the cursor. */} + + +
    +
    +
    + + {game.short} + + + {event.title} + +
    + + + {countdown} + +
    + + {event.summary !== null && ( +

    + {event.summary} +

    + )} + +
    + +
    +
    + + {/* Sits above the row target so ticking done never opens the sheet. */} + +
  • ); } diff --git a/src/client/styles.css b/src/client/styles.css index 5caa3f3..e489056 100644 --- a/src/client/styles.css +++ b/src/client/styles.css @@ -113,6 +113,110 @@ body { } } +/* ---- Event rows -------------------------------------------------------- */ +/* + * Hover should feel instant. Everything here is fast (110–160ms) and eased out, + * so the row responds on the way in and settles on the way out — a slow + * symmetric fade is what makes an interface feel laggy even at 60fps. + * + * All hover states are behind `hover: hover` so a tap on a touch device does + * not leave a row stuck in its hover appearance. + */ +.event-row { + --row-bg: transparent; + background: var(--row-bg); + transition: background-color 130ms ease-out; +} +.event-row.is-complete { + opacity: 0.4; +} + +.row-rail { + background: var(--hue); + opacity: 0.55; + transform: scaleY(0.82); + transform-origin: center; + transition: + transform 160ms cubic-bezier(0.2, 0.7, 0.3, 1), + opacity 130ms ease-out, + box-shadow 160ms ease-out; +} + +.row-title, +.row-count, +.row-summary { + transition: color 130ms ease-out; +} + +.row-check { + transition: + border-color 130ms ease-out, + color 130ms ease-out, + transform 120ms cubic-bezier(0.2, 0.7, 0.3, 1); +} + +@media (hover: hover) { + .event-row:hover { + --row-bg: color-mix(in srgb, var(--color-raised) 62%, transparent); + } + .event-row:hover .row-rail { + opacity: 1; + transform: scaleY(1); + box-shadow: 0 0 10px -2px var(--hue); + } + /* The meter is the signature element, so it is the thing that visibly + wakes up: live ticks gain a little light under the cursor. */ + .event-row:hover .meter-tick[data-live="true"] { + filter: brightness(1.28) saturate(1.08); + } + .event-row:hover .row-summary { + color: var(--color-ink); + } + .event-row:hover .row-check { + border-color: var(--color-faint); + color: var(--color-muted); + } + .row-check:hover { + transform: scale(1.08); + border-color: var(--color-muted); + color: var(--color-ink); + } +} + +/* Press feedback lands on both mouse and touch — the tactile half of + "responsive". */ +.event-row:active { + --row-bg: color-mix(in srgb, var(--color-raised) 82%, transparent); + transition-duration: 40ms; +} +.row-check:active { + transform: scale(0.9); + transition-duration: 60ms; +} + +.meter-tick { + transition: + background-color 240ms ease, + filter 130ms ease-out; +} + +@media (prefers-reduced-motion: reduce) { + .event-row, + .row-rail, + .row-title, + .row-count, + .row-summary, + .row-check, + .meter-tick { + transition: none; + } + .row-check:hover, + .row-check:active, + .event-row:hover .row-rail { + transform: none; + } +} + /* Focus ring: visible, and never removed. */ :where(a, button, [tabindex]):focus-visible { outline: 2px solid var(--color-near);