Travel — Day-by-day Itinerary
An editorial day-by-day trip itinerary for a fictional five-day Amalfi Coast loop. A horizontal day selector swaps a vertical timeline of stops — each a POI card with time, rating, price tier, best-time badge and travel leg between them. Switching days redraws an inline SVG route map with numbered pins. Check off completed stops, watch the day progress ring fill, collapse or expand notes, and save or print the whole plan.
MCP
Code
:root {
--bg: #fbf7f1;
--surface: #ffffff;
--ink: #241f1a;
--muted: #6b6259;
--teal: #1f8a8a;
--teal-deep: #146868;
--coral: #e8623f;
--coral-deep: #c84a2a;
--sand: #e7d8c3;
--sand-soft: #f3ead9;
--gold: #c9963f;
--line: rgba(36, 31, 26, 0.12);
--line-soft: rgba(36, 31, 26, 0.07);
--done: #2f9e6f;
--shadow-sm: 0 1px 2px rgba(36, 31, 26, 0.06), 0 2px 8px rgba(36, 31, 26, 0.05);
--shadow-md: 0 6px 24px rgba(36, 31, 26, 0.1);
--radius: 16px;
--radius-sm: 11px;
--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 {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: var(--sans);
font-size: 16px;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(900px 480px at 88% -8%, rgba(31, 138, 138, 0.08), transparent 60%),
radial-gradient(820px 460px at -6% 4%, rgba(232, 98, 63, 0.07), transparent 60%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
h1,
h2,
h3,
h4 {
margin: 0;
font-family: var(--serif);
font-weight: 600;
line-height: 1.12;
letter-spacing: -0.01em;
}
p {
margin: 0;
}
ul,
ol {
margin: 0;
padding: 0;
list-style: none;
}
button {
font-family: inherit;
font-size: inherit;
cursor: pointer;
}
:focus-visible {
outline: 3px solid var(--teal);
outline-offset: 2px;
border-radius: 6px;
}
.skip-link {
position: absolute;
left: 12px;
top: -48px;
z-index: 50;
background: var(--ink);
color: #fff;
padding: 9px 14px;
border-radius: 9px;
text-decoration: none;
transition: top 0.18s ease;
}
.skip-link:focus {
top: 12px;
}
/* ---------- Top bar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 30;
background: rgba(251, 247, 241, 0.86);
backdrop-filter: saturate(1.4) blur(10px);
border-bottom: 1px solid var(--line);
}
.topbar__inner {
max-width: 1120px;
margin: 0 auto;
padding: 13px 22px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
font-family: var(--serif);
font-weight: 600;
font-size: 1.18rem;
letter-spacing: -0.01em;
}
.brand__mark {
display: inline-grid;
place-items: center;
width: 36px;
height: 36px;
border-radius: 11px;
color: #fff;
background: linear-gradient(135deg, var(--teal), var(--teal-deep));
box-shadow: var(--shadow-sm);
}
.brand__text em {
font-style: normal;
color: var(--coral);
}
.topbar__actions {
display: flex;
gap: 10px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
border: 1px solid transparent;
border-radius: 999px;
padding: 9px 16px;
font-weight: 600;
font-size: 0.9rem;
transition: transform 0.12s ease, background 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}
.btn:active {
transform: translateY(1px);
}
.btn--ghost {
background: transparent;
border-color: var(--line);
color: var(--ink);
}
.btn--ghost:hover {
background: var(--sand-soft);
border-color: var(--sand);
}
.btn--solid {
background: linear-gradient(135deg, var(--coral), var(--coral-deep));
color: #fff;
box-shadow: 0 4px 14px rgba(232, 98, 63, 0.32);
}
.btn--solid:hover {
box-shadow: 0 6px 20px rgba(232, 98, 63, 0.42);
}
.btn--solid[aria-pressed="true"] {
background: linear-gradient(135deg, var(--teal), var(--teal-deep));
box-shadow: 0 4px 14px rgba(31, 138, 138, 0.32);
}
.btn__heart {
font-size: 1rem;
line-height: 1;
}
/* ---------- Layout ---------- */
.wrap {
max-width: 1120px;
margin: 0 auto;
padding: 24px 22px 64px;
}
/* ---------- Hero ---------- */
.hero {
position: relative;
border-radius: 24px;
overflow: hidden;
background: var(--surface);
border: 1px solid var(--line);
box-shadow: var(--shadow-md);
}
.hero__scene {
position: absolute;
inset: 0;
z-index: 0;
}
.hero__svg {
width: 100%;
height: 100%;
display: block;
}
.hero__scene::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
100deg,
rgba(251, 247, 241, 0.97) 0%,
rgba(251, 247, 241, 0.92) 42%,
rgba(251, 247, 241, 0.32) 78%,
rgba(251, 247, 241, 0) 100%
);
}
.hero__body {
position: relative;
z-index: 1;
padding: 34px 32px 30px;
max-width: 640px;
}
.hero__eyebrow {
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 0.72rem;
font-weight: 700;
color: var(--teal-deep);
margin-bottom: 10px;
}
.hero h1 {
font-size: clamp(1.9rem, 4.6vw, 3rem);
margin-bottom: 12px;
}
.hero__lede {
color: var(--muted);
max-width: 52ch;
font-size: 1.02rem;
}
.hero__meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 22px 0 0;
}
.hero__meta div {
background: rgba(255, 255, 255, 0.75);
border: 1px solid var(--line);
border-radius: 12px;
padding: 9px 14px;
min-width: 104px;
}
.hero__meta dt {
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
font-weight: 600;
}
.hero__meta dd {
margin: 2px 0 0;
font-family: var(--serif);
font-size: 1.18rem;
font-weight: 600;
}
/* ---------- Day tabs ---------- */
.days {
margin: 26px 0 0;
}
.days__list {
display: flex;
gap: 10px;
overflow-x: auto;
padding: 6px 2px 12px;
scrollbar-width: thin;
scroll-snap-type: x proximity;
}
.day-tab {
flex: 0 0 auto;
scroll-snap-align: start;
display: flex;
flex-direction: column;
gap: 3px;
text-align: left;
min-width: 138px;
padding: 12px 16px;
border-radius: var(--radius-sm);
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink);
box-shadow: var(--shadow-sm);
transition: transform 0.14s ease, border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
}
.day-tab:hover {
transform: translateY(-2px);
border-color: var(--sand);
}
.day-tab__kicker {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.12em;
font-weight: 700;
color: var(--muted);
}
.day-tab__city {
font-family: var(--serif);
font-size: 1.06rem;
font-weight: 600;
}
.day-tab__meta {
font-size: 0.78rem;
color: var(--muted);
}
.day-tab[aria-selected="true"] {
background: linear-gradient(150deg, #2a3530, var(--ink));
border-color: var(--ink);
color: #fdf6ec;
box-shadow: var(--shadow-md);
}
.day-tab[aria-selected="true"] .day-tab__kicker {
color: var(--gold);
}
.day-tab[aria-selected="true"] .day-tab__meta {
color: rgba(253, 246, 236, 0.78);
}
.day-tab__bar {
margin-top: 6px;
height: 4px;
border-radius: 999px;
background: rgba(36, 31, 26, 0.12);
overflow: hidden;
}
.day-tab[aria-selected="true"] .day-tab__bar {
background: rgba(253, 246, 236, 0.22);
}
.day-tab__bar i {
display: block;
height: 100%;
width: 0;
border-radius: inherit;
background: var(--done);
transition: width 0.4s ease;
}
/* ---------- Board ---------- */
.board {
margin-top: 6px;
}
.day-panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
padding: 24px;
}
.day-panel:focus-visible {
outline-offset: 4px;
}
.day-panel__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding-bottom: 18px;
border-bottom: 1px solid var(--line-soft);
}
.day-panel__kicker {
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: 0.72rem;
font-weight: 700;
color: var(--coral);
}
.day-panel__title {
font-size: clamp(1.5rem, 3.2vw, 2rem);
margin: 4px 0 4px;
}
.day-panel__sub {
color: var(--muted);
font-size: 0.95rem;
}
.day-panel__progress {
display: flex;
align-items: center;
gap: 12px;
}
.day-panel__count {
font-size: 0.85rem;
font-weight: 600;
color: var(--muted);
}
.progress-ring {
position: relative;
display: inline-grid;
place-items: center;
width: 44px;
height: 44px;
}
.progress-ring svg {
transform: rotate(-90deg);
}
.progress-ring__bg {
fill: none;
stroke: var(--line);
stroke-width: 5;
}
.progress-ring__fg {
fill: none;
stroke: var(--done);
stroke-width: 5;
stroke-linecap: round;
stroke-dasharray: 113.1;
stroke-dashoffset: 113.1;
transition: stroke-dashoffset 0.5s ease;
}
.progress-ring__num {
position: absolute;
font-size: 0.66rem;
font-weight: 700;
color: var(--ink);
}
.day-panel__grid {
display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
gap: 28px;
margin-top: 22px;
align-items: start;
}
/* ---------- Timeline ---------- */
.timeline {
position: relative;
margin-left: 6px;
}
.timeline::before {
content: "";
position: absolute;
left: 62px;
top: 14px;
bottom: 14px;
width: 2px;
background: repeating-linear-gradient(
to bottom,
var(--sand) 0 7px,
transparent 7px 14px
);
}
.stop {
position: relative;
display: grid;
grid-template-columns: 50px 1fr;
gap: 14px;
padding: 0 0 18px;
}
.stop:last-child {
padding-bottom: 0;
}
.stop__time {
text-align: right;
padding-top: 16px;
font-weight: 700;
font-size: 0.8rem;
color: var(--muted);
white-space: nowrap;
}
.stop__node {
position: relative;
}
.stop__dot {
position: absolute;
left: 6px;
top: 18px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--surface);
border: 3px solid var(--coral);
z-index: 1;
transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.poi {
margin-left: 30px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--surface);
box-shadow: var(--shadow-sm);
overflow: hidden;
transition: border-color 0.2s ease, box-shadow 0.2s ease, opacity 0.25s ease;
}
.poi:hover {
border-color: var(--sand);
box-shadow: var(--shadow-md);
}
.poi__head {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 13px 14px;
}
.poi__icon {
flex: 0 0 auto;
width: 38px;
height: 38px;
display: grid;
place-items: center;
border-radius: 10px;
font-size: 1.15rem;
background: var(--sand-soft);
border: 1px solid var(--line-soft);
}
.poi__main {
flex: 1 1 auto;
min-width: 0;
}
.poi__top {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.poi__name {
font-family: var(--serif);
font-size: 1.06rem;
font-weight: 600;
margin: 0;
}
.poi__cat {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 700;
color: var(--teal-deep);
background: rgba(31, 138, 138, 0.12);
border-radius: 999px;
padding: 2px 8px;
}
.poi__row {
display: flex;
flex-wrap: wrap;
gap: 6px 12px;
margin-top: 6px;
font-size: 0.82rem;
color: var(--muted);
}
.poi__row span {
display: inline-flex;
align-items: center;
gap: 4px;
}
.poi__stars {
color: var(--gold);
letter-spacing: 1px;
}
.poi__controls {
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-end;
}
.icon-btn {
display: inline-grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 9px;
border: 1px solid var(--line);
background: var(--surface);
color: var(--muted);
transition: background 0.16s ease, color 0.16s ease, border-color 0.16s ease, transform 0.12s ease;
}
.icon-btn:hover {
background: var(--sand-soft);
color: var(--ink);
}
.icon-btn:active {
transform: scale(0.94);
}
.check-btn[aria-pressed="true"] {
background: var(--done);
border-color: var(--done);
color: #fff;
}
.check-btn svg {
width: 17px;
height: 17px;
}
.toggle-btn svg {
width: 16px;
height: 16px;
transition: transform 0.22s ease;
}
.poi__body {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.26s ease;
border-top: 1px solid transparent;
}
.poi.is-open .poi__body {
grid-template-rows: 1fr;
border-top-color: var(--line-soft);
}
.poi.is-open .toggle-btn svg {
transform: rotate(180deg);
}
.poi__body-inner {
overflow: hidden;
}
.poi__note {
padding: 12px 14px 14px;
font-size: 0.88rem;
color: var(--muted);
}
.poi__note strong {
color: var(--ink);
font-weight: 600;
}
/* done state */
.stop.is-done .stop__dot {
background: var(--done);
border-color: var(--done);
}
.stop.is-done .poi {
opacity: 0.72;
}
.stop.is-done .poi__name {
text-decoration: line-through;
text-decoration-color: var(--muted);
}
/* travel leg between stops */
.leg {
display: flex;
align-items: center;
gap: 8px;
margin: -8px 0 10px 80px;
font-size: 0.78rem;
font-weight: 600;
color: var(--teal-deep);
}
.leg__line {
flex: 0 0 26px;
height: 2px;
background: var(--sand);
border-radius: 2px;
}
.leg__mode {
font-size: 0.95rem;
}
/* ---------- Side column ---------- */
.day-side {
display: grid;
gap: 18px;
position: sticky;
top: 80px;
}
.map-card,
.summary-card {
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
box-shadow: var(--shadow-sm);
padding: 16px;
}
.map-card__title,
.summary-card__title {
font-size: 1.05rem;
margin-bottom: 10px;
}
.map {
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--line-soft);
aspect-ratio: 1 / 1;
}
.map__svg {
width: 100%;
height: 100%;
display: block;
}
.map__route {
stroke: var(--coral);
stroke-width: 3.5;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 5 8;
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.12));
}
.map__pin {
cursor: default;
}
.map__pin circle {
transition: r 0.18s ease;
}
.map__pin-num {
font: 700 11px var(--sans);
fill: #fff;
text-anchor: middle;
dominant-baseline: central;
pointer-events: none;
}
.map__pin.is-active circle.pin-body {
stroke: var(--ink);
stroke-width: 2.5;
}
.map-card__note {
margin-top: 8px;
font-size: 0.74rem;
color: var(--muted);
}
.summary-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 14px;
}
.summary-stats li {
background: var(--sand-soft);
border: 1px solid var(--line-soft);
border-radius: 10px;
padding: 8px 10px;
}
.summary-stats b {
display: block;
font-family: var(--serif);
font-size: 1.15rem;
font-weight: 600;
}
.summary-stats small {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
font-weight: 600;
}
.summary-card__sub {
font-family: var(--sans);
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
font-weight: 700;
margin: 0 0 8px;
}
.highlights li {
display: flex;
gap: 9px;
align-items: flex-start;
font-size: 0.88rem;
padding: 5px 0;
}
.highlights li::before {
content: "✦";
color: var(--coral);
font-size: 0.85rem;
line-height: 1.5;
}
/* ---------- Footer ---------- */
.footnote {
text-align: center;
color: var(--muted);
font-size: 0.82rem;
padding: 8px 22px 40px;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%) translateY(16px);
background: var(--ink);
color: #fdf6ec;
padding: 11px 18px;
border-radius: 999px;
font-size: 0.88rem;
font-weight: 600;
box-shadow: var(--shadow-md);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
z-index: 60;
max-width: calc(100vw - 32px);
text-align: center;
}
.toast.is-show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ---------- Responsive ---------- */
@media (max-width: 860px) {
.day-panel__grid {
grid-template-columns: 1fr;
}
.day-side {
position: static;
grid-template-columns: 1fr 1fr;
}
.map {
aspect-ratio: 16 / 11;
}
}
@media (max-width: 620px) {
.wrap {
padding: 18px 14px 52px;
}
.hero__body {
padding: 26px 20px 24px;
}
.hero__scene::after {
background: linear-gradient(
180deg,
rgba(251, 247, 241, 0.55) 0%,
rgba(251, 247, 241, 0.93) 58%,
rgba(251, 247, 241, 0.98) 100%
);
}
.day-panel {
padding: 18px 14px;
}
.day-side {
grid-template-columns: 1fr;
}
.stop {
grid-template-columns: 42px 1fr;
gap: 10px;
}
.timeline::before {
left: 52px;
}
.poi {
margin-left: 24px;
}
.leg {
margin-left: 60px;
}
.btn__label {
display: none;
}
.btn--solid {
padding: 9px 13px;
}
}
@media (max-width: 380px) {
.stop__time {
font-size: 0.72rem;
}
.summary-stats {
grid-template-columns: 1fr;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition-duration: 0.001ms !important;
animation-duration: 0.001ms !important;
}
}
/* ---------- Print ---------- */
@media print {
body {
background: #fff;
}
.topbar,
.days,
.poi__controls,
.footnote,
.toast,
.skip-link {
display: none !important;
}
.hero,
.day-panel,
.map-card,
.summary-card {
box-shadow: none;
border-color: #ccc;
}
.poi__body {
grid-template-rows: 1fr !important;
}
.day-panel__grid {
grid-template-columns: 1.5fr 1fr;
}
}(function () {
"use strict";
/* ---------------------------------------------------------------
* Fictional 5-day Amalfi Coast itinerary.
* Each stop carries a map point {x,y} in a 0..320 SVG viewBox.
* ------------------------------------------------------------- */
var TRIP = [
{
id: "d1",
label: "Day 1",
city: "Sorrento",
title: "Arrival in Sorrento",
sub: "Settle in, cliff-top sunset, first taste of limoncello.",
distance: "9 km",
walk: "3.1 km",
cost: "€120",
stops: [
{ time: "13:00", name: "Hotel Bellavista check-in", cat: "Stay", icon: "🛎", rating: 4, price: "€€", best: "Anytime", pt: { x: 70, y: 110 }, leg: null,
note: "Drop bags, grab the room with the sea balcony. <strong>Ask reception for the ferry timetable</strong> — schedules shift after October." },
{ time: "14:30", name: "Lunch at Trattoria da Emilia", cat: "Food", icon: "🍝", rating: 5, price: "€€", best: "Lunch", pt: { x: 120, y: 150 }, leg: { mode: "🚶", text: "8 min walk" },
note: "Order the gnocchi alla sorrentina. Cash-only; tables on the quay book out by 13:30 in summer." },
{ time: "17:00", name: "Villa Comunale Gardens", cat: "View", icon: "🌅", rating: 4, price: "Free", best: "Golden hour", pt: { x: 96, y: 196 }, leg: { mode: "🚶", text: "12 min walk" },
note: "Free terrace looking over the Bay of Naples toward Vesuvius. Arrive ~30 min before sunset for the bench." },
{ time: "20:00", name: "Limoncello tasting, Old Town", cat: "Drinks", icon: "🍋", rating: 4, price: "€", best: "Evening", pt: { x: 158, y: 168 }, leg: { mode: "🚶", text: "10 min walk" },
note: "Family cellar off Via San Cesareo. The crema di limone is the one to take home." }
]
},
{
id: "d2",
label: "Day 2",
city: "Positano",
title: "Ferry to Positano",
sub: "Vertical village, pebble beach, the famous staircase streets.",
distance: "31 km",
walk: "4.6 km",
cost: "€210",
stops: [
{ time: "09:15", name: "Morning ferry Sorrento → Positano", cat: "Transit", icon: "⛴", rating: 5, price: "€€", best: "Morning", pt: { x: 60, y: 90 }, leg: null,
note: "Sit on the <strong>left side</strong> for the coastline. Buy tickets the evening before to skip the queue." },
{ time: "10:30", name: "Spiaggia Grande beach", cat: "Beach", icon: "🏖", rating: 4, price: "€€", best: "Late morning", pt: { x: 132, y: 130 }, leg: { mode: "⛴", text: "45 min ferry" },
note: "Rent two loungers at the far end where it's quieter. Water shoes help on the pebbles." },
{ time: "13:30", name: "Lunch above the dome", cat: "Food", icon: "🍤", rating: 5, price: "€€€", best: "Lunch", pt: { x: 176, y: 168 }, leg: { mode: "🚶", text: "9 min climb" },
note: "Terrace tables overlook the majolica dome of Santa Maria Assunta. Try the spaghetti alle vongole." },
{ time: "16:00", name: "Path of the Gods (lower loop)", cat: "Hike", icon: "🥾", rating: 5, price: "Free", best: "Afternoon", pt: { x: 224, y: 120 }, leg: { mode: "🚌", text: "15 min bus" },
note: "Do the short Nocelle loop, not the full trail. Sturdy shoes; carry water — no fountains up top." },
{ time: "21:00", name: "Dinner on the staircase street", cat: "Food", icon: "🍷", rating: 4, price: "€€€", best: "Evening", pt: { x: 152, y: 196 }, leg: { mode: "🚶", text: "20 min descent" },
note: "Candle-lit steps of Via Pasitea. Reserve a railing table for the lights coming on across the cove." }
]
},
{
id: "d3",
label: "Day 3",
city: "Amalfi",
title: "Amalfi & Atrani",
sub: "Cathedral steps, paper museum, hidden fishing cove next door.",
distance: "26 km",
walk: "5.2 km",
cost: "€140",
stops: [
{ time: "09:00", name: "SITA coast bus to Amalfi", cat: "Transit", icon: "🚌", rating: 3, price: "€", best: "Early", pt: { x: 58, y: 80 }, leg: null,
note: "Window seat on the sea side. Validate your ticket on board or you'll be fined." },
{ time: "10:15", name: "Cathedral of St. Andrew", cat: "Culture", icon: "⛪", rating: 5, price: "€", best: "Morning", pt: { x: 118, y: 120 }, leg: { mode: "🚌", text: "50 min ride" },
note: "Climb the 62 striped steps, then the Cloister of Paradise. Shoulders covered to enter." },
{ time: "12:00", name: "Paper Museum (Museo della Carta)", cat: "Culture", icon: "📜", rating: 4, price: "€", best: "Midday", pt: { x: 156, y: 96 }, leg: { mode: "🚶", text: "11 min walk" },
note: "A working 13th-century mill in a valley grotto — cool and quiet on a hot day." },
{ time: "14:30", name: "Lunch in Atrani square", cat: "Food", icon: "🐟", rating: 5, price: "€€", best: "Lunch", pt: { x: 196, y: 158 }, leg: { mode: "🚶", text: "15 min coastal path" },
note: "Atrani is the tiny village around the headland — half the crowds, all the charm. Fresh anchovies." },
{ time: "17:30", name: "Marina di Praia swim", cat: "Beach", icon: "🌊", rating: 4, price: "Free", best: "Late day", pt: { x: 232, y: 200 }, leg: { mode: "🚌", text: "18 min bus" },
note: "A slim fjord-like inlet between cliffs. The water stays calm and deep blue late into the afternoon." }
]
},
{
id: "d4",
label: "Day 4",
city: "Ravello",
title: "Ravello in the clouds",
sub: "Garden terraces, infinite-view belvedere, an evening concert.",
distance: "22 km",
walk: "3.8 km",
cost: "€175",
stops: [
{ time: "09:30", name: "Drive up to Ravello", cat: "Transit", icon: "🚐", rating: 4, price: "€€", best: "Morning", pt: { x: 64, y: 96 }, leg: null,
note: "Hairpins climb 350 m above the coast. Pre-book a small-van transfer; parking up top is scarce." },
{ time: "10:30", name: "Villa Rufolo gardens", cat: "Garden", icon: "🌿", rating: 5, price: "€€", best: "Morning", pt: { x: 122, y: 132 }, leg: { mode: "🚐", text: "40 min drive" },
note: "The cliff-edge garden that inspired Wagner. The stage is set here for the summer concerts." },
{ time: "13:00", name: "Lunch with a valley view", cat: "Food", icon: "🍋", rating: 4, price: "€€€", best: "Lunch", pt: { x: 168, y: 110 }, leg: { mode: "🚶", text: "6 min walk" },
note: "Lemon-leaf wrapped provola and a glass of local Furore white. Tables face the Dragone valley." },
{ time: "15:30", name: "Villa Cimbrone — Terrace of Infinity", cat: "View", icon: "🗿", rating: 5, price: "€€", best: "Afternoon", pt: { x: 214, y: 150 }, leg: { mode: "🚶", text: "18 min walk" },
note: "Marble busts lining a balustrade over a 300 m drop to the sea. Easily the best view of the trip." },
{ time: "20:30", name: "Sunset concert at Villa Rufolo", cat: "Music", icon: "🎻", rating: 5, price: "€€€", best: "Evening", pt: { x: 130, y: 196 }, leg: { mode: "🚶", text: "12 min walk" },
note: "Chamber strings on the garden stage as the sky turns coral. Bring a light layer — it cools fast." }
]
},
{
id: "d5",
label: "Day 5",
city: "Sorrento",
title: "Slow return & departure",
sub: "Lemon-grove brunch, last market stroll, ferry home.",
distance: "98 km",
walk: "2.4 km",
cost: "€135",
stops: [
{ time: "09:00", name: "Lemon-grove brunch", cat: "Food", icon: "🍳", rating: 5, price: "€€", best: "Morning", pt: { x: 72, y: 104 }, leg: null,
note: "A family agriturismo terrace shaded by lemon trees. Ricotta, honey, and the last espresso." },
{ time: "11:00", name: "Sorrento market & souvenirs", cat: "Shop", icon: "🛍", rating: 4, price: "€", best: "Late morning", pt: { x: 136, y: 142 }, leg: { mode: "🚐", text: "55 min drive" },
note: "Inlaid wood (intarsio) and ceramics around Piazza Tasso. Haggle gently; most stalls take cards." },
{ time: "13:30", name: "Farewell lunch by the marina", cat: "Food", icon: "🦐", rating: 4, price: "€€", best: "Lunch", pt: { x: 178, y: 178 }, leg: { mode: "🚶", text: "10 min walk" },
note: "Grilled prawns and a final spritz at Marina Grande before the bags come out." },
{ time: "16:00", name: "Ferry / transfer to airport", cat: "Transit", icon: "✈", rating: 4, price: "€€", best: "Afternoon", pt: { x: 232, y: 210 }, leg: { mode: "⛴", text: "Ferry + shuttle" },
note: "Allow 3 hours door-to-gate. The fast ferry to Naples is the calmest way to start the journey home." }
]
}
];
/* ----------------------------- helpers ----------------------------- */
var $ = function (sel, root) { return (root || document).querySelector(sel); };
var SVGNS = "http://www.w3.org/2000/svg";
var toastEl = $("#toast");
var toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2200);
}
function stars(n) {
var full = "★★★★★".slice(0, n);
var empty = "☆☆☆☆☆".slice(0, 5 - n);
return full + empty;
}
function svgEl(name, attrs) {
var el = document.createElementNS(SVGNS, name);
for (var k in attrs) {
if (Object.prototype.hasOwnProperty.call(attrs, k)) {
el.setAttribute(k, attrs[k]);
}
}
return el;
}
/* completion state, keyed by stop id "d1-0" */
var done = {};
var activeIndex = 0;
function key(dayId, stopIdx) { return dayId + "-" + stopIdx; }
/* ----------------------------- build tabs ----------------------------- */
var tabsList = $("#dayTabs");
var tabEls = [];
TRIP.forEach(function (day, i) {
var li = document.createElement("li");
li.setAttribute("role", "presentation");
var btn = document.createElement("button");
btn.type = "button";
btn.className = "day-tab";
btn.id = "tab-" + day.id;
btn.setAttribute("role", "tab");
btn.setAttribute("aria-controls", "dayPanel");
btn.setAttribute("aria-selected", i === 0 ? "true" : "false");
btn.tabIndex = i === 0 ? 0 : -1;
btn.dataset.index = String(i);
btn.innerHTML =
'<span class="day-tab__kicker">' + day.label + "</span>" +
'<span class="day-tab__city">' + day.city + "</span>" +
'<span class="day-tab__meta">' + day.stops.length + " stops · " + day.distance + "</span>" +
'<span class="day-tab__bar"><i></i></span>';
btn.addEventListener("click", function () { selectDay(i, true); });
btn.addEventListener("keydown", onTabKey);
li.appendChild(btn);
tabsList.appendChild(li);
tabEls.push(btn);
});
function onTabKey(e) {
var idx = activeIndex;
var next = null;
if (e.key === "ArrowRight" || e.key === "ArrowDown") next = (idx + 1) % TRIP.length;
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") next = (idx - 1 + TRIP.length) % TRIP.length;
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = TRIP.length - 1;
if (next === null) return;
e.preventDefault();
selectDay(next, true);
tabEls[next].focus();
}
/* ----------------------------- render a day ----------------------------- */
var timelineEl = $("#timeline");
var kickerEl = $("#dayKicker");
var titleEl = $("#dayPanelTitle");
var subEl = $("#dayPanelSub");
function renderTimeline(day, dayIdx) {
timelineEl.innerHTML = "";
day.stops.forEach(function (stop, sIdx) {
var k = key(day.id, sIdx);
var isDone = !!done[k];
// travel leg between stops
if (stop.leg) {
var leg = document.createElement("li");
leg.className = "leg";
leg.setAttribute("aria-hidden", "false");
leg.innerHTML =
'<span class="leg__line"></span>' +
'<span class="leg__mode" aria-hidden="true">' + stop.leg.mode + "</span>" +
"<span>" + stop.leg.text + "</span>";
timelineEl.appendChild(leg);
}
var li = document.createElement("li");
li.className = "stop" + (isDone ? " is-done" : "");
li.dataset.stop = String(sIdx);
var poiOpen = sIdx === 0 ? " is-open" : "";
li.innerHTML =
'<div class="stop__time">' + stop.time + "</div>" +
'<div class="stop__node">' +
'<span class="stop__dot" aria-hidden="true"></span>' +
'<div class="poi' + poiOpen + '">' +
'<div class="poi__head">' +
'<div class="poi__icon" aria-hidden="true">' + stop.icon + "</div>" +
'<div class="poi__main">' +
'<div class="poi__top">' +
'<h3 class="poi__name">' + stop.name + "</h3>" +
'<span class="poi__cat">' + stop.cat + "</span>" +
"</div>" +
'<div class="poi__row">' +
'<span class="poi__stars" aria-label="' + stop.rating + ' out of 5">' + stars(stop.rating) + "</span>" +
"<span>💶 " + stop.price + "</span>" +
"<span>🕑 Best: " + stop.best + "</span>" +
"</div>" +
"</div>" +
'<div class="poi__controls">' +
checkBtnHTML(isDone, sIdx) +
toggleBtnHTML(sIdx) +
"</div>" +
"</div>" +
'<div class="poi__body">' +
'<div class="poi__body-inner">' +
'<p class="poi__note">' + stop.note + "</p>" +
"</div>" +
"</div>" +
"</div>" +
"</div>";
timelineEl.appendChild(li);
});
// wire interactions for this render
Array.prototype.forEach.call(timelineEl.querySelectorAll(".check-btn"), function (b) {
b.addEventListener("click", function () { onCheck(day, dayIdx, b); });
});
Array.prototype.forEach.call(timelineEl.querySelectorAll(".toggle-btn"), function (b) {
b.addEventListener("click", function () { onToggle(b); });
});
// tapping the header (not on a button) also toggles
Array.prototype.forEach.call(timelineEl.querySelectorAll(".poi__head"), function (h) {
h.addEventListener("click", function (e) {
if (e.target.closest("button")) return;
var t = h.parentElement.querySelector(".toggle-btn");
if (t) onToggle(t);
});
});
}
function checkBtnHTML(isDone, sIdx) {
return (
'<button type="button" class="icon-btn check-btn" data-stop="' + sIdx + '" ' +
'aria-pressed="' + (isDone ? "true" : "false") + '" ' +
'title="Mark stop complete" aria-label="Mark stop complete">' +
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
'<path d="M5 12.5 10 17.5 19.5 7" /></svg>' +
"</button>"
);
}
function toggleBtnHTML(sIdx) {
return (
'<button type="button" class="icon-btn toggle-btn" data-stop="' + sIdx + '" ' +
'aria-expanded="' + (sIdx === 0 ? "true" : "false") + '" ' +
'title="Show notes" aria-label="Show notes">' +
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
'<path d="M6 9l6 6 6-6" /></svg>' +
"</button>"
);
}
function onToggle(btn) {
var poi = btn.closest(".poi");
var open = poi.classList.toggle("is-open");
btn.setAttribute("aria-expanded", open ? "true" : "false");
}
function onCheck(day, dayIdx, btn) {
var sIdx = parseInt(btn.dataset.stop, 10);
var k = key(day.id, sIdx);
var nowDone = !done[k];
done[k] = nowDone;
var stopLi = btn.closest(".stop");
stopLi.classList.toggle("is-done", nowDone);
btn.setAttribute("aria-pressed", nowDone ? "true" : "false");
updateProgress(day, dayIdx);
updateMapPins(day);
var stopName = day.stops[sIdx].name;
toast(nowDone ? "✓ Checked off — " + stopName : "Unchecked — " + stopName);
}
/* ----------------------------- progress ----------------------------- */
var progressArc = $("#progressArc");
var progressNum = $("#progressNum");
var dayCountEl = $("#dayCount");
var ARC_LEN = 2 * Math.PI * 18; // r=18
function dayDoneCount(day) {
var c = 0;
day.stops.forEach(function (_, i) { if (done[key(day.id, i)]) c++; });
return c;
}
function updateProgress(day, dayIdx) {
var total = day.stops.length;
var doneN = dayDoneCount(day);
var pct = total ? Math.round((doneN / total) * 100) : 0;
if (progressArc) progressArc.style.strokeDashoffset = String(ARC_LEN * (1 - pct / 100));
if (progressNum) progressNum.textContent = pct + "%";
if (dayCountEl) dayCountEl.textContent = doneN + " of " + total + " done";
// update this tab's mini progress bar
var bar = tabEls[dayIdx] && tabEls[dayIdx].querySelector(".day-tab__bar i");
if (bar) bar.style.width = pct + "%";
}
function refreshAllTabBars() {
TRIP.forEach(function (day, i) {
var total = day.stops.length;
var pct = total ? Math.round((dayDoneCount(day) / total) * 100) : 0;
var bar = tabEls[i].querySelector(".day-tab__bar i");
if (bar) bar.style.width = pct + "%";
});
}
/* ----------------------------- map ----------------------------- */
var routePath = $("#routePath");
var pinsGroup = $("#mapPins");
var mapNote = $("#mapNote");
function renderMap(day) {
// route polyline through stop points
var pts = day.stops.map(function (s) { return s.pt; });
var d = pts.map(function (p, i) { return (i === 0 ? "M" : "L") + p.x + " " + p.y; }).join(" ");
routePath.setAttribute("d", d);
pinsGroup.innerHTML = "";
day.stops.forEach(function (s, i) {
var g = svgEl("g", { class: "map__pin", "data-stop": String(i) });
var halo = svgEl("circle", { cx: s.pt.x, cy: s.pt.y, r: 13, fill: "rgba(232,98,63,0.16)" });
var body = svgEl("circle", {
class: "pin-body",
cx: s.pt.x, cy: s.pt.y, r: 10,
fill: done[key(day.id, i)] ? "#2f9e6f" : "#e8623f",
stroke: "#fff", "stroke-width": "2"
});
var num = svgEl("text", { class: "map__pin-num", x: s.pt.x, y: s.pt.y });
num.textContent = String(i + 1);
g.appendChild(halo);
g.appendChild(body);
g.appendChild(num);
pinsGroup.appendChild(g);
});
if (mapNote) {
mapNote.textContent = day.city + " day route · " + day.stops.length + " stops · illustrative map";
}
}
function updateMapPins(day) {
Array.prototype.forEach.call(pinsGroup.querySelectorAll(".map__pin"), function (g) {
var i = parseInt(g.dataset.stop, 10);
var body = g.querySelector(".pin-body");
if (body) body.setAttribute("fill", done[key(day.id, i)] ? "#2f9e6f" : "#e8623f");
});
}
/* ----------------------------- summary ----------------------------- */
var summaryStats = $("#summaryStats");
var highlightsEl = $("#highlights");
function renderSummary(day) {
summaryStats.innerHTML =
'<li><small>Distance</small><b>' + day.distance + "</b></li>" +
'<li><small>On foot</small><b>' + day.walk + "</b></li>" +
'<li><small>Est. cost</small><b>' + day.cost + "</b></li>" +
'<li><small>Stops</small><b>' + day.stops.length + "</b></li>";
// highlights = top-rated stops (4★+)
var picks = day.stops
.filter(function (s) { return s.rating >= 4; })
.slice(0, 4);
highlightsEl.innerHTML = picks
.map(function (s) { return "<li>" + s.name + "</li>"; })
.join("");
}
/* ----------------------------- select day ----------------------------- */
function selectDay(idx, announce) {
activeIndex = idx;
var day = TRIP[idx];
tabEls.forEach(function (t, i) {
var sel = i === idx;
t.setAttribute("aria-selected", sel ? "true" : "false");
t.tabIndex = sel ? 0 : -1;
if (sel) {
t.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" });
}
});
kickerEl.textContent = day.label + " · " + day.city;
titleEl.textContent = day.title;
subEl.textContent = day.sub;
$("#dayPanel").setAttribute("aria-labelledby", "dayPanelTitle");
renderTimeline(day, idx);
renderMap(day);
updateMapPins(day);
renderSummary(day);
updateProgress(day, idx);
if (announce) toast(day.label + " — " + day.city);
}
/* ----------------------------- save / print ----------------------------- */
var saveBtn = $("#saveTripBtn");
saveBtn.addEventListener("click", function () {
var saved = saveBtn.getAttribute("aria-pressed") === "true";
saved = !saved;
saveBtn.setAttribute("aria-pressed", saved ? "true" : "false");
saveBtn.querySelector(".btn__label").textContent = saved ? "Saved" : "Save trip";
toast(saved ? "♥ Trip saved to your wishlist" : "Removed from wishlist");
});
var printBtn = $("#printBtn");
printBtn.addEventListener("click", function () {
toast("Opening print view…");
setTimeout(function () { window.print(); }, 120);
});
/* ----------------------------- init ----------------------------- */
selectDay(0, false);
refreshAllTabBars();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Amalfi Coast — Day-by-day Itinerary</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,wght@9..144,400;9..144,500;9..144,600;9..144,700&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="#main">Skip to itinerary</a>
<header class="topbar" role="banner">
<div class="topbar__inner">
<div class="brand">
<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__text">Wayfare<em>Journal</em></span>
</div>
<div class="topbar__actions">
<button class="btn btn--ghost" id="printBtn" type="button">
<span aria-hidden="true">🖨</span> Print
</button>
<button class="btn btn--solid" id="saveTripBtn" type="button" aria-pressed="false">
<span class="btn__heart" aria-hidden="true">♥</span>
<span class="btn__label">Save trip</span>
</button>
</div>
</div>
</header>
<main id="main" class="wrap">
<section class="hero" aria-labelledby="tripTitle">
<div class="hero__scene" aria-hidden="true">
<svg viewBox="0 0 1200 320" preserveAspectRatio="xMidYMid slice" class="hero__svg">
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffd9a8" />
<stop offset="0.55" stop-color="#f6b08a" />
<stop offset="1" stop-color="#e8836a" />
</linearGradient>
<linearGradient id="sea" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#2aa5a0" />
<stop offset="1" stop-color="#137070" />
</linearGradient>
</defs>
<rect width="1200" height="320" fill="url(#sky)" />
<circle cx="980" cy="92" r="46" fill="#fff3da" opacity="0.85" />
<path d="M0 200 L150 120 L260 188 L360 96 L470 196 L600 110 L720 200 L860 130 L980 205 L1100 150 L1200 210 L1200 320 L0 320 Z" fill="#9a5a4f" opacity="0.55" />
<path d="M0 236 L180 168 L320 226 L470 150 L640 232 L820 176 L1000 240 L1200 190 L1200 320 L0 320 Z" fill="#7c4036" opacity="0.7" />
<rect y="262" width="1200" height="58" fill="url(#sea)" />
<path d="M0 262 q60 14 120 0 t120 0 t120 0 t120 0 t120 0 t120 0 t120 0 t120 0 t120 0" fill="none" stroke="#bdeeea" stroke-width="2" opacity="0.5" />
</svg>
</div>
<div class="hero__body">
<p class="hero__eyebrow">Italy · Campania · 5 days</p>
<h1 id="tripTitle">Amalfi Coast by Road & Ferry</h1>
<p class="hero__lede">
Cliffside villages, lemon groves and slow boat crossings — a relaxed
five-day loop from Sorrento down to Ravello. Tap a day to see its route,
tick off stops as you go.
</p>
<dl class="hero__meta">
<div><dt>Total distance</dt><dd id="tripDistance">186 km</dd></div>
<div><dt>Est. budget</dt><dd id="tripBudget">€940</dd></div>
<div><dt>Travellers</dt><dd>2 adults</dd></div>
<div><dt>Pace</dt><dd>Easy</dd></div>
</dl>
</div>
</section>
<nav class="days" aria-label="Trip days">
<ul class="days__list" id="dayTabs" role="tablist" aria-label="Choose a day"></ul>
</nav>
<section class="board" aria-live="polite">
<article class="day-panel" id="dayPanel" role="tabpanel" tabindex="0" aria-labelledby="dayPanelTitle">
<header class="day-panel__head">
<div>
<p class="day-panel__kicker" id="dayKicker">Day 1</p>
<h2 class="day-panel__title" id="dayPanelTitle">Day title</h2>
<p class="day-panel__sub" id="dayPanelSub">Subtitle</p>
</div>
<div class="day-panel__progress">
<span class="progress-ring" id="progressRing" aria-hidden="true">
<svg viewBox="0 0 44 44" width="44" height="44">
<circle class="progress-ring__bg" cx="22" cy="22" r="18" />
<circle class="progress-ring__fg" id="progressArc" cx="22" cy="22" r="18" />
</svg>
<span class="progress-ring__num" id="progressNum">0%</span>
</span>
<span class="day-panel__count" id="dayCount">0 of 0 done</span>
</div>
</header>
<div class="day-panel__grid">
<div class="timeline-col">
<ol class="timeline" id="timeline" aria-label="Stops for the selected day"></ol>
</div>
<aside class="day-side">
<section class="map-card" aria-label="Route map for the selected day">
<h3 class="map-card__title">Day route</h3>
<div class="map" id="mapBox">
<svg viewBox="0 0 320 320" class="map__svg" role="img" id="mapSvg" aria-label="Map of today's route">
<rect width="320" height="320" fill="#eef2ec" />
<g class="map__land">
<path d="M-10 230 Q70 180 140 210 T300 200 L330 330 L-10 330 Z" fill="#dfe7d8" />
<path d="M-10 250 Q90 215 180 245 T330 240 L330 330 L-10 330 Z" fill="#d2ddc8" />
</g>
<g class="map__water">
<rect x="-10" y="-10" width="340" height="200" fill="#cfe8e6" />
<path d="M0 150 q40 8 80 0 t80 0 t80 0 t80 0" fill="none" stroke="#a9d6d2" stroke-width="2" opacity="0.7" />
<path d="M0 175 q40 8 80 0 t80 0 t80 0 t80 0" fill="none" stroke="#a9d6d2" stroke-width="2" opacity="0.5" />
</g>
<path id="routePath" class="map__route" fill="none" />
<g id="mapPins"></g>
</svg>
</div>
<p class="map-card__note" id="mapNote">Route shown is illustrative.</p>
</section>
<section class="summary-card" aria-label="Day summary">
<h3 class="summary-card__title">Day summary</h3>
<ul class="summary-stats" id="summaryStats"></ul>
<h4 class="summary-card__sub">Highlights</h4>
<ul class="highlights" id="highlights"></ul>
</section>
</aside>
</div>
</article>
</section>
</main>
<footer class="footnote" role="contentinfo">
<p>WayfareJournal · Illustrative travel UI only — fictional prices, routes and maps.</p>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Day-by-day Itinerary
A warm, magazine-style trip planner built around a five-day Amalfi Coast loop. A landscape hero sets the mood, then a horizontal day selector (Day 1–5) lets you jump between days. Each day renders as a vertical timeline: every stop is a POI card showing its time, category, star rating, price tier and best-time hint, with a small travel leg (walk / ferry / bus) drawn between consecutive stops.
Switching days swaps the timeline and redraws an inline SVG mini-map, tracing the day’s route through numbered pins over a stylised coast. You can check off completed stops — the timeline dot turns green, the matching map pin recolours, and a circular progress ring plus each tab’s mini-bar update in step. Stop notes collapse and expand (tap the card header or the chevron), and a day summary lists distance, on-foot total, estimated cost and the day’s highlights. Save the trip to a wishlist or print a clean, control-free version of the plan.
Everything is vanilla HTML, CSS and JavaScript — no frameworks, no external images, no build step. The map, hero landscape and pins are all CSS gradients and inline SVG, and the layout collapses gracefully from a two-column board down to a single stacked column on narrow phones.
Illustrative travel UI only — fictional destinations, prices, and maps.