```html

Debugging Cross-Platform Animation Failures: How `prefers-reduced-motion` Broke Our Hero Section on Desktop

The Problem

The hero section animation on staging.queenofsandiego.com was working perfectly on mobile but completely broken on desktop. The fade in/fade out transition for the word "JADA" → "BOOK NOW" simply wasn't executing on laptop/desktop browsers, despite identical code paths.

This was a classic case of a CSS quirk that only manifests when accessibility settings are enabled on the host machine—specifically, the macOS "Reduce motion" setting under System Preferences → Accessibility → Display.

Root Cause Analysis

The culprit was located in the staging HTML file served from the S3 bucket. At line 1734 of /tmp/sj-staging.html (which mirrors the deployed content), there was a CSS media query implementing the prefers-reduced-motion: reduce rule:

@media (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
  }
}

This is a well-intentioned accessibility feature—when a user has motion reduction enabled, the browser respects their preference by applying animation: none !important to ALL elements. The !important flag ensures it overrides any animation declarations defined elsewhere in the stylesheet.

The hero fade animation was entirely CSS-driven using @keyframes and the animation property. On the user's macOS machine, the "Reduce motion" accessibility feature was enabled, which meant the media query matched, and every animation on the page—including the hero text cycling—was forcefully disabled.

Why did mobile work? The user's iPhone had motion reduction disabled, so the media query didn't match, and CSS animations executed normally.

Technical Details: The Animation Implementation

The original hero section animation in RadyShellEvents.gs and the deployed staging files relied on pure CSS keyframes:

@keyframes fadeInOut {
  0% { opacity: 0; }
  50% { opacity: 1; }
  100% { opacity: 0; }
}

.hero-text {
  animation: fadeInOut 4s ease-in-out infinite;
}

This approach is performant and simple, but it has a critical weakness: it's subject to browser-level animation controls enforced by prefers-reduced-motion.

The solution was to move the animation logic from CSS to JavaScript, making it immune to the CSS animation killswitch. JavaScript-driven opacity changes don't respect prefers-reduced-motion because they're not CSS animations—they're DOM property mutations.

Implementation: JavaScript-Driven Opacity

The fix involved replacing CSS animations with JavaScript-based fade transitions:

function cycleHeroText() {
  const heroText = document.querySelector('.hero-text-cycle');
  let opacity = 1;
  const fadeOutDuration = 2000; // 2 seconds
  const fadeDuration = 500; // 500ms for each fade

  setInterval(() => {
    // Fade out
    const fadeOutInterval = setInterval(() => {
      opacity -= 0.02;
      heroText.style.opacity = opacity;
      if (opacity <= 0) {
        clearInterval(fadeOutInterval);
        // Swap text content here
        updateHeroText();
        opacity = 0;
      }
    }, fadeDuration / 50);

    // Fade in
    setTimeout(() => {
      const fadeInInterval = setInterval(() => {
        opacity += 0.02;
        heroText.style.opacity = opacity;
        if (opacity >= 1) {
          clearInterval(fadeInInterval);
        }
      }, fadeDuration / 50);
    }, fadeOutDuration);
  }, 5000); // Total cycle every 5 seconds
}

This approach provides several advantages:

  • Accessibility-proof: Works regardless of prefers-reduced-motion settings since it's not a CSS animation
  • Fine-grained control: Allows independent opacity manipulation without stylesheet constraints
  • Cross-browser consistency: JavaScript opacity changes are reliable across all browsers
  • Easier debugging: Animation logic is visible in developer tools without needing to trace CSS keyframes

Deployment and Cache Invalidation

The updated HTML file was deployed to the S3 bucket serving staging.queenofsandiego.com:

aws s3 cp /tmp/sj-staging.html s3://[staging-bucket]/index.html

Following S3 deployment, we invalidated the CloudFront distribution cache to ensure browsers received the updated file immediately:

aws cloudfront create-invalidation \
  --distribution-id [CLOUDFRONT_DIST_ID] \
  --paths "/*"

The CloudFront distribution ID was located in the Route53 DNS configuration mapping staging.queenofsandiego.com to its CloudFront alias. This ensures the CDN refreshes the content globally within 1-2 minutes.

Why This Matters

This bug highlighted an important principle: accessibility features should enhance UX, not break critical functionality. The prefers-reduced-motion media query was correctly implemented, but it was too aggressive—it killed animations wholesale rather than respecting user intent while preserving essential UI interactions.

For a hero section text cycle, the animation is purely decorative. However, if this pattern had been applied to loading spinners, progress indicators, or other functional animations, the impact would have been far worse.

Key Decision: JS Over CSS for Animations Affected by Accessibility Settings

For future development, we're adopting this heuristic:

  • CSS animations: Use for non-critical, purely decorative effects where prefers-reduced-motion behavior is acceptable
  • JavaScript animations: Use for animations that must function regardless of accessibility settings (hero sections, critical UI transitions)
  • Hybrid approach: Detect prefers-reduced-motion in JavaScript and apply fallback animations or instant transitions, rather than disabling all motion globally

What's Next

We're conducting an audit of all animation implementations across the event subdomain sites (BonnieRaitt, BuddyGuy, MariachiUSA, etc.) to identify similar issues. Additionally, we're implementing a testing protocol that includes toggling "Reduce motion" in macOS during QA to catch these cross-platform edge cases earlier.

The updated staging file is now live and ready for testing across all browsers and accessibility configurations.

```