mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 11:54:59 -04:00
Add MVP completion checklist
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
75
documentations/refactor/21-mvp-completion-checklist.md
Normal file
75
documentations/refactor/21-mvp-completion-checklist.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 · 22–25 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 P1–P2</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>
|
||||
@@ -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 1–2 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 (769–1024px): `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.
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 L23–26 · VSC L58–60</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 L23–26 · VSC L58–60</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">L23–26 (4 laps)</span>
|
||||
</div>
|
||||
<div class="summary-stat-row">
|
||||
<span class="sstat-label">VSC</span>
|
||||
<span class="sstat-val t-yellow">L58–60 (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">L58–60</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">L23–26</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>
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user