diff --git a/flight-comparator/.gitignore b/flight-comparator/.gitignore
index 07f4698..b4161d0 100644
--- a/flight-comparator/.gitignore
+++ b/flight-comparator/.gitignore
@@ -46,10 +46,12 @@ htmlcov/
*.csv
*.log
-# JSON — keep fixture and airport data, ignore everything else
+# JSON — keep fixture, airport data, and frontend manifests
*.json
!data/airports_by_country.json
!tests/confirmed_flights.json
+!frontend/package.json
+!frontend/package-lock.json
# Database files
*.db
@@ -57,3 +59,4 @@ htmlcov/
# Node
frontend/node_modules/
frontend/dist/
+!frontend/src/lib/
diff --git a/flight-comparator/frontend/index.html b/flight-comparator/frontend/index.html
index 072a57e..697f724 100644
--- a/flight-comparator/frontend/index.html
+++ b/flight-comparator/frontend/index.html
@@ -4,7 +4,10 @@
-
- {/* Header */}
-
+ const showNewScan = location.pathname !== '/scans';
+ const pageTitle = getPageTitle(location.pathname);
+
+ return (
+
+
+ {/* ── Sidebar (desktop ≥ lg) ───────────────────────────────── */}
+
+
+ {/* ── Main content column ──────────────────────────────────── */}
+
+
+ {/* Top bar */}
+
+
+ {/* Mobile: wordmark */}
+
+
+ {/* Desktop: page title */}
+
+ {pageTitle}
+
+
+ {/* Actions */}
+ {showNewScan && (
+
+
+ New Scan
+
+ )}
+
+
+ {/* Page content */}
+
+
+
+
+
+
+
+ {/* ── Bottom nav (mobile < lg) ─────────────────────────────── */}
+
- {/* Main Content */}
-
-
-
);
}
diff --git a/flight-comparator/frontend/src/index.css b/flight-comparator/frontend/src/index.css
index 572f102..3412358 100644
--- a/flight-comparator/frontend/src/index.css
+++ b/flight-comparator/frontend/src/index.css
@@ -1,18 +1,81 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
+@import "tailwindcss";
+@theme {
+ /* Brand colours */
+ --color-primary: #1A73E8;
+ --color-primary-container: #D2E3FC;
+ --color-on-primary: #FFFFFF;
+ --color-on-primary-container: #003E8C;
+ --color-secondary: #0F9D58;
+ --color-secondary-container: #C8E6C9;
+ --color-on-secondary: #FFFFFF;
+ --color-on-secondary-container: #00391A;
+ --color-tertiary: #F4B400;
+ --color-tertiary-container: #FFF0C0;
+ --color-on-tertiary: #3B2A00;
+ --color-error: #D93025;
+ --color-error-container: #FDECEA;
+ --color-on-error: #FFFFFF;
+
+ /* Surface roles */
+ --color-bg: #F8F9FA;
+ --color-surface: #FFFFFF;
+ --color-surface-2: #F1F3F4;
+ --color-surface-variant: #E8EAED;
+ --color-on-surface: #202124;
+ --color-on-surface-variant: #5F6368;
+ --color-outline: #DADCE0;
+ --color-outline-variant: #F1F3F4;
+
+ /* Shape */
+ --radius-xs: 4px;
+ --radius-sm: 8px;
+ --radius-md: 12px;
+ --radius-lg: 16px;
+ --radius-xl: 20px;
+ --radius-full: 9999px;
+
+ /* Typography */
+ --font-sans: 'Google Sans', 'Roboto', system-ui, -apple-system, sans-serif;
+ --font-mono: 'Roboto Mono', 'JetBrains Mono', ui-monospace, monospace;
+
+ /* Elevation shadows */
+ --shadow-level-1: 0 1px 2px rgba(0,0,0,.08);
+ --shadow-level-2: 0 2px 6px rgba(0,0,0,.10);
+ --shadow-level-3: 0 4px 12px rgba(0,0,0,.12);
+}
+
+/* Base */
+body {
+ background-color: var(--color-bg);
+ color: var(--color-on-surface);
+ font-family: var(--font-sans);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* Skeleton shimmer */
+@keyframes shimmer {
+ 0% { background-position: -200% 0; }
+ 100% { background-position: 200% 0; }
+}
+.skeleton {
+ background: linear-gradient(90deg, #E8EAED 25%, #F1F3F4 50%, #E8EAED 75%);
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+}
+
+/* Slide-up (toasts) */
@keyframes slide-up {
- from {
- transform: translateY(100%);
- opacity: 0;
- }
- to {
- transform: translateY(0);
- opacity: 1;
- }
+ from { transform: translateY(100%); opacity: 0; }
+ to { transform: translateY(0); opacity: 1; }
}
+.animate-slide-up { animation: slide-up 0.3s ease-out; }
-.animate-slide-up {
- animation: slide-up 0.3s ease-out;
+/* Reduced motion */
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after {
+ animation-duration: 0.01ms !important;
+ transition-duration: 0.01ms !important;
+ }
}
diff --git a/flight-comparator/frontend/src/lib/utils.ts b/flight-comparator/frontend/src/lib/utils.ts
new file mode 100644
index 0000000..2819a83
--- /dev/null
+++ b/flight-comparator/frontend/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}