Travel — Destination Guide
A magazine-grade destination guide for the fictional island of Isla Verde, opening on a full-bleed gradient hero with the title and key facts — best time, currency, language, and flight time. A sticky section nav scrollspies across Overview, See, Eat, Stay, Map, and Tips while rich POI cards carry ratings, price tiers, and add-to-trip hearts. A CSS and SVG island map links interactive pins to a synced legend, a horizontal photo gallery band scrolls beneath, and a save-guide action toggles with a confirming toast.
MCP
Code
:root {
--bg: #fbf7f1;
--card: #ffffff;
--ink: #241f1a;
--muted: #6b6259;
--teal: #1f8a8a;
--teal-deep: #14666a;
--coral: #e8623f;
--coral-deep: #c84a2a;
--sand: #e7d8c3;
--sand-soft: #f3e9da;
--line: rgba(36, 31, 26, 0.12);
--line-strong: rgba(36, 31, 26, 0.22);
--shadow: 0 18px 44px -24px rgba(36, 31, 26, 0.45);
--shadow-sm: 0 8px 22px -16px rgba(36, 31, 26, 0.5);
--radius: 18px;
--radius-sm: 12px;
--nav-h: 56px;
--serif: "Fraunces", Georgia, "Times New Roman", serif;
--sans: "Work Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
font-family: var(--sans);
color: var(--ink);
background: var(--bg);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1,
h2,
h3 {
margin: 0;
font-family: var(--serif);
font-weight: 600;
letter-spacing: -0.01em;
}
p {
margin: 0;
}
a {
color: inherit;
}
img,
svg {
display: block;
}
:focus-visible {
outline: 3px solid var(--teal);
outline-offset: 3px;
border-radius: 6px;
}
.skip-link {
position: absolute;
left: 50%;
top: -60px;
transform: translateX(-50%);
background: var(--ink);
color: var(--bg);
padding: 0.55rem 1rem;
border-radius: 0 0 12px 12px;
z-index: 60;
text-decoration: none;
font-weight: 600;
transition: top 0.2s ease;
}
.skip-link:focus {
top: 0;
}
/* ===================== HERO ===================== */
.hero {
position: relative;
min-height: clamp(440px, 78vh, 640px);
display: flex;
align-items: flex-end;
color: #fff5e8;
overflow: hidden;
isolation: isolate;
}
.hero__scene {
position: absolute;
inset: 0;
z-index: -2;
}
.hero__sky {
width: 100%;
height: 100%;
}
.palm {
transform-origin: bottom center;
animation: sway 6s ease-in-out infinite;
}
@keyframes sway {
0%,
100% {
transform: translate(120px, 360px) rotate(-1.5deg);
}
50% {
transform: translate(120px, 360px) rotate(1.5deg);
}
}
.hero__grain {
position: absolute;
inset: 0;
z-index: -1;
background:
radial-gradient(120% 90% at 50% 120%, rgba(36, 31, 26, 0.62), transparent 60%),
linear-gradient(0deg, rgba(36, 31, 26, 0.5), transparent 45%);
pointer-events: none;
}
.hero__inner {
width: min(1080px, 92vw);
margin: 0 auto;
padding: clamp(2rem, 6vw, 4rem) 0 clamp(2.4rem, 6vw, 3.6rem);
}
.hero__kicker {
text-transform: uppercase;
letter-spacing: 0.22em;
font-size: 0.72rem;
font-weight: 700;
margin-bottom: 0.7rem;
color: #ffe6c9;
}
.hero__title {
font-size: clamp(3.2rem, 12vw, 7rem);
line-height: 0.92;
font-weight: 600;
font-style: italic;
text-shadow: 0 6px 30px rgba(36, 31, 26, 0.45);
}
.hero__sub {
max-width: 38ch;
margin-top: 1rem;
font-size: clamp(1rem, 2.4vw, 1.18rem);
color: #fbeede;
}
.facts {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.6rem;
margin: 1.6rem 0 0;
max-width: 640px;
}
.fact {
background: rgba(255, 247, 235, 0.16);
border: 1px solid rgba(255, 247, 235, 0.3);
backdrop-filter: blur(6px);
border-radius: var(--radius-sm);
padding: 0.65rem 0.8rem;
}
.fact dt {
font-size: 0.66rem;
text-transform: uppercase;
letter-spacing: 0.14em;
color: #ffe6c9;
font-weight: 700;
}
.fact dd {
margin: 0.18rem 0 0;
font-weight: 600;
font-size: 0.96rem;
}
.hero__actions {
display: flex;
gap: 0.7rem;
flex-wrap: wrap;
margin-top: 1.6rem;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-family: var(--sans);
font-weight: 600;
font-size: 0.95rem;
padding: 0.7rem 1.2rem;
border-radius: 999px;
border: 1px solid transparent;
cursor: pointer;
text-decoration: none;
transition:
transform 0.15s ease,
background 0.2s ease,
box-shadow 0.2s ease,
color 0.2s ease;
}
.btn:active {
transform: translateY(1px);
}
.btn--save {
background: #fff5e8;
color: var(--coral-deep);
box-shadow: var(--shadow-sm);
}
.btn--save:hover {
background: #fff;
}
.btn--save .btn__icon {
font-size: 1.1rem;
transition: transform 0.2s ease;
}
.btn--save.is-saved {
background: var(--coral);
color: #fff;
}
.btn--save.is-saved .btn__icon {
transform: scale(1.25);
}
.btn--ghost {
background: rgba(255, 247, 235, 0.12);
border-color: rgba(255, 247, 235, 0.5);
color: #fff5e8;
}
.btn--ghost:hover {
background: rgba(255, 247, 235, 0.22);
}
/* ===================== STICKY NAV ===================== */
.sectionnav {
position: sticky;
top: 0;
z-index: 40;
background: rgba(251, 247, 241, 0.86);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.sectionnav__list {
list-style: none;
display: flex;
gap: 0.2rem;
margin: 0 auto;
padding: 0 max(1rem, calc((100vw - 1080px) / 2));
width: 100%;
height: var(--nav-h);
align-items: center;
overflow-x: auto;
scrollbar-width: none;
}
.sectionnav__list::-webkit-scrollbar {
display: none;
}
.sectionnav__link {
position: relative;
text-decoration: none;
color: var(--muted);
font-weight: 600;
font-size: 0.92rem;
padding: 0.45rem 0.85rem;
border-radius: 999px;
white-space: nowrap;
transition:
color 0.18s ease,
background 0.18s ease;
}
.sectionnav__link:hover {
color: var(--ink);
background: var(--sand-soft);
}
.sectionnav__link.is-active {
color: var(--teal-deep);
background: rgba(31, 138, 138, 0.12);
}
/* ===================== SECTIONS ===================== */
.guide {
width: min(1080px, 92vw);
margin: 0 auto;
}
.section {
padding: clamp(2.6rem, 7vw, 4.6rem) 0;
border-bottom: 1px solid var(--line);
scroll-margin-top: calc(var(--nav-h) + 12px);
}
.section:focus {
outline: none;
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.2em;
font-size: 0.72rem;
font-weight: 700;
color: var(--coral);
margin-bottom: 0.5rem;
}
.section__title {
font-size: clamp(1.7rem, 4.4vw, 2.7rem);
max-width: 22ch;
}
.section__intro {
margin-top: 0.9rem;
max-width: 58ch;
color: var(--muted);
}
/* Overview */
.overview {
margin-top: 1.6rem;
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: clamp(1.4rem, 4vw, 3rem);
align-items: start;
}
.overview__lede p + p {
margin-top: 1rem;
}
.overview__lede {
font-size: 1.05rem;
color: #34302a;
}
.quickfacts {
list-style: none;
margin: 0;
padding: 1.2rem;
background: var(--card);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
}
.quickfacts li {
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 0.55rem 0;
border-bottom: 1px dashed var(--line);
}
.quickfacts li:last-child {
border-bottom: 0;
}
.qf__k {
color: var(--muted);
font-size: 0.9rem;
}
.qf__v {
font-weight: 600;
text-align: right;
}
/* ===================== POI CARDS ===================== */
.poi-grid {
margin-top: 1.8rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(255px, 1fr));
gap: 1.2rem;
}
.poi {
position: relative;
display: flex;
flex-direction: column;
background: var(--card);
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.poi:hover {
transform: translateY(-4px);
box-shadow: var(--shadow);
}
.poi__media {
position: relative;
height: 138px;
background: var(--scene, linear-gradient(135deg, #1f8a8a, #14666a));
}
.poi__media::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 45%, rgba(36, 31, 26, 0.32));
}
.poi__badge {
position: absolute;
left: 12px;
top: 12px;
z-index: 2;
background: rgba(251, 247, 241, 0.95);
color: var(--ink);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.04em;
padding: 0.28rem 0.6rem;
border-radius: 999px;
}
.poi__price {
position: absolute;
right: 12px;
bottom: 12px;
z-index: 2;
color: #fff5e8;
font-weight: 700;
font-size: 0.92rem;
letter-spacing: 0.06em;
}
.poi__body {
padding: 0.95rem 1.05rem 1.1rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
flex: 1;
}
.poi__name {
font-size: 1.15rem;
line-height: 1.2;
}
.poi__meta {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
color: var(--muted);
}
.poi__rating {
color: var(--coral-deep);
font-weight: 700;
}
.poi__dot {
opacity: 0.5;
}
.poi__desc {
font-size: 0.92rem;
color: #4a443d;
flex: 1;
}
.poi__foot {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 0.4rem;
}
.poi__tag {
font-size: 0.72rem;
font-weight: 600;
color: var(--teal-deep);
background: rgba(31, 138, 138, 0.1);
padding: 0.22rem 0.55rem;
border-radius: 999px;
}
.poi__save {
background: none;
border: 1px solid var(--line-strong);
width: 38px;
height: 38px;
border-radius: 50%;
cursor: pointer;
font-size: 1.05rem;
color: var(--muted);
display: grid;
place-items: center;
transition:
color 0.18s ease,
border-color 0.18s ease,
background 0.18s ease,
transform 0.15s ease;
}
.poi__save:hover {
border-color: var(--coral);
color: var(--coral);
}
.poi__save:active {
transform: scale(0.92);
}
.poi__save.is-saved {
background: var(--coral);
border-color: var(--coral);
color: #fff;
}
/* ===================== MAP ===================== */
.mapwrap {
margin-top: 1.8rem;
display: grid;
grid-template-columns: 1.7fr 1fr;
gap: clamp(1.2rem, 3vw, 2rem);
align-items: start;
}
.mapcanvas {
position: relative;
border-radius: var(--radius);
overflow: hidden;
border: 1px solid var(--line);
box-shadow: var(--shadow-sm);
background: #bfe9e6;
}
.map {
width: 100%;
height: auto;
}
.map__pins {
position: absolute;
inset: 0;
}
.pin {
position: absolute;
transform: translate(-50%, -100%);
width: 30px;
height: 30px;
padding: 0;
border: 0;
background: none;
cursor: pointer;
filter: drop-shadow(0 4px 5px rgba(36, 31, 26, 0.35));
transition: transform 0.18s ease;
}
.pin::before {
content: "";
position: absolute;
inset: 0;
background: var(--teal);
border: 2px solid #fff5e8;
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
}
.pin::after {
content: "";
position: absolute;
left: 50%;
top: 38%;
width: 8px;
height: 8px;
background: #fff5e8;
border-radius: 50%;
transform: translate(-50%, -50%);
}
.pin:hover,
.pin:focus-visible {
transform: translate(-50%, -106%) scale(1.12);
}
.pin.is-active::before {
background: var(--coral);
}
.pin__n {
position: absolute;
left: 50%;
top: 30%;
transform: translate(-50%, -50%);
color: #fff5e8;
font-size: 0.72rem;
font-weight: 800;
z-index: 2;
}
.maplegend {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.legitem {
display: flex;
gap: 0.7rem;
align-items: center;
width: 100%;
text-align: left;
background: var(--card);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0.6rem 0.7rem;
cursor: pointer;
font-family: var(--sans);
transition:
border-color 0.18s ease,
background 0.18s ease,
transform 0.15s ease;
}
.legitem:hover {
transform: translateX(2px);
}
.legitem.is-active {
border-color: var(--coral);
background: rgba(232, 98, 63, 0.07);
}
.legitem__num {
flex: none;
width: 26px;
height: 26px;
border-radius: 50%;
background: var(--teal);
color: #fff5e8;
font-weight: 700;
font-size: 0.82rem;
display: grid;
place-items: center;
}
.legitem.is-active .legitem__num {
background: var(--coral);
}
.legitem__txt {
display: flex;
flex-direction: column;
line-height: 1.25;
}
.legitem__name {
font-weight: 600;
font-size: 0.95rem;
}
.legitem__kind {
font-size: 0.78rem;
color: var(--muted);
}
/* ===================== GALLERY ===================== */
.gallery {
width: 100%;
padding: clamp(1.4rem, 4vw, 2.4rem) 0;
background: var(--ink);
}
.gallery__band {
display: flex;
gap: 0.5rem;
overflow-x: auto;
padding: 0 max(1rem, calc((100vw - 1080px) / 2));
scroll-snap-type: x mandatory;
scrollbar-width: thin;
scrollbar-color: var(--coral) transparent;
}
.gframe {
flex: 0 0 auto;
width: clamp(180px, 28vw, 250px);
height: clamp(150px, 22vw, 200px);
border-radius: var(--radius-sm);
scroll-snap-align: start;
position: relative;
overflow: hidden;
border: 0;
cursor: pointer;
background: var(--scene);
transition: transform 0.2s ease;
}
.gframe:hover {
transform: scale(1.02);
}
.gframe__cap {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 1.4rem 0.7rem 0.6rem;
background: linear-gradient(transparent, rgba(36, 31, 26, 0.66));
color: #fff5e8;
font-size: 0.82rem;
font-weight: 600;
text-align: left;
}
/* ===================== TIPS ===================== */
.tips {
margin-top: 1.8rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1.1rem;
}
.tip {
background: var(--card);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1.2rem;
box-shadow: var(--shadow-sm);
}
.tip__icon {
font-size: 1.7rem;
}
.tip h3 {
margin: 0.6rem 0 0.3rem;
font-size: 1.15rem;
}
.tip p {
font-size: 0.92rem;
color: #4a443d;
}
/* ===================== FOOTER ===================== */
.foot {
width: min(1080px, 92vw);
margin: 0 auto;
padding: 2.4rem 0 3rem;
text-align: center;
}
.foot__brand {
font-family: var(--serif);
font-style: italic;
font-size: 1.3rem;
color: var(--teal-deep);
}
.foot__note {
margin-top: 0.4rem;
font-size: 0.84rem;
color: var(--muted);
}
/* ===================== TOAST ===================== */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 28px);
background: var(--ink);
color: #fff5e8;
padding: 0.7rem 1.2rem;
border-radius: 999px;
font-size: 0.92rem;
font-weight: 600;
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition:
opacity 0.25s ease,
transform 0.25s ease;
z-index: 80;
}
.toast.is-on {
opacity: 1;
transform: translate(-50%, 0);
}
/* ===================== RESPONSIVE ===================== */
@media (max-width: 880px) {
.overview,
.mapwrap {
grid-template-columns: 1fr;
}
.maplegend {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 560px) {
.facts {
grid-template-columns: 1fr 1fr;
}
.maplegend {
grid-template-columns: 1fr;
}
.hero__actions .btn {
flex: 1;
justify-content: center;
}
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
.palm {
animation: none;
}
* {
transition-duration: 0.01ms !important;
}
}(function () {
"use strict";
/* ---------------------------------------------------------------
* Data — fictional Isla Verde points of interest
* ------------------------------------------------------------- */
var POIS = {
see: [
{
name: "Crater Rim Trail",
badge: "Cloud forest",
meta: "Hike · 4h loop",
rating: "4.9",
price: "Free",
tag: "Best at dawn",
desc: "A misty switchback up to the old crater, ending at a lookout over the whole island.",
scene: "linear-gradient(150deg,#9cc28a,#3f7e4f 70%)",
},
{
name: "Porto Lima Harbour",
badge: "Old town",
meta: "Walk · half day",
rating: "4.7",
price: "Free",
tag: "Pastel houses",
desc: "Cobbled lanes ring a horseshoe bay lined with fishing boats and citrus-bright facades.",
scene: "linear-gradient(160deg,#f6a17a,#e8623f 75%)",
},
{
name: "Verdean Coffee Farm",
badge: "Tour",
meta: "1.5h · book ahead",
rating: "4.8",
price: "VR 90",
tag: "Tastings",
desc: "Terraced hillsides where you pick, roast, and cup the island's prized high-grown beans.",
scene: "linear-gradient(150deg,#cdb893,#8a6b3f 80%)",
},
{
name: "Glass Cove Snorkel",
badge: "Beach",
meta: "Half day",
rating: "4.6",
price: "VR 120",
tag: "Reef",
desc: "A sheltered cove with water so clear the reef looks suspended in air.",
scene: "linear-gradient(160deg,#7fd0cf,#1f6d7a 80%)",
},
{
name: "Lantern Night Market",
badge: "Evening",
meta: "Fri–Sun · sunset",
rating: "4.8",
price: "Free",
tag: "Street food",
desc: "Strung lanterns, grilled skewers, and live fado spill across the old customs square.",
scene: "linear-gradient(150deg,#5a4a6e,#e8623f 95%)",
},
],
eat: [
{
name: "Casa do Mar",
badge: "Seafood",
meta: "Harbourfront · $$",
rating: "4.9",
price: "VR 240",
tag: "Book ahead",
desc: "The island's grilled catch of the day, served on a terrace inches from the tide.",
scene: "linear-gradient(160deg,#1f8a8a,#14666a 85%)",
},
{
name: "Tia Marisol's",
badge: "Home cooking",
meta: "Old town · $",
rating: "4.8",
price: "VR 110",
tag: "Cash only",
desc: "Eight tables, no menu — just whatever the matriarch simmered that morning.",
scene: "linear-gradient(150deg,#e8a85c,#c84a2a 90%)",
},
{
name: "Café Cumulus",
badge: "Coffee",
meta: "Hilltop · $",
rating: "4.7",
price: "VR 45",
tag: "Views",
desc: "Single-origin pour-overs above the clouds, with banana bread still warm from the oven.",
scene: "linear-gradient(160deg,#cdb893,#6b5230 90%)",
},
{
name: "Verde Verde Bar",
badge: "Drinks",
meta: "Marina · $$",
rating: "4.6",
price: "VR 70",
tag: "Sunset spot",
desc: "Tamarind sours and grilled corn while the sun melts into the bay.",
scene: "linear-gradient(150deg,#f6a17a,#5a4a6e 95%)",
},
],
stay: [
{
name: "Pousada Horizonte",
badge: "Boutique",
meta: "Sea view · 12 rooms",
rating: "4.9",
price: "VR 980/nt",
tag: "Adults only",
desc: "Whitewashed suites tumbling down the cliff, each with a hammock facing the horizon.",
scene: "linear-gradient(160deg,#7fd0cf,#1f8a8a 85%)",
},
{
name: "Forest Canopy Lodge",
badge: "Eco",
meta: "Cloud forest · cabins",
rating: "4.7",
price: "VR 640/nt",
tag: "Off-grid",
desc: "Timber cabins on stilts among the ferns, woken each morning by birdsong, not alarms.",
scene: "linear-gradient(150deg,#9cc28a,#3f7e4f 80%)",
},
{
name: "Harbour House Hostel",
badge: "Budget",
meta: "Old town · dorms",
rating: "4.5",
price: "VR 120/nt",
tag: "Sociable",
desc: "A bright, tiled townhouse with a rooftop and a wall of trip tips left by past wanderers.",
scene: "linear-gradient(150deg,#e8a85c,#e8623f 90%)",
},
],
};
/* Map locations — percentage coordinates over the SVG canvas */
var MAP_POINTS = [
{ n: 1, name: "Porto Lima", kind: "Harbour town", x: 30, y: 76 },
{ n: 2, name: "Crater Rim", kind: "Cloud forest trail", x: 50, y: 42 },
{ n: 3, name: "Glass Cove", kind: "Snorkel beach", x: 70, y: 47 },
{ n: 4, name: "Coffee Farm", kind: "Plantation tour", x: 43, y: 58 },
{ n: 5, name: "Lantern Market", kind: "Night market", x: 35, y: 67 },
];
var GALLERY = [
{ cap: "Sunrise over the crater rim", scene: "linear-gradient(160deg,#ffd9a8,#e8623f)" },
{ cap: "Porto Lima at golden hour", scene: "linear-gradient(160deg,#f6a17a,#c84a2a)" },
{ cap: "The reef at Glass Cove", scene: "linear-gradient(160deg,#7fd0cf,#14666a)" },
{ cap: "Cloud forest ferns", scene: "linear-gradient(160deg,#9cc28a,#3f7e4f)" },
{ cap: "Lanterns in the old square", scene: "linear-gradient(160deg,#5a4a6e,#e8623f)" },
{ cap: "Terraced coffee hills", scene: "linear-gradient(160deg,#cdb893,#6b5230)" },
];
/* ---------------------------------------------------------------
* Tiny helpers
* ------------------------------------------------------------- */
function el(tag, attrs, html) {
var node = document.createElement(tag);
if (attrs) {
Object.keys(attrs).forEach(function (k) {
node.setAttribute(k, attrs[k]);
});
}
if (html != null) node.innerHTML = html;
return node;
}
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("is-on");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-on");
}, 2200);
}
/* ---------------------------------------------------------------
* Render POI cards
* ------------------------------------------------------------- */
function makeCard(p) {
var card = el("article", { class: "poi" });
card.style.setProperty("--scene", p.scene);
card.innerHTML =
'<div class="poi__media" style="--scene:' +
p.scene +
'">' +
'<span class="poi__badge">' +
p.badge +
"</span>" +
'<span class="poi__price">' +
p.price +
"</span>" +
"</div>" +
'<div class="poi__body">' +
'<h3 class="poi__name">' +
p.name +
"</h3>" +
'<p class="poi__meta"><span class="poi__rating">★ ' +
p.rating +
'</span><span class="poi__dot">·</span>' +
p.meta +
"</p>" +
'<p class="poi__desc">' +
p.desc +
"</p>" +
'<div class="poi__foot">' +
'<span class="poi__tag">' +
p.tag +
"</span>" +
'<button class="poi__save" type="button" aria-pressed="false" aria-label="Add ' +
p.name +
' to trip" title="Add to trip">+</button>' +
"</div>" +
"</div>";
// give the media a real background
card.querySelector(".poi__media").style.background = p.scene;
return card;
}
Object.keys(POIS).forEach(function (key) {
var grid = document.querySelector('[data-grid="' + key + '"]');
if (!grid) return;
POIS[key].forEach(function (p) {
grid.appendChild(makeCard(p));
});
});
/* Trip save toggles (event delegation) */
var tripCount = 0;
document.addEventListener("click", function (e) {
var btn = e.target.closest(".poi__save");
if (!btn) return;
var saved = btn.classList.toggle("is-saved");
btn.setAttribute("aria-pressed", String(saved));
btn.textContent = saved ? "✓" : "+";
var name = btn.getAttribute("aria-label").replace(/^Add | to trip$/g, "");
tripCount += saved ? 1 : -1;
if (tripCount < 0) tripCount = 0;
toast(
saved
? "Added " + name + " to your trip (" + tripCount + ")"
: "Removed " + name + " from your trip"
);
});
/* ---------------------------------------------------------------
* Map pins + legend (kept in sync)
* ------------------------------------------------------------- */
var pinHost = document.getElementById("mapPins");
var legendHost = document.getElementById("mapLegend");
function setActivePoint(n) {
document.querySelectorAll(".pin").forEach(function (p) {
p.classList.toggle("is-active", p.dataset.n === String(n));
});
document.querySelectorAll(".legitem").forEach(function (l) {
var on = l.dataset.n === String(n);
l.classList.toggle("is-active", on);
l.setAttribute("aria-pressed", String(on));
});
}
MAP_POINTS.forEach(function (pt) {
var pin = el("button", {
class: "pin",
type: "button",
"data-n": pt.n,
"aria-label": pt.name + " — " + pt.kind,
title: pt.name,
});
pin.style.left = pt.x + "%";
pin.style.top = pt.y + "%";
pin.innerHTML = '<span class="pin__n">' + pt.n + "</span>";
pin.addEventListener("click", function () {
setActivePoint(pt.n);
toast(pt.name + " · " + pt.kind);
});
if (pinHost) pinHost.appendChild(pin);
var item = el("button", {
class: "legitem",
type: "button",
"data-n": pt.n,
"aria-pressed": "false",
});
item.innerHTML =
'<span class="legitem__num">' +
pt.n +
"</span>" +
'<span class="legitem__txt"><span class="legitem__name">' +
pt.name +
'</span><span class="legitem__kind">' +
pt.kind +
"</span></span>";
item.addEventListener("click", function () {
setActivePoint(pt.n);
toast(pt.name + " · " + pt.kind);
});
if (legendHost) legendHost.appendChild(item);
});
/* ---------------------------------------------------------------
* Gallery band
* ------------------------------------------------------------- */
var galleryHost = document.getElementById("galleryBand");
if (galleryHost) {
GALLERY.forEach(function (g) {
var frame = el("button", {
class: "gframe",
type: "button",
"aria-label": "Photo: " + g.cap,
});
frame.style.setProperty("--scene", g.scene);
frame.style.background = g.scene;
frame.innerHTML = '<span class="gframe__cap">' + g.cap + "</span>";
frame.addEventListener("click", function () {
toast("📷 " + g.cap);
});
galleryHost.appendChild(frame);
});
}
/* ---------------------------------------------------------------
* Save-guide hero button
* ------------------------------------------------------------- */
var saveBtn = document.getElementById("saveBtn");
if (saveBtn) {
saveBtn.addEventListener("click", function () {
var saved = saveBtn.classList.toggle("is-saved");
saveBtn.setAttribute("aria-pressed", String(saved));
saveBtn.querySelector(".btn__label").textContent = saved
? "Guide saved"
: "Save guide";
toast(
saved
? "Isla Verde saved to your guides"
: "Removed from your saved guides"
);
});
}
/* ---------------------------------------------------------------
* Scrollspy + smooth-scroll for the sticky nav
* ------------------------------------------------------------- */
var navLinks = Array.prototype.slice.call(
document.querySelectorAll(".sectionnav__link")
);
var spyMap = {};
navLinks.forEach(function (link) {
var id = link.getAttribute("data-spy");
var section = document.getElementById(id);
if (section) spyMap[id] = link;
});
function activateLink(id) {
navLinks.forEach(function (l) {
l.classList.toggle("is-active", l.getAttribute("data-spy") === id);
});
}
if ("IntersectionObserver" in window) {
var observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
activateLink(entry.target.id);
}
});
},
{ rootMargin: "-45% 0px -50% 0px", threshold: 0 }
);
Object.keys(spyMap).forEach(function (id) {
var section = document.getElementById(id);
if (section) observer.observe(section);
});
}
/* Smooth scroll + move focus for keyboard users */
navLinks.forEach(function (link) {
link.addEventListener("click", function (e) {
var id = link.getAttribute("data-spy");
var target = document.getElementById(id);
if (!target) return;
e.preventDefault();
activateLink(id);
target.scrollIntoView({ behavior: "smooth", block: "start" });
// move focus without re-scrolling (focus after scroll completes)
setTimeout(function () {
target.focus({ preventScroll: true });
}, 420);
history.replaceState(null, "", "#" + id);
});
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Travel — Destination Guide · Isla Verde</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:ital,opsz,wght@0,9..144,400;0,9..144,500;0,9..144,600;0,9..144,700;1,9..144,500&family=Work+Sans:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#overview">Skip to guide</a>
<!-- ========================= HERO ========================= -->
<header class="hero" role="banner">
<div class="hero__scene" aria-hidden="true">
<svg
class="hero__sky"
viewBox="0 0 1200 520"
preserveAspectRatio="xMidYMid slice"
role="presentation"
>
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffd9a8" />
<stop offset="0.45" stop-color="#f6a17a" />
<stop offset="1" stop-color="#e8623f" />
</linearGradient>
<linearGradient id="sea" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#2aa3a3" />
<stop offset="1" stop-color="#1f6d7a" />
</linearGradient>
</defs>
<rect width="1200" height="520" fill="url(#sky)" />
<circle cx="880" cy="150" r="66" fill="#fff3da" opacity="0.92" />
<circle cx="880" cy="150" r="96" fill="#fff3da" opacity="0.18" />
<!-- far ridge -->
<path d="M0 300 L180 220 L340 285 L520 195 L700 280 L900 210 L1100 285 L1200 250 L1200 520 L0 520 Z" fill="#c9684c" opacity="0.55" />
<!-- mid ridge -->
<path d="M0 350 L160 300 L320 360 L500 290 L660 355 L860 300 L1040 360 L1200 320 L1200 520 L0 520 Z" fill="#a64a36" opacity="0.7" />
<!-- sea -->
<rect y="380" width="1200" height="140" fill="url(#sea)" />
<path d="M0 380 L1200 380 L1200 420 Q900 405 600 420 T0 420 Z" fill="#7fd0cf" opacity="0.35" />
<!-- sun glints -->
<g fill="#fff3da" opacity="0.5">
<ellipse cx="860" cy="400" rx="40" ry="4" />
<ellipse cx="840" cy="430" rx="70" ry="5" />
<ellipse cx="880" cy="460" rx="50" ry="4" />
</g>
<!-- palms -->
<g class="palm" transform="translate(120 360)">
<rect x="-4" y="0" width="8" height="120" rx="4" fill="#3c2a1f" />
<path d="M0 0 C-50 -20 -80 -10 -110 10 C-70 -8 -36 -6 0 4 Z" fill="#1f6d3a" />
<path d="M0 0 C50 -20 80 -10 110 10 C70 -8 36 -6 0 4 Z" fill="#26824a" />
<path d="M0 0 C-20 -50 -10 -80 12 -104 C-6 -64 -6 -30 4 0 Z" fill="#2f9356" />
</g>
</svg>
<span class="hero__grain"></span>
</div>
<div class="hero__inner">
<p class="hero__kicker">The Atlas Wanderer · Coastal Issue</p>
<h1 class="hero__title">Isla Verde</h1>
<p class="hero__sub">
A jade-green volcanic island where cobbled harbour towns meet cloud
forest trails and the slowest, sweetest afternoons of your life.
</p>
<dl class="facts">
<div class="fact">
<dt>Best time</dt>
<dd>Apr–Jun</dd>
</div>
<div class="fact">
<dt>Currency</dt>
<dd>Verdean real (VR)</dd>
</div>
<div class="fact">
<dt>Language</dt>
<dd>Verdean Creole</dd>
</div>
<div class="fact">
<dt>Flight time</dt>
<dd>~5h from hub</dd>
</div>
</dl>
<div class="hero__actions">
<button
id="saveBtn"
class="btn btn--save"
type="button"
aria-pressed="false"
>
<span class="btn__icon" aria-hidden="true">♥</span>
<span class="btn__label">Save guide</span>
</button>
<a class="btn btn--ghost" href="#map">View map</a>
</div>
</div>
</header>
<!-- ========================= STICKY NAV ========================= -->
<nav class="sectionnav" aria-label="Guide sections">
<ul class="sectionnav__list">
<li><a class="sectionnav__link is-active" href="#overview" data-spy="overview">Overview</a></li>
<li><a class="sectionnav__link" href="#see" data-spy="see">See</a></li>
<li><a class="sectionnav__link" href="#eat" data-spy="eat">Eat</a></li>
<li><a class="sectionnav__link" href="#stay" data-spy="stay">Stay</a></li>
<li><a class="sectionnav__link" href="#map" data-spy="map">Map</a></li>
<li><a class="sectionnav__link" href="#tips" data-spy="tips">Tips</a></li>
</ul>
</nav>
<main class="guide">
<!-- ========================= OVERVIEW ========================= -->
<section id="overview" class="section" aria-labelledby="overview-h" tabindex="-1">
<p class="eyebrow">Overview</p>
<h2 id="overview-h" class="section__title">Why Isla Verde</h2>
<div class="overview">
<div class="overview__lede">
<p>
Isla Verde rises straight out of the Coral Sea in folds of
terraced green. Its capital, <strong>Porto Lima</strong>, wraps a
horseshoe harbour in pastel houses; beyond it, switchback roads
climb past coffee farms into a cloud forest that keeps the whole
island impossibly lush.
</p>
<p>
Days here run on island time — long market mornings, a
two-hour lunch, and a sunset everyone stops to watch. This guide
walks you through the highlights, the cooking you cannot miss, and
where to lay your head.
</p>
</div>
<ul class="quickfacts" aria-label="At a glance">
<li><span class="qf__k">Vibe</span><span class="qf__v">Slow & salty</span></li>
<li><span class="qf__k">Getting around</span><span class="qf__v">Ferries & mopeds</span></li>
<li><span class="qf__k">Plug</span><span class="qf__v">Type C, 230V</span></li>
<li><span class="qf__k">Tipping</span><span class="qf__v">Round up, ~5%</span></li>
<li><span class="qf__k">Stay</span><span class="qf__v">4–6 nights</span></li>
</ul>
</div>
</section>
<!-- ========================= SEE ========================= -->
<section id="see" class="section" aria-labelledby="see-h" tabindex="-1">
<p class="eyebrow">See & do</p>
<h2 id="see-h" class="section__title">Five things worth the climb</h2>
<div class="poi-grid" data-grid="see"></div>
</section>
<!-- ========================= EAT ========================= -->
<section id="eat" class="section" aria-labelledby="eat-h" tabindex="-1">
<p class="eyebrow">Eat & drink</p>
<h2 id="eat-h" class="section__title">Where the island eats</h2>
<div class="poi-grid" data-grid="eat"></div>
</section>
<!-- ========================= STAY ========================= -->
<section id="stay" class="section" aria-labelledby="stay-h" tabindex="-1">
<p class="eyebrow">Stay</p>
<h2 id="stay-h" class="section__title">Rooms with a horizon</h2>
<div class="poi-grid" data-grid="stay"></div>
</section>
<!-- ========================= MAP ========================= -->
<section id="map" class="section" aria-labelledby="map-h" tabindex="-1">
<p class="eyebrow">Orientation</p>
<h2 id="map-h" class="section__title">Find your way</h2>
<p class="section__intro">
Tap a pin to highlight it — the harbour town clusters along the
south coast, with the cloud forest and crater trail up north.
</p>
<div class="mapwrap">
<div class="mapcanvas" role="group" aria-label="Map of Isla Verde">
<svg viewBox="0 0 600 440" class="map" role="presentation" aria-hidden="true">
<defs>
<radialGradient id="water" cx="50%" cy="40%" r="80%">
<stop offset="0" stop-color="#bfe9e6" />
<stop offset="1" stop-color="#8fcfca" />
</radialGradient>
<linearGradient id="landg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#e7d8c3" />
<stop offset="1" stop-color="#cdb893" />
</linearGradient>
</defs>
<rect width="600" height="440" fill="url(#water)" />
<!-- island landmass -->
<path
d="M150 120 C210 70 330 70 400 110 C470 150 510 220 470 300 C440 360 360 400 280 390 C190 380 120 330 100 260 C85 200 110 160 150 120 Z"
fill="url(#landg)"
stroke="#b79a6e"
stroke-width="3"
/>
<!-- forest blob -->
<path
d="M250 130 C320 120 380 150 380 200 C380 250 320 270 270 255 C220 240 210 160 250 130 Z"
fill="#9cc28a"
opacity="0.7"
/>
<!-- crater -->
<circle cx="300" cy="185" r="22" fill="#7a5a3a" opacity="0.6" />
<circle cx="300" cy="185" r="10" fill="#5a3f28" opacity="0.7" />
<!-- route -->
<path
id="route"
d="M180 330 Q250 300 300 280 Q360 250 410 200"
fill="none"
stroke="#e8623f"
stroke-width="3.5"
stroke-dasharray="9 8"
stroke-linecap="round"
opacity="0.85"
/>
<!-- compass -->
<g transform="translate(520 60)" opacity="0.85">
<circle r="20" fill="#fbf7f1" stroke="#b79a6e" />
<path d="M0 -14 L5 0 L0 14 L-5 0 Z" fill="#e8623f" />
<text x="0" y="-24" text-anchor="middle" font-size="11" fill="#6b6259">N</text>
</g>
</svg>
<div class="map__pins" id="mapPins"></div>
</div>
<ul class="maplegend" id="mapLegend" aria-label="Map locations"></ul>
</div>
</section>
<!-- ========================= GALLERY ========================= -->
<section class="gallery" aria-label="Photo gallery">
<div class="gallery__band" id="galleryBand"></div>
</section>
<!-- ========================= TIPS ========================= -->
<section id="tips" class="section" aria-labelledby="tips-h" tabindex="-1">
<p class="eyebrow">Know before you go</p>
<h2 id="tips-h" class="section__title">Local tips</h2>
<div class="tips">
<article class="tip">
<span class="tip__icon" aria-hidden="true">🌦️</span>
<h3>Pack for two climates</h3>
<p>Coast is hot and humid; the cloud forest is 10°C cooler and wet. A light shell earns its place.</p>
</article>
<article class="tip">
<span class="tip__icon" aria-hidden="true">⛴️</span>
<h3>Ferries beat roads</h3>
<p>The coastal ferry loop is faster and prettier than the switchback road. Buy a 3-day hopper pass.</p>
</article>
<article class="tip">
<span class="tip__icon" aria-hidden="true">💵</span>
<h3>Carry small cash</h3>
<p>Markets and ferries are cash-first. ATMs cluster in Porto Lima, so stock up before heading north.</p>
</article>
<article class="tip">
<span class="tip__icon" aria-hidden="true">🌅</span>
<h3>Sunset is sacred</h3>
<p>Book dinner for after dusk — locals down tools at golden hour, and so should you.</p>
</article>
</div>
</section>
</main>
<footer class="foot" role="contentinfo">
<p class="foot__brand">The Atlas Wanderer</p>
<p class="foot__note">
Illustrative travel guide — Isla Verde, its towns, prices, and map
are entirely fictional.
</p>
</footer>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Destination Guide
An editorial, full-page travel guide for the invented island of Isla Verde. It opens on a full-bleed hero built entirely from layered inline SVG — a sunset sky, volcanic ridges, a glinting sea, and a gently swaying palm — over which the title, a wanderlust lede, and a four-up fact strip (best time, currency, language, flight time) sit in a serif display face. A Save guide button toggles between saved and unsaved states with a confirming toast.
Below, a sticky section nav follows you down the page. An IntersectionObserver scrollspy keeps the active tab in sync as you scroll through Overview, See, Eat, Stay, Map, and Tips, and clicking a tab smooth-scrolls to the section and moves keyboard focus there. Each themed section renders POI cards from data, complete with gradient “photography”, best-time and category badges, star ratings, price tiers, and an add to trip affordance that tallies a running count in the toast.
The Map section is first-class: a CSS-and-SVG island mockup with a dashed route line and a compass rose, overlaid with numbered pins that stay in sync with a clickable legend list — select either and both highlight together. A dark, horizontally scrolling gallery band and a grid of practical local tips round out the guide. The layout collapses gracefully to a single column down to 360px, respects reduced-motion preferences, and keeps every control keyboard-usable with visible focus rings.
Illustrative travel UI only — fictional destinations, prices, and maps.