Halcyon. Hospitality landing page.

A warm, editorial luxury-hospitality homepage. A pinned hero paints a layered procedural Amalfi landscape (sky, sun, sea, a cliff village of 120 houses, mist and bougainvillea) that parallaxes by depth and shifts from golden hour to dusk as you scroll, with a live clock and village lights coming on. A booking bar floats at the bottom of the first screen and docks under the condensing nav; then clip-path image reveals, a pinned horizontal gallery of four houses, hover-zoom room cards, an interactive rituals list, a crossfading guest book, illustrated maps whose coastlines draw in and pins drop, and a newsletter footer. Large serif display, sand / ink / terracotta, no images or web fonts.

  • Time-of-day hero
  • Layered parallax
  • Docking booking bar
  • Clip-path reveals
  • Pinned horizontal gallery
  • Testimonial crossfade
  • Map pins draw in
Full screen

The preview is interactive — scroll inside it to run the animations.

The whole page. One file, nothing left out.

1,101 lines · 74.0 KB · imports React only. This is the exact file behind the preview above — every section, interaction and style.

  1. 01Save it as halcyon-hotels.tsx in any React 18+ project.
  2. 02Render <Page /> on a route of its own — it brings its own nav and footer.
  3. 03Swap the copy and tokens. Keep the DESIGN.md next to it so your AI agent follows the system.
halcyon-hotels.tsx
1/**
2 * Halcyon — boutique hotels on the Amalfi coast and in Kyoto.
3 *
4 * Demo images: /img/halcyon/{positano-sunset,pool-lemons,suite,ryokan,courtyard-pool,journals}.webp (each with
5 * -900 and -480 variants), included in the repo's public/img/halcyon — copy them next to your page or swap in your own.
6 *
7 * A warm, editorial luxury-hospitality homepage with real photography
8 * and no web fonts.
9 *
10 * Motion (all scroll effects are transform/opacity driven by one rAF loop
11 * that writes a `--p` progress variable onto `[data-hal-p]` elements):
12 * - Pinned hero: the Positano sunset photograph slowly pushes in and
13 * darkens to dusk as you scroll.
14 * - Booking bar: sticky at the bottom of the first screen, then docks
15 * under the condensed nav once you scroll past the hero.
16 * - Clip-path image reveals, a pinned horizontal "houses" gallery,
17 * hover-zoom room cards, an editorial rituals list with crossfading
18 * frame, an auto-advancing testimonial crossfade, map pins that drop
19 * in with routes drawing, and a newsletter form in the footer.
20 *
21 * Reduced motion: parallax, pinning and autoplay are removed; the gallery
22 * becomes a native swipe row; state changes stay instant.
23 * SSR-safe: no window/document access during render.
24 * Accessibility: real buttons/inputs/labels, visible focus rings,
25 * aria-live for the booking result and testimonial, AA contrast.
26 */
27import { memo, useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
28
29/* ------------------------------------------------------------------ */
30/* Photography (pre-rendered, see /img/lux) */
31/* ------------------------------------------------------------------ */
32const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
33type SceneKind = "positano" | "ravello" | "gion" | "arashiyama" | "roomSea" | "roomKyoto" | "pool" | "boat" | "tea" | "lantern";
34
35type Ph = { f: string; alt?: string; pos: string; zoom?: number };
36const PHOTO: Record<SceneKind, Ph> = {
37 positano: { f: "positano-sunset", alt: "pastel houses stacked up the hillside above the sea at sunset", pos: "50% 55%" },
38 ravello: { f: "pool-lemons", alt: "lemon pergola and infinity pool above the Amalfi coastline", pos: "50% 50%" },
39 gion: { f: "ryokan", alt: "tatami room with shoji screens opening onto a maple garden", pos: "40% 45%" },
40 arashiyama: { f: "ryokan", alt: "red maple and moss garden seen through an open shoji screen", pos: "78% 30%", zoom: 1.7 },
41 roomSea: { f: "suite", alt: "white bed and sea-view terrace beyond an open arched window", pos: "50% 55%" },
42 roomKyoto: { f: "ryokan", alt: "hinoki tub and tatami floor beside a garden doorway", pos: "40% 95%", zoom: 1.5 },
43 pool: { f: "courtyard-pool", alt: "plunge pool and sun loungers under a lemon tree against stone walls", pos: "50% 60%" },
44 boat: { f: "positano-sunset", alt: "calm Amalfi sea at sunset with a distant headland", pos: "8% 30%", zoom: 1.9 },
45 tea: { f: "ryokan", alt: "tatami room and wooden tub beside a moss garden", pos: "70% 55%", zoom: 1.25 },
46 lantern: { f: "ryokan", alt: "wooden shoji lattice windows of a Kyoto townhouse", pos: "8% 40%", zoom: 1.7 },
47};
48
49/** Responsive photograph from public/img/halcyon (480 / 900 / 1024 px, square). */
50function Photo({ f, alt, className, style, eager = false, sizes }: { f: string; alt: string; className?: string; style?: CSSProperties; eager?: boolean; sizes: string }) {
51 return (
52 <img
53 className={className}
54 src={`/img/halcyon/${f}-900.webp`}
55 srcSet={`/img/halcyon/${f}-480.webp 480w, /img/halcyon/${f}-900.webp 900w, /img/halcyon/${f}.webp 1024w`}
56 sizes={sizes}
57 width={1024}
58 height={1024}
59 alt={alt}
60 loading={eager ? undefined : "lazy"}
61 fetchPriority={eager ? "high" : undefined}
62 decoding="async"
63 style={style}
64 />
65 );
66}
67
68const Scene = memo(function Scene({ kind, label }: { kind: SceneKind; label: string }) {
69 const p = PHOTO[kind];
70 return <Photo f={p.f} alt={label.split(" ").length > 7 ? label : `${label}: ${p.alt}`} className="hal-scene" sizes="(max-width: 700px) 100vw, 40vw" style={{ objectPosition: p.pos, scale: p.zoom, transformOrigin: p.pos }} />;
71});
72
73/* Hero: the Positano photograph; a graded copy fades in as you scroll for dusk. */
74const HeroLandscape = memo(function HeroLandscape() {
75 return (
76 <div className="hal-hero-scene">
77 <Photo f="positano-sunset" alt="Positano terraces at golden hour, pastel houses stacked up the hillside above the Amalfi Coast sea" className="hal-hero-img" eager sizes="100vw" />
78 <Photo f="positano-sunset" alt="" className="hal-hero-img hal-dusk" sizes="100vw" />
79 </div>
80 );
81});
82
83/* ------------------------------------------------------------------ */
84/* Data */
85/* ------------------------------------------------------------------ */
86const HOUSES: { name: string; place: string; scene: SceneKind; rooms: number; since: string; from: string; note: string }[] = [
87 { name: "Casa Halcyon", place: "Positano, Amalfi Coast", scene: "positano", rooms: 24, since: "1962", from: "€690", note: "Fourteen terraces stacked above Spiaggia Grande; the lift opens into a lemon grove." },
88 { name: "Villa Limoni", place: "Ravello, Amalfi Coast", scene: "ravello", rooms: 17, since: "2009", from: "€820", note: "A 13th-century merchant's villa, 365 metres above the sea, with a pergola you can dine under." },
89 { name: "Halcyon Gion", place: "Higashiyama, Kyoto", scene: "gion", rooms: 21, since: "2018", from: "¥96,000", note: "Three restored machiya townhouses joined by a moss courtyard, a minute from Yasaka-jinja." },
90 { name: "Halcyon Arashiyama", place: "Ukyō-ku, Kyoto", scene: "arashiyama", rooms: 24, since: "2023", from: "¥118,000", note: "A riverside ryokan with hinoki baths and a private path into the bamboo before 7 a.m." },
91];
92
93const ROOMS: { name: string; house: string; scene: SceneKind; size: string; feat: string; price: string }[] = [
94 { name: "Sea-view Junior Suite", house: "Casa Halcyon · Positano", scene: "roomSea", size: "42 m²", feat: "Private terrace · rain shower", price: "€740" },
95 { name: "Garden Ryokan Room", house: "Halcyon Arashiyama · Kyoto", scene: "roomKyoto", size: "38 m²", feat: "Hinoki tub · futon or bed", price: "¥118,000" },
96 { name: "Lemon Terrace Suite", house: "Villa Limoni · Ravello", scene: "pool", size: "64 m²", feat: "Plunge pool · pergola", price: "€1,180" },
97];
98
99const RITUALS: { time: string; title: string; where: string; scene: SceneKind }[] = [
100 { time: "06:10", title: "The first boat to Li Galli", where: "Casa Halcyon · with Capitano Enzo", scene: "boat" },
101 { time: "11:00", title: "Lemon harvest on the upper terraces", where: "Villa Limoni · with Signora Aurelia", scene: "ravello" },
102 { time: "16:30", title: "Tea in the moss courtyard", where: "Halcyon Gion · usucha & wagashi", scene: "tea" },
103 { time: "21:00", title: "Lantern walk through Higashiyama", where: "Halcyon Gion · after the crowds", scene: "lantern" },
104];
105
106const QUOTES: { q: string; who: string; stay: string }[] = [
107 { q: "We came for three nights and stayed six. Somebody had pressed our linen with lemon leaves; I still don't know how they knew.", who: "Margot & Lise Verhoeven", stay: "Casa Halcyon · June 2026" },
108 { q: "The bath was filled at the exact moment the bamboo went quiet. Nothing was announced. Everything was anticipated.", who: "Daniel Okafor", stay: "Halcyon Arashiyama · April 2026" },
109 { q: "Dinner under the pergola, the coast turning violet below us — the best table I have sat at in twenty years of travel.", who: "Inés Calderón", stay: "Villa Limoni · September 2025" },
110 { q: "They walked us through Gion at nine, when the lanterns are lit and the streets finally belong to the people who live there.", who: "Henrik & Sofie Lund", stay: "Halcyon Gion · November 2025" },
111];
112
113const DATES = ["Thu 13 May", "Fri 14 May", "Sat 15 May", "Sun 16 May", "Mon 17 May", "Tue 18 May", "Wed 19 May", "Thu 20 May", "Fri 21 May", "Sat 22 May", "Sun 23 May", "Mon 24 May", "Tue 25 May", "Wed 26 May"];
114
115/* ------------------------------------------------------------------ */
116/* Page */
117/* ------------------------------------------------------------------ */
118export function Page() {
119 const rootRef = useRef<HTMLDivElement>(null);
120 const clockRef = useRef<HTMLSpanElement>(null);
121 const [navSmall, setNavSmall] = useState(false);
122 const [docked, setDocked] = useState(false);
123 const [dest, setDest] = useState<"amalfi" | "kyoto">("amalfi");
124 const [arrive, setArrive] = useState(1);
125 const [nights, setNights] = useState(4);
126 const [guests, setGuests] = useState(2);
127 const [datesOpen, setDatesOpen] = useState(false);
128 const [result, setResult] = useState("");
129 const [ritual, setRitual] = useState(0);
130 const [quote, setQuote] = useState(0);
131 const [quotePaused, setQuotePaused] = useState(false);
132 const [email, setEmail] = useState("");
133 const [subscribed, setSubscribed] = useState(false);
134 const [menuOpen, setMenuOpen] = useState(false);
135
136 /* Scroll engine: progress vars, nav + dock state, reveals, gallery sizing. */
137 useEffect(() => {
138 const root = rootRef.current;
139 if (!root) return;
140 const doc = root.ownerDocument;
141 const win = doc.defaultView;
142 if (!win) return;
143 const reduce = win.matchMedia("(prefers-reduced-motion: reduce)").matches;
144 root.classList.add("hal-js");
145
146 // Hosts that put overflow:hidden on BOTH <html> and <body> turn <body> into a
147 // non-scrolling scroll container, which silently breaks position:sticky.
148 // `clip` gives the same visual result without that side effect.
149 let restoreBody: (() => void) | null = null;
150 const fixRaf = win.requestAnimationFrame(() => {
151 const b = doc.body;
152 const bs = win.getComputedStyle(b);
153 const hs = win.getComputedStyle(doc.documentElement);
154 if (hs.overflowX !== "visible" && (bs.overflowX === "hidden" || bs.overflowY === "auto")) {
155 const prev = [b.style.overflowX, b.style.overflowY];
156 b.style.overflowX = "clip";
157 b.style.overflowY = "visible";
158 restoreBody = () => {
159 b.style.overflowX = prev[0];
160 b.style.overflowY = prev[1];
161 };
162 }
163 });
164
165 const progEls = Array.from(root.querySelectorAll<HTMLElement>("[data-hal-p]"));
166 const gallery = root.querySelector<HTMLElement>(".hal-houses");
167 const track = root.querySelector<HTMLElement>(".hal-houses-track");
168 const bar = root.querySelector<HTMLElement>(".hal-book");
169 const cur = root.querySelector<HTMLElement>(".hal-houses-cur");
170 const hero = root.querySelector<HTMLElement>(".hal-hero");
171 let galleryOn = false;
172
173 const measure = () => {
174 galleryOn = !reduce && win.innerWidth > 900;
175 if (gallery && track) {
176 const dist = galleryOn ? Math.max(0, track.scrollWidth - win.innerWidth) : 0;
177 gallery.style.setProperty("--dist", `${dist}px`);
178 gallery.dataset.dist = String(dist);
179 }
180 };
181
182 let raf = 0;
183 let lastSmall = false;
184 let lastDock = false;
185 const update = () => {
186 raf = 0;
187 const vh = win.innerHeight;
188 for (const el of progEls) {
189 const r = el.getBoundingClientRect();
190 const mode = el.dataset.halP;
191 let p = 0;
192 if (mode === "pin") p = (-r.top) / Math.max(1, r.height - vh);
193 else p = (vh - r.top) / (vh + r.height);
194 p = Math.min(1, Math.max(0, p));
195 el.style.setProperty("--p", p.toFixed(4));
196 if (el === gallery && track) {
197 const dist = Number(gallery.dataset.dist || 0);
198 track.style.transform = galleryOn ? `translate3d(${(-p * dist).toFixed(1)}px,0,0)` : "";
199 if (cur) cur.textContent = `0${Math.min(HOUSES.length, 1 + Math.floor(p * HOUSES.length * 0.999))}`;
200 }
201 if (el === hero && clockRef.current) {
202 const mins = Math.round(lerp(17 * 60 + 42, 20 * 60 + 16, Math.min(1, p * 1.25)));
203 clockRef.current.textContent = `${String(Math.floor(mins / 60)).padStart(2, "0")}:${String(mins % 60).padStart(2, "0")}`;
204 el.classList.toggle("is-dusk", p > 0.45);
205 }
206 }
207 const y = win.scrollY || doc.documentElement.scrollTop;
208 const small = y > 24;
209 if (small !== lastSmall) {
210 lastSmall = small;
211 setNavSmall(small);
212 }
213 if (bar) {
214 const top = bar.getBoundingClientRect().top;
215 const dock = top < 120 && y > vh * 0.5;
216 if (dock !== lastDock) {
217 lastDock = dock;
218 setDocked(dock);
219 }
220 }
221 };
222 const onScroll = () => {
223 if (!raf) raf = win.requestAnimationFrame(update);
224 };
225 const onResize = () => {
226 measure();
227 onScroll();
228 };
229 measure();
230 update();
231 win.addEventListener("scroll", onScroll, { passive: true });
232 win.addEventListener("resize", onResize);
233
234 // One-shot reveals.
235 const io = new win.IntersectionObserver(
236 (entries) => {
237 for (const e of entries) {
238 if (e.isIntersecting) {
239 e.target.classList.add("is-in");
240 io.unobserve(e.target);
241 }
242 }
243 },
244 { rootMargin: "0px 0px -12% 0px", threshold: 0.12 },
245 );
246 root.querySelectorAll("[data-hal-rv]").forEach((el) => io.observe(el));
247
248 return () => {
249 win.cancelAnimationFrame(fixRaf);
250 if (raf) win.cancelAnimationFrame(raf);
251 win.removeEventListener("scroll", onScroll);
252 win.removeEventListener("resize", onResize);
253 io.disconnect();
254 restoreBody?.();
255 root.classList.remove("hal-js");
256 };
257 }, []);
258
259 /* Testimonial autoplay (paused on hover/focus, off for reduced motion or hidden tab). */
260 useEffect(() => {
261 const root = rootRef.current;
262 const win = root?.ownerDocument.defaultView;
263 if (!win || quotePaused) return;
264 if (win.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
265 const t = win.setInterval(() => {
266 if (!root!.ownerDocument.hidden) setQuote((q) => (q + 1) % QUOTES.length);
267 }, 6500);
268 return () => win.clearInterval(t);
269 }, [quotePaused]);
270
271 const depart = DATES[Math.min(DATES.length - 1, arrive + nights)] ?? DATES[DATES.length - 1];
272 const nightly = dest === "amalfi" ? 690 : 640;
273 const check = () => {
274 const suites = dest === "amalfi" ? 5 + (arrive % 3) : 3 + (arrive % 4);
275 setResult(
276 `${suites} rooms open ${DATES[arrive].replace(/^\w+ /, "")}–${depart.replace(/^\w+ /, "")} · from €${(nightly + guests * 35).toLocaleString("en-GB")} a night`,
277 );
278 };
279
280 const navLinks = ["Houses", "Rooms", "Rituals", "Journal", "Gift cards"];
281
282 return (
283 <div ref={rootRef} className="hal">
284 <style>{css}</style>
285 <a className="hal-skip" href="#hal-main">Skip to content</a>
286
287 {/* NAV */}
288 <header className={`hal-nav${navSmall ? " is-small" : ""}${menuOpen ? " is-open" : ""}`}>
289 <div className="hal-nav-bar">
290 <a className="hal-logo" href="#hal-top" aria-label="Halcyon — home">
291 <svg viewBox="0 0 28 28" aria-hidden="true" focusable="false"><circle cx="14" cy="14" r="13" fill="none" stroke="currentColor" strokeWidth="1.2" /><path d="M3 16.5 C 8 13.5 11 13.5 14 15 S 20 16.5 25 13.5" fill="none" stroke="currentColor" strokeWidth="1.2" /><circle cx="14" cy="10" r="3.2" fill="currentColor" /></svg>
292 <span>Halcyon</span>
293 </a>
294 <nav className="hal-nav-links" aria-label="Primary">
295 {navLinks.map((l) => (
296 <a key={l} href={`#hal-${l.toLowerCase().replace(/\s/g, "")}`}>{l}</a>
297 ))}
298 </nav>
299 <div className="hal-nav-end">
300 <a className="hal-nav-lang" href="#hal-top" aria-label="Language: English">EN</a>
301 <a className="hal-btn hal-btn-ink hal-btn-sm" href="#hal-book">Reserve</a>
302 <button className="hal-burger" type="button" aria-label={menuOpen ? "Close menu" : "Open menu"} aria-expanded={menuOpen} onClick={() => setMenuOpen((o) => !o)}>
303 <span /><span />
304 </button>
305 </div>
306 </div>
307 <div className="hal-sheet" hidden={!menuOpen}>
308 {navLinks.map((l) => (
309 <a key={l} href={`#hal-${l.toLowerCase().replace(/\s/g, "")}`} onClick={() => setMenuOpen(false)}>{l}</a>
310 ))}
311 </div>
312 </header>
313
314 <main id="hal-main">
315 {/* HERO — pinned, time-of-day */}
316 <section id="hal-top" className="hal-hero" data-hal-p="pin" aria-labelledby="hal-hero-title">
317 <div className="hal-hero-pin">
318 <HeroLandscape />
319 <div className="hal-hero-copy">
320 <p className="hal-eyebrow hal-hero-eyebrow">
321 <span>Positano</span>
322 <i aria-hidden="true" />
323 <span ref={clockRef} className="hal-clock">17:42</span>
324 <i aria-hidden="true" />
325 <span>24°C, sea calm</span>
326 </p>
327 <h1 id="hal-hero-title" className="hal-hero-title">
328 <span className="hal-line"><span>Where the light</span></span>
329 <span className="hal-line"><span><em>lingers</em> longer.</span></span>
330 </h1>
331 <p className="hal-hero-sub">Four small houses on the Amalfi coast and in Kyoto, kept by the same families since 1962. Eighty-six rooms, no lobbies, and a staff who will remember how you take your coffee.</p>
332 </div>
333 <p className="hal-hero-scroll" aria-hidden="true"><span />Scroll for dusk</p>
334 </div>
335 </section>
336
337 {/* BOOKING BAR — floats at the bottom of screen one, docks under the nav */}
338 <div id="hal-book" className={`hal-book${docked ? " is-docked" : ""}`}>
339 <form className="hal-book-card" onSubmit={(e) => { e.preventDefault(); check(); }} aria-label="Check availability">
340 <div className="hal-field hal-field-dest" role="group" aria-labelledby="hal-dest-l">
341 <span className="hal-label" id="hal-dest-l">Destination</span>
342 <div className="hal-seg" role="radiogroup" aria-label="Destination">
343 {(["amalfi", "kyoto"] as const).map((v) => (
344 <button key={v} type="button" role="radio" aria-checked={dest === v} className={dest === v ? "is-on" : ""} onClick={() => { setDest(v); setResult(""); }}>
345 {v === "amalfi" ? "Amalfi Coast" : "Kyoto"}
346 </button>
347 ))}
348 <span className="hal-seg-thumb" style={{ transform: `translateX(${dest === "amalfi" ? 0 : 100}%)` }} aria-hidden="true" />
349 </div>
350 </div>
351 <div className="hal-field hal-field-date">
352 <span className="hal-label" id="hal-arrive-l">Arrive</span>
353 <button type="button" className="hal-date-btn" aria-labelledby="hal-arrive-l" aria-expanded={datesOpen} onClick={() => setDatesOpen((o) => !o)}>
354 {DATES[arrive]} <small>→ {depart}</small>
355 </button>
356 {datesOpen && (
357 <div className="hal-pop" role="listbox" aria-label="Arrival date, May 2027">
358 <p className="hal-pop-title">May 2027</p>
359 <div className="hal-pop-grid">
360 {DATES.slice(0, 12).map((dd, i) => (
361 <button key={dd} type="button" role="option" aria-selected={arrive === i} className={arrive === i ? "is-on" : ""} onClick={() => { setArrive(i); setDatesOpen(false); setResult(""); }}>
362 <small>{dd.slice(0, 3)}</small>
363 {dd.split(" ")[1]}
364 </button>
365 ))}
366 </div>
367 </div>
368 )}
369 </div>
370 <div className="hal-field">
371 <span className="hal-label">Nights</span>
372 <div className="hal-step">
373 <button type="button" aria-label="Fewer nights" onClick={() => setNights((n) => Math.max(1, n - 1))}>−</button>
374 <output aria-live="polite">{nights}</output>
375 <button type="button" aria-label="More nights" onClick={() => setNights((n) => Math.min(10, n + 1))}>+</button>
376 </div>
377 </div>
378 <div className="hal-field">
379 <span className="hal-label">Guests</span>
380 <div className="hal-step">
381 <button type="button" aria-label="Fewer guests" onClick={() => setGuests((n) => Math.max(1, n - 1))}>−</button>
382 <output aria-live="polite">{guests}</output>
383 <button type="button" aria-label="More guests" onClick={() => setGuests((n) => Math.min(6, n + 1))}>+</button>
384 </div>
385 </div>
386 <button className="hal-btn hal-btn-terra hal-book-cta" type="submit">
387 <span className="hal-cta-full">Check availability</span>
388 <span className="hal-cta-short" aria-hidden="true">Search</span>
389 <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 8h10M9 4l4 4-4 4" fill="none" stroke="currentColor" strokeWidth="1.5" /></svg>
390 </button>
391 <p className={`hal-book-result${result ? " is-on" : ""}`} aria-live="polite">{result}</p>
392 </form>
393 </div>
394
395 {/* INTRO */}
396 <section className="hal-intro" aria-labelledby="hal-intro-title">
397 <div className="hal-wrap hal-intro-grid">
398 <div className="hal-intro-text">
399 <p className="hal-eyebrow" data-hal-rv>Est. 1962 · Positano</p>
400 <h2 id="hal-intro-title" className="hal-h2" data-hal-rv>A house is not a hotel until somebody waits up for you.</h2>
401 <p className="hal-lede hal-drop" data-hal-rv>Nonna Teresa Aiello opened eight rooms above Spiaggia Grande with a borrowed espresso machine and a rule that every guest be met at the boat. Sixty-four years later her grandchildren keep four houses on two coasts — and the rule.</p>
402 <p className="hal-body" data-hal-rv>We have never built a lobby. You arrive into a kitchen, a garden, a tea room. Rooms are named, not numbered. In Kyoto we found a family who had been doing the same for nine generations, and asked if we might learn.</p>
403 <dl className="hal-figs" data-hal-rv>
404 <div><dt>Houses</dt><dd>4</dd></div>
405 <div><dt>Rooms & suites</dt><dd>86</dd></div>
406 <div><dt>Guests who return</dt><dd>61%</dd></div>
407 </dl>
408 </div>
409 <div className="hal-intro-media" data-hal-p="view">
410 <figure className="hal-clip hal-clip-a" data-hal-rv>
411 <div className="hal-clip-in"><Scene kind="positano" label="Casa Halcyon's terraces above Positano at golden hour" /></div>
412 <figcaption>Casa Halcyon, from the ferry at 18:20</figcaption>
413 </figure>
414 <figure className="hal-clip hal-clip-b" data-hal-rv>
415 <div className="hal-clip-in"><Scene kind="roomKyoto" label="A tatami room at Halcyon Arashiyama opening onto a maple garden" /></div>
416 <figcaption>Room Kaede, Arashiyama</figcaption>
417 </figure>
418 </div>
419 </div>
420 </section>
421
422 {/* HOUSES — pinned horizontal gallery */}
423 <section id="hal-houses" className="hal-houses" data-hal-p="pin" aria-labelledby="hal-houses-title">
424 <div className="hal-houses-pin">
425 <div className="hal-houses-head hal-wrap">
426 <div>
427 <p className="hal-eyebrow">The houses</p>
428 <h2 id="hal-houses-title" className="hal-h2">Four houses, two coasts.</h2>
429 </div>
430 <div className="hal-houses-meter" aria-hidden="true">
431 <span className="hal-houses-bar"><i /></span>
432 <span className="hal-houses-count"><b className="hal-houses-cur">01</b> — 04</span>
433 </div>
434 </div>
435 <ol className="hal-houses-track">
436 {HOUSES.map((h, i) => (
437 <li key={h.name} className="hal-house">
438 <a href="#hal-rooms" className="hal-house-link" aria-label={`${h.name}, ${h.place} — from ${h.from} a night`}>
439 <div className="hal-house-img"><div className="hal-house-par"><Scene kind={h.scene} label={`${h.name}, ${h.place}`} /></div></div>
440 <div className="hal-house-info">
441 <span className="hal-house-n">0{i + 1}</span>
442 <div>
443 <h3 className="hal-h3">{h.name}</h3>
444 <p className="hal-house-place">{h.place} · since {h.since}</p>
445 <p className="hal-house-note">{h.note}</p>
446 </div>
447 <p className="hal-house-meta"><span>{h.rooms} rooms</span><span>from {h.from}</span></p>
448 </div>
449 </a>
450 </li>
451 ))}
452 </ol>
453 </div>
454 </section>
455
456 {/* ROOMS */}
457 <section id="hal-rooms" className="hal-rooms" aria-labelledby="hal-rooms-title">
458 <div className="hal-wrap">
459 <div className="hal-sec-head">
460 <p className="hal-eyebrow" data-hal-rv>Rooms & suites</p>
461 <h2 id="hal-rooms-title" className="hal-h2" data-hal-rv>Rooms are named, never numbered.</h2>
462 <p className="hal-body hal-sec-sub" data-hal-rv>Every room has a window that frames something worth waking for. Rates include breakfast, the minibar, and the boat or car from wherever you land.</p>
463 </div>
464 <ul className="hal-room-grid">
465 {ROOMS.map((r, i) => (
466 <li key={r.name} className="hal-room" data-hal-rv style={{ "--d": `${i * 90}ms` } as CSSProperties}>
467 <a href="#hal-book" className="hal-room-link">
468 <div className="hal-room-img"><Scene kind={r.scene} label={`${r.name} at ${r.house}`} /></div>
469 <div className="hal-room-body">
470 <p className="hal-room-house">{r.house}</p>
471 <h3 className="hal-h3">{r.name}</h3>
472 <p className="hal-room-feat">{r.size} · {r.feat}</p>
473 <p className="hal-room-foot">
474 <span>from <strong>{r.price}</strong> / night</span>
475 <span className="hal-room-cta">View room <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 8h10M9 4l4 4-4 4" fill="none" stroke="currentColor" strokeWidth="1.5" /></svg></span>
476 </p>
477 </div>
478 </a>
479 </li>
480 ))}
481 </ul>
482 </div>
483 </section>
484
485 {/* RITUALS */}
486 <section id="hal-rituals" className="hal-rituals" aria-labelledby="hal-rituals-title">
487 <div className="hal-wrap hal-rituals-grid">
488 <div className="hal-rituals-frame" aria-hidden="true">
489 {RITUALS.map((r, i) => (
490 <div key={r.title} className={`hal-rituals-img${ritual === i ? " is-on" : ""}`}><Scene kind={r.scene} label={r.title} /></div>
491 ))}
492 <span className="hal-rituals-time">{RITUALS[ritual].time}</span>
493 </div>
494 <div>
495 <p className="hal-eyebrow" data-hal-rv>A day, kept slowly</p>
496 <h2 id="hal-rituals-title" className="hal-h2" data-hal-rv>Small rituals, booked by nobody.</h2>
497 <ol className="hal-rituals-list">
498 {RITUALS.map((r, i) => (
499 <li key={r.title}>
500 <button type="button" className={`hal-ritual${ritual === i ? " is-on" : ""}`} aria-pressed={ritual === i} onMouseEnter={() => setRitual(i)} onFocus={() => setRitual(i)} onClick={() => setRitual(i)}>
501 <span className="hal-ritual-time">{r.time}</span>
502 <span className="hal-ritual-title">{r.title}</span>
503 <span className="hal-ritual-where">{r.where}</span>
504 </button>
505 </li>
506 ))}
507 </ol>
508 </div>
509 </div>
510 </section>
511
512 {/* TESTIMONIALS */}
513 <section className="hal-quotes" aria-labelledby="hal-quotes-title" onMouseEnter={() => setQuotePaused(true)} onMouseLeave={() => setQuotePaused(false)} onFocus={() => setQuotePaused(true)} onBlur={() => setQuotePaused(false)}>
514 <div className="hal-wrap hal-quotes-in">
515 <figure className="hal-quotes-fig" data-hal-p="view">
516 <Photo f="journals" alt="Hand-written family journals and faded photographs spread on terracotta" className="hal-quotes-img" sizes="(max-width: 900px) 90vw, 480px" />
517 </figure>
518 <div className="hal-quotes-body">
519 <h2 id="hal-quotes-title" className="hal-eyebrow">From the guest book</h2>
520 <div className="hal-quote-stack" aria-live="polite">
521 {QUOTES.map((q, i) => (
522 <figure key={q.who} className={`hal-quote${quote === i ? " is-on" : ""}`} aria-hidden={quote !== i}>
523 <blockquote><p>“{q.q}”</p></blockquote>
524 <figcaption><strong>{q.who}</strong><span>{q.stay}</span></figcaption>
525 </figure>
526 ))}
527 </div>
528 <div className="hal-quote-dots">
529 {QUOTES.map((q, i) => (
530 <button key={q.who} type="button" aria-label={`Show quote ${i + 1} of ${QUOTES.length}: ${q.who}`} aria-current={quote === i} className={quote === i ? "is-on" : ""} onClick={() => setQuote(i)}>
531 <i style={{ animationPlayState: quotePaused ? "paused" : "running" }} />
532 </button>
533 ))}
534 </div>
535 </div>
536 </div>
537 </section>
538
539 {/* MAP */}
540 <section id="hal-journal" className="hal-map" aria-labelledby="hal-map-title">
541 <div className="hal-wrap">
542 <div className="hal-sec-head">
543 <p className="hal-eyebrow" data-hal-rv>Getting here</p>
544 <h2 id="hal-map-title" className="hal-h2" data-hal-rv>Two coasts, 9,600 km apart, one way of keeping house.</h2>
545 </div>
546 <div className="hal-map-grid">
547 <figure className="hal-map-card" data-hal-rv>
548 <svg className="hal-map-svg" viewBox="0 0 600 420" role="img" aria-label="Map of the Amalfi Coast with Casa Halcyon in Positano and Villa Limoni in Ravello">
549 <rect width="600" height="420" fill="#E7EEEC" />
550 <path d="M0 120 C 60 130 110 170 150 200 C 190 230 230 226 270 240 C 320 256 350 250 390 238 C 430 226 470 236 520 210 C 550 196 580 170 600 160 L600 0 L0 0Z" fill="#EDE3D2" />
551 <path className="hal-draw" d="M0 120 C 60 130 110 170 150 200 C 190 230 230 226 270 240 C 320 256 350 250 390 238 C 430 226 470 236 520 210 C 550 196 580 170 600 160" fill="none" stroke="#A9502B" strokeWidth="1.5" />
552 <path className="hal-draw hal-draw-route" d="M180 220 C 240 300 380 300 420 240" fill="none" stroke="#2F5866" strokeWidth="1.5" strokeDasharray="4 6" />
553 <ellipse cx="80" cy="330" rx="40" ry="16" fill="#EDE3D2" />
554 <text x="80" y="370" className="hal-map-t hal-map-t-sm" textAnchor="middle">Capri</text>
555 <text x="300" y="380" className="hal-map-t hal-map-t-sea" textAnchor="middle">Golfo di Salerno</text>
556 <text x="300" y="90" className="hal-map-t hal-map-t-sm" textAnchor="middle">Monti Lattari</text>
557 <g className="hal-pin" style={{ "--d": "300ms" } as CSSProperties} transform="translate(180 214)">
558 <path d="M0 0 C -10 -14 -14 -20 -14 -28 A14 14 0 0 1 14 -28 C 14 -20 10 -14 0 0Z" fill="#A9502B" />
559 <circle cy="-28" r="5" fill="#FBF8F3" />
560 </g>
561 <text x="180" y="244" className="hal-map-t" textAnchor="middle">Positano</text>
562 <g className="hal-pin" style={{ "--d": "520ms" } as CSSProperties} transform="translate(420 232)">
563 <path d="M0 0 C -10 -14 -14 -20 -14 -28 A14 14 0 0 1 14 -28 C 14 -20 10 -14 0 0Z" fill="#A9502B" />
564 <circle cy="-28" r="5" fill="#FBF8F3" />
565 </g>
566 <text x="420" y="262" className="hal-map-t" textAnchor="middle">Ravello</text>
567 </svg>
568 <figcaption>
569 <strong>Amalfi Coast</strong>
570 <span>Naples airport → Positano 75 min by car, or 40 min by our boat from Salerno. Positano → Ravello 35 min along the SS163.</span>
571 </figcaption>
572 </figure>
573 <figure className="hal-map-card" data-hal-rv style={{ "--d": "120ms" } as CSSProperties}>
574 <svg className="hal-map-svg" viewBox="0 0 600 420" role="img" aria-label="Map of Kyoto with Halcyon Gion in Higashiyama and Halcyon Arashiyama in the west">
575 <rect width="600" height="420" fill="#EEEADF" />
576 {Array.from({ length: 9 }).map((_, i) => (
577 <line key={`v${i}`} x1={160 + i * 40} y1="40" x2={160 + i * 40} y2="400" stroke="#DCD3C1" strokeWidth="1" />
578 ))}
579 {Array.from({ length: 9 }).map((_, i) => (
580 <line key={`h${i}`} x1="160" y1={40 + i * 45} x2="500" y2={40 + i * 45} stroke="#DCD3C1" strokeWidth="1" />
581 ))}
582 <path d="M0 0 L120 0 C 100 80 90 160 110 240 C 120 300 90 360 60 420 L0 420Z" fill="#DCE2CC" />
583 <path className="hal-draw" d="M430 0 C 440 80 445 160 438 240 C 432 320 440 380 450 420" fill="none" stroke="#7FA3AC" strokeWidth="5" />
584 <path className="hal-draw" d="M70 60 C 90 140 70 220 100 300 C 120 350 140 390 150 420" fill="none" stroke="#7FA3AC" strokeWidth="4" />
585 <path className="hal-draw hal-draw-route" d="M110 200 C 200 250 360 250 470 190" fill="none" stroke="#2F5866" strokeWidth="1.5" strokeDasharray="4 6" />
586 <text x="448" y="400" className="hal-map-t hal-map-t-sea">Kamo-gawa</text>
587 <rect x="300" y="355" width="70" height="16" fill="#D6CDB9" />
588 <text x="335" y="390" className="hal-map-t hal-map-t-sm" textAnchor="middle">Kyoto Station</text>
589 <g className="hal-pin" style={{ "--d": "300ms" } as CSSProperties} transform="translate(110 196)">
590 <path d="M0 0 C -10 -14 -14 -20 -14 -28 A14 14 0 0 1 14 -28 C 14 -20 10 -14 0 0Z" fill="#A9502B" />
591 <circle cy="-28" r="5" fill="#FBF8F3" />
592 </g>
593 <text x="110" y="226" className="hal-map-t" textAnchor="middle">Arashiyama</text>
594 <g className="hal-pin" style={{ "--d": "520ms" } as CSSProperties} transform="translate(470 186)">
595 <path d="M0 0 C -10 -14 -14 -20 -14 -28 A14 14 0 0 1 14 -28 C 14 -20 10 -14 0 0Z" fill="#A9502B" />
596 <circle cy="-28" r="5" fill="#FBF8F3" />
597 </g>
598 <text x="470" y="216" className="hal-map-t" textAnchor="middle">Gion</text>
599 </svg>
600 <figcaption>
601 <strong>Kyoto</strong>
602 <span>Kyoto Station → Gion 18 min, → Arashiyama 25 min. Our car meets every Nozomi; a house-to-house transfer runs twice daily.</span>
603 </figcaption>
604 </figure>
605 </div>
606 </div>
607 </section>
608 </main>
609
610 {/* FOOTER + NEWSLETTER */}
611 <footer className="hal-foot">
612 <div className="hal-wrap">
613 <div className="hal-news">
614 <div>
615 <p className="hal-eyebrow hal-eyebrow-light">Letters from the coast</p>
616 <h2 className="hal-h2 hal-news-title">Four letters a year. Never more.</h2>
617 <p className="hal-news-sub">First word on the lemon harvest, autumn rooms in Kyoto, and the week each spring we open Casa Halcyon early.</p>
618 </div>
619 <form className="hal-news-form" onSubmit={(e) => { e.preventDefault(); if (/\S+@\S+\.\S+/.test(email)) setSubscribed(true); }}>
620 <label className="hal-news-label" htmlFor="hal-email">Email address</label>
621 <div className={`hal-news-row${subscribed ? " is-done" : ""}`}>
622 <input id="hal-email" type="email" required placeholder="you@example.com" autoComplete="email" value={email} onChange={(e) => setEmail(e.target.value)} disabled={subscribed} />
623 <button type="submit" className="hal-btn hal-btn-sand" disabled={subscribed}>{subscribed ? "Subscribed" : "Subscribe"}</button>
624 </div>
625 <p className="hal-news-note" aria-live="polite">{subscribed ? `Grazie — the next letter reaches ${email} in early spring.` : "We never share your address. Unsubscribe in one click."}</p>
626 </form>
627 </div>
628 <div className="hal-foot-cols">
629 <div>
630 <p className="hal-foot-h">Houses</p>
631 <ul>{HOUSES.map((h) => <li key={h.name}><a href="#hal-houses">{h.name}</a></li>)}</ul>
632 </div>
633 <div>
634 <p className="hal-foot-h">Stay</p>
635 <ul>{["Rooms & suites", "Seasonal offers", "Gift cards", "Weddings & buy-outs"].map((x) => <li key={x}><a href="#hal-rooms">{x}</a></li>)}</ul>
636 </div>
637 <div>
638 <p className="hal-foot-h">Halcyon</p>
639 <ul>{["Our story", "Careers", "Press room", "Sustainability report 2025"].map((x) => <li key={x}><a href="#hal-top">{x}</a></li>)}</ul>
640 </div>
641 <div>
642 <p className="hal-foot-h">Reservations</p>
643 <ul>
644 <li><a href="#hal-book">+39 089 875 0162</a></li>
645 <li><a href="#hal-book">stay@halcyon.example</a></li>
646 <li><span>Daily 08:00–22:00 CET</span></li>
647 </ul>
648 </div>
649 </div>
650 <p className="hal-foot-mark" aria-hidden="true">Halcyon</p>
651 <div className="hal-foot-legal">
652 <span>© 2026 Halcyon Case & Ryokan S.r.l. · Via Cristoforo Colombo 41, 84017 Positano (SA)</span>
653 <span><a href="#hal-top">Privacy</a><a href="#hal-top">Cookies</a><a href="#hal-top">Accessibility</a></span>
654 </div>
655 </div>
656 </footer>
657 </div>
658 );
659}
660
661export default Page;
662
663/* ------------------------------------------------------------------ */
664/* Styles */
665/* ------------------------------------------------------------------ */
666const css = String.raw`
667.hal {
668 --hal-sand: #F3ECE2;
669 --hal-paper: #FBF8F3;
670 --hal-ink: #1E1915;
671 --hal-ink-2: #5B5048;
672 --hal-terra: #A9502B;
673 --hal-terra-2: #C8693F;
674 --hal-sea: #2F5866;
675 --hal-night: #191513;
676 --hal-line: rgba(30, 25, 21, 0.14);
677 --hal-serif: ui-serif, "New York", "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
678 --hal-sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif;
679 --fd-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
680 --fd-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
681 --fd-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
682 --fd-dur-1: 120ms;
683 --fd-dur-2: 240ms;
684 --fd-dur-3: 420ms;
685 --fd-dur-4: 700ms;
686 --hal-nav-h: 84px;
687 --hal-nav-s: 60px;
688 position: relative;
689 display: block;
690 width: 100%;
691 margin: 0;
692 padding: 0;
693 overflow-x: clip;
694 background: var(--hal-sand);
695 color: var(--hal-ink);
696 font: 400 17px/1.6 var(--hal-sans);
697 letter-spacing: 0;
698 text-align: left;
699 -webkit-font-smoothing: antialiased;
700 -moz-osx-font-smoothing: grayscale;
701}
702.hal *, .hal *::before, .hal *::after {
703 box-sizing: border-box;
704 margin: 0;
705 padding: 0;
706 border: 0 solid transparent;
707 font: inherit;
708 color: inherit;
709 letter-spacing: inherit;
710 text-decoration: none;
711 list-style: none;
712 background: none;
713 -webkit-appearance: none;
714 appearance: none;
715 min-width: 0;
716}
717.hal :where(svg) { display: block; overflow: hidden; }
718.hal :where(button) { cursor: pointer; text-align: inherit; line-height: inherit; }
719.hal :where(button:disabled) { cursor: default; }
720.hal :focus-visible { outline: 2px solid rgba(169, 80, 43, 0.7); outline-offset: 2px; border-radius: 6px; }
721.hal [hidden] { display: none !important; }
722.hal-skip { position: absolute; left: 16px; top: -60px; z-index: 100; padding: 10px 16px; background: var(--hal-ink); color: var(--hal-paper); border-radius: 9999px; font: 500 13px/1 var(--hal-sans); }
723.hal-skip:focus { top: 12px; }
724
725.hal-wrap { width: 100%; max-width: 1280px; margin: 0 auto; padding: 0 40px; }
726.hal-eyebrow { display: flex; align-items: center; gap: 10px; font: 500 12px/1.4 var(--hal-sans); letter-spacing: 0.18em; text-transform: uppercase; color: var(--hal-terra); }
727.hal-h2 { font: 400 clamp(32px, 4.2vw, 64px)/1.06 var(--hal-serif); letter-spacing: -0.025em; color: var(--hal-ink); text-wrap: balance; }
728.hal-h3 { font: 400 28px/1.15 var(--hal-serif); letter-spacing: -0.015em; color: var(--hal-ink); }
729.hal-lede { font: 400 24px/1.5 var(--hal-serif); letter-spacing: -0.01em; color: var(--hal-ink); }
730.hal-body { font: 400 17px/1.7 var(--hal-sans); color: var(--hal-ink-2); }
731.hal-scene { display: block; width: 100%; height: 100%; max-width: none; object-fit: cover; }
732
733/* Buttons */
734.hal-btn { display: inline-flex; align-items: center; justify-content: center; gap: 10px; height: 52px; padding: 0 26px; border-radius: 9999px; font: 500 15px/1 var(--hal-sans); letter-spacing: 0.01em; white-space: nowrap; transition: background-color var(--fd-dur-2) var(--fd-ease-out), color var(--fd-dur-2) var(--fd-ease-out), transform var(--fd-dur-2) var(--fd-spring); }
735.hal-btn svg { width: 16px; height: 16px; transition: transform var(--fd-dur-2) var(--fd-ease-out); }
736.hal-btn:hover svg { transform: translateX(3px); }
737.hal-btn:active { transform: scale(0.96); transition-duration: var(--fd-dur-1); }
738.hal-btn-sm { height: 40px; padding: 0 18px; font-size: 13px; }
739.hal-btn-ink { background: var(--hal-ink); color: var(--hal-paper); }
740.hal-btn-ink:hover { background: #3A302A; }
741.hal-btn-terra { background: var(--hal-terra); color: #FFF8F1; }
742.hal-btn-terra:hover { background: #923F1E; }
743.hal-btn-sand { background: var(--hal-sand); color: var(--hal-ink); }
744.hal-btn-sand:hover { background: #FFFFFF; }
745
746/* NAV */
747.hal-nav { position: sticky; top: 0; z-index: 60; height: var(--hal-nav-h); margin-bottom: calc(var(--hal-nav-h) * -1); pointer-events: none; }
748.hal-nav-bar { pointer-events: auto; position: relative; display: flex; align-items: center; justify-content: space-between; gap: 24px; height: var(--hal-nav-h); padding: 0 40px; transition: height var(--fd-dur-3) var(--fd-ease-out), background-color var(--fd-dur-3) var(--fd-ease-out), box-shadow var(--fd-dur-3) var(--fd-ease-out); }
749.hal-nav.is-small .hal-nav-bar { height: var(--hal-nav-s); background: rgba(243, 236, 226, 0.86); -webkit-backdrop-filter: saturate(1.4) blur(18px); backdrop-filter: saturate(1.4) blur(18px); box-shadow: 0 1px 0 var(--hal-line); }
750.hal-logo { display: inline-flex; align-items: center; gap: 12px; color: var(--hal-ink); font: 400 22px/1 var(--hal-serif); letter-spacing: 0.22em; text-transform: uppercase; }
751.hal-logo svg { width: 28px; height: 28px; transition: transform var(--fd-dur-3) var(--fd-ease-out); }
752.hal-nav.is-small .hal-logo svg { transform: scale(0.86); }
753.hal-nav-links { display: flex; gap: 34px; }
754.hal-nav-links a { position: relative; font: 500 14px/1 var(--hal-sans); letter-spacing: 0.02em; color: var(--hal-ink); padding: 8px 0; }
755.hal-nav-links a::after { content: ""; position: absolute; left: 0; right: 0; bottom: 2px; height: 1px; background: currentColor; transform: scaleX(0); transform-origin: right; transition: transform var(--fd-dur-3) var(--fd-ease-out); }
756.hal-nav-links a:hover::after { transform: scaleX(1); transform-origin: left; }
757.hal-nav-end { display: flex; align-items: center; gap: 18px; }
758.hal-nav-lang { font: 500 13px/1 var(--hal-sans); letter-spacing: 0.08em; color: var(--hal-ink-2); }
759.hal-burger { display: none; width: 44px; height: 44px; border-radius: 9999px; position: relative; }
760.hal-burger span { position: absolute; left: 13px; right: 13px; height: 1.5px; background: var(--hal-ink); transition: transform var(--fd-dur-3) var(--fd-ease-out); }
761.hal-burger span:first-child { top: 18px; }
762.hal-burger span:last-child { top: 25px; }
763.hal-nav.is-open .hal-burger span:first-child { transform: translateY(3.5px) rotate(45deg); }
764.hal-nav.is-open .hal-burger span:last-child { transform: translateY(-3.5px) rotate(-45deg); }
765.hal-sheet { pointer-events: auto; display: flex; flex-direction: column; padding: 8px 24px 24px; background: rgba(243, 236, 226, 0.97); box-shadow: 0 1px 0 var(--hal-line), 0 30px 60px -30px rgba(30, 25, 21, 0.3); }
766.hal-sheet a { font: 400 28px/1.2 var(--hal-serif); padding: 12px 0; border-bottom: 1px solid var(--hal-line); color: var(--hal-ink); }
767
768/* HERO */
769.hal-hero { position: relative; height: 190vh; height: 190svh; background: #EAD3B4; --p: 0; }
770.hal-hero-pin { position: sticky; top: 0; height: 100vh; height: 100svh; min-height: 620px; overflow: hidden; }
771.hal-hero-scene { position: absolute; inset: 0; }
772.hal-hero-scene::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, rgba(246, 228, 204, 0.88) 0%, rgba(246, 228, 204, 0.62) 34%, rgba(246, 228, 204, 0) 68%); transition: background var(--fd-dur-4) var(--fd-ease-out); }
773.hal-hero.is-dusk .hal-hero-scene::after { background: linear-gradient(90deg, rgba(26, 16, 36, 0.72) 0%, rgba(26, 16, 36, 0.42) 34%, rgba(26, 16, 36, 0) 68%); }
774.hal-hero-img { position: absolute; left: 0; top: 0; display: block; width: 100%; height: 100%; max-width: none; object-fit: cover; object-position: 55% 40%; transform: scale(calc(1 + var(--p) * 0.08)); transform-origin: 60% 70%; will-change: transform; }
775.hal-dusk { opacity: var(--p); filter: brightness(0.55) saturate(0.85) hue-rotate(-14deg) contrast(1.1); }
776/* Camera cranes up as you scroll: near layers fall away faster than far ones, the sun sets behind the sea. */
777
778.hal-hero-copy { position: absolute; left: 0; right: 0; top: 0; bottom: 0; display: flex; flex-direction: column; justify-content: center; padding: var(--hal-nav-h) 40px 150px; max-width: 1280px; margin: 0 auto; transform: translate3d(0, calc(var(--p) * -60px), 0); opacity: calc(1.6 - var(--p) * 1.9); }
779.hal-hero-eyebrow { color: var(--hal-ink); transition: color var(--fd-dur-4) var(--fd-ease-out); margin-bottom: 28px; }
780.hal-hero-eyebrow i { width: 4px; height: 4px; border-radius: 50%; background: currentColor; opacity: 0.6; }
781.hal-clock { font-variant-numeric: tabular-nums; min-width: 3.2em; }
782.hal-hero-title { font: 400 clamp(56px, 7.4vw, 120px)/0.98 var(--hal-serif); letter-spacing: -0.04em; color: var(--hal-ink); max-width: 9.5em; transition: color var(--fd-dur-4) var(--fd-ease-out); }
783.hal-hero-title em { font-style: italic; color: var(--hal-terra); transition: color var(--fd-dur-4) var(--fd-ease-out); }
784.hal-line { display: block; overflow: hidden; padding-bottom: 0.08em; margin-bottom: -0.08em; }
785.hal-line > span { display: block; transform: translateY(105%); animation: hal-rise 1.2s var(--fd-ease-out) forwards; }
786.hal-line:nth-child(2) > span { animation-delay: 140ms; }
787@keyframes hal-rise { to { transform: translateY(0); } }
788.hal-hero-sub { max-width: 470px; margin-top: 30px; font: 400 17px/1.65 var(--hal-sans); color: var(--hal-ink-2); opacity: 0; animation: hal-fade 1s var(--fd-ease-out) 500ms forwards; transition: color var(--fd-dur-4) var(--fd-ease-out); }
789@keyframes hal-fade { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
790.hal-hero.is-dusk .hal-hero-title, .hal-hero.is-dusk .hal-hero-eyebrow { color: #FBF3E8; }
791.hal-hero.is-dusk .hal-hero-title em { color: #F2B48C; }
792.hal-hero.is-dusk .hal-hero-sub { color: rgba(251, 243, 232, 0.86); }
793.hal-hero-scroll { position: absolute; right: 40px; bottom: 150px; display: flex; align-items: center; gap: 12px; font: 500 12px/1 var(--hal-sans); letter-spacing: 0.18em; text-transform: uppercase; color: var(--hal-ink); writing-mode: vertical-rl; opacity: calc(1 - var(--p) * 4); }
794.hal-hero-scroll span { display: block; width: 1px; height: 56px; background: currentColor; transform-origin: top; animation: hal-tick 2.4s var(--fd-ease-in-out) infinite; }
795@keyframes hal-tick { 0% { transform: scaleY(0); } 50% { transform: scaleY(1); transform-origin: top; } 51% { transform-origin: bottom; } 100% { transform: scaleY(0); transform-origin: bottom; } }
796.hal-l-sky, .hal-l-sun, .hal-l-far, .hal-l-sea, .hal-l-cliff, .hal-l-front { animation: hal-settle 1.6s var(--fd-ease-out) both; }
797@keyframes hal-settle { from { opacity: 0; translate: 0 3%; } to { opacity: 1; translate: 0 0; } }
798.hal-l-sky { animation-name: hal-fade-in; }
799@keyframes hal-fade-in { from { opacity: 0; } to { opacity: 1; } }
800
801/* BOOKING BAR */
802.hal-book { position: sticky; top: calc(var(--hal-nav-s) + 10px); bottom: 24px; z-index: 50; display: flex; justify-content: center; margin-top: -120px; padding: 0 40px; height: 96px; pointer-events: none; }
803.hal-book-card { pointer-events: auto; position: relative; display: grid; grid-template-columns: auto 1.3fr 0.8fr 0.8fr auto; align-items: center; gap: 0; width: 100%; max-width: 1120px; height: 96px; padding: 0 12px 0 24px; border-radius: 24px; background: rgba(251, 248, 243, 0.94); -webkit-backdrop-filter: saturate(1.3) blur(20px); backdrop-filter: saturate(1.3) blur(20px); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.6) inset, 0 24px 60px -24px rgba(30, 25, 21, 0.35), 0 0 0 1px rgba(30, 25, 21, 0.06); transition: height var(--fd-dur-3) var(--fd-ease-out), border-radius var(--fd-dur-3) var(--fd-ease-out), box-shadow var(--fd-dur-3) var(--fd-ease-out), max-width var(--fd-dur-4) var(--fd-ease-out); }
804.hal-book.is-docked .hal-book-card { height: 68px; max-width: 1200px; border-radius: 9999px; box-shadow: 0 0 0 1px rgba(30, 25, 21, 0.08), 0 12px 30px -16px rgba(30, 25, 21, 0.3); }
805.hal-field { position: relative; display: flex; flex-direction: column; justify-content: center; gap: 6px; height: 100%; padding: 0 22px; border-left: 1px solid var(--hal-line); }
806.hal-field-dest { border-left: 0; padding-left: 0; }
807.hal-label { display: block; font: 500 11px/1 var(--hal-sans); letter-spacing: 0.16em; text-transform: uppercase; color: var(--hal-ink-2); transition: opacity var(--fd-dur-2) var(--fd-ease-out), height var(--fd-dur-3) var(--fd-ease-out); }
808.hal-book.is-docked .hal-label { opacity: 0; height: 0; margin-bottom: -6px; overflow: hidden; }
809.hal-seg { position: relative; display: grid; grid-template-columns: 1fr 1fr; padding: 4px; border-radius: 9999px; background: rgba(30, 25, 21, 0.06); }
810.hal-seg button { position: relative; z-index: 1; height: 36px; padding: 0 16px; border-radius: 9999px; font: 500 14px/1 var(--hal-sans); color: var(--hal-ink-2); text-align: center; white-space: nowrap; transition: color var(--fd-dur-2) var(--fd-ease-out); }
811.hal-seg button.is-on { color: var(--hal-ink); }
812.hal-seg-thumb { position: absolute; top: 4px; bottom: 4px; left: 4px; width: calc(50% - 4px); border-radius: 9999px; background: #FFFFFF; box-shadow: 0 1px 2px rgba(30, 25, 21, 0.12), 0 4px 12px -4px rgba(30, 25, 21, 0.18); transition: transform var(--fd-dur-3) var(--fd-spring); }
813.hal-date-btn { display: flex; align-items: baseline; gap: 8px; height: 32px; font: 400 20px/32px var(--hal-serif); letter-spacing: -0.01em; color: var(--hal-ink); white-space: nowrap; }
814.hal-date-btn small { font: 400 14px/1 var(--hal-sans); color: var(--hal-ink-2); }
815.hal-pop { position: absolute; left: 10px; bottom: calc(100% + 14px); width: 360px; padding: 18px; border-radius: 20px; background: var(--hal-paper); box-shadow: 0 0 0 1px var(--hal-line), 0 30px 60px -20px rgba(30, 25, 21, 0.4); animation: hal-pop var(--fd-dur-3) var(--fd-ease-out); }
816.hal-book.is-docked .hal-pop { bottom: auto; top: calc(100% + 14px); }
817@keyframes hal-pop { from { opacity: 0; transform: translateY(8px) scale(0.98); } to { opacity: 1; transform: none; } }
818.hal-pop-title { font: 400 18px/1.2 var(--hal-serif); margin-bottom: 12px; color: var(--hal-ink); }
819.hal-pop-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; }
820.hal-pop-grid button { display: flex; flex-direction: column; align-items: center; gap: 2px; height: 52px; justify-content: center; border-radius: 14px; font: 500 16px/1 var(--hal-sans); color: var(--hal-ink); transition: background-color var(--fd-dur-1) var(--fd-ease-out); }
821.hal-pop-grid button small { font: 500 10px/1 var(--hal-sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--hal-ink-2); }
822.hal-pop-grid button:hover { background: rgba(30, 25, 21, 0.06); }
823.hal-pop-grid button.is-on { background: var(--hal-ink); color: var(--hal-paper); }
824.hal-pop-grid button.is-on small { color: rgba(251, 248, 243, 0.7); }
825.hal-step { display: flex; align-items: center; gap: 10px; height: 32px; }
826.hal-step button { width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--hal-line); font: 400 18px/1 var(--hal-sans); color: var(--hal-ink); text-align: center; transition: background-color var(--fd-dur-1) var(--fd-ease-out), transform var(--fd-dur-2) var(--fd-spring); }
827.hal-step button:hover { background: rgba(30, 25, 21, 0.06); }
828.hal-step button:active { transform: scale(0.9); }
829.hal-step output { min-width: 18px; text-align: center; font: 400 20px/1 var(--hal-serif); color: var(--hal-ink); font-variant-numeric: tabular-nums; }
830.hal-cta-short { display: none; }
831.hal-book-cta { height: 64px; padding: 0 28px; transition: height var(--fd-dur-3) var(--fd-ease-out), background-color var(--fd-dur-2) var(--fd-ease-out), transform var(--fd-dur-2) var(--fd-spring); }
832.hal-book.is-docked .hal-book-cta { height: 48px; }
833.hal-book-result { position: absolute; left: 50%; top: calc(100% + 12px); transform: translate(-50%, -6px); padding: 10px 18px; border-radius: 9999px; background: var(--hal-ink); color: var(--hal-paper); font: 500 13px/1.3 var(--hal-sans); white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity var(--fd-dur-3) var(--fd-ease-out), transform var(--fd-dur-3) var(--fd-ease-out); }
834.hal-book-result.is-on { opacity: 1; transform: translate(-50%, 0); }
835.hal-book:not(.is-docked) .hal-book-result { top: auto; bottom: calc(100% + 12px); }
836
837/* REVEALS */
838.hal-js [data-hal-rv] { opacity: 0; transform: translate3d(0, 28px, 0); transition: opacity 1s var(--fd-ease-out), transform 1.1s var(--fd-ease-out); transition-delay: var(--d, 0ms); }
839.hal-js [data-hal-rv].is-in { opacity: 1; transform: none; }
840
841/* INTRO */
842.hal-intro { padding: 180px 0 160px; }
843.hal-intro-grid { display: grid; grid-template-columns: 1fr 1.05fr; gap: 96px; align-items: start; }
844.hal-intro-text { position: relative; padding-top: 40px; }
845.hal-intro-text .hal-h2 { margin: 22px 0 36px; }
846.hal-drop::first-letter { float: left; font: 400 88px/0.82 var(--hal-serif); color: var(--hal-terra); margin: 8px 12px 0 0; }
847.hal-intro-text .hal-body { margin-top: 22px; max-width: 520px; }
848.hal-figs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin-top: 56px; padding-top: 28px; border-top: 1px solid var(--hal-line); }
849.hal-figs dt { font: 500 12px/1.3 var(--hal-sans); letter-spacing: 0.12em; text-transform: uppercase; color: var(--hal-ink-2); }
850.hal-figs dd { margin-top: 8px; font: 400 44px/1 var(--hal-serif); letter-spacing: -0.025em; color: var(--hal-ink); }
851.hal-intro-media { position: relative; height: 820px; --p: 0; }
852.hal-clip { position: absolute; margin: 0; }
853.hal-clip-a { right: 0; top: 0; width: 78%; height: 620px; transform: translate3d(0, calc(var(--p) * -40px), 0); }
854.hal-clip-b { left: 0; bottom: 0; width: 46%; height: 380px; z-index: 2; transform: translate3d(0, calc(var(--p) * -110px), 0); }
855.hal-clip-in { width: 100%; height: calc(100% - 34px); overflow: hidden; border-radius: 4px; background: #D9C4A6; }
856.hal-clip-b .hal-clip-in { box-shadow: 0 0 0 10px var(--hal-sand); }
857.hal-clip figcaption { margin-top: 12px; font: italic 400 15px/1.3 var(--hal-serif); color: var(--hal-ink-2); }
858.hal-js .hal-clip[data-hal-rv] { opacity: 1; transform: translate3d(0, calc(var(--p) * -40px), 0); }
859.hal-js .hal-clip-b[data-hal-rv] { transform: translate3d(0, calc(var(--p) * -110px), 0); }
860.hal-js .hal-clip .hal-clip-in { clip-path: inset(100% 0 0 0); transition: clip-path 1.4s var(--fd-ease-out); }
861.hal-js .hal-clip .hal-scene { transform: scale(1.18); transition: transform 2s var(--fd-ease-out); }
862.hal-js .hal-clip.is-in .hal-clip-in { clip-path: inset(0 0 0 0); }
863.hal-js .hal-clip.is-in .hal-scene { transform: scale(1); }
864.hal-js .hal-clip-b .hal-clip-in { transition-delay: 220ms; }
865.hal-js .hal-clip figcaption { opacity: 0; transition: opacity 1s var(--fd-ease-out) 700ms; }
866.hal-js .hal-clip.is-in figcaption { opacity: 1; }
867
868/* HOUSES — pinned horizontal */
869.hal-houses { position: relative; height: calc(100vh + var(--dist, 0px)); height: calc(100svh + var(--dist, 0px)); background: var(--hal-paper); --p: 0; }
870.hal-houses-pin { position: sticky; top: 0; height: 100vh; height: 100svh; min-height: 640px; display: flex; flex-direction: column; justify-content: center; gap: 44px; overflow: hidden; padding-top: 150px; }
871.hal-houses-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 40px; }
872.hal-houses-head .hal-h2 { margin-top: 16px; }
873.hal-houses-meter { display: flex; align-items: center; gap: 18px; padding-bottom: 10px; }
874.hal-houses-bar { position: relative; display: block; width: 180px; height: 1px; background: var(--hal-line); }
875.hal-houses-bar i { position: absolute; inset: 0; background: var(--hal-ink); transform: scaleX(calc(0.25 + var(--p) * 0.75)); transform-origin: left; }
876.hal-houses-cur { font-weight: 600; color: var(--hal-ink); }
877.hal-houses-count { font: 500 13px/1 var(--hal-sans); letter-spacing: 0.12em; color: var(--hal-ink-2); font-variant-numeric: tabular-nums; }
878.hal-houses-track { display: flex; gap: 32px; padding: 0 40px; width: max-content; will-change: transform; }
879.hal-house { flex: 0 0 auto; width: min(58vw, 860px); }
880.hal-house-link { display: grid; grid-template-columns: 1.35fr 1fr; gap: 32px; height: min(58vh, 560px); color: var(--hal-ink); }
881.hal-house-img { position: relative; overflow: hidden; border-radius: 4px; background: #D9C4A6; }
882.hal-house-par { position: absolute; inset: 0 -12%; transform: translate3d(calc(var(--p) * 14% - 7%), 0, 0); }
883.hal-house-link:hover .hal-house-par .hal-scene { transform: scale(1.04); }
884.hal-house-par .hal-scene { transition: transform 1.2s var(--fd-ease-out); }
885.hal-house-info { display: flex; flex-direction: column; justify-content: space-between; padding: 8px 0 4px; }
886.hal-house-n { font: italic 400 20px/1 var(--hal-serif); color: var(--hal-terra); }
887.hal-house-info .hal-h3 { font-size: 40px; letter-spacing: -0.025em; }
888.hal-house-place { margin-top: 10px; font: 500 13px/1.4 var(--hal-sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--hal-ink-2); }
889.hal-house-note { margin-top: 22px; font: 400 17px/1.65 var(--hal-sans); color: var(--hal-ink-2); max-width: 34ch; }
890.hal-house-meta { display: flex; justify-content: space-between; padding-top: 18px; border-top: 1px solid var(--hal-line); font: 400 20px/1 var(--hal-serif); color: var(--hal-ink); }
891
892/* ROOMS */
893.hal-rooms { padding: 180px 0 140px; }
894.hal-sec-head { max-width: 760px; margin-bottom: 64px; }
895.hal-sec-head .hal-h2 { margin-top: 18px; }
896.hal-sec-sub { margin-top: 22px; max-width: 560px; }
897.hal-room-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; }
898.hal-room-link { display: block; color: var(--hal-ink); }
899.hal-room-img { position: relative; aspect-ratio: 4 / 5; overflow: hidden; border-radius: 4px; background: #D9C4A6; }
900.hal-room-img .hal-scene { transition: transform 1.1s var(--fd-ease-out); }
901.hal-room-img::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(30, 25, 21, 0) 60%, rgba(30, 25, 21, 0.18)); opacity: 0; transition: opacity var(--fd-dur-4) var(--fd-ease-out); }
902.hal-room-link:hover .hal-room-img .hal-scene, .hal-room-link:focus-visible .hal-room-img .hal-scene { transform: scale(1.07); }
903.hal-room-link:hover .hal-room-img::after { opacity: 1; }
904.hal-room-body { padding-top: 22px; }
905.hal-room-house { font: 500 12px/1.4 var(--hal-sans); letter-spacing: 0.14em; text-transform: uppercase; color: var(--hal-terra); }
906.hal-room-body .hal-h3 { margin-top: 10px; }
907.hal-room-feat { margin-top: 8px; font: 400 15px/1.5 var(--hal-sans); color: var(--hal-ink-2); }
908.hal-room-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--hal-line); font: 400 15px/1 var(--hal-sans); color: var(--hal-ink-2); }
909.hal-room-foot strong { font: 400 20px/1 var(--hal-serif); color: var(--hal-ink); }
910.hal-room-cta { display: inline-flex; align-items: center; gap: 8px; font: 500 14px/1 var(--hal-sans); color: var(--hal-ink); }
911.hal-room-cta svg { width: 16px; height: 16px; transition: transform var(--fd-dur-3) var(--fd-ease-out); }
912.hal-room-link:hover .hal-room-cta svg { transform: translateX(4px); }
913
914/* RITUALS */
915.hal-rituals { padding: 140px 0; background: var(--hal-night); color: #F3ECE2; }
916.hal-rituals .hal-h2 { color: #F7F0E6; margin: 18px 0 44px; }
917.hal-rituals .hal-eyebrow { color: #E6A47F; }
918.hal-rituals-grid { display: grid; grid-template-columns: 0.9fr 1.1fr; gap: 96px; align-items: center; }
919.hal-rituals-frame { position: relative; aspect-ratio: 4 / 5; overflow: hidden; border-radius: 4px; background: #2A221D; }
920.hal-rituals-img { position: absolute; inset: 0; opacity: 0; transform: scale(1.06); transition: opacity var(--fd-dur-4) var(--fd-ease-in-out), transform 1.4s var(--fd-ease-out); }
921.hal-rituals-img.is-on { opacity: 1; transform: scale(1); }
922.hal-rituals-time { position: absolute; left: 24px; bottom: 20px; font: 400 64px/1 var(--hal-serif); letter-spacing: -0.04em; color: #FFF8EE; text-shadow: 0 2px 30px rgba(0, 0, 0, 0.35); font-variant-numeric: tabular-nums; }
923.hal-rituals-list { border-top: 1px solid rgba(243, 236, 226, 0.16); }
924.hal-ritual { display: grid; grid-template-columns: 72px 1fr; grid-template-rows: auto auto; column-gap: 20px; row-gap: 6px; width: 100%; padding: 26px 0; border-bottom: 1px solid rgba(243, 236, 226, 0.16); color: rgba(243, 236, 226, 0.6); text-align: left; transition: color var(--fd-dur-3) var(--fd-ease-out), padding var(--fd-dur-3) var(--fd-ease-out); }
925.hal-ritual-time { grid-row: span 2; font: 500 14px/2 var(--hal-sans); letter-spacing: 0.1em; font-variant-numeric: tabular-nums; }
926.hal-ritual-title { font: 400 30px/1.15 var(--hal-serif); letter-spacing: -0.015em; }
927.hal-ritual-where { font: 400 15px/1.4 var(--hal-sans); color: rgba(243, 236, 226, 0.6); }
928.hal-ritual.is-on, .hal-ritual:hover { color: #FFF8EE; padding-left: 12px; }
929.hal-ritual.is-on .hal-ritual-time { color: #E6A47F; }
930.hal-ritual:focus-visible { outline-color: #E6A47F; }
931
932/* QUOTES */
933.hal-quotes { padding: 160px 0; background: var(--hal-sand); }
934.hal-quotes-in { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); gap: clamp(40px, 7vw, 110px); align-items: center; text-align: left; }
935.hal-quotes-fig { margin: 0; aspect-ratio: 1; overflow: hidden; border-radius: 4px; box-shadow: 0 30px 60px -30px rgba(60, 30, 10, 0.4); }
936.hal-quotes-img { display: block; width: 100%; height: 100%; max-width: none; object-fit: cover; }
937.hal-quotes-body { display: flex; flex-direction: column; align-items: flex-start; min-width: 0; }
938.hal-quote-stack { display: grid; width: 100%; max-width: 980px; margin-top: 36px; }
939.hal-quote { grid-area: 1 / 1; opacity: 0; transform: translateY(10px); filter: blur(4px); transition: opacity 900ms var(--fd-ease-in-out), transform 900ms var(--fd-ease-out), filter 900ms var(--fd-ease-out); pointer-events: none; }
940.hal-quote.is-on { opacity: 1; transform: none; filter: none; pointer-events: auto; }
941.hal-quote blockquote p { font: italic 400 clamp(24px, 2.6vw, 38px)/1.3 var(--hal-serif); letter-spacing: -0.015em; color: var(--hal-ink); text-wrap: balance; }
942.hal-quote figcaption { display: flex; flex-direction: column; gap: 6px; margin-top: 32px; font: 400 15px/1.4 var(--hal-sans); color: var(--hal-ink-2); }
943.hal-quote figcaption strong { font: 500 15px/1.4 var(--hal-sans); color: var(--hal-ink); }
944.hal-quote-dots { display: flex; gap: 10px; margin-top: 44px; }
945.hal-quote-dots button { position: relative; width: 44px; height: 44px; }
946.hal-quote-dots button::before { content: ""; position: absolute; left: 4px; right: 4px; top: 21px; height: 2px; background: var(--hal-line); }
947.hal-quote-dots i { position: absolute; left: 4px; right: 4px; top: 21px; height: 2px; background: var(--hal-ink); transform: scaleX(0); transform-origin: left; }
948.hal-quote-dots button.is-on i { animation: hal-fill 6.5s linear forwards; }
949@keyframes hal-fill { to { transform: scaleX(1); } }
950
951/* MAP */
952.hal-map { padding: 160px 0; background: var(--hal-paper); }
953.hal-map-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; }
954.hal-map-card { margin: 0; }
955.hal-map-svg { width: 100%; height: auto; border-radius: 4px; }
956.hal-map-card figcaption { display: grid; grid-template-columns: 160px 1fr; gap: 20px; margin-top: 22px; font: 400 15px/1.6 var(--hal-sans); color: var(--hal-ink-2); }
957.hal-map-card figcaption strong { font: 400 24px/1.2 var(--hal-serif); color: var(--hal-ink); }
958.hal-map-t { font: 500 13px/1 var(--hal-sans); fill: #3E342D; letter-spacing: 0.02em; }
959.hal-map-t-sm { font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; fill: #7A6D62; }
960.hal-map-t-sea { font: italic 400 15px/1 var(--hal-serif); fill: #50727C; }
961.hal-js .hal-draw { stroke-dasharray: 1200; stroke-dashoffset: 1200; transition: stroke-dashoffset 2.4s var(--fd-ease-out) 200ms; }
962.hal-js .hal-draw-route { stroke-dasharray: 4 6; stroke-dashoffset: 0; opacity: 0; transition: opacity 1s var(--fd-ease-out) 1.2s; }
963.hal-js .is-in .hal-draw { stroke-dashoffset: 0; }
964.hal-js .is-in .hal-draw-route { opacity: 1; animation: hal-march 1.2s linear infinite; }
965@keyframes hal-march { to { stroke-dashoffset: -20; } }
966.hal-pin { transform-box: fill-box; }
967.hal-js .hal-pin path, .hal-js .hal-pin circle { opacity: 0; transform: translateY(-36px); transition: opacity 400ms var(--fd-ease-out) var(--d), transform 700ms var(--fd-spring) var(--d); }
968.hal-js .is-in .hal-pin path, .hal-js .is-in .hal-pin circle { opacity: 1; transform: none; }
969
970/* FOOTER */
971.hal-foot { padding: 120px 0 40px; background: var(--hal-night); color: #E9E0D4; overflow: hidden; }
972.hal-news { display: grid; grid-template-columns: 1.1fr 1fr; gap: 80px; align-items: end; padding-bottom: 96px; border-bottom: 1px solid rgba(243, 236, 226, 0.14); }
973.hal-eyebrow-light { color: #E6A47F; }
974.hal-news-title { color: #F7F0E6; margin-top: 18px; }
975.hal-news-sub { margin-top: 18px; max-width: 470px; font: 400 17px/1.65 var(--hal-sans); color: rgba(243, 236, 226, 0.72); }
976.hal-news-label { display: block; margin-bottom: 12px; font: 500 12px/1 var(--hal-sans); letter-spacing: 0.14em; text-transform: uppercase; color: rgba(243, 236, 226, 0.72); }
977.hal-news-row { display: flex; gap: 8px; padding: 6px; border-radius: 9999px; background: rgba(243, 236, 226, 0.08); box-shadow: inset 0 0 0 1px rgba(243, 236, 226, 0.16); transition: box-shadow var(--fd-dur-2) var(--fd-ease-out); }
978.hal-news-row:focus-within { box-shadow: inset 0 0 0 1px rgba(230, 164, 127, 0.8); }
979.hal-news-row input { flex: 1; height: 52px; padding: 0 20px; border-radius: 9999px; background: transparent; font: 400 17px/1 var(--hal-sans); color: #FBF3E8; outline: none; }
980.hal-news-row input::placeholder { color: rgba(243, 236, 226, 0.5); }
981.hal-news-row.is-done { box-shadow: inset 0 0 0 1px rgba(230, 164, 127, 0.8); }
982.hal-news-row.is-done .hal-btn { background: #E6A47F; color: var(--hal-night); }
983.hal-news-note { margin-top: 14px; font: 400 14px/1.5 var(--hal-sans); color: rgba(243, 236, 226, 0.62); }
984.hal-foot-cols { display: grid; grid-template-columns: repeat(4, 1fr); gap: 40px; padding: 72px 0; }
985.hal-foot-h { font: 500 12px/1 var(--hal-sans); letter-spacing: 0.16em; text-transform: uppercase; color: rgba(243, 236, 226, 0.6); margin-bottom: 20px; }
986.hal-foot-cols li { margin-bottom: 12px; font: 400 15px/1.4 var(--hal-sans); color: #F3ECE2; }
987.hal-foot-cols a { color: #F3ECE2; transition: color var(--fd-dur-1) var(--fd-ease-out); }
988.hal-foot-cols a:hover { color: #E6A47F; }
989.hal-foot-mark { font: 400 clamp(88px, 21vw, 340px)/0.8 var(--hal-serif); letter-spacing: -0.05em; color: transparent; background: linear-gradient(180deg, rgba(243, 236, 226, 0.22), rgba(243, 236, 226, 0.02)); -webkit-background-clip: text; background-clip: text; text-align: center; white-space: nowrap; margin: 0 -40px; user-select: none; }
990.hal-foot-legal { display: flex; justify-content: space-between; gap: 24px; flex-wrap: wrap; padding-top: 28px; border-top: 1px solid rgba(243, 236, 226, 0.14); font: 400 13px/1.5 var(--hal-sans); color: rgba(243, 236, 226, 0.62); }
991.hal-foot-legal span:last-child { display: flex; gap: 22px; }
992.hal-foot-legal a { color: rgba(243, 236, 226, 0.78); }
993.hal-foot-legal a:hover { color: #FBF3E8; }
994
995/* RESPONSIVE */
996@media (max-width: 1080px) {
997 .hal-nav-links { gap: 22px; }
998 .hal-intro-grid { gap: 56px; }
999 .hal-book-card { grid-template-columns: auto 1.2fr 0.9fr 0.9fr auto; padding-left: 16px; }
1000 .hal-field { padding: 0 14px; }
1001 .hal-book-cta { padding: 0 20px; }
1002}
1003@media (max-width: 900px) {
1004 .hal-wrap { padding: 0 24px; }
1005 .hal-nav-bar { padding: 0 20px; }
1006 .hal-nav-links, .hal-nav-lang { display: none; }
1007 .hal-burger { display: block; }
1008 .hal-hero-copy { padding: var(--hal-nav-h) 24px 150px; }
1009 .hal-hero-scroll { display: none; }
1010 .hal-intro { padding: 120px 0 100px; }
1011 .hal-intro-grid, .hal-rituals-grid, .hal-news { grid-template-columns: 1fr; gap: 56px; }
1012 .hal-intro-media { height: 640px; }
1013 .hal-houses { height: auto; }
1014 .hal-houses-pin { position: relative; height: auto; min-height: 0; padding: 110px 0 90px; }
1015 .hal-houses-meter { display: none; }
1016 .hal-houses-track { width: auto; overflow-x: auto; scroll-snap-type: x mandatory; padding-bottom: 12px; scrollbar-width: none; transform: none !important; }
1017 .hal-houses-track::-webkit-scrollbar { display: none; }
1018 .hal-house { width: 82vw; scroll-snap-align: center; }
1019 .hal-house-link { grid-template-columns: 1fr; height: auto; gap: 20px; }
1020 .hal-house-img { aspect-ratio: 4 / 3; }
1021 .hal-house-par { inset: 0; transform: none; }
1022 .hal-house-note { margin-top: 14px; }
1023 .hal-house-meta { margin-top: 18px; }
1024 .hal-rooms { padding: 110px 0; }
1025 .hal-room-grid { grid-template-columns: 1fr; gap: 48px; }
1026 .hal-rituals { padding: 100px 0; }
1027 .hal-rituals-frame { aspect-ratio: 4 / 3; }
1028 .hal-quotes, .hal-map { padding: 110px 0; }
1029 .hal-quotes-in { grid-template-columns: 1fr; }
1030 .hal-quotes-fig { max-width: 420px; }
1031 .hal-map-grid { grid-template-columns: 1fr; }
1032 .hal-foot-cols { grid-template-columns: 1fr 1fr; }
1033}
1034@media (max-width: 760px) {
1035 .hal-book { padding: 0 12px; height: auto; margin-top: -196px; bottom: 12px; top: calc(var(--hal-nav-s) + 6px); }
1036 .hal-book-card { grid-template-columns: 1fr 1fr; grid-template-rows: auto auto auto; height: auto; padding: 14px; gap: 10px 0; border-radius: 22px; }
1037 .hal-field-dest { grid-column: 1 / -1; padding: 0; }
1038 .hal-field-dest .hal-label { display: none; }
1039 .hal-field-date { grid-column: 1 / -1; padding: 0 4px; border-left: 0; }
1040 .hal-field { padding: 0 4px; height: auto; gap: 4px; }
1041 .hal-field:nth-child(4) { padding-left: 16px; }
1042 .hal-field:nth-child(3) { border-left: 0; }
1043 .hal-book-cta { grid-column: 1 / -1; height: 52px; }
1044 .hal-label { font-size: 10px; }
1045 .hal-book.is-docked .hal-book-card { height: auto; grid-template-columns: 1fr auto; grid-template-rows: auto; gap: 8px; padding: 6px; border-radius: 9999px; }
1046 .hal-book.is-docked .hal-field:nth-child(3), .hal-book.is-docked .hal-field:nth-child(4), .hal-book.is-docked .hal-field-date { display: none; }
1047 .hal-book.is-docked .hal-field-dest { grid-column: 1; }
1048 .hal-book.is-docked .hal-book-cta { grid-column: 2; height: 44px; padding: 0 18px; }
1049 .hal-book.is-docked .hal-cta-full { display: none; }
1050 .hal-book.is-docked .hal-cta-short { display: inline; }
1051 .hal-pop { width: calc(100vw - 48px); left: -4px; }
1052 .hal-pop-grid { grid-template-columns: repeat(4, 1fr); }
1053 .hal-book-result { white-space: normal; width: max-content; max-width: calc(100vw - 48px); text-align: center; }
1054 .hal-hero-copy { padding: calc(var(--hal-nav-h) + 36px) 24px 290px; justify-content: flex-start; }
1055 /* Portrait crop: frame the sea + the cliff edge (viewBox x≈700–1125) instead of the centre. */
1056 .hal-hero-img { object-position: 60% 40%; }
1057 .hal-hero-scene::after { background: linear-gradient(180deg, rgba(246, 228, 204, 0.9) 0%, rgba(246, 228, 204, 0.7) 45%, rgba(246, 228, 204, 0) 75%); }
1058 .hal-hero.is-dusk .hal-hero-scene::after { background: linear-gradient(180deg, rgba(26, 16, 36, 0.75) 0%, rgba(26, 16, 36, 0.5) 45%, rgba(26, 16, 36, 0) 75%); }
1059 .hal-hero-title { font-size: clamp(48px, 14vw, 64px); }
1060 .hal-hero-sub { font-size: 15px; margin-top: 18px; }
1061 .hal-hero-eyebrow { margin-bottom: 18px; font-size: 11px; gap: 8px; }
1062}
1063@media (max-width: 560px) {
1064 .hal-h3 { font-size: 24px; }
1065 .hal-house-info .hal-h3 { font-size: 32px; }
1066 .hal-intro-media { height: 520px; }
1067 .hal-clip-a { width: 86%; height: 400px; }
1068 .hal-clip-b { width: 54%; height: 250px; }
1069 .hal-figs dd { font-size: 32px; }
1070 .hal-lede { font-size: 21px; }
1071 .hal-drop::first-letter { font-size: 64px; }
1072 .hal-ritual-title { font-size: 24px; }
1073 .hal-ritual { grid-template-columns: 56px 1fr; }
1074 .hal-rituals-time { font-size: 44px; }
1075 .hal-map-card figcaption { grid-template-columns: 1fr; gap: 6px; }
1076 .hal-foot-legal { flex-direction: column; }
1077 .hal-news-row { flex-direction: column; border-radius: 22px; }
1078 .hal-news-row .hal-btn { width: 100%; }
1079}
1080
1081@media (prefers-reduced-motion: reduce) {
1082 .hal *, .hal *::before, .hal *::after { animation-duration: 1ms !important; animation-iteration-count: 1 !important; transition-duration: 120ms !important; transition-delay: 0ms !important; }
1083 .hal-hero { height: 100vh; height: 100svh; }
1084 .hal-hero-img, .hal-hero-copy, .hal-house-par, .hal-clip-a, .hal-clip-b, .hal-js .hal-clip[data-hal-rv], .hal-js .hal-clip-b[data-hal-rv] { transform: none !important; }
1085 .hal-houses { height: auto; }
1086 .hal-houses-pin { position: relative; height: auto; min-height: 0; padding: 110px 0 90px; }
1087 .hal-houses-track { width: auto; overflow-x: auto; scroll-snap-type: x mandatory; transform: none !important; }
1088 .hal-house { scroll-snap-align: start; }
1089 .hal-houses-meter, .hal-hero-scroll { display: none; }
1090 .hal-js [data-hal-rv] { transform: none; }
1091 .hal-js .hal-clip .hal-clip-in { clip-path: none; }
1092 .hal-js .hal-clip .hal-scene { transform: none; }
1093}
1094@media (prefers-contrast: more) {
1095 .hal-book-card, .hal-nav.is-small .hal-nav-bar { background: var(--hal-paper); -webkit-backdrop-filter: none; backdrop-filter: none; box-shadow: 0 0 0 1px var(--hal-ink); }
1096}
1097@media (prefers-reduced-transparency: reduce) {
1098 .hal-book-card, .hal-nav.is-small .hal-nav-bar { background: var(--hal-paper); -webkit-backdrop-filter: none; backdrop-filter: none; }
1099}
1100`;

The design brief. For you and your AI agent.

Tokens, type, layout, components and motion — written from the real values in the file, so a model can extend the page without drifting.

Halcyon — DESIGN.md

Boutique hotels on the Amalfi coast and in Kyoto. Warm luxury editorial: a large serif display face with tight tracking, sand / ink / terracotta palette, generous whitespace, and real editorial photography from public/img/halcyon (Positano terrace, lemon pool, suite, ryokan, journals).

All CSS lives in Page.tsx under the .hal prefix. Every element is reset explicitly (margin, padding, border, font, appearance, list-style, box-sizing), so the page looks the same under Tailwind preflight, Bootstrap or no reset at all.

1. Palette

RoleTokenHex
Page background (sand)--hal-sand#F3ECE2
Raised surface / houses / map band--hal-paper#FBF8F3
Primary text, ink buttons--hal-ink#1E1915
Secondary text--hal-ink-2#5B5048
Accent: eyebrows, italic emphasis, primary CTA--hal-terra#A9502B (hover #923F1E)
Secondary terracotta (reserved)--hal-terra-2#C8693F
Sea (map routes)--hal-sea#2F5866
Dark bands (rituals, footer)--hal-night#191513
Hairlines--hal-linergba(30,25,21,0.14)
Accent on dark—#E6A47F
Light text on dark—#F3ECE2 / #F7F0E6 / #FBF3E8

The hero illustration has its own golden-hour palette (sky #D9C6B8 → #EED3B2 → #F6E2C6, sea #9EAEAA → #2B4D5A, cliff #9C6A4D → #5A3A2B, bougainvillea #B3466A/#C95C80) and a dusk palette that fades in over it (sky #262640 → #5E4560 → #C2735D → #E9A170, sea #B07A6E → #1C2635, window lights #FFD48E).

Contrast: ink on sand is about 15:1, --hal-ink-2 on sand about 7:1, terracotta on sand about 5:1 (AA for text). On night, #E6A47F is about 8:1.

2. Typography

  • Display serif (--hal-serif): ui-serif, "New York", "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif. Weight 400 only; italic is used for emphasis.
  • UI sans (--hal-sans): ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif.
StyleSpec
Hero H1serif 400, clamp(56px, 7.4vw, 120px) / 0.98, tracking −0.04em, max-width 9.5em; mobile clamp(48px, 14vw, 64px)
H2serif 400, clamp(32px, 4.2vw, 64px) / 1.06, −0.025em, text-wrap: balance
H3serif 400, 28px / 1.15, −0.015em (house names 40px, −0.025em)
Ledeserif 400, 24px / 1.5, −0.01em; drop cap 88px terracotta
Quoteserif italic, clamp(26px, 3.2vw, 44px) / 1.3
Ritual time on frameserif 64px, −0.04em, tabular numerals
Bodysans 400, 17px / 1.7, --hal-ink-2
Eyebrowsans 500, 12px, tracking 0.18em, uppercase, terracotta
Nav linksans 500, 14px, 0.02em
Logoserif 22px, tracking 0.22em, uppercase
Field labelsans 500, 11px, 0.16em uppercase

3. Spacing, layout, radii

  • Container: max-width: 1280px, side padding 40px (24px at ≤900px).
  • Section padding: 160–180px top/bottom on desktop, 100–120px on mobile.
  • Radii: imagery 4px (deliberately near-square and editorial). Controls are capsules (9999px). Booking card 24px, docked 9999px. Date popover 20px, date cells 14px.
  • Elevation: booking card 0 24px 60px -24px rgba(30,25,21,.35) plus a 1px ring; when docked, 0 12px 30px -16px.

4. Motion tokens

--fd-ease-out:    cubic-bezier(0.22, 1, 0.36, 1)   entrances, reveals
--fd-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1)   crossfades, state swaps
--fd-spring:      cubic-bezier(0.34, 1.56, 0.64, 1) segmented thumb, map pins, steppers
--fd-dur-1 120ms · --fd-dur-2 240ms · --fd-dur-3 420ms · --fd-dur-4 700ms

One rAF scroll loop writes a --p progress value (0→1) onto every [data-hal-p] element. pin mode measures progress through a tall sticky section; view mode measures progress across the viewport. All scroll effects animate transform and opacity only.

Choreography

  1. Load. The headline lines rise out of masks (1.2s, 140ms stagger). The sub-line fades up at 500ms. Landscape layers settle from 3% below (1.6s, staggered 0/80/160ms).
  2. Hero, pinned for 190vh, time of day. As --p goes 0→1:
  3. the sun sinks 21% behind the sea
  4. the cliff drops 5% and drifts 2% right; the foreground bougainvillea and parapet fall 16% and scale to 1.06
  5. mist drifts left
  6. dusk overlays fade in with opacity: var(--p); window lights appear after p≈0.35 and stars after p≈0.55
  7. the live clock in the eyebrow counts 17:42 → 20:16
  8. after p>0.45 the copy switches to light text (.is-dusk, 700ms colour transition)
  9. the copy drifts up 60px and fades out near the end.
  10. Booking bar. Sticky with both bottom: 24px and top: nav + 10px. It floats at the bottom of the first screen, then docks under the condensed nav. Docking (JS .is-docked, triggered when its top is under 120px) shrinks it from 96px to 68px, turns it into a capsule, and hides the field labels.
  11. Intro. Two figures reveal with clip-path: inset(100% 0 0 0) → inset(0) (1.4s; the second is delayed 220ms), while the inner scene scales 1.18→1. The two figures parallax at different rates (−40px and −110px across the viewport).
  12. Houses gallery, pinned horizontally. The section height is 100vh + track overflow. The track translates -p × overflow. Each image pans inside its frame (±7%). The meter fills from 25% to 100% and the counter reads 01–04.
  13. Rooms. Cards stagger in (90ms). On hover the image zooms 1.07 (1.1s) and the arrow nudges 4px.
  14. Rituals. Hovering, focusing or clicking a row crossfades the 4:5 frame (700ms in-out plus a 1.06→1 settle) and indents the row 12px.
  15. Testimonials. Autoplay every 6.5s with a crossfade, 10px lift and 4px blur clear (900ms). Progress dashes fill linearly. Hover or focus pauses it; it is disabled for reduced motion.
  16. Map. The coastline and rivers draw in (dash offset, 2.4s). Pins drop with the spring (300/520ms delays). The dashed routes fade in and then march.

5. Sections

  1. Sticky condensing nav. 84px and transparent, condensing to 60px with a sand glass background (blur(18px)) and hairline. Logo, five links (underline grows from the left), language, and an ink "Reserve" pill. Below 900px a burger opens a sheet with 28px serif links.
  2. Hero. Full-bleed layered SVG: sky, sun, far headland, sea with glints, a cliff village of about 120 deterministic houses, mist, and a bougainvillea branch with a stone parapet.
  3. Booking bar. Destination segmented control (spring thumb), arrival date popover (6-column day grid, May 2027), nights and guests steppers, terracotta CTA. The result pill is announced via aria-live.
  4. Intro. Two-column story with a drop cap and a 3-figure stats row (4 houses, 86 rooms, 61% returning), plus an overlapping two-image collage.
  5. Houses. Pinned horizontal gallery of 4 properties (image, number, name, place, note, rooms, from-price).
  6. Rooms. 3-up grid of 4:5 cards.
  7. Rituals. Dark band; sticky-looking frame plus a list of four timed rituals.
  8. Guest book. Centred italic quote crossfade with progress dashes.
  9. Getting here. Two illustrated maps (Amalfi, Kyoto) with transfer notes.
  10. Footer. Newsletter, four link columns, a giant gradient wordmark, and a legal row.

6. Components and states

  • Buttons. 52px capsules (40px small). Hover darkens the fill and moves the arrow 3px; press scales to 0.96 over 120ms and springs back.
  • Segmented control. A white thumb slides with --fd-spring (420ms); options use role="radio" and aria-checked.
  • Stepper. 32px round buttons that press to 0.9; the value sits in an <output aria-live>.
  • Date popover. Selected cell is ink-filled; it opens above the bar when floating and below it when docked.
  • Room card. Whole card is a link; the image zooms and gets a bottom gradient wash on hover or focus.
  • Ritual row. aria-pressed; active row has a terracotta time and light title.
  • Newsletter. Glass capsule on night. Focus-within shows a terracotta ring. After success the button turns #E6A47F and the note reads back the address.

7. Responsive

  • ≤1080px: tighter nav gaps, narrower booking columns.
  • ≤900px: burger nav; single-column intro, rituals and footer news. The gallery becomes a native horizontal scroll-snap row (82vw cards, no pinning). Rooms and maps stack.
  • ≤760px:
  • the booking card becomes a 2-row grid (destination, then date, then nights and guests, then a full-width CTA)
  • when docked it collapses to a capsule with the destination toggle plus a short "Search" CTA
  • hero layers are re-framed (1469px wide, offset −643px) so the portrait crop keeps the sea and cliff edge
  • copy is top-aligned under the nav.
  • ≤560px: smaller collage, 32px stats, stacked newsletter.

8. Accessibility and reduced motion

  • Skip link, landmarks, aria-labelledby on sections, labelled controls, aria-live for availability, quotes, steppers and newsletter. Focus ring is 2px terracotta at 70%, offset 2px.
  • prefers-reduced-motion:
  • hero is 100vh with no layer parallax
  • houses become a swipe row
  • no clip-path reveals or scale-ins
  • testimonial autoplay is off
  • all transitions are capped at 120ms.
  • prefers-contrast: more and prefers-reduced-transparency: the booking card and nav become opaque paper with an ink ring.
  • The page fixes a host quirk: if both <html> and <body> clip overflow (which makes <body> a non-scrolling scroll container and breaks sticky), it switches <body> to overflow-x: clip and restores it on unmount.

9. Do / Don't

  • Do keep the serif at weight 400 and let size and italics carry hierarchy.
  • Do reserve terracotta for one emphasis per view: an eyebrow, an italic word, or the primary CTA.
  • Do keep imagery nearly square-cornered (4px) and controls fully round.
  • Don't add drop shadows to imagery; depth comes from parallax and overlap.
  • Don't animate layout properties; scroll effects use --p with transform and opacity only.
  • Don't run more than one autoplaying element per screen.

Assets

Photography is supplied (not generated) and lives in public/img/halcyon/: positano-sunset (hero, fades to a graded dusk copy on scroll), pool-lemons, suite, ryokan, courtyard-pool and journals, each as name.webp (1024 square), name-900.webp and name-480.webp. Cards re-use the same photographs through object-position and scale crops (see the PHOTO map in Page.tsx); always set explicit width/height, srcSet, loading="lazy" (hero is fetchPriority="high") and keep the left hero scrim for AA text contrast. Copy the files next to your page or swap in your own.

Questions about Halcyon

Boutique hotels on the Amalfi coast and in Kyoto — golden hour to dusk as you scroll. It is a free hospitality landing page for React with a matching DESIGN.md brief.

Open the page, click Copy full source or Download .tsx, and drop the single Page file into your React project. Copy the public/img folder next to it for the demo photography, or swap in your own images.

Yes. It is MIT-licensed: free for personal and commercial projects, no signup and no attribution required.

Yes. The DESIGN.md describes tokens, type scale, layout, components and motion so Claude Code, Cursor, Lovable or v0 can extend the page without drifting from its look.