Templates/Oral & Facial Clinic
A quiet, almost monochrome page for a private clinic. Anthracite, white and one very light grey carry everything; the only colour is the gold of the review stars. The hero photograph holds three labels that follow the cursor, the treatment panels grow when you point at them and fold their text open, and the reviews travel past on their own. Generous rounded photo frames, no shadows, no coloured overlays.
The finished page to scroll through, switchable between desktop, tablet and mobile.
Source code
The complete page as a single file. Copy it into ./components and add the component to your route.
import {
useCallback,
useEffect,
useRef,
useState,
type CSSProperties,
type ReactNode,
} from "react";
import {
motion,
useInView,
useMotionValue,
useReducedMotion,
useSpring,
useTransform,
} from "framer-motion";
import {
ArrowRight,
ArrowUpRight,
Award,
BadgeCheck,
Building2,
CalendarDays,
Check,
ChevronDown,
Facebook,
Instagram,
Linkedin,
Mail,
MapPin,
Menu,
Phone,
Smile,
Sparkles,
Star,
Stethoscope,
X,
} from "lucide-react";
/*
* Luma Clinic: a full page for a private oral and facial surgery practice.
*
* Nearly monochrome. Anthracite, white and one very light grey carry the whole
* page; the only colour is the gold of the review stars and the four Google
* tones in its mark. Sections separate by a change of surface, white against
* near-white, never by a rule.
*
* Two motifs run through it. Photographs sit in generously rounded frames and
* grow very slowly under the cursor, and every block arrives once with a short
* lift. Everything else stays still.
*
* House rules: no shadows, no coloured overlays on the photographs, buttons
* with a small radius rather than fully round ones.
*/
const INK = "#3b3e44";
const INK_SOFT = "#8a8d93";
const SURFACE = "#f9fafb";
const SURFACE_2 = "#fafafa";
const LINE = "#e7e8ea";
const GOLD = "#c79a2e";
const SANS = '"Figtree", ui-sans-serif, system-ui, sans-serif';
const HEAD = '"Manrope", ui-sans-serif, system-ui, sans-serif';
const kopf: CSSProperties = { fontFamily: HEAD, fontWeight: 500, letterSpacing: "-0.02em" };
/** Ein kleiner Radius statt runder Kapseln, so wie im uebrigen Designsystem. */
const KNOPF = 6;
const BILD = 22;
const WEICH = [0.22, 1, 0.36, 1] as const;
/* ------------------------------------------------------------------ content */
const KLINIK = {
name: "Luma Clinic",
zusatz: "Oral & Facial Clinic",
strasse: "123 Example Street",
ort: "12345 Your City",
telefon: "+1 (555) 123 4567",
telefonHref: "tel:+15551234567",
mail: "hello@your-domain.com",
mailHref: "mailto:hello@your-domain.com",
};
const NAV = [
{ label: "Clinic", href: "#clinic" },
{ label: "Treatments", href: "#treatments", mega: true },
{ label: "Patient care", href: "#audiences" },
{ label: "For referrers", href: "#audiences" },
];
const MEGA = [
{
titel: "Implantology",
bild: "/templates/luma/menu-implantology.webp",
anriss: "Fixed restorations with a natural appearance, for quality of life that lasts.",
punkte: ["Single implants", "All-on-4", "Bone grafting", "Immediate implants"],
},
{
titel: "Oral surgery",
bild: "/templates/luma/menu-surgery.webp",
anriss: "Precise surgery of the mouth, jaw and face, performed by experienced specialists.",
punkte: ["Wisdom teeth", "Jaw surgery", "Cyst removal", "Facial trauma"],
},
{
titel: "Aesthetics",
bild: "/templates/luma/menu-aesthetics.webp",
anriss: "Gentle procedures for a fresh, natural result and a rested appearance.",
punkte: ["Botulinum toxin", "Dermal fillers", "Eyelid lift", "PRP therapy"],
},
];
/** Der dunkle Streifen rechts im Menue: ein Weg fuer alle, die nicht wissen,
* unter welche der drei Spalten ihr Anliegen faellt. */
const MEGA_RAT = {
kicker: "Personal consultation",
titel: "Not sure which treatment fits?",
text: "Talk to Dr. Reiter directly. In a calm first conversation we work through every detail of your individual treatment plan.",
knopf: "Book an appointment",
};
const HERO_PILLS = [
{ label: "Implantology", Icon: Smile, pos: "left-5 top-6", faktor: [14, 10] },
{
label: "Oral surgery",
Icon: Stethoscope,
pos: "right-5 top-1/2 -translate-y-1/2",
faktor: [-22, 16],
},
{
label: "Aesthetics",
Icon: Sparkles,
pos: "bottom-6 left-1/2 -translate-x-1/2",
faktor: [18, -12],
},
] as const;
const ZAHLEN = [
{ wert: 15, suffix: "+", label: "Years of experience" },
{ wert: 8000, suffix: "+", label: "Procedures" },
{ wert: 100, suffix: "%", label: "Private practice" },
];
const FELDER = [
{
kicker: "Implantology",
titel: "Fixed teeth. For a lifetime.",
text: "High-grade ceramic and titanium implants, planned on a 3D scan and placed with minimally invasive techniques. For a smile that simply stays.",
bild: "/templates/luma/bento-implantology.webp",
alt: "Close-up of a patient smiling after implant treatment",
},
{
kicker: "Oral surgery",
titel: "Surgery with a steady hand",
text: "Wisdom teeth, jaw corrections, cysts and trauma. Routine, gentle and supported by a modern operating suite.",
bild: "/templates/luma/bento-surgery.webp",
alt: "Surgeon at work in the operating suite",
},
{
kicker: "Aesthetics",
titel: "Your natural expression",
text: "Botulinum toxin, dermal fillers and aesthetic procedures, used with restraint. So you look like yourself, only more rested.",
bild: "/templates/luma/bento-aesthetics.webp",
alt: "Aesthetic consultation in a bright treatment room",
},
];
const GALERIE = [
{
src: "/templates/luma/clinic-4.webp",
alt: "Surgeon and assistant reviewing patient notes",
spalten: "md:col-span-8",
format: "aspect-[4/3]",
fokus: "",
},
{
src: "/templates/luma/clinic-2.webp",
alt: "Surgeon taking a quiet moment in the clinic lounge",
spalten: "md:col-span-4",
format: "",
fokus: "35% center",
},
{
src: "/templates/luma/clinic-3.webp",
alt: "Surgeon reviewing a panoramic x-ray",
spalten: "md:col-span-4",
format: "",
fokus: "62% center",
},
{
src: "/templates/luma/clinic-5.webp",
alt: "Consultation in the treatment room",
spalten: "md:col-span-8",
format: "aspect-[4/3]",
fokus: "",
},
{
src: "/templates/luma/clinic-1.webp",
alt: "Reception area of the clinic",
spalten: "md:col-span-12",
format: "aspect-[5/2]",
fokus: "",
},
];
const GRUENDE = [
{
Icon: Award,
kicker: "Experience",
titel: "15+ years alongside our patients",
text: "Thousands of procedures in implantology, oral and aesthetic facial surgery, carefully planned and calmly performed.",
},
{
Icon: Building2,
kicker: "The clinic",
titel: "A modern high-tech practice in the heart of the city",
text: "3D diagnostics, digital implant planning and our own operating suite, so every treatment is safe and comfortable.",
},
{
Icon: Sparkles,
kicker: "Specialisation",
titel: "Recognised experts in implantology",
text: "Specialised in complex cases, immediate implants and aesthetically perfect restorations, always with a natural result.",
},
];
const STIMMEN = [
{
text: "Finally a surgeon who listens. Dr. Reiter took his time over my implant consultation and explained everything calmly. The result looks completely natural.",
name: "Sarah M.",
behandlung: "Implant restoration",
quelle: "Google",
},
{
text: "An outstanding surgeon with a calm, professional manner. The wisdom tooth removal was completely painless and I was back on my feet after two days.",
name: "Mark T.",
behandlung: "Wisdom tooth removal",
quelle: "Verified",
},
{
text: "I look like myself, just fresher. That is exactly what I had hoped for. A very subtle, aesthetic result and a genuinely pleasant team.",
name: "Caroline B.",
behandlung: "Aesthetic treatment",
quelle: "Google",
},
{
text: "The consultation was honest and unhurried. Not a sales pitch, but a real plan for my smile. Today I wear it without thinking about it.",
name: "Julia R.",
behandlung: "Immediate implant",
quelle: "Google",
},
{
text: "From reception to aftercare, everything was at the highest level. You are not one patient among many here, you are genuinely seen.",
name: "Andrew K.",
behandlung: "Bone grafting",
quelle: "Verified",
},
];
const ZIELGRUPPEN = [
{
kicker: "For patients",
titel: "Your path to us",
text: "From the first enquiry to aftercare, we guide you personally. Answers to common questions and details on the procedure, costs and anaesthesia are all here.",
punkte: ["Process and booking", "Frequently asked questions", "Getting here and parking"],
cta: "View patient information",
},
{
kicker: "For referrers",
titel: "A reliable partnership",
text: "Collegial referrals with clear communication, fast reporting and prompt hand-back. We treat your patients the way you would treat them yourself.",
punkte: ["Online referral form", "Digital reporting", "Direct line to the clinical lead"],
cta: "Start a referral",
},
];
const FUSS_GRUPPEN = [
{ titel: "Clinic", links: ["About the clinic", "Team", "Careers"] },
{ titel: "Treatments", links: ["Implantology", "Oral surgery", "Aesthetics"] },
{ titel: "Service", links: ["For patients", "For referrers", "Book an appointment"] },
];
/* --------------------------------------------------------------- primitives */
/** Alles kommt gleich: ein kurzer Hub, ein langes Auslaufen, einmal. */
function Reveal({
children,
delay = 0,
className = "",
style,
}: {
children: ReactNode;
delay?: number;
className?: string;
style?: CSSProperties;
}) {
const reduce = useReducedMotion();
const ref = useRef<HTMLDivElement>(null);
const sichtbar = useInView(ref, { once: true, margin: "-80px" });
return (
<motion.div
ref={ref}
className={className}
style={style}
initial={reduce ? false : { opacity: 0, y: 28 }}
animate={sichtbar || reduce ? { opacity: 1, y: 0 } : { opacity: 0, y: 28 }}
transition={{ duration: reduce ? 0 : 0.9, ease: WEICH, delay: reduce ? 0 : delay }}
>
{children}
</motion.div>
);
}
/** Die Wortmarke: vier Buchstaben aus je einem Pfad, Farbe vom Elternteil. */
function Wortmarke({ className = "" }: { className?: string }) {
return (
<svg
viewBox="0 0 264 68"
role="img"
aria-label="Luma"
className={className}
fill="none"
stroke="currentColor"
strokeWidth={3.6}
strokeLinecap="butt"
strokeLinejoin="miter"
>
<path d="M4 4v60h42" />
<path d="M68 4v39a21 21 0 0 0 42 0V4" />
<path d="M132 64V4l28 38 28-38v60" />
<path d="M210 64 235 4l25 60M219 45h32" />
</svg>
);
}
/** Die vier Google-Farben, so wie sie in der Marke stehen. */
function GoogleMarke({ className = "" }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" className={className} role="img" aria-label="Google">
<path
fill="#4285F4"
d="M23.49 12.27c0-.79-.07-1.54-.19-2.27H12v4.51h6.44c-.28 1.48-1.12 2.73-2.39 3.58v2.98h3.86c2.26-2.08 3.58-5.15 3.58-8.8z"
/>
<path
fill="#34A853"
d="M12 24c3.24 0 5.95-1.08 7.93-2.91l-3.86-2.98c-1.07.72-2.45 1.16-4.07 1.16-3.13 0-5.78-2.11-6.73-4.96H1.29v3.09C3.26 21.3 7.31 24 12 24z"
/>
<path
fill="#FBBC05"
d="M5.27 14.31a7.2 7.2 0 010-4.62V6.6H1.29a12 12 0 000 10.8l3.98-3.09z"
/>
<path
fill="#EA4335"
d="M12 4.75c1.77 0 3.35.61 4.6 1.8l3.42-3.42C17.95 1.19 15.24 0 12 0 7.31 0 3.26 2.7 1.29 6.6l3.98 3.09C6.22 6.86 8.87 4.75 12 4.75z"
/>
</svg>
);
}
function Sterne({ groesse = 16, farbe = GOLD }: { groesse?: number; farbe?: string }) {
return (
<span className="inline-flex items-center gap-1" style={{ color: farbe }} aria-hidden>
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
style={{ width: groesse, height: groesse }}
fill="currentColor"
strokeWidth={0}
/>
))}
</span>
);
}
/** Die Zahl zaehlt einmal hoch, wenn sie ins Bild kommt. */
function Zaehler({ ziel, suffix = "" }: { ziel: number; suffix?: string }) {
const reduce = useReducedMotion();
const ref = useRef<HTMLSpanElement>(null);
const sichtbar = useInView(ref, { once: true, margin: "-60px" });
const [wert, setWert] = useState(0);
useEffect(() => {
if (!sichtbar) return;
if (reduce) {
setWert(ziel);
return;
}
const dauer = 1400;
const start = performance.now();
let id = 0;
const schritt = (jetzt: number) => {
const t = Math.min(1, (jetzt - start) / dauer);
// Am Ende ausrollen: eine Zahl, die abrupt stehen bleibt, wirkt wie ein
// Sprung, nicht wie ein Zaehler.
setWert(Math.round(ziel * (1 - Math.pow(1 - t, 3))));
if (t < 1) id = requestAnimationFrame(schritt);
};
id = requestAnimationFrame(schritt);
return () => cancelAnimationFrame(id);
}, [sichtbar, reduce, ziel]);
return (
<span ref={ref} className="tabular-nums">
{wert.toLocaleString("en-US")}
{suffix}
</span>
);
}
function Kicker({ children, hell = false }: { children: ReactNode; hell?: boolean }) {
return (
<span
className="text-[11px] uppercase"
style={{
fontFamily: SANS,
letterSpacing: "0.2em",
color: hell ? "rgba(255,255,255,0.55)" : "rgba(59,62,68,0.6)",
}}
>
{children}
</span>
);
}
function Knopf({
children,
href = "#appointment",
variante = "voll",
Icon,
}: {
children: ReactNode;
href?: string;
variante?: "voll" | "linie";
Icon?: typeof ArrowRight;
}) {
const voll = variante === "voll";
return (
<a
href={href}
className="group inline-flex items-center justify-center gap-0 px-6 py-3.5 text-sm transition-colors sm:px-7"
style={{
borderRadius: KNOPF,
fontFamily: SANS,
background: voll ? INK : "#ffffff",
color: voll ? "#ffffff" : INK,
border: voll ? "1px solid transparent" : `1px solid ${LINE}`,
}}
>
{children}
{Icon ? (
// Das Symbol erscheint erst beim Hover und schiebt dabei den Text nicht
// um: es waechst aus der Breite null heraus.
<Icon
className="h-4 w-4 max-w-0 -translate-x-1 opacity-0 transition-all duration-300 ease-out group-hover:ml-2 group-hover:max-w-[1rem] group-hover:translate-x-0 group-hover:opacity-100"
strokeWidth={1.75}
/>
) : null}
</a>
);
}
/* ------------------------------------------------------------------- header */
function Kopfzeile() {
const [offen, setOffen] = useState(false);
const [megaOffen, setMegaOffen] = useState(false);
const [behandlungen, setBehandlungen] = useState(false);
useEffect(() => {
if (!offen) return;
const vorher = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = vorher;
};
}, [offen]);
return (
<>
{/* Der Schleier liegt ausserhalb des Kopfs. Der Kopf hat einen
Hintergrund-Weichzeichner, und ein Element mit backdrop-filter ist der
Bezugsrahmen fuer position:fixed seiner Nachfahren: drinnen waere der
Schleier nur so hoch wie der Kopf selbst. */}
<div
aria-hidden
className="pointer-events-none fixed inset-x-0 bottom-0 top-20 z-40 hidden transition-opacity duration-300 lg:block"
style={{
background: "rgba(24,26,29,0.4)",
opacity: behandlungen ? 1 : 0,
visibility: behandlungen ? "visible" : "hidden",
}}
/>
<header
className="sticky top-0 z-50 backdrop-blur"
style={{ borderBottom: `1px solid ${LINE}`, background: "rgba(255,255,255,0.9)" }}
>
<div className="mx-auto flex h-20 max-w-7xl items-center justify-between px-5 sm:px-6">
<a href="#top" className="flex items-center gap-3" aria-label={KLINIK.name}>
<Wortmarke className="h-6 w-auto" />
<span className="hidden h-6 w-px sm:block" style={{ background: LINE }} />
<span className="hidden text-sm sm:inline" style={{ ...kopf, color: INK_SOFT }}>
{KLINIK.zusatz}
</span>
</a>
<nav className="hidden items-center gap-10 lg:flex">
{NAV.map((eintrag) =>
eintrag.mega ? (
// Auf- und zugehen haengt am Zustand, nicht an :hover. So kann
// der Schleier ausserhalb des Kopfs liegen, und die Tastatur
// kommt ueber den Fokus an dieselbe Tafel.
<div
key={eintrag.label}
onMouseEnter={() => setBehandlungen(true)}
onMouseLeave={() => setBehandlungen(false)}
onFocus={() => setBehandlungen(true)}
onBlur={(e) => {
if (!e.currentTarget.contains(e.relatedTarget as Node)) setBehandlungen(false);
}}
>
<button
type="button"
aria-expanded={behandlungen}
className="inline-flex items-center gap-1 py-7 text-sm transition-colors"
style={{ fontFamily: SANS, color: INK_SOFT }}
>
{eintrag.label}
<ChevronDown
className={`h-4 w-4 transition-transform duration-300 ${
behandlungen ? "rotate-180" : ""
}`}
/>
</button>
<Megamenue offen={behandlungen} />
</div>
) : (
<a
key={eintrag.label}
href={eintrag.href}
className="text-sm transition-colors hover:opacity-70"
style={{ fontFamily: SANS, color: INK_SOFT }}
>
{eintrag.label}
</a>
),
)}
</nav>
<div className="flex items-center gap-3">
<a
href="#appointment"
className="hidden px-5 py-2.5 text-sm text-white transition-colors sm:inline-flex"
style={{ borderRadius: KNOPF, background: INK, fontFamily: SANS }}
>
Book appointment
</a>
<button
type="button"
aria-label={offen ? "Close menu" : "Open menu"}
aria-expanded={offen}
onClick={() => setOffen((v) => !v)}
className="inline-flex h-10 w-10 items-center justify-center lg:hidden"
style={{ borderRadius: KNOPF, border: `1px solid ${LINE}`, color: INK_SOFT }}
>
{offen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
</div>
{offen ? (
<div
className="absolute inset-x-0 top-full z-[60] max-h-[calc(100svh-5rem)] overflow-y-auto bg-white lg:hidden"
style={{ borderTop: `1px solid ${LINE}` }}
>
<nav className="flex flex-col px-5 py-6">
{NAV.map((eintrag) =>
eintrag.mega ? (
<div key={eintrag.label} style={{ borderBottom: `1px solid ${LINE}` }}>
<button
type="button"
onClick={() => setMegaOffen((v) => !v)}
aria-expanded={megaOffen}
className="flex w-full items-center justify-between py-4 text-lg"
style={{ color: INK, fontFamily: SANS }}
>
{eintrag.label}
<ChevronDown
className={`h-5 w-5 transition-transform duration-300 ${
megaOffen ? "rotate-180" : ""
}`}
/>
</button>
{/* Auf- und zuklappen ueber die Zeilenhoehe des Rasters: das
laeuft weich, ohne eine Hoehe zu kennen. */}
<div
className={`grid transition-all duration-300 ease-out ${
megaOffen ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"
}`}
>
<div className="overflow-hidden">
<div className="flex flex-col gap-6 pb-5">
{MEGA.map((gruppe) => (
<div key={gruppe.titel} className="flex flex-col gap-2">
<Kicker>{gruppe.titel}</Kicker>
<div className="flex flex-col">
{gruppe.punkte.map((punkt) => (
<a
key={punkt}
href="#treatments"
onClick={() => setOffen(false)}
className="py-2 text-base"
style={{ color: INK, fontFamily: SANS }}
>
{punkt}
</a>
))}
</div>
</div>
))}
</div>
</div>
</div>
</div>
) : (
<a
key={eintrag.label}
href={eintrag.href}
onClick={() => setOffen(false)}
className="block py-4 text-lg"
style={{ borderBottom: `1px solid ${LINE}`, color: INK, fontFamily: SANS }}
>
{eintrag.label}
</a>
),
)}
<a
href="#appointment"
onClick={() => setOffen(false)}
className="mt-8 inline-flex items-center justify-center px-6 py-3.5 text-sm text-white"
style={{ borderRadius: KNOPF, background: INK, fontFamily: SANS }}
>
Book appointment
</a>
</nav>
</div>
) : null}
</header>
</>
);
}
/** Das grosse Menue unter "Treatments". Nur ab lg, per Hover. */
function Megamenue({ offen }: { offen: boolean }) {
return (
<div
className="absolute inset-x-0 top-full z-40 overflow-hidden transition-all duration-200"
style={{
visibility: offen ? "visible" : "hidden",
opacity: offen ? 1 : 0,
background: "#ffffff",
borderTop: `1px solid ${LINE}`,
borderBottomLeftRadius: 24,
borderBottomRightRadius: 24,
}}
>
<div className="mx-auto flex w-full max-w-[1600px] flex-col lg:flex-row">
<div className="grid flex-1 grid-cols-1 md:grid-cols-3">
{MEGA.map((gruppe, i) => (
<div
key={gruppe.titel}
className="group/spalte flex cursor-pointer flex-col gap-5 px-8 py-9"
style={i < MEGA.length - 1 ? { borderRight: `1px solid ${LINE}` } : undefined}
>
<div
className="aspect-[16/10] w-full overflow-hidden"
style={{ borderRadius: 16, background: SURFACE }}
>
{/* Das Bild waechst nur beim Anfahren der Spalte, langsam,
wie ueberall sonst auf dieser Seite. */}
<img
src={gruppe.bild}
alt=""
aria-hidden
loading="lazy"
className="h-full w-full object-cover transition-transform duration-700 ease-out group-hover/spalte:scale-[1.05]"
/>
</div>
<div>
<h3 className="text-[19px] leading-tight" style={{ ...kopf, color: INK }}>
{gruppe.titel}
</h3>
<p
className="mt-2 text-sm leading-relaxed"
style={{ fontFamily: SANS, color: INK_SOFT }}
>
{gruppe.anriss}
</p>
</div>
<nav className="flex flex-col gap-3">
{gruppe.punkte.map((punkt) => (
<a
key={punkt}
href="#treatments"
className="group/zeile flex items-center justify-between text-sm transition-colors hover:opacity-70"
style={{ fontFamily: SANS, color: INK }}
>
{punkt}
{/* Der Pfeil faehrt von links ein, statt einfach da zu
sein: die Zeile bekommt eine Richtung. */}
<ArrowRight className="h-3.5 w-3.5 -translate-x-2 opacity-0 transition-all duration-300 group-hover/zeile:translate-x-0 group-hover/zeile:opacity-100" />
</a>
))}
</nav>
</div>
))}
</div>
<aside
className="w-full shrink-0 px-8 py-9 lg:w-[340px] lg:px-9"
style={{ background: INK }}
>
<div className="flex h-full flex-col justify-between gap-9">
<div>
<span
className="inline-block px-3 py-1 text-[10px] uppercase"
style={{
borderRadius: 999,
background: "rgba(255,255,255,0.12)",
color: "rgba(255,255,255,0.8)",
fontFamily: SANS,
letterSpacing: "0.2em",
}}
>
{MEGA_RAT.kicker}
</span>
<h4 className="mt-5 text-[22px] leading-tight" style={{ ...kopf, color: "#ffffff" }}>
{MEGA_RAT.titel}
</h4>
<p
className="mt-4 text-sm leading-relaxed"
style={{ fontFamily: SANS, color: "rgba(255,255,255,0.7)" }}
>
{MEGA_RAT.text}
</p>
</div>
<a
href="#appointment"
className="group/rat flex w-full items-center justify-between px-6 py-3.5 text-sm transition-opacity hover:opacity-90"
style={{ borderRadius: KNOPF, background: "#ffffff", color: INK, fontFamily: SANS }}
>
{MEGA_RAT.knopf}
<ArrowUpRight className="h-4 w-4 transition-transform duration-300 group-hover/rat:-translate-y-0.5 group-hover/rat:translate-x-0.5" />
</a>
</div>
</aside>
</div>
</div>
);
}
/* --------------------------------------------------------------------- hero */
function Held() {
return (
<section id="top" className="bg-white">
<div className="mx-auto grid max-w-7xl grid-cols-1 items-center gap-12 px-5 py-16 sm:px-6 sm:py-24 lg:grid-cols-2 lg:gap-20 lg:py-32">
<Reveal className="flex flex-col items-center text-center lg:items-start lg:text-left">
<span
className="inline-flex w-fit items-center gap-2 px-3.5 py-1.5 text-xs"
style={{
borderRadius: KNOPF,
border: `1px solid ${LINE}`,
color: "rgba(59,62,68,0.62)",
fontFamily: SANS,
letterSpacing: "0.02em",
}}
>
<MapPin className="h-3.5 w-3.5" style={{ color: INK }} strokeWidth={1.75} />
In the heart of the city
</span>
<h1
className="mt-6 text-4xl leading-[1.08] sm:mt-8 sm:text-5xl lg:text-[4.25rem] lg:leading-[1.04]"
style={{ ...kopf, color: INK }}
>
<span className="block">Natural beauty</span>
<span className="block sm:hidden">begins with your smile</span>
<span className="hidden sm:block">begins with</span>
<span className="hidden sm:block">your smile</span>
</h1>
<p
className="mt-6 max-w-xl text-base leading-relaxed sm:mt-8 sm:text-lg"
style={{ fontFamily: SANS, color: INK_SOFT }}
>
The clinic for implantology, oral surgery and aesthetic facial surgery.
</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-3 sm:mt-10 lg:justify-start">
<Knopf href="#treatments" Icon={ArrowRight}>
Explore treatments
</Knopf>
<Knopf href="#appointment" variante="linie" Icon={CalendarDays}>
Book an appointment
</Knopf>
</div>
<div className="mt-12 flex flex-wrap items-center justify-center gap-5 lg:justify-start">
<div className="flex -space-x-2">
{[
"/templates/luma/avatar-1.webp",
"/templates/luma/avatar-2.webp",
"/templates/luma/bento-surgery.webp",
].map((src) => (
<img
key={src}
src={src}
alt=""
aria-hidden
loading="lazy"
className="h-10 w-10 rounded-full border-2 border-white object-cover"
/>
))}
</div>
<div className="flex flex-col items-center lg:items-start">
<div className="flex items-center gap-2">
<Sterne groesse={15} />
<span className="text-sm" style={{ fontFamily: SANS, color: INK, fontWeight: 500 }}>
4.9 / 5
</span>
</div>
<p className="mt-1 text-sm" style={{ fontFamily: SANS, color: INK_SOFT }}>
from more than 380 verified patient reviews
</p>
</div>
</div>
</Reveal>
<Reveal delay={0.15} className="relative">
<HeldBild />
</Reveal>
</div>
</section>
);
}
/**
* Das Heldenbild mit drei Schildern darauf. Sie folgen der Maus, jedes mit
* eigenem Faktor und eigener Richtung: gleiche Faktoren saehen aus wie eine
* verschobene Ebene, verschiedene wie drei Dinge im Raum.
*/
function HeldBild() {
const reduce = useReducedMotion();
const ref = useRef<HTMLDivElement>(null);
const mx = useMotionValue(0);
const my = useMotionValue(0);
const sx = useSpring(mx, { stiffness: 60, damping: 18, mass: 0.8 });
const sy = useSpring(my, { stiffness: 60, damping: 18, mass: 0.8 });
const bewegen = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (reduce) return;
const r = ref.current?.getBoundingClientRect();
if (!r) return;
mx.set(((e.clientX - r.left) / r.width - 0.5) * 2);
my.set(((e.clientY - r.top) / r.height - 0.5) * 2);
},
[mx, my, reduce],
);
const zurueck = useCallback(() => {
mx.set(0);
my.set(0);
}, [mx, my]);
const x1 = useTransform(sx, (v) => v * HERO_PILLS[0].faktor[0]);
const y1 = useTransform(sy, (v) => v * HERO_PILLS[0].faktor[1]);
const x2 = useTransform(sx, (v) => v * HERO_PILLS[1].faktor[0]);
const y2 = useTransform(sy, (v) => v * HERO_PILLS[1].faktor[1]);
const x3 = useTransform(sx, (v) => v * HERO_PILLS[2].faktor[0]);
const y3 = useTransform(sy, (v) => v * HERO_PILLS[2].faktor[1]);
const bewegungen = [
{ x: x1, y: y1 },
{ x: x2, y: y2 },
{ x: x3, y: y3 },
];
return (
<div
ref={ref}
onMouseMove={bewegen}
onMouseLeave={zurueck}
className="group relative overflow-hidden"
style={{ borderRadius: BILD, background: SURFACE }}
>
<img
src="/templates/luma/hero-portrait.webp"
alt="Three patients of the clinic, close together"
loading="eager"
decoding="async"
className="h-[440px] w-full object-cover transition-transform duration-[1200ms] ease-out group-hover:scale-105 sm:h-[620px] lg:h-[720px]"
/>
<div className="pointer-events-none absolute inset-0">
{HERO_PILLS.map((pill, i) => (
<motion.div
key={pill.label}
style={{
x: bewegungen[i].x,
y: bewegungen[i].y,
borderRadius: KNOPF,
background: "rgba(255,255,255,0.88)",
color: INK,
fontFamily: SANS,
}}
className={`absolute ${pill.pos} flex items-center gap-2 px-3.5 py-2 text-xs font-medium backdrop-blur-md`}
>
<pill.Icon className="h-4 w-4" strokeWidth={1.75} />
{pill.label}
</motion.div>
))}
</div>
</div>
);
}
/* -------------------------------------------------------------------- arzt */
function Arzt() {
return (
<section id="clinic" style={{ background: SURFACE }}>
<div className="mx-auto grid max-w-7xl grid-cols-1 gap-12 px-5 py-20 sm:gap-8 sm:px-6 sm:py-24 lg:grid-cols-12 lg:gap-16 lg:py-32">
<Reveal className="order-1 lg:order-2 lg:col-span-7">
<Kicker>Personal. Precise. Discreet.</Kicker>
<h2
className="mt-4 text-[28px] leading-tight sm:text-4xl lg:text-5xl"
style={{ ...kopf, color: INK }}
>
Dr. Jonas Reiter
</h2>
<p className="mt-3 text-base sm:text-lg" style={{ fontFamily: SANS, color: INK_SOFT }}>
Consultant in oral and maxillofacial surgery
</p>
<div
className="mt-8 max-w-xl space-y-5 text-[15px] leading-relaxed sm:text-base"
style={{ fontFamily: SANS, color: INK_SOFT }}
>
<p>
Beauty is never the goal for me, it is the result of a careful procedure. I take my
time, I listen, and together with every patient I develop a plan that suits their face
and their story.
</p>
<p>
With more than 15 years in implantology, oral surgery and aesthetic facial surgery, I
combine medical precision with a fine sense for natural results.
</p>
</div>
<div className="mt-10 hidden lg:block">
<Knopf href="#impressions" variante="linie">
More about the clinic
</Knopf>
</div>
</Reveal>
<Reveal delay={0.05} className="order-2 lg:order-3 lg:col-span-7">
<div
className="grid grid-cols-3 pt-2 sm:gap-10 sm:pt-8"
style={{ borderTop: `1px solid ${LINE}` }}
>
{ZAHLEN.map((z, i) => (
<div
key={z.label}
className="px-3 text-center sm:px-0 sm:text-left"
style={i > 0 ? { borderLeft: `1px solid ${LINE}` } : undefined}
>
<div className="text-2xl sm:text-3xl" style={{ ...kopf, color: INK }}>
<Zaehler ziel={z.wert} suffix={z.suffix} />
</div>
<div
className="mt-1 text-[11px] leading-tight sm:text-sm"
style={{ fontFamily: SANS, color: "rgba(59,62,68,0.7)" }}
>
{z.label}
</div>
</div>
))}
</div>
<div className="mt-10 lg:hidden">
<Knopf href="#impressions" variante="linie">
More about the clinic
</Knopf>
</div>
</Reveal>
<Reveal delay={0.1} className="order-3 lg:order-1 lg:col-span-5 lg:row-span-2">
<div
className="group relative overflow-hidden"
style={{ borderRadius: BILD, background: "#eef0f2" }}
>
<img
src="/templates/luma/doctor-portrait.webp"
alt="Dr. Jonas Reiter in the consulting room"
loading="lazy"
className="aspect-[4/5] w-full object-cover transition-transform duration-[1200ms] ease-out group-hover:scale-105 lg:aspect-[3/4]"
/>
<span
className="absolute left-4 top-4 inline-flex items-center px-3.5 py-1.5 text-xs backdrop-blur sm:left-5 sm:top-5"
style={{
borderRadius: KNOPF,
background: "rgba(255,255,255,0.82)",
color: INK,
fontFamily: SANS,
}}
>
Your surgeon
</span>
</div>
</Reveal>
</div>
</section>
);
}
/* --------------------------------------------------------------- treatments */
function Behandlungen() {
return (
<section id="treatments" className="bg-white">
<div className="mx-auto max-w-7xl px-5 py-16 sm:px-6 sm:py-24 lg:py-32">
<Reveal>
<div className="grid grid-cols-1 items-end gap-10 lg:grid-cols-12">
<div className="lg:col-span-7">
<Kicker>Our treatments</Kicker>
<h2 className="mt-5 text-3xl sm:text-4xl lg:text-5xl" style={{ ...kopf, color: INK }}>
Three specialties, one standard
</h2>
</div>
<p className="text-base lg:col-span-5" style={{ fontFamily: SANS, color: INK_SOFT }}>
Implantology, oral surgery and aesthetic facial surgery under one roof. Specialised.
Discreet. At the highest level.
</p>
</div>
</Reveal>
<Reveal delay={0.1}>
{/* Ab md teilen sich die drei Tafeln die Breite und die angefahrene
waechst auf gut das Doppelte. Darunter stehen sie untereinander,
weil eine Ziehharmonika auf dem Telefon nichts zu greifen hat. */}
<div className="mt-10 flex flex-col gap-3 sm:mt-14 md:h-[560px] md:flex-row lg:h-[620px]">
{FELDER.map((feld) => (
<article
key={feld.kicker}
className="group relative h-[420px] flex-1 overflow-hidden transition-[flex-grow] duration-[900ms] md:h-auto md:hover:grow-[2.4]"
style={{
borderRadius: BILD,
transitionTimingFunction: "cubic-bezier(0.22,1,0.36,1)",
}}
>
<img
src={feld.bild}
alt={feld.alt}
loading="lazy"
className="absolute inset-0 h-full w-full object-cover transition-transform duration-[1600ms] group-hover:scale-[1.06]"
style={{ transitionTimingFunction: "cubic-bezier(0.22,1,0.36,1)" }}
/>
{/* Nur so viel Verlauf, wie die weisse Schrift braucht, und in
der Farbe des Textes statt in einer bunten. */}
<div
className="absolute inset-0"
style={{
background:
"linear-gradient(to top, rgba(31,33,37,0.86) 0%, rgba(31,33,37,0.42) 46%, rgba(31,33,37,0.1) 100%)",
}}
/>
<div className="relative flex h-full flex-col justify-between p-7 text-white lg:p-10">
<div className="flex items-start justify-end">
<span
className="inline-flex h-10 w-10 items-center justify-center opacity-0 transition-opacity duration-500 group-hover:opacity-100"
style={{ borderRadius: KNOPF, border: "1px solid rgba(255,255,255,0.4)" }}
>
<ArrowUpRight className="h-4 w-4" />
</span>
</div>
<div>
<div
className="text-[11px] uppercase"
style={{
fontFamily: SANS,
letterSpacing: "0.3em",
color: "rgba(255,255,255,0.6)",
}}
>
{feld.kicker}
</div>
<h3
className="mt-4 max-w-md text-2xl leading-tight lg:text-[2rem]"
style={{ ...kopf, color: "#ffffff" }}
>
{feld.titel}
</h3>
{/* Der Satz steht auf dem Telefon immer, am Schreibtisch
klappt er beim Anfahren auf: 0fr zu 1fr, ohne feste
Hoehe. */}
<div
className="grid grid-rows-[1fr] transition-[grid-template-rows] duration-[1100ms] md:grid-rows-[0fr] md:group-hover:grid-rows-[1fr]"
style={{ transitionTimingFunction: "cubic-bezier(0.22,1,0.36,1)" }}
>
<div className="overflow-hidden">
<p
className="mt-5 max-w-md text-[13px] leading-relaxed transition-all duration-[800ms] md:translate-y-2 md:opacity-0 md:group-hover:translate-y-0 md:group-hover:opacity-100 md:group-hover:delay-200 lg:text-sm"
style={{
fontFamily: SANS,
color: "rgba(255,255,255,0.78)",
transitionTimingFunction: "cubic-bezier(0.22,1,0.36,1)",
}}
>
{feld.text}
</p>
</div>
</div>
</div>
</div>
</article>
))}
</div>
</Reveal>
<Reveal delay={0.2}>
<div
id="appointment"
className="relative mt-6 flex flex-col items-start justify-between gap-6 overflow-hidden p-6 sm:flex-row sm:items-center sm:p-8 lg:p-10"
style={{ borderRadius: BILD, background: SURFACE }}
>
<img
src="/templates/luma/brand-mark.svg"
alt=""
aria-hidden
className="pointer-events-none absolute -right-10 -top-12 h-48 w-auto opacity-[0.06] sm:h-56 lg:h-64"
/>
<div className="relative">
<Kicker>Appointment</Kicker>
<h3
className="mt-3 max-w-xl text-xl sm:text-2xl lg:text-[1.75rem]"
style={{ ...kopf, color: INK }}
>
First consultation within 48 hours. In person, with no waiting.
</h3>
</div>
<a
href="#contact"
className="relative inline-flex items-center gap-2 px-6 py-3.5 text-sm text-white transition-colors"
style={{ borderRadius: KNOPF, background: INK, fontFamily: SANS }}
>
Book an appointment
<ArrowUpRight className="h-4 w-4" />
</a>
</div>
</Reveal>
</div>
</section>
);
}
/* ------------------------------------------------------------------ galerie */
function Galerie() {
const [aktiv, setAktiv] = useState(0);
const start = useRef<number | null>(null);
return (
<section id="impressions" className="overflow-x-hidden" style={{ background: SURFACE }}>
<div className="mx-auto max-w-7xl px-5 py-16 sm:px-6 sm:py-24 lg:py-32">
<Reveal className="grid grid-cols-1 items-end gap-10 lg:grid-cols-12">
<div className="lg:col-span-7">
<Kicker>Inside the clinic</Kicker>
<h2 className="mt-5 text-3xl sm:text-4xl lg:text-5xl" style={{ ...kopf, color: INK }}>
A place where people feel at ease
</h2>
</div>
<p
className="max-w-md text-base leading-relaxed lg:col-span-5"
style={{ fontFamily: SANS, color: INK_SOFT }}
>
Bright rooms, a calm atmosphere and a team that takes its time, so every visit feels
like less of an appointment.
</p>
</Reveal>
{/* Telefon: ein Bild je Ansicht, mit dem Finger weiterschieben. */}
<div
className="mt-10 touch-pan-y overflow-hidden overscroll-x-contain md:hidden"
onTouchStart={(e) => {
start.current = e.touches[0]?.clientX ?? null;
}}
onTouchEnd={(e) => {
if (start.current === null) return;
const ende = e.changedTouches[0]?.clientX;
if (ende === undefined) return;
const weg = ende - start.current;
if (Math.abs(weg) >= 40) {
setAktiv((i) => (weg < 0 ? Math.min(i + 1, GALERIE.length - 1) : Math.max(i - 1, 0)));
}
start.current = null;
}}
>
<div
className="flex will-change-transform"
style={{
transform: `translate3d(-${aktiv * 100}%, 0, 0)`,
transition: "transform 420ms cubic-bezier(0.22, 1, 0.36, 1)",
}}
>
{GALERIE.map((bild) => (
<div key={bild.src} className="w-full shrink-0 px-1">
<div
className="overflow-hidden"
style={{ borderRadius: BILD, background: "#eef0f2" }}
>
<img
src={bild.src}
alt={bild.alt}
loading="lazy"
className="aspect-[4/3] w-full object-cover"
/>
</div>
</div>
))}
</div>
</div>
<div className="mt-4 flex justify-center gap-2 md:hidden">
{GALERIE.map((bild, i) => (
<button
key={bild.src}
type="button"
onClick={() => setAktiv(i)}
aria-label={`Image ${i + 1}`}
className="h-2 rounded-full transition-all duration-300"
style={{
width: i === aktiv ? 24 : 8,
background: i === aktiv ? INK : "rgba(59,62,68,0.2)",
}}
/>
))}
</div>
{/* Schreibtisch: ein Raster aus sechs Bildern in drei Reihen. */}
<div className="mt-10 hidden grid-cols-12 gap-3 sm:mt-16 sm:gap-4 md:grid lg:gap-6">
{GALERIE.map((bild, i) => (
<Reveal key={bild.src} delay={(i % 2) * 0.08} className={`col-span-12 ${bild.spalten}`}>
<div
className="group h-full overflow-hidden"
style={{ borderRadius: BILD, background: "#eef0f2" }}
>
<img
src={bild.src}
alt={bild.alt}
loading="lazy"
style={bild.fokus ? { objectPosition: bild.fokus } : undefined}
className={`${bild.format || "h-full"} w-full object-cover transition-transform duration-[1200ms] ease-out group-hover:scale-105`}
/>
</div>
</Reveal>
))}
</div>
</div>
</section>
);
}
/* ------------------------------------------------------------------ gruende */
function Gruende() {
return (
<section className="bg-white">
<div className="mx-auto max-w-7xl px-5 py-16 sm:px-6 sm:py-20 lg:py-28">
<Reveal>
<div className="mx-auto max-w-2xl text-center">
<Kicker>Why Luma</Kicker>
<h2 className="mt-5 text-2xl sm:text-3xl lg:text-4xl" style={{ ...kopf, color: INK }}>
Three reasons that speak for themselves
</h2>
</div>
</Reveal>
<div className="mt-10 grid grid-cols-1 gap-4 sm:mt-14 sm:gap-6 md:grid-cols-3">
{GRUENDE.map((grund, i) => (
<Reveal key={grund.kicker} delay={i * 0.08}>
{/* Zwei Lagen uebereinander: die helle geht hoch und weg, die
dunkle kommt hoch und bleibt. Auf dem Telefon steht beides
ohne Wechsel untereinander. */}
<div
className="group relative h-auto overflow-hidden md:h-[340px]"
style={{ borderRadius: BILD, background: SURFACE_2 }}
>
<div
className="relative flex flex-col items-center gap-6 p-8 text-center transition-all duration-[900ms] md:absolute md:inset-0 md:justify-between md:group-hover:-translate-y-3 md:group-hover:opacity-0 lg:p-10"
style={{
background: SURFACE_2,
transitionTimingFunction: "cubic-bezier(0.22,1,0.36,1)",
}}
>
<div
className="flex h-12 w-12 items-center justify-center transition-transform duration-[900ms] group-hover:-translate-y-1"
style={{ borderRadius: 14, background: "#ffffff", color: INK }}
>
<grund.Icon className="h-5 w-5" strokeWidth={1.5} />
</div>
<div>
<Kicker>{grund.kicker}</Kicker>
<h3
className="mt-4 text-xl sm:text-2xl lg:text-[1.625rem] lg:leading-snug"
style={{ ...kopf, color: INK }}
>
{grund.titel}
</h3>
</div>
<p
className="text-sm leading-relaxed md:hidden"
style={{ fontFamily: SANS, color: INK_SOFT }}
>
{grund.text}
</p>
</div>
<div
className="absolute inset-0 hidden translate-y-4 flex-col items-center justify-between p-8 text-center opacity-0 transition-all duration-[900ms] md:flex md:group-hover:translate-y-0 md:group-hover:opacity-100 lg:p-10"
style={{
borderRadius: BILD,
background: INK,
color: "#ffffff",
transitionTimingFunction: "cubic-bezier(0.22,1,0.36,1)",
}}
>
<div
className="flex h-12 w-12 items-center justify-center"
style={{ borderRadius: 14, background: "rgba(255,255,255,0.12)" }}
>
<grund.Icon className="h-5 w-5" strokeWidth={1.5} />
</div>
<p
className="text-base leading-relaxed"
style={{ fontFamily: SANS, color: "rgba(255,255,255,0.86)" }}
>
{grund.text}
</p>
<Kicker hell>{grund.kicker}</Kicker>
</div>
</div>
</Reveal>
))}
</div>
</div>
</section>
);
}
/* ------------------------------------------------------------------ stimmen */
function Stimmen() {
const reduce = useReducedMotion();
const doppelt = [...STIMMEN, ...STIMMEN];
return (
<section className="bg-white">
<div className="mx-auto px-5 py-16 sm:px-6 sm:py-24 lg:px-12 lg:py-32">
<div className="grid grid-cols-1 gap-12 lg:grid-cols-12 lg:gap-16">
<Reveal className="lg:col-span-4">
<div className="lg:sticky lg:top-28">
<Kicker>Reviews</Kicker>
<h2 className="mt-5 text-3xl sm:text-4xl lg:text-5xl" style={{ ...kopf, color: INK }}>
What our patients say
</h2>
<p
className="mt-6 max-w-sm text-base leading-relaxed"
style={{ fontFamily: SANS, color: INK_SOFT }}
>
More than 380 verified reviews on Google and independent platforms. Thank you to our
patients for their trust.
</p>
<div className="mt-10 flex items-center gap-5">
<div className="flex items-baseline gap-2">
<span className="text-5xl" style={{ ...kopf, color: INK }}>
4.9
</span>
<span className="text-sm" style={{ fontFamily: SANS, color: INK_SOFT }}>
/ 5
</span>
</div>
<div className="h-10 w-px" style={{ background: LINE }} />
<div>
<Sterne groesse={14} />
<div className="mt-1 text-xs" style={{ fontFamily: SANS, color: INK_SOFT }}>
from more than 380 reviews
</div>
</div>
</div>
</div>
</Reveal>
<div className="lg:col-span-8">
<div className="relative -mx-5 overflow-hidden sm:-mx-6 lg:-mr-12">
<div
aria-hidden
className="pointer-events-none absolute inset-y-0 left-0 z-10 hidden w-16 sm:w-24 md:block"
style={{ background: "linear-gradient(to right, #ffffff, rgba(255,255,255,0))" }}
/>
<div
aria-hidden
className="pointer-events-none absolute inset-y-0 right-0 z-10 hidden w-16 sm:w-24 md:block"
style={{ background: "linear-gradient(to left, #ffffff, rgba(255,255,255,0))" }}
/>
{/* Telefon: schieben. Schreibtisch: eine ruhige Wanderung, die
beim Anfahren stehen bleibt. Bei reduzierter Bewegung steht
sie ganz. */}
<motion.div
className="flex gap-5 snap-x snap-mandatory overflow-x-auto overscroll-x-contain px-5 pb-4 sm:px-6 md:w-max md:snap-none md:overflow-visible [&::-webkit-scrollbar]:hidden"
style={{ scrollbarWidth: "none" }}
animate={reduce ? undefined : { x: ["0%", "-50%"] }}
transition={reduce ? undefined : { duration: 46, ease: "linear", repeat: Infinity }}
>
{doppelt.map((stimme, i) => (
<article
key={`${stimme.name}-${i}`}
className="flex w-[85vw] max-w-[360px] shrink-0 snap-center flex-col p-9 md:w-[360px] md:snap-none"
style={{ borderRadius: BILD, background: SURFACE_2 }}
>
<Sterne groesse={15} />
<p
className="mt-6 text-base leading-relaxed"
style={{ fontFamily: SANS, color: INK_SOFT }}
>
{stimme.text}
</p>
<div className="mt-auto flex items-center gap-4 pt-10">
<span
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-full"
style={{ background: "#ffffff" }}
>
{stimme.quelle === "Google" ? (
<GoogleMarke className="h-6 w-6" />
) : (
<BadgeCheck
className="h-6 w-6"
style={{ color: INK }}
strokeWidth={1.5}
/>
)}
</span>
<div className="min-w-0">
<div
className="text-sm"
style={{ fontFamily: SANS, color: INK, fontWeight: 500 }}
>
{stimme.name}
</div>
<div
className="mt-1 text-[11px] uppercase"
style={{
fontFamily: SANS,
letterSpacing: "0.18em",
color: "rgba(59,62,68,0.4)",
}}
>
{stimme.behandlung}
</div>
</div>
</div>
</article>
))}
</motion.div>
</div>
</div>
</div>
</div>
</section>
);
}
/* --------------------------------------------------------------- zielgruppen */
function Zielgruppen() {
return (
<section id="audiences" className="bg-white">
<div className="mx-auto max-w-7xl px-5 pb-16 sm:px-6 sm:pb-24 lg:pb-32">
<div className="grid grid-cols-1 gap-3 sm:gap-4 md:grid-cols-2">
{ZIELGRUPPEN.map((box, i) => (
<Reveal key={box.kicker} delay={i * 0.1} className="h-full">
<article
className="flex h-full flex-col justify-between p-7 sm:p-10 lg:p-14"
style={{ borderRadius: BILD, background: SURFACE_2 }}
>
<div>
<Kicker>{box.kicker}</Kicker>
<h3
className="mt-5 text-2xl sm:text-3xl lg:text-4xl"
style={{ ...kopf, color: INK }}
>
{box.titel}
</h3>
<p
className="mt-5 max-w-lg text-base leading-relaxed"
style={{ fontFamily: SANS, color: INK_SOFT }}
>
{box.text}
</p>
<ul className="mt-8 space-y-4">
{box.punkte.map((punkt) => (
<li
key={punkt}
className="flex items-center gap-3 text-sm"
style={{ fontFamily: SANS, color: INK_SOFT }}
>
<span
className="flex h-6 w-6 shrink-0 items-center justify-center"
style={{ borderRadius: KNOPF, background: "#ffffff", color: INK }}
>
<Check className="h-3.5 w-3.5" strokeWidth={2} />
</span>
{punkt}
</li>
))}
</ul>
</div>
<a
href="#contact"
className="group/link mt-10 inline-flex w-fit items-center gap-2 px-6 py-3 text-sm text-white transition-colors"
style={{ borderRadius: KNOPF, background: INK, fontFamily: SANS }}
>
{box.cta}
<ArrowUpRight className="h-4 w-4 transition-transform group-hover/link:-translate-y-0.5 group-hover/link:translate-x-0.5" />
</a>
</article>
</Reveal>
))}
</div>
</div>
</section>
);
}
/* ------------------------------------------------------------------- fusszeile */
function Fusszeile() {
return (
<footer
id="contact"
className="relative overflow-hidden"
style={{ background: INK, color: "#ffffff" }}
>
<div className="mx-auto max-w-7xl px-5 pt-20 sm:px-6 lg:pt-28">
<Reveal>
<div className="grid grid-cols-1 gap-14 pb-16 lg:grid-cols-12 lg:gap-10">
<div className="lg:col-span-4">
<div className="flex items-center gap-3">
<Wortmarke className="h-6 w-auto" />
<span className="h-6 w-px" style={{ background: "rgba(255,255,255,0.2)" }} />
<span className="text-sm" style={{ ...kopf, color: "rgba(255,255,255,0.7)" }}>
{KLINIK.zusatz}
</span>
</div>
<p
className="mt-6 max-w-xs text-sm leading-relaxed"
style={{ fontFamily: SANS, color: "rgba(255,255,255,0.6)" }}
>
Personal. Precise. Discreet. Your clinic for implantology, oral surgery and
aesthetic treatments.
</p>
<div
className="mt-5 text-xs"
style={{ fontFamily: SANS, color: "rgba(255,255,255,0.42)" }}
>
Mon to Fri 08:00 to 18:00 · Sat by appointment
</div>
</div>
<div className="grid grid-cols-1 gap-10 sm:grid-cols-3 lg:col-span-5">
{FUSS_GRUPPEN.map((gruppe) => (
<div key={gruppe.titel}>
<Kicker hell>{gruppe.titel}</Kicker>
<ul className="mt-6 space-y-3">
{gruppe.links.map((link) => (
<li key={link}>
<a
href="#top"
className="group inline-flex items-center text-sm transition-colors"
style={{ fontFamily: SANS, color: "rgba(255,255,255,0.8)" }}
>
<span className="relative">
{link}
<span className="absolute -bottom-0.5 left-0 h-px w-0 bg-white transition-all duration-300 group-hover:w-full" />
</span>
</a>
</li>
))}
</ul>
</div>
))}
</div>
<div className="lg:col-span-3">
<Kicker hell>Contact</Kicker>
<address className="mt-6 not-italic">
<ul
className="space-y-5 text-sm leading-relaxed"
style={{ fontFamily: SANS, color: "rgba(255,255,255,0.7)" }}
>
<li className="flex items-center gap-4">
<span
className="flex h-9 w-9 shrink-0 items-center justify-center"
style={{ borderRadius: KNOPF, border: "1px solid rgba(255,255,255,0.16)" }}
>
<MapPin className="h-4 w-4" strokeWidth={1.5} />
</span>
<span>
<span className="block text-white">{KLINIK.strasse}</span>
<span className="block" style={{ color: "rgba(255,255,255,0.6)" }}>
{KLINIK.ort}
</span>
</span>
</li>
<li className="flex items-center gap-4">
<span
className="flex h-9 w-9 shrink-0 items-center justify-center"
style={{ borderRadius: KNOPF, border: "1px solid rgba(255,255,255,0.16)" }}
>
<Phone className="h-4 w-4" strokeWidth={1.5} />
</span>
<a
href={KLINIK.telefonHref}
className="text-white transition-colors hover:opacity-75"
>
{KLINIK.telefon}
</a>
</li>
<li className="flex items-center gap-4">
<span
className="flex h-9 w-9 shrink-0 items-center justify-center"
style={{ borderRadius: KNOPF, border: "1px solid rgba(255,255,255,0.16)" }}
>
<Mail className="h-4 w-4" strokeWidth={1.5} />
</span>
<a
href={KLINIK.mailHref}
className="text-white transition-colors hover:opacity-75"
>
{KLINIK.mail}
</a>
</li>
</ul>
</address>
</div>
</div>
</Reveal>
<div
className="flex flex-col items-start justify-between gap-6 py-8 sm:flex-row sm:items-center"
style={{ borderTop: "1px solid rgba(255,255,255,0.1)" }}
>
<div
className="flex flex-wrap items-center gap-x-6 gap-y-2 text-xs"
style={{ fontFamily: SANS, color: "rgba(255,255,255,0.5)" }}
>
<span>© 2026 {KLINIK.name}</span>
<a href="#top" className="transition-colors hover:text-white">
Legal notice
</a>
<a href="#top" className="transition-colors hover:text-white">
Privacy
</a>
<a href="#top" className="transition-colors hover:text-white">
Careers
</a>
</div>
<div className="flex items-center gap-2">
{[
{ label: "Instagram", Icon: Instagram },
{ label: "LinkedIn", Icon: Linkedin },
{ label: "Facebook", Icon: Facebook },
].map(({ label, Icon }) => (
<a
key={label}
href="#top"
aria-label={label}
className="flex h-9 w-9 items-center justify-center transition-colors hover:text-white"
style={{
borderRadius: KNOPF,
border: "1px solid rgba(255,255,255,0.12)",
color: "rgba(255,255,255,0.7)",
}}
>
<Icon className="h-4 w-4" strokeWidth={1.5} />
</a>
))}
</div>
</div>
</div>
</footer>
);
}
/* --------------------------------------------------------------------- page */
export function LumaClinicPage() {
return (
<div style={{ background: "#ffffff", fontFamily: SANS, color: INK }}>
<Kopfzeile />
<main>
<Held />
<Arzt />
<Behandlungen />
<Galerie />
<Gruende />
<Stimmen />
<Zielgruppen />
</main>
<Fusszeile />
</div>
);
}
export default LumaClinicPage;
Frequently asked questions
A curated landing page made of matching library sections in a sensible order from hero to footer. You copy the sections one by one and assemble them with the composition snippet.
Didn't find your question? We're happy to help.
Get in touch