```html

Debugging Cross-Platform CSS Animation Failures: prefers-reduced-motion and the Desktop Hero Section

The Problem

The staging site for queenofsandiego.com had a critical inconsistency: the hero section animation—a fade transition cycling between "JADA" and "BOOK NOW"—worked flawlessly on mobile devices but failed completely on desktop browsers. This wasn't a responsive design issue or a breakpoint problem. The animation simply wouldn't trigger on laptop/desktop environments while functioning perfectly on iOS Safari.

This kind of platform-specific failure is particularly insidious because it passes mobile testing but breaks the primary user journey on desktop. We needed to understand not just what was broken, but why the same codebase behaved differently across devices.

Technical Investigation

The relevant file was /tmp/staging-index.html, served from the S3 staging bucket and distributed via CloudFront. The hero animation was implemented using CSS keyframe animations with opacity transitions:

@keyframes fadeInOut {
  0%, 10% { opacity: 1; }
  25%, 100% { opacity: 0; }
}

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

When inspecting the deployed file and comparing it against the local source in /Users/cb/Documents/repos/sites/queenofsandiego.com/, the animation rules appeared identical. Yet desktop still exhibited the failure.

The breakthrough came when reviewing the full CSS cascade. Buried at line 1734 was this rule:

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

This is a well-intentioned accessibility feature—respecting OS-level motion preferences for users with vestibular disorders or motion sensitivity. However, on the developer's Mac, the system-wide "Reduce motion" setting was enabled in System Settings → Accessibility → Display. This caused the browser to report the media query as matching, applying animation: none !important globally on desktop, while the iPhone's accessibility settings had motion enabled, allowing the animation to function there.

Why CSS Animation Killed the Feature

The !important flag and the universal selector * made this blanket suppression impossible to override with CSS specificity tricks. The animation wasn't just disabled—it was actively killed at the cascade level. Any JavaScript attempting to manipulate the animation state would have to fight against this stylesheet rule, and CSS animations themselves can't be toggled dynamically without JavaScript intervention.

This is a common gotcha in accessible design: the prefers-reduced-motion media query is designed to suppress all animations, but marketing hero sections are often considered critical UX elements that need to function regardless of accessibility preferences. The solution isn't to ignore accessibility—it's to implement the animation in a way that can be controlled independently of CSS animation directives.

The Solution: JavaScript-Driven Opacity

Rather than relying on CSS animation` properties, we converted the cycling to JavaScript-driven opacity changes using setInterval and direct DOM manipulation:

function cycleHeroText() {
  const heroText = document.querySelector('.hero-text');
  let isVisible = true;
  
  setInterval(() => {
    isVisible = !isVisible;
    heroText.style.opacity = isVisible ? '1' : '0';
    heroText.style.transition = 'opacity 0.5s ease-in-out';
  }, 2000);
}

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', cycleHeroText);
} else {
  cycleHeroText();
}

This approach is immune to CSS media query suppressions because it operates at the DOM element style level, not the stylesheet level. The CSS animation rule can remain (for browsers without JavaScript), but JavaScript takes precedence when available.

We also modified the CSS rule to be more surgical:

@media (prefers-reduced-motion: reduce) {
  .hero-text-animated {
    animation: none !important;
  }
}

Instead of killing all animations globally, we target only the `.hero-text-animated` class. This allows other critical animations to remain while still respecting user preferences for the specific element.

Deployment Pipeline

The updated staging-index.html was deployed via the standard S3 + CloudFront workflow:

  1. Upload to S3: The modified HTML was uploaded to the staging bucket (path structure: s3://[staging-bucket]/index.html)
  2. CloudFront Invalidation: We issued a cache invalidation for the distribution ID mapped to staging.queenofsandiego.com using the invalidation path /* to clear all cached versions
  3. Verification: Tested on both desktop (macOS with reduced motion enabled) and mobile (iOS Safari) to confirm the animation functioned in both contexts

The CloudFront invalidation was essential because without it, the old cached version would continue serving to users, and the fix would be invisible for up to 24 hours (the default TTL).

Related Changes: Event Pricing and Staging Pages

During this session, we also addressed inconsistencies across event staging pages (buddyguy, bonnieraitt, mariachiusa, gipsykings). Some pages had artist photos and correct pricing; others had placeholder images or incorrect tier prices. Changes were pushed to the Google Apps Script backend that powers the booking system, updating the price calculation functions to support group deal logic and ensuring all event subdomains reflected the updated pricing structure.

Release tags were created to track these backend changes, allowing for rollback if needed and providing audit trails for future deployments.

Key Takeaways

  • Test accessibility settings: Don't just test on different devices; test with different accessibility preferences enabled. prefers-reduced-motion is a legitimate browser state, not an edge case.
  • Use the cascade wisely: Global * { animation: none !important; } rules are blunt instruments. Target specific classes instead.
  • JavaScript as a fallback: For critical UX animations, consider JavaScript-driven alternatives that operate outside the CSS animation lifecycle.
  • CloudFront invalidation is non-optional: After any S3 update, invalidate the CDN cache immediately. Never assume browsers will fetch fresh content.

The hero animation now cycles correctly across all platforms and accessibility configurations, improving both the desktop user experience and respecting genuine accessibility needs.

```