style(website): landing motion, interaction & layout polish

Feedback round (calmer, less "template/AI"):
- Dial back animated background motion globally (sway/leaf-drift halved,
  fewer falling leaves, gentler godrays/fireflies/light-sweep).
- Fire leaf confetti on click of real controls only (was pointerdown
  everywhere, which showered mobile users on every scroll).
- Remove hover lift/scale from non-interactive cards, screenshots and
  mascots so they no longer read as clickable.
- Fix mobile nav menu colours in the light theme (was a hard-coded dark
  panel with low-contrast links).
- Calm how-it-works steps 1-2 (no button pulse/shine/cursor bob); step 3
  keeps its lively sync animation. Remove the now-static "copied" toast
  from step 1 since it only made sense mid-animation.

UX audit follow-ups:
- Unify vertical rhythm with one section-padding scale; vertically centre
  the hero so space is balanced instead of pooling under the CTAs.
- Split the mixed "Why KoalaSync?" grid into use-case scenarios and a
  labelled "Features" subsection, and move the #features nav anchor there.
- Demote the hero GitHub CTA to a quiet text link (Chrome > Firefox >
  GitHub hierarchy); scale down oversized section mascots and give them a
  shared soft shadow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Timo
2026-07-13 02:42:19 +02:00
parent 63414bfcdd
commit af0da75d38
10 changed files with 127 additions and 143 deletions
+9 -9
View File
@@ -379,19 +379,19 @@ document.addEventListener('DOMContentLoaded', () => {
}
};
// Listen for `click`, not `pointerdown`: a click never fires while the
// page is being scrolled, so touch users no longer trigger a shower of
// leaves every time they swipe. Leaves are also only shed by actual
// interactive controls (links/buttons and the demo widgets), so a stray
// tap on plain text or an illustration stays quiet.
let lastBurst = 0;
document.addEventListener('pointerdown', (e) => {
if (e.button !== 0 || !e.isPrimary) return;
document.addEventListener('click', (e) => {
const control = e.target.closest('a, button, [role="button"], .btn, .mock-tab, select, .demo-progress');
if (!control) return;
const now = Date.now();
if (now - lastBurst < 180) return;
lastBurst = now;
const control = e.target.closest('button, [role="button"], .btn, .mock-tab, select, .demo-progress');
if (control) {
burstFromElement(control);
} else {
const count = 4 + Math.floor(Math.random() * 3);
for (let i = 0; i < count; i++) spawnLeaf(e.clientX, e.clientY);
}
burstFromElement(control);
}, { passive: true });
}
+2 -6
View File
@@ -1147,12 +1147,8 @@ html.theme-light .hero-demo-scene .demo-video::after {
font-size: 1.1rem;
pointer-events: none;
z-index: 10;
animation: pointer-click 1.6s infinite ease-in-out;
}
@keyframes pointer-click {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(-3px, -3px) scale(0.9); }
/* Static hand — the looping tap bob was the busiest part of step 2 and
distracted from the copy (step 3 keeps its lively animation). */
}
/* Step 3 forest movie continuous animation bindings */
+27 -12
View File
@@ -119,8 +119,11 @@ body {
/* --- Animated Bamboo Forest Background (v3 Greens Update) --- */
.bg-nature {
--sway: 1.2deg;
--leaf-drift: 26px;
/* Motion dialled back so the scene reads as a calm backdrop rather than
competing with the text that scrolls over it (sway/drift roughly
halved from the original 1.2deg / 26px). */
--sway: 0.6deg;
--leaf-drift: 13px;
}
.bg-nature {
@@ -354,11 +357,20 @@ body {
@keyframes leafFall {
0% { transform: translateY(-40px) rotate(0); opacity: 0; }
8% { opacity: 0.65; }
92% { opacity: 0.5; }
8% { opacity: 0.3; }
92% { opacity: 0.24; }
100% { transform: translateY(110vh) translateX(120px) rotate(280deg); opacity: 0; }
}
/* Thin the drifting leaves down to four so fewer of them cross the reading
column at once (the even ones stay hidden, keeping an even spread). */
.fall-leaf-2,
.fall-leaf-4,
.fall-leaf-6,
.fall-leaf-8 {
display: none;
}
@keyframes leafSway {
0%, 100% { margin-left: 0; }
50% { margin-left: var(--leaf-drift, 26px); }
@@ -413,8 +425,8 @@ body {
.godray-3 { left: 56%; width: 140px; animation-duration: 13s; animation-delay: -6s; background: linear-gradient(180deg, oklch(0.85 0.05 110 / 0.08), transparent 75%); }
@keyframes rayShimmer {
0% { opacity: 0.45; }
100% { opacity: 1; }
0% { opacity: 0.35; }
100% { opacity: 0.6; }
}
/* (e) Canopy: dark vignette at the top plus hanging leaf arcs */
@@ -579,8 +591,8 @@ body {
}
@keyframes fireflyPulse {
0%, 100% { opacity: 0.2; transform: scale(1); }
50% { opacity: 0.9; transform: scale(1.35); }
0%, 100% { opacity: 0.15; transform: scale(1); }
50% { opacity: 0.55; transform: scale(1.2); }
}
@keyframes fireflyWander {
@@ -675,8 +687,8 @@ html.theme-light .bg-horizon {
left: -30%;
width: 160%;
height: 160%;
background: linear-gradient(115deg, transparent 42%, oklch(0.68 0.13 150 / 0.05) 50%, transparent 58%);
animation: lightSweep 26s ease-in-out infinite alternate;
background: linear-gradient(115deg, transparent 42%, oklch(0.68 0.13 150 / 0.03) 50%, transparent 58%);
animation: lightSweep 38s ease-in-out infinite alternate;
will-change: transform;
}
@@ -1061,12 +1073,15 @@ html.theme-light :is(.use-case-card, .feature-card, .faq-item, .comparison-table
padding: 0 2rem;
}
/* Unified vertical rhythm: every content section shares one padding scale so
the page reads as an even cadence instead of a series of tall, half-empty
panels. Tightened from the old fixed 6rem. */
section {
padding: 6rem 0;
padding: clamp(3.5rem, 6.5vh, 5rem) 0;
}
#how-it-works {
padding: 4.5rem 0;
padding: clamp(3.5rem, 6.5vh, 5rem) 0;
}
/* Content-visibility for offscreen sections to improve INP */
+26 -5
View File
@@ -1,10 +1,13 @@
/* --- Hero Section --- */
.hero {
min-height: 100vh;
min-height: 100svh;
display: flex;
align-items: flex-start;
padding-top: clamp(8rem, 12vh, 10rem);
padding-bottom: clamp(2rem, 5vh, 4rem);
/* Centre the hero content vertically so the empty space is balanced above
and below instead of pooling under the CTAs. Padding-top only needs to
clear the fixed nav. */
align-items: center;
padding-top: clamp(6rem, 9vh, 8rem);
padding-bottom: clamp(3rem, 6vh, 5rem);
}
.hero-grid {
@@ -20,7 +23,6 @@
gap: 3rem;
text-align: left;
width: 100%;
min-height: calc(100svh - 12.5rem);
}
@media (min-width: 1024px) {
@@ -333,6 +335,25 @@ html.theme-light .koala-speech-bubble::after {
.btn-install.is-detected .btn-label-short { display: none; }
.btn-install.is-detected .btn-label-full { display: inline; }
/* GitHub is the tertiary hero action. Strip the button chrome so it reads as a
quiet link beside the two install CTAs, keeping a clear primary → secondary →
tertiary hierarchy (Chrome filled → Firefox outline → GitHub text). The
theme-light selector matches the specificity of the light-theme .btn-secondary
override in foundation.css so the transparent treatment wins in both themes. */
.hero-github-cta.btn-secondary,
html.theme-light .hero-github-cta.btn-secondary {
background: transparent;
border-color: transparent;
color: var(--text-muted);
}
.hero-github-cta.btn-secondary:hover,
html.theme-light .hero-github-cta.btn-secondary:hover {
background: transparent;
color: var(--text);
transform: none;
}
.version-badge {
display: inline-block;
background: oklch(0.68 0.13 150 / 0.1);
+2 -7
View File
@@ -54,15 +54,10 @@
.selfhost-mascot {
display: block;
width: 300px;
width: 220px;
height: auto;
margin: 0 auto;
filter: drop-shadow(0 4px 12px rgba(0,0,0,0.15));
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.selfhost-mascot:hover {
transform: scale(1.05) translateY(-4px) rotate(-1deg);
filter: drop-shadow(0 6px 14px oklch(0.15 0.02 145 / 0.22));
}
.status-ring {
-5
View File
@@ -167,11 +167,6 @@ html.theme-light .lang-dropdown option {
max-width: 270px;
width: 100%;
height: auto;
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.compat-mascot:hover {
transform: scale(1.05) translateY(-5px) rotate(1deg);
}
.compat-heading {
+11 -63
View File
@@ -51,15 +51,8 @@
pointer-events: none;
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 30px oklch(0.68 0.13 150 / 0.2);
border-color: oklch(0.68 0.13 150 / 0.3);
}
.feature-card:hover::before {
background: linear-gradient(to bottom right, oklch(0.68 0.13 150 / 0.3), transparent);
}
/* Feature cards are static content, not links — no hover lift/glow, so they
don't read as clickable. */
/* Feature Icon with Inline SVG Wrapper */
.feature-icon-svg {
@@ -73,14 +66,6 @@
color: var(--accent);
border-radius: 12px;
margin-right: 14px;
transition: background 0.3s ease, color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
}
.feature-card:hover .feature-icon-svg {
background: var(--accent);
color: var(--text-on-green);
box-shadow: 0 0 15px var(--accent-glow);
transform: scale(1.05);
}
.bento-icon {
@@ -152,11 +137,6 @@
right: 2.5rem;
height: 2px;
background: linear-gradient(90deg, transparent, oklch(0.68 0.13 150 / 0.4), transparent);
opacity: 0;
transition: opacity 0.3s ease;
}
.feature-card.bento-large:hover::after {
opacity: 1;
}
@@ -237,7 +217,7 @@
.steps {
display: flex;
flex-direction: column;
gap: 4rem;
gap: 2.75rem;
}
.step {
@@ -331,11 +311,7 @@ html.theme-light .step-num {
}
}
.step-illustration-1:hover, .step-illustration-2:hover, .step-illustration-3:hover {
transform: translateY(-6px) scale(1.02);
border-color: oklch(0.68 0.13 150 / 0.25);
box-shadow: 0 25px 50px oklch(0.68 0.13 150 / 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
/* Step illustrations are screenshots, not controls — no hover lift/scale. */
/* ==========================================================================
Step 1 Illustration: Browser & Installed Success Popup
@@ -704,28 +680,14 @@ html.theme-light .step-num {
position: relative;
overflow: hidden;
cursor: default;
animation: create-btn-pulse 2s infinite alternate;
}
@keyframes create-btn-pulse {
0% { transform: scale(1); box-shadow: 0 4px 12px oklch(0.68 0.13 150 / 0.3); }
100% { transform: scale(1.03); box-shadow: 0 6px 16px oklch(0.68 0.13 150 / 0.5); }
/* Static: the constant scale/shadow pulse pulled the eye away from the
copy while reading the steps (step 3 keeps the lively animation). */
}
/* Shine sweep removed — kept the element off-canvas so the button reads as a
calm static control rather than a repeatedly glinting one. */
.illus-btn-shine {
position: absolute;
top: 0;
left: -100%;
width: 50%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.25), transparent);
transform: skewX(-20deg);
animation: shimmer-sweep 3s infinite ease-in-out;
}
@keyframes shimmer-sweep {
0% { left: -100%; }
50%, 100% { left: 150%; }
display: none;
}
.illus-collapsed-details {
@@ -764,16 +726,12 @@ html.theme-light .step-num {
align-items: center;
gap: 4px;
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.45), 0 0 12px oklch(0.68 0.13 150 / 0.18);
animation: floating-success-fade 5s infinite;
/* Shown statically instead of fading in and out on a loop — the toast now
reads as part of the popup state rather than a flashing notification. */
white-space: nowrap;
z-index: 10;
}
@keyframes floating-success-fade {
0%, 100%, 75% { opacity: 0; transform: translate(-50%, 5px) scale(0.9); }
15%, 60% { opacity: 1; transform: translate(-50%, 0) scale(1); }
}
.illus-floating-success .success-icon {
font-size: 0.65rem;
}
@@ -1022,16 +980,6 @@ html.theme-light .step-illustration-3 {
inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
html.theme-light .step-illustration-1:hover,
html.theme-light .step-illustration-2:hover,
html.theme-light .step-illustration-3:hover {
border-color: oklch(0.56 0.13 150 / 0.28);
box-shadow:
0 22px 44px oklch(0.20 0.025 140 / 0.15),
0 0 22px oklch(0.56 0.13 150 / 0.10),
inset 0 1px 0 #ffffff;
}
html.theme-light .step-illustration-1 .illus-popup-card,
html.theme-light .step-illustration-2 .illus-popup-card,
html.theme-light .step-illustration-3 .illus-player-card {
+31 -23
View File
@@ -1,6 +1,6 @@
/* --- Use Cases / Anwendungsszenarien Section --- */
.use-cases-section {
padding: 6rem 0;
padding: clamp(3.5rem, 6.5vh, 5rem) 0;
position: relative;
border-bottom: 0;
}
@@ -12,6 +12,18 @@
margin-top: 1rem;
}
/* Sub-heading that separates the use-case scenarios from the feature tiles so
the two card types read as intentional groups (and gives the "Features" nav
link a real target). */
.section-subheading {
text-align: center;
font-size: 1.55rem;
font-weight: 800;
letter-spacing: -0.03em;
margin: 3.25rem 0 1.5rem;
scroll-margin-top: 96px;
}
.use-cases-feature-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
@@ -63,13 +75,9 @@
.use-case-card:nth-child(3) { --card-accent-border: oklch(0.75 0.10 150); }
.use-case-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
border-color: rgba(255, 255, 255, 0.15);
}
.use-case-card:hover::before {
/* Use-case cards are static content, not links — no hover lift so they don't
read as clickable. The top accent bar (::before) stays visible instead. */
.use-case-card::before {
opacity: 1;
}
@@ -91,30 +99,30 @@
margin: 1.5rem auto;
}
/* Shared framing so the section mascots read as one set even though the
underlying illustrations differ slightly in style (a consistent, soft ground
shadow — true art-direction alignment needs the assets themselves redrawn). */
.compat-mascot,
.features-questions-mascot,
.comparison-mascot,
.selfhost-mascot {
filter: drop-shadow(0 6px 14px oklch(0.15 0.02 145 / 0.22));
}
.features-questions-mascot {
display: block;
width: 100%;
max-width: 250px;
max-width: 185px;
height: auto;
margin: -1.5rem auto 0.2rem auto;
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.features-questions-mascot:hover {
transform: scale(1.05) translateY(-4px) rotate(-1.5deg);
margin: -1rem auto 0.4rem auto;
}
.comparison-mascot {
display: block;
width: 100%;
max-width: 260px;
max-width: 195px;
height: auto;
margin: -2.2rem auto 0.2rem auto;
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.comparison-mascot:hover {
transform: scale(1.05) translateY(-4px) rotate(1deg);
margin: -1.4rem auto 0.4rem auto;
}
.use-case-icon {
@@ -141,7 +149,7 @@
/* --- Comparison Section --- */
.comparison-section {
padding: 6rem 0;
padding: clamp(3.5rem, 6.5vh, 5rem) 0;
background: transparent;
border-bottom: 0;
}
+12 -6
View File
@@ -101,6 +101,16 @@
.nav-links.open {
display: flex;
}
/* The dropdown panel is dark-green by default; in the light theme that
hard-coded colour clashed with the pale navbar and washed out the link
text. Match the panel and its links to the active theme instead. */
.nav-links a {
color: var(--text);
}
html.theme-light .nav-links {
background: oklch(0.95 0.01 100 / 0.97);
border-bottom-color: oklch(0.20 0.025 140 / 0.10);
}
.nav-right {
margin-left: auto;
}
@@ -144,12 +154,8 @@
@media (min-width: 769px) and (max-height: 920px) {
.hero {
padding-top: clamp(7.5rem, 10vh, 9rem);
padding-bottom: clamp(1.5rem, 3vh, 2.75rem);
}
.hero-grid {
min-height: calc(100svh - 9.5rem);
padding-top: clamp(6.5rem, 9vh, 8rem);
padding-bottom: clamp(2rem, 4vh, 3.5rem);
}
}
+7 -7
View File
@@ -960,8 +960,13 @@
</h3>
<p>{{USE_CASE_3_DESC}}</p>
</div>
</div>
<h3 id="features" class="section-subheading" data-reveal>
<span>{{NAV_FEATURES}}</span>
</h3>
<div class="use-cases-grid use-cases-feature-grid">
<!-- Tile 1: Instant Invites / 1-Click Join -->
<div id="features" class="feature-card feature-card-compact" data-reveal>
<div class="feature-card feature-card-compact" data-reveal>
<h3>
<span class="feature-icon-svg">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="bento-icon"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
@@ -1166,11 +1171,6 @@
<span class="details-arrow"></span>
<span>{{STEP_2_ILLUS_MANUAL}}</span>
</div>
<div class="illus-floating-success">
<span class="success-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></span>
<span>{{STEP_2_ILLUS_COPIED}}</span>
</div>
</div>
</div>
</div>
@@ -1510,7 +1510,7 @@
</div>
</section>
<section style="text-align: center; padding: 6rem 0 2rem 0;">
<section style="text-align: center; padding: clamp(3.5rem, 6.5vh, 5rem) 0 2rem 0;">
<div class="container">
<h2 data-reveal style="font-size: 2.2rem; margin-bottom: 1rem;">
<span>{{BOTTOM_TITLE}}</span>