Airline — Regional Landing
A friendly regional short-hop airline landing page with a quick-search hero, live boarding-pass card with countdown, an interactive SVG route map you can switch between hubs, fast-turnaround perks, two simple fares, an app promo with a phone mockup, and a full footer. Sky-blue and green palette, rounded approachable styling, scroll reveals, mobile nav, and a toast helper. Vanilla HTML, CSS, and JavaScript with no frameworks or build step.
MCP
Code
:root {
--sky: #2b9fe0;
--sky-d: #1f7cb4;
--sky-50: #e8f5fd;
--green: #1f9d62;
--green-50: #e7f6ee;
--white: #ffffff;
--ink: #14283a;
--ink-2: #3a506b;
--muted: #6b7c93;
--bg: #f4fafe;
--surface: #ffffff;
--line: rgba(20, 40, 58, 0.1);
--line-2: rgba(20, 40, 58, 0.16);
--ok: #1f9d62;
--warn: #e0962a;
--danger: #d4493e;
--r-sm: 10px;
--r-md: 16px;
--r-lg: 24px;
--r-pill: 999px;
--sh-sm: 0 2px 8px rgba(20, 40, 58, 0.06);
--sh-md: 0 10px 30px rgba(20, 40, 58, 0.1);
--sh-lg: 0 20px 50px rgba(31, 124, 180, 0.16);
--tnum: "tnum" 1, "lnum" 1;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3, h4 { margin: 0; line-height: 1.15; letter-spacing: -0.02em; }
p { margin: 0; }
a { color: inherit; text-decoration: none; }
img, svg { display: block; }
.tnum { font-feature-settings: var(--tnum); font-variant-numeric: tabular-nums; }
.wrap { width: min(1140px, 92vw); margin: 0 auto; }
.muted { color: var(--muted); }
/* Buttons */
.btn {
font: inherit;
font-weight: 600;
border: 0;
cursor: pointer;
border-radius: var(--r-pill);
padding: 0.7em 1.3em;
display: inline-flex;
align-items: center;
gap: 0.5em;
transition: transform 0.15s ease, box-shadow 0.2s ease, background 0.2s ease;
}
.btn:active { transform: translateY(1px) scale(0.99); }
.btn-primary { background: var(--sky); color: #fff; box-shadow: 0 6px 16px rgba(43, 159, 224, 0.32); }
.btn-primary:hover { background: var(--sky-d); box-shadow: 0 10px 22px rgba(43, 159, 224, 0.4); }
.btn-soft { background: var(--sky-50); color: var(--sky-d); }
.btn-soft:hover { background: #d7eefb; }
.btn-ghost { background: transparent; color: var(--ink-2); }
.btn-ghost:hover { background: var(--sky-50); color: var(--sky-d); }
/* Pills */
.pill {
display: inline-flex;
align-items: center;
gap: 0.4em;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.02em;
padding: 0.3em 0.7em;
border-radius: var(--r-pill);
text-transform: uppercase;
}
.pill-boarding { background: var(--green-50); color: var(--green); }
.pill-boarding::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--green); animation: blink 1.4s ease-in-out infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
/* Nav */
.nav {
position: sticky;
top: 0;
z-index: 50;
background: rgba(244, 250, 254, 0.85);
backdrop-filter: blur(12px);
border-bottom: 1px solid transparent;
transition: border-color 0.25s ease, box-shadow 0.25s ease, background 0.25s ease;
}
.nav.scrolled { border-color: var(--line); box-shadow: var(--sh-sm); background: rgba(255, 255, 255, 0.9); }
.nav-inner { display: flex; align-items: center; justify-content: space-between; height: 68px; gap: 1rem; }
.brand { display: flex; align-items: center; gap: 0.5rem; font-weight: 800; }
.brand-mark { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 11px; background: var(--sky); color: #fff; box-shadow: 0 4px 12px rgba(43, 159, 224, 0.4); }
.brand-name { font-size: 1.2rem; letter-spacing: -0.03em; }
.brand-tag { font-size: 0.62rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--green); background: var(--green-50); padding: 0.2em 0.5em; border-radius: var(--r-pill); }
.nav-links { display: flex; gap: 0.4rem; }
.nav-links a { padding: 0.5rem 0.85rem; border-radius: var(--r-pill); font-weight: 500; color: var(--ink-2); transition: background 0.18s ease, color 0.18s ease; }
.nav-links a:hover { background: var(--sky-50); color: var(--sky-d); }
.nav-actions { display: flex; align-items: center; gap: 0.5rem; }
.nav-burger { display: none; flex-direction: column; gap: 4px; background: none; border: 0; cursor: pointer; padding: 8px; }
.nav-burger span { width: 22px; height: 2px; background: var(--ink); border-radius: 2px; transition: transform 0.25s ease, opacity 0.25s ease; }
.nav-burger.open span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.nav-burger.open span:nth-child(2) { opacity: 0; }
.nav-burger.open span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
.mobile-menu { display: flex; flex-direction: column; gap: 0.25rem; padding: 0.5rem 4vw 1.2rem; background: #fff; border-bottom: 1px solid var(--line); }
.mm-link { padding: 0.8rem 0.5rem; font-weight: 600; border-bottom: 1px solid var(--line); }
.mm-cta { margin-top: 0.75rem; justify-content: center; }
/* Reveal */
.reveal { opacity: 0; transform: translateY(26px); transition: opacity 0.6s ease, transform 0.6s ease; }
.reveal.in { opacity: 1; transform: none; }
/* Hero */
.hero { padding: clamp(2.5rem, 6vw, 5rem) 0 clamp(3rem, 6vw, 5rem); position: relative; overflow: hidden; }
.hero::before { content: ""; position: absolute; inset: 0; background: radial-gradient(1100px 480px at 78% -8%, var(--sky-50), transparent 60%); z-index: -1; }
.hero-inner { display: grid; grid-template-columns: 1.15fr 0.85fr; gap: clamp(1.5rem, 4vw, 3.5rem); align-items: center; }
.eyebrow { display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.82rem; font-weight: 600; color: var(--sky-d); background: var(--sky-50); padding: 0.4em 0.9em; border-radius: var(--r-pill); }
.eyebrow .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); }
.hero-copy h1 { font-size: clamp(2.3rem, 5.5vw, 3.7rem); font-weight: 800; margin: 1rem 0 0.9rem; }
.hl { color: var(--sky); }
.lede { font-size: clamp(1rem, 1.6vw, 1.15rem); color: var(--ink-2); max-width: 36ch; }
.search {
margin-top: 1.6rem;
background: #fff;
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 0.6rem;
display: grid;
grid-template-columns: 1fr auto 1fr 1fr auto;
gap: 0.5rem;
align-items: end;
box-shadow: var(--sh-md);
}
.field { display: flex; flex-direction: column; gap: 0.25rem; padding: 0.35rem 0.6rem; border-radius: var(--r-sm); transition: background 0.18s ease; }
.field:focus-within { background: var(--sky-50); }
.field label { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
.field input { border: 0; background: none; font: inherit; font-weight: 600; color: var(--ink); padding: 0; outline: none; width: 100%; }
.field input::placeholder { color: var(--muted); font-weight: 500; }
.swap { align-self: center; background: var(--sky-50); color: var(--sky-d); border: 0; border-radius: 50%; width: 36px; height: 36px; display: grid; place-items: center; cursor: pointer; transition: transform 0.3s ease, background 0.18s ease; }
.swap:hover { background: #d7eefb; transform: rotate(180deg); }
.search-go { justify-content: center; height: 46px; }
.hero-stats { list-style: none; margin: 1.6rem 0 0; padding: 0; display: flex; gap: 2rem; flex-wrap: wrap; }
.hero-stats li { display: flex; flex-direction: column; }
.hero-stats strong { font-size: 1.5rem; font-weight: 800; color: var(--ink); }
.hero-stats span { font-size: 0.82rem; color: var(--muted); }
/* Hero card (boarding pass) */
.hero-card {
background: #fff;
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 1.4rem 1.4rem 1.2rem;
box-shadow: var(--sh-lg);
position: relative;
}
.hcard-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.1rem; }
.hcard-flight { font-weight: 800; color: var(--ink-2); font-feature-settings: var(--tnum); }
.hcard-route { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 0.6rem; }
.rt-end { display: flex; flex-direction: column; }
.rt-right { text-align: right; align-items: flex-end; }
.code { font-size: 1.9rem; font-weight: 800; letter-spacing: 0.02em; }
.time { font-weight: 700; color: var(--ink-2); }
.rt-mid { display: flex; align-items: center; gap: 0.35rem; }
.rt-mid .line { flex: 1; height: 2px; background: repeating-linear-gradient(90deg, var(--line-2) 0 5px, transparent 5px 10px); }
.rt-mid .plane { color: var(--sky); animation: fly 3.5s ease-in-out infinite; }
@keyframes fly { 0%, 100% { transform: translateX(-3px); } 50% { transform: translateX(3px); } }
.hcard-meta { display: flex; gap: 1.4rem; margin: 1.3rem 0 0.4rem; }
.hcard-meta .lbl { display: block; font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); font-weight: 700; }
.hcard-meta b { font-size: 1.1rem; }
.hcard-perf { position: relative; height: 1px; margin: 1rem -1.4rem 0.9rem; border-top: 2px dashed var(--line-2); }
.hcard-perf::before, .hcard-perf::after { content: ""; position: absolute; top: -10px; width: 20px; height: 20px; border-radius: 50%; background: var(--bg); }
.hcard-perf::before { left: -10px; }
.hcard-perf::after { right: -10px; }
.hcard-foot { font-size: 0.82rem; color: var(--muted); }
/* Section heads */
.sec-head { max-width: 56ch; margin-bottom: 2.2rem; }
.kicker { font-size: 0.78rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--sky); }
.sec-head h2 { font-size: clamp(1.7rem, 3.6vw, 2.5rem); font-weight: 800; margin: 0.5rem 0 0.7rem; }
.sec-head p { color: var(--ink-2); font-size: 1.02rem; }
/* Routes */
.routes { padding: clamp(3rem, 7vw, 5.5rem) 0; }
.routes-grid { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(1.5rem, 4vw, 3rem); align-items: start; }
.map-panel { background: #fff; border: 1px solid var(--line); border-radius: var(--r-lg); padding: 0.8rem; box-shadow: var(--sh-md); }
.map-svg { width: 100%; height: auto; }
.map-node { cursor: pointer; }
.map-node circle { transition: r 0.25s ease, fill 0.25s ease; }
.map-node text { font: 700 11px "Inter", sans-serif; fill: var(--ink); }
.map-node.hub circle { fill: var(--sky); }
.map-node.spoke circle { fill: #fff; stroke: var(--sky); stroke-width: 2; }
.map-node.dim { opacity: 0.35; }
.route-tabs { display: flex; gap: 0.4rem; background: var(--sky-50); padding: 0.35rem; border-radius: var(--r-pill); margin-bottom: 1.2rem; }
.rt-tab { flex: 1; border: 0; background: none; font: inherit; font-weight: 600; color: var(--ink-2); padding: 0.6rem; border-radius: var(--r-pill); cursor: pointer; transition: background 0.18s ease, color 0.18s ease, box-shadow 0.18s ease; }
.rt-tab.is-active { background: #fff; color: var(--sky-d); box-shadow: var(--sh-sm); }
.route-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.7rem; }
.route-row { display: grid; grid-template-columns: auto 1fr auto; gap: 1rem; align-items: center; background: #fff; border: 1px solid var(--line); border-radius: var(--r-md); padding: 0.9rem 1.1rem; transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; cursor: pointer; }
.route-row:hover { transform: translateX(4px); box-shadow: var(--sh-sm); border-color: var(--sky); }
.route-pair { display: flex; align-items: center; gap: 0.55rem; font-weight: 800; }
.route-pair .arr { color: var(--sky); }
.route-info { display: flex; flex-direction: column; }
.route-info .dest { font-weight: 600; }
.route-info .det { font-size: 0.8rem; color: var(--muted); }
.route-price { font-weight: 800; color: var(--ink); font-feature-settings: var(--tnum); text-align: right; }
.route-price small { display: block; font-size: 0.7rem; font-weight: 600; color: var(--muted); }
/* Perks */
.perks { padding: clamp(3rem, 7vw, 5.5rem) 0; background: linear-gradient(180deg, #fff, var(--sky-50)); }
.perk-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.2rem; }
.perk { background: #fff; border: 1px solid var(--line); border-radius: var(--r-lg); padding: 1.5rem 1.3rem; transition: transform 0.2s ease, box-shadow 0.2s ease; }
.perk:hover { transform: translateY(-6px); box-shadow: var(--sh-md); }
.perk-ic { display: grid; place-items: center; width: 48px; height: 48px; border-radius: 14px; background: var(--sky-50); font-size: 1.5rem; margin-bottom: 1rem; }
.perk h3 { font-size: 1.1rem; margin-bottom: 0.5rem; }
.perk p { color: var(--ink-2); font-size: 0.92rem; }
/* Fares */
.fares { padding: clamp(3rem, 7vw, 5.5rem) 0; }
.fare-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.4rem; max-width: 760px; }
.fare { position: relative; background: #fff; border: 1px solid var(--line); border-radius: var(--r-lg); padding: 1.8rem 1.6rem; display: flex; flex-direction: column; box-shadow: var(--sh-sm); transition: transform 0.2s ease, box-shadow 0.2s ease; }
.fare:hover { transform: translateY(-5px); box-shadow: var(--sh-md); }
.fare-pop { border-color: var(--sky); box-shadow: var(--sh-md); }
.badge { position: absolute; top: -0.8rem; right: 1.4rem; background: var(--green); color: #fff; font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; padding: 0.35em 0.8em; border-radius: var(--r-pill); box-shadow: var(--sh-sm); }
.fare-name { font-weight: 700; color: var(--sky-d); font-size: 0.95rem; }
.fare-price { font-size: 2.6rem; font-weight: 800; margin: 0.4rem 0 1.1rem; }
.fare-price small { font-size: 0.95rem; font-weight: 500; color: var(--muted); }
.fare ul { list-style: none; margin: 0 0 1.5rem; padding: 0; display: flex; flex-direction: column; gap: 0.6rem; flex: 1; }
.fare li { position: relative; padding-left: 1.6rem; font-size: 0.94rem; color: var(--ink-2); }
.fare li::before { content: "✓"; position: absolute; left: 0; color: var(--green); font-weight: 800; }
.fare-pick { justify-content: center; }
/* App */
.app { padding: clamp(3rem, 7vw, 5.5rem) 0; }
.app-inner { background: linear-gradient(135deg, var(--sky), var(--sky-d)); border-radius: var(--r-lg); padding: clamp(2rem, 5vw, 3.5rem); display: grid; grid-template-columns: 1.3fr 0.7fr; gap: 2rem; align-items: center; overflow: hidden; box-shadow: var(--sh-lg); }
.app-copy { color: #fff; }
.app-copy .kicker { color: #d6efff; }
.app-copy h2 { color: #fff; margin: 0.5rem 0 0.8rem; font-size: clamp(1.6rem, 3.4vw, 2.3rem); }
.app-copy p { color: #eaf6ff; max-width: 40ch; }
.store-row { display: flex; gap: 0.8rem; margin-top: 1.6rem; flex-wrap: wrap; }
.store { background: rgba(255, 255, 255, 0.16); color: #fff; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: var(--r-pill); padding: 0.7em 1.3em; font: inherit; font-weight: 600; cursor: pointer; transition: background 0.18s ease; }
.store:hover { background: rgba(255, 255, 255, 0.28); }
.app-phone { display: grid; place-items: center; }
.phone { width: 230px; background: #fff; border-radius: 26px; padding: 1.2rem; box-shadow: 0 24px 50px rgba(20, 40, 58, 0.32); }
.phone-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.1rem; }
.phone-route { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
.phone-route > div { display: flex; flex-direction: column; }
.phone-route b { font-size: 1.3rem; font-weight: 800; }
.phone-route span { font-size: 0.78rem; color: var(--muted); }
.phone-route .rt-right { text-align: right; align-items: flex-end; }
.phone-plane { font-size: 1.2rem; color: var(--sky); }
.phone-bar { height: 6px; background: var(--sky-50); border-radius: 99px; overflow: hidden; margin-bottom: 0.7rem; }
.phone-bar span { display: block; height: 100%; background: var(--green); border-radius: 99px; }
.phone-note { font-size: 0.78rem; color: var(--ink-2); font-weight: 600; }
/* Footer */
.footer { background: var(--ink); color: #c6d3e2; padding: 3rem 0 1.6rem; margin-top: 2rem; }
.foot-grid { display: grid; grid-template-columns: 1.6fr repeat(3, 1fr); gap: 2rem; padding-bottom: 2.4rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1); }
.foot-brand strong { color: #fff; font-size: 1.15rem; display: inline-block; margin: 0.6rem 0 0.4rem; }
.foot-brand .brand-mark { box-shadow: none; }
.foot-brand p { font-size: 0.9rem; max-width: 24ch; }
.foot-col h4 { color: #fff; font-size: 0.95rem; margin-bottom: 0.9rem; }
.foot-col a { display: block; padding: 0.32rem 0; font-size: 0.9rem; color: #aebccf; transition: color 0.15s ease; }
.foot-col a:hover { color: #fff; }
.foot-bottom { display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap; padding-top: 1.4rem; font-size: 0.82rem; color: #8ea0b6; }
/* Toast */
.toast { position: fixed; left: 50%; bottom: 28px; transform: translate(-50%, 140%); background: var(--ink); color: #fff; padding: 0.85rem 1.3rem; border-radius: var(--r-pill); font-weight: 600; font-size: 0.92rem; box-shadow: var(--sh-md); z-index: 100; opacity: 0; transition: transform 0.4s cubic-bezier(0.2, 0.9, 0.3, 1.2), opacity 0.3s ease; max-width: 90vw; }
.toast.show { transform: translate(-50%, 0); opacity: 1; }
/* Responsive */
@media (max-width: 920px) {
.nav-links { display: none; }
.nav-burger { display: flex; }
.hero-inner { grid-template-columns: 1fr; }
.hero-card { max-width: 420px; }
.routes-grid { grid-template-columns: 1fr; }
.perk-grid { grid-template-columns: repeat(2, 1fr); }
.app-inner { grid-template-columns: 1fr; }
.app-phone { order: -1; }
.foot-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 520px) {
.nav-actions #signin { display: none; }
.search { grid-template-columns: 1fr; }
.swap { justify-self: center; }
.field-date { grid-row: auto; }
.hero-stats { gap: 1.4rem; }
.perk-grid { grid-template-columns: 1fr; }
.fare-grid { grid-template-columns: 1fr; }
.foot-grid { grid-template-columns: 1fr; }
.foot-bottom { flex-direction: column; gap: 0.4rem; }
.code { font-size: 1.6rem; }
}
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; scroll-behavior: auto; }
.reveal { transition: none; opacity: 1; transform: none; }
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2600);
}
/* ---------- Sticky nav shadow ---------- */
var nav = document.getElementById("nav");
function onScroll() {
if (window.scrollY > 8) nav.classList.add("scrolled");
else nav.classList.remove("scrolled");
}
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
/* ---------- Mobile menu ---------- */
var burger = document.getElementById("burger");
var menu = document.getElementById("mobileMenu");
function setMenu(open) {
menu.hidden = !open;
burger.classList.toggle("open", open);
burger.setAttribute("aria-expanded", String(open));
}
burger.addEventListener("click", function () {
setMenu(menu.hidden);
});
menu.querySelectorAll("a, .mm-cta").forEach(function (el) {
el.addEventListener("click", function () { setMenu(false); });
});
/* ---------- Scroll reveal ---------- */
var io = new IntersectionObserver(
function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) {
e.target.classList.add("in");
io.unobserve(e.target);
}
});
},
{ threshold: 0.12 }
);
document.querySelectorAll(".reveal").forEach(function (el) { io.observe(el); });
/* ---------- Default date = today ---------- */
var dateInput = document.getElementById("date");
if (dateInput) {
var d = new Date();
d.setDate(d.getDate() + 1);
dateInput.value = d.toISOString().slice(0, 10);
dateInput.min = new Date().toISOString().slice(0, 10);
}
/* ---------- Hero search ---------- */
var form = document.getElementById("searchForm");
var fromEl = document.getElementById("from");
var toEl = document.getElementById("to");
document.getElementById("swap").addEventListener("click", function () {
var t = fromEl.value;
fromEl.value = toEl.value;
toEl.value = t;
toast("Swapped — now flying " + fromEl.value + " → " + toEl.value);
});
form.addEventListener("submit", function (e) {
e.preventDefault();
if (!fromEl.value.trim() || !toEl.value.trim()) {
toast("Pick a from and to airport first.");
return;
}
if (fromEl.value.trim() === toEl.value.trim()) {
toast("Origin and destination can't match.");
return;
}
toast("Found 6 hops " + code(fromEl.value) + " → " + code(toEl.value) + " for " + (dateInput.value || "soon"));
});
function code(v) {
var m = /\(([A-Z]{3})\)/.exec(v);
return m ? m[1] : v;
}
/* ---------- Hero boarding countdown ---------- */
var countEl = document.getElementById("heroCount");
var heroStatus = document.getElementById("heroStatus");
var secs = 4 * 60 + 32;
setInterval(function () {
if (secs <= 0) {
countEl.textContent = "Now";
heroStatus.textContent = "Final call";
return;
}
secs--;
var m = String(Math.floor(secs / 60)).padStart(2, "0");
var s = String(secs % 60).padStart(2, "0");
countEl.textContent = m + ":" + s;
}, 1000);
/* ---------- Routes data ---------- */
var ROUTES = {
HBR: {
label: "Harbor Bay",
spokes: [
{ code: "PNR", name: "Pine Ridge", min: 45, price: 49, daily: 5 },
{ code: "MPC", name: "Maple Cove", min: 35, price: 39, daily: 6 },
{ code: "WLC", name: "Willow Creek", min: 55, price: 59, daily: 3 },
{ code: "GRH", name: "Granite Hills", min: 50, price: 55, daily: 4 }
]
},
LKS: {
label: "Lakeshore",
spokes: [
{ code: "CDF", name: "Cedar Falls", min: 40, price: 45, daily: 5 },
{ code: "MPC", name: "Maple Cove", min: 30, price: 35, daily: 7 },
{ code: "HBR", name: "Harbor Bay", min: 48, price: 52, daily: 4 }
]
},
CDF: {
label: "Cedar Falls",
spokes: [
{ code: "LKS", name: "Lakeshore", min: 40, price: 45, daily: 5 },
{ code: "GRH", name: "Granite Hills", min: 38, price: 42, daily: 4 },
{ code: "PNR", name: "Pine Ridge", min: 60, price: 64, daily: 2 }
]
}
};
var rowsEl = document.getElementById("routeRows");
var tabsEl = document.getElementById("routeTabs");
var activeHub = "HBR";
function renderRows(hub) {
var data = ROUTES[hub];
rowsEl.innerHTML = "";
data.spokes.forEach(function (sp) {
var li = document.createElement("li");
li.className = "route-row";
li.tabIndex = 0;
li.setAttribute("role", "button");
li.innerHTML =
'<span class="route-pair tnum">' + hub + ' <span class="arr">→</span> ' + sp.code + "</span>" +
'<span class="route-info"><span class="dest">' + sp.name + '</span>' +
'<span class="det tnum">' + sp.min + " min · " + sp.daily + " hops/day</span></span>" +
'<span class="route-price tnum">$' + sp.price + "<small>one way</small></span>";
li.addEventListener("click", function () {
toast("Hop " + hub + " → " + sp.code + " from $" + sp.price + " — opening times");
});
li.addEventListener("keydown", function (ev) {
if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); li.click(); }
});
rowsEl.appendChild(li);
});
drawMap(hub);
}
tabsEl.querySelectorAll(".rt-tab").forEach(function (tab) {
tab.addEventListener("click", function () {
tabsEl.querySelectorAll(".rt-tab").forEach(function (t) {
t.classList.remove("is-active");
t.setAttribute("aria-selected", "false");
});
tab.classList.add("is-active");
tab.setAttribute("aria-selected", "true");
activeHub = tab.dataset.hub;
renderRows(activeHub);
});
});
/* ---------- Map drawing ---------- */
var COORDS = {
HBR: { x: 110, y: 150 }, PNR: { x: 300, y: 70 }, MPC: { x: 250, y: 200 },
WLC: { x: 330, y: 250 }, GRH: { x: 200, y: 280 }, LKS: { x: 90, y: 70 },
CDF: { x: 200, y: 130 }
};
var linesG = document.getElementById("mapLines");
var nodesG = document.getElementById("mapNodes");
var SVGNS = "http://www.w3.org/2000/svg";
function el(name, attrs) {
var n = document.createElementNS(SVGNS, name);
for (var k in attrs) n.setAttribute(k, attrs[k]);
return n;
}
function drawMap(hub) {
linesG.innerHTML = "";
nodesG.innerHTML = "";
var hubC = COORDS[hub];
var connected = ROUTES[hub].spokes.map(function (s) { return s.code; });
// lines from hub to each spoke
connected.forEach(function (sc) {
var c = COORDS[sc];
var ln = el("line", { x1: hubC.x, y1: hubC.y, x2: c.x, y2: c.y });
linesG.appendChild(ln);
});
// all nodes
Object.keys(COORDS).forEach(function (codeKey) {
var c = COORDS[codeKey];
var isHub = codeKey === hub;
var isConn = connected.indexOf(codeKey) !== -1;
var g = el("g", { class: "map-node " + (isHub ? "hub" : "spoke") + (!isHub && !isConn ? " dim" : "") });
g.appendChild(el("circle", { cx: c.x, cy: c.y, r: isHub ? 11 : 7 }));
var txt = el("text", { x: c.x, y: c.y - 14, "text-anchor": "middle" });
txt.textContent = codeKey;
g.appendChild(txt);
if (!isHub) {
g.style.cursor = "pointer";
g.addEventListener("click", function () {
if (isConn) toast("Hop " + hub + " → " + codeKey + " — see fares below");
else toast("No direct hop yet — switch hubs to reach " + codeKey);
});
}
nodesG.appendChild(g);
});
}
renderRows(activeHub);
/* ---------- Fare picker ---------- */
document.querySelectorAll(".fare-pick").forEach(function (btn) {
btn.addEventListener("click", function () {
toast(btn.dataset.fare + " selected — building your hop");
});
});
/* ---------- Misc CTAs ---------- */
document.getElementById("signin").addEventListener("click", function () {
toast("Sign in — welcome back to SkyHop");
});
document.getElementById("bookNav").addEventListener("click", function () {
document.getElementById("hero").scrollIntoView({ behavior: "smooth" });
setTimeout(function () { fromEl.focus(); }, 500);
});
document.querySelectorAll(".store").forEach(function (b) {
b.addEventListener("click", function () { toast("App download link sent to your phone"); });
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>SkyHop — Regional Flights, Closer to Home</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>
<!-- Top nav -->
<header class="nav" id="nav">
<div class="wrap nav-inner">
<a href="#top" class="brand" aria-label="SkyHop home">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none"><path d="M2 12l9-2 4-7 2 1-2 6 5 1-1 3-5-1-3 6-2-1 1-6-7-1 1-2z" fill="currentColor"/></svg>
</span>
<span class="brand-name">SkyHop</span>
<span class="brand-tag">regional</span>
</a>
<nav class="nav-links" aria-label="Primary">
<a href="#routes">Routes</a>
<a href="#perks">Why SkyHop</a>
<a href="#fares">Fares</a>
<a href="#app">App</a>
</nav>
<div class="nav-actions">
<button class="btn btn-ghost" id="signin" type="button">Sign in</button>
<button class="btn btn-primary" id="bookNav" type="button">Book a hop</button>
<button class="nav-burger" id="burger" type="button" aria-label="Open menu" aria-expanded="false">
<span></span><span></span><span></span>
</button>
</div>
</div>
<div class="mobile-menu" id="mobileMenu" hidden>
<a href="#routes" class="mm-link">Routes</a>
<a href="#perks" class="mm-link">Why SkyHop</a>
<a href="#fares" class="mm-link">Fares</a>
<a href="#app" class="mm-link">App</a>
<button class="btn btn-primary mm-cta" type="button">Book a hop</button>
</div>
</header>
<main id="top">
<!-- Hero -->
<section class="hero reveal" id="hero">
<div class="wrap hero-inner">
<div class="hero-copy">
<span class="eyebrow"><span class="dot"></span> 38 friendly towns & cities</span>
<h1>Short hops, <span class="hl">closer to home.</span></h1>
<p class="lede">Tiny airports, no long queues, and a window seat almost guaranteed. SkyHop flies the routes the big carriers skip — quick, easy, and surprisingly affordable.</p>
<form class="search" id="searchForm" aria-label="Find a flight">
<div class="field">
<label for="from">From</label>
<input id="from" list="airports" value="Harbor Bay (HBR)" autocomplete="off" />
</div>
<button class="swap" id="swap" type="button" aria-label="Swap origin and destination">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none"><path d="M7 7h11l-3-3m3 3l-3 3M17 17H6l3-3m-3 3l3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="field">
<label for="to">To</label>
<input id="to" list="airports" value="Pine Ridge (PNR)" autocomplete="off" />
</div>
<div class="field field-date">
<label for="date">Depart</label>
<input id="date" type="date" />
</div>
<button class="btn btn-primary search-go" type="submit">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none"><circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2"/><path d="M21 21l-4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
Search hops
</button>
<datalist id="airports">
<option value="Harbor Bay (HBR)"></option>
<option value="Pine Ridge (PNR)"></option>
<option value="Maple Cove (MPC)"></option>
<option value="Cedar Falls (CDF)"></option>
<option value="Lakeshore (LKS)"></option>
<option value="Granite Hills (GRH)"></option>
<option value="Willow Creek (WLC)"></option>
</datalist>
</form>
<ul class="hero-stats">
<li><strong>45 min</strong><span>avg flight time</span></li>
<li><strong>20 min</strong><span>airport to gate</span></li>
<li><strong>1 bag</strong><span>free, always</span></li>
</ul>
</div>
<aside class="hero-card" aria-label="Next departure">
<div class="hcard-top">
<span class="pill pill-boarding" id="heroStatus">Boarding</span>
<span class="hcard-flight">SH 412</span>
</div>
<div class="hcard-route">
<div class="rt-end">
<strong class="code">HBR</strong>
<span class="time tnum">09:40</span>
<span class="muted">Harbor Bay</span>
</div>
<div class="rt-mid" aria-hidden="true">
<span class="line"></span>
<span class="plane">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M2 12l9-2 4-7 2 1-2 6 5 1-1 3-5-1-3 6-2-1 1-6-7-1 1-2z"/></svg>
</span>
<span class="line"></span>
</div>
<div class="rt-end rt-right">
<strong class="code">PNR</strong>
<span class="time tnum">10:25</span>
<span class="muted">Pine Ridge</span>
</div>
</div>
<div class="hcard-meta">
<div><span class="lbl">Gate</span><b class="tnum">3</b></div>
<div><span class="lbl">Seat</span><b class="tnum">12A</b></div>
<div><span class="lbl">Boards</span><b class="tnum" id="heroCount">04:32</b></div>
</div>
<div class="hcard-perf" aria-hidden="true"></div>
<p class="hcard-foot">Window seat & one free bag included.</p>
</aside>
</div>
</section>
<!-- Routes map -->
<section class="routes reveal" id="routes">
<div class="wrap">
<header class="sec-head">
<span class="kicker">The network</span>
<h2>Hop between local favorites</h2>
<p>Pick a hub to see where you can fly today. Every route is a single short hop — no connections, no fuss.</p>
</header>
<div class="routes-grid">
<div class="map-panel" aria-label="Route map">
<svg viewBox="0 0 400 320" class="map-svg" role="img" aria-label="Map of regional routes">
<defs>
<radialGradient id="g" cx="50%" cy="40%" r="70%">
<stop offset="0%" stop-color="#e8f5fd"/><stop offset="100%" stop-color="#f3fbff"/>
</radialGradient>
</defs>
<rect width="400" height="320" fill="url(#g)" rx="16"/>
<g class="map-lines" id="mapLines" stroke="#2b9fe0" stroke-width="2" stroke-dasharray="5 5" fill="none" opacity="0.55"></g>
<g id="mapNodes"></g>
</svg>
</div>
<div class="route-list">
<div class="route-tabs" id="routeTabs" role="tablist" aria-label="Choose a hub">
<button class="rt-tab is-active" data-hub="HBR" role="tab" type="button">Harbor Bay</button>
<button class="rt-tab" data-hub="LKS" role="tab" type="button">Lakeshore</button>
<button class="rt-tab" data-hub="CDF" role="tab" type="button">Cedar Falls</button>
</div>
<ul class="route-rows" id="routeRows"></ul>
</div>
</div>
</div>
</section>
<!-- Perks -->
<section class="perks reveal" id="perks">
<div class="wrap">
<header class="sec-head">
<span class="kicker">Why SkyHop</span>
<h2>Built for the quick getaway</h2>
</header>
<div class="perk-grid">
<article class="perk">
<span class="perk-ic">⏱️</span>
<h3>20-minute turnaround</h3>
<p>Small airports mean you walk from the curb to the gate in minutes. Show up 30 before, not three hours.</p>
</article>
<article class="perk">
<span class="perk-ic">🎒</span>
<h3>A free bag, always</h3>
<p>Every fare includes a carry-on and a checked bag. No surprise fees when you reach the counter.</p>
</article>
<article class="perk">
<span class="perk-ic">🪟</span>
<h3>Window seats for all</h3>
<p>Our props seat 2 across — almost everyone gets a window. The views over the coast are the whole point.</p>
</article>
<article class="perk">
<span class="perk-ic">🤝</span>
<h3>Crews from your town</h3>
<p>Our pilots and agents live where they fly. You might recognize them from the farmers market.</p>
</article>
</div>
</div>
</section>
<!-- Fares -->
<section class="fares reveal" id="fares">
<div class="wrap">
<header class="sec-head">
<span class="kicker">Simple fares</span>
<h2>Two ways to hop</h2>
<p>No fare classes to decode. Pick light if you travel often, or comfort for a bit more room.</p>
</header>
<div class="fare-grid">
<article class="fare">
<span class="fare-name">Hop Light</span>
<p class="fare-price"><span class="tnum">$49</span><small>/ one way</small></p>
<ul>
<li>One free carry-on</li>
<li>Window or aisle seat</li>
<li>Free changes up to 24h</li>
<li>Mobile boarding pass</li>
</ul>
<button class="btn btn-soft fare-pick" data-fare="Hop Light" type="button">Choose Light</button>
</article>
<article class="fare fare-pop">
<span class="badge">Most loved</span>
<span class="fare-name">Hop Comfort</span>
<p class="fare-price"><span class="tnum">$79</span><small>/ one way</small></p>
<ul>
<li>Carry-on + checked bag</li>
<li>Front-row legroom seat</li>
<li>Free changes anytime</li>
<li>Priority boarding & snack</li>
</ul>
<button class="btn btn-primary fare-pick" data-fare="Hop Comfort" type="button">Choose Comfort</button>
</article>
</div>
</div>
</section>
<!-- App -->
<section class="app reveal" id="app">
<div class="wrap app-inner">
<div class="app-copy">
<span class="kicker">SkyHop app</span>
<h2>Your boarding pass, in your pocket</h2>
<p>Track your hop in real time, get a nudge when boarding starts, and tap to fly. Download once, hop anywhere.</p>
<div class="store-row">
<button class="store" type="button">▸ App Store</button>
<button class="store" type="button">▸ Google Play</button>
</div>
</div>
<div class="app-phone" aria-hidden="true">
<div class="phone">
<div class="phone-top">
<span class="pill pill-boarding">Boarding now</span>
<span class="tnum muted">SH 412</span>
</div>
<div class="phone-route">
<div><b class="tnum">HBR</b><span>09:40</span></div>
<div class="phone-plane">✈</div>
<div class="rt-right"><b class="tnum">PNR</b><span>10:25</span></div>
</div>
<div class="phone-bar"><span style="width:62%"></span></div>
<p class="phone-note">Gate 3 · Seat 12A · 4 min to board</p>
</div>
</div>
</div>
</section>
</main>
<footer class="footer">
<div class="wrap foot-grid">
<div class="foot-brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none"><path d="M2 12l9-2 4-7 2 1-2 6 5 1-1 3-5-1-3 6-2-1 1-6-7-1 1-2z" fill="currentColor"/></svg>
</span>
<strong>SkyHop</strong>
<p>Regional flights, closer to home.</p>
</div>
<div class="foot-col">
<h4>Fly</h4>
<a href="#routes">Route map</a>
<a href="#fares">Fares</a>
<a href="#">Schedules</a>
<a href="#">Group hops</a>
</div>
<div class="foot-col">
<h4>Help</h4>
<a href="#">Manage booking</a>
<a href="#">Baggage</a>
<a href="#">Contact us</a>
<a href="#">Status</a>
</div>
<div class="foot-col">
<h4>Community</h4>
<a href="#">Our crews</a>
<a href="#">Local partners</a>
<a href="#">Careers</a>
<a href="#">Press</a>
</div>
</div>
<div class="wrap foot-bottom">
<span>© 2026 SkyHop Regional Air. Fictional carrier.</span>
<span>Privacy · Terms · Cookies</span>
</div>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Regional Landing
A marketing landing page for SkyHop, a fictional regional carrier that flies the short hops the big airlines skip. The hero pairs a quick-search form — with airport autocomplete, an animated swap button, and a date picker — against a live boarding-pass card whose departure countdown ticks down in real time and flips to a final-call status.
The route section is the centerpiece: an inline SVG map redraws its dashed flight lines and node states whenever you switch hubs via the segmented tabs, and the matching list of routes shows flight time, daily frequency, and a one-way price. Clicking nodes or rows fires a toast. Below, fast-turnaround perks, two plain-language fares (Light and Comfort), an app promo with a phone mockup, and a four-column footer round out the page.
Everything is vanilla: an IntersectionObserver drives scroll reveals, a sticky nav gains a shadow on scroll, the mobile burger toggles an accessible menu, and a small toast(msg) helper surfaces feedback for every interaction. Responsive down to ~360px and respectful of reduced-motion preferences.
Illustrative UI only — fictional airline, not a real booking or flight system.