// Scroll reveal animations // Entry: CSS animation (slide-up + fade-in) — keeps translateY out of the // static hidden state so removing .revealed never shifts layout and can't // cause an intersection-observer feedback loop. // Exit: opacity-only CSS transition — smooth fade, no position change. @keyframes reveal-in { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: translateY(0); } } // Fallback: show content if JS hasn't loaded [data-reveal] { opacity: 1; transform: none; } // Only apply hidden state when JS is ready .js-loaded [data-reveal] { opacity: 0; transition: opacity 0.5s ease-out; will-change: opacity, transform; } .js-loaded [data-reveal].revealed { opacity: 1; animation: reveal-in 0.6s ease-out backwards; } // Stagger delays for grouped items .js-loaded [data-reveal-delay="1"] { transition-delay: 0.1s; animation-delay: 0.1s; } .js-loaded [data-reveal-delay="2"] { transition-delay: 0.2s; animation-delay: 0.2s; } .js-loaded [data-reveal-delay="3"] { transition-delay: 0.3s; animation-delay: 0.3s; } .js-loaded [data-reveal-delay="4"] { transition-delay: 0.4s; animation-delay: 0.4s; } // Respect reduced motion preference @media (prefers-reduced-motion: reduce) { .js-loaded [data-reveal] { opacity: 1; transform: none; transition: none; animation: none; } }