All 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 & NatureFull page

The finished page to scroll through, switchable between desktop, tablet and mobile.

Source code

The complete page as a single file. Copy it into ./components and add the component to your route.

import { useCallback, useEffect, useRef, useState, type 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

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