Add local-first refactor foundation

This commit is contained in:
2026-05-25 00:45:46 -04:00
parent 161e871c53
commit 517c6b987b
45 changed files with 11545 additions and 697 deletions

View File

@@ -0,0 +1,683 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>box-box — Command Center</title>
<link rel="stylesheet" href="styles.css">
<style>
/* ── Command Center Layout ── */
.cc-layout {
display: grid;
grid-template-columns: 220px 1fr 260px;
grid-template-rows: 1fr;
height: calc(100vh - 44px);
}
/* ── Left: Weekend Nav ── */
.cc-sidebar {
border-right: 1px solid var(--c-border);
padding: var(--s5);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--s5);
}
.weekend-badge {
display: flex;
align-items: center;
gap: var(--s2);
font-size: 11px;
color: var(--c-text-2);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: var(--s1);
}
.session-list { display: flex; flex-direction: column; gap: 1px; }
.session-row {
display: flex;
align-items: center;
gap: var(--s2);
padding: 7px var(--s3);
border-radius: 2px;
font-size: 12px;
cursor: pointer;
transition: background 0.1s;
}
.session-row:hover { background: var(--c-surface-2); }
.session-row.active { background: var(--c-surface-2); }
.session-row.current { border-left: 2px solid var(--c-red); padding-left: calc(var(--s3) - 2px); }
.session-name { flex: 1; color: var(--c-text-2); }
.session-name.done { color: var(--c-text-3); }
.session-time {
font-family: var(--f-mono);
font-size: 10px;
color: var(--c-text-3);
}
.year-switcher {
display: flex;
gap: 2px;
}
.year-btn {
padding: 3px 8px;
background: none;
border: 1px solid var(--c-border);
border-radius: 2px;
font-size: 11px;
color: var(--c-text-3);
transition: all 0.1s;
}
.year-btn.active {
background: var(--c-surface-2);
border-color: var(--c-border-2);
color: var(--c-text);
}
/* ── Center: Main Content ── */
.cc-main {
overflow-y: auto;
padding: var(--s5);
display: flex;
flex-direction: column;
gap: var(--sec-gap);
}
.race-hero {
display: flex;
flex-direction: column;
gap: var(--s4);
}
.race-location {
display: flex;
align-items: baseline;
gap: var(--s3);
}
.race-flag { font-size: 24px; line-height: 1; }
.race-name {
font-size: 22px;
font-weight: 700;
letter-spacing: -0.01em;
color: var(--c-text);
}
.race-meta {
font-size: 12px;
color: var(--c-text-3);
}
.race-round {
display: inline-block;
padding: 2px 7px;
border: 1px solid var(--c-border-2);
border-radius: 2px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--c-text-3);
}
.countdown-section {
display: flex;
flex-direction: column;
gap: var(--s3);
}
.next-session-label {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--c-text-3);
}
.next-session-name {
font-size: 13px;
font-weight: 600;
color: var(--c-red);
}
.schedule-table th:first-child,
.schedule-table td:first-child { padding-left: 0; }
.session-status-cell {
display: flex;
align-items: center;
gap: 6px;
}
.weather-strip {
display: flex;
gap: var(--s5);
padding: var(--s3) var(--s4);
background: var(--c-surface);
border: 1px solid var(--c-border);
}
.wx-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.wx-label {
font-size: 9px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--c-text-3);
}
.wx-val {
font-family: var(--f-mono);
font-size: 13px;
font-weight: 600;
color: var(--c-text);
}
/* ── Right: Championship + Data ── */
.cc-right {
border-left: 1px solid var(--c-border);
padding: var(--s5);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--s5);
}
.champ-tabs {
display: flex;
gap: 2px;
margin-bottom: var(--s3);
}
.champ-tab-btn {
padding: 3px 8px;
background: none;
border: 1px solid transparent;
border-radius: 2px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--c-text-3);
transition: all 0.1s;
}
.champ-tab-btn.active {
color: var(--c-text);
border-color: var(--c-border-2);
background: var(--c-surface-2);
}
.champ-table { width: 100%; font-size: 11px; }
.champ-row {
display: grid;
grid-template-columns: 20px 1fr 48px;
align-items: center;
gap: var(--s2);
padding: 5px 0;
border-bottom: 1px solid var(--c-border);
}
.champ-row:last-child { border-bottom: none; }
.champ-pos { font-family: var(--f-mono); color: var(--c-text-3); font-size: 11px; text-align: right; }
.champ-name { font-weight: 600; }
.champ-pts { font-family: var(--f-mono); font-weight: 700; text-align: right; }
.data-avail-list {
display: flex;
flex-direction: column;
gap: 1px;
}
.avail-row {
display: flex;
align-items: center;
gap: var(--s2);
padding: 5px 0;
border-bottom: 1px solid var(--c-border);
font-size: 11px;
}
.avail-row:last-child { border-bottom: none; }
.avail-name { flex: 1; color: var(--c-text-2); }
.avail-count { font-family: var(--f-mono); font-size: 10px; color: var(--c-text-3); }
/* ── Responsive ── */
/* ── iPad: 2-col, hide left sidebar ── */
@media (min-width: 769px) and (max-width: 1100px) {
.cc-layout { grid-template-columns: 1fr 260px; }
.cc-sidebar { display: none; }
}
/* ── Phone: full-width stacked ── */
@media (max-width: 768px) {
.cc-layout { grid-template-columns: 1fr; grid-template-rows: auto; height: auto; }
.cc-sidebar { display: none; }
.cc-right { border-left: none; border-top: 1px solid var(--c-border); }
/* On phone, show session list inline inside main */
.cc-mobile-sessions { display: flex !important; }
}
</style>
</head>
<body>
<nav class="app-nav">
<a href="index.html" class="nav-logo">box<em>-</em>box</a>
<div class="nav-links">
<a href="command-center.html" class="active">Command Center</a>
<a href="live-timing.html">Live</a>
<a href="race-hub.html">Race Hub</a>
<a href="data-library.html">Data Library</a>
</div>
<div class="nav-right">
<div class="live-badge"><span class="live-dot"></span>LIVE</div>
<div class="density-toggle">
<button class="active" onclick="setDensity('default',this)">D</button>
<button onclick="setDensity('compact',this)">C</button>
</div>
</div>
</nav>
<div class="cc-layout">
<!-- ── Left: Weekend sessions ── -->
<aside class="cc-sidebar">
<div>
<div class="weekend-badge">
<span class="dot local"></span>
Monaco 2025
</div>
<div class="year-switcher" style="margin-bottom: var(--s4)">
<button class="year-btn active">2025</button>
<button class="year-btn">2024</button>
<button class="year-btn">2023</button>
</div>
<div class="session-list">
<div class="session-row">
<span class="dot local"></span>
<span class="session-name done">FP1</span>
<span class="session-time">Thu 11:30</span>
</div>
<div class="session-row">
<span class="dot local"></span>
<span class="session-name done">FP2</span>
<span class="session-time">Thu 15:00</span>
</div>
<div class="session-row">
<span class="dot local"></span>
<span class="session-name done">FP3</span>
<span class="session-time">Sat 11:30</span>
</div>
<div class="session-row">
<span class="dot local"></span>
<span class="session-name done">Qualifying</span>
<span class="session-time">Sat 15:00</span>
</div>
<div class="session-row current active">
<span class="dot live"></span>
<span class="session-name" style="color: var(--c-red); font-weight: 600;">Race</span>
<span class="session-time" style="color: var(--c-red);">LIVE</span>
</div>
</div>
</div>
<div>
<div class="sec-header">
<span class="sec-title">Upcoming</span>
</div>
<div class="session-list">
<div class="session-row">
<span class="dot upcoming"></span>
<span class="session-name done">Canada GP</span>
<span class="session-time">Jun 13</span>
</div>
<div class="session-row">
<span class="dot upcoming"></span>
<span class="session-name done">Austria GP</span>
<span class="session-time">Jun 27</span>
</div>
<div class="session-row">
<span class="dot upcoming"></span>
<span class="session-name done">British GP</span>
<span class="session-time">Jul 4</span>
</div>
</div>
</div>
</aside>
<!-- ── Center: Race weekend hero ── -->
<main class="cc-main">
<!-- Hero -->
<div class="race-hero">
<div style="display:flex; align-items:center; gap: var(--s3)">
<span class="race-round">Round 8</span>
<span class="badge live">
<span class="dot live"></span>
Race In Progress
</span>
<a href="live-timing.html" style="margin-left:auto; font-size:11px; color:var(--c-text-2); border:1px solid var(--c-border); padding: 3px 10px; border-radius:2px; font-weight:600; letter-spacing:0.04em;">Open Live →</a>
</div>
<div class="race-location">
<span class="race-flag">🇲🇨</span>
<div>
<div class="race-name">Monaco Grand Prix</div>
<div class="race-meta">Circuit de Monaco · Monte Carlo · 2225 May 2025</div>
</div>
</div>
</div>
<!-- Inline session list: visible on phone/iPad where sidebar is hidden -->
<div class="cc-mobile-sessions" style="display:none; gap:2px; flex-wrap:wrap; margin-top:var(--s1)">
<span style="font-size:10px; color:var(--c-text-3); margin-right:var(--s2); align-self:center; text-transform:uppercase; letter-spacing:.08em;">Weekend</span>
<span class="badge local">FP1 ✓</span>
<span class="badge local">FP2 ✓</span>
<span class="badge local">FP3 ✓</span>
<span class="badge local">Qual ✓</span>
<span class="badge live">Race ●</span>
</div>
<!-- Live race state / countdown if not live -->
<div style="padding: var(--s4); background: var(--c-surface); border: 1px solid var(--c-border); border-left: 3px solid var(--c-red);">
<div style="display:flex; justify-content: space-between; align-items: baseline; margin-bottom: var(--s3)">
<span style="font-size:11px; font-weight:700; letter-spacing:0.1em; text-transform:uppercase; color: var(--c-red)">Race — In Progress</span>
<a href="live-timing.html" style="font-size:11px; color: var(--c-text-2); border: 1px solid var(--c-border); padding: 2px 8px; border-radius:2px;">Open Live Timing →</a>
</div>
<div style="display:grid; grid-template-columns: repeat(4, 1fr); gap: var(--s4)">
<div>
<div style="font-size:9px; text-transform:uppercase; letter-spacing:0.1em; color:var(--c-text-3); margin-bottom:3px">Lap</div>
<div style="font-family: var(--f-mono); font-size:22px; font-weight:700; color: var(--c-text)">45<span style="font-size:14px; color: var(--c-text-3)">/78</span></div>
</div>
<div>
<div style="font-size:9px; text-transform:uppercase; letter-spacing:0.1em; color:var(--c-text-3); margin-bottom:3px">Leader</div>
<div style="font-size:18px; font-weight:700; color: var(--t-fer)">LEC</div>
</div>
<div>
<div style="font-size:9px; text-transform:uppercase; letter-spacing:0.1em; color:var(--c-text-3); margin-bottom:3px">Gap P1P2</div>
<div style="font-family: var(--f-mono); font-size:20px; font-weight:700; color:var(--c-text)">+3.4s</div>
</div>
<div>
<div style="font-size:9px; text-transform:uppercase; letter-spacing:0.1em; color:var(--c-text-3); margin-bottom:3px">Track</div>
<div style="font-size:13px; font-weight:700;" class="track-green">● GREEN</div>
</div>
</div>
</div>
<!-- Schedule -->
<div>
<div class="sec-header">
<span class="sec-title">Weekend Schedule</span>
<span class="sec-meta">Circuit de Monaco · 3.337 km · 78 laps</span>
</div>
<table class="data-table schedule-table">
<thead>
<tr>
<th>Session</th>
<th>Date</th>
<th>Local Time</th>
<th>UTC</th>
<th>Status</th>
<th>Data</th>
</tr>
</thead>
<tbody>
<tr>
<td><span style="font-weight:600">FP1</span></td>
<td class="t3">Thu 22 May</td>
<td class="mono t2">11:30</td>
<td class="mono t3">09:30</td>
<td><span class="badge local">Done</span></td>
<td><span class="badge local">Full</span></td>
</tr>
<tr>
<td><span style="font-weight:600">FP2</span></td>
<td class="t3">Thu 22 May</td>
<td class="mono t2">15:00</td>
<td class="mono t3">13:00</td>
<td><span class="badge local">Done</span></td>
<td><span class="badge local">Full</span></td>
</tr>
<tr>
<td><span style="font-weight:600">FP3</span></td>
<td class="t3">Sat 24 May</td>
<td class="mono t2">11:30</td>
<td class="mono t3">09:30</td>
<td><span class="badge local">Done</span></td>
<td><span class="badge local">Full</span></td>
</tr>
<tr>
<td><span style="font-weight:600">Qualifying</span></td>
<td class="t3">Sat 24 May</td>
<td class="mono t2">15:00</td>
<td class="mono t3">13:00</td>
<td><span class="badge local">Done</span></td>
<td><span class="badge local">Full</span></td>
</tr>
<tr>
<td><span style="font-weight:600; color: var(--c-red)">Race</span></td>
<td class="t3">Sun 25 May</td>
<td class="mono" style="color:var(--c-red)">14:00</td>
<td class="mono t3">12:00</td>
<td><span class="badge live">Live</span></td>
<td><span class="badge live">Streaming</span></td>
</tr>
</tbody>
</table>
</div>
<!-- Weather -->
<div>
<div class="sec-header">
<span class="sec-title">Weather</span>
<span class="sec-meta">Race day · updated 2 min ago</span>
</div>
<div class="weather-strip">
<div class="wx-item">
<span class="wx-label">Air</span>
<span class="wx-val">24°C</span>
</div>
<div class="wx-item">
<span class="wx-label">Track</span>
<span class="wx-val">36°C</span>
</div>
<div class="wx-item">
<span class="wx-label">Humidity</span>
<span class="wx-val">62%</span>
</div>
<div class="wx-item">
<span class="wx-label">Wind</span>
<span class="wx-val">7 km/h</span>
</div>
<div class="wx-item">
<span class="wx-label">Direction</span>
<span class="wx-val">NW</span>
</div>
<div class="wx-item">
<span class="wx-label">Rain Risk</span>
<span class="wx-val t-green">2%</span>
</div>
<div class="wx-item">
<span class="wx-label">Pressure</span>
<span class="wx-val">1014 hPa</span>
</div>
</div>
</div>
</main>
<!-- ── Right: Championship + Data ── -->
<aside class="cc-right">
<!-- Championship -->
<div>
<div class="sec-header">
<span class="sec-title">Championship</span>
<span class="sec-meta">After R7</span>
</div>
<div class="champ-tabs">
<button class="champ-tab-btn active" onclick="switchChamp('drivers', this)">Drivers</button>
<button class="champ-tab-btn" onclick="switchChamp('constructors', this)">Constructors</button>
</div>
<div id="champ-drivers">
<div class="champ-row">
<span class="champ-pos">1</span>
<span class="champ-name" style="color: var(--t-rb)">Verstappen</span>
<span class="champ-pts">138</span>
</div>
<div class="champ-row">
<span class="champ-pos">2</span>
<span class="champ-name" style="color: var(--t-mcl)">Norris</span>
<span class="champ-pts">116</span>
</div>
<div class="champ-row">
<span class="champ-pos">3</span>
<span class="champ-name" style="color: var(--t-fer)">Leclerc</span>
<span class="champ-pts">98</span>
</div>
<div class="champ-row">
<span class="champ-pos">4</span>
<span class="champ-name" style="color: var(--t-mcl)">Piastri</span>
<span class="champ-pts">86</span>
</div>
<div class="champ-row">
<span class="champ-pos">5</span>
<span class="champ-name" style="color: var(--t-mer)">Russell</span>
<span class="champ-pts">74</span>
</div>
<div class="champ-row">
<span class="champ-pos">6</span>
<span class="champ-name" style="color: var(--t-fer)">Hamilton</span>
<span class="champ-pts">62</span>
</div>
<div class="champ-row">
<span class="champ-pos">7</span>
<span class="champ-name" style="color: var(--t-am)">Alonso</span>
<span class="champ-pts">41</span>
</div>
<div class="champ-row">
<span class="champ-pos">8</span>
<span class="champ-name" style="color: var(--t-wil)">Sainz</span>
<span class="champ-pts">34</span>
</div>
</div>
<div id="champ-constructors" style="display:none">
<div class="champ-row">
<span class="champ-pos">1</span>
<span class="champ-name" style="color: var(--t-mcl)">McLaren</span>
<span class="champ-pts">202</span>
</div>
<div class="champ-row">
<span class="champ-pos">2</span>
<span class="champ-name" style="color: var(--t-rb)">Red Bull</span>
<span class="champ-pts">186</span>
</div>
<div class="champ-row">
<span class="champ-pos">3</span>
<span class="champ-name" style="color: var(--t-fer)">Ferrari</span>
<span class="champ-pts">162</span>
</div>
<div class="champ-row">
<span class="champ-pos">4</span>
<span class="champ-name" style="color: var(--t-mer)">Mercedes</span>
<span class="champ-pts">136</span>
</div>
<div class="champ-row">
<span class="champ-pos">5</span>
<span class="champ-name" style="color: var(--t-am)">Aston Martin</span>
<span class="champ-pts">54</span>
</div>
</div>
</div>
<!-- Data availability -->
<div>
<div class="sec-header">
<span class="sec-title">Local Data</span>
<a href="data-library.html" style="font-size:10px; color: var(--c-text-3); margin-left: auto">View all →</a>
</div>
<div class="data-avail-list">
<div class="avail-row">
<span class="dot local"></span>
<span class="avail-name">Bahrain GP</span>
<span class="avail-count t-green">Full</span>
</div>
<div class="avail-row">
<span class="dot local"></span>
<span class="avail-name">Saudi Arabia GP</span>
<span class="avail-count t-green">Full</span>
</div>
<div class="avail-row">
<span class="dot local"></span>
<span class="avail-name">Australian GP</span>
<span class="avail-count t-green">Full</span>
</div>
<div class="avail-row">
<span class="dot partial"></span>
<span class="avail-name">Japanese GP</span>
<span class="avail-count t-yellow">Partial</span>
</div>
<div class="avail-row">
<span class="dot partial"></span>
<span class="avail-name">Chinese GP</span>
<span class="avail-count t-yellow">Partial</span>
</div>
<div class="avail-row">
<span class="dot local"></span>
<span class="avail-name">Miami GP</span>
<span class="avail-count t-green">Full</span>
</div>
<div class="avail-row">
<span class="dot local"></span>
<span class="avail-name">Emilia Romagna GP</span>
<span class="avail-count t-green">Full</span>
</div>
<div class="avail-row">
<span class="dot live"></span>
<span class="avail-name">Monaco GP</span>
<span class="avail-count t-red">Live</span>
</div>
</div>
<div style="margin-top: var(--s3); padding: var(--s3) var(--s3); background: var(--c-surface-2); border: 1px solid var(--c-border); font-size: 10px; color: var(--c-text-3)">
<span style="color: var(--c-yellow)"></span>
2 weekends have partial data.
<a href="data-library.html" style="color: var(--c-text-2); text-decoration: underline; text-underline-offset: 2px;">View ingest commands</a>
</div>
</div>
</aside>
</div>
<script>
function setDensity(mode, btn) {
document.querySelectorAll('.density-toggle button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.documentElement.classList.toggle('compact', mode === 'compact');
}
function switchChamp(tab, btn) {
document.querySelectorAll('.champ-tab-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('champ-drivers').style.display = tab === 'drivers' ? '' : 'none';
document.getElementById('champ-constructors').style.display = tab === 'constructors' ? '' : 'none';
}
</script>
</body>
</html>

View File

@@ -0,0 +1,375 @@
# Component Notes
Implementation reference for translating the static mockups into React components.
Each entry covers purpose, screens, data shape, and responsive behaviour.
---
## App Navigation — `<AppNav>`
**Purpose**: Persistent sticky top bar across all screens. Shows logo, route links, live status badge, and density toggle.
**Appears in**: All screens (always mounted).
**Props / data needed**:
```ts
interface AppNavProps {
activePage: 'home' | 'live' | 'race-hub' | 'data-library' | 'standings' | 'drivers'
isLive: boolean // shows animated red dot + RACE/QUALI badge
sessionLabel?: string // e.g. "Monaco GP — Race"
densityMode: 'default' | 'compact'
onDensityChange: (mode) => void
}
```
**Responsive**: On phone (<768px), collapse nav links to icon-only or a hamburger. Live badge and density toggle remain visible. Session label moves to the session banner.
---
## Density Toggle — `<DensityToggle>`
**Purpose**: Switches between default and compact row heights. Effect is applied as a CSS class on `<html>`, not via React state cascade.
**Appears in**: `<AppNav>`.
**Implementation note**: Call `document.documentElement.classList.toggle('compact', ...)` directly. Store preference in `localStorage`. React state only tracks the value for rendering the active button — does not gate CSS.
**Responsive**: Always visible. Two-button [D][C] works at any width.
---
## Session Status Banner — `<SessionBanner>`
**Purpose**: Real-time session state strip: session name, current lap, session clock, track status, DRS state, fastest lap. Fed by SSE.
**Appears in**: `/live` (always visible at top, sticky). On phone, replaces AppNav as the primary context header.
**Props / data needed**:
```ts
interface SessionBannerProps {
sessionName: string // "Monaco GP — Race"
lapCurrent: number
lapTotal: number
sessionClock: string // "1:02:34"
trackStatus: 'green' | 'yellow' | 'red' | 'sc' | 'vsc' | 'unknown'
drsEnabled: boolean
fastestLap?: { driverCode: string; lapTime: number; lap: number }
airTemp: number
trackTemp: number
isConnected: boolean // SSE connection state
}
```
**Critical state**: `isConnected: false` must show a visible DISCONNECTED indicator. Do not silently go stale.
**Responsive**: Horizontal scroll on narrow viewports. On phone, show session + lap + track status as minimum; other fields scroll off-screen.
---
## Timing Tower — `<TimingTower>`
**Purpose**: Dense real-time table. One row per driver. Driven by SSE. This is the primary surface of the live screen.
**Appears in**: `/live`.
**Props / data needed**:
```ts
interface TimingEntry {
position: number
driverCode: string
driverNumber: number
teamColor: string
gap: string // "LEADER" | "+3.456" | "+1 LAP"
interval: string // "+3.456" | "—"
tyre: 'S' | 'M' | 'H' | 'I' | 'W'
tyreAge: number // laps on current tyre
lastLap: number // seconds
bestLap: number // seconds
s1State: 'pb' | 'ob' | 'slow' | 'none'
s2State: 'pb' | 'ob' | 'slow' | 'none'
s3State: 'pb' | 'ob' | 'slow' | 'none'
isPitIn: boolean
isPitOut: boolean
hasFastestLap: boolean
isDNF: boolean
}
interface TimingTowerProps {
entries: TimingEntry[]
pinnedDrivers: string[] // driver codes
fastestLapDriver: string
}
```
**Performance critical**: SSE ticks every 12 seconds. Memoize `<TimingRow>` by driver number. Diff at the entry level, not the full array. Use `React.memo` + stable references. Avoid full re-renders.
**Columns on phone**: P · Driver · Gap · Tyre · Last (hide Int, Age, Best, Sectors).
**Columns on iPad**: P · Driver · Gap · Int · Tyre · Age · Last (hide Best, Sectors).
**Columns on desktop**: All columns.
---
## Race Control Feed — `<RaceControlFeed>`
**Purpose**: Scrolling list of race control messages. Auto-scrolls to newest. Color-coded by message type (SC, VSC, DRS, penalty, fastest lap, flag).
**Appears in**: `/live` sidebar, `/race-hub` Race Control tab (static version).
**Props / data needed**:
```ts
interface RCMessage {
id: string
lap: number
type: 'sc' | 'vsc' | 'drs' | 'penalty' | 'fl' | 'flag' | 'info'
text: string
timestamp: string
}
interface RaceControlFeedProps {
messages: RCMessage[]
autoScroll: boolean
}
```
**Responsive**: In `/live` sidebar on desktop. On phone, a full-screen panel accessed via bottom tab. In race-hub, inline full-width list.
---
## Battle Row / Battles Panel — `<BattlesList>`
**Purpose**: Shows detected on-track pairs with gap value and closing/stable/opening trend.
**Appears in**: `/live` sidebar.
**Props / data needed**:
```ts
interface Battle {
aheadCode: string
aheadTeamColor: string
behindCode: string
behindTeamColor: string
gapSeconds: number
trend: 'closing' | 'stable' | 'opening'
}
interface BattlesListProps {
battles: Battle[]
}
```
**Responsive**: On phone, shown in the Battles bottom-tab panel alongside Pit Window.
---
## Pinned Driver Strip — `<PinnedStrip>`
**Purpose**: Compact horizontal cards for drivers the user has pinned. Shows position, gap, last lap, tyre state at a glance without scrolling the timing tower.
**Appears in**: `/live` (below session banner, above timing tower). Hidden on phone to preserve space.
**Props / data needed**:
```ts
interface PinnedCardData {
driverCode: string
teamColor: string
position: number
gap: string
lastLap: string
tyre: string
specialState?: 'pit-out' | 'fastest-lap' | 'dnf'
}
interface PinnedStripProps {
cards: PinnedCardData[]
onUnpin: (driverCode: string) => void
}
```
**Responsive**: Desktop + iPad only. Hide on phone (<768px) — tower already shows all data.
---
## Pit Window Panel — `<PitWindowPanel>`
**Purpose**: For each driver with a notable stint age, shows current tyre, age, and pit window status (OPEN / SOON / OVERDUE / DONE). Computed from stint age and expected compound life.
**Appears in**: `/live` sidebar (third section after RC and Battles).
**Props / data needed**:
```ts
interface PitWindowEntry {
driverCode: string
teamColor: string
tyre: 'S' | 'M' | 'H' | 'I' | 'W'
tyreAge: number
windowStatus: 'open' | 'soon' | 'overdue' | 'done'
}
```
**Note**: Window status logic lives in the backend or a pure TS utility — not component logic.
**Responsive**: Desktop sidebar. On phone, shown in Battles tab panel below battle list.
---
## Strategy Chart — `<StrategyChart>`
**Purpose**: D3-owned horizontal stint chart. One row per driver (top 10), colored blocks = tyre compound, width = laps. SC/VSC period shading. Lap counter axis.
**Appears in**: `/race-hub` Strategy tab.
**Props / data needed**:
```ts
interface Stint {
compound: 'S' | 'M' | 'H' | 'I' | 'W'
startLap: number
endLap: number
}
interface StrategyDriver {
code: string
teamColor: string
stints: Stint[]
}
interface StrategyChartProps {
drivers: StrategyDriver[]
totalLaps: number
scPeriods: Array<{ start: number; end: number }>
vscPeriods: Array<{ start: number; end: number }>
}
```
**D3 contract**: Component owns its SVG DOM node. Mount/update via `useEffect` with D3. Resize via `ResizeObserver`. No React inside the SVG.
**Responsive**: Full-width SVG with `viewBox`, scales with container. Label column width fixed. On narrow screens (< 480px), driver labels may need abbreviating.
---
## Position Evolution Chart — `<PositionEvolution>`
**Purpose**: D3-owned line chart. X = lap, Y = position (1 at top). One line per driver (top 6). SC/VSC period bands. End labels per driver.
**Appears in**: `/race-hub` Positions tab.
**Props / data needed**:
```ts
interface PositionPoint { lap: number; position: number }
interface PositionDriver {
code: string
color: string
dashed: boolean // true for teammate (same team color)
points: PositionPoint[]
}
interface PositionEvolutionProps {
drivers: PositionDriver[]
totalLaps: number
scPeriods: Array<{ start: number; end: number }>
vscPeriods: Array<{ start: number; end: number }>
}
```
**D3 contract**: Same as StrategyChart — D3 owns the SVG, React manages data and container sizing.
**Responsive**: Full-width `viewBox` SVG. Right-side labels need right-padding.
---
## Dataset Status Indicator — `<DatasetStatus>`
**Purpose**: Shows completeness of local data for a session. Used in two modes: full grid (Race Hub Dataset tab, Data Library detail) and mini (Race Hub aside, API response metadata strip).
**Appears in**: `/race-hub` Dataset tab, `/race-hub` aside, `/data-library` detail panel. API response metadata.
**Props / data needed**:
```ts
type DatasetState = 'local' | 'partial' | 'missing' | 'stale' | 'live'
interface DatasetEntry {
name: string // e.g. "laps", "car_data_samples"
state: DatasetState
lastIngestedAt?: string
error?: string
}
interface DatasetStatusProps {
datasets: DatasetEntry[]
variant: 'grid' | 'mini' | 'strip'
}
```
**Responsive**: Grid variant reflows to 1-column on phone. Mini variant stays compact at all widths. Strip variant is a horizontal overflow row (source strip in the sub-header).
---
## Ingest Command Block — `<IngestCommandBlock>`
**Purpose**: Displays one or more CLI ingest commands with syntax highlighting (comment lines, command lines). Not interactive — display only.
**Appears in**: `/race-hub` Dataset tab, `/data-library` detail panel.
**Props / data needed**:
```ts
interface IngestLine {
type: 'comment' | 'command'
text: string
}
interface IngestCommandBlockProps {
lines: IngestLine[]
}
```
**Responsive**: Horizontal scroll on overflow. Monospace font required.
---
## Responsive Panel Shell — `<PanelShell>`
**Purpose**: Layout wrapper that switches between sidebar-on-right (desktop), stacked (tablet), and tab-driven (phone) based on viewport. Used in Live Timing and Race Hub.
**Appears in**: Used internally by `/live` and `/race-hub`.
**Props / data needed**:
```ts
interface PanelShellProps {
main: React.ReactNode // always visible
aside: React.ReactNode // sidebar on desktop, bottom on tablet
phoneTabs?: Array<{ // only on phone — replaces aside with tabs
id: string
label: string
icon: string
content: React.ReactNode
}>
asideWidth?: number // default 300
}
```
**Responsive**:
- Desktop (>1024px): `main | aside` grid
- iPad (7691024px): `main | aside` with narrower aside
- Phone (<768px): `main` full-width + bottom tab nav switching aside panels
**Implementation note**: Density mode class on `<html>` flows through without prop drilling. `PanelShell` does not need to know about density — CSS handles it.
---
## Source/Freshness Strip — `<SourceStrip>`
**Purpose**: Inline metadata bar attached to API responses. Shows data source, last ingest time, staleness state, and list of missing datasets.
**Appears in**: Sub-headers of `/race-hub`, `/command-center` (data status for weekend), any screen that reads from the local DB.
**Props / data needed**:
```ts
interface SourceStripProps {
source: 'local' | 'api' | 'cache' | 'live' | 'missing'
lastIngestedAt?: string
isStale?: boolean
missingDatasets?: string[]
}
```
**Responsive**: Wraps on narrow viewports. Badges remain readable at any width.

View File

@@ -0,0 +1,651 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>box-box — Data Library</title>
<link rel="stylesheet" href="styles.css">
<style>
/* ── Layout ── */
.dl-layout {
display: grid;
grid-template-columns: 200px 1fr;
height: calc(100vh - 44px);
overflow: hidden;
}
/* ── Left: Season nav ── */
.dl-nav {
border-right: 1px solid var(--c-border);
padding: var(--s5);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--s5);
}
.season-list { display: flex; flex-direction: column; gap: 1px; }
.season-row {
display: flex;
align-items: center;
gap: var(--s2);
padding: 7px var(--s3);
border-radius: 2px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.1s;
}
.season-row:hover { background: var(--c-surface-2); }
.season-row.active { background: var(--c-surface-2); color: var(--c-text); }
.season-row-count {
margin-left: auto;
font-size: 10px;
font-family: var(--f-mono);
color: var(--c-text-3);
}
/* Summary stats */
.dl-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1px;
border: 1px solid var(--c-border);
}
.dl-stat {
padding: var(--s3) var(--s3);
background: var(--c-surface);
display: flex;
flex-direction: column;
gap: 2px;
}
.dl-stat-label {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--c-text-3);
}
.dl-stat-val {
font-family: var(--f-mono);
font-size: 16px;
font-weight: 700;
}
/* ── Right: Content ── */
.dl-content {
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Content header */
.dl-content-header {
display: flex;
align-items: center;
gap: var(--s4);
padding: var(--s3) var(--s5);
background: var(--c-surface);
border-bottom: 1px solid var(--c-border);
flex-shrink: 0;
flex-wrap: wrap;
}
/* Content body split */
.dl-content-body {
display: grid;
grid-template-columns: 1fr 340px;
flex: 1;
overflow: hidden;
}
/* Round table */
.dl-round-scroll {
overflow-y: auto;
border-right: 1px solid var(--c-border);
}
.rounds-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.rounds-table th {
position: sticky;
top: 0;
padding: var(--s1) var(--pad-h);
background: var(--c-surface);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.07em;
text-transform: uppercase;
color: var(--c-text-3);
border-bottom: 1px solid var(--c-border);
white-space: nowrap;
z-index: 10;
}
.rounds-table td {
padding: var(--pad-v) var(--pad-h);
border-bottom: 1px solid var(--c-border);
height: var(--row-h);
white-space: nowrap;
}
.rounds-table tbody tr { cursor: pointer; transition: background 0.08s; }
.rounds-table tbody tr:hover { background: var(--c-surface-2); }
.rounds-table tbody tr.active { background: var(--c-surface-3); }
.rounds-table tbody tr:last-child td { border-bottom: none; }
.session-icons {
display: flex;
gap: 3px;
align-items: center;
}
.si {
width: 14px;
height: 14px;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
font-weight: 700;
font-family: var(--f-mono);
}
.si.full { background: rgba(0,204,106,0.2); color: var(--c-green); }
.si.partial { background: rgba(255,214,0,0.2); color: var(--c-yellow); }
.si.missing { background: rgba(50,50,50,0.5); color: var(--c-text-3); }
.si.live { background: rgba(225,6,0,0.2); color: var(--c-red); }
.si.future { background: rgba(40,40,40,0.5); color: var(--c-text-3); }
/* ── Detail panel ── */
.dl-detail {
overflow-y: auto;
padding: var(--s4);
display: flex;
flex-direction: column;
gap: var(--s4);
}
.detail-header {
display: flex;
flex-direction: column;
gap: var(--s2);
padding-bottom: var(--s4);
border-bottom: 1px solid var(--c-border);
}
.detail-title {
font-size: 14px;
font-weight: 700;
}
.detail-meta {
font-size: 11px;
color: var(--c-text-3);
}
.session-detail-row {
margin-bottom: var(--s4);
}
.session-detail-head {
display: flex;
align-items: center;
gap: var(--s2);
margin-bottom: var(--s2);
font-size: 11px;
font-weight: 700;
color: var(--c-text-2);
text-transform: uppercase;
letter-spacing: 0.07em;
}
.ingest-btn {
margin-left: auto;
padding: 2px 8px;
background: none;
border: 1px solid var(--c-border-2);
border-radius: 2px;
font-size: 10px;
font-weight: 600;
color: var(--c-text-3);
letter-spacing: 0.04em;
transition: all 0.1s;
text-transform: uppercase;
}
.ingest-btn:hover {
border-color: var(--c-green);
color: var(--c-green);
}
.dataset-list { display: flex; flex-direction: column; }
.ds-row {
display: flex;
align-items: center;
gap: var(--s2);
padding: 4px 0;
border-bottom: 1px solid var(--c-border);
font-size: 11px;
}
.ds-row:last-child { border-bottom: none; }
.ds-row-name { flex: 1; color: var(--c-text-2); font-family: var(--f-mono); font-size: 10px; }
.ds-row-time { font-family: var(--f-mono); font-size: 10px; color: var(--c-text-3); }
/* ── Responsive ── */
/* ── iPad ── */
@media (max-width: 900px) {
.dl-layout { grid-template-columns: 1fr; height: auto; overflow: auto; }
.dl-nav { border-right: none; border-bottom: 1px solid var(--c-border); padding: var(--s4); }
/* On iPad, show season as horizontal tabs */
.season-list { flex-direction: row; gap: var(--s2); }
.dl-content { height: auto; overflow: visible; }
.dl-content-body { grid-template-columns: 1fr; height: auto; overflow: visible; }
.dl-round-scroll { border-right: none; overflow: visible; }
.dl-detail { border-top: 1px solid var(--c-border); }
}
/* ── Phone ── */
@media (max-width: 480px) {
.dl-stats { grid-template-columns: repeat(4, 1fr); }
.dl-content-body { display: block; }
.dl-round-scroll .rounds-table td:nth-child(3),
.dl-round-scroll .rounds-table th:nth-child(3) { display: none; }
}
</style>
</head>
<body>
<nav class="app-nav">
<a href="index.html" class="nav-logo">box<em>-</em>box</a>
<div class="nav-links">
<a href="command-center.html">Command Center</a>
<a href="live-timing.html">Live</a>
<a href="race-hub.html">Race Hub</a>
<a href="data-library.html" class="active">Data Library</a>
</div>
<div class="nav-right">
<div class="density-toggle">
<button class="active" onclick="setDensity('default',this)">D</button>
<button onclick="setDensity('compact',this)">C</button>
</div>
</div>
</nav>
<div class="dl-layout">
<!-- ── Left: season nav ── -->
<aside class="dl-nav">
<div>
<div class="sec-header">
<span class="sec-title">Seasons</span>
</div>
<div class="season-list">
<div class="season-row active" onclick="selectSeason(this, '2025')">
<span class="dot local"></span>
<span>2025</span>
<span class="season-row-count">7 / 24</span>
</div>
<div class="season-row" onclick="selectSeason(this, '2024')">
<span class="dot local"></span>
<span>2024</span>
<span class="season-row-count">24 / 24</span>
</div>
<div class="season-row" onclick="selectSeason(this, '2023')">
<span class="dot missing"></span>
<span>2023</span>
<span class="season-row-count">0 / 22</span>
</div>
</div>
</div>
<div>
<div class="sec-header">
<span class="sec-title">Storage</span>
</div>
<div style="font-size:11px; display:flex; flex-direction:column; gap:5px;">
<div class="flex ai-c gap-2">
<span class="t3">Database</span>
<span class="mono t2" style="margin-left:auto">~/.cache/box-box/db.sqlite</span>
</div>
<div class="flex ai-c gap-2">
<span class="t3">Size</span>
<span class="mono t2" style="margin-left:auto">142 MB</span>
</div>
<div class="flex ai-c gap-2">
<span class="t3">Raw payloads</span>
<span class="mono t2" style="margin-left:auto">89 MB</span>
</div>
</div>
</div>
<div>
<div class="dl-stats">
<div class="dl-stat">
<span class="dl-stat-label">Full</span>
<span class="dl-stat-val t-green">7</span>
</div>
<div class="dl-stat">
<span class="dl-stat-label">Partial</span>
<span class="dl-stat-val t-yellow">2</span>
</div>
<div class="dl-stat">
<span class="dl-stat-label">Missing</span>
<span class="dl-stat-val t3">15</span>
</div>
<div class="dl-stat">
<span class="dl-stat-label">Live</span>
<span class="dl-stat-val t-red">1</span>
</div>
</div>
</div>
</aside>
<!-- ── Right: content ── -->
<div class="dl-content">
<!-- Content header -->
<div class="dl-content-header">
<span style="font-size:13px; font-weight:700">2025 Season</span>
<span class="t3" style="font-size:12px">24 rounds · 7 complete · 2 partial · 1 live</span>
<div style="margin-left:auto; display:flex; gap:var(--s2)">
<select style="background:var(--c-surface-2); border:1px solid var(--c-border); border-radius:2px; color:var(--c-text-2); font-size:11px; padding:3px 6px; font-family:var(--f-ui)">
<option>All rounds</option>
<option>Complete only</option>
<option>Partial / missing</option>
</select>
</div>
</div>
<div class="dl-content-body">
<!-- Round table -->
<div class="dl-round-scroll">
<table class="rounds-table">
<thead>
<tr>
<th style="width:32px">Rnd</th>
<th>Weekend</th>
<th>Date</th>
<th>Status</th>
<th class="hide-mobile">Sessions</th>
<th class="hide-mobile">Last Sync</th>
</tr>
</thead>
<tbody>
<tr onclick="selectRound(this,'bahrain')" class="active">
<td class="mono t3" style="text-align:center">1</td>
<td><span style="font-weight:600">🇧🇭 Bahrain GP</span></td>
<td class="mono t3">2 Mar</td>
<td><span class="badge local"><span class="dot local"></span>Full</span></td>
<td class="hide-mobile">
<div class="session-icons">
<div class="si full">F1</div>
<div class="si full">F2</div>
<div class="si full">F3</div>
<div class="si full">Q</div>
<div class="si full">R</div>
</div>
</td>
<td class="mono t3 hide-mobile">2d ago</td>
</tr>
<tr onclick="selectRound(this,'saudi')">
<td class="mono t3" style="text-align:center">2</td>
<td><span style="font-weight:600">🇸🇦 Saudi Arabia GP</span></td>
<td class="mono t3">16 Mar</td>
<td><span class="badge local"><span class="dot local"></span>Full</span></td>
<td class="hide-mobile">
<div class="session-icons">
<div class="si full">F1</div>
<div class="si full">F2</div>
<div class="si full">F3</div>
<div class="si full">Q</div>
<div class="si full">R</div>
</div>
</td>
<td class="mono t3 hide-mobile">5d ago</td>
</tr>
<tr onclick="selectRound(this,'australia')">
<td class="mono t3" style="text-align:center">3</td>
<td><span style="font-weight:600">🇦🇺 Australian GP</span></td>
<td class="mono t3">30 Mar</td>
<td><span class="badge local"><span class="dot local"></span>Full</span></td>
<td class="hide-mobile">
<div class="session-icons">
<div class="si full">F1</div>
<div class="si full">F2</div>
<div class="si full">F3</div>
<div class="si full">Q</div>
<div class="si full">R</div>
</div>
</td>
<td class="mono t3 hide-mobile">8d ago</td>
</tr>
<tr onclick="selectRound(this,'japan')">
<td class="mono t3" style="text-align:center">4</td>
<td><span style="font-weight:600">🇯🇵 Japanese GP</span></td>
<td class="mono t3">13 Apr</td>
<td><span class="badge partial"><span class="dot partial"></span>Partial</span></td>
<td class="hide-mobile">
<div class="session-icons">
<div class="si full">F1</div>
<div class="si full">F2</div>
<div class="si partial">F3</div>
<div class="si full">Q</div>
<div class="si full">R</div>
</div>
</td>
<td class="mono t3 hide-mobile">21d ago</td>
</tr>
<tr onclick="selectRound(this,'china')">
<td class="mono t3" style="text-align:center">5</td>
<td><span style="font-weight:600">🇨🇳 Chinese GP</span></td>
<td class="mono t3">20 Apr</td>
<td><span class="badge partial"><span class="dot partial"></span>Partial</span></td>
<td class="hide-mobile">
<div class="session-icons">
<div class="si full">F1</div>
<div class="si full">F2</div>
<div class="si full">F3</div>
<div class="si missing">Q</div>
<div class="si full">R</div>
</div>
</td>
<td class="mono t3 hide-mobile">28d ago</td>
</tr>
<tr onclick="selectRound(this,'miami')">
<td class="mono t3" style="text-align:center">6</td>
<td><span style="font-weight:600">🇺🇸 Miami GP</span></td>
<td class="mono t3">4 May</td>
<td><span class="badge local"><span class="dot local"></span>Full</span></td>
<td class="hide-mobile">
<div class="session-icons">
<div class="si full">F1</div>
<div class="si full">F2</div>
<div class="si full">F3</div>
<div class="si full">Q</div>
<div class="si full">R</div>
</div>
</td>
<td class="mono t3 hide-mobile">14d ago</td>
</tr>
<tr onclick="selectRound(this,'imola')">
<td class="mono t3" style="text-align:center">7</td>
<td><span style="font-weight:600">🇮🇹 Emilia Romagna GP</span></td>
<td class="mono t3">18 May</td>
<td><span class="badge local"><span class="dot local"></span>Full</span></td>
<td class="hide-mobile">
<div class="session-icons">
<div class="si full">F1</div>
<div class="si full">F2</div>
<div class="si full">F3</div>
<div class="si full">Q</div>
<div class="si full">R</div>
</div>
</td>
<td class="mono t3 hide-mobile">3d ago</td>
</tr>
<tr onclick="selectRound(this,'monaco')">
<td class="mono t3" style="text-align:center">8</td>
<td><span style="font-weight:600">🇲🇨 Monaco GP</span></td>
<td class="mono t3">25 May</td>
<td><span class="badge live"><span class="dot live"></span>Live</span></td>
<td class="hide-mobile">
<div class="session-icons">
<div class="si full">F1</div>
<div class="si full">F2</div>
<div class="si full">F3</div>
<div class="si full">Q</div>
<div class="si live">R</div>
</div>
</td>
<td class="mono t3 hide-mobile">Now</td>
</tr>
<tr onclick="selectRound(this,'canada')">
<td class="mono t3" style="text-align:center">9</td>
<td><span class="t3">🇨🇦 Canadian GP</span></td>
<td class="mono t3">13 Jun</td>
<td><span class="badge missing"><span class="dot missing"></span>Missing</span></td>
<td class="hide-mobile">
<div class="session-icons">
<div class="si future">F1</div>
<div class="si future">F2</div>
<div class="si future">F3</div>
<div class="si future">Q</div>
<div class="si future">R</div>
</div>
</td>
<td class="mono t3 hide-mobile"></td>
</tr>
<tr>
<td class="mono t3" style="text-align:center">10</td>
<td><span class="t3">🇦🇹 Austrian GP</span></td>
<td class="mono t3">27 Jun</td>
<td><span class="badge missing"><span class="dot missing"></span>Missing</span></td>
<td class="hide-mobile">
<div class="session-icons">
<div class="si future">F1</div>
<div class="si future">F2</div>
<div class="si future">F3</div>
<div class="si future">Q</div>
<div class="si future">R</div>
</div>
</td>
<td class="mono t3 hide-mobile"></td>
</tr>
<tr>
<td class="mono t3" style="text-align:center" colspan="6" style="text-align:left; padding: var(--s3) var(--pad-h); color:var(--c-text-3); font-size:11px">
+ 14 upcoming rounds not yet available
</td>
</tr>
</tbody>
</table>
</div>
<!-- Detail panel -->
<div class="dl-detail" id="detail-panel">
<div class="detail-header">
<div style="display:flex; align-items:center; gap:var(--s2)">
<span class="detail-title">🇧🇭 Bahrain Grand Prix</span>
<span class="badge local">Full</span>
</div>
<div class="detail-meta">Round 1 · 2 March 2025 · meeting_key 1230</div>
<div class="detail-meta">Ingested: 2 Mar 2025 18:42 · 142 MB across 5 sessions</div>
</div>
<!-- FP1 -->
<div class="session-detail-row">
<div class="session-detail-head">
<span class="dot local"></span> FP1
<span class="t3" style="font-weight:400; text-transform:none; letter-spacing:0">session_key 9120</span>
</div>
<div class="dataset-list">
<div class="ds-row"><span class="dot local"></span><span class="ds-row-name">laps</span><span class="ds-row-time">2d ago</span></div>
<div class="ds-row"><span class="dot local"></span><span class="ds-row-name">stints</span><span class="ds-row-time">2d ago</span></div>
<div class="ds-row"><span class="dot local"></span><span class="ds-row-name">weather</span><span class="ds-row-time">2d ago</span></div>
<div class="ds-row"><span class="dot missing"></span><span class="ds-row-name">car_data_samples</span><span class="ds-row-time t3">not ingested</span></div>
</div>
</div>
<!-- Race -->
<div class="session-detail-row">
<div class="session-detail-head">
<span class="dot local"></span> Race
<span class="t3" style="font-weight:400; text-transform:none; letter-spacing:0">session_key 9125</span>
<button class="ingest-btn">↓ Refresh</button>
</div>
<div class="dataset-list">
<div class="ds-row"><span class="dot local"></span><span class="ds-row-name">session_results</span><span class="ds-row-time">2d ago</span></div>
<div class="ds-row"><span class="dot local"></span><span class="ds-row-name">starting_grid</span><span class="ds-row-time">2d ago</span></div>
<div class="ds-row"><span class="dot local"></span><span class="ds-row-name">laps</span><span class="ds-row-time">2d ago</span></div>
<div class="ds-row"><span class="dot local"></span><span class="ds-row-name">stints</span><span class="ds-row-time">2d ago</span></div>
<div class="ds-row"><span class="dot local"></span><span class="ds-row-name">pit_stops</span><span class="ds-row-time">2d ago</span></div>
<div class="ds-row"><span class="dot local"></span><span class="ds-row-name">positions</span><span class="ds-row-time">2d ago</span></div>
<div class="ds-row"><span class="dot local"></span><span class="ds-row-name">race_control</span><span class="ds-row-time">2d ago</span></div>
<div class="ds-row"><span class="dot local"></span><span class="ds-row-name">weather</span><span class="ds-row-time">2d ago</span></div>
<div class="ds-row"><span class="dot missing"></span><span class="ds-row-name">car_data_samples</span><span class="ds-row-time t3">not ingested</span></div>
<div class="ds-row"><span class="dot missing"></span><span class="ds-row-name">location_samples</span><span class="ds-row-time t3">not ingested</span></div>
<div class="ds-row"><span class="dot missing"></span><span class="ds-row-name">team_radio</span><span class="ds-row-time t3">not ingested</span></div>
</div>
</div>
<!-- CLI -->
<div>
<div class="sec-header">
<span class="sec-title">Ingest Commands</span>
</div>
<div class="cli-block">
<div class="comment"># Full weekend ingest (all sessions, default datasets)</div>
<div class="cmd">box-box --ingest-meeting 1230</div>
<br>
<div class="comment"># Race session only</div>
<div class="cmd">box-box --ingest-session 9125</div>
<br>
<div class="comment"># High-volume telemetry (explicit, large download)</div>
<div class="cmd">box-box --ingest-session 9125 --datasets car_data,location</div>
<br>
<div class="comment"># Preview without downloading</div>
<div class="cmd">box-box --ingest-meeting 1230 --dry-run</div>
</div>
</div>
</div><!-- /dl-detail -->
</div><!-- /dl-content-body -->
</div><!-- /dl-content -->
</div><!-- /dl-layout -->
<script>
function setDensity(mode, btn) {
document.querySelectorAll('.density-toggle button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.documentElement.classList.toggle('compact', mode === 'compact');
}
function selectSeason(el, season) {
document.querySelectorAll('.season-row').forEach(r => r.classList.remove('active'));
el.classList.add('active');
}
function selectRound(row, id) {
document.querySelectorAll('.rounds-table tbody tr').forEach(r => r.classList.remove('active'));
row.classList.add('active');
}
</script>
</body>
</html>

View File

@@ -0,0 +1,180 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>box-box — Screen Index</title>
<link rel="stylesheet" href="styles.css">
<style>
.index-layout {
max-width: 680px;
margin: 60px auto;
padding: 0 var(--s5);
}
.index-header {
margin-bottom: var(--s8);
}
.index-title {
font-size: 24px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.index-title em { color: var(--c-red); font-style: normal; }
.index-subtitle {
margin-top: var(--s2);
font-size: 12px;
color: var(--c-text-3);
}
.screen-list {
display: flex;
flex-direction: column;
gap: 1px;
border: 1px solid var(--c-border);
}
.screen-item {
display: grid;
grid-template-columns: 200px 1fr auto;
align-items: center;
gap: var(--s4);
padding: var(--s4) var(--s5);
background: var(--c-surface);
transition: background 0.1s;
}
.screen-item:hover { background: var(--c-surface-2); }
.screen-name {
font-size: 13px;
font-weight: 600;
color: var(--c-text);
}
.screen-desc {
font-size: 11px;
color: var(--c-text-3);
line-height: 1.5;
}
.screen-link {
font-family: var(--f-mono);
font-size: 10px;
color: var(--c-text-3);
padding: 3px 8px;
border: 1px solid var(--c-border);
border-radius: 2px;
white-space: nowrap;
}
.screen-item:hover .screen-link {
border-color: var(--c-border-2);
color: var(--c-text-2);
}
.index-note {
margin-top: var(--s5);
padding: var(--s4);
background: var(--c-surface);
border: 1px solid var(--c-border);
border-left: 3px solid var(--c-border-2);
font-size: 11px;
color: var(--c-text-3);
line-height: 1.7;
}
@media (max-width: 600px) {
.screen-item { grid-template-columns: 1fr; gap: var(--s2); }
.screen-link { display: none; }
}
</style>
</head>
<body>
<nav class="app-nav">
<a href="index.html" class="nav-logo">box<em>-</em>box</a>
<div class="nav-links">
<a href="index.html" class="active">Screens</a>
</div>
<div class="nav-right">
<div class="density-toggle">
<button class="active" onclick="setDensity('default', this)">D</button>
<button onclick="setDensity('compact', this)">C</button>
</div>
</div>
</nav>
<div class="index-layout">
<div class="index-header">
<div class="index-title">box<em>-</em>box — Static Mockups</div>
<div class="index-subtitle">
Visual / product validation screens. Static HTML only, no build tooling. Open directly in a browser.
</div>
</div>
<div class="screen-list">
<a href="command-center.html" class="screen-item">
<span class="screen-name">Command Center</span>
<span class="screen-desc">
Landing screen. Current race weekend, countdown, schedule, championship snapshot, data status.
</span>
<span class="screen-link">command-center.html</span>
</a>
<a href="race-hub.html" class="screen-item">
<span class="screen-name">Race Hub</span>
<span class="screen-desc">
Completed race analysis. Classification, strategy chart, position evolution, race control, weather, dataset status.
</span>
<span class="screen-link">race-hub.html</span>
</a>
<a href="live-timing.html" class="screen-item">
<span class="screen-name">Live Timing</span>
<span class="screen-desc">
Active session screen. Timing tower, track status, race control feed, battles, fastest lap strip.
</span>
<span class="screen-link">live-timing.html</span>
</a>
<a href="data-library.html" class="screen-item">
<span class="screen-name">Data Library</span>
<span class="screen-desc">
Local data transparency. Season/weekend ingestion status, missing datasets, CLI commands.
</span>
<span class="screen-link">data-library.html</span>
</a>
<a href="mobile-live.html" class="screen-item" style="border-top: 1px solid var(--c-border-2);">
<span class="screen-name">
Mobile Live
<span style="font-size:10px; font-weight:500; color:var(--c-text-3); margin-left:8px;">layout demo</span>
</span>
<span class="screen-desc">
Dedicated phone layout for Live Timing. Bottom tab bar, full-screen panels. Press D to toggle disconnected state; S to cycle track status.
</span>
<span class="screen-link">mobile-live.html</span>
</a>
</div>
<div class="index-note">
Design direction: <strong style="color: var(--c-text-2)">F1 Ops Room.</strong>
Dense where useful, fast to scan, team color as data not decoration.
No gradient hero panels, no floating card sludge, no decorative chrome.
Responsive: phone-first for live screens, desktop-rich for analysis.
</div>
</div>
<script>
function setDensity(mode, btn) {
document.querySelectorAll('.density-toggle button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.documentElement.classList.toggle('compact', mode === 'compact');
}
</script>
</body>
</html>

View File

@@ -0,0 +1,774 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>box-box — Live Timing · Monaco GP Race</title>
<link rel="stylesheet" href="styles.css">
<style>
/* ── Full-height root ── */
html, body { height: 100%; overflow: hidden; }
.lt-root {
display: flex;
flex-direction: column;
height: calc(100vh - 44px);
}
/* ── Session Banner ── */
.lt-banner {
display: flex;
align-items: center;
height: 40px;
padding: 0 var(--s5);
background: var(--c-surface);
border-bottom: 2px solid var(--c-border);
font-family: var(--f-mono);
font-size: 12px;
overflow-x: auto;
flex-shrink: 0;
}
.bi {
display: flex;
align-items: center;
gap: var(--s2);
padding: 0 var(--s4);
border-right: 1px solid var(--c-border);
flex-shrink: 0;
height: 100%;
}
.bi:first-child { padding-left: 0; }
.bi-label { font-size: 9px; font-family: var(--f-ui); letter-spacing: 0.08em; text-transform: uppercase; color: var(--c-text-3); }
.bi-val { font-weight: 600; color: var(--c-text); }
/* ── Pinned strip ── */
.lt-pinned {
display: flex;
align-items: center;
gap: var(--s1);
padding: 0 var(--s4);
height: 36px;
background: var(--c-surface-2);
border-bottom: 1px solid var(--c-border);
flex-shrink: 0;
overflow-x: auto;
}
.lt-pinned-label {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--c-text-3);
margin-right: var(--s2);
flex-shrink: 0;
}
.pin-card {
display: flex;
align-items: center;
gap: var(--s2);
padding: 4px var(--s3);
background: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: 2px;
flex-shrink: 0;
}
.pin-pos { font-family: var(--f-mono); font-size: 11px; color: var(--c-text-3); min-width: 16px; }
.pin-gap { font-family: var(--f-mono); font-size: 11px; color: var(--c-text-2); }
.pin-last { font-family: var(--f-mono); font-size: 10px; color: var(--c-text-3); }
/* ── Main body ── */
.lt-body {
display: grid;
grid-template-columns: 1fr 300px;
flex: 1;
overflow: hidden;
}
/* ── Tower pane ── */
.lt-tower {
display: flex;
flex-direction: column;
overflow: hidden;
border-right: 1px solid var(--c-border);
}
.lt-tower-scroll { overflow-y: auto; flex: 1; }
/* Timing table */
.tt-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.tt-table th {
position: sticky;
top: 0;
padding: var(--s1) var(--s2);
background: var(--c-surface);
font-size: 9px;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
color: var(--c-text-3);
border-bottom: 1px solid var(--c-border);
text-align: right;
white-space: nowrap;
z-index: 10;
}
.tt-table th.left { text-align: left; }
.tt-table td {
padding: var(--pad-v) var(--s2);
border-bottom: 1px solid var(--c-border);
height: var(--row-h);
text-align: right;
white-space: nowrap;
font-family: var(--f-mono);
font-size: 12px;
}
.tt-table td.left { text-align: left; font-family: var(--f-ui); }
.tt-table tbody tr:hover { background: var(--c-surface-2); }
.tt-pos { font-size: 14px; font-weight: 700; text-align: center; width: 28px; }
.tt-gap { color: var(--c-text-2); min-width: 68px; }
.tt-int { color: var(--c-text-3); min-width: 64px; }
.tt-age { color: var(--c-text-3); min-width: 28px; }
.tt-last { color: var(--c-text-2); min-width: 74px; }
.tt-best { color: var(--c-text-3); min-width: 74px; }
.tt-table tr.fl .tt-best { color: var(--c-purple); font-weight: 700; }
.tt-table tr.pit td { background: rgba(255,214,0,0.04); }
.tt-table tr.pit .tt-gap { color: var(--c-yellow); }
/* Sector dots */
.sectors { display: flex; gap: 2px; align-items: center; }
.s-dot { width: 6px; height: 6px; border-radius: 1px; }
.s-dot.pb { background: var(--c-green); }
.s-dot.ob { background: var(--c-purple); }
.s-dot.sl { background: var(--c-yellow); }
.s-dot.nor { background: var(--c-border-2); }
/* ── Right sidebar ── */
.lt-sidebar {
display: flex;
flex-direction: column;
overflow: hidden;
}
.sb-panel {
display: flex;
flex-direction: column;
overflow: hidden;
border-bottom: 1px solid var(--c-border);
}
.sb-panel:last-child { border-bottom: none; }
.sb-head {
display: flex;
align-items: center;
gap: var(--s2);
padding: var(--s2) var(--s3);
border-bottom: 1px solid var(--c-border);
flex-shrink: 0;
}
.sb-title {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--c-text-3);
}
.sb-body { flex: 1; overflow-y: auto; }
/* RC in sidebar */
.rc-side-msg {
display: grid;
grid-template-columns: 44px 1fr;
gap: var(--s2);
padding: 5px var(--s3);
border-bottom: 1px solid var(--c-border);
font-size: 11px;
}
.rc-side-msg:last-child { border-bottom: none; }
.rc-side-lap { font-family: var(--f-mono); color: var(--c-text-3); padding-top: 1px; }
.rc-side-text { color: var(--c-text-2); line-height: 1.4; }
.rc-side-msg.sc .rc-side-text { color: var(--c-yellow); font-weight: 600; }
.rc-side-msg.drs .rc-side-text { color: var(--c-green); }
.rc-side-msg.fl .rc-side-text { color: var(--c-purple); }
.rc-side-msg.flag .rc-side-text { color: var(--c-red); font-weight: 600; }
/* Battles in sidebar */
.battle-row {
display: flex;
align-items: center;
gap: var(--s2);
padding: 6px var(--s3);
border-bottom: 1px solid var(--c-border);
font-size: 11px;
}
.battle-row:last-child { border-bottom: none; }
.battle-drivers { display: flex; align-items: center; gap: 4px; flex: 1; }
.battle-gap { font-family: var(--f-mono); font-size: 12px; font-weight: 700; }
.battle-trend { font-size: 10px; font-family: var(--f-mono); }
/* ── Footer ── */
.lt-footer {
display: flex;
align-items: center;
gap: var(--s5);
height: 36px;
padding: 0 var(--s5);
background: var(--c-surface);
border-top: 1px solid var(--c-border);
font-size: 11px;
flex-shrink: 0;
overflow-x: auto;
}
.fl-item { display: flex; align-items: center; gap: var(--s2); flex-shrink: 0; }
.fl-label { font-size: 9px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--c-text-3); }
/* ── RESPONSIVE: phone ── */
@media (max-width: 768px) {
html, body { height: auto; overflow: auto; }
.lt-root {
height: auto;
min-height: calc(100vh - 44px);
padding-bottom: 58px;
}
.lt-pinned { display: none; } /* reduce clutter on phone */
.lt-body {
grid-template-columns: 1fr;
flex: none;
overflow: visible;
}
.lt-tower {
border-right: none;
overflow: visible;
flex: none;
}
.lt-tower-scroll { overflow: visible; flex: none; }
.lt-sidebar { display: none; } /* replaced by phone panels */
.lt-footer { display: none; }
/* Phone panels */
.phone-panel {
display: none;
padding: var(--s3);
border-top: 1px solid var(--c-border);
}
.phone-panel.active { display: block; }
.phone-tab-bar { display: flex; }
}
/* ── RESPONSIVE: iPad ── */
@media (min-width: 769px) and (max-width: 1024px) {
.lt-body { grid-template-columns: 1fr 260px; }
.hide-mobile { display: none !important; }
}
</style>
</head>
<body>
<nav class="app-nav">
<a href="index.html" class="nav-logo">box<em>-</em>box</a>
<div class="nav-links">
<a href="command-center.html">Command Center</a>
<a href="live-timing.html" class="active">Live</a>
<a href="race-hub.html">Race Hub</a>
<a href="data-library.html">Data Library</a>
</div>
<div class="nav-right">
<div class="live-badge"><span class="live-dot"></span>RACE</div>
<div class="density-toggle">
<button class="active" onclick="setDensity('default',this)">D</button>
<button onclick="setDensity('compact',this)">C</button>
</div>
</div>
</nav>
<div class="lt-root">
<!-- ── Session Banner ── -->
<div class="lt-banner">
<div class="bi">
<span class="bi-label">Session</span>
<span class="bi-val">Monaco GP — Race</span>
</div>
<div class="bi">
<span class="bi-label">Lap</span>
<span class="bi-val">45 <span style="color:var(--c-text-3);font-size:11px">/ 78</span></span>
</div>
<div class="bi">
<span class="bi-label">Clock</span>
<span class="bi-val mono">1:02:34</span>
</div>
<div class="bi">
<span class="bi-label">Track</span>
<span class="bi-val track-green">● GREEN</span>
</div>
<div class="bi">
<span class="bi-label">DRS</span>
<span class="bi-val t-green">ENABLED</span>
</div>
<div class="bi">
<span class="bi-label">Air / Track</span>
<span class="bi-val t2">25°C / 38°C</span>
</div>
<div class="bi">
<span class="bi-label">Fastest Lap</span>
<span class="bi-val" style="color:var(--c-purple)">NOR 1:14.756</span>
</div>
</div>
<!-- ── Pinned drivers strip ── -->
<div class="lt-pinned">
<span class="lt-pinned-label">Pinned</span>
<div class="pin-card">
<span class="pin-pos">P1</span>
<div class="drv-cell" style="gap:5px">
<div class="drv-bar" style="background:var(--t-fer);height:14px"></div>
<span class="drv-code" style="font-size:12px;color:var(--t-fer)">LEC</span>
</div>
<span class="pin-gap">LEADER</span>
<span class="pin-last">1:15.234</span>
</div>
<div class="pin-card">
<span class="pin-pos">P3</span>
<div class="drv-cell" style="gap:5px">
<div class="drv-bar" style="background:var(--t-mcl);height:14px"></div>
<span class="drv-code" style="font-size:12px;color:var(--t-mcl)">NOR</span>
</div>
<span class="pin-gap">+7.2</span>
<span class="pin-last" style="color:var(--c-purple)">FL 1:14.756</span>
</div>
<div class="pin-card">
<span class="pin-pos">P8</span>
<div class="drv-cell" style="gap:5px">
<div class="drv-bar" style="background:var(--t-wil);height:14px"></div>
<span class="drv-code" style="font-size:12px;color:var(--t-wil)">SAI</span>
</div>
<span class="pin-gap t-yellow">PIT OUT</span>
<span class="pin-last">1:41.678</span>
</div>
<span style="margin-left:var(--s3);font-size:10px;color:var(--c-text-3)">Pin drivers with P in TUI or browser shortcut</span>
</div>
<!-- ── Main body ── -->
<div class="lt-body">
<!-- ── Timing Tower ── -->
<div class="lt-tower" id="panel-tower">
<div class="lt-tower-scroll">
<table class="tt-table">
<thead>
<tr>
<th class="left" style="width:28px;padding-left:var(--s4)">P</th>
<th class="left" style="min-width:110px">Driver</th>
<th>Gap</th>
<th>Int</th>
<th style="text-align:center;width:26px">Tyre</th>
<th>Age</th>
<th>Last</th>
<th>Best</th>
<th class="hide-mobile" style="text-align:left">S1 S2 S3</th>
</tr>
</thead>
<tbody>
<tr class="fl">
<td class="left tt-pos" style="padding-left:var(--s4)">1</td>
<td class="left"><div class="drv-cell"><div class="drv-bar" style="background:var(--t-fer)"></div><span class="drv-code">LEC</span><span class="drv-num">16</span></div></td>
<td class="tt-gap" style="color:var(--c-text-3);font-size:11px">LEADER</td>
<td class="tt-int"></td>
<td style="text-align:center"><span class="tyre M">M</span></td>
<td class="tt-age">12</td>
<td class="tt-last">1:15.234</td>
<td class="tt-best" style="color:var(--c-text-2)">1:14.892</td>
<td class="hide-mobile left"><div class="sectors"><div class="s-dot pb"></div><div class="s-dot pb"></div><div class="s-dot sl"></div></div></td>
</tr>
<tr>
<td class="left tt-pos" style="padding-left:var(--s4)">2</td>
<td class="left"><div class="drv-cell"><div class="drv-bar" style="background:var(--t-rb)"></div><span class="drv-code">VER</span><span class="drv-num">1</span></div></td>
<td class="tt-gap">+3.456</td>
<td class="tt-int">+3.456</td>
<td style="text-align:center"><span class="tyre H">H</span></td>
<td class="tt-age">8</td>
<td class="tt-last">1:15.623</td>
<td class="tt-best">1:15.023</td>
<td class="hide-mobile left"><div class="sectors"><div class="s-dot nor"></div><div class="s-dot pb"></div><div class="s-dot nor"></div></div></td>
</tr>
<tr class="fl">
<td class="left tt-pos" style="padding-left:var(--s4)">3</td>
<td class="left"><div class="drv-cell"><div class="drv-bar" style="background:var(--t-mcl)"></div><span class="drv-code">NOR</span><span class="drv-num">4</span></div></td>
<td class="tt-gap">+7.234</td>
<td class="tt-int">+3.778</td>
<td style="text-align:center"><span class="tyre H">H</span></td>
<td class="tt-age">28</td>
<td class="tt-last">1:15.012</td>
<td class="tt-best">1:14.756</td>
<td class="hide-mobile left"><div class="sectors"><div class="s-dot ob"></div><div class="s-dot ob"></div><div class="s-dot ob"></div></div></td>
</tr>
<tr>
<td class="left tt-pos" style="padding-left:var(--s4)">4</td>
<td class="left"><div class="drv-cell"><div class="drv-bar" style="background:var(--t-mcl)"></div><span class="drv-code">PIA</span><span class="drv-num">81</span></div></td>
<td class="tt-gap">+12.567</td>
<td class="tt-int">+5.333</td>
<td style="text-align:center"><span class="tyre H">H</span></td>
<td class="tt-age">22</td>
<td class="tt-last">1:15.890</td>
<td class="tt-best">1:15.234</td>
<td class="hide-mobile left"><div class="sectors"><div class="s-dot pb"></div><div class="s-dot nor"></div><div class="s-dot nor"></div></div></td>
</tr>
<tr>
<td class="left tt-pos" style="padding-left:var(--s4)">5</td>
<td class="left"><div class="drv-cell"><div class="drv-bar" style="background:var(--t-mer)"></div><span class="drv-code">RUS</span><span class="drv-num">63</span></div></td>
<td class="tt-gap">+18.234</td>
<td class="tt-int">+5.667</td>
<td style="text-align:center"><span class="tyre H">H</span></td>
<td class="tt-age">14</td>
<td class="tt-last">1:15.456</td>
<td class="tt-best">1:15.100</td>
<td class="hide-mobile left"><div class="sectors"><div class="s-dot nor"></div><div class="s-dot pb"></div><div class="s-dot pb"></div></div></td>
</tr>
<tr>
<td class="left tt-pos" style="padding-left:var(--s4)">6</td>
<td class="left"><div class="drv-cell"><div class="drv-bar" style="background:var(--t-fer)"></div><span class="drv-code">HAM</span><span class="drv-num">44</span></div></td>
<td class="tt-gap">+24.567</td>
<td class="tt-int">+6.333</td>
<td style="text-align:center"><span class="tyre M">M</span></td>
<td class="tt-age">8</td>
<td class="tt-last">1:16.012</td>
<td class="tt-best">1:15.567</td>
<td class="hide-mobile left"><div class="sectors"><div class="s-dot nor"></div><div class="s-dot nor"></div><div class="s-dot pb"></div></div></td>
</tr>
<tr>
<td class="left tt-pos" style="padding-left:var(--s4)">7</td>
<td class="left"><div class="drv-cell"><div class="drv-bar" style="background:var(--t-am)"></div><span class="drv-code">ALO</span><span class="drv-num">14</span></div></td>
<td class="tt-gap">+31.234</td>
<td class="tt-int">+6.667</td>
<td style="text-align:center"><span class="tyre H">H</span></td>
<td class="tt-age">43</td>
<td class="tt-last">1:16.234</td>
<td class="tt-best">1:15.890</td>
<td class="hide-mobile left"><div class="sectors"><div class="s-dot nor"></div><div class="s-dot nor"></div><div class="s-dot nor"></div></div></td>
</tr>
<tr class="pit">
<td class="left tt-pos" style="padding-left:var(--s4)">8</td>
<td class="left">
<div class="drv-cell">
<div class="drv-bar" style="background:var(--t-wil)"></div>
<span class="drv-code">SAI</span>
<span class="drv-num">55</span>
<span style="font-size:9px;color:var(--c-yellow);font-weight:700;margin-left:2px">PIT OUT</span>
</div>
</td>
<td class="tt-gap" style="color:var(--c-yellow)">+38.901</td>
<td class="tt-int">+7.667</td>
<td style="text-align:center"><span class="tyre S">S</span></td>
<td class="tt-age">2</td>
<td class="tt-last">1:41.678</td>
<td class="tt-best">1:15.456</td>
<td class="hide-mobile left"><div class="sectors"><div class="s-dot nor"></div><div class="s-dot nor"></div><div class="s-dot nor"></div></div></td>
</tr>
<tr>
<td class="left tt-pos" style="padding-left:var(--s4)">9</td>
<td class="left"><div class="drv-cell"><div class="drv-bar" style="background:var(--t-alp)"></div><span class="drv-code t3">GAS</span><span class="drv-num">10</span></div></td>
<td class="tt-gap t3">+45.234</td>
<td class="tt-int t3">+6.333</td>
<td style="text-align:center"><span class="tyre H">H</span></td>
<td class="tt-age t3">37</td>
<td class="tt-last t3">1:16.890</td>
<td class="tt-best t3">1:16.234</td>
<td class="hide-mobile left"><div class="sectors"><div class="s-dot nor"></div><div class="s-dot nor"></div><div class="s-dot nor"></div></div></td>
</tr>
<tr>
<td class="left tt-pos" style="padding-left:var(--s4)">10</td>
<td class="left"><div class="drv-cell"><div class="drv-bar" style="background:var(--t-haas)"></div><span class="drv-code t3">OCO</span><span class="drv-num">31</span></div></td>
<td class="tt-gap t3">+52.567</td>
<td class="tt-int t3">+7.333</td>
<td style="text-align:center"><span class="tyre H">H</span></td>
<td class="tt-age t3">23</td>
<td class="tt-last t3">1:17.012</td>
<td class="tt-best t3">1:16.567</td>
<td class="hide-mobile left"><div class="sectors"><div class="s-dot nor"></div><div class="s-dot nor"></div><div class="s-dot nor"></div></div></td>
</tr>
<tr style="opacity:0.45">
<td class="left" style="padding-left:var(--s4);font-family:var(--f-mono);font-size:12px;color:var(--c-text-3);text-align:center">11</td>
<td class="left"><div class="drv-cell"><div class="drv-bar" style="background:var(--t-vcarb)"></div><span class="drv-code t3">TSU</span><span class="drv-num">22</span></div></td>
<td class="tt-gap t3 mono">+1 LAP</td>
<td class="tt-int t3"></td>
<td style="text-align:center"><span class="tyre H">H</span></td>
<td class="tt-age t3">45</td>
<td class="tt-last t3">1:17.456</td>
<td class="tt-best t3">1:17.012</td>
<td class="hide-mobile left"><div class="sectors"><div class="s-dot nor"></div><div class="s-dot nor"></div><div class="s-dot nor"></div></div></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ── Right Sidebar ── -->
<div class="lt-sidebar">
<!-- Race Control -->
<div class="sb-panel" style="flex:1.8">
<div class="sb-head">
<span class="sb-title">Race Control</span>
<span class="dot live" style="margin-left:auto"></span>
</div>
<div class="sb-body">
<div class="rc-side-msg drs">
<span class="rc-side-lap">L27</span>
<span class="rc-side-text">DRS ENABLED — Lap 27</span>
</div>
<div class="rc-side-msg sc">
<span class="rc-side-lap">L26</span>
<span class="rc-side-text">SAFETY CAR IN THIS LAP</span>
</div>
<div class="rc-side-msg sc">
<span class="rc-side-lap">L23</span>
<span class="rc-side-text">SC DEPLOYED — ALB retirement T10</span>
</div>
<div class="rc-side-msg">
<span class="rc-side-lap">L35</span>
<span class="rc-side-text">5s PENALTY — RUS · Unsafe release</span>
</div>
<div class="rc-side-msg fl">
<span class="rc-side-lap">L38</span>
<span class="rc-side-text">FASTEST LAP — NOR 1:14.756</span>
</div>
<div class="rc-side-msg">
<span class="rc-side-lap">L3</span>
<span class="rc-side-text">DRS ENABLED — Lap 3</span>
</div>
<div class="rc-side-msg">
<span class="rc-side-lap">L1</span>
<span class="rc-side-text">RACE START — Track Clear</span>
</div>
</div>
</div>
<!-- Battles -->
<div class="sb-panel" style="flex:1">
<div class="sb-head">
<span class="sb-title">Battles</span>
</div>
<div class="sb-body">
<div class="battle-row">
<div class="battle-drivers">
<span class="drv-code" style="color:var(--t-mcl)">NOR</span>
<span class="t3" style="font-size:10px">vs</span>
<span class="drv-code" style="color:var(--t-mcl);opacity:.7">PIA</span>
</div>
<span class="battle-gap">+5.3s</span>
<span class="battle-trend t-green"></span>
</div>
<div class="battle-row">
<div class="battle-drivers">
<span class="drv-code" style="color:var(--t-fer)">HAM</span>
<span class="t3" style="font-size:10px">vs</span>
<span class="drv-code" style="color:var(--t-am)">ALO</span>
</div>
<span class="battle-gap">+6.3s</span>
<span class="battle-trend t3"></span>
</div>
<div class="battle-row">
<div class="battle-drivers">
<span class="drv-code" style="color:var(--t-wil)">SAI</span>
<span class="t3" style="font-size:10px">vs</span>
<span class="drv-code" style="color:var(--t-alp)">GAS</span>
</div>
<span class="battle-gap t-orange">+6.4s</span>
<span class="battle-trend t-red"></span>
</div>
</div>
</div>
<!-- Pit Window -->
<div class="sb-panel" style="flex:1.2">
<div class="sb-head">
<span class="sb-title">Pit Window</span>
<span class="t3" style="font-size:10px;margin-left:var(--s2)">L45</span>
</div>
<div class="sb-body">
<div class="pit-row">
<div class="drv-cell" style="gap:5px;flex:1">
<div class="drv-bar" style="background:var(--t-fer);height:14px"></div>
<span class="drv-code" style="font-size:12px">LEC</span>
</div>
<span class="tyre M" style="width:16px;height:16px;font-size:8px;margin:0 4px">M</span>
<span class="pit-tyre-age">L12</span>
<span class="pit-status soon">SOON</span>
</div>
<div class="pit-row">
<div class="drv-cell" style="gap:5px;flex:1">
<div class="drv-bar" style="background:var(--t-rb);height:14px"></div>
<span class="drv-code" style="font-size:12px">VER</span>
</div>
<span class="tyre H" style="width:16px;height:16px;font-size:8px;margin:0 4px">H</span>
<span class="pit-tyre-age">L8</span>
<span class="pit-status open">OPEN</span>
</div>
<div class="pit-row">
<div class="drv-cell" style="gap:5px;flex:1">
<div class="drv-bar" style="background:var(--t-mcl);height:14px"></div>
<span class="drv-code" style="font-size:12px">NOR</span>
</div>
<span class="tyre H" style="width:16px;height:16px;font-size:8px;margin:0 4px">H</span>
<span class="pit-tyre-age">L28</span>
<span class="pit-status overdue">OVERDUE</span>
</div>
<div class="pit-row">
<div class="drv-cell" style="gap:5px;flex:1">
<div class="drv-bar" style="background:var(--t-mer);height:14px"></div>
<span class="drv-code" style="font-size:12px">RUS</span>
</div>
<span class="tyre H" style="width:16px;height:16px;font-size:8px;margin:0 4px">H</span>
<span class="pit-tyre-age">L14</span>
<span class="pit-status open">OPEN</span>
</div>
<div class="pit-row">
<div class="drv-cell" style="gap:5px;flex:1">
<div class="drv-bar" style="background:var(--t-wil);height:14px"></div>
<span class="drv-code" style="font-size:12px">SAI</span>
</div>
<span class="tyre S" style="width:16px;height:16px;font-size:8px;margin:0 4px">S</span>
<span class="pit-tyre-age">L2</span>
<span class="pit-status done">DONE</span>
</div>
</div>
</div>
</div><!-- /lt-sidebar -->
</div><!-- /lt-body -->
<!-- ── Footer ── -->
<div class="lt-footer">
<div class="fl-item">
<span class="fl-label">Fastest Lap</span>
<div class="drv-cell" style="gap:5px">
<div class="drv-bar" style="background:var(--t-mcl);height:14px"></div>
<span style="font-weight:700;color:var(--c-purple)">NOR</span>
</div>
<span class="mono" style="color:var(--c-purple)">1:14.756</span>
<span class="t3 mono" style="font-size:11px">L38</span>
</div>
<div style="margin-left:auto;display:flex;align-items:center;gap:var(--s4)">
<div class="fl-item">
<span class="fl-label">S1</span>
<span class="mono t-purple">NOR 19.234</span>
</div>
<div class="fl-item">
<span class="fl-label">S2</span>
<span class="mono t-purple">LEC 32.456</span>
</div>
<div class="fl-item">
<span class="fl-label">S3</span>
<span class="mono t-purple">NOR 23.066</span>
</div>
<div class="fl-item" style="margin-left:var(--s3)">
<span class="fl-label">Lead Change</span>
<span class="t2 mono">L2 LEC overtook VER</span>
</div>
</div>
</div>
<!-- ── Phone panels (desktop: hidden via CSS above) ── -->
<div class="phone-panel active" id="phone-panel-tower" style="padding:0;display:none"></div>
<div class="phone-panel" id="phone-panel-rc" style="display:none">
<div style="font-size:10px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--c-text-3);margin-bottom:var(--s3)">Race Control</div>
<div class="rc-side-msg drs"><span class="rc-side-lap">L27</span><span class="rc-side-text">DRS ENABLED — Lap 27</span></div>
<div class="rc-side-msg sc"><span class="rc-side-lap">L26</span><span class="rc-side-text">SAFETY CAR IN THIS LAP</span></div>
<div class="rc-side-msg sc"><span class="rc-side-lap">L23</span><span class="rc-side-text">SC DEPLOYED — ALB retirement T10</span></div>
<div class="rc-side-msg"><span class="rc-side-lap">L35</span><span class="rc-side-text">5s PENALTY — RUS · Unsafe release</span></div>
<div class="rc-side-msg fl"><span class="rc-side-lap">L38</span><span class="rc-side-text">FASTEST LAP — NOR 1:14.756</span></div>
</div>
<div class="phone-panel" id="phone-panel-battles" style="display:none">
<div style="font-size:10px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--c-text-3);margin-bottom:var(--s3)">Battles</div>
<div class="battle-row"><div class="battle-drivers"><span class="drv-code" style="color:var(--t-mcl)">NOR</span><span class="t3" style="font-size:10px">vs</span><span class="drv-code" style="color:var(--t-mcl);opacity:.7">PIA</span></div><span class="battle-gap">+5.3s</span><span class="battle-trend t-green">▼ closing</span></div>
<div class="battle-row"><div class="battle-drivers"><span class="drv-code" style="color:var(--t-fer)">HAM</span><span class="t3" style="font-size:10px">vs</span><span class="drv-code" style="color:var(--t-am)">ALO</span></div><span class="battle-gap">+6.3s</span><span class="battle-trend t3">— stable</span></div>
<div class="battle-row"><div class="battle-drivers"><span class="drv-code" style="color:var(--t-wil)">SAI</span><span class="t3" style="font-size:10px">vs</span><span class="drv-code" style="color:var(--t-alp)">GAS</span></div><span class="battle-gap t-orange">+6.4s</span><span class="battle-trend t-red">▲ SAI catching</span></div>
<div style="margin-top:var(--s4);font-size:10px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--c-text-3);margin-bottom:var(--s3)">Pit Window</div>
<div class="pit-row"><div class="drv-cell" style="gap:5px;flex:1"><div class="drv-bar" style="background:var(--t-fer);height:14px"></div><span class="drv-code" style="font-size:12px">LEC</span></div><span class="tyre M" style="width:16px;height:16px;font-size:8px;margin:0 4px">M</span><span class="pit-tyre-age">L12</span><span class="pit-status soon">SOON</span></div>
<div class="pit-row"><div class="drv-cell" style="gap:5px;flex:1"><div class="drv-bar" style="background:var(--t-mcl);height:14px"></div><span class="drv-code" style="font-size:12px">NOR</span></div><span class="tyre H" style="width:16px;height:16px;font-size:8px;margin:0 4px">H</span><span class="pit-tyre-age">L28</span><span class="pit-status overdue">OVERDUE</span></div>
</div>
<!-- ── Phone bottom tabs ── -->
<nav class="phone-tab-bar">
<button class="active" onclick="phoneTab('tower',this)">
<span class="tab-icon"></span>Tower
</button>
<button onclick="phoneTab('rc',this)">
<span class="tab-icon">📡</span>RC
</button>
<button onclick="phoneTab('battles',this)">
<span class="tab-icon"></span>Battles
</button>
</nav>
</div><!-- /lt-root -->
<script>
function setDensity(mode, btn) {
document.querySelectorAll('.density-toggle button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.documentElement.classList.toggle('compact', mode === 'compact');
}
function phoneTab(panel, btn) {
if (window.innerWidth > 768) return;
document.querySelectorAll('.phone-tab-bar button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Show/hide timing tower
const tower = document.getElementById('panel-tower');
tower.style.display = panel === 'tower' ? '' : 'none';
// Show/hide phone panels
document.querySelectorAll('.phone-panel').forEach(p => p.style.display = 'none');
if (panel !== 'tower') {
const phonePanel = document.getElementById('phone-panel-' + panel);
if (phonePanel) phonePanel.style.display = 'block';
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,939 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>box-box · Live Timing — Phone Layout</title>
<link rel="stylesheet" href="styles.css">
<style>
/* ─── Phone Live Layout ───────────────────────────────────────────────── */
/* This file demonstrates the phone layout model in isolation.
The real responsive live-timing.html collapses to this at <768px.
Reading this file answers: "What exactly does the phone experience look like?" */
body {
margin: 0;
background: var(--c-bg);
color: var(--c-text);
font-family: var(--font-mono);
font-size: 13px;
}
/* Simulate phone chrome — constrain to 390px centered */
.phone-frame {
max-width: 390px;
margin: 0 auto;
min-height: 100vh;
position: relative;
display: flex;
flex-direction: column;
background: var(--c-bg);
border-left: 1px solid var(--c-border-2);
border-right: 1px solid var(--c-border-2);
}
/* Demo note outside phone frame */
.demo-meta {
text-align: center;
padding: 12px 16px;
font-family: var(--font-sans);
font-size: 11px;
color: var(--c-text-3);
letter-spacing: 0.04em;
border-bottom: 1px solid var(--c-border);
}
.demo-meta a { color: var(--c-text-2); text-decoration: none; }
.demo-meta a:hover { color: var(--c-text); }
/* ─── Session Banner (phone variant) ─────────────────────────────────── */
/* Compact — session name + lap + track status only.
Other fields (temps, DRS, fastest lap) are not shown on phone. */
.sb {
background: var(--c-surface);
border-bottom: 1px solid var(--c-border);
padding: 0 12px;
height: 46px;
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
position: sticky;
top: 0;
z-index: 40;
}
.sb-live-dot {
width: 7px; height: 7px;
border-radius: 50%;
background: var(--c-red);
flex-shrink: 0;
animation: pulse 1.4s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
.sb-session {
font-family: var(--font-sans);
font-size: 13px;
font-weight: 600;
color: var(--c-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.sb-lap {
font-size: 12px;
color: var(--c-text-2);
white-space: nowrap;
flex-shrink: 0;
}
.sb-lap span { color: var(--c-text); font-weight: 600; }
.sb-track-status {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
padding: 2px 6px;
border-radius: 2px;
flex-shrink: 0;
}
.sb-track-status.green { background: rgba(0,204,106,0.15); color: var(--c-green); }
.sb-track-status.yellow { background: rgba(255,214,0,0.15); color: var(--c-yellow); }
.sb-track-status.sc { background: rgba(255,214,0,0.2); color: var(--c-yellow); }
.sb-track-status.red { background: rgba(225,6,0,0.15); color: var(--c-red); }
.sb-disconnected {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
padding: 2px 6px;
border-radius: 2px;
background: rgba(225,6,0,0.2);
color: var(--c-red);
animation: blink 1s step-end infinite;
flex-shrink: 0;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
/* ─── Panel Container ─────────────────────────────────────────────────── */
/* Each panel occupies the full space between banner and tab bar.
Only one panel is visible at a time. */
.panel-area {
flex: 1;
overflow: hidden;
position: relative;
/* bottom padding = tab bar height so content is not obscured */
padding-bottom: 54px;
}
.phone-panel {
display: none;
height: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.phone-panel.active { display: block; }
/* ─── PANEL: Tower ────────────────────────────────────────────────────── */
/* Phone columns: P · Driver · Gap · Tyre · Last
Hidden: Interval, Age, Best, Sectors */
.tower-table {
width: 100%;
border-collapse: collapse;
}
.tower-table thead th {
position: sticky;
top: 0;
background: var(--c-bg);
border-bottom: 1px solid var(--c-border);
padding: 5px 8px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.06em;
color: var(--c-text-3);
text-align: right;
white-space: nowrap;
z-index: 10;
}
.tower-table thead th.col-driver { text-align: left; }
.tower-table tbody tr {
border-bottom: 1px solid var(--c-border);
height: 38px;
}
.tower-table tbody tr:hover { background: var(--c-surface); }
.tower-table tbody tr.pinned { background: rgba(255,255,255,0.03); }
.tower-table td {
padding: 0 8px;
text-align: right;
vertical-align: middle;
white-space: nowrap;
}
.td-pos {
width: 26px;
font-size: 13px;
font-weight: 700;
color: var(--c-text);
text-align: center;
}
.td-driver {
text-align: left;
min-width: 0;
}
.driver-cell {
display: flex;
align-items: center;
gap: 7px;
}
.team-bar {
width: 3px;
height: 22px;
border-radius: 1px;
flex-shrink: 0;
}
.driver-code {
font-weight: 700;
font-size: 13px;
color: var(--c-text);
letter-spacing: 0.02em;
}
.td-gap { font-size: 12px; color: var(--c-text-2); min-width: 58px; }
.td-gap.leader { color: var(--c-text); font-weight: 600; font-size: 10px; letter-spacing: 0.05em; }
.td-tyre {
width: 32px;
}
.tyre-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px; height: 22px;
border-radius: 50%;
font-size: 11px;
font-weight: 700;
}
.tyre-badge.S { background: rgba(232,0,45,0.18); color: var(--tyre-s); }
.tyre-badge.M { background: rgba(200,180,0,0.18); color: var(--tyre-m); }
.tyre-badge.H { background: rgba(184,184,184,0.15); color: var(--tyre-h); }
.td-last { font-size: 12px; color: var(--c-text); min-width: 58px; }
.td-last.pb { color: var(--c-green); }
.td-last.ob { color: var(--c-purple); }
/* Special states */
.row-pit td { opacity: 0.55; }
.row-pit .driver-code::after {
content: " PIT";
font-size: 9px;
font-weight: 600;
color: var(--c-yellow);
letter-spacing: 0.08em;
vertical-align: middle;
margin-left: 2px;
}
.row-fl td.td-last { color: var(--c-purple); }
.row-dnf td { opacity: 0.35; text-decoration: line-through; text-decoration-color: var(--c-border-2); }
/* ─── PANEL: Race Control ─────────────────────────────────────────────── */
.rc-panel { padding: 0; }
.rc-panel-header {
padding: 10px 12px 8px;
font-family: var(--font-sans);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.1em;
color: var(--c-text-3);
text-transform: uppercase;
border-bottom: 1px solid var(--c-border);
position: sticky;
top: 0;
background: var(--c-bg);
z-index: 10;
}
.rc-list { list-style: none; margin: 0; padding: 0; }
.rc-item {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 9px 12px;
border-bottom: 1px solid var(--c-border);
font-size: 12px;
line-height: 1.45;
}
.rc-item:last-child { border-bottom: none; }
.rc-lap {
font-size: 10px;
color: var(--c-text-3);
white-space: nowrap;
padding-top: 1px;
flex-shrink: 0;
min-width: 32px;
}
.rc-dot {
width: 8px; height: 8px;
border-radius: 50%;
margin-top: 3px;
flex-shrink: 0;
}
.rc-text { color: var(--c-text-2); flex: 1; }
.rc-text strong { color: var(--c-text); }
.rc-item.sc .rc-dot { background: var(--c-yellow); }
.rc-item.sc .rc-text { color: var(--c-yellow); }
.rc-item.vsc .rc-dot { background: var(--c-yellow); }
.rc-item.vsc .rc-text { color: var(--c-yellow); }
.rc-item.drs .rc-dot { background: var(--c-green); }
.rc-item.penalty .rc-dot { background: var(--c-red); }
.rc-item.fl .rc-dot { background: var(--c-purple); }
.rc-item.fl .rc-text { color: var(--c-purple); }
.rc-item.flag .rc-dot { background: var(--c-red); }
.rc-item.flag .rc-text { color: var(--c-red); }
.rc-item.info .rc-dot { background: var(--c-text-3); }
/* ─── PANEL: Battles + Pit ─────────────────────────────────────────────── */
.battles-section {
padding: 10px 12px 0;
}
.section-label {
font-family: var(--font-sans);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.1em;
color: var(--c-text-3);
text-transform: uppercase;
margin-bottom: 8px;
}
.battle-row {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 0;
border-bottom: 1px solid var(--c-border);
font-size: 12px;
}
.battle-row:last-child { border-bottom: none; }
.battle-driver {
display: flex;
align-items: center;
gap: 5px;
width: 52px;
}
.bd-bar {
width: 3px; height: 18px;
border-radius: 1px;
flex-shrink: 0;
}
.bd-code {
font-weight: 700;
font-size: 12px;
color: var(--c-text);
letter-spacing: 0.02em;
}
.battle-vs { color: var(--c-text-3); font-size: 10px; }
.battle-gap { color: var(--c-text); font-weight: 600; font-size: 13px; min-width: 50px; text-align: right; }
.battle-trend { margin-left: auto; font-size: 12px; }
.battle-trend.closing { color: var(--c-red); }
.battle-trend.stable { color: var(--c-text-3); }
.battle-trend.opening { color: var(--c-text-3); }
.pit-section {
padding: 10px 12px 0;
margin-top: 4px;
border-top: 1px solid var(--c-border);
}
.pit-row {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 0;
border-bottom: 1px solid var(--c-border);
font-size: 12px;
}
.pit-row:last-child { border-bottom: none; }
.pit-driver {
display: flex;
align-items: center;
gap: 5px;
flex: 1;
}
.pit-tyre-age { color: var(--c-text-3); font-size: 11px; margin-left: 2px; }
.pit-status-badge {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.07em;
padding: 2px 6px;
border-radius: 2px;
flex-shrink: 0;
}
.pit-status-badge.open { background: rgba(0,204,106,0.12); color: var(--c-green); }
.pit-status-badge.soon { background: rgba(255,214,0,0.12); color: var(--c-yellow); }
.pit-status-badge.overdue { background: rgba(255,140,0,0.12); color: #ff8c00; }
.pit-status-badge.done { background: rgba(255,255,255,0.04); color: var(--c-text-3); }
/* ─── Bottom Tab Bar ─────────────────────────────────────────────────── */
.tab-bar {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 390px;
height: 54px;
background: var(--c-surface);
border-top: 1px solid var(--c-border-2);
display: flex;
align-items: stretch;
z-index: 50;
}
.tab-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
background: none;
border: none;
cursor: pointer;
padding: 0;
color: var(--c-text-3);
font-family: var(--font-sans);
font-size: 10px;
font-weight: 500;
letter-spacing: 0.04em;
transition: color 0.1s;
-webkit-tap-highlight-color: transparent;
}
.tab-btn .tab-icon {
font-size: 16px;
line-height: 1;
}
.tab-btn.active {
color: var(--c-text);
}
.tab-btn.active .tab-icon { color: var(--c-red); }
.tab-btn:hover { color: var(--c-text-2); }
/* RC badge — unread count */
.tab-badge {
position: relative;
}
.tab-badge::after {
content: "3";
position: absolute;
top: -4px;
right: -8px;
background: var(--c-red);
color: white;
font-size: 9px;
font-weight: 700;
width: 14px;
height: 14px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
/* ─── Disconnected banner (full-panel overlay) ────────────────────────── */
/* This is what happens when SSE drops. Not just the status in the banner —
a real-estate banner below the session bar. */
.disconnect-banner {
display: none; /* toggle .show to show */
background: rgba(225,6,0,0.12);
border-bottom: 1px solid rgba(225,6,0,0.3);
padding: 8px 12px;
font-family: var(--font-sans);
font-size: 12px;
color: var(--c-red);
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.disconnect-banner.show { display: flex; }
.disconnect-banner svg { flex-shrink: 0; }
.reconnect-btn {
margin-left: auto;
background: rgba(225,6,0,0.15);
border: 1px solid rgba(225,6,0,0.35);
color: var(--c-red);
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: 3px;
cursor: pointer;
font-family: var(--font-sans);
white-space: nowrap;
}
</style>
</head>
<body>
<div class="demo-meta">
Mobile Live Layout Demo · 390px phone viewport ·
<a href="live-timing.html">← full responsive version</a> ·
<a href="index.html">index</a>
</div>
<!-- ─── Phone Frame ──────────────────────────────────────────────────────── -->
<div class="phone-frame">
<!-- Session Banner (phone variant: session name + lap + track status) -->
<div class="sb">
<div class="sb-live-dot"></div>
<div class="sb-session">Monaco GP — Race</div>
<div class="sb-lap">Lap <span>47</span>/78</div>
<div class="sb-track-status sc">SC</div>
</div>
<!-- Disconnected state banner (hidden by default — uncomment class to preview) -->
<div class="disconnect-banner">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M7 1L13 12H1L7 1Z" stroke="currentColor" stroke-width="1.5" fill="none"/>
<path d="M7 5.5V8.5M7 10V10.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
SSE disconnected — data may be stale
<button class="reconnect-btn">Reconnect</button>
</div>
<!-- Panel area — only one .phone-panel.active at a time -->
<div class="panel-area">
<!-- PANEL: Timing Tower (default active) -->
<div id="panel-tower" class="phone-panel active">
<table class="tower-table">
<thead>
<tr>
<th style="width:26px; text-align:center;">P</th>
<th class="col-driver" style="min-width:90px;">Driver</th>
<th style="min-width:58px;">Gap</th>
<th style="width:36px;">Tyre</th>
<th style="min-width:58px;">Last</th>
</tr>
</thead>
<tbody>
<!-- P1 - Leclerc -->
<tr class="row-fl">
<td class="td-pos">1</td>
<td class="td-driver">
<div class="driver-cell">
<div class="team-bar" style="background: var(--t-fer);"></div>
<span class="driver-code">LEC</span>
</div>
</td>
<td class="td-gap leader">LEADER</td>
<td class="td-tyre"><span class="tyre-badge M">M</span></td>
<td class="td-last ob">1:14.812</td>
</tr>
<!-- P2 - Verstappen -->
<tr>
<td class="td-pos">2</td>
<td class="td-driver">
<div class="driver-cell">
<div class="team-bar" style="background: var(--t-rb);"></div>
<span class="driver-code">VER</span>
</div>
</td>
<td class="td-gap">+3.456</td>
<td class="td-tyre"><span class="tyre-badge M">M</span></td>
<td class="td-last">1:15.203</td>
</tr>
<!-- P3 - Norris (in pits during SC) -->
<tr class="row-pit">
<td class="td-pos">3</td>
<td class="td-driver">
<div class="driver-cell">
<div class="team-bar" style="background: var(--t-mcl);"></div>
<span class="driver-code">NOR</span>
</div>
</td>
<td class="td-gap">+8.102</td>
<td class="td-tyre"><span class="tyre-badge S">S</span></td>
<td class="td-last">1:16.044</td>
</tr>
<!-- P4 - Hamilton -->
<tr>
<td class="td-pos">4</td>
<td class="td-driver">
<div class="driver-cell">
<div class="team-bar" style="background: var(--t-fer);"></div>
<span class="driver-code">HAM</span>
</div>
</td>
<td class="td-gap">+11.723</td>
<td class="td-tyre"><span class="tyre-badge H">H</span></td>
<td class="td-last pb">1:15.991</td>
</tr>
<!-- P5 - Russell -->
<tr>
<td class="td-pos">5</td>
<td class="td-driver">
<div class="driver-cell">
<div class="team-bar" style="background: var(--t-mer);"></div>
<span class="driver-code">RUS</span>
</div>
</td>
<td class="td-gap">+14.088</td>
<td class="td-tyre"><span class="tyre-badge M">M</span></td>
<td class="td-last">1:16.388</td>
</tr>
<!-- P6 - Piastri -->
<tr>
<td class="td-pos">6</td>
<td class="td-driver">
<div class="driver-cell">
<div class="team-bar" style="background: var(--t-mcl);"></div>
<span class="driver-code">PIA</span>
</div>
</td>
<td class="td-gap">+18.430</td>
<td class="td-tyre"><span class="tyre-badge S">S</span></td>
<td class="td-last">1:16.701</td>
</tr>
<!-- P7 - Sainz -->
<tr>
<td class="td-pos">7</td>
<td class="td-driver">
<div class="driver-cell">
<div class="team-bar" style="background: rgba(255,255,255,0.25);"></div>
<span class="driver-code">SAI</span>
</div>
</td>
<td class="td-gap">+22.115</td>
<td class="td-tyre"><span class="tyre-badge H">H</span></td>
<td class="td-last">1:16.924</td>
</tr>
<!-- P8 - Antonelli -->
<tr>
<td class="td-pos">8</td>
<td class="td-driver">
<div class="driver-cell">
<div class="team-bar" style="background: var(--t-mer);"></div>
<span class="driver-code">ANT</span>
</div>
</td>
<td class="td-gap">+27.660</td>
<td class="td-tyre"><span class="tyre-badge M">M</span></td>
<td class="td-last">1:17.204</td>
</tr>
<!-- P9 - Alonso -->
<tr>
<td class="td-pos">9</td>
<td class="td-driver">
<div class="driver-cell">
<div class="team-bar" style="background: rgba(0,143,93,0.9);"></div>
<span class="driver-code">ALO</span>
</div>
</td>
<td class="td-gap">+35.291</td>
<td class="td-tyre"><span class="tyre-badge H">H</span></td>
<td class="td-last">1:17.450</td>
</tr>
<!-- P10 - Gasly -->
<tr>
<td class="td-pos">10</td>
<td class="td-driver">
<div class="driver-cell">
<div class="team-bar" style="background: rgba(0,144,209,0.85);"></div>
<span class="driver-code">GAS</span>
</div>
</td>
<td class="td-gap">+42.880</td>
<td class="td-tyre"><span class="tyre-badge M">M</span></td>
<td class="td-last">1:17.692</td>
</tr>
<!-- P11-P15 (midfield) -->
<tr>
<td class="td-pos">11</td>
<td class="td-driver"><div class="driver-cell"><div class="team-bar" style="background: rgba(100,192,84,0.85);"></div><span class="driver-code">OCO</span></div></td>
<td class="td-gap">+51.334</td>
<td class="td-tyre"><span class="tyre-badge H">H</span></td>
<td class="td-last">1:18.103</td>
</tr>
<tr>
<td class="td-pos">12</td>
<td class="td-driver"><div class="driver-cell"><div class="team-bar" style="background: rgba(100,192,84,0.85);"></div><span class="driver-code">HUL</span></div></td>
<td class="td-gap">+55.771</td>
<td class="td-tyre"><span class="tyre-badge S">S</span></td>
<td class="td-last">1:18.320</td>
</tr>
<tr>
<td class="td-pos">13</td>
<td class="td-driver"><div class="driver-cell"><div class="team-bar" style="background: rgba(90,90,100,0.7);"></div><span class="driver-code">STR</span></div></td>
<td class="td-gap">+1:04.2</td>
<td class="td-tyre"><span class="tyre-badge M">M</span></td>
<td class="td-last">1:18.890</td>
</tr>
<tr>
<td class="td-pos">14</td>
<td class="td-driver"><div class="driver-cell"><div class="team-bar" style="background: rgba(200,175,60,0.75);"></div><span class="driver-code">TSU</span></div></td>
<td class="td-gap">+1 LAP</td>
<td class="td-tyre"><span class="tyre-badge H">H</span></td>
<td class="td-last">1:19.210</td>
</tr>
<!-- P15 — DNF -->
<tr class="row-dnf">
<td class="td-pos" style="color: var(--c-text-3);">15</td>
<td class="td-driver"><div class="driver-cell"><div class="team-bar" style="background: rgba(255,255,255,0.12);"></div><span class="driver-code">ZHO</span></div></td>
<td class="td-gap" style="color: var(--c-text-3);">DNF</td>
<td class="td-tyre"><span class="tyre-badge S" style="opacity:0.4;">S</span></td>
<td class="td-last" style="color: var(--c-text-3);"></td>
</tr>
</tbody>
</table>
</div>
<!-- PANEL: Race Control -->
<div id="panel-rc" class="phone-panel">
<div class="rc-panel-header">Race Control</div>
<ul class="rc-list">
<li class="rc-item sc">
<span class="rc-lap">L47</span>
<div class="rc-dot"></div>
<span class="rc-text">SAFETY CAR DEPLOYED — Incident at Turn 10 (Bottas, Albon)</span>
</li>
<li class="rc-item sc">
<span class="rc-lap">L47</span>
<div class="rc-dot"></div>
<span class="rc-text">PIT LANE OPEN during Safety Car period</span>
</li>
<li class="rc-item penalty">
<span class="rc-lap">L44</span>
<div class="rc-dot"></div>
<span class="rc-text"><strong>VER</strong> — 5 SECOND PENALTY · Causing a collision with NOR at T1</span>
</li>
<li class="rc-item drs">
<span class="rc-lap">L42</span>
<div class="rc-dot"></div>
<span class="rc-text">DRS ENABLED — Zones 1, 2 and 3</span>
</li>
<li class="rc-item fl">
<span class="rc-lap">L41</span>
<div class="rc-dot"></div>
<span class="rc-text"><strong>LEC</strong> — FASTEST LAP · 1:12.456 on Lap 41</span>
</li>
<li class="rc-item flag">
<span class="rc-lap">L38</span>
<div class="rc-dot"></div>
<span class="rc-text">RED FLAG — Track debris at Casino corner, extraction in progress</span>
</li>
<li class="rc-item info">
<span class="rc-lap">L38</span>
<div class="rc-dot"></div>
<span class="rc-text">RACE SUSPENDED · All cars to proceed to pit lane</span>
</li>
<li class="rc-item info">
<span class="rc-lap">L36</span>
<div class="rc-dot"></div>
<span class="rc-text">Stewards investigating incident between ALO and STR — Turn 6</span>
</li>
<li class="rc-item vsc">
<span class="rc-lap">L31</span>
<div class="rc-dot"></div>
<span class="rc-text">VIRTUAL SAFETY CAR DEPLOYED — Bottas car recovered to pit lane</span>
</li>
<li class="rc-item vsc">
<span class="rc-lap">L33</span>
<div class="rc-dot"></div>
<span class="rc-text">VIRTUAL SAFETY CAR ENDING — Racing to resume next lap</span>
</li>
<li class="rc-item drs">
<span class="rc-lap">L34</span>
<div class="rc-dot"></div>
<span class="rc-text">DRS ENABLED — Zones 1, 2 and 3</span>
</li>
<li class="rc-item info">
<span class="rc-lap">L28</span>
<div class="rc-dot"></div>
<span class="rc-text">Weather: Track Temp 44°C / Air Temp 28°C / Humidity 62%</span>
</li>
</ul>
</div>
<!-- PANEL: Battles + Pit Window -->
<div id="panel-battles" class="phone-panel">
<div class="battles-section">
<div class="section-label">On-Track Battles</div>
<div class="battle-row">
<div class="battle-driver">
<div class="bd-bar" style="background: var(--t-fer);"></div>
<span class="bd-code">LEC</span>
</div>
<span class="battle-vs">vs</span>
<div class="battle-driver">
<div class="bd-bar" style="background: var(--t-rb);"></div>
<span class="bd-code">VER</span>
</div>
<span class="battle-gap">3.456s</span>
<span class="battle-trend closing">↓ closing</span>
</div>
<div class="battle-row">
<div class="battle-driver">
<div class="bd-bar" style="background: var(--t-fer);"></div>
<span class="bd-code">HAM</span>
</div>
<span class="battle-vs">vs</span>
<div class="battle-driver">
<div class="bd-bar" style="background: var(--t-mer);"></div>
<span class="bd-code">RUS</span>
</div>
<span class="battle-gap">2.365s</span>
<span class="battle-trend stable">— stable</span>
</div>
<div class="battle-row">
<div class="battle-driver">
<div class="bd-bar" style="background: var(--t-mcl);"></div>
<span class="bd-code">PIA</span>
</div>
<span class="battle-vs">vs</span>
<div class="battle-driver">
<div class="bd-bar" style="background: rgba(255,255,255,0.25);"></div>
<span class="bd-code">SAI</span>
</div>
<span class="battle-gap">4.315s</span>
<span class="battle-trend closing">↓ closing</span>
</div>
<div class="battle-row">
<div class="battle-driver">
<div class="bd-bar" style="background: rgba(0,143,93,0.9);"></div>
<span class="bd-code">ALO</span>
</div>
<span class="battle-vs">vs</span>
<div class="battle-driver">
<div class="bd-bar" style="background: rgba(0,144,209,0.85);"></div>
<span class="bd-code">GAS</span>
</div>
<span class="battle-gap">6.411s</span>
<span class="battle-trend opening">↑ opening</span>
</div>
</div>
<!-- Pit Window below battles in same panel -->
<div class="pit-section">
<div class="section-label">Pit Window</div>
<div class="pit-row">
<div class="pit-driver">
<div class="bd-bar" style="background: var(--t-rb);"></div>
<span class="bd-code">VER</span>
<span class="tyre-badge M" style="width:18px;height:18px;font-size:10px;">M</span>
<span class="pit-tyre-age">+24</span>
</div>
<span class="pit-status-badge overdue">OVERDUE</span>
</div>
<div class="pit-row">
<div class="pit-driver">
<div class="bd-bar" style="background: var(--t-fer);"></div>
<span class="bd-code">HAM</span>
<span class="tyre-badge H" style="width:18px;height:18px;font-size:10px;">H</span>
<span class="pit-tyre-age">+31</span>
</div>
<span class="pit-status-badge soon">SOON</span>
</div>
<div class="pit-row">
<div class="pit-driver">
<div class="bd-bar" style="background: var(--t-mer);"></div>
<span class="bd-code">RUS</span>
<span class="tyre-badge M" style="width:18px;height:18px;font-size:10px;">M</span>
<span class="pit-tyre-age">+19</span>
</div>
<span class="pit-status-badge open">OPEN</span>
</div>
<div class="pit-row">
<div class="pit-driver">
<div class="bd-bar" style="background: rgba(0,143,93,0.9);"></div>
<span class="bd-code">ALO</span>
<span class="tyre-badge H" style="width:18px;height:18px;font-size:10px;">H</span>
<span class="pit-tyre-age">+38</span>
</div>
<span class="pit-status-badge overdue">OVERDUE</span>
</div>
<div class="pit-row">
<div class="pit-driver">
<div class="bd-bar" style="background: var(--t-fer);"></div>
<span class="bd-code">LEC</span>
<span class="tyre-badge M" style="width:18px;height:18px;font-size:10px;">M</span>
<span class="pit-tyre-age">+12</span>
</div>
<span class="pit-status-badge done">DONE</span>
</div>
<div class="pit-row">
<div class="pit-driver">
<div class="bd-bar" style="background: var(--t-mcl);"></div>
<span class="bd-code">NOR</span>
<span class="tyre-badge S" style="width:18px;height:18px;font-size:10px;">S</span>
<span class="pit-tyre-age">+1</span>
</div>
<span class="pit-status-badge done">DONE</span>
</div>
</div>
</div>
</div><!-- /panel-area -->
<!-- Bottom Tab Bar (always visible, fixed) -->
<div class="tab-bar">
<button class="tab-btn active" id="tab-tower" onclick="switchTab('tower', this)">
<span class="tab-icon"></span>
Tower
</button>
<button class="tab-btn" id="tab-rc" onclick="switchTab('rc', this)">
<span class="tab-icon tab-badge">📻</span>
Race Ctrl
</button>
<button class="tab-btn" id="tab-battles" onclick="switchTab('battles', this)">
<span class="tab-icon"></span>
Battles
</button>
</div>
</div><!-- /phone-frame -->
<script>
function switchTab(panel, btn) {
// Update tab button states
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Show/hide panels
document.querySelectorAll('.phone-panel').forEach(p => p.classList.remove('active'));
document.getElementById('panel-' + panel).classList.add('active');
// Clear RC badge on visit
if (panel === 'rc') {
const badge = document.querySelector('.tab-badge');
if (badge) badge.style.setProperty('--badge-content', '""');
// In React: mark messages as read via callback
}
}
// Demo: toggle the disconnect state by pressing 'd'
document.addEventListener('keydown', e => {
if (e.key === 'd') {
const banner = document.querySelector('.disconnect-banner');
banner.classList.toggle('show');
}
if (e.key === 's') {
// Cycle track status for demo
const s = document.querySelector('.sb-track-status');
const states = ['green', 'sc', 'yellow', 'red'];
const labels = { green: 'GREEN', sc: 'SC', yellow: 'VSC', red: 'RED' };
const cur = states.findIndex(x => s.classList.contains(x));
const next = states[(cur + 1) % states.length];
states.forEach(st => s.classList.remove(st));
s.classList.add(next);
s.textContent = labels[next];
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,847 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>box-box — Race Hub · Monaco GP 2025</title>
<link rel="stylesheet" href="styles.css">
<style>
/* ── Race Hub sub-header ── */
.rh-header {
display: flex;
align-items: center;
gap: var(--s4);
padding: var(--s3) var(--s5);
background: var(--c-surface);
border-bottom: 1px solid var(--c-border);
flex-wrap: wrap;
flex-shrink: 0;
}
.rh-breadcrumb {
display: flex;
align-items: center;
gap: var(--s2);
font-size: 12px;
}
.rh-breadcrumb a { color: var(--c-text-3); }
.rh-breadcrumb a:hover { color: var(--c-text-2); }
.rh-breadcrumb .sep { color: var(--c-text-3); }
.rh-breadcrumb .cur { font-weight: 600; color: var(--c-text); }
.rh-session-btns {
display: flex;
gap: 2px;
margin-left: auto;
}
.rh-session-btn {
padding: 3px 8px;
background: none;
border: 1px solid var(--c-border);
border-radius: 2px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.05em;
color: var(--c-text-3);
text-transform: uppercase;
}
.rh-session-btn.active {
background: var(--c-surface-2);
border-color: var(--c-border-2);
color: var(--c-text);
}
/* ── Section tabs (in rh-main) ── */
.rh-section-tabs {
display: flex;
gap: 2px;
padding: var(--s3) var(--s5);
border-bottom: 1px solid var(--c-border);
overflow-x: auto;
flex-shrink: 0;
}
/* ── Main content area ── */
.rh-content {
padding: var(--s5);
display: flex;
flex-direction: column;
gap: var(--sec-gap);
max-width: 860px;
}
/* ── Classification ── */
.result-pos { font-family: var(--f-mono); font-size: 14px; font-weight: 700; text-align: center; width: 28px; }
/* ── Strategy ── */
#strategy-chart { width: 100%; min-height: 320px; }
.compound-legend {
display: flex;
gap: var(--s4);
margin-top: var(--s3);
flex-wrap: wrap;
}
.legend-item { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--c-text-3); }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
/* ── Position Evolution ── */
#position-chart { width: 100%; }
/* ── Race Control ── */
.rc-event {
display: grid;
grid-template-columns: 52px 90px 1fr;
gap: var(--s3);
padding: 7px 0;
border-bottom: 1px solid var(--c-border);
font-size: 11px;
align-items: baseline;
}
.rc-event:last-child { border-bottom: none; }
.rc-event-lap { font-family: var(--f-mono); font-weight: 600; color: var(--c-text-3); }
.rc-event-type { font-size: 10px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }
.rc-event-msg { color: var(--c-text-2); line-height: 1.4; }
.rc-event.sc .rc-event-type { color: var(--c-yellow); }
.rc-event.vsc .rc-event-type { color: var(--c-yellow); }
.rc-event.drs .rc-event-type { color: var(--c-green); }
.rc-event.flag .rc-event-type { color: var(--c-red); }
.rc-event.fl .rc-event-type { color: var(--c-purple); }
.rc-event.info .rc-event-type { color: var(--c-text-3); }
/* ── Weather ── */
.wx-row {
display: grid;
grid-template-columns: 50px repeat(6, 1fr);
gap: var(--s3);
padding: 5px 0;
border-bottom: 1px solid var(--c-border);
font-size: 11px;
font-family: var(--f-mono);
align-items: center;
}
.wx-row:last-child { border-bottom: none; }
.wx-hdr {
font-family: var(--f-ui);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--c-text-3);
}
/* ── Dataset ── */
.ds-grid-main {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1px;
border: 1px solid var(--c-border);
margin-bottom: var(--s4);
}
.ds-cell {
display: flex;
align-items: center;
gap: var(--s2);
padding: var(--s3) var(--s4);
background: var(--c-surface);
font-size: 11px;
}
.ds-cell-name { flex: 1; color: var(--c-text-2); }
.ds-cell-time { font-family: var(--f-mono); font-size: 10px; color: var(--c-text-3); }
/* ── Aside: condensed RC ── */
.rc-condensed { display: flex; flex-direction: column; }
.rc-cond-row {
display: grid;
grid-template-columns: 44px 1fr;
gap: var(--s2);
padding: 5px 0;
border-bottom: 1px solid var(--c-border);
font-size: 11px;
}
.rc-cond-row:last-child { border-bottom: none; }
.rc-cond-lap { font-family: var(--f-mono); color: var(--c-text-3); font-size: 10px; }
.rc-cond-msg { color: var(--c-text-2); line-height: 1.4; }
.rc-cond-row.sc .rc-cond-msg { color: var(--c-yellow); }
.rc-cond-row.vsc .rc-cond-msg { color: var(--c-yellow); }
.rc-cond-row.drs .rc-cond-msg { color: var(--c-green); }
.rc-cond-row.fl .rc-cond-msg { color: var(--c-purple); }
.rc-cond-row.flag .rc-cond-msg { color: var(--c-red); }
/* ── Aside: dataset mini ── */
.ds-mini { display: flex; flex-direction: column; gap: 3px; }
.ds-mini-row {
display: flex;
align-items: center;
gap: var(--s2);
font-size: 10px;
font-family: var(--f-mono);
color: var(--c-text-3);
}
.ds-mini-row .dot { flex-shrink: 0; }
</style>
</head>
<body>
<nav class="app-nav">
<a href="index.html" class="nav-logo">box<em>-</em>box</a>
<div class="nav-links">
<a href="command-center.html">Command Center</a>
<a href="live-timing.html">Live</a>
<a href="race-hub.html" class="active">Race Hub</a>
<a href="data-library.html">Data Library</a>
</div>
<div class="nav-right">
<div class="density-toggle">
<button class="active" onclick="setDensity('default',this)">D</button>
<button onclick="setDensity('compact',this)">C</button>
</div>
</div>
</nav>
<!-- Sub-header -->
<div class="rh-header">
<div class="rh-breadcrumb">
<a href="command-center.html">2025</a>
<span class="sep"></span>
<a href="#">Monaco GP</a>
<span class="sep"></span>
<span class="cur">Race</span>
</div>
<div style="display:flex;align-items:center;gap:var(--s3);font-size:12px">
<span class="t3 mono">78 Laps · 260.286 km · 25 May 2025</span>
<span class="badge local">Local data</span>
</div>
<div class="rh-session-btns">
<button class="rh-session-btn">FP1</button>
<button class="rh-session-btn">FP2</button>
<button class="rh-session-btn">FP3</button>
<button class="rh-session-btn">Qual</button>
<button class="rh-session-btn active">Race</button>
</div>
</div>
<!-- ── 2-column body ── -->
<div class="rh-body">
<!-- Left: tabbed content -->
<div style="display:flex;flex-direction:column;overflow:hidden">
<!-- Section tabs -->
<div class="rh-section-tabs">
<button class="tab-btn active" onclick="showTab('classification',this)">Classification</button>
<button class="tab-btn" onclick="showTab('strategy',this)">Strategy</button>
<button class="tab-btn" onclick="showTab('positions',this)">Positions</button>
<button class="tab-btn" onclick="showTab('racecontrol',this)">Race Control</button>
<button class="tab-btn" onclick="showTab('weather',this)">Weather</button>
<button class="tab-btn" onclick="showTab('dataset',this)">Dataset</button>
</div>
<!-- Tab panels -->
<div class="rh-content">
<!-- CLASSIFICATION -->
<div id="tab-classification" class="tab-panel active">
<div class="sec-header">
<span class="sec-title">Final Classification</span>
<span class="sec-meta">78 laps · 1:32:14.456</span>
</div>
<div class="scroll-x">
<table class="data-table" style="min-width:620px">
<thead>
<tr>
<th class="center" style="width:28px">P</th>
<th>Driver</th>
<th>Team</th>
<th class="center">Grid</th>
<th class="center">Δ</th>
<th class="num">Time / Gap</th>
<th class="num">Fastest Lap</th>
<th class="num">Pts</th>
</tr>
</thead>
<tbody>
<tr>
<td class="center"><span class="pos-p1">1</span></td>
<td>
<div class="drv-cell">
<div class="drv-bar" style="background:var(--t-fer)"></div>
<span class="drv-code">LEC</span><span class="drv-num">16</span>
</div>
</td>
<td class="t2">Ferrari</td>
<td class="center t3 mono">3</td>
<td class="center"><span class="pos-gain">↑2</span></td>
<td class="num mono">1:32:14.456</td>
<td class="num mono t3">1:14.892</td>
<td class="num" style="font-weight:700">25</td>
</tr>
<tr>
<td class="center"><span class="pos-p2">2</span></td>
<td>
<div class="drv-cell">
<div class="drv-bar" style="background:var(--t-rb)"></div>
<span class="drv-code">VER</span><span class="drv-num">1</span>
</div>
</td>
<td class="t2">Red Bull</td>
<td class="center t3 mono">1</td>
<td class="center"><span class="pos-loss">↓1</span></td>
<td class="num mono t2">+3.456</td>
<td class="num mono t3">1:15.023</td>
<td class="num" style="font-weight:700">18</td>
</tr>
<tr>
<td class="center"><span class="pos-p3">3</span></td>
<td>
<div class="drv-cell">
<div class="drv-bar" style="background:var(--t-mcl)"></div>
<span class="drv-code">NOR</span><span class="drv-num">4</span>
</div>
</td>
<td class="t2">McLaren</td>
<td class="center t3 mono">4</td>
<td class="center"><span class="pos-gain">↑1</span></td>
<td class="num mono t2">+8.123</td>
<td class="num mono" style="color:var(--c-purple)">1:14.756 ●</td>
<td class="num" style="font-weight:700">15</td>
</tr>
<tr>
<td class="center t3 mono">4</td>
<td>
<div class="drv-cell">
<div class="drv-bar" style="background:var(--t-mcl)"></div>
<span class="drv-code">PIA</span><span class="drv-num">81</span>
</div>
</td>
<td class="t2">McLaren</td>
<td class="center t3 mono">6</td>
<td class="center"><span class="pos-gain">↑2</span></td>
<td class="num mono t2">+12.345</td>
<td class="num mono t3">1:15.234</td>
<td class="num t2">12</td>
</tr>
<tr>
<td class="center t3 mono">5</td>
<td>
<div class="drv-cell">
<div class="drv-bar" style="background:var(--t-mer)"></div>
<span class="drv-code">RUS</span><span class="drv-num">63</span>
</div>
</td>
<td class="t2">Mercedes</td>
<td class="center t3 mono">2</td>
<td class="center"><span class="pos-loss">↓3</span></td>
<td class="num mono t2">+15.678</td>
<td class="num mono t3">1:15.456</td>
<td class="num t2">10</td>
</tr>
<tr>
<td class="center t3 mono">6</td>
<td>
<div class="drv-cell">
<div class="drv-bar" style="background:var(--t-fer)"></div>
<span class="drv-code">HAM</span><span class="drv-num">44</span>
</div>
</td>
<td class="t2">Ferrari</td>
<td class="center t3 mono">5</td>
<td class="center"><span class="pos-loss">↓1</span></td>
<td class="num mono t2">+21.234</td>
<td class="num mono t3">1:15.890</td>
<td class="num t2">8</td>
</tr>
<tr>
<td class="center t3 mono">7</td>
<td>
<div class="drv-cell">
<div class="drv-bar" style="background:var(--t-am)"></div>
<span class="drv-code">ALO</span><span class="drv-num">14</span>
</div>
</td>
<td class="t2">Aston Martin</td>
<td class="center t3 mono">8</td>
<td class="center"><span class="pos-gain">↑1</span></td>
<td class="num mono t2">+28.456</td>
<td class="num mono t3">1:16.234</td>
<td class="num t2">6</td>
</tr>
<tr>
<td class="center t3 mono">8</td>
<td>
<div class="drv-cell">
<div class="drv-bar" style="background:var(--t-wil)"></div>
<span class="drv-code">SAI</span><span class="drv-num">55</span>
</div>
</td>
<td class="t2">Williams</td>
<td class="center t3 mono">7</td>
<td class="center"><span class="pos-loss">↓1</span></td>
<td class="num mono t2">+35.789</td>
<td class="num mono t3">1:16.567</td>
<td class="num t2">4</td>
</tr>
<tr>
<td class="center t3 mono">9</td>
<td>
<div class="drv-cell">
<div class="drv-bar" style="background:var(--t-alp)"></div>
<span class="drv-code t3">GAS</span><span class="drv-num">10</span>
</div>
</td>
<td class="t3">Alpine</td>
<td class="center t3 mono">9</td>
<td class="center"><span class="pos-same"></span></td>
<td class="num mono t3">+42.123</td>
<td class="num mono t3">1:16.890</td>
<td class="num t3">2</td>
</tr>
<tr>
<td class="center t3 mono">10</td>
<td>
<div class="drv-cell">
<div class="drv-bar" style="background:var(--t-haas)"></div>
<span class="drv-code t3">OCO</span><span class="drv-num">31</span>
</div>
</td>
<td class="t3">Haas</td>
<td class="center t3 mono">10</td>
<td class="center"><span class="pos-same"></span></td>
<td class="num mono t3">+48.456</td>
<td class="num mono t3">1:17.012</td>
<td class="num t3">1</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- STRATEGY -->
<div id="tab-strategy" class="tab-panel">
<div class="sec-header">
<span class="sec-title">Tyre Strategy</span>
<span class="sec-meta">78 laps · SC L2326 · VSC L5860</span>
</div>
<div id="strategy-chart"></div>
<div class="compound-legend">
<div class="legend-item"><div class="legend-dot" style="background:var(--tyre-s)"></div>Soft</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--tyre-m)"></div>Medium</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--tyre-h)"></div>Hard</div>
<div class="legend-item" style="margin-left:var(--s4)">
<div style="width:14px;height:2px;background:rgba(255,214,0,0.45);border-radius:1px"></div>
<span>SC / VSC period</span>
</div>
</div>
</div>
<!-- POSITIONS -->
<div id="tab-positions" class="tab-panel">
<div class="sec-header">
<span class="sec-title">Position Evolution</span>
<span class="sec-meta">Top 6 · SC L2326 · VSC L5860</span>
</div>
<div id="position-chart"></div>
<div class="compound-legend" style="margin-top:var(--s3)">
<div class="legend-item"><div class="legend-dot" style="background:var(--t-fer)"></div>LEC</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--t-rb)"></div>VER</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--t-mcl)"></div>NOR</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--t-mcl);opacity:.55"></div>PIA</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--t-mer)"></div>RUS</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--t-mer);opacity:.55"></div>HAM</div>
</div>
</div>
<!-- RACE CONTROL -->
<div id="tab-racecontrol" class="tab-panel">
<div class="sec-header">
<span class="sec-title">Race Control</span>
<span class="sec-meta">14 messages</span>
</div>
<div>
<div class="rc-event info">
<span class="rc-event-lap">L1</span>
<span class="rc-event-type">Start</span>
<span class="rc-event-msg">RACE START — Track Clear</span>
</div>
<div class="rc-event drs">
<span class="rc-event-lap">L3</span>
<span class="rc-event-type">DRS</span>
<span class="rc-event-msg">DRS ENABLED</span>
</div>
<div class="rc-event info">
<span class="rc-event-lap">L18</span>
<span class="rc-event-type">Incident</span>
<span class="rc-event-msg">ALB/STR at T10 under investigation</span>
</div>
<div class="rc-event sc">
<span class="rc-event-lap">L23</span>
<span class="rc-event-type">Safety Car</span>
<span class="rc-event-msg">SAFETY CAR DEPLOYED — Incident T10 (ALB retirement)</span>
</div>
<div class="rc-event sc">
<span class="rc-event-lap">L26</span>
<span class="rc-event-type">Safety Car</span>
<span class="rc-event-msg">SAFETY CAR IN THIS LAP</span>
</div>
<div class="rc-event drs">
<span class="rc-event-lap">L27</span>
<span class="rc-event-type">DRS</span>
<span class="rc-event-msg">DRS ENABLED — Lap 27</span>
</div>
<div class="rc-event info">
<span class="rc-event-lap">L35</span>
<span class="rc-event-type">Penalty</span>
<span class="rc-event-msg">5-SECOND PENALTY — RUS · Unsafe release pit lane</span>
</div>
<div class="rc-event fl">
<span class="rc-event-lap">L38</span>
<span class="rc-event-type">Fastest Lap</span>
<span class="rc-event-msg">NOR · 1:14.756</span>
</div>
<div class="rc-event vsc">
<span class="rc-event-lap">L58</span>
<span class="rc-event-type">VSC</span>
<span class="rc-event-msg">VIRTUAL SAFETY CAR DEPLOYED — Debris T6</span>
</div>
<div class="rc-event vsc">
<span class="rc-event-lap">L60</span>
<span class="rc-event-type">VSC</span>
<span class="rc-event-msg">VIRTUAL SAFETY CAR ENDING</span>
</div>
<div class="rc-event drs">
<span class="rc-event-lap">L61</span>
<span class="rc-event-type">DRS</span>
<span class="rc-event-msg">DRS ENABLED — Lap 61</span>
</div>
<div class="rc-event flag">
<span class="rc-event-lap">L78</span>
<span class="rc-event-type">Finish</span>
<span class="rc-event-msg">CHEQUERED FLAG — Race complete</span>
</div>
</div>
</div>
<!-- WEATHER -->
<div id="tab-weather" class="tab-panel">
<div class="sec-header">
<span class="sec-title">Weather</span>
<span class="sec-meta">Race · sampled every 5 laps · highlighted = SC/VSC</span>
</div>
<div class="scroll-x">
<div class="wx-row"><span class="wx-hdr">Lap</span><span class="wx-hdr">Air °C</span><span class="wx-hdr">Track °C</span><span class="wx-hdr">Humid %</span><span class="wx-hdr">Wind km/h</span><span class="wx-hdr">Dir</span><span class="wx-hdr">Rain</span></div>
<div class="wx-row"><span class="t3">1</span><span>22.4</span><span>32.1</span><span>65</span><span>6</span><span class="t3">NW</span><span class="t-green">0%</span></div>
<div class="wx-row"><span class="t3">10</span><span>23.1</span><span>34.2</span><span>63</span><span>7</span><span class="t3">NW</span><span class="t-green">0%</span></div>
<div class="wx-row"><span class="t3">20</span><span>23.8</span><span>35.6</span><span>62</span><span>6</span><span class="t3">N</span><span class="t-green">1%</span></div>
<div class="wx-row" style="background:rgba(255,214,0,0.05)"><span class="t-yellow">23</span><span>24.0</span><span>36.0</span><span>62</span><span>7</span><span class="t3">N</span><span class="t-green">1%</span></div>
<div class="wx-row"><span class="t3">30</span><span>24.3</span><span>36.8</span><span>61</span><span>8</span><span class="t3">NW</span><span class="t-green">2%</span></div>
<div class="wx-row"><span class="t3">40</span><span>24.9</span><span>37.4</span><span>60</span><span>8</span><span class="t3">NW</span><span class="t-green">2%</span></div>
<div class="wx-row"><span class="t3">50</span><span>25.2</span><span>38.1</span><span>59</span><span>9</span><span class="t3">W</span><span class="t-green">3%</span></div>
<div class="wx-row" style="background:rgba(255,214,0,0.05)"><span class="t-yellow">58</span><span>25.5</span><span>38.3</span><span>59</span><span>8</span><span class="t3">W</span><span class="t-green">3%</span></div>
<div class="wx-row"><span class="t3">70</span><span>25.8</span><span>38.6</span><span>58</span><span>7</span><span class="t3">W</span><span class="t-green">4%</span></div>
<div class="wx-row"><span class="t3">78</span><span>26.1</span><span>38.8</span><span>57</span><span>7</span><span class="t3">NW</span><span class="t-green">4%</span></div>
</div>
</div>
<!-- DATASET -->
<div id="tab-dataset" class="tab-panel">
<div class="sec-header">
<span class="sec-title">Dataset Status</span>
<span class="sec-meta">session_key 9158 · Monaco GP Race</span>
</div>
<div class="ds-grid-main">
<div class="ds-cell"><span class="dot local"></span><span class="ds-cell-name">session_results</span><span class="ds-cell-time">3h ago</span></div>
<div class="ds-cell"><span class="dot local"></span><span class="ds-cell-name">starting_grid</span><span class="ds-cell-time">3h ago</span></div>
<div class="ds-cell"><span class="dot local"></span><span class="ds-cell-name">laps</span><span class="ds-cell-time">3h ago</span></div>
<div class="ds-cell"><span class="dot local"></span><span class="ds-cell-name">stints</span><span class="ds-cell-time">3h ago</span></div>
<div class="ds-cell"><span class="dot local"></span><span class="ds-cell-name">pit_stops</span><span class="ds-cell-time">3h ago</span></div>
<div class="ds-cell"><span class="dot local"></span><span class="ds-cell-name">positions</span><span class="ds-cell-time">3h ago</span></div>
<div class="ds-cell"><span class="dot local"></span><span class="ds-cell-name">race_control</span><span class="ds-cell-time">3h ago</span></div>
<div class="ds-cell"><span class="dot local"></span><span class="ds-cell-name">weather</span><span class="ds-cell-time">3h ago</span></div>
<div class="ds-cell"><span class="dot local"></span><span class="ds-cell-name">overtakes</span><span class="ds-cell-time">3h ago</span></div>
<div class="ds-cell"><span class="dot local"></span><span class="ds-cell-name">track_outline</span><span class="ds-cell-time">cached</span></div>
<div class="ds-cell"><span class="dot missing"></span><span class="ds-cell-name">car_data_samples</span><span class="ds-cell-time t3">not ingested</span></div>
<div class="ds-cell"><span class="dot missing"></span><span class="ds-cell-name">location_samples</span><span class="ds-cell-time t3">not ingested</span></div>
<div class="ds-cell"><span class="dot missing"></span><span class="ds-cell-name">team_radio</span><span class="ds-cell-time t3">not ingested</span></div>
</div>
<div class="cli-block">
<div class="comment"># Ingest telemetry (high-volume, explicit only)</div>
<div class="cmd">box-box --ingest-session 9158 --datasets car_data,location</div>
<br>
<div class="comment"># Refresh all datasets for this session</div>
<div class="cmd">box-box --ingest-session 9158 --refresh</div>
</div>
</div>
</div><!-- /rh-content -->
</div><!-- /rh-main -->
<!-- ── Persistent right aside ── -->
<aside class="rh-aside">
<!-- Podium -->
<div>
<div class="sec-header">
<span class="sec-title">Podium</span>
</div>
<div class="podium-list">
<div class="podium-item">
<span class="podium-p pos-p1">P1</span>
<div class="drv-cell" style="gap:var(--s2)">
<div class="drv-bar" style="background:var(--t-fer);height:16px"></div>
<span class="drv-code" style="font-size:13px">LEC</span>
</div>
<span class="podium-gap-val">1:32:14.456</span>
</div>
<div class="podium-item">
<span class="podium-p pos-p2">P2</span>
<div class="drv-cell" style="gap:var(--s2)">
<div class="drv-bar" style="background:var(--t-rb);height:16px"></div>
<span class="drv-code" style="font-size:13px">VER</span>
</div>
<span class="podium-gap-val">+3.456</span>
</div>
<div class="podium-item">
<span class="podium-p pos-p3">P3</span>
<div class="drv-cell" style="gap:var(--s2)">
<div class="drv-bar" style="background:var(--t-mcl);height:16px"></div>
<span class="drv-code" style="font-size:13px">NOR</span>
</div>
<span class="podium-gap-val">+8.123</span>
</div>
</div>
</div>
<!-- Race Stats -->
<div>
<div class="sec-header">
<span class="sec-title">Race Stats</span>
</div>
<div>
<div class="summary-stat-row">
<span class="sstat-label">Fastest Lap</span>
<span class="sstat-val" style="color:var(--c-purple)">NOR 1:14.756 L38</span>
</div>
<div class="summary-stat-row">
<span class="sstat-label">Pole Position</span>
<span class="sstat-val">VER</span>
</div>
<div class="summary-stat-row">
<span class="sstat-label">Safety Car</span>
<span class="sstat-val t-yellow">L2326 (4 laps)</span>
</div>
<div class="summary-stat-row">
<span class="sstat-label">VSC</span>
<span class="sstat-val t-yellow">L5860 (3 laps)</span>
</div>
<div class="summary-stat-row">
<span class="sstat-label">Total Pit Stops</span>
<span class="sstat-val">22</span>
</div>
<div class="summary-stat-row">
<span class="sstat-label">DNF</span>
<span class="sstat-val">ALB (T10 crash)</span>
</div>
<div class="summary-stat-row">
<span class="sstat-label">Penalties</span>
<span class="sstat-val">RUS +5s</span>
</div>
</div>
</div>
<!-- Condensed RC -->
<div>
<div class="sec-header">
<span class="sec-title">Key Events</span>
</div>
<div class="rc-condensed">
<div class="rc-cond-row flag">
<span class="rc-cond-lap">L78</span>
<span class="rc-cond-msg">Chequered flag</span>
</div>
<div class="rc-cond-row drs">
<span class="rc-cond-lap">L61</span>
<span class="rc-cond-msg">DRS enabled</span>
</div>
<div class="rc-cond-row vsc">
<span class="rc-cond-lap">L5860</span>
<span class="rc-cond-msg">VSC — debris T6</span>
</div>
<div class="rc-cond-row fl">
<span class="rc-cond-lap">L38</span>
<span class="rc-cond-msg">FL: NOR 1:14.756</span>
</div>
<div class="rc-cond-row drs">
<span class="rc-cond-lap">L27</span>
<span class="rc-cond-msg">DRS enabled</span>
</div>
<div class="rc-cond-row sc">
<span class="rc-cond-lap">L2326</span>
<span class="rc-cond-msg">SC — ALB retirement T10</span>
</div>
</div>
</div>
<!-- Dataset mini -->
<div>
<div class="sec-header">
<span class="sec-title">Data</span>
<span class="t3" style="font-size:10px;margin-left:auto">10/13 datasets</span>
</div>
<div class="ds-mini">
<div class="ds-mini-row"><span class="dot local"></span>session_results · laps · stints</div>
<div class="ds-mini-row"><span class="dot local"></span>pit_stops · positions · race_control</div>
<div class="ds-mini-row"><span class="dot local"></span>weather · overtakes · track_outline</div>
<div class="ds-mini-row"><span class="dot missing"></span>car_data · location · team_radio</div>
</div>
</div>
</aside><!-- /rh-aside -->
</div><!-- /rh-body -->
<script>
function setDensity(mode, btn) {
document.querySelectorAll('.density-toggle button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.documentElement.classList.toggle('compact', mode === 'compact');
renderCharts();
}
function showTab(id, btn) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-' + id).classList.add('active');
if (id === 'strategy' || id === 'positions') renderCharts();
}
/* ── Strategy Chart ── */
function renderStrategyChart() {
const el = document.getElementById('strategy-chart');
if (!el) return;
const TOTAL = 78;
const compounds = {
S: { fill: '#e8002d', text: '#fff' },
M: { fill: '#c8b400', text: '#000' },
H: { fill: '#909090', text: '#fff' }
};
const drivers = [
{ code: 'LEC', color: '#e8002d', stints: [{c:'S',s:1,e:20},{c:'M',s:21,e:55},{c:'H',s:56,e:78}] },
{ code: 'VER', color: '#3671c6', stints: [{c:'M',s:1,e:28},{c:'H',s:29,e:60},{c:'M',s:61,e:78}] },
{ code: 'NOR', color: '#ff8000', stints: [{c:'S',s:1,e:18},{c:'M',s:19,e:52},{c:'H',s:53,e:78}] },
{ code: 'PIA', color: '#cc6600', stints: [{c:'S',s:1,e:22},{c:'M',s:23,e:50},{c:'H',s:51,e:78}] },
{ code: 'RUS', color: '#27f4d2', stints: [{c:'M',s:1,e:30},{c:'H',s:31,e:62},{c:'S',s:63,e:78}] },
{ code: 'HAM', color: '#1abfa8', stints: [{c:'S',s:1,e:15},{c:'M',s:16,e:55},{c:'H',s:56,e:78}] },
{ code: 'ALO', color: '#229971', stints: [{c:'M',s:1,e:35},{c:'H',s:36,e:78}] },
{ code: 'SAI', color: '#64c4ff', stints: [{c:'S',s:1,e:20},{c:'M',s:21,e:58},{c:'S',s:59,e:78}] },
{ code: 'GAS', color: '#cc6699', stints: [{c:'M',s:1,e:28},{c:'H',s:29,e:65},{c:'S',s:66,e:78}] },
{ code: 'OCO', color: '#909090', stints: [{c:'S',s:1,e:20},{c:'M',s:21,e:55},{c:'H',s:56,e:78}] },
];
const ROW_H = 26, ROW_GAP = 7, LBL = 48, PAD = 8;
const W = el.clientWidth || 720;
const CHART_W = W - LBL - PAD;
const H = drivers.length * (ROW_H + ROW_GAP) + 24;
const lx = l => LBL + ((l - 1) / TOTAL) * CHART_W;
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">`;
svg += `<rect x="${lx(23)}" y="0" width="${lx(27)-lx(23)}" height="${drivers.length*(ROW_H+ROW_GAP)}" fill="rgba(255,214,0,0.07)"/>`;
svg += `<rect x="${lx(58)}" y="0" width="${lx(61)-lx(58)}" height="${drivers.length*(ROW_H+ROW_GAP)}" fill="rgba(255,214,0,0.05)"/>`;
[1,10,20,30,40,50,60,70,78].forEach(l => {
svg += `<text x="${lx(l)}" y="${H-4}" text-anchor="middle" font-size="9" fill="#4a4a4a" font-family="monospace">L${l}</text>`;
});
drivers.forEach((d, i) => {
const y = i * (ROW_H + ROW_GAP);
svg += `<text x="${LBL-5}" y="${y+ROW_H/2+4}" text-anchor="end" font-size="11" font-weight="700" fill="${d.color}" font-family="monospace">${d.code}</text>`;
d.stints.forEach(st => {
const x1 = lx(st.s), x2 = lx(st.e+1), w = x2 - x1 - 1;
const laps = st.e - st.s + 1, c = compounds[st.c];
svg += `<rect x="${x1}" y="${y}" width="${w}" height="${ROW_H}" fill="${c.fill}" rx="1"/>`;
if (w > 28) svg += `<text x="${x1+w/2}" y="${y+ROW_H/2+4}" text-anchor="middle" font-size="10" font-weight="700" fill="${c.text}" font-family="monospace">${st.c} ${laps}</text>`;
else if (w > 10) svg += `<text x="${x1+w/2}" y="${y+ROW_H/2+4}" text-anchor="middle" font-size="9" font-weight="700" fill="${c.text}" font-family="monospace">${st.c}</text>`;
});
});
svg += '</svg>';
el.innerHTML = svg;
}
/* ── Position Evolution ── */
function renderPositionChart() {
const el = document.getElementById('position-chart');
if (!el) return;
const W = el.clientWidth || 720, H = 200;
const PAD = { t: 12, r: 40, b: 28, l: 28 };
const CW = W - PAD.l - PAD.r, CH = H - PAD.t - PAD.b;
const LAPS = 78, POSITIONS = 8;
const xp = l => PAD.l + (l / LAPS) * CW;
const yp = p => PAD.t + ((p - 1) / (POSITIONS - 1)) * CH;
const drivers = [
{ code: 'LEC', color: '#e8002d', dash: false, pts: [{l:0,p:3},{l:2,p:1},{l:78,p:1}] },
{ code: 'VER', color: '#3671c6', dash: false, pts: [{l:0,p:1},{l:2,p:2},{l:78,p:2}] },
{ code: 'NOR', color: '#ff8000', dash: false, pts: [{l:0,p:4},{l:6,p:3},{l:78,p:3}] },
{ code: 'PIA', color: '#ff8000', dash: true, pts: [{l:0,p:6},{l:10,p:4},{l:78,p:4}] },
{ code: 'RUS', color: '#27f4d2', dash: false, pts: [{l:0,p:2},{l:2,p:5},{l:78,p:5}] },
{ code: 'HAM', color: '#27f4d2', dash: true, pts: [{l:0,p:5},{l:78,p:6}] },
];
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">`;
svg += `<rect x="${xp(23)}" y="${PAD.t}" width="${xp(26)-xp(23)}" height="${CH}" fill="rgba(255,214,0,0.08)"/>`;
svg += `<rect x="${xp(58)}" y="${PAD.t}" width="${xp(60)-xp(58)}" height="${CH}" fill="rgba(255,214,0,0.05)"/>`;
svg += `<text x="${xp(24.5)}" y="${PAD.t+10}" text-anchor="middle" font-size="8" fill="#6a5800" font-family="sans-serif" font-weight="700">SC</text>`;
svg += `<text x="${xp(59)}" y="${PAD.t+10}" text-anchor="middle" font-size="8" fill="#6a5800" font-family="sans-serif" font-weight="700">VSC</text>`;
for (let p = 1; p <= POSITIONS; p++) {
svg += `<line x1="${PAD.l}" y1="${yp(p)}" x2="${W-PAD.r}" y2="${yp(p)}" stroke="#1e1e1e" stroke-width="1"/>`;
svg += `<text x="${PAD.l-4}" y="${yp(p)+3}" text-anchor="end" font-size="9" fill="#4a4a4a" font-family="monospace">P${p}</text>`;
}
[0,10,20,30,40,50,60,70,78].forEach(l => {
svg += `<text x="${xp(l)}" y="${H-4}" text-anchor="middle" font-size="9" fill="#4a4a4a" font-family="monospace">L${l||1}</text>`;
});
drivers.forEach(d => {
const pts = d.pts.map(p => `${xp(p.l)},${yp(p.p)}`).join(' ');
const dash = d.dash ? 'stroke-dasharray="5,3"' : '';
svg += `<polyline points="${pts}" stroke="${d.color}" stroke-width="1.5" fill="none" ${dash} stroke-opacity="0.9"/>`;
const last = d.pts[d.pts.length - 1];
svg += `<text x="${xp(last.l)+4}" y="${yp(last.p)+4}" font-size="9" fill="${d.color}" font-family="monospace" font-weight="700">${d.code}</text>`;
});
svg += '</svg>';
el.innerHTML = svg;
}
function renderCharts() {
renderStrategyChart();
renderPositionChart();
}
document.addEventListener('DOMContentLoaded', renderCharts);
window.addEventListener('resize', renderCharts);
</script>
</body>
</html>

View File

@@ -0,0 +1,779 @@
/* ================================================================
box-box design system — F1 Ops Room
================================================================ */
:root {
/* Backgrounds */
--c-bg: #0a0a0a;
--c-surface: #111111;
--c-surface-2: #181818;
--c-surface-3: #202020;
/* Borders */
--c-border: #242424;
--c-border-2: #303030;
/* Text */
--c-text: #ebebeb;
--c-text-2: #9c9c9c;
--c-text-3: #6c6c6c;
--c-text-inv: #0a0a0a;
/* Signal colors */
--c-red: #e10600;
--c-yellow: #ffd600;
--c-green: #00cc6a;
--c-purple: #b06fff;
--c-blue: #4499ff;
--c-orange: #ff8833;
/* Tyre compounds */
--tyre-s: #e8002d;
--tyre-m: #c8b400;
--tyre-h: #b8b8b8;
--tyre-i: #39b54a;
--tyre-w: #0067ff;
/* Team colors */
--t-rb: #3671c6;
--t-mcl: #ff8000;
--t-fer: #e8002d;
--t-mer: #27f4d2;
--t-am: #229971;
--t-alp: #ff87bc;
--t-wil: #64c4ff;
--t-vcarb: #6692ff;
--t-haas: #b6babd;
--t-ks: #52e252;
/* Spacing */
--s1: 4px;
--s2: 8px;
--s3: 12px;
--s4: 16px;
--s5: 20px;
--s6: 24px;
--s8: 32px;
/* Typography */
--f-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif;
--f-mono: 'SF Mono', ui-monospace, 'Fira Code', 'Courier New', monospace;
/* Density — comfortable default */
--row-h: 34px;
--pad-v: 6px;
--pad-h: 10px;
--sec-gap: 28px;
}
html.compact {
--row-h: 26px;
--pad-v: 3px;
--pad-h: 8px;
--sec-gap: 18px;
}
/* ── Reset ──────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html {
font-size: 13px;
background: var(--c-bg);
color: var(--c-text);
scroll-behavior: smooth;
}
body {
font-family: var(--f-ui);
line-height: 1.4;
min-height: 100vh;
}
a { color: inherit; text-decoration: none; }
button { cursor: pointer; font-family: var(--f-ui); }
/* ── App Nav ────────────────────────────────────────────────────── */
.app-nav {
display: flex;
align-items: center;
gap: var(--s2);
height: 44px;
padding: 0 var(--s5);
background: var(--c-surface);
border-bottom: 1px solid var(--c-border);
position: sticky;
top: 0;
z-index: 200;
}
.nav-logo {
font-size: 13px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--c-text);
margin-right: var(--s3);
}
.nav-logo em {
color: var(--c-red);
font-style: normal;
}
.nav-links {
display: flex;
gap: 2px;
flex: 1;
}
.nav-links a {
padding: 4px var(--s3);
border-radius: 3px;
font-size: 12px;
color: var(--c-text-3);
letter-spacing: 0.02em;
transition: color 0.1s, background 0.1s;
}
.nav-links a:hover { color: var(--c-text-2); background: var(--c-surface-2); }
.nav-links a.active { color: var(--c-text); }
.nav-right {
display: flex;
align-items: center;
gap: var(--s3);
}
.live-badge {
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
color: var(--c-red);
}
.live-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--c-red);
animation: blink 1.4s ease-in-out infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.25; }
}
.density-toggle {
display: flex;
gap: 2px;
}
.density-toggle button {
padding: 2px 7px;
background: none;
border: 1px solid var(--c-border);
color: var(--c-text-3);
font-size: 10px;
border-radius: 2px;
letter-spacing: 0.04em;
}
.density-toggle button.active {
background: var(--c-surface-2);
border-color: var(--c-border-2);
color: var(--c-text-2);
}
/* ── Section Headers ────────────────────────────────────────────── */
.sec-header {
display: flex;
align-items: baseline;
gap: var(--s3);
padding-bottom: var(--s2);
border-bottom: 1px solid var(--c-border);
margin-bottom: var(--s4);
}
.sec-title {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--c-text-2);
}
.sec-meta {
font-size: 11px;
color: var(--c-text-3);
font-family: var(--f-mono);
}
/* ── Data Table ─────────────────────────────────────────────────── */
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.data-table th {
padding: var(--s1) var(--pad-h);
text-align: left;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--c-text-3);
border-bottom: 1px solid var(--c-border);
white-space: nowrap;
}
.data-table td {
padding: var(--pad-v) var(--pad-h);
border-bottom: 1px solid var(--c-border);
height: var(--row-h);
white-space: nowrap;
}
.data-table tbody tr:hover { background: var(--c-surface-2); }
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table .num { font-family: var(--f-mono); text-align: right; }
.data-table .center { text-align: center; }
/* ── Driver Identity Cell ───────────────────────────────────────── */
.drv-cell {
display: flex;
align-items: center;
gap: var(--s2);
}
.drv-bar {
width: 3px;
height: 20px;
border-radius: 1px;
flex-shrink: 0;
}
.drv-code {
font-size: 13px;
font-weight: 700;
letter-spacing: 0.03em;
}
.drv-num {
font-family: var(--f-mono);
font-size: 10px;
color: var(--c-text-3);
}
/* ── Tyre Badge ─────────────────────────────────────────────────── */
.tyre {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
font-size: 9px;
font-weight: 800;
font-family: var(--f-mono);
flex-shrink: 0;
}
.tyre.S { background: var(--tyre-s); color: #fff; }
.tyre.M { background: var(--tyre-m); color: #000; }
.tyre.H { background: var(--tyre-h); color: #333; }
.tyre.I { background: var(--tyre-i); color: #fff; }
.tyre.W { background: var(--tyre-w); color: #fff; }
/* ── Status Dot ─────────────────────────────────────────────────── */
.dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
.dot.live { background: var(--c-red); }
.dot.local { background: var(--c-green); }
.dot.partial { background: var(--c-yellow); }
.dot.missing { background: var(--c-text-3); }
.dot.stale { background: var(--c-orange); }
.dot.upcoming{ background: var(--c-border-2); }
/* ── Status Badge (inline) ──────────────────────────────────────── */
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 6px;
border-radius: 2px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
white-space: nowrap;
}
.badge.local { background: rgba(0,204,106,0.12); color: var(--c-green); }
.badge.live { background: rgba(225,6,0,0.12); color: var(--c-red); }
.badge.partial { background: rgba(255,214,0,0.12); color: var(--c-yellow); }
.badge.missing { background: rgba(85,85,85,0.12); color: var(--c-text-3); }
.badge.stale { background: rgba(255,136,51,0.12); color: var(--c-orange); }
/* ── Source Strip ───────────────────────────────────────────────── */
.source-strip {
display: flex;
align-items: center;
gap: var(--s3);
padding: var(--s2) var(--s4);
background: var(--c-surface);
border: 1px solid var(--c-border);
font-size: 11px;
color: var(--c-text-2);
}
/* ── Countdown ──────────────────────────────────────────────────── */
.countdown {
display: flex;
align-items: baseline;
gap: var(--s3);
}
.cd-unit {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.cd-num {
font-family: var(--f-mono);
font-size: 36px;
font-weight: 700;
color: var(--c-text);
line-height: 1;
}
.cd-label {
font-size: 9px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--c-text-3);
}
.cd-sep {
font-family: var(--f-mono);
font-size: 30px;
color: var(--c-border-2);
padding-bottom: 4px;
}
/* ── Session Banner (Live Timing) ───────────────────────────────── */
.session-banner {
display: flex;
align-items: center;
gap: var(--s5);
height: 40px;
padding: 0 var(--s5);
background: var(--c-surface);
border-bottom: 2px solid var(--c-border);
font-family: var(--f-mono);
font-size: 12px;
overflow-x: auto;
overflow-y: hidden;
flex-shrink: 0;
}
.banner-item {
display: flex;
align-items: center;
gap: var(--s2);
flex-shrink: 0;
}
.banner-label {
font-size: 9px;
font-family: var(--f-ui);
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--c-text-3);
}
.banner-val { font-weight: 600; }
.banner-sep { color: var(--c-border-2); padding: 0 var(--s1); }
.track-green { color: var(--c-green); }
.track-yellow { color: var(--c-yellow); }
.track-red { color: var(--c-red); }
/* ── Race Control Messages ──────────────────────────────────────── */
.rc-list { display: flex; flex-direction: column; }
.rc-msg {
display: grid;
grid-template-columns: 56px 1fr;
gap: var(--s2);
padding: 6px var(--s3);
border-bottom: 1px solid var(--c-border);
font-size: 11px;
line-height: 1.4;
}
.rc-msg:last-child { border-bottom: none; }
.rc-lap { font-family: var(--f-mono); color: var(--c-text-3); padding-top: 1px; }
.rc-text { color: var(--c-text-2); }
.rc-msg.sc .rc-text { color: var(--c-yellow); font-weight: 600; }
.rc-msg.vsc .rc-text { color: var(--c-yellow); }
.rc-msg.drs .rc-text { color: var(--c-green); }
.rc-msg.flag .rc-text { color: var(--c-red); font-weight: 600; }
.rc-msg.fl .rc-text { color: var(--c-purple); }
/* ── Position Indicators ────────────────────────────────────────── */
.pos-gain { color: var(--c-green); font-weight: 600; }
.pos-loss { color: var(--c-red); }
.pos-same { color: var(--c-text-3); }
/* ── Timing Tower specific ──────────────────────────────────────── */
.timing-table .pos-col { width: 30px; }
.timing-table .drv-col { min-width: 120px; }
.timing-table .gap-col { width: 80px; }
.timing-table .int-col { width: 70px; }
.timing-table .tyre-col { width: 40px; text-align: center; }
.timing-table .age-col { width: 36px; }
.timing-table .last-col { width: 78px; }
.timing-table .best-col { width: 78px; }
.timing-table tr.fastest-lap td { background: rgba(176,111,255,0.06); }
.timing-table tr.pit-in td { background: rgba(255,214,0,0.05); }
.timing-table tr.dnf td { opacity: 0.45; }
/* ── Section Tabs ───────────────────────────────────────────────── */
.section-tabs {
display: flex;
gap: 2px;
padding: var(--s3) var(--s5);
border-bottom: 1px solid var(--c-border);
overflow-x: auto;
}
.tab-btn {
padding: 4px var(--s3);
background: none;
border: 1px solid transparent;
border-radius: 2px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--c-text-3);
white-space: nowrap;
transition: color 0.1s;
}
.tab-btn:hover { color: var(--c-text-2); }
.tab-btn.active { color: var(--c-text); border-color: var(--c-border-2); background: var(--c-surface-2); }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* ── Panels / Boxes ─────────────────────────────────────────────── */
.panel {
background: var(--c-surface);
border: 1px solid var(--c-border);
}
/* ── Strategy Chart Container ───────────────────────────────────── */
.strategy-chart { width: 100%; overflow: hidden; }
.strategy-chart svg { display: block; width: 100%; height: auto; }
/* ── CLI Block ──────────────────────────────────────────────────── */
.cli-block {
background: var(--c-surface-2);
border: 1px solid var(--c-border);
border-left: 3px solid var(--c-border-2);
padding: var(--s3) var(--s4);
font-family: var(--f-mono);
font-size: 11px;
color: var(--c-text-2);
line-height: 1.8;
}
.cli-block .comment { color: var(--c-text-3); }
.cli-block .cmd { color: var(--c-green); }
/* ── Dataset Status Row ─────────────────────────────────────────── */
.dataset-row {
display: flex;
align-items: center;
gap: var(--s3);
padding: var(--pad-v) 0;
border-bottom: 1px solid var(--c-border);
font-size: 11px;
}
.dataset-row:last-child { border-bottom: none; }
.ds-name { flex: 1; color: var(--c-text-2); }
.ds-status { font-family: var(--f-mono); font-size: 10px; }
.ds-time { font-family: var(--f-mono); font-size: 10px; color: var(--c-text-3); min-width: 80px; text-align: right; }
/* ── Utilities ──────────────────────────────────────────────────── */
.mono { font-family: var(--f-mono); }
.t2 { color: var(--c-text-2); }
.t3 { color: var(--c-text-3); }
.t-green { color: var(--c-green); }
.t-red { color: var(--c-red); }
.t-yellow { color: var(--c-yellow); }
.t-purple { color: var(--c-purple); }
.t-orange { color: var(--c-orange); }
.t-blue { color: var(--c-blue); }
.flex { display: flex; }
.flex-col { display: flex; flex-direction: column; }
.ai-c { align-items: center; }
.gap-1 { gap: var(--s1); }
.gap-2 { gap: var(--s2); }
.gap-3 { gap: var(--s3); }
.gap-4 { gap: var(--s4); }
.divider { height: 1px; background: var(--c-border); margin: var(--sec-gap) 0; }
.scroll-y { overflow-y: auto; }
.scroll-x { overflow-x: auto; }
/* ── Responsive ─────────────────────────────────────────────────── */
@media (max-width: 768px) {
.nav-links a { font-size: 11px; padding: 4px var(--s2); }
.hide-mobile { display: none !important; }
}
@media (max-width: 480px) {
.nav-links { gap: 0; }
.hide-sm { display: none !important; }
}
/* ── Podium row accents ──────────────────────────────────────────── */
.pos-p1 { color: #d4a93a; font-size: 15px; font-weight: 800; }
.pos-p2 { color: #a8a8a8; font-size: 14px; font-weight: 700; }
.pos-p3 { color: #b07840; font-size: 14px; font-weight: 700; }
/* ── Race Hub 2-column body ─────────────────────────────────────── */
.rh-body {
display: grid;
grid-template-columns: 1fr 290px;
align-items: start;
min-height: calc(100vh - 88px);
}
.rh-aside {
border-left: 1px solid var(--c-border);
position: sticky;
top: 44px;
max-height: calc(100vh - 44px);
overflow-y: auto;
padding: var(--s4);
display: flex;
flex-direction: column;
gap: var(--s5);
}
@media (max-width: 980px) {
.rh-body { grid-template-columns: 1fr; }
.rh-aside {
position: static;
max-height: none;
border-left: none;
border-top: 2px solid var(--c-border);
padding: var(--s5);
}
}
/* ── Race Summary (aside) ────────────────────────────────────────── */
.podium-list { display: flex; flex-direction: column; gap: 1px; }
.podium-item {
display: grid;
grid-template-columns: 22px 1fr auto;
align-items: center;
gap: var(--s2);
padding: 6px 0;
border-bottom: 1px solid var(--c-border);
}
.podium-item:last-child { border-bottom: none; }
.podium-p { font-family: var(--f-mono); font-size: 12px; font-weight: 700; color: var(--c-text-3); }
.podium-gap-val { font-family: var(--f-mono); font-size: 11px; color: var(--c-text-2); }
.summary-stat-row {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 4px 0;
border-bottom: 1px solid var(--c-border);
font-size: 11px;
}
.summary-stat-row:last-child { border-bottom: none; }
.sstat-label { color: var(--c-text-3); }
.sstat-val { font-family: var(--f-mono); color: var(--c-text-2); font-size: 11px; }
/* ── Pit Window ─────────────────────────────────────────────────── */
.pit-row {
display: flex;
align-items: center;
gap: var(--s2);
padding: 5px var(--s3);
border-bottom: 1px solid var(--c-border);
font-size: 11px;
}
.pit-row:last-child { border-bottom: none; }
.pit-driver { flex: 1; }
.pit-tyre-age { font-family: var(--f-mono); font-size: 11px; color: var(--c-text-2); min-width: 32px; text-align: right; }
.pit-status {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.04em;
min-width: 56px;
text-align: right;
}
.pit-status.open { color: var(--c-green); }
.pit-status.soon { color: var(--c-yellow); }
.pit-status.overdue { color: var(--c-orange); }
.pit-status.done { color: var(--c-text-3); }
/* ── Pinned Driver Cards ─────────────────────────────────────────── */
.pinned-strip {
display: flex;
gap: var(--s2);
padding: var(--s2) var(--s4);
background: var(--c-surface);
border-bottom: 1px solid var(--c-border);
overflow-x: auto;
flex-shrink: 0;
}
.pin-card {
display: flex;
align-items: center;
gap: var(--s2);
padding: 5px var(--s3);
background: var(--c-surface-2);
border: 1px solid var(--c-border);
border-radius: 2px;
flex-shrink: 0;
}
.pin-pos { font-family: var(--f-mono); font-size: 11px; color: var(--c-text-3); min-width: 16px; }
.pin-gap { font-family: var(--f-mono); font-size: 11px; color: var(--c-text-2); }
/* ── Phone bottom tab bar ────────────────────────────────────────── */
.phone-tab-bar {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 54px;
background: var(--c-surface);
border-top: 2px solid var(--c-border);
z-index: 300;
}
.phone-tab-bar button {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
background: none;
border: none;
color: var(--c-text-3);
font-size: 9px;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
padding-bottom: 4px;
transition: color 0.1s;
}
.phone-tab-bar button.active { color: var(--c-text); }
.phone-tab-bar button .tab-icon { font-size: 17px; line-height: 1; }
/* Phone panel system */
.lt-phone-panel { }
@media (max-width: 768px) {
.phone-tab-bar { display: flex; }
.has-phone-tabs {
padding-bottom: 58px;
overflow: hidden;
height: calc(100vh - 44px - 40px);
}
.lt-phone-panel { display: none !important; }
.lt-phone-panel.phone-active { display: flex !important; }
/* Override grid for phone */
.lt-body-phone { display: block !important; }
}
/* ── Responsive: iPad intermediate ─────────────────────────────── */
@media (min-width: 769px) and (max-width: 1024px) {
.rh-body { grid-template-columns: 1fr 240px; }
}
/* ── Gap sparkline ──────────────────────────────────────────────── */
.gap-spark {
display: inline-flex;
align-items: flex-end;
gap: 1px;
height: 12px;
margin-left: 4px;
vertical-align: middle;
opacity: 0.7;
}
.gap-spark span {
width: 3px;
background: currentColor;
border-radius: 1px;
}