WDB-0002 · Yuki Tanabe (kimi-k2.7-code) (default)
```tsx App.tsx
import { useRef, useState, useEffect, useMemo, createContext, useContext } from "react";
import { motion, useScroll, useTransform, useReducedMotion, useMotionValue, useSpring } from "framer-motion";
import {
  Anchor,
  Ship,
  Wind,
  Compass,
  Watch,
  Bell,
  MapPin,
  Phone,
  Mail,
  Clock,
  AlertTriangle,
  ChevronDown,
  ExternalLink,
  Menu,
  X
} from "lucide-react";

const EASE_REVEAL = [0.16, 1, 0.3, 1] as const;
const EASE_SCENE = [0.7, 0, 0.2, 1] as const;
const SCENES = [
  { id: "hero", label: "Surface", depth: 0 },
  { id: "ship", label: "The Ship", depth: 0 },
  { id: "night", label: "The Night", depth: 0 },
  { id: "dive", label: "The Descent", depth: 28 },
  { id: "artifacts", label: "Artifacts", depth: 34 },
  { id: "timeline", label: "Timeline", depth: 36 },
  { id: "rules", label: "Dive Rules", depth: 38 },
  { id: "visit", label: "Visit", depth: 40 }
];

const ReducedMotionContext = createContext(false);

function useIsReduced() {
  return useContext(ReducedMotionContext);
}

function Timecode({ label }: { label: string }) {
  return <span className="timecode">{label}</span>;
}

function SceneLabel({ number, intent }: { number: string; intent: string }) {
  return (
    <div className="scene-label">
      <Timecode label={`Scene ${number} / 08`} />
      <span className="scene-intent">{intent}</span>
    </div>
  );
}

function Reveal({
  children,
  delay = 0,
  className = "",
  as: Component = "div"
}: {
  children: React.ReactNode;
  delay?: number;
  className?: string;
  as?: keyof JSX.IntrinsicElements;
}) {
  const reduce = useIsReduced();
  const MotionComponent = motion.create(Component as any);
  return (
    <MotionComponent
      className={className}
      initial={reduce ? false : { opacity: 0, y: 40 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, margin: "-80px" }}
      transition={{ duration: 0.72, delay, ease: EASE_REVEAL }}
    >
      {children}
    </MotionComponent>
  );
}

function CustomCursor() {
  const reduce = useIsReduced();
  const [hovering, setHovering] = useState(false);
  const [visible, setVisible] = useState(false);
  const x = useMotionValue(0);
  const y = useMotionValue(0);
  const springX = useSpring(x, { stiffness: 220, damping: 22 });
  const springY = useSpring(y, { stiffness: 220, damping: 22 });

  useEffect(() => {
    if (reduce) return;
    if (window.matchMedia("(hover: none)").matches) return;
    const onMove = (e: MouseEvent) => {
      x.set(e.clientX);
      y.set(e.clientY);
      setVisible(true);
    };
    const onEnter = () => setVisible(true);
    const onLeave = () => setVisible(false);
    const onOver = (e: MouseEvent) => {
      const target = e.target as HTMLElement;
      if (target.closest("a, button, [role='button'], input, textarea, select")) {
        setHovering(true);
      } else {
        setHovering(false);
      }
    };
    window.addEventListener("mousemove", onMove);
    document.body.addEventListener("mouseenter", onEnter);
    document.body.addEventListener("mouseleave", onLeave);
    document.body.addEventListener("mouseover", onOver);
    return () => {
      window.removeEventListener("mousemove", onMove);
      document.body.removeEventListener("mouseenter", onEnter);
      document.body.removeEventListener("mouseleave", onLeave);
      document.body.removeEventListener("mouseover", onOver);
    };
  }, [reduce, x, y]);

  if (reduce) return null;
  if (!visible) return null;

  return (
    <motion.div
      className="custom-cursor"
      style={{ x: springX, y: springY }}
      animate={{ scale: hovering ? 2.2 : 1, opacity: 1 }}
      transition={{ duration: 0.24, ease: EASE_REVEAL }}
    />
  );
}

function MarineSnow() {
  const reduce = useIsReduced();
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    if (reduce) return;
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    let width = window.innerWidth;
    let height = window.innerHeight;
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = width * dpr;
    canvas.height = height * dpr;
    canvas.style.width = `${width}px`;
    canvas.style.height = `${height}px`;
    ctx.scale(dpr, dpr);

    const particleCount = Math.min(width < 768 ? 40 : 90, 120);
    const particles = Array.from({ length: particleCount }, () => ({
      x: Math.random() * width,
      y: Math.random() * height,
      r: Math.random() * 1.4 + 0.4,
      vy: Math.random() * 0.6 + 0.2,
      vx: (Math.random() - 0.5) * 0.3,
      a: Math.random() * 0.5 + 0.1
    }));

    let raf = 0;
    const animate = () => {
      ctx.clearRect(0, 0, width, height);
      ctx.fillStyle = "rgba(76, 224, 255, 0.55)";
      for (const p of particles) {
        p.y += p.vy;
        p.x += p.vx;
        if (p.y > height) p.y = -4;
        if (p.x > width) p.x = 0;
        if (p.x < 0) p.x = width;
        ctx.globalAlpha = p.a;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
        ctx.fill();
      }
      raf = requestAnimationFrame(animate);
    };
    raf = requestAnimationFrame(animate);

    const onResize = () => {
      width = window.innerWidth;
      height = window.innerHeight;
      canvas.width = width * dpr;
      canvas.height = height * dpr;
      canvas.style.width = `${width}px`;
      canvas.style.height = `${height}px`;
      ctx.scale(dpr, dpr);
    };

    window.addEventListener("resize", onResize);
    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener("resize", onResize);
    };
  }, [reduce]);

  if (reduce) return null;
  return <canvas ref={canvasRef} aria-hidden className="marine-snow" />;
}

function GlobalBackground() {
  const reduce = useIsReduced();
  const { scrollYProgress } = useScroll();
  const surfaceOpacity = useTransform(scrollYProgress, [0, 0.35], [reduce ? 0.55 : 1, 0]);
  const depthTint = useTransform(scrollYProgress, [0, 0.5], [0, reduce ? 0.35 : 1]);

  return (
    <div className="global-background" aria-hidden>
      <motion.div
        className="surface-light"
        style={{ opacity: surfaceOpacity }}
      />
      <motion.div
        className="depth-veil"
        style={{ opacity: depthTint }}
      />
    </div>
  );
}

function Navigation() {
  const [open, setOpen] = useState(false);
  return (
    <nav className="main-nav">
      <button className="nav-toggle" onClick={() => setOpen(!open)} aria-label="Menu">
        {open ? <X size={20} /> : <Menu size={20} />}
      </button>
      <div className={`nav-panel ${open ? "open" : ""}`}>
        {SCENES.map((s) => (
          <a
            key={s.id}
            href={`#${s.id}`}
            className="nav-link"
            onClick={() => setOpen(false)}
          >
            <span className="nav-number">0{SCENES.indexOf(s) + 1}</span>
            <span className="nav-name">{s.label}</span>
          </a>
        ))}
      </div>
    </nav>
  );
}

function Hero() {
  const reduce = useIsReduced();
  const title = ["THE", "WRECK", "OF THE", "CORALYN"];
  return (
    <section id="hero" className="scene hero-scene">
      <div className="hud">
        <Timecode label="Scene 01 / 08" />
        <Timecode label="00:00:00" />
      </div>
      <div className="hero-content">
        <motion.h1 className="display" aria-label="The Wreck of the Coralyn">
          {title.map((word, i) => (
            <span key={word} className="word">
              <motion.span
                className="word-inner"
                initial={reduce ? false : { y: "110%" }}
                animate={{ y: "0%" }}
                transition={{ duration: 0.82, delay: 0.2 + i * 0.07, ease: EASE_REVEAL }}
              >
                {word}
              </motion.span>
            </span>
          ))}
        </motion.h1>
        <motion.p
          className="lead"
          initial={reduce ? false : { opacity: 0, y: 24 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.72, delay: 0.72, ease: EASE_REVEAL }}
        >
          A coastal steamer lost off Port Camden Heads in 1908. Scroll to descend.
        </motion.p>
      </div>
      <motion.div
        className="scroll-hint"
        initial={reduce ? false : { opacity: 0 }}
        animate={{ opacity: 1 }}
        transition={{ duration: 0.72, delay: 1.2, ease: EASE_REVEAL }}
        aria-hidden
      >
        <span className="scroll-hint-label">DESCENT</span>
        <ChevronDown size={16} className="scroll-hint-chevron" />
      </motion.div>
    </section>
  );
}

function Ship() {
  const reduce = useIsReduced();
  const facts = [
    { label: "Length", value: "62 m" },
    { label: "Gross tonnage", value: "812 tons" },
    { label: "Launched", value: "3 May 1899" },
    { label: "Shipyard", value: "Ferguson & Aird, Port Glasgow" },
    { label: "Engine", value: "Single triple-expansion steam engine" },
    { label: "Boilers", value: "Two coal-fired" },
    { label: "Service speed", value: "About 10 knots" },
    { label: "Master", value: "Captain William Renshaw" }
  ];

  const manifest = [
    "Steam coal - 60 tons",
    "Wool bales - 120 bales",
    "Sawn red cedar - 30 lengths",
    "Royal Mail - 14 canvas sacks",
    "Tinned provisions - 40 cases",
    "Tallow - 22 barrels",
    "Ironmongery - 8 crates",
    "Rum - 6 hogsheads",
    "Crated pianos - 3",
    "Fencing wire - 15 bundles"
  ];

  return (
    <section id="ship" className="scene ship-scene">
      <div className="hud">
        <Timecode label="Scene 02 / 08" />
        <Timecode label="00:00:18" />
      </div>
      <div className="scene-grid">
        <div className="scene-main">
          <SceneLabel number="02" intent="The vessel and its cargo." />
          <Reveal>
            <h2 className="scene-title">THE SHIP</h2>
            <p className="lead">
              The SS Coralyn was a 62-metre iron-hulled coastal steamer of 812 gross tons.
            </p>
            <p className="body">
              She was launched in 1899 at the Ferguson and Aird shipyard at Port Glasgow, on the Clyde, and delivered later that year to her owners, the Ardmore Coastal Line of Sydney. She was driven by a single triple-expansion steam engine fed by two coal-fired boilers, and made a service speed of about ten knots. She carried general cargo and up to twelve passengers, and worked the coastal run between Sydney and Newcastle, taking coal south and general goods north. Her master on her final voyage was Captain William Renshaw, who had commanded her for six years.
            </p>
          </Reveal>
          <Reveal delay={0.15}>
            <div className="facts-grid">
              {facts.map((f) => (
                <div key={f.label} className="fact">
                  <span className="fact-label">{f.label}</span>
                  <span className="fact-value">{f.value}</span>
                </div>
              ))}
            </div>
          </Reveal>
        </div>
        <Reveal delay={0.25} className="scene-side">
          <div className="ship-silhouette" aria-hidden>
            <Ship size={120} strokeWidth={0.5} />
          </div>
          <div className="manifest">
            <h3 className="manifest-title">FINAL MANIFEST</h3>
            <ul className="manifest-list">
              {manifest.map((item) => (
                <li key={item} className="manifest-item">
                  <span className="manifest-bullet" />
                  {item}
                </li>
              ))}
            </ul>
          </div>
        </Reveal>
      </div>
    </section>
  );
}

function Night() {
  const reduce = useIsReduced();
  const lost = [
    { name: "James Corradine", role: "Second engineer, who stayed below trying to restart the engine." },
    { name: "Patrick Hoyle", role: "Fireman." },
    { name: "Thomas Byrne", role: "Able seaman." },
    { name: "Eleanor Vance", role: "Passenger, travelling to Newcastle." },
    { name: "Albert Sturrock", role: "Passenger." }
  ];

  return (
    <section id="night" className="scene night-scene">
      <div className="hud">
        <Timecode label="Scene 03 / 08" />
        <Timecode label="00:00:42" />
      </div>
      <div className="scene-grid">
        <div className="scene-main">
          <SceneLabel number="03" intent="The gale, the reef, and the five lost." />
          <Reveal>
            <h2 className="scene-title">THE NIGHT OF THE SINKING</h2>
          </Reveal>
          <Reveal delay={0.1}>
            <p className="lead">
              On the night of 14 August 1908 the Coralyn was on her regular northbound run to Newcastle when she was caught by a severe southerly gale off Port Camden Heads.
            </p>
            <p className="body">
              In heavy seas her engine failed, and without power she was driven onto the reef that lies off the Heads, now marked on charts as Coralyn Reef. She struck at about 11:40 pm, held on the reef for a short time, then slipped off it and foundered before half past midnight - down in under an hour. Of the twenty-six people aboard that night, eighteen crew and eight passengers, twenty-one got away in two of the ship's boats and were picked up at first light. Five were lost.
            </p>
          </Reveal>
        </div>
        <Reveal delay={0.2} className="scene-side">
          <div className="lost-panel">
            <h3 className="lost-title">THE FIVE LOST</h3>
            <ul className="lost-list">
              {lost.map((p) => (
                <li key={p.name} className="lost-person">
                  <span className="lost-name">{p.name}</span>
                  <span className="lost-role">{p.role}</span>
                </li>
              ))}
            </ul>
            <p className="lost-note">Named plainly and with respect.</p>
          </div>
        </Reveal>
      </div>
      <div className="storm-layer" aria-hidden>
        <Wind size={320} strokeWidth={0.5} />
      </div>
    </section>
  );
}

function DepthBand({ depth, title, children, opacity }: { depth: string; title: string; children: React.ReactNode; opacity: any }) {
  const reduce = useIsReduced();
  return (
    <motion.div className="depth-band" style={reduce ? {} : { opacity }}>
      <div className="depth-band-inner">
        <span className="depth-band-number">{depth}</span>
        <h3 className="depth-band-title">{title}</h3>
        <p className="depth-band-body">{children}</p>
      </div>
    </motion.div>
  );
}

function Descent() {
  const reduce = useIsReduced();
  const ref = useRef<HTMLElement>(null);
  const { scrollYProgress } = useScroll({ target: ref, offset: ["start start", "end end"] });

  const depth = useTransform(scrollYProgress, [0, 1], [0, 40]);
  const depthText = useTransform(depth, (v) => `${Math.round(v)}m`);

  const bowOpacity = useTransform(scrollYProgress, [0, 0.22, 0.32], [1, 1, 0]);
  const boilersOpacity = useTransform(scrollYProgress, [0.22, 0.32, 0.56, 0.66], [0, 1, 1, 0]);
  const debrisOpacity = useTransform(scrollYProgress, [0.56, 0.66, 1], [0, 1, 1]);

  if (reduce) {
    return (
      <section id="dive" className="scene descent-scene-static">
        <div className="hud">
          <Timecode label="Scene 04 / 08" />
          <Timecode label="00:01:10" />
        </div>
        <SceneLabel number="04" intent="The three depth bands of the wreck." />
        <h2 className="scene-title">THE DESCENT</h2>
        <div className="depth-bands-static">
          <div className="depth-band-static">
            <span className="depth-band-number">28 m</span>
            <h3 className="depth-band-title">The bow</h3>
            <p>The most intact part of the wreck. The bow section stands clear of the sand and is where the bell was found.</p>
          </div>
          <div className="depth-band-static">
            <span className="depth-band-number">34 m</span>
            <h3 className="depth-band-title">The boilers</h3>
            <p>The two boilers and the engine are the largest structures on the wreck, in the midships section.</p>
          </div>
          <div className="depth-band-static">
            <span className="depth-band-number">40 m</span>
            <h3 className="depth-band-title">The debris field</h3>
            <p>The stern and the scattered cargo lie in a debris field on the sand at the wreck's deepest point, where the pocket watch and the dinner plate were recovered.</p>
          </div>
        </div>
        <p className="body dive-note">
          Visibility is typically 8 to 15 metres and is usually best in autumn. The water is cold below the bow and there can be surge across the debris field. This is a deep dive for experienced, appropriately certified divers.
        </p>
      </section>
    );
  }

  return (
    <section id="dive" ref={ref} className="descent-scene">
      <div className="sticky-depth">
        <div className="hud">
          <Timecode label="Scene 04 / 08" />
          <Timecode label="00:01:10" />
        </div>
        <div className="depth-hud">
          <span className="depth-hud-label">DEPTH</span>
          <motion.span className="depth-hud-value">{depthText}</motion.span>
        </div>

        <DepthBand depth="28 m" title="The bow" opacity={bowOpacity}>
          The most intact part of the wreck. The bow section stands clear of the sand and is where the bell was found.
        </DepthBand>
        <DepthBand depth="34 m" title="The boilers" opacity={boilersOpacity}>
          The two boilers and the engine are the largest structures on the wreck, in the midships section.
        </DepthBand>
        <DepthBand depth="40 m" title="The debris field" opacity={debrisOpacity}>
          The stern and the scattered cargo lie in a debris field on the sand at the wreck's deepest point, where the pocket watch and the dinner plate were recovered.
        </DepthBand>

        <div className="dive-context">
          <p className="body">
            Visibility is typically 8 to 15 metres and is usually best in autumn. The water is cold below the bow and there can be surge across the debris field. This is a deep dive for experienced, appropriately certified divers.
          </p>
        </div>
      </div>
    </section>
  );
}

function ArtifactIcon({ type }: { type: string }) {
  const stroke = "var(--color-accent)";
  const fill = "none";
  switch (type) {
    case "telegraph":
      return (
        <svg viewBox="0 0 48 48" fill={fill} stroke={stroke} strokeWidth="1.5" className="artifact-icon">
          <circle cx="24" cy="24" r="18" />
          <line x1="24" y1="12" x2="24" y2="30" />
          <circle cx="24" cy="24" r="4" />
          <line x1="18" y1="18" x2="30" y2="18" />
        </svg>
      );
    case "watch":
      return <Watch size={48} strokeWidth={1.5} className="artifact-icon" />;
    case "bell":
      return <Bell size={48} strokeWidth={1.5} className="artifact-icon" />;
    case "compass":
      return <Compass size={48} strokeWidth={1.5} className="artifact-icon" />;
    case "plate":
      return (
        <svg viewBox="0 0 48 48" fill={fill} stroke={stroke} strokeWidth="1.5" className="artifact-icon">
          <ellipse cx="24" cy="24" rx="20" ry="8" />
          <ellipse cx="24" cy="22" rx="20" ry="8" />
          <path d="M16 22c0-4 3.5-6 8-6s8 2 8 6" />
        </svg>
      );
    case "whistle":
      return (
        <svg viewBox="0 0 48 48" fill={fill} stroke={stroke} strokeWidth="1.5" className="artifact-icon">
          <path d="M8 20h20c6 0 10 4 10 9s-4 9-10 9H8V20z" />
          <rect x="28" y="16" width="8" height="6" />
          <line x1="8" y1="20" x2="8" y2="38" />
        </svg>
      );
    default:
      return <Anchor size={48} strokeWidth={1.5} className="artifact-icon" />;
  }
}

function Artifacts() {
  const artifacts = [
    { id: "telegraph", year: "1957", title: "The engine-room telegraph", text: "A brass ship's telegraph recovered from the engine room, its handle still set to 'Stand By' - the last order Captain Renshaw is thought to have rung down before the engine failed. It is the museum's centrepiece." },
    { id: "watch", year: "1961", title: "A passenger's pocket watch", text: "A gold half-hunter pocket watch, its hands stopped at 11:47, a few minutes after the Coralyn struck the reef. It was found in the debris field and has never been matched to a name." },
    { id: "bell", year: "1984", title: "The ship's bell", text: "The Coralyn's bell, cast with 'CORALYN 1899' around its rim, recovered from the sand near the bow. It is rung once each year on the anniversary of the loss." },
    { id: "compass", year: "1969", title: "The binnacle compass", text: "The ship's steering compass and its brass binnacle, recovered from the wreck of the bridge. The card is gone but the gimbals still swing freely." },
    { id: "plate", year: "1972", title: "An Ardmore Line dinner plate", text: "A white porcelain plate bearing the blue crest of the Ardmore Coastal Line, recovered intact from the debris field - one of a service that went down with the ship." },
    { id: "whistle", year: "1998", title: "The steam whistle", text: "The Coralyn's brass steam whistle, recovered from the after funnel stay. Divers who have handled it say it is the piece that most makes the ship feel real." }
  ];

  return (
    <section id="artifacts" className="scene artifacts-scene">
      <div className="hud">
        <Timecode label="Scene 05 / 08" />
        <Timecode label="00:02:14" />
      </div>
      <SceneLabel number="05" intent="Six items recovered, held by the museum." />
      <Reveal>
        <h2 className="scene-title">RECOVERED</h2>
      </Reveal>
      <div className="artifacts-grid">
        {artifacts.map((a, i) => (
          <Reveal key={a.id} delay={i * 0.08}>
            <article className="artifact-card">
              <div className="artifact-icon-wrap">
                <ArtifactIcon type={a.id} />
              </div>
              <span className="artifact-year">{a.year}</span>
              <h3 className="artifact-title">{a.title}</h3>
              <p className="artifact-text">{a.text}</p>
            </article>
          </Reveal>
        ))}
      </div>
    </section>
  );
}

function Timeline() {
  const events = [
    { date: "3 May 1899", text: "Launched at the Ferguson and Aird shipyard, Port Glasgow." },
    { date: "September 1899", text: "Arrived in Sydney and entered the Ardmore Coastal Line's Sydney to Newcastle run." },
    { date: "1904", text: "Refitted with two new boilers after a boiler-room fire off Broken Bay." },
    { date: "14 August 1908", text: "Foundered on Coralyn Reef off Port Camden Heads; five lives lost." },
    { date: "November 1908", text: "A Court of Marine Inquiry found the loss was caused by engine failure in heavy weather and laid no blame on Captain Renshaw or his crew." },
    { date: "1953", text: "The wreck was relocated by local fishermen whose nets snagged on it, and dived soon after." },
    { date: "1976", text: "Declared a protected historic shipwreck under the Historic Shipwrecks Act." },
    { date: "2004", text: "A public mooring was installed over the wreck and the Port Camden Maritime Museum became its custodian." }
  ];

  return (
    <section id="timeline" className="scene timeline-scene">
      <div className="hud">
        <Timecode label="Scene 06 / 08" />
        <Timecode label="00:02:56" />
      </div>
      <SceneLabel number="06" intent="From launch to protection." />
      <Reveal>
        <h2 className="scene-title">TIMELINE</h2>
      </Reveal>
      <div className="timeline">
        {events.map((e, i) => (
          <Reveal key={e.date} delay={i * 0.06} className="timeline-event">
            <span className="timeline-date">{e.date}</span>
            <span className="timeline-line" aria-hidden />
            <span className="timeline-text">{e.text}</span>
          </Reveal>
        ))}
      </div>
    </section>
  );
}

function DiveRules() {
  const operators = ["Camden Heads Dive Charters", "Southern Reef Diving Company"];

  return (
    <section id="rules" className="scene rules-scene">
      <div className="hud">
        <Timecode label="Scene 07 / 08" />
        <Timecode label="00:03:38" />
      </div>
      <div className="scene-grid">
        <div className="scene-main">
          <SceneLabel number="07" intent="The wreck is protected." />
          <Reveal>
            <h2 className="scene-title">DIVE ACCESS</h2>
          </Reveal>
          <Reveal delay={0.1}>
            <div className="rules-alert">
              <AlertTriangle size={20} />
              <span>The wreck is a protected historic shipwreck. These rules are not optional.</span>
            </div>
          </Reveal>
          <Reveal delay={0.15}>
            <ul className="rules-list">
              <li>A permit is required to dive the wreck. Apply through Heritage NSW before you dive.</li>
              <li>Do not anchor on or near the wreck. Use the public mooring installed over the site.</li>
              <li>Do not remove, disturb, or damage anything on the wreck. Everything on it is protected, down to loose fittings in the sand.</li>
              <li>Penalties for interfering with a protected historic shipwreck are severe and include substantial fines.</li>
            </ul>
          </Reveal>
        </div>
        <Reveal delay={0.25} className="scene-side">
          <div className="operators-panel">
            <h3 className="operators-title">LICENSED CHARTER OPERATORS</h3>
            <ul className="operators-list">
              {operators.map((op) => (
                <li key={op} className="operator">{op}</li>
              ))}
            </ul>
            <a
              className="button"
              href="https://www.heritage.nsw.gov.au/"
              target="_blank"
              rel="noreferrer"
            >
              Apply for a permit <ExternalLink size={14} />
            </a>
          </div>
        </Reveal>
      </div>
    </section>
  );
}

function Visit() {
  return (
    <section id="visit" className="scene visit-scene">
      <div className="hud">
        <Timecode label="Scene 08 / 08" />
        <Timecode label="00:04:20" />
      </div>
      <SceneLabel number="08" intent="Come to the museum, or ask for a school pack." />
      <div className="scene-grid">
        <div className="scene-main">
          <Reveal>
            <h2 className="scene-title">PLAN YOUR VISIT</h2>
          </Reveal>
          <Reveal delay={0.1}>
            <div className="contact-block">
              <div className="contact-row">
                <MapPin size={18} />
                <span>3 Wharf Road, Port Camden NSW 2445</span>
              </div>
              <div className="contact-row">
                <Clock size={18} />
                <span>Open daily 10 am - 4 pm, closed Christmas Day</span>
              </div>
              <div className="contact-row">
                <Phone size={18} />
                <span>(02) 6555 0192</span>
              </div>
              <div className="contact-row">
                <Mail size={18} />
                <a href="mailto:visit@portcamdenmaritime.org.au">visit@portcamdenmaritime.org.au</a>
              </div>
            </div>
          </Reveal>
        </div>
        <Reveal delay={0.2} className="scene-side">
          <div className="school-panel">
            <h3 className="school-title">SCHOOL GROUP PACK</h3>
            <p className="body">
              The museum runs school programs on the Coralyn for upper-primary and secondary classes.
            </p>
            <p className="body">
              To request a school group pack, email the education officer, Harriet Doyle.
            </p>
            <a className="button" href="mailto:schools@portcamdenmaritime.org.au">
              Email Harriet Doyle <Mail size={14} />
            </a>
          </div>
        </Reveal>
      </div>
      <footer className="site-footer">
        <Timecode label="Port Camden Maritime Museum" />
        <Timecode label="The Wreck of the Coralyn" />
      </footer>
    </section>
  );
}

export default function App() {
  const reduce = useReducedMotion();
  return (
    <ReducedMotionContext.Provider value={reduce ?? false}>
      <div className="site">
        <GlobalBackground />
        <MarineSnow />
        <CustomCursor />
        <Navigation />
        <main>
          <Hero />
          <Ship />
          <Night />
          <Descent />
          <Artifacts />
          <Timeline />
          <DiveRules />
          <Visit />
        </main>
      </div>
    </ReducedMotionContext.Provider>
  );
}
```

```css theme.css
@import "tailwindcss";

@theme {
  --color-ink: #05060a;
  --color-ink-2: #0b0d14;
  --color-haze: #161923;
  --color-text: #f4f5f7;
  --color-muted: #808799;
  --color-accent: #4ce0ff;
  --font-display: "Space Grotesk", sans-serif;
  --font-body: "IBM Plex Sans", sans-serif;
  --font-mono: "IBM Plex Mono", monospace;
  --ease-reveal: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-scene: cubic-bezier(0.7, 0, 0.2, 1);
}

:root {
  --section-pad: clamp(1.5rem, 4vw, 3.5rem);
  --scene-pad-y: 12vh;
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--color-ink);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  line-height: 1.6;
  overflow-x: hidden;
}

.site {
  position: relative;
  width: 100%;
  min-height: 100dvh;
}

a {
  color: var(--color-accent);
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

button {
  font-family: inherit;
}

/* Global background - the descent from surface light to dark water */
.global-background {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: var(--color-ink);
}

.surface-light {
  position: absolute;
  inset: -30% -30% 0 -30%;
  background: radial-gradient(70% 50% at 50% 0%,
    color-mix(in oklab, var(--color-accent) 24%, transparent),
    transparent 70%);
  filter: blur(60px);
  transform: translateZ(0);
}

.depth-veil {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
    transparent 0%,
    color-mix(in oklab, var(--color-ink-2) 40%, transparent) 50%,
    var(--color-ink-2) 100%);
}

/* Marine snow canvas */
.marine-snow {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  width: 100%;
  height: 100%;
  opacity: 0.55;
}

/* Custom cursor */
.custom-cursor {
  position: fixed;
  top: 0;
  left: 0;
  width: 12px;
  height: 12px;
  border-radius: 999px;
  background: var(--color-accent);
  mix-blend-mode: screen;
  pointer-events: none;
  z-index: 9999;
  transform: translate(-50%, -50%);
  box-shadow: 0 0 18px 4px color-mix(in oklab, var(--color-accent) 45%, transparent);
}

/* Navigation */
.main-nav {
  position: fixed;
  top: 1.5rem;
  right: var(--section-pad);
  z-index: 100;
}

.nav-toggle {
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-ink-2);
  border: 1px solid rgb(244 245 247 / 0.08);
  color: var(--color-text);
  border-radius: 0;
  cursor: pointer;
}

.nav-panel {
  position: absolute;
  top: 52px;
  right: 0;
  width: 260px;
  background: var(--color-ink-2);
  border: 1px solid rgb(244 245 247 / 0.08);
  padding: 1rem;
  opacity: 0;
  pointer-events: none;
  transform: translateY(-8px);
  transition: opacity 0.24s var(--ease-reveal), transform 0.24s var(--ease-reveal);
}

.nav-panel.open {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0);
}

.nav-link {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  padding: 0.75rem 0;
  border-bottom: 1px solid rgb(244 245 247 / 0.06);
  color: var(--color-text);
  text-decoration: none;
}

.nav-link:hover {
  color: var(--color-accent);
  text-decoration: none;
}

.nav-number {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.24em;
  color: var(--color-accent);
}

.nav-name {
  font-family: var(--font-body);
  font-size: 0.9375rem;
}

/* HUD */
.hud {
  position: relative;
  z-index: 2;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-bottom: 1rem;
  border-bottom: 1px solid rgb(244 245 247 / 0.08);
  margin-bottom: 2rem;
}

.timecode {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.24em;
  color: var(--color-accent);
}

.scene-label {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  margin-bottom: 1.5rem;
}

.scene-intent {
  font-family: var(--font-body);
  font-size: 0.875rem;
  color: var(--color-muted);
}

/* Scenes */
.scene {
  position: relative;
  z-index: 2;
  min-height: 100dvh;
  padding: var(--scene-pad-y) var(--section-pad);
  overflow: hidden;
}

.scene-grid {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: 1fr;
  gap: 3rem;
  max-width: 1400px;
}

@media (min-width: 900px) {
  .scene-grid {
    grid-template-columns: 7fr 5fr;
    gap: 4rem;
  }
}

.scene-main {
  max-width: 66ch;
}

.scene-side {
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

.scene-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(2.5rem, 6vw, 5rem);
  line-height: 0.88;
  letter-spacing: -0.04em;
  margin: 0 0 1.5rem 0;
}

.lead {
  font-size: 1.5rem;
  line-height: 1.35;
  color: var(--color-text);
  max-width: 40ch;
  margin: 0 0 1.5rem 0;
}

.body {
  font-size: 1.0625rem;
  line-height: 1.6;
  color: var(--color-muted);
  max-width: 62ch;
  margin: 0 0 1rem 0;
}

/* Hero */
.hero-scene {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.hero-content {
  position: relative;
  z-index: 2;
  margin-top: 8vh;
}

.display {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(4rem, 14vw, 16rem);
  line-height: 0.88;
  letter-spacing: -0.04em;
  margin: 0;
}

.word {
  display: block;
  overflow: hidden;
}

.word-inner {
  display: block;
}

.scroll-hint {
  position: relative;
  z-index: 2;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  color: var(--color-muted);
  margin-top: 2rem;
}

.scroll-hint-label {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.24em;
}

.scroll-hint-chevron {
  animation: chevron-bob 1.6s var(--ease-scene) infinite;
}

@keyframes chevron-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(6px); }
}

/* Ship scene */
.ship-silhouette {
  color: var(--color-accent);
  opacity: 0.18;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 2rem;
  border: 1px solid rgb(244 245 247 / 0.08);
  background: var(--color-ink-2);
}

.facts-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1rem;
  margin-top: 2.5rem;
}

.fact {
  padding: 1rem;
  border: 1px solid rgb(244 245 247 / 0.08);
  background: var(--color-ink-2);
}

.fact-label {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--color-muted);
  margin-bottom: 0.5rem;
}

.fact-value {
  display: block;
  font-family: var(--font-body);
  font-size: 1.0625rem;
  color: var(--color-text);
}

.manifest {
  padding: 1.5rem;
  border: 1px solid rgb(244 245 247 / 0.08);
  background: var(--color-ink-2);
}

.manifest-title {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.24em;
  color: var(--color-accent);
  margin: 0 0 1rem 0;
}

.manifest-list {
  list-style: none;
  margin: 0;
  padding: 0;
  columns: 1;
}

@media (min-width: 600px) {
  .manifest-list {
    columns: 2;
  }
}

.manifest-item {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  padding: 0.4rem 0;
  font-size: 0.9375rem;
  color: var(--color-muted);
}

.manifest-bullet {
  width: 4px;
  height: 4px;
  background: var(--color-accent);
  border-radius: 999px;
  flex-shrink: 0;
}

/* Night scene */
.night-scene {
  position: relative;
}

.lost-panel {
  padding: 1.5rem;
  border: 1px solid rgb(244 245 247 / 0.08);
  background: var(--color-ink-2);
}

.lost-title {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.24em;
  color: var(--color-accent);
  margin: 0 0 1.25rem 0;
}

.lost-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.lost-person {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.875rem 0;
  border-bottom: 1px solid rgb(244 245 247 / 0.06);
}

.lost-person:last-child {
  border-bottom: none;
}

.lost-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.125rem;
  color: var(--color-text);
}

.lost-role {
  font-size: 0.9375rem;
  color: var(--color-muted);
}

.lost-note {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--color-muted);
  margin-top: 1.5rem;
}

.storm-layer {
  position: absolute;
  right: -5%;
  bottom: 10%;
  color: var(--color-accent);
  opacity: 0.06;
  pointer-events: none;
  z-index: 0;
}

/* Descent scene */
.descent-scene {
  position: relative;
  z-index: 2;
  height: 350vh;
  padding: 0;
}

.sticky-depth {
  position: sticky;
  top: 0;
  height: 100dvh;
  width: 100%;
  padding: var(--scene-pad-y) var(--section-pad);
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: hidden;
}

.depth-hud {
  position: absolute;
  top: var(--scene-pad-y);
  right: var(--section-pad);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.25rem;
}

.depth-hud-label {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.24em;
  color: var(--color-accent);
}

.depth-hud-value {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(3rem, 10vw, 8rem);
  line-height: 0.85;
  color: var(--color-text);
  letter-spacing: -0.04em;
}

.depth-band {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: var(--scene-pad-y) var(--section-pad);
  pointer-events: none;
}

.depth-band-inner {
  max-width: 60ch;
  pointer-events: auto;
}

.depth-band-number {
  display: block;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(3rem, 8vw, 6rem);
  line-height: 0.85;
  color: var(--color-accent);
  letter-spacing: -0.04em;
  margin-bottom: 0.5rem;
}

.depth-band-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(2rem, 5vw, 3.5rem);
  line-height: 0.9;
  color: var(--color-text);
  margin: 0 0 1rem 0;
}

.depth-band-body {
  font-size: 1.125rem;
  line-height: 1.55;
  color: var(--color-muted);
  max-width: 52ch;
}

.dive-context {
  position: absolute;
  bottom: var(--scene-pad-y);
  left: var(--section-pad);
  max-width: 55ch;
  z-index: 3;
}

.dive-context .body {
  font-size: 0.9375rem;
}

/* Static descent fallback */
.descent-scene-static {
  min-height: auto;
}

.depth-bands-static {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.5rem;
  margin: 2rem 0;
}

@media (min-width: 768px) {
  .depth-bands-static {
    grid-template-columns: repeat(3, 1fr);
  }
}

.depth-band-static {
  padding: 1.5rem;
  border: 1px solid rgb(244 245 247 / 0.08);
  background: var(--color-ink-2);
}

.depth-band-static .depth-band-number {
  font-size: 2.5rem;
}

.depth-band-static .depth-band-title {
  font-size: 1.5rem;
}

.depth-band-static p {
  color: var(--color-muted);
  font-size: 1rem;
  line-height: 1.55;
}

.dive-note {
  margin-top: 1rem;
}

/* Artifacts */
.artifacts-scene {
  min-height: auto;
  padding-bottom: 10vh;
}

.artifacts-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.5rem;
  margin-top: 3rem;
}

@media (min-width: 640px) {
  .artifacts-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1100px) {
  .artifacts-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

.artifact-card {
  padding: 1.5rem;
  border: 1px solid rgb(244 245 247 / 0.08);
  background: var(--color-ink-2);
  display: flex;
  flex-direction: column;
  gap: 1rem;
  transition: border-color 0.24s var(--ease-reveal);
}

.artifact-card:hover {
  border-color: color-mix(in oklab, var(--color-accent) 40%, transparent);
}

.artifact-icon-wrap {
  color: var(--color-accent);
  opacity: 0.9;
}

.artifact-icon {
  width: 40px;
  height: 40px;
  stroke: var(--color-accent);
}

.artifact-year {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.24em;
  color: var(--color-accent);
}

.artifact-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.25rem;
  line-height: 1.1;
  color: var(--color-text);
  margin: 0;
}

.artifact-text {
  font-size: 0.9375rem;
  line-height: 1.55;
  color: var(--color-muted);
  margin: 0;
}

/* Timeline */
.timeline-scene {
  min-height: auto;
  padding-bottom: 10vh;
}

.timeline {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0;
  margin-top: 3rem;
  max-width: 900px;
  padding-left: 1rem;
  border-left: 1px solid rgb(244 245 247 / 0.08);
}

.timeline-event {
  position: relative;
  display: grid;
  grid-template-columns: 140px 1fr;
  gap: 1.5rem;
  padding: 1.25rem 0 1.25rem 1.5rem;
}

@media (max-width: 600px) {
  .timeline-event {
    grid-template-columns: 1fr;
    gap: 0.5rem;
  }
}

.timeline-date {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--color-accent);
  white-space: nowrap;
}

.timeline-text {
  font-size: 1.0625rem;
  color: var(--color-muted);
  line-height: 1.55;
}

.timeline-event::before {
  content: "";
  position: absolute;
  left: -5px;
  top: 1.65rem;
  width: 9px;
  height: 9px;
  border-radius: 999px;
  background: var(--color-accent);
  box-shadow: 0 0 12px 2px color-mix(in oklab, var(--color-accent) 45%, transparent);
}

/* Dive rules */
.rules-alert {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 1rem 1.25rem;
  border: 1px solid color-mix(in oklab, var(--color-accent) 40%, transparent);
  background: color-mix(in oklab, var(--color-accent) 8%, transparent);
  color: var(--color-text);
  font-size: 0.9375rem;
  margin-bottom: 1.5rem;
}

.rules-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.rules-list li {
  position: relative;
  padding-left: 1.5rem;
  font-size: 1.0625rem;
  color: var(--color-muted);
  line-height: 1.55;
}

.rules-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.55rem;
  width: 6px;
  height: 6px;
  background: var(--color-accent);
  border-radius: 999px;
}

.operators-panel {
  padding: 1.5rem;
  border: 1px solid rgb(244 245 247 / 0.08);
  background: var(--color-ink-2);
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.operators-title {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.24em;
  color: var(--color-accent);
  margin: 0;
}

.operators-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.operator {
  padding: 0.75rem 0;
  border-bottom: 1px solid rgb(244 245 247 / 0.06);
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--color-text);
}

.operator:last-child {
  border-bottom: none;
}

.button {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.875rem 1.25rem;
  background: var(--color-accent);
  color: var(--color-ink);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-weight: 500;
  text-decoration: none;
  border-radius: 0;
  transition: transform 0.24s var(--ease-reveal), box-shadow 0.24s var(--ease-reveal);
}

.button:hover {
  text-decoration: none;
  transform: translateY(-2px);
  box-shadow: 0 0 24px 4px color-mix(in oklab, var(--color-accent) 35%, transparent);
}

/* Visit */
.contact-block {
  display: flex;
  flex-direction: column;
  gap: 0.875rem;
  margin-top: 1rem;
}

.contact-row {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  color: var(--color-muted);
  font-size: 1.0625rem;
}

.contact-row a {
  color: var(--color-accent);
}

.school-panel {
  padding: 1.5rem;
  border: 1px solid rgb(244 245 247 / 0.08);
  background: var(--color-ink-2);
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.school-title {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.24em;
  color: var(--color-accent);
  margin: 0;
}

.site-footer {
  position: relative;
  z-index: 2;
  display: flex;
  justify-content: space-between;
  margin-top: 12vh;
  padding-top: 1.5rem;
  border-top: 1px solid rgb(244 245 247 / 0.08);
}

/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  .scroll-hint-chevron {
    animation: none;
  }

  .marine-snow {
    display: none;
  }

  .custom-cursor {
    display: none;
  }

  .surface-light {
    opacity: 0.45;
    filter: blur(24px);
  }

  .depth-veil {
    opacity: 0.25;
  }
}
```