Files
Flights/design/flight-row-variants.html
T
Trey T e1b7fd4b0d History row: boarding-pass classic design
Replaces PassportFlightRow with the "Classic" boarding-pass design
selected from design/boarding-pass-variants.html.

Anatomy:
- BoardingPassShape: custom SwiftUI Shape — rounded rect with two
  semicircular cutouts at the perforation column (top + bottom).
  Hand-drawn clockwise from top-left so the path closes cleanly.
- Stub (88pt wide): orange linear-gradient background. "WN"
  monospaced eyebrow at 9pt/tracking 2.2 at top, padded flight
  number ("0007") at 28pt monospaced heavy in the middle,
  BarcodeStripe at the bottom.
- BarcodeStripe: Canvas-drawn faux barcode — 16-element width
  pattern cycles across the width, even indices fill, odd are gaps.
- Body (flex): card background. Route IATA pair at 24pt mono heavy
  with an orange ▶ between, date in 10pt tracked mono uppercase,
  meta row of EQP / TAIL / MI metadata with mono labels in tertiary
  ink and values in primary.
- Perforation: GeometryReader-driven dashed line drawn between stub
  and body, inset top/bottom to stop short of the cutouts.

Distance is recomputed inline via haversine from the AirportDatabase
since the row doesn't get the FlightHistoryStore passed in. Mile
display only — clean integer rounded value.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 18:08:55 -05:00

1332 lines
38 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Flight Row — Six Variants</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=Anton&family=Allerta+Stencil&family=Caveat:wght@500;700&family=DM+Mono:wght@400;500&family=Fraunces:opsz,wght@9..144,400;9..144,700;9..144,900&family=JetBrains+Mono:wght@400;500;700;800&family=VT323&display=swap" rel="stylesheet">
<style>
/* =========================================================
Variables — palette comes straight from the SwiftUI design
system the engineer has already committed to.
========================================================= */
:root {
--orange: #FF5722;
--orange-deep: #D9461A;
--orange-soft: #FF8C59;
--amber: #FFB938;
--navy: #0A1424;
--navy-mid: #142440;
--navy-soft: #0F1E38;
--navy-deep: #050A14;
--cream: #F4ECD8;
--cream-soft: #FAF5E8;
--cream-deep: #E8DDC4;
--kraft: #C9B68A;
--stamp-green: #2D5A3D;
--postal-red: #B53026;
--gold: #C8A951;
}
[data-theme="dark"] {
--bg: var(--navy);
--bg-2: var(--navy-deep);
--surface: var(--navy-mid);
--surface-2: var(--navy-soft);
--ink: #F4ECD8;
--ink-2: rgba(244, 236, 216, 0.65);
--ink-3: rgba(244, 236, 216, 0.4);
--hairline: rgba(255, 255, 255, 0.08);
--shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
--frame-bezel: #1a1a1a;
}
[data-theme="light"] {
--bg: var(--cream);
--bg-2: var(--cream-deep);
--surface: var(--cream-soft);
--surface-2: #FFFFFF;
--ink: #0A1424;
--ink-2: rgba(10, 20, 36, 0.65);
--ink-3: rgba(10, 20, 36, 0.45);
--hairline: rgba(0, 0, 0, 0.08);
--shadow: 0 8px 22px rgba(60, 30, 0, 0.12);
--frame-bezel: #d9d3c0;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
transition: background 280ms ease;
}
/* =========================================================
Page header
========================================================= */
.page {
max-width: 1240px;
margin: 0 auto;
padding: 32px 24px 96px;
}
.page-head {
display: flex;
align-items: baseline;
gap: 24px;
flex-wrap: wrap;
padding-bottom: 16px;
border-bottom: 1px solid var(--hairline);
margin-bottom: 40px;
}
.page-eyebrow {
font-family: "JetBrains Mono", monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--orange);
}
.page-title {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 144, "wght" 900;
font-size: 56px;
line-height: 1;
letter-spacing: -0.02em;
margin: 4px 0 0;
}
.page-sub {
font-size: 14px;
color: var(--ink-2);
max-width: 540px;
margin: 8px 0 0;
line-height: 1.5;
}
.theme-toggle {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
border: 1px solid var(--hairline);
border-radius: 999px;
cursor: pointer;
background: var(--surface);
color: var(--ink);
font-family: "JetBrains Mono", monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.15em;
text-transform: uppercase;
user-select: none;
}
.theme-toggle:hover { border-color: var(--orange); color: var(--orange); }
/* =========================================================
Grid of variants
========================================================= */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
gap: 48px;
}
.variant {
background: var(--bg-2);
border-radius: 28px;
padding: 28px 24px 36px;
border: 1px solid var(--hairline);
}
.variant-head {
display: flex;
align-items: baseline;
gap: 16px;
margin-bottom: 4px;
flex-wrap: wrap;
}
.variant-no {
font-family: "JetBrains Mono", monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.2em;
color: var(--orange);
}
.variant-name {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 96, "wght" 800;
font-size: 26px;
line-height: 1.1;
margin: 0;
}
.variant-note {
font-size: 13px;
color: var(--ink-2);
line-height: 1.5;
margin: 8px 0 24px;
max-width: 38ch;
}
/* iPhone-ish frame so the rows feel placed */
.phone {
width: 100%;
max-width: 394px;
margin: 0 auto;
background: var(--frame-bezel);
padding: 10px;
border-radius: 44px;
box-shadow: var(--shadow);
}
.phone-screen {
background: var(--bg);
border-radius: 36px;
padding: 18px 14px 22px;
position: relative;
overflow: hidden;
}
.island {
width: 92px; height: 28px;
background: #000;
border-radius: 16px;
margin: 0 auto 14px;
}
.rows {
display: flex;
flex-direction: column;
gap: 12px;
}
/* =========================================================
VARIANT 1 — BOARDING PASS STUB
A perforated row with a left "stub" and right "body".
Mono numerics. Subtle barcode strip. Felt-tip date stamp.
========================================================= */
.v1-row {
display: grid;
grid-template-columns: 88px 1fr;
background: var(--surface);
border-radius: 14px;
overflow: hidden;
position: relative;
min-height: 102px;
cursor: pointer;
transition: transform 180ms ease;
}
.v1-row:hover { transform: translateY(-1px); }
.v1-row::before {
/* Perforation between stub and body */
content: "";
position: absolute;
left: 88px;
top: 8px; bottom: 8px;
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--ink-3) 0 4px,
transparent 4px 8px
);
}
.v1-row::after {
/* Two punched holes top-bottom on the perforation */
content: "";
position: absolute;
left: 82px; top: -8px;
width: 14px; height: 14px;
background: var(--bg);
border-radius: 50%;
box-shadow: 0 110px 0 var(--bg);
}
.v1-stub {
background: linear-gradient(135deg, var(--orange) 0%, var(--orange-deep) 100%);
padding: 14px 12px 10px;
color: white;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.v1-stub-iata {
font-family: "JetBrains Mono", monospace;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.2em;
opacity: 0.85;
}
.v1-stub-flight {
font-family: "JetBrains Mono", monospace;
font-weight: 800;
font-size: 28px;
line-height: 1;
letter-spacing: -0.02em;
}
.v1-stub-bar {
/* Faux barcode */
height: 12px;
background-image: repeating-linear-gradient(
to right,
white 0 1px,
transparent 1px 3px,
white 3px 4px,
transparent 4px 7px,
white 7px 9px,
transparent 9px 11px
);
border-radius: 1px;
opacity: 0.95;
}
.v1-body {
padding: 14px 16px;
display: grid;
grid-template-columns: 1fr auto;
gap: 8px;
align-items: start;
}
.v1-route {
display: flex;
align-items: baseline;
gap: 8px;
}
.v1-iata {
font-family: "JetBrains Mono", monospace;
font-weight: 800;
font-size: 22px;
letter-spacing: -0.02em;
color: var(--ink);
}
.v1-arrow {
color: var(--orange);
font-weight: 900;
font-size: 13px;
}
.v1-date {
font-family: "JetBrains Mono", monospace;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.18em;
color: var(--ink-3);
text-transform: uppercase;
text-align: right;
}
.v1-meta {
grid-column: 1 / -1;
display: flex;
gap: 18px;
margin-top: 4px;
font-family: "JetBrains Mono", monospace;
font-size: 10px;
color: var(--ink-2);
}
.v1-meta dt {
display: inline;
color: var(--ink-3);
margin-right: 4px;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.v1-meta dd {
display: inline;
margin: 0 14px 0 0;
font-weight: 600;
color: var(--ink);
}
/* =========================================================
VARIANT 2 — HERO PHOTO DOMINANT
Wide airframe photo. Bottom gradient. Type overlaid.
Strongest visual identity per row.
========================================================= */
.v2-row {
position: relative;
border-radius: 14px;
overflow: hidden;
aspect-ratio: 16/9;
background: #1c2a40;
box-shadow: 0 1px 0 var(--hairline) inset;
cursor: pointer;
transition: transform 180ms ease;
}
.v2-row:hover { transform: translateY(-1px); }
.v2-photo {
position: absolute; inset: 0;
background-size: cover;
background-position: center;
filter: saturate(1.05) contrast(1.05);
}
.v2-scrim {
position: absolute; inset: 0;
background:
linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.0) 55%),
linear-gradient(to bottom, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0) 30%);
}
.v2-top {
position: absolute; top: 12px; left: 14px; right: 14px;
display: flex; justify-content: space-between; align-items: flex-start;
}
.v2-flight {
font-family: "Anton", sans-serif;
color: white;
font-size: 22px;
letter-spacing: 0.08em;
line-height: 1;
text-shadow: 0 2px 12px rgba(0,0,0,0.5);
}
.v2-date {
font-family: "JetBrains Mono", monospace;
color: white;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.2em;
text-transform: uppercase;
opacity: 0.95;
text-shadow: 0 2px 12px rgba(0,0,0,0.5);
text-align: right;
}
.v2-bottom {
position: absolute; bottom: 12px; left: 14px; right: 14px;
display: flex; align-items: flex-end; justify-content: space-between;
color: white;
}
.v2-route {
font-family: "Anton", sans-serif;
font-size: 38px;
letter-spacing: 0.04em;
line-height: 1;
display: flex;
align-items: baseline;
gap: 10px;
}
.v2-route .arrow {
color: var(--orange);
font-size: 22px;
transform: translateY(-4px);
}
.v2-pill {
background: rgba(255,255,255,0.18);
backdrop-filter: blur(8px);
padding: 5px 10px;
border-radius: 999px;
font-family: "JetBrains Mono", monospace;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
/* =========================================================
VARIANT 3 — LUGGAGE TAG
Off-cream tag shape. Eyelet. Italic serif "destination" line.
Punched-card aesthetic. Slight rotation for charm.
========================================================= */
.v3-row {
background:
radial-gradient(circle at 28px 24px, transparent 9px, var(--kraft) 9.5px),
var(--kraft);
color: #1a1408;
border-radius: 14px 14px 14px 22px;
padding: 18px 18px 16px 50px;
position: relative;
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.25),
0 1px 0 rgba(0,0,0,0.05);
min-height: 102px;
cursor: pointer;
transition: transform 180ms ease;
}
.v3-row:hover { transform: translateY(-1px); }
.v3-row::before {
/* Eyelet ring */
content: "";
position: absolute;
left: 18px; top: 14px;
width: 20px; height: 20px;
border-radius: 50%;
box-shadow:
inset 0 0 0 1.5px rgba(0,0,0,0.25),
inset 0 0 0 4px var(--kraft);
background: var(--bg);
}
.v3-row::after {
/* Subtle noise / paper texture */
content: "";
position: absolute; inset: 0;
background-image: radial-gradient(rgba(0,0,0,0.05) 1px, transparent 1.5px);
background-size: 4px 4px;
border-radius: inherit;
pointer-events: none;
mix-blend-mode: multiply;
opacity: 0.5;
}
.v3-eyebrow {
font-family: "JetBrains Mono", monospace;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
color: rgba(26,20,8,0.55);
margin-bottom: 6px;
}
.v3-route {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 144, "wght" 900;
font-size: 26px;
line-height: 1;
letter-spacing: -0.02em;
margin-bottom: 4px;
}
.v3-route em {
color: var(--orange-deep);
font-style: italic;
font-variation-settings: "opsz" 144, "wght" 700;
}
.v3-info {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-top: 10px;
}
.v3-flight {
font-family: "JetBrains Mono", monospace;
font-weight: 700;
font-size: 12px;
letter-spacing: 0.1em;
color: rgba(26,20,8,0.8);
}
.v3-date {
font-family: "Fraunces", serif;
font-style: italic;
font-size: 13px;
color: rgba(26,20,8,0.6);
}
/* =========================================================
VARIANT 4 — SPLIT-FLAP BOARD
Solari mechanical timetable. Pure black. Amber pixel font.
Each "letter" in a tiny flap-style chiclet.
========================================================= */
.v4-row {
background: #0a0a0a;
border-radius: 14px;
padding: 14px 16px 16px;
cursor: pointer;
border: 1px solid #1f1f1f;
transition: transform 180ms ease;
position: relative;
overflow: hidden;
}
.v4-row:hover { transform: translateY(-1px); }
/* Scanline effect for CRT/board vibe */
.v4-row::before {
content: "";
position: absolute;
inset: 0;
background-image: repeating-linear-gradient(
to bottom,
rgba(255, 255, 255, 0.025) 0 1px,
transparent 1px 3px
);
pointer-events: none;
}
.v4-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px dashed rgba(255, 185, 56, 0.2);
}
.v4-tag {
font-family: "VT323", monospace;
font-size: 14px;
letter-spacing: 0.16em;
color: var(--amber);
text-transform: uppercase;
}
.v4-date {
font-family: "VT323", monospace;
color: rgba(255, 185, 56, 0.6);
font-size: 14px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.v4-route {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.v4-iata {
display: flex; gap: 2px;
}
.v4-flap {
background: #1c1c1c;
color: var(--amber);
width: 26px;
height: 38px;
display: flex;
align-items: center;
justify-content: center;
font-family: "VT323", monospace;
font-size: 30px;
line-height: 1;
border-radius: 2px;
box-shadow:
inset 0 -1px 0 rgba(0,0,0,0.4),
inset 0 1px 0 rgba(255,185,56,0.05);
position: relative;
}
.v4-flap::after {
/* mid-line gap to simulate split flap */
content: "";
position: absolute;
left: 0; right: 0; top: 50%;
height: 1px;
background: rgba(0,0,0,0.6);
transform: translateY(-0.5px);
}
.v4-arrow {
color: var(--amber);
font-family: "VT323", monospace;
font-size: 26px;
}
.v4-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 14px;
color: rgba(255, 185, 56, 0.85);
font-family: "VT323", monospace;
font-size: 16px;
letter-spacing: 0.06em;
}
/* =========================================================
VARIANT 5 — POSTCARD STAMP
Square photo as the "postcard front" on the left.
Postage stamp with airline. Cancellation circle stamp.
Date in postal typography.
========================================================= */
.v5-row {
background: var(--surface);
border-radius: 14px;
overflow: hidden;
display: grid;
grid-template-columns: 116px 1fr;
position: relative;
cursor: pointer;
transition: transform 180ms ease;
min-height: 116px;
}
.v5-row:hover { transform: translateY(-1px); }
.v5-photo {
background-size: cover;
background-position: center;
background-color: #1c2a40;
position: relative;
}
.v5-stamp {
/* Postage stamp w/ serrated edge using radial-gradient mask */
position: absolute;
top: 8px; left: 8px;
width: 38px; height: 46px;
background:
linear-gradient(135deg, var(--orange), var(--orange-deep));
color: white;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: "Allerta Stencil", sans-serif;
font-size: 8px;
letter-spacing: 0.15em;
line-height: 1;
gap: 3px;
/* faux serrated edge via box-shadow steps */
box-shadow:
0 0 0 2px var(--surface),
0 1px 6px rgba(0,0,0,0.3);
}
.v5-stamp-num {
font-family: "JetBrains Mono", monospace;
font-weight: 800;
font-size: 14px;
letter-spacing: 0;
}
.v5-stamp-wn {
font-size: 7px;
}
.v5-body {
padding: 14px 16px;
position: relative;
}
.v5-cancel {
/* Cancellation stamp circle */
position: absolute;
top: 10px; right: 10px;
width: 64px; height: 64px;
border-radius: 50%;
border: 1.5px solid var(--postal-red);
color: var(--postal-red);
display: flex;
align-items: center;
justify-content: center;
font-family: "Allerta Stencil", sans-serif;
font-size: 9px;
letter-spacing: 0.12em;
text-transform: uppercase;
text-align: center;
line-height: 1.1;
transform: rotate(-12deg);
opacity: 0.85;
background:
repeating-linear-gradient(
45deg,
transparent 0 6px,
rgba(181, 48, 38, 0.05) 6px 8px
);
}
[data-theme="dark"] .v5-cancel {
border-color: #FF8567;
color: #FF8567;
}
.v5-route {
font-family: "Fraunces", serif;
font-variation-settings: "opsz" 144, "wght" 900;
font-size: 22px;
letter-spacing: -0.02em;
margin: 0 80px 6px 0;
line-height: 1.05;
}
.v5-meta {
font-family: "JetBrains Mono", monospace;
font-size: 11px;
color: var(--ink-2);
letter-spacing: 0.05em;
margin-bottom: 8px;
}
.v5-stack {
display: flex;
gap: 10px;
font-family: "JetBrains Mono", monospace;
font-size: 10px;
color: var(--ink);
font-weight: 700;
}
.v5-stack span {
background: var(--bg);
padding: 4px 8px;
border-radius: 4px;
letter-spacing: 0.08em;
}
/* =========================================================
VARIANT 6 — BRUTALIST MONO
No photo. Pure typography. Aggressive hierarchy. Reads
like a price-list or a Jasper Morrison product card.
========================================================= */
.v6-row {
background: var(--surface);
border-radius: 14px;
padding: 16px 16px 16px 18px;
cursor: pointer;
transition: transform 180ms ease;
border-left: 4px solid var(--orange);
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto auto auto;
column-gap: 16px;
row-gap: 4px;
}
.v6-row:hover { transform: translateY(-1px); }
.v6-route {
font-family: "JetBrains Mono", monospace;
font-weight: 800;
font-size: 32px;
letter-spacing: -0.04em;
line-height: 1;
grid-column: 1;
grid-row: 1 / span 2;
color: var(--ink);
}
.v6-route .sep {
color: var(--orange);
margin: 0 4px;
font-weight: 400;
}
.v6-flight {
grid-column: 2;
grid-row: 1;
font-family: "JetBrains Mono", monospace;
font-weight: 800;
font-size: 14px;
letter-spacing: 0.08em;
color: var(--ink);
text-align: right;
}
.v6-date {
grid-column: 2;
grid-row: 2;
font-family: "JetBrains Mono", monospace;
font-size: 11px;
color: var(--ink-3);
letter-spacing: 0.1em;
text-align: right;
}
.v6-tags {
grid-column: 1 / -1;
grid-row: 3;
display: flex;
gap: 10px;
margin-top: 6px;
padding-top: 10px;
border-top: 1px solid var(--hairline);
font-family: "JetBrains Mono", monospace;
font-size: 10px;
color: var(--ink-2);
letter-spacing: 0.08em;
}
.v6-tags em {
color: var(--orange);
font-style: normal;
font-weight: 800;
}
/* =========================================================
Footer note
========================================================= */
.footer {
margin-top: 64px;
padding-top: 24px;
border-top: 1px solid var(--hairline);
font-size: 12px;
color: var(--ink-3);
line-height: 1.6;
}
.footer code {
font-family: "JetBrains Mono", monospace;
background: var(--surface);
padding: 2px 6px;
border-radius: 4px;
color: var(--orange);
}
/* =========================================================
Tiny utilities
========================================================= */
.skel {
background: linear-gradient(90deg,
var(--surface-2) 25%,
var(--surface) 50%,
var(--surface-2) 75%);
background-size: 200% 100%;
animation: skel 1.4s ease-in-out infinite;
}
@keyframes skel { from { background-position: 200% 0 } to { background-position: -200% 0 } }
</style>
</head>
<body>
<div class="page">
<header class="page-head">
<div>
<div class="page-eyebrow">FLIGHT ROW · SIX DIRECTIONS</div>
<h1 class="page-title">Make the row earn its rent</h1>
<p class="page-sub">Six aesthetic directions for the History tab's flight row, each executed as 3 stacked rows so you can see how they sit in a feed. Pick one — or mix elements across — and we'll port it back to SwiftUI.</p>
</div>
<button class="theme-toggle" id="themeToggle" type="button">
<span id="themeLabel">DARK</span>
</button>
</header>
<div class="grid">
<!-- ============================================
V1 — BOARDING PASS STUB
============================================ -->
<section class="variant">
<div class="variant-head">
<span class="variant-no">01</span>
<h2 class="variant-name">Boarding Pass Stub</h2>
</div>
<p class="variant-note">Perforated tear-line, mono numerics, faux barcode strip. Reads like the stub you'd tear off at the gate. Most "I flew this" of the bunch.</p>
<div class="phone">
<div class="phone-screen">
<div class="island"></div>
<div class="rows">
<!-- Row 1 -->
<div class="v1-row">
<div class="v1-stub">
<div class="v1-stub-iata">WN</div>
<div class="v1-stub-flight">0007</div>
<div class="v1-stub-bar"></div>
</div>
<div class="v1-body">
<div class="v1-route">
<span class="v1-iata">DAL</span>
<span class="v1-arrow"></span>
<span class="v1-iata">HOU</span>
</div>
<div class="v1-date">27 MAY 26</div>
<dl class="v1-meta">
<dt>EQP</dt><dd>B737</dd>
<dt>TAIL</dt><dd>N7747C</dd>
<dt>NM</dt><dd>239</dd>
</dl>
</div>
</div>
<!-- Row 2 -->
<div class="v1-row">
<div class="v1-stub">
<div class="v1-stub-iata">WN</div>
<div class="v1-stub-flight">1942</div>
<div class="v1-stub-bar"></div>
</div>
<div class="v1-body">
<div class="v1-route">
<span class="v1-iata">LAS</span>
<span class="v1-arrow"></span>
<span class="v1-iata">DAL</span>
</div>
<div class="v1-date">27 JAN 24</div>
<dl class="v1-meta">
<dt>EQP</dt><dd>B738</dd>
<dt>TAIL</dt><dd>N281WN</dd>
<dt>NM</dt><dd>1056</dd>
</dl>
</div>
</div>
<!-- Row 3 -->
<div class="v1-row">
<div class="v1-stub">
<div class="v1-stub-iata">WN</div>
<div class="v1-stub-flight">5476</div>
<div class="v1-stub-bar"></div>
</div>
<div class="v1-body">
<div class="v1-route">
<span class="v1-iata">BNA</span>
<span class="v1-arrow"></span>
<span class="v1-iata">BOS</span>
</div>
<div class="v1-date">19 MAR 24</div>
<dl class="v1-meta">
<dt>EQP</dt><dd>B38M</dd>
<dt>TAIL</dt><dd>N8642E</dd>
<dt>NM</dt><dd>943</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ============================================
V2 — HERO PHOTO DOMINANT
============================================ -->
<section class="variant">
<div class="variant-head">
<span class="variant-no">02</span>
<h2 class="variant-name">Hero Photo Dominant</h2>
</div>
<p class="variant-note">Full-width airframe photo. Bottom gradient. Anton display sets the route in capital letters. The most magazine-like.</p>
<div class="phone">
<div class="phone-screen">
<div class="island"></div>
<div class="rows">
<div class="v2-row">
<div class="v2-photo" style="background-image:url('https://images.unsplash.com/photo-1583416750470-965b2707b355?auto=format&fit=crop&w=900&q=70')"></div>
<div class="v2-scrim"></div>
<div class="v2-top">
<div class="v2-flight">WN 0007</div>
<div class="v2-date">MAY 27 · 2026</div>
</div>
<div class="v2-bottom">
<div class="v2-route">DAL <span class="arrow"></span> HOU</div>
<div class="v2-pill">239 NM · B737</div>
</div>
</div>
<div class="v2-row">
<div class="v2-photo" style="background-image:url('https://images.unsplash.com/photo-1542296332-2e4473faf563?auto=format&fit=crop&w=900&q=70')"></div>
<div class="v2-scrim"></div>
<div class="v2-top">
<div class="v2-flight">WN 1942</div>
<div class="v2-date">JAN 27 · 2024</div>
</div>
<div class="v2-bottom">
<div class="v2-route">LAS <span class="arrow"></span> DAL</div>
<div class="v2-pill">1056 NM · B738</div>
</div>
</div>
<div class="v2-row">
<!-- Skeleton fallback row -->
<div class="v2-photo skel"></div>
<div class="v2-scrim" style="background:linear-gradient(to top,rgba(0,0,0,0.85),rgba(0,0,0,0) 55%)"></div>
<div class="v2-top">
<div class="v2-flight">WN 5476</div>
<div class="v2-date">MAR 19 · 2024</div>
</div>
<div class="v2-bottom">
<div class="v2-route">BNA <span class="arrow"></span> BOS</div>
<div class="v2-pill">943 NM · B38M</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ============================================
V3 — LUGGAGE TAG
============================================ -->
<section class="variant">
<div class="variant-head">
<span class="variant-no">03</span>
<h2 class="variant-name">Luggage Tag</h2>
</div>
<p class="variant-note">Kraft-paper colorway, eyelet rivet, Fraunces italic for the destination. Reads like a souvenir glued in a scrapbook. Cleanly distinct from anything else in the app.</p>
<div class="phone">
<div class="phone-screen">
<div class="island"></div>
<div class="rows">
<div class="v3-row">
<div class="v3-eyebrow">SOUTHWEST · WN 7</div>
<div class="v3-route">DAL <em>to</em> HOU</div>
<div class="v3-info">
<span class="v3-flight">B737 · N7747C · 239 mi</span>
<span class="v3-date">May 27, 2026</span>
</div>
</div>
<div class="v3-row">
<div class="v3-eyebrow">SOUTHWEST · WN 1942</div>
<div class="v3-route">LAS <em>to</em> DAL</div>
<div class="v3-info">
<span class="v3-flight">B738 · N281WN · 1056 mi</span>
<span class="v3-date">Jan 27, 2024</span>
</div>
</div>
<div class="v3-row">
<div class="v3-eyebrow">SOUTHWEST · WN 1136</div>
<div class="v3-route">SJU <em>to</em> MCO</div>
<div class="v3-info">
<span class="v3-flight">B738 · N280WN · 1187 mi</span>
<span class="v3-date">Apr 11, 2022</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ============================================
V4 — SPLIT-FLAP BOARD
============================================ -->
<section class="variant">
<div class="variant-head">
<span class="variant-no">04</span>
<h2 class="variant-name">Split-Flap Departures</h2>
</div>
<p class="variant-note">Solari mechanical aesthetic. Pure black, amber pixel-font, faux scanlines, individual character chiclets with a split line. Lives outside light/dark mode by design.</p>
<div class="phone">
<div class="phone-screen">
<div class="island"></div>
<div class="rows">
<div class="v4-row">
<div class="v4-head">
<span class="v4-tag">WN 0007</span>
<span class="v4-date">27 MAY 26</span>
</div>
<div class="v4-route">
<div class="v4-iata">
<div class="v4-flap">D</div><div class="v4-flap">A</div><div class="v4-flap">L</div>
</div>
<span class="v4-arrow">▶▶▶</span>
<div class="v4-iata">
<div class="v4-flap">H</div><div class="v4-flap">O</div><div class="v4-flap">U</div>
</div>
</div>
<div class="v4-meta">
<span>B737</span>
<span>N7747C</span>
<span>239 NM</span>
</div>
</div>
<div class="v4-row">
<div class="v4-head">
<span class="v4-tag">WN 1942</span>
<span class="v4-date">27 JAN 24</span>
</div>
<div class="v4-route">
<div class="v4-iata">
<div class="v4-flap">L</div><div class="v4-flap">A</div><div class="v4-flap">S</div>
</div>
<span class="v4-arrow">▶▶▶</span>
<div class="v4-iata">
<div class="v4-flap">D</div><div class="v4-flap">A</div><div class="v4-flap">L</div>
</div>
</div>
<div class="v4-meta">
<span>B738</span>
<span>N281WN</span>
<span>1056 NM</span>
</div>
</div>
<div class="v4-row">
<div class="v4-head">
<span class="v4-tag">WN 5476</span>
<span class="v4-date">19 MAR 24</span>
</div>
<div class="v4-route">
<div class="v4-iata">
<div class="v4-flap">B</div><div class="v4-flap">N</div><div class="v4-flap">A</div>
</div>
<span class="v4-arrow">▶▶▶</span>
<div class="v4-iata">
<div class="v4-flap">B</div><div class="v4-flap">O</div><div class="v4-flap">S</div>
</div>
</div>
<div class="v4-meta">
<span>B38M</span>
<span>N8642E</span>
<span>943 NM</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ============================================
V5 — POSTCARD STAMP
============================================ -->
<section class="variant">
<div class="variant-head">
<span class="variant-no">05</span>
<h2 class="variant-name">Postcard + Cancellation</h2>
</div>
<p class="variant-note">Photo as postcard front. Postage stamp overlays it. Red cancellation circle on the body shouts the route. Most "collected stamp in a passport" of all six.</p>
<div class="phone">
<div class="phone-screen">
<div class="island"></div>
<div class="rows">
<div class="v5-row">
<div class="v5-photo" style="background-image:url('https://images.unsplash.com/photo-1583416750470-965b2707b355?auto=format&fit=crop&w=600&q=70')">
<div class="v5-stamp">
<span class="v5-stamp-num">WN7</span>
<span class="v5-stamp-wn">★ ★ ★</span>
</div>
</div>
<div class="v5-body">
<div class="v5-cancel">DAL · HOU<br>MAY 27 26</div>
<h3 class="v5-route">Dallas to Houston</h3>
<p class="v5-meta">239 mi · 50 m airborne</p>
<div class="v5-stack">
<span>B737</span><span>N7747C</span>
</div>
</div>
</div>
<div class="v5-row">
<div class="v5-photo" style="background-image:url('https://images.unsplash.com/photo-1436491865332-7a61a109cc05?auto=format&fit=crop&w=600&q=70')">
<div class="v5-stamp">
<span class="v5-stamp-num">WN1942</span>
<span class="v5-stamp-wn">★ ★ ★</span>
</div>
</div>
<div class="v5-body">
<div class="v5-cancel">LAS · DAL<br>JAN 27 24</div>
<h3 class="v5-route">Las Vegas to Dallas</h3>
<p class="v5-meta">1,056 mi · 2h 14m airborne</p>
<div class="v5-stack">
<span>B738</span><span>N281WN</span>
</div>
</div>
</div>
<div class="v5-row">
<div class="v5-photo skel"></div>
<div class="v5-body">
<div class="v5-cancel">BNA · BOS<br>MAR 19 24</div>
<h3 class="v5-route">Nashville to Boston</h3>
<p class="v5-meta">943 mi · 2h 08m airborne</p>
<div class="v5-stack">
<span>B38M</span><span>N8642E</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ============================================
V6 — BRUTALIST MONO
============================================ -->
<section class="variant">
<div class="variant-head">
<span class="variant-no">06</span>
<h2 class="variant-name">Brutalist Mono</h2>
</div>
<p class="variant-note">No photo. No chrome. Type does all the work. Reads like a Swiss timetable or a Jasper Morrison product card. Pure information density.</p>
<div class="phone">
<div class="phone-screen">
<div class="island"></div>
<div class="rows">
<div class="v6-row">
<div class="v6-route">DAL<span class="sep">/</span>HOU</div>
<div class="v6-flight">WN0007</div>
<div class="v6-date">27.05.26</div>
<div class="v6-tags">
<span>B737</span>·
<span>N7747C</span>·
<span><em>239</em> nm</span>·
<span>2nd time on this airframe</span>
</div>
</div>
<div class="v6-row">
<div class="v6-route">LAS<span class="sep">/</span>DAL</div>
<div class="v6-flight">WN1942</div>
<div class="v6-date">27.01.24</div>
<div class="v6-tags">
<span>B738</span>·
<span>N281WN</span>·
<span><em>1056</em> nm</span>
</div>
</div>
<div class="v6-row">
<div class="v6-route">BNA<span class="sep">/</span>BOS</div>
<div class="v6-flight">WN5476</div>
<div class="v6-date">19.03.24</div>
<div class="v6-tags">
<span>B38M</span>·
<span>N8642E</span>·
<span><em>943</em> nm</span>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
<footer class="footer">
Tap the toggle to switch theme. All six are buildable in SwiftUI — variant 4 (split-flap) needs the most custom views, variant 6 (brutalist mono) needs the least. Photo-dominant variants assume planespotters photos load (~50150KB ea.). Skeleton states are shown for variants 2 + 5. Edit <code>flight-row-variants.html</code> to tweak.
</footer>
</div>
<script>
const root = document.documentElement;
const btn = document.getElementById('themeToggle');
const label = document.getElementById('themeLabel');
btn.addEventListener('click', () => {
const next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
root.setAttribute('data-theme', next);
label.textContent = next.toUpperCase();
});
</script>
</body>
</html>