Travel — Explore Destinations
A discovery-first explore page for the fictional Wanderlist travel magazine, opening on a full-bleed CSS-and-SVG sunset horizon and a rounded hero search. Filter chips for region, vibe, budget and best month combine with live keyword search to narrow a responsive grid of gradient destination cards, each carrying a star rating, from-price, price tier and best-time badge. A live result count, four sort orders, an empty state and a heart-driven saved-trip drawer keep the whole page warm, editorial and genuinely interactive.
MCP
Code
:root {
--bg: #fbf7f1;
--surface: #ffffff;
--ink: #241f1a;
--muted: #6b6259;
--teal: #1f8a8a;
--teal-deep: #15605f;
--coral: #e8623f;
--coral-deep: #c44a2c;
--sand: #e7d8c3;
--gold: #e2a32b;
--line: rgba(36, 31, 26, 0.12);
--line-strong: rgba(36, 31, 26, 0.2);
--shadow-sm: 0 1px 2px rgba(36, 31, 26, 0.06);
--shadow-md: 0 10px 30px -16px rgba(36, 31, 26, 0.4);
--shadow-lg: 0 24px 60px -28px rgba(36, 31, 26, 0.55);
--radius: 16px;
--radius-sm: 10px;
--serif: "Fraunces", Georgia, "Times New Roman", serif;
--sans: "Work Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
*,
*::before,
*::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: var(--sans);
font-size: 16px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
img, svg { display: block; max-width: 100%; }
button { font: inherit; color: inherit; cursor: pointer; }
a { color: inherit; }
.wrap {
width: min(1180px, 100% - 2.5rem);
margin-inline: auto;
}
.skip-link {
position: absolute;
left: 1rem;
top: -3rem;
z-index: 60;
background: var(--ink);
color: #fff;
padding: 0.55rem 0.9rem;
border-radius: 8px;
text-decoration: none;
transition: top 0.18s ease;
}
.skip-link:focus { top: 0.75rem; }
:focus-visible {
outline: 2.5px solid var(--teal);
outline-offset: 2px;
border-radius: 6px;
}
/* ---------- Header ---------- */
.site-head {
position: sticky;
top: 0;
z-index: 40;
background: rgba(251, 247, 241, 0.86);
backdrop-filter: saturate(140%) blur(10px);
border-bottom: 1px solid var(--line);
}
.head-inner {
display: flex;
align-items: center;
gap: 1rem;
height: 66px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 0.55rem;
text-decoration: none;
font-family: var(--serif);
font-weight: 700;
font-size: 1.32rem;
letter-spacing: -0.01em;
}
.brand-mark {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 10px;
background: linear-gradient(135deg, var(--teal), var(--teal-deep));
color: #fff;
box-shadow: var(--shadow-sm);
}
.head-nav {
margin-left: 1.5rem;
display: flex;
gap: 1.4rem;
}
.head-nav a {
text-decoration: none;
color: var(--muted);
font-weight: 500;
font-size: 0.95rem;
padding: 0.25rem 0;
border-bottom: 2px solid transparent;
transition: color 0.15s ease, border-color 0.15s ease;
}
.head-nav a:hover { color: var(--ink); }
.head-nav a[aria-current="page"] {
color: var(--ink);
border-bottom-color: var(--coral);
}
.trip-btn {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: var(--ink);
color: #fff;
border: none;
padding: 0.55rem 0.95rem;
border-radius: 999px;
font-weight: 600;
font-size: 0.92rem;
box-shadow: var(--shadow-sm);
transition: transform 0.12s ease, background 0.15s ease;
}
.trip-btn:hover { transform: translateY(-1px); background: #34302a; }
.trip-btn:active { transform: translateY(0); }
.trip-heart { display: inline-flex; color: var(--coral); }
.trip-count {
display: inline-grid;
place-items: center;
min-width: 20px;
height: 20px;
padding: 0 5px;
border-radius: 999px;
background: var(--coral);
color: #fff;
font-size: 0.74rem;
font-weight: 700;
}
/* ---------- Hero ---------- */
.hero {
position: relative;
isolation: isolate;
overflow: hidden;
padding: clamp(3rem, 7vw, 5.5rem) 0 clamp(2.5rem, 5vw, 3.75rem);
}
.hero-scene {
position: absolute;
inset: 0;
z-index: -2;
}
.hero-svg { width: 100%; height: 100%; }
.hero::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background:
radial-gradient(120% 90% at 18% 12%, rgba(36, 31, 26, 0.42), transparent 60%),
linear-gradient(180deg, rgba(36, 31, 26, 0.28), rgba(36, 31, 26, 0.06) 45%, var(--bg) 99%);
}
.hero-inner { color: #fff8ef; max-width: 720px; }
.hero-kicker {
margin: 0 0 0.7rem;
text-transform: uppercase;
letter-spacing: 0.18em;
font-size: 0.74rem;
font-weight: 600;
color: #ffe6cf;
}
#hero-title {
font-family: var(--serif);
font-weight: 700;
font-size: clamp(2.1rem, 5.6vw, 3.6rem);
line-height: 1.04;
letter-spacing: -0.015em;
margin: 0 0 0.8rem;
text-shadow: 0 2px 24px rgba(36, 31, 26, 0.35);
}
.hero-sub {
margin: 0 0 1.6rem;
font-size: clamp(1rem, 2.4vw, 1.18rem);
max-width: 38ch;
color: #fdeede;
}
.search {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--surface);
border-radius: 999px;
padding: 0.4rem 0.4rem 0.4rem 1rem;
box-shadow: var(--shadow-lg);
max-width: 560px;
}
.search-icon { display: inline-flex; color: var(--muted); }
.search input {
flex: 1;
min-width: 0;
border: none;
background: transparent;
font-size: 1rem;
color: var(--ink);
padding: 0.55rem 0.25rem;
}
.search input:focus { outline: none; }
.search input::placeholder { color: #9a9087; }
.search-clear {
border: none;
background: var(--sand);
color: var(--ink);
width: 28px;
height: 28px;
border-radius: 999px;
font-size: 1.15rem;
line-height: 1;
display: grid;
place-items: center;
}
.search-clear:hover { background: #dcc9ae; }
.search-go {
border: none;
background: var(--coral);
color: #fff;
font-weight: 600;
padding: 0.6rem 1.3rem;
border-radius: 999px;
transition: background 0.15s ease, transform 0.12s ease;
}
.search-go:hover { background: var(--coral-deep); transform: translateY(-1px); }
.search-go:active { transform: translateY(0); }
/* ---------- Controls ---------- */
.controls {
padding: 1.5rem 0 0.5rem;
}
.filter-block {
display: flex;
align-items: center;
gap: 0.7rem;
flex-wrap: wrap;
padding: 0.55rem 0;
}
.filter-row {
display: flex;
gap: 1.8rem;
flex-wrap: wrap;
}
.filter-label {
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted);
min-width: 58px;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
}
.chip {
border: 1px solid var(--line-strong);
background: var(--surface);
color: var(--ink);
padding: 0.4rem 0.85rem;
border-radius: 999px;
font-size: 0.9rem;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 0.35rem;
transition: background 0.14s ease, border-color 0.14s ease, color 0.14s ease, transform 0.1s ease;
}
.chip:hover { border-color: var(--teal); color: var(--teal-deep); }
.chip:active { transform: scale(0.97); }
.chip.is-active {
background: var(--ink);
border-color: var(--ink);
color: #fff;
}
.chip.is-active:hover { color: #fff; }
.month-select,
.sort-select {
border: 1px solid var(--line-strong);
background: var(--surface);
color: var(--ink);
border-radius: 999px;
padding: 0.45rem 0.9rem;
font-size: 0.9rem;
font-weight: 500;
}
.month-select:hover,
.sort-select:hover { border-color: var(--teal); }
/* ---------- Results head ---------- */
.results-head { padding: 0.4rem 0 0.9rem; }
.results-head-inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
border-top: 1px solid var(--line);
padding-top: 1.1rem;
margin-top: 0.6rem;
}
.result-count {
margin: 0;
font-family: var(--serif);
font-weight: 600;
font-size: 1.15rem;
}
.result-count b { color: var(--coral-deep); }
.results-tools {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.sort-wrap { display: inline-flex; align-items: center; gap: 0.5rem; }
.reset-btn {
border: 1px solid var(--line-strong);
background: transparent;
color: var(--muted);
border-radius: 999px;
padding: 0.4rem 0.85rem;
font-size: 0.85rem;
font-weight: 600;
}
.reset-btn:hover { color: var(--coral-deep); border-color: var(--coral); }
.reset-btn.solid {
background: var(--coral);
color: #fff;
border-color: var(--coral);
}
.reset-btn.solid:hover { background: var(--coral-deep); color: #fff; }
/* ---------- Grid ---------- */
.grid {
list-style: none;
margin: 0 0 3rem;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
gap: 1.4rem;
}
.card {
position: relative;
display: flex;
flex-direction: column;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-md);
border-color: var(--line-strong);
}
.card-hero {
position: relative;
aspect-ratio: 16 / 10;
overflow: hidden;
}
.card-hero::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 40%, rgba(36, 31, 26, 0.45));
}
.card-hero .scene {
position: absolute;
inset: 0;
background-size: cover;
}
.card-badges {
position: absolute;
top: 0.7rem;
left: 0.7rem;
z-index: 2;
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.badge {
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.03em;
padding: 0.22rem 0.55rem;
border-radius: 999px;
background: rgba(255, 248, 239, 0.92);
color: var(--ink);
backdrop-filter: blur(2px);
}
.badge.month { background: var(--gold); color: #3a2c08; }
.badge.vibe { background: rgba(31, 138, 138, 0.92); color: #fff; }
.save-btn {
position: absolute;
top: 0.6rem;
right: 0.6rem;
z-index: 3;
width: 38px;
height: 38px;
border-radius: 999px;
border: none;
background: rgba(255, 248, 239, 0.92);
color: var(--ink);
display: grid;
place-items: center;
box-shadow: var(--shadow-sm);
transition: transform 0.12s ease, background 0.15s ease, color 0.15s ease;
}
.save-btn:hover { transform: scale(1.08); }
.save-btn:active { transform: scale(0.94); }
.save-btn svg { transition: transform 0.18s ease; }
.save-btn[aria-pressed="true"] {
background: var(--coral);
color: #fff;
}
.save-btn[aria-pressed="true"] svg { fill: #fff; transform: scale(1.08); }
.card-body {
display: flex;
flex-direction: column;
gap: 0.45rem;
padding: 0.95rem 1.05rem 1.1rem;
}
.card-place {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.6rem;
}
.card-name {
font-family: var(--serif);
font-weight: 600;
font-size: 1.28rem;
line-height: 1.1;
letter-spacing: -0.01em;
margin: 0;
}
.card-country {
font-size: 0.82rem;
color: var(--muted);
font-weight: 500;
white-space: nowrap;
}
.card-blurb {
margin: 0;
font-size: 0.9rem;
color: var(--muted);
line-height: 1.45;
}
.card-meta {
margin-top: auto;
padding-top: 0.55rem;
border-top: 1px solid var(--line);
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
}
.rating {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-weight: 600;
font-size: 0.9rem;
}
.rating .star { color: var(--gold); }
.rating .reviews { color: var(--muted); font-weight: 400; font-size: 0.8rem; }
.price {
text-align: right;
line-height: 1.15;
}
.price .from { display: block; font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted); }
.price .amount { font-family: var(--serif); font-weight: 600; font-size: 1.1rem; }
.price .tier { color: var(--teal); margin-left: 0.15rem; font-weight: 600; }
/* ---------- Empty ---------- */
.empty {
text-align: center;
padding: 3.5rem 1rem 4.5rem;
border: 1px dashed var(--line-strong);
border-radius: var(--radius);
background: linear-gradient(180deg, #fffdf9, var(--surface));
margin-bottom: 3rem;
}
.empty-mark { font-size: 2.6rem; margin-bottom: 0.5rem; }
.empty h2 { font-family: var(--serif); margin: 0 0 0.35rem; font-size: 1.5rem; }
.empty p { margin: 0 0 1.3rem; color: var(--muted); }
/* ---------- Footer ---------- */
.site-foot {
border-top: 1px solid var(--line);
background: #f4ecdf;
padding: 2rem 0;
margin-top: 1rem;
}
.foot-inner { display: flex; flex-direction: column; gap: 0.3rem; }
.site-foot p { margin: 0; }
.foot-note { color: var(--muted); font-size: 0.88rem; }
/* ---------- Drawer ---------- */
.drawer-scrim {
position: fixed;
inset: 0;
z-index: 50;
background: rgba(36, 31, 26, 0.45);
backdrop-filter: blur(2px);
animation: fade 0.2s ease;
}
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.drawer {
position: fixed;
top: 0;
right: 0;
z-index: 55;
width: min(380px, 92vw);
height: 100dvh;
background: var(--surface);
border-left: 1px solid var(--line);
box-shadow: var(--shadow-lg);
transform: translateX(100%);
transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
padding: 1.25rem;
overflow-y: auto;
}
.drawer.is-open { transform: translateX(0); }
.drawer-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.drawer-head h2 { font-family: var(--serif); margin: 0; font-size: 1.45rem; }
.drawer-close {
border: none;
background: var(--sand);
width: 34px;
height: 34px;
border-radius: 999px;
font-size: 1.3rem;
line-height: 1;
display: grid;
place-items: center;
}
.drawer-close:hover { background: #dcc9ae; }
.saved-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.7rem; }
.saved-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.55rem;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
}
.saved-thumb { width: 52px; height: 52px; border-radius: 8px; flex: none; }
.saved-text { flex: 1; min-width: 0; }
.saved-text strong { display: block; font-family: var(--serif); font-size: 1rem; }
.saved-text span { font-size: 0.8rem; color: var(--muted); }
.saved-remove {
border: none;
background: transparent;
color: var(--muted);
font-size: 1.2rem;
width: 30px;
height: 30px;
border-radius: 999px;
}
.saved-remove:hover { background: var(--sand); color: var(--coral-deep); }
.saved-empty { color: var(--muted); font-size: 0.92rem; margin-top: 0.5rem; }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 1.5rem;
transform: translate(-50%, 1.5rem);
z-index: 70;
background: var(--ink);
color: #fff;
padding: 0.7rem 1.1rem;
border-radius: 999px;
font-size: 0.9rem;
font-weight: 500;
box-shadow: var(--shadow-lg);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 720px) {
.head-nav { display: none; }
.trip-btn { margin-left: auto; }
.filter-row { flex-direction: column; gap: 0.3rem; }
.results-head-inner { flex-direction: column; align-items: flex-start; }
}
@media (max-width: 420px) {
.wrap { width: min(1180px, 100% - 1.5rem); }
.search { flex-wrap: wrap; }
.search-go { width: 100%; }
.grid { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}(function () {
"use strict";
// ---- Fictional destination data -----------------------------------------
// priceFrom in USD (illustrative). budget tier 1=$ 2=$$ 3=$$$.
// gradients are pure CSS — no external images.
var DESTINATIONS = [
{ id: "lisbon", name: "Lisbon", country: "Portugal", region: "Europe", vibe: "city",
rating: 4.8, reviews: 2140, priceFrom: 620, budget: 2, months: ["Apr", "May", "Sep", "Oct"], popularity: 98,
blurb: "Tiled hills, custard tarts and tram-yellow afternoons.",
g: "linear-gradient(135deg,#f0a07a,#e8623f 55%,#9a3b6f)" },
{ id: "kyoto", name: "Kyoto", country: "Japan", region: "Asia", vibe: "city",
rating: 4.9, reviews: 3180, priceFrom: 940, budget: 3, months: ["Mar", "Apr", "Nov"], popularity: 96,
blurb: "Lantern lanes, moss gardens and slow temple mornings.",
g: "linear-gradient(135deg,#f4c4c0,#d76a8a 55%,#6e3b8a)" },
{ id: "santorini", name: "Santorini", country: "Greece", region: "Europe", vibe: "beach",
rating: 4.6, reviews: 1870, priceFrom: 880, budget: 3, months: ["May", "Jun", "Sep"], popularity: 91,
blurb: "White cliffs over a caldera the colour of deep ink.",
g: "linear-gradient(135deg,#9fd0e8,#3f86c4 55%,#1f3d8a)" },
{ id: "queenstown", name: "Queenstown", country: "New Zealand", region: "Oceania", vibe: "nature",
rating: 4.7, reviews: 1320, priceFrom: 1150, budget: 3, months: ["Dec", "Jan", "Feb"], popularity: 84,
blurb: "Alpine lakes, jet boats and wide southern skies.",
g: "linear-gradient(135deg,#bfe6c8,#2f9e7a 55%,#1c5a6e)" },
{ id: "marrakech", name: "Marrakech", country: "Morocco", region: "Africa", vibe: "city",
rating: 4.5, reviews: 1640, priceFrom: 410, budget: 1, months: ["Mar", "Apr", "Oct", "Nov"], popularity: 88,
blurb: "Spice-stall colour, riad courtyards and rooftop dusk.",
g: "linear-gradient(135deg,#f3c969,#e8623f 55%,#a8323b)" },
{ id: "tulum", name: "Tulum", country: "Mexico", region: "Americas", vibe: "beach",
rating: 4.4, reviews: 1990, priceFrom: 560, budget: 2, months: ["Jan", "Feb", "Mar", "Dec"], popularity: 87,
blurb: "Jungle-edge cenotes and a long, soft Caribbean shore.",
g: "linear-gradient(135deg,#a8e6d4,#1f8a8a 55%,#1d4f5e)" },
{ id: "banff", name: "Banff", country: "Canada", region: "Americas", vibe: "nature",
rating: 4.8, reviews: 1450, priceFrom: 720, budget: 2, months: ["Jun", "Jul", "Aug", "Sep"], popularity: 82,
blurb: "Glacier-fed lakes mirroring a wall of dark pines.",
g: "linear-gradient(135deg,#bfe2ee,#3f86c4 55%,#244a6e)" },
{ id: "bali", name: "Ubud, Bali", country: "Indonesia", region: "Asia", vibe: "nature",
rating: 4.6, reviews: 2760, priceFrom: 380, budget: 1, months: ["Apr", "May", "Jun", "Sep"], popularity: 93,
blurb: "Terraced rice fields and incense-scented mornings.",
g: "linear-gradient(135deg,#d6e89a,#5aa83b 55%,#1f6e4a)" },
{ id: "cinque", name: "Cinque Terre", country: "Italy", region: "Europe", vibe: "beach",
rating: 4.7, reviews: 1710, priceFrom: 690, budget: 2, months: ["May", "Jun", "Sep"], popularity: 85,
blurb: "Pastel cliff villages stitched by a coastal trail.",
g: "linear-gradient(135deg,#f6b486,#e8623f 55%,#1f6e7a)" },
{ id: "capetown", name: "Cape Town", country: "South Africa", region: "Africa", vibe: "city",
rating: 4.6, reviews: 1580, priceFrom: 540, budget: 2, months: ["Nov", "Dec", "Jan", "Feb"], popularity: 80,
blurb: "A flat-topped mountain above two restless oceans.",
g: "linear-gradient(135deg,#f3c969,#1f8a8a 55%,#23406e)" },
{ id: "hoian", name: "Hoi An", country: "Vietnam", region: "Asia", vibe: "city",
rating: 4.7, reviews: 2230, priceFrom: 320, budget: 1, months: ["Feb", "Mar", "Apr"], popularity: 86,
blurb: "Lantern-lit old town reflected in a slow river.",
g: "linear-gradient(135deg,#f7d27a,#e8623f 55%,#7a2f6e)" },
{ id: "reykjavik", name: "Reykjavík", country: "Iceland", region: "Europe", vibe: "nature",
rating: 4.5, reviews: 1240, priceFrom: 980, budget: 3, months: ["Jun", "Jul", "Sep"], popularity: 78,
blurb: "Geysers, black sand and a sky that barely sleeps.",
g: "linear-gradient(135deg,#b8c8e8,#3f5fc4 55%,#1c2a5e)" },
{ id: "maui", name: "Maui", country: "USA", region: "Oceania", vibe: "beach",
rating: 4.7, reviews: 2410, priceFrom: 1090, budget: 3, months: ["Apr", "May", "Sep", "Oct"], popularity: 89,
blurb: "Volcano roads down to a quiet, sun-warmed lagoon.",
g: "linear-gradient(135deg,#9fe2d4,#1f8a8a 55%,#1d5a6e)" },
{ id: "cartagena", name: "Cartagena", country: "Colombia", region: "Americas", vibe: "city",
rating: 4.5, reviews: 1360, priceFrom: 430, budget: 1, months: ["Jan", "Feb", "Mar"], popularity: 81,
blurb: "Walled city of bougainvillea and warm night plazas.",
g: "linear-gradient(135deg,#f6a8c0,#e8623f 55%,#9a3b6f)" },
{ id: "lapland", name: "Lapland", country: "Finland", region: "Europe", vibe: "nature",
rating: 4.6, reviews: 980, priceFrom: 860, budget: 3, months: ["Dec", "Jan", "Feb"], popularity: 76,
blurb: "Snow-hushed forests under a green-ribboned sky.",
g: "linear-gradient(135deg,#c4d8ee,#2f9ea8 55%,#1c2e5e)" },
{ id: "zanzibar", name: "Zanzibar", country: "Tanzania", region: "Africa", vibe: "beach",
rating: 4.5, reviews: 1120, priceFrom: 590, budget: 2, months: ["Jun", "Jul", "Aug", "Sep"], popularity: 79,
blurb: "Spice-island sails on water like pale jade glass.",
g: "linear-gradient(135deg,#a8e6d4,#2f9e7a 55%,#1f5a6e)" }
];
var TIER = { 1: "$", 2: "$$", 3: "$$$" };
var VIBE_LABEL = { beach: "Beach", city: "City", nature: "Nature" };
// ---- State ---------------------------------------------------------------
var state = {
q: "",
region: "all",
vibe: "all",
budget: "all",
month: "all",
sort: "popular",
saved: Object.create(null)
};
// ---- DOM refs ------------------------------------------------------------
var $ = function (s, r) { return (r || document).querySelector(s); };
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
var grid = $("#grid");
var empty = $("#empty");
var resultCount = $("#resultCount");
var resetBtn = $("#resetBtn");
var searchInput = $("#searchInput");
var searchClear = $("#searchClear");
var monthSelect = $("#monthSelect");
var sortSelect = $("#sortSelect");
var tripCount = $("#tripCount");
var savedList = $("#savedList");
var savedEmpty = $("#savedEmpty");
// ---- Toast ---------------------------------------------------------------
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2200);
}
// ---- Filtering / sorting -------------------------------------------------
function matches(d) {
if (state.region !== "all" && d.region !== state.region) return false;
if (state.vibe !== "all" && d.vibe !== state.vibe) return false;
if (state.budget !== "all" && String(d.budget) !== state.budget) return false;
if (state.month !== "all" && d.months.indexOf(state.month) === -1) return false;
if (state.q) {
var hay = (d.name + " " + d.country + " " + d.region + " " + d.vibe + " " + d.blurb).toLowerCase();
if (hay.indexOf(state.q) === -1) return false;
}
return true;
}
function sortList(list) {
var arr = list.slice();
switch (state.sort) {
case "rating": arr.sort(function (a, b) { return b.rating - a.rating || b.reviews - a.reviews; }); break;
case "price-asc": arr.sort(function (a, b) { return a.priceFrom - b.priceFrom; }); break;
case "price-desc": arr.sort(function (a, b) { return b.priceFrom - a.priceFrom; }); break;
default: arr.sort(function (a, b) { return b.popularity - a.popularity; });
}
return arr;
}
function isFiltered() {
return state.q || state.region !== "all" || state.vibe !== "all" ||
state.budget !== "all" || state.month !== "all";
}
// ---- Card rendering ------------------------------------------------------
function stars(rating) {
return '<span class="star" aria-hidden="true">★</span>' +
'<span>' + rating.toFixed(1) + '</span>';
}
function cardHtml(d) {
var saved = !!state.saved[d.id];
var bestMonth = d.months[0];
return '' +
'<li class="card" data-id="' + d.id + '">' +
'<div class="card-hero">' +
'<div class="scene" style="background:' + d.g + '"></div>' +
'<div class="card-badges">' +
'<span class="badge vibe">' + VIBE_LABEL[d.vibe] + '</span>' +
'<span class="badge month">Best in ' + bestMonth + '</span>' +
'</div>' +
'<button class="save-btn" type="button" data-id="' + d.id + '" ' +
'aria-pressed="' + saved + '" ' +
'aria-label="' + (saved ? "Remove " : "Save ") + d.name + (saved ? " from" : " to") + ' your trip">' +
'<svg viewBox="0 0 24 24" width="20" height="20" fill="' + (saved ? "currentColor" : "none") +
'" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M12 20s-7-4.4-9-9a5 5 0 0 1 9-2 5 5 0 0 1 9 2c-2 4.6-9 9-9 9z"/></svg>' +
'</button>' +
'</div>' +
'<div class="card-body">' +
'<div class="card-place">' +
'<h3 class="card-name">' + d.name + '</h3>' +
'<span class="card-country">' + d.country + '</span>' +
'</div>' +
'<p class="card-blurb">' + d.blurb + '</p>' +
'<div class="card-meta">' +
'<span class="rating">' + stars(d.rating) +
'<span class="reviews">(' + d.reviews.toLocaleString() + ')</span></span>' +
'<span class="price"><span class="from">From</span>' +
'<span class="amount">$' + d.priceFrom + '<span class="tier">' + TIER[d.budget] + '</span></span></span>' +
'</div>' +
'</div>' +
'</li>';
}
function render() {
var visible = sortList(DESTINATIONS.filter(matches));
if (visible.length === 0) {
grid.innerHTML = "";
empty.hidden = false;
} else {
empty.hidden = true;
grid.innerHTML = visible.map(cardHtml).join("");
}
// Result count message
var total = DESTINATIONS.length;
if (!isFiltered()) {
resultCount.innerHTML = "Showing all <b>" + total + "</b> destinations";
} else if (visible.length === 0) {
resultCount.innerHTML = "<b>0</b> destinations match";
} else {
resultCount.innerHTML = "<b>" + visible.length + "</b> of " + total + " destinations";
}
resetBtn.hidden = !isFiltered();
}
// ---- Chip groups ---------------------------------------------------------
$$(".chips").forEach(function (group) {
var key = group.getAttribute("data-group");
group.addEventListener("click", function (e) {
var btn = e.target.closest(".chip");
if (!btn || !group.contains(btn)) return;
state[key] = btn.getAttribute("data-value");
$$(".chip", group).forEach(function (c) {
var on = c === btn;
c.classList.toggle("is-active", on);
c.setAttribute("aria-pressed", String(on));
});
render();
});
});
// ---- Search --------------------------------------------------------------
function applySearch() {
state.q = searchInput.value.trim().toLowerCase();
searchClear.hidden = state.q.length === 0;
render();
}
searchInput.addEventListener("input", applySearch);
$("#searchForm").addEventListener("submit", function (e) {
e.preventDefault();
applySearch();
document.getElementById("results").scrollIntoView({ behavior: "smooth", block: "start" });
});
searchClear.addEventListener("click", function () {
searchInput.value = "";
applySearch();
searchInput.focus();
});
// ---- Month + sort --------------------------------------------------------
monthSelect.addEventListener("change", function () {
state.month = monthSelect.value;
render();
});
sortSelect.addEventListener("change", function () {
state.sort = sortSelect.value;
render();
});
// ---- Reset ---------------------------------------------------------------
function resetFilters() {
state.q = ""; state.region = "all"; state.vibe = "all"; state.budget = "all"; state.month = "all";
searchInput.value = "";
searchClear.hidden = true;
monthSelect.value = "all";
$$(".chips").forEach(function (group) {
$$(".chip", group).forEach(function (c, i) {
var on = i === 0;
c.classList.toggle("is-active", on);
c.setAttribute("aria-pressed", String(on));
});
});
render();
toast("Filters cleared");
}
resetBtn.addEventListener("click", resetFilters);
$("#emptyReset").addEventListener("click", resetFilters);
// ---- Save / trip drawer --------------------------------------------------
function destById(id) {
for (var i = 0; i < DESTINATIONS.length; i++) if (DESTINATIONS[i].id === id) return DESTINATIONS[i];
return null;
}
function updateTrip() {
var ids = Object.keys(state.saved);
tripCount.textContent = String(ids.length);
if (ids.length === 0) {
savedList.innerHTML = "";
savedEmpty.hidden = false;
return;
}
savedEmpty.hidden = true;
savedList.innerHTML = ids.map(function (id) {
var d = destById(id);
return '' +
'<li class="saved-item" data-id="' + id + '">' +
'<span class="saved-thumb" style="background:' + d.g + '"></span>' +
'<span class="saved-text"><strong>' + d.name + '</strong><span>' + d.country +
' · from $' + d.priceFrom + '</span></span>' +
'<button class="saved-remove" type="button" data-id="' + id + '" ' +
'aria-label="Remove ' + d.name + ' from your trip">×</button>' +
'</li>';
}).join("");
}
function toggleSave(id) {
var d = destById(id);
if (state.saved[id]) {
delete state.saved[id];
toast("Removed " + d.name + " from your trip");
} else {
state.saved[id] = true;
toast("Saved " + d.name + " to your trip ♥");
}
// sync any visible save buttons for this id
$$('.save-btn[data-id="' + id + '"]').forEach(function (btn) {
var on = !!state.saved[id];
btn.setAttribute("aria-pressed", String(on));
btn.setAttribute("aria-label", (on ? "Remove " : "Save ") + d.name + (on ? " from" : " to") + " your trip");
var path = btn.querySelector("svg");
if (path) path.setAttribute("fill", on ? "currentColor" : "none");
});
updateTrip();
}
// delegated save clicks in grid
grid.addEventListener("click", function (e) {
var btn = e.target.closest(".save-btn");
if (btn) { toggleSave(btn.getAttribute("data-id")); }
});
// delegated remove clicks in drawer
savedList.addEventListener("click", function (e) {
var btn = e.target.closest(".saved-remove");
if (btn) { toggleSave(btn.getAttribute("data-id")); }
});
// ---- Drawer open/close ---------------------------------------------------
var drawer = $("#drawer");
var scrim = $("#drawerScrim");
var tripBtn = $("#tripBtn");
var drawerClose = $("#drawerClose");
var lastFocus = null;
function openDrawer() {
lastFocus = document.activeElement;
drawer.classList.add("is-open");
drawer.setAttribute("aria-hidden", "false");
scrim.hidden = false;
drawerClose.focus();
}
function closeDrawer() {
drawer.classList.remove("is-open");
drawer.setAttribute("aria-hidden", "true");
scrim.hidden = true;
if (lastFocus && lastFocus.focus) lastFocus.focus();
}
tripBtn.addEventListener("click", openDrawer);
drawerClose.addEventListener("click", closeDrawer);
scrim.addEventListener("click", closeDrawer);
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && drawer.classList.contains("is-open")) closeDrawer();
});
// ---- Boot ----------------------------------------------------------------
render();
updateTrip();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Wanderlist — Explore Destinations</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,[email protected],500;9..144,600;9..144,700&family=Work+Sans:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#results">Skip to destinations</a>
<header class="site-head" role="banner">
<div class="wrap head-inner">
<a class="brand" href="#" aria-label="Wanderlist home">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 21s-7-5.5-7-11a7 7 0 0 1 14 0c0 5.5-7 11-7 11z"/>
<circle cx="12" cy="10" r="2.4"/>
</svg>
</span>
<span class="brand-name">Wanderlist</span>
</a>
<nav class="head-nav" aria-label="Primary">
<a href="#" aria-current="page">Explore</a>
<a href="#">Guides</a>
<a href="#">Magazine</a>
</nav>
<button class="trip-btn" id="tripBtn" type="button" aria-haspopup="dialog">
<span class="trip-heart" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20s-7-4.4-9-9a5 5 0 0 1 9-2 5 5 0 0 1 9 2c-2 4.6-9 9-9 9z"/>
</svg>
</span>
Saved <span class="trip-count" id="tripCount">0</span>
</button>
</div>
</header>
<main>
<section class="hero" aria-labelledby="hero-title">
<div class="hero-scene" aria-hidden="true">
<svg class="hero-svg" viewBox="0 0 1200 520" preserveAspectRatio="xMidYMax slice" role="img">
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#f8c99a"/>
<stop offset="0.45" stop-color="#f0a07a"/>
<stop offset="1" stop-color="#e8623f"/>
</linearGradient>
<linearGradient id="hillA" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#1f8a8a"/>
<stop offset="1" stop-color="#15605f"/>
</linearGradient>
<linearGradient id="hillB" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#2a6f6a"/>
<stop offset="1" stop-color="#1c4a47"/>
</linearGradient>
</defs>
<rect width="1200" height="520" fill="url(#sky)"/>
<circle cx="930" cy="150" r="64" fill="#fff3df" opacity="0.92"/>
<path d="M0 360 C 180 300 320 350 480 320 C 660 286 820 340 1010 300 C 1100 282 1160 300 1200 292 L1200 520 L0 520 Z" fill="url(#hillB)" opacity="0.85"/>
<path d="M0 420 C 160 372 300 410 470 392 C 660 372 840 420 1010 398 C 1110 386 1170 400 1200 396 L1200 520 L0 520 Z" fill="url(#hillA)"/>
<g opacity="0.5" fill="#fff3df">
<circle cx="180" cy="120" r="2"/><circle cx="260" cy="80" r="1.6"/>
<circle cx="520" cy="60" r="1.6"/><circle cx="700" cy="110" r="2"/>
<circle cx="1080" cy="90" r="1.6"/>
</g>
</svg>
</div>
<div class="wrap hero-inner">
<p class="hero-kicker">The world, edited for the curious</p>
<h1 id="hero-title">Find the trip you didn’t know you needed</h1>
<p class="hero-sub">Browse a world of hand-picked destinations — filter by region, vibe, budget and the best month to go.</p>
<form class="search" id="searchForm" role="search" aria-label="Search destinations">
<span class="search-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="7"/><path d="m20 20-3.2-3.2"/>
</svg>
</span>
<input id="searchInput" type="search" name="q" placeholder="Try “Lisbon”, “islands”, or “Japan”" autocomplete="off" aria-label="Search by name, country, or keyword" />
<button type="button" class="search-clear" id="searchClear" aria-label="Clear search" hidden>×</button>
<button type="submit" class="search-go">Explore</button>
</form>
</div>
</section>
<section class="controls" aria-label="Filters and sorting">
<div class="wrap">
<div class="filter-block">
<span class="filter-label" id="lbl-region">Region</span>
<div class="chips" role="group" aria-labelledby="lbl-region" data-group="region">
<button class="chip is-active" data-value="all" aria-pressed="true">All</button>
<button class="chip" data-value="Europe" aria-pressed="false">Europe</button>
<button class="chip" data-value="Asia" aria-pressed="false">Asia</button>
<button class="chip" data-value="Americas" aria-pressed="false">Americas</button>
<button class="chip" data-value="Africa" aria-pressed="false">Africa</button>
<button class="chip" data-value="Oceania" aria-pressed="false">Oceania</button>
</div>
</div>
<div class="filter-block">
<span class="filter-label" id="lbl-vibe">Vibe</span>
<div class="chips" role="group" aria-labelledby="lbl-vibe" data-group="vibe">
<button class="chip is-active" data-value="all" aria-pressed="true">Any</button>
<button class="chip" data-value="beach" aria-pressed="false"><span aria-hidden="true">🏝️</span> Beach</button>
<button class="chip" data-value="city" aria-pressed="false"><span aria-hidden="true">🏙️</span> City</button>
<button class="chip" data-value="nature" aria-pressed="false"><span aria-hidden="true">⛰️</span> Nature</button>
</div>
</div>
<div class="filter-row">
<div class="filter-block">
<span class="filter-label" id="lbl-budget">Budget</span>
<div class="chips" role="group" aria-labelledby="lbl-budget" data-group="budget">
<button class="chip is-active" data-value="all" aria-pressed="true">All</button>
<button class="chip" data-value="1" aria-pressed="false">$</button>
<button class="chip" data-value="2" aria-pressed="false">$$</button>
<button class="chip" data-value="3" aria-pressed="false">$$$</button>
</div>
</div>
<div class="filter-block month-block">
<label class="filter-label" for="monthSelect">Best in</label>
<select id="monthSelect" class="month-select">
<option value="all">Any month</option>
<option value="Jan">January</option><option value="Feb">February</option>
<option value="Mar">March</option><option value="Apr">April</option>
<option value="May">May</option><option value="Jun">June</option>
<option value="Jul">July</option><option value="Aug">August</option>
<option value="Sep">September</option><option value="Oct">October</option>
<option value="Nov">November</option><option value="Dec">December</option>
</select>
</div>
</div>
</div>
</section>
<section class="results-head" aria-live="polite">
<div class="wrap results-head-inner">
<p class="result-count" id="resultCount">Showing all destinations</p>
<div class="results-tools">
<button class="reset-btn" id="resetBtn" type="button" hidden>Clear filters</button>
<div class="sort-wrap">
<label class="filter-label" for="sortSelect">Sort</label>
<select id="sortSelect" class="sort-select">
<option value="popular">Most popular</option>
<option value="rating">Top rated</option>
<option value="price-asc">Price: low to high</option>
<option value="price-desc">Price: high to low</option>
</select>
</div>
</div>
</div>
</section>
<section id="results" class="wrap" aria-label="Destination results" tabindex="-1">
<ul class="grid" id="grid"></ul>
<div class="empty" id="empty" hidden>
<div class="empty-mark" aria-hidden="true">🧭</div>
<h2>No destinations match yet</h2>
<p>Try loosening a filter or searching a different place.</p>
<button class="reset-btn solid" id="emptyReset" type="button">Clear all filters</button>
</div>
</section>
</main>
<footer class="site-foot" role="contentinfo">
<div class="wrap foot-inner">
<p><strong>Wanderlist</strong> — an illustrative travel magazine.</p>
<p class="foot-note">Fictional destinations, prices and ratings. No bookings here.</p>
</div>
</footer>
<!-- Saved trip drawer -->
<div class="drawer-scrim" id="drawerScrim" hidden></div>
<aside class="drawer" id="drawer" role="dialog" aria-modal="true" aria-labelledby="drawerTitle" aria-hidden="true">
<div class="drawer-head">
<h2 id="drawerTitle">Your saved trip</h2>
<button class="drawer-close" id="drawerClose" type="button" aria-label="Close saved trip">×</button>
</div>
<ul class="saved-list" id="savedList"></ul>
<p class="saved-empty" id="savedEmpty">No spots saved yet. Tap the heart on any destination to start a trip.</p>
</aside>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Explore Destinations
An invitation-first explore page for the invented Wanderlist travel magazine. A full-bleed hero, rendered entirely from CSS gradients and a layered inline SVG horizon of rolling hills and a low sun, sits behind a serif headline and a single rounded search field. Beneath it, a tidy control bar offers chip groups for Region and Vibe (beach, city, nature), a Budget tier and a Best in month select, so a traveller can shape the catalogue from a glance.
Every control is wired to a live grid of sixteen gradient destination cards. Typing in the search box, toggling any chip or changing the month narrows the grid instantly while a result count updates — 16 of 16, 5 of 16, or a friendly empty state when nothing matches. A sort menu reorders the visible cards by popularity, rating or price in either direction, and a single Clear filters control resets everything with a toast. Each card pairs a star rating, a from-price, a price tier and a best-time badge with a save heart.
Saving runs through the whole page: tap a card’s heart to drop a destination into the Saved trip drawer, remove it from either place, and the heart, the header badge count and a toast all stay in sync. The drawer is a focus-trapped dialog that closes on the scrim or the Escape key, and the layout collapses from a multi-column grid to a single readable column down to about 360px.
Illustrative travel UI only — fictional destinations, prices, and maps.