This is a static dashboard for the GPT RS F1 Fantasy 2026 team, designed to be hosted on GitHub Pages. It is a single-page HTML app with no build step, no framework, and no server-side code.
Display the current GPT RS team composition (5 drivers + 2 constructors), prices, budget status, and upcoming race info in a dark-mode, F1-branded CoverFlow-style carousel that fits entirely within the viewport without scrolling.
This dashboard is a subproject of the F1 Fantasy automation system located at ../. The parent project’s CLAUDE.md governs the automated Claude agent that manages the GPT RS team — do not modify that file from within this dashboard project. However, you can and should read it for context about the team, data formats, and season structure.
Everything lives in index.html — styles, markup, and JavaScript are all inline. There is no build step. Keep it this way. Do not split into separate CSS/JS files unless the user explicitly requests it.
renderTeam()BASE: https://raw.githubusercontent.com/JoshCBruce/JoshCBruce.github.io/main/fantasy-data-26
Team: {BASE}/claude-team/team-PRE-ROUND-{N}.json
Prices: {BASE}/all-prices/prices-PRE-ROUND-{N}.json
Currently hardcoded to PRE-ROUND-1. As the season progresses, the TEAM_FILE and PRICES_FILE constants at the top of the <script> block must be updated to point to the latest round’s data, or the page should be made to discover the latest file automatically.
The FALLBACK_TEAM, FALLBACK_PREV_TEAM, and FALLBACK_PRICES constants contain full copies of the JSON. When updating for a new round, update both the remote file references AND the embedded fallback data. FALLBACK_PREV_TEAM represents the previous round and is always passed as the second slide in the carousel.
dashboard/
index.html # The entire app — HTML, CSS, JS
CLAUDE.md # This file
fonts/ # F1 and Titillium Web font files (TTF)
Formula1-Regular.ttf
Formula1-Bold.ttf
Formula1-Wide.ttf
Formula1-Black.ttf
TitilliumWeb-Regular.ttf
TitilliumWeb-SemiBold.ttf
TitilliumWeb-Bold.ttf
TitilliumWeb-Light.ttf
team-PRE-ROUND-{N}.json){
"metadata": {
"scraped_at": "ISO8601",
"round_context": {
"last_completed_round": null | number,
"upcoming_round": number,
"upcoming_race": "string",
"is_race_week": boolean,
"file_type": "pre-round" | "post-round"
}
},
"team": {
"drivers": [
{
"name": "Full Name",
"team": "Constructor Name", // must match keys in TEAM_SLUGS
"price_millions": 27.4,
"price_change_millions": 0.0,
"is_captain": true | false // DRS Boost (2x) target
}
],
"constructors": [
{
"name": "Constructor Name",
"price_millions": 23.3,
"price_change_millions": 0.0
}
],
"total_team_value_millions": 99.9
}
}
prices-PRE-ROUND-{N}.json){
"metadata": { /* same as team JSON */ },
"drivers": [
{
"name": "Full Name",
"team": "Constructor Name",
"price_millions": 27.7,
"ownership_pct": 28.0
}
],
"constructors": [
{
"name": "Constructor Name",
"price_millions": 29.3,
"ownership_pct": 23.0
}
]
}
Driver/constructor names in the team JSON must exactly match the keys in DRIVER_CODES, TEAM_SLUGS, TEAM_COLORS, and TEAM_BG_RGB lookup objects. If F1 changes a team name (e.g. “Audi” becomes “Sauber”), all four maps must be updated.
Driver and constructor images are loaded from Formula 1’s Cloudinary CDN. The URL patterns are:
https://media.formula1.com/image/upload/ar_16:9,c_crop,g_north/b_rgb:{TEAM_BG}/c_fill,w_480/q_auto/f_webp/common/f1/2026/{TEAM_SLUG}/{DRIVER_CODE}/2026{TEAM_SLUG}{DRIVER_CODE}front.png
https://media.formula1.com/image/upload/c_lpad,h_400,w_900/b_rgb:{TEAM_BG}/c_fill,w_600/q_auto/f_webp/common/f1/2026/{TEAM_SLUG}/2026{TEAM_SLUG}carright.png
These are the complete 2026 season mappings. If a new driver joins mid-season, add their code here.
Team slugs (maps display name to URL path segment):
| Display Name | Slug |
|—|—|
| Red Bull Racing | redbullracing |
| Mercedes | mercedes |
| Ferrari | ferrari |
| McLaren | mclaren |
| Aston Martin | astonmartin |
| Alpine | alpine |
| Williams | williams |
| Racing Bulls | racingbulls |
| Haas F1 Team | haasf1team |
| Audi | audi |
| Cadillac | cadillac |
Driver codes (maps full name to F1 media code):
| Driver | Code |
|—|—|
| Max Verstappen | maxver01 |
| George Russell | georus01 |
| Lando Norris | lannor01 |
| Oscar Piastri | oscpia01 |
| Kimi Antonelli | andant01 |
| Charles Leclerc | chalec01 |
| Lewis Hamilton | lewham01 |
| Isack Hadjar | isahad01 |
| Pierre Gasly | piegas01 |
| Carlos Sainz | carsai01 |
| Alexander Albon | alealb01 |
| Fernando Alonso | feralo01 |
| Lance Stroll | lanstr01 |
| Oliver Bearman | olibea01 |
| Esteban Ocon | estoco01 |
| Nico Hulkenberg | nichul01 |
| Gabriel Bortoleto | gabbor01 |
| Liam Lawson | lialaw01 |
| Arvid Lindblad | arvlin01 |
| Franco Colapinto | fracol01 |
| Sergio Perez | serper01 |
| Valtteri Bottas | valbot01 |
Team brand colors (hex, used for card color bars and image backgrounds):
| Team | Color | BG RGB (no #) |
|—|—|—|
| Red Bull Racing | #3671C6 | 3671c6 |
| Mercedes | #27F4D2 | 27f4d2 |
| Ferrari | #E8002D | e8002d |
| McLaren | #FF8000 | ff8000 |
| Aston Martin | #229971 | 229971 |
| Alpine | #00A1E8 | 00a1e8 |
| Williams | #1868DB | 1868db |
| Racing Bulls | #6692FF | 6692ff |
| Haas F1 Team | #B6BABD | b6babd |
| Audi | #FF2D00 | ff2d00 |
| Cadillac | #AAAAAD | aaaaad |
100vh with no scrolling on standard desktop viewports.main is full-width (no max-width) with overflow: hidden — clipping happens at the viewport edgewindow.innerWidth * 0.22), placing its centre at roughly 42% of the viewport (slightly left of centre by design)The team viewer is a CoverFlow-style horizontal carousel:
.carousel-wrap — width: 100%; overflow: hidden — clips the rail at the viewport edge.team-rail — flex row, gap: 80px between slides, animated via transform: translateX().team-slide — each round’s team, fixed width: 590px
.is-active — full scale, full opacity, pointer-events on.is-inactive — scale(0.9) translateY(31px), dimmed, grayscale, pointer-events offCarousel constants (JS):
SLIDE_WIDTH = 590 // px — matches 3×190px cards + 2×10px gaps
SLIDE_GAP = 80 // px — between slides in the rail
SLIDE_STRIDE = 670 // px — SLIDE_WIDTH + SLIDE_GAP
Rail transform formula:
tx = -currentSlide * SLIDE_STRIDE + getActiveLeft()
// getActiveLeft() = Math.round(window.innerWidth * 0.22)
The two slides are vertically aligned so that the centre of the 3-wide (second) driver row is at the same y-coordinate across both slides. This row centre sits at y ≈ 279px from each slide’s top:
With scale(0.9) from transform-origin: top center, the inactive slide’s 3-wide row would fall at 0.9 × 279 = 251px. Adding translateY(31px) before scaling corrects this: y' = 0.9(y + 31) = 0.9y + 27.9, so y'(279) = 279. The alignment holds within <1px throughout the transition animation.
max-width: 590pxrenderTeam)< / > buttons — same navigate function; disabled with opacity: 0.25 at carousel boundarieswindow.addEventListener('resize', ...) repositions the rail without animationmarginLeft#15151escale(0.9), opacity: 0.5, grayscale(50%) brightness(0.65)Formula1 font family (weights: 400, 700, 800, 900)TitilliumWeb font family (weights: 300, 400, 600, 700)fonts/--bg-primary: #15151e
--bg-secondary: #1e1e2e
--bg-card: #252536
--text-primary: #ffffff
--text-secondary: #a0a0b8
--text-muted: #6b6b82
--accent: #e10600 /* F1 red */
--green: #00d27a /* captain glow, positive price changes, budget remaining */
--red: #e10600 /* negative price changes */
--border: #33334a
--border-subtle: #2a2a3e
cd dashboard/
python3 -m http.server 8901
# Open http://localhost:8901/
Copy the entire dashboard/ folder contents to the GitHub Pages repo root (or a subdirectory). No build step needed.
TEAM_FILE and PRICES_FILE constants to point to the new round’s JSON filenamesFALLBACK_TEAM and FALLBACK_PRICES with the new round’s dataFALLBACK_TEAM data into FALLBACK_PREV_TEAM (it becomes the previous-round slide)name and team match the lookup tablesDRIVER_CODESUpdate ALL of these maps: TEAM_SLUGS, TEAM_COLORS, TEAM_BG_RGB, and verify the team JSON uses the new name.
The carousel is designed for N slides. To add a third round slide (e.g. post-round history):
FALLBACK_ROUND_N constantrenderTeam, append another <div class="team-slide is-inactive"> to the rail HTMLnavigate() and applyCarousel() functions will handle it automatically — count is read from the DOMonerror handler hides broken images).$100.0M in the BUDGET_CAP constant. This is unlikely to change mid-season but confirm against the scoring guide if uncertain.is_captain: true) should ideally be index 0 so they appear top-left.onerror="this.style.display='none'" handler prevents a broken image icon.getActiveLeft() uses window.innerWidth * 0.22. This places the active team slightly left of centre. The translateY(31px) alignment offset is calculated for a 190px-wide card at 16:9 aspect ratio. If card dimensions change, recalculate: offset = row_centre_y * (1 - scale) / scale where row_centre_y = 14 + card_height + 10 + card_height/2.keydown listener is registered at script parse time (outside renderTeam), so it persists across re-renders. Do not add it inside renderTeam or it will stack up duplicates.formatPriceChange function exists but isn’t wired into the card templates yet)