diff --git a/website/alternatives/index.html b/website/alternatives/index.html index 8c31896..aaac15e 100644 --- a/website/alternatives/index.html +++ b/website/alternatives/index.html @@ -123,6 +123,8 @@ + +
@@ -138,6 +140,12 @@
+
+ + + + +
@@ -155,12 +163,12 @@
- - - - - - + + + + + +
diff --git a/website/alternatives/kosmi.html b/website/alternatives/kosmi.html index fe203bd..1d91e35 100644 --- a/website/alternatives/kosmi.html +++ b/website/alternatives/kosmi.html @@ -129,6 +129,8 @@ + +
@@ -144,6 +146,12 @@
+
+ + + + +
@@ -161,12 +169,12 @@
- - - - - - + + + + + +
diff --git a/website/alternatives/scener.html b/website/alternatives/scener.html index 9a71030..f56bb81 100644 --- a/website/alternatives/scener.html +++ b/website/alternatives/scener.html @@ -129,6 +129,8 @@ + +
@@ -144,6 +146,12 @@
+
+ + + + +
@@ -161,12 +169,12 @@
- - - - - - + + + + + +
diff --git a/website/alternatives/screen-sharing.html b/website/alternatives/screen-sharing.html index 6f05e4c..0ea0b04 100644 --- a/website/alternatives/screen-sharing.html +++ b/website/alternatives/screen-sharing.html @@ -129,6 +129,8 @@ + +
@@ -144,6 +146,12 @@
+
+ + + + +
@@ -161,12 +169,12 @@
- - - - - - + + + + + +
diff --git a/website/alternatives/teleparty.html b/website/alternatives/teleparty.html index 6c3c4c6..9bb3b48 100644 --- a/website/alternatives/teleparty.html +++ b/website/alternatives/teleparty.html @@ -129,6 +129,8 @@ + +
@@ -144,6 +146,12 @@
+
+ + + + +
@@ -161,12 +169,12 @@
- - - - - - + + + + + +
diff --git a/website/alternatives/twoseven.html b/website/alternatives/twoseven.html index c6b125e..9003f8c 100644 --- a/website/alternatives/twoseven.html +++ b/website/alternatives/twoseven.html @@ -129,6 +129,8 @@ + +
@@ -144,6 +146,12 @@
+
+ + + + +
@@ -161,12 +169,12 @@
- - - - - - + + + + + +
diff --git a/website/alternatives/watch2gether.html b/website/alternatives/watch2gether.html index 3ea86da..5cc901f 100644 --- a/website/alternatives/watch2gether.html +++ b/website/alternatives/watch2gether.html @@ -129,6 +129,8 @@ + +
@@ -144,6 +146,12 @@
+
+ + + + +
@@ -161,12 +169,12 @@
- - - - - - + + + + + +
diff --git a/website/app.js b/website/app.js index a2ee7ae..b30f1bc 100644 --- a/website/app.js +++ b/website/app.js @@ -60,6 +60,9 @@ document.addEventListener('DOMContentLoaded', () => { } }; + // Populated by the forest renderer further down (no-op until then) + let forestGreet = null; + // Theme toggle (saved preference wins; otherwise follows system preference, // pre-paint class is applied by lang-init.js to avoid a flash) const themeToggle = document.getElementById('theme-toggle'); @@ -108,6 +111,14 @@ document.addEventListener('DOMContentLoaded', () => { applyTheme(nextTheme); safeSetLocalStorage(themeStorageKey, nextTheme); syncThemeMeta(); + // Switching into light mode flares the horizon once (sunrise) + const scene = document.querySelector('.bg-nature'); + if (nextTheme === 'light' && scene && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + scene.classList.remove('sunrise'); + void scene.offsetWidth; + scene.classList.add('sunrise'); + setTimeout(() => scene.classList.remove('sunrise'), 1700); + } }); } @@ -200,6 +211,7 @@ document.addEventListener('DOMContentLoaded', () => { entries.forEach(entry => { if (entry.isIntersecting) { history.replaceState(null, null, '#' + entry.target.id); + if (forestGreet) forestGreet(); } }); }, { threshold: 0.3 }); @@ -220,11 +232,18 @@ document.addEventListener('DOMContentLoaded', () => { // pointer devices the layers tilt slightly toward the cursor. const bambooFar = document.getElementById('bamboo-far'); const bambooNear = document.getElementById('bamboo-near'); - if (bambooFar && bambooNear && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + const forestScene = document.querySelector('.bg-nature'); + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (bambooFar && bambooNear && !prefersReducedMotion) { const depthDay = document.querySelector('.bg-depth-day'); const depthDusk = document.querySelector('.bg-depth-dusk'); const duskTint = document.querySelector('.bg-dusk-tint'); + const fireflies = Array.from(document.querySelectorAll('.firefly-wrap'), (el) => ({ el, x: 0, y: 0 })); + const canHover = window.matchMedia('(hover: hover)').matches; let mouseX = 0; + let pointerX = -9999; + let pointerY = -9999; + let depthStep = ''; let framePending = false; const renderForest = () => { @@ -238,6 +257,39 @@ document.addEventListener('DOMContentLoaded', () => { if (depthDay) depthDay.style.opacity = (1 - depth * 0.75).toFixed(3); if (depthDusk) depthDusk.style.opacity = (0.35 + depth * 0.65).toFixed(3); if (duskTint) duskTint.style.opacity = (depth * 0.85).toFixed(3); + + // Depth of field: step the far layer's blur on threshold crossings + // only (filter changes are too expensive to run per frame) + if (forestScene) { + const step = depth < 0.33 ? '' : depth < 0.66 ? 'depth-mid' : 'depth-deep'; + if (step !== depthStep) { + forestScene.classList.remove('depth-mid', 'depth-deep'); + if (step) forestScene.classList.add(step); + depthStep = step; + } + } + + // Fireflies shy away from the cursor and drift back once it leaves + let settling = false; + for (const fly of fireflies) { + const cx = fly.el.offsetLeft + 2; + const cy = fly.el.offsetTop + 2; + const dx = cx - pointerX; + const dy = cy - pointerY; + const dist = Math.hypot(dx, dy); + let tx = 0; + let ty = 0; + if (dist < 130 && dist > 0.001) { + const push = ((130 - dist) / 130) * 46; + tx = (dx / dist) * push; + ty = (dy / dist) * push; + } + fly.x += (tx - fly.x) * 0.14; + fly.y += (ty - fly.y) * 0.14; + if (Math.abs(tx - fly.x) > 0.4 || Math.abs(ty - fly.y) > 0.4) settling = true; + fly.el.style.transform = `translate(${fly.x.toFixed(1)}px, ${fly.y.toFixed(1)}px)`; + } + if (settling) requestForestFrame(); }; const requestForestFrame = () => { @@ -246,16 +298,51 @@ document.addEventListener('DOMContentLoaded', () => { requestAnimationFrame(renderForest); } }; + // Let one firefly greet newly reached sections (used by the section + // observer below; index rotates so it is not always the same one) + let helloIndex = 0; + forestGreet = () => { + if (!fireflies.length) return; + const wrap = fireflies[helloIndex % fireflies.length].el; + helloIndex += 1; + wrap.classList.remove('firefly-hello'); + void wrap.offsetWidth; + wrap.classList.add('firefly-hello'); + setTimeout(() => wrap.classList.remove('firefly-hello'), 1300); + }; window.addEventListener('scroll', requestForestFrame, { passive: true }); window.addEventListener('resize', requestForestFrame, { passive: true }); - if (window.matchMedia('(hover: hover)').matches) { + if (canHover) { window.addEventListener('pointermove', (e) => { mouseX = (e.clientX / window.innerWidth - 0.5) * 2; + pointerX = e.clientX; + pointerY = e.clientY; requestForestFrame(); }, { passive: true }); + window.addEventListener('pointerleave', () => { + pointerX = -9999; + pointerY = -9999; + requestForestFrame(); + }); } renderForest(); + + // Wind gusts: every 12-24s the sway variables ramp up for ~3s. The + // custom properties are registered via @property, so they transition + // smoothly inside the running keyframes instead of jumping. + if (forestScene) { + const scheduleGust = () => { + setTimeout(() => { + forestScene.classList.add('gusting'); + setTimeout(() => { + forestScene.classList.remove('gusting'); + scheduleGust(); + }, 3000); + }, 12000 + Math.random() * 12000); + }; + scheduleGust(); + } } // Invite Detection & Bridge diff --git a/website/datenschutz-de.html b/website/datenschutz-de.html index 6160106..7df088c 100644 --- a/website/datenschutz-de.html +++ b/website/datenschutz-de.html @@ -67,6 +67,8 @@ + +
@@ -82,6 +84,12 @@
+
+ + + + +
@@ -99,12 +107,12 @@
- - - - - - + + + + + +
diff --git a/website/impressum-de.html b/website/impressum-de.html index f44fb02..9b54bf5 100644 --- a/website/impressum-de.html +++ b/website/impressum-de.html @@ -67,6 +67,8 @@ + +
@@ -82,6 +84,12 @@
+
+ + + + +
@@ -99,12 +107,12 @@
- - - - - - + + + + + +
diff --git a/website/imprint.html b/website/imprint.html index 0ac7508..6aeb49a 100644 --- a/website/imprint.html +++ b/website/imprint.html @@ -67,6 +67,8 @@ + +
@@ -82,6 +84,12 @@
+
+ + + + +
@@ -99,12 +107,12 @@
- - - - - - + + + + + +
diff --git a/website/join.html b/website/join.html index 8262bc3..93a0ccd 100644 --- a/website/join.html +++ b/website/join.html @@ -52,6 +52,8 @@ + +
@@ -67,6 +69,12 @@
+
+ + + + +
@@ -84,12 +92,12 @@
- - - - - - + + + + + +
diff --git a/website/privacy.html b/website/privacy.html index 24bc63b..084bc56 100644 --- a/website/privacy.html +++ b/website/privacy.html @@ -67,6 +67,8 @@ + +
@@ -82,6 +84,12 @@
+
+ + + + +
@@ -99,12 +107,12 @@
- - - - - - + + + + + +
diff --git a/website/style.css b/website/style.css index e175bfc..d2782bb 100644 --- a/website/style.css +++ b/website/style.css @@ -115,6 +115,30 @@ body { } /* --- Animated Bamboo Forest Background (v3 Greens Update) --- */ +/* Registered so wind gusts can TRANSITION these instead of jumping */ +@property --sway { + syntax: ''; + initial-value: 1.2deg; + inherits: true; +} + +@property --leaf-drift { + syntax: ''; + initial-value: 26px; + inherits: true; +} + +.bg-nature { + --sway: 1.2deg; + --leaf-drift: 26px; + transition: --sway 1.6s ease-in-out, --leaf-drift 1.6s ease-in-out; +} + +.bg-nature.gusting { + --sway: 3.4deg; + --leaf-drift: 68px; +} + .bg-nature { position: fixed; top: 0; @@ -143,12 +167,37 @@ body { height: 140%; border-radius: 999px; filter: blur(5.5px); + transition: filter 1.2s ease; background: linear-gradient(180deg, oklch(0.32 0.05 145 / 0.35), oklch(0.24 0.04 140 / 0.15)); } #bamboo-far .far-1 { left: 2%; width: 26px; } #bamboo-far .far-2 { left: 7.5%; width: 18px; } #bamboo-far .far-3 { right: 4%; width: 30px; } +#bamboo-far .far-4 { left: 13%; width: 20px; } +#bamboo-far .far-5 { right: 10.5%; width: 16px; } + +/* Two barely-there trunks break up the empty middle without competing with + the content. They share the far layer's single parallax transform. */ +#bamboo-far::before, +#bamboo-far::after { + content: ''; + position: absolute; + top: -20%; + height: 140%; + border-radius: 999px; + filter: blur(8px); + opacity: 0.28; + background: linear-gradient(180deg, oklch(0.34 0.05 145 / 0.3), oklch(0.23 0.04 140 / 0.1)); +} + +#bamboo-far::before { left: 24%; width: 13px; } +#bamboo-far::after { right: 25%; width: 17px; } + +/* Depth of field: the far layer blurs further as you descend (stepped by + JS on threshold crossings, never per frame) */ +.bg-nature.depth-mid #bamboo-far .bamboo-stalk { filter: blur(6.5px); } +.bg-nature.depth-deep #bamboo-far .bamboo-stalk { filter: blur(8px); } /* (b) Near bamboo layer: sharp stalks with nodes and leaves, gentle sway */ #bamboo-near { @@ -169,6 +218,7 @@ body { #bamboo-near .near-1 { left: 4.5%; width: 13px; animation-duration: 9.2s; } #bamboo-near .near-2 { right: 7%; width: 11px; animation-duration: 11s; animation-delay: -3.5s; } +#bamboo-near .near-3 { left: 10%; width: 9px; animation-duration: 12.5s; animation-delay: -6s; opacity: 0.75; } #bamboo-near .bamboo-node { position: absolute; @@ -194,10 +244,11 @@ body { #bamboo-near .near-1 .leaf-1 { top: 27.5%; left: 10px; transform: rotate(18deg); } #bamboo-near .near-1 .leaf-2 { top: 47.5%; left: -28px; transform: rotate(-160deg); width: 22px; height: 10px; } #bamboo-near .near-2 .leaf-1 { top: 44%; left: -26px; transform: rotate(-155deg); } +#bamboo-near .near-3 .leaf-1 { top: 33%; left: 7px; transform: rotate(24deg); width: 18px; height: 9px; } @keyframes bambooSway { 0%, 100% { transform: rotate(0); } - 50% { transform: rotate(1.2deg); } + 50% { transform: rotate(var(--sway, 1.2deg)); } } /* (d) Falling leaves: outer element falls + rotates, inner leaf sways */ @@ -245,7 +296,7 @@ body { @keyframes leafSway { 0%, 100% { margin-left: 0; } - 50% { margin-left: 26px; } + 50% { margin-left: var(--leaf-drift, 26px); } } /* Scroll journey: JS drives the opacity of these two groups so the page @@ -377,11 +428,32 @@ body { 50% { opacity: 0.85; } } -/* (i) Fireflies */ -.firefly { +/* Theme switch to light: the horizon flares up once like a sunrise */ +.bg-nature.sunrise .bg-horizon { + animation: + horizonBreathe 10s ease-in-out infinite, + sunriseFlash 1.6s ease-out; +} + +@keyframes sunriseFlash { + 0% { opacity: 0.5; } + 30% { opacity: 1; } + 100% { opacity: 0.5; } +} + +/* (i) Fireflies: the wrapper holds the base position and is what the JS + pushes away from the cursor; the inner dot pulses (transform) and + wanders (translate) independently. */ +.firefly-wrap { position: absolute; width: 4px; height: 4px; +} + +.firefly { + display: block; + width: 4px; + height: 4px; border-radius: 50%; background: oklch(0.68 0.13 150); box-shadow: 0 0 8px oklch(0.68 0.13 150); @@ -390,19 +462,38 @@ body { fireflyWander 21s ease-in-out infinite alternate; } -.firefly:nth-child(even) { +.fw-2 .firefly, +.fw-4 .firefly, +.fw-6 .firefly { width: 3px; height: 3px; background: oklch(0.62 0.14 45); box-shadow: 0 0 8px oklch(0.62 0.14 45); } -.firefly-1 { top: 32%; left: 12%; animation-delay: -0.4s, -4s; } -.firefly-2 { top: 58%; left: 22%; animation-duration: 4.4s, 26s; animation-delay: -2.1s, -11s; } -.firefly-3 { top: 44%; left: 46%; animation-duration: 3.4s, 18s; animation-delay: -1.2s, -7s; } -.firefly-4 { top: 70%; left: 64%; animation-duration: 4.1s, 29s; animation-delay: -3s, -19s; } -.firefly-5 { top: 26%; left: 78%; animation-duration: 3.6s, 23s; animation-delay: -1.8s, -14s; } -.firefly-6 { top: 62%; left: 90%; animation-duration: 4.2s, 17s; animation-delay: -0.9s, -2s; } +.fw-1 { top: 32%; left: 12%; } +.fw-2 { top: 58%; left: 22%; } +.fw-3 { top: 44%; left: 46%; } +.fw-4 { top: 70%; left: 64%; } +.fw-5 { top: 26%; left: 78%; } +.fw-6 { top: 62%; left: 90%; } + +.fw-1 .firefly { animation-delay: -0.4s, -4s; } +.fw-2 .firefly { animation-duration: 4.4s, 26s; animation-delay: -2.1s, -11s; } +.fw-3 .firefly { animation-duration: 3.4s, 18s; animation-delay: -1.2s, -7s; } +.fw-4 .firefly { animation-duration: 4.1s, 29s; animation-delay: -3s, -19s; } +.fw-5 .firefly { animation-duration: 3.6s, 23s; animation-delay: -1.8s, -14s; } +.fw-6 .firefly { animation-duration: 4.2s, 17s; animation-delay: -0.9s, -2s; } + +/* One firefly greets you when a new section scrolls into view */ +.firefly-wrap.firefly-hello { + animation: fireflyHello 1.2s ease-out; +} + +@keyframes fireflyHello { + 0%, 100% { transform: scale(1); filter: brightness(1); } + 35% { transform: scale(2.4); filter: brightness(1.7); } +} @keyframes fireflyPulse { 0%, 100% { opacity: 0.2; transform: scale(1); } @@ -443,6 +534,22 @@ body { background-size: 160px 160px; } +@media (max-width: 768px) { + /* Half the leaves, no god rays / light sweep / extra stalks: the scene + stays recognisable while the animation count roughly halves. */ + .fall-leaf-5, + .fall-leaf-6, + .fall-leaf-7, + .fall-leaf-8, + .bg-godrays, + .bg-light-sweep, + #bamboo-far .far-4, + #bamboo-far .far-5, + #bamboo-near .near-3 { + display: none; + } +} + @media (prefers-reduced-motion: reduce) { .bg-nature *, .bg-nature *::before, @@ -5265,6 +5372,19 @@ header[id] { box-shadow: 0 0 8px var(--accent-glow); } +.scroll-progress-bar::after { + content: ''; + position: absolute; + right: -3px; + top: 50%; + width: 10px; + height: 7px; + border-radius: 0 70% 0 70%; + background: var(--accent-green); + box-shadow: 0 0 6px var(--accent-glow); + transform: translateY(-50%) rotate(24deg); +} + /* Navigation logo pop & spin */ .nav-logo-img { animation: logo-entrance 1.2s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; diff --git a/website/template.html b/website/template.html index 6ec5708..71bd297 100644 --- a/website/template.html +++ b/website/template.html @@ -232,6 +232,8 @@ + +
@@ -247,6 +249,12 @@
+
+ + + + +
@@ -264,12 +272,12 @@
- - - - - - + + + + + +