Enhance forest scene with mid bamboo layer, butterflies, click confetti, and demo scene polish

This commit is contained in:
Timo
2026-07-11 06:08:12 +02:00
parent 21383b8b9c
commit 04df35bbe6
12 changed files with 1060 additions and 182 deletions
+382 -30
View File
@@ -163,8 +163,8 @@ body {
#bamboo-far .bamboo-stalk {
position: absolute;
top: -20%;
height: 140%;
top: -70%;
height: 240%;
border-radius: 999px;
filter: blur(5.5px);
transition: filter 1.2s ease;
@@ -183,8 +183,8 @@ body {
#bamboo-far::after {
content: '';
position: absolute;
top: -20%;
height: 140%;
top: -70%;
height: 240%;
border-radius: 999px;
filter: blur(8px);
opacity: 0.28;
@@ -199,6 +199,71 @@ body {
.bg-nature.depth-mid #bamboo-far .bamboo-stalk { filter: blur(6.5px); }
.bg-nature.depth-deep #bamboo-far .bamboo-stalk { filter: blur(8px); }
/* (a2) Mid bamboo layer: sits between far and near with its own parallax
rate (app.js) and half the near layer's sway, so the three layers drift
apart subtly instead of moving as one sheet. Landing page only — the
static pages keep the lighter two-layer scene. */
#bamboo-mid {
position: absolute;
inset: 0;
will-change: transform;
}
#bamboo-mid .bamboo-stalk {
position: absolute;
top: -70%;
height: 240%;
border-radius: 999px;
filter: blur(2.5px);
transition: filter 1.2s ease;
/* Segment rings + a soft left-of-centre sheen give the stalks culm
structure without any extra elements; the blur keeps both faint. */
background:
repeating-linear-gradient(180deg,
transparent 0, transparent 84px,
oklch(0.46 0.075 145 / 0.28) 84px, oklch(0.46 0.075 145 / 0.28) 87px),
linear-gradient(90deg, transparent 14%, oklch(0.58 0.07 145 / 0.14) 38%, transparent 68%),
linear-gradient(180deg, oklch(0.36 0.06 145 / 0.45), oklch(0.25 0.045 140 / 0.2));
transform-origin: bottom center;
animation: bambooSwayHalf 13s ease-in-out infinite;
}
#bamboo-mid .mid-1 { left: 17%; width: 15px; animation-duration: 12s; }
#bamboo-mid .mid-2 { left: 31%; width: 9px; animation-delay: -5s; opacity: 0.55; }
#bamboo-mid .mid-3 { right: 15.5%; width: 17px; animation-duration: 14.5s; animation-delay: -8s; }
#bamboo-mid .mid-4 { right: 30%; width: 8px; animation-duration: 11.5s; animation-delay: -2.5s; opacity: 0.5; }
/* A few leaves sprout from the mid stalks (pseudo-elements, so they ride
the stalk's sway for free). The slim centre stalks stay bare. */
#bamboo-mid .bamboo-stalk::before,
#bamboo-mid .bamboo-stalk::after {
content: '';
position: absolute;
width: 24px;
height: 11px;
border-radius: 0 100% 0 100%;
background: oklch(0.5 0.1 145 / 0.32);
}
#bamboo-mid .mid-1::before { top: 31%; left: 13px; transform: rotate(20deg); }
#bamboo-mid .mid-1::after { top: 55%; left: -26px; transform: rotate(-158deg); width: 20px; height: 9px; }
#bamboo-mid .mid-3::before { top: 26%; left: -27px; transform: rotate(-156deg); }
#bamboo-mid .mid-3::after { top: 49%; left: 15px; transform: rotate(24deg); width: 21px; height: 10px; }
#bamboo-mid .mid-2::before,
#bamboo-mid .mid-2::after,
#bamboo-mid .mid-4::before,
#bamboo-mid .mid-4::after {
display: none;
}
.bg-nature.depth-mid #bamboo-mid .bamboo-stalk { filter: blur(3.5px); }
.bg-nature.depth-deep #bamboo-mid .bamboo-stalk { filter: blur(4.5px); }
@keyframes bambooSwayHalf {
0%, 100% { transform: rotate(0); }
50% { transform: rotate(calc(var(--sway, 1.2deg) * 0.5)); }
}
/* (b) Near bamboo layer: sharp stalks with nodes and leaves, gentle sway */
#bamboo-near {
position: absolute;
@@ -208,10 +273,14 @@ body {
#bamboo-near .bamboo-stalk {
position: absolute;
top: -20%;
height: 140%;
top: -70%;
height: 240%;
border-radius: 999px;
background: linear-gradient(180deg, oklch(0.4 0.07 145 / 0.55), oklch(0.26 0.05 140 / 0.25));
/* Vertical sheen over the culm gradient: reads as light from the
canopy catching the rounded stalk instead of a flat strip. */
background:
linear-gradient(90deg, transparent 12%, oklch(0.62 0.08 145 / 0.18) 36%, transparent 64%),
linear-gradient(180deg, oklch(0.4 0.07 145 / 0.55), oklch(0.26 0.05 140 / 0.25));
transform-origin: bottom center;
animation: bambooSway 10s ease-in-out infinite;
}
@@ -375,13 +444,31 @@ body {
.canopy-arc-3 { top: -4px; left: 61%; width: 46px; height: 14px; transform: rotate(-3deg); }
.canopy-arc-4 { top: -2px; left: 84%; width: 30px; height: 10px; transform: rotate(7deg); background: oklch(0.33 0.06 145 / 0.4); }
/* (f) Undergrowth: grass blades in the lower corners */
/* (f) Undergrowth: grass blades in the lower corners. Each blade carries a
shorter companion blade (::before) so the corners read as small tufts. */
.bg-grass {
position: absolute;
bottom: -4px;
transform-origin: bottom;
}
.bg-grass::before {
content: '';
position: absolute;
bottom: 0;
width: 72%;
height: 55%;
border-radius: inherit;
background: inherit;
opacity: 0.7;
transform-origin: bottom;
}
.bg-grass-1::before { left: -5px; transform: rotate(-13deg); }
.bg-grass-2::before { left: 4px; transform: rotate(11deg); }
.bg-grass-3::before { right: -5px; transform: rotate(12deg); }
.bg-grass-4::before { right: 4px; transform: rotate(-9deg); }
.bg-grass-1 { left: 1.5%; width: 6px; height: 52px; border-radius: 100% 0 0 0; background: oklch(0.38 0.07 145 / 0.4); transform: rotate(-9deg); }
.bg-grass-2 { left: 3.2%; width: 5px; height: 38px; border-radius: 100% 0 0 0; background: oklch(0.35 0.07 145 / 0.3); transform: rotate(-12deg); }
.bg-grass-3 { right: 2%; width: 6px; height: 56px; border-radius: 0 100% 0 0; background: oklch(0.4 0.07 145 / 0.4); transform: rotate(8deg); }
@@ -508,6 +595,83 @@ body {
100% { translate: -14px -18px; }
}
/* Day/night: fireflies belong to the dark theme. By daylight the same six
spots are visited by small butterflies instead — the wrapper (and with it
the cursor-shy push and the section greeting) is shared, only the inner
dot is redressed. Only animation-name is swapped so the per-firefly
duration/delay variety above keeps applying to the butterflies. */
html.theme-light .firefly {
position: relative;
width: 2px;
height: 9px;
border-radius: 1px;
background: oklch(0.35 0.05 140 / 0.8);
box-shadow: none;
opacity: 0.85;
animation-name: butterflyBob, fireflyWander;
}
html.theme-light .firefly::before,
html.theme-light .firefly::after {
content: '';
position: absolute;
top: -2px;
width: 7px;
height: 10px;
background: oklch(0.62 0.14 45 / 0.7);
animation: wingFlap 0.5s ease-in-out infinite alternate;
}
html.theme-light .firefly::before {
left: -6px;
border-radius: 90% 10% 60% 40%;
transform-origin: 100% 55%;
}
html.theme-light .firefly::after {
right: -6px;
border-radius: 10% 90% 40% 60%;
transform-origin: 0% 55%;
animation-name: wingFlapR;
}
/* Every other butterfly wears the sage of the canopy and flaps lazier */
html.theme-light .fw-2 .firefly::before,
html.theme-light .fw-2 .firefly::after,
html.theme-light .fw-4 .firefly::before,
html.theme-light .fw-4 .firefly::after,
html.theme-light .fw-6 .firefly::before,
html.theme-light .fw-6 .firefly::after {
background: oklch(0.52 0.12 150 / 0.55);
animation-duration: 0.62s;
}
@keyframes butterflyBob {
0%, 100% { transform: translateY(0) rotate(-4deg); }
50% { transform: translateY(-8px) rotate(5deg); }
}
@keyframes wingFlap {
from { transform: scaleX(1) rotate(-6deg); }
to { transform: scaleX(0.3) rotate(8deg); }
}
@keyframes wingFlapR {
from { transform: scaleX(1) rotate(6deg); }
to { transform: scaleX(0.3) rotate(-8deg); }
}
/* By day the dusk layer must not dim the butterflies with scroll depth
(the JS crossfade keeps writing inline opacity for the dark theme), and
the warm horizon glow steps back so it reads as haze, not dusk. */
html.theme-light .bg-depth-dusk {
opacity: 1 !important;
}
html.theme-light .bg-horizon {
background: radial-gradient(ellipse 60% 100% at 50% 100%, oklch(0.8 0.08 60 / 0.1), transparent 70%);
}
/* (j) Wandering light sweep */
.bg-light-sweep {
position: absolute;
@@ -525,6 +689,20 @@ body {
100% { transform: translate(20%, 20%) rotate(8deg); }
}
/* (j2) Corner vignette: pulls the eye toward the content and grounds the
scene — the forest darkens gently toward the edges like real depth. */
.bg-nature::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(ellipse 120% 90% at 50% 38%, transparent 55%, oklch(0.10 0.025 142 / 0.4) 100%);
}
html.theme-light .bg-nature::after {
background: radial-gradient(ellipse 120% 90% at 50% 38%, transparent 62%, oklch(0.78 0.03 120 / 0.3) 100%);
}
/* (k) Film grain (SVG feTurbulence, no external assets) */
.bg-grain {
position: absolute;
@@ -543,6 +721,7 @@ body {
.fall-leaf-8,
.bg-godrays,
.bg-light-sweep,
#bamboo-mid,
#bamboo-far .far-4,
#bamboo-far .far-5,
#bamboo-near .near-3 {
@@ -550,7 +729,67 @@ body {
}
}
/* (l) Click confetti: a small handful of eucalyptus leaves bursts from
wherever the visitor presses (spawned by app.js). Built like the falling
leaves above — the outer span owns the flight path (fast burst that
brakes at its peak, then sinks), the inner leaf rocks around its stem
the whole way down. Deliberately subtle: few, tiny, short-lived. */
.click-leaf {
position: fixed;
z-index: 3000;
margin: -6px 0 0 -4px;
pointer-events: none;
opacity: 0;
animation: leafBurst var(--leaf-dur, 1.1s) forwards;
will-change: transform, opacity;
}
.click-leaf i {
display: block;
width: 8px;
height: 12px;
border-radius: 0 70% 0 70%;
background: linear-gradient(135deg, oklch(0.62 0.12 150 / 0.95), oklch(0.45 0.09 145 / 0.85));
transform: rotate(var(--leaf-rot, 0deg));
animation: leafRock var(--flutter-dur, 0.6s) ease-in-out infinite alternate;
}
.click-leaf-sage i {
background: linear-gradient(135deg, oklch(0.72 0.09 140 / 0.9), oklch(0.55 0.08 145 / 0.8));
}
.click-leaf-amber i {
background: linear-gradient(135deg, oklch(0.68 0.14 55 / 0.95), oklch(0.55 0.13 40 / 0.85));
}
/* Two-phase flight: --leaf-x/--leaf-y is the burst PEAK; from there each
leaf loses momentum and sinks past it while fading. Per-segment timing
functions give the fast pop and the gentle fall. */
@keyframes leafBurst {
0% {
opacity: 0;
transform: translate(0, 0) scale(var(--leaf-scale, 1));
animation-timing-function: cubic-bezier(0.16, 0.84, 0.44, 1);
}
10% { opacity: 1; }
45% {
opacity: 1;
transform: translate(var(--leaf-x, 0), var(--leaf-y, -20px)) scale(var(--leaf-scale, 1));
animation-timing-function: cubic-bezier(0.55, 0.06, 0.68, 0.19);
}
100% {
opacity: 0;
transform: translate(calc(var(--leaf-x, 0) * 1.4), calc(var(--leaf-y, -20px) + 44px)) scale(calc(var(--leaf-scale, 1) * 0.92));
}
}
@keyframes leafRock {
from { transform: rotate(calc(var(--leaf-rot, 0deg) - 24deg)); }
to { transform: rotate(calc(var(--leaf-rot, 0deg) + 24deg)); }
}
@media (prefers-reduced-motion: reduce) {
.click-leaf { display: none; }
.bg-nature *,
.bg-nature *::before,
.bg-nature *::after {
@@ -771,6 +1010,7 @@ html.theme-light .bg-nature {
html.theme-light .bg-canopy,
html.theme-light .canopy-arc,
html.theme-light #bamboo-far .bamboo-stalk,
html.theme-light #bamboo-mid .bamboo-stalk,
html.theme-light #bamboo-near .bamboo-stalk,
html.theme-light .bg-grass {
opacity: 0.45;
@@ -1158,14 +1398,16 @@ html.theme-light .koala-speech-bubble::after {
font-size: 0.95rem;
}
.btn-primary {
background: var(--accent-green);
color: var(--text-on-green);
box-shadow: 0 10px 15px -3px var(--accent-glow);
background: linear-gradient(135deg, oklch(0.48 0.13 150), oklch(0.39 0.10 145));
color: #fff;
border: 1px solid oklch(0.72 0.11 95 / 0.24);
box-shadow: 0 12px 24px -8px oklch(0.38 0.11 148 / 0.65), 0 0 0 1px oklch(0.80 0.08 90 / 0.06) inset;
}
.btn-primary:hover {
background: linear-gradient(135deg, oklch(0.54 0.14 150), oklch(0.44 0.11 145));
transform: translateY(-2px);
box-shadow: 0 20px 25px -5px var(--accent-glow);
box-shadow: 0 18px 32px -8px oklch(0.38 0.11 148 / 0.75), 0 0 18px oklch(0.68 0.13 150 / 0.18);
}
.btn-firefox {
@@ -1615,6 +1857,19 @@ html.theme-light .koala-speech-bubble::after {
put white text on a colour get a darker shade (these beat inline styles). */
.hero-demo-scene {
--accent: oklch(0.80 0.10 150);
--demo-sky-top: oklch(0.22 0.055 152);
--demo-sky-mid: oklch(0.145 0.035 150);
--demo-sky-bottom: oklch(0.075 0.018 145);
--demo-glow: oklch(0.76 0.13 150 / 0.34);
--demo-stars: oklch(0.94 0.025 95);
--demo-moon: oklch(0.97 0.025 92);
--demo-mountain: oklch(0.18 0.055 148 / 0.9);
--demo-midground: oklch(0.22 0.065 145 / 0.88);
--demo-foreground: oklch(0.065 0.025 145);
--demo-card: oklch(0.18 0.032 142);
--demo-toolbar: oklch(0.12 0.025 142);
--demo-control-fade: oklch(0.055 0.015 145 / 0.94);
--demo-track: oklch(0.92 0.02 95 / 0.2);
}
.hero-demo-scene .mock-tab.active,
.hero-demo-scene .mock-btn-force,
@@ -1636,17 +1891,33 @@ html.theme-light .koala-speech-bubble::after {
html.theme-light .hero-demo-scene {
--accent: oklch(0.60 0.12 150);
--demo-sky-top: oklch(0.86 0.085 78);
--demo-sky-mid: oklch(0.76 0.095 112);
--demo-sky-bottom: oklch(0.54 0.10 150);
--demo-glow: oklch(0.92 0.13 72 / 0.44);
--demo-stars: oklch(0.98 0.025 90);
--demo-moon: oklch(0.99 0.025 88);
--demo-mountain: oklch(0.48 0.095 150 / 0.82);
--demo-midground: oklch(0.40 0.10 148 / 0.8);
--demo-foreground: oklch(0.24 0.075 145);
--demo-card: oklch(0.985 0.008 95);
--demo-toolbar: oklch(0.955 0.014 100);
--demo-control-fade: oklch(0.16 0.035 145 / 0.88);
--demo-track: oklch(0.98 0.01 95 / 0.34);
}
html.theme-light .hero-demo-scene .demo-tab-card {
background: #ffffff;
background: var(--demo-card);
border-color: oklch(0.20 0.025 140 / 0.1);
box-shadow: 0 18px 38px oklch(0.20 0.025 140 / 0.14);
box-shadow:
0 18px 38px oklch(0.20 0.025 140 / 0.16),
0 0 28px oklch(0.82 0.10 85 / 0.14),
inset 0 1px rgba(255, 255, 255, 0.9);
}
html.theme-light .hero-demo-scene .demo-tab-titlebar {
background: oklch(0.96 0.01 90);
background: var(--demo-toolbar);
border-bottom-color: oklch(0.88 0.015 90);
color: oklch(0.45 0.02 110);
}
@@ -2941,6 +3212,61 @@ footer a:hover {
transform: translateY(0);
}
html.theme-light .hero-demo-scene .demo-user-a,
html.theme-light .hero-demo-scene .demo-user-b {
color: oklch(0.36 0.105 150);
background: oklch(0.60 0.12 150 / 0.13);
}
html.theme-light .hero-demo-scene .extension-mockup {
--bg: oklch(0.935 0.018 110) !important;
--card: oklch(0.995 0.006 100) !important;
--accent: oklch(0.56 0.13 150) !important;
--accent-glow: oklch(0.56 0.13 150 / 0.22) !important;
--text: oklch(0.21 0.03 140) !important;
--text-muted: oklch(0.43 0.03 135) !important;
--success: oklch(0.56 0.13 150) !important;
--error: oklch(0.57 0.17 25) !important;
--glass-border: oklch(0.28 0.035 140 / 0.10) !important;
background: oklch(0.935 0.018 110) !important;
border-color: oklch(0.28 0.035 140 / 0.12);
box-shadow:
0 20px 42px oklch(0.20 0.025 140 / 0.16),
0 0 26px oklch(0.82 0.10 85 / 0.12);
}
html.theme-light .hero-demo-scene .mock-tabs,
html.theme-light .hero-demo-scene .mock-card,
html.theme-light .hero-demo-scene .mock-input {
background: oklch(0.995 0.006 100);
border-color: oklch(0.28 0.035 140 / 0.14);
color: oklch(0.21 0.03 140);
}
html.theme-light .hero-demo-scene .mock-peer-item {
background: oklch(0.28 0.035 140 / 0.035);
border-color: oklch(0.28 0.035 140 / 0.09);
}
html.theme-light .hero-demo-scene .mock-joined-room,
html.theme-light .hero-demo-scene .mock-status-pill {
background: oklch(0.56 0.13 150 / 0.10);
border-color: oklch(0.56 0.13 150 / 0.24);
}
html.theme-light .hero-demo-scene .mock-close-btn:hover {
color: oklch(0.21 0.03 140);
background: oklch(0.28 0.035 140 / 0.08);
}
/* A direct hash destination is content, not an entrance animation. Keep it
readable from the first paint even before app.js has initialized. */
section:target [data-reveal],
section:target[data-reveal] {
opacity: 1;
transform: translateY(0);
}
/* --- Legal Pages --- */
.legal-content {
@@ -3685,9 +4011,9 @@ footer {
}
.cross {
color: oklch(0.55 0.15 25);
font-weight: 500;
opacity: 0.8;
color: oklch(0.68 0.17 25);
font-weight: 600;
opacity: 1;
}
@media (max-width: 768px) {
@@ -3838,7 +4164,7 @@ footer {
display: inline-flex;
align-items: center;
vertical-align: super;
opacity: 0.6;
opacity: 0.82;
transition: opacity 0.2s ease, color 0.2s ease, transform 0.2s ease;
}
@@ -4396,10 +4722,10 @@ header[id] {
position: absolute;
width: 310px;
width: 70cqw;
background: oklch(0.21 0.025 138);
background: var(--demo-card);
border: 1px solid rgba(255, 255, 255, 0.09);
border-radius: 14px;
box-shadow: 0 18px 36px rgba(0, 0, 0, 0.45);
box-shadow: 0 18px 36px rgba(0, 0, 0, 0.45), inset 0 1px rgba(255, 255, 255, 0.05);
overflow: hidden;
cursor: pointer;
user-select: none;
@@ -4471,7 +4797,7 @@ header[id] {
align-items: center;
gap: 8px;
padding: 6px 8px 6px 10px;
background: oklch(0.15 0.02 138);
background: var(--demo-toolbar);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
font-size: 10.5px;
color: var(--text-muted);
@@ -4596,18 +4922,35 @@ header[id] {
.demo-video {
position: relative;
aspect-ratio: 16 / 9;
background: #020617;
background: var(--demo-sky-bottom);
overflow: hidden;
}
.demo-video::after {
content: '';
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.05),
inset 0 -24px 36px oklch(0.04 0.015 145 / 0.2);
}
html.theme-light .hero-demo-scene .demo-video::after {
box-shadow:
inset 0 0 0 1px oklch(0.30 0.04 145 / 0.13),
inset 0 18px 32px oklch(0.98 0.03 90 / 0.1),
inset 0 -22px 34px oklch(0.18 0.05 145 / 0.16);
}
.demo-video-art {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 78% 22%, oklch(0.96 0.01 90 / 0.28) 0 4%, transparent 5.5%),
radial-gradient(120% 90% at 70% 115%, oklch(0.26 0.05 140) 0%, transparent 60%),
radial-gradient(100% 80% at 12% 112%, oklch(0.20 0.04 145) 0%, transparent 55%),
linear-gradient(160deg, oklch(0.27 0.035 130) 0%, oklch(0.15 0.02 138) 55%, #020617 100%);
radial-gradient(30% 42% at 76% 29%, var(--demo-glow), transparent 64%),
radial-gradient(90% 85% at 18% 5%, var(--demo-glow), transparent 66%),
linear-gradient(160deg, var(--demo-sky-top) 0%, var(--demo-sky-mid) 52%, var(--demo-sky-bottom) 100%);
transform-origin: 60% 40%;
animation:
demoKenBurns 9s ease-in-out infinite alternate,
@@ -4624,7 +4967,7 @@ header[id] {
width: 70%;
aspect-ratio: 1;
border-radius: 50%;
background: radial-gradient(circle, oklch(0.75 0.13 150 / 0.32), transparent 65%);
background: radial-gradient(circle, var(--demo-glow), transparent 65%);
animation: demoGlowDrift 7s ease-in-out infinite alternate;
animation-play-state: paused;
}
@@ -4718,7 +5061,7 @@ header[id] {
align-items: center;
gap: 8px;
padding: 14px 10px 8px;
background: linear-gradient(transparent, oklch(0.10 0.01 135 / 0.85));
background: linear-gradient(transparent, var(--demo-control-fade));
font-size: 9.5px;
color: oklch(0.88 0.015 90);
z-index: 1;
@@ -4737,7 +5080,7 @@ header[id] {
flex: 1;
height: 5px;
border-radius: 99px;
background: rgba(255, 255, 255, 0.18);
background: var(--demo-track);
cursor: pointer;
}
@@ -4925,6 +5268,15 @@ header[id] {
height: 100%;
}
.hero-demo-scene .film-stars { fill: var(--demo-stars) !important; }
.hero-demo-scene .film-moon {
fill: var(--demo-moon) !important;
filter: drop-shadow(0 0 6px var(--demo-moon));
}
.hero-demo-scene .film-mtn-scroll { fill: var(--demo-mountain) !important; }
.hero-demo-scene .film-mid-scroll > * { fill: var(--demo-midground) !important; }
.hero-demo-scene .film-fore-scroll > * { fill: var(--demo-foreground) !important; }
/* Stars: staggered twinkle so the sky feels alive */
.film-star {
transform-box: fill-box;