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">&amp; 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">&amp; 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