Airline — Flight Information Display (FIDS)
A polished airport flight information display board with switchable departures and arrivals tabs, a status-forward table of times, flight numbers, destinations, gates and color-coded status pills, plus live search and status filters. Rows animate with a split-flap style flip as flights progress from on time to boarding to departed, a ticking local clock and a relative last-updated stamp keep the board feeling alive, and toasts announce boarding calls. Responsive down to mobile widths.
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;
--shadow-sm: 0 1px 2px rgba(19, 35, 59, 0.06), 0 1px 3px rgba(19, 35, 59, 0.05);
--shadow-md: 0 8px 28px rgba(19, 35, 59, 0.1);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
background:
radial-gradient(1200px 600px at 80% -10%, var(--sky-50), transparent 60%),
var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: "tnum" 1, "cv05" 1;
min-height: 100vh;
padding: 24px;
display: flex;
justify-content: center;
}
.fids {
width: 100%;
max-width: 920px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-md);
overflow: hidden;
align-self: flex-start;
}
/* Header */
.board-head {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 16px;
padding: 18px 22px;
background: linear-gradient(180deg, var(--sky) 0%, var(--sky-d) 100%);
color: #fff;
}
.brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
.brand-mark {
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.16);
color: #fff;
flex: none;
}
.brand-text { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.brand-text strong { font-size: 16px; font-weight: 800; letter-spacing: -0.01em; }
.brand-text span { font-size: 12.5px; color: rgba(255, 255, 255, 0.78); font-weight: 500; }
.tabs {
display: inline-flex;
background: rgba(255, 255, 255, 0.14);
border-radius: 999px;
padding: 4px;
gap: 4px;
}
.tab {
appearance: none;
border: 0;
cursor: pointer;
font: inherit;
font-weight: 600;
font-size: 13.5px;
color: rgba(255, 255, 255, 0.82);
background: transparent;
padding: 8px 18px;
border-radius: 999px;
transition: background 0.18s ease, color 0.18s ease;
}
.tab:hover { color: #fff; }
.tab.is-active { background: #fff; color: var(--sky-d); box-shadow: var(--shadow-sm); }
.tab:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.clock { text-align: right; }
.clock-label {
display: block;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: rgba(255, 255, 255, 0.7);
font-weight: 600;
}
.clock-time {
font-size: 20px;
font-weight: 800;
letter-spacing: 0.02em;
font-variant-numeric: tabular-nums;
}
/* Controls */
.controls {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
justify-content: space-between;
padding: 16px 22px;
border-bottom: 1px solid var(--line);
background: var(--cloud);
}
.search {
position: relative;
display: flex;
align-items: center;
gap: 9px;
flex: 1 1 280px;
min-width: 220px;
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
padding: 0 14px;
color: var(--muted);
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.search:focus-within {
border-color: var(--sky);
box-shadow: 0 0 0 3px var(--sky-50);
}
.search input {
flex: 1;
border: 0;
outline: 0;
background: transparent;
font: inherit;
font-size: 14px;
color: var(--ink);
padding: 11px 0;
}
.search input::placeholder { color: var(--muted); }
.filters { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
appearance: none;
cursor: pointer;
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 8px 14px;
transition: all 0.16s ease;
}
.chip:hover { border-color: var(--sky); color: var(--sky-d); }
.chip.is-active { background: var(--sky); border-color: var(--sky); color: #fff; }
.chip:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; }
/* Board */
.board { padding: 6px 0 0; }
.board-row {
display: grid;
grid-template-columns: 92px 116px 1fr 80px 140px;
align-items: center;
gap: 14px;
padding: 14px 22px;
}
.board-row--head {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 700;
color: var(--muted);
padding-top: 8px;
padding-bottom: 10px;
border-bottom: 1px solid var(--line);
}
.rows .board-row {
border-bottom: 1px solid var(--line);
transition: background 0.16s ease;
}
.rows .board-row:hover { background: var(--sky-50); }
.rows .board-row:last-child { border-bottom: 0; }
.col-time {
font-size: 18px;
font-weight: 800;
letter-spacing: 0.01em;
font-variant-numeric: tabular-nums;
}
.col-time small {
display: block;
font-size: 11px;
font-weight: 600;
color: var(--danger);
text-decoration: line-through;
text-decoration-color: var(--line-2);
}
.col-flight { display: flex; flex-direction: column; }
.flight-no {
font-weight: 700;
font-size: 14.5px;
font-variant-numeric: tabular-nums;
letter-spacing: 0.02em;
}
.airline { font-size: 11.5px; color: var(--muted); font-weight: 500; }
.col-dest { display: flex; flex-direction: column; min-width: 0; }
.dest-city {
font-weight: 600;
font-size: 14.5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dest-code {
font-size: 11.5px;
color: var(--muted);
font-weight: 600;
letter-spacing: 0.04em;
}
.col-gate {
font-weight: 700;
font-size: 15px;
font-variant-numeric: tabular-nums;
}
.col-gate.tbd { color: var(--muted); font-weight: 600; }
/* Status pills */
.pill {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 12.5px;
font-weight: 700;
padding: 7px 12px;
border-radius: 999px;
letter-spacing: 0.01em;
white-space: nowrap;
width: fit-content;
}
.pill .pdot {
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
flex: none;
}
.pill.ontime { background: rgba(31, 157, 98, 0.12); color: var(--ok); }
.pill.boarding { background: rgba(31, 157, 98, 0.14); color: var(--boarding); }
.pill.boarding .pdot { animation: blink 1s steps(2, start) infinite; }
.pill.delayed { background: rgba(224, 150, 42, 0.14); color: var(--warn); }
.pill.departed { background: rgba(19, 35, 59, 0.07); color: var(--ink-2); }
.pill.cancelled { background: rgba(212, 73, 62, 0.12); color: var(--danger); }
.pill.landed { background: rgba(10, 102, 194, 0.1); color: var(--sky-d); }
@keyframes blink { 50% { opacity: 0.25; } }
/* Row enter / flip animation */
.rows .board-row { animation: rowin 0.32s ease both; }
@keyframes rowin {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: none; }
}
.flip {
display: inline-block;
animation: flip 0.42s ease;
}
@keyframes flip {
0% { transform: rotateX(0); }
45% { transform: rotateX(-90deg); opacity: 0.2; }
55% { transform: rotateX(90deg); opacity: 0.2; }
100% { transform: rotateX(0); opacity: 1; }
}
.empty {
text-align: center;
color: var(--muted);
font-size: 14px;
padding: 40px 20px;
margin: 0;
}
/* Footer */
.board-foot {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
padding: 14px 22px;
border-top: 1px solid var(--line);
background: var(--cloud);
font-size: 12.5px;
color: var(--muted);
font-weight: 600;
}
.legend { display: inline-flex; align-items: center; gap: 7px; }
.dot { width: 9px; height: 9px; border-radius: 50%; }
.dot.ok { background: var(--ok); }
.dot.boarding { background: var(--boarding); }
.dot.warn { background: var(--warn); }
.dot.danger { background: var(--danger); }
.updated { margin-left: auto; font-variant-numeric: tabular-nums; }
.updated time { color: var(--ink-2); }
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 24px);
background: var(--ink);
color: #fff;
font-size: 13.5px;
font-weight: 600;
padding: 12px 18px;
border-radius: 999px;
box-shadow: var(--shadow-md);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
z-index: 50;
max-width: calc(100vw - 40px);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* Responsive */
@media (max-width: 720px) {
.board-head { grid-template-columns: 1fr auto; gap: 12px; }
.clock { grid-column: 1 / -1; text-align: left; order: 3; }
.clock-time { font-size: 17px; }
}
@media (max-width: 520px) {
body { padding: 12px; }
.board-row {
grid-template-columns: 70px 1fr auto;
gap: 10px 12px;
padding: 14px 16px;
}
.board-row--head { display: none; }
.col-gate { display: none; }
.col-status { grid-column: 2 / -1; }
.rows .col-status { justify-self: start; margin-top: 4px; }
.col-time { font-size: 16px; }
.controls { padding: 14px 16px; }
.board-head { padding: 16px; }
.board-foot { padding: 14px 16px; gap: 12px; }
.updated { width: 100%; margin-left: 0; }
}(function () {
"use strict";
// --- Fictional flight data -------------------------------------------------
var DEPARTURES = [
{ time: "06:40", flight: "NW 218", airline: "Northwind Air", city: "London", code: "LHR", gate: "B12", status: "departed" },
{ time: "07:15", flight: "NW 644", airline: "Northwind Air", city: "Reykjavík", code: "KEF", gate: "B7", status: "boarding" },
{ time: "07:55", flight: "AR 1102", airline: "Aurora Lines", city: "Toronto", code: "YYZ", gate: "C3", status: "ontime" },
{ time: "08:20", flight: "NW 305", airline: "Northwind Air", city: "Paris", code: "CDG", gate: "B14", status: "ontime", delayed: "09:05" },
{ time: "08:45", flight: "NW 512", airline: "Northwind Air", city: "Amsterdam", code: "AMS", gate: "B9", status: "ontime" },
{ time: "09:10", flight: "PL 880", airline: "Polaris Jet", city: "Lisbon", code: "LIS", gate: null, status: "ontime" },
{ time: "09:30", flight: "NW 077", airline: "Northwind Air", city: "Dublin", code: "DUB", gate: "B5", status: "delayed", delayed: "10:25" },
{ time: "10:05", flight: "AR 2240", airline: "Aurora Lines", city: "Madrid", code: "MAD", gate: "C8", status: "ontime" },
{ time: "10:40", flight: "NW 419", airline: "Northwind Air", city: "Zürich", code: "ZRH", gate: "B2", status: "cancelled" },
{ time: "11:15", flight: "MS 631", airline: "Meridian Sky", city: "Rome", code: "FCO", gate: "C11", status: "ontime" },
{ time: "11:50", flight: "NW 188", airline: "Northwind Air", city: "Copenhagen", code: "CPH", gate: "B10", status: "ontime" },
{ time: "12:25", flight: "PL 904", airline: "Polaris Jet", city: "Oslo", code: "OSL", gate: null, status: "ontime" },
];
var ARRIVALS = [
{ time: "06:25", flight: "NW 217", airline: "Northwind Air", city: "London", code: "LHR", gate: "B12", status: "landed" },
{ time: "07:05", flight: "MS 410", airline: "Meridian Sky", city: "Munich", code: "MUC", gate: "C2", status: "landed" },
{ time: "07:40", flight: "AR 1101", airline: "Aurora Lines", city: "Toronto", code: "YYZ", gate: "C3", status: "ontime" },
{ time: "08:10", flight: "NW 306", airline: "Northwind Air", city: "Paris", code: "CDG", gate: "B14", status: "ontime" },
{ time: "08:35", flight: "PL 771", airline: "Polaris Jet", city: "Helsinki", code: "HEL", gate: "B6", status: "delayed", delayed: "09:20" },
{ time: "09:00", flight: "NW 511", airline: "Northwind Air", city: "Amsterdam", code: "AMS", gate: null, status: "ontime" },
{ time: "09:25", flight: "AR 2239", airline: "Aurora Lines", city: "Madrid", code: "MAD", gate: "C8", status: "ontime" },
{ time: "10:00", flight: "NW 420", airline: "Northwind Air", city: "Zürich", code: "ZRH", gate: "B2", status: "ontime" },
{ time: "10:35", flight: "MS 632", airline: "Meridian Sky", city: "Rome", code: "FCO", gate: "C11", status: "delayed", delayed: "11:30" },
{ time: "11:10", flight: "NW 189", airline: "Northwind Air", city: "Stockholm", code: "ARN", gate: "B8", status: "ontime" },
{ time: "11:45", flight: "PL 903", airline: "Polaris Jet", city: "Oslo", code: "OSL", gate: "B3", status: "ontime" },
{ time: "12:20", flight: "NW 622", airline: "Northwind Air", city: "Edinburgh", code: "EDI", gate: null, status: "ontime" },
];
var STATUS_LABEL = {
ontime: "On time",
boarding: "Boarding",
delayed: "Delayed",
departed: "Departed",
cancelled: "Cancelled",
landed: "Landed",
};
var FILTER_MATCH = {
all: function () { return true; },
boarding: function (s) { return s === "boarding"; },
ontime: function (s) { return s === "ontime"; },
delayed: function (s) { return s === "delayed"; },
};
// --- State -----------------------------------------------------------------
var state = { tab: "departures", filter: "all", query: "" };
var $ = function (sel, ctx) { return (ctx || document).querySelector(sel); };
var $$ = function (sel, ctx) { return Array.prototype.slice.call((ctx || document).querySelectorAll(sel)); };
var rowsEl = $("#rows");
var emptyEl = $("#empty");
var searchEl = $("#search");
var destHead = $("#destHead");
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2400);
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
function activeData() {
return state.tab === "departures" ? DEPARTURES : ARRIVALS;
}
function pillHtml(status) {
return '<span class="pill ' + status + '"><i class="pdot"></i>' + STATUS_LABEL[status] + "</span>";
}
function rowHtml(f) {
var time = f.status === "delayed" && f.delayed
? '<span class="col-time">' + f.delayed + "<small>" + f.time + "</small></span>"
: '<span class="col-time">' + f.time + "</span>";
var gate = f.gate
? '<span class="col-gate">' + escapeHtml(f.gate) + "</span>"
: '<span class="col-gate tbd">—</span>';
return (
'<div class="board-row" role="row" data-flight="' + escapeHtml(f.flight) + '">' +
time +
'<span class="col-flight">' +
'<span class="flight-no">' + escapeHtml(f.flight) + "</span>" +
'<span class="airline">' + escapeHtml(f.airline) + "</span>" +
"</span>" +
'<span class="col-dest">' +
'<span class="dest-city">' + escapeHtml(f.city) + "</span>" +
'<span class="dest-code">' + (state.tab === "departures" ? "JFK → " : "") + escapeHtml(f.code) + "</span>" +
"</span>" +
gate +
'<span class="col-status">' + pillHtml(f.status) + "</span>" +
"</div>"
);
}
function filtered() {
var q = state.query.trim().toLowerCase();
var pass = FILTER_MATCH[state.filter] || FILTER_MATCH.all;
return activeData().filter(function (f) {
if (!pass(f.status)) return false;
if (!q) return true;
return (
f.flight.toLowerCase().indexOf(q) > -1 ||
f.city.toLowerCase().indexOf(q) > -1 ||
f.code.toLowerCase().indexOf(q) > -1 ||
f.airline.toLowerCase().indexOf(q) > -1
);
});
}
function render() {
var list = filtered();
if (list.length === 0) {
rowsEl.innerHTML = "";
emptyEl.hidden = false;
return;
}
emptyEl.hidden = true;
rowsEl.innerHTML = list.map(rowHtml).join("");
}
// --- Tabs ------------------------------------------------------------------
$$(".tab").forEach(function (tab) {
tab.addEventListener("click", function () {
if (tab.dataset.tab === state.tab) return;
state.tab = tab.dataset.tab;
$$(".tab").forEach(function (t) {
var on = t === tab;
t.classList.toggle("is-active", on);
t.setAttribute("aria-selected", on ? "true" : "false");
});
destHead.textContent = state.tab === "departures" ? "Destination" : "Origin";
render();
});
});
// --- Filters ---------------------------------------------------------------
$$(".chip").forEach(function (chip) {
chip.addEventListener("click", function () {
state.filter = chip.dataset.filter;
$$(".chip").forEach(function (c) { c.classList.toggle("is-active", c === chip); });
render();
});
});
// --- Search ----------------------------------------------------------------
searchEl.addEventListener("input", function () {
state.query = searchEl.value;
render();
});
// --- Clock -----------------------------------------------------------------
function pad(n) { return n < 10 ? "0" + n : "" + n; }
function tickClock() {
var d = new Date();
$("#clock").textContent = pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds());
}
tickClock();
setInterval(tickClock, 1000);
// --- "Last updated" relative timer ----------------------------------------
var lastUpdate = Date.now();
function refreshUpdated() {
var secs = Math.floor((Date.now() - lastUpdate) / 1000);
var label;
if (secs < 5) label = "just now";
else if (secs < 60) label = secs + "s ago";
else label = Math.floor(secs / 60) + "m ago";
$("#updated").textContent = label;
}
setInterval(refreshUpdated, 5000);
// --- Live status updates (split-flap feel) --------------------------------
// Walk a small progression so the board feels alive without confusing data.
var PROGRESSION = { ontime: "boarding", boarding: "departed" };
function liveUpdate() {
var candidates = DEPARTURES.filter(function (f) {
return PROGRESSION[f.status] && f.status !== "cancelled";
});
if (candidates.length === 0) return;
var f = candidates[Math.floor(Math.random() * candidates.length)];
var next = PROGRESSION[f.status];
f.status = next;
lastUpdate = Date.now();
refreshUpdated();
// Re-render, then animate the changed pill with a flip.
render();
var row = rowsEl.querySelector('[data-flight="' + f.flight + '"] .pill');
if (row) {
row.classList.add("flip");
setTimeout(function () { row.classList.remove("flip"); }, 460);
}
if (next === "boarding") toast(f.flight + " to " + f.city + " is now boarding at gate " + (f.gate || "TBD"));
else if (next === "departed") toast(f.flight + " to " + f.city + " has departed");
}
setInterval(liveUpdate, 6500);
// --- Init ------------------------------------------------------------------
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Northwind Air — Flight Information Display</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>
<main class="fids" aria-label="Flight information display">
<header class="board-head">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17.8 19.2 16 11l3.5-3.5a2.5 2.5 0 0 0-3.5-3.5L12.5 7.5 4.3 5.7l-1.4 1.4 6.4 4-2.8 2.8-2.8-.7-1 1L6 17l1.3 3.4 1-1-.7-2.8 2.8-2.8 4 6.4z"/>
</svg>
</span>
<div class="brand-text">
<strong>Northwind Air</strong>
<span>JFK · Terminal 5</span>
</div>
</div>
<nav class="tabs" role="tablist" aria-label="Departures or arrivals">
<button class="tab is-active" role="tab" aria-selected="true" data-tab="departures">Departures</button>
<button class="tab" role="tab" aria-selected="false" data-tab="arrivals">Arrivals</button>
</nav>
<div class="clock" aria-live="off">
<span class="clock-label">Local time</span>
<time id="clock" class="clock-time">--:--:--</time>
</div>
</header>
<div class="controls">
<div class="search">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/>
</svg>
<input id="search" type="search" placeholder="Search flight, city or airport code…" aria-label="Search flights" autocomplete="off" />
</div>
<div class="filters" role="group" aria-label="Filter by status">
<button class="chip is-active" data-filter="all">All</button>
<button class="chip" data-filter="boarding">Boarding</button>
<button class="chip" data-filter="ontime">On time</button>
<button class="chip" data-filter="delayed">Delayed</button>
</div>
</div>
<section class="board" aria-label="Flight list">
<div class="board-row board-row--head" role="row">
<span class="col-time">Time</span>
<span class="col-flight">Flight</span>
<span class="col-dest" id="destHead">Destination</span>
<span class="col-gate">Gate</span>
<span class="col-status">Status</span>
</div>
<div id="rows" class="rows" role="rowgroup" aria-live="polite"></div>
<p id="empty" class="empty" hidden>No flights match your search.</p>
</section>
<footer class="board-foot">
<span class="legend"><i class="dot ok"></i>On time</span>
<span class="legend"><i class="dot boarding"></i>Boarding</span>
<span class="legend"><i class="dot warn"></i>Delayed</span>
<span class="legend"><i class="dot danger"></i>Cancelled</span>
<span class="updated">Last updated <time id="updated">just now</time></span>
</footer>
</main>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Flight Information Display (FIDS)
An airport-style flight information display for the fictional Northwind Air hub at JFK Terminal 5. The board pairs a deep aviation-blue header with a clean, status-forward table: each row shows a tabular-figure departure time, the flight number and operating carrier, the destination city with its airport code, the assigned gate, and a large color-coded status pill (On time, Boarding, Delayed, Departed, Cancelled, Landed).
Switch between the Departures and Arrivals tabs to swap the dataset and relabel the destination column to origin. Search by flight number, city, or airport code to filter instantly, or tap the status chips to narrow the list to boarding, on-time, or delayed flights. A live clock ticks in the header and a relative last-updated stamp sits in the footer beside the status legend.
The board updates itself on an interval: flights progress through realistic states and the changed status pill performs a split-flap flip animation, while a toast announces boarding calls and departures. Delayed flights show the new time with the original struck through. Everything is vanilla JavaScript with accessible roles, keyboard-usable controls, and a layout that collapses gracefully to a mobile-first passenger view below 520px.
Illustrative UI only — fictional airline, not a real booking or flight system.