Airline — Destinations Page
A polished airline destinations page in clean aviation blue and sunrise orange. A live search filters cities, countries and airport codes while region chips narrow the network and a sort control reorders by price or name. A featured spotlight headlines a route, responsive cards show from-prices and flight times, a quick-view dialog surfaces cabin and frequency details, and a stylised route map teaser rounds out the layout, all built with vanilla JS.
MCP
Code
:root {
--sky: #0a66c2;
--sky-d: #084e95;
--sky-50: #e9f2fb;
--cloud: #f5f8fc;
--sunrise: #ff7a33;
--sunrise-50: #fff0e7;
--ink: #13233b;
--ink-2: #3a4d68;
--muted: #6b7c93;
--bg: #f5f8fc;
--surface: #ffffff;
--line: rgba(19, 35, 59, 0.1);
--line-2: rgba(19, 35, 59, 0.18);
--ok: #1f9d62;
--warn: #e0962a;
--danger: #d4493e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow: 0 1px 2px rgba(19, 35, 59, 0.06), 0 8px 24px rgba(19, 35, 59, 0.08);
--shadow-lg: 0 18px 50px rgba(19, 35, 59, 0.18);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
font-size: 15px;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.tab { font-variant-numeric: tabular-nums; }
.skip {
position: absolute;
left: -999px;
top: 0;
background: var(--ink);
color: #fff;
padding: 10px 16px;
border-radius: 0 0 var(--r-sm) 0;
z-index: 100;
}
.skip:focus { left: 0; }
button { font-family: inherit; cursor: pointer; }
:focus-visible { outline: 3px solid var(--sunrise); outline-offset: 2px; }
/* ---------- Topbar ---------- */
.topbar {
display: flex;
align-items: center;
gap: 24px;
padding: 14px clamp(16px, 5vw, 56px);
background: var(--surface);
border-bottom: 1px solid var(--line);
position: sticky;
top: 0;
z-index: 40;
}
.brand {
display: flex;
align-items: center;
gap: 9px;
font-weight: 800;
font-size: 18px;
letter-spacing: -0.02em;
color: var(--ink);
}
.brand b { color: var(--sky); font-weight: 800; }
.logo { width: 26px; height: 26px; color: var(--sky); }
.nav { display: flex; gap: 4px; margin-left: auto; }
.nav a {
text-decoration: none;
color: var(--ink-2);
font-weight: 600;
font-size: 14px;
padding: 8px 12px;
border-radius: var(--r-sm);
}
.nav a:hover { background: var(--sky-50); color: var(--sky-d); }
.nav a.active { color: var(--sky-d); background: var(--sky-50); }
.signin {
background: var(--ink);
color: #fff;
border: 0;
padding: 9px 18px;
border-radius: 999px;
font-weight: 600;
font-size: 14px;
}
.signin:hover { background: var(--sky-d); }
/* ---------- Hero ---------- */
.hero {
position: relative;
margin: clamp(16px, 3vw, 32px) clamp(16px, 5vw, 56px) 0;
border-radius: var(--r-lg);
overflow: hidden;
min-height: 360px;
display: flex;
align-items: flex-end;
color: #fff;
box-shadow: var(--shadow);
}
.hero-bg {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
transition: opacity 0.4s ease;
}
.hero-veil {
position: absolute;
inset: 0;
background: linear-gradient(90deg, rgba(8, 37, 64, 0.86) 0%, rgba(8, 37, 64, 0.55) 45%, rgba(8, 37, 64, 0.15) 100%);
}
.hero-inner {
position: relative;
padding: clamp(22px, 4vw, 44px);
max-width: 620px;
}
.kicker {
display: inline-block;
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 11px;
font-weight: 700;
color: var(--sunrise);
margin-bottom: 8px;
}
.kicker.dark { color: var(--sunrise); }
.hero h1 {
margin: 0;
font-size: clamp(34px, 6vw, 56px);
font-weight: 800;
letter-spacing: -0.03em;
line-height: 1.02;
}
.hero-sub {
margin: 6px 0 0;
font-weight: 600;
font-variant-numeric: tabular-nums;
opacity: 0.92;
}
.hero-copy {
margin: 14px 0 0;
max-width: 460px;
opacity: 0.92;
font-size: 15px;
}
.hero-row {
display: flex;
align-items: center;
gap: 20px;
margin-top: 22px;
flex-wrap: wrap;
}
.hero-price { display: flex; align-items: baseline; gap: 6px; }
.hero-price .from { font-size: 12px; opacity: 0.8; }
.hero-price .amt {
font-size: 30px;
font-weight: 800;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
.hero-price .rt { font-size: 12px; opacity: 0.8; }
.hero-tags {
position: relative;
padding: 0 clamp(22px, 4vw, 44px) clamp(22px, 4vw, 44px);
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.tag {
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.25);
color: #fff;
font-size: 12px;
font-weight: 600;
padding: 5px 11px;
border-radius: 999px;
backdrop-filter: blur(4px);
}
.btn-primary {
background: var(--sunrise);
color: #fff;
border: 0;
padding: 12px 22px;
border-radius: 999px;
font-weight: 700;
font-size: 15px;
box-shadow: 0 6px 16px rgba(255, 122, 51, 0.35);
}
.btn-primary:hover { background: #f06a25; transform: translateY(-1px); }
.btn-primary:active { transform: translateY(0); }
.btn-ghost {
background: transparent;
color: var(--ink);
border: 1.5px solid var(--line-2);
padding: 11px 20px;
border-radius: 999px;
font-weight: 600;
}
.btn-ghost:hover { border-color: var(--sky); color: var(--sky-d); }
/* ---------- Controls ---------- */
.controls {
display: flex;
align-items: center;
gap: 14px;
padding: clamp(18px, 3vw, 28px) clamp(16px, 5vw, 56px) 0;
flex-wrap: wrap;
}
.search {
position: relative;
flex: 1 1 280px;
min-width: 240px;
}
.search svg {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
width: 18px;
height: 18px;
color: var(--muted);
}
.search input {
width: 100%;
border: 1px solid var(--line-2);
background: var(--surface);
border-radius: 999px;
padding: 12px 16px 12px 42px;
font-size: 14px;
color: var(--ink);
box-shadow: var(--shadow);
}
.search input::placeholder { color: var(--muted); }
.search input:focus { border-color: var(--sky); outline: none; box-shadow: 0 0 0 3px var(--sky-50); }
.regions {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.chip {
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink-2);
font-weight: 600;
font-size: 13px;
padding: 8px 14px;
border-radius: 999px;
transition: all 0.15s ease;
}
.chip:hover { border-color: var(--sky); color: var(--sky-d); }
.chip[aria-selected="true"] {
background: var(--sky);
border-color: var(--sky);
color: #fff;
}
.sortwrap { display: flex; align-items: center; gap: 8px; }
.sortlabel { font-size: 13px; color: var(--muted); font-weight: 600; }
#sortSelect {
border: 1px solid var(--line-2);
background: var(--surface);
border-radius: var(--r-sm);
padding: 10px 12px;
font-size: 14px;
font-family: inherit;
color: var(--ink);
font-weight: 600;
}
#sortSelect:focus { border-color: var(--sky); outline: none; }
/* ---------- Main / grid ---------- */
main { padding: 0 clamp(16px, 5vw, 56px); }
.resultbar {
display: flex;
align-items: baseline;
justify-content: space-between;
margin: 22px 0 14px;
gap: 12px;
}
#resultCount { font-weight: 700; font-size: 15px; }
.resultbar .hint { color: var(--muted); font-size: 13px; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
gap: 18px;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
box-shadow: var(--shadow);
cursor: pointer;
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
text-align: left;
padding: 0;
width: 100%;
display: flex;
flex-direction: column;
}
.card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--line-2);
}
.card-img {
position: relative;
height: 158px;
background-size: cover;
background-position: center;
}
.card-img::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(8, 37, 64, 0) 40%, rgba(8, 37, 64, 0.55) 100%);
}
.card-code {
position: absolute;
top: 10px;
left: 10px;
z-index: 1;
background: rgba(8, 37, 64, 0.78);
color: #fff;
font-weight: 700;
font-size: 12px;
letter-spacing: 0.06em;
padding: 4px 9px;
border-radius: var(--r-sm);
font-variant-numeric: tabular-nums;
backdrop-filter: blur(3px);
}
.card-deal {
position: absolute;
top: 10px;
right: 10px;
z-index: 1;
background: var(--sunrise);
color: #fff;
font-weight: 700;
font-size: 11px;
padding: 4px 9px;
border-radius: 999px;
}
.card-region {
position: absolute;
bottom: 10px;
left: 12px;
z-index: 1;
color: #fff;
font-size: 12px;
font-weight: 600;
opacity: 0.95;
}
.card-body {
padding: 13px 14px 15px;
display: flex;
flex-direction: column;
gap: 2px;
}
.card-city { font-weight: 700; font-size: 17px; letter-spacing: -0.01em; }
.card-country { color: var(--muted); font-size: 13px; }
.card-foot {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-top: 10px;
}
.card-price {
font-variant-numeric: tabular-nums;
}
.card-price .from { font-size: 11px; color: var(--muted); display: block; }
.card-price .amt { font-size: 20px; font-weight: 800; color: var(--ink); letter-spacing: -0.01em; }
.card-time {
font-size: 12px;
color: var(--ink-2);
font-weight: 600;
font-variant-numeric: tabular-nums;
display: inline-flex;
align-items: center;
gap: 4px;
}
.empty {
text-align: center;
color: var(--muted);
padding: 48px 16px;
font-size: 15px;
}
/* ---------- Map teaser ---------- */
.mapteaser {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
margin: clamp(28px, 5vw, 56px) clamp(16px, 5vw, 56px);
border-radius: var(--r-lg);
overflow: hidden;
background: #0a2540;
color: #fff;
box-shadow: var(--shadow);
}
.map-copy { padding: clamp(26px, 4vw, 44px); display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
.map-copy h2 { margin: 4px 0 10px; font-size: clamp(22px, 3vw, 30px); font-weight: 800; letter-spacing: -0.02em; line-height: 1.1; }
.map-copy p { margin: 0 0 18px; opacity: 0.82; max-width: 360px; }
.map-copy .btn-ghost { color: #fff; border-color: rgba(255, 255, 255, 0.3); }
.map-copy .btn-ghost:hover { border-color: var(--sunrise); color: var(--sunrise); }
.map-art svg { display: block; width: 100%; height: 100%; min-height: 220px; }
.map-art .routes path { animation: dash 18s linear infinite; }
@keyframes dash { to { stroke-dashoffset: -200; } }
/* ---------- Footer ---------- */
.foot {
display: flex;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
padding: 24px clamp(16px, 5vw, 56px) 40px;
color: var(--muted);
font-size: 13px;
border-top: 1px solid var(--line);
}
.foot .iata { font-variant-numeric: tabular-nums; }
/* ---------- Modal ---------- */
.overlay {
position: fixed;
inset: 0;
background: rgba(8, 37, 64, 0.5);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
padding: 18px;
z-index: 60;
animation: fade 0.18s ease;
}
@keyframes fade { from { opacity: 0; } }
.modal {
position: relative;
background: var(--surface);
border-radius: var(--r-lg);
max-width: 460px;
width: 100%;
overflow: hidden;
box-shadow: var(--shadow-lg);
animation: pop 0.2s cubic-bezier(0.2, 0.8, 0.3, 1);
}
@keyframes pop { from { transform: translateY(14px) scale(0.97); opacity: 0; } }
.modal-close {
position: absolute;
top: 10px;
right: 12px;
z-index: 2;
width: 34px;
height: 34px;
border-radius: 999px;
border: 0;
background: rgba(8, 37, 64, 0.55);
color: #fff;
font-size: 22px;
line-height: 1;
}
.modal-close:hover { background: rgba(8, 37, 64, 0.8); }
.modal-img { height: 190px; background-size: cover; background-position: center; }
.modal-body { padding: 20px 22px 22px; }
.qv-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.qv-head h3 { margin: 0; font-size: 24px; font-weight: 800; letter-spacing: -0.02em; }
.qv-sub { margin: 2px 0 0; color: var(--muted); font-size: 13px; }
.code-pill {
background: var(--sky-50);
color: var(--sky-d);
font-weight: 700;
font-size: 13px;
letter-spacing: 0.06em;
padding: 6px 11px;
border-radius: var(--r-sm);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.qv-copy { margin: 14px 0 0; color: var(--ink-2); font-size: 14px; }
.qv-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin: 18px 0;
padding: 14px 0;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
}
.qv-stats .lbl { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); font-weight: 700; }
.qv-stats .val { display: block; margin-top: 3px; font-weight: 700; font-size: 14px; font-variant-numeric: tabular-nums; }
.qv-foot { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
.qv-price { display: flex; align-items: baseline; gap: 5px; }
.qv-price .from { font-size: 11px; color: var(--muted); }
.qv-price .amt { font-size: 26px; font-weight: 800; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
.qv-price .rt { font-size: 11px; color: var(--muted); }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%) translateY(20px);
background: var(--ink);
color: #fff;
padding: 12px 20px;
border-radius: 999px;
font-size: 14px;
font-weight: 600;
box-shadow: var(--shadow-lg);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 90;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ---------- Responsive ---------- */
@media (max-width: 820px) {
.mapteaser { grid-template-columns: 1fr; }
.map-art { order: -1; }
}
@media (max-width: 640px) {
.nav { display: none; }
}
@media (max-width: 520px) {
body { font-size: 14px; }
.topbar { gap: 12px; padding: 12px 16px; }
.hero { min-height: 320px; margin: 14px 16px 0; }
.controls { padding: 16px 16px 0; }
.sortwrap { width: 100%; justify-content: space-between; }
#sortSelect { flex: 1; }
.grid { grid-template-columns: 1fr; gap: 14px; }
.qv-stats { grid-template-columns: 1fr 1fr 1fr; gap: 6px; }
.qv-foot { gap: 10px; }
.qv-foot .btn-primary { width: 100%; }
}/* Skyward Airlines — Destinations page (vanilla JS, illustrative only) */
(function () {
"use strict";
// Fictional destination network. Photos via deterministic Unsplash source URLs.
var IMG = function (id) {
return "https://images.unsplash.com/photo-" + id + "?auto=format&fit=crop&w=640&q=70";
};
var DESTINATIONS = [
{ city: "Lisbon", country: "Portugal", code: "LIS", region: "Europe", price: 389, time: "8h 05m", cabin: "A321neo · Economy & Premium", freq: "3 weekly", featured: true,
copy: "Pastel facades, tram-clattering hills and golden Atlantic light. Direct daytime service three times weekly aboard the new A321neo.",
img: "1585208798174-6cedd86e019a", tags: ["Direct", "New route", "Atlantic coast", "Visa-free 90d"] },
{ city: "Tokyo", country: "Japan", code: "HND", region: "Asia", price: 742, time: "13h 40m", cabin: "B787-9 · Economy to Suite", freq: "Daily",
copy: "Neon districts, quiet shrines and the world's best transit. Overnight departures land you mid-morning at Haneda.", img: "1540959733332-eab4deabeeaf" },
{ city: "Reykjavik", country: "Iceland", code: "KEF", region: "Europe", price: 311, time: "5h 50m", cabin: "A320neo · Economy", freq: "5 weekly",
copy: "Geothermal lagoons, lava fields and the aurora. Stopover programme lets you add up to 7 nights for free.", img: "1504218817356-1c12c3b9d9d6", deal: true },
{ city: "Marrakech", country: "Morocco", code: "RAK", region: "Africa", price: 268, time: "3h 25m", cabin: "A320 · Economy", freq: "4 weekly",
copy: "Souks, riads and the snow-tipped Atlas on the horizon. A short hop into a different world.", img: "1597212618440-806262de4f6b", deal: true },
{ city: "New York", country: "United States", code: "JFK", region: "Americas", price: 498, time: "7h 55m", cabin: "B787-9 · Economy & Business", freq: "Daily",
copy: "Skyline, galleries and 24-hour energy. Lie-flat business suites on every departure.", img: "1496442226666-8d4d0e62e6e9" },
{ city: "Singapore", country: "Singapore", code: "SIN", region: "Asia", price: 689, time: "12h 30m", cabin: "A350-900 · Economy to Suite", freq: "6 weekly",
copy: "Hawker stalls, hyper-gardens and a spotless transit hub. The Jewel waterfall is worth the layover.", img: "1525625293386-3f8f99389edd" },
{ city: "Cape Town", country: "South Africa", code: "CPT", region: "Africa", price: 612, time: "11h 20m", cabin: "A350-900 · Economy & Business", freq: "3 weekly",
copy: "Table Mountain, vineyards and two oceans meeting at the Cape. Seasonal summer schedule.", img: "1580060839134-75a5edca2e99" },
{ city: "Buenos Aires", country: "Argentina", code: "EZE", region: "Americas", price: 734, time: "13h 05m", cabin: "B787-9 · Economy & Business", freq: "4 weekly",
copy: "Tango, steak and grand boulevards. Overnight service with a full lie-flat business cabin.", img: "1589909202802-8f4aadce1849" },
{ city: "Amsterdam", country: "Netherlands", code: "AMS", region: "Europe", price: 214, time: "6h 40m", cabin: "A320neo · Economy", freq: "Daily",
copy: "Canals, masterpieces and bicycle bells. Our most frequent European link, twice daily in summer.", img: "1534351590666-13e3e96b5017", deal: true },
{ city: "Bangkok", country: "Thailand", code: "BKK", region: "Asia", price: 658, time: "12h 10m", cabin: "A350-900 · Economy to Suite", freq: "5 weekly",
copy: "Temples, street food and the river of kings. A gateway to all of Southeast Asia.", img: "1508009603885-50cf7c579365" },
{ city: "Vancouver", country: "Canada", code: "YVR", region: "Americas", price: 545, time: "9h 35m", cabin: "B787-9 · Economy & Business", freq: "5 weekly",
copy: "Mountains meeting the sea, with the city right between them. Ski and surf in the same week.", img: "1560814304-4f05b8e0c1d2" },
{ city: "Nairobi", country: "Kenya", code: "NBO", region: "Africa", price: 587, time: "10h 50m", cabin: "B787-9 · Economy & Business", freq: "3 weekly",
copy: "Safari gateway with a national park inside the city limits. Daytime arrival for onward connections.", img: "1547471080-7cc2caa01a7e" }
];
var REGIONS = ["All", "Europe", "Asia", "Americas", "Africa"];
// ---- State ----
var state = { region: "All", query: "", sort: "featured" };
// ---- Elements ----
var grid = document.getElementById("grid");
var emptyEl = document.getElementById("empty");
var resultCount = document.getElementById("resultCount");
var regionsEl = document.getElementById("regions");
var searchInput = document.getElementById("searchInput");
var sortSelect = document.getElementById("sortSelect");
// ---- Toast helper ----
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2400);
}
document.addEventListener("click", function (e) {
var t = e.target.closest("[data-toast]");
if (t) toast(t.getAttribute("data-toast"));
});
// ---- Featured spotlight ----
function renderFeatured() {
var f = DESTINATIONS.find(function (d) { return d.featured; }) || DESTINATIONS[0];
document.getElementById("featuredBg").style.backgroundImage =
"url('" + IMG(f.img) + "')";
document.getElementById("featuredTitle").textContent = f.city;
document.getElementById("featuredSub").textContent = f.code + " · " + f.country + " · " + f.region;
document.getElementById("featuredCopy").textContent = f.copy;
document.getElementById("featuredPrice").textContent = "$" + f.price;
var tagWrap = document.getElementById("featuredTags");
tagWrap.innerHTML = "";
(f.tags || []).forEach(function (tag) {
var s = document.createElement("span");
s.className = "tag";
s.textContent = tag;
tagWrap.appendChild(s);
});
document.getElementById("featuredBook").addEventListener("click", function () {
toast("Searching fares to " + f.city + " — illustrative only.");
});
}
// ---- Region chips ----
function renderRegions() {
REGIONS.forEach(function (r) {
var b = document.createElement("button");
b.className = "chip";
b.type = "button";
b.setAttribute("role", "tab");
b.textContent = r;
b.setAttribute("aria-selected", String(r === state.region));
b.addEventListener("click", function () {
state.region = r;
regionsEl.querySelectorAll(".chip").forEach(function (c) {
c.setAttribute("aria-selected", String(c === b));
});
render();
});
regionsEl.appendChild(b);
});
}
// ---- Filtering / sorting ----
function getList() {
var q = state.query.trim().toLowerCase();
var list = DESTINATIONS.filter(function (d) {
if (state.region !== "All" && d.region !== state.region) return false;
if (!q) return true;
return (
d.city.toLowerCase().indexOf(q) !== -1 ||
d.country.toLowerCase().indexOf(q) !== -1 ||
d.code.toLowerCase().indexOf(q) !== -1 ||
d.region.toLowerCase().indexOf(q) !== -1
);
});
switch (state.sort) {
case "price-asc": list.sort(function (a, b) { return a.price - b.price; }); break;
case "price-desc": list.sort(function (a, b) { return b.price - a.price; }); break;
case "az": list.sort(function (a, b) { return a.city.localeCompare(b.city); }); break;
default:
list.sort(function (a, b) { return (b.featured ? 1 : 0) - (a.featured ? 1 : 0); });
}
return list;
}
// ---- Card rendering ----
function render() {
var list = getList();
grid.innerHTML = "";
resultCount.textContent = list.length + (list.length === 1 ? " destination" : " destinations");
emptyEl.hidden = list.length !== 0;
list.forEach(function (d) {
var card = document.createElement("button");
card.className = "card";
card.type = "button";
card.setAttribute("role", "listitem");
card.setAttribute("aria-label", "Quick view " + d.city + ", " + d.country + ", from $" + d.price);
card.innerHTML =
'<div class="card-img" style="background-image:url(\'' + IMG(d.img) + '\')">' +
'<span class="card-code">' + d.code + '</span>' +
(d.deal ? '<span class="card-deal">Deal</span>' : '') +
'<span class="card-region">' + d.region + '</span>' +
'</div>' +
'<div class="card-body">' +
'<span class="card-city">' + d.city + '</span>' +
'<span class="card-country">' + d.country + '</span>' +
'<div class="card-foot">' +
'<span class="card-price"><span class="from">From</span><span class="amt">$' + d.price + '</span></span>' +
'<span class="card-time">✈ ' + d.time + '</span>' +
'</div>' +
'</div>';
card.addEventListener("click", function () { openQuickView(d); });
grid.appendChild(card);
});
}
// ---- Quick view modal ----
var overlay = document.getElementById("overlay");
var lastFocus = null;
function openQuickView(d) {
lastFocus = document.activeElement;
document.getElementById("qvImg").style.backgroundImage = "url('" + IMG(d.img) + "')";
document.getElementById("qvTitle").textContent = d.city;
document.getElementById("qvSub").textContent = d.country + " · " + d.region;
document.getElementById("qvCode").textContent = d.code;
document.getElementById("qvCopy").textContent = d.copy;
document.getElementById("qvTime").textContent = d.time;
document.getElementById("qvCabin").textContent = d.cabin.split(" · ")[0];
document.getElementById("qvFreq").textContent = d.freq;
document.getElementById("qvPrice").textContent = "$" + d.price;
var book = document.getElementById("qvBook");
book.onclick = function () { toast("Searching fares to " + d.city + " — illustrative only."); };
overlay.hidden = false;
document.body.style.overflow = "hidden";
document.getElementById("modalClose").focus();
}
function closeQuickView() {
overlay.hidden = true;
document.body.style.overflow = "";
if (lastFocus) lastFocus.focus();
}
document.getElementById("modalClose").addEventListener("click", closeQuickView);
overlay.addEventListener("click", function (e) {
if (e.target === overlay) closeQuickView();
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !overlay.hidden) closeQuickView();
});
// ---- Inputs ----
var debounce;
searchInput.addEventListener("input", function () {
clearTimeout(debounce);
debounce = setTimeout(function () {
state.query = searchInput.value;
render();
}, 120);
});
sortSelect.addEventListener("change", function () {
state.sort = sortSelect.value;
render();
});
// ---- Init ----
renderFeatured();
renderRegions();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Skyward Airlines — 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=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip" href="#grid">Skip to destinations</a>
<header class="topbar">
<div class="brand">
<svg class="logo" viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 14.5l19-7.5-3 9.5-6-2-3 4-1-3.5-5-.5z" fill="currentColor"/></svg>
<span>Skyward<b>Airlines</b></span>
</div>
<nav class="nav" aria-label="Primary">
<a href="#" class="active">Destinations</a>
<a href="#">Deals</a>
<a href="#">Loyalty</a>
<a href="#">Help</a>
</nav>
<button class="signin" type="button" data-toast="Sign in is illustrative only.">Sign in</button>
</header>
<section class="hero" aria-label="Featured destination">
<div class="hero-bg" id="featuredBg"></div>
<div class="hero-veil"></div>
<div class="hero-inner">
<div class="hero-meta">
<span class="kicker">Featured · Spotlight route</span>
<h1 id="featuredTitle">Lisbon</h1>
<p class="hero-sub" id="featuredSub">LIS · Portugal · Southern Europe</p>
<p class="hero-copy" id="featuredCopy">Pastel facades, tram-clattering hills and golden Atlantic light. Direct daytime service three times weekly aboard the new A321neo.</p>
<div class="hero-row">
<div class="hero-price">
<span class="from">From</span>
<span class="amt" id="featuredPrice">$389</span>
<span class="rt">round trip</span>
</div>
<button class="btn-primary" id="featuredBook" type="button" data-toast="Booking flow is illustrative only.">Search fares</button>
</div>
</div>
<div class="hero-tags" id="featuredTags"></div>
</div>
</section>
<section class="controls" aria-label="Filter destinations">
<div class="search">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 4a6 6 0 104.47 10.03l4.25 4.25 1.41-1.41-4.25-4.25A6 6 0 0010 4zm0 2a4 4 0 110 8 4 4 0 010-8z" fill="currentColor"/></svg>
<input id="searchInput" type="search" placeholder="Search city, country or code (e.g. Tokyo, JP, NRT)" aria-label="Search destinations" autocomplete="off" />
</div>
<div class="regions" role="tablist" aria-label="Region filter" id="regions"></div>
<div class="sortwrap">
<label for="sortSelect" class="sortlabel">Sort</label>
<select id="sortSelect" aria-label="Sort destinations">
<option value="featured">Featured</option>
<option value="price-asc">Price · low to high</option>
<option value="price-desc">Price · high to low</option>
<option value="az">Name · A–Z</option>
</select>
</div>
</section>
<main>
<div class="resultbar">
<span id="resultCount" aria-live="polite">0 destinations</span>
<span class="hint">Tap a card for a quick view</span>
</div>
<div class="grid" id="grid" role="list"></div>
<p class="empty" id="empty" hidden>No destinations match your search. Try another city or region.</p>
</main>
<section class="mapteaser" aria-label="Route map">
<div class="map-copy">
<span class="kicker dark">Network</span>
<h2>62 destinations across 4 continents</h2>
<p>New seasonal routes launching this winter. Explore the full Skyward network on our interactive route map.</p>
<button class="btn-ghost" type="button" data-toast="Interactive map is illustrative only.">Open route map</button>
</div>
<div class="map-art" aria-hidden="true">
<svg viewBox="0 0 400 220" preserveAspectRatio="xMidYMid slice">
<defs>
<radialGradient id="g" cx="50%" cy="40%" r="70%">
<stop offset="0%" stop-color="#1b4f86"/><stop offset="100%" stop-color="#0a2540"/>
</radialGradient>
</defs>
<rect width="400" height="220" fill="url(#g)"/>
<g stroke="rgba(255,255,255,.14)" stroke-width="1" fill="none">
<path d="M0 60 H400 M0 110 H400 M0 160 H400 M80 0 V220 M160 0 V220 M240 0 V220 M320 0 V220"/>
</g>
<g class="routes" stroke="#ff7a33" stroke-width="1.6" fill="none" stroke-dasharray="4 4">
<path d="M70 150 Q160 60 250 90"/>
<path d="M70 150 Q200 200 340 120"/>
<path d="M250 90 Q300 60 340 120"/>
</g>
<g fill="#fff">
<circle cx="70" cy="150" r="4"/><circle cx="250" cy="90" r="4"/>
<circle cx="340" cy="120" r="4"/><circle cx="160" cy="55" r="3" fill="#ff7a33"/>
</g>
</svg>
</div>
</section>
<footer class="foot">
<span>© Skyward Airlines — a fictional carrier for demo purposes.</span>
<span class="iata">Member, Star demo alliance · IATA SK</span>
</footer>
<!-- Quick-view dialog -->
<div class="overlay" id="overlay" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="qvTitle" id="modal">
<button class="modal-close" id="modalClose" aria-label="Close quick view">×</button>
<div class="modal-img" id="qvImg"></div>
<div class="modal-body">
<div class="qv-head">
<div>
<h3 id="qvTitle">City</h3>
<p class="qv-sub" id="qvSub">Country</p>
</div>
<span class="code-pill" id="qvCode">XXX</span>
</div>
<p class="qv-copy" id="qvCopy"></p>
<div class="qv-stats">
<div><span class="lbl">Flight time</span><span class="val" id="qvTime">—</span></div>
<div><span class="lbl">Cabin</span><span class="val" id="qvCabin">—</span></div>
<div><span class="lbl">Frequency</span><span class="val" id="qvFreq">—</span></div>
</div>
<div class="qv-foot">
<div class="qv-price"><span class="from">From</span><span class="amt" id="qvPrice">$0</span><span class="rt">round trip</span></div>
<button class="btn-primary" id="qvBook" type="button" data-toast="Booking flow is illustrative only.">Search fares</button>
</div>
</div>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Destinations Page
A marketing-style destinations page for the fictional Skyward Airlines. A full-bleed spotlight headlines a featured route with its from-price, descriptive copy and travel tags, while a stylised SVG route-map teaser communicates the wider network. The aviation-blue and sunrise-orange palette, Inter typography and tabular figures for prices and flight times give it a precise, status-forward feel.
The destination grid is fully interactive. A debounced search box matches across city, country, region and three-letter airport codes (try “JP”, “NRT” or “coast”); region chips filter the network down to a single continent; and a sort control reorders results by price ascending or descending, or alphabetically. Each responsive card shows the airport code, region, deal badge, from-price and flight time.
Tapping any card opens a keyboard-accessible quick-view dialog with the destination photo, airport code pill, cabin and aircraft, service frequency and a fares call-to-action. The modal closes on Escape, backdrop click or the close button, restoring focus to the originating card. A small toast helper acknowledges the illustrative booking actions. Everything runs on a single dependency-free script and adapts cleanly down to 360px.
Illustrative UI only — fictional airline, not a real booking or flight system.