Skip to content

Instantly share code, notes, and snippets.

@milushov
Last active May 23, 2026 13:45
Show Gist options
  • Select an option

  • Save milushov/b4a8a1b1c1c9850edaa728a2216115ce to your computer and use it in GitHub Desktop.

Select an option

Save milushov/b4a8a1b1c1c9850edaa728a2216115ce to your computer and use it in GitHub Desktop.
Lapster — Notifications color direction (3 token-only variants, light + dark)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lapster — Notifications Color Proposals</title>
<style>
/* ---------- LapsterTheme tokens (mirrored from ios/Lapster/Common/Theme/LapsterTheme.swift) ---------- */
:root {
/* Light scheme */
--surfacePrimary: #FFFFFF;
--surfaceGrouped: #F2F2F7;
--surfaceSecondary: #FFFFFF;
--surfaceTertiary: #F2F2F7;
--surfaceElevated: #FFFFFF;
--surfaceQuiet: #F2F2F7; /* systemGray6 */
--surfaceMuted: #E5E5EA; /* systemGray5 */
--surfaceFaint: #D1D1D6; /* systemGray4 */
--surfaceInverse: #1C1C1E; /* near-black in light */
--surfaceInverseQuiet: #2C2C2E;
--contentPrimary: #000000;
--contentSecondary: rgba(60,60,67,.60);
--contentTertiary: rgba(60,60,67,.30);
--contentInverse: #FFFFFF;
--contentInverseSecondary: rgba(235,235,245,.60);
--divider: rgba(60,60,67,.29);
--accent: #007AFF;
--success: #34C759;
--warning: #FF9500;
--error: #FF3B30;
--info: #007AFF;
--bestLap: #FF3B30;
--bestLapRowTint: rgba(255,59,48,.05);
/* Spacing & radius */
--sp-xs: 4px; --sp-sm: 8px; --sp-md: 12px; --sp-lg: 16px; --sp-xl: 20px;
--r-sm: 8px; --r-md: 12px; --r-lg: 16px;
--opacity-iconBg: 0.12;
--opacity-subtle: 0.10;
--opacity-verySubtle: 0.05;
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro", "Helvetica Neue", system-ui, sans-serif;
}
/* ---------- Page chrome ---------- */
* { box-sizing: border-box; }
html, body {
margin: 0;
font-family: var(--font-sans);
color: #111;
background: #FAFAFB;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
body { padding: 40px 24px 80px; }
.page-header {
max-width: 1280px;
margin: 0 auto 32px;
}
.page-header h1 {
font-size: 28px;
margin: 0 0 8px;
letter-spacing: -0.02em;
font-weight: 700;
}
.page-header p {
margin: 0;
color: #555;
font-size: 15px;
line-height: 1.5;
max-width: 760px;
}
.legend {
max-width: 1280px;
margin: 24px auto 32px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
}
.legend-card {
background: #fff;
border: 1px solid #EEE;
border-radius: 12px;
padding: 14px 16px;
font-size: 13px;
line-height: 1.5;
}
.legend-card b { display: block; font-size: 13.5px; margin-bottom: 4px; }
.legend-card .swatch-row { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
.swatch {
display: inline-block;
width: 18px; height: 18px;
border-radius: 4px;
border: 1px solid rgba(0,0,0,.08);
}
/* ---------- Variant grid ---------- */
.variants {
max-width: 1280px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 28px;
}
@media (max-width: 1100px) { .variants { grid-template-columns: 1fr; } }
.variant {
background: #FFFFFF;
border-radius: 20px;
padding: 20px;
border: 1px solid #ECECEE;
}
.variant h2 {
font-size: 18px;
margin: 0 0 4px;
letter-spacing: -0.01em;
}
.variant .tag { font-size: 12px; color: #888; margin-bottom: 14px; }
.phones { display: flex; flex-direction: column; gap: 16px; }
/* ---------- "Phone" frames ---------- */
.phone {
border-radius: 28px;
padding: 14px;
border: 1px solid var(--divider);
background: var(--surfaceGrouped);
overflow: hidden;
color: var(--contentPrimary);
}
.phone.dark {
--surfacePrimary: #000000;
--surfaceGrouped: #000000;
--surfaceSecondary: #1C1C1E;
--surfaceTertiary: #2C2C2E;
--surfaceElevated: #1C1C1E;
--surfaceQuiet: #1C1C1E;
--surfaceMuted: #2C2C2E;
--surfaceFaint: #3A3A3C;
--surfaceInverse: #FFFFFF;
--surfaceInverseQuiet: #EBEBF0;
--contentPrimary: #FFFFFF;
--contentSecondary: rgba(235,235,245,.60);
--contentTertiary: rgba(235,235,245,.30);
--contentInverse: #000000;
--contentInverseSecondary: rgba(60,60,67,.60);
--divider: rgba(84,84,88,.65);
--bestLapRowTint: rgba(255,69,58,.08);
}
.phone-bar {
display: flex; align-items: center; justify-content: space-between;
padding: 4px 6px 12px;
font-size: 17px;
font-weight: 600;
letter-spacing: -0.01em;
}
.phone-bar .x {
width: 28px; height: 28px;
border-radius: 50%;
background: var(--surfaceQuiet);
color: var(--contentSecondary);
font-size: 14px;
display: flex; align-items: center; justify-content: center;
}
.phone-list {
background: var(--surfaceSecondary);
border-radius: 14px;
overflow: hidden;
}
/* ---------- Notification row (shared) ---------- */
.row {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 14px;
border-bottom: 0.5px solid var(--divider);
}
.row:last-child { border-bottom: none; }
.icon-tile {
flex: 0 0 36px;
width: 36px; height: 36px;
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
font-size: 16px;
position: relative;
}
.avatar {
flex: 0 0 36px;
width: 36px; height: 36px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
color: #fff; font-size: 14px; font-weight: 700;
background: linear-gradient(135deg, #4F46E5, #EC4899);
}
.avatar.a2 { background: linear-gradient(135deg, #0EA5E9, #6366F1); }
.avatar.a3 { background: linear-gradient(135deg, #F59E0B, #EF4444); }
.avatar.a4 { background: linear-gradient(135deg, #10B981, #06B6D4); }
.avatar.a5 { background: linear-gradient(135deg, #8B5CF6, #EC4899); }
.avatar.a6 { background: linear-gradient(135deg, #14B8A6, #4F46E5); }
.body { flex: 1; min-width: 0; }
.msg {
font-size: 14.5px;
line-height: 1.35;
color: var(--contentPrimary);
margin: 0 0 3px;
}
.msg b { font-weight: 600; }
.meta {
font-size: 12px;
color: var(--contentSecondary);
}
.meta .dot {
display: inline-block;
width: 4px; height: 4px;
background: var(--contentTertiary);
border-radius: 50%;
margin: 0 6px;
vertical-align: middle;
}
.unread-dot {
width: 8px; height: 8px;
border-radius: 50%;
margin-top: 14px;
flex: 0 0 8px;
}
/* SF Symbols via SVG (system-style icons) */
.icn {
width: 18px; height: 18px;
stroke-width: 1.7;
stroke: currentColor;
fill: none;
stroke-linecap: round; stroke-linejoin: round;
}
.icn.solid { fill: currentColor; stroke: none; }
/* ---------- Variant A: Monochrome Calm ---------- */
.v-a .icon-tile {
background: var(--surfaceQuiet);
color: var(--contentSecondary);
}
.v-a .icon-tile .pip {
position: absolute;
bottom: -2px; right: -2px;
width: 12px; height: 12px;
border-radius: 50%;
border: 2px solid var(--surfaceSecondary);
}
.v-a .pip.social { background: var(--accent); }
.v-a .pip.performance { background: var(--bestLap); }
.v-a .pip.sharing { background: var(--contentSecondary); }
.v-a .pip.teams { background: var(--success); }
.v-a .pip.story { background: var(--warning); }
.v-a .unread-dot { background: var(--contentPrimary); }
/* ---------- Variant B: Semantic Tints (4 buckets, theme-derived) ---------- */
.v-b .icon-tile.social { background: rgba(0,122,255,.10); color: var(--accent); }
.v-b .icon-tile.performance { background: var(--bestLapRowTint); color: var(--bestLap); }
.v-b .icon-tile.sharing { background: var(--surfaceMuted); color: var(--contentPrimary); }
.v-b .icon-tile.teams { background: rgba(52,199,89,.12); color: var(--success); }
.v-b .icon-tile.story { background: rgba(255,149,0,.12); color: var(--warning); }
.v-b .unread-dot.social { background: var(--accent); }
.v-b .unread-dot.performance { background: var(--bestLap); }
.v-b .unread-dot.sharing { background: var(--contentSecondary); }
.v-b .unread-dot.teams { background: var(--success); }
.v-b .unread-dot.story { background: var(--warning); }
/* ---------- Variant C: Inverse Hero — neutral default + loud-without-chroma for moments ---------- */
.v-c .icon-tile {
background: var(--surfaceQuiet);
color: var(--contentSecondary);
}
.v-c .row.hero {
background: var(--bestLapRowTint);
}
.v-c .row.hero .icon-tile {
background: var(--surfaceInverse);
color: var(--contentInverse);
}
.v-c .unread-dot { background: var(--contentPrimary); }
.v-c .row.hero .unread-dot { background: var(--bestLap); }
/* Token call-out */
.tokens {
margin-top: 24px;
padding: 14px;
background: #FAFAFB;
border-radius: 10px;
font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
color: #444;
}
.tokens b { color: #111; }
.scheme-label {
font-size: 11px;
color: #999;
text-transform: uppercase;
letter-spacing: 0.06em;
margin: 6px 0 4px 4px;
}
/* Compare row at the bottom */
.compare {
max-width: 1280px;
margin: 48px auto 0;
padding: 20px;
border-radius: 20px;
background: #FFFFFF;
border: 1px solid #ECECEE;
}
.compare h2 { margin: 0 0 8px; font-size: 18px; }
.compare p { margin: 0 0 16px; color: #555; font-size: 14px; }
.compare-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 18px;
}
.compare-card { border-radius: 14px; overflow: hidden; border: 1px solid #ECECEE; }
.compare-card h3 {
margin: 0; padding: 10px 14px;
font-size: 13px; color: #555;
background: #FAFAFB; border-bottom: 1px solid #ECECEE;
}
</style>
</head>
<body>
<div class="page-header">
<h1>Notifications — Color Direction</h1>
<p>
Today's screen paints <b>12 different saturated system colors</b> (blue, purple, green, orange, yellow, cyan, pink, red, mint, teal, indigo, gray) — one per action. That's the "rainbow" feel.
Below are three token-only directions, all using <b>LapsterTheme</b> only. Each is rendered in light + dark to verify both schemes survive (per the LapsterTheme rule).
</p>
</div>
<div class="legend">
<div class="legend-card">
<b>Variant A — Monochrome Calm</b>
Neutral icon tiles, tiny semantic pip on the corner to encode meaning. Quietest, very premium.
<div class="swatch-row">
<span class="swatch" style="background:#F2F2F7"></span>
<span class="swatch" style="background:#007AFF"></span>
<span class="swatch" style="background:#FF3B30"></span>
<span class="swatch" style="background:#34C759"></span>
<span class="swatch" style="background:#FF9500"></span>
</div>
</div>
<div class="legend-card">
<b>Variant B — Semantic Tints (4 buckets)</b>
Group by meaning, not per-action. Each bucket gets one muted theme tint at 10–12% opacity.
<div class="swatch-row">
<span class="swatch" style="background:rgba(0,122,255,.10)"></span>
<span class="swatch" style="background:rgba(255,59,48,.10)"></span>
<span class="swatch" style="background:#E5E5EA"></span>
<span class="swatch" style="background:rgba(52,199,89,.12)"></span>
<span class="swatch" style="background:rgba(255,149,0,.12)"></span>
</div>
</div>
<div class="legend-card">
<b>Variant C — Inverse Hero</b>
Everyday rows are neutral. Special moments (PB, tier-up, shared) use <code>surfaceInverse&nbsp;+&nbsp;contentInverse</code> — loud without chroma. Matches the new session header.
<div class="swatch-row">
<span class="swatch" style="background:#F2F2F7"></span>
<span class="swatch" style="background:#1C1C1E"></span>
<span class="swatch" style="background:rgba(255,59,48,.05)"></span>
</div>
</div>
</div>
<div class="variants">
<!-- ===================== VARIANT A ===================== -->
<div class="variant v-a">
<h2>A · Monochrome Calm</h2>
<div class="tag">Neutral tile · semantic pip · contentPrimary unread dot</div>
<div class="phones">
<div class="scheme-label">Light</div>
<div class="phone">
<div class="phone-bar"><span>Notifications</span><span class="x">×</span></div>
<div class="phone-list">
<div class="row">
<div class="icon-tile">
<svg class="icn solid" viewBox="0 0 24 24"><path d="M12 12a4 4 0 100-8 4 4 0 000 8zm-7 8a7 7 0 0114 0H5z"/></svg>
<span class="pip social"></span>
</div>
<div class="avatar">M</div>
<div class="body">
<p class="msg"><b>Maya</b> started following you</p>
<div class="meta">2 min ago</div>
</div>
<div class="unread-dot"></div>
</div>
<div class="row">
<div class="icon-tile">
<svg class="icn solid" viewBox="0 0 24 24"><path d="M12 2l2.6 6.6L22 9.3l-5.5 4.7L18 22l-6-3.4L6 22l1.5-8L2 9.3l7.4-.7z"/></svg>
<span class="pip performance"></span>
</div>
<div class="avatar a2">A</div>
<div class="body">
<p class="msg">Personal best at <b>Sentul</b> — 1:43.812</p>
<div class="meta">14 min ago</div>
</div>
<div class="unread-dot"></div>
</div>
<div class="row">
<div class="icon-tile">
<svg class="icn solid" viewBox="0 0 24 24"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4z"/></svg>
<span class="pip sharing"></span>
</div>
<div class="avatar a3">D</div>
<div class="body">
<p class="msg"><b>Dani</b> shared a session with you</p>
<div class="meta">1 h ago</div>
</div>
</div>
<div class="row">
<div class="icon-tile">
<svg class="icn solid" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3"/><circle cx="17" cy="8" r="3"/><path d="M2 21a7 7 0 0114 0H2zm12 0a7 7 0 018-7"/></svg>
<span class="pip teams"></span>
</div>
<div class="avatar a4">R</div>
<div class="body">
<p class="msg"><b>Apex Racing</b> invited you to join</p>
<div class="meta">3 h ago</div>
</div>
</div>
<div class="row">
<div class="icon-tile">
<svg class="icn solid" viewBox="0 0 24 24"><path d="M21 15a4 4 0 01-4 4H7l-4 4V5a4 4 0 014-4h10a4 4 0 014 4z"/></svg>
<span class="pip social"></span>
</div>
<div class="avatar a5">K</div>
<div class="body">
<p class="msg"><b>Kai</b> replied to your comment</p>
<div class="meta">Yesterday</div>
</div>
</div>
<div class="row">
<div class="icon-tile">
<svg class="icn solid" viewBox="0 0 24 24"><path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"/><circle cx="12" cy="13" r="4" fill="none" stroke="currentColor"/></svg>
<span class="pip story"></span>
</div>
<div class="avatar a6">N</div>
<div class="body">
<p class="msg"><b>Nadia</b> reacted ❤️ to your story</p>
<div class="meta">2 days ago</div>
</div>
</div>
</div>
</div>
<div class="scheme-label">Dark</div>
<div class="phone dark">
<div class="phone-bar"><span>Notifications</span><span class="x">×</span></div>
<div class="phone-list">
<div class="row">
<div class="icon-tile"><svg class="icn solid" viewBox="0 0 24 24"><path d="M12 12a4 4 0 100-8 4 4 0 000 8zm-7 8a7 7 0 0114 0H5z"/></svg><span class="pip social"></span></div>
<div class="avatar">M</div>
<div class="body"><p class="msg"><b>Maya</b> started following you</p><div class="meta">2 min ago</div></div>
<div class="unread-dot"></div>
</div>
<div class="row">
<div class="icon-tile"><svg class="icn solid" viewBox="0 0 24 24"><path d="M12 2l2.6 6.6L22 9.3l-5.5 4.7L18 22l-6-3.4L6 22l1.5-8L2 9.3l7.4-.7z"/></svg><span class="pip performance"></span></div>
<div class="avatar a2">A</div>
<div class="body"><p class="msg">Personal best at <b>Sentul</b> — 1:43.812</p><div class="meta">14 min ago</div></div>
<div class="unread-dot"></div>
</div>
<div class="row">
<div class="icon-tile"><svg class="icn solid" viewBox="0 0 24 24"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4z"/></svg><span class="pip sharing"></span></div>
<div class="avatar a3">D</div>
<div class="body"><p class="msg"><b>Dani</b> shared a session with you</p><div class="meta">1 h ago</div></div>
</div>
<div class="row">
<div class="icon-tile"><svg class="icn solid" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3"/><circle cx="17" cy="8" r="3"/><path d="M2 21a7 7 0 0114 0H2zm12 0a7 7 0 018-7"/></svg><span class="pip teams"></span></div>
<div class="avatar a4">R</div>
<div class="body"><p class="msg"><b>Apex Racing</b> invited you to join</p><div class="meta">3 h ago</div></div>
</div>
</div>
</div>
</div>
<div class="tokens">
<b>Tile bg:</b> Colors.surfaceQuiet<br/>
<b>Icon:</b> Colors.contentSecondary<br/>
<b>Pip (5 categories):</b> accent · bestLap · contentSecondary · success · warning<br/>
<b>Unread dot:</b> Colors.contentPrimary
</div>
</div>
<!-- ===================== VARIANT B ===================== -->
<div class="variant v-b">
<h2>B · Semantic Tints</h2>
<div class="tag">4 muted buckets · same color owns tile + unread dot</div>
<div class="phones">
<div class="scheme-label">Light</div>
<div class="phone">
<div class="phone-bar"><span>Notifications</span><span class="x">×</span></div>
<div class="phone-list">
<div class="row">
<div class="icon-tile social"><svg class="icn solid" viewBox="0 0 24 24"><path d="M12 12a4 4 0 100-8 4 4 0 000 8zm-7 8a7 7 0 0114 0H5z"/></svg></div>
<div class="avatar">M</div>
<div class="body">
<p class="msg"><b>Maya</b> started following you</p>
<div class="meta">2 min ago</div>
</div>
<div class="unread-dot social"></div>
</div>
<div class="row">
<div class="icon-tile performance"><svg class="icn solid" viewBox="0 0 24 24"><path d="M12 2l2.6 6.6L22 9.3l-5.5 4.7L18 22l-6-3.4L6 22l1.5-8L2 9.3l7.4-.7z"/></svg></div>
<div class="avatar a2">A</div>
<div class="body">
<p class="msg">Personal best at <b>Sentul</b> — 1:43.812</p>
<div class="meta">14 min ago</div>
</div>
<div class="unread-dot performance"></div>
</div>
<div class="row">
<div class="icon-tile sharing"><svg class="icn solid" viewBox="0 0 24 24"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4z"/></svg></div>
<div class="avatar a3">D</div>
<div class="body">
<p class="msg"><b>Dani</b> shared a session with you</p>
<div class="meta">1 h ago</div>
</div>
</div>
<div class="row">
<div class="icon-tile teams"><svg class="icn solid" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3"/><circle cx="17" cy="8" r="3"/><path d="M2 21a7 7 0 0114 0H2zm12 0a7 7 0 018-7"/></svg></div>
<div class="avatar a4">R</div>
<div class="body">
<p class="msg"><b>Apex Racing</b> invited you to join</p>
<div class="meta">3 h ago</div>
</div>
</div>
<div class="row">
<div class="icon-tile social"><svg class="icn solid" viewBox="0 0 24 24"><path d="M21 15a4 4 0 01-4 4H7l-4 4V5a4 4 0 014-4h10a4 4 0 014 4z"/></svg></div>
<div class="avatar a5">K</div>
<div class="body">
<p class="msg"><b>Kai</b> replied to your comment</p>
<div class="meta">Yesterday</div>
</div>
</div>
<div class="row">
<div class="icon-tile story"><svg class="icn solid" viewBox="0 0 24 24"><path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"/><circle cx="12" cy="13" r="4" fill="none" stroke="currentColor"/></svg></div>
<div class="avatar a6">N</div>
<div class="body">
<p class="msg"><b>Nadia</b> reacted ❤️ to your story</p>
<div class="meta">2 days ago</div>
</div>
</div>
</div>
</div>
<div class="scheme-label">Dark</div>
<div class="phone dark">
<div class="phone-bar"><span>Notifications</span><span class="x">×</span></div>
<div class="phone-list">
<div class="row">
<div class="icon-tile social"><svg class="icn solid" viewBox="0 0 24 24"><path d="M12 12a4 4 0 100-8 4 4 0 000 8zm-7 8a7 7 0 0114 0H5z"/></svg></div>
<div class="avatar">M</div>
<div class="body"><p class="msg"><b>Maya</b> started following you</p><div class="meta">2 min ago</div></div>
<div class="unread-dot social"></div>
</div>
<div class="row">
<div class="icon-tile performance"><svg class="icn solid" viewBox="0 0 24 24"><path d="M12 2l2.6 6.6L22 9.3l-5.5 4.7L18 22l-6-3.4L6 22l1.5-8L2 9.3l7.4-.7z"/></svg></div>
<div class="avatar a2">A</div>
<div class="body"><p class="msg">Personal best at <b>Sentul</b> — 1:43.812</p><div class="meta">14 min ago</div></div>
<div class="unread-dot performance"></div>
</div>
<div class="row">
<div class="icon-tile sharing"><svg class="icn solid" viewBox="0 0 24 24"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4z"/></svg></div>
<div class="avatar a3">D</div>
<div class="body"><p class="msg"><b>Dani</b> shared a session with you</p><div class="meta">1 h ago</div></div>
</div>
<div class="row">
<div class="icon-tile teams"><svg class="icn solid" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3"/><circle cx="17" cy="8" r="3"/><path d="M2 21a7 7 0 0114 0H2zm12 0a7 7 0 018-7"/></svg></div>
<div class="avatar a4">R</div>
<div class="body"><p class="msg"><b>Apex Racing</b> invited you to join</p><div class="meta">3 h ago</div></div>
</div>
</div>
</div>
</div>
<div class="tokens">
<b>Social</b> (follow/comment/mention/reply): accent @ 10%<br/>
<b>Performance</b> (PB, tier-up, new session): bestLapRowTint (red @ 5%)<br/>
<b>Sharing</b> (shared session/lap, invite accepted): surfaceMuted<br/>
<b>Teams:</b> success @ 12%<br/>
<b>Story:</b> warning @ 12%<br/>
<b>Unread dot:</b> same family color
</div>
</div>
<!-- ===================== VARIANT C ===================== -->
<div class="variant v-c">
<h2>C · Inverse Hero</h2>
<div class="tag">Neutral default · loud-without-chroma for milestones</div>
<div class="phones">
<div class="scheme-label">Light</div>
<div class="phone">
<div class="phone-bar"><span>Notifications</span><span class="x">×</span></div>
<div class="phone-list">
<div class="row hero">
<div class="icon-tile">
<svg class="icn solid" viewBox="0 0 24 24"><path d="M12 2l2.6 6.6L22 9.3l-5.5 4.7L18 22l-6-3.4L6 22l1.5-8L2 9.3l7.4-.7z"/></svg>
</div>
<div class="avatar a2">A</div>
<div class="body">
<p class="msg"><b>Personal best</b> at Sentul — 1:43.812</p>
<div class="meta">14 min ago</div>
</div>
<div class="unread-dot"></div>
</div>
<div class="row">
<div class="icon-tile"><svg class="icn solid" viewBox="0 0 24 24"><path d="M12 12a4 4 0 100-8 4 4 0 000 8zm-7 8a7 7 0 0114 0H5z"/></svg></div>
<div class="avatar">M</div>
<div class="body">
<p class="msg"><b>Maya</b> started following you</p>
<div class="meta">2 min ago</div>
</div>
<div class="unread-dot"></div>
</div>
<div class="row">
<div class="icon-tile"><svg class="icn solid" viewBox="0 0 24 24"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4z"/></svg></div>
<div class="avatar a3">D</div>
<div class="body">
<p class="msg"><b>Dani</b> shared a session with you</p>
<div class="meta">1 h ago</div>
</div>
</div>
<div class="row hero">
<div class="icon-tile"><svg class="icn solid" viewBox="0 0 24 24"><path d="M5 12l5 5 9-12" fill="none" stroke="currentColor" stroke-width="2.4"/></svg></div>
<div class="avatar a4">R</div>
<div class="body">
<p class="msg"><b>Apex Racing</b> accepted your invite</p>
<div class="meta">3 h ago</div>
</div>
</div>
<div class="row">
<div class="icon-tile"><svg class="icn solid" viewBox="0 0 24 24"><path d="M21 15a4 4 0 01-4 4H7l-4 4V5a4 4 0 014-4h10a4 4 0 014 4z"/></svg></div>
<div class="avatar a5">K</div>
<div class="body">
<p class="msg"><b>Kai</b> replied to your comment</p>
<div class="meta">Yesterday</div>
</div>
</div>
</div>
</div>
<div class="scheme-label">Dark</div>
<div class="phone dark">
<div class="phone-bar"><span>Notifications</span><span class="x">×</span></div>
<div class="phone-list">
<div class="row hero">
<div class="icon-tile"><svg class="icn solid" viewBox="0 0 24 24"><path d="M12 2l2.6 6.6L22 9.3l-5.5 4.7L18 22l-6-3.4L6 22l1.5-8L2 9.3l7.4-.7z"/></svg></div>
<div class="avatar a2">A</div>
<div class="body"><p class="msg"><b>Personal best</b> at Sentul — 1:43.812</p><div class="meta">14 min ago</div></div>
<div class="unread-dot"></div>
</div>
<div class="row">
<div class="icon-tile"><svg class="icn solid" viewBox="0 0 24 24"><path d="M12 12a4 4 0 100-8 4 4 0 000 8zm-7 8a7 7 0 0114 0H5z"/></svg></div>
<div class="avatar">M</div>
<div class="body"><p class="msg"><b>Maya</b> started following you</p><div class="meta">2 min ago</div></div>
<div class="unread-dot"></div>
</div>
<div class="row">
<div class="icon-tile"><svg class="icn solid" viewBox="0 0 24 24"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4z"/></svg></div>
<div class="avatar a3">D</div>
<div class="body"><p class="msg"><b>Dani</b> shared a session with you</p><div class="meta">1 h ago</div></div>
</div>
</div>
</div>
</div>
<div class="tokens">
<b>Default row icon tile:</b> surfaceQuiet + contentSecondary<br/>
<b>Hero row bg:</b> bestLapRowTint<br/>
<b>Hero icon tile:</b> surfaceInverse + contentInverse (loud, no chroma)<br/>
<b>Hero unread dot:</b> bestLap; default unread dot: contentPrimary<br/>
<b>Triggers hero:</b> personal_best · tier_up · invite_accepted
</div>
</div>
</div>
<div class="compare">
<h2>Today vs. Proposal — side by side</h2>
<p>Same five notifications. Current rainbow on the left, Variant A on the right.</p>
<div class="compare-grid">
<div class="compare-card">
<h3>Today (12-color rainbow)</h3>
<div class="phone" style="border:none;border-radius:0">
<div class="phone-list" style="border-radius:0">
<div class="row">
<div class="icon-tile" style="background:rgba(0,122,255,.12);color:#007AFF">
<svg class="icn solid" viewBox="0 0 24 24"><path d="M12 12a4 4 0 100-8 4 4 0 000 8zm-7 8a7 7 0 0114 0H5z"/></svg>
</div>
<div class="avatar">M</div>
<div class="body"><p class="msg"><b>Maya</b> started following you</p><div class="meta">2 min ago</div></div>
<div class="unread-dot" style="background:#007AFF"></div>
</div>
<div class="row">
<div class="icon-tile" style="background:rgba(255,149,0,.12);color:#FF9500">
<svg class="icn solid" viewBox="0 0 24 24"><path d="M12 2l2.6 6.6L22 9.3l-5.5 4.7L18 22l-6-3.4L6 22l1.5-8L2 9.3l7.4-.7z"/></svg>
</div>
<div class="avatar a2">A</div>
<div class="body"><p class="msg">Personal best at <b>Sentul</b> — 1:43.812</p><div class="meta">14 min ago</div></div>
<div class="unread-dot" style="background:#FF9500"></div>
</div>
<div class="row">
<div class="icon-tile" style="background:rgba(50,173,230,.12);color:#32ADE6">
<svg class="icn solid" viewBox="0 0 24 24"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4z"/></svg>
</div>
<div class="avatar a3">D</div>
<div class="body"><p class="msg"><b>Dani</b> shared a session with you</p><div class="meta">1 h ago</div></div>
</div>
<div class="row">
<div class="icon-tile" style="background:rgba(0,122,255,.12);color:#007AFF">
<svg class="icn solid" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3"/><circle cx="17" cy="8" r="3"/><path d="M2 21a7 7 0 0114 0H2zm12 0a7 7 0 018-7"/></svg>
</div>
<div class="avatar a4">R</div>
<div class="body"><p class="msg"><b>Apex Racing</b> invited you to join</p><div class="meta">3 h ago</div></div>
</div>
<div class="row">
<div class="icon-tile" style="background:rgba(88,86,214,.12);color:#5856D6">
<svg class="icn solid" viewBox="0 0 24 24"><path d="M21 15a4 4 0 01-4 4H7l-4 4V5a4 4 0 014-4h10a4 4 0 014 4z"/></svg>
</div>
<div class="avatar a5">K</div>
<div class="body"><p class="msg"><b>Kai</b> replied to your comment</p><div class="meta">Yesterday</div></div>
</div>
</div>
</div>
</div>
<div class="compare-card">
<h3>Variant A · Monochrome Calm</h3>
<div class="phone v-a" style="border:none;border-radius:0">
<div class="phone-list" style="border-radius:0">
<div class="row">
<div class="icon-tile" style="background:#F2F2F7;color:rgba(60,60,67,.6)">
<svg class="icn solid" viewBox="0 0 24 24"><path d="M12 12a4 4 0 100-8 4 4 0 000 8zm-7 8a7 7 0 0114 0H5z"/></svg>
<span class="pip" style="position:absolute;bottom:-2px;right:-2px;width:12px;height:12px;border-radius:50%;border:2px solid #FFFFFF;background:#007AFF"></span>
</div>
<div class="avatar">M</div>
<div class="body"><p class="msg"><b>Maya</b> started following you</p><div class="meta">2 min ago</div></div>
<div class="unread-dot" style="background:#000"></div>
</div>
<div class="row">
<div class="icon-tile" style="background:#F2F2F7;color:rgba(60,60,67,.6)">
<svg class="icn solid" viewBox="0 0 24 24"><path d="M12 2l2.6 6.6L22 9.3l-5.5 4.7L18 22l-6-3.4L6 22l1.5-8L2 9.3l7.4-.7z"/></svg>
<span class="pip" style="position:absolute;bottom:-2px;right:-2px;width:12px;height:12px;border-radius:50%;border:2px solid #FFFFFF;background:#FF3B30"></span>
</div>
<div class="avatar a2">A</div>
<div class="body"><p class="msg">Personal best at <b>Sentul</b> — 1:43.812</p><div class="meta">14 min ago</div></div>
<div class="unread-dot" style="background:#000"></div>
</div>
<div class="row">
<div class="icon-tile" style="background:#F2F2F7;color:rgba(60,60,67,.6)">
<svg class="icn solid" viewBox="0 0 24 24"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4z"/></svg>
<span class="pip" style="position:absolute;bottom:-2px;right:-2px;width:12px;height:12px;border-radius:50%;border:2px solid #FFFFFF;background:rgba(60,60,67,.6)"></span>
</div>
<div class="avatar a3">D</div>
<div class="body"><p class="msg"><b>Dani</b> shared a session with you</p><div class="meta">1 h ago</div></div>
</div>
<div class="row">
<div class="icon-tile" style="background:#F2F2F7;color:rgba(60,60,67,.6)">
<svg class="icn solid" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3"/><circle cx="17" cy="8" r="3"/><path d="M2 21a7 7 0 0114 0H2zm12 0a7 7 0 018-7"/></svg>
<span class="pip" style="position:absolute;bottom:-2px;right:-2px;width:12px;height:12px;border-radius:50%;border:2px solid #FFFFFF;background:#34C759"></span>
</div>
<div class="avatar a4">R</div>
<div class="body"><p class="msg"><b>Apex Racing</b> invited you to join</p><div class="meta">3 h ago</div></div>
</div>
<div class="row">
<div class="icon-tile" style="background:#F2F2F7;color:rgba(60,60,67,.6)">
<svg class="icn solid" viewBox="0 0 24 24"><path d="M21 15a4 4 0 01-4 4H7l-4 4V5a4 4 0 014-4h10a4 4 0 014 4z"/></svg>
<span class="pip" style="position:absolute;bottom:-2px;right:-2px;width:12px;height:12px;border-radius:50%;border:2px solid #FFFFFF;background:#007AFF"></span>
</div>
<div class="avatar a5">K</div>
<div class="body"><p class="msg"><b>Kai</b> replied to your comment</p><div class="meta">Yesterday</div></div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lapster — Update Banner: More Contrast</title>
<style>
/* LapsterTheme tokens (mirrored from ios/Lapster/Common/Theme/LapsterTheme.swift) */
:root {
--surfacePrimary: #FFFFFF;
--surfaceGrouped: #F2F2F7;
--surfaceSecondary: #FFFFFF;
--surfaceTertiary: #F2F2F7;
--surfaceElevated: #EAEAF0;
--surfaceQuiet: #F2F2F7;
--surfaceMuted: #E5E5EA;
--surfaceFaint: #D1D1D6;
--surfaceInverse: #1C1C1E;
--surfaceInverseQuiet: #2C2C2E;
--contentPrimary: #000000;
--contentSecondary: rgba(60,60,67,.60);
--contentTertiary: rgba(60,60,67,.30);
--contentInverse: #FFFFFF;
--contentInverseSecondary: rgba(235,235,245,.60);
--divider: rgba(60,60,67,.29);
--dividerOpaque: #C6C6C8;
--accent: #007AFF;
--accentSubtle: rgba(0,122,255,.10);
--accentTint12: rgba(0,122,255,.12);
--accentTint20: rgba(0,122,255,.20);
--bestLap: #FF3B30;
--bestLapRowTint: rgba(255,59,48,.05);
--bestLapTint12: rgba(255,59,48,.12);
--contentOnAccent: #FFFFFF;
--r-md: 12px;
--sp-xs: 4px; --sp-sm: 8px; --sp-md: 12px; --sp-lg: 16px;
--cardPadding: 14px;
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro", system-ui, sans-serif;
}
* { box-sizing: border-box; }
html, body { margin: 0; font-family: var(--font-sans); -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
body { padding: 36px 24px 80px; background: #FAFAFB; color: #111; }
.page-header { max-width: 1280px; margin: 0 auto 28px; }
.page-header h1 { font-size: 26px; margin: 0 0 6px; letter-spacing: -.02em; font-weight: 700; }
.page-header p { margin: 0; color: #555; font-size: 14.5px; line-height: 1.5; max-width: 800px; }
.grid {
max-width: 1280px;
margin: 28px auto 0;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
@media (max-width: 1100px) { .grid { grid-template-columns: 1fr; } }
.card {
background: #FFFFFF;
border-radius: 20px;
padding: 22px;
border: 1px solid #ECECEE;
}
.card h2 { font-size: 17px; margin: 0 0 4px; letter-spacing: -.01em; }
.card .tag {
display: inline-block;
font-size: 11px; font-weight: 600;
color: #666; background: #F4F4F7;
padding: 2px 8px; border-radius: 999px;
margin-bottom: 12px;
}
.card .tag.live { color: #fff; background: #B22; }
.card .tag.pick { color: #fff; background: #111; }
.card .desc { font-size: 13px; color: #555; line-height: 1.5; margin: 0 0 14px; }
.stages { display: flex; flex-direction: column; gap: 10px; }
/* Dashboard stage — mimics the actual dashboard background, with a faint
"previous lap card" peek above so contrast can be judged against
neighbouring elements, like in the screenshot. */
.stage {
border-radius: 22px;
padding: 16px 14px 18px;
background: var(--surfaceGrouped);
border: 1px solid var(--divider);
}
.stage.dark {
--surfacePrimary: #000000;
--surfaceGrouped: #000000;
--surfaceSecondary: #1C1C1E;
--surfaceElevated: #2C2C30;
--surfaceTertiary: #2C2C2E;
--surfaceQuiet: #1C1C1E;
--surfaceMuted: #2C2C2E;
--surfaceFaint: #3A3A3C;
--surfaceInverse: #FFFFFF;
--surfaceInverseQuiet: #EBEBF0;
--contentPrimary: #FFFFFF;
--contentSecondary: rgba(235,235,245,.60);
--contentTertiary: rgba(235,235,245,.30);
--contentInverse: #000000;
--contentInverseSecondary: rgba(60,60,67,.60);
--divider: rgba(84,84,88,.65);
--dividerOpaque: #38383A;
--accentSubtle: rgba(10,132,255,.18);
--accentTint12: rgba(10,132,255,.24);
--accentTint20: rgba(10,132,255,.32);
--accent: #0A84FF;
--bestLap: #FF453A;
--bestLapRowTint: rgba(255,69,58,.08);
--bestLapTint12: rgba(255,69,58,.18);
}
/* Fake "lap row" peek above the banner — for contrast comparison */
.peek {
background: var(--surfaceSecondary);
border-radius: var(--r-md);
padding: 10px 14px;
margin-bottom: 12px;
font-size: 13px;
color: var(--contentSecondary);
display: flex; justify-content: space-between;
}
.peek b { color: var(--contentPrimary); font-weight: 600; }
.scheme-label {
font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase;
color: #999; margin: 4px 4px 6px;
}
/* Banner skeleton */
.banner {
display: flex; align-items: center; gap: 12px;
padding: var(--cardPadding);
border-radius: var(--r-md);
position: relative;
overflow: hidden;
}
.banner .icon-wrap {
width: 36px; height: 36px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
flex: 0 0 36px;
}
.banner .icon { width: 22px; height: 22px; fill: currentColor; }
.banner .icon.lg { width: 26px; height: 26px; }
.banner .text { flex: 1; min-width: 0; }
.banner .title { font-size: 15px; font-weight: 600; letter-spacing: -.005em; }
.banner .sub { font-size: 12px; margin-top: 1px; }
.banner .cta {
font-size: 14px; font-weight: 600;
padding: 7px 14px; border-radius: 999px;
white-space: nowrap;
}
.shadow-card { box-shadow: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.05); }
/* ---------- LIVE (shipped) ---------- */
.v-live {
background: var(--surfaceElevated);
color: var(--contentPrimary);
}
.v-live .icon-wrap { background: transparent; color: var(--accent); }
.v-live .icon { width: 28px; height: 28px; }
.v-live .sub { color: var(--contentSecondary); }
.v-live .cta { background: var(--accent); color: var(--contentOnAccent); }
/* ---------- A · Inverse High Contrast ---------- */
.v-inverse {
background: var(--surfaceInverse);
color: var(--contentInverse);
}
.v-inverse .icon-wrap { background: transparent; color: var(--contentInverse); }
.v-inverse .icon { width: 28px; height: 28px; }
.v-inverse .sub { color: var(--contentInverse); opacity: .55; }
.v-inverse .cta { background: var(--accent); color: var(--contentOnAccent); }
/* ---------- B · Accent Tint Wash ---------- */
.v-accent {
background: var(--accentSubtle);
color: var(--contentPrimary);
}
.v-accent .icon-wrap { background: transparent; color: var(--accent); }
.v-accent .icon { width: 28px; height: 28px; }
.v-accent .sub { color: var(--contentSecondary); }
.v-accent .cta { background: var(--accent); color: var(--contentOnAccent); }
/* ---------- C · Accent Stripe ---------- */
.v-stripe {
background: var(--surfaceElevated);
color: var(--contentPrimary);
padding-left: calc(var(--cardPadding) + 4px);
}
.v-stripe::before {
content: "";
position: absolute;
left: 0; top: 0; bottom: 0;
width: 4px;
background: var(--accent);
}
.v-stripe .icon-wrap { background: transparent; color: var(--accent); }
.v-stripe .icon { width: 28px; height: 28px; }
.v-stripe .sub { color: var(--contentSecondary); }
.v-stripe .cta { background: var(--accent); color: var(--contentOnAccent); }
/* ---------- D · Outlined Accent Card ---------- */
.v-outline {
background: var(--surfaceSecondary);
color: var(--contentPrimary);
border: 1.5px solid var(--accent);
}
.v-outline .icon-wrap { background: transparent; color: var(--accent); }
.v-outline .icon { width: 28px; height: 28px; }
.v-outline .sub { color: var(--contentSecondary); }
.v-outline .cta { background: var(--accent); color: var(--contentOnAccent); }
/* ---------- E · Solid Accent Fill ---------- */
.v-fill {
background: var(--accent);
color: var(--contentOnAccent);
}
.v-fill .icon-wrap { background: rgba(255,255,255,.18); color: var(--contentOnAccent); }
.v-fill .icon { width: 22px; height: 22px; }
.v-fill .sub { color: var(--contentOnAccent); opacity: .80; }
.v-fill .cta { background: var(--surfacePrimary); color: var(--accent); }
/* ---------- F · Best-Lap Hero ---------- */
.v-best {
background: var(--bestLapTint12);
color: var(--contentPrimary);
}
.v-best .icon-wrap { background: transparent; color: var(--bestLap); }
.v-best .icon { width: 28px; height: 28px; }
.v-best .sub { color: var(--contentSecondary); }
.v-best .cta { background: var(--surfaceInverse); color: var(--contentInverse); }
.tokens {
margin-top: 14px;
padding: 12px;
background: #FAFAFB;
border-radius: 10px;
font: 11.5px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
color: #444;
}
.tokens b { color: #111; }
/* Summary section */
.summary {
max-width: 1280px;
margin: 28px auto 0;
padding: 18px 20px;
border-radius: 16px;
background: #fff;
border: 1px solid #ECECEE;
font-size: 13.5px;
line-height: 1.55;
color: #444;
}
.summary code {
background: #F4F4F7; padding: 1px 6px; border-radius: 5px;
font: 12px ui-monospace, SFMono-Regular, Menlo, monospace;
}
/* Side-by-side dark contrast comparison strip */
.dark-strip {
max-width: 1280px;
margin: 32px auto 0;
padding: 20px;
border-radius: 18px;
background: #0B0B0E;
color: #DDD;
}
.dark-strip h2 { color: #fff; font-size: 16px; margin: 0 0 12px; }
.dark-strip-row {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 10px;
}
@media (max-width: 1100px) { .dark-strip-row { grid-template-columns: 1fr 1fr; } }
.dark-strip .mini {
background: #000;
border-radius: 12px;
padding: 10px;
--surfaceSecondary: #1C1C1E;
--surfaceElevated: #2C2C30;
--accent: #0A84FF;
--accentSubtle: rgba(10,132,255,.18);
--bestLap: #FF453A;
--bestLapTint12: rgba(255,69,58,.18);
--contentPrimary: #FFF;
--contentSecondary: rgba(235,235,245,.6);
--contentInverse: #000;
--contentOnAccent: #FFF;
--surfaceInverse: #FFF;
--surfacePrimary: #000;
}
.dark-strip .mini .banner {
padding: 8px; border-radius: 8px; gap: 6px;
}
.dark-strip .mini .banner .icon-wrap { width: 26px; height: 26px; flex: 0 0 26px; }
.dark-strip .mini .banner .icon { width: 16px; height: 16px; }
.dark-strip .mini .banner .icon.lg { width: 20px; height: 20px; }
.dark-strip .mini .banner .text { font-size: 11px; }
.dark-strip .mini .banner .title { font-size: 11px; }
.dark-strip .mini .banner .sub { display: none; }
.dark-strip .mini .banner .cta { font-size: 11px; padding: 4px 8px; }
.dark-strip .lab { color: #999; font-size: 11px; text-align: center; margin-top: 6px; }
</style>
</head>
<body>
<div class="page-header">
<h1>Update Banner — More Contrast (Dark-mode focus)</h1>
<p>
Today's shipped version (variant 0) is <b>surfaceElevated</b> (#2C2C30 in dark) — that bumped one notch up from <code>surfaceSecondary</code>, but on a dashboard already full of <code>surfaceSecondary</code>-tinted lap cards the banner still reads quietly. Below are six louder directions, all using only <b>LapsterTheme</b> tokens. Every stage shows a fake "lap card" above the banner so you can judge contrast in context — same as the screenshot you sent.
</p>
</div>
<div class="dark-strip">
<h2>Dark-mode contrast at a glance — same banner, six new directions</h2>
<div class="dark-strip-row">
<div class="mini">
<div class="banner v-live"><div class="icon-wrap"><svg class="icon lg" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update</div></div><div class="cta">Update</div></div>
<div class="lab">0 · Live</div>
</div>
<div class="mini">
<div class="banner v-inverse"><div class="icon-wrap"><svg class="icon lg" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update</div></div><div class="cta">Update</div></div>
<div class="lab">A · Inverse</div>
</div>
<div class="mini">
<div class="banner v-accent"><div class="icon-wrap"><svg class="icon lg" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update</div></div><div class="cta">Update</div></div>
<div class="lab">B · Tint</div>
</div>
<div class="mini">
<div class="banner v-stripe"><div class="icon-wrap"><svg class="icon lg" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update</div></div><div class="cta">Update</div></div>
<div class="lab">C · Stripe</div>
</div>
<div class="mini">
<div class="banner v-outline"><div class="icon-wrap"><svg class="icon lg" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update</div></div><div class="cta">Update</div></div>
<div class="lab">D · Outline</div>
</div>
<div class="mini">
<div class="banner v-fill"><div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update</div></div><div class="cta">Update</div></div>
<div class="lab">E · Solid Accent</div>
</div>
<div class="mini">
<div class="banner v-best"><div class="icon-wrap"><svg class="icon lg" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update</div></div><div class="cta">Update</div></div>
<div class="lab">F · Best-Lap</div>
</div>
</div>
</div>
<div class="grid">
<!-- ===== LIVE ===== -->
<div class="card">
<h2>0 · Live (shipped today)</h2>
<span class="tag live">current</span>
<p class="desc">For reference: <code>surfaceElevated</code> card + solid accent SF Symbol + accent CTA pill. The dark-mode complaint: still feels too close to the lap-card chrome above it.</p>
<div class="stages">
<div class="scheme-label">Light</div>
<div class="stage">
<div class="peek"><span>Sentul · 2 laps</span><b>1:43.812</b></div>
<div class="banner v-live shadow-card"><div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update Available</div><div class="sub">Version 1.2.3 is ready to install</div></div><div class="cta">Update</div></div>
</div>
<div class="scheme-label">Dark</div>
<div class="stage dark">
<div class="peek"><span>Sentul · 2 laps</span><b>1:43.812</b></div>
<div class="banner v-live shadow-card"><div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update Available</div><div class="sub">Version 1.2.3 is ready to install</div></div><div class="cta">Update</div></div>
</div>
</div>
<div class="tokens"><b>bg:</b> surfaceElevated · <b>icon fg:</b> accent · <b>pill:</b> accent / contentOnAccent</div>
</div>
<!-- ===== A · Inverse ===== -->
<div class="card">
<h2>A · Inverse High Contrast</h2>
<span class="tag">loudest neutral</span>
<p class="desc">Returns to the "loud CTA" pair — <code>surfaceInverse</code> + <code>contentInverse</code>. In dark mode the banner becomes a <b>bright white card</b> against the black dashboard; in light mode it becomes near-black against white. Same direction as the unsaved-changes Save button. Single accent pill survives.</p>
<div class="stages">
<div class="scheme-label">Light</div>
<div class="stage">
<div class="peek"><span>Sentul · 2 laps</span><b>1:43.812</b></div>
<div class="banner v-inverse"><div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update Available</div><div class="sub">Version 1.2.3 is ready to install</div></div><div class="cta">Update</div></div>
</div>
<div class="scheme-label">Dark</div>
<div class="stage dark">
<div class="peek"><span>Sentul · 2 laps</span><b>1:43.812</b></div>
<div class="banner v-inverse"><div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update Available</div><div class="sub">Version 1.2.3 is ready to install</div></div><div class="cta">Update</div></div>
</div>
</div>
<div class="tokens"><b>bg:</b> surfaceInverse · <b>fg:</b> contentInverse · <b>sub:</b> contentInverse @ .55 · <b>pill:</b> accent / contentOnAccent</div>
</div>
<!-- ===== B · Accent Tint ===== -->
<div class="card">
<h2>B · Accent Tint Wash</h2>
<span class="tag">soft + colored</span>
<p class="desc">Whole banner is washed in <code>accentSubtle</code>. Dark mode bumps the tint to ~18% so the blue actually reads. Tells the user "this is a system message about something positive" without any neutrals fighting it.</p>
<div class="stages">
<div class="scheme-label">Light</div>
<div class="stage">
<div class="peek"><span>Sentul · 2 laps</span><b>1:43.812</b></div>
<div class="banner v-accent"><div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update Available</div><div class="sub">Version 1.2.3 is ready to install</div></div><div class="cta">Update</div></div>
</div>
<div class="scheme-label">Dark</div>
<div class="stage dark">
<div class="peek"><span>Sentul · 2 laps</span><b>1:43.812</b></div>
<div class="banner v-accent"><div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update Available</div><div class="sub">Version 1.2.3 is ready to install</div></div><div class="cta">Update</div></div>
</div>
</div>
<div class="tokens"><b>bg:</b> accentSubtle · <b>icon fg:</b> accent · <b>pill:</b> accent / contentOnAccent</div>
</div>
<!-- ===== C · Stripe ===== -->
<div class="card">
<h2>C · Accent Stripe</h2>
<span class="tag pick">my pick</span>
<p class="desc">Calm <code>surfaceElevated</code> card with a 4pt accent stripe on the leading edge. The stripe carries all the visual weight; the rest of the banner stays neutral. iOS "what's new" / Apple Health pattern. Reads loud-and-clear in both modes without flooding the row.</p>
<div class="stages">
<div class="scheme-label">Light</div>
<div class="stage">
<div class="peek"><span>Sentul · 2 laps</span><b>1:43.812</b></div>
<div class="banner v-stripe shadow-card"><div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update Available</div><div class="sub">Version 1.2.3 is ready to install</div></div><div class="cta">Update</div></div>
</div>
<div class="scheme-label">Dark</div>
<div class="stage dark">
<div class="peek"><span>Sentul · 2 laps</span><b>1:43.812</b></div>
<div class="banner v-stripe shadow-card"><div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update Available</div><div class="sub">Version 1.2.3 is ready to install</div></div><div class="cta">Update</div></div>
</div>
</div>
<div class="tokens"><b>bg:</b> surfaceElevated · <b>stripe:</b> 4pt accent leading edge · <b>icon fg:</b> accent · <b>pill:</b> accent / contentOnAccent</div>
</div>
<!-- ===== D · Outline ===== -->
<div class="card">
<h2>D · Outlined Accent Card</h2>
<span class="tag">framed</span>
<p class="desc"><code>surfaceSecondary</code> card with a <b>1.5pt accent border</b>. The blue outline locks the card in place visually — works on both grouped and plain parent surfaces because the stroke is doing the contrast work, not the fill.</p>
<div class="stages">
<div class="scheme-label">Light</div>
<div class="stage">
<div class="peek"><span>Sentul · 2 laps</span><b>1:43.812</b></div>
<div class="banner v-outline"><div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update Available</div><div class="sub">Version 1.2.3 is ready to install</div></div><div class="cta">Update</div></div>
</div>
<div class="scheme-label">Dark</div>
<div class="stage dark">
<div class="peek"><span>Sentul · 2 laps</span><b>1:43.812</b></div>
<div class="banner v-outline"><div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update Available</div><div class="sub">Version 1.2.3 is ready to install</div></div><div class="cta">Update</div></div>
</div>
</div>
<div class="tokens"><b>bg:</b> surfaceSecondary · <b>border:</b> 1.5pt accent · <b>icon fg:</b> accent · <b>pill:</b> accent / contentOnAccent</div>
</div>
<!-- ===== E · Solid Accent Fill ===== -->
<div class="card">
<h2>E · Solid Accent Fill</h2>
<span class="tag">loudest possible</span>
<p class="desc">The whole banner is <code>accent</code> blue. White text and a white pill with accent text. Maximum attention, single accent moment. Identical in both schemes — accent is the canonical brand colour.</p>
<div class="stages">
<div class="scheme-label">Light</div>
<div class="stage">
<div class="peek"><span>Sentul · 2 laps</span><b>1:43.812</b></div>
<div class="banner v-fill"><div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update Available</div><div class="sub">Version 1.2.3 is ready to install</div></div><div class="cta">Update</div></div>
</div>
<div class="scheme-label">Dark</div>
<div class="stage dark">
<div class="peek"><span>Sentul · 2 laps</span><b>1:43.812</b></div>
<div class="banner v-fill"><div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update Available</div><div class="sub">Version 1.2.3 is ready to install</div></div><div class="cta">Update</div></div>
</div>
</div>
<div class="tokens"><b>bg:</b> accent · <b>fg:</b> contentOnAccent · <b>icon-wrap bg:</b> contentOnAccent @ 18% · <b>pill:</b> surfacePrimary / accent</div>
</div>
<!-- ===== F · Best-Lap Hero ===== -->
<div class="card">
<h2>F · Best-Lap Hero</h2>
<span class="tag">"this is a win"</span>
<p class="desc">Borrows the personal-best treatment: <code>bestLapTint12</code> background, red icon, <code>surfaceInverse</code> CTA. Frames the update as a positive racing moment rather than a chore — and the red tint is unmistakably distinct from any other dashboard card.</p>
<div class="stages">
<div class="scheme-label">Light</div>
<div class="stage">
<div class="peek"><span>Sentul · 2 laps</span><b>1:43.812</b></div>
<div class="banner v-best"><div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update Available</div><div class="sub">Version 1.2.3 is ready to install</div></div><div class="cta">Update</div></div>
</div>
<div class="scheme-label">Dark</div>
<div class="stage dark">
<div class="peek"><span>Sentul · 2 laps</span><b>1:43.812</b></div>
<div class="banner v-best"><div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div><div class="text"><div class="title">Update Available</div><div class="sub">Version 1.2.3 is ready to install</div></div><div class="cta">Update</div></div>
</div>
</div>
<div class="tokens"><b>bg:</b> bestLap @ 12% · <b>icon fg:</b> bestLap · <b>pill:</b> surfaceInverse / contentInverse</div>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lapster — Update Banner Color Directions</title>
<style>
/* LapsterTheme tokens, mirrored from ios/Lapster/Common/Theme/LapsterTheme.swift */
:root {
--surfacePrimary: #FFFFFF;
--surfaceGrouped: #F2F2F7;
--surfaceSecondary: #FFFFFF;
--surfaceTertiary: #F2F2F7;
--surfaceElevated: #FFFFFF;
--surfaceQuiet: #F2F2F7;
--surfaceMuted: #E5E5EA;
--surfaceFaint: #D1D1D6;
--surfaceInverse: #1C1C1E;
--surfaceInverseQuiet: #2C2C2E;
--contentPrimary: #000000;
--contentSecondary: rgba(60,60,67,.60);
--contentTertiary: rgba(60,60,67,.30);
--contentInverse: #FFFFFF;
--contentInverseSecondary: rgba(235,235,245,.60);
--divider: rgba(60,60,67,.29);
--accent: #007AFF;
--accentSubtle: rgba(0,122,255,.10);
--accentTint12: rgba(0,122,255,.12);
--success: #34C759;
--warning: #FF9500;
--warningSubtle: rgba(255,149,0,.12);
--error: #FF3B30;
--bestLap: #FF3B30;
--bestLapRowTint: rgba(255,59,48,.05);
--bestLapTint12: rgba(255,59,48,.12);
--r-sm: 8px; --r-md: 12px; --r-lg: 16px;
--sp-xs: 4px; --sp-sm: 8px; --sp-md: 12px; --sp-lg: 16px; --sp-xl: 20px;
--cardPadding: 14px;
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro", system-ui, sans-serif;
}
* { box-sizing: border-box; }
html, body { margin: 0; font-family: var(--font-sans); -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
body { padding: 36px 24px 80px; background: #FAFAFB; color: #111; }
.page-header { max-width: 1240px; margin: 0 auto 28px; }
.page-header h1 { font-size: 26px; margin: 0 0 6px; letter-spacing: -.02em; font-weight: 700; }
.page-header p { margin: 0; color: #555; font-size: 14.5px; line-height: 1.5; max-width: 780px; }
.grid {
max-width: 1240px;
margin: 28px auto 0;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
@media (max-width: 1100px) { .grid { grid-template-columns: 1fr; } }
.card {
background: #FFFFFF;
border-radius: 20px;
padding: 22px;
border: 1px solid #ECECEE;
}
.card h2 { font-size: 17px; margin: 0 0 4px; letter-spacing: -.01em; }
.card .tag {
display: inline-block;
font-size: 11px; font-weight: 600;
color: #666; background: #F4F4F7;
padding: 2px 8px; border-radius: 999px;
margin-bottom: 12px;
}
.card .tag.pick { color: #fff; background: #111; }
.card .desc { font-size: 13px; color: #555; line-height: 1.5; margin: 0 0 14px; }
.stages { display: flex; flex-direction: column; gap: 10px; }
/* "Phone surface" — emulates the Dashboard background where banner lives */
.stage {
border-radius: 22px;
padding: 18px 16px;
background: var(--surfaceGrouped);
border: 1px solid var(--divider);
}
.stage.dark {
--surfacePrimary: #000000;
--surfaceGrouped: #000000;
--surfaceSecondary: #1C1C1E;
--surfaceElevated: #1C1C1E;
--surfaceQuiet: #1C1C1E;
--surfaceMuted: #2C2C2E;
--surfaceFaint: #3A3A3C;
--surfaceInverse: #FFFFFF;
--surfaceInverseQuiet: #EBEBF0;
--contentPrimary: #FFFFFF;
--contentSecondary: rgba(235,235,245,.60);
--contentTertiary: rgba(235,235,245,.30);
--contentInverse: #000000;
--contentInverseSecondary: rgba(60,60,67,.60);
--divider: rgba(84,84,88,.65);
--accentSubtle: rgba(10,132,255,.18);
--accentTint12: rgba(10,132,255,.22);
--accent: #0A84FF;
--warning: #FF9F0A;
--warningSubtle: rgba(255,159,10,.18);
--bestLap: #FF453A;
--bestLapRowTint: rgba(255,69,58,.08);
--bestLapTint12: rgba(255,69,58,.18);
}
.scheme-label {
font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase;
color: #999; margin: 4px 4px 6px;
}
/* ----- Shared banner skeleton ----- */
.banner {
display: flex; align-items: center; gap: 12px;
padding: var(--cardPadding);
border-radius: var(--r-md);
}
.banner .icon-wrap {
width: 36px; height: 36px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
flex: 0 0 36px;
}
.banner .icon { width: 22px; height: 22px; fill: currentColor; }
.banner .text { flex: 1; min-width: 0; }
.banner .title { font-size: 15px; font-weight: 600; letter-spacing: -.005em; }
.banner .sub { font-size: 12px; margin-top: 1px; }
.banner .cta {
font-size: 14px; font-weight: 600;
padding: 7px 14px; border-radius: 999px;
white-space: nowrap;
}
/* ----- Variant 0: Today (loud inverse) ----- */
.v-today {
background: var(--surfaceInverse);
color: var(--contentInverse);
}
.v-today .icon-wrap { background: transparent; color: var(--contentInverse); }
.v-today .sub { color: var(--contentInverse); opacity: .40; }
.v-today .cta { background: var(--surfacePrimary); color: var(--contentPrimary); }
/* ----- Variant 1: Quiet Card (surfaceElevated + accent) ----- */
.v-quiet {
background: var(--surfaceSecondary);
color: var(--contentPrimary);
border: 1px solid var(--divider);
}
.v-quiet .icon-wrap { background: var(--accentSubtle); color: var(--accent); }
.v-quiet .sub { color: var(--contentSecondary); }
.v-quiet .cta { background: var(--accent); color: #fff; }
/* ----- Variant 2: Accent Tinted (accentSubtle bg) ----- */
.v-accent {
background: var(--accentSubtle);
color: var(--contentPrimary);
}
.v-accent .icon-wrap { background: var(--surfaceSecondary); color: var(--accent); }
.v-accent .sub { color: var(--contentSecondary); }
.v-accent .cta { background: var(--accent); color: #fff; }
/* ----- Variant 3: TestFlight Warning (channel-aware) ----- */
.v-beta {
background: var(--warningSubtle);
color: var(--contentPrimary);
}
.v-beta .icon-wrap { background: var(--surfaceSecondary); color: var(--warning); }
.v-beta .sub { color: var(--contentSecondary); }
.v-beta .cta { background: var(--warning); color: #fff; }
.v-beta .beta-pill {
display: inline-block;
font-size: 10px; font-weight: 700; letter-spacing: .04em;
padding: 1px 7px; border-radius: 999px;
background: var(--warning); color: #fff;
margin-right: 6px; vertical-align: middle;
transform: translateY(-1px);
}
/* ----- Variant 4: Best-Lap Hero (red, like PB) ----- */
.v-best {
background: var(--bestLapRowTint);
color: var(--contentPrimary);
border: 1px solid var(--divider);
}
.v-best .icon-wrap { background: var(--bestLapTint12); color: var(--bestLap); }
.v-best .sub { color: var(--contentSecondary); }
.v-best .cta { background: var(--surfaceInverse); color: var(--contentInverse); }
/* ----- Variant 5: Inverse + Accent Pill (loud bg, blue CTA) ----- */
.v-inverse-accent {
background: var(--surfaceInverse);
color: var(--contentInverse);
}
.v-inverse-accent .icon-wrap { background: rgba(255,255,255,.12); color: var(--contentInverse); }
.v-inverse-accent .sub { color: var(--contentInverse); opacity: .55; }
.v-inverse-accent .cta { background: var(--accent); color: #fff; }
/* ----- Variant 6: Quiet Strip (compact, one-line) ----- */
.v-strip {
background: var(--surfaceQuiet);
color: var(--contentPrimary);
padding: 10px var(--cardPadding);
border-radius: var(--r-md);
display: flex; align-items: center; gap: 10px;
}
.v-strip .icon-wrap {
width: 22px; height: 22px; flex: 0 0 22px;
background: transparent; color: var(--accent);
}
.v-strip .icon-wrap .icon { width: 18px; height: 18px; }
.v-strip .text { font-size: 13.5px; flex: 1; }
.v-strip .text b { font-weight: 600; }
.v-strip .text .sub { color: var(--contentSecondary); margin-left: 6px; font-size: 12.5px; }
.v-strip .cta {
font-size: 13.5px; font-weight: 600; color: var(--accent);
background: transparent; padding: 4px 0;
}
/* Token call-out */
.tokens {
margin-top: 14px;
padding: 12px;
background: #FAFAFB;
border-radius: 10px;
font: 11.5px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
color: #444;
}
.tokens b { color: #111; }
/* The summary card up top */
.summary {
max-width: 1240px;
margin: 28px auto 0;
padding: 18px 20px;
border-radius: 16px;
background: #fff;
border: 1px solid #ECECEE;
font-size: 13.5px;
line-height: 1.55;
color: #444;
}
.summary code {
background: #F4F4F7; padding: 1px 6px; border-radius: 5px;
font: 12px ui-monospace, SFMono-Regular, Menlo, monospace;
}
</style>
</head>
<body>
<div class="page-header">
<h1>Update Available — Banner Color Directions</h1>
<p>
The banner sits on the Dashboard above the cards and tells the user a new TestFlight (or App Store) build is ready.
Today it's the loud <code style="background:#F4F4F7;padding:1px 6px;border-radius:4px">surfaceInverse&nbsp;+&nbsp;contentInverse</code> CTA — visually it screams.
Below are six token-only directions, each in light + dark. All colors come from <b>LapsterTheme</b>.
</p>
</div>
<div class="summary">
<b>Context:</b> You're on the <b>TestFlight</b> channel. <code>AppBuildChannel.current</code> resolves to <code>.testflight</code> for sandbox-receipt installs and <code>.appstore</code> for prod. The banner copy says <i>"Update Available — Version 1.2.3 is ready to install"</i>. Tap opens the TestFlight or App Store app via <code>UpdateLinkOpener</code>.
</div>
<div class="grid">
<!-- =================== Today =================== -->
<div class="card">
<h2>0 · Today — Loud Inverse</h2>
<span class="tag">current</span>
<p class="desc">Black banner (white in dark mode) + white "Update" pill. Maximum attention. Reuses the "loud CTA" pair from the unsaved-changes Save button.</p>
<div class="stages">
<div class="scheme-label">Light</div>
<div class="stage">
<div class="banner v-today">
<div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div>
<div class="text">
<div class="title">Update Available</div>
<div class="sub">Version 1.2.3 is ready to install</div>
</div>
<div class="cta">Update</div>
</div>
</div>
<div class="scheme-label">Dark</div>
<div class="stage dark">
<div class="banner v-today">
<div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div>
<div class="text">
<div class="title">Update Available</div>
<div class="sub">Version 1.2.3 is ready to install</div>
</div>
<div class="cta">Update</div>
</div>
</div>
</div>
<div class="tokens"><b>bg:</b> surfaceInverse · <b>fg:</b> contentInverse · <b>pill:</b> surfacePrimary / contentPrimary</div>
</div>
<!-- =================== 1 · Quiet Card =================== -->
<div class="card">
<h2>1 · Quiet Card</h2>
<span class="tag pick">my pick</span>
<p class="desc">Sits as a normal elevated card in the dashboard stack — same visual weight as the other dashboard sections. Accent owns just the icon badge and the CTA pill. Calm, premium, "of-a-piece" with the rest of the home.</p>
<div class="stages">
<div class="scheme-label">Light</div>
<div class="stage">
<div class="banner v-quiet">
<div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div>
<div class="text">
<div class="title">Update Available</div>
<div class="sub">Version 1.2.3 is ready to install</div>
</div>
<div class="cta">Update</div>
</div>
</div>
<div class="scheme-label">Dark</div>
<div class="stage dark">
<div class="banner v-quiet">
<div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div>
<div class="text">
<div class="title">Update Available</div>
<div class="sub">Version 1.2.3 is ready to install</div>
</div>
<div class="cta">Update</div>
</div>
</div>
</div>
<div class="tokens"><b>bg:</b> surfaceSecondary + 1px divider · <b>icon bg:</b> accentSubtle · <b>icon fg:</b> accent · <b>title:</b> contentPrimary · <b>sub:</b> contentSecondary · <b>pill:</b> accent / white</div>
</div>
<!-- =================== 2 · Accent Tint =================== -->
<div class="card">
<h2>2 · Accent Tinted</h2>
<span class="tag">soft attention</span>
<p class="desc">Soft blue wash. Apple-style "informational nudge". Reads as friendly + clearly clickable without yelling.</p>
<div class="stages">
<div class="scheme-label">Light</div>
<div class="stage">
<div class="banner v-accent">
<div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div>
<div class="text">
<div class="title">Update Available</div>
<div class="sub">Version 1.2.3 is ready to install</div>
</div>
<div class="cta">Update</div>
</div>
</div>
<div class="scheme-label">Dark</div>
<div class="stage dark">
<div class="banner v-accent">
<div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div>
<div class="text">
<div class="title">Update Available</div>
<div class="sub">Version 1.2.3 is ready to install</div>
</div>
<div class="cta">Update</div>
</div>
</div>
</div>
<div class="tokens"><b>bg:</b> accentSubtle · <b>icon bg:</b> surfaceSecondary · <b>icon fg:</b> accent · <b>pill:</b> accent / white</div>
</div>
<!-- =================== 3 · TestFlight (warning) =================== -->
<div class="card">
<h2>3 · TestFlight Beta</h2>
<span class="tag">channel-aware</span>
<p class="desc">For <code style="background:#F4F4F7;padding:1px 5px;border-radius:4px">.testflight</code> only — warm orange "beta" language with a BETA pill. App Store releases would render Variant 1 or 2 instead. Tells the user "you're on the beta channel, here's a beta update" without rainbow.</p>
<div class="stages">
<div class="scheme-label">Light</div>
<div class="stage">
<div class="banner v-beta">
<div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div>
<div class="text">
<div class="title"><span class="beta-pill">BETA</span>Update Available</div>
<div class="sub">TestFlight 1.2.3 (20260522170045)</div>
</div>
<div class="cta">Update</div>
</div>
</div>
<div class="scheme-label">Dark</div>
<div class="stage dark">
<div class="banner v-beta">
<div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div>
<div class="text">
<div class="title"><span class="beta-pill">BETA</span>Update Available</div>
<div class="sub">TestFlight 1.2.3 (20260522170045)</div>
</div>
<div class="cta">Update</div>
</div>
</div>
</div>
<div class="tokens"><b>bg:</b> warningSubtle · <b>icon bg:</b> surfaceSecondary · <b>icon fg:</b> warning · <b>pill + BETA chip:</b> warning / white</div>
</div>
<!-- =================== 4 · Best-Lap Hero =================== -->
<div class="card">
<h2>4 · Best-Lap Hero</h2>
<span class="tag">"this is a win"</span>
<p class="desc">Borrows the personal-best language: <code style="background:#F4F4F7;padding:1px 5px;border-radius:4px">bestLapRowTint</code> background, red icon, inverse-loud CTA. Frames the update as a positive racing moment rather than a chore.</p>
<div class="stages">
<div class="scheme-label">Light</div>
<div class="stage">
<div class="banner v-best">
<div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div>
<div class="text">
<div class="title">Update Available</div>
<div class="sub">Version 1.2.3 is ready to install</div>
</div>
<div class="cta">Update</div>
</div>
</div>
<div class="scheme-label">Dark</div>
<div class="stage dark">
<div class="banner v-best">
<div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div>
<div class="text">
<div class="title">Update Available</div>
<div class="sub">Version 1.2.3 is ready to install</div>
</div>
<div class="cta">Update</div>
</div>
</div>
</div>
<div class="tokens"><b>bg:</b> bestLapRowTint · <b>icon bg:</b> bestLap @ 12% · <b>icon fg:</b> bestLap · <b>pill:</b> surfaceInverse / contentInverse</div>
</div>
<!-- =================== 5 · Inverse + Accent Pill =================== -->
<div class="card">
<h2>5 · Inverse + Accent Pill</h2>
<span class="tag">small swap from today</span>
<p class="desc">Keep today's loud black banner — just swap the white "Update" pill for an accent blue one. One step less monochrome; the blue pill reads as the unambiguous action target.</p>
<div class="stages">
<div class="scheme-label">Light</div>
<div class="stage">
<div class="banner v-inverse-accent">
<div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div>
<div class="text">
<div class="title">Update Available</div>
<div class="sub">Version 1.2.3 is ready to install</div>
</div>
<div class="cta">Update</div>
</div>
</div>
<div class="scheme-label">Dark</div>
<div class="stage dark">
<div class="banner v-inverse-accent">
<div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div>
<div class="text">
<div class="title">Update Available</div>
<div class="sub">Version 1.2.3 is ready to install</div>
</div>
<div class="cta">Update</div>
</div>
</div>
</div>
<div class="tokens"><b>bg:</b> surfaceInverse · <b>fg:</b> contentInverse · <b>icon bg:</b> contentInverse @ 12% · <b>pill:</b> accent / white</div>
</div>
<!-- =================== 6 · Quiet Strip =================== -->
<div class="card">
<h2>6 · Quiet Strip</h2>
<span class="tag">compact / second-pass</span>
<p class="desc">If the user dismisses or has already seen the banner, fall back to this compact one-line strip — surfaceQuiet with a tiny up-chevron and accent text. Low footprint, still discoverable.</p>
<div class="stages">
<div class="scheme-label">Light</div>
<div class="stage">
<div class="v-strip">
<div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div>
<div class="text"><b>Update Available</b> <span class="sub">Version 1.2.3</span></div>
<div class="cta">Update</div>
</div>
</div>
<div class="scheme-label">Dark</div>
<div class="stage dark">
<div class="v-strip">
<div class="icon-wrap"><svg class="icon" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm5 11h-3v4h-4v-4H7l5-5 5 5z"/></svg></div>
<div class="text"><b>Update Available</b> <span class="sub">Version 1.2.3</span></div>
<div class="cta">Update</div>
</div>
</div>
</div>
<div class="tokens"><b>bg:</b> surfaceQuiet · <b>icon fg:</b> accent · <b>title:</b> contentPrimary · <b>sub:</b> contentSecondary · <b>cta:</b> accent text-button</div>
</div>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment