diff --git a/hero.js b/hero.js index 13ecc74..3393b6c 100644 --- a/hero.js +++ b/hero.js @@ -149,8 +149,13 @@ const CSS = ` /* Sticky at the viewport top for the whole scroll (its containing block is the page itself), z-index 0 so the cards (z-index 1) stack over it. The height is a floor, frozen to a measured pixel - value by the module once it runs (see size()); the svh ladder is - the first paint and the no-JS fallback. A floor and not a height, + value by the module once it runs (see size()) - as a custom + property on , never as an inline style on the header: the + module runs before the app hydrates, and an attribute written + onto an element the app is about to hydrate throws the hydration + off, and with it client-side navigation (uhhm's hero shows the + same). The svh ladder is the first paint and the no-JS fallback. + A floor and not a height, so a copy that needs more than a screen (a phone) grows the header rather than being clipped, and the cards start under it. */ position: sticky; @@ -160,6 +165,7 @@ const CSS = ` width: 100%; min-height: 100vh; min-height: 100svh; + min-height: var(--westra-hero-h, 100svh); padding: 0; justify-content: flex-end; overflow: hidden; @@ -200,8 +206,11 @@ const CSS = ` transform: none; } +/* The copy's fade is a custom property too, for the same reason. */ .hero-module .hero-copy { position: static; + opacity: var(--westra-copy, 1); + visibility: var(--westra-copy-vis, visible); padding: 0 1.5rem 3.5rem; gap: 0.6rem; text-shadow: 0 0.12em 1.4em var(--hero-glow); @@ -325,7 +334,9 @@ class WestraHero { ensureStyle(); this.stopped = false; this.hero = container.closest('.hero') || container.parentElement; - this.copy = this.hero ? this.hero.querySelector('.hero-copy') : null; + // Where the module's custom properties go: an element the app + // never hydrates, so writing to it cannot disturb hydration. + this.host = document.body; this.reduced = window.matchMedia('(prefers-reduced-motion: reduce)'); this.dark = window.matchMedia('(prefers-color-scheme: dark)'); @@ -418,12 +429,11 @@ class WestraHero { const p = ease(this.progress); this.name.style.setProperty('--p', p.toFixed(4)); // The copy goes sooner than the name: it has to be gone before - // the first card reaches it, on a short screen too. - if (this.copy) { - const visible = 1 - clamp01(this.progress * 2.5); - this.copy.style.opacity = visible.toFixed(3); - this.copy.style.visibility = visible <= 0.01 ? 'hidden' : ''; - } + // the first card reaches it, on a short screen too. Written + // on , which the app does not hydrate (see the CSS). + const visible = 1 - clamp01(this.progress * 2.5); + this.host.style.setProperty('--westra-copy', visible.toFixed(3)); + this.host.style.setProperty('--westra-copy-vis', visible <= 0.01 ? 'hidden' : 'visible'); this.draw(this.reduced.matches ? 0 : (now - this.t0) / 1000); @@ -446,7 +456,7 @@ class WestraHero { size() { this.width = window.innerWidth; this.height = Math.max(1, window.innerHeight); - if (this.hero) this.hero.style.minHeight = this.height + 'px'; + this.host.style.setProperty('--westra-hero-h', this.height + 'px'); this.fit(); } @@ -583,10 +593,8 @@ class WestraHero { const lose = this.gl.getExtension('WEBGL_lose_context'); if (lose) lose.loseContext(); } - if (this.hero) this.hero.style.minHeight = ''; - if (this.copy) { - this.copy.style.opacity = ''; - this.copy.style.visibility = ''; + for (const prop of ['--westra-hero-h', '--westra-copy', '--westra-copy-vis']) { + this.host.style.removeProperty(prop); } this.canvas.remove(); this.name.remove();