From 1aeb457b612d00a52faefc48ea51ae02408307ea Mon Sep 17 00:00:00 2001 From: Jon Froehlich Date: Wed, 17 Jun 2026 07:29:46 -0700 Subject: [PATCH] fix(carousel): eliminate banner "blink" on crossfade switch (#1010) The hero/banner carousel used a symmetric opacity crossfade: on switch the outgoing slide faded 1->0 while the incoming faded 0->1 at the same time. Each slide is an opaque unit (black bg + cover image), so while both are semi-transparent mid-transition, up to ~25% of #main-carousel's bright magenta gradient backdrop bleeds through at the midpoint -- a visible pink/magenta "blink" when banners switch. Make the crossfade dip-free by splitting the transition: the incoming slide fades in over a still-opaque outgoing slide, which only snaps to 0 (0s change, delayed by the fade duration) after it's fully covered. An opaque slide always covers the backdrop, so there is zero bleed-through, while the 0.6s crossfade look is preserved. Reduced-motion still gets an instant cut. This is the residual half of #1010: the original 2022 report predated the Bootstrap->vanilla rewrite (#1288). The decode-lag trigger (huge undecoded images) was already fixed by server-side thumbnailing; this addresses the crossfade backdrop-bleed that the rewrite carried over. Verified with a headless-browser A/B at the transition midpoint: the old symmetric fade shows a magenta wash (mean RGB blue channel spikes ~+45); the new asymmetric fade shows a clean photo-to-photo blend. Co-Authored-By: Claude Opus 4.8 (1M context) --- website/static/website/css/carousel_fade.css | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/website/static/website/css/carousel_fade.css b/website/static/website/css/carousel_fade.css index d02708fb..16a368fd 100644 --- a/website/static/website/css/carousel_fade.css +++ b/website/static/website/css/carousel_fade.css @@ -10,6 +10,14 @@ * (.next/.prev/.left/.right); that machinery went away with the Bootstrap * carousel JS (Track A — issues #1288 / #1253), so the fade is now self- * contained CSS rather than a hack layered on Bootstrap's animation. + * + * Dip-free crossfade (issue #1010): the transition is asymmetric on purpose. + * The INCOMING slide fades in (opacity 0 -> 1) while the OUTGOING slide holds + * at full opacity, then snaps to 0 only after the incoming has fully covered + * it. This guarantees an opaque slide always covers the carousel's magenta + * gradient backdrop (#main-carousel in base.css). A naive *symmetric* crossfade + * (both slides fading at once) lets up to ~25% of that backdrop bleed through + * at the midpoint, which read as a "blink"/flash when switching banners. */ .carousel-fade .carousel-inner > .item { @@ -23,19 +31,24 @@ z-index: 0; /* Only the visible slide should capture clicks. */ pointer-events: none; - transition: opacity 0.6s ease-in-out; + /* Deactivating: hold opacity (0s change) until the incoming slide has faded + fully in, i.e. delay the snap-to-0 by the fade duration. */ + transition: opacity 0s ease-in-out 0.6s; } .carousel-fade .carousel-inner > .item.active { opacity: 1; z-index: 1; pointer-events: auto; + /* Activating: fade in over the still-opaque outgoing slide. */ + transition: opacity 0.6s ease-in-out 0s; } /* Respect users who prefer reduced motion: switch instantly, no crossfade. (carousel.js also disables autoplay under this preference.) */ @media (prefers-reduced-motion: reduce) { - .carousel-fade .carousel-inner > .item { + .carousel-fade .carousel-inner > .item, + .carousel-fade .carousel-inner > .item.active { transition: none; } }