Add MVP completion checklist

This commit is contained in:
2026-05-25 02:58:36 -04:00
parent e410a01db1
commit 404029ff24
12 changed files with 86 additions and 5245 deletions

View File

@@ -93,9 +93,7 @@ TUI requirements during v1:
- Persist live events/snapshots as a separate append-only stream only after the
extracted package has stable event/state types.
- See [09 Phase 1 Live Extraction](09-phase-1-live-extraction.md) for the
implementation brief and
[Cursor Phase 1 Prompt](cursor-phase-1-live-extraction-prompt.md) for the
fresh-agent handoff.
original implementation brief.
### Phase 2: Store Foundation
@@ -106,9 +104,7 @@ TUI requirements during v1:
- Add normalized tables required for Race Hub v1.
- Keep existing HTTP cache behavior unchanged.
- See [10 Phase 2 Store Foundation](10-phase-2-store-foundation.md) for the
implementation brief and
[Cursor Phase 2 Prompt](cursor-phase-2-store-foundation-prompt.md) for the
fresh-agent handoff.
original implementation brief.
### Phase 3: Ingestion Foundation
@@ -118,9 +114,7 @@ TUI requirements during v1:
- Add conservative request delay, bounded retry, and 429/live-lockout handling.
- Make ingestion idempotent and resumable.
- See [11 Phase 3 Ingestion Foundation](11-phase-3-ingestion-foundation.md) for
the implementation brief and
[Cursor Phase 3 Prompt](cursor-phase-3-ingestion-foundation-prompt.md) for the
coding-agent handoff.
the original implementation brief.
### Phase 4: Local-First Web API
@@ -129,9 +123,7 @@ TUI requirements during v1:
- Migrate selected Web endpoints from direct OpenF1 calls to local-first reads.
- Allow small opportunistic fetches only for missing screen-level data.
- See [12 Phase 4 Local-First Web API](12-phase-4-local-first-web-api.md) for
the implementation brief and
[Cursor Phase 4 Prompt](cursor-phase-4-local-first-web-api-prompt.md) for the
coding-agent handoff.
the original implementation brief.
### Phase 5: React Race Hub Slice
@@ -142,9 +134,7 @@ TUI requirements during v1:
- Keep the old Web UI available until the replacement route is credible.
- This is the first frontend phase. Use Claude for this phase.
- See [13 Phase 5 React Race Hub](13-phase-5-react-race-hub.md) for the
implementation brief and
[Claude Phase 5 Prompt](claude-phase-5-react-race-hub-prompt.md) for the
frontend-agent handoff.
original implementation brief.
## Ingestion Rate-Limit Defaults

View File

@@ -46,8 +46,10 @@ Use `/api/v1/race-hub?session_key=...` as the primary API.
## Visual Direction
Follow the existing mockups in `documentations/refactor/screens/`, but treat
them as direction, not rigid specs.
The temporary static mockups used during early product exploration have been
removed now that the production React routes exist. Use the implemented React
screens as the current source of truth, and keep this visual direction in mind
for future refinement.
The UI should feel like an F1 operations room:

View File

@@ -0,0 +1,75 @@
# MVP Completion Checklist
## Status
The local-first Web UI MVP is now functionally assembled. The app can ingest
OpenF1 data into the SQLite domain store, serve local-first Race Hub and
navigation APIs, render the React Race Hub/Data Library/Live Timing routes, and
serve the built React app from Go web mode when `frontend/dist` is present.
This document replaces the temporary agent handoff prompts as the main
checkpoint for what has been completed and what remains.
## Completed
- Live SignalR code extracted into `internal/live` and reused by TUI and Web
mode.
- SQLite domain store added for local historical data.
- Session, meeting, and weekend ingestion paths added with dry-run support.
- Optional analytics ingestion failures are partial, not hard blockers.
- Local-first Race Hub API added with dataset availability metadata.
- Local-first season, meeting, and weekend navigation APIs added.
- React + TypeScript frontend added with TanStack Query and Router.
- Race Hub route added for classification, grid, strategy, positions, laps,
race control, weather, and dataset status.
- Data Library route added for local season/weekend coverage and CLI guidance.
- Live Timing route added for current backend live snapshot/SSE state.
- Go web mode serves the built React app from `frontend/dist` and falls back to
embedded legacy assets when no build is present.
## Verification Commands
Run these before cutting an MVP tag or handing the repo to another agent:
```bash
go test ./internal/live ./internal/store ./internal/ingest ./internal/query ./internal/web
npm test -- --run --prefix frontend
npm run build --prefix frontend
npm run test:e2e
```
For a local manual smoke test:
```bash
go run ./scripts/seed-e2e-db/main.go --db /tmp/boxbox-mvp.db
BOXBOX_DISABLE_LIVE=1 go run ./cmd/main.go --web --db /tmp/boxbox-mvp.db --port 18080
BOXBOX_API_PORT=18080 npm run dev --prefix frontend -- --host 127.0.0.1 --port 15173 --strictPort
```
Then open:
- `http://127.0.0.1:15173/race-hub?session_key=9472`
- `http://127.0.0.1:15173/data-library`
- `http://127.0.0.1:15173/live`
## Remaining Post-MVP Work
- Add real visual-regression checks for the React screens.
- Improve high-density mobile/iPad behavior for Live Timing and Race Hub tables.
- Add persisted live-event capture and reconciliation only after defining the
live storage model.
- Add track outline ingestion/read models to the React app if the local data
source is reliable enough.
- Expand from weekend/session ingestion toward safe full-season backfill.
- Add Command Center, Drivers, Standings, and Settings as separate product
phases.
- Revisit static archive feasibility after source mapping is proven.
## Notes
- `--ingest-year` currently discovers season meetings and sessions. Use
`--ingest-meeting <meeting_key>` for full weekend ingestion.
- The React app should continue avoiding direct OpenF1 reads. New Web UI routes
should call local-first Go APIs.
- The TUI live mode remains intentionally preserved. Historical Web UI parity
with the TUI is not required for this MVP.

View File

@@ -75,6 +75,8 @@ not implementation tickets yet.
slice for making one command ingest a whole race weekend into the local DB.
- [20 Phase 12 Data Library UI](20-phase-12-data-library-ui.md): frontend slice
for showing local ingestion coverage and next CLI actions.
- [21 MVP Completion Checklist](21-mvp-completion-checklist.md): current
implementation status, verification commands, and remaining post-MVP work.
## External References

View File

@@ -1,683 +0,0 @@
<!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

@@ -1,375 +0,0 @@
# 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

@@ -1,651 +0,0 @@
<!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

@@ -1,180 +0,0 @@
<!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

@@ -1,774 +0,0 @@
<!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

@@ -1,939 +0,0 @@
<!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

@@ -1,847 +0,0 @@
<!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

@@ -1,779 +0,0 @@
/* ================================================================
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;
}