:root {
--page-bg: #faf8f4;
--page-surface: #ffffff;
--page-border: #e8e4dc;
--page-text: #1a1a1a;
--page-muted: #8a8478;
--page-accent: #c4682b;
--page-accent-dark: #8b4513;
--page-sea: #2e7d9c;
--page-sky: #87ceeb;
--page-sand: #d4b896;
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--page-bg);
color: var(--page-text);
font-family: Georgia, 'Times New Roman', 'Playfair Display', serif;
line-height: 1.8;
overflow-x: hidden;
}
/* ── Demo shell overrides for light theme ── */
.demo-shell-back {
background: rgba(255, 255, 255, 0.85) !important;
border-color: rgba(0, 0, 0, 0.12) !important;
color: var(--page-accent) !important;
}
.demo-shell-info {
background: rgba(250, 248, 244, 0.95) !important;
border-top-color: rgba(0, 0, 0, 0.08) !important;
color: var(--page-muted) !important;
}
.demo-shell-info .demo-title { color: var(--page-text) !important; }
.demo-shell-info .demo-tag {
background: rgba(196, 104, 43, 0.1) !important;
border-color: rgba(196, 104, 43, 0.2) !important;
color: var(--page-accent) !important;
}
.motion-toggle {
background: rgba(255, 255, 255, 0.85) !important;
border-color: rgba(0, 0, 0, 0.12) !important;
color: var(--page-accent) !important;
}
/* ── Sections ── */
.section {
position: relative;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 6rem 2rem;
}
/* ── Hero ── */
.hero-section {
position: relative;
overflow: hidden;
text-align: center;
}
.hero-bg {
position: absolute;
inset: 0;
background: linear-gradient(180deg,
var(--page-sky) 0%,
#5ba8c8 30%,
var(--page-sea) 50%,
var(--page-bg) 85%,
var(--page-bg) 100%
);
z-index: 0;
}
.hero-content {
position: relative;
z-index: 1;
}
.hero-overline {
display: block;
font-family: 'Inter', system-ui, sans-serif;
font-size: 0.7rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 1.5rem;
font-weight: 500;
}
.hero-title {
font-size: clamp(3rem, 10vw, 7rem);
font-weight: 700;
color: #ffffff;
line-height: 1;
letter-spacing: -0.02em;
text-shadow: 0 4px 40px rgba(0, 0, 0, 0.15);
margin-bottom: 1rem;
}
.hero-rule {
width: 60px;
height: 2px;
border: none;
background: var(--page-accent);
margin: 1.5rem auto;
opacity: 0;
}
.hero-subtitle {
font-size: clamp(1rem, 3vw, 1.5rem);
color: rgba(255, 255, 255, 0.8);
font-style: italic;
font-weight: 400;
opacity: 0;
}
.scroll-arrow {
margin-top: 4rem;
color: rgba(255, 255, 255, 0.5);
opacity: 0;
animation: scroll-float 2.5s ease-in-out infinite;
}
@keyframes scroll-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(8px); }
}
.reduced-motion .scroll-arrow { animation: none; }
/* ── Prose Section ── */
.prose-section {
min-height: auto;
padding: 8rem 2rem;
}
.prose-column {
max-width: 680px;
width: 100%;
}
.prose-text {
font-size: clamp(1rem, 2vw, 1.15rem);
color: var(--page-muted);
margin-bottom: 2rem;
transition: color 0.3s ease;
}
.prose-text.revealed {
color: var(--page-text);
}
.reduced-motion .prose-text {
color: var(--page-text);
}
/* Drop cap */
.dropcap::first-letter {
float: left;
font-size: 3.5em;
line-height: 0.8;
padding-right: 0.1em;
color: var(--page-accent);
font-weight: 700;
}
/* Pull quote */
.pull-quote {
border-left: 3px solid var(--page-accent);
padding: 1rem 0 1rem 2rem;
margin: 3rem 0;
}
.quote-text {
font-size: clamp(1.1rem, 2.5vw, 1.35rem);
font-style: italic;
color: var(--page-accent-dark);
line-height: 1.6;
}
/* ── Horizontal Gallery ── */
.gallery-section {
position: relative;
height: 400vh;
}
.gallery-track {
position: sticky;
top: 0;
height: 100vh;
display: flex;
width: 400vw;
overflow: hidden;
}
.gallery-panel {
width: 100vw;
height: 100vh;
position: relative;
flex-shrink: 0;
overflow: hidden;
}
.panel-landscape {
position: absolute;
inset: 0;
}
/* CSS-only landscape compositions */
.panel-cliffs .panel-landscape {
background:
linear-gradient(180deg, var(--page-sky) 0%, #6bb3d4 40%, transparent 50%),
linear-gradient(180deg, transparent 40%, var(--page-sea) 55%, #1a5f7a 100%);
}
.panel-cliffs .panel-landscape::before {
content: '';
position: absolute;
bottom: 0;
left: 10%;
width: 35%;
height: 75%;
background: linear-gradient(160deg, #d4a574 0%, #a67c52 40%, #8b6342 100%);
clip-path: polygon(20% 100%, 0% 40%, 15% 15%, 40% 0%, 65% 10%, 80% 25%, 100% 50%, 90% 100%);
}
.panel-cliffs .panel-landscape::after {
content: '';
position: absolute;
bottom: 0;
right: 5%;
width: 25%;
height: 60%;
background: linear-gradient(170deg, #c49a6c 0%, #9e7a54 100%);
clip-path: polygon(10% 100%, 0% 50%, 30% 10%, 60% 0%, 90% 20%, 100% 60%, 95% 100%);
}
.panel-sea .panel-landscape {
background: linear-gradient(180deg, #6bb3d4 0%, var(--page-sea) 30%, #1a6b8a 60%, #144d65 100%);
}
.panel-sea .panel-landscape::before {
content: '';
position: absolute;
bottom: 30%;
left: 0;
right: 0;
height: 20%;
background: rgba(255, 255, 255, 0.08);
border-radius: 50% 50% 0 0 / 100% 100% 0 0;
transform: scaleX(1.5);
}
.panel-sea .panel-landscape::after {
content: '';
position: absolute;
bottom: 20%;
left: -10%;
right: -10%;
height: 15%;
background: rgba(255, 255, 255, 0.05);
border-radius: 50% 50% 0 0 / 100% 100% 0 0;
}
.panel-sunset .panel-landscape {
background: linear-gradient(180deg,
#2c1654 0%,
#8b3a62 20%,
#d4614a 40%,
#f0a040 55%,
#ffd080 65%,
var(--page-sea) 80%,
#1a5f7a 100%
);
}
.panel-sunset .panel-landscape::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80px;
height: 80px;
border-radius: 50%;
background: radial-gradient(circle, #fff8e0 0%, #ffd060 40%, #f0a040 70%, transparent 100%);
filter: blur(8px);
}
.panel-village .panel-landscape {
background: linear-gradient(180deg,
#2a1a3e 0%,
#4a2a5e 25%,
#8b5a3e 50%,
#c49a6c 70%,
var(--page-sand) 85%,
var(--page-bg) 100%
);
}
.panel-village .panel-landscape::before {
content: '';
position: absolute;
bottom: 25%;
left: 15%;
width: 70%;
height: 35%;
background: #1a1a2e;
clip-path: polygon(
0% 100%, 2% 60%, 5% 40%, 8% 60%, 12% 30%, 15% 50%, 18% 20%, 22% 45%,
25% 15%, 28% 35%, 32% 10%, 35% 25%, 38% 40%, 42% 20%, 45% 35%, 48% 5%,
52% 30%, 55% 15%, 58% 40%, 62% 25%, 65% 45%, 68% 20%, 72% 50%, 75% 30%,
78% 55%, 82% 35%, 85% 50%, 88% 40%, 92% 60%, 95% 45%, 100% 70%, 100% 100%
);
opacity: 0.7;
}
/* Tiny windows */
.panel-village .panel-landscape::after {
content: '';
position: absolute;
bottom: 32%;
left: 25%;
width: 50%;
height: 15%;
background:
radial-gradient(circle 2px at 10% 30%, #ffd060 0%, transparent 100%),
radial-gradient(circle 2px at 25% 50%, #ffd060 0%, transparent 100%),
radial-gradient(circle 2px at 40% 20%, #ffd060 0%, transparent 100%),
radial-gradient(circle 2px at 55% 60%, #ffd060 0%, transparent 100%),
radial-gradient(circle 2px at 70% 35%, #ffd060 0%, transparent 100%),
radial-gradient(circle 2px at 85% 45%, #ffd060 0%, transparent 100%);
}
.panel-caption {
position: absolute;
bottom: 4rem;
left: 4rem;
z-index: 2;
display: flex;
align-items: baseline;
gap: 1rem;
opacity: 0;
transform: translateY(20px);
}
.reduced-motion .panel-caption {
opacity: 1;
transform: none;
}
.caption-number {
font-family: 'Inter', system-ui, sans-serif;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.1em;
color: rgba(255, 255, 255, 0.5);
}
.caption-text {
font-size: clamp(1.2rem, 3vw, 1.8rem);
color: #ffffff;
font-weight: 600;
text-shadow: 0 2px 16px rgba(0, 0, 0, 0.3);
}
/* Gallery progress bar */
.gallery-progress {
position: fixed;
bottom: 50px;
left: 50%;
transform: translateX(-50%);
width: 200px;
height: 2px;
background: rgba(196, 104, 43, 0.2);
border-radius: 1px;
z-index: 10;
opacity: 0;
transition: opacity 0.3s ease;
}
.gallery-progress.visible {
opacity: 1;
}
.gallery-progress-fill {
height: 100%;
width: 0%;
background: var(--page-accent);
border-radius: 1px;
transition: width 0.1s linear;
}
/* ── Stories Section (FLIP) ── */
.stories-section {
min-height: auto;
padding: 8rem 2rem;
}
.stories-heading {
font-size: clamp(1.8rem, 5vw, 3rem);
font-weight: 700;
text-align: center;
margin-bottom: 3rem;
color: var(--page-text);
}
.stories-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
max-width: 1000px;
width: 100%;
margin: 0 auto;
}
.story-card {
background: var(--page-surface);
border: 1px solid var(--page-border);
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: box-shadow 0.3s ease;
}
.story-card:hover {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
}
.story-card.expanded {
grid-column: 1 / -1;
cursor: default;
}
.story-header {
height: 120px;
}
.header-path {
background: linear-gradient(135deg, #c49a6c 0%, #8b6342 60%, #5a3e2b 100%);
}
.header-lemon {
background: linear-gradient(135deg, #ffd060 0%, #f0a040 50%, #c4682b 100%);
}
.header-fisher {
background: linear-gradient(135deg, var(--page-sea) 0%, #1a5f7a 50%, #0e3d4f 100%);
}
.story-title {
font-size: 1.2rem;
font-weight: 700;
padding: 1.25rem 1.5rem 0.5rem;
color: var(--page-text);
}
.story-preview {
font-size: 0.9rem;
color: var(--page-muted);
padding: 0 1.5rem 1.5rem;
line-height: 1.6;
}
.story-full {
display: none;
padding: 0 1.5rem;
}
.story-card.expanded .story-full {
display: block;
}
.story-full p {
font-size: 0.95rem;
color: var(--page-muted);
margin-bottom: 1.25rem;
line-height: 1.7;
}
.story-toggle {
display: block;
width: 100%;
padding: 1rem 1.5rem;
background: none;
border: none;
border-top: 1px solid var(--page-border);
color: var(--page-accent);
font: 600 0.8rem/1 'Inter', system-ui, sans-serif;
cursor: pointer;
text-align: left;
transition: background 0.2s ease;
}
.story-toggle:hover {
background: rgba(196, 104, 43, 0.05);
}
.story-card.expanded .story-toggle::after {
content: ' (click to close)';
color: var(--page-muted);
font-weight: 400;
}
/* ── Closing ── */
.closing-section {
padding: 10rem 2rem;
text-align: center;
}
.closing-quote {
font-size: clamp(1.5rem, 5vw, 3rem);
font-weight: 700;
font-style: italic;
color: var(--page-text);
max-width: 700px;
margin-bottom: 3rem;
line-height: 1.3;
}
.closing-meta {
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
font-family: 'Inter', system-ui, sans-serif;
font-size: 0.75rem;
color: var(--page-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
flex-wrap: wrap;
}
.meta-divider {
opacity: 0.3;
}
/* ── Responsive ── */
@media (max-width: 768px) {
.stories-grid {
grid-template-columns: 1fr;
}
.section {
padding: 4rem 1.25rem;
}
.panel-caption {
left: 2rem;
bottom: 3rem;
}
.prose-column {
padding: 0;
}
}
if (!window.MotionPreference) {
const __mql = window.matchMedia("(prefers-reduced-motion: reduce)");
const __listeners = new Set();
const MotionPreference = {
prefersReducedMotion() {
return __mql.matches;
},
setOverride(value) {
const reduced = Boolean(value);
document.documentElement.classList.toggle("reduced-motion", reduced);
window.dispatchEvent(new CustomEvent("motion-preference", { detail: { reduced } }));
for (const listener of __listeners) {
try {
listener({ reduced, override: reduced, systemReduced: __mql.matches });
} catch {}
}
},
onChange(listener) {
__listeners.add(listener);
try {
listener({
reduced: __mql.matches,
override: null,
systemReduced: __mql.matches,
});
} catch {}
return () => __listeners.delete(listener);
},
getState() {
return { reduced: __mql.matches, override: null, systemReduced: __mql.matches };
},
};
window.MotionPreference = MotionPreference;
}
function prefersReducedMotion() {
return window.MotionPreference.prefersReducedMotion();
}
function initDemoShell() {
// No-op shim in imported standalone snippets.
}
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { SplitText } from 'gsap/SplitText';
import { Flip } from 'gsap/Flip';
import Lenis from 'lenis';
gsap.registerPlugin(ScrollTrigger, SplitText, Flip);
// ── Demo Shell ──
initDemoShell({
title: 'Travel Editorial',
category: 'pages',
tech: ['gsap', 'flip', 'lenis', 'view-transitions-api'],
});
// ── Lenis ──
const lenis = new Lenis({ lerp: 0.1, smoothWheel: true });
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
let reduced = prefersReducedMotion();
if (reduced) document.documentElement.classList.add('reduced-motion');
window.addEventListener('motion-preference', (e) => {
reduced = e.detail.reduced;
document.documentElement.classList.toggle('reduced-motion', reduced);
ScrollTrigger.refresh();
});
const dur = (d) => reduced ? 0 : d;
// ═══════════════════════════════════════════════════════════════════════
// HERO ENTRANCE
// ═══════════════════════════════════════════════════════════════════════
const heroTitle = document.querySelector('.hero-title');
const heroOverline = document.querySelector('.hero-overline');
const heroRule = document.querySelector('.hero-rule');
const heroSubtitle = document.querySelector('.hero-subtitle');
const scrollArrow = document.querySelector('.scroll-arrow');
// SplitText for hero title
const titleSplit = new SplitText(heroTitle, { type: 'lines', linesClass: 'line' });
gsap.set(titleSplit.lines, {
opacity: 0,
y: reduced ? 0 : 80,
});
gsap.set(heroOverline, { opacity: 0, y: reduced ? 0 : 20 });
const heroTl = gsap.timeline({ delay: 0.3 });
heroTl
.to(heroOverline, {
opacity: 1, y: 0,
duration: dur(0.6),
ease: 'expo.out',
})
.to(titleSplit.lines, {
opacity: 1, y: 0,
duration: dur(0.8),
ease: 'expo.out',
stagger: { each: 0.15 },
}, 0.2)
.to(heroRule, {
opacity: 1,
width: 60,
duration: dur(0.6),
ease: 'expo.out',
}, 0.8)
.to(heroSubtitle, {
opacity: 1,
duration: dur(0.6),
ease: 'expo.out',
}, 1.0)
.to(scrollArrow, {
opacity: 1,
duration: dur(0.5),
ease: 'expo.out',
}, 1.3);
// ═══════════════════════════════════════════════════════════════════════
// PROSE SECTION: Scroll-scrubbed reading progress
// ═══════════════════════════════════════════════════════════════════════
const proseTexts = document.querySelectorAll('.prose-text');
proseTexts.forEach((el) => {
const split = new SplitText(el, { type: 'lines', linesClass: 'prose-line' });
if (reduced) {
// All lines fully visible immediately
split.lines.forEach(line => line.style.color = 'var(--page-text)');
return;
}
// Each line transitions from muted to full color via scrub
split.lines.forEach((line, i) => {
gsap.fromTo(line, {
color: 'rgba(138, 132, 120, 1)', // --page-muted
}, {
color: 'rgba(26, 26, 26, 1)', // --page-text
scrollTrigger: {
trigger: line,
start: 'top 85%',
end: 'top 50%',
scrub: 1,
},
});
});
});
// Pull quote
const quoteText = document.querySelector('.quote-text');
if (quoteText) {
const quoteSplit = new SplitText(quoteText, { type: 'words', wordsClass: 'word' });
gsap.set(quoteSplit.words, {
opacity: 0,
y: reduced ? 0 : 15,
});
gsap.to(quoteSplit.words, {
opacity: 1, y: 0,
duration: dur(0.5),
ease: 'expo.out',
stagger: { each: 0.04 },
scrollTrigger: {
trigger: quoteText,
start: 'top 80%',
toggleActions: 'play none none reverse',
},
});
}
// ═══════════════════════════════════════════════════════════════════════
// HORIZONTAL PHOTO GALLERY
// ═══════════════════════════════════════════════════════════════════════
const gallerySection = document.querySelector('.gallery-section');
const galleryTrack = document.querySelector('.gallery-track');
const panels = document.querySelectorAll('.gallery-panel');
const galleryFill = document.getElementById('gallery-fill');
const galleryProgress = document.querySelector('.gallery-progress');
if (galleryTrack && panels.length > 0) {
const totalWidth = panels.length * window.innerWidth;
// Horizontal scroll via pin
const galleryTween = gsap.to(galleryTrack, {
x: () => -(totalWidth - window.innerWidth),
ease: 'none',
scrollTrigger: {
trigger: gallerySection,
start: 'top top',
end: () => `+=${totalWidth}`,
scrub: 1.5,
pin: true,
anticipatePin: 1,
onUpdate: (self) => {
// Update progress bar
if (galleryFill) {
galleryFill.style.width = `${self.progress * 100}%`;
}
},
onEnter: () => galleryProgress?.classList.add('visible'),
onLeave: () => galleryProgress?.classList.remove('visible'),
onEnterBack: () => galleryProgress?.classList.add('visible'),
onLeaveBack: () => galleryProgress?.classList.remove('visible'),
},
});
// Panel captions fade in
panels.forEach((panel, i) => {
const caption = panel.querySelector('.panel-caption');
if (!caption || reduced) return;
gsap.to(caption, {
opacity: 1,
y: 0,
duration: 0.5,
ease: 'expo.out',
scrollTrigger: {
trigger: panel,
containerAnimation: galleryTween,
start: 'left 60%',
toggleActions: 'play none none reverse',
},
});
});
}
// ═══════════════════════════════════════════════════════════════════════
// FLIP STORIES SECTION
// ═══════════════════════════════════════════════════════════════════════
const storiesGrid = document.getElementById('stories-grid');
const storyCards = document.querySelectorAll('.story-card');
// Stories section entrance
const storiesHeading = document.querySelector('.stories-heading');
if (storiesHeading) {
const storiesSplit = new SplitText(storiesHeading, { type: 'words', wordsClass: 'word' });
gsap.set(storiesSplit.words, {
opacity: 0,
y: reduced ? 0 : 20,
});
gsap.to(storiesSplit.words, {
opacity: 1, y: 0,
duration: dur(0.5),
ease: 'expo.out',
stagger: { each: 0.06 },
scrollTrigger: {
trigger: '.stories-section',
start: 'top 75%',
toggleActions: 'play none none reverse',
},
});
}
// Card entrance animation
storyCards.forEach((card, i) => {
gsap.set(card, { opacity: 0, y: reduced ? 0 : 40 });
gsap.to(card, {
opacity: 1, y: 0,
duration: dur(0.6),
ease: 'expo.out',
delay: i * 0.1,
scrollTrigger: {
trigger: '.stories-section',
start: 'top 70%',
toggleActions: 'play none none reverse',
},
});
});
// FLIP toggle
storyCards.forEach((card) => {
const toggleBtn = card.querySelector('.story-toggle');
function toggleCard() {
const isExpanded = card.classList.contains('expanded');
if (reduced) {
// No animation, just toggle
if (isExpanded) {
card.classList.remove('expanded');
toggleBtn.textContent = 'Read more';
} else {
// Collapse any other expanded cards
storyCards.forEach(c => {
c.classList.remove('expanded');
c.querySelector('.story-toggle').textContent = 'Read more';
});
card.classList.add('expanded');
toggleBtn.textContent = 'Close';
}
return;
}
// Get current state for FLIP
const flipState = Flip.getState(storyCards);
if (isExpanded) {
card.classList.remove('expanded');
toggleBtn.textContent = 'Read more';
} else {
// Collapse any other expanded cards first
storyCards.forEach(c => {
c.classList.remove('expanded');
c.querySelector('.story-toggle').textContent = 'Read more';
});
card.classList.add('expanded');
toggleBtn.textContent = 'Close';
}
// Animate the layout change
Flip.from(flipState, {
duration: 0.7,
ease: 'expo.inOut',
stagger: 0.04,
absolute: true,
onComplete: () => ScrollTrigger.refresh(),
});
}
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
toggleCard();
});
card.addEventListener('click', () => {
if (!card.classList.contains('expanded')) {
toggleCard();
}
});
});
// ═══════════════════════════════════════════════════════════════════════
// CLOSING SECTION
// ═══════════════════════════════════════════════════════════════════════
const closingQuote = document.querySelector('.closing-quote');
if (closingQuote) {
const closingSplit = new SplitText(closingQuote, { type: 'words', wordsClass: 'word' });
gsap.set(closingSplit.words, {
opacity: 0,
y: reduced ? 0 : 25,
});
gsap.to(closingSplit.words, {
opacity: 1, y: 0,
duration: dur(0.6),
ease: 'expo.out',
stagger: { each: 0.08 },
scrollTrigger: {
trigger: '.closing-section',
start: 'top 70%',
toggleActions: 'play none none reverse',
},
});
}
const closingMeta = document.querySelector('.closing-meta');
if (closingMeta) {
gsap.set(closingMeta, { opacity: 0, y: reduced ? 0 : 15 });
gsap.to(closingMeta, {
opacity: 1, y: 0,
duration: dur(0.6),
ease: 'expo.out',
scrollTrigger: {
trigger: '.closing-section',
start: 'top 60%',
toggleActions: 'play none none reverse',
},
});
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Travel Editorial — stealthisdesign</title>
<link rel="stylesheet" href="style.css">
<script type="importmap">{"imports":{"gsap":"/vendor/gsap/index.js","gsap/ScrollTrigger":"/vendor/gsap/ScrollTrigger.js","gsap/SplitText":"/vendor/gsap/SplitText.js","gsap/Flip":"/vendor/gsap/Flip.js","gsap/ScrambleTextPlugin":"/vendor/gsap/ScrambleTextPlugin.js","gsap/TextPlugin":"/vendor/gsap/TextPlugin.js","gsap/all":"/vendor/gsap/all.js","gsap/":"/vendor/gsap/","lenis":"/vendor/lenis/dist/lenis.mjs","three":"/vendor/three/build/three.module.js","three/addons/":"/vendor/three/examples/jsm/"}}</script>
<style>html.lenis,
html.lenis body {
height: auto;
}
.lenis:not(.lenis-autoToggle).lenis-stopped {
overflow: clip;
}
.lenis [data-lenis-prevent],
.lenis [data-lenis-prevent-wheel],
.lenis [data-lenis-prevent-touch] {
overscroll-behavior: contain;
}
.lenis.lenis-smooth iframe {
pointer-events: none;
}
.lenis.lenis-autoToggle {
transition-property: overflow;
transition-duration: 1ms;
transition-behavior: allow-discrete;
}</style>
</head>
<body>
<!-- Editorial Hero -->
<section class="section hero-section" id="hero">
<div class="hero-bg" aria-hidden="true"></div>
<div class="hero-content">
<span class="hero-overline">A Visual Journey</span>
<h1 class="hero-title">The Amalfi Coast</h1>
<hr class="hero-rule">
<p class="hero-subtitle">Where Sea Meets Sky</p>
<div class="scroll-arrow" id="scroll-arrow">
<svg width="20" height="32" viewBox="0 0 20 32" fill="none">
<path d="M10 0v28M2 20l8 8 8-8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</div>
</section>
<!-- Opening Paragraph -->
<section class="section prose-section" id="prose">
<div class="prose-column">
<p class="prose-text dropcap">Along the southern edge of Italy's Sorrentine Peninsula, the Amalfi Coast unfolds like a watercolor painting left out in the Mediterranean sun. Pastel villages cling to limestone cliffs that plunge dramatically into turquoise waters, while terraced lemon groves perfume the salt-tinged air with citrus sweetness.</p>
<p class="prose-text">This stretch of coastline has inspired artists, poets, and wanderers for centuries. Every turn in the winding road reveals another vista that seems too beautiful to be real — a composition of color and light that no camera can fully capture.</p>
<blockquote class="pull-quote">
<p class="quote-text">"The coast is a place where time moves differently — not faster or slower, but in waves."</p>
</blockquote>
<p class="prose-text">The ancient towns of Positano, Amalfi, and Ravello each possess their own character. Positano cascades down its cliff face in a tumble of pink, terracotta, and white. Amalfi's cathedral rises from its piazza like a jewel. Ravello floats above it all, its gardens offering views that stretch to the edge of the world.</p>
</div>
</section>
<!-- Horizontal Photo Gallery -->
<section class="gallery-section" id="gallery">
<div class="gallery-track">
<div class="gallery-panel panel-cliffs">
<div class="panel-landscape" aria-hidden="true"></div>
<div class="panel-caption">
<span class="caption-number">01</span>
<span class="caption-text">The Cliffs of Positano</span>
</div>
</div>
<div class="gallery-panel panel-sea">
<div class="panel-landscape" aria-hidden="true"></div>
<div class="panel-caption">
<span class="caption-number">02</span>
<span class="caption-text">Mediterranean Waters</span>
</div>
</div>
<div class="gallery-panel panel-sunset">
<div class="panel-landscape" aria-hidden="true"></div>
<div class="panel-caption">
<span class="caption-number">03</span>
<span class="caption-text">Golden Hour</span>
</div>
</div>
<div class="gallery-panel panel-village">
<div class="panel-landscape" aria-hidden="true"></div>
<div class="panel-caption">
<span class="caption-number">04</span>
<span class="caption-text">The Village at Dusk</span>
</div>
</div>
</div>
<!-- Progress bar -->
<div class="gallery-progress">
<div class="gallery-progress-fill" id="gallery-fill"></div>
</div>
</section>
<!-- FLIP Stories Section -->
<section class="section stories-section" id="stories">
<h2 class="stories-heading">Stories from the Coast</h2>
<div class="stories-grid" id="stories-grid">
<article class="story-card" data-story="0">
<div class="story-header header-path"></div>
<h3 class="story-title">The Coastal Path</h3>
<p class="story-preview">The Sentiero degli Dei — the Path of the Gods — winds along the ridgeline high above the sea, offering views that ancient Greeks believed were worthy of Olympus itself.</p>
<div class="story-full">
<p>Starting from the town of Agerola, the trail descends gradually through terraced hillsides thick with Mediterranean scrub. Wild rosemary and thyme release their fragrance underfoot. To one side, the cliff drops hundreds of meters to the sea below; to the other, the peaks of the Lattari Mountains rise into wisps of cloud.</p>
<p>It takes roughly four hours to complete the walk, ending with a steep descent into Nocelle and onward to Positano. But no one rushes this path. Every bend invites you to stop, to breathe, to absorb a panorama that shifts with the light.</p>
</div>
<button class="story-toggle">Read more</button>
</article>
<article class="story-card" data-story="1">
<div class="story-header header-lemon"></div>
<h3 class="story-title">Limoncello Sunset</h3>
<p class="story-preview">The lemons of the Amalfi Coast are unlike any others in the world — enormous, fragrant, and sweet enough to eat raw, their thick rinds holding centuries of tradition.</p>
<div class="story-full">
<p>In the terraced groves that climb the hillsides, the sfusato amalfitano lemons grow under canopies of chestnut poles and straw mats. These are not the small, tart lemons of commerce — they are giants, sometimes the size of a grapefruit, with rinds so aromatic they perfume entire valleys.</p>
<p>At sunset, when the golden light catches the fruit hanging heavy on the branches, you understand why limoncello became the coast's signature drink. The recipe is simple: lemon zest, sugar, water, alcohol. The magic is in the lemons themselves.</p>
</div>
<button class="story-toggle">Read more</button>
</article>
<article class="story-card" data-story="2">
<div class="story-header header-fisher"></div>
<h3 class="story-title">Fisherman's Dawn</h3>
<p class="story-preview">Before the tourists wake, before the cafes open their shutters, the fishermen of Cetara have already been at sea for hours, continuing a tradition as old as the coast itself.</p>
<div class="story-full">
<p>Cetara is the quietest of the Amalfi Coast towns, overlooked by most visitors in favor of its more photogenic neighbors. But it is here, in this small harbor village, that you find the coast's oldest and most authentic tradition: the colatura di alici, an anchovy sauce descended directly from the Roman garum.</p>
<p>The fishermen set out in darkness, navigating by the stars and the feel of the current. They return with the dawn, their small boats heavy with the silver catch that will be salted, pressed, and aged into liquid gold.</p>
</div>
<button class="story-toggle">Read more</button>
</article>
</div>
</section>
<!-- Closing -->
<section class="section closing-section" id="closing">
<div class="closing-content">
<p class="closing-quote">Every journey begins with a single step.</p>
<div class="closing-meta">
<span class="meta-author">Words by Elena Voss</span>
<span class="meta-divider">—</span>
<span class="meta-date">Summer 2025</span>
<span class="meta-divider">—</span>
<span class="meta-time">8 min read</span>
</div>
</div>
</section>
<script type="module" src="script.js"></script>
</body>
</html>