Templates/Natural Health Practice
A quiet page for practices, therapists and everyone whose work needs time. Near-white and one deep sage olive, with a warm light grey for the calm surfaces. An italic serif runs against the sans in every heading, so a two word phrase can change voice mid sentence. Two photographs drift against the scroll, the headline and the quote arrive word by word, and a very fine grain lies over the dark image sections so the large areas do not read as flat colour.
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 { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
import {
AnimatePresence,
motion,
useInView,
useReducedMotion,
useScroll,
useTransform,
} from "framer-motion";
import {
ArrowRight,
Clock,
Compass,
HeartHandshake,
Leaf,
Mail,
MapPin,
Menu,
Moon,
Phone,
Sparkles,
Sun,
Waves,
X,
} from "lucide-react";
/*
* Meridian: a full page for a practice for Traditional Chinese Medicine.
*
* Near-white and one deep sage olive, with a warm light grey for the calm
* surfaces. An italic serif runs against the sans in every heading, so a two
* word phrase can change voice mid sentence. That contrast is the whole
* typographic idea; there is no third font and no second accent.
*
* Motion is slow on purpose. Two photographs drift against the scroll, the
* headline and the quote arrive word by word, and everything else fades once
* and then holds still. A very fine grain lies over the two dark image
* sections so the large areas do not read as flat colour.
*/
const GRUND = "#fdfcfb";
const TINTE = "#171c16";
const CREME = "#fbfaf7";
const SAND = "#f5f3ef";
const SALBEI = "#40533c";
const SALBEI_HELL = "#7c8d6d";
const LEISE = "#61655e";
const KANTE = "#e6e4de";
const SANS = '"Albert Sans", ui-sans-serif, system-ui, sans-serif';
const SERIF = '"Instrument Serif", ui-serif, Georgia, serif';
const serif: CSSProperties = { fontFamily: SERIF, fontWeight: 400, letterSpacing: "-0.01em" };
/** Sehr feines Korn ueber den dunklen Bildflaechen. */
const KORN =
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\")";
const PRAXIS = {
name: "Meridian",
ort: "Your City",
telefon: "+00 000 000 000",
mail: "hello@example.com",
};
const NAV = [
{ href: "#methods", label: "Methods" },
{ href: "#about", label: "About" },
{ href: "#contact", label: "Contact" },
];
const UEBERSCHRIFT: { wort: string; kursiv?: boolean }[] = [
{ wort: "A" },
{ wort: "holistic" },
{ wort: "view" },
{ wort: "of" },
{ wort: "body,", kursiv: true },
{ wort: "daily", kursiv: true },
{ wort: "life", kursiv: true },
{ wort: "and" },
{ wort: "inner" },
{ wort: "balance." },
];
const KONTAKT = [
{ Icon: Clock, label: "Appointments", wert: "By arrangement" },
{ Icon: Phone, label: "Phone", wert: PRAXIS.telefon },
{ Icon: Mail, label: "Email", wert: PRAXIS.mail },
{ Icon: MapPin, label: "Address", wert: PRAXIS.ort },
];
const SCHWERPUNKTE = [
{
Icon: Sparkles,
titel: "Acupuncture and TCM",
text: "For complaints that, from a TCM perspective, relate to the flow of energy, organ function and inner balance.",
},
{
Icon: Moon,
titel: "Stress, exhaustion and sleep",
text: "When the body no longer settles properly and tension, fatigue or sleeplessness persist.",
},
{
Icon: Waves,
titel: "Pain and tension",
text: "Support with recurring physical complaints, tension and restrictions in the musculoskeletal system.",
},
];
const METHODEN = [
{
bild: "/templates/meridian/method-acupuncture.webp",
alt: "Fine acupuncture needles laid out on a cloth",
titel: "Acupuncture",
text: "Fine needle stimulation, placed individually and matched to your symptoms. It can encourage the flow of energy and release inner tension.",
},
{
bild: "/templates/meridian/method-herbs.webp",
alt: "Dried herbs and roots in small bowls",
titel: "Chinese herbal medicine",
text: "Herbal formulas following the principles of TCM. Every blend is carefully composed around your constitution and how you are feeling right now.",
},
{
bild: "/templates/meridian/method-cupping.webp",
alt: "Glass cupping vessels on a wooden tray",
titel: "Cupping",
text: "A traditional method used in TCM for muscular tension, stagnation and energetic blockages.",
},
{
bild: "/templates/meridian/bg-moxa.webp",
alt: "Smoke rising from a moxa stick",
titel: "Moxibustion",
text: "Gentle warming with mugwort. It is applied deliberately to warm and strengthen the body and guide it back into balance.",
},
{
bild: "/templates/meridian/hero-bg.webp",
alt: "A large leaf in soft light",
titel: "TCM nutrition",
text: "Practical guidance on warmth, rhythm, digestibility and building strength. Nutrition is understood as a quiet, continuous companion to treatment.",
},
];
const ERWARTUNG = [
{
titel: "Time to talk",
text: "It starts with a thorough consultation. Complaints, daily life and medical history are looked at calmly and without rushing.",
},
{
titel: "An individual treatment plan",
text: "Methods are combined to suit the situation. No treatment by template, but tailored to what actually shows up.",
},
{
titel: "Support at your own pace",
text: "Treatment follows what the body needs right now. Appointments by arrangement only, in a calm atmosphere.",
},
];
const ABLAUF = [
{ n: "01", titel: "Enquiry", text: "You send me a message or ask for an appointment." },
{
n: "02",
titel: "First consultation",
text: "In the first appointment we talk through your complaints, daily life and personal history in detail.",
},
{ n: "03", titel: "Assessment", text: "I interpret those observations from a TCM perspective." },
{
n: "04",
titel: "Treatment",
text: "I put together the treatment that suits you individually.",
},
];
const PASSEND = [
{
Icon: HeartHandshake,
text: "If you would like a holistic perspective and prefer not to see complaints in isolation.",
},
{
Icon: Compass,
text: "If you want to make better sense of recurring complaints and understand how they connect.",
},
{
Icon: Sun,
text: "If you are looking for approaches that complement conventional medical care.",
},
];
const ZITAT = [
["When", "the", "water", "grows", "still,"],
["it", "reflects", "the", "sky."],
];
const ZITAT_KURSIV = new Set(["still,", "sky."]);
/* --------------------------------------------------------------- primitives */
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: 22 }}
animate={sichtbar || reduce ? { opacity: 1, y: 0 } : { opacity: 0, y: 22 }}
transition={{
duration: reduce ? 0 : 0.8,
ease: [0.22, 1, 0.36, 1],
delay: reduce ? 0 : delay,
}}
>
{children}
</motion.div>
);
}
/** Die kleine Zeile ueber einer Ueberschrift. */
function Marke({ children }: { children: ReactNode }) {
return (
<p
className="text-xs uppercase"
style={{ fontFamily: SANS, letterSpacing: "0.22em", color: LEISE }}
>
{children}
</p>
);
}
/**
* Ein Bild in einem gerundeten Rahmen. Der Schleier darauf ist kein Filter:
* er liegt nur in einer Ecke und sehr schwach, damit die Kante des Bildes
* nicht hart gegen die helle Flaeche stoesst.
*/
function Bildrahmen({ children, className = "" }: { children: ReactNode; className?: string }) {
return (
<div
className={`relative overflow-hidden rounded-3xl ${className}`}
style={{ background: SAND }}
>
{children}
<span
aria-hidden
className="pointer-events-none absolute inset-0"
style={{
background: `linear-gradient(160deg, rgba(64,83,60,0.1) 0%, rgba(251,250,247,0) 55%)`,
}}
/>
</div>
);
}
/** Eine Ueberschrift aus Serif, mit einem kursiven Wort darin. */
function Serifzeile({
vor,
kursiv,
nach,
className = "",
}: {
vor: string;
kursiv: string;
nach?: string;
className?: string;
}) {
return (
<h2
className={`text-3xl leading-tight sm:text-4xl md:text-5xl lg:text-[3.5rem] ${className}`}
style={{ ...serif, color: TINTE }}
>
{vor}
<em style={{ fontStyle: "italic" }}>{kursiv}</em>
{nach}
</h2>
);
}
/* ------------------------------------------------------------------- header */
function Kopfzeile() {
const [gescrollt, setGescrollt] = useState(false);
const [offen, setOffen] = useState(false);
useEffect(() => {
const beim = () => setGescrollt(window.scrollY > 12);
beim();
window.addEventListener("scroll", beim, { passive: true });
return () => window.removeEventListener("scroll", beim);
}, []);
useEffect(() => {
if (!offen) return;
const vorher = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = vorher;
};
}, [offen]);
// Ueber dem Hero steht die Leiste hell auf dem Bild, danach dunkel auf Weiss.
const aufBild = !gescrollt && !offen;
const farbe = aufBild ? "#fbfaf7" : TINTE;
return (
<header
className="fixed inset-x-0 top-0 z-40 transition-all duration-500"
style={{
background: offen || gescrollt ? "rgba(253,252,251,0.9)" : "transparent",
borderBottom: offen || gescrollt ? `1px solid ${KANTE}` : "1px solid transparent",
backdropFilter: gescrollt ? "blur(10px)" : undefined,
color: farbe,
}}
>
<div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-5 lg:px-10">
<a
href="#top"
aria-label={`${PRAXIS.name}, back to the top`}
className="inline-flex items-center"
>
<span className="text-2xl leading-none md:text-3xl" style={{ ...serif, color: farbe }}>
{PRAXIS.name}
</span>
</a>
<nav className="hidden items-center gap-8 md:flex">
{NAV.map((eintrag) => (
<a
key={eintrag.href}
href={eintrag.href}
className="text-sm tracking-wide transition-opacity hover:opacity-70"
style={{
fontFamily: SANS,
color: aufBild ? "rgba(251,250,247,0.86)" : "rgba(23,28,22,0.8)",
}}
>
{eintrag.label}
</a>
))}
<a
href="#contact"
className="px-5 py-2 text-sm transition-colors"
style={{
borderRadius: 6,
border: `1px solid ${aufBild ? "rgba(251,250,247,0.6)" : "rgba(64,83,60,0.4)"}`,
color: aufBild ? "#fbfaf7" : SALBEI,
fontFamily: SANS,
}}
>
Request an appointment
</a>
</nav>
<button
type="button"
onClick={() => setOffen((v) => !v)}
aria-label={offen ? "Close menu" : "Open menu"}
aria-expanded={offen}
className="relative z-50 inline-flex h-10 w-10 items-center justify-center md:hidden"
style={{ borderRadius: 6, color: farbe }}
>
{offen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
<AnimatePresence>
{offen ? (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
className="overflow-hidden md:hidden"
style={{ background: GRUND }}
>
<nav className="flex flex-col gap-1 px-6 pb-8 pt-2">
{NAV.map((eintrag) => (
<a
key={eintrag.href}
href={eintrag.href}
onClick={() => setOffen(false)}
className="py-3 text-xl"
style={{ ...serif, color: TINTE, borderBottom: `1px solid ${KANTE}` }}
>
{eintrag.label}
</a>
))}
<a
href="#contact"
onClick={() => setOffen(false)}
className="mt-6 inline-flex items-center justify-center px-6 py-3 text-sm"
style={{ borderRadius: 6, background: SALBEI, color: CREME, fontFamily: SANS }}
>
Request an appointment
</a>
</nav>
</motion.div>
) : null}
</AnimatePresence>
</header>
);
}
/* --------------------------------------------------------------------- hero */
function Held() {
const reduce = useReducedMotion();
const ref = useRef<HTMLElement>(null);
const { scrollYProgress } = useScroll({ target: ref, offset: ["start start", "end start"] });
const y = useTransform(scrollYProgress, [0, 1], ["0%", "18%"]);
return (
<section
id="top"
ref={ref}
className="relative isolate flex min-h-screen items-center justify-center overflow-hidden"
>
<motion.img
src="/templates/meridian/hero-bg.webp"
alt="A large leaf in soft light"
style={reduce ? undefined : { y }}
className="absolute inset-0 h-[118%] w-full object-cover"
loading="eager"
decoding="async"
/>
{/* Nur so viel Schleier, wie die weisse Schrift braucht. Keine Farbe
darin: das Bild soll gruen bleiben, nicht getoent werden. */}
<div className="absolute inset-0" style={{ background: "rgba(23,28,22,0.42)" }} />
<div
className="absolute inset-0"
style={{
background:
"linear-gradient(to bottom, rgba(23,28,22,0.22) 0%, rgba(23,28,22,0.08) 45%, rgba(23,28,22,0.44) 100%)",
}}
/>
<span
aria-hidden
className="pointer-events-none absolute inset-0"
style={{ backgroundImage: KORN, opacity: 0.16, mixBlendMode: "overlay" }}
/>
<div className="relative mx-auto w-full max-w-5xl px-6 pb-20 pt-28 text-center sm:pb-32 lg:px-10 lg:pb-48 lg:pt-48">
<div className="mx-auto flex flex-col items-center" style={{ color: CREME }}>
<motion.div
initial={reduce ? false : { opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7 }}
className="inline-flex items-center gap-2 px-4 py-1.5 text-[11px] uppercase backdrop-blur"
style={{
borderRadius: 6,
background: "rgba(251,250,247,0.12)",
letterSpacing: "0.28em",
fontFamily: SANS,
}}
>
<Leaf className="h-3.5 w-3.5" strokeWidth={1.5} />
<span>TCM practitioner</span>
</motion.div>
{/* Die Ueberschrift kommt Wort fuer Wort. Das ist der Ton der Seite:
nichts kommt auf einmal. */}
<motion.h1
initial="ruhe"
animate="da"
variants={{
ruhe: {},
da: { transition: { staggerChildren: 0.04, delayChildren: 0.1 } },
}}
className="mt-7 text-[clamp(2.5rem,7vw,4.75rem)] leading-[1.06]"
style={{ ...serif, color: CREME }}
>
{UEBERSCHRIFT.map((teil, i) => (
<motion.span
key={i}
variants={{
ruhe: reduce ? {} : { opacity: 0, y: 16 },
da: { opacity: 1, y: 0, transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] } },
}}
className="inline-block"
style={teil.kursiv ? { fontStyle: "italic" } : undefined}
>
{teil.wort}
{i < UEBERSCHRIFT.length - 1 ? " " : ""}
</motion.span>
))}
</motion.h1>
<motion.p
initial={reduce ? false : { opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.75 }}
className="mx-auto mt-7 max-w-2xl text-base leading-relaxed md:mt-10 md:text-lg"
style={{ fontFamily: SANS, color: "rgba(251,250,247,0.86)" }}
>
Traditional Chinese Medicine, acupuncture and naturopathic support in a calm practice
setting in {PRAXIS.ort}.
</motion.p>
<motion.div
initial={reduce ? false : { opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, delay: 0.95 }}
className="mt-9 flex flex-col items-center justify-center gap-5 sm:flex-row sm:flex-wrap sm:gap-7 md:mt-12"
>
<a
href="#contact"
className="px-7 py-3 text-sm transition-opacity hover:opacity-90"
style={{ borderRadius: 6, background: CREME, color: TINTE, fontFamily: SANS }}
>
Request an appointment
</a>
<a
href="#methods"
className="group inline-flex items-center gap-2 text-sm tracking-wide"
style={{ fontFamily: SANS, color: "rgba(251,250,247,0.86)" }}
>
View methods
<ArrowRight className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
</motion.div>
</div>
</div>
</section>
);
}
/* ------------------------------------------------------------------- praxis */
function Praxis() {
return (
<section className="relative py-14 md:py-28">
<div className="mx-auto grid max-w-7xl gap-14 px-6 lg:grid-cols-12 lg:gap-20 lg:px-10">
<Reveal className="lg:col-span-7">
<Marke>Practice</Marke>
<div className="mt-8">
<Serifzeile vor="Natural health practice in " kursiv={PRAXIS.ort} nach="." />
</div>
<p
className="mt-8 max-w-xl text-base leading-relaxed md:text-lg"
style={{ fontFamily: SANS, color: LEISE }}
>
Support for physical complaints, stress, exhaustion, pain and inner imbalance. I work
with Traditional Chinese Medicine, acupuncture and naturopathic methods, always tailored
to the individual person.
</p>
</Reveal>
<Reveal delay={0.1} className="lg:col-span-5">
<div
className="rounded-3xl p-7 sm:p-9 md:p-10"
style={{ background: CREME, border: `1px solid ${KANTE}` }}
>
<Marke>Contact</Marke>
<ul className="mt-6 space-y-5">
{KONTAKT.map(({ Icon, label, wert }) => (
<li key={label} className="flex items-start gap-4">
<span
className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-full"
style={{ background: GRUND, color: SALBEI }}
>
<Icon className="h-4 w-4" strokeWidth={1.5} />
</span>
<div>
<p
className="text-[11px] uppercase"
style={{ fontFamily: SANS, letterSpacing: "0.2em", color: LEISE }}
>
{label}
</p>
<p className="mt-1 text-sm" style={{ fontFamily: SANS, color: TINTE }}>
{wert}
</p>
</div>
</li>
))}
</ul>
<a
href="#contact"
className="group mt-8 inline-flex items-center gap-2 px-6 py-3 text-sm transition-opacity hover:opacity-90"
style={{ borderRadius: 6, background: SALBEI, color: CREME, fontFamily: SANS }}
>
Request an appointment
<ArrowRight className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1" />
</a>
</div>
</Reveal>
</div>
</section>
);
}
/* ------------------------------------------------------------- ungleichgewicht */
function Ungleichgewicht() {
return (
<section className="relative py-14 md:py-28">
<div className="mx-auto max-w-3xl px-6 text-center lg:px-10">
<Reveal>
<h2
className="text-3xl leading-tight sm:text-4xl md:text-5xl lg:text-[3.25rem]"
style={{ ...serif, color: TINTE }}
>
When the body falls out of <em style={{ fontStyle: "italic" }}>balance</em>.
</h2>
</Reveal>
<Reveal delay={0.15}>
<p
className="mx-auto mt-8 max-w-xl text-base leading-relaxed md:text-lg"
style={{ fontFamily: SANS, color: LEISE }}
>
Sleep, digestion, cycle, energy, tension or inner restlessness can all be signals. In
TCM I look not at a single symptom, but at patterns in the context of your everyday
life.
</p>
</Reveal>
</div>
</section>
);
}
/* ------------------------------------------------------------- schwerpunkte */
function Schwerpunkte() {
return (
<section className="relative py-14 md:py-28">
<div className="mx-auto max-w-7xl px-6 lg:px-10">
<Reveal className="max-w-2xl">
<Marke>Focus areas</Marke>
<div className="mt-8">
<Serifzeile vor="The practice’s " kursiv="focus areas" nach="." />
</div>
<p
className="mt-6 max-w-xl text-base leading-relaxed md:text-lg"
style={{ fontFamily: SANS, color: LEISE }}
>
Three areas in which I support people most often.
</p>
</Reveal>
<div className="mt-14 grid auto-rows-fr gap-5 md:mt-16 md:grid-cols-3 md:gap-6">
{SCHWERPUNKTE.map((feld, i) => (
<Reveal key={feld.titel} delay={i * 0.1} className="h-full">
<motion.div
whileHover={{ y: -4 }}
transition={{ type: "spring", stiffness: 280, damping: 24 }}
className="flex h-full flex-col rounded-3xl p-7 sm:p-9 md:p-12"
style={{ background: CREME }}
>
<feld.Icon
className="h-7 w-7 md:h-8 md:w-8"
style={{ color: SALBEI }}
strokeWidth={1.5}
/>
<h3
className="mt-8 text-2xl md:mt-10 md:text-3xl"
style={{ ...serif, color: TINTE }}
>
{feld.titel}
</h3>
<p
className="mt-3 text-base leading-relaxed md:mt-4"
style={{ fontFamily: SANS, color: LEISE }}
>
{feld.text}
</p>
</motion.div>
</Reveal>
))}
</div>
</div>
</section>
);
}
/* -------------------------------------------------------------------- zitat */
function Zitat() {
const reduce = useReducedMotion();
const ref = useRef<HTMLElement>(null);
const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "end start"] });
const y = useTransform(scrollYProgress, [0, 1], ["-12%", "12%"]);
return (
<section
ref={ref}
className="relative isolate flex min-h-[70vh] items-center justify-center overflow-hidden md:min-h-[85vh]"
>
<motion.img
src="/templates/meridian/quote-water.webp"
alt=""
aria-hidden
loading="lazy"
style={reduce ? undefined : { y }}
className="absolute inset-0 h-[124%] w-full object-cover"
/>
<div
className="absolute inset-0"
style={{
background:
"linear-gradient(to bottom, rgba(23,28,22,0.4) 0%, rgba(23,28,22,0.24) 50%, rgba(23,28,22,0.5) 100%)",
}}
/>
<span
aria-hidden
className="pointer-events-none absolute inset-0"
style={{ backgroundImage: KORN, opacity: 0.14, mixBlendMode: "overlay" }}
/>
<div
className="relative mx-auto max-w-5xl px-6 py-24 text-center md:py-32 lg:px-10"
style={{ color: CREME }}
>
<motion.span
aria-hidden
initial={reduce ? false : { opacity: 0, y: 16 }}
whileInView={{ opacity: 0.4, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 1.2, ease: [0.22, 1, 0.36, 1] }}
className="mb-6 block text-[5rem] leading-none md:text-[7rem]"
style={{ ...serif, color: "rgba(251,250,247,0.45)" }}
>
“
</motion.span>
<motion.blockquote
initial="ruhe"
whileInView="da"
viewport={{ once: true, amount: 0.2 }}
variants={{ ruhe: {}, da: { transition: { staggerChildren: 0.06, delayChildren: 0.1 } } }}
className="text-[2.25rem] leading-[1.2] sm:text-5xl md:text-6xl lg:text-7xl"
style={{ ...serif, color: CREME, textWrap: "balance" }}
>
{ZITAT.map((zeile, z) => (
<span key={z} className="block">
{zeile.map((wort, i) => (
<motion.span
key={`${z}-${i}`}
variants={{
ruhe: reduce ? {} : { opacity: 0, y: 14 },
da: {
opacity: 1,
y: 0,
transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] },
},
}}
className="inline-block"
style={ZITAT_KURSIV.has(wort) ? { fontStyle: "italic" } : undefined}
>
{wort}
{i < zeile.length - 1 ? " " : ""}
</motion.span>
))}
</span>
))}
</motion.blockquote>
<motion.p
initial={reduce ? false : { opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.6, delay: 0.4, ease: [0.22, 1, 0.36, 1] }}
className="mx-auto mt-10 max-w-xl text-base leading-relaxed md:text-lg"
style={{ fontFamily: SANS, color: "rgba(251,250,247,0.86)" }}
>
Treatment is not about getting rid of individual symptoms. It is about recognising again
where the body has fallen out of rhythm.
</motion.p>
</div>
</section>
);
}
/* ----------------------------------------------------------------- methoden */
function Methoden() {
return (
<section id="methods" className="relative scroll-mt-24 py-14 md:py-28">
<div className="mx-auto max-w-7xl px-6 lg:px-10">
<Reveal className="max-w-2xl">
<Marke>Methods</Marke>
<div className="mt-8">
<Serifzeile vor="Treatment " kursiv="methods" nach="." />
</div>
<p
className="mt-6 max-w-xl text-base leading-relaxed md:text-lg"
style={{ fontFamily: SANS, color: LEISE }}
>
A selection of the methods used in the practice, applied on their own or in combination
depending on the findings.
</p>
</Reveal>
<div className="mt-14 space-y-14 md:mt-20 md:space-y-28">
{METHODEN.map((methode, i) => {
const gedreht = i % 2 === 1;
return (
<Reveal key={methode.titel}>
<div
className="grid items-center gap-8 pt-10 md:grid-cols-12 md:gap-14 md:pt-16"
style={{ borderTop: `1px solid ${KANTE}` }}
>
<div className={`md:col-span-5 ${gedreht ? "md:order-2" : ""}`}>
<Bildrahmen className="group aspect-[4/5] w-full md:aspect-[3/4]">
<img
src={methode.bild}
alt={methode.alt}
loading="lazy"
decoding="async"
className="h-full w-full object-cover transition-transform duration-[1200ms] group-hover:scale-[1.04]"
style={{ transitionTimingFunction: "cubic-bezier(0.22,1,0.36,1)" }}
/>
</Bildrahmen>
</div>
<div className={`md:col-span-7 ${gedreht ? "md:order-1" : ""}`}>
<p
className="text-xs uppercase"
style={{ fontFamily: SANS, letterSpacing: "0.22em", color: LEISE }}
>
{String(i + 1).padStart(2, "0")}
</p>
<h3
className="mt-4 text-3xl leading-tight md:text-4xl lg:text-[2.75rem]"
style={{ ...serif, color: TINTE }}
>
{methode.titel}
</h3>
<p
className="mt-6 max-w-xl text-base leading-relaxed md:text-lg"
style={{ fontFamily: SANS, color: LEISE }}
>
{methode.text}
</p>
</div>
</div>
</Reveal>
);
})}
</div>
</div>
</section>
);
}
/* -------------------------------------------------------------------- ueber */
function Ueber() {
const reduce = useReducedMotion();
const ref = useRef<HTMLElement>(null);
const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "end start"] });
const y = useTransform(scrollYProgress, [0, 1], ["-6%", "6%"]);
return (
<section
id="about"
ref={ref}
className="relative scroll-mt-24 py-16 md:py-32"
style={{ background: CREME }}
>
<div className="mx-auto grid max-w-7xl gap-16 px-6 lg:grid-cols-12 lg:gap-24 lg:px-10">
<div className="lg:col-span-5">
<div className="lg:sticky lg:top-28">
<Reveal>
<Bildrahmen className="aspect-[4/5] w-full max-w-md md:aspect-[3/4]">
<motion.img
src="/templates/meridian/bg-meditation.webp"
alt="A lavender field in soft light"
loading="lazy"
style={reduce ? undefined : { y }}
className="h-[112%] w-full object-cover"
/>
</Bildrahmen>
</Reveal>
</div>
</div>
<div className="lg:col-span-7">
<Reveal>
<Marke>About</Marke>
<div className="mt-8">
<Serifzeile vor="A practitioner who " kursiv="listens" nach="." />
</div>
</Reveal>
<div className="mt-10 space-y-10 md:mt-12 md:space-y-16">
<Reveal delay={0.05}>
<p
className="max-w-xl text-base leading-relaxed md:text-lg"
style={{ fontFamily: SANS, color: LEISE }}
>
I am a practitioner focused on Traditional Chinese Medicine. In my work I take the
time to look closely. At the centre is the person, with their history, their
everyday life and how they are feeling right now.
</p>
</Reveal>
{[
{
marke: "Approach",
text: "For me, treating means looking closely first. Which patterns show up, what does daily life bring with it, where is the body seeking balance. Out of those observations grows an approach that fits, unhurried by design.",
},
{
marke: "Practice",
text: "The practice is deliberately kept small. Few appointments a day, and in return enough room for conversation, treatment and the quiet afterwards.",
},
].map((teil) => (
<Reveal key={teil.marke} delay={0.1}>
<p
className="text-xs uppercase"
style={{ fontFamily: SANS, letterSpacing: "0.28em", color: SALBEI_HELL }}
>
{teil.marke}
</p>
<p
className="mt-5 max-w-xl text-base leading-relaxed md:text-lg"
style={{ fontFamily: SANS, color: LEISE }}
>
{teil.text}
</p>
</Reveal>
))}
</div>
</div>
</div>
</section>
);
}
/* --------------------------------------------------------------- erwartung */
function Erwartung() {
return (
<section className="relative py-14 md:py-28">
<div className="mx-auto max-w-7xl px-6 lg:px-10">
<div className="grid gap-12 lg:grid-cols-12 lg:gap-16">
<Reveal className="lg:col-span-5">
<Bildrahmen className="aspect-[4/5] w-full md:aspect-[3/4]">
<img
src="/templates/meridian/portrait-calm-woman.webp"
alt="A calm atmosphere in the practice"
loading="lazy"
decoding="async"
className="h-full w-full object-cover"
/>
</Bildrahmen>
</Reveal>
<div className="lg:col-span-7">
<Reveal>
<Marke>In the practice</Marke>
<div className="mt-8">
<Serifzeile vor="What to " kursiv="expect" nach=" in the practice." />
</div>
</Reveal>
<div className="mt-12 space-y-10">
{ERWARTUNG.map((punkt, i) => (
<Reveal key={punkt.titel} delay={i * 0.1}>
<p
className="text-sm"
style={{ fontFamily: SANS, color: SALBEI_HELL, letterSpacing: "0.08em" }}
>
{String(i + 1).padStart(2, "0")}
</p>
<h3 className="mt-3 text-2xl" style={{ ...serif, color: TINTE }}>
{punkt.titel}
</h3>
<p
className="mt-3 max-w-xl text-base leading-relaxed md:text-lg"
style={{ fontFamily: SANS, color: LEISE }}
>
{punkt.text}
</p>
</Reveal>
))}
</div>
</div>
</div>
</div>
</section>
);
}
/* ------------------------------------------------------------------- ablauf */
function Ablauf() {
const reduce = useReducedMotion();
const ref = useRef<HTMLDivElement>(null);
const sichtbar = useInView(ref, { once: true, margin: "-80px" });
return (
<section className="relative py-14 md:py-28">
<div className="mx-auto max-w-7xl px-6 lg:px-10">
<Reveal>
<div className="max-w-2xl">
<Marke>Process</Marke>
<div className="mt-8">
<Serifzeile vor="How the " kursiv="journey" nach=" begins." />
</div>
</div>
</Reveal>
<div
ref={ref}
className="relative mt-14 grid gap-8 sm:gap-10 md:mt-20 md:grid-cols-2 md:gap-12 lg:grid-cols-4"
>
{/* Die gestrichelte Linie verbindet die vier Punkte, quer am
Schreibtisch, senkrecht auf dem Telefon. */}
<svg
aria-hidden
viewBox="0 0 100 1"
preserveAspectRatio="none"
className="pointer-events-none absolute inset-x-0 top-6 hidden h-px w-full lg:block"
>
<motion.line
x1="0"
y1="0.5"
x2="100"
y2="0.5"
stroke={KANTE}
strokeWidth="0.4"
strokeDasharray="0.6 1.2"
initial={reduce ? false : { pathLength: 0 }}
animate={sichtbar || reduce ? { pathLength: 1 } : { pathLength: 0 }}
transition={{ duration: reduce ? 0 : 1.6, ease: [0.22, 1, 0.36, 1] }}
/>
</svg>
<motion.span
aria-hidden
initial={reduce ? false : { scaleY: 0 }}
animate={sichtbar || reduce ? { scaleY: 1 } : { scaleY: 0 }}
transition={{ duration: reduce ? 0 : 1.4, ease: [0.22, 1, 0.36, 1] }}
style={{ transformOrigin: "top", background: KANTE }}
className="pointer-events-none absolute bottom-10 left-5 top-10 w-px md:hidden"
/>
{ABLAUF.map((schritt, i) => (
<Reveal key={schritt.n} delay={i * 0.1} className="relative">
<div className="flex items-start gap-5 md:block">
<div
className="relative flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-xs md:h-12 md:w-12 md:text-sm"
style={{
border: `1px solid ${KANTE}`,
background: GRUND,
color: SALBEI,
fontFamily: SANS,
letterSpacing: "0.06em",
}}
>
{schritt.n}
</div>
<div className="flex-1 md:mt-6">
<h3 className="text-xl md:text-2xl" style={{ ...serif, color: TINTE }}>
{schritt.titel}
</h3>
<p
className="mt-2 text-base leading-relaxed md:mt-3"
style={{ fontFamily: SANS, color: LEISE }}
>
{schritt.text}
</p>
</div>
</div>
</Reveal>
))}
</div>
</div>
</section>
);
}
/* ------------------------------------------------------------------ passend */
function Passend() {
return (
<section className="relative py-14 md:py-28">
<div className="mx-auto max-w-7xl px-6 lg:px-10">
<Reveal>
<div
className="rounded-[2rem] px-6 py-12 sm:px-8 sm:py-20 md:rounded-[2.5rem] md:px-16 md:py-24 lg:px-20"
style={{ background: CREME }}
>
<div className="grid items-center gap-16 lg:grid-cols-12">
<div className="lg:col-span-5">
<Bildrahmen className="mx-auto aspect-[3/4] w-full max-w-sm">
<img
src="/templates/meridian/bg-moxa.webp"
alt="A moxibustion treatment"
loading="lazy"
className="h-full w-full object-cover"
/>
</Bildrahmen>
</div>
<div className="lg:col-span-7">
<Marke>Who it is for</Marke>
<div className="mt-8">
<Serifzeile vor="Who this practice may be " kursiv="right" nach=" for." />
</div>
<ul className="mt-10 max-w-xl space-y-6">
{PASSEND.map(({ Icon, text }) => (
<li key={text} className="flex items-start gap-5">
<span
className="mt-1 flex h-11 w-11 shrink-0 items-center justify-center rounded-full"
style={{ background: GRUND, color: SALBEI }}
>
<Icon className="h-5 w-5" strokeWidth={1.5} />
</span>
<span
className="text-base leading-relaxed md:text-lg"
style={{ fontFamily: SANS, color: LEISE }}
>
{text}
</span>
</li>
))}
</ul>
<p
className="mt-12 max-w-xl text-sm leading-relaxed"
style={{ fontFamily: SANS, fontStyle: "italic", color: LEISE }}
>
Treatment in this practice does not replace necessary medical diagnosis or
therapy.
</p>
</div>
</div>
</div>
</Reveal>
</div>
</section>
);
}
/* ----------------------------------------------------------------- kontakt */
const FELD: CSSProperties = {
marginTop: 8,
width: "100%",
borderRadius: 12,
border: `1px solid ${KANTE}`,
background: "#ffffff",
padding: "0.75rem 1.15rem",
fontSize: "0.875rem",
fontFamily: SANS,
color: TINTE,
};
function Kontakt() {
const [gesendet, setGesendet] = useState(false);
return (
<section id="contact" className="relative scroll-mt-24 pb-4 pt-20 md:pb-6 md:pt-28">
<div className="mx-auto max-w-7xl px-6 lg:px-10">
<Reveal>
<div className="grid gap-12 lg:grid-cols-2 lg:items-start lg:gap-20">
<div>
<Marke>Contact</Marke>
<div className="mt-8">
<Serifzeile vor="Would you like to request an " kursiv="appointment" nach="?" />
</div>
<p
className="mt-8 max-w-md text-base leading-relaxed md:text-lg"
style={{ fontFamily: SANS, color: LEISE }}
>
Feel free to send me a message. In that first contact we can work out whether and
how treatment might be a good fit.
</p>
<ul className="mt-10 space-y-5">
{KONTAKT.map(({ Icon, label, wert }) => (
<li key={label} className="flex items-start gap-4">
<span
className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-full"
style={{ background: CREME, color: SALBEI }}
>
<Icon className="h-4 w-4" strokeWidth={1.5} />
</span>
<div>
<p
className="text-[11px] uppercase"
style={{ fontFamily: SANS, letterSpacing: "0.2em", color: LEISE }}
>
{label}
</p>
<p className="mt-1 text-sm" style={{ fontFamily: SANS, color: TINTE }}>
{wert}
</p>
</div>
</li>
))}
</ul>
</div>
<form
onSubmit={(e) => {
e.preventDefault();
setGesendet(true);
}}
className="space-y-5 rounded-3xl p-6 md:p-10"
style={{ background: CREME }}
>
<label className="block">
<span
className="text-xs uppercase"
style={{ fontFamily: SANS, letterSpacing: "0.22em", color: LEISE }}
>
Name
</span>
<input type="text" placeholder="Your name" style={FELD} />
</label>
<label className="block">
<span
className="text-xs uppercase"
style={{ fontFamily: SANS, letterSpacing: "0.22em", color: LEISE }}
>
Email
</span>
<input type="email" placeholder="you@example.com" style={FELD} />
</label>
<label className="block">
<span
className="text-xs uppercase"
style={{ fontFamily: SANS, letterSpacing: "0.22em", color: LEISE }}
>
Message
</span>
<textarea
rows={5}
placeholder="What is this about? A short description is enough."
style={{ ...FELD, resize: "none" }}
/>
</label>
<button
type="submit"
className="group inline-flex items-center gap-2 px-8 py-3 text-sm transition-opacity hover:opacity-90"
style={{ borderRadius: 6, background: SALBEI, color: CREME, fontFamily: SANS }}
>
{gesendet ? "Thank you" : "Send message"}
<ArrowRight className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1" />
</button>
</form>
</div>
</Reveal>
</div>
</section>
);
}
/* --------------------------------------------------------------- fusszeile */
function Fusszeile() {
return (
<footer className="relative isolate">
{/* Das Moos liegt hinter der weissen Karte und laeuft nach oben aus. Es
schliesst die Seite mit demselben Gruen ab, mit dem sie anfaengt. */}
<img
src="/templates/meridian/footer-moss.webp"
alt=""
aria-hidden
loading="lazy"
className="pointer-events-none absolute inset-x-0 bottom-0 -z-10 h-[420px] w-full object-cover object-top md:h-[520px]"
style={{
maskImage: "linear-gradient(to bottom, transparent 0%, black 22%, black 100%)",
WebkitMaskImage: "linear-gradient(to bottom, transparent 0%, black 22%, black 100%)",
}}
/>
<div className="mx-auto max-w-7xl px-6 pb-16 pt-8 md:pb-24 md:pt-12 lg:px-10">
<div className="rounded-3xl" style={{ background: "#ffffff" }}>
<div className="grid gap-10 p-8 md:grid-cols-4 md:p-10">
<div className="space-y-4 md:col-span-2">
<span
className="block text-2xl leading-none md:text-3xl"
style={{ ...serif, color: TINTE }}
>
{PRAXIS.name}
</span>
<p
className="max-w-xs text-sm leading-relaxed"
style={{ fontFamily: SANS, color: LEISE }}
>
A natural health practice focused on Traditional Chinese Medicine. A calm look at
the body, everyday life and wellbeing.
</p>
</div>
{[
{ titel: "Navigation", links: ["Home", "Methods", "About", "Contact"] },
{ titel: "Legal", links: ["Imprint", "Privacy", "Cookie settings"] },
].map((spalte) => (
<div key={spalte.titel}>
<h3 className="mb-4 text-lg" style={{ ...serif, color: TINTE }}>
{spalte.titel}
</h3>
<ul className="space-y-2 text-sm" style={{ fontFamily: SANS, color: LEISE }}>
{spalte.links.map((link) => (
<li key={link}>
<a href="#top" className="transition-colors hover:opacity-70">
{link}
</a>
</li>
))}
</ul>
</div>
))}
</div>
<div
className="flex flex-col gap-2 px-8 pb-8 text-xs md:flex-row md:items-center md:justify-between md:px-10 md:pb-10"
style={{ fontFamily: SANS, color: LEISE }}
>
<p>© 2026 {PRAXIS.name}. All rights reserved.</p>
<p>All contact details on this page are placeholders.</p>
</div>
</div>
</div>
</footer>
);
}
/* --------------------------------------------------------------------- page */
export function MeridianTcmPage() {
return (
<div style={{ background: GRUND, color: TINTE, fontFamily: SANS }}>
<Kopfzeile />
<main>
<Held />
<Praxis />
<Ungleichgewicht />
<Schwerpunkte />
<Zitat />
<Methoden />
<Ueber />
<Erwartung />
<Ablauf />
<Passend />
<Kontakt />
</main>
<Fusszeile />
</div>
);
}
export default MeridianTcmPage;
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