Airline — Baggage Tracker
A polished aviation-themed baggage tracker UI showing each checked bag's tag ID, a five-stage status tracker from checked through loaded, in transit, arrived, and on carousel, plus flight info, carousel number, and last-update line. Features animated progress fills, colour-coded status and delayed pills, a bag-tag lookup, a simulate-progress control, and an accessible report-issue modal with toast confirmations. Mobile-first and responsive down to 360px.
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;
--boarding: #1f9d62;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-sm: 0 1px 2px rgba(19, 35, 59, 0.06);
--sh-md: 0 8px 24px rgba(19, 35, 59, 0.08);
--sh-lg: 0 18px 50px rgba(19, 35, 59, 0.12);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.tnum { font-variant-numeric: tabular-nums; letter-spacing: 0.01em; }
.app {
max-width: 880px;
margin: 0 auto;
padding: 0 16px 56px;
}
/* ---------- Topbar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding: 16px 0;
margin-bottom: 8px;
background: linear-gradient(var(--bg) 70%, rgba(245, 248, 252, 0));
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand__mark {
display: grid;
place-items: center;
width: 42px;
height: 42px;
border-radius: var(--r-md);
background: var(--sky);
color: #fff;
box-shadow: var(--sh-sm);
}
.brand__txt { display: flex; flex-direction: column; line-height: 1.15; }
.brand__txt strong { font-size: 16px; font-weight: 800; letter-spacing: -0.01em; }
.brand__txt span { font-size: 12.5px; color: var(--muted); font-weight: 500; }
.lookup { display: flex; align-items: flex-end; gap: 8px; }
.lookup__field { display: flex; flex-direction: column; gap: 4px; }
.lookup__hint { font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; padding-left: 2px; }
.lookup__field input {
font: inherit;
font-variant-numeric: tabular-nums;
width: 190px;
padding: 9px 12px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--surface);
color: var(--ink);
transition: border-color .15s, box-shadow .15s;
}
.lookup__field input::placeholder { color: var(--muted); }
.lookup__field input:focus-visible {
outline: none;
border-color: var(--sky);
box-shadow: 0 0 0 3px var(--sky-50);
}
/* ---------- Buttons ---------- */
.btn {
font: inherit;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 7px;
padding: 9px 15px;
border-radius: var(--r-sm);
border: 1px solid transparent;
cursor: pointer;
transition: transform .08s, background .15s, box-shadow .15s, border-color .15s;
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--sky-50); }
.btn--primary { background: var(--sky); color: #fff; box-shadow: var(--sh-sm); }
.btn--primary:hover { background: var(--sky-d); }
.btn--ghost { background: var(--surface); color: var(--ink-2); border-color: var(--line-2); }
.btn--ghost:hover { border-color: var(--sky); color: var(--sky); }
.btn--link {
background: none;
border: none;
padding: 0;
color: var(--sky);
font-weight: 600;
cursor: pointer;
font-size: 13px;
}
.btn--link:hover { color: var(--sky-d); text-decoration: underline; }
/* ---------- Summary card ---------- */
.summary {
background: linear-gradient(135deg, var(--sky) 0%, var(--sky-d) 100%);
color: #fff;
border-radius: var(--r-lg);
padding: 22px 24px;
box-shadow: var(--sh-md);
margin-bottom: 22px;
}
.summary__row { display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap; }
.summary__route { display: flex; align-items: center; gap: 14px; }
.ap { display: flex; flex-direction: column; }
.ap__code { font-size: 28px; font-weight: 800; letter-spacing: 0.02em; line-height: 1; }
.ap__city { font-size: 12.5px; opacity: 0.82; font-weight: 500; margin-top: 3px; }
.ap--end { text-align: right; }
.ap__path { display: flex; align-items: center; gap: 6px; color: rgba(255, 255, 255, 0.9); }
.ap__line { width: 26px; height: 2px; background: rgba(255, 255, 255, 0.45); border-radius: 2px; }
.summary__meta { display: flex; gap: 26px; margin: 0; }
.summary__meta div { display: flex; flex-direction: column; gap: 2px; }
.summary__meta dt { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.72; font-weight: 600; }
.summary__meta dd { margin: 0; font-size: 15px; font-weight: 700; }
/* ---------- Bags section ---------- */
.bags__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
.bags__head h1 { margin: 0; font-size: 18px; font-weight: 800; letter-spacing: -0.01em; }
.bag-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 16px; }
.bag {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-sm);
overflow: hidden;
transition: box-shadow .18s, transform .18s;
}
.bag:hover { box-shadow: var(--sh-md); transform: translateY(-1px); }
.bag__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 18px;
border-bottom: 1px dashed var(--line-2);
position: relative;
}
/* boarding-pass style perforation notches */
.bag__top::before, .bag__top::after {
content: "";
position: absolute;
bottom: -8px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--bg);
border: 1px solid var(--line);
}
.bag__top::before { left: -9px; }
.bag__top::after { right: -9px; }
.bag__id { display: flex; align-items: center; gap: 12px; }
.bag__icon {
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: var(--r-md);
background: var(--sky-50);
color: var(--sky);
flex-shrink: 0;
}
.bag__labels { display: flex; flex-direction: column; }
.bag__tag { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: 0.02em; }
.bag__desc { font-size: 12.5px; color: var(--muted); font-weight: 500; }
/* status pills */
.pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 13px;
border-radius: 999px;
font-size: 12.5px;
font-weight: 700;
letter-spacing: 0.01em;
white-space: nowrap;
}
.pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.pill--transit { background: var(--sky-50); color: var(--sky-d); }
.pill--checked { background: var(--sunrise-50); color: #b8531c; }
.pill--arrived { background: #e6f5ee; color: var(--ok); }
.pill--carousel { background: #e6f5ee; color: var(--boarding); }
.pill--carousel::before { animation: blip 1.1s infinite; }
.pill--delayed { background: #fbf1de; color: #9a6512; }
@keyframes blip { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
/* ---------- Progress tracker ---------- */
.track { padding: 20px 18px 18px; }
.track__steps { display: flex; justify-content: space-between; position: relative; }
.track__bar {
position: absolute;
top: 13px;
left: 14px;
right: 14px;
height: 3px;
background: var(--line);
border-radius: 3px;
overflow: hidden;
}
.track__fill {
position: absolute;
inset: 0;
width: 0%;
background: linear-gradient(90deg, var(--sky), var(--ok));
border-radius: 3px;
transition: width .6s cubic-bezier(.4, 0, .2, 1);
}
.step { display: flex; flex-direction: column; align-items: center; gap: 8px; flex: 1; position: relative; z-index: 1; }
.step__dot {
width: 28px;
height: 28px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--surface);
border: 2px solid var(--line-2);
color: var(--muted);
transition: all .3s;
}
.step__dot svg { width: 14px; height: 14px; }
.step__label { font-size: 10.5px; font-weight: 600; color: var(--muted); text-align: center; letter-spacing: 0.01em; }
.step.is-done .step__dot { background: var(--ok); border-color: var(--ok); color: #fff; }
.step.is-current .step__dot {
background: var(--sky);
border-color: var(--sky);
color: #fff;
box-shadow: 0 0 0 5px var(--sky-50);
animation: pulse 1.8s infinite;
}
.step.is-done .step__label, .step.is-current .step__label { color: var(--ink); }
@keyframes pulse {
0%,100% { box-shadow: 0 0 0 5px var(--sky-50); }
50% { box-shadow: 0 0 0 8px rgba(10, 102, 194, 0.08); }
}
.bag__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
padding: 14px 18px;
border-top: 1px solid var(--line);
background: #fbfcfe;
}
.bag__update { font-size: 12.5px; color: var(--muted); }
.bag__carousel {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
}
.bag__carousel strong {
display: inline-grid;
place-items: center;
min-width: 30px;
height: 26px;
padding: 0 6px;
border-radius: var(--r-sm);
background: var(--ink);
color: #fff;
font-variant-numeric: tabular-nums;
font-size: 14px;
}
.bag__carousel.is-hidden { display: none; }
/* ---------- Modal ---------- */
.modal { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; padding: 16px; }
.modal[hidden] { display: none; }
.modal__backdrop { position: absolute; inset: 0; background: rgba(19, 35, 59, 0.5); backdrop-filter: blur(2px); animation: fade .2s; }
.modal__panel {
position: relative;
width: 100%;
max-width: 440px;
background: var(--surface);
border-radius: var(--r-lg);
box-shadow: var(--sh-lg);
padding: 26px 24px 22px;
animation: rise .25s cubic-bezier(.2, .9, .3, 1);
}
@keyframes fade { from { opacity: 0; } }
@keyframes rise { from { opacity: 0; transform: translateY(14px); } }
.modal__x {
position: absolute;
top: 12px;
right: 14px;
width: 32px;
height: 32px;
border: none;
background: none;
font-size: 24px;
line-height: 1;
color: var(--muted);
cursor: pointer;
border-radius: var(--r-sm);
}
.modal__x:hover { background: var(--sky-50); color: var(--ink); }
.modal__title { margin: 0 0 4px; font-size: 19px; font-weight: 800; }
.modal__sub { margin: 0 0 18px; font-size: 13.5px; color: var(--muted); }
.form { display: flex; flex-direction: column; gap: 14px; }
.form__group { display: flex; flex-direction: column; gap: 6px; }
.form__group span { font-size: 12.5px; font-weight: 600; color: var(--ink-2); }
.form__group select,
.form__group input,
.form__group textarea {
font: inherit;
padding: 10px 12px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--surface);
color: var(--ink);
transition: border-color .15s, box-shadow .15s;
}
.form__group textarea { resize: vertical; }
.form__group select:focus-visible,
.form__group input:focus-visible,
.form__group textarea:focus-visible {
outline: none;
border-color: var(--sky);
box-shadow: 0 0 0 3px var(--sky-50);
}
.form__actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 4px; }
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed;
bottom: 18px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 8px;
z-index: 60;
width: max-content;
max-width: calc(100vw - 32px);
}
.toast {
display: flex;
align-items: center;
gap: 9px;
padding: 11px 16px;
border-radius: var(--r-md);
background: var(--ink);
color: #fff;
font-size: 13.5px;
font-weight: 500;
box-shadow: var(--sh-lg);
animation: toastIn .25s ease;
}
.toast--ok { background: var(--ok); }
.toast--warn { background: var(--warn); }
.toast.is-out { animation: toastOut .3s ease forwards; }
@keyframes toastIn { from { opacity: 0; transform: translateY(10px); } }
@keyframes toastOut { to { opacity: 0; transform: translateY(10px); } }
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.app { padding: 0 12px 48px; }
.topbar { padding: 14px 0; }
.lookup { width: 100%; }
.lookup__field { flex: 1; }
.lookup__field input { width: 100%; }
.summary { padding: 18px; }
.summary__row { gap: 16px; }
.ap__code { font-size: 24px; }
.summary__meta { gap: 18px; width: 100%; }
.step__label { font-size: 9.5px; }
.step__dot { width: 24px; height: 24px; }
.track__bar { top: 11px; }
.bags__head h1 { font-size: 16px; }
}(function () {
"use strict";
// status flow: index into STEPS
var STEPS = [
{ key: "checked", label: "Checked in", icon: "M4 7h16M4 12h16M4 17h10" },
{ key: "loaded", label: "Loaded", icon: "M3 16v-2l8-5V3.5A1 1 0 0 1 13 4v5l8 5v2l-8-2.5V19l2 1.5V22l-3-1-3 1v-1.5L11 19v-5.5z" },
{ key: "transit", label: "In transit", icon: "M2 12h20M14 6l6 6-6 6" },
{ key: "arrived", label: "Arrived", icon: "M5 12l5 5L20 7" },
{ key: "carousel", label: "On carousel", icon: "M4 7a8 4 0 0 1 16 0v10a8 4 0 0 1-16 0z" }
];
var PILL = {
checked: { cls: "pill--checked", text: "Checked" },
loaded: { cls: "pill--checked", text: "Loaded" },
transit: { cls: "pill--transit", text: "In transit" },
arrived: { cls: "pill--arrived", text: "Arrived" },
carousel: { cls: "pill--carousel", text: "On carousel" }
};
var bags = [
{ tag: "0125 4467 901", desc: "Hard-shell · 23 kg · Navy", step: 4, carousel: 7, updated: "2 min ago · LHR T5", delayed: false },
{ tag: "0125 4467 902", desc: "Duffel · 18 kg · Olive", step: 2, carousel: 7, updated: "12 min ago · in transit", delayed: false },
{ tag: "0125 4467 903", desc: "Soft case · 27 kg · Charcoal", step: 1, carousel: 7, updated: "26 min ago · loading bay", delayed: true }
];
var listEl = document.getElementById("bagList");
var bagCountEl = document.getElementById("bagCount");
function svg(path) {
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="' + path + '"/></svg>';
}
function pillFor(bag) {
if (bag.delayed && bag.step < 4) {
return '<span class="pill pill--delayed">Delayed</span>';
}
var p = PILL[STEPS[bag.step].key];
return '<span class="pill ' + p.cls + '">' + p.text + "</span>";
}
function render() {
bagCountEl.textContent = String(bags.length);
listEl.innerHTML = "";
bags.forEach(function (bag, i) {
var li = document.createElement("li");
li.className = "bag";
li.dataset.index = String(i);
var steps = STEPS.map(function (s, si) {
var state = si < bag.step ? "is-done" : si === bag.step ? "is-current" : "";
var mark = si < bag.step ? svg("M5 12l5 5L20 7") : svg(s.icon);
return (
'<div class="step ' + state + '">' +
'<span class="step__dot">' + mark + "</span>" +
'<span class="step__label">' + s.label + "</span>" +
"</div>"
);
}).join("");
var pct = (bag.step / (STEPS.length - 1)) * 100;
var showCarousel = bag.step >= 4;
li.innerHTML =
'<div class="bag__top">' +
'<div class="bag__id">' +
'<span class="bag__icon">' + svg("M6 7V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2M4 7h16v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zM9 11v6M15 11v6") + "</span>" +
'<span class="bag__labels">' +
'<span class="bag__tag tnum">' + bag.tag + "</span>" +
'<span class="bag__desc">' + bag.desc + "</span>" +
"</span>" +
"</div>" +
pillFor(bag) +
"</div>" +
'<div class="track">' +
'<div class="track__steps">' +
'<div class="track__bar"><span class="track__fill"></span></div>' +
steps +
"</div>" +
"</div>" +
'<div class="bag__footer">' +
'<span class="bag__update">Last update: ' + bag.updated + "</span>" +
'<span class="bag__carousel' + (showCarousel ? "" : " is-hidden") + '">Carousel <strong class="tnum">' + bag.carousel + "</strong></span>" +
'<button type="button" class="btn--link" data-report="' + i + '">Report issue</button>' +
"</div>";
listEl.appendChild(li);
// animate the fill after paint
var fill = li.querySelector(".track__fill");
requestAnimationFrame(function () {
requestAnimationFrame(function () { fill.style.width = pct + "%"; });
});
});
}
// ---------- Advance simulation ----------
document.getElementById("advanceBtn").addEventListener("click", function () {
var moved = false;
bags.forEach(function (bag) {
if (bag.step < STEPS.length - 1) {
bag.step++;
moved = true;
if (bag.step >= 4) bag.delayed = false;
bag.updated = "just now · " + STEPS[bag.step].label;
if (bag.step === STEPS.length - 1) {
toast("Bag " + bag.tag + " is on carousel " + bag.carousel, "ok");
}
}
});
render();
if (!moved) toast("All bags have reached the carousel", "ok");
else toast("Status updated", "");
});
// ---------- Lookup ----------
document.getElementById("lookupForm").addEventListener("submit", function (e) {
e.preventDefault();
var q = document.getElementById("lookupInput").value.trim().replace(/\s+/g, "");
if (!q) { toast("Enter a bag tag ID to track", "warn"); return; }
var found = -1;
bags.forEach(function (bag, i) {
if (bag.tag.replace(/\s+/g, "").indexOf(q) !== -1) found = i;
});
if (found === -1) {
toast("No bag found for " + q, "warn");
return;
}
var card = listEl.querySelector('[data-index="' + found + '"]');
if (card) {
card.scrollIntoView({ behavior: "smooth", block: "center" });
card.style.boxShadow = "0 0 0 3px var(--sky-50), var(--sh-md)";
setTimeout(function () { card.style.boxShadow = ""; }, 1400);
}
toast("Showing bag " + bags[found].tag, "ok");
});
// ---------- Report modal ----------
var modal = document.getElementById("reportModal");
var reportSub = document.getElementById("reportSub");
var activeTag = null;
function openModal(idx) {
activeTag = bags[idx] ? bags[idx].tag : null;
reportSub.textContent = activeTag
? "Bag tag " + activeTag + " — tell us what happened."
: "Tell us what happened with your bag.";
modal.hidden = false;
var first = modal.querySelector("select");
if (first) first.focus();
}
function closeModal() {
modal.hidden = true;
document.getElementById("reportForm").reset();
}
listEl.addEventListener("click", function (e) {
var btn = e.target.closest("[data-report]");
if (btn) openModal(parseInt(btn.dataset.report, 10));
});
modal.addEventListener("click", function (e) {
if (e.target.closest("[data-close]")) closeModal();
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !modal.hidden) closeModal();
});
document.getElementById("reportForm").addEventListener("submit", function (e) {
e.preventDefault();
var type = document.getElementById("issueType").value;
if (!type) { toast("Please choose an issue type", "warn"); return; }
closeModal();
toast("Report filed" + (activeTag ? " for " + activeTag : "") + " — case #SK" + (Math.floor(Math.random() * 9000) + 1000), "ok");
});
// ---------- Toast ----------
var toastWrap = document.getElementById("toastWrap");
function toast(msg, kind) {
var el = document.createElement("div");
el.className = "toast" + (kind ? " toast--" + kind : "");
el.textContent = msg;
toastWrap.appendChild(el);
setTimeout(function () {
el.classList.add("is-out");
setTimeout(function () { el.remove(); }, 300);
}, 2600);
}
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Skyline Air — Baggage Tracker</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>
<div class="app">
<header class="topbar">
<div class="brand">
<span class="brand__mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M21 16v-2l-8-5V3.5A1.5 1.5 0 0 0 11.5 2 1.5 1.5 0 0 0 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5z"/></svg>
</span>
<div class="brand__txt">
<strong>Skyline Air</strong>
<span>Baggage Tracker</span>
</div>
</div>
<form class="lookup" id="lookupForm" autocomplete="off">
<label class="lookup__field">
<span class="lookup__hint">Bag tag ID</span>
<input id="lookupInput" type="text" inputmode="numeric" placeholder="e.g. 0125 4467 901" aria-label="Bag tag ID" />
</label>
<button type="submit" class="btn btn--primary">Track bag</button>
</form>
</header>
<main class="main">
<section class="summary" aria-label="Trip summary">
<div class="summary__row">
<div class="summary__route">
<div class="ap">
<span class="ap__code">JFK</span>
<span class="ap__city">New York</span>
</div>
<div class="ap__path" aria-hidden="true">
<span class="ap__line"></span>
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M21 16v-2l-8-5V3.5A1.5 1.5 0 0 0 11.5 2 1.5 1.5 0 0 0 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5z"/></svg>
<span class="ap__line"></span>
</div>
<div class="ap ap--end">
<span class="ap__code">LHR</span>
<span class="ap__city">London</span>
</div>
</div>
<dl class="summary__meta">
<div><dt>Flight</dt><dd class="tnum">SK 482</dd></div>
<div><dt>Passenger</dt><dd>R. Okonkwo</dd></div>
<div><dt>Bags</dt><dd class="tnum" id="bagCount">3</dd></div>
</dl>
</div>
</section>
<section class="bags" aria-label="Tracked bags">
<div class="bags__head">
<h1>Your checked baggage</h1>
<button type="button" class="btn btn--ghost" id="advanceBtn">
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
Simulate progress
</button>
</div>
<ul class="bag-list" id="bagList"></ul>
</section>
</main>
</div>
<!-- Report issue modal -->
<div class="modal" id="reportModal" hidden>
<div class="modal__backdrop" data-close></div>
<div class="modal__panel" role="dialog" aria-modal="true" aria-labelledby="reportTitle">
<button type="button" class="modal__x" data-close aria-label="Close dialog">×</button>
<h2 id="reportTitle" class="modal__title">Report a baggage issue</h2>
<p class="modal__sub" id="reportSub">Tell us what happened with your bag.</p>
<form id="reportForm" class="form">
<label class="form__group">
<span>Issue type</span>
<select id="issueType" required>
<option value="">Select an issue…</option>
<option>Bag not on carousel</option>
<option>Damaged bag</option>
<option>Wrong bag collected</option>
<option>Contents missing</option>
<option>Other</option>
</select>
</label>
<label class="form__group">
<span>Contact email</span>
<input id="issueEmail" type="email" required placeholder="you@example.com" />
</label>
<label class="form__group">
<span>Details</span>
<textarea id="issueNote" rows="3" placeholder="Describe the issue (optional)"></textarea>
</label>
<div class="form__actions">
<button type="button" class="btn btn--ghost" data-close>Cancel</button>
<button type="submit" class="btn btn--primary">Submit report</button>
</div>
</form>
</div>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Baggage Tracker
A self-contained baggage tracking screen for the fictional Skyline Air flight SK 482 (JFK → LHR). A gradient trip-summary header shows the route, flight number, passenger, and bag count, while each checked bag renders as a boarding-pass-style card with dashed perforation notches, a tag ID in tabular figures, a short description, and a live status pill.
The heart of each card is a five-stage tracker — checked → loaded → in transit → arrived → on carousel — with a progress bar that animates its fill on render, completed steps marked with checks, and a pulsing dot on the current stage. Bags that fall behind show a colour-coded Delayed pill, and once a bag reaches the carousel its assigned carousel number is revealed with a blinking indicator.
Interactions are all vanilla JS: the bag tag lookup scrolls to and highlights a matching bag, Simulate progress advances every bag one stage with smooth re-animation and toast feedback, and each card’s Report issue link opens an accessible modal (focus management, Escape to close, backdrop dismiss) that files a mock case number. A small toast() helper surfaces success, warning, and neutral confirmations.
Illustrative UI only — fictional airline, not a real booking or flight system.