Templates/Klinik für Kieferchirurgie
Eine ruhige, fast einfarbige Seite für eine Privatklinik. Anthrazit, Weiß und ein sehr helles Grau tragen alles; die einzige Farbe ist das Gold der Bewertungssterne. Auf dem Heldenbild liegen drei Schilder, die dem Zeiger folgen, die Behandlungs-Tafeln wachsen beim Anfahren und klappen ihren Text auf, und die Bewertungen wandern von allein vorbei. Großzügig gerundete Bildrahmen, keine Schatten, keine bunten Überlagerungen.
Die fertige Seite zum Durchscrollen, umschaltbar zwischen Desktop, Tablet und Handy.
Quellcode
Die Seite ist eine Datei, lädt aber 16 Bilder (584 KB) aus public/. Kopierst du nur den Code, fehlen sie. Das Paket enthält beides an den richtigen Pfaden.
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 = "#717479";
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 head: CSSProperties = { fontFamily: HEAD, fontWeight: 500, letterSpacing: "-0.02em" };
// A small radius instead of round capsules, the same as in the rest of the design system.
const BUTTON = 6;
const IMAGE = 22;
const SMOOTH = [0.22, 1, 0.36, 1] as const;
/* ------------------------------------------------------------------ content */
const CLINIC = {
name: "Luma Clinic",
zusatz: "Oral & Facial Clinic",
strasse: "123 Example Street",
place: "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 = [
{
title: "Implantology",
image: "/templates/luma/menu-implantology.webp",
anriss: "Fixed restorations with a natural appearance, for quality of life that lasts.",
points: ["Single implants", "All-on-4", "Bone grafting", "Immediate implants"],
},
{
title: "Oral surgery",
image: "/templates/luma/menu-surgery.webp",
anriss: "Precise surgery of the mouth, jaw and face, performed by experienced specialists.",
points: ["Wisdom teeth", "Jaw surgery", "Cyst removal", "Facial trauma"],
},
{
title: "Aesthetics",
image: "/templates/luma/menu-aesthetics.webp",
anriss: "Gentle procedures for a fresh, natural result and a rested appearance.",
points: ["Botulinum toxin", "Dermal fillers", "Eyelid lift", "PRP therapy"],
},
];
/* The dark strip on the right of the menu: a way through for everyone who does not know
which of the three columns their question falls under. */
const MEGA_ADVICE = {
kicker: "Personal consultation",
title: "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 FIGURES = [
{ value: 15, suffix: "+", label: "Years of experience" },
{ value: 8000, suffix: "+", label: "Procedures" },
{ value: 100, suffix: "%", label: "Private practice" },
];
const FIELDS = [
{
kicker: "Implantology",
title: "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.",
image: "/templates/luma/bento-implantology.webp",
alt: "Close-up of a patient smiling after implant treatment",
},
{
kicker: "Oral surgery",
title: "Surgery with a steady hand",
text: "Wisdom teeth, jaw corrections, cysts and trauma. Routine, gentle and supported by a modern operating suite.",
image: "/templates/luma/bento-surgery.webp",
alt: "Surgeon at work in the operating suite",
},
{
kicker: "Aesthetics",
title: "Your natural expression",
text: "Botulinum toxin, dermal fillers and aesthetic procedures, used with restraint. So you look like yourself, only more rested.",
image: "/templates/luma/bento-aesthetics.webp",
alt: "Aesthetic consultation in a bright treatment room",
},
];
const GALLERY = [
{
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 REASONS = [
{
Icon: Award,
kicker: "Experience",
title: "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",
title: "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",
title: "Recognised experts in implantology",
text: "Specialised in complex cases, immediate implants and aesthetically perfect restorations, always with a natural result.",
},
];
const VOICES = [
{
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 AUDIENCES = [
{
kicker: "For patients",
title: "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.",
points: ["Process and booking", "Frequently asked questions", "Getting here and parking"],
cta: "View patient information",
},
{
kicker: "For referrers",
title: "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.",
points: ["Online referral form", "Digital reporting", "Direct line to the clinical lead"],
cta: "Start a referral",
},
];
const FOOT_GROUPS = [
{ title: "Clinic", links: ["About the clinic", "Team", "Careers"] },
{ title: "Treatments", links: ["Implantology", "Oral surgery", "Aesthetics"] },
{ title: "Service", links: ["For patients", "For referrers", "Book an appointment"] },
];
/* --------------------------------------------------------------- primitives */
// Everything arrives the same way: a short lift, a long settle, once.
function Reveal({
children,
delay = 0,
className = "",
style,
}: {
children: ReactNode;
delay?: number;
className?: string;
style?: CSSProperties;
}) {
const reduce = useReducedMotion();
const ref = useRef<HTMLDivElement>(null);
const visible = useInView(ref, { once: true, margin: "-80px" });
return (
<motion.div
ref={ref}
className={className}
style={style}
initial={reduce ? false : { opacity: 0, y: 28 }}
animate={visible || reduce ? { opacity: 1, y: 0 } : { opacity: 0, y: 28 }}
transition={{ duration: reduce ? 0 : 0.9, ease: SMOOTH, delay: reduce ? 0 : delay }}
>
{children}
</motion.div>
);
}
// The wordmark: four letters, one path each, colour from the parent.
function Wordmark({ 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>
);
}
// The four Google colours, as they stand in the brand.
function GoogleMark({ 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 Stars({ groesse = 16, color = GOLD }: { groesse?: number; color?: string }) {
return (
<span className="inline-flex items-center gap-1" style={{ color: color }} aria-hidden>
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
style={{ width: groesse, height: groesse }}
fill="currentColor"
strokeWidth={0}
/>
))}
</span>
);
}
// The number counts up once when it comes into view.
function Counter({ target, suffix = "" }: { target: number; suffix?: string }) {
const reduce = useReducedMotion();
const ref = useRef<HTMLSpanElement>(null);
const visible = useInView(ref, { once: true, margin: "-60px" });
const [value, setValue] = useState(0);
useEffect(() => {
if (!visible) return;
if (reduce) {
setValue(target);
return;
}
const duration = 1400;
const start = performance.now();
let id = 0;
const step = (now: number) => {
const t = Math.min(1, (now - start) / duration);
/* Rolling out at the end: a number that stops abruptly reads as a jump, not as a
counter. */
setValue(Math.round(target * (1 - Math.pow(1 - t, 3))));
if (t < 1) id = requestAnimationFrame(step);
};
id = requestAnimationFrame(step);
return () => cancelAnimationFrame(id);
}, [visible, reduce, target]);
return (
<span ref={ref} className="tabular-nums">
{value.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.75)",
}}
>
{children}
</span>
);
}
function Button({
children,
href = "#appointment",
variant = "solid",
Icon,
}: {
children: ReactNode;
href?: string;
variant?: "solid" | "line";
Icon?: typeof ArrowRight;
}) {
const full = variant === "solid";
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: BUTTON,
fontFamily: SANS,
background: full ? INK : "#ffffff",
color: full ? "#ffffff" : INK,
border: full ? "1px solid transparent" : `1px solid ${LINE}`,
}}
>
{children}
{Icon ? (
/* The symbol only appears on hover and does not push the text around while doing
so: it grows out of zero width. */
<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 Header() {
const [open, setOpen] = useState(false);
const [megaOpen, setMegaOpen] = useState(false);
const [behandlungen, setTreatments] = useState(false);
useEffect(() => {
if (!open) return;
const before = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = before;
};
}, [open]);
return (
<>
{/* The scrim lies outside the header. The header has a backdrop blur, and an
element with backdrop-filter is the containing block for position:fixed in its
descendants: inside, the scrim would only be as tall as the header itself. */}
<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={CLINIC.name}>
<Wordmark 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={{ ...head, color: INK_SOFT }}>
{CLINIC.zusatz}
</span>
</a>
<nav className="hidden items-center gap-10 lg:flex">
{NAV.map((entry) =>
entry.mega ? (
/* Opening and closing hang on state, not on :hover. That way the scrim can
lie outside the header, and the keyboard reaches the same panel through
focus. */
<div
key={entry.label}
onMouseEnter={() => setTreatments(true)}
onMouseLeave={() => setTreatments(false)}
onFocus={() => setTreatments(true)}
onBlur={(e) => {
if (!e.currentTarget.contains(e.relatedTarget as Node)) setTreatments(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 }}
>
{entry.label}
<ChevronDown
className={`h-4 w-4 transition-transform duration-300 ${
behandlungen ? "rotate-180" : ""
}`}
/>
</button>
<MegaMenu open={behandlungen} />
</div>
) : (
<a
key={entry.label}
href={entry.href}
className="text-sm transition-colors hover:opacity-70"
style={{ fontFamily: SANS, color: INK_SOFT }}
>
{entry.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: BUTTON, background: INK, fontFamily: SANS }}
>
Book appointment
</a>
<button
type="button"
aria-label={open ? "Close menu" : "Open menu"}
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
className="inline-flex h-10 w-10 items-center justify-center lg:hidden"
style={{ borderRadius: BUTTON, border: `1px solid ${LINE}`, color: INK_SOFT }}
>
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
</div>
{open ? (
<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((entry) =>
entry.mega ? (
<div key={entry.label} style={{ borderBottom: `1px solid ${LINE}` }}>
<button
type="button"
onClick={() => setMegaOpen((v) => !v)}
aria-expanded={megaOpen}
className="flex w-full items-center justify-between py-4 text-lg"
style={{ color: INK, fontFamily: SANS }}
>
{entry.label}
<ChevronDown
className={`h-5 w-5 transition-transform duration-300 ${
megaOpen ? "rotate-180" : ""
}`}
/>
</button>
{/* Opening and closing through the row height of the grid: that runs
smoothly without knowing a height. */}
<div
className={`grid transition-all duration-300 ease-out ${
megaOpen ? "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((group) => (
<div key={group.title} className="flex flex-col gap-2">
<Kicker>{group.title}</Kicker>
<div className="flex flex-col">
{group.points.map((dot) => (
<a
key={dot}
href="#treatments"
onClick={() => setOpen(false)}
className="py-2 text-base"
style={{ color: INK, fontFamily: SANS }}
>
{dot}
</a>
))}
</div>
</div>
))}
</div>
</div>
</div>
</div>
) : (
<a
key={entry.label}
href={entry.href}
onClick={() => setOpen(false)}
className="block py-4 text-lg"
style={{ borderBottom: `1px solid ${LINE}`, color: INK, fontFamily: SANS }}
>
{entry.label}
</a>
),
)}
<a
href="#appointment"
onClick={() => setOpen(false)}
className="mt-8 inline-flex items-center justify-center px-6 py-3.5 text-sm text-white"
style={{ borderRadius: BUTTON, background: INK, fontFamily: SANS }}
>
Book appointment
</a>
</nav>
</div>
) : null}
</header>
</>
);
}
// The large menu under "Treatments". Only from lg on, on hover.
function MegaMenu({ open }: { open: boolean }) {
return (
<div
className="absolute inset-x-0 top-full z-40 overflow-hidden transition-all duration-200"
style={{
visibility: open ? "visible" : "hidden",
opacity: open ? 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((group, i) => (
<div
key={group.title}
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 }}
>
{/* The picture only grows when the column is hovered, slowly, as
everywhere else on this page. */}
<img
src={group.image}
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={{ ...head, color: INK }}>
{group.title}
</h3>
<p
className="mt-2 text-sm leading-relaxed"
style={{ fontFamily: SANS, color: INK_SOFT }}
>
{group.anriss}
</p>
</div>
<nav className="flex flex-col gap-3">
{group.points.map((dot) => (
<a
key={dot}
href="#treatments"
className="group/zeile flex items-center justify-between text-sm transition-colors hover:opacity-70"
style={{ fontFamily: SANS, color: INK }}
>
{dot}
{/* The arrow drives in from the left instead of simply being there:
the line gets a direction. */}
<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_ADVICE.kicker}
</span>
<h4 className="mt-5 text-[22px] leading-tight" style={{ ...head, color: "#ffffff" }}>
{MEGA_ADVICE.title}
</h4>
<p
className="mt-4 text-sm leading-relaxed"
style={{ fontFamily: SANS, color: "rgba(255,255,255,0.7)" }}
>
{MEGA_ADVICE.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: BUTTON, background: "#ffffff", color: INK, fontFamily: SANS }}
>
{MEGA_ADVICE.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: BUTTON,
border: `1px solid ${LINE}`,
color: "rgba(59,62,68,0.75)",
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={{ ...head, 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">
<Button href="#treatments" Icon={ArrowRight}>
Explore treatments
</Button>
<Button href="#appointment" variant="line" Icon={CalendarDays}>
Book an appointment
</Button>
</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">
<Stars 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">
<HeldImage />
</Reveal>
</div>
</section>
);
}
/* The hero picture with three plates on it. They follow the mouse, each with its own factor
and its own direction: equal factors would look like one shifted layer, different ones
like three things in space. */
function HeldImage() {
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 move = 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 back = 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 motions = [
{ x: x1, y: y1 },
{ x: x2, y: y2 },
{ x: x3, y: y3 },
];
return (
<div
ref={ref}
onMouseMove={move}
onMouseLeave={back}
className="group relative overflow-hidden"
style={{ borderRadius: IMAGE, 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: motions[i].x,
y: motions[i].y,
borderRadius: BUTTON,
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>
);
}
/* ------------------------------------------------------------------ doctor */
function Doctor() {
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={{ ...head, 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">
<Button href="#impressions" variant="line">
More about the clinic
</Button>
</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}` }}
>
{FIGURES.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={{ ...head, color: INK }}>
<Counter target={z.value} suffix={z.suffix} />
</div>
<div
className="mt-1 text-[11px] leading-tight sm:text-sm"
style={{ fontFamily: SANS, color: "rgba(59,62,68,0.75)" }}
>
{z.label}
</div>
</div>
))}
</div>
<div className="mt-10 lg:hidden">
<Button href="#impressions" variant="line">
More about the clinic
</Button>
</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: IMAGE, 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: BUTTON,
background: "rgba(255,255,255,0.82)",
color: INK,
fontFamily: SANS,
}}
>
Your surgeon
</span>
</div>
</Reveal>
</div>
</section>
);
}
/* --------------------------------------------------------------- treatments */
function Treatments() {
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={{ ...head, 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}>
{/* From md on, the three panels share the width and the hovered one grows to a
good double. Below that they stand one under the other, because an accordion
has nothing to grab on a phone. */}
<div className="mt-10 flex flex-col gap-3 sm:mt-14 md:h-[560px] md:flex-row lg:h-[620px]">
{FIELDS.map((field) => (
<article
key={field.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: IMAGE,
transitionTimingFunction: "cubic-bezier(0.22,1,0.36,1)",
}}
>
<img
src={field.image}
alt={field.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)" }}
/>
{/* Only as much gradient as the white type needs, and in the colour of
the text rather than a coloured one. */}
<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: BUTTON, 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)",
}}
>
{field.kicker}
</div>
<h3
className="mt-4 max-w-md text-2xl leading-tight lg:text-[2rem]"
style={{ ...head, color: "#ffffff" }}
>
{field.title}
</h3>
{/* On the phone the sentence is always there, on the desktop it opens
on hover: 0fr to 1fr, without a fixed height. */}
<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)",
}}
>
{field.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: IMAGE, 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={{ ...head, 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: BUTTON, background: INK, fontFamily: SANS }}
>
Book an appointment
<ArrowUpRight className="h-4 w-4" />
</a>
</div>
</Reveal>
</div>
</section>
);
}
/* ------------------------------------------------------------------ galerie */
function Gallery() {
const [active, setActive] = 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={{ ...head, 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>
{/* Phone: one picture per view, pushed on with the finger. */}
<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 end = e.changedTouches[0]?.clientX;
if (end === undefined) return;
const weg = end - start.current;
if (Math.abs(weg) >= 40) {
setActive((i) =>
weg < 0 ? Math.min(i + 1, GALLERY.length - 1) : Math.max(i - 1, 0),
);
}
start.current = null;
}}
>
<div
className="flex will-change-transform"
style={{
transform: `translate3d(-${active * 100}%, 0, 0)`,
transition: "transform 420ms cubic-bezier(0.22, 1, 0.36, 1)",
}}
>
{GALLERY.map((image) => (
<div key={image.src} className="w-full shrink-0 px-1">
<div
className="overflow-hidden"
style={{ borderRadius: IMAGE, background: "#eef0f2" }}
>
<img
src={image.src}
alt={image.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">
{GALLERY.map((image, i) => (
<button
key={image.src}
type="button"
onClick={() => setActive(i)}
aria-label={`Image ${i + 1}`}
className="h-2 rounded-full transition-all duration-300"
style={{
width: i === active ? 24 : 8,
background: i === active ? INK : "rgba(59,62,68,0.2)",
}}
/>
))}
</div>
{/* Desktop: a grid of six pictures in three rows. */}
<div className="mt-10 hidden grid-cols-12 gap-3 sm:mt-16 sm:gap-4 md:grid lg:gap-6">
{GALLERY.map((image, i) => (
<Reveal
key={image.src}
delay={(i % 2) * 0.08}
className={`col-span-12 ${image.spalten}`}
>
<div
className="group h-full overflow-hidden"
style={{ borderRadius: IMAGE, background: "#eef0f2" }}
>
<img
src={image.src}
alt={image.alt}
loading="lazy"
style={image.fokus ? { objectPosition: image.fokus } : undefined}
className={`${image.format || "h-full"} w-full object-cover transition-transform duration-[1200ms] ease-out group-hover:scale-105`}
/>
</div>
</Reveal>
))}
</div>
</div>
</section>
);
}
/* ------------------------------------------------------------------ reasons */
function Reasons() {
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={{ ...head, 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">
{REASONS.map((ground, i) => (
<Reveal key={ground.kicker} delay={i * 0.08}>
{/* Two layers on top of each other: the light one goes up and away, the
dark one comes up and stays. On the phone both stand below each other
without a change. */}
<div
className="group relative h-auto overflow-hidden md:h-[340px]"
style={{ borderRadius: IMAGE, 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 }}
>
<ground.Icon className="h-5 w-5" strokeWidth={1.5} />
</div>
<div>
<Kicker>{ground.kicker}</Kicker>
<h3
className="mt-4 text-xl sm:text-2xl lg:text-[1.625rem] lg:leading-snug"
style={{ ...head, color: INK }}
>
{ground.title}
</h3>
</div>
<p
className="text-sm leading-relaxed md:hidden"
style={{ fontFamily: SANS, color: INK_SOFT }}
>
{ground.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: IMAGE,
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)" }}
>
<ground.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)" }}
>
{ground.text}
</p>
<Kicker hell>{ground.kicker}</Kicker>
</div>
</div>
</Reveal>
))}
</div>
</div>
</section>
);
}
/* ------------------------------------------------------------------- voices */
function Voices() {
const reduce = useReducedMotion();
const double = [...VOICES, ...VOICES];
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={{ ...head, 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={{ ...head, 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>
<Stars 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))" }}
/>
{/* Phone: push. Desktop: a quiet drift that stops on hover. Under reduced
motion it stands still. */}
<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 }}
>
{double.map((voice, i) => (
<article
key={`${voice.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: IMAGE, background: SURFACE_2 }}
>
<Stars groesse={15} />
<p
className="mt-6 text-base leading-relaxed"
style={{ fontFamily: SANS, color: INK_SOFT }}
>
{voice.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" }}
>
{voice.quelle === "Google" ? (
<GoogleMark 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 }}
>
{voice.name}
</div>
<div
className="mt-1 text-[11px] uppercase"
style={{
fontFamily: SANS,
letterSpacing: "0.18em",
color: "rgba(59,62,68,0.75)",
}}
>
{voice.behandlung}
</div>
</div>
</div>
</article>
))}
</motion.div>
</div>
</div>
</div>
</div>
</section>
);
}
/* ----------------------------------------------------------------- audiences */
function Audiences() {
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">
{AUDIENCES.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: IMAGE, background: SURFACE_2 }}
>
<div>
<Kicker>{box.kicker}</Kicker>
<h3
className="mt-5 text-2xl sm:text-3xl lg:text-4xl"
style={{ ...head, color: INK }}
>
{box.title}
</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.points.map((dot) => (
<li
key={dot}
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: BUTTON, background: "#ffffff", color: INK }}
>
<Check className="h-3.5 w-3.5" strokeWidth={2} />
</span>
{dot}
</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: BUTTON, 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>
);
}
/* ---------------------------------------------------------------------- footer */
function Footer() {
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">
<Wordmark className="h-6 w-auto" />
<span className="h-6 w-px" style={{ background: "rgba(255,255,255,0.2)" }} />
<span className="text-sm" style={{ ...head, color: "rgba(255,255,255,0.7)" }}>
{CLINIC.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.6)" }}
>
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">
{FOOT_GROUPS.map((group) => (
<div key={group.title}>
<Kicker hell>{group.title}</Kicker>
<ul className="mt-6 space-y-3">
{group.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: BUTTON, 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">{CLINIC.strasse}</span>
<span className="block" style={{ color: "rgba(255,255,255,0.6)" }}>
{CLINIC.place}
</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: BUTTON, border: "1px solid rgba(255,255,255,0.16)" }}
>
<Phone className="h-4 w-4" strokeWidth={1.5} />
</span>
<a
href={CLINIC.telefonHref}
className="text-white transition-colors hover:opacity-75"
>
{CLINIC.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: BUTTON, border: "1px solid rgba(255,255,255,0.16)" }}
>
<Mail className="h-4 w-4" strokeWidth={1.5} />
</span>
<a
href={CLINIC.mailHref}
className="text-white transition-colors hover:opacity-75"
>
{CLINIC.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.6)" }}
>
<span>© 2026 {CLINIC.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: BUTTON,
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 }}>
<Header />
<main>
<Held />
<Doctor />
<Treatments />
<Gallery />
<Reasons />
<Voices />
<Audiences />
</main>
<Footer />
</div>
);
}
export default LumaClinicPage;
Häufige Fragen
Eine kuratierte Landingpage aus aufeinander abgestimmten Sections der Library, in sinnvoller Reihenfolge von Hero bis Footer. Du kopierst die Sections einzeln und setzt sie mit dem Kompositions-Snippet zusammen.
Deine Frage war nicht dabei? Wir helfen gern weiter.
Kontakt aufnehmen