Alle Templates
Templates/Ranch & Retreat
A warm, nature-focused full-page template for farms, retreats and leisure offers. Gentle fade-ins, clear typography and generous space lead calmly through offer, team and contact.
Leisure & NatureKomplette Seite
Die fertige Seite zum Durchscrollen, umschaltbar zwischen Desktop, Tablet und Handy.
Quellcode
Die komplette Seite als eine Datei. Kopiere sie nach ./components und binde die Komponente in deine Route ein.
import { useCallback, useEffect, useRef, useState, type HTMLAttributes } from "react";
import {
ArrowLeft,
ArrowRight,
ChevronDown,
Clock,
Compass,
Dog,
GraduationCap,
HelpCircle,
Home,
Mail,
Menu,
MessageSquareQuote,
PawPrint,
Phone,
Star,
Sun,
Trees,
Trophy,
X,
type LucideIcon,
} from "lucide-react";
// Neutral, fictional brand: "Auhof Ranch" in Ostheide. A landing page for a
// riding and dog ranch. No real people, names, addresses or external links.
// Photos of people come from /public/heros, scene photos (hero, pasture,
// text-free emblem) from /public/templates/ranch-retreat. All styles, colour
// tokens, reveal animations and fonts are shipped portably by the component
// itself in the scoped <style> block.
const BRAND_MARK = "/templates/ranch-retreat/logo-emblem.png";
const HERO_IMG = "/templates/ranch-retreat/hero.webp";
const RANCH_1 = "/templates/ranch-retreat/ranch-1.webp";
const RANCH_2 = "/templates/ranch-retreat/ranch-2.webp";
const FOUNDER_DOG = "/heros/editorial-photography-person-in-modern-outfit.webp";
const FOUNDER_HORSE = "/heros/editorial-photography-of-a-young-professional-standi.webp";
const FAQ_IMG = "/heros/photo-young-creative-sitting-top.webp";
const scopedStyles = `
@import url('https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600&family=Instrument+Serif:ital@0;1&display=swap');
.ranch-retreat {
--background: oklch(0.965 0.012 85);
--foreground: oklch(0.22 0.008 60);
--card: oklch(0.995 0.003 85);
--card-foreground: oklch(0.22 0.008 60);
--popover: oklch(0.995 0.003 85);
--popover-foreground: oklch(0.22 0.008 60);
--primary: oklch(0.34 0.045 150);
--primary-foreground: oklch(0.98 0.005 85);
--secondary: oklch(0.91 0.018 80);
--secondary-foreground: oklch(0.22 0.008 60);
--muted: oklch(0.93 0.014 82);
--muted-foreground: oklch(0.48 0.012 70);
--accent: oklch(0.88 0.022 80);
--accent-foreground: oklch(0.22 0.008 60);
--destructive: oklch(0.55 0.18 27);
--destructive-foreground: oklch(0.98 0.005 85);
--border: oklch(0.84 0.018 80);
--input: oklch(0.84 0.018 80);
--ring: oklch(0.34 0.045 150);
--font-body: "Figtree", ui-sans-serif, system-ui, sans-serif;
--font-serif: "Instrument Serif", ui-serif, Georgia, serif;
font-family: var(--font-body);
background-color: var(--background);
color: var(--foreground);
line-height: 1.45;
-webkit-font-smoothing: antialiased;
overflow-x: clip;
}
.ranch-retreat h1,
.ranch-retreat h2,
.ranch-retreat h3,
.ranch-retreat h4 {
font-family: var(--font-serif);
font-weight: 400;
letter-spacing: -0.01em;
line-height: 1.05;
}
.ranch-retreat .font-serif { font-family: var(--font-serif); }
.ranch-retreat .eyebrow {
font-family: var(--font-body);
font-size: 0.72rem;
letter-spacing: 0.22em;
text-transform: uppercase;
font-weight: 500;
color: var(--muted-foreground);
}
.ranch-retreat .reveal {
opacity: 0;
transform: translate3d(0, 24px, 0);
filter: blur(6px);
transition:
opacity 1100ms cubic-bezier(0.22, 0.61, 0.36, 1),
transform 1100ms cubic-bezier(0.22, 0.61, 0.36, 1),
filter 1100ms cubic-bezier(0.22, 0.61, 0.36, 1);
will-change: opacity, transform, filter;
}
.ranch-retreat .reveal.is-visible {
opacity: 1;
transform: translate3d(0, 0, 0);
filter: blur(0);
}
.ranch-retreat .reveal-left { transform: translate3d(-32px, 0, 0); }
.ranch-retreat .reveal-right { transform: translate3d(32px, 0, 0); }
.ranch-retreat .reveal-scale { transform: translate3d(0, 16px, 0) scale(0.96); }
.ranch-retreat .reveal-scale.is-visible { transform: translate3d(0, 0, 0) scale(1); }
.ranch-retreat .reveal-stagger > * {
opacity: 0;
transform: translate3d(0, 18px, 0);
filter: blur(4px);
transition:
opacity 900ms cubic-bezier(0.22, 0.61, 0.36, 1),
transform 900ms cubic-bezier(0.22, 0.61, 0.36, 1),
filter 900ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.ranch-retreat .reveal-stagger.is-visible > * {
opacity: 1;
transform: translate3d(0, 0, 0);
filter: blur(0);
}
.ranch-retreat .reveal-stagger.is-visible > *:nth-child(1) { transition-delay: 0ms; }
.ranch-retreat .reveal-stagger.is-visible > *:nth-child(2) { transition-delay: 100ms; }
.ranch-retreat .reveal-stagger.is-visible > *:nth-child(3) { transition-delay: 200ms; }
.ranch-retreat .reveal-stagger.is-visible > *:nth-child(4) { transition-delay: 300ms; }
.ranch-retreat .reveal-stagger.is-visible > *:nth-child(5) { transition-delay: 400ms; }
.ranch-retreat .reveal-stagger.is-visible > *:nth-child(6) { transition-delay: 500ms; }
@keyframes rr-rise-in {
0% { opacity: 0; transform: translate3d(0, 28px, 0); filter: blur(8px); }
100% { opacity: 1; transform: translate3d(0, 0, 0); filter: blur(0); }
}
.ranch-retreat .rise-in {
opacity: 0;
animation: rr-rise-in 1100ms cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
will-change: opacity, transform, filter;
}
@keyframes rr-ken-burns {
0% { transform: scale(1.08) translate3d(0, 0, 0); transform-origin: 50% 55%; }
100% { transform: scale(1.18) translate3d(-1.5%, -2%, 0); transform-origin: 50% 55%; }
}
.ranch-retreat .ken-burns {
animation: rr-ken-burns 24s ease-in-out infinite alternate;
will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
.ranch-retreat .reveal,
.ranch-retreat .reveal-stagger > *,
.ranch-retreat .rise-in,
.ranch-retreat .ken-burns {
opacity: 1 !important;
transform: none !important;
filter: none !important;
transition: none !important;
animation: none !important;
}
}
`;
/* Reveal-on-scroll: rebuilt inline (IntersectionObserver + class toggle). */
function useReveal<T extends HTMLElement = HTMLDivElement>() {
const ref = useRef<T | null>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
if (typeof IntersectionObserver === "undefined") {
el.classList.add("is-visible");
return;
}
const io = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
io.unobserve(entry.target);
}
});
},
{ threshold: 0.12, rootMargin: "0px 0px -60px 0px" },
);
io.observe(el);
return () => io.disconnect();
}, []);
return ref;
}
interface RevealProps extends HTMLAttributes<HTMLDivElement> {
delay?: number;
variant?: "up" | "left" | "right" | "scale";
stagger?: boolean;
}
function Reveal({
className = "",
style,
delay = 0,
variant = "up",
stagger = false,
...props
}: RevealProps) {
const ref = useReveal<HTMLDivElement>();
const classes = [
stagger ? "reveal-stagger" : "reveal",
variant === "left" ? "reveal-left" : "",
variant === "right" ? "reveal-right" : "",
variant === "scale" ? "reveal-scale" : "",
className,
]
.filter(Boolean)
.join(" ");
return (
<div
ref={ref}
className={classes}
style={{ transitionDelay: `${delay}ms`, ...style }}
{...props}
/>
);
}
/* ------------------------------------------------------------------ Hero */
const nav = [
{ label: "Dogs", href: "#hunde" },
{ label: "Horses", href: "#pferde" },
{ label: "Ranch", href: "#ranch" },
{ label: "Contact", href: "#kontakt" },
];
const keypoints = [
{ icon: Clock, label: "Since 1987" },
{ icon: Trees, label: "12 hectares of nature" },
{ icon: PawPrint, label: "For horse and dog" },
];
function Hero() {
const [scrolled, setScrolled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 80);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
useEffect(() => {
if (menuOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [menuOpen]);
return (
<section className="relative min-h-screen w-full overflow-hidden rounded-b-[2rem] md:min-h-[110vh] md:rounded-b-[3rem]">
<div className="absolute inset-0">
<img
src={HERO_IMG}
alt="Rider with a dog on the pastures of Auhof Ranch"
width={1920}
height={1280}
fetchPriority="high"
decoding="async"
className="ken-burns h-full w-full object-cover"
/>
</div>
<div className="absolute inset-0 bg-gradient-to-b from-black/55 via-black/35 to-black/75" />
{/* Nav appears on scroll and stays fixed */}
<header
className={`fixed inset-x-0 top-0 z-50 transition-all duration-500 ${
scrolled
? "translate-y-0 opacity-100 bg-background/80 backdrop-blur-md border-b border-foreground/10"
: "-translate-y-full opacity-0 pointer-events-none"
}`}
>
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4 md:px-10">
<a href="#" className="font-serif text-xl text-foreground md:text-2xl">
Auhof <em className="italic">Ranch</em>
</a>
<nav className="hidden items-center gap-10 md:flex">
{nav.map((n) => (
<a
key={n.label}
href={n.href}
className="text-sm text-foreground/75 transition-colors hover:text-foreground"
>
{n.label}
</a>
))}
</nav>
<a
href="#kontakt"
className="hidden rounded-full bg-foreground px-5 py-2 text-sm text-background transition-colors hover:bg-foreground/85 md:inline-flex"
>
Request an Appointment
</a>
<button
type="button"
aria-label="Open menu"
onClick={() => setMenuOpen(true)}
className="grid h-10 w-10 place-items-center rounded-full text-foreground md:hidden"
>
<Menu className="h-5 w-5" />
</button>
</div>
</header>
{/* Mobile menu */}
<div
className={`fixed inset-0 z-[60] md:hidden transition-opacity duration-300 ${
menuOpen ? "opacity-100" : "pointer-events-none opacity-0"
}`}
>
<div className="absolute inset-0 bg-background" />
<div className="relative flex h-full flex-col px-6 py-5">
<div className="flex items-center justify-between">
<span className="font-serif text-xl text-foreground">
Auhof <em className="italic">Ranch</em>
</span>
<button
type="button"
aria-label="Close menu"
onClick={() => setMenuOpen(false)}
className="grid h-10 w-10 place-items-center rounded-full text-foreground"
>
<X className="h-5 w-5" />
</button>
</div>
<nav className="mt-16 flex flex-col gap-7">
{nav.map((n, i) => (
<a
key={n.label}
href={n.href}
onClick={() => setMenuOpen(false)}
className={`font-serif text-4xl text-foreground ${menuOpen ? "rise-in" : "opacity-0"}`}
style={{ animationDelay: `${120 + i * 80}ms` }}
>
{n.label}
</a>
))}
</nav>
<a
href="#kontakt"
onClick={() => setMenuOpen(false)}
className="mt-auto inline-flex items-center justify-center rounded-full bg-foreground px-6 py-4 text-sm tracking-wide text-background"
>
Request an Appointment
</a>
</div>
</div>
{/* Central content */}
<div className="relative z-10 mx-auto flex min-h-[calc(100vh-96px)] max-w-5xl flex-col items-center justify-center px-6 pb-32 text-center text-white md:min-h-[calc(110vh-96px)] md:pb-40">
<img
src={BRAND_MARK}
alt="Auhof Ranch emblem: horse and dog"
className="rise-in mb-8 h-24 w-auto opacity-95 sm:h-28 md:mb-10 md:h-36"
style={{ animationDelay: "150ms" }}
/>
<h1
className="rise-in font-serif text-[clamp(3rem,9vw,8.5rem)] leading-[0.95] text-white"
style={{ animationDelay: "350ms" }}
>
Auhof <em className="italic">Ranch</em>
</h1>
<p
className="rise-in mt-6 max-w-xl text-base text-white/90 leading-relaxed md:mt-8 md:text-lg"
style={{ animationDelay: "550ms" }}
>
Exclusive care and first-class training for horses and dogs in the green countryside.
</p>
<div
className="rise-in mt-10 flex flex-col items-center gap-5 sm:flex-row sm:gap-8 md:mt-12"
style={{ animationDelay: "750ms" }}
>
<a
href="#pferde"
className="group inline-flex items-center justify-center gap-2.5 rounded-full bg-primary px-8 py-4 text-sm tracking-wide text-primary-foreground transition-all duration-300 sm:gap-0 sm:hover:gap-2.5 sm:hover:bg-primary/90"
>
<Compass
className="h-4 w-4 opacity-100 transition-all duration-300 sm:h-0 sm:w-0 sm:opacity-0 sm:group-hover:h-4 sm:group-hover:w-4 sm:group-hover:opacity-100"
strokeWidth={1.5}
/>
Explore the horse area
</a>
<a
href="#hunde"
className="group relative inline-flex items-center justify-center gap-2 px-2 py-2 text-sm tracking-wide text-white transition-all duration-300 sm:gap-0 sm:hover:gap-2"
>
<Dog
className="h-4 w-4 opacity-100 transition-all duration-300 sm:h-0 sm:w-0 sm:opacity-0 sm:group-hover:h-4 sm:group-hover:w-4 sm:group-hover:opacity-100"
strokeWidth={1.5}
/>
Explore the dog area
<span className="absolute inset-x-2 -bottom-0.5 h-px origin-left scale-x-0 bg-white transition-transform duration-500 ease-out group-hover:scale-x-100" />
</a>
</div>
</div>
{/* Keypoints */}
<div className="absolute inset-x-0 bottom-8 z-10 px-6 md:bottom-14">
<div className="mx-auto flex max-w-5xl flex-col items-center justify-center gap-4 sm:flex-row sm:justify-between sm:gap-10">
{keypoints.map((k, i) => {
const Icon = k.icon;
return (
<div
key={k.label}
className="rise-in flex items-center gap-3 text-white"
style={{ animationDelay: `${950 + i * 150}ms` }}
>
<Icon className="h-5 w-5 text-white/80" strokeWidth={1.4} />
<span className="text-xs uppercase tracking-[0.08em] text-white/90">{k.label}</span>
</div>
);
})}
</div>
</div>
</section>
);
}
/* -------------------------------------------------------------- Founders */
type Feature = { icon: LucideIcon; title: string; body: string };
function FeatureCard({ icon: Icon, title, body }: Feature) {
return (
<div className="rounded-2xl bg-secondary/60 px-6 py-6">
<Icon className="h-5 w-5 text-primary/80" />
<p className="mt-5 text-base font-medium text-foreground">{title}</p>
<p className="mt-2 text-sm text-foreground/70">{body}</p>
</div>
);
}
function Cta({ label }: { label: string }) {
return (
<a
href="#kontakt"
className="group mt-10 inline-flex items-center gap-3 text-xs uppercase tracking-[0.08em] text-foreground"
>
<span>{label}</span>
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</a>
);
}
function Founders() {
return (
<section className="px-6 py-20 sm:py-28 md:py-40">
<div className="mx-auto max-w-6xl space-y-24 sm:space-y-28 md:space-y-40">
{/* Dogs */}
<div id="hunde" className="grid grid-cols-12 gap-y-10 md:gap-8 md:gap-y-0 md:gap-x-16">
<Reveal className="col-span-12 md:col-span-5">
<div className="overflow-hidden rounded-3xl">
<img
src={FOUNDER_DOG}
alt="Neele, dog trainer at Auhof Ranch"
width={896}
height={1216}
loading="lazy"
className="h-auto w-full object-cover"
/>
</div>
</Reveal>
<Reveal delay={150} className="col-span-12 flex flex-col md:col-span-6 md:col-start-7">
<p className="eyebrow mb-6">Neele Marquardt</p>
<h2 className="font-serif text-[2.5rem] sm:text-5xl md:text-6xl">
Dog Training <em className="italic">& Boarding</em>
</h2>
<p className="mt-8 max-w-md text-base text-foreground/70">
Whether a challenging dog or a young puppy, our dog school trains every breed. With
plenty of calm, consistency and expertise, we help you become a relaxed team with your
four-legged friend.
</p>
<Reveal stagger className="mt-10 grid grid-cols-1 gap-4 sm:grid-cols-2">
<FeatureCard
icon={GraduationCap}
title="Training & Guidance"
body="Professional dog training and in-depth behavioural guidance for a harmonious life together."
/>
<FeatureCard
icon={Home}
title="Dog Boarding"
body="Caring, species-appropriate boarding for your four-legged friend while you are away."
/>
</Reveal>
<Cta label="See the dogs" />
</Reveal>
</div>
{/* Horses */}
<div id="pferde" className="grid grid-cols-12 gap-y-10 md:gap-8 md:gap-y-0 md:gap-x-16">
<Reveal
delay={150}
className="col-span-12 order-2 flex flex-col md:col-span-6 md:order-1"
>
<p className="eyebrow mb-6">Jonas Halder</p>
<h2 className="font-serif text-[2.5rem] sm:text-5xl md:text-6xl">
Western Riding <em className="italic">& Horse Boarding</em>
</h2>
<p className="mt-8 max-w-md text-base text-foreground/70">
With direct access to the riding trails of the floodplain forest and first-class
facilities, we offer ideal conditions for you and your horse.
</p>
<Reveal stagger className="mt-10 grid grid-cols-1 gap-4 sm:grid-cols-2">
<FeatureCard
icon={Sun}
title="12 ha of pasture"
body="Spacious boarding, paddock boxes and an open stable. Plenty of high-quality hay is always on offer."
/>
<FeatureCard
icon={Trophy}
title="Western Training"
body="Patient, relaxed western training as well as professional western show support."
/>
</Reveal>
<Cta label="See the horses" />
</Reveal>
<Reveal className="col-span-12 order-1 md:col-span-5 md:col-start-8 md:order-2">
<div className="overflow-hidden rounded-3xl">
<img
src={FOUNDER_HORSE}
alt="Jonas, western trainer at Auhof Ranch"
width={896}
height={1216}
loading="lazy"
className="h-auto w-full object-cover"
/>
</div>
</Reveal>
</div>
</div>
</section>
);
}
/* ------------------------------------------------------------ AboutRanch */
function AboutRanch() {
const sectionRef = useRef<HTMLElement>(null);
const [offset, setOffset] = useState(0);
useEffect(() => {
let raf = 0;
const update = () => {
raf = 0;
const el = sectionRef.current;
if (!el) return;
if (
typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches
) {
setOffset(0);
return;
}
const rect = el.getBoundingClientRect();
const vh = window.innerHeight || 1;
const progress = (vh / 2 - (rect.top + rect.height / 2)) / (vh / 2 + rect.height / 2);
setOffset(Math.max(-1, Math.min(1, progress)));
};
const onScroll = () => {
if (!raf) raf = requestAnimationFrame(update);
};
update();
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onScroll);
if (raf) cancelAnimationFrame(raf);
};
}, []);
const leftY = offset * 80;
const rightY = offset * -80;
return (
<section
ref={sectionRef}
id="ranch"
className="relative overflow-hidden bg-secondary/40 px-6 py-20 sm:py-32 md:py-64"
>
<div
aria-hidden
className="pointer-events-none absolute inset-y-0 left-0 hidden w-[26vw] max-w-[360px] items-center lg:flex"
>
<img
src={RANCH_1}
alt=""
width={1456}
height={816}
loading="lazy"
className="h-[60vh] w-full rounded-r-3xl object-cover will-change-transform"
style={{ transform: `translate3d(0, ${leftY}px, 0)` }}
/>
</div>
<div
aria-hidden
className="pointer-events-none absolute inset-y-0 right-0 hidden w-[26vw] max-w-[360px] items-center lg:flex"
>
<img
src={RANCH_2}
alt=""
width={1920}
height={1280}
loading="lazy"
className="h-[60vh] w-full rounded-l-3xl object-cover will-change-transform"
style={{ transform: `translate3d(0, ${rightY}px, 0)` }}
/>
</div>
<div className="relative mx-auto max-w-2xl text-center">
<Reveal>
<p className="eyebrow mb-6">Since 1987</p>
<h2 className="font-serif text-[2.75rem] leading-[1.05] tracking-[-0.01em] sm:text-5xl md:text-[4.25rem]">
Room for people
<br />
<em className="italic font-normal">and animals.</em>
</h2>
<p className="mx-auto mt-10 max-w-xl text-base text-foreground/80">
In the summer of 1987, the dream of our own western riding centre in Ostheide began.
Today, Auhof is a well-established home for western riders and dog lovers. Across
roughly 12 hectares of pasture right beside the idyllic floodplain forest, we bring
together tradition and modern standards.
</p>
<p className="mx-auto mt-6 max-w-xl text-base text-foreground/80">
We offer you and your animal generous space, bright riding halls and trails leading
straight from the yard. Since 2007, Neele and Jonas have run the ranch with a clear
vision for the complete wellbeing of every animal.
</p>
</Reveal>
<div className="mt-12 grid grid-cols-1 gap-4 lg:hidden">
<Reveal>
<img
src={RANCH_1}
alt="Horse and rider in the arena"
loading="lazy"
className="h-auto w-full rounded-2xl object-cover"
/>
</Reveal>
<Reveal delay={120}>
<img
src={RANCH_2}
alt="Dog on the pasture"
loading="lazy"
className="h-auto w-full rounded-2xl object-cover"
/>
</Reveal>
</div>
</div>
</section>
);
}
/* ---------------------------------------------------------------- Process */
const steps = [
{
n: "1",
title: "Get in touch",
text: "You reach out to us by phone or email, with no obligation.",
},
{
n: "2",
title: "Meet up",
text: "We arrange a visit to the ranch to get to know one another.",
},
{
n: "3",
title: "Assessment",
text: "Together we work out what you and your animal really need.",
},
{
n: "4",
title: "Training plan",
text: "You receive a tailored plan, matched to your goals.",
},
{
n: "5",
title: "Get started",
text: "Together we begin the training or plan the move in.",
},
];
function Process() {
const scrollerRef = useRef<HTMLDivElement | null>(null);
const [canPrev, setCanPrev] = useState(false);
const [canNext, setCanNext] = useState(true);
useEffect(() => {
const el = scrollerRef.current;
if (!el) return;
const update = () => {
setCanPrev(el.scrollLeft > 4);
setCanNext(el.scrollLeft + el.clientWidth < el.scrollWidth - 4);
};
update();
el.addEventListener("scroll", update, { passive: true });
window.addEventListener("resize", update);
return () => {
el.removeEventListener("scroll", update);
window.removeEventListener("resize", update);
};
}, []);
const scrollBy = (dir: 1 | -1) => {
const el = scrollerRef.current;
if (!el) return;
const first = el.querySelector<HTMLElement>("article");
const step = first ? first.offsetWidth + 24 : el.clientWidth * 0.85;
el.scrollBy({ left: dir * step, behavior: "smooth" });
};
return (
<section className="py-20 sm:py-28 md:py-40">
<div className="mx-auto max-w-6xl px-6">
<Reveal>
<p className="eyebrow mb-6">The Path</p>
<div className="flex items-end justify-between gap-6">
<h2 className="font-serif text-[2.5rem] sm:text-5xl md:text-6xl">
Your path <em className="italic">to us</em>
</h2>
<div className="hidden shrink-0 gap-3 md:flex">
<button
type="button"
onClick={() => scrollBy(-1)}
disabled={!canPrev}
aria-label="Previous step"
className="grid h-11 w-11 place-items-center rounded-full border border-foreground/15 text-foreground/70 transition-colors hover:border-foreground/40 hover:text-foreground disabled:opacity-30 disabled:hover:border-foreground/15 disabled:hover:text-foreground/70"
>
<ArrowLeft className="h-4 w-4" />
</button>
<button
type="button"
onClick={() => scrollBy(1)}
disabled={!canNext}
aria-label="Next step"
className="grid h-11 w-11 place-items-center rounded-full border border-foreground/15 text-foreground/70 transition-colors hover:border-foreground/40 hover:text-foreground disabled:opacity-30 disabled:hover:border-foreground/15 disabled:hover:text-foreground/70"
>
<ArrowRight className="h-4 w-4" />
</button>
</div>
</div>
</Reveal>
</div>
<Reveal delay={150}>
<div
className="relative mt-16 md:mt-20"
style={{
WebkitMaskImage:
"linear-gradient(to right, transparent 0, #000 4rem, #000 calc(100% - 4rem), transparent 100%)",
maskImage:
"linear-gradient(to right, transparent 0, #000 4rem, #000 calc(100% - 4rem), transparent 100%)",
}}
>
<div
ref={scrollerRef}
className="flex snap-x snap-mandatory gap-6 overflow-x-auto px-6 pb-6 md:gap-8 md:px-[max(1.5rem,calc((100vw-72rem)/2))]"
style={{ scrollbarWidth: "none" }}
>
{steps.map((s) => (
<article
key={s.n}
className="flex w-[85vw] shrink-0 snap-start flex-col rounded-3xl bg-[oklch(0.99_0.004_85)] p-8 md:w-[380px] md:p-10"
>
<div className="flex items-start justify-between gap-4">
<h3 className="font-serif text-2xl tracking-[-0.01em] md:text-3xl">{s.title}</h3>
<span
aria-hidden
className="font-serif text-3xl italic text-foreground/25 tabular-nums md:text-4xl"
>
{s.n.padStart(2, "0")}
</span>
</div>
<p className="mt-4 text-[15px] leading-relaxed text-foreground/55">{s.text}</p>
</article>
))}
<div className="w-2 shrink-0 md:hidden" aria-hidden />
</div>
</div>
</Reveal>
</section>
);
}
/* ----------------------------------------------------------- Testimonials */
const quotes = [
{
text: "A wonderful dog school. They gave me amazing help with my difficult dog, one that many trainers had given up on before.",
author: "Vivian K.",
service: "Dog School",
},
{
text: "A lovely, well-kept yard with plenty of riding options and yet never overcrowded. There is plenty of hay and the trail is genuinely great.",
author: "Jana W.",
service: "Horse Boarding",
},
{
text: "Such a great ranch! Everyone is super relaxed, friendly and helpful. The mood is wonderful and Jonas is the most patient riding instructor.",
author: "Franziska S.",
service: "Riding Lessons",
},
{
text: "My dog loves the training at the ranch. The atmosphere is calm and the trainers know exactly what they are doing.",
author: "Markus B.",
service: "Puppy Training",
},
{
text: "Finally a place where my horse is kept the way it should be. Big pastures, fair feeding, warm-hearted people.",
author: "Lena T.",
service: "Open Stable",
},
{
text: "The western riding lessons with Jonas are a real highlight. Patient, technically excellent and with a lot of heart for both people and horses.",
author: "Tobias R.",
service: "Western Riding",
},
];
const PAGE_SIZE = 3;
const DURATION = 8000;
function Testimonials() {
const pages = Math.ceil(quotes.length / PAGE_SIZE);
const [page, setPage] = useState(0);
const [paused, setPaused] = useState(false);
const goTo = useCallback((resolver: (current: number) => number) => {
setPage((p) => resolver(p));
}, []);
useEffect(() => {
if (paused) return;
const id = window.setTimeout(() => {
setPage((p) => (p + 1) % pages);
}, DURATION);
return () => window.clearTimeout(id);
}, [page, paused, pages]);
return (
<section className="bg-primary text-primary-foreground px-6 py-20 sm:py-28 md:py-40">
<div className="mx-auto max-w-6xl">
<Reveal>
<p className="eyebrow mb-6 flex items-center justify-center gap-2 !text-primary-foreground/60">
<MessageSquareQuote className="h-3.5 w-3.5" />
Voices
</p>
<h2 className="text-center font-serif text-[2.5rem] sm:text-5xl md:text-6xl text-primary-foreground">
What our <em className="italic">guests say</em>
</h2>
</Reveal>
<div
className="relative mt-16"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
<div className="grid">
{Array.from({ length: pages }).map((_, p) => {
const items = quotes.slice(p * PAGE_SIZE, p * PAGE_SIZE + PAGE_SIZE);
const active = p === page;
return (
<div
key={p}
aria-hidden={!active}
className={`[grid-area:1/1] grid grid-cols-1 items-stretch gap-6 md:grid-cols-3 md:gap-8 transition-[opacity,transform] duration-700 ease-[cubic-bezier(0.22,0.61,0.36,1)] ${
active
? "opacity-100 translate-y-0 pointer-events-auto"
: "opacity-0 translate-y-2 pointer-events-none"
}`}
>
{items.map((q) => (
<div
key={q.author}
className="relative flex h-full flex-col rounded-3xl bg-primary-foreground/[0.06] px-8 py-10 backdrop-blur-sm"
>
<div className="flex gap-1 text-primary-foreground/80">
{Array.from({ length: 5 }).map((_, j) => (
<Star key={j} className="h-4 w-4 fill-current" strokeWidth={0} />
))}
</div>
<blockquote className="mt-6 font-serif text-xl italic leading-snug text-primary-foreground/95">
“{q.text}”
</blockquote>
<div className="mt-auto pt-8">
<p className="text-xs uppercase tracking-[0.22em] text-primary-foreground/80">
{q.author}
</p>
<p className="mt-1 text-xs tracking-wide text-primary-foreground/50">
{q.service}
</p>
</div>
</div>
))}
</div>
);
})}
</div>
<div className="mt-10 flex justify-center gap-3 md:justify-end">
<button
type="button"
onClick={() => goTo((p) => (p - 1 + pages) % pages)}
aria-label="Previous reviews"
className="grid h-11 w-11 place-items-center rounded-full border border-primary-foreground/20 text-primary-foreground/80 transition-colors hover:border-primary-foreground/60 hover:text-primary-foreground"
>
<ArrowLeft className="h-4 w-4" />
</button>
<button
type="button"
onClick={() => goTo((p) => (p + 1) % pages)}
aria-label="Next reviews"
className="grid h-11 w-11 place-items-center rounded-full border border-primary-foreground/20 text-primary-foreground/80 transition-colors hover:border-primary-foreground/60 hover:text-primary-foreground"
>
<ArrowRight className="h-4 w-4" />
</button>
</div>
</div>
</div>
</section>
);
}
/* -------------------------------------------------------------------- Faq */
const faqItems = [
{
q: "Do you also take on difficult dogs?",
a: "Yes, absolutely. Dogs with behavioural issues, as well as fear or aggression problems, are a core part of Neele's work. We look at the underlying cause and work towards a solution calmly and confidently.",
},
{
q: "Which riding styles do you teach?",
a: "Jonas specialises in western riding, with a focus on subtle communication and a partnership with the horse. Beginners and advanced riders are equally welcome.",
},
{
q: "Are there free boarding spots at the ranch?",
a: "Current availability is best discussed in person. Feel free to get in touch with an enquiry and we will give you a clear, honest answer.",
},
{
q: "Do you also work with positive reinforcement?",
a: "Yes. Our training is built on trust, clear communication and positive reinforcement, without harsh methods or pressure.",
},
{
q: "Does the training take place at my home or at the ranch?",
a: "Both are possible. Depending on the topic and what you prefer, we come to your home or you visit us at Auhof.",
},
];
function Faq() {
const frameRef = useRef<HTMLDivElement>(null);
const imageRef = useRef<HTMLImageElement>(null);
const [openIndex, setOpenIndex] = useState<number | null>(null);
useEffect(() => {
if (typeof window === "undefined") return;
let raf = 0;
const update = () => {
raf = 0;
const frame = frameRef.current;
const image = imageRef.current;
if (!frame || !image) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
image.style.transform = "translate3d(0, 0, 0)";
return;
}
const rect = frame.getBoundingClientRect();
const vh = window.innerHeight;
const progress = (vh - rect.top) / (vh + rect.height);
const clamped = Math.max(0, Math.min(1, progress));
const translate = 5 - clamped * 10;
image.style.transform = `translate3d(0, ${translate}%, 0)`;
};
const onScroll = () => {
if (!raf) raf = requestAnimationFrame(update);
};
update();
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onScroll);
if (raf) cancelAnimationFrame(raf);
};
}, []);
return (
<section className="px-6 py-20 sm:py-28 md:py-40">
<div className="mx-auto grid max-w-6xl grid-cols-1 gap-12 md:grid-cols-2 md:gap-16 lg:gap-20">
<div
ref={frameRef}
className="relative aspect-[4/5] h-full w-full overflow-hidden rounded-3xl bg-muted md:self-start"
>
<img
ref={imageRef}
src={FAQ_IMG}
alt="A calm moment at Auhof Ranch"
draggable={false}
className="pointer-events-none absolute inset-x-0 -top-[8%] h-[116%] w-full select-none object-cover will-change-transform"
style={{ transform: "translate3d(0, 5%, 0)" }}
/>
</div>
<Reveal delay={150}>
<p className="eyebrow mb-6 flex items-center gap-2">
<HelpCircle className="h-3.5 w-3.5" />
Frequently Asked Questions
</p>
<h2 className="font-serif text-[2.5rem] sm:text-5xl md:text-6xl">
Good to <em className="italic">know</em>
</h2>
<div className="mt-10 flex w-full flex-col gap-3">
{faqItems.map((it, i) => {
const open = openIndex === i;
return (
<div key={i} className="rounded-2xl bg-card shadow-[0_1px_2px_rgba(0,0,0,0.04)]">
<h3>
<button
type="button"
onClick={() => setOpenIndex(open ? null : i)}
aria-expanded={open}
className="group flex w-full items-center justify-between gap-6 px-6 py-5 text-left md:px-7 md:py-6"
>
<span className="text-sm font-medium md:text-base">{it.q}</span>
<ChevronDown
className={`h-4 w-4 shrink-0 text-foreground/50 transition-transform duration-300 ${
open ? "rotate-180" : ""
}`}
strokeWidth={1.8}
/>
</button>
</h3>
<div
className="grid overflow-hidden transition-[grid-template-rows] duration-300 ease-out"
style={{ gridTemplateRows: open ? "1fr" : "0fr" }}
>
<div className="overflow-hidden">
<p className="px-6 pb-6 text-sm leading-relaxed text-foreground/65 md:px-7 md:pb-7 md:text-base">
{it.a}
</p>
</div>
</div>
</div>
);
})}
</div>
<div className="mt-10 flex flex-col items-start gap-5 rounded-2xl bg-[oklch(0.99_0.004_85)] px-6 py-7 md:flex-row md:items-center md:justify-between md:px-7">
<div>
<p className="font-serif text-xl md:text-2xl">Still have questions?</p>
<p className="mt-1 text-sm text-foreground/60">Write to us, we reply personally.</p>
</div>
<a
href="#kontakt"
className="inline-flex shrink-0 items-center gap-2 rounded-full bg-foreground px-6 py-3 text-sm tracking-wide text-background transition-colors hover:bg-foreground/90"
>
Get in touch
<ArrowRight className="h-4 w-4" />
</a>
</div>
</Reveal>
</div>
</section>
);
}
/* ------------------------------------------------------------- SiteFooter */
function SiteFooter() {
return (
<footer id="kontakt" className="bg-card">
<div className="mx-auto max-w-4xl px-6 py-24 text-center sm:py-32 md:py-40">
<Reveal>
<p className="eyebrow mb-8 flex items-center justify-center gap-2">
<Mail className="h-3.5 w-3.5" />
Contact
</p>
<h2 className="font-serif text-[2.75rem] leading-[1.05] tracking-[-0.01em] sm:text-5xl md:text-[5rem]">
We look forward
<br />
<em className="italic font-normal">to meeting you</em>
</h2>
<p className="mx-auto mt-10 max-w-xl text-base text-foreground/70">
Whether dog or horse, boarding or training, get in touch with us. We are happy to talk
through in person how we can help.
</p>
<div className="mt-12 flex flex-col items-center justify-center gap-4 sm:flex-row">
<a
href="mailto:hallo@auhof-ranch.de"
className="group inline-flex items-center justify-center gap-3 rounded-full bg-foreground px-8 py-4 text-sm tracking-wide text-background transition-all duration-300 sm:gap-0 sm:hover:gap-3 sm:hover:bg-foreground/90"
>
<Mail className="h-4 w-4 opacity-100 transition-all duration-300 sm:h-0 sm:w-0 sm:opacity-0 sm:group-hover:h-4 sm:group-hover:w-4 sm:group-hover:opacity-100" />
Write an email
</a>
<a
href="tel:+4900000000"
className="group inline-flex items-center justify-center gap-3 rounded-full bg-secondary/70 px-8 py-4 text-sm tracking-wide text-foreground transition-all duration-300 sm:gap-0 sm:hover:gap-3 sm:hover:bg-secondary"
>
<Phone className="h-4 w-4 opacity-100 transition-all duration-300 sm:h-0 sm:w-0 sm:opacity-0 sm:group-hover:h-4 sm:group-hover:w-4 sm:group-hover:opacity-100" />
Call us
</a>
</div>
</Reveal>
</div>
<div className="border-t border-foreground/15">
<div className="mx-auto flex max-w-6xl flex-col items-center gap-8 px-6 py-12 text-center md:flex-row md:items-center md:justify-between md:gap-10 md:text-left">
<p className="font-serif text-2xl">
Auhof <em className="italic">Ranch</em>
</p>
<nav className="flex flex-wrap justify-center gap-x-7 gap-y-3 text-sm md:gap-x-10">
{["Dogs", "Horses", "News", "Appointments"].map((l) => (
<a
key={l}
href="#"
className="text-foreground/70 transition-colors hover:text-foreground"
>
{l}
</a>
))}
</nav>
<div className="flex flex-wrap justify-center gap-x-6 gap-y-2 text-xs uppercase tracking-[0.18em] text-muted-foreground md:gap-8">
<a href="#" className="hover:text-foreground">
Legal Notice
</a>
<a href="#" className="hover:text-foreground">
Privacy
</a>
</div>
</div>
<div className="mx-auto max-w-6xl px-6 pb-10 text-center text-xs text-muted-foreground md:text-left">
© {new Date().getFullYear()} Auhof Ranch. Ostheide.
</div>
</div>
</footer>
);
}
/* ------------------------------------------------------------------- Page */
export function RanchRetreatPage() {
return (
<div className="ranch-retreat">
<style dangerouslySetInnerHTML={{ __html: scopedStyles }} />
<main>
<Hero />
<Founders />
<AboutRanch />
<Process />
<Testimonials />
<Faq />
<SiteFooter />
</main>
</div>
);
}
export default RanchRetreatPage;
FAQ
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