/* * ParallaxImage styles — served as a plain static asset via * _content/DeepDrftShared.Client/css/parallax.css. * * Why global, not scoped (.razor.css): * DeepDrftShared.Client is a WASM RCL referenced only by DeepDrftPublic.Client, * not by the DeepDrftPublic server host. Blazor merges scoped-CSS bundles only * from RCLs the *host* references, so this component's scoped bundle is absent * from DeepDrftPublic.styles.css and never reaches the SSR first paint — it * arrives only after WASM boots. Structural rules AND the scroll-driven * animation must be present at first paint, so they live here as global CSS, * delivered as a static web asset regardless of which project references the RCL. * * ParallaxImage is the sole producer of .parallax-window / .layer, so unscoped * class selectors are unambiguous. */ .parallax-window { position: relative; overflow: hidden; height: var(--window-height, 300px); width: 100%; } .parallax-window.full-width { width: 100vw; position: relative; left: 50%; right: 50%; margin-left: -50vw; margin-right: -50vw; } .layer { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-repeat: no-repeat; background-position-x: 50%; background-position-y: var(--parallax-from, 0%); } .layer-1 { opacity: 1; } .layer-2 { opacity: 0; transition: opacity 700ms ease; } .parallax-window:hover .layer-2 { opacity: 1; } /* * Scroll-driven parallax, present at SSR first paint (no JS, no custom-property * inheritance chain): * * Before WASM: * The view() timeline animates background-position-y on each .layer directly, * from --parallax-from to --parallax-to (both percentages set inline on * .parallax-window by the component, encoding ParallaxSpeed/InvertDirection). * The layer pans as the window scrolls through the viewport — correct from * first paint. * * After WASM: * JS sets data-parallax-active on .parallax-window, which cancels the CSS * animation (animation: none). JS then drives background-position-y via the * scroll listener. One writer at a time — the two never compete. * * prefers-reduced-motion: * animation: none → static image at --parallax-from. JS also skips its * scroll listener (see parallax.ts), so the image stays put. */ @supports (animation-timeline: view()) { @keyframes parallax-pan { from { background-position-y: var(--parallax-from, 0%); } to { background-position-y: var(--parallax-to, 0%); } } /* Animate layers directly — no --parallax-pos inheritance chain. .parallax-window uses overflow: hidden, which establishes a block formatting context but NOT a scroll container (that needs overflow: scroll/auto), so view() correctly resolves to the root scroller. */ .parallax-window > .layer { animation: parallax-pan linear both; animation-timeline: view(); } /* JS takes over on register: cancel the CSS animation so the two writers to background-position-y never compete. */ .parallax-window[data-parallax-active] > .layer { animation: none; } @media (prefers-reduced-motion: reduce) { .parallax-window > .layer { animation: none; } } } @media (prefers-reduced-motion: reduce) { .layer-2 { transition-duration: 0ms; } }