Add mountain climbing animation + fix typography scale

- MountainClimb: SVG stick figure climbs 11-step mountain, text card above head each step, flag + bounce at summit, IntersectionObserver auto-start, mobile numbered list fallback
- Replace static content grid with MountainClimb animation
- Add mc-fade-in + mc-bounce keyframes to globals.css
- Typography: IconCard title text-sm→text-base, IconCard sub text-xs→text-sm
- Bonus card title text-sm→text-base, desc text-xs→text-sm
- FAQ answers text-sm→text-base
- WeekTimeline card title text-base→text-lg

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Athena 2026-04-30 23:32:40 +02:00
parent 1c83760afd
commit db7d7530f4
4 changed files with 294 additions and 39 deletions

View file

@ -0,0 +1,266 @@
"use client";
import { useState, useEffect, useRef } from "react";
const STEPS = [
"Deinen Wunschkunden klar definiert \u2014 wer er ist, was er braucht, was er schon versucht hat",
"Dein Angebot auf den Punkt gebracht \u2014 was du anbietest, welches Problem du l\u00f6st",
"Deine Positionierung in einem Satz \u2014 klar, sofort verst\u00e4ndlich, f\u00fcr alle Kan\u00e4le nutzbar",
"Die komplette Struktur f\u00fcr deine Verkaufsseite \u2014 mit Feedback zur Umsetzung",
"Deine Marketing-Systemkarte \u2014 wie Sichtbarkeit, Vertrauen, Anfrage und Kauf zusammenh\u00e4ngen",
"Deinen 90-Tage-Marketingplan + Entscheidung welche 2 Kan\u00e4le du fokussierst",
"Deinen 4-Wochen Content-Plan + Themen die zeigen wof\u00fcr du stehst \u2014 f\u00fcr alle Plattformen",
"Deinen Launch-Plan \u2014 wie du dein Angebot aktiv \u00fcber Social Media in den Markt bringst",
"Video-Setup erkl\u00e4rt \u2014 professionelle Videos mit dem Handy",
"Dein pers\u00f6nliches Verkaufsskript f\u00fcr Direktansprache und Netzwerk",
"Dein Google Business Profil optimiert + SEO-Grundlagen f\u00fcr deine Website",
];
// Waypoints along the mountain ridge (SVG user units, viewBox 940 × 580)
const WP = [
{ x: 65, y: 520, lean: 8 },
{ x: 155, y: 500, lean: 9 },
{ x: 248, y: 477, lean: 12 },
{ x: 338, y: 449, lean: 13 },
{ x: 425, y: 416, lean: 16 },
{ x: 508, y: 378, lean: 19 },
{ x: 587, y: 334, lean: 20 },
{ x: 660, y: 290, lean: 19 },
{ x: 727, y: 252, lean: 18 },
{ x: 787, y: 220, lean: 18 },
{ x: 840, y: 192, lean: 0 },
] as const;
const VW = 940;
const VH = 580;
// Smooth quadratic-bezier path through all waypoints (midpoint technique)
const RIDGE =
"M 65 520" +
" Q 110 510 155 500" +
" Q 201.5 488.5 248 477" +
" Q 293 463 338 449" +
" Q 381.5 432.5 425 416" +
" Q 466.5 397 508 378" +
" Q 547.5 356 587 334" +
" Q 623.5 312 660 290" +
" Q 693.5 271 727 252" +
" Q 757 236 787 220" +
" Q 813.5 206 840 192";
// Filled mountain silhouette (ridge + right slope + base)
const FILL =
`M 0 ${VH} L 0 540 L ` +
RIDGE.slice(2) + // "65 520 Q …" — continues from first waypoint to summit
` L 940 265 L 940 ${VH} Z`;
const CARD_W = 234;
const CARD_H = 128;
export default function MountainClimb() {
const [step, setStep] = useState(-1);
const sectionRef = useRef<HTMLDivElement>(null);
const started = useRef(false);
// Auto-start when section scrolls into view
useEffect(() => {
const el = sectionRef.current;
if (!el) return;
const obs = new IntersectionObserver(
([e]) => {
if (e.isIntersecting && !started.current) {
started.current = true;
setStep(0);
}
},
{ threshold: 0.25 }
);
obs.observe(el);
return () => obs.disconnect();
}, []);
// Advance one step every 1.6 s
useEffect(() => {
if (step < 0 || step >= STEPS.length - 1) return;
const t = setTimeout(() => setStep((s) => s + 1), 1600);
return () => clearTimeout(t);
}, [step]);
const atSummit = step === STEPS.length - 1;
const wp = step >= 0 ? WP[step] : WP[0];
// Card x: centred on person, clamped inside viewBox
const rawCX = wp.x - CARD_W / 2;
const cardX = Math.min(Math.max(rawCX, 8), VW - CARD_W - 8);
// Card y: above figure head, clamped to top of viewBox
const cardY = Math.max(wp.y - CARD_H - 58, 4);
const replay = () => {
started.current = true;
setStep(0);
};
return (
<section className="py-20" ref={sectionRef}>
<div className="max-w-5xl mx-auto px-6">
{/* Header */}
<div className="text-center mb-12">
<span className="inline-flex items-center gap-2 bg-indigo-50 text-indigo-700 text-xs font-bold uppercase tracking-widest rounded-full px-4 py-2 border border-indigo-100">
<span className="w-2 h-2 rounded-full bg-indigo-500 animate-pulse" />
Komplettpaket
</span>
<h2 className="mt-4 text-3xl md:text-4xl font-extrabold text-slate-900">
Was du in 8 Wochen baust und mit nach Hause nimmst
</h2>
</div>
{/* ── Mountain animation (md+) ── */}
<div className="hidden md:block">
<svg
viewBox={`0 0 ${VW} ${VH}`}
className="w-full"
style={{ display: "block", overflow: "visible" }}
aria-hidden="true"
>
<defs>
<linearGradient id="mgFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#e0e7ff" stopOpacity="0.55" />
<stop offset="100%" stopColor="#ede9fe" stopOpacity="0.12" />
</linearGradient>
</defs>
{/* Mountain silhouette */}
<path d={FILL} fill="url(#mgFill)" />
{/* Ridge line */}
<path d={RIDGE} fill="none" stroke="#a5b4fc" strokeWidth="2.5" strokeLinecap="round" />
{/* Step dots */}
{WP.map((w, i) => (
<circle
key={i}
cx={w.x}
cy={w.y}
r={step >= 0 && i <= step ? 5.5 : 3.5}
fill={step >= 0 && i <= step ? "#6366f1" : "#c7d2fe"}
style={{ transition: "r 0.3s, fill 0.3s" }}
/>
))}
{/* ── Stick figure ── */}
{step >= 0 && (
<g
transform={`translate(${wp.x}, ${wp.y}) rotate(${wp.lean})`}
style={{ transition: "transform 0.75s cubic-bezier(0.4,0.2,0.2,1)" }}
>
{/* Inner group — receives bounce animation at summit */}
<g
className={atSummit ? "mc-bounce" : ""}
style={{ transformBox: "fill-box", transformOrigin: "50% 100%" }}
>
{/* Head */}
<circle cx="0" cy="-29" r="7.5" fill="none" stroke="#4f46e5" strokeWidth="1.5" />
{/* Body */}
<line x1="0" y1="-21" x2="0" y2="-3" stroke="#4f46e5" strokeWidth="1.5" strokeLinecap="round" />
{/* Left arm (down, holding mountain) */}
<line x1="0" y1="-16" x2="-10" y2="-8" stroke="#4f46e5" strokeWidth="1.5" strokeLinecap="round" />
{/* Right arm (raised) */}
<line x1="0" y1="-16" x2="10" y2="-24" stroke="#4f46e5" strokeWidth="1.5" strokeLinecap="round" />
{/* Legs */}
<line x1="0" y1="-3" x2="-7" y2="10" stroke="#4f46e5" strokeWidth="1.5" strokeLinecap="round" />
<line x1="0" y1="-3" x2="7" y2="10" stroke="#4f46e5" strokeWidth="1.5" strokeLinecap="round" />
{/* Flag — appears at summit, attached to raised right arm */}
{atSummit && (
<g className="mc-fade-in" transform="translate(9, -25)">
<line x1="0" y1="0" x2="0" y2="-22" stroke="#6366f1" strokeWidth="1.5" strokeLinecap="round" />
<polygon points="0,-22 18,-15 0,-8" fill="#6366f1" />
</g>
)}
</g>
</g>
)}
{/* ── Text card ── */}
{step >= 0 && (
<>
{/* Dashed connector: card → figure head */}
<line
x1={cardX + CARD_W / 2}
y1={cardY + CARD_H}
x2={wp.x}
y2={wp.y - 37}
stroke="#c7d2fe"
strokeWidth="1"
strokeDasharray="4 3"
/>
{/* foreignObject re-mounts on each step → re-triggers mc-fade-in */}
<foreignObject
key={step}
x={cardX}
y={cardY}
width={CARD_W}
height={CARD_H}
className="mc-fade-in"
style={{ overflow: "visible" }}
>
<div
className="bg-white rounded-xl border border-indigo-100 shadow-lg"
style={{ padding: "10px 12px" }}
>
<div
style={{
fontSize: "9px",
fontWeight: 800,
letterSpacing: "0.12em",
textTransform: "uppercase",
color: "#6366f1",
marginBottom: "5px",
}}
>
Schritt {step + 1} / {STEPS.length}
</div>
<div
style={{
fontSize: "12.5px",
color: "#334155",
lineHeight: 1.55,
fontWeight: 500,
}}
>
{STEPS[step]}
</div>
</div>
</foreignObject>
</>
)}
</svg>
{/* Replay button */}
{atSummit && (
<div className="text-center mt-4">
<button
onClick={replay}
className="text-sm text-indigo-600 border border-indigo-200 rounded-full px-5 py-2 hover:bg-indigo-50 transition-colors"
>
Nochmal ansehen
</button>
</div>
)}
</div>
{/* ── Mobile fallback: numbered list ── */}
<div className="md:hidden space-y-3">
{STEPS.map((text, i) => (
<div key={i} className="mc-card p-4 flex items-start gap-3">
<span className="w-7 h-7 rounded-full bg-gradient-to-br from-indigo-500 to-violet-500 text-white text-xs font-bold flex items-center justify-center flex-shrink-0 mt-0.5">
{i + 1}
</span>
<p className="text-slate-700 text-sm leading-relaxed">{text}</p>
</div>
))}
</div>
</div>
</section>
);
}

View file

@ -141,7 +141,7 @@ function WeekCard({ week, isLeft }: { week: Week; isLeft: boolean }) {
)}
</div>
{/* Title */}
<h4 className="font-extrabold text-slate-900 text-base leading-snug">{week.title}</h4>
<h4 className="font-extrabold text-slate-900 text-lg leading-snug">{week.title}</h4>
{/* Body */}
<p className="text-slate-500 text-sm leading-relaxed">{week.body}</p>
{/* Vorlagen */}

View file

@ -75,6 +75,23 @@ p {
details summary::-webkit-details-marker { display: none; }
/* ── Shared micro-animations ── */
@keyframes mc-fade-in {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
.mc-fade-in { animation: mc-fade-in 0.35s ease forwards; }
@keyframes mc-bounce {
0% { transform: translateY(0); }
30% { transform: translateY(-14px); }
55% { transform: translateY(-6px); }
75% { transform: translateY(-10px); }
90% { transform: translateY(-2px); }
100% { transform: translateY(0); }
}
.mc-bounce { animation: mc-bounce 0.9s ease forwards; }
/* ── Flip Cards ── */
.flip-card-wrapper {
perspective: 1000px;

View file

@ -1,11 +1,12 @@
import Image from "next/image";
import WeekTimeline from "./components/WeekTimeline";
import FlipCards from "./components/FlipCards";
import MountainClimb from "./components/MountainClimb";
import {
CalendarIcon, GradCapIcon, UsersIcon, ClipboardIcon,
TargetIcon, LightbulbIcon, DocumentIcon, PhoneIcon,
VideoCameraIcon, HandshakeIcon, SearchIcon, MapPinIcon,
MapIcon, RocketIcon, PackageIcon, ChatIcon, CheckIcon,
VideoCameraIcon, HandshakeIcon, SearchIcon, MapIcon,
PackageIcon, ChatIcon, CheckIcon,
} from "./components/Icons";
const CTA_HREF = "#platz-sichern";
@ -37,8 +38,8 @@ function IconCard({ icon, title, sub }: { icon: React.ReactNode; title: string;
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-indigo-500 to-violet-500 flex items-center justify-center shadow-lg">
{icon}
</div>
<p className="font-bold text-slate-900 text-sm leading-snug">{title}</p>
<p className="text-slate-500 text-xs leading-relaxed">{sub}</p>
<p className="font-bold text-slate-900 text-base leading-snug">{title}</p>
<p className="text-slate-500 text-sm leading-relaxed">{sub}</p>
</div>
);
}
@ -199,38 +200,9 @@ export default function Home() {
</section>
{/*
S4 WAS DU BAUST (Content Grid)
S4 WAS DU BAUST (Mountain animation)
*/}
<section className="py-20">
<div className="max-w-5xl mx-auto px-6">
<div className="text-center mb-12">
<Pill>Komplettpaket</Pill>
<h2 className="mt-4 text-3xl md:text-4xl font-extrabold text-slate-900">Was du in 8 Wochen baust und mit nach Hause nimmst</h2>
</div>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{(
[
{ icon: <TargetIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Deinen Wunschkunden klar definiert — wer er ist, was er braucht, was er schon versucht hat" },
{ icon: <LightbulbIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Dein Angebot auf den Punkt gebracht — was du anbietest, welches Problem du löst" },
{ icon: <MapPinIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Deine Positionierung in einem Satz — klar, sofort verständlich, für alle Kanäle nutzbar" },
{ icon: <DocumentIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Die komplette Struktur für deine Verkaufsseite — mit Feedback zur Umsetzung" },
{ icon: <MapIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Deine Marketing-Systemkarte — wie Sichtbarkeit, Vertrauen, Anfrage und Kauf zusammenhängen" },
{ icon: <CalendarIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Deinen 90-Tage-Marketingplan + Entscheidung welche 2 Kanäle du fokussierst" },
{ icon: <PhoneIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Deinen 4-Wochen Content-Plan + Themen die zeigen wofür du stehst — für alle Plattformen" },
{ icon: <RocketIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Deinen Launch-Plan — wie du dein Angebot aktiv über Social Media in den Markt bringst" },
{ icon: <VideoCameraIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Video-Setup erklärt — professionelle Videos mit dem Handy" },
{ icon: <HandshakeIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Dein persönliches Verkaufsskript für Direktansprache und Netzwerk" },
{ icon: <SearchIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Dein Google Business Profil optimiert + SEO-Grundlagen für deine Website" },
] as { icon: React.ReactNode; text: string }[]
).map(({ icon, text }, i) => (
<div key={i} className="mc-card p-4 flex items-start gap-3">
{icon}
<p className="text-slate-600 text-sm leading-relaxed">{text}</p>
</div>
))}
</div>
</div>
</section>
<MountainClimb />
<WeekTimeline />
@ -351,8 +323,8 @@ export default function Home() {
<span className="text-xs font-bold text-emerald-600 bg-emerald-50 border border-emerald-200 rounded-full px-2 py-0.5">Wert: {val}</span>
</div>
<p className="text-xs font-bold text-indigo-600 uppercase tracking-wide">Bonus {n}</p>
<p className="font-bold text-slate-900 text-sm">{title}</p>
<p className="text-slate-500 text-xs leading-relaxed">{desc}</p>
<p className="font-bold text-slate-900 text-base">{title}</p>
<p className="text-slate-500 text-sm leading-relaxed">{desc}</p>
</div>
))}
</div>
@ -449,7 +421,7 @@ export default function Home() {
{q}
<span className="text-indigo-500 group-open:rotate-180 transition-transform flex-shrink-0 ml-4 text-sm"></span>
</summary>
<div className="px-5 pb-5 text-slate-600 text-sm leading-relaxed border-t border-slate-100 pt-4">{a}</div>
<div className="px-5 pb-5 text-slate-600 text-base leading-relaxed border-t border-slate-100 pt-4">{a}</div>
</details>
))}
</div>