docs: import TRM design handoff + defer adoption to phase 3.8

A design handoff bundle generated by Claude Design (claude.ai/design)
on 2026-05-02. Defines the Bloomberg/F1-pit-wall aesthetic for TRM:
- ink-on-paper base + race-flag red accent (#E8412B)
- square-edged everything, sharp printed offset shadows
- mono numerics (JetBrains Mono) for any changing value
- Goldplay (real licensed font, three weights in bundle fonts/)
- four surfaces designed: dashboard / leaderboard / mobile / marketing
  (SPA scope is the first two)

The bundle is committed in-tree at TRM_Design_System-handoff/ so 3.8
has the full source material when it picks the work up. Includes:
- Top-level + project READMEs (the design spec)
- chats/chat1.md (intent + iteration history)
- colors_and_type.css (token set, drop-in for Tailwind 4 @theme)
- fonts/ (Goldplay regular/semibold/bold)
- ui_kits/ (HTML prototypes per surface)
- preview/ (per-token visual reference cards)

Updated phase-3-dogfood-readiness/README.md task 3.8 row to point at
the bundle and document the recommended approach (retheme shadcn via
CSS variable overrides + Tailwind 4 @theme, not replace).

Why deferred: foundational tokens are non-blocking for Phase 1 (login
+ placeholder home) and Phase 2 (live map without chrome). Applying
them now would either delay dogfood-blocking work or land partial
styling that gets reworked when 3.8 lands the full pass.
This commit is contained in:
2026-05-02 19:10:11 +02:00
parent 0467a4b7ef
commit 8223a566e4
72 changed files with 3154 additions and 1 deletions
@@ -0,0 +1,26 @@
function BibCard({ bib, name, event, projected, status }) {
return (
<div style={{ border: '1px solid var(--ink)', padding: 18, background: 'var(--paper)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span className="pill live" style={{ fontSize: 9 }}><span className="dot"></span>LIVE</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)', letterSpacing: '0.08em', textTransform: 'uppercase' }}>{event}</span>
</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 12, marginTop: 14 }}>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-4)', letterSpacing: '0.10em', textTransform: 'uppercase' }}>BIB</span>
<span style={{ fontFamily: 'var(--font-mono)', fontVariantNumeric: 'tabular-nums', fontSize: 56, fontWeight: 700, lineHeight: 1, letterSpacing: '-0.03em' }}>{bib}</span>
</div>
<div style={{ fontFamily: 'var(--font-display)', fontSize: 20, fontWeight: 700, marginTop: 6 }}>{name}</div>
<div style={{ marginTop: 14, paddingTop: 12, borderTop: '1px solid var(--hairline)', display: 'flex', justifyContent: 'space-between' }}>
<div>
<div style={{ fontFamily: 'var(--font-display)', fontSize: 10, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--ink-3)' }}>Projected finish</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 22, fontWeight: 600, marginTop: 4 }}>{projected}</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontFamily: 'var(--font-display)', fontSize: 10, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--ink-3)' }}>Status</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 14, color: 'var(--green)', marginTop: 6, fontWeight: 600 }}>{status}</div>
</div>
</div>
</div>
);
}
window.BibCard = BibCard;
@@ -0,0 +1,21 @@
function BottomNav({ active, onNav }) {
const items = [
{ id: 'race', label: 'My race', icon: 'M12 2v4 M12 18v4 M2 12h4 M18 12h4' },
{ id: 'follow',label: 'Following', icon: 'M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2 M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8 M23 21v-2a4 4 0 0 0-3-3.87 M16 3.13a4 4 0 0 1 0 7.75' },
{ id: 'find', label: 'Find', icon: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16 M21 21l-4.35-4.35' },
{ id: 'me', label: 'Profile', icon: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2 M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8' },
];
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', borderTop: '1px solid var(--ink)', background: 'var(--paper)' }}>
{items.map(it => (
<div key={it.id} onClick={() => onNav(it.id)} style={{ padding: '10px 0 6px', textAlign: 'center', cursor: 'pointer', borderTop: active === it.id ? '2px solid var(--flag)' : '2px solid transparent', marginTop: -1 }}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke={active === it.id ? 'var(--flag)' : 'var(--ink-3)'} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block', margin: '0 auto' }}>
{it.icon.split(' M').map((d, i) => <path key={i} d={(i === 0 ? '' : 'M') + d} />)}
</svg>
<div style={{ fontFamily: 'var(--font-display)', fontSize: 10, fontWeight: 600, letterSpacing: '0.06em', textTransform: 'uppercase', marginTop: 4, color: active === it.id ? 'var(--flag)' : 'var(--ink-3)' }}>{it.label}</div>
</div>
))}
</div>
);
}
window.BottomNav = BottomNav;
@@ -0,0 +1,24 @@
function FollowList({ people, onSelect }) {
return (
<div style={{ border: '1px solid var(--ink)', background: 'var(--paper)' }}>
<div style={{ padding: '10px 14px', borderBottom: '1px solid var(--ink)', background: 'var(--paper-2)', display: 'flex', alignItems: 'center' }}>
<span style={{ fontFamily: 'var(--font-display)', fontSize: 11, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase' }}>Following</span>
<span style={{ marginLeft: 'auto', fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)' }}>{people.length}</span>
</div>
{people.map((p, i) => (
<div key={i} onClick={() => onSelect && onSelect(p)} style={{ display: 'grid', gridTemplateColumns: '40px 1fr auto', gap: 12, padding: '12px 14px', borderBottom: i < people.length - 1 ? '1px solid var(--hairline)' : 'none', alignItems: 'center', cursor: 'pointer' }}>
<div style={{ width: 36, height: 36, border: '1px solid var(--ink)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700 }}>{p.bib}</div>
<div>
<div style={{ fontFamily: 'var(--font-display)', fontSize: 14, fontWeight: 600 }}>{p.name}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)', marginTop: 2 }}>{p.event} · {p.lastSplit}</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600 }}>{p.time}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: p.delta && p.delta.startsWith('-') ? 'var(--green)' : 'var(--flag)', marginTop: 2 }}>{p.delta}</div>
</div>
</div>
))}
</div>
);
}
window.FollowList = FollowList;
@@ -0,0 +1,13 @@
# Mobile UI Kit — TRM
Athlete & spectator mobile companion. Inside an iOS frame for context.
## Screens
- **My race** — athlete view: bib, current split, projected finish, course progress
- **Following** — spectator view: list of tracked athletes with live status
## Components
- `BibCard.jsx` — hero bib + projected finish
- `SplitList.jsx` — vertical timeline of splits
- `FollowList.jsx` — list of followed athletes
- `BottomNav.jsx` — tab bar
@@ -0,0 +1,22 @@
function SplitList({ splits }) {
return (
<div style={{ border: '1px solid var(--ink)', background: 'var(--paper)' }}>
<div style={{ padding: '10px 14px', borderBottom: '1px solid var(--ink)', background: 'var(--paper-2)', display: 'flex', alignItems: 'center' }}>
<span style={{ fontFamily: 'var(--font-display)', fontSize: 11, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase' }}>Splits</span>
<span style={{ marginLeft: 'auto', fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)' }}>4 of 6</span>
</div>
{splits.map((s, i) => (
<div key={i} style={{ display: 'grid', gridTemplateColumns: '24px 1fr auto auto', gap: 12, padding: '10px 14px', borderBottom: i < splits.length - 1 ? '1px solid var(--hairline)' : 'none', alignItems: 'center' }}>
<span style={{ width: 10, height: 10, borderRadius: 999, background: s.passed ? 'var(--flag)' : 'var(--paper)', border: '2px solid ' + (s.passed ? 'var(--flag)' : 'var(--ink-5)'), justifySelf: 'center' }}></span>
<div>
<div style={{ fontFamily: 'var(--font-display)', fontSize: 13, fontWeight: 600 }}>{s.name}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--ink-4)', marginTop: 2 }}>{s.km} km</div>
</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600, color: s.passed ? 'var(--ink)' : 'var(--ink-4)' }}>{s.time}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: s.passed ? (s.delta.startsWith('-') ? 'var(--green)' : 'var(--flag)') : 'var(--ink-4)' }}>{s.delta}</div>
</div>
))}
</div>
);
}
window.SplitList = SplitList;
@@ -0,0 +1,84 @@
<!doctype html>
<html><head><meta charset="utf-8">
<title>TRM Mobile · UI Kit</title>
<link rel="stylesheet" href="../../colors_and_type.css">
<link rel="stylesheet" href="../kit.css">
<style>
body { background: var(--paper-3); display: flex; align-items: center; justify-content: center; padding: 32px; min-height: 100vh; }
.stage { display: flex; gap: 32px; align-items: flex-start; }
</style>
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel" src="ios-frame.jsx"></script>
<script type="text/babel" src="BibCard.jsx"></script>
<script type="text/babel" src="SplitList.jsx"></script>
<script type="text/babel" src="FollowList.jsx"></script>
<script type="text/babel" src="BottomNav.jsx"></script>
<script type="text/babel">
const { useState } = React;
const SPLITS = [
{ name: 'Start', km: 0, time: '09:00:00.0', delta: '—', passed: true },
{ name: 'Split 1', km: 2.5, time: '00:10:38.4', delta: '-0:01.2', passed: true },
{ name: 'Split 2', km: 5.0, time: '00:21:14.7', delta: '-0:02.0', passed: true },
{ name: 'Split 3', km: 7.5, time: '00:31:55.1', delta: '+0:00.4', passed: true },
{ name: 'Split 4', km: 9.0, time: '—', delta: '—', passed: false },
{ name: 'Finish', km: 10, time: '—', delta: '—', passed: false },
];
const PEOPLE = [
{ bib: 247, name: 'Maya Chen', event: 'Coastline 10K', lastSplit: 'Split 3', time: '31:55.1', delta: '+0:00.4' },
{ bib: 188, name: 'Ravi Park', event: 'Coastline 10K', lastSplit: 'Split 3', time: '32:01.6', delta: '-0:01.8' },
{ bib: 44, name: 'Noemi Vega', event: 'Coastline 10K', lastSplit: 'Split 3', time: '32:08.4', delta: '+0:02.1' },
{ bib: 612, name: 'Diego Acosta', event: 'Twin Peaks Tri', lastSplit: 'Bike', time: '1:12:04', delta: '-0:18.0' },
];
function MyRace() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, padding: 14 }}>
<BibCard bib="247" name="Maya Chen" event="Coastline 10K · Spring 2026" projected="00:42:12" status="ON PACE · PB" />
<SplitList splits={SPLITS} />
</div>
);
}
function Following({ onSelect }) {
return (
<div style={{ padding: 14 }}>
<FollowList people={PEOPLE} onSelect={onSelect} />
</div>
);
}
function App() {
const [tab, setTab] = useState('race');
return (
<div className="stage">
<IOSDevice title="My race" width={390} height={780}>
<div style={{ background: 'var(--paper-2)', minHeight: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ flex: 1, overflow: 'auto' }}>
{tab === 'race' && <MyRace />}
{tab === 'follow' && <Following onSelect={() => setTab('race')} />}
{tab === 'find' && <div style={{ padding: 24, textAlign: 'center', color: 'var(--ink-4)', fontFamily: 'var(--font-mono)', fontSize: 12 }}>Search any event by name or bib.</div>}
{tab === 'me' && <div style={{ padding: 24, textAlign: 'center', color: 'var(--ink-4)', fontFamily: 'var(--font-mono)', fontSize: 12 }}>Profile · history · personal bests</div>}
</div>
<BottomNav active={tab} onNav={setTab} />
</div>
</IOSDevice>
<IOSDevice title="Following" width={390} height={780}>
<div style={{ background: 'var(--paper-2)', minHeight: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ flex: 1, overflow: 'auto' }}><Following /></div>
<BottomNav active="follow" onNav={() => {}} />
</div>
</IOSDevice>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body></html>
@@ -0,0 +1,338 @@
// iOS.jsx — Simplified iOS 26 (Liquid Glass) device frame
// Based on the iOS 26 UI Kit + Figma status bar spec. No assets, no deps.
// Exports: IOSDevice, IOSStatusBar, IOSNavBar, IOSGlassPill, IOSList, IOSListRow, IOSKeyboard
// ─────────────────────────────────────────────────────────────
// Status bar
// ─────────────────────────────────────────────────────────────
function IOSStatusBar({ dark = false, time = '9:41' }) {
const c = dark ? '#fff' : '#000';
return (
<div style={{
display: 'flex', gap: 154, alignItems: 'center', justifyContent: 'center',
padding: '21px 24px 19px', boxSizing: 'border-box',
position: 'relative', zIndex: 20, width: '100%',
}}>
<div style={{ flex: 1, height: 22, display: 'flex', alignItems: 'center', justifyContent: 'center', paddingTop: 1.5 }}>
<span style={{
fontFamily: '-apple-system, "SF Pro", system-ui', fontWeight: 590,
fontSize: 17, lineHeight: '22px', color: c,
}}>{time}</span>
</div>
<div style={{ flex: 1, height: 22, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 7, paddingTop: 1, paddingRight: 1 }}>
<svg width="19" height="12" viewBox="0 0 19 12">
<rect x="0" y="7.5" width="3.2" height="4.5" rx="0.7" fill={c}/>
<rect x="4.8" y="5" width="3.2" height="7" rx="0.7" fill={c}/>
<rect x="9.6" y="2.5" width="3.2" height="9.5" rx="0.7" fill={c}/>
<rect x="14.4" y="0" width="3.2" height="12" rx="0.7" fill={c}/>
</svg>
<svg width="17" height="12" viewBox="0 0 17 12">
<path d="M8.5 3.2C10.8 3.2 12.9 4.1 14.4 5.6L15.5 4.5C13.7 2.7 11.2 1.5 8.5 1.5C5.8 1.5 3.3 2.7 1.5 4.5L2.6 5.6C4.1 4.1 6.2 3.2 8.5 3.2Z" fill={c}/>
<path d="M8.5 6.8C9.9 6.8 11.1 7.3 12 8.2L13.1 7.1C11.8 5.9 10.2 5.1 8.5 5.1C6.8 5.1 5.2 5.9 3.9 7.1L5 8.2C5.9 7.3 7.1 6.8 8.5 6.8Z" fill={c}/>
<circle cx="8.5" cy="10.5" r="1.5" fill={c}/>
</svg>
<svg width="27" height="13" viewBox="0 0 27 13">
<rect x="0.5" y="0.5" width="23" height="12" rx="3.5" stroke={c} strokeOpacity="0.35" fill="none"/>
<rect x="2" y="2" width="20" height="9" rx="2" fill={c}/>
<path d="M25 4.5V8.5C25.8 8.2 26.5 7.2 26.5 6.5C26.5 5.8 25.8 4.8 25 4.5Z" fill={c} fillOpacity="0.4"/>
</svg>
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// Liquid glass pill — blur + tint + shine
// ─────────────────────────────────────────────────────────────
function IOSGlassPill({ children, dark = false, style = {} }) {
return (
<div style={{
height: 44, minWidth: 44, borderRadius: 9999,
position: 'relative', overflow: 'hidden',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: dark
? '0 2px 6px rgba(0,0,0,0.35), 0 6px 16px rgba(0,0,0,0.2)'
: '0 1px 3px rgba(0,0,0,0.07), 0 3px 10px rgba(0,0,0,0.06)',
...style,
}}>
{/* blur + tint */}
<div style={{
position: 'absolute', inset: 0, borderRadius: 9999,
backdropFilter: 'blur(12px) saturate(180%)',
WebkitBackdropFilter: 'blur(12px) saturate(180%)',
background: dark ? 'rgba(120,120,128,0.28)' : 'rgba(255,255,255,0.5)',
}} />
{/* shine */}
<div style={{
position: 'absolute', inset: 0, borderRadius: 9999,
boxShadow: dark
? 'inset 1.5px 1.5px 1px rgba(255,255,255,0.15), inset -1px -1px 1px rgba(255,255,255,0.08)'
: 'inset 1.5px 1.5px 1px rgba(255,255,255,0.7), inset -1px -1px 1px rgba(255,255,255,0.4)',
border: dark ? '0.5px solid rgba(255,255,255,0.15)' : '0.5px solid rgba(0,0,0,0.06)',
}} />
<div style={{ position: 'relative', zIndex: 1, display: 'flex', alignItems: 'center', padding: '0 4px' }}>
{children}
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// Navigation bar — glass pills + large title
// ─────────────────────────────────────────────────────────────
function IOSNavBar({ title = 'Title', dark = false, trailingIcon = true }) {
const muted = dark ? 'rgba(255,255,255,0.6)' : '#404040';
const text = dark ? '#fff' : '#000';
const pillIcon = (content) => (
<IOSGlassPill dark={dark}>
<div style={{ width: 36, height: 36, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{content}
</div>
</IOSGlassPill>
);
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 10,
paddingTop: 62, paddingBottom: 10, position: 'relative', zIndex: 5,
}}>
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '0 16px',
}}>
{/* back chevron */}
{pillIcon(
<svg width="12" height="20" viewBox="0 0 12 20" fill="none" style={{ marginLeft: -1 }}>
<path d="M10 2L2 10l8 8" stroke={muted} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)}
{/* trailing ellipsis */}
{trailingIcon && pillIcon(
<svg width="22" height="6" viewBox="0 0 22 6">
<circle cx="3" cy="3" r="2.5" fill={muted}/>
<circle cx="11" cy="3" r="2.5" fill={muted}/>
<circle cx="19" cy="3" r="2.5" fill={muted}/>
</svg>
)}
</div>
{/* large title */}
<div style={{
padding: '0 16px',
fontFamily: '-apple-system, system-ui',
fontSize: 34, fontWeight: 700, lineHeight: '41px',
color: text, letterSpacing: 0.4,
}}>{title}</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// Grouped list (inset card, r:26) + row (52px)
// ─────────────────────────────────────────────────────────────
function IOSListRow({ title, detail, icon, chevron = true, isLast = false, dark = false }) {
const text = dark ? '#fff' : '#000';
const sec = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)';
const ter = dark ? 'rgba(235,235,245,0.3)' : 'rgba(60,60,67,0.3)';
const sep = dark ? 'rgba(84,84,88,0.65)' : 'rgba(60,60,67,0.12)';
return (
<div style={{
display: 'flex', alignItems: 'center', minHeight: 52,
padding: '0 16px', position: 'relative',
fontFamily: '-apple-system, system-ui', fontSize: 17,
letterSpacing: -0.43,
}}>
{icon && (
<div style={{
width: 30, height: 30, borderRadius: 7, background: icon,
marginRight: 12, flexShrink: 0,
}} />
)}
<div style={{ flex: 1, color: text }}>{title}</div>
{detail && <span style={{ color: sec, marginRight: 6 }}>{detail}</span>}
{chevron && (
<svg width="8" height="14" viewBox="0 0 8 14" style={{ flexShrink: 0 }}>
<path d="M1 1l6 6-6 6" stroke={ter} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
)}
{!isLast && (
<div style={{
position: 'absolute', bottom: 0, right: 0,
left: icon ? 58 : 16, height: 0.5, background: sep,
}} />
)}
</div>
);
}
function IOSList({ header, children, dark = false }) {
const hc = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)';
const bg = dark ? '#1C1C1E' : '#fff';
return (
<div>
{header && (
<div style={{
fontFamily: '-apple-system, system-ui', fontSize: 13,
color: hc, textTransform: 'uppercase',
padding: '8px 36px 6px', letterSpacing: -0.08,
}}>{header}</div>
)}
<div style={{
background: bg, borderRadius: 26,
margin: '0 16px', overflow: 'hidden',
}}>{children}</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// Device frame
// ─────────────────────────────────────────────────────────────
function IOSDevice({
children, width = 402, height = 874, dark = false,
title, keyboard = false,
}) {
return (
<div style={{
width, height, borderRadius: 48, overflow: 'hidden',
position: 'relative', background: dark ? '#000' : '#F2F2F7',
boxShadow: '0 40px 80px rgba(0,0,0,0.18), 0 0 0 1px rgba(0,0,0,0.12)',
fontFamily: '-apple-system, system-ui, sans-serif',
WebkitFontSmoothing: 'antialiased',
}}>
{/* dynamic island */}
<div style={{
position: 'absolute', top: 11, left: '50%', transform: 'translateX(-50%)',
width: 126, height: 37, borderRadius: 24, background: '#000', zIndex: 50,
}} />
{/* status bar (absolute) */}
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 10 }}>
<IOSStatusBar dark={dark} />
</div>
{/* nav + content */}
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
{title !== undefined && <IOSNavBar title={title} dark={dark} />}
<div style={{ flex: 1, overflow: 'auto' }}>{children}</div>
{keyboard && <IOSKeyboard dark={dark} />}
</div>
{/* home indicator — always on top */}
<div style={{
position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 60,
height: 34, display: 'flex', justifyContent: 'center', alignItems: 'flex-end',
paddingBottom: 8, pointerEvents: 'none',
}}>
<div style={{
width: 139, height: 5, borderRadius: 100,
background: dark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.25)',
}} />
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// Keyboard — iOS 26 liquid glass
// ─────────────────────────────────────────────────────────────
function IOSKeyboard({ dark = false }) {
const glyph = dark ? 'rgba(255,255,255,0.7)' : '#595959';
const sugg = dark ? 'rgba(255,255,255,0.6)' : '#333';
const keyBg = dark ? 'rgba(255,255,255,0.22)' : 'rgba(255,255,255,0.85)';
// special-key icons
const icons = {
shift: <svg width="19" height="17" viewBox="0 0 19 17"><path d="M9.5 1L1 9.5h4.5V16h8V9.5H18L9.5 1z" fill={glyph}/></svg>,
del: <svg width="23" height="17" viewBox="0 0 23 17"><path d="M7 1h13a2 2 0 012 2v11a2 2 0 01-2 2H7l-6-7.5L7 1z" fill="none" stroke={glyph} strokeWidth="1.6" strokeLinejoin="round"/><path d="M10 5l7 7M17 5l-7 7" stroke={glyph} strokeWidth="1.6" strokeLinecap="round"/></svg>,
ret: <svg width="20" height="14" viewBox="0 0 20 14"><path d="M18 1v6H4m0 0l4-4M4 7l4 4" fill="none" stroke="#fff" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>,
};
const key = (content, { w, flex, ret, fs = 25, k } = {}) => (
<div key={k} style={{
height: 42, borderRadius: 8.5,
flex: flex ? 1 : undefined, width: w, minWidth: 0,
background: ret ? '#08f' : keyBg,
boxShadow: '0 1px 0 rgba(0,0,0,0.075)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: '-apple-system, "SF Compact", system-ui',
fontSize: fs, fontWeight: 458, color: ret ? '#fff' : glyph,
}}>{content}</div>
);
const row = (keys, pad = 0) => (
<div style={{ display: 'flex', gap: 6.5, justifyContent: 'center', padding: `0 ${pad}px` }}>
{keys.map(l => key(l, { flex: true, k: l }))}
</div>
);
return (
<div style={{
position: 'relative', zIndex: 15, borderRadius: 27, overflow: 'hidden',
padding: '11px 0 2px',
display: 'flex', flexDirection: 'column', alignItems: 'center',
boxShadow: dark
? '0 -2px 20px rgba(0,0,0,0.09)'
: '0 -1px 6px rgba(0,0,0,0.018), 0 -3px 20px rgba(0,0,0,0.012)',
}}>
{/* liquid glass bg — same recipe as nav pills */}
<div style={{
position: 'absolute', inset: 0, borderRadius: 27,
backdropFilter: 'blur(12px) saturate(180%)',
WebkitBackdropFilter: 'blur(12px) saturate(180%)',
background: dark ? 'rgba(120,120,128,0.14)' : 'rgba(255,255,255,0.25)',
}} />
<div style={{
position: 'absolute', inset: 0, borderRadius: 27,
boxShadow: dark
? 'inset 1.5px 1.5px 1px rgba(255,255,255,0.15)'
: 'inset 1.5px 1.5px 1px rgba(255,255,255,0.7), inset -1px -1px 1px rgba(255,255,255,0.4)',
border: dark ? '0.5px solid rgba(255,255,255,0.15)' : '0.5px solid rgba(0,0,0,0.06)',
pointerEvents: 'none',
}} />
{/* autocorrect bar */}
<div style={{
display: 'flex', gap: 20, alignItems: 'center',
padding: '8px 22px 13px', width: '100%', boxSizing: 'border-box',
position: 'relative',
}}>
{['"The"', 'the', 'to'].map((w, i) => (
<React.Fragment key={i}>
{i > 0 && <div style={{ width: 1, height: 25, background: '#ccc', opacity: 0.3 }} />}
<div style={{
flex: 1, textAlign: 'center',
fontFamily: '-apple-system, system-ui', fontSize: 17,
color: sugg, letterSpacing: -0.43, lineHeight: '22px',
}}>{w}</div>
</React.Fragment>
))}
</div>
{/* key layout */}
<div style={{
display: 'flex', flexDirection: 'column', gap: 13,
padding: '0 6.5px', width: '100%', boxSizing: 'border-box',
position: 'relative',
}}>
{row(['q','w','e','r','t','y','u','i','o','p'])}
{row(['a','s','d','f','g','h','j','k','l'], 20)}
<div style={{ display: 'flex', gap: 14.25, alignItems: 'center' }}>
{key(icons.shift, { w: 45, k: 'shift' })}
<div style={{ display: 'flex', gap: 6.5, flex: 1 }}>
{['z','x','c','v','b','n','m'].map(l => key(l, { flex: true, k: l }))}
</div>
{key(icons.del, { w: 45, k: 'del' })}
</div>
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
{key('ABC', { w: 92.25, fs: 18, k: 'abc' })}
{key('', { flex: true, k: 'space' })}
{key(icons.ret, { w: 92.25, ret: true, k: 'ret' })}
</div>
</div>
{/* bottom spacer (emoji+mic area, icons omitted) */}
<div style={{ height: 56, width: '100%', position: 'relative' }} />
</div>
);
}
Object.assign(window, {
IOSDevice, IOSStatusBar, IOSNavBar, IOSGlassPill, IOSList, IOSListRow, IOSKeyboard,
});