diff --git a/src/app/(site)/events/[slugOrId]/event-register-button.tsx b/src/app/(site)/events/[slugOrId]/event-register-button.tsx index 627cb5e5..d2c9da5b 100644 --- a/src/app/(site)/events/[slugOrId]/event-register-button.tsx +++ b/src/app/(site)/events/[slugOrId]/event-register-button.tsx @@ -33,6 +33,8 @@ export function EventRegisterButton({ run(() => registerForEvent({ eventId }), { successMessage: t("registerSuccess"), errorMessage: t("registerError"), + // registerForEvent revalidates /events and /events/. + revalidated: true, }) } > diff --git a/src/app/(site)/polls/[id]/poll-vote-form.tsx b/src/app/(site)/polls/[id]/poll-vote-form.tsx index d3681d2c..261e5db3 100644 --- a/src/app/(site)/polls/[id]/poll-vote-form.tsx +++ b/src/app/(site)/polls/[id]/poll-vote-form.tsx @@ -69,6 +69,8 @@ export function PollVoteForm({ run(() => voteOnPoll({ pollId, votes }), { successMessage: t("voteSuccess"), errorMessage: t("voteError"), + // voteOnPoll revalidates /polls and /polls/. + revalidated: true, }); } diff --git a/src/app/globals.css b/src/app/globals.css index a9bd9afd..ecea745a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -182,6 +182,88 @@ html { animation: page-enter-rise 240ms ease-out both; } +/* ── Radio player entrance (was a motion/react initial/animate pair) ────── */ +@keyframes radio-player-enter { + from { + opacity: 0; + transform: translate3d(0, 20px, 0) scale(0.95); + } + to { + opacity: 1; + transform: none; + } +} +.radio-player-enter { + animation: radio-player-enter 400ms cubic-bezier(0.25, 0.1, 0.25, 1) both; +} + +/* ── Animated counter entrance (was motion/react on a ) ───────────── */ +@keyframes counter-rise { + from { + opacity: 0; + transform: translate3d(0, 12px, 0); + } + to { + opacity: 1; + transform: none; + } +} +.counter-rise { + animation: counter-rise 500ms ease-out both; +} + +/* ── Scroll reveal (was motion/react whileInView) ───────────────────────── */ +.motion-reveal { + opacity: 0; +} +@keyframes motion-reveal-rise { + from { + opacity: 0; + transform: translate3d(0, 24px, 0); + } + to { + opacity: 1; + transform: none; + } +} +.motion-reveal-in { + animation: motion-reveal-rise 550ms cubic-bezier(0.22, 1, 0.36, 1) both; +} +/* The reveal hides its content until the animation runs, so a visitor who asks + for less motion must get it back immediately. The global reduced-motion block + below already flattens the animation duration; this restores the content. */ +@media (prefers-reduced-motion: reduce) { + .motion-reveal { + opacity: 1; + } +} + +/* ── Photo lightbox (was an AnimatePresence opacity pair) ───────────────── */ +@keyframes lightbox-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes lightbox-fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} +.lightbox-enter { + animation: lightbox-fade-in 200ms ease-out both; +} +/* Paired with the 180ms unmount delay in PhotoLightbox: 200ms in, 180ms out, + matching the 200ms the previous motion transition used. */ +.lightbox-exit { + animation: lightbox-fade-out 180ms ease-in both; +} + /* ── Route progress indicator ─────────────────────────────────────────── */ .route-progress { position: fixed; @@ -941,7 +1023,10 @@ a:hover { font-weight: 700; font-size: 14px; cursor: pointer; - min-height: 40px; + /* 44px: the comfortable touch-target size (WCAG 2.1 AAA 2.5.5 / Apple HIG). + WCAG 2.2 AA only asks for 24px, so this is a comfort floor rather than a + compliance fix, but every button in the system shares this class. */ + min-height: 44px; transition: all 0.18s ease; background: color-mix( in srgb, @@ -1534,15 +1619,85 @@ a.content-card { outline: 3px solid color-mix(in srgb, var(--color-primary) 42%, transparent); outline-offset: 3px; } -.cms-info-backdrop { +/* Centering layer. Transparent and click-through itself, so only the backdrop + button and the dialog catch pointer events. */ +.cms-info-layer { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; padding: 1rem; + pointer-events: none; +} +.cms-info-backdrop { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + padding: 0; + border: 0; + cursor: default; background: rgba(8, 10, 14, 0.68); backdrop-filter: blur(6px); + pointer-events: auto; +} + +/* Enter/exit were motion/react opacity pairs; the dialog itself also used a + scale/translate pair. Paired with the 180ms unmount delay in CmsInfoPopup. */ +@keyframes cms-info-backdrop-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes cms-info-backdrop-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} +@keyframes cms-info-dialog-in { + from { + opacity: 0; + transform: scale(0.95) translate3d(0, 10px, 0); + } + to { + opacity: 1; + transform: none; + } +} +@keyframes cms-info-dialog-out { + from { + opacity: 1; + transform: none; + } + to { + opacity: 0; + transform: scale(0.95) translate3d(0, 10px, 0); + } +} +.cms-info-backdrop-enter { + animation: cms-info-backdrop-in 200ms ease-out both; +} +.cms-info-backdrop-exit { + animation: cms-info-backdrop-out 180ms ease-in both; +} +.cms-info-dialog-enter { + animation: cms-info-dialog-in 200ms ease-out both; +} +.cms-info-dialog-exit { + animation: cms-info-dialog-out 180ms ease-in both; +} +/* Sits above the backdrop button inside the pointer-transparent layer. */ +.cms-info-dialog-layer { + pointer-events: auto; + position: relative; + z-index: 1; } .cms-info-dialog { position: relative; @@ -1573,8 +1728,10 @@ a.content-card { top: 0.75rem; right: 0.75rem; display: grid; - width: 2rem; - height: 2rem; + /* 44px touch target (was 2rem/32px) while keeping the circular button visually + the same weight via its border and glyph. */ + width: 2.75rem; + height: 2.75rem; place-items: center; border: 1px solid color-mix( diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 423d9bb9..8c39c8e7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -82,6 +82,12 @@ export default async function RootLayout({ > + {/* Scroll-revealed content starts transparent and is only shown by a + client-side animation. Without JavaScript that animation never + runs, so force it visible instead of leaving the page blank. */} +