Solace Travel slower. Feel further. AI website prompt
A slow-travel studio in nine sections: a preloader that flips into the hero, three pinned scroll sequences and a sticky stack of four retreats.
Now put it in WordPress
You have a site. Now make it yours.
An AI build hands you a front end and stops there. No CMS, no client login, no real ownership: you cannot hand it over, and you cannot change it without going back and re-prompting. UiChemy turns that build into native, editable WordPress you own, in the builder you already use.
Bolt to WordPressClaude to WordPressCodex to WordPressCursor to WordPressLovable to WordPressv0 to WordPressAI to WordPress
Get Started
More prompts
Showing 6 prompts
Frequently asked questions
What does the Solace Travel slower. Feel further. prompt build?
A slow-travel studio in nine sections: a preloader that flips into the hero, three pinned scroll sequences and a sticky stack of four retreats.
Which AI tools does the Solace Travel slower. Feel further. prompt work with?
It's written for Claude. Paste the whole prompt into Claude and it builds the complete site for you, then bring the result into WordPress with UiChemy. This prompt also has a WordPress version: with the UiChemy MCP connected, Claude builds the page straight into WordPress.
How do I turn the result into a WordPress site?
After the AI builds your site, UiChemy Composer rebuilds it as native, editable WordPress in Elementor or Gutenberg (Bricks coming soon) — you own it and edit it in your builder. See AI to WordPress.
Is the Solace Travel slower. Feel further. prompt free?
Yes — it's free to copy, no account needed.
Can I edit the website after it's built?
Yes. Once it's in WordPress with UiChemy, every section is native and editable — no re-prompting, no lock-in.
Copy prompt
Copy code
Coming soon
Copy SKILL.md
Coming soon
Solace Travel slower. Feel further. · Section prompt
# MASTER PROMPT — Solace — Travel slower. Feel further.
### Plain HTML, CSS and JavaScript. No WordPress, no page builder, no framework, no build step.
## ▶ HOW TO USE THIS PROMPT
**Paste this whole document into Claude. Nothing to attach.** It carries the complete source of the site plus the address of every image, so Claude can assemble a folder that owns its own assets and hand it back as a zip.
**INSTRUCTION TO CLAUDE — BUILD IT, DO NOT ASK.** Every decision is already made in this document. Do not stop to ask the user questions, do not offer options, and do not ask for confirmation before starting. Write the files, zip them, hand over the archive, then report what you built.
1. Create a folder named `solace` and write out every file in the appendix below **exactly as written** — do not reformat, rename, shorten, reorder or "improve" anything.
2. **Download the 24 images and 8 fonts into `assets/`.** The code already points at `assets/…` — nothing in it references a remote host. Run this from inside the folder; it reads the ASSET MANIFEST at the end of this document:
```bash
mkdir -p assets
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-32001b823e74.woff2' -o 'assets/32001b823e74.woff2'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-387685fe2885.woff2' -o 'assets/387685fe2885.woff2'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-460724fd1a62.woff2' -o 'assets/460724fd1a62.woff2'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-a18ee4a68cd6.woff2' -o 'assets/a18ee4a68cd6.woff2'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-a43a80ef7fa7.woff2' -o 'assets/a43a80ef7fa7.woff2'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-c466948efa95.woff2' -o 'assets/c466948efa95.woff2'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-e8e658f4ba02.woff2' -o 'assets/e8e658f4ba02.woff2'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-fc88c23021fa.woff2' -o 'assets/fc88c23021fa.woff2'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmNnXZcgRg5WKHiCZGnNbKoLTHt4oWAQj5N76cprzPQqF7.jpg' -o 'assets/qmnnxzcgrg5wkhiczgnnbkoltht4owaqj5n76cprzpqqf7.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmPouA6uaBBj4h46WS4U66NmWuKbX47SPat1tY2jTzs9i5.jpg' -o 'assets/qmpoua6uabbj4h46ws4u66nmwukbx47spat1ty2jtzs9i5.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmRPv49VnKwjZvSMaiywXMeGg8b3T9X2FqFCL4E4DhWfCB.jpg' -o 'assets/qmrpv49vnkwjzvsmaiywxmegg8b3t9x2fqfcl4e4dhwfcb.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmRwoR8Q7JdizxfS1mZ8Rc4qXBBcibimjkWexuzUsUgUzK.jpg' -o 'assets/qmrwor8q7jdizxfs1mz8rc4qxbbcibimjkwexuzusuguzk.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmSQVib63UCXCJidnt3D3C6m82pBfNDR559J1ZiaYkZuTU.jpg' -o 'assets/qmsqvib63ucxcjidnt3d3c6m82pbfndr559j1ziaykzutu.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmSvZycEM8UikqGSY8VB6vPrCJKgTJGUg3PiDTLZ7TCrRR.jpg' -o 'assets/qmsvzycem8uikqgsy8vb6vprcjkgtjgug3pidtlz7tcrrr.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmTs6jC8TXZHmvDSETmSYGnCNYNiWzppfjjrZXXPwnvBrs.jpg' -o 'assets/qmts6jc8txzhmvdsetmsygncnyniwzppfjjrzxxpwnvbrs.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmTsmwNeUjHSBQEX28gawuDqnZkAdJ2WSLbKkXFNo4Jb7h.jpg' -o 'assets/qmtsmwneujhsbqex28gawudqnzkadj2wslbkkxfno4jb7h.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmTz2VuSivJ3qwi1i1vDztoaDSzj8gjm3xKGgUD7Qmy3Uj.jpg' -o 'assets/qmtz2vusivj3qwi1i1vdztoadszj8gjm3xkggud7qmy3uj.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmUTeGqxmW9nyPks6KEJBiAhXvYWKGQ856LnbX6iwBQ1ZZ.jpg' -o 'assets/qmutegqxmw9nypks6kejbiahxvywkgq856lnbx6iwbq1zz.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmUu6NgJVtKUtuaQnnXwVKZ3XPhRfeHj57kmLVUYKX2Kf1.jpg' -o 'assets/qmuu6ngjvtkutuaqnnxwvkz3xphrfehj57kmlvuykx2kf1.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmV8bNtpK1sLQm8CXSUniTcg3whBEoZgiXzrAESmkNdD3i.jpg' -o 'assets/qmv8bntpk1slqm8cxsunitcg3whbeozgixzraesmkndd3i.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmWoJ44Cmh4dC37Bj3x35VYRoKZ3piVxaZsscheZRwz8RH.jpg' -o 'assets/qmwoj44cmh4dc37bj3x35vyrokz3pivxazsschezrwz8rh.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmXD6fykET5mtufUQkfcKyvx1g4cgA9BD72NrjzUfMFWh8.jpg' -o 'assets/qmxd6fyket5mtufuqkfckyvx1g4cga9bd72nrjzufmfwh8.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmXw8XxZHvESeMzFbPyfwxH7rKTAXiTJ2pYQgiLDb8e7CS.jpg' -o 'assets/qmxw8xxzhvesemzfbpyfwxh7rktaxitj2pyqgildb8e7cs.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmY9ebRZeB7m6Np5wTHiaumbFaHEMPzrcXhcgi4XmmDFGV.jpg' -o 'assets/qmy9ebrzeb7m6np5wthiaumbfahempzrcxhcgi4xmmdfgv.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmYAMXr8B2jLFxbDeNmbRqD9egsdHqLg57i82UTDxrjkWJ.jpg' -o 'assets/qmyamxr8b2jlfxbdenmbrqd9egsdhqlg57i82utdxrjkwj.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmZ42qsPKJVEUuQuVaGf4xwzmvAFVJpmTRrjigYr6XUZQN.jpg' -o 'assets/qmz42qspkjveuuquvagf4xwzmvafvjpmtrrjigyr6xuzqn.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmcCrB9jkXYsFxqxAv7djQwxfxXCifJdQApLHDtDh6vWg1.jpg' -o 'assets/qmccrb9jkxysfxqxav7djqwxfxxcifjdqaplhdtdh6vwg1.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmdvjZNXQWkbd7Chff18zeAnGtuxp6nbnUygjbVTgG9qBi.jpg' -o 'assets/qmdvjznxqwkbd7chff18zeangtuxp6nbnuygjbvtgg9qbi.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmeNikyHVAnh2GBvDwfDKDyEnQYYdNvPzgneBqSkG5oPYD.jpg' -o 'assets/qmenikyhvanh2gbvdwfdkdyenqyydnvpzgnebqskg5opyd.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmegeQkkT1CFjDcnJEjEgTrhK43a8EXqgvJYofSgvVF7dy.jpg' -o 'assets/qmegeqkkt1cfjdcnjejegtrhk43a8exqgvjyofsgvvf7dy.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmevohDT63YqQwxZqkgHcWrPWBjMzcGqJ24cM7cHFrCAJq.jpg' -o 'assets/qmevohdt63yqqwxzqkghcwrpwbjmzcgqj24cm7chfrcajq.jpg'
curl -fsSL 'https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmewWkucjt6k9RqKN1Z49W7ruM7y6SXE7Gia6F5i8vi39J.jpg' -o 'assets/qmewwkucjt6k9rqkn1z49w7rum7y6sxe7gia6f5i8vi39j.jpg'
```
Then check every file arrived and none is a zero-byte or HTML error page:
```bash
ls assets | wc -l # expect 32 files
find assets -size -1k # expect no output
```
If any download fails, retry it — hosts occasionally drop a request. Do **not** fall back to hotlinking the remote URL: the whole point is a folder that works offline.
3. **Zip that folder and give the user the archive.** Its contents:
```
solace/
index.html
fonts/fonts.css
script.js
style.css
assets/
32001b823e74.woff2
387685fe2885.woff2
460724fd1a62.woff2
a18ee4a68cd6.woff2
… 28 more
```
```bash
zip -r solace.zip solace -x '*.DS_Store'
```
Then hand over `solace.zip` as a file the user can download, not as text in the chat.
4. Before handing it over, serve the folder and confirm it runs:
```bash
cd solace && python3 -m http.server 8000 # then open http://localhost:8000
```
Check the page renders, the images load from `assets/` with the network throttled or offline, and scrolling drives the animations.
The finished folder needs no network for its images — they are in `assets/`; only the motion libraries load from a CDN.
Do not rebuild the design from the description below. The description is there so a person can understand and edit the code. The appendix is the deliverable.
---
## ▶ WHAT YOU GET
Solace plans small-group slow-travel journeys and remote retreats for eight guests at most — from Cappadocia to the Atacama.
**4 code files plus an `assets/` folder holding 24 images and 8 fonts, delivered as `solace.zip`.**
| File | What it is |
|---|---|
| `index.html` | a page, 44 KB |
| `fonts/fonts.css` | the stylesheet, 3 KB |
| `script.js` | the script, 58 KB |
| `style.css` | the stylesheet, 47 KB |
| `assets/` | 24 images and 8 fonts, downloaded at build time |
**Libraries, from a CDN, already referenced in the HTML:** `gsap.min.js`, `ScrollTrigger.min.js`, `SplitText.min.js`, `Flip.min.js`, `Observer.min.js`, `lenis.min.js`. No npm install, no bundler, no framework.
**Sections (9):** `#top` · `#manifesto` · `#journeys` · `#arrive` · `#retreats` · `#experiences` · `#places` · `#journal` · `#plan`.
---
## ▶ WHERE THE MEDIA COMES FROM
All 32 files (24 images and 8 fonts) live in `assets/` inside the folder, and every reference in the markup, the CSS and the script is a relative `assets/…` path. Once the folder is assembled it owns its own media: no CDN, no media library, no external image host. Open it on a laptop with the wifi off and it still renders.
They are fetched **once, at build time**, from the WordPress media library on `demo.uichemy.com` — the same `solace__` files the WordPress build of this page uses. That is only how they travel into the folder; nothing in the delivered code points back at that host. Replacing an image means dropping a new file into `assets/` under the same name.
The manifest at the end of this document lists every source URL against the filename it is saved as, so the download step is reproducible and checkable.
---
## ▶ HOW THE MOTION IS PUT TOGETHER
Nearly every visual effect is scroll-driven: 123 GSAP calls and 30 ScrollTrigger attachments.
- Reveals generally run both ways, so scrolling back up replays them in reverse rather than leaving them finished.
- Pinned sections own their scroll distance in the markup, so the document height is authored rather than grown by script.
- `prefers-reduced-motion` is honoured: every animated block checks it and falls back to the finished state, so the page stays readable with motion off.
- The numbers are inline — durations, eases, stagger, parallax speeds. There is no config layer and no control panel; it is plain GSAP.
---
## ▶ DESIGN TOKENS
Everything is driven from custom properties at the top of the stylesheet:
`--paper` `--paper-2` `--ink` `--muted` `--muted-strong` `--accent` `--olive` `--sand` `--rule` `--rule-light` `--serif` `--sans` `--fs-display` `--fs-h2` `--fs-h3` `--fs-lead` `--fs-body` `--fs-small` `--fs-label` `--gutter` `--section-y` `--radius` `--nav-h` `--ease-out`
---
## ▶ WHAT THIS PROMPT DELIBERATELY DOES NOT CONTAIN
- No WordPress, no page builder, and none of the wrapper classes or compatibility CSS a builder needs. The WordPress build is a separate prompt.
- No framework, no TypeScript, no bundler, no `package.json`.
- No backend. Any form here validates and replies in place; it posts nowhere.
- No CMS. The content lives in the files.
- No external image host. The images ship inside the zip; nothing in the code calls out to a media library at runtime.
---
## ASSET MANIFEST — download these into `assets/`
Source is the media library on `demo.uichemy.com`. Save each one under the name in the second column; the code already refers to it by that name.
| Download from | Save as |
|---|---|
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-32001b823e74.woff2` | `assets/32001b823e74.woff2` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-387685fe2885.woff2` | `assets/387685fe2885.woff2` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-460724fd1a62.woff2` | `assets/460724fd1a62.woff2` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-a18ee4a68cd6.woff2` | `assets/a18ee4a68cd6.woff2` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-a43a80ef7fa7.woff2` | `assets/a43a80ef7fa7.woff2` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-c466948efa95.woff2` | `assets/c466948efa95.woff2` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-e8e658f4ba02.woff2` | `assets/e8e658f4ba02.woff2` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-fc88c23021fa.woff2` | `assets/fc88c23021fa.woff2` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmNnXZcgRg5WKHiCZGnNbKoLTHt4oWAQj5N76cprzPQqF7.jpg` | `assets/qmnnxzcgrg5wkhiczgnnbkoltht4owaqj5n76cprzpqqf7.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmPouA6uaBBj4h46WS4U66NmWuKbX47SPat1tY2jTzs9i5.jpg` | `assets/qmpoua6uabbj4h46ws4u66nmwukbx47spat1ty2jtzs9i5.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmRPv49VnKwjZvSMaiywXMeGg8b3T9X2FqFCL4E4DhWfCB.jpg` | `assets/qmrpv49vnkwjzvsmaiywxmegg8b3t9x2fqfcl4e4dhwfcb.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmRwoR8Q7JdizxfS1mZ8Rc4qXBBcibimjkWexuzUsUgUzK.jpg` | `assets/qmrwor8q7jdizxfs1mz8rc4qxbbcibimjkwexuzusuguzk.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmSQVib63UCXCJidnt3D3C6m82pBfNDR559J1ZiaYkZuTU.jpg` | `assets/qmsqvib63ucxcjidnt3d3c6m82pbfndr559j1ziaykzutu.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmSvZycEM8UikqGSY8VB6vPrCJKgTJGUg3PiDTLZ7TCrRR.jpg` | `assets/qmsvzycem8uikqgsy8vb6vprcjkgtjgug3pidtlz7tcrrr.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmTs6jC8TXZHmvDSETmSYGnCNYNiWzppfjjrZXXPwnvBrs.jpg` | `assets/qmts6jc8txzhmvdsetmsygncnyniwzppfjjrzxxpwnvbrs.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmTsmwNeUjHSBQEX28gawuDqnZkAdJ2WSLbKkXFNo4Jb7h.jpg` | `assets/qmtsmwneujhsbqex28gawudqnzkadj2wslbkkxfno4jb7h.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmTz2VuSivJ3qwi1i1vDztoaDSzj8gjm3xKGgUD7Qmy3Uj.jpg` | `assets/qmtz2vusivj3qwi1i1vdztoadszj8gjm3xkggud7qmy3uj.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmUTeGqxmW9nyPks6KEJBiAhXvYWKGQ856LnbX6iwBQ1ZZ.jpg` | `assets/qmutegqxmw9nypks6kejbiahxvywkgq856lnbx6iwbq1zz.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmUu6NgJVtKUtuaQnnXwVKZ3XPhRfeHj57kmLVUYKX2Kf1.jpg` | `assets/qmuu6ngjvtkutuaqnnxwvkz3xphrfehj57kmlvuykx2kf1.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmV8bNtpK1sLQm8CXSUniTcg3whBEoZgiXzrAESmkNdD3i.jpg` | `assets/qmv8bntpk1slqm8cxsunitcg3whbeozgixzraesmkndd3i.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmWoJ44Cmh4dC37Bj3x35VYRoKZ3piVxaZsscheZRwz8RH.jpg` | `assets/qmwoj44cmh4dc37bj3x35vyrokz3pivxazsschezrwz8rh.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmXD6fykET5mtufUQkfcKyvx1g4cgA9BD72NrjzUfMFWh8.jpg` | `assets/qmxd6fyket5mtufuqkfckyvx1g4cga9bd72nrjzufmfwh8.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmXw8XxZHvESeMzFbPyfwxH7rKTAXiTJ2pYQgiLDb8e7CS.jpg` | `assets/qmxw8xxzhvesemzfbpyfwxh7rktaxitj2pyqgildb8e7cs.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmY9ebRZeB7m6Np5wTHiaumbFaHEMPzrcXhcgi4XmmDFGV.jpg` | `assets/qmy9ebrzeb7m6np5wthiaumbfahempzrcxhcgi4xmmdfgv.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmYAMXr8B2jLFxbDeNmbRqD9egsdHqLg57i82UTDxrjkWJ.jpg` | `assets/qmyamxr8b2jlfxbdenmbrqd9egsdhqlg57i82utdxrjkwj.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmZ42qsPKJVEUuQuVaGf4xwzmvAFVJpmTRrjigYr6XUZQN.jpg` | `assets/qmz42qspkjveuuquvagf4xwzmvafvjpmtrrjigyr6xuzqn.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmcCrB9jkXYsFxqxAv7djQwxfxXCifJdQApLHDtDh6vWg1.jpg` | `assets/qmccrb9jkxysfxqxav7djqwxfxxcifjdqaplhdtdh6vwg1.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmdvjZNXQWkbd7Chff18zeAnGtuxp6nbnUygjbVTgG9qBi.jpg` | `assets/qmdvjznxqwkbd7chff18zeangtuxp6nbnuygjbvtgg9qbi.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmeNikyHVAnh2GBvDwfDKDyEnQYYdNvPzgneBqSkG5oPYD.jpg` | `assets/qmenikyhvanh2gbvdwfdkdyenqyydnvpzgnebqskg5opyd.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmegeQkkT1CFjDcnJEjEgTrhK43a8EXqgvJYofSgvVF7dy.jpg` | `assets/qmegeqkkt1cfjdcnjejegtrhk43a8exqgvjyofsgvvf7dy.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmevohDT63YqQwxZqkgHcWrPWBjMzcGqJ24cM7cHFrCAJq.jpg` | `assets/qmevohdt63yqqwxzqkghcwrpwbjmzcgqj24cm7chfrcajq.jpg` |
| `https://demo.uichemy.com/wp-content/uploads/2026/09/sl-QmewWkucjt6k9RqKN1Z49W7ruM7y6SXE7Gia6F5i8vi39J.jpg` | `assets/qmewwkucjt6k9rqkn1z49w7rum7y6sxe7gia6f5i8vi39j.jpg` |
---
# APPENDIX · VERBATIM SOURCE
The complete source of every file. **Write them out exactly** — do not change a value, a comment, a class name, or the order of a declaration. Recreate this file layout:
```
index.html
fonts/fonts.css
script.js
style.css
```
## `index.html`
```html
<!doctype html>
<html lang="en" class="no-js">
<head>
<script>if("scrollRestoration" in history)history.scrollRestoration="manual";window.scrollTo(0,0);</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Solace — Travel slower. Feel further.</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect width='24' height='24' rx='6' fill='%23FFFFFF'/%3E%3Cpath d='M5 15a7 7 0 0 1 14 0z' fill='%231C1C1C'/%3E%3Crect x='2' y='16.6' width='20' height='1.7' rx='.85' fill='%231C1C1C'/%3E%3Crect x='7' y='19.9' width='10' height='1.7' rx='.85' fill='%231C1C1C'/%3E%3C/svg%3E" />
<meta name="description" content="Solace plans small-group slow-travel journeys and remote retreats for eight guests at most — from Cappadocia to the Atacama.">
<meta name="theme-color" content="#FFFFFF">
<link href="fonts/fonts.css" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<!-- GSAP core loads in the head so we know, before first paint, whether the animated
experience (preloader etc.) can run. Everything else loads at the end of <body>. -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<script>
(function (d) {
var r = d.documentElement;
r.classList.remove('no-js');
r.classList.add('js');
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (window.gsap && !reduce) r.classList.add('is-anim');
})(document);
</script>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<!-- Film grain + custom cursor (decorative) -->
<div class="grain" aria-hidden="true"></div>
<div class="cursor" aria-hidden="true"><span class="cursor__label"></span></div>
<!-- =========================================================
01 · PRELOADER
========================================================= -->
<div class="preloader" aria-hidden="true">
<div class="preloader__stack">
<div class="preloader__card"><img src="assets/qmxw8xxzhvesemzfbpyfwxh7rktaxitj2pyqgildb8e7cs.jpg" alt=""></div>
<div class="preloader__card"><img src="assets/qmevohdt63yqqwxzqkghcwrpwbjmzcgqj24cm7chfrcajq.jpg" alt=""></div>
<div class="preloader__card"><img src="assets/qmnnxzcgrg5wkhiczgnnbkoltht4owaqj5n76cprzpqqf7.jpg" alt=""></div>
<div class="preloader__card"><img src="assets/qmuu6ngjvtkutuaqnnxwvkz3xphrfehj57kmlvuykx2kf1.jpg" alt=""></div>
<div class="preloader__card"><img src="assets/qmwoj44cmh4dc37bj3x35vyrokz3pivxazsschezrwz8rh.jpg" alt=""></div>
<div class="preloader__card preloader__card--hero"><img src="assets/qmsqvib63ucxcjidnt3d3c6m82pbfndr559j1ziaykzutu.jpg" alt=""></div>
</div>
<p class="preloader__word"><span>Solace</span></p>
<p class="preloader__count"><span class="preloader__num">0</span><span class="preloader__pct">%</span></p>
<p class="preloader__tag">Travel slower. Feel further.</p>
</div>
<!-- =========================================================
02 · NAV
========================================================= -->
<header class="nav">
<nav class="nav__left" aria-label="Primary">
<a href="#journeys">Journeys</a>
<a href="#retreats">Retreats</a>
<a href="#journal">Journal</a>
</nav>
<a class="nav__brand" href="#top" aria-label="Solace — back to top"><svg class="brand-mark" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 15a7 7 0 0 1 14 0z" fill="currentColor"/><rect x="2" y="16.6" width="20" height="1.7" rx=".85" fill="currentColor"/><rect x="7" y="19.9" width="10" height="1.7" rx=".85" fill="currentColor"/></svg><span>Solace</span></a>
<div class="nav__right">
<a class="nav__cta" href="#plan">Plan a journey</a>
<button class="nav__toggle" type="button" aria-expanded="false" aria-controls="menu">
<span class="nav__toggle-text">Menu</span>
<span class="nav__toggle-lines" aria-hidden="true"><i></i><i></i></span>
</button>
</div>
</header>
<div class="menu" id="menu" hidden>
<nav class="menu__inner" aria-label="Mobile">
<ul class="menu__list">
<li><a href="#journeys"><span>Journeys</span></a></li>
<li><a href="#retreats"><span>Retreats</span></a></li>
<li><a href="#experiences"><span>Experiences</span></a></li>
<li><a href="#journal"><span>Journal</span></a></li>
<li><a href="#plan"><span><em>Plan a journey</em></span></a></li>
</ul>
<p class="menu__foot">[email protected] · +44 20 0000 0000</p>
</nav>
</div>
<main id="main">
<!-- =========================================================
03 · HERO
========================================================= -->
<section class="hero" id="top" aria-labelledby="hero-title">
<div class="hero__media">
<img class="hero__img" src="assets/qmsqvib63ucxcjidnt3d3c6m82pbfndr559j1ziaykzutu.jpg" alt="A solitary whitewashed retreat above a misty Atlantic cove at sunrise" fetchpriority="high">
<div class="hero__veil" aria-hidden="true"></div>
</div>
<div class="hero__inner">
<p class="label hero__label"><span class="label__num">(01)</span> Small-group journeys & remote retreats</p>
<h1 class="hero__title" id="hero-title">
<span class="hero__row hero__row--a">Travel slower.</span>
<span class="hero__row hero__row--b">Feel <em>further.</em></span>
</h1>
<div class="hero__foot">
<p class="hero__lede">Unhurried journeys for eight guests at most — to places that ask you to stay a little longer than you planned.</p>
<p class="hero__ticker" aria-hidden="true">
<span class="ticker__label">Now in</span>
<span class="ticker__slot"><span class="ticker__place">Cappadocia</span></span>
<span class="ticker__slot ticker__meta"><span class="ticker__coord">38.64°N 34.83°E</span></span>
<span class="ticker__slot ticker__meta"><span class="ticker__time">--:--</span></span>
</p>
<p class="hero__scroll"><span>Scroll to wander</span><span class="hero__scroll-line" aria-hidden="true"></span></p>
</div>
</div>
</section>
<!-- =========================================================
04 · MANIFESTO
========================================================= -->
<section class="manifesto section" id="manifesto" aria-labelledby="manifesto-label">
<div class="container">
<div class="mf">
<div class="mf__copy">
<p class="label" id="manifesto-label"><span class="label__num">(02)</span> Manifesto</p>
<h2 class="mf__title"><span class="mf__line">Measured in</span> <span class="mf__line"><em>mornings,</em> not miles.</span></h2>
<p class="mf__lead">The hour the fog lifts off a fjord. The slow walk to a bakery nobody has reviewed. The conversation that runs past midnight. So we travel in small circles, stay longer than itineraries allow, and leave room for the unplanned.</p>
<p class="mf__sign"><span>Inês & Tomás</span> Founders, Solace</p>
</div>
<div class="mf__media" aria-hidden="true">
<figure class="mf__pic mf__pic--main"><img src="assets/qmegeqkkt1cfjdcnjejegtrhk43a8exqgvjyofsgvvf7dy.jpg" alt="" loading="lazy"></figure>
<figure class="mf__pic mf__pic--small"><img src="assets/qmxd6fyket5mtufuqkfckyvx1g4cga9bd72nrjzufmfwh8.jpg" alt="" loading="lazy"></figure>
<span class="mf__tag">First light · 06:40</span>
</div>
</div>
</div>
</section>
<!-- =========================================================
05 · JOURNEYS (pinned horizontal gallery)
========================================================= -->
<section class="journeys" id="journeys" aria-labelledby="journeys-title">
<div class="journeys__pin">
<header class="journeys__head container">
<div>
<p class="label"><span class="label__num">(03)</span> Journeys</p>
<h2 class="h2" id="journeys-title">Six journeys, <em>no rush.</em></h2>
</div>
<div class="journeys__meta">
<p class="journeys__count" aria-hidden="true"><span class="journeys__current">01</span> / 06</p>
<div class="journeys__progress" aria-hidden="true"><span class="journeys__bar"></span></div>
</div>
</header>
<div class="journeys__viewport">
<ol class="journeys__track">
<li class="jcard">
<div class="jcard__media"><img src="assets/qmrwor8q7jdizxfs1mz8rc4qxbbcibimjkwexuzusuguzk.jpg" alt="Hot-air balloons drifting over Cappadocia's rock chimneys at sunset" loading="lazy" draggable="false"></div>
<div class="jcard__body">
<p class="jcard__top"><span class="jcard__num">01</span><span>Cappadocia, Türkiye</span></p>
<h3 class="jcard__title">The Chimney Hours</h3>
<dl class="jcard__facts"><div><dt>Nights</dt><dd>7</dd></div><div><dt>From</dt><dd>€4,280</dd></div></dl>
</div>
</li>
<li class="jcard">
<div class="jcard__media"><img src="assets/qmccrb9jkxysfxqxav7djqwxfxxcifjdqaplhdtdh6vwg1.jpg" alt="A lone walker crossing a vast, empty desert" loading="lazy" draggable="false"></div>
<div class="jcard__body">
<p class="jcard__top"><span class="jcard__num">02</span><span>Atacama, Chile</span></p>
<h3 class="jcard__title">Salt & Silence</h3>
<dl class="jcard__facts"><div><dt>Nights</dt><dd>9</dd></div><div><dt>From</dt><dd>€5,640</dd></div></dl>
</div>
</li>
<li class="jcard">
<div class="jcard__media"><img src="assets/qmewwkucjt6k9rqkn1z49w7rum7y6sxe7gia6f5i8vi39j.jpg" alt="A quiet cove of turquoise water beneath pale cliffs" loading="lazy" draggable="false"></div>
<div class="jcard__body">
<p class="jcard__top"><span class="jcard__num">03</span><span>Lycian Coast, Türkiye</span></p>
<h3 class="jcard__title">The Slow Coast</h3>
<dl class="jcard__facts"><div><dt>Nights</dt><dd>8</dd></div><div><dt>From</dt><dd>€3,960</dd></div></dl>
</div>
</li>
<li class="jcard">
<div class="jcard__media"><img src="assets/qmenikyhvanh2gbvdwfdkdyenqyydnvpzgnebqskg5opyd.jpg" alt="A snow-covered cabin beside a frozen lake at blue hour" loading="lazy" draggable="false"></div>
<div class="jcard__body">
<p class="jcard__top"><span class="jcard__num">04</span><span>Lofoten, Norway</span></p>
<h3 class="jcard__title">Blue Hour North</h3>
<dl class="jcard__facts"><div><dt>Nights</dt><dd>6</dd></div><div><dt>From</dt><dd>€4,720</dd></div></dl>
</div>
</li>
<li class="jcard">
<div class="jcard__media"><img src="assets/qmyamxr8b2jlfxbdenmbrqd9egsdhqlg57i82utdxrjkwj.jpg" alt="Red-rock desert canyon glowing ochre in low evening light" loading="lazy" draggable="false"></div>
<div class="jcard__body">
<p class="jcard__top"><span class="jcard__num">05</span><span>Wadi Rum, Jordan</span></p>
<h3 class="jcard__title">A Sea of Ochre</h3>
<dl class="jcard__facts"><div><dt>Nights</dt><dd>5</dd></div><div><dt>From</dt><dd>€2,890</dd></div></dl>
</div>
</li>
<li class="jcard">
<div class="jcard__media"><img src="assets/qmy9ebrzeb7m6np5wthiaumbfahempzrcxhcgi4xmmdfgv.jpg" alt="Sunset light falling across rugged sea cliffs" loading="lazy" draggable="false"></div>
<div class="jcard__body">
<p class="jcard__top"><span class="jcard__num">06</span><span>Azores, Portugal</span></p>
<h3 class="jcard__title">Edge of the Atlantic</h3>
<dl class="jcard__facts"><div><dt>Nights</dt><dd>7</dd></div><div><dt>From</dt><dd>€3,410</dd></div></dl>
</div>
</li>
<li class="jcard jcard--end">
<p class="jcard__end-kicker">Can’t choose?</p>
<p class="jcard__end-title">We’ll write one <em>around you.</em></p>
<a class="link-arrow" href="#plan">Start a private journey</a>
</li>
</ol>
</div>
</div>
</section>
<!-- =========================================================
06 · ZOOM-THROUGH
========================================================= -->
<section class="zoom" id="arrive" aria-labelledby="zoom-title">
<div class="zoom__stage">
<p class="zoom__side zoom__side--l" aria-hidden="true">Leave the noise</p>
<p class="zoom__side zoom__side--r" aria-hidden="true"><em>at the door</em></p>
<div class="zoom__window">
<img src="assets/qmutegqxmw9nypks6kejbiahxvywkgq856lnbx6iwbq1zz.jpg" alt="An aerial view of a misty forest canopy at dawn" loading="lazy">
<span class="zoom__veil" aria-hidden="true"></span>
</div>
<div class="zoom__content">
<p class="label"><span class="label__num">(04)</span> Arrive</p>
<h2 class="zoom__title" id="zoom-title">Arrive somewhere <em>quieter.</em></h2>
<p class="zoom__text">Between journeys, our retreats hold still. Four houses at the edges of maps, each with room for a handful of guests and nothing to prove.</p>
</div>
</div>
</section>
<!-- =========================================================
07 · RETREATS (stacked sticky cards)
========================================================= -->
<section class="retreats" id="retreats" aria-labelledby="retreats-title">
<header class="container retreats__head">
<p class="label"><span class="label__num">(05)</span> Retreats</p>
<h2 class="h2" id="retreats-title">Four houses, <em>one pace.</em></h2>
</header>
<div class="retreats__stack">
<article class="retreat" data-tone="sand">
<div class="retreat__inner">
<div class="retreat__media"><img src="assets/qmrpv49vnkwjzvsmaiywxmegg8b3t9x2fqfcl4e4dhwfcb.jpg" alt="A whitewashed house beneath limestone cliffs above the sea" loading="lazy"></div>
<div class="retreat__body">
<p class="retreat__index">R.01 <span>—</span> Menorca, Spain</p>
<h3 class="retreat__title">Casa <em>Alta</em></h3>
<p class="retreat__desc">Five whitewashed suites cut into the limestone above a cove only fishermen used to know. Swim before breakfast, read until the light goes amber.</p>
<ul class="retreat__list"><li>5 suites</li><li>Sea-cut pool</li><li>Private chef</li></ul>
<p class="retreat__price">From €680 a night · 4-night minimum</p>
<a class="link-arrow" href="#plan">Reserve a stay</a>
</div>
<span class="retreat__shade" aria-hidden="true"></span>
</div>
</article>
<article class="retreat" data-tone="olive">
<div class="retreat__inner">
<div class="retreat__media"><img src="assets/qmpoua6uabbj4h46ws4u66nmwukbx47spat1ty2jtzs9i5.jpg" alt="A secluded log cabin set in a wide mountain landscape" loading="lazy"></div>
<div class="retreat__body">
<p class="retreat__index">R.02 <span>—</span> Dolomites, Italy</p>
<h3 class="retreat__title">Hütte <em>Nebel</em></h3>
<p class="retreat__desc">A larch-wood cabin two hours’ walk from the nearest road. Wood-fired sauna, a library of mountain writing, and a sky with no light in it but stars.</p>
<ul class="retreat__list"><li>3 rooms</li><li>Wood sauna</li><li>Guided ridge walks</li></ul>
<p class="retreat__price">From €520 a night · 3-night minimum</p>
<a class="link-arrow" href="#plan">Reserve a stay</a>
</div>
<span class="retreat__shade" aria-hidden="true"></span>
</div>
</article>
<article class="retreat" data-tone="paper">
<div class="retreat__inner">
<div class="retreat__media"><img src="assets/qmts6jc8txzhmvdsetmsygncnyniwzppfjjrzxxpwnvbrs.jpg" alt="A wooden bathhouse on a misty, forested shore at dusk" loading="lazy"></div>
<div class="retreat__body">
<p class="retreat__index">R.03 <span>—</span> Hokkaido, Japan</p>
<h3 class="retreat__title">Umi <em>Baths</em></h3>
<p class="retreat__desc">A quiet bathhouse retreat on a forested shore. Mineral pools, seasonal kaiseki, and long evenings spent doing precisely nothing, beautifully.</p>
<ul class="retreat__list"><li>6 rooms</li><li>Onsen pools</li><li>Tea ceremony</li></ul>
<p class="retreat__price">From €740 a night · 3-night minimum</p>
<a class="link-arrow" href="#plan">Reserve a stay</a>
</div>
<span class="retreat__shade" aria-hidden="true"></span>
</div>
</article>
<article class="retreat" data-tone="ink">
<div class="retreat__inner">
<div class="retreat__media"><img src="assets/qmtsmwneujhsbqex28gawudqnzkadj2wslbkkxfno4jb7h.jpg" alt="An open-sided tropical wooden retreat surrounded by greenery" loading="lazy"></div>
<div class="retreat__body">
<p class="retreat__index">R.04 <span>—</span> Kerala, India</p>
<h3 class="retreat__title">Kaluna <em>House</em></h3>
<p class="retreat__desc">Teak pavilions in a spice garden, a short paddle from the backwaters. Ayurvedic mornings, monsoon-green afternoons, and supper under the fans.</p>
<ul class="retreat__list"><li>4 pavilions</li><li>Ayurveda</li><li>Backwater canoe</li></ul>
<p class="retreat__price">From €460 a night · 5-night minimum</p>
<a class="link-arrow" href="#plan">Reserve a stay</a>
</div>
<span class="retreat__shade" aria-hidden="true"></span>
</div>
</article>
</div>
</section>
<!-- =========================================================
08 · EXPERIENCES (masonry + Flip lightbox)
========================================================= -->
<section class="experiences section" id="experiences" aria-labelledby="exp-title">
<div class="container">
<header class="section-head">
<p class="label"><span class="label__num">(06)</span> Experiences</p>
<h2 class="h2" id="exp-title">Small rituals, <em>long memories.</em></h2>
<p class="section-head__text">Every journey is built around a few moments we won’t schedule tightly — and a great many we won’t schedule at all.</p>
</header>
<ul class="exp__grid">
<li class="exp__item" style="--ar: 4 / 5">
<button class="exp__btn" type="button" data-caption="Dawn swim above the Indian Ocean — Uluwatu, Bali">
<span class="exp__frame"><img src="assets/qmdvjznxqwkbd7chff18zeangtuxp6nbnuygjbvtgg9qbi.jpg" alt="An infinity pool overlooking a wide blue ocean" loading="lazy"></span>
<span class="exp__cap"><span class="exp__num">01</span> Dawn swims</span>
</button>
</li>
<li class="exp__item" style="--ar: 1 / 1">
<button class="exp__btn" type="button" data-caption="Steam, stone and cedar — the bathhouse at Umi">
<span class="exp__frame"><img src="assets/qmtz2vusivj3qwi1i1vdztoadszj8gjm3xkggud7qmy3uj.jpg" alt="A serene spa setting with soft light and natural materials" loading="lazy"></span>
<span class="exp__cap"><span class="exp__num">02</span> Bathhouse evenings</span>
</button>
</li>
<li class="exp__item" style="--ar: 3 / 4">
<button class="exp__btn" type="button" data-caption="The walk to the edge, before anyone else is awake — São Miguel">
<span class="exp__frame"><img src="assets/qmnnxzcgrg5wkhiczgnnbkoltht4owaqj5n76cprzpqqf7.jpg" alt="A solitary figure standing at the edge of a coastal cliff" loading="lazy"></span>
<span class="exp__cap"><span class="exp__num">03</span> Cliff-top walks</span>
</button>
</li>
<li class="exp__item" style="--ar: 4 / 3">
<button class="exp__btn" type="button" data-caption="A lake that keeps its own hours — Hütte Nebel">
<span class="exp__frame"><img src="assets/qmevohdt63yqqwxzqkghcwrpwbjmzcgqj24cm7chfrcajq.jpg" alt="A wooden cabin beside a still, reflective lake" loading="lazy"></span>
<span class="exp__cap"><span class="exp__num">04</span> Lakeside mornings</span>
</button>
</li>
<li class="exp__item" style="--ar: 4 / 5">
<button class="exp__btn" type="button" data-caption="Sitting with the dunes until the colour drains — Wadi Rum">
<span class="exp__frame"><img src="assets/qmwoj44cmh4dc37bj3x35vyrokz3pivxazsschezrwz8rh.jpg" alt="A person in quiet contemplation among golden dunes" loading="lazy"></span>
<span class="exp__cap"><span class="exp__num">05</span> Desert stillness</span>
</button>
</li>
<li class="exp__item" style="--ar: 1 / 1">
<button class="exp__btn" type="button" data-caption="Linen, teak, and nowhere to be — Kaluna House">
<span class="exp__frame"><img src="assets/qmv8bntpk1slqm8cxsunitcg3whbeozgixzraesmkndd3i.jpg" alt="A calm, modern tropical bedroom with natural textures" loading="lazy"></span>
<span class="exp__cap"><span class="exp__num">06</span> Long lie-ins</span>
</button>
</li>
<li class="exp__item" style="--ar: 3 / 4">
<button class="exp__btn" type="button" data-caption="Forest bathing in the literal sense — Yakushima">
<span class="exp__frame"><img src="assets/qmuu6ngjvtkutuaqnnxwvkz3xphrfehj57kmlvuykx2kf1.jpg" alt="Soft mist moving through a quiet green forest" loading="lazy"></span>
<span class="exp__cap"><span class="exp__num">07</span> Forest hours</span>
</button>
</li>
<li class="exp__item" style="--ar: 4 / 3">
<button class="exp__btn" type="button" data-caption="The pool at Casa Alta, the moment before the sun drops">
<span class="exp__frame"><img src="assets/qmsvzycem8uikqgsy8vb6vprcjkgtjgug3pidtlz7tcrrr.jpg" alt="The sea glowing gold at sunset" loading="lazy"></span>
<span class="exp__cap"><span class="exp__num">08</span> Golden-hour swims</span>
</button>
</li>
</ul>
</div>
<div class="lightbox" role="dialog" aria-modal="true" aria-label="Expanded photograph" hidden>
<div class="lightbox__backdrop"></div>
<div class="lightbox__frame"></div>
<p class="lightbox__caption"></p>
<button class="lightbox__close" type="button">Close <span aria-hidden="true">✕</span></button>
</div>
</section>
<!-- =========================================================
09 · PLACES MARQUEE
========================================================= -->
<section class="places" id="places" aria-labelledby="places-label">
<div class="container"><p class="label" id="places-label"><span class="label__num">(07)</span> Where we wander</p></div>
<div class="places__row" data-dir="-1">
<div class="places__track">
<p class="places__group">
<span>Cappadocia</span><i></i><span>Atacama</span><i></i><span>Lofoten</span><i></i><span>Wadi Rum</span><i></i><span>Azores</span><i></i><span>Hokkaido</span><i></i>
</p>
</div>
</div>
<div class="places__row places__row--alt" data-dir="1">
<div class="places__track">
<p class="places__group">
<span>Menorca</span><i></i><span>Dolomites</span><i></i><span>Kerala</span><i></i><span>Patagonia</span><i></i><span>Faroe Islands</span><i></i><span>Oaxaca</span><i></i>
</p>
</div>
</div>
</section>
<!-- =========================================================
10 · JOURNAL / TESTIMONIALS
========================================================= -->
<section class="journal section" id="journal" aria-labelledby="journal-title">
<div class="container">
<header class="journal__head">
<p class="label"><span class="label__num">(08)</span> Journal</p>
<h2 class="journal__title" id="journal-title">Letters from <em>the road</em></h2>
</header>
<div class="letters">
<div class="letters__photo">
<div class="letters__frame">
<img class="letters__img is-active" src="assets/qmevohdt63yqqwxzqkghcwrpwbjmzcgqj24cm7chfrcajq.jpg" alt="A quiet lakeside cabin in the far north" loading="lazy">
<img class="letters__img" src="assets/qmewwkucjt6k9rqkn1z49w7rum7y6sxe7gia6f5i8vi39j.jpg" alt="Turquoise water below a rocky coast" loading="lazy">
<img class="letters__img" src="assets/qmz42qspkjveuuquvagf4xwzmvafvjpmtrrjigyr6xuzqn.jpg" alt="Salt flats mirroring the sunset sky in the Atacama" loading="lazy">
<img class="letters__img" src="assets/qmrwor8q7jdizxfs1mz8rc4qxbbcibimjkwexuzusuguzk.jpg" alt="Hot-air balloons over Cappadocia at sunset" loading="lazy">
</div>
<span class="letters__stamp"><span class="letters__stamp-place">Lofoten</span><span class="letters__stamp-year">2025</span></span>
</div>
<div class="letters__body" id="letters-panel" role="tabpanel" aria-live="polite">
<span class="letters__mark" aria-hidden="true">“</span>
<blockquote class="letters__quote">By the fourth morning I stopped checking the time. By the sixth I had forgotten which day it was.</blockquote>
<p class="letters__who"><span class="letters__name">Margit Aalto</span><span class="letters__trip">Blue Hour North · Lofoten, 2025</span></p>
<div class="letters__tabs" role="tablist" aria-label="Guest letters">
<button class="ltab is-active" type="button" role="tab" aria-selected="true" aria-controls="letters-panel" data-i="0">
<span class="ltab__num">01</span><span class="ltab__name">Margit Aalto</span><span class="ltab__bar" aria-hidden="true"><i></i></span>
</button>
<button class="ltab" type="button" role="tab" aria-selected="false" aria-controls="letters-panel" data-i="1">
<span class="ltab__num">02</span><span class="ltab__name">Daniel Okafor</span><span class="ltab__bar" aria-hidden="true"><i></i></span>
</button>
<button class="ltab" type="button" role="tab" aria-selected="false" aria-controls="letters-panel" data-i="2">
<span class="ltab__num">03</span><span class="ltab__name">Inés & Paulo Ferreira</span><span class="ltab__bar" aria-hidden="true"><i></i></span>
</button>
<button class="ltab" type="button" role="tab" aria-selected="false" aria-controls="letters-panel" data-i="3">
<span class="ltab__num">04</span><span class="ltab__name">Hannah Weiss</span><span class="ltab__bar" aria-hidden="true"><i></i></span>
</button>
</div>
<template data-letter data-place="Lofoten" data-year="2025" data-name="Margit Aalto" data-trip="Blue Hour North · Lofoten, 2025">By the fourth morning I stopped checking the time. By the sixth I had forgotten which day it was.</template>
<template data-letter data-place="Lycian Coast" data-year="2024" data-name="Daniel Okafor" data-trip="The Slow Coast · Lycian Coast, 2024">Eight strangers, one long table under the olive trees. The group chat is still going two years later.</template>
<template data-letter data-place="Atacama" data-year="2025" data-name="Inés & Paulo Ferreira" data-trip="Salt & Silence · Atacama, 2025">Our guide knew when to talk about the stars, and when to simply let us look at them.</template>
<template data-letter data-place="Cappadocia" data-year="2026" data-name="Hannah Weiss" data-trip="The Chimney Hours · Cappadocia, 2026">I came for the balloons at sunrise. I will come back for the slow afternoons in a cave hotel.</template>
</div>
</div>
</div>
<div class="reviews" aria-label="Guest reviews">
<div class="container reviews__head">
<p class="reviews__score"><span class="reviews__num">4.97</span><span class="reviews__stars" aria-hidden="true"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg></span><span class="reviews__meta">from 380 guest reviews</span></p>
<p class="reviews__note">Every review from a guest who travelled with us — unedited, unhurried.</p>
</div>
<div class="reviews__row">
<div class="reviews__track">
<figure class="rcard">
<div class="rcard__top"><img src="assets/qmevohdt63yqqwxzqkghcwrpwbjmzcgqj24cm7chfrcajq.jpg" alt="" loading="lazy" width="96" height="96"><span class="rcard__place">Lofoten</span><span class="rcard__stars" aria-label="5 out of 5"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg></span></div>
<blockquote>The quietest week of my life, in the best way. Every detail was simply taken care of.</blockquote>
<figcaption>— Margit A.</figcaption>
</figure>
<figure class="rcard">
<div class="rcard__top"><img src="assets/qmewwkucjt6k9rqkn1z49w7rum7y6sxe7gia6f5i8vi39j.jpg" alt="" loading="lazy" width="96" height="96"><span class="rcard__place">Lycian Coast</span><span class="rcard__stars" aria-label="5 out of 5"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg></span></div>
<blockquote>Eight strangers at dinner on night one, eight friends by the last morning.</blockquote>
<figcaption>— Daniel O.</figcaption>
</figure>
<figure class="rcard">
<div class="rcard__top"><img src="assets/qmz42qspkjveuuquvagf4xwzmvafvjpmtrrjigyr6xuzqn.jpg" alt="" loading="lazy" width="96" height="96"><span class="rcard__place">Atacama</span><span class="rcard__stars" aria-label="5 out of 5"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg></span></div>
<blockquote>The stargazing night alone was worth the flight. Our guide knew when to be silent.</blockquote>
<figcaption>— Inés F.</figcaption>
</figure>
<figure class="rcard">
<div class="rcard__top"><img src="assets/qmrwor8q7jdizxfs1mz8rc4qxbbcibimjkwexuzusuguzk.jpg" alt="" loading="lazy" width="96" height="96"><span class="rcard__place">Cappadocia</span><span class="rcard__stars" aria-label="5 out of 5"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg></span></div>
<blockquote>Balloons at sunrise, bread still warm at breakfast. I’ve already booked again.</blockquote>
<figcaption>— Hannah W.</figcaption>
</figure>
<figure class="rcard">
<div class="rcard__top"><img src="assets/qmy9ebrzeb7m6np5wthiaumbfahempzrcxhcgi4xmmdfgv.jpg" alt="" loading="lazy" width="96" height="96"><span class="rcard__place">Azores</span><span class="rcard__stars" aria-label="5 out of 5"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg></span></div>
<blockquote>No rushing, no crowds, no itinerary panic. Just the ocean and very good cheese.</blockquote>
<figcaption>— Tom R.</figcaption>
</figure>
<figure class="rcard">
<div class="rcard__top"><img src="assets/qmyamxr8b2jlfxbdenmbrqd9egsdhqlg57i82utdxrjkwj.jpg" alt="" loading="lazy" width="96" height="96"><span class="rcard__place">Wadi Rum</span><span class="rcard__stars" aria-label="5 out of 5"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.8l2.47 5.2 5.7.72-4.2 3.93 1.07 5.65L10 14.55 4.96 17.3l1.07-5.65-4.2-3.93 5.7-.72z"/></svg></span></div>
<blockquote>Sleeping under that sky changed how I think about travel. Unhurried, truly.</blockquote>
<figcaption>— Leila K.</figcaption>
</figure>
</div>
</div>
</div>
</section>
<!-- =========================================================
11 · BOOKING CTA
========================================================= -->
<section class="booking" id="plan" aria-labelledby="plan-title">
<div class="container">
<header class="booking__head">
<div>
<p class="label"><span class="label__num">(09)</span> Plan</p>
<h2 class="booking__title" id="plan-title">Plan your <em>journey.</em></h2>
</div>
<p class="booking__text">Tell us where you’re drawn and roughly when. A real person — usually Inès or Tomas — writes back within two working days with ideas, not a sales pitch.</p>
</header>
<div class="booking__card">
<!-- Live preview of the trip being planned -->
<div class="bv" aria-hidden="true">
<div class="bv__media">
<img class="bv__img is-front" src="assets/qmdvjznxqwkbd7chff18zeangtuxp6nbnuygjbvtgg9qbi.jpg" alt="" loading="lazy">
<img class="bv__img" alt="" loading="lazy">
</div>
<div class="bv__shade"></div>
<div class="bv__top">
<span class="bv__chip">Your journey</span>
<span class="bv__chip bv__month">Any month</span>
</div>
<div class="bv__meta">
<p class="bv__region">Choose a destination</p>
<p class="bv__name">Somewhere <em>quieter</em></p>
<div class="bv__facts">
<span><b class="bv__nights">—</b> nights</span>
<span><b class="bv__party">2</b> travellers</span>
<span class="bv__est">Est. <b class="bv__total">—</b></span>
</div>
</div>
<div class="bv__stamp"><span>Enquiry</span><b>Sent</b><span class="bv__stamp-date"></span></div>
</div>
<div class="booking__panel">
<form class="booking__form" novalidate>
<div class="field">
<label for="f-destination">Destination</label>
<select id="f-destination" name="destination" required>
<option value="">Choose a journey or retreat</option>
<optgroup label="Journeys">
<option>The Chimney Hours — Cappadocia</option>
<option>Salt & Silence — Atacama</option>
<option>The Slow Coast — Lycian Coast</option>
<option>Blue Hour North — Lofoten</option>
<option>A Sea of Ochre — Wadi Rum</option>
<option>Edge of the Atlantic — Azores</option>
</optgroup>
<optgroup label="Retreats">
<option>Casa Alta — Menorca</option>
<option>Hütte Nebel — Dolomites</option>
<option>Umi Baths — Hokkaido</option>
<option>Kaluna House — Kerala</option>
</optgroup>
<option>Not sure yet — surprise me</option>
</select>
</div>
<div class="field-row">
<div class="field">
<label for="f-month">Month</label>
<select id="f-month" name="month" required>
<option value="">Select a month</option>
</select>
</div>
<div class="field">
<label for="f-travellers">Travellers</label>
<div class="stepper">
<button type="button" class="stepper__btn" data-step="-1" aria-label="Fewer travellers">−</button>
<input id="f-travellers" name="travellers" type="number" inputmode="numeric" min="1" max="8" value="2">
<button type="button" class="stepper__btn" data-step="1" aria-label="More travellers">+</button>
</div>
</div>
</div>
<div class="party" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><span>Groups of eight, never more</span></div>
<div class="field">
<label for="f-email">Email</label>
<input id="f-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required>
</div>
<p class="form-error" role="alert"></p>
<div class="booking__submit">
<button class="btn-send" type="submit"><span class="btn-send__label">Send enquiry</span><span class="btn-send__icon" aria-hidden="true">→</span></button>
<p class="booking__note">No payment, no obligation.</p>
</div>
</form>
<div class="booking__success" role="status" tabindex="-1" hidden>
<p class="label">Enquiry received</p>
<p class="booking__success-title">Thank you. <em>Breathe out.</em></p>
<p class="booking__success-text">We’ll be in touch about <strong class="booking__success-dest">your journey</strong> within two working days. (This is a demo — nothing was actually sent.)</p>
<button class="link-arrow booking__reset" type="button">Plan another</button>
</div>
</div>
</div>
<p class="booking__small">Journeys depart year-round · Groups of up to eight · Private departures on request</p>
</div>
</section>
</main>
<!-- =========================================================
12 · FOOTER
========================================================= -->
<footer class="footer">
<div class="container footer__top">
<div class="footer__news">
<p class="label">Newsletter</p>
<h2 class="footer__heading">Slow letters, <em>four times a year.</em></h2>
<form class="news" novalidate>
<label class="sr-only" for="news-email">Email address</label>
<input id="news-email" type="email" name="email" placeholder="Your email address" autocomplete="email" required>
<button type="submit">Subscribe</button>
</form>
<p class="news__msg" role="status"></p>
</div>
<nav class="footer__cols" aria-label="Footer">
<div>
<p class="footer__col-title">Travel</p>
<ul>
<li><a href="#journeys">Journeys</a></li>
<li><a href="#retreats">Retreats</a></li>
<li><a href="#experiences">Experiences</a></li>
<li><a href="#plan">Private journeys</a></li>
</ul>
</div>
<div>
<p class="footer__col-title">Solace</p>
<ul>
<li><a href="#manifesto">Manifesto</a></li>
<li><a href="#journal">Journal</a></li>
<li><a href="#places">Destinations</a></li>
</ul>
</div>
<div>
<p class="footer__col-title">Contact</p>
<ul>
<li>[email protected]</li>
<li>+44 20 0000 0000</li>
<li>Lisbon · London</li>
</ul>
</div>
</nav>
</div>
<div class="footer__mark" aria-hidden="true"><span class="footer__mark-text">Solace</span></div>
<div class="container footer__bottom">
<p>© 2026 Solace Journeys — a fictional brand.</p>
<a href="#top" class="footer__top-link">Back to top ↑</a>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/SplitText.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/Flip.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/Observer.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/lenis.min.js"></script>
<script src="script.js"></script>
</body>
</html>
```
## `fonts/fonts.css`
```css
/* Self-hosted Google Fonts (SIL Open Font License 1.1, see OFL-*.txt) */
/* latin-ext */
@font-face {
font-family: 'DM Sans';
font-style: normal;
font-weight: 400 600;
font-display: swap;
src: url(../assets/a18ee4a68cd6.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'DM Sans';
font-style: normal;
font-weight: 400 600;
font-display: swap;
src: url(../assets/fc88c23021fa.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
@font-face {
font-family: 'Fraunces';
font-style: italic;
font-weight: 300 600;
font-display: swap;
src: url(../assets/e8e658f4ba02.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Fraunces';
font-style: italic;
font-weight: 300 600;
font-display: swap;
src: url(../assets/a43a80ef7fa7.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Fraunces';
font-style: italic;
font-weight: 300 600;
font-display: swap;
src: url(../assets/460724fd1a62.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 300 600;
font-display: swap;
src: url(../assets/32001b823e74.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 300 600;
font-display: swap;
src: url(../assets/c466948efa95.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 300 600;
font-display: swap;
src: url(../assets/387685fe2885.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
```
## `script.js`
```js
/* ==========================================================================
SOLACE — script.js
Vanilla JS + GSAP 3.13 (ScrollTrigger, SplitText, Flip, Observer) + Lenis.
Structure
00 · Setup & helpers
01 · Always-on UI (menu, anchors, forms, lightbox, journal)
02 · Motion bootstrap (Lenis, cursor, image refresh)
03 · Preloader → hero hand-off
04 · Hero
05 · Manifesto
06 · Journeys (pinned horizontal)
07 · Zoom-through
08 · Retreats (stacked cards)
09 · Experiences reveals
10 · Places marquee
11 · Booking + magnetic button
12 · Footer wordmark
13 · Generic heading reveals
14 · Init
========================================================================== */
(() => {
'use strict';
/* ------------------------------------------------------------------------
00 · Setup & helpers
------------------------------------------------------------------------ */
const root = document.documentElement;
const $ = (s, c = document) => c.querySelector(s);
const $$ = (s, c = document) => Array.from(c.querySelectorAll(s));
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const pad2 = (n) => String(n).padStart(2, '0');
const hasGSAP =
typeof window.gsap !== 'undefined' &&
typeof window.ScrollTrigger !== 'undefined' &&
typeof window.SplitText !== 'undefined' &&
typeof window.Flip !== 'undefined';
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const MOTION = hasGSAP && !prefersReduced;
const finePointer = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
const isMobile = () => window.innerWidth < 640;
const preloader = $('.preloader');
let lenis = null;
const imgReady = (img) =>
new Promise((res) => {
if (img.complete && img.naturalWidth) return res();
img.addEventListener('load', res, { toggleActions: 'play none none none' });
img.addEventListener('error', res, { toggleActions: 'play none none none' });
});
function removePreloader() {
if (preloader && preloader.parentNode) preloader.parentNode.removeChild(preloader);
}
function stopScroll() { if (lenis) lenis.stop(); }
function startScroll() { if (lenis) lenis.start(); }
function scrollToTarget(target) {
if (lenis) {
lenis.scrollTo(target, { duration: 1.6, easing: (t) => 1 - Math.pow(1 - t, 4) });
} else if (target === 0) {
window.scrollTo({ top: 0, behavior: prefersReduced ? 'auto' : 'smooth' });
} else {
target.scrollIntoView({ behavior: prefersReduced ? 'auto' : 'smooth' });
}
}
/* ------------------------------------------------------------------------
01a · Mobile menu
------------------------------------------------------------------------ */
const menu = $('#menu');
const toggle = $('.nav__toggle');
let menuOpen = false;
function openMenu() {
if (!menu || menuOpen) return;
menuOpen = true;
menu.hidden = false;
toggle.setAttribute('aria-expanded', 'true');
$('.nav__toggle-text', toggle).textContent = 'Close';
stopScroll();
if (MOTION) {
gsap.killTweensOf(menu);
gsap.fromTo(menu, { clipPath: 'inset(0% 0% 100% 0%)' }, { clipPath: 'inset(0% 0% 0% 0%)', duration: 0.85, ease: 'expo.inOut' });
gsap.fromTo($$('.menu__list a span', menu), { yPercent: 110 }, { yPercent: 0, duration: 1.1, ease: 'expo.out', stagger: 0.07, delay: 0.3 });
gsap.fromTo($$('.menu__list li', menu), { borderTopColor: 'rgba(255,255,255,0)' }, { borderTopColor: 'rgba(255,255,255,0.28)', duration: 0.8, stagger: 0.07, delay: 0.35 });
gsap.fromTo($('.menu__foot', menu), { autoAlpha: 0, y: 10 }, { autoAlpha: 1, y: 0, duration: 0.8, delay: 0.7 });
}
const first = $('.menu__list a', menu);
if (first) first.focus({ preventScroll: true });
}
function closeMenu(returnFocus) {
if (!menu || !menuOpen) return;
menuOpen = false;
toggle.setAttribute('aria-expanded', 'false');
$('.nav__toggle-text', toggle).textContent = 'Menu';
startScroll();
const done = () => { menu.hidden = true; if (MOTION) gsap.set(menu, { clearProps: 'clipPath' }); };
if (MOTION) {
gsap.killTweensOf(menu);
gsap.to(menu, { clipPath: 'inset(0% 0% 100% 0%)', duration: 0.7, ease: 'expo.inOut', onComplete: done });
} else {
done();
}
if (returnFocus) toggle.focus({ preventScroll: true });
}
function initMenu() {
if (!menu || !toggle) return;
toggle.addEventListener('click', () => (menuOpen ? closeMenu(true) : openMenu()));
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && menuOpen) closeMenu(true);
});
window.addEventListener('resize', () => {
if (menuOpen && window.innerWidth >= 900) closeMenu(false);
});
}
/* ------------------------------------------------------------------------
01b · In-page anchors (Lenis aware)
------------------------------------------------------------------------ */
function initAnchors() {
document.addEventListener('click', (e) => {
const a = e.target.closest('a[href^="#"]');
if (!a) return;
const id = a.getAttribute('href');
if (!id || id.length < 2) return;
const target = document.getElementById(id.slice(1));
if (!target) return;
e.preventDefault();
closeMenu(false);
scrollToTarget(id === '#top' ? 0 : target);
// Move focus for keyboard / screen-reader users without jumping the scroll
if (!target.hasAttribute('tabindex')) target.setAttribute('tabindex', '-1');
target.focus({ preventScroll: true });
});
}
/* ------------------------------------------------------------------------
01c · Booking form (inline success only — nothing is submitted)
------------------------------------------------------------------------ */
/* ------------------------------------------------------------------------
01c′ · Booking live preview — photo, name, nights, party, estimate
------------------------------------------------------------------------ */
const TRIPS = {
'Cappadocia': { img: 'QmRwoR8Q7JdizxfS1mZ8Rc4qXBBcibimjkWexuzUsUgUzK', region: 'Central Anatolia, Türkiye', title: 'The Chimney <em>Hours</em>', nights: 6, price: 3650, per: 'person' },
'Atacama': { img: 'QmZ42qsPKJVEUuQuVaGf4xwzmvAFVJpmTRrjigYr6XUZQN', region: 'Atacama Desert, Chile', title: 'Salt & <em>Silence</em>', nights: 8, price: 4180, per: 'person' },
'Lycian Coast': { img: 'QmewWkucjt6k9RqKN1Z49W7ruM7y6SXE7Gia6F5i8vi39J', region: 'Lycian Coast, Türkiye', title: 'The Slow <em>Coast</em>', nights: 9, price: 3960, per: 'person' },
'Lofoten': { img: 'QmeNikyHVAnh2GBvDwfDKDyEnQYYdNvPzgneBqSkG5oPYD', region: 'Lofoten, Norway', title: 'Blue Hour <em>North</em>', nights: 6, price: 4720, per: 'person' },
'Wadi Rum': { img: 'QmYAMXr8B2jLFxbDeNmbRqD9egsdHqLg57i82UTDxrjkWJ', region: 'Wadi Rum, Jordan', title: 'A Sea of <em>Ochre</em>', nights: 5, price: 2890, per: 'person' },
'Azores': { img: 'QmY9ebRZeB7m6Np5wTHiaumbFaHEMPzrcXhcgi4XmmDFGV', region: 'Azores, Portugal', title: 'Edge of the <em>Atlantic</em>', nights: 7, price: 3410, per: 'person' },
'Menorca': { img: 'QmRPv49VnKwjZvSMaiywXMeGg8b3T9X2FqFCL4E4DhWfCB', region: 'Menorca, Spain', title: 'Casa <em>Alta</em>', nights: 3, price: 610, per: 'night' },
'Dolomites': { img: 'QmPouA6uaBBj4h46WS4U66NmWuKbX47SPat1tY2jTzs9i5', region: 'Dolomites, Italy', title: 'Hütte <em>Nebel</em>', nights: 3, price: 520, per: 'night' },
'Hokkaido': { img: 'QmTs6jC8TXZHmvDSETmSYGnCNYNiWzppfjjrZXXPwnvBrs', region: 'Hokkaido, Japan', title: 'Umi <em>Baths</em>', nights: 3, price: 740, per: 'night' },
'Kerala': { img: 'QmTsmwNeUjHSBQEX28gawuDqnZkAdJ2WSLbKkXFNo4Jb7h', region: 'Kerala, India', title: 'Kaluna <em>House</em>', nights: 3, price: 430, per: 'night' },
};
const DEFAULT_TRIP = { img: 'QmdvjZNXQWkbd7Chff18zeAnGtuxp6nbnUygjbVTgG9qBi', region: 'Choose a destination', title: 'Somewhere <em>quieter</em>', nights: null, price: null };
const SURPRISE_TRIP = { img: 'QmUTeGqxmW9nyPks6KEJBiAhXvYWKGQ856LnbX6iwBQ1ZZ', region: 'We’ll pick for you', title: 'Surprise <em>me</em>', nights: null, price: null };
function bookingPreview() {
const bv = $('.bv');
if (!bv) return null;
const imgs = $$('.bv__img', bv);
const region = $('.bv__region', bv), name = $('.bv__name', bv);
const nights = $('.bv__nights', bv), party = $('.bv__party', bv), total = $('.bv__total', bv);
const month = $('.bv__month', bv), stamp = $('.bv__stamp', bv);
const figures = $$('.party i');
const anim = MOTION && window.gsap;
let trip = DEFAULT_TRIP, people = 2, shownTotal = 0, busy = null;
const money = (v) => '€' + Math.round(v).toLocaleString('en-GB');
const url = (id) => 'assets/' + id.toLowerCase() + '.jpg';
const estimate = () => (trip.price == null ? null : trip.per === 'person' ? trip.price * people : trip.price * trip.nights);
function paintTotal() {
const t = estimate();
if (t == null) { total.textContent = '—'; shownTotal = 0; return; }
if (!anim) { total.textContent = money(t); shownTotal = t; return; }
const o = { v: shownTotal };
gsap.to(o, { v: t, duration: 1.1, ease: 'expo.out', overwrite: true, onUpdate: () => { total.textContent = money(o.v); } });
shownTotal = t;
}
function paintParty() {
party.textContent = people;
figures.forEach((f, i) => f.classList.toggle('is-on', i < people));
if (anim) {
const f = figures[people - 1];
if (f) gsap.fromTo(f, { y: -8, scale: 1.3 }, { y: 0, scale: 1, duration: 0.6, ease: 'back.out(3)' });
gsap.fromTo(party, { yPercent: -60, opacity: 0 }, { yPercent: 0, opacity: 1, duration: 0.5, ease: 'expo.out' });
}
}
function setTrip(key) {
trip = key === '__surprise' ? SURPRISE_TRIP : (TRIPS[key] || DEFAULT_TRIP);
const front = imgs.find((i) => i.classList.contains('is-front'));
const back = imgs.find((i) => i !== front);
const text = () => {
region.textContent = trip.region;
name.innerHTML = trip.title;
nights.textContent = trip.nights == null ? '—' : trip.nights;
paintTotal();
};
if (!anim) { front.src = url(trip.img); text(); return; }
if (busy) busy.progress(1);
back.src = url(trip.img);
gsap.set(back, { opacity: 1, zIndex: 2, clipPath: 'inset(0% 0% 100% 0%)' });
gsap.set(front, { zIndex: 1 });
busy = gsap.timeline({ onComplete: () => { busy = null; } })
.to([region, name], { yPercent: -40, opacity: 0, duration: 0.35, ease: 'power2.in', stagger: 0.04 }, 0)
.to(back, { clipPath: 'inset(0% 0% 0% 0%)', duration: 1.15, ease: 'expo.inOut' }, 0.05)
.fromTo(back, { scale: 1.25 }, { scale: 1, duration: 1.6, ease: 'expo.out' }, 0.05)
.to(front, { scale: 0.92, duration: 1.15, ease: 'expo.inOut' }, 0.05)
.add(() => {
text();
const chars = window.SplitText ? SplitText.create(name, { type: 'chars', charsClass: 'bv-ch' }).chars : [name];
gsap.fromTo(chars, { yPercent: 90, opacity: 0, rotate: 8 }, { yPercent: 0, opacity: 1, rotate: 0, duration: 0.9, ease: 'expo.out', stagger: 0.022 });
gsap.fromTo(region, { yPercent: 40, opacity: 0 }, { yPercent: 0, opacity: 1, duration: 0.7, ease: 'expo.out' });
gsap.set(name, { yPercent: 0, opacity: 1 });
gsap.fromTo(nights, { yPercent: -60, opacity: 0 }, { yPercent: 0, opacity: 1, duration: 0.6, ease: 'expo.out' });
}, 0.4)
.add(() => {
front.classList.remove('is-front');
back.classList.add('is-front');
gsap.set(front, { opacity: 0, scale: 1, zIndex: 0 });
gsap.set(back, { zIndex: 1, clearProps: 'clipPath' });
});
}
function setMonth(label) {
month.textContent = label || 'Any month';
if (anim) gsap.fromTo(month, { rotationX: -90, opacity: 0 }, { rotationX: 0, opacity: 1, duration: 0.7, ease: 'back.out(2)' });
}
function setPeople(n) { people = n; paintParty(); paintTotal(); }
function sent(on) {
$('.bv__stamp-date', bv).textContent = new Date().toLocaleDateString('en-GB', { day: '2-digit', month: 'short' });
if (!anim) { stamp.style.opacity = on ? 1 : 0; return; }
if (on) gsap.fromTo(stamp, { scale: 2.6, opacity: 0, rotate: -40 }, { scale: 1, opacity: 1, rotate: -14, duration: 0.7, ease: 'back.out(1.8)', delay: 0.35 });
else gsap.to(stamp, { opacity: 0, scale: 0.8, duration: 0.3 });
}
paintParty();
return { setTrip, setMonth, setPeople, sent };
}
function initBookingForm() {
const form = $('.booking__form');
if (!form) return;
const success = $('.booking__success');
const monthSel = $('#f-month', form);
const trav = $('#f-travellers', form);
const destSel = $('#f-destination', form);
const email = $('#f-email', form);
const err = $('.form-error', form);
const pv = bookingPreview();
const tripKey = (v) => (!v ? '' : /surprise/i.test(v) ? '__surprise' : v.split(' — ')[1]);
if (pv) {
destSel.addEventListener('change', () => pv.setTrip(tripKey(destSel.value)));
monthSel.addEventListener('change', () => pv.setMonth(monthSel.value));
}
// Populate the next 12 months
const now = new Date();
for (let i = 1; i <= 12; i++) {
const d = new Date(now.getFullYear(), now.getMonth() + i, 1);
const opt = document.createElement('option');
opt.textContent = d.toLocaleString('en-GB', { month: 'long', year: 'numeric' });
monthSel.appendChild(opt);
}
// Travellers stepper
const clampTrav = (v) => Math.min(8, Math.max(1, parseInt(v, 10) || 1));
$$('.stepper__btn', form).forEach((b) =>
b.addEventListener('click', () => {
trav.value = clampTrav(+trav.value + +b.dataset.step);
if (MOTION) gsap.fromTo(trav, { y: +b.dataset.step > 0 ? 8 : -8, opacity: 0 }, { y: 0, opacity: 1, duration: 0.4, ease: 'power3.out' });
if (pv) pv.setPeople(+trav.value);
})
);
trav.addEventListener('change', () => { trav.value = clampTrav(trav.value); if (pv) pv.setPeople(+trav.value); });
const setInvalid = (el, bad) => {
el.classList.toggle('is-invalid', bad);
el.setAttribute('aria-invalid', bad ? 'true' : 'false');
};
[destSel, monthSel, email].forEach((el) => el.addEventListener('input', () => setInvalid(el, false)));
form.addEventListener('submit', (e) => {
e.preventDefault();
const bad = [];
if (!destSel.value) bad.push([destSel, 'a destination']);
if (!monthSel.value) bad.push([monthSel, 'a month']);
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email.value.trim())) bad.push([email, 'a valid email']);
[destSel, monthSel, email].forEach((el) => setInvalid(el, bad.some((b) => b[0] === el)));
if (bad.length) {
err.textContent = 'Please add ' + bad.map((b) => b[1]).join(', ').replace(/, ([^,]*)$/, ' and $1') + '.';
bad[0][0].focus();
if (MOTION) gsap.fromTo(err, { x: -6 }, { x: 0, duration: 0.5, ease: 'elastic.out(1, 0.3)' });
return;
}
err.textContent = '';
$('.booking__success-dest', success).textContent = destSel.value.split(' — ')[0];
if (pv) pv.sent(true);
const show = () => {
form.hidden = true;
success.hidden = false;
success.focus({ preventScroll: true });
if (MOTION) {
gsap.fromTo(success.children, { autoAlpha: 0, y: 30 }, { autoAlpha: 1, y: 0, duration: 1, ease: 'expo.out', stagger: 0.08 });
}
};
if (MOTION) gsap.to(form, { autoAlpha: 0, y: -20, duration: 0.5, ease: 'power2.in', onComplete: show });
else show();
});
$('.booking__reset', success).addEventListener('click', () => {
form.reset();
trav.value = 2;
if (pv) { pv.sent(false); pv.setTrip(''); pv.setMonth(''); pv.setPeople(2); }
success.hidden = true;
form.hidden = false;
if (MOTION) gsap.fromTo(form, { autoAlpha: 0, y: 20 }, { autoAlpha: 1, y: 0, duration: 0.8, ease: 'expo.out' });
destSel.focus();
});
}
/* ------------------------------------------------------------------------
01d · Newsletter (inline message only)
------------------------------------------------------------------------ */
function initNewsletter() {
const form = $('.news');
if (!form) return;
const input = $('input', form);
const msg = $('.news__msg');
form.addEventListener('submit', (e) => {
e.preventDefault();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(input.value.trim())) {
msg.textContent = 'That email doesn’t look quite right.';
input.setAttribute('aria-invalid', 'true');
input.focus();
return;
}
input.setAttribute('aria-invalid', 'false');
form.reset();
msg.textContent = 'Thank you — the first letter arrives with the equinox. (Demo only.)';
if (MOTION) gsap.fromTo(msg, { autoAlpha: 0, y: 8 }, { autoAlpha: 1, y: 0, duration: 0.6 });
});
}
/* ------------------------------------------------------------------------
01e · Experiences lightbox (GSAP Flip, instant fallback)
------------------------------------------------------------------------ */
function initLightbox(animate) {
const box = $('.lightbox');
if (!box || box.dataset.ready) return;
box.dataset.ready = '1';
const frame = $('.lightbox__frame', box);
const cap = $('.lightbox__caption', box);
const closeBtn = $('.lightbox__close', box);
const backdrop = $('.lightbox__backdrop', box);
let active = null;
let busy = false;
function placeFrame() {
if (!active) return;
const vw = window.innerWidth;
const vh = window.innerHeight;
const padX = vw < 640 ? 16 : 72;
const maxW = vw - padX * 2;
const maxH = vh - (vw < 640 ? 190 : 200);
let w = maxW;
let h = w / active.ar;
if (h > maxH) { h = maxH; w = h * active.ar; }
frame.style.width = w + 'px';
frame.style.height = h + 'px';
frame.style.left = (vw - w) / 2 + 'px';
frame.style.top = Math.max(72, (vh - h) / 2 - 24) + 'px';
}
function open(btn) {
if (busy || active) return;
const home = $('.exp__frame', btn);
const img = $('img', home);
const r = home.getBoundingClientRect();
active = { btn, home, img, ar: r.width / r.height };
cap.textContent = btn.dataset.caption || img.alt;
box.hidden = false;
placeFrame();
stopScroll();
if (animate) {
busy = true;
gsap.killTweensOf(img);
gsap.set(img, { clearProps: 'transform' });
img.style.transition = 'none';
img.style.scale = '1';
const state = Flip.getState(img);
frame.appendChild(img);
gsap.fromTo(backdrop, { opacity: 0 }, { opacity: 1, duration: 0.6, ease: 'power2.out' });
gsap.fromTo([cap, closeBtn], { autoAlpha: 0, y: 16 }, { autoAlpha: 1, y: 0, duration: 0.7, delay: 0.55, stagger: 0.08, ease: 'power3.out' });
Flip.from(state, { duration: 1, ease: 'expo.inOut', onComplete: () => { busy = false; } });
} else {
frame.appendChild(img);
}
closeBtn.focus({ preventScroll: true });
}
function close() {
if (!active || busy) return;
const { home, img, btn } = active;
const finish = () => {
home.appendChild(img);
if (window.gsap) gsap.set(img, { clearProps: 'all' });
img.removeAttribute('style');
box.hidden = true;
active = null;
busy = false;
startScroll();
btn.focus({ preventScroll: true });
};
if (animate) {
busy = true;
gsap.to([cap, closeBtn], { autoAlpha: 0, duration: 0.3 });
gsap.to(backdrop, { opacity: 0, duration: 0.7, delay: 0.25, ease: 'power2.inOut' });
Flip.fit(img, home, { duration: 0.9, ease: 'expo.inOut', onComplete: finish });
} else {
finish();
}
}
$$('.exp__btn').forEach((btn) => btn.addEventListener('click', () => open(btn)));
closeBtn.addEventListener('click', close);
backdrop.addEventListener('click', close);
frame.addEventListener('click', close);
document.addEventListener('keydown', (e) => {
if (!active) return;
if (e.key === 'Escape') close();
if (e.key === 'Tab') { e.preventDefault(); closeBtn.focus(); } // single-control focus trap
});
window.addEventListener('resize', placeFrame);
}
/* ------------------------------------------------------------------------
01f · Journal — postcard photo + big serif quote, guest tabs with
progress lines, auto-play, click / swipe / arrow keys
------------------------------------------------------------------------ */
function initJournal(animate) {
const box = $('.letters');
if (!box || box.dataset.ready) return;
box.dataset.ready = '1';
const data = $$('[data-letter]', box).map((t) => ({
q: t.innerHTML.trim(), place: t.dataset.place, year: t.dataset.year, name: t.dataset.name, trip: t.dataset.trip,
}));
const imgs = $$('.letters__img', box);
const tabs = $$('.ltab', box);
const quote = $('.letters__quote', box);
const nameEl = $('.letters__name', box);
const tripEl = $('.letters__trip', box);
const stamp = $('.letters__stamp', box);
const stampP = $('.letters__stamp-place', box);
const stampY = $('.letters__stamp-year', box);
const frame = $('.letters__frame', box);
const photo = $('.letters__photo', box);
const mark = $('.letters__mark', box);
const bars = tabs.map((t) => $('.ltab__bar i', t));
let idx = 0;
const setTabs = (i) => tabs.forEach((t, k) => {
t.classList.toggle('is-active', k === i);
t.setAttribute('aria-selected', String(k === i));
t.tabIndex = k === i ? 0 : -1;
});
const fill = (i) => {
const d = data[i];
quote.innerHTML = d.q;
nameEl.textContent = d.name; tripEl.textContent = d.trip;
stampP.textContent = d.place; stampY.textContent = d.year;
};
function bind(go) {
tabs.forEach((t, k) => t.addEventListener('click', () => go(k)));
$('.letters__tabs', box).addEventListener('keydown', (e) => {
const d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
if (!d) return;
e.preventDefault();
const n = (idx + d + data.length) % data.length;
go(n); tabs[n].focus();
});
let sx = null;
[photo, quote].forEach((el) => el.addEventListener('pointerdown', (e) => { sx = e.clientX; }));
window.addEventListener('pointerup', (e) => {
if (sx === null) return;
const dx = e.clientX - sx; sx = null;
if (Math.abs(dx) > 50) go((idx + (dx < 0 ? 1 : -1) + data.length) % data.length);
});
}
setTabs(0);
if (!animate) {
bind((i) => {
imgs[idx].classList.remove('is-active');
idx = i;
imgs[i].classList.add('is-active');
fill(i); setTabs(i);
});
return;
}
const DUR = 7;
let busy = false, split = null, prog = null, inView = false, hover = false, shown = false;
function runProg() {
if (prog) prog.kill();
gsap.set(bars, { scaleX: 0 });
prog = gsap.to(bars[idx], { scaleX: 1, duration: DUR, ease: 'none', paused: !inView || hover, onComplete: () => go((idx + 1) % data.length) });
}
function wordsIn(delay) {
split = SplitText.create(quote, { type: 'words', mask: 'words', wordsClass: 'lw' });
return gsap.from(split.words, { yPercent: 110, duration: 1.05, ease: 'expo.out', stagger: 0.032, delay: delay || 0 });
}
function go(i) {
if (busy) return;
if (i === idx) { runProg(); return; }
busy = true;
const oldImg = imgs[idx];
const newImg = imgs[i];
setTabs(i);
gsap.set(newImg, { opacity: 1, zIndex: 2, clipPath: 'inset(100% 0% 0% 0%)' });
gsap.set(oldImg, { zIndex: 1 });
gsap.timeline({ onComplete: () => { busy = false; } })
// text out
.to(split ? split.words : quote, { yPercent: -110, duration: 0.45, ease: 'power3.in', stagger: 0.012 }, 0)
.to([nameEl, tripEl], { autoAlpha: 0, y: -10, duration: 0.35, ease: 'power2.in' }, 0)
.to(stamp, { rotationY: 90, duration: 0.35, ease: 'power2.in' }, 0)
// postcard: new photo wipes up over the old one, frame settles at a new tilt
.to(newImg, { clipPath: 'inset(0% 0% 0% 0%)', duration: 1.1, ease: 'expo.inOut' }, 0.1)
.fromTo(newImg, { scale: 1.35 }, { scale: 1, duration: 1.5, ease: 'expo.out' }, 0.1)
.to(oldImg, { scale: 0.9, duration: 1.1, ease: 'expo.inOut' }, 0.1)
.to(frame, { rotation: gsap.utils.random(-6, 2), duration: 1.3, ease: 'elastic.out(1, .5)' }, 0.15)
// text in
.add(() => {
if (split) { split.revert(); split = null; }
idx = i;
fill(i);
wordsIn();
runProg();
}, 0.47)
.fromTo([nameEl, tripEl], { autoAlpha: 0, y: 12 }, { autoAlpha: 1, y: 0, duration: 0.7, stagger: 0.06, ease: 'expo.out' }, 0.62)
.to(stamp, { rotationY: 0, duration: 0.9, ease: 'back.out(2.2)' }, 0.5)
.fromTo(mark, { scale: 0.6, rotation: -20 }, { scale: 1, rotation: 0, duration: 1.2, ease: 'elastic.out(1, .45)' }, 0.47)
.add(() => {
oldImg.classList.remove('is-active');
gsap.set(oldImg, { opacity: 0, scale: 1, zIndex: 0 });
newImg.classList.add('is-active');
gsap.set(newImg, { zIndex: 1, clearProps: 'clipPath' });
}, 1.3);
}
// first reveal as the section arrives; auto-play only while visible
gsap.set([quote, nameEl, tripEl], { autoAlpha: 0 });
ScrollTrigger.create({
trigger: box, start: 'top 75%', end: 'bottom top',
onToggle: (self) => {
inView = self.isActive;
if (inView && !shown) {
shown = true;
gsap.set([quote, nameEl, tripEl], { autoAlpha: 1 });
gsap.from(frame, { rotation: -14, yPercent: 12, scale: 0.9, autoAlpha: 0, duration: 1.4, ease: 'expo.out' });
gsap.from(stamp, { scale: 0, rotation: -120, duration: 1.2, ease: 'back.out(2)', delay: 0.45 });
gsap.from(tabs, { y: 24, autoAlpha: 0, duration: 0.9, stagger: 0.07, ease: 'expo.out', delay: 0.3 });
wordsIn(0.2);
gsap.from([nameEl, tripEl], { y: 12, autoAlpha: 0, duration: 0.8, stagger: 0.06, ease: 'expo.out', delay: 0.6 });
runProg();
}
if (prog) { if (inView && !hover) prog.resume(); else prog.pause(); }
},
});
// pause while the reader lingers
box.addEventListener('pointerenter', () => { hover = true; if (prog) prog.pause(); });
box.addEventListener('pointerleave', () => { hover = false; if (prog && inView) prog.resume(); });
box.addEventListener('focusin', () => { hover = true; if (prog) prog.pause(); });
box.addEventListener('focusout', () => { hover = false; if (prog && inView) prog.resume(); });
// postcard tilts toward the pointer
if (finePointer) {
gsap.set(frame, { transformPerspective: 900 });
const rx = gsap.quickTo(frame, 'rotationX', { duration: 0.8, ease: 'power3' });
const ry = gsap.quickTo(frame, 'rotationY', { duration: 0.8, ease: 'power3' });
photo.addEventListener('pointermove', (e) => {
const r = photo.getBoundingClientRect();
ry(((e.clientX - r.left) / r.width - 0.5) * 16);
rx(-((e.clientY - r.top) / r.height - 0.5) * 12);
});
photo.addEventListener('pointerleave', () => { rx(0); ry(0); });
}
bind(go);
}
/* ------------------------------------------------------------------------
02a · Lenis smooth scroll ↔ ScrollTrigger
------------------------------------------------------------------------ */
function initLenis() {
if (typeof window.Lenis === 'undefined') return;
lenis = new Lenis({ lerp: 0.085, smoothWheel: true, wheelMultiplier: 1 });
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((time) => { if (lenis) lenis.raf(time * 1000); });
gsap.ticker.lagSmoothing(0);
}
/* ------------------------------------------------------------------------
02b · Custom cursor (fine pointers only)
------------------------------------------------------------------------ */
function initCursor() {
if (!finePointer) return;
const c = $('.cursor');
const label = $('.cursor__label', c);
if (!c) return;
root.classList.add('has-cursor');
gsap.set(c, { xPercent: -50, yPercent: -50, x: window.innerWidth / 2, y: window.innerHeight / 2, opacity: 0 });
const xTo = gsap.quickTo(c, 'x', { duration: 0.45, ease: 'power3' });
const yTo = gsap.quickTo(c, 'y', { duration: 0.45, ease: 'power3' });
let shown = false;
let current = null;
let px = -1;
let py = -1;
function update(target) {
const el = target && target.closest ? target.closest('[data-cursor], a, button, select, input, label') : null;
if (el === current) return;
current = el;
c.classList.remove('is-label', 'is-link');
if (!el) return;
if (el.hasAttribute('data-cursor') && !el.closest('.lightbox')) {
label.textContent = el.getAttribute('data-cursor');
c.classList.add('is-label');
} else if (el.matches('a, button')) {
c.classList.add('is-link');
}
}
window.addEventListener('pointermove', (e) => {
if (e.pointerType !== 'mouse') return;
px = e.clientX; py = e.clientY;
if (!shown) { gsap.to(c, { opacity: 1, duration: 0.3 }); shown = true; }
xTo(px); yTo(py);
update(e.target);
}, { passive: true });
document.documentElement.addEventListener('pointerleave', () => { gsap.to(c, { opacity: 0, duration: 0.3 }); shown = false; });
// Content moves under a still pointer while scrolling: re-check the hovered element
let raf = 0;
const onScroll = () => {
if (px < 0 || raf) return;
raf = requestAnimationFrame(() => { raf = 0; update(document.elementFromPoint(px, py)); });
};
if (lenis) lenis.on('scroll', onScroll); else window.addEventListener('scroll', onScroll, { passive: true });
}
/* ------------------------------------------------------------------------
02c · Refresh ScrollTrigger after images load
------------------------------------------------------------------------ */
function initImageRefresh() {
let t = 0;
const queue = () => { clearTimeout(t); t = setTimeout(() => ScrollTrigger.refresh(), 250); };
$$('img').forEach((img) => { if (!img.complete) img.addEventListener('load', queue, { toggleActions: 'play none none none' }); });
window.addEventListener('load', queue);
if (document.fonts && document.fonts.ready) document.fonts.ready.then(queue);
}
/* ------------------------------------------------------------------------
03 · Preloader → hero hand-off
------------------------------------------------------------------------ */
function runPreloader(heroIntro, startHeroLoop) {
const finishAll = () => {
removePreloader();
startScroll();
startHeroLoop();
ScrollTrigger.refresh();
};
if (!preloader) { heroIntro.play(); finishAll(); return; }
preloader.style.animation = 'none'; // JS owns the preloader now (CSS failsafe off)
const cards = $$('.preloader__card', preloader);
const heroCard = cards[cards.length - 1];
const cardImgs = cards.map((c) => $('img', c));
const word = $('.preloader__word span', preloader);
const count = $('.preloader__count', preloader);
const num = $('.preloader__num', preloader);
const tag = $('.preloader__tag', preloader);
const heroMedia = $('.hero__media');
const veil = $('.hero__veil');
const counter = { v: 0 };
let finished = false;
gsap.set(cards, { clipPath: 'inset(100% 0% 0% 0%)', rotate: () => gsap.utils.random(-8, 8), scale: 1.06 });
gsap.set(cardImgs, { scale: 1.35 });
gsap.set(word, { yPercent: 110 });
gsap.set([count, tag], { autoAlpha: 0 });
const tl = gsap.timeline({ paused: true });
tl.to(word, { yPercent: 0, duration: 1.2, ease: 'expo.out' }, 0)
.to([count, tag], { autoAlpha: 1, duration: 0.6 }, 0.1)
.to(counter, {
v: 100,
duration: 2.7,
ease: 'power2.inOut',
onUpdate: () => { num.textContent = Math.round(counter.v); },
}, 0.15);
// Rapid slideshow: each card wipes up over the last, settling from a tilt
cards.forEach((card, i) => {
const t = 0.25 + i * 0.4;
tl.to(card, { clipPath: 'inset(0% 0% 0% 0%)', scale: 1, duration: 0.6, ease: 'power3.inOut' }, t)
.to(cardImgs[i], { scale: 1, duration: 0.9, ease: 'power3.out' }, t);
if (i > 0) tl.to(cards[i - 1], { scale: 0.94, duration: 0.6, ease: 'power3.inOut' }, t);
});
tl.addLabel('out', '>+0.15')
.to(word, { yPercent: -110, duration: 0.7, ease: 'expo.in' }, 'out')
.to([count, tag], { autoAlpha: 0, y: -20, duration: 0.5, ease: 'power2.in' }, 'out')
.to(heroCard, { rotate: 0, duration: 0.6, ease: 'power2.inOut' }, 'out')
.set(cards.slice(0, -1), { autoAlpha: 0 }, 'out+=0.6')
.add(() => {
// Grow the last photo into the full-bleed hero image
Flip.fit(heroCard, heroMedia, {
duration: 1.35,
ease: 'expo.inOut',
scale: false,
absolute: true,
onComplete: () => {
// The card now sits exactly over the identical hero image: swap, then fade the veil in
finished = true;
finishAll();
gsap.fromTo(veil, { opacity: 0 }, { opacity: 1, duration: 1.2, ease: 'power2.out' });
heroIntro.play();
},
});
gsap.to(heroCard, { borderRadius: 0, duration: 1.35, ease: 'expo.inOut' });
}, 'out+=0.6');
const ready = Promise.race([Promise.all(cardImgs.map(imgReady)), wait(4000)]);
ready.then(() => tl.play());
// Safety net: never leave the page behind the loader
setTimeout(() => {
if (finished || !preloader.parentNode) return;
tl.kill();
heroIntro.progress(1);
finished = true;
finishAll();
}, 14000);
}
/* ------------------------------------------------------------------------
04 · Hero — masked line reveal, Ken Burns, scroll-to-frame + title split
------------------------------------------------------------------------ */
function setupHero() {
const hero = $('.hero');
const media = $('.hero__media', hero);
const img = $('.hero__img', hero);
const rows = $$('.hero__row', hero);
const label = $('.hero__label', hero);
const foot = $('.hero__foot', hero);
const nav = $('.nav');
if (nav) {
// white nav while it sits over the hero photo (robust to big scroll jumps)
let onHero = null;
const syncNav = () => {
const v = hero.getBoundingClientRect().bottom > nav.offsetHeight;
if (v !== onHero) { onHero = v; nav.classList.toggle('is-on-hero', v); }
};
window.addEventListener('scroll', syncNav, { passive: true });
window.addEventListener('resize', syncNav);
syncNav();
}
const splits = rows.map((r) => SplitText.create(r, { type: 'lines', mask: 'lines', linesClass: 'heroline' }));
const lines = splits.flatMap((s) => s.lines);
gsap.set(lines, { yPercent: 118 });
gsap.set([label, foot], { autoAlpha: 0, y: 24 });
gsap.set(nav, { autoAlpha: 0, y: -12 });
const intro = gsap.timeline({ paused: true });
intro.to(lines, { yPercent: 0, duration: 1.5, ease: 'expo.out', stagger: 0.14 })
.to(label, { autoAlpha: 1, y: 0, duration: 1.1, ease: 'expo.out' }, 0.2)
.to(foot, { autoAlpha: 1, y: 0, duration: 1.1, ease: 'expo.out' }, 0.45)
.to(nav, { autoAlpha: 1, y: 0, duration: 1.1, ease: 'expo.out' }, 0.5);
// Scroll: image shrinks into a rounded frame, title rows drift apart
const frameInset = () => (isMobile() ? 'inset(12% 4% 16% 4% round 16px)' : 'inset(9% 6% 9% 6% round 28px)');
gsap.timeline({
scrollTrigger: {
trigger: hero,
start: 'top top',
end: '+=85%',
pin: true,
scrub: true,
invalidateOnRefresh: true,
},
})
.fromTo(media, { clipPath: 'inset(0% 0% 0% 0% round 0px)' }, { clipPath: frameInset, ease: 'none', duration: 1 }, 0)
.to(rows, { autoAlpha: 0, ease: 'power1.in', duration: 0.55 }, 0.45)
.fromTo([label, foot], { autoAlpha: 1, y: 0 }, { autoAlpha: 0, y: -30, ease: 'none', duration: 0.4, immediateRender: false }, 0);
const ticker = setupTicker();
// Slow Ken Burns (runs once the preloader has handed off)
const startLoop = () => {
const kb = gsap.fromTo(img, { scale: 1 }, { scale: 1.12, duration: 18, ease: 'sine.inOut', yoyo: true, repeat: -1 });
ScrollTrigger.create({ trigger: hero, start: 'top bottom', end: 'bottom top', onToggle: (self) => (self.isActive ? kb.resume() : kb.pause()) });
if (ticker) ticker.start();
};
return { intro, startLoop };
}
/* Hero ticker — cycles destinations with coordinates and live local time */
function setupTicker() {
const place = $('.ticker__place');
const coord = $('.ticker__coord');
const time = $('.ticker__time');
if (!place) return null;
const PLACES = [
['Cappadocia', '38.64°N 34.83°E', 3],
['Lofoten', '68.23°N 14.56°E', 2],
['Atacama', '22.91°S 68.20°W', -3],
['Lycian Coast', '36.55°N 29.12°E', 3],
['Wadi Rum', '29.58°N 35.42°E', 3],
['Azores', '37.74°N 25.67°W', 0],
];
let i = 0;
const clock = (off) => {
const d = new Date(Date.now() + off * 3600e3);
return pad2(d.getUTCHours()) + ':' + pad2(d.getUTCMinutes()) + ' local';
};
const paint = () => { place.textContent = PLACES[i][0]; coord.textContent = PLACES[i][1]; time.textContent = clock(PLACES[i][2]); };
paint();
const parts = [place, coord, time];
function next() {
gsap.timeline()
.to(parts, { autoAlpha: 0, duration: 0.4, ease: 'power1.in' })
.add(() => { i = (i + 1) % PLACES.length; paint(); })
.to(parts, { autoAlpha: 1, duration: 0.6, ease: 'power1.out' });
}
return {
start() {
gsap.from(parts, { autoAlpha: 0, duration: 0.8, ease: 'power1.out' });
setInterval(() => { if (!document.hidden && window.scrollY < window.innerHeight) next(); }, 3400);
},
};
}
/* ------------------------------------------------------------------------
05 · Manifesto — scrubbed word highlight + popping inline images
------------------------------------------------------------------------ */
function setupManifesto() {
const sec = $('.manifesto');
if (!sec) return;
// headline lines rise out of a mask
$$('.mf__line', sec).forEach((l) => { l.style.overflow = 'hidden'; l.innerHTML = '<span class="mf__line-in" style="display:inline-block">' + l.innerHTML + '</span>'; });
gsap.from($$('.mf__line-in', sec), { yPercent: 110, duration: 1.3, ease: 'expo.out', stagger: 0.1, scrollTrigger: { trigger: '.mf__title', start: 'top 82%', once: true } });
gsap.from(['.mf__lead', '.mf__sign'], { y: 30, autoAlpha: 0, duration: 1.1, ease: 'expo.out', stagger: 0.1, scrollTrigger: { trigger: '.mf__lead', start: 'top 85%', once: true } });
// photos unveil, then drift at different speeds (transform-only parallax)
const main = $('.mf__pic--main'), small = $('.mf__pic--small');
gsap.fromTo(main, { clipPath: 'inset(100% 0% 0% 0% round 22px)' }, { clipPath: 'inset(0% 0% 0% 0% round 22px)', duration: 1.5, ease: 'expo.inOut', scrollTrigger: { trigger: '.mf__media', start: 'top 80%', once: true } });
gsap.fromTo(small, { autoAlpha: 0, y: 60 }, { autoAlpha: 1, y: 0, duration: 1.3, ease: 'expo.out', delay: 0.5, scrollTrigger: { trigger: '.mf__media', start: 'top 80%', once: true } });
gsap.fromTo($('img', main), { yPercent: -6, scale: 1.12 }, { yPercent: 6, scale: 1.12, ease: 'none', scrollTrigger: { trigger: '.mf__media', start: 'top bottom', end: 'bottom top', scrub: true } });
gsap.fromTo(small, { yPercent: 15 }, { yPercent: -15, ease: 'none', immediateRender: false, scrollTrigger: { trigger: '.mf__media', start: 'top bottom', end: 'bottom top', scrub: true } });
}
/* ------------------------------------------------------------------------
06 · Journeys — pinned horizontal track, inner parallax, counter, drag
------------------------------------------------------------------------ */
function setupJourneys() {
const section = $('.journeys');
if (!section) return;
const pin = $('.journeys__pin', section);
const viewport = $('.journeys__viewport', section);
const track = $('.journeys__track', section);
const cards = $$('.jcard:not(.jcard--end)', track);
const all = $$('.jcard', track);
const current = $('.journeys__current', section);
const bar = $('.journeys__bar', section);
const n = cards.length;
let lastIdx = 1;
section.classList.add('is-pinned');
const dist = () => Math.max(0, track.scrollWidth - document.documentElement.clientWidth);
const scrollTween = gsap.to(track, {
x: () => -dist(),
ease: 'none',
scrollTrigger: {
trigger: pin,
start: 'top top',
end: () => '+=' + dist(),
pin: true,
scrub: true,
invalidateOnRefresh: true,
anticipatePin: 1,
onUpdate: (self) => {
const p = self.progress;
gsap.set(bar, { scaleX: 1 / n + p * (1 - 1 / n) });
const idx = Math.min(n, 1 + Math.floor(p * n * 0.999));
if (idx !== lastIdx) {
lastIdx = idx;
current.textContent = pad2(idx);
gsap.fromTo(current, { yPercent: 40, opacity: 0 }, { yPercent: 0, opacity: 1, duration: 0.45, ease: 'power3.out' });
}
},
},
});
// Inner image parallax relative to the horizontal movement
cards.forEach((card) => {
const img = $('.jcard__media img', card);
gsap.fromTo(img, { xPercent: 8 }, {
xPercent: -8,
ease: 'none',
scrollTrigger: { trigger: card, containerAnimation: scrollTween, start: 'left right', end: 'right left', scrub: true },
});
});
// Cards swell up from their baseline, one after another (y is owned by the wave)
gsap.from(all, {
autoAlpha: 0,
scale: 0.86,
transformOrigin: '50% 100%',
duration: 1.3,
ease: 'expo.out',
stagger: 0.09,
scrollTrigger: { trigger: section, start: 'top 65%', toggleActions: 'play none none none' },
});
// Waves — every card rides a travelling sine wave (y + tilt) as the row slides by;
// a thin wave thread runs behind the cards so you can see the swell they ride.
const NS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(NS, 'svg');
svg.setAttribute('class', 'journeys__wave');
svg.setAttribute('aria-hidden', 'true');
const lineA = svg.appendChild(document.createElementNS(NS, 'path'));
const lineB = svg.appendChild(document.createElementNS(NS, 'path'));
lineB.setAttribute('class', 'is-faint');
track.prepend(svg);
const setY = all.map((c) => gsap.quickSetter(c, 'y', 'px'));
const setR = all.map((c) => gsap.quickSetter(c, 'rotation', 'deg'));
let geo = [], baseY = 0, trackW = 0, vw = 0, k = 0;
const measure = () => {
vw = document.documentElement.clientWidth;
k = (Math.PI * 2) / (vw * 1.5); // long, gentle swells
trackW = track.scrollWidth;
geo = all.map((c) => c.offsetLeft + c.offsetWidth / 2);
const media = $('.jcard__media', cards[0]);
baseY = cards[0].offsetTop + (media ? media.offsetHeight / 2 : cards[0].offsetHeight / 2);
svg.setAttribute('width', trackW); svg.setAttribute('height', track.offsetHeight);
svg.setAttribute('viewBox', '0 0 ' + trackW + ' ' + track.offsetHeight);
};
measure();
ScrollTrigger.addEventListener('refresh', measure);
let active = false, t = 0, swell = 1;
ScrollTrigger.create({ trigger: section, start: 'top bottom', end: 'bottom top', onToggle: (self) => { active = self.isActive; } });
gsap.ticker.add((time, dt) => {
if (!active) return;
t += Math.min(dt, 50) / 1000;
const st = scrollTween.scrollTrigger;
const v = st ? Math.abs(st.getVelocity()) : 0;
swell += (1 + Math.min(v / 3000, 0.35) - swell) * 0.03; // fast scrolling swells it slightly
const amp = Math.min(16, vw * 0.011) * swell;
const tx = gsap.getProperty(track, 'x');
const ph = t * 0.35; // slow drift
for (let i = 0; i < all.length; i++) {
const a = (geo[i] + tx) * k - ph;
setY[i](Math.sin(a) * amp);
setR[i](Math.cos(a) * 0.5);
}
// wave thread: only the visible stretch, sampled every 28px
const x0 = Math.max(0, -tx - 60), x1 = Math.min(trackW, -tx + vw + 60);
let dA = '', dB = '';
for (let x = x0; x <= x1; x += 28) {
const a = (x + tx) * k - ph;
dA += (dA ? 'L' : 'M') + x.toFixed(0) + ' ' + (baseY + Math.sin(a) * amp).toFixed(1);
dB += (dB ? 'L' : 'M') + x.toFixed(0) + ' ' + (baseY + 26 + Math.sin(a - 0.9) * amp * 0.7).toFixed(1);
}
lineA.setAttribute('d', dA); lineB.setAttribute('d', dB);
});
// Drag-to-scroll on desktop (moves the page, so ScrollTrigger stays the source of truth)
if (finePointer && lenis && window.Observer) {
Observer.create({
target: viewport,
type: 'pointer',
dragMinimum: 3,
onPress: () => viewport.classList.add('is-grabbing'),
onRelease: () => viewport.classList.remove('is-grabbing'),
onDrag: (self) => {
const st = scrollTween.scrollTrigger;
if (!st || !st.isActive) return;
const target = gsap.utils.clamp(st.start, st.end, lenis.targetScroll - self.deltaX * 1.6);
lenis.scrollTo(target);
},
});
}
}
/* ------------------------------------------------------------------------
07 · Zoom-through — small window scales to fill the viewport while pinned
------------------------------------------------------------------------ */
function setupZoom() {
const stage = $('.zoom__stage');
if (!stage) return;
const win = $('.zoom__window', stage);
const img = $('img', win);
const veil = $('.zoom__veil', stage);
const sideL = $('.zoom__side--l', stage);
const sideR = $('.zoom__side--r', stage);
const parts = $$('.zoom__content > *', stage);
const smallInset = () => {
const vw = window.innerWidth;
const vh = window.innerHeight;
const w = vw < 640 ? vw * 0.56 : Math.min(vw * 0.24, 380);
const h = Math.min(w * 1.25, vh * 0.5);
const x = ((vw - w) / 2 / vw) * 100;
const y = ((vh - h) / 2 / vh) * 100;
return `inset(${y.toFixed(2)}% ${x.toFixed(2)}% ${y.toFixed(2)}% ${x.toFixed(2)}% round 14px)`;
};
gsap.set([sideL, sideR], { opacity: 1 });
gsap.set(veil, { opacity: 0 });
gsap.set(parts, { autoAlpha: 0, y: 50 });
gsap.timeline({
scrollTrigger: {
trigger: stage,
start: 'top top',
end: '+=170%',
pin: true,
scrub: true,
invalidateOnRefresh: true,
},
})
.fromTo(win, { clipPath: smallInset }, { clipPath: 'inset(0% 0% 0% 0% round 0px)', ease: 'power2.inOut', duration: 1 }, 0)
.fromTo(img, { scale: 1.6 }, { scale: 1, ease: 'power2.inOut', duration: 1 }, 0)
.fromTo(sideL, { xPercent: 0 }, { xPercent: -40, opacity: 0, ease: 'power1.in', duration: 0.55 }, 0.05)
.fromTo(sideR, { xPercent: 0 }, { xPercent: 40, opacity: 0, ease: 'power1.in', duration: 0.55 }, 0.05)
.to(veil, { opacity: 1, ease: 'none', duration: 0.5 }, 0.5)
.to(parts, { autoAlpha: 1, y: 0, ease: 'power3.out', duration: 0.35, stagger: 0.08 }, 0.78)
.to({}, { duration: 0.35 }); // hold on the revealed state
}
/* ------------------------------------------------------------------------
08 · Retreats — native sticky stack; previous card scales + darkens
Offsets are computed from the (non-sticky) stack wrapper so they stay
correct whatever the scroll position at refresh time.
------------------------------------------------------------------------ */
function setupRetreats() {
const stack = $('.retreats__stack');
if (!stack) return;
const cards = $$('.retreat', stack);
const H = () => cards[0].offsetHeight;
const at = (n) => (n >= 0 ? `top+=${Math.round(n)} top` : `top-=${Math.round(-n)} top`);
cards.forEach((card, i) => {
const inner = $('.retreat__inner', card);
const shade = $('.retreat__shade', card);
const img = $('.retreat__media img', card);
const body = $$('.retreat__body > *', card);
// Entering: image settles from a slight zoom
gsap.fromTo(img, { scale: 1.25 }, {
scale: 1,
ease: 'none',
scrollTrigger: {
trigger: stack,
start: () => at((i - 1) * H()),
end: () => at(i * H()),
scrub: true,
invalidateOnRefresh: true,
},
});
// Text reveal as the card lands
gsap.from(body, {
y: 40,
autoAlpha: 0,
duration: 1,
ease: 'expo.out',
stagger: 0.07,
scrollTrigger: {
trigger: stack,
start: () => at((i - 0.35) * H()),
toggleActions: 'play none none none',
invalidateOnRefresh: true,
},
});
if (i === cards.length - 1) return;
// Being covered: scale down + darken
gsap.timeline({
scrollTrigger: {
trigger: stack,
start: () => at(i * H()),
end: () => at((i + 1) * H()),
scrub: true,
invalidateOnRefresh: true,
},
})
.to(inner, { scale: 0.88, ease: 'none' }, 0)
.to(shade, { opacity: 0.6, ease: 'none' }, 0);
});
}
/* ------------------------------------------------------------------------
09 · Experiences — clip-path wipes + scale
------------------------------------------------------------------------ */
function setupExperiences() {
$$('.exp__item').forEach((item, i) => {
const frame = $('.exp__frame', item);
const img = $('img', frame);
const cap = $('.exp__cap', item);
const from = ['inset(100% 0% 0% 0%)', 'inset(0% 0% 100% 0%)', 'inset(0% 100% 0% 0%)'][i % 3];
gsap.timeline({ scrollTrigger: { trigger: item, start: 'top 90%', toggleActions: 'play none none none' } })
.fromTo(frame, { clipPath: from }, { clipPath: 'inset(0% 0% 0% 0%)', duration: 1.4, ease: 'expo.inOut' })
.fromTo(img, { scale: 1.4 }, { scale: 1, duration: 1.8, ease: 'expo.out', onComplete: () => gsap.set(img, { clearProps: 'transform' }) }, 0.15)
.fromTo(cap, { autoAlpha: 0, y: 12 }, { autoAlpha: 1, y: 0, duration: 0.8, ease: 'power3.out' }, 0.7);
});
}
/* ------------------------------------------------------------------------
10 · Places marquee — infinite loops, scroll-velocity skew + boost
------------------------------------------------------------------------ */
function setupMarquee() {
// Place names are a still list now (no looping / skewing text); just fade the rows in
const rows = $$('.places__row');
if (!rows.length) return;
gsap.from(rows, { autoAlpha: 0, duration: 1.2, ease: 'power1.out', stagger: 0.15,
scrollTrigger: { trigger: '.places', start: 'top 75%', once: true } });
}
/* ------------------------------------------------------------------------
11 · Booking — magnetic submit button
------------------------------------------------------------------------ */
function setupBooking() {
const card = $('.booking__card');
if (!card) return;
const bv = $('.bv', card);
const media = $('.bv__media', card);
const trig = { trigger: card, start: 'top 80%', toggleActions: 'play none none none' };
gsap.from(card, { y: 90, clipPath: 'inset(12% 6% 0% 6% round 40px)', duration: 1.5, ease: 'expo.out', scrollTrigger: trig });
gsap.from('.bv__img.is-front', { scale: 1.3, duration: 2.2, ease: 'expo.out', scrollTrigger: { ...trig } });
gsap.from('.bv__top > *, .bv__meta > *', { y: 30, autoAlpha: 0, duration: 1.1, ease: 'expo.out', stagger: 0.08, delay: 0.35, scrollTrigger: { ...trig } });
gsap.from('.booking__form > *', { y: 26, autoAlpha: 0, duration: 1, ease: 'expo.out', stagger: 0.06, delay: 0.25, scrollTrigger: { ...trig } });
gsap.from('.party i', { scaleY: 0, transformOrigin: '50% 100%', duration: 0.6, ease: 'back.out(3)', stagger: 0.05, delay: 0.8, scrollTrigger: { ...trig } });
// photo drifts slower than the page
gsap.fromTo(media, { yPercent: -3 }, { yPercent: 3, ease: 'none', scrollTrigger: { trigger: card, start: 'top bottom', end: 'bottom top', scrub: true } });
if (!finePointer) return;
// photo leans toward the pointer
const mx = gsap.quickTo(media, 'x', { duration: 1, ease: 'power3' });
const rotY = gsap.quickTo(media, 'rotationY', { duration: 1, ease: 'power3' });
bv.addEventListener('pointermove', (e) => {
const r = bv.getBoundingClientRect();
const nx = (e.clientX - r.left) / r.width - 0.5;
mx(nx * -18); rotY(nx * 4);
});
bv.addEventListener('pointerleave', () => { mx(0); rotY(0); });
// gentle magnetic pull on the send button
const btn = $('.btn-send', card);
const icon = $('.btn-send__icon', btn);
const bx = gsap.quickTo(btn, 'x', { duration: 0.7, ease: 'elastic.out(1, 0.5)' });
const by = gsap.quickTo(btn, 'y', { duration: 0.7, ease: 'elastic.out(1, 0.5)' });
const ix = gsap.quickTo(icon, 'x', { duration: 0.7, ease: 'elastic.out(1, 0.5)' });
btn.addEventListener('pointermove', (e) => {
const r = btn.getBoundingClientRect();
const dx = e.clientX - (r.left + r.width / 2), dy = e.clientY - (r.top + r.height / 2);
bx(dx * 0.18); by(dy * 0.3); ix(dx * 0.12);
});
btn.addEventListener('pointerleave', () => { bx(0); by(0); ix(0); });
}
/* ------------------------------------------------------------------------
12 · Footer — giant wordmark letter-by-letter
------------------------------------------------------------------------ */
function setupReviews() {
const box = $('.reviews');
if (!box) return;
const track = $('.reviews__track', box);
Array.from(track.children).forEach((card) => {
const c = card.cloneNode(true);
c.setAttribute('aria-hidden', 'true');
track.appendChild(c);
});
box.classList.add('is-ready');
if ('IntersectionObserver' in window) {
new IntersectionObserver((es) => box.classList.toggle('is-off', !es[0].isIntersecting)).observe(box);
}
}
function setupFooter() {
const mark = $('.footer__mark-text');
if (!mark) return;
const split = SplitText.create(mark, { type: 'chars', charsClass: 'mark-char' });
gsap.fromTo(split.chars, { yPercent: 105 }, {
yPercent: 0,
ease: 'power2.out',
stagger: 0.12,
scrollTrigger: { trigger: '.footer__mark', start: 'top 98%', end: 'bottom bottom', scrub: true },
});
gsap.from('.footer__top > *', {
y: 40, autoAlpha: 0, duration: 1.1, ease: 'expo.out', stagger: 0.1,
scrollTrigger: { trigger: '.footer', start: 'top 80%', toggleActions: 'play none none none' },
});
}
/* ------------------------------------------------------------------------
13 · Generic heading reveals (masked lines) + labels
------------------------------------------------------------------------ */
function setupHeadings() {
const heads = $$('.h2, .journal__title, .booking__title, .footer__heading');
heads.forEach((h) => {
SplitText.create(h, {
type: 'lines',
mask: 'lines',
linesClass: 'revealline',
autoSplit: true,
onSplit: (self) =>
gsap.from(self.lines, {
yPercent: 110,
duration: 1.3,
ease: 'expo.out',
stagger: 0.1,
scrollTrigger: { trigger: h, start: 'top 88%', toggleActions: 'play none none none' },
}),
});
});
$$('.label').forEach((l) => {
if (l.closest('.hero, .zoom, .preloader')) return;
gsap.from(l, { autoAlpha: 0, x: -16, duration: 1, ease: 'expo.out', scrollTrigger: { trigger: l, start: 'top 90%', toggleActions: 'play none none none' } });
});
$$('.section-head__text, .booking__text, .booking__small').forEach((el) => {
gsap.from(el, { autoAlpha: 0, y: 24, duration: 1.1, ease: 'expo.out', scrollTrigger: { trigger: el, start: 'top 90%', toggleActions: 'play none none none' } });
});
}
/* ------------------------------------------------------------------------
14 · Init
------------------------------------------------------------------------ */
function initMotion() {
gsap.registerPlugin(ScrollTrigger, SplitText, Flip);
if (window.Observer) gsap.registerPlugin(Observer);
ScrollTrigger.config({ ignoreMobileResize: true });
if ('scrollRestoration' in history) history.scrollRestoration = 'manual';
window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
initLenis();
stopScroll();
initCursor();
// Create ScrollTriggers in document order (pins first-come) for correct offsets
const hero = setupHero();
setupManifesto();
setupJourneys();
setupZoom();
setupRetreats();
setupExperiences();
setupMarquee();
initJournal(true);
setupBooking();
setupReviews();
setupFooter();
setupHeadings();
initLightbox(true);
initImageRefresh();
runPreloader(hero.intro, hero.startLoop);
}
function initStatic() {
root.classList.remove('is-anim');
removePreloader();
initLightbox(false);
initJournal(false);
}
// Always-on
initMenu();
initAnchors();
initBookingForm();
initNewsletter();
if (!MOTION) {
initStatic();
return;
}
// Wait for webfonts (bounded) so SplitText measures real glyphs
const fontsReady = document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve();
Promise.race([fontsReady, wait(2500)]).then(() => {
try {
initMotion();
} catch (err) {
// Never leave the visitor behind a loader or with hidden content
console.error('[Solace] motion init failed:', err);
if (window.ScrollTrigger) ScrollTrigger.getAll().forEach((t) => t.kill());
if (lenis) { lenis.destroy(); lenis = null; }
const j = $('.journeys');
if (j) j.classList.remove('is-pinned');
gsap.killTweensOf('*');
gsap.set([
'.nav', '.hero__media', '.hero__img', '.hero__veil', '.hero__row', '.hero__label', '.hero__foot', '.heroline',
'.m-word', '.m-img', '.m-img img', '.stats__item', '.jcard', '.jcard img', '.journeys__track',
'.zoom__window', '.zoom__window img', '.zoom__veil', '.zoom__side', '.zoom__content > *',
'.retreat__inner', '.retreat__shade', '.retreat__media img', '.retreat__body > *',
'.exp__frame', '.exp__frame img', '.exp__cap', '.places__row', '.places__track', '.label', '.revealline',
'.section-head__text', '.booking__text', '.booking__small', '.booking__panel', '.booking__card', '.bv__img', '.bv__media', '.bv__top > *', '.bv__meta > *', '.booking__form > *', '.party i', '.mark-char', '.footer__top > *',
'.letters__frame', '.letters__img', '.letters__stamp', '.letters__quote', '.letters__name', '.letters__trip', '.ltab', '.ltab__bar i', '.letters__mark', '.m-mark', '.hero__title',
].join(','), { clearProps: 'all' });
initStatic();
}
});
})();
```
## `style.css`
```css
/* ==========================================================================
SOLACE — slow travel & retreats · monochrome palette: white paper, grey-black ink
Plain CSS. Content is always visible by default; animation start-states
are applied from JS (gsap.set) so the page reads fine without GSAP.
========================================================================== */
/* --------------------------------------------------------------------------
00 · Tokens
-------------------------------------------------------------------------- */
:root {
--paper: #FFFFFF;
--paper-2: #FAFAFA;
--ink: #1C1C1C;
--muted: #767676;
--muted-strong: #595959; /* AA-safe muted for small text on paper */
--accent: #8A8A8A;
--olive: #161616;
--sand: #F6F6F6;
--rule: rgba(28, 28, 28, 0.12);
--rule-light: rgba(255, 255, 255, 0.28);
--serif: "Fraunces", "Iowan Old Style", Georgia, serif;
--sans: "DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
--fs-display: clamp(3.5rem, 13.5vw, 12.5rem);
--fs-h2: clamp(2.75rem, 6.4vw, 6.5rem);
--fs-h3: clamp(1.75rem, 3vw, 2.75rem);
--fs-lead: clamp(1.9rem, 4.3vw, 4.4rem);
--fs-body: clamp(1rem, 0.95rem + 0.2vw, 1.0625rem);
--fs-small: 0.8125rem;
--fs-label: 0.72rem;
--gutter: clamp(1rem, 3.2vw, 2.75rem);
--section-y: clamp(6rem, 13vw, 12rem);
--radius: clamp(12px, 1.6vw, 22px);
--nav-h: 4.75rem;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}
/* --------------------------------------------------------------------------
01 · Base / reset
-------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html {
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
html:not(.lenis) { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: var(--sans);
font-size: var(--fs-body);
font-weight: 400;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: clip;
text-rendering: optimizeLegibility;
}
img { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
button, input, select { font: inherit; color: inherit; }
button { cursor: pointer; }
ul, ol { list-style: none; margin: 0; padding: 0; }
h1, h2, h3, p, figure, blockquote, dl, dd { margin: 0; }
em { font-style: italic; }
[hidden] { display: none !important; }
::selection { background: var(--accent); color: var(--paper); }
:focus { outline: none; }
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
border-radius: 2px;
}
.sr-only {
position: absolute !important;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0);
white-space: nowrap; border: 0;
}
.skip-link {
position: fixed;
top: 0.75rem; left: 0.75rem;
z-index: 200;
padding: 0.6rem 1rem;
background: var(--ink);
color: var(--paper);
font-size: var(--fs-small);
transform: translateY(-160%);
transition: transform 0.3s var(--ease-out);
}
.skip-link:focus-visible { transform: none; }
/* Lenis */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: hidden; }
/* --------------------------------------------------------------------------
02 · Shared type & utilities
-------------------------------------------------------------------------- */
.container {
width: 100%;
max-width: 1600px;
margin-inline: auto;
padding-inline: var(--gutter);
}
.section { padding-block: var(--section-y); }
.label {
display: inline-flex;
align-items: baseline;
gap: 0.6em;
font-family: var(--sans);
font-size: var(--fs-label);
font-weight: 500;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted-strong);
line-height: 1.4;
}
.label__num {
color: var(--accent);
font-variant-numeric: tabular-nums;
letter-spacing: 0.06em;
}
.h2 {
font-family: var(--serif);
font-weight: 400;
font-size: var(--fs-h2);
line-height: 0.95;
letter-spacing: -0.02em;
}
.h2 em, .zoom__title em, .booking__title em, .journal__title em, .footer__heading em,
.retreat__title em, .jcard__end-title em {
color: var(--accent);
}
.section-head {
display: grid;
grid-template-columns: 1fr;
gap: 1.25rem;
margin-bottom: clamp(3rem, 6vw, 5.5rem);
}
.section-head__text {
max-width: 32rem;
color: var(--muted-strong);
}
@media (min-width: 900px) {
.section-head {
grid-template-columns: 1fr minmax(0, 26rem);
align-items: end;
}
.section-head .label { grid-column: 1 / -1; }
}
.link-arrow {
display: inline-flex;
align-items: center;
gap: 0.6em;
padding: 0 0 0.35em;
border: 0;
border-bottom: 1px solid currentColor;
background: none;
font-size: var(--fs-small);
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.link-arrow::after {
content: "→";
transition: transform 0.45s var(--ease-out);
}
.link-arrow:hover::after { transform: translateX(0.35em); }
/* SplitText masks (JS-only elements): give descenders & italic overhang room */
.heroline-mask, .revealline-mask {
padding: 0.02em 0.08em 0.16em;
margin: -0.02em -0.08em -0.16em;
}
[tabindex="-1"]:focus { outline: none; }
html.has-cursor [data-cursor] { cursor: none; }
.journeys__viewport.is-grabbing { cursor: grabbing; }
/* --------------------------------------------------------------------------
03 · Grain + cursor
-------------------------------------------------------------------------- */
.grain {
position: fixed;
inset: -50%;
z-index: 95;
pointer-events: none;
opacity: 0.075;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.9 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
animation: grain 1.2s steps(6) infinite;
}
@media (max-width: 767px) { .grain { opacity: 0.06; } }
@keyframes grain {
0% { transform: translate(0, 0); }
20% { transform: translate(-4%, 3%); }
40% { transform: translate(3%, -5%); }
60% { transform: translate(-2%, 6%); }
80% { transform: translate(5%, -2%); }
100% { transform: translate(0, 0); }
}
.cursor { display: none; }
html.has-cursor .cursor {
position: fixed;
top: 0; left: 0;
z-index: 90;
display: grid;
place-items: center;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--paper);
mix-blend-mode: difference;
pointer-events: none;
transition: width 0.45s var(--ease-out), height 0.45s var(--ease-out), background-color 0.3s;
will-change: transform;
}
html.has-cursor .cursor.is-link { width: 38px; height: 38px; }
html.has-cursor .cursor.is-label {
width: 88px;
height: 88px;
background: var(--accent);
mix-blend-mode: normal;
}
.cursor__label {
font-size: 0.68rem;
font-weight: 500;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--paper);
opacity: 0;
transition: opacity 0.25s;
}
.cursor.is-label .cursor__label { opacity: 1; }
html.has-cursor.is-hidden-cursor .cursor { opacity: 0; }
/* --------------------------------------------------------------------------
04 · Preloader (only shown when GSAP is available & motion allowed)
-------------------------------------------------------------------------- */
.preloader { display: none; }
.is-anim .preloader {
display: block;
position: fixed;
inset: 0;
z-index: 100;
background: var(--paper);
color: var(--ink);
/* Failsafe: if script.js never runs, don't trap the page behind the loader */
animation: pl-failsafe 0.6s ease 12s forwards;
}
@keyframes pl-failsafe { to { opacity: 0; visibility: hidden; } }
.preloader__stack {
position: absolute;
inset: 0;
margin: auto;
width: clamp(150px, 17vw, 250px);
height: clamp(188px, 21.25vw, 312px);
z-index: 2;
}
.preloader__card {
position: absolute;
inset: 0;
overflow: hidden;
border-radius: 6px;
background: var(--sand);
}
.preloader__card img {
width: 100%;
height: 100%;
object-fit: cover;
}
.preloader__word,
.preloader__count {
position: absolute;
bottom: var(--gutter);
z-index: 1;
overflow: hidden;
font-family: var(--serif);
font-size: clamp(3rem, 9vw, 8rem);
line-height: 0.95;
padding-top: 0.05em;
}
.preloader__word { left: var(--gutter); }
.preloader__word span { display: inline-block; }
.preloader__count {
right: var(--gutter);
display: flex;
align-items: flex-start;
font-variant-numeric: tabular-nums;
}
.preloader__pct {
font-size: 0.35em;
margin-left: 0.1em;
margin-top: 0.3em;
color: var(--accent);
}
.preloader__tag {
position: absolute;
top: calc(var(--gutter) + 0.5rem);
left: 0; right: 0;
text-align: center;
font-size: var(--fs-label);
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted-strong);
}
/* --------------------------------------------------------------------------
05 · Nav + mobile menu
-------------------------------------------------------------------------- */
.nav {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 60;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
height: var(--nav-h);
padding-inline: var(--gutter);
color: var(--paper);
mix-blend-mode: difference;
pointer-events: none;
}
.nav > * { pointer-events: auto; }
.nav__left { display: flex; gap: 2rem; }
.nav__left a, .nav__cta {
position: relative;
font-size: var(--fs-small);
font-weight: 500;
letter-spacing: 0.02em;
padding-block: 0.4rem;
}
.nav__left a::after, .nav__cta::after {
content: "";
position: absolute;
left: 0; right: 0; bottom: 0.1rem;
height: 1px;
background: currentColor;
transform: scaleX(0);
transform-origin: right;
transition: transform 0.5s var(--ease-out);
}
.nav__left a:hover::after, .nav__cta:hover::after {
transform: scaleX(1);
transform-origin: left;
}
.nav__brand {
display: inline-flex; align-items: center; gap: .32em;
font-family: var(--serif);
font-size: clamp(1.6rem, 2.2vw, 2rem);
letter-spacing: -0.01em;
line-height: 1;
}
.nav__right {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 1.25rem;
}
.nav__toggle {
display: none;
align-items: center;
gap: 0.6rem;
padding: 0.5rem 0;
border: 0;
background: none;
font-size: var(--fs-small);
font-weight: 500;
}
.nav__toggle-lines {
position: relative;
display: block;
width: 22px;
height: 8px;
}
.nav__toggle-lines i {
position: absolute;
left: 0; right: 0;
height: 1px;
background: currentColor;
transition: transform 0.5s var(--ease-out), top 0.5s var(--ease-out);
}
.nav__toggle-lines i:first-child { top: 0; }
.nav__toggle-lines i:last-child { top: 7px; }
.nav__toggle[aria-expanded="true"] .nav__toggle-lines i:first-child { top: 3.5px; transform: rotate(45deg); }
.nav__toggle[aria-expanded="true"] .nav__toggle-lines i:last-child { top: 3.5px; transform: rotate(-45deg); }
@media (max-width: 899px) {
.nav__left, .nav__cta { display: none; }
.nav__toggle { display: inline-flex; }
.nav__brand { grid-column: 1; justify-self: start; }
.nav__right { grid-column: 3; }
}
.menu {
position: fixed;
inset: 0;
z-index: 55;
display: flex;
align-items: flex-end;
background: var(--ink);
color: var(--paper);
padding: calc(var(--nav-h) + 2rem) var(--gutter) var(--gutter);
}
.menu__inner { width: 100%; }
.menu__list li {
overflow: hidden;
border-top: 1px solid var(--rule-light);
}
.menu__list a {
display: block;
padding: 0.35rem 0 0.2rem;
font-family: var(--serif);
font-size: clamp(2.75rem, 12vw, 5rem);
line-height: 1.05;
}
.menu__list a span { display: inline-block; }
.menu__list a em { color: rgba(255, 255, 255, 0.72); }
.menu__foot {
margin-top: 2rem;
font-size: var(--fs-small);
color: rgba(255, 255, 255, 0.7);
}
/* --------------------------------------------------------------------------
06 · Hero
-------------------------------------------------------------------------- */
.hero {
position: relative;
height: 100vh;
height: 100svh;
min-height: 560px;
overflow: hidden;
color: var(--paper);
}
.hero__media {
position: absolute;
inset: 0;
overflow: hidden;
background: var(--sand);
}
.hero__img {
width: 100%;
height: 100%;
object-fit: cover;
transform-origin: 60% 40%;
}
.hero__veil {
position: absolute;
inset: 0;
background:
linear-gradient(180deg, rgba(20, 20, 20, 0.35) 0%, rgba(20, 20, 20, 0) 24%),
linear-gradient(0deg, rgba(20, 20, 20, 0.62) 0%, rgba(20, 20, 20, 0.1) 52%, rgba(20, 20, 20, 0) 70%);
}
.hero__inner {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: var(--nav-h) var(--gutter) clamp(1.25rem, 2.5vw, 2.25rem);
}
.hero__label { color: rgba(255, 255, 255, 0.85); margin-bottom: clamp(1rem, 2vw, 1.75rem); }
.hero__label .label__num { color: rgba(255, 255, 255, 0.72); }
.hero__title {
font-family: var(--serif);
font-weight: 400;
font-size: min(var(--fs-display), 16.5svh);
line-height: 0.9;
letter-spacing: -0.025em;
}
.hero__row { display: block; will-change: transform; }
.hero__row--b { padding-left: 18vw; }
.hero__row em { color: rgba(255, 255, 255, 0.72); }
.hero__foot {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 2rem;
margin-top: clamp(1.5rem, 3vw, 2.75rem);
padding-top: 1.1rem;
border-top: 1px solid var(--rule-light);
}
.hero__lede {
max-width: 27rem;
font-size: var(--fs-body);
color: rgba(255, 255, 255, 0.9);
}
.hero__scroll {
display: flex;
align-items: center;
gap: 0.9rem;
flex: none;
font-size: var(--fs-label);
font-weight: 500;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.hero__scroll-line {
position: relative;
width: 3.5rem;
height: 1px;
overflow: hidden;
background: var(--rule-light);
}
.hero__scroll-line::after {
content: "";
position: absolute;
inset: 0;
background: var(--paper);
animation: scroll-line 2.4s var(--ease-out) infinite;
}
@keyframes scroll-line {
0% { transform: translateX(-100%); }
60%, 100% { transform: translateX(100%); }
}
/* hero v2: live destination ticker */
.hero__ticker {
display: flex; align-items: baseline; gap: .9rem; flex-wrap: wrap;
font-size: var(--fs-label); letter-spacing: .16em; text-transform: uppercase; font-weight: 500;
color: rgba(255, 255, 255, .75);
}
.ticker__slot { display: inline-block; overflow: hidden; vertical-align: bottom; padding-bottom: .1em; }
.ticker__slot > span { display: inline-block; }
.ticker__place { font-family: var(--serif); font-size: 1.6rem; letter-spacing: 0; text-transform: none; font-style: italic; color: rgba(255, 255, 255, 0.72); font-weight: 400; }
.ticker__meta { font-variant-numeric: tabular-nums; }
@media (max-width: 900px) { .hero__ticker { display: none; } }
@media (max-width: 640px) {
.hero__row--b { padding-left: 10vw; }
.hero__foot { flex-direction: column; align-items: flex-start; gap: 1rem; }
.hero__lede { font-size: 0.9375rem; }
}
/* --------------------------------------------------------------------------
07 · Manifesto
-------------------------------------------------------------------------- */
/* manifesto: split editorial layout + principles */
.mf { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); gap: clamp(2.5rem, 7vw, 7rem); align-items: center; }
.mf__copy .label { margin-bottom: clamp(1.5rem, 3vw, 2.5rem); }
.mf__title { font-family: var(--serif); font-weight: 400; font-size: var(--fs-h2); line-height: .95; letter-spacing: -.02em; margin: 0; }
.mf__line { display: block; }
.mf__title em { color: var(--accent); }
.mf__lead { margin-top: clamp(1.5rem, 3vw, 2.25rem); max-width: 34rem; font-size: clamp(1.05rem, 1rem + .3vw, 1.25rem); line-height: 1.65; color: var(--muted-strong); }
.mf__sign { margin-top: 1.75rem; display: flex; align-items: baseline; gap: .75rem; font-size: var(--fs-label); letter-spacing: .14em; text-transform: uppercase; color: var(--muted); }
.mf__sign span { font-family: var(--serif); font-style: italic; font-size: 1.5rem; letter-spacing: 0; text-transform: none; color: var(--ink); }
.mf__media { position: relative; padding: 0 0 12% 14%; }
.mf__pic { margin: 0; overflow: hidden; border-radius: var(--radius); }
.mf__pic img { display: block; width: 100%; height: 100%; object-fit: cover; }
.mf__pic--main { aspect-ratio: 4 / 5; }
.mf__pic--small { position: absolute; left: 0; bottom: 0; width: 42%; aspect-ratio: 3 / 4; border: 6px solid var(--paper); box-shadow: 0 24px 50px -24px rgba(20, 20, 20, .45); }
.mf__tag { position: absolute; right: 1rem; top: 1rem; padding: .45rem .8rem; border-radius: 999px; background: var(--paper); color: var(--ink);
font-size: var(--fs-label); letter-spacing: .14em; text-transform: uppercase; font-weight: 500; }
@media (max-width: 860px) {
.mf { grid-template-columns: 1fr; }
}
/* --------------------------------------------------------------------------
08 · Journeys (horizontal)
Default: native horizontal scroll row. JS adds .is-pinned for the
pinned, scroll-driven version.
-------------------------------------------------------------------------- */
.journeys { position: relative; overflow: clip; }
.journeys__pin {
display: flex;
flex-direction: column;
gap: clamp(1.5rem, 4vh, 3rem);
padding-block: var(--section-y);
}
.journeys.is-pinned .journeys__pin {
height: 100vh;
height: 100svh;
justify-content: center;
padding-block: calc(var(--nav-h) * 0.9) clamp(1rem, 3vh, 2rem);
}
.journeys__head {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 2rem;
}
.journeys__head .label { margin-bottom: 1rem; }
.journeys__meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.9rem;
flex: none;
}
.journeys__count {
font-family: var(--serif);
font-size: clamp(1.25rem, 1.8vw, 1.6rem);
font-variant-numeric: tabular-nums;
color: var(--muted-strong);
}
.journeys__current { color: var(--ink); }
.journeys__progress {
position: relative;
width: clamp(6rem, 14vw, 13rem);
height: 1px;
background: var(--rule);
}
.journeys__bar {
position: absolute;
inset: 0;
background: var(--ink);
transform: scaleX(0.1667);
transform-origin: left;
}
.journeys__viewport {
overflow-x: auto;
scrollbar-width: thin;
-webkit-overflow-scrolling: touch;
}
.journeys.is-pinned .journeys__viewport { overflow: visible; }
.journeys.is-pinned .journeys__track { padding-top: 24px; } /* room for the wave's crest below the heading */
.journeys__track {
position: relative;
display: flex;
gap: clamp(1rem, 2.4vw, 2.5rem);
width: max-content;
padding-inline: var(--gutter);
will-change: transform;
}
.jcard {
--card-w: min(76vw, 52vh * 0.8, 440px);
flex: none;
width: var(--card-w);
user-select: none;
}
@supports (height: 1svh) {
.jcard { --card-w: min(76vw, 52svh * 0.8, 440px); }
}
.jcard__media {
position: relative;
aspect-ratio: 4 / 5;
overflow: hidden;
border-radius: 6px;
background: var(--sand);
}
.jcard__media img {
width: 124%;
max-width: none;
height: 100%;
margin-left: -12%;
object-fit: cover;
transition: scale 1.1s var(--ease-out);
}
.jcard:hover .jcard__media img { scale: 1.05; }
.jcard__body { padding-top: 1rem; }
.jcard__top {
display: flex;
justify-content: space-between;
gap: 1rem;
font-size: var(--fs-label);
font-weight: 500;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted-strong);
}
.jcard__num { color: var(--accent); }
.jcard__title {
margin-top: 0.35rem;
font-family: var(--serif);
font-weight: 400;
font-size: clamp(1.6rem, 2.3vw, 2.25rem);
line-height: 1.05;
}
.jcard__facts {
display: flex;
gap: 1.75rem;
margin-top: 0.7rem;
padding-top: 0.7rem;
border-top: 1px solid var(--rule);
font-size: var(--fs-small);
}
.jcard__facts div { display: flex; gap: 0.45rem; }
.jcard__facts dt { color: var(--muted-strong); }
.jcard__facts dd { font-weight: 500; }
.jcard--end {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 1.1rem;
padding: clamp(1.5rem, 3vw, 2.5rem);
border: 1px solid var(--rule);
border-radius: 6px;
aspect-ratio: 4 / 5;
align-self: flex-start;
}
.jcard__end-kicker {
font-size: var(--fs-label);
font-weight: 500;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted-strong);
}
.jcard__end-title {
font-family: var(--serif);
font-size: clamp(2rem, 3.2vw, 3rem);
line-height: 1.02;
}
@media (max-width: 640px) {
.journeys__head { flex-direction: column; align-items: flex-start; gap: 1.25rem; }
.journeys__meta { flex-direction: row; align-items: center; gap: 1rem; }
}
/* --------------------------------------------------------------------------
09 · Zoom-through
-------------------------------------------------------------------------- */
.zoom { position: relative; }
.zoom__stage {
position: relative;
height: 100vh;
height: 100svh;
min-height: 560px;
overflow: hidden;
}
.zoom__window {
position: absolute;
inset: 0;
overflow: hidden;
background: var(--sand);
will-change: clip-path;
}
.zoom__window img {
width: 100%;
height: 100%;
object-fit: cover;
}
.zoom__veil {
position: absolute;
inset: 0;
background: radial-gradient(ellipse at center, rgba(20, 20, 20, 0.5) 0%, rgba(20, 20, 20, 0.2) 70%);
}
.zoom__side {
position: absolute;
top: 50%;
z-index: 1;
margin-top: -0.5em;
font-family: var(--serif);
font-size: clamp(1.75rem, 4vw, 4rem);
line-height: 1;
opacity: 0; /* decorative, aria-hidden; revealed from JS only */
white-space: nowrap;
}
.zoom__side--l { left: var(--gutter); }
.zoom__side--r { right: var(--gutter); color: var(--accent); }
.zoom__content {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1.5rem;
padding: var(--nav-h) var(--gutter);
text-align: center;
color: var(--paper);
}
.zoom__content .label { color: rgba(255, 255, 255, 0.85); }
.zoom__content .label__num { color: rgba(255, 255, 255, 0.72); }
.zoom__title {
max-width: 11em;
font-family: var(--serif);
font-weight: 400;
font-size: clamp(3rem, 9vw, 9rem);
line-height: 0.92;
letter-spacing: -0.02em;
}
.zoom__title em { color: rgba(255, 255, 255, 0.72); }
.zoom__text { max-width: 30rem; color: rgba(255, 255, 255, 0.92); }
@media (max-width: 640px) {
.zoom__side { font-size: 1.5rem; top: 16%; margin-top: 0; }
.zoom__side--r { top: auto; bottom: 16%; }
}
/* --------------------------------------------------------------------------
10 · Retreats (sticky stacked cards — native sticky, GSAP adds depth)
-------------------------------------------------------------------------- */
.retreats { padding-top: var(--section-y); }
.retreats__head {
display: flex;
flex-direction: column;
gap: 1.25rem;
margin-bottom: clamp(1.5rem, 4vw, 3rem);
}
.retreat {
position: sticky;
top: 0;
height: 100vh;
height: 100svh;
padding: calc(var(--nav-h) + 0.25rem) var(--gutter) var(--gutter);
}
.retreat__inner {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
height: 100%;
overflow: hidden;
border-radius: var(--radius);
background: var(--sand);
color: var(--ink);
transform-origin: 50% 0%;
will-change: transform;
box-shadow: 0 -18px 40px -24px rgba(20, 20, 20, 0.35);
}
.retreat[data-tone="olive"] .retreat__inner { background: var(--olive); color: var(--paper); }
.retreat[data-tone="paper"] .retreat__inner { background: var(--paper-2); }
.retreat[data-tone="ink"] .retreat__inner { background: var(--ink); color: var(--paper); }
.retreat__media { position: relative; overflow: hidden; }
.retreat__media img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: scale 1.4s var(--ease-out);
}
.retreat__inner:hover .retreat__media img { scale: 1.04; }
.retreat__body {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: clamp(0.9rem, 2vh, 1.5rem);
padding: clamp(1.25rem, 4vw, 4.5rem);
}
.retreat__index {
font-size: var(--fs-label);
font-weight: 500;
letter-spacing: 0.16em;
text-transform: uppercase;
opacity: 0.8;
}
.retreat__index span { color: var(--accent); }
.retreat__title {
font-family: var(--serif);
font-weight: 400;
font-size: clamp(2.75rem, 6.2vw, 6.5rem);
line-height: 0.92;
letter-spacing: -0.02em;
}
.retreat[data-tone="olive"] .retreat__title em,
.retreat[data-tone="ink"] .retreat__title em { color: rgba(255, 255, 255, 0.72); }
.retreat__desc { max-width: 28rem; opacity: 0.88; }
.retreat__list { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.retreat__list li {
padding: 0.35rem 0.8rem;
border: 1px solid currentColor;
border-radius: 999px;
font-size: 0.75rem;
opacity: 0.85;
}
.retreat__price { font-size: var(--fs-small); font-weight: 500; }
.retreat__shade {
position: absolute;
inset: 0;
background: #0f0e0c;
opacity: 0;
pointer-events: none;
}
.retreat[data-tone="olive"] :focus-visible,
.retreat[data-tone="ink"] :focus-visible { outline-color: var(--paper); }
@media (max-width: 899px) {
.retreat__inner { grid-template-columns: 1fr; grid-template-rows: 38% 1fr; }
.retreat__body { justify-content: flex-start; }
.retreat__desc { font-size: 0.9375rem; line-height: 1.5; }
}
@media (max-width: 899px) and (max-height: 760px) {
.retreat__list { display: none; }
}
@media (max-height: 620px) {
.retreat__desc { display: none; }
}
/* --------------------------------------------------------------------------
11 · Experiences masonry + lightbox
-------------------------------------------------------------------------- */
.experiences { padding-top: calc(var(--section-y) * 1.1); }
.exp__grid {
columns: 3;
column-gap: clamp(0.75rem, 2vw, 1.75rem);
}
.exp__item {
break-inside: avoid;
margin-bottom: clamp(1.25rem, 3vw, 2.5rem);
}
.exp__btn {
display: block;
width: 100%;
padding: 0;
border: 0;
background: none;
text-align: left;
}
.exp__frame {
display: block;
aspect-ratio: var(--ar, 4 / 5);
overflow: hidden;
border-radius: 4px;
background: var(--sand);
}
.exp__frame img {
width: 100%;
height: 100%;
object-fit: cover;
transition: scale 1.1s var(--ease-out);
}
.exp__btn:hover .exp__frame img { scale: 1.06; }
.exp__cap {
display: flex;
gap: 0.7rem;
margin-top: 0.7rem;
font-size: var(--fs-small);
color: var(--muted-strong);
}
.exp__num { color: var(--accent); font-variant-numeric: tabular-nums; }
@media (max-width: 767px) { .exp__grid { columns: 2; } }
.lightbox {
position: fixed;
inset: 0;
z-index: 80;
}
.lightbox__backdrop {
position: absolute;
inset: 0;
background: rgba(20, 19, 17, 0.94);
}
.lightbox__frame { position: fixed; }
.lightbox__frame img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 6px;
}
.lightbox__caption {
position: absolute;
left: var(--gutter);
right: 9rem;
bottom: var(--gutter);
font-family: var(--serif);
font-size: clamp(1.25rem, 2.2vw, 2rem);
line-height: 1.15;
color: var(--paper);
}
.lightbox__close {
position: absolute;
top: var(--gutter);
right: var(--gutter);
z-index: 2;
display: inline-flex;
gap: 0.6rem;
align-items: center;
padding: 0.7rem 1.1rem;
border: 1px solid var(--rule-light);
border-radius: 999px;
background: rgba(20, 19, 17, 0.4);
color: var(--paper);
font-size: var(--fs-small);
font-weight: 500;
letter-spacing: 0.06em;
}
.lightbox__close:hover { background: var(--paper); color: var(--ink); }
.lightbox :focus-visible { outline-color: var(--paper); }
/* --------------------------------------------------------------------------
12 · Places marquee
-------------------------------------------------------------------------- */
.places {
padding-block: calc(var(--section-y) * 0.6);
overflow: clip;
border-top: 1px solid var(--rule);
border-bottom: 1px solid var(--rule);
}
.places .label { margin-bottom: clamp(1.5rem, 3vw, 2.5rem); }
.places__row {
font-family: var(--serif);
font-style: italic;
font-size: clamp(2.6rem, 6.4vw, 6.25rem);
line-height: 1.1;
padding-inline: var(--gutter);
}
.places__row--alt { color: var(--accent); }
.places__track { display: flex; justify-content: center; }
.places__group {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
row-gap: .1em;
margin: 0;
}
.places__group i:last-child { display: none; }
.places__group span { padding-inline: 0.28em; }
.places__group i {
display: inline-block;
width: 0.14em;
height: 0.14em;
border-radius: 50%;
background: currentColor;
opacity: 0.6;
}
.places__row--alt .places__group i { background: var(--ink); }
/* --------------------------------------------------------------------------
13 · Journal / testimonials
-------------------------------------------------------------------------- */
.journal__head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 1.5rem;
flex-wrap: wrap;
margin-bottom: clamp(3rem, 6vw, 5rem);
padding-bottom: 1.25rem;
border-bottom: 1px solid var(--rule);
}
.journal__title {
font-family: var(--serif);
font-weight: 400;
font-size: clamp(1.5rem, 2.4vw, 2.25rem);
line-height: 1.1;
}
.letters {
display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
gap: clamp(2.5rem, 7vw, 7rem); align-items: center;
}
.letters__photo { position: relative; perspective: 1000px; }
.letters__frame {
position: relative; aspect-ratio: 4 / 5; border-radius: var(--radius); overflow: hidden;
background: var(--sand); transform: rotate(-3deg);
box-shadow: 0 40px 80px -40px rgba(20, 20, 20, .55);
outline: clamp(8px, 1vw, 12px) solid #fff;
}
.letters__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0; }
.letters__img.is-active { opacity: 1; }
.letters__stamp {
position: absolute; right: -1.2rem; bottom: 2.2rem; z-index: 2;
display: flex; flex-direction: column; align-items: center; justify-content: center;
width: 7.2rem; height: 7.2rem; border-radius: 50%;
background: var(--accent); color: var(--paper); text-align: center;
transform: rotate(12deg);
box-shadow: 0 0 0 5px var(--paper), 0 0 0 6px var(--accent);
}
.letters__stamp-place { font-family: var(--serif); font-style: italic; font-size: 1.25rem; line-height: 1; padding: 0 .4rem; }
.letters__stamp-year { font-size: .62rem; letter-spacing: .2em; margin-top: .35rem; }
.letters__body { position: relative; }
.letters__mark { position: absolute; left: -.1em; top: -.5em; font-family: var(--serif); font-size: clamp(8rem, 14vw, 13rem); line-height: 1; color: var(--accent); opacity: .16; pointer-events: none; }
.letters__quote {
position: relative; font-family: var(--serif); font-size: clamp(2rem, 3.8vw, 3.9rem);
line-height: 1.08; letter-spacing: -.012em; min-height: 3.3em;
}
.letters__quote .lw { display: inline-block; }
.letters__who { display: flex; flex-wrap: wrap; gap: .4rem 1.25rem; margin-top: clamp(1.5rem, 3vw, 2.25rem); font-size: var(--fs-small); }
.letters__name { font-weight: 500; }
.letters__name::before { content: ""; display: inline-block; width: 1.75rem; height: 1px; margin-right: .8rem; vertical-align: middle; background: var(--accent); }
.letters__trip { color: var(--muted-strong); }
.letters__tabs { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1rem; margin-top: clamp(2.5rem, 5vw, 4rem); }
.ltab {
display: grid; grid-template-columns: auto 1fr; gap: .15rem .6rem; align-items: baseline;
padding: 0; border: 0; background: none; text-align: left; color: var(--muted); cursor: pointer;
transition: color .4s;
}
.ltab:hover, .ltab.is-active { color: var(--ink); }
.ltab__num { font-family: var(--serif); font-size: 1.15rem; }
.ltab__name { font-size: var(--fs-small); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ltab__bar { grid-column: 1 / -1; height: 1px; margin-top: .7rem; background: var(--rule); overflow: hidden; }
.ltab__bar i { display: block; height: 100%; background: var(--accent); transform: scaleX(0); transform-origin: 0 50%; }
.ltab.is-active .ltab__bar i { transform: scaleX(1); }
.is-anim .ltab.is-active .ltab__bar i { transform: scaleX(0); }
.ltab:focus-visible { outline: 2px solid var(--accent); outline-offset: 6px; }
@media (max-width: 860px) {
.letters { grid-template-columns: 1fr; }
.letters__photo { max-width: 22rem; margin-inline: auto; width: 82%; }
.letters__tabs { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 1.4rem; }
}
/* --------------------------------------------------------------------------
14 · Booking CTA
-------------------------------------------------------------------------- */
.booking {
padding-block: var(--section-y);
background: var(--olive);
color: var(--paper);
}
.booking .label { color: rgba(255, 255, 255, 0.8); }
.booking .label__num { color: rgba(255, 255, 255, 0.72); }
.booking__head {
display: flex; justify-content: space-between; align-items: flex-end; gap: 2rem 4rem; flex-wrap: wrap;
margin-bottom: clamp(2.5rem, 5vw, 4rem);
}
.booking__title {
margin-top: 1.25rem;
font-family: var(--serif); font-weight: 400;
font-size: clamp(3.2rem, 8vw, 8rem); line-height: 0.9; letter-spacing: -0.025em;
}
.booking__title em { color: rgba(255, 255, 255, 0.72); }
.booking__text { max-width: 26rem; color: rgba(255, 255, 255, 0.86); }
.booking__small { margin-top: 1.5rem; font-size: var(--fs-small); color: rgba(255, 255, 255, 0.7); text-align: center; }
/* the card: live preview + form */
.booking__card {
display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
border-radius: calc(var(--radius) * 1.4); overflow: hidden;
background: var(--paper); color: var(--ink);
box-shadow: 0 60px 120px -50px rgba(0, 0, 0, .55);
}
.bv { position: relative; min-height: 560px; overflow: hidden; color: var(--paper); perspective: 1000px; }
.bv__media { position: absolute; inset: 0; }
.bv__img { position: absolute; inset: -4%; width: 108%; height: 108%; max-width: none; object-fit: cover; opacity: 0; }
.bv__img.is-front { opacity: 1; }
.bv__shade { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(20, 20, 20,.35) 0%, rgba(20, 20, 20,0) 30%, rgba(20, 20, 20,.1) 50%, rgba(20, 20, 20,.72) 100%); }
.bv__top { position: absolute; left: clamp(1.25rem, 2.5vw, 2rem); right: clamp(1.25rem, 2.5vw, 2rem); top: clamp(1.25rem, 2.5vw, 2rem); display: flex; justify-content: space-between; gap: 1rem; }
.bv__chip {
display: inline-block; padding: .45rem .85rem; border-radius: 999px;
background: rgba(20, 20, 20, .42); border: 1px solid rgba(255, 255, 255, .35);
font-size: var(--fs-label); letter-spacing: .14em; text-transform: uppercase; font-weight: 500;
}
.bv__meta { position: absolute; left: clamp(1.25rem, 2.5vw, 2rem); right: clamp(1.25rem, 2.5vw, 2rem); bottom: clamp(1.25rem, 2.5vw, 2rem); }
.bv__region { font-size: var(--fs-label); letter-spacing: .16em; text-transform: uppercase; color: rgba(255, 255, 255, .82); }
.bv__name { margin-top: .5rem; font-family: var(--serif); font-size: clamp(2.6rem, 5vw, 4.6rem); line-height: .92; letter-spacing: -.02em; }
.bv__name em { color: rgba(255, 255, 255, 0.72); }
.bv__name .bv-ch { display: inline-block; }
.bv__facts { display: flex; flex-wrap: wrap; gap: .5rem 1.4rem; margin-top: 1.1rem; padding-top: 1rem; border-top: 1px solid rgba(255, 255, 255, .3); font-size: var(--fs-small); color: rgba(255, 255, 255, .85); }
.bv__facts b { font-family: var(--serif); font-weight: 400; font-size: 1.5rem; color: var(--paper); font-variant-numeric: tabular-nums; margin-right: .15rem; }
.bv__est { margin-left: auto; }
.bv__stamp {
position: absolute; right: 12%; top: 22%;
width: 9rem; height: 9rem; border-radius: 50%;
display: grid; place-content: center; text-align: center; line-height: 1.05;
border: 2px solid rgba(255, 255, 255, 0.72); color: rgba(255, 255, 255, 0.72);
box-shadow: inset 0 0 0 6px rgba(255, 255, 255, .0), inset 0 0 0 8px rgba(255, 255, 255, 0.72);
transform: rotate(-14deg); opacity: 0;
font-size: .7rem; letter-spacing: .22em; text-transform: uppercase;
}
.bv__stamp b { font-family: var(--serif); font-style: italic; font-weight: 400; font-size: 2.4rem; letter-spacing: 0; text-transform: none; }
.booking__panel { position: relative; padding: clamp(1.75rem, 4vw, 3.5rem); display: flex; flex-direction: column; justify-content: center; }
.booking :focus-visible { outline-color: var(--paper); }
.booking__panel :focus-visible { outline-color: var(--accent); }
.booking__form { display: grid; gap: 1.6rem; }
.field { display: grid; gap: 0.35rem; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1.6rem; }
.field label { font-size: var(--fs-label); font-weight: 500; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted-strong); }
.field select,
.field input[type="email"] {
width: 100%; padding: 0.75rem 0; border: 0; border-bottom: 1px solid var(--rule); border-radius: 0;
background: transparent; color: var(--ink); font-size: 1.0625rem; transition: border-color 0.3s;
}
.field select {
appearance: none; -webkit-appearance: none; padding-right: 1.75rem; cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5l5 5 5-5' fill='none' stroke='%231C1A17' stroke-width='1.2'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-position: right 0.25rem center;
}
.field select option, .field select optgroup { color: var(--ink); background: var(--paper); }
.field input::placeholder { color: var(--muted); }
.field select:hover, .field input:hover,
.field select:focus-visible, .field input:focus-visible { border-bottom-color: var(--ink); }
.field .is-invalid { border-bottom-color: var(--accent) !important; }
.stepper { display: flex; align-items: center; gap: 0.75rem; padding: 0.4rem 0; border-bottom: 1px solid var(--rule); }
.stepper__btn {
display: grid; place-items: center; width: 2.1rem; height: 2.1rem; flex: none;
border: 1px solid var(--rule); border-radius: 50%; background: transparent; color: var(--ink);
font-size: 1rem; line-height: 1; transition: background-color 0.3s, color 0.3s, border-color .3s;
}
.stepper__btn:hover { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.stepper input {
width: 100%; min-width: 0; padding: 0; border: 0; background: transparent; color: var(--ink);
text-align: center; font-family: var(--serif); font-size: 1.5rem; -moz-appearance: textfield; appearance: textfield;
}
.stepper input::-webkit-outer-spin-button,
.stepper input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.party { display: flex; align-items: center; gap: .4rem; margin-top: -.4rem; }
.party i {
width: .72rem; height: 1.1rem; border-radius: .36rem .36rem .15rem .15rem; background: var(--rule);
position: relative; transition: background-color .35s;
}
.party i::before { content: ""; position: absolute; left: 50%; top: -.55rem; width: .5rem; height: .5rem; margin-left: -.25rem; border-radius: 50%; background: inherit; }
.party i.is-on { background: var(--accent); }
.party span { margin-left: .75rem; font-size: var(--fs-small); color: var(--muted-strong); }
.form-error { min-height: 1.4em; margin-top: -0.6rem; font-size: var(--fs-small); color: var(--accent); }
.booking__submit { display: flex; align-items: center; gap: 1.25rem; flex-wrap: wrap; }
.booking__note { font-size: var(--fs-small); color: var(--muted-strong); }
.btn-send {
position: relative; display: inline-flex; align-items: center; gap: .9rem;
padding: .55rem .55rem .55rem 1.6rem; border: 0; border-radius: 999px; overflow: hidden; isolation: isolate;
background: var(--ink); color: var(--paper); font-size: .95rem; font-weight: 500; letter-spacing: .02em;
}
.btn-send::before {
content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit; background: var(--accent);
transform: scaleX(0); transform-origin: 0 50%; transition: transform .6s var(--ease-out);
}
.btn-send:hover::before { transform: scaleX(1); }
.btn-send__icon {
display: grid; place-items: center; width: 2.6rem; height: 2.6rem; border-radius: 50%;
background: var(--paper); color: var(--ink); font-size: 1.1rem; transition: transform .5s var(--ease-out);
}
.btn-send:hover .btn-send__icon { transform: rotate(-45deg); }
.booking__success { display: grid; gap: 1.25rem; justify-items: start; }
.booking__success .label { color: var(--muted-strong); }
.booking__success-title { font-family: var(--serif); font-size: clamp(2.5rem, 4.5vw, 4rem); line-height: 0.95; }
.booking__success-title em { color: var(--accent); }
.booking__success-text { max-width: 28rem; color: var(--muted-strong); }
.booking__success-dest { font-weight: 500; color: var(--ink); }
@media (max-width: 899px) {
.booking__card { grid-template-columns: 1fr; }
.bv { min-height: 0; aspect-ratio: 4 / 3.4; }
.bv__stamp { width: 7rem; height: 7rem; right: 8%; top: 18%; }
}
@media (max-width: 480px) {
.field-row { grid-template-columns: 1fr; }
.bv { aspect-ratio: 4 / 4.6; }
.bv__est { margin-left: 0; }
}
/* --------------------------------------------------------------------------
15 · Footer
-------------------------------------------------------------------------- */
.footer {
padding-top: var(--section-y);
background: var(--sand);
color: var(--ink);
}
.footer__top {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
gap: clamp(3rem, 6vw, 6rem);
}
.footer__heading {
margin-top: 1rem;
font-family: var(--serif);
font-weight: 400;
font-size: clamp(2.25rem, 4vw, 3.75rem);
line-height: 1;
}
.news {
display: flex;
max-width: 30rem;
margin-top: 2rem;
border-bottom: 1px solid var(--ink);
}
.news input {
flex: 1;
min-width: 0;
padding: 0.85rem 0;
border: 0;
background: transparent;
}
.news input::placeholder { color: var(--muted-strong); }
.news button {
padding: 0.85rem 0 0.85rem 1rem;
border: 0;
background: none;
font-size: var(--fs-label);
font-weight: 500;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.news button:hover { color: var(--accent); }
.news__msg { min-height: 1.5em; margin-top: 0.75rem; font-size: var(--fs-small); color: var(--muted-strong); }
.footer__cols {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1.5rem;
}
.footer__col-title {
margin-bottom: 1rem;
font-size: var(--fs-label);
font-weight: 500;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted-strong);
}
.footer__cols li { font-size: 0.9375rem; line-height: 2; }
.footer__cols a { background: linear-gradient(currentColor, currentColor) 0 100% / 0 1px no-repeat; transition: background-size 0.45s var(--ease-out); }
.footer__cols a:hover { background-size: 100% 1px; }
.footer__mark {
margin-top: clamp(3rem, 8vw, 7rem);
overflow: hidden;
padding-top: 0.04em;
font-family: var(--serif);
font-size: clamp(7rem, 31vw, 36rem);
line-height: 0.8;
letter-spacing: -0.045em;
text-align: center;
white-space: nowrap;
user-select: none;
}
.footer__mark-text { display: inline-block; padding-bottom: 0.06em; }
.footer__mark .mark-char {
background: linear-gradient(180deg, var(--ink) 20%, rgba(28,28,28,.5) 60%, rgba(28,28,28,.1) 100%);
-webkit-background-clip: text; background-clip: text; color: transparent;
}
.footer__bottom {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.75rem 2rem;
padding-block: 1.5rem;
border-top: 1px solid var(--rule);
font-size: var(--fs-small);
color: var(--muted-strong);
}
.footer__bottom a { color: var(--ink); text-decoration: underline; text-underline-offset: 0.2em; }
.footer__bottom .footer__top-link { text-decoration: none; }
@media (max-width: 899px) {
.footer__top { grid-template-columns: 1fr; }
}
@media (max-width: 480px) {
.footer__cols { grid-template-columns: 1fr 1fr; }
}
/* --------------------------------------------------------------------------
16 · Reduced motion
-------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
.grain { animation: none; }
.places__row { white-space: normal; }
.places__track { width: auto; flex-wrap: wrap; }
.places__group[aria-hidden="true"] { display: none; }
.places__group { flex-wrap: wrap; }
}
/* ---------- brand mark (sun over the horizon) ---------- */
.brand-mark { width: .82em; height: .82em; flex: none; transform: translateY(-.04em); }
/* ---------- Guest reviews: one slow postcard marquee ---------- */
.reviews { margin-top: clamp(4.5rem, 9vw, 8rem); }
.reviews__head { display: flex; justify-content: space-between; align-items: flex-end; gap: 1.5rem; flex-wrap: wrap;
padding-bottom: 1.25rem; margin-bottom: clamp(1.75rem, 3vw, 2.5rem); border-bottom: 1px solid var(--rule); }
.reviews__score { display: flex; align-items: baseline; gap: .75rem; flex-wrap: wrap; }
.reviews__num { font-family: var(--serif); font-size: clamp(2.6rem, 5vw, 4.2rem); line-height: .9; letter-spacing: -.02em; }
.reviews__stars, .rcard__stars { display: inline-flex; gap: 2px; color: var(--accent); }
.reviews__stars svg { width: 15px; height: 15px; fill: currentColor; }
.reviews__meta { font-size: var(--fs-label, .75rem); letter-spacing: .14em; text-transform: uppercase; opacity: .7; }
.reviews__note { max-width: 32ch; font-size: .95rem; opacity: .75; }
.reviews__row { overflow: hidden;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent); }
.reviews__track { display: flex; gap: 1.25rem; width: max-content; padding: .5rem 0 1rem; }
.rcard { flex: none; width: clamp(270px, 26vw, 330px); margin: 0; padding: 1.4rem; display: flex; flex-direction: column; gap: 1rem;
background: #FFFFFF; border: 1px solid var(--rule); border-radius: var(--radius, 14px);
box-shadow: 0 1px 0 rgba(20, 20, 20,.04), 0 18px 40px -28px rgba(20, 20, 20,.35);
transition: transform .5s var(--ease-out, cubic-bezier(.22,1,.36,1)); }
.rcard:nth-child(odd) { transform: rotate(-1.2deg); }
.rcard:nth-child(even) { transform: rotate(1deg); }
.rcard:hover { transform: rotate(0) translateY(-6px); }
.rcard__top { display: flex; align-items: center; gap: .75rem; }
.rcard__top img { width: 44px; height: 44px; border-radius: 10px; object-fit: cover; }
.rcard__place { font-family: var(--serif); font-size: 1.25rem; line-height: 1; flex: 1; }
.rcard__stars svg { width: 12px; height: 12px; fill: currentColor; }
.rcard blockquote { margin: 0; font-family: var(--serif); font-size: 1.3rem; line-height: 1.25; letter-spacing: -.005em; }
.rcard figcaption { margin-top: auto; font-size: .8rem; letter-spacing: .12em; text-transform: uppercase; opacity: .6; }
@media (prefers-reduced-motion: no-preference) {
.reviews.is-ready .reviews__track { animation: reviews-scroll 75s linear infinite; }
.reviews__row:hover .reviews__track, .reviews.is-off .reviews__track { animation-play-state: paused; }
}
@media (prefers-reduced-motion: reduce) { .reviews__row { overflow-x: auto; } }
@keyframes reviews-scroll { to { transform: translateX(-50%); } }
/* nav sits on the hero photo: no difference-blend there (it garbled colours),
plain white text; below the hero the blend returns and reads black on white */
.nav.is-on-hero { mix-blend-mode: normal; color: #fff; }
.nav.is-on-hero a { text-shadow: 0 1px 12px rgba(0,0,0,.25); }
/* journeys: wave thread the cards ride (path drawn from JS) */
.journeys__wave { position: absolute; left: 0; top: 0; z-index: 0; pointer-events: none; overflow: visible; }
.journeys__wave path { fill: none; stroke: var(--ink); stroke-width: 1.2; opacity: .22; stroke-linecap: round; stroke-linejoin: round; }
.journeys__wave path.is-faint { opacity: .1; stroke-dasharray: 2 6; }
.jcard { position: relative; z-index: 1; will-change: transform; }
/* Fraunces: rounded, friendly letterforms (SOFT axis); DM Sans ignores it */
body { font-variation-settings: "SOFT" 70; }
```