diff --git a/public/portfolio/PHOTO-SOURCES.md b/public/portfolio/PHOTO-SOURCES.md index 5796c58..39c3a5d 100644 --- a/public/portfolio/PHOTO-SOURCES.md +++ b/public/portfolio/PHOTO-SOURCES.md @@ -1 +1 @@ -Manyangles portfolio card uses the existing study-abroad group dinner photo at /trips/engr290/insta290.jpg, also shown on the travel page. This is illustrative portfolio imagery, not a claim that the trip used Manyangles. +The Manyangles portfolio card uses an illustrative sample gallery built with CSS. It contains no event-specific photos or personal details. diff --git a/public/portfolio/puter/01-processes-light.png b/public/portfolio/puter/01-processes-light.png new file mode 100644 index 0000000..cf8f1aa Binary files /dev/null and b/public/portfolio/puter/01-processes-light.png differ diff --git a/public/portfolio/puter/02-cpu-light.png b/public/portfolio/puter/02-cpu-light.png new file mode 100644 index 0000000..04ad90b Binary files /dev/null and b/public/portfolio/puter/02-cpu-light.png differ diff --git a/public/portfolio/puter/03-cpu-cores-light.png b/public/portfolio/puter/03-cpu-cores-light.png new file mode 100644 index 0000000..bc1b514 Binary files /dev/null and b/public/portfolio/puter/03-cpu-cores-light.png differ diff --git a/public/portfolio/puter/04-memory-light.png b/public/portfolio/puter/04-memory-light.png new file mode 100644 index 0000000..7171964 Binary files /dev/null and b/public/portfolio/puter/04-memory-light.png differ diff --git a/public/portfolio/puter/05-processes-dark.png b/public/portfolio/puter/05-processes-dark.png new file mode 100644 index 0000000..2eff700 Binary files /dev/null and b/public/portfolio/puter/05-processes-dark.png differ diff --git a/public/portfolio/puter/icon.png b/public/portfolio/puter/icon.png new file mode 100644 index 0000000..178ed11 Binary files /dev/null and b/public/portfolio/puter/icon.png differ diff --git a/public/race/fonts/OFL.txt b/public/race/fonts/OFL.txt new file mode 100644 index 0000000..330d7a2 --- /dev/null +++ b/public/race/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2009-2011 by Accademia di Belle Arti di Urbino and students of MA course of Visual design. Some rights reserved. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/race/fonts/TitilliumWeb-Black.ttf b/public/race/fonts/TitilliumWeb-Black.ttf new file mode 100644 index 0000000..577b3ae Binary files /dev/null and b/public/race/fonts/TitilliumWeb-Black.ttf differ diff --git a/public/race/fonts/TitilliumWeb-Bold.ttf b/public/race/fonts/TitilliumWeb-Bold.ttf new file mode 100644 index 0000000..811600a Binary files /dev/null and b/public/race/fonts/TitilliumWeb-Bold.ttf differ diff --git a/public/race/fonts/TitilliumWeb-Regular.ttf b/public/race/fonts/TitilliumWeb-Regular.ttf new file mode 100644 index 0000000..4fd6b7f Binary files /dev/null and b/public/race/fonts/TitilliumWeb-Regular.ttf differ diff --git a/scripts/generate-race-replay.py b/scripts/generate-race-replay.py new file mode 100644 index 0000000..5f930d8 --- /dev/null +++ b/scripts/generate-race-replay.py @@ -0,0 +1,218 @@ +"""Build the compact 2024 São Paulo replay snapshot from public race data. + +Run from the repository root: python3 scripts/generate-race-replay.py +The website reads the checked-in snapshot and makes no live API requests. +""" + +from __future__ import annotations + +import json +import math +import time +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from urllib.parse import urlencode +from urllib.request import Request, urlopen + +JOLPICA = "https://api.jolpi.ca/ergast/f1/2024/21" +OPENF1 = "https://api.openf1.org/v1" +OUTPUT = Path(__file__).resolve().parents[1] / "src/lib/race-replay-data.json" + + +def fetch(url: str): + for attempt in range(5): + try: + with urlopen(Request(url, headers={"User-Agent": "SeanOConnorRaceReplay/1.0"}), timeout=30) as response: + return json.load(response) + except Exception: + if attempt == 4: + raise + time.sleep(2 ** attempt) + + +def seconds(value: str) -> float: + total = 0.0 + for part in value.split(":"): + total = total * 60 + float(part) + return round(total, 3) + + +def jolpica(endpoint: str): + payload = fetch(f"{JOLPICA}/{endpoint}/?limit=100") + return payload["MRData"] + + +first_page = jolpica("laps") +total = int(first_page["total"]) +offsets = range(100, total, 100) +with ThreadPoolExecutor(max_workers=4) as pool: + more_pages = list(pool.map(lambda offset: fetch(f"{JOLPICA}/laps/?limit=100&offset={offset}")["MRData"], offsets)) + +all_laps = {} +for page in [first_page, *more_pages]: + for lap in page["RaceTable"]["Races"][0]["Laps"]: + all_laps.setdefault(int(lap["number"]), []).extend(lap["Timings"]) + +results = jolpica("results")["RaceTable"]["Races"][0] +pits = jolpica("pitstops")["RaceTable"]["Races"][0]["PitStops"] +openf1_drivers = fetch(f"{OPENF1}/drivers?session_key=9636") +driver_colors = {int(driver["driver_number"]): f"#{driver['team_colour']}" for driver in openf1_drivers} + +drivers = [] +for result in results["Results"]: + driver = result["Driver"] + number = int(result["number"]) + drivers.append({ + "id": driver["driverId"], + "number": number, + "code": driver.get("code", driver["familyName"][:3].upper()), + "name": f"{driver['givenName']} {driver['familyName']}", + "team": result["Constructor"]["name"], + "color": driver_colors.get(number, "#ffffff"), + "grid": int(result["grid"]), + "finish": int(result["position"]), + "status": result["status"], + }) + +laps = [] +for number, timings in sorted(all_laps.items()): + laps.append({ + "number": number, + "timings": [ + {"id": timing["driverId"], "position": int(timing["position"]), "seconds": seconds(timing["time"])} + for timing in timings + ], + }) + +pit_stops = [ + {"id": pit["driverId"], "lap": int(pit["lap"]), "seconds": seconds(pit["duration"])} + for pit in pits +] + +# A complete lap from Verstappen's on-car coordinates, rather than an +# unrelated circuit image or generated illustration. +lap_eight = next(item for item in fetch(f"{OPENF1}/laps?session_key=9636&driver_number=1") if item["lap_number"] == 8) +from datetime import datetime, timedelta + +start = datetime.fromisoformat(lap_eight["date_start"]) +end = start + timedelta(seconds=lap_eight["lap_duration"]) +params = urlencode({ + "session_key": 9636, + "driver_number": 1, + "date>": start.isoformat(timespec="milliseconds"), + "date<": end.isoformat(timespec="milliseconds"), +}) +location = fetch(f"{OPENF1}/location?{params}") +points = [(row["x"], row["y"]) for row in location if row["x"] is not None and row["y"] is not None] +if len(points) < 100: + raise RuntimeError("Not enough location samples for the circuit trace") +if math.dist(points[0], points[-1]) > 400: + raise RuntimeError("The sampled track lap does not close") +points.append(points[0]) +# Mirror the telemetry Y axis to match the FIA's 2024 circuit map: the +# start/finish straight is on the left and the back straight is on the right. +min_x, max_x = min(x for x, _ in points), max(x for x, _ in points) +min_y, max_y = min(y for _, y in points), max(y for _, y in points) +scale = min(650 / (max_x - min_x), 630 / (max_y - min_y)) +offset_x = (1000 - (max_x - min_x) * scale) / 2 +offset_y = (740 - (max_y - min_y) * scale) / 2 + + +def map_point(x: float, y: float) -> list[float]: + return [round(offset_x + (x - min_x) * scale, 1), round(offset_y + (max_y - y) * scale, 1)] + + +track = [map_point(x, y) for x, y in points] +circuit = fetch("https://api.multiviewer.app/api/v1/circuits/14/2024") +corners = [] +for corner in circuit["corners"]: + x, y = map_point(corner["trackPosition"]["x"], corner["trackPosition"]["y"]) + corners.append({"number": corner["number"], "x": x, "y": y}) + + +def nearest_track_distance(x: float, y: float) -> float: + """Project a mapped circuit marker onto the same polyline as the cars.""" + best_distance = float("inf") + best_progress = 0.0 + progress = 0.0 + for (ax, ay), (bx, by) in zip(track, track[1:]): + length_sq = (bx - ax) ** 2 + (by - ay) ** 2 + fraction = max(0.0, min(1.0, ((x - ax) * (bx - ax) + (y - ay) * (by - ay)) / length_sq)) if length_sq else 0.0 + px, py = ax + fraction * (bx - ax), ay + fraction * (by - ay) + distance = math.dist((x, y), (px, py)) + if distance < best_distance: + best_distance = distance + best_progress = progress + math.sqrt(length_sq) * fraction + progress += math.sqrt(length_sq) + return best_progress + + +track_length = sum(math.dist(a, b) for a, b in zip(track, track[1:])) +corner_distance = {corner["number"]: nearest_track_distance(corner["x"], corner["y"]) for corner in corners} + + +def point_at(distance: float) -> list[float]: + remaining = distance % track_length + for (ax, ay), (bx, by) in zip(track, track[1:]): + segment = math.dist((ax, ay), (bx, by)) + if remaining <= segment: + fraction = remaining / segment if segment else 0 + return [round(ax + (bx - ax) * fraction, 1), round(ay + (by - ay) * fraction, 1)] + remaining -= segment + return track[0] + + +def zone_path(start: float, end: float) -> list[list[float]]: + if end < start: + end += track_length + # Follow the same polyline as the animated dots, including the lap seam. + samples = max(2, math.ceil((end - start) / 3)) + return [point_at(start + (end - start) * index / samples) for index in range(samples + 1)] + + +# FIA 2024 São Paulo Circuit Map: activation 1 is 30m after T3; +# activation 2 is 160m before T15. Zones end at the next braking turn. +metres_to_track = track_length / 4309 +drs_zones = [ + {"number": 1, "track": zone_path(corner_distance[3] + 30 * metres_to_track, corner_distance[4] - 30 * metres_to_track)}, + {"number": 2, "track": zone_path(corner_distance[15] - 160 * metres_to_track, corner_distance[1] - 30 * metres_to_track)}, +] + +control = fetch(f"{OPENF1}/race_control?session_key=9636") +event_specs = [ + (1, "start", "A race before the race", "An aborted start delays lights out on a wet Interlagos grid.", "ABORTED START"), + (28, "vsc", "Virtual safety car", "A VSC changes the pit-stop calculation as the rain intensifies.", "VIRTUAL SAFETY CAR DEPLOYED"), + (30, "safety", "Safety car deployed", "The field bunches up in difficult conditions.", "SAFETY CAR DEPLOYED"), + (32, "red", "Red flag", "The race stops, and the order is reset for a rolling restart.", "RED FLAG"), + (33, "restart", "Rolling restart", "The field gets going again behind Ocon.", "ROLLING START"), + (39, "safety", "Safety car returns", "Another interruption bunches the field.", "SAFETY CAR DEPLOYED"), + (42, "restart", "Safety car in", "The race returns to green-flag running.", "SAFETY CAR IN THIS LAP"), + (69, "finish", "Chequered flag", "Verstappen wins from P17; Alpine finishes second and third.", "CHEQUERED FLAG"), +] +events = [] +for lap, kind, title, description, message in event_specs: + if not any(item.get("lap_number") == lap and message in item["message"] for item in control): + raise RuntimeError(f"Race-control event not found: lap {lap}, {message}") + events.append({"lap": lap, "kind": kind, "title": title, "description": description}) + +data = { + "race": {"title": "São Paulo Grand Prix", "year": 2024, "date": "3 November 2024", "circuit": "Autódromo José Carlos Pace", "location": "Interlagos · Brazil", "laps": 69}, + "drivers": drivers, + "laps": laps, + "pitStops": pit_stops, + "events": events, + "track": track, + "corners": corners, + "drsZones": drs_zones, + "sources": { + "laps": f"{JOLPICA}/laps/", + "results": f"{JOLPICA}/results/", + "pitStops": f"{JOLPICA}/pitstops/", + "raceControl": f"{OPENF1}/race_control?session_key=9636", + "track": f"{OPENF1}/location?session_key=9636&driver_number=1", + "corners": "https://api.multiviewer.app/api/v1/circuits/14/2024", + "circuitMap": "https://www.fia.com/sites/default/files/decision-document/2024%20S%C3%A3o%20Paulo%20Grand%20Prix%20-%20Event%20Notes%20-%20Circuit%20Map,%20Pit%20Lane%20Drawing%20and%20Red%20Zones.pdf", + }, +} +OUTPUT.write_text(json.dumps(data, ensure_ascii=False, separators=(",", ":")) + "\n") +print(f"Wrote {OUTPUT}: {len(drivers)} drivers, {len(laps)} laps, {len(track)} track points") diff --git a/src/app/page.tsx b/src/app/page.tsx index 93afa23..2b46de6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,6 +3,8 @@ import Image from "next/image"; import { JusticeParagraph } from "~/components/JusticeParagraph"; import { ManyanglesPreview } from "~/components/ManyanglesPreview"; import { BeenvoicePreview } from "~/components/BeenvoicePreview"; +import { PuterPreview } from "~/components/PuterPreview"; +import { ProjectBrand } from "~/components/ProjectBrand"; import Link from "next/link"; import { ArrowUpRight } from "lucide-react"; @@ -32,7 +34,7 @@ const work = [ { name: "Puter", category: "Native macOS app", - image: "/images/puter.jpg", + image: "/portfolio/puter/01-processes-light.png", alt: "Puter’s macOS performance dashboard", tone: "puter", href: "https://git.soconnor.dev/soconnor/Puter", @@ -44,7 +46,7 @@ const work = [ name: "Manyangles", category: "Event photo sharing", image: "/portfolio/manyangles.svg", - alt: "Manyangles event photo sharing", + alt: "Manyangles shared albums", tone: "manyangles", href: "https://ma.hadlock.tech", description: @@ -122,6 +124,8 @@ export default function HomePage() { > {project.name === "beenvoice" ? ( + ) : project.name === "Puter" ? ( + ) : project.name === "Manyangles" ? ( ) : ( @@ -133,21 +137,11 @@ export default function HomePage() { sizes="(max-width: 700px) 100vw, 50vw" /> )} - {(project.name === "Racetix" || project.name === "beenvoice") && ( -
- -
- )}

- {project.name} +

{project.category} @@ -202,7 +196,9 @@ export default function HomePage() {

Outside of software, I’m into specialty coffee, Formula One, and - travel. I cofounded Bucknell’s Coffee Society—and, naturally, made a{" "} + travel. I built a{" "} + lap-by-lap race replay + {" "}for the 2024 São Paulo Grand Prix. I also cofounded Bucknell’s Coffee Society—and, naturally, made a{" "} coffee map for Lewisburg and Boston diff --git a/src/app/race-replay/page.tsx b/src/app/race-replay/page.tsx new file mode 100644 index 0000000..2363d4c --- /dev/null +++ b/src/app/race-replay/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from "next"; +import { RaceReplay } from "~/components/RaceReplay"; +import "./race-replay.css"; + +export const metadata: Metadata = { + title: "Race Replay — São Paulo 2024", + description: "Explore the 2024 São Paulo Grand Prix lap by lap on an interactive Interlagos circuit map.", +}; + +export default function RaceReplayPage() { + return ; +} diff --git a/src/app/race-replay/race-replay.css b/src/app/race-replay/race-replay.css new file mode 100644 index 0000000..7c5ae85 --- /dev/null +++ b/src/app/race-replay/race-replay.css @@ -0,0 +1,224 @@ +@font-face { + font-family: "Race Titillium"; + src: url("/race/fonts/TitilliumWeb-Regular.ttf") format("truetype"); + font-weight: 400; + font-display: swap; +} +@font-face { + font-family: "Race Titillium"; + src: url("/race/fonts/TitilliumWeb-Bold.ttf") format("truetype"); + font-weight: 700; + font-display: swap; +} +@font-face { + font-family: "Race Titillium"; + src: url("/race/fonts/TitilliumWeb-Black.ttf") format("truetype"); + font-weight: 900; + font-display: swap; +} + +.race-site-main { width: 100%; max-width: none; padding: 0; } +.race-replay { + --race-bg: #f2f2f4; + --race-surface: #fff; + --race-ink: #15151e; + --race-muted: #62626b; + --race-line: #d8d8dc; + --race-red: #e10600; + --race-red-soft: #ffe9e8; + min-height: 100vh; + background: var(--race-bg); + color: var(--race-ink); + font-family: "Race Titillium", "Helvetica Neue", Arial, sans-serif; +} +.race-replay * { box-sizing: border-box; } +.race-replay button { font: inherit; } +.race-replay button:focus-visible, .race-replay a:focus-visible, .race-replay input:focus-visible { outline: 2px solid var(--race-red); outline-offset: 3px; } +.replay-wrap { width: min(1500px, 100%); margin: auto; padding: 40px clamp(18px, 3.4vw, 54px) 34px; } +.replay-intro { display: flex; justify-content: space-between; gap: 48px; align-items: end; min-height: 232px; margin-bottom: 34px; } +.replay-intro-copy { min-width: 0; } +.replay-eyebrow, .replay-intro-meta, .replay-panel-heading p, .replay-panel-heading .replay-track-tag, .replay-stage-top, .replay-map-legend, .replay-playback-top span, .replay-sidebar-head, .replay-driver-focus > p, .replay-focus-stats span, .replay-moments-title p, .replay-moments-title > span, .replay-moment > span:first-child, .replay-sources { font-size: 10px; font-weight: 720; letter-spacing: .15em; } +.replay-eyebrow { display: flex; align-items: center; gap: 12px; color: var(--race-red); } +.replay-live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--race-red); box-shadow: 0 0 0 4px color-mix(in srgb, var(--race-red) 14%, transparent); } +.replay-eyebrow-divider { color: var(--race-muted); font-weight: 400; } +.replay-intro h1 { margin: 18px 0 10px; font-family: "Race Titillium", "Helvetica Neue", sans-serif; font-weight: 900; font-size: clamp(58px, 7.5vw, 118px); line-height: .94; letter-spacing: -.04em; text-transform: uppercase; } +.replay-intro h1 em { color: var(--race-red); font-style: italic; } +.replay-title-period { color: var(--race-red); } +.replay-intro-copy > p { max-width: 650px; margin: 20px 0 20px; color: var(--race-muted); font-size: clamp(13px, 1.15vw, 16px); line-height: 1.6; } +.replay-intro-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 23px; color: var(--race-muted); } +.replay-intro-meta span { display: inline-flex; align-items: center; gap: 7px; } +.replay-headline-stat { flex: 0 0 254px; padding: 16px 0 7px 24px; border-left: 2px solid var(--race-red); } +.replay-headline-stat > span { color: var(--race-muted); font-size: 10px; font-weight: 760; letter-spacing: .18em; } +.replay-headline-stat strong { display: block; margin-top: 7px; font-size: clamp(44px, 4.2vw, 70px); font-weight: 850; line-height: 1; letter-spacing: -.095em; white-space: nowrap; } +.replay-headline-stat small { font-size: .45em; letter-spacing: -.03em; } +.replay-headline-stat i { color: var(--race-red); font-size: .7em; font-style: normal; } +.replay-headline-stat p { margin: 9px 0 0; color: var(--race-muted); font-size: 11px; } +.replay-dashboard { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 340px); gap: 16px; align-items: start; } +.replay-map-panel, .replay-sidebar { min-width: 0; border: 1px solid var(--race-line); background: var(--race-surface); box-shadow: 0 8px 35px #0a1e2a08; } +.replay-panel-heading { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 19px 24px 17px; } +.replay-panel-heading p, .replay-moments-title p { margin: 0 0 7px; color: var(--race-red); } +.replay-panel-heading h2 { margin: 0; font-size: clamp(16px, 1.5vw, 22px); font-weight: 700; letter-spacing: -.02em; } +.replay-track-tag { padding: 8px 9px; border: 1px solid var(--race-line); color: var(--race-muted); white-space: nowrap; } +.replay-map-stage { position: relative; height: clamp(540px, 54vw, 700px); overflow: hidden; background: radial-gradient(ellipse at 50% 48%, #252530, #15151e 64%, #0d0d14 100%); color: #f5f5f6; } +.replay-map-stage::before { content: ""; position: absolute; inset: 0; opacity: .46; background-image: linear-gradient(#75849217 1px, transparent 1px), linear-gradient(90deg, #75849217 1px, transparent 1px); background-size: 38px 38px; mask-image: linear-gradient(120deg, transparent, #000 20%, #000 80%, transparent); } +.replay-map-stage::after { content: ""; position: absolute; inset: 0; pointer-events: none; border: 1px solid #eaf3fd14; box-shadow: inset 0 0 130px #07090d60; } +.replay-stage-top { position: absolute; top: 20px; left: 24px; right: 24px; z-index: 2; display: flex; justify-content: space-between; gap: 12px; color: #8da2b5; } +.replay-circuit { position: absolute; inset: 10px 0 11px; width: 100%; height: calc(100% - 21px); overflow: visible; } +.replay-track-shadow, .replay-track-outer, .replay-drs-zone path { fill: none; stroke-linecap: round; stroke-linejoin: round; } +.replay-track-shadow { stroke: #79818a; stroke-width: 40; opacity: .25; filter: blur(12px); } +.replay-track-outer { stroke: #e0e2e6; stroke-width: 24; } +.replay-drs-zone path { stroke: #23d160; stroke-width: 24; } +.replay-drs-zone text { fill: #40e57b; font-family: "Race Titillium", Arial, sans-serif; font-size: 17px; font-weight: 900; letter-spacing: .12em; dominant-baseline: middle; paint-order: stroke; stroke: #15151e; stroke-width: 5px; } +.replay-car text { fill: #fff; font-family: "Race Titillium", Arial, sans-serif; font-size: 15px; font-weight: 900; letter-spacing: .04em; paint-order: stroke; stroke: #091017; stroke-width: 4px; } +.replay-corner circle { fill: #dfe8f0; stroke: #1d2833; stroke-width: 2; } +.replay-corner text { fill: #b3b7bf; font-family: "Race Titillium", Arial, sans-serif; font-size: 14px; font-weight: 700; letter-spacing: .06em; paint-order: stroke; stroke: #10151d; stroke-width: 4px; } +.replay-start-marker rect { fill: #fafafa; stroke: #151b20; stroke-width: 2; } +.replay-start-marker text { fill: #c1c3c8; font-family: "Race Titillium", Arial, sans-serif; font-size: 14px; font-weight: 700; letter-spacing: .07em; } +.replay-map-legend { position: absolute; bottom: 18px; left: 24px; right: 24px; z-index: 3; display: flex; align-items: center; gap: 18px; color: #94a5b4; } +.replay-map-legend span { display: inline-flex; align-items: center; gap: 7px; } +.replay-map-legend i { display: inline-block; width: 9px; height: 9px; border: 2px solid #c0cede; border-radius: 50%; } +.replay-map-legend .replay-drs-legend i { width: 14px; height: 4px; border: 0; border-radius: 0; background: #23d160; } +.replay-map-legend button { margin-left: auto; border: 1px solid #63718177; background: #121b24b8; padding: 7px 10px; color: #d0dae2; font-size: 9px; font-weight: 760; letter-spacing: .1em; cursor: pointer; } +.replay-map-legend button:hover { background: #263342; } +.replay-stage-event { position: absolute; top: 60px; right: 24px; z-index: 3; display: flex; align-items: center; gap: 9px; max-width: 220px; padding: 10px 12px; background: #121923d9; border-left: 3px solid #eb3545; box-shadow: 0 8px 20px #0005; } +.replay-stage-event span { font-size: 9px; letter-spacing: .12em; color: #a5b1bd; white-space: nowrap; } +.replay-stage-event strong { font-size: 11px; font-weight: 720; } +.replay-stage-event--vsc, .replay-stage-event--safety { border-left-color: #f5c445; } +.replay-stage-event--restart { border-left-color: #70ce9a; } +.replay-playback { padding: 20px 24px 17px; } +.replay-playback-top { display: flex; justify-content: space-between; gap: 16px; align-items: end; margin-bottom: 13px; } +.replay-playback-top span { display: block; margin-bottom: 4px; color: var(--race-muted); } +.replay-playback-top strong { font-size: 28px; line-height: 1; letter-spacing: -.06em; font-variant-numeric: tabular-nums; } +.replay-lap-readout { text-align: right; } +.replay-lap-readout strong { font-size: 32px; } +.replay-lap-readout small { color: var(--race-muted); font-size: 16px; } +.replay-transport { display: flex; align-items: center; gap: 5px; } +.replay-transport button { display: grid; place-items: center; height: 38px; flex: none; cursor: pointer; } +.replay-play-button { width: 43px; border: none; background: var(--race-red); color: #fff; } +.replay-play-button:hover { filter: brightness(1.15); } +.replay-step-button, .replay-reset-button { width: 34px; border: none; background: transparent; color: var(--race-muted); } +.replay-step-button:hover, .replay-reset-button:hover { color: var(--race-ink); background: var(--race-bg); } +.replay-scrubber { position: relative; flex: 1; min-width: 60px; height: 35px; display: flex; align-items: center; } +.replay-scrubber input { width: 100%; height: 24px; margin: 0; appearance: none; background: transparent; cursor: pointer; position: relative; z-index: 2; } +.replay-scrubber input::-webkit-slider-runnable-track { height: 4px; border-radius: 0; background: linear-gradient(90deg, var(--race-red) 0 var(--replay-progress), var(--race-line) var(--replay-progress) 100%); } +.replay-scrubber input::-moz-range-track { height: 4px; background: var(--race-line); } +.replay-scrubber input::-moz-range-progress { height: 4px; background: var(--race-red); } +.replay-scrubber input::-webkit-slider-thumb { width: 14px; height: 14px; margin-top: -5px; appearance: none; border: 3px solid var(--race-surface); border-radius: 50%; background: var(--race-red); box-shadow: 0 0 0 1px var(--race-red); } +.replay-scrubber input::-moz-range-thumb { width: 10px; height: 10px; border: 3px solid var(--race-surface); border-radius: 50%; background: var(--race-red); box-shadow: 0 0 0 1px var(--race-red); } +.replay-timeline-ticks { position: absolute; top: 26px; inset-inline: 7px; height: 6px; pointer-events: none; } +.replay-timeline-ticks span { position: absolute; top: 0; width: 3px; height: 6px; background: var(--race-red); transform: translateX(-50%); } +.replay-timeline-ticks .is-safety, .replay-timeline-ticks .is-vsc { background: #d9ab25; } +.replay-speed { display: flex; align-items: center; margin-left: 3px; border: 1px solid var(--race-line); } +.replay-speed button { width: 30px; height: 30px; border: none; background: transparent; color: var(--race-muted); font-size: 10px; font-weight: 760; } +.replay-speed button[aria-pressed="true"] { color: #fff; background: var(--race-ink); } +.replay-playback > p { margin: 12px 0 0; color: var(--race-muted); font-size: 10px; line-height: 1.5; } +.replay-sidebar-head { display: flex; justify-content: space-between; padding: 19px 18px 14px; border-bottom: 1px solid var(--race-line); color: var(--race-muted); } +.replay-sidebar-head strong { color: var(--race-red); } +.replay-leaderboard { height: 506px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--race-line) transparent; } +.replay-driver-row { display: flex; align-items: center; width: 100%; height: 45px; padding: 0 13px; border: none; border-bottom: 1px solid var(--race-line); background: transparent; color: var(--race-ink); text-align: left; cursor: pointer; } +.replay-driver-row:hover { background: var(--race-bg); } +.replay-driver-row.is-selected { background: var(--race-red-soft); } +.replay-driver-row.is-out { opacity: .48; } +.replay-row-position { width: 28px; color: var(--race-muted); font-size: 13px; font-weight: 850; font-variant-numeric: tabular-nums; } +.replay-row-color { width: 4px; height: 24px; margin-right: 10px; } +.replay-row-identity { display: flex; flex-direction: column; min-width: 0; flex: 1; gap: 0; } +.replay-row-identity strong { font-size: 13px; font-weight: 830; letter-spacing: .015em; } +.replay-row-identity small { overflow: hidden; color: var(--race-muted); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; } +.replay-row-change { color: var(--race-muted); font-size: 11px; font-weight: 780; font-variant-numeric: tabular-nums; } +.replay-row-change.is-up { color: #18895f; } +.replay-row-change.is-down { color: var(--race-red); } +.replay-driver-focus { padding: 18px; border-top: 2px solid var(--race-ink); } +.replay-driver-focus > p { display: flex; justify-content: space-between; margin: 0 0 12px; color: var(--race-muted); } +.replay-focus-head { display: flex; align-items: start; justify-content: space-between; gap: 8px; } +.replay-focus-head h3 { margin: 0; font-size: 20px; font-weight: 700; letter-spacing: -.02em; } +.replay-focus-head span:not(.replay-focus-position) { color: var(--race-muted); font-size: 11px; } +.replay-focus-position { font-size: 35px; font-weight: 900; letter-spacing: -.07em; line-height: 1; } +.replay-focus-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 7px; margin-top: 17px; } +.replay-focus-stats div { padding: 8px 0; border-top: 1px solid var(--race-line); } +.replay-focus-stats span { display: block; margin-bottom: 4px; color: var(--race-muted); font-size: 8px; } +.replay-focus-stats strong { font-size: 14px; font-variant-numeric: tabular-nums; } +.replay-chart { position: relative; margin-top: 9px; padding-left: 23px; } +.replay-chart svg { display: block; width: 100%; height: 74px; overflow: visible; } +.replay-chart line { stroke: var(--race-line); stroke-width: 1; stroke-dasharray: 3 4; } +.replay-chart-axis { position: absolute; top: 0; bottom: 16px; left: 0; display: flex; flex-direction: column; justify-content: space-between; color: var(--race-muted); font-size: 9px; } +.replay-chart-bottom { display: flex; justify-content: space-between; margin-top: 4px; color: var(--race-muted); font-size: 8px; font-weight: 750; letter-spacing: .12em; } +.replay-no-laps { margin: 16px 0 0; color: var(--race-muted); font-size: 11px; } +.replay-moments { margin-top: 34px; } +.replay-moments-title { display: flex; align-items: end; justify-content: space-between; gap: 15px; margin-bottom: 15px; } +.replay-moments-title h2 { margin: 0; font-size: clamp(26px, 2.6vw, 38px); font-weight: 700; letter-spacing: -.02em; } +.replay-moments-title > span { color: var(--race-muted); white-space: nowrap; } +.replay-moments-track { display: grid; grid-auto-columns: minmax(215px, 1fr); grid-auto-flow: column; gap: 10px; overflow-x: auto; padding-bottom: 10px; scrollbar-width: thin; } +.replay-moment { position: relative; min-height: 148px; padding: 15px 35px 15px 16px; border: 1px solid var(--race-line); border-top: 3px solid var(--race-line); background: var(--race-surface); color: var(--race-ink); text-align: left; cursor: pointer; } +.replay-moment:hover, .replay-moment.is-active { border-top-color: var(--race-red); background: var(--race-red-soft); } +.replay-moment > span:first-child { display: flex; align-items: center; gap: 8px; color: var(--race-red); } +.replay-moment i { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: currentColor; } +.replay-moment i.is-safety, .replay-moment i.is-vsc { color: #dbac1b; } +.replay-moment strong { display: block; margin-top: 12px; font-size: 15px; letter-spacing: -.035em; } +.replay-moment p { margin: 7px 0 0; color: var(--race-muted); font-size: 10px; line-height: 1.45; } +.replay-moment-arrow { position: absolute; right: 13px; bottom: 13px; color: var(--race-red); font-size: 15px; } +.replay-sources { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 14px; margin-top: 24px; padding-top: 18px; border-top: 1px solid var(--race-line); color: var(--race-muted); font-size: 9px; } +.replay-sources div { display: flex; align-items: center; gap: 12px; } +.replay-sources a { display: inline-flex; align-items: center; gap: 2px; color: inherit; text-decoration: none; } +.replay-sources a:hover { color: var(--race-red); } + +@media (prefers-color-scheme: dark) { + .race-replay { --race-bg: #0d0d14; --race-surface: #15151e; --race-ink: #f2f2f4; --race-muted: #a6a6ae; --race-line: #34343e; --race-red: #ff322b; --race-red-soft: #371c20; } + .replay-map-panel, .replay-sidebar { box-shadow: none; } + .replay-row-change.is-up { color: #78d7a8; } + .replay-speed button[aria-pressed="true"] { background: #e5edf2; color: #111820; } +} + +@media (max-width: 1050px) { + .replay-dashboard { grid-template-columns: minmax(0, 1fr) 300px; } + .replay-headline-stat { flex-basis: 210px; } + .replay-track-tag { display: none; } + .replay-map-stage { height: 640px; } +} +@media (max-width: 800px) { + .replay-wrap { padding-top: 28px; } + .replay-intro { align-items: start; gap: 14px; margin-bottom: 24px; } + .replay-headline-stat { flex-basis: 160px; padding-left: 13px; } + .replay-headline-stat strong { font-size: 40px; } + .replay-headline-stat p { font-size: 9px; } + .replay-dashboard { grid-template-columns: 1fr; } + .replay-map-stage { height: min(85vw, 650px); min-height: 500px; } + .replay-sidebar { display: grid; grid-template-columns: minmax(0, 1fr) minmax(260px, .85fr); } + .replay-sidebar-head { grid-column: 1 / -1; } + .replay-leaderboard { height: 510px; } + .replay-driver-focus { border-top: 0; border-left: 1px solid var(--race-line); } +} +@media (max-width: 580px) { + .replay-wrap { padding: 22px 12px 30px; } + .replay-intro { display: block; padding-inline: 5px; } + .replay-intro h1 { font-size: clamp(51px, 12vw, 70px); } + .replay-intro-copy > p { margin: 15px 0; font-size: 12px; } + .replay-intro-meta { gap: 8px 15px; font-size: 9px; } + .replay-headline-stat { display: none; } + .replay-panel-heading { padding: 16px; } + .replay-map-stage { height: 500px; min-height: 0; } + .replay-circuit { inset: 22px 0 5px; height: calc(100% - 27px); } + .replay-stage-top { top: 14px; left: 13px; right: 13px; font-size: 8px; } + .replay-stage-top span:last-child { display: none; } + .replay-stage-event { top: 38px; right: 13px; padding: 7px; max-width: 130px; flex-direction: column; align-items: start; gap: 2px; } + .replay-stage-event strong { font-size: 9px; } + .replay-map-legend { left: 13px; right: 13px; bottom: 10px; gap: 6px; font-size: 8px; } + .replay-map-legend span:nth-child(2) { display: none; } + .replay-map-legend button { font-size: 8px; } + .replay-playback { padding: 15px 12px; } + .replay-transport { gap: 0; } + .replay-play-button { width: 38px; } + .replay-step-button { width: 27px; } + .replay-reset-button { display: none !important; } + .replay-speed { margin-left: 5px; } + .replay-speed button { width: 25px; } + .replay-sidebar { display: block; } + .replay-leaderboard { height: 368px; } + .replay-driver-focus { border-left: 0; border-top: 2px solid var(--race-ink); } + .replay-moments-title { align-items: start; } + .replay-moments-title > span { display: none; } + .replay-moments-track { grid-auto-columns: minmax(215px, 72%); } + .replay-sources { line-height: 1.7; } +} +@media (prefers-reduced-motion: reduce) { + .replay-car { transition: none; } +} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index a8d5f69..8607b4c 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -33,6 +33,7 @@ export function Footer() { Research In the press Coffee map + Race Replay Trips & interests October Résumé & CV diff --git a/src/components/ManyanglesPreview.tsx b/src/components/ManyanglesPreview.tsx index 196253c..5402e4b 100644 --- a/src/components/ManyanglesPreview.tsx +++ b/src/components/ManyanglesPreview.tsx @@ -1,39 +1,34 @@ -import Image from "next/image"; -import localFont from "next/font/local"; - -const geologica = localFont({ - src: "../../public/portfolio/fonts/geologica.woff2", - display: "swap", - weight: "100 900", -}); +import { ArrowUpRight, ImagePlus, Images } from "lucide-react"; export function ManyanglesPreview() { return ( -

- Sean’s study-abroad group gathered for dinner in Paris -
- - Manyangles +
+
); diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index 6b06665..9b997a5 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { useState } from "react"; import { createPortal } from "react-dom"; -import { ChevronDown, Menu, X, Home, Book, Newspaper, FolderGit2, BookOpenText, Briefcase, Plane, FileText } from "lucide-react"; +import { ChevronDown, Menu, X, Home, Book, Newspaper, FolderGit2, BookOpenText, Briefcase, Plane, FileText, FlagTriangleRight } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { Button } from "~/components/ui/button"; import { @@ -35,6 +35,7 @@ const mobilePages = [ { href: "/publications", label: "Publications", icon: BookOpenText }, { href: "/experience", label: "Experience", icon: Briefcase }, { href: "/travel", label: "Travel", icon: Plane }, + { href: "/race-replay", label: "Race Replay", icon: FlagTriangleRight }, { href: "/cv", label: "Résumé & CV", icon: FileText }, ]; diff --git a/src/components/ProjectBrand.tsx b/src/components/ProjectBrand.tsx new file mode 100644 index 0000000..e048eca --- /dev/null +++ b/src/components/ProjectBrand.tsx @@ -0,0 +1,47 @@ +import Image from "next/image"; +import localFont from "next/font/local"; + +const geologica = localFont({ + src: "../../public/portfolio/fonts/geologica.woff2", + display: "swap", + weight: "100 900", +}); + +export function ProjectBrand({ name }: { name: string }) { + if (name === "Racetix" || name === "beenvoice") { + return ( + + {name} + + ); + } + + if (name === "Puter") { + return ( + + + Puter + + ); + } + + if (name === "Manyangles") { + return ( + + + Manyangles + + ); + } + + return {name}; +} diff --git a/src/components/ProjectExplorer.tsx b/src/components/ProjectExplorer.tsx index fb5e409..fdaafde 100644 --- a/src/components/ProjectExplorer.tsx +++ b/src/components/ProjectExplorer.tsx @@ -6,6 +6,8 @@ import Link from "next/link"; import { ArrowUpRight, Search } from "lucide-react"; import { BeenvoicePreview } from "~/components/BeenvoicePreview"; import { ManyanglesPreview } from "~/components/ManyanglesPreview"; +import { PuterPreview } from "~/components/PuterPreview"; +import { ProjectBrand } from "~/components/ProjectBrand"; import { JusticeParagraph } from "~/components/JusticeParagraph"; import type { Project } from "~/lib/data"; @@ -32,6 +34,8 @@ function ProjectCard({ {project.image ? ( project.title === "beenvoice" ? ( + ) : project.title === "Puter" ? ( + ) : project.title === "Manyangles" ? ( ) : ( @@ -46,20 +50,10 @@ function ProjectCard({ ) : ( )} - {(project.title === "Racetix" || project.title === "beenvoice") && ( -
- -
- )}
-

{project.title}

+

{project.description}

diff --git a/src/components/PuterPreview.tsx b/src/components/PuterPreview.tsx new file mode 100644 index 0000000..9448fab --- /dev/null +++ b/src/components/PuterPreview.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { useState } from "react"; +import Image from "next/image"; + +const screenshots = [ + { src: "/portfolio/puter/01-processes-light.png", label: "Processes", alt: "Puter showing grouped processes and system activity in light mode" }, + { src: "/portfolio/puter/02-cpu-light.png", label: "CPU", alt: "Puter CPU performance summary with a live utilization graph" }, + { src: "/portfolio/puter/03-cpu-cores-light.png", label: "Cores", alt: "Puter showing individual CPU core utilization graphs" }, + { src: "/portfolio/puter/04-memory-light.png", label: "Memory", alt: "Puter memory performance and pressure overview" }, + { src: "/portfolio/puter/05-processes-dark.png", label: "Dark mode", alt: "Puter grouped process view in dark mode" }, +] as const; + +export function PuterPreview({ interactive = false, priority = false }: { interactive?: boolean; priority?: boolean }) { + const [active, setActive] = useState(0); + const screenshot = screenshots[active]!; + + return ( +
+ {screenshot.alt} + {interactive && ( +
+ {screenshots.map((item, index) => ( + + ))} +
+ )} +
+ ); +} diff --git a/src/components/RaceReplay.tsx b/src/components/RaceReplay.tsx new file mode 100644 index 0000000..4cb764a --- /dev/null +++ b/src/components/RaceReplay.tsx @@ -0,0 +1,265 @@ +"use client"; + +import { useEffect, useMemo, useRef, useState } from "react"; +import { ArrowUpRight, ChevronLeft, ChevronRight, CloudRain, FlagTriangleRight, Pause, Play, RotateCcw } from "lucide-react"; +import rawData from "~/lib/race-replay-data.json"; + +type Driver = { + id: string; + number: number; + code: string; + name: string; + team: string; + color: string; + grid: number; + finish: number; + status: string; +}; +type Timing = { id: string; position: number; seconds: number }; +type RaceData = { + race: { title: string; year: number; date: string; circuit: string; location: string; laps: number }; + drivers: Driver[]; + laps: { number: number; timings: Timing[] }[]; + pitStops: { id: string; lap: number; seconds: number }[]; + events: { lap: number; kind: string; title: string; description: string }[]; + track: [number, number][]; + corners: { number: number; x: number; y: number }[]; + drsZones: { number: number; track: [number, number][] }[]; + sources: Record; +}; + +const data = rawData as unknown as RaceData; +const driverById = new Map(data.drivers.map((driver) => [driver.id, driver])); +const trackSegments = data.track.slice(1).map((point, index) => Math.hypot(point[0] - data.track[index]![0], point[1] - data.track[index]![1])); +const trackDistances = [0]; +for (const segment of trackSegments) trackDistances.push(trackDistances.at(-1)! + segment); +const trackLength = trackDistances.at(-1)!; +const trackPath = data.track.map(([x, y], index) => `${index ? "L" : "M"}${x} ${y}`).join(" "); +const drsPaths = data.drsZones.map((zone) => ({ + number: zone.number, + path: zone.track.map(([x, y], index) => `${index ? "L" : "M"}${x} ${y}`).join(" "), + middle: zone.track[Math.floor(zone.track.length / 2)]!, +})); + +const elapsedByDriver = new Map(); +for (const driver of data.drivers) elapsedByDriver.set(driver.id, [0]); +for (const lap of data.laps) { + for (const timing of lap.timings) { + const elapsed = elapsedByDriver.get(timing.id)!; + elapsed.push(Math.round((elapsed.at(-1)! + timing.seconds) * 1000) / 1000); + } +} +const leaderCrossings = [0]; +for (let lap = 1; lap <= data.race.laps; lap++) { + leaderCrossings.push(Math.min(...[...elapsedByDriver.values()].map((times) => times[lap] ?? Infinity))); +} + +function pointOnTrack(progress: number): [number, number] { + const distance = (((progress % 1) + 1) % 1) * trackLength; + let low = 0; + let high = trackSegments.length - 1; + while (low < high) { + const middle = (low + high) >> 1; + if (trackDistances[middle + 1]! < distance) low = middle + 1; + else high = middle; + } + const fraction = (distance - trackDistances[low]!) / (trackSegments[low] ?? 1); + const start = data.track[low]!; + const end = data.track[low + 1]!; + return [start[0] + (end[0] - start[0]) * fraction, start[1] + (end[1] - start[1]) * fraction]; +} + +function formatTime(seconds: number) { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); + return `${hours ? `${hours}:` : ""}${String(minutes).padStart(hours ? 2 : 1, "0")}:${String(secs).padStart(2, "0")}`; +} + +function formatLapTime(seconds: number) { + return `${Math.floor(seconds / 60)}:${(seconds % 60).toFixed(3).padStart(6, "0")}`; +} + +function inactiveLabel(driver: Driver) { + if (driver.status === "Did not start") return "DNS"; + if (driver.status === "Disqualified") return "DSQ"; + return "OUT"; +} + +function positionAtLap(driverId: string, lap: number) { + if (lap === 0) { + const driver = driverById.get(driverId); + return driver?.status === "Did not start" ? null : driver?.grid ?? null; + } + return data.laps[lap - 1]?.timings.find((timing) => timing.id === driverId)?.position ?? null; +} + +function carProgress(driverId: string, seconds: number, grid: number) { + const times = elapsedByDriver.get(driverId)!; + if (seconds > times.at(-1)! + 2) return null; + let low = 0; + let high = times.length - 1; + while (low < high) { + const middle = Math.ceil((low + high) / 2); + if (times[middle]! <= seconds) low = middle; + else high = middle - 1; + } + if (low >= times.length - 1) return seconds >= times.at(-1)! ? 0 : null; + const fraction = (seconds - times[low]!) / (times[low + 1]! - times[low]!); + const gridOffset = Math.max(0, 1 - seconds / 12) * grid * 0.008; + return fraction - gridOffset; +} + +function PositionChart({ driver }: { driver: Driver }) { + const positions = [driver.grid, ...data.laps.map((lap) => lap.timings.find((timing) => timing.id === driver.id)?.position).filter((position): position is number => position !== undefined)]; + if (positions.length === 1) return

No race laps recorded.

; + const points = positions.map((position, index) => `${(index / Math.max(positions.length - 1, 1)) * 284 + 8},${((position - 1) / 19) * 73 + 8}`).join(" "); + return
+
P1P20
+ +
STARTFINISH
+
; +} + +export function RaceReplay() { + const [progress, setProgress] = useState(0); + const [playing, setPlaying] = useState(false); + const [speed, setSpeed] = useState(1); + const [selectedId, setSelectedId] = useState("max_verstappen"); + const [showAllCars, setShowAllCars] = useState(true); + const progressRef = useRef(0); + + useEffect(() => { + if (!playing) return; + let frame = 0; + let last = 0; + function tick(now: number) { + if (last && now - last >= 35) { + const elapsed = Math.min(now - last, 100); + progressRef.current = Math.min(data.race.laps, progressRef.current + elapsed / 1000 * 0.8 * speed); + setProgress(progressRef.current); + if (progressRef.current >= data.race.laps) setPlaying(false); + last = now; + } else if (!last) last = now; + frame = requestAnimationFrame(tick); + } + frame = requestAnimationFrame(tick); + return () => cancelAnimationFrame(frame); + }, [playing, speed]); + + const completedLap = Math.min(data.race.laps, Math.floor(progress)); + const displayLap = Math.min(data.race.laps, completedLap + (progress < data.race.laps ? 1 : 0)); + const lapFraction = progress - completedLap; + const elapsed = completedLap === data.race.laps + ? leaderCrossings.at(-1)! + : leaderCrossings[completedLap]! + (leaderCrossings[completedLap + 1]! - leaderCrossings[completedLap]!) * lapFraction; + const selected = driverById.get(selectedId)!; + const selectedStatus = inactiveLabel(selected); + const selectedPosition = positionAtLap(selectedId, completedLap); + const activeEvent = [...data.events].reverse().find((event) => event.lap <= displayLap); + const lapTimings = completedLap ? data.laps[completedLap - 1]!.timings : []; + const timingById = new Map(lapTimings.map((timing) => [timing.id, timing])); + const orderedDrivers = [...data.drivers].sort((a, b) => { + if (!completedLap && a.status === "Did not start") return b.status === "Did not start" ? a.grid - b.grid : 1; + if (!completedLap && b.status === "Did not start") return -1; + const aPosition = timingById.get(a.id)?.position; + const bPosition = timingById.get(b.id)?.position; + if (aPosition !== undefined && bPosition !== undefined) return aPosition - bPosition; + if (aPosition !== undefined) return -1; + if (bPosition !== undefined) return 1; + return completedLap ? a.finish - b.finish : a.grid - b.grid; + }); + const visibleCars = useMemo(() => data.drivers.filter((driver) => driver.status !== "Did not start" && (showAllCars || driver.id === selectedId)), [showAllCars, selectedId]); + const selectedPits = data.pitStops.filter((pit) => pit.id === selectedId); + const validLaps = data.laps.flatMap((lap) => lap.timings.filter((timing) => timing.id === selectedId && timing.seconds < 200).map((timing) => timing.seconds)); + const bestLap = validLaps.length ? Math.min(...validLaps) : null; + + function jumpTo(lap: number) { + setPlaying(false); + progressRef.current = Math.max(0, Math.min(data.race.laps, lap)); + setProgress(progressRef.current); + } + + return
+
+
+
+
RACE REPLAY / 2024 ARCHIVE / ROUND 21
+

São Paulo ’24.

+

One very wet Sunday at Interlagos. Scrub the race, follow a driver, and watch the order change lap by lap.

+
INTERLAGOS, BRAZIL WET RACE03 NOV 2024
+
+
THE COMEBACKP17 P1

Max Verstappen · Red Bull Racing

+
+ +
+
+

01 / CIRCUIT VIEW

Autódromo José Carlos Pace

4.309 KM · ANTI-CLOCKWISE
+
+
INTERLAGOS / SÃO PAULO24° 42′ S   46° 41′ W
+ + + + + {drsPaths.map((zone) => + + DRS {zone.number} + )} + {data.corners.filter((corner) => [1, 3, 4, 6, 8, 12, 13, 15].includes(corner.number)).map((corner) => T{corner.number})} + {visibleCars.map((driver) => { + const onTrack = carProgress(driver.id, elapsed, driver.grid); + if (onTrack === null) return null; + const [x, y] = pointOnTrack(onTrack); + const focused = driver.id === selectedId; + return + {focused && } + + {focused && {driver.code}} + ; + })} + START / FINISH + +
SELECTED DRIVER DRS ZONES · DISABLED IN WET RACE
+ {activeEvent &&
LAP {String(activeEvent.lap).padStart(2, "0")}{activeEvent.title}
} +
+
+
RACE CLOCK{formatTime(elapsed)}
CURRENT LAP{String(displayLap).padStart(2, "0")} / {data.race.laps}
+
+ + +
{ setPlaying(false); progressRef.current = Number(event.target.value); setProgress(progressRef.current); }} aria-label="Replay lap" style={{ "--replay-progress": `${progress / data.race.laps * 100}%` } as React.CSSProperties} />
+ + +
{[1, 2, 4].map((option) => )}
+
+

Cars are interpolated from recorded lap times on an OpenF1 circuit trace. Order updates at each timing line; dots are not live GPS.

+
+
+ + +
+ +

03 / RACE CONTROL

The moments that moved it.

SELECT A MOMENT TO JUMP
{data.events.map((event) => )}
+
RACE REPLAY / AN INDEPENDENT EXPERIMENT BY SEAN O’CONNOR
+
+
; +} diff --git a/src/components/SiteFrame.tsx b/src/components/SiteFrame.tsx index e5556a7..9455001 100644 --- a/src/components/SiteFrame.tsx +++ b/src/components/SiteFrame.tsx @@ -8,7 +8,7 @@ export function SiteFrame({ children }: React.PropsWithChildren) { <>
{children}
diff --git a/src/lib/data.tsx b/src/lib/data.tsx index 717473d..0fcddcc 100644 --- a/src/lib/data.tsx +++ b/src/lib/data.tsx @@ -482,7 +482,7 @@ export const projects: Project[] = [ tags: ["Next.js", "TypeScript", "S3", "Image processing"], websiteLink: "https://ma.hadlock.tech", image: "/portfolio/manyangles.svg", - imageAlt: "Manyangles event photo sharing", + imageAlt: "Manyangles shared albums", featured: true, }, { @@ -521,9 +521,9 @@ export const projects: Project[] = [ "Hardware Telemetry", ], gitLink: "https://git.soconnor.dev/soconnor/Puter", - image: "/images/puter.jpg", + image: "/portfolio/puter/01-processes-light.png", imageAlt: - "Puter's macOS performance dashboard showing CPU, memory, and disk utilization", + "Puter's macOS process manager with live CPU, memory, disk, and network activity", featured: true, }, { diff --git a/src/lib/race-replay-data.json b/src/lib/race-replay-data.json new file mode 100644 index 0000000..a699e54 --- /dev/null +++ b/src/lib/race-replay-data.json @@ -0,0 +1 @@ +{"race":{"title":"São Paulo Grand Prix","year":2024,"date":"3 November 2024","circuit":"Autódromo José Carlos Pace","location":"Interlagos · Brazil","laps":69},"drivers":[{"id":"max_verstappen","number":1,"code":"VER","name":"Max Verstappen","team":"Red Bull","color":"#3671C6","grid":17,"finish":1,"status":"Finished"},{"id":"ocon","number":31,"code":"OCO","name":"Esteban Ocon","team":"Alpine F1 Team","color":"#0093cc","grid":4,"finish":2,"status":"Finished"},{"id":"gasly","number":10,"code":"GAS","name":"Pierre Gasly","team":"Alpine F1 Team","color":"#0093cc","grid":13,"finish":3,"status":"Finished"},{"id":"russell","number":63,"code":"RUS","name":"George Russell","team":"Mercedes","color":"#27F4D2","grid":2,"finish":4,"status":"Finished"},{"id":"leclerc","number":16,"code":"LEC","name":"Charles Leclerc","team":"Ferrari","color":"#E80020","grid":6,"finish":5,"status":"Finished"},{"id":"norris","number":4,"code":"NOR","name":"Lando Norris","team":"McLaren","color":"#FF8000","grid":1,"finish":6,"status":"Finished"},{"id":"tsunoda","number":22,"code":"TSU","name":"Yuki Tsunoda","team":"RB F1 Team","color":"#6692FF","grid":3,"finish":7,"status":"Finished"},{"id":"piastri","number":81,"code":"PIA","name":"Oscar Piastri","team":"McLaren","color":"#FF8000","grid":8,"finish":8,"status":"Finished"},{"id":"lawson","number":30,"code":"LAW","name":"Liam Lawson","team":"RB F1 Team","color":"#6692FF","grid":5,"finish":9,"status":"Finished"},{"id":"hamilton","number":44,"code":"HAM","name":"Lewis Hamilton","team":"Mercedes","color":"#27F4D2","grid":14,"finish":10,"status":"Finished"},{"id":"perez","number":11,"code":"PER","name":"Sergio Pérez","team":"Red Bull","color":"#3671C6","grid":12,"finish":11,"status":"Finished"},{"id":"bearman","number":50,"code":"BEA","name":"Oliver Bearman","team":"Haas F1 Team","color":"#B6BABD","grid":15,"finish":12,"status":"Finished"},{"id":"bottas","number":77,"code":"BOT","name":"Valtteri Bottas","team":"Sauber","color":"#52e252","grid":11,"finish":13,"status":"Finished"},{"id":"alonso","number":14,"code":"ALO","name":"Fernando Alonso","team":"Aston Martin","color":"#229971","grid":9,"finish":14,"status":"Finished"},{"id":"zhou","number":24,"code":"ZHO","name":"Guanyu Zhou","team":"Sauber","color":"#52e252","grid":19,"finish":15,"status":"Finished"},{"id":"sainz","number":55,"code":"SAI","name":"Carlos Sainz","team":"Ferrari","color":"#E80020","grid":20,"finish":16,"status":"Retired"},{"id":"colapinto","number":43,"code":"COL","name":"Franco Colapinto","team":"Williams","color":"#64C4FF","grid":16,"finish":17,"status":"Retired"},{"id":"albon","number":23,"code":"ALB","name":"Alexander Albon","team":"Williams","color":"#64C4FF","grid":7,"finish":18,"status":"Did not start"},{"id":"stroll","number":18,"code":"STR","name":"Lance Stroll","team":"Aston Martin","color":"#229971","grid":10,"finish":19,"status":"Did not start"},{"id":"hulkenberg","number":27,"code":"HUL","name":"Nico Hülkenberg","team":"Haas F1 Team","color":"#B6BABD","grid":18,"finish":20,"status":"Disqualified"}],"laps":[{"number":1,"timings":[{"id":"russell","position":1,"seconds":90.516},{"id":"norris","position":2,"seconds":91.803},{"id":"tsunoda","position":3,"seconds":93.749},{"id":"ocon","position":4,"seconds":94.467},{"id":"leclerc","position":5,"seconds":94.968},{"id":"lawson","position":6,"seconds":95.992},{"id":"piastri","position":7,"seconds":96.644},{"id":"alonso","position":8,"seconds":97.318},{"id":"gasly","position":9,"seconds":98.052},{"id":"hamilton","position":10,"seconds":98.874},{"id":"max_verstappen","position":11,"seconds":99.161},{"id":"colapinto","position":12,"seconds":99.943},{"id":"bearman","position":13,"seconds":100.491},{"id":"hulkenberg","position":14,"seconds":101.582},{"id":"bottas","position":15,"seconds":101.65},{"id":"zhou","position":16,"seconds":102.75},{"id":"sainz","position":17,"seconds":103.434},{"id":"perez","position":18,"seconds":109.387}]},{"number":2,"timings":[{"id":"russell","position":1,"seconds":85.804},{"id":"norris","position":2,"seconds":86.207},{"id":"tsunoda","position":3,"seconds":86.351},{"id":"ocon","position":4,"seconds":86.376},{"id":"leclerc","position":5,"seconds":86.521},{"id":"lawson","position":6,"seconds":86.807},{"id":"piastri","position":7,"seconds":87.194},{"id":"alonso","position":8,"seconds":87.116},{"id":"gasly","position":9,"seconds":87.33},{"id":"max_verstappen","position":10,"seconds":87.134},{"id":"hamilton","position":11,"seconds":87.832},{"id":"colapinto","position":12,"seconds":87.983},{"id":"bearman","position":13,"seconds":88.376},{"id":"hulkenberg","position":14,"seconds":87.576},{"id":"bottas","position":15,"seconds":88.923},{"id":"zhou","position":16,"seconds":89.022},{"id":"sainz","position":17,"seconds":89.021},{"id":"perez","position":18,"seconds":87.047}]},{"number":3,"timings":[{"id":"russell","position":1,"seconds":85.598},{"id":"norris","position":2,"seconds":85.402},{"id":"tsunoda","position":3,"seconds":86.018},{"id":"ocon","position":4,"seconds":86.338},{"id":"leclerc","position":5,"seconds":86.205},{"id":"lawson","position":6,"seconds":86.348},{"id":"piastri","position":7,"seconds":86.233},{"id":"alonso","position":8,"seconds":86.966},{"id":"gasly","position":9,"seconds":86.668},{"id":"max_verstappen","position":10,"seconds":86.24},{"id":"hamilton","position":11,"seconds":86.853},{"id":"colapinto","position":12,"seconds":87.14},{"id":"bearman","position":13,"seconds":86.798},{"id":"hulkenberg","position":14,"seconds":87.18},{"id":"bottas","position":15,"seconds":87.735},{"id":"zhou","position":16,"seconds":87.514},{"id":"sainz","position":17,"seconds":86.884},{"id":"perez","position":18,"seconds":85.891}]},{"number":4,"timings":[{"id":"russell","position":1,"seconds":85.642},{"id":"norris","position":2,"seconds":85.165},{"id":"tsunoda","position":3,"seconds":85.425},{"id":"ocon","position":4,"seconds":85.703},{"id":"leclerc","position":5,"seconds":86.165},{"id":"lawson","position":6,"seconds":86.217},{"id":"piastri","position":7,"seconds":85.913},{"id":"alonso","position":8,"seconds":86.72},{"id":"gasly","position":9,"seconds":86.864},{"id":"max_verstappen","position":10,"seconds":86.702},{"id":"hamilton","position":11,"seconds":86.659},{"id":"colapinto","position":12,"seconds":87.685},{"id":"hulkenberg","position":13,"seconds":87.959},{"id":"bottas","position":14,"seconds":87.071},{"id":"sainz","position":15,"seconds":86.54},{"id":"zhou","position":16,"seconds":88.172},{"id":"bearman","position":17,"seconds":92.155},{"id":"perez","position":18,"seconds":86.274}]},{"number":5,"timings":[{"id":"russell","position":1,"seconds":85.317},{"id":"norris","position":2,"seconds":85.012},{"id":"tsunoda","position":3,"seconds":85.429},{"id":"ocon","position":4,"seconds":85.417},{"id":"leclerc","position":5,"seconds":85.018},{"id":"lawson","position":6,"seconds":85.366},{"id":"piastri","position":7,"seconds":85.547},{"id":"alonso","position":8,"seconds":86.044},{"id":"max_verstappen","position":9,"seconds":85.394},{"id":"gasly","position":10,"seconds":86.804},{"id":"hamilton","position":11,"seconds":85.992},{"id":"colapinto","position":12,"seconds":86.684},{"id":"hulkenberg","position":13,"seconds":85.532},{"id":"bottas","position":14,"seconds":86.53},{"id":"sainz","position":15,"seconds":86.185},{"id":"bearman","position":16,"seconds":86.485},{"id":"perez","position":17,"seconds":86.062},{"id":"zhou","position":18,"seconds":89.677}]},{"number":6,"timings":[{"id":"russell","position":1,"seconds":84.675},{"id":"norris","position":2,"seconds":84.922},{"id":"tsunoda","position":3,"seconds":84.903},{"id":"ocon","position":4,"seconds":85.011},{"id":"leclerc","position":5,"seconds":85.087},{"id":"lawson","position":6,"seconds":85.072},{"id":"piastri","position":7,"seconds":85.2},{"id":"max_verstappen","position":8,"seconds":84.311},{"id":"alonso","position":9,"seconds":86.445},{"id":"gasly","position":10,"seconds":85.604},{"id":"hamilton","position":11,"seconds":86.101},{"id":"hulkenberg","position":12,"seconds":85.155},{"id":"colapinto","position":13,"seconds":87.143},{"id":"bottas","position":14,"seconds":86.143},{"id":"sainz","position":15,"seconds":86.915},{"id":"bearman","position":16,"seconds":85.728},{"id":"perez","position":17,"seconds":86.304},{"id":"zhou","position":18,"seconds":86.557}]},{"number":7,"timings":[{"id":"russell","position":1,"seconds":84.854},{"id":"norris","position":2,"seconds":85.001},{"id":"tsunoda","position":3,"seconds":85.024},{"id":"ocon","position":4,"seconds":85.38},{"id":"leclerc","position":5,"seconds":85.353},{"id":"lawson","position":6,"seconds":85.411},{"id":"piastri","position":7,"seconds":85.367},{"id":"max_verstappen","position":8,"seconds":84.342},{"id":"alonso","position":9,"seconds":85.264},{"id":"gasly","position":10,"seconds":85.323},{"id":"hamilton","position":11,"seconds":85.254},{"id":"hulkenberg","position":12,"seconds":85.526},{"id":"colapinto","position":13,"seconds":85.837},{"id":"bottas","position":14,"seconds":85.823},{"id":"sainz","position":15,"seconds":85.419},{"id":"bearman","position":16,"seconds":85.412},{"id":"perez","position":17,"seconds":85.83},{"id":"zhou","position":18,"seconds":86.941}]},{"number":8,"timings":[{"id":"russell","position":1,"seconds":84.588},{"id":"norris","position":2,"seconds":84.454},{"id":"tsunoda","position":3,"seconds":85.28},{"id":"ocon","position":4,"seconds":84.954},{"id":"leclerc","position":5,"seconds":85.076},{"id":"lawson","position":6,"seconds":85.237},{"id":"piastri","position":7,"seconds":85.322},{"id":"max_verstappen","position":8,"seconds":84.657},{"id":"alonso","position":9,"seconds":84.707},{"id":"gasly","position":10,"seconds":85.227},{"id":"hamilton","position":11,"seconds":85.153},{"id":"hulkenberg","position":12,"seconds":84.701},{"id":"colapinto","position":13,"seconds":85.762},{"id":"bottas","position":14,"seconds":85.484},{"id":"sainz","position":15,"seconds":85.454},{"id":"bearman","position":16,"seconds":85.421},{"id":"perez","position":17,"seconds":85.102},{"id":"zhou","position":18,"seconds":86.164}]},{"number":9,"timings":[{"id":"russell","position":1,"seconds":84.238},{"id":"norris","position":2,"seconds":84.382},{"id":"tsunoda","position":3,"seconds":84.832},{"id":"ocon","position":4,"seconds":84.654},{"id":"leclerc","position":5,"seconds":84.674},{"id":"lawson","position":6,"seconds":84.682},{"id":"piastri","position":7,"seconds":84.869},{"id":"max_verstappen","position":8,"seconds":84.778},{"id":"alonso","position":9,"seconds":84.702},{"id":"gasly","position":10,"seconds":85.105},{"id":"hamilton","position":11,"seconds":84.646},{"id":"hulkenberg","position":12,"seconds":84.425},{"id":"colapinto","position":13,"seconds":85.048},{"id":"bottas","position":14,"seconds":84.851},{"id":"sainz","position":15,"seconds":85.191},{"id":"bearman","position":16,"seconds":85.164},{"id":"perez","position":17,"seconds":85.253},{"id":"zhou","position":18,"seconds":85.915}]},{"number":10,"timings":[{"id":"russell","position":1,"seconds":84.295},{"id":"norris","position":2,"seconds":84.088},{"id":"tsunoda","position":3,"seconds":84.592},{"id":"ocon","position":4,"seconds":84.493},{"id":"leclerc","position":5,"seconds":84.672},{"id":"lawson","position":6,"seconds":84.976},{"id":"max_verstappen","position":7,"seconds":84.015},{"id":"piastri","position":8,"seconds":85.762},{"id":"alonso","position":9,"seconds":85.472},{"id":"gasly","position":10,"seconds":85.021},{"id":"hamilton","position":11,"seconds":85.33},{"id":"hulkenberg","position":12,"seconds":85.084},{"id":"colapinto","position":13,"seconds":85.975},{"id":"bottas","position":14,"seconds":85.851},{"id":"sainz","position":15,"seconds":85.406},{"id":"bearman","position":16,"seconds":85.115},{"id":"perez","position":17,"seconds":84.911},{"id":"zhou","position":18,"seconds":85.712}]},{"number":11,"timings":[{"id":"russell","position":1,"seconds":83.928},{"id":"norris","position":2,"seconds":84.11},{"id":"tsunoda","position":3,"seconds":84.953},{"id":"ocon","position":4,"seconds":84.782},{"id":"leclerc","position":5,"seconds":84.432},{"id":"max_verstappen","position":6,"seconds":84.156},{"id":"lawson","position":7,"seconds":86.02},{"id":"piastri","position":8,"seconds":84.792},{"id":"alonso","position":9,"seconds":84.692},{"id":"gasly","position":10,"seconds":84.829},{"id":"hulkenberg","position":11,"seconds":85.005},{"id":"hamilton","position":12,"seconds":91.858},{"id":"colapinto","position":13,"seconds":85.549},{"id":"bottas","position":14,"seconds":86.246},{"id":"sainz","position":15,"seconds":86.203},{"id":"bearman","position":16,"seconds":86.216},{"id":"perez","position":17,"seconds":86.025},{"id":"zhou","position":18,"seconds":85.544}]},{"number":12,"timings":[{"id":"russell","position":1,"seconds":84.216},{"id":"norris","position":2,"seconds":84.072},{"id":"tsunoda","position":3,"seconds":84.877},{"id":"ocon","position":4,"seconds":84.641},{"id":"leclerc","position":5,"seconds":84.755},{"id":"max_verstappen","position":6,"seconds":83.43},{"id":"lawson","position":7,"seconds":84.834},{"id":"piastri","position":8,"seconds":84.83},{"id":"alonso","position":9,"seconds":84.657},{"id":"gasly","position":10,"seconds":86.151},{"id":"hulkenberg","position":11,"seconds":84.372},{"id":"colapinto","position":12,"seconds":85.418},{"id":"hamilton","position":13,"seconds":86.433},{"id":"sainz","position":14,"seconds":85.633},{"id":"bearman","position":15,"seconds":85.992},{"id":"perez","position":16,"seconds":85.993},{"id":"bottas","position":17,"seconds":89.257},{"id":"zhou","position":18,"seconds":86.543}]},{"number":13,"timings":[{"id":"russell","position":1,"seconds":84.672},{"id":"norris","position":2,"seconds":84.725},{"id":"tsunoda","position":3,"seconds":85.172},{"id":"ocon","position":4,"seconds":84.926},{"id":"leclerc","position":5,"seconds":84.943},{"id":"max_verstappen","position":6,"seconds":84.512},{"id":"lawson","position":7,"seconds":85.21},{"id":"piastri","position":8,"seconds":85.187},{"id":"alonso","position":9,"seconds":84.886},{"id":"gasly","position":10,"seconds":84.937},{"id":"hulkenberg","position":11,"seconds":84.796},{"id":"colapinto","position":12,"seconds":84.565},{"id":"hamilton","position":13,"seconds":84.559},{"id":"sainz","position":14,"seconds":84.293},{"id":"bearman","position":15,"seconds":85.215},{"id":"perez","position":16,"seconds":85.641},{"id":"bottas","position":17,"seconds":86.638},{"id":"zhou","position":18,"seconds":87.829}]},{"number":14,"timings":[{"id":"russell","position":1,"seconds":84.462},{"id":"norris","position":2,"seconds":84.437},{"id":"tsunoda","position":3,"seconds":84.827},{"id":"ocon","position":4,"seconds":85.044},{"id":"leclerc","position":5,"seconds":84.948},{"id":"max_verstappen","position":6,"seconds":84.791},{"id":"lawson","position":7,"seconds":84.813},{"id":"piastri","position":8,"seconds":85.307},{"id":"alonso","position":9,"seconds":84.793},{"id":"gasly","position":10,"seconds":84.798},{"id":"hulkenberg","position":11,"seconds":84.91},{"id":"colapinto","position":12,"seconds":84.75},{"id":"hamilton","position":13,"seconds":84.51},{"id":"sainz","position":14,"seconds":84.312},{"id":"bearman","position":15,"seconds":84.365},{"id":"perez","position":16,"seconds":85.755},{"id":"bottas","position":17,"seconds":85.315},{"id":"zhou","position":18,"seconds":87.146}]},{"number":15,"timings":[{"id":"russell","position":1,"seconds":84.383},{"id":"norris","position":2,"seconds":84.461},{"id":"tsunoda","position":3,"seconds":84.635},{"id":"ocon","position":4,"seconds":84.896},{"id":"leclerc","position":5,"seconds":84.652},{"id":"max_verstappen","position":6,"seconds":84.669},{"id":"lawson","position":7,"seconds":85.154},{"id":"piastri","position":8,"seconds":84.924},{"id":"alonso","position":9,"seconds":84.579},{"id":"gasly","position":10,"seconds":84.66},{"id":"hulkenberg","position":11,"seconds":84.845},{"id":"colapinto","position":12,"seconds":85.655},{"id":"hamilton","position":13,"seconds":85.793},{"id":"sainz","position":14,"seconds":85.434},{"id":"bearman","position":15,"seconds":84.73},{"id":"perez","position":16,"seconds":85.279},{"id":"bottas","position":17,"seconds":85.431},{"id":"zhou","position":18,"seconds":86.38}]},{"number":16,"timings":[{"id":"russell","position":1,"seconds":83.984},{"id":"norris","position":2,"seconds":84.484},{"id":"tsunoda","position":3,"seconds":84.762},{"id":"ocon","position":4,"seconds":85.045},{"id":"leclerc","position":5,"seconds":85.249},{"id":"max_verstappen","position":6,"seconds":85.131},{"id":"lawson","position":7,"seconds":84.984},{"id":"piastri","position":8,"seconds":84.896},{"id":"gasly","position":9,"seconds":84.351},{"id":"alonso","position":10,"seconds":88.626},{"id":"hulkenberg","position":11,"seconds":85.646},{"id":"colapinto","position":12,"seconds":85.338},{"id":"hamilton","position":13,"seconds":85.448},{"id":"sainz","position":14,"seconds":85.792},{"id":"bearman","position":15,"seconds":85.055},{"id":"perez","position":16,"seconds":84.469},{"id":"bottas","position":17,"seconds":84.896},{"id":"zhou","position":18,"seconds":86.125}]},{"number":17,"timings":[{"id":"russell","position":1,"seconds":84.71},{"id":"norris","position":2,"seconds":83.973},{"id":"tsunoda","position":3,"seconds":84.724},{"id":"ocon","position":4,"seconds":84.243},{"id":"leclerc","position":5,"seconds":84.271},{"id":"max_verstappen","position":6,"seconds":84.328},{"id":"lawson","position":7,"seconds":84.525},{"id":"piastri","position":8,"seconds":84.555},{"id":"gasly","position":9,"seconds":84.237},{"id":"alonso","position":10,"seconds":84.51},{"id":"hulkenberg","position":11,"seconds":84.397},{"id":"colapinto","position":12,"seconds":84.967},{"id":"hamilton","position":13,"seconds":84.732},{"id":"sainz","position":14,"seconds":84.667},{"id":"bearman","position":15,"seconds":84.941},{"id":"perez","position":16,"seconds":84.395},{"id":"bottas","position":17,"seconds":84.927},{"id":"zhou","position":18,"seconds":85.855}]},{"number":18,"timings":[{"id":"russell","position":1,"seconds":84.423},{"id":"norris","position":2,"seconds":84.505},{"id":"tsunoda","position":3,"seconds":84.547},{"id":"ocon","position":4,"seconds":84.423},{"id":"leclerc","position":5,"seconds":84.351},{"id":"max_verstappen","position":6,"seconds":84.248},{"id":"lawson","position":7,"seconds":84.682},{"id":"piastri","position":8,"seconds":84.526},{"id":"gasly","position":9,"seconds":84.199},{"id":"alonso","position":10,"seconds":84.035},{"id":"hulkenberg","position":11,"seconds":85.094},{"id":"colapinto","position":12,"seconds":84.836},{"id":"sainz","position":13,"seconds":84.874},{"id":"hamilton","position":14,"seconds":86.035},{"id":"bearman","position":15,"seconds":84.883},{"id":"perez","position":16,"seconds":85.85},{"id":"bottas","position":17,"seconds":85.189},{"id":"zhou","position":18,"seconds":85.579}]},{"number":19,"timings":[{"id":"russell","position":1,"seconds":84.338},{"id":"norris","position":2,"seconds":84.465},{"id":"tsunoda","position":3,"seconds":84.275},{"id":"ocon","position":4,"seconds":84.222},{"id":"leclerc","position":5,"seconds":84.243},{"id":"max_verstappen","position":6,"seconds":84.493},{"id":"lawson","position":7,"seconds":84.607},{"id":"piastri","position":8,"seconds":84.563},{"id":"gasly","position":9,"seconds":84.214},{"id":"alonso","position":10,"seconds":84.787},{"id":"hulkenberg","position":11,"seconds":84.415},{"id":"colapinto","position":12,"seconds":85.028},{"id":"sainz","position":13,"seconds":84.201},{"id":"bearman","position":14,"seconds":85.601},{"id":"hamilton","position":15,"seconds":86.872},{"id":"perez","position":16,"seconds":84.414},{"id":"bottas","position":17,"seconds":85.089},{"id":"zhou","position":18,"seconds":85.376}]},{"number":20,"timings":[{"id":"russell","position":1,"seconds":83.751},{"id":"norris","position":2,"seconds":84.197},{"id":"tsunoda","position":3,"seconds":84.163},{"id":"ocon","position":4,"seconds":84.282},{"id":"leclerc","position":5,"seconds":84.231},{"id":"max_verstappen","position":6,"seconds":83.915},{"id":"lawson","position":7,"seconds":84.034},{"id":"piastri","position":8,"seconds":84.278},{"id":"gasly","position":9,"seconds":84.381},{"id":"alonso","position":10,"seconds":83.949},{"id":"hulkenberg","position":11,"seconds":84.354},{"id":"colapinto","position":12,"seconds":85.366},{"id":"sainz","position":13,"seconds":85.301},{"id":"bearman","position":14,"seconds":84.414},{"id":"hamilton","position":15,"seconds":84.315},{"id":"perez","position":16,"seconds":84.522},{"id":"bottas","position":17,"seconds":84.984},{"id":"zhou","position":18,"seconds":85.053}]},{"number":21,"timings":[{"id":"russell","position":1,"seconds":83.517},{"id":"norris","position":2,"seconds":83.777},{"id":"tsunoda","position":3,"seconds":84.179},{"id":"ocon","position":4,"seconds":84.037},{"id":"leclerc","position":5,"seconds":84.802},{"id":"max_verstappen","position":6,"seconds":84.269},{"id":"lawson","position":7,"seconds":84.17},{"id":"piastri","position":8,"seconds":84.098},{"id":"gasly","position":9,"seconds":83.92},{"id":"alonso","position":10,"seconds":84.09},{"id":"hulkenberg","position":11,"seconds":83.764},{"id":"colapinto","position":12,"seconds":84.296},{"id":"sainz","position":13,"seconds":84.631},{"id":"bearman","position":14,"seconds":84.482},{"id":"hamilton","position":15,"seconds":84.385},{"id":"perez","position":16,"seconds":84.408},{"id":"bottas","position":17,"seconds":84.726},{"id":"zhou","position":18,"seconds":86.01}]},{"number":22,"timings":[{"id":"russell","position":1,"seconds":83.883},{"id":"norris","position":2,"seconds":83.592},{"id":"tsunoda","position":3,"seconds":84.39},{"id":"ocon","position":4,"seconds":84.37},{"id":"leclerc","position":5,"seconds":84.309},{"id":"max_verstappen","position":6,"seconds":85.055},{"id":"lawson","position":7,"seconds":84.365},{"id":"piastri","position":8,"seconds":84.225},{"id":"gasly","position":9,"seconds":83.996},{"id":"alonso","position":10,"seconds":84.384},{"id":"hulkenberg","position":11,"seconds":84.062},{"id":"colapinto","position":12,"seconds":84.878},{"id":"bearman","position":13,"seconds":84.517},{"id":"hamilton","position":14,"seconds":84.506},{"id":"sainz","position":15,"seconds":87.774},{"id":"perez","position":16,"seconds":85.382},{"id":"bottas","position":17,"seconds":85.144},{"id":"zhou","position":18,"seconds":85.637}]},{"number":23,"timings":[{"id":"russell","position":1,"seconds":84.127},{"id":"norris","position":2,"seconds":83.732},{"id":"tsunoda","position":3,"seconds":84.543},{"id":"ocon","position":4,"seconds":84.606},{"id":"leclerc","position":5,"seconds":84.137},{"id":"max_verstappen","position":6,"seconds":84.267},{"id":"lawson","position":7,"seconds":85.0},{"id":"piastri","position":8,"seconds":84.774},{"id":"gasly","position":9,"seconds":84.744},{"id":"alonso","position":10,"seconds":84.204},{"id":"hulkenberg","position":11,"seconds":84.583},{"id":"bearman","position":12,"seconds":84.306},{"id":"hamilton","position":13,"seconds":84.555},{"id":"colapinto","position":14,"seconds":88.291},{"id":"sainz","position":15,"seconds":85.187},{"id":"perez","position":16,"seconds":85.158},{"id":"bottas","position":17,"seconds":84.966},{"id":"zhou","position":18,"seconds":86.438}]},{"number":24,"timings":[{"id":"russell","position":1,"seconds":84.407},{"id":"norris","position":2,"seconds":83.982},{"id":"tsunoda","position":3,"seconds":84.617},{"id":"ocon","position":4,"seconds":84.516},{"id":"max_verstappen","position":5,"seconds":84.53},{"id":"leclerc","position":6,"seconds":88.997},{"id":"lawson","position":7,"seconds":84.757},{"id":"piastri","position":8,"seconds":84.789},{"id":"gasly","position":9,"seconds":84.399},{"id":"alonso","position":10,"seconds":84.27},{"id":"hulkenberg","position":11,"seconds":84.756},{"id":"bearman","position":12,"seconds":84.429},{"id":"hamilton","position":13,"seconds":83.973},{"id":"colapinto","position":14,"seconds":85.116},{"id":"sainz","position":15,"seconds":85.684},{"id":"perez","position":16,"seconds":85.871},{"id":"bottas","position":17,"seconds":85.203},{"id":"zhou","position":18,"seconds":86.086}]},{"number":25,"timings":[{"id":"russell","position":1,"seconds":84.47},{"id":"norris","position":2,"seconds":84.877},{"id":"tsunoda","position":3,"seconds":84.91},{"id":"ocon","position":4,"seconds":85.061},{"id":"max_verstappen","position":5,"seconds":84.156},{"id":"lawson","position":6,"seconds":84.915},{"id":"piastri","position":7,"seconds":84.918},{"id":"gasly","position":8,"seconds":84.41},{"id":"alonso","position":9,"seconds":84.443},{"id":"hulkenberg","position":10,"seconds":90.41},{"id":"bearman","position":11,"seconds":84.737},{"id":"hamilton","position":12,"seconds":85.165},{"id":"leclerc","position":13,"seconds":105.082},{"id":"colapinto","position":14,"seconds":85.835},{"id":"sainz","position":15,"seconds":85.396},{"id":"perez","position":16,"seconds":86.257},{"id":"bottas","position":17,"seconds":85.525},{"id":"zhou","position":18,"seconds":87.336}]},{"number":26,"timings":[{"id":"russell","position":1,"seconds":85.404},{"id":"norris","position":2,"seconds":85.114},{"id":"tsunoda","position":3,"seconds":85.424},{"id":"ocon","position":4,"seconds":85.47},{"id":"max_verstappen","position":5,"seconds":85.252},{"id":"gasly","position":6,"seconds":85.245},{"id":"piastri","position":7,"seconds":88.032},{"id":"alonso","position":8,"seconds":86.96},{"id":"lawson","position":9,"seconds":92.246},{"id":"bearman","position":10,"seconds":85.027},{"id":"hamilton","position":11,"seconds":84.983},{"id":"leclerc","position":12,"seconds":84.479},{"id":"sainz","position":13,"seconds":86.167},{"id":"perez","position":14,"seconds":86.572},{"id":"colapinto","position":15,"seconds":89.885},{"id":"bottas","position":16,"seconds":87.132},{"id":"hulkenberg","position":17,"seconds":106.643},{"id":"zhou","position":18,"seconds":88.22}]},{"number":27,"timings":[{"id":"russell","position":1,"seconds":86.877},{"id":"norris","position":2,"seconds":86.879},{"id":"ocon","position":3,"seconds":86.62},{"id":"tsunoda","position":4,"seconds":88.833},{"id":"max_verstappen","position":5,"seconds":88.393},{"id":"gasly","position":6,"seconds":89.666},{"id":"piastri","position":7,"seconds":96.487},{"id":"alonso","position":8,"seconds":98.089},{"id":"lawson","position":9,"seconds":97.664},{"id":"leclerc","position":10,"seconds":95.274},{"id":"bearman","position":11,"seconds":98.431},{"id":"hamilton","position":12,"seconds":100.067},{"id":"sainz","position":13,"seconds":101.195},{"id":"perez","position":14,"seconds":102.155},{"id":"bottas","position":15,"seconds":100.966},{"id":"colapinto","position":16,"seconds":122.141},{"id":"zhou","position":17,"seconds":108.354},{"id":"hulkenberg","position":18,"seconds":230.215}]},{"number":28,"timings":[{"id":"russell","position":1,"seconds":114.428},{"id":"norris","position":2,"seconds":115.212},{"id":"ocon","position":3,"seconds":110.714},{"id":"max_verstappen","position":4,"seconds":111.717},{"id":"tsunoda","position":5,"seconds":117.034},{"id":"gasly","position":6,"seconds":109.087},{"id":"lawson","position":7,"seconds":113.045},{"id":"leclerc","position":8,"seconds":106.521},{"id":"piastri","position":9,"seconds":121.36},{"id":"alonso","position":10,"seconds":120.925},{"id":"bottas","position":11,"seconds":106.762},{"id":"hamilton","position":12,"seconds":119.838},{"id":"sainz","position":13,"seconds":122.82},{"id":"bearman","position":14,"seconds":131.553},{"id":"perez","position":15,"seconds":120.667},{"id":"colapinto","position":16,"seconds":102.804},{"id":"zhou","position":17,"seconds":126.376},{"id":"hulkenberg","position":18,"seconds":168.684}]},{"number":29,"timings":[{"id":"ocon","position":1,"seconds":100.823},{"id":"max_verstappen","position":2,"seconds":105.014},{"id":"gasly","position":3,"seconds":102.264},{"id":"russell","position":4,"seconds":122.979},{"id":"norris","position":5,"seconds":122.458},{"id":"tsunoda","position":6,"seconds":115.413},{"id":"leclerc","position":7,"seconds":99.37},{"id":"piastri","position":8,"seconds":101.078},{"id":"alonso","position":9,"seconds":105.422},{"id":"hamilton","position":10,"seconds":105.316},{"id":"lawson","position":11,"seconds":125.322},{"id":"bottas","position":12,"seconds":110.114},{"id":"sainz","position":13,"seconds":102.131},{"id":"perez","position":14,"seconds":101.662},{"id":"bearman","position":15,"seconds":103.986},{"id":"colapinto","position":16,"seconds":103.469},{"id":"zhou","position":17,"seconds":138.727},{"id":"hulkenberg","position":18,"seconds":127.542}]},{"number":30,"timings":[{"id":"ocon","position":1,"seconds":118.147},{"id":"max_verstappen","position":2,"seconds":119.319},{"id":"gasly","position":3,"seconds":118.4},{"id":"norris","position":4,"seconds":117.236},{"id":"russell","position":5,"seconds":122.081},{"id":"tsunoda","position":6,"seconds":114.797},{"id":"leclerc","position":7,"seconds":116.007},{"id":"piastri","position":8,"seconds":117.584},{"id":"alonso","position":9,"seconds":119.045},{"id":"lawson","position":10,"seconds":114.922},{"id":"hamilton","position":11,"seconds":116.004},{"id":"bottas","position":12,"seconds":128.365},{"id":"sainz","position":13,"seconds":127.308},{"id":"perez","position":14,"seconds":128.473},{"id":"bearman","position":15,"seconds":128.143},{"id":"colapinto","position":16,"seconds":130.874},{"id":"zhou","position":17,"seconds":125.363},{"id":"hulkenberg","position":18,"seconds":174.656}]},{"number":31,"timings":[{"id":"ocon","position":1,"seconds":132.11},{"id":"max_verstappen","position":2,"seconds":124.967},{"id":"gasly","position":3,"seconds":126.773},{"id":"norris","position":4,"seconds":126.964},{"id":"russell","position":5,"seconds":125.485},{"id":"tsunoda","position":6,"seconds":125.198},{"id":"leclerc","position":7,"seconds":120.957},{"id":"piastri","position":8,"seconds":119.357},{"id":"alonso","position":9,"seconds":116.643},{"id":"lawson","position":10,"seconds":112.061},{"id":"hamilton","position":11,"seconds":112.031},{"id":"bottas","position":12,"seconds":112.258},{"id":"sainz","position":13,"seconds":111.416},{"id":"perez","position":14,"seconds":111.015},{"id":"bearman","position":15,"seconds":112.001},{"id":"zhou","position":16,"seconds":160.52}]},{"number":32,"timings":[{"id":"ocon","position":1,"seconds":158.23},{"id":"max_verstappen","position":2,"seconds":158.629},{"id":"gasly","position":3,"seconds":160.808},{"id":"norris","position":4,"seconds":159.932},{"id":"russell","position":5,"seconds":162.892},{"id":"tsunoda","position":6,"seconds":163.084},{"id":"leclerc","position":7,"seconds":163.725},{"id":"piastri","position":8,"seconds":164.524},{"id":"alonso","position":9,"seconds":161.09},{"id":"lawson","position":10,"seconds":158.789},{"id":"hamilton","position":11,"seconds":160.982},{"id":"bottas","position":12,"seconds":151.847},{"id":"sainz","position":13,"seconds":154.208},{"id":"perez","position":14,"seconds":156.778},{"id":"bearman","position":15,"seconds":158.79},{"id":"zhou","position":16,"seconds":1392.307}]},{"number":33,"timings":[{"id":"ocon","position":1,"seconds":1557.824},{"id":"max_verstappen","position":2,"seconds":1556.41},{"id":"gasly","position":3,"seconds":1550.5},{"id":"norris","position":4,"seconds":1549.718},{"id":"russell","position":5,"seconds":1544.799},{"id":"tsunoda","position":6,"seconds":1543.702},{"id":"leclerc","position":7,"seconds":1541.212},{"id":"piastri","position":8,"seconds":1537.798},{"id":"alonso","position":9,"seconds":1536.089},{"id":"lawson","position":10,"seconds":1534.389},{"id":"hamilton","position":11,"seconds":1532.48},{"id":"bottas","position":12,"seconds":1527.455},{"id":"sainz","position":13,"seconds":1524.273},{"id":"perez","position":14,"seconds":1520.559},{"id":"zhou","position":15,"seconds":173.772},{"id":"bearman","position":16,"seconds":1524.788}]},{"number":34,"timings":[{"id":"ocon","position":1,"seconds":89.391},{"id":"max_verstappen","position":2,"seconds":90.36},{"id":"gasly","position":3,"seconds":93.504},{"id":"russell","position":4,"seconds":95.067},{"id":"norris","position":5,"seconds":96.887},{"id":"leclerc","position":6,"seconds":97.098},{"id":"piastri","position":7,"seconds":98.248},{"id":"tsunoda","position":8,"seconds":99.418},{"id":"hamilton","position":9,"seconds":97.691},{"id":"alonso","position":10,"seconds":99.529},{"id":"lawson","position":11,"seconds":99.368},{"id":"perez","position":12,"seconds":97.821},{"id":"bottas","position":13,"seconds":100.982},{"id":"sainz","position":14,"seconds":100.313},{"id":"bearman","position":15,"seconds":92.73},{"id":"zhou","position":16,"seconds":98.583}]},{"number":35,"timings":[{"id":"ocon","position":1,"seconds":86.913},{"id":"max_verstappen","position":2,"seconds":88.006},{"id":"gasly","position":3,"seconds":88.741},{"id":"russell","position":4,"seconds":88.677},{"id":"norris","position":5,"seconds":89.337},{"id":"leclerc","position":6,"seconds":89.146},{"id":"piastri","position":7,"seconds":89.882},{"id":"tsunoda","position":8,"seconds":91.883},{"id":"hamilton","position":9,"seconds":91.457},{"id":"alonso","position":10,"seconds":92.61},{"id":"lawson","position":11,"seconds":92.634},{"id":"perez","position":12,"seconds":92.534},{"id":"sainz","position":13,"seconds":91.779},{"id":"bearman","position":14,"seconds":91.578},{"id":"bottas","position":15,"seconds":95.37},{"id":"zhou","position":16,"seconds":95.014}]},{"number":36,"timings":[{"id":"ocon","position":1,"seconds":85.773},{"id":"max_verstappen","position":2,"seconds":86.509},{"id":"gasly","position":3,"seconds":86.658},{"id":"russell","position":4,"seconds":86.885},{"id":"norris","position":5,"seconds":87.235},{"id":"leclerc","position":6,"seconds":87.467},{"id":"piastri","position":7,"seconds":87.878},{"id":"tsunoda","position":8,"seconds":89.062},{"id":"hamilton","position":9,"seconds":88.763},{"id":"alonso","position":10,"seconds":88.918},{"id":"lawson","position":11,"seconds":89.301},{"id":"perez","position":12,"seconds":89.093},{"id":"sainz","position":13,"seconds":88.886},{"id":"bottas","position":14,"seconds":92.055},{"id":"zhou","position":15,"seconds":93.356},{"id":"bearman","position":16,"seconds":107.937}]},{"number":37,"timings":[{"id":"ocon","position":1,"seconds":85.271},{"id":"max_verstappen","position":2,"seconds":85.286},{"id":"gasly","position":3,"seconds":85.284},{"id":"russell","position":4,"seconds":85.886},{"id":"norris","position":5,"seconds":85.337},{"id":"leclerc","position":6,"seconds":86.055},{"id":"piastri","position":7,"seconds":86.052},{"id":"tsunoda","position":8,"seconds":86.916},{"id":"hamilton","position":9,"seconds":87.526},{"id":"alonso","position":10,"seconds":88.143},{"id":"lawson","position":11,"seconds":88.046},{"id":"perez","position":12,"seconds":87.886},{"id":"sainz","position":13,"seconds":87.101},{"id":"bottas","position":14,"seconds":88.061},{"id":"zhou","position":15,"seconds":89.472},{"id":"bearman","position":16,"seconds":91.737}]},{"number":38,"timings":[{"id":"ocon","position":1,"seconds":84.602},{"id":"max_verstappen","position":2,"seconds":84.391},{"id":"gasly","position":3,"seconds":85.236},{"id":"russell","position":4,"seconds":84.89},{"id":"norris","position":5,"seconds":85.282},{"id":"leclerc","position":6,"seconds":84.88},{"id":"piastri","position":7,"seconds":85.182},{"id":"tsunoda","position":8,"seconds":85.788},{"id":"hamilton","position":9,"seconds":86.37},{"id":"alonso","position":10,"seconds":86.561},{"id":"lawson","position":11,"seconds":86.873},{"id":"perez","position":12,"seconds":87.244},{"id":"sainz","position":13,"seconds":86.771},{"id":"bottas","position":14,"seconds":87.343},{"id":"zhou","position":15,"seconds":87.983},{"id":"bearman","position":16,"seconds":87.141}]},{"number":39,"timings":[{"id":"ocon","position":1,"seconds":84.42},{"id":"max_verstappen","position":2,"seconds":85.296},{"id":"gasly","position":3,"seconds":90.233},{"id":"russell","position":4,"seconds":93.033},{"id":"norris","position":5,"seconds":93.718},{"id":"leclerc","position":6,"seconds":95.846},{"id":"piastri","position":7,"seconds":96.338},{"id":"tsunoda","position":8,"seconds":97.004},{"id":"hamilton","position":9,"seconds":98.741},{"id":"alonso","position":10,"seconds":98.579},{"id":"lawson","position":11,"seconds":99.603},{"id":"perez","position":12,"seconds":101.208},{"id":"bottas","position":13,"seconds":99.942},{"id":"zhou","position":14,"seconds":102.318},{"id":"bearman","position":15,"seconds":104.52}]},{"number":40,"timings":[{"id":"ocon","position":1,"seconds":132.164},{"id":"max_verstappen","position":2,"seconds":129.449},{"id":"gasly","position":3,"seconds":120.75},{"id":"russell","position":4,"seconds":117.269},{"id":"norris","position":5,"seconds":116.606},{"id":"leclerc","position":6,"seconds":115.628},{"id":"piastri","position":7,"seconds":113.528},{"id":"tsunoda","position":8,"seconds":111.544},{"id":"hamilton","position":9,"seconds":109.874},{"id":"alonso","position":10,"seconds":110.941},{"id":"lawson","position":11,"seconds":110.352},{"id":"perez","position":12,"seconds":108.85},{"id":"bottas","position":13,"seconds":111.569},{"id":"zhou","position":14,"seconds":110.597},{"id":"bearman","position":15,"seconds":112.287}]},{"number":41,"timings":[{"id":"ocon","position":1,"seconds":125.513},{"id":"max_verstappen","position":2,"seconds":125.095},{"id":"gasly","position":3,"seconds":125.971},{"id":"russell","position":4,"seconds":125.823},{"id":"norris","position":5,"seconds":125.139},{"id":"leclerc","position":6,"seconds":125.604},{"id":"piastri","position":7,"seconds":125.903},{"id":"tsunoda","position":8,"seconds":125.331},{"id":"hamilton","position":9,"seconds":125.297},{"id":"alonso","position":10,"seconds":123.546},{"id":"lawson","position":11,"seconds":124.355},{"id":"perez","position":12,"seconds":125.017},{"id":"bottas","position":13,"seconds":117.089},{"id":"zhou","position":14,"seconds":110.526},{"id":"bearman","position":15,"seconds":98.426}]},{"number":42,"timings":[{"id":"ocon","position":1,"seconds":143.552},{"id":"max_verstappen","position":2,"seconds":142.964},{"id":"gasly","position":3,"seconds":140.928},{"id":"russell","position":4,"seconds":139.051},{"id":"norris","position":5,"seconds":137.692},{"id":"leclerc","position":6,"seconds":134.283},{"id":"piastri","position":7,"seconds":132.899},{"id":"tsunoda","position":8,"seconds":129.387},{"id":"hamilton","position":9,"seconds":128.111},{"id":"alonso","position":10,"seconds":127.072},{"id":"lawson","position":11,"seconds":125.853},{"id":"perez","position":12,"seconds":124.054},{"id":"bottas","position":13,"seconds":122.559},{"id":"zhou","position":14,"seconds":120.988},{"id":"bearman","position":15,"seconds":119.934}]},{"number":43,"timings":[{"id":"max_verstappen","position":1,"seconds":85.676},{"id":"ocon","position":2,"seconds":87.354},{"id":"gasly","position":3,"seconds":87.53},{"id":"leclerc","position":4,"seconds":88.797},{"id":"russell","position":5,"seconds":89.684},{"id":"piastri","position":6,"seconds":89.836},{"id":"norris","position":7,"seconds":91.039},{"id":"tsunoda","position":8,"seconds":91.609},{"id":"lawson","position":9,"seconds":90.558},{"id":"perez","position":10,"seconds":92.206},{"id":"hamilton","position":11,"seconds":94.637},{"id":"bearman","position":12,"seconds":92.19},{"id":"bottas","position":13,"seconds":93.797},{"id":"zhou","position":14,"seconds":94.48},{"id":"alonso","position":15,"seconds":113.466}]},{"number":44,"timings":[{"id":"max_verstappen","position":1,"seconds":83.884},{"id":"ocon","position":2,"seconds":84.504},{"id":"gasly","position":3,"seconds":84.886},{"id":"leclerc","position":4,"seconds":85.195},{"id":"russell","position":5,"seconds":85.346},{"id":"piastri","position":6,"seconds":85.697},{"id":"norris","position":7,"seconds":85.605},{"id":"tsunoda","position":8,"seconds":86.547},{"id":"lawson","position":9,"seconds":86.375},{"id":"perez","position":10,"seconds":85.233},{"id":"hamilton","position":11,"seconds":85.777},{"id":"bearman","position":12,"seconds":87.016},{"id":"bottas","position":13,"seconds":87.017},{"id":"zhou","position":14,"seconds":87.953},{"id":"alonso","position":15,"seconds":85.846}]},{"number":45,"timings":[{"id":"max_verstappen","position":1,"seconds":82.876},{"id":"ocon","position":2,"seconds":83.771},{"id":"gasly","position":3,"seconds":84.087},{"id":"leclerc","position":4,"seconds":84.39},{"id":"russell","position":5,"seconds":84.451},{"id":"piastri","position":6,"seconds":84.29},{"id":"norris","position":7,"seconds":84.446},{"id":"tsunoda","position":8,"seconds":84.985},{"id":"lawson","position":9,"seconds":85.12},{"id":"perez","position":10,"seconds":85.156},{"id":"hamilton","position":11,"seconds":84.825},{"id":"bearman","position":12,"seconds":85.307},{"id":"bottas","position":13,"seconds":86.05},{"id":"zhou","position":14,"seconds":86.202},{"id":"alonso","position":15,"seconds":83.861}]},{"number":46,"timings":[{"id":"max_verstappen","position":1,"seconds":83.106},{"id":"ocon","position":2,"seconds":83.395},{"id":"gasly","position":3,"seconds":83.511},{"id":"russell","position":4,"seconds":84.385},{"id":"leclerc","position":5,"seconds":87.032},{"id":"norris","position":6,"seconds":84.879},{"id":"piastri","position":7,"seconds":86.611},{"id":"tsunoda","position":8,"seconds":84.787},{"id":"lawson","position":9,"seconds":84.814},{"id":"perez","position":10,"seconds":84.858},{"id":"hamilton","position":11,"seconds":84.622},{"id":"bearman","position":12,"seconds":84.578},{"id":"bottas","position":13,"seconds":84.963},{"id":"zhou","position":14,"seconds":85.396},{"id":"alonso","position":15,"seconds":85.094}]},{"number":47,"timings":[{"id":"max_verstappen","position":1,"seconds":82.453},{"id":"ocon","position":2,"seconds":82.946},{"id":"gasly","position":3,"seconds":83.498},{"id":"russell","position":4,"seconds":82.843},{"id":"leclerc","position":5,"seconds":84.145},{"id":"norris","position":6,"seconds":84.475},{"id":"piastri","position":7,"seconds":84.618},{"id":"tsunoda","position":8,"seconds":84.402},{"id":"lawson","position":9,"seconds":84.404},{"id":"perez","position":10,"seconds":84.656},{"id":"hamilton","position":11,"seconds":84.794},{"id":"bearman","position":12,"seconds":84.616},{"id":"bottas","position":13,"seconds":84.722},{"id":"zhou","position":14,"seconds":85.031},{"id":"alonso","position":15,"seconds":83.697}]},{"number":48,"timings":[{"id":"max_verstappen","position":1,"seconds":82.29},{"id":"ocon","position":2,"seconds":82.93},{"id":"gasly","position":3,"seconds":83.53},{"id":"russell","position":4,"seconds":82.744},{"id":"leclerc","position":5,"seconds":83.536},{"id":"norris","position":6,"seconds":83.99},{"id":"piastri","position":7,"seconds":84.174},{"id":"tsunoda","position":8,"seconds":84.293},{"id":"lawson","position":9,"seconds":84.324},{"id":"perez","position":10,"seconds":83.911},{"id":"hamilton","position":11,"seconds":84.246},{"id":"bearman","position":12,"seconds":83.945},{"id":"bottas","position":13,"seconds":84.284},{"id":"zhou","position":14,"seconds":84.728},{"id":"alonso","position":15,"seconds":83.379}]},{"number":49,"timings":[{"id":"max_verstappen","position":1,"seconds":82.241},{"id":"ocon","position":2,"seconds":82.998},{"id":"gasly","position":3,"seconds":83.183},{"id":"russell","position":4,"seconds":82.552},{"id":"leclerc","position":5,"seconds":83.324},{"id":"norris","position":6,"seconds":83.272},{"id":"piastri","position":7,"seconds":83.036},{"id":"tsunoda","position":8,"seconds":83.943},{"id":"lawson","position":9,"seconds":84.049},{"id":"perez","position":10,"seconds":84.142},{"id":"hamilton","position":11,"seconds":84.02},{"id":"bearman","position":12,"seconds":84.035},{"id":"bottas","position":13,"seconds":84.112},{"id":"zhou","position":14,"seconds":84.415},{"id":"alonso","position":15,"seconds":83.573}]},{"number":50,"timings":[{"id":"max_verstappen","position":1,"seconds":82.043},{"id":"ocon","position":2,"seconds":82.688},{"id":"gasly","position":3,"seconds":83.033},{"id":"russell","position":4,"seconds":82.698},{"id":"leclerc","position":5,"seconds":83.72},{"id":"norris","position":6,"seconds":83.373},{"id":"piastri","position":7,"seconds":83.473},{"id":"tsunoda","position":8,"seconds":83.596},{"id":"lawson","position":9,"seconds":83.687},{"id":"perez","position":10,"seconds":83.686},{"id":"hamilton","position":11,"seconds":83.698},{"id":"bearman","position":12,"seconds":84.172},{"id":"bottas","position":13,"seconds":83.925},{"id":"zhou","position":14,"seconds":84.37},{"id":"alonso","position":15,"seconds":83.712}]},{"number":51,"timings":[{"id":"max_verstappen","position":1,"seconds":82.306},{"id":"ocon","position":2,"seconds":83.167},{"id":"gasly","position":3,"seconds":83.001},{"id":"russell","position":4,"seconds":82.661},{"id":"leclerc","position":5,"seconds":83.5},{"id":"norris","position":6,"seconds":83.443},{"id":"piastri","position":7,"seconds":86.267},{"id":"tsunoda","position":8,"seconds":83.717},{"id":"lawson","position":9,"seconds":84.09},{"id":"perez","position":10,"seconds":84.085},{"id":"hamilton","position":11,"seconds":83.963},{"id":"bearman","position":12,"seconds":85.085},{"id":"bottas","position":13,"seconds":84.507},{"id":"zhou","position":14,"seconds":84.812},{"id":"alonso","position":15,"seconds":83.484}]},{"number":52,"timings":[{"id":"max_verstappen","position":1,"seconds":82.38},{"id":"ocon","position":2,"seconds":82.569},{"id":"gasly","position":3,"seconds":82.912},{"id":"russell","position":4,"seconds":82.953},{"id":"leclerc","position":5,"seconds":83.142},{"id":"norris","position":6,"seconds":83.349},{"id":"piastri","position":7,"seconds":82.933},{"id":"tsunoda","position":8,"seconds":84.536},{"id":"lawson","position":9,"seconds":84.199},{"id":"perez","position":10,"seconds":84.086},{"id":"hamilton","position":11,"seconds":84.276},{"id":"bearman","position":12,"seconds":83.909},{"id":"bottas","position":13,"seconds":84.36},{"id":"zhou","position":14,"seconds":84.69},{"id":"alonso","position":15,"seconds":83.078}]},{"number":53,"timings":[{"id":"max_verstappen","position":1,"seconds":82.251},{"id":"ocon","position":2,"seconds":82.559},{"id":"gasly","position":3,"seconds":82.512},{"id":"russell","position":4,"seconds":82.427},{"id":"leclerc","position":5,"seconds":83.118},{"id":"norris","position":6,"seconds":83.615},{"id":"piastri","position":7,"seconds":82.73},{"id":"tsunoda","position":8,"seconds":83.545},{"id":"lawson","position":9,"seconds":84.048},{"id":"perez","position":10,"seconds":84.023},{"id":"hamilton","position":11,"seconds":83.84},{"id":"bearman","position":12,"seconds":83.969},{"id":"bottas","position":13,"seconds":84.36},{"id":"zhou","position":14,"seconds":84.727},{"id":"alonso","position":15,"seconds":83.265}]},{"number":54,"timings":[{"id":"max_verstappen","position":1,"seconds":82.246},{"id":"ocon","position":2,"seconds":82.815},{"id":"gasly","position":3,"seconds":82.739},{"id":"russell","position":4,"seconds":82.777},{"id":"leclerc","position":5,"seconds":82.957},{"id":"norris","position":6,"seconds":82.817},{"id":"piastri","position":7,"seconds":82.908},{"id":"tsunoda","position":8,"seconds":83.512},{"id":"lawson","position":9,"seconds":83.942},{"id":"perez","position":10,"seconds":83.744},{"id":"hamilton","position":11,"seconds":83.528},{"id":"bearman","position":12,"seconds":83.601},{"id":"bottas","position":13,"seconds":84.444},{"id":"zhou","position":14,"seconds":84.925},{"id":"alonso","position":15,"seconds":82.791}]},{"number":55,"timings":[{"id":"max_verstappen","position":1,"seconds":82.291},{"id":"ocon","position":2,"seconds":82.67},{"id":"gasly","position":3,"seconds":82.75},{"id":"russell","position":4,"seconds":82.527},{"id":"leclerc","position":5,"seconds":82.707},{"id":"norris","position":6,"seconds":82.71},{"id":"piastri","position":7,"seconds":82.521},{"id":"tsunoda","position":8,"seconds":83.118},{"id":"lawson","position":9,"seconds":83.697},{"id":"hamilton","position":10,"seconds":83.659},{"id":"perez","position":11,"seconds":85.533},{"id":"bearman","position":12,"seconds":83.815},{"id":"bottas","position":13,"seconds":83.957},{"id":"zhou","position":14,"seconds":84.899},{"id":"alonso","position":15,"seconds":83.683}]},{"number":56,"timings":[{"id":"max_verstappen","position":1,"seconds":82.005},{"id":"ocon","position":2,"seconds":82.804},{"id":"gasly","position":3,"seconds":82.747},{"id":"russell","position":4,"seconds":82.496},{"id":"leclerc","position":5,"seconds":82.728},{"id":"norris","position":6,"seconds":82.985},{"id":"piastri","position":7,"seconds":82.644},{"id":"tsunoda","position":8,"seconds":83.061},{"id":"lawson","position":9,"seconds":83.385},{"id":"hamilton","position":10,"seconds":82.945},{"id":"perez","position":11,"seconds":82.757},{"id":"bearman","position":12,"seconds":83.14},{"id":"bottas","position":13,"seconds":83.508},{"id":"zhou","position":14,"seconds":84.658},{"id":"alonso","position":15,"seconds":84.72}]},{"number":57,"timings":[{"id":"max_verstappen","position":1,"seconds":81.862},{"id":"ocon","position":2,"seconds":82.462},{"id":"gasly","position":3,"seconds":82.468},{"id":"russell","position":4,"seconds":82.815},{"id":"leclerc","position":5,"seconds":82.512},{"id":"norris","position":6,"seconds":82.411},{"id":"piastri","position":7,"seconds":82.524},{"id":"tsunoda","position":8,"seconds":82.766},{"id":"lawson","position":9,"seconds":82.953},{"id":"hamilton","position":10,"seconds":82.901},{"id":"perez","position":11,"seconds":82.777},{"id":"bearman","position":12,"seconds":83.395},{"id":"bottas","position":13,"seconds":83.451},{"id":"zhou","position":14,"seconds":84.302},{"id":"alonso","position":15,"seconds":84.175}]},{"number":58,"timings":[{"id":"max_verstappen","position":1,"seconds":81.782},{"id":"ocon","position":2,"seconds":82.393},{"id":"gasly","position":3,"seconds":82.385},{"id":"russell","position":4,"seconds":82.43},{"id":"leclerc","position":5,"seconds":82.017},{"id":"norris","position":6,"seconds":82.42},{"id":"piastri","position":7,"seconds":82.32},{"id":"tsunoda","position":8,"seconds":83.0},{"id":"lawson","position":9,"seconds":83.769},{"id":"hamilton","position":10,"seconds":83.888},{"id":"perez","position":11,"seconds":83.714},{"id":"bearman","position":12,"seconds":82.992},{"id":"bottas","position":13,"seconds":83.185},{"id":"alonso","position":14,"seconds":82.968},{"id":"zhou","position":15,"seconds":86.018}]},{"number":59,"timings":[{"id":"max_verstappen","position":1,"seconds":81.551},{"id":"ocon","position":2,"seconds":82.16},{"id":"gasly","position":3,"seconds":82.408},{"id":"russell","position":4,"seconds":82.163},{"id":"leclerc","position":5,"seconds":81.893},{"id":"norris","position":6,"seconds":82.137},{"id":"piastri","position":7,"seconds":82.18},{"id":"tsunoda","position":8,"seconds":82.872},{"id":"lawson","position":9,"seconds":83.152},{"id":"hamilton","position":10,"seconds":83.028},{"id":"perez","position":11,"seconds":83.086},{"id":"bearman","position":12,"seconds":82.897},{"id":"bottas","position":13,"seconds":83.418},{"id":"alonso","position":14,"seconds":82.293},{"id":"zhou","position":15,"seconds":83.806}]},{"number":60,"timings":[{"id":"max_verstappen","position":1,"seconds":81.331},{"id":"ocon","position":2,"seconds":82.111},{"id":"gasly","position":3,"seconds":82.093},{"id":"russell","position":4,"seconds":82.057},{"id":"leclerc","position":5,"seconds":81.631},{"id":"norris","position":6,"seconds":81.759},{"id":"piastri","position":7,"seconds":82.106},{"id":"tsunoda","position":8,"seconds":82.664},{"id":"lawson","position":9,"seconds":83.1},{"id":"hamilton","position":10,"seconds":83.122},{"id":"perez","position":11,"seconds":83.112},{"id":"bearman","position":12,"seconds":82.944},{"id":"bottas","position":13,"seconds":82.911},{"id":"alonso","position":14,"seconds":84.826},{"id":"zhou","position":15,"seconds":83.471}]},{"number":61,"timings":[{"id":"max_verstappen","position":1,"seconds":81.336},{"id":"ocon","position":2,"seconds":82.285},{"id":"gasly","position":3,"seconds":82.22},{"id":"russell","position":4,"seconds":82.161},{"id":"leclerc","position":5,"seconds":81.91},{"id":"norris","position":6,"seconds":81.756},{"id":"piastri","position":7,"seconds":82.125},{"id":"tsunoda","position":8,"seconds":82.405},{"id":"lawson","position":9,"seconds":82.653},{"id":"hamilton","position":10,"seconds":82.747},{"id":"perez","position":11,"seconds":82.715},{"id":"bearman","position":12,"seconds":84.192},{"id":"bottas","position":13,"seconds":83.177},{"id":"alonso","position":14,"seconds":82.71},{"id":"zhou","position":15,"seconds":83.473}]},{"number":62,"timings":[{"id":"max_verstappen","position":1,"seconds":81.069},{"id":"ocon","position":2,"seconds":82.036},{"id":"gasly","position":3,"seconds":82.097},{"id":"russell","position":4,"seconds":82.139},{"id":"leclerc","position":5,"seconds":81.789},{"id":"norris","position":6,"seconds":82.541},{"id":"piastri","position":7,"seconds":82.222},{"id":"tsunoda","position":8,"seconds":82.236},{"id":"lawson","position":9,"seconds":82.729},{"id":"hamilton","position":10,"seconds":82.583},{"id":"perez","position":11,"seconds":82.892},{"id":"bearman","position":12,"seconds":82.816},{"id":"bottas","position":13,"seconds":83.241},{"id":"alonso","position":14,"seconds":83.387},{"id":"zhou","position":15,"seconds":83.712}]},{"number":63,"timings":[{"id":"max_verstappen","position":1,"seconds":81.232},{"id":"ocon","position":2,"seconds":82.02},{"id":"gasly","position":3,"seconds":81.828},{"id":"russell","position":4,"seconds":81.908},{"id":"leclerc","position":5,"seconds":81.675},{"id":"norris","position":6,"seconds":81.791},{"id":"piastri","position":7,"seconds":82.216},{"id":"tsunoda","position":8,"seconds":82.047},{"id":"lawson","position":9,"seconds":82.292},{"id":"hamilton","position":10,"seconds":82.611},{"id":"perez","position":11,"seconds":82.373},{"id":"bearman","position":12,"seconds":82.837},{"id":"bottas","position":13,"seconds":83.015},{"id":"alonso","position":14,"seconds":83.931},{"id":"zhou","position":15,"seconds":83.453}]},{"number":64,"timings":[{"id":"max_verstappen","position":1,"seconds":81.029},{"id":"ocon","position":2,"seconds":81.771},{"id":"gasly","position":3,"seconds":81.707},{"id":"russell","position":4,"seconds":81.78},{"id":"leclerc","position":5,"seconds":81.682},{"id":"norris","position":6,"seconds":81.538},{"id":"piastri","position":7,"seconds":81.838},{"id":"tsunoda","position":8,"seconds":82.035},{"id":"lawson","position":9,"seconds":82.427},{"id":"hamilton","position":10,"seconds":82.197},{"id":"perez","position":11,"seconds":82.322},{"id":"bearman","position":12,"seconds":82.494},{"id":"bottas","position":13,"seconds":82.996},{"id":"alonso","position":14,"seconds":83.313},{"id":"zhou","position":15,"seconds":83.515}]},{"number":65,"timings":[{"id":"max_verstappen","position":1,"seconds":80.99},{"id":"ocon","position":2,"seconds":81.825},{"id":"gasly","position":3,"seconds":81.824},{"id":"russell","position":4,"seconds":81.851},{"id":"leclerc","position":5,"seconds":81.752},{"id":"norris","position":6,"seconds":81.64},{"id":"piastri","position":7,"seconds":81.84},{"id":"tsunoda","position":8,"seconds":81.955},{"id":"lawson","position":9,"seconds":82.515},{"id":"hamilton","position":10,"seconds":82.422},{"id":"perez","position":11,"seconds":82.22},{"id":"bearman","position":12,"seconds":82.981},{"id":"bottas","position":13,"seconds":82.903},{"id":"alonso","position":14,"seconds":83.676},{"id":"zhou","position":15,"seconds":83.058}]},{"number":66,"timings":[{"id":"max_verstappen","position":1,"seconds":81.111},{"id":"ocon","position":2,"seconds":81.914},{"id":"gasly","position":3,"seconds":81.645},{"id":"russell","position":4,"seconds":81.645},{"id":"leclerc","position":5,"seconds":82.131},{"id":"norris","position":6,"seconds":82.018},{"id":"piastri","position":7,"seconds":81.884},{"id":"tsunoda","position":8,"seconds":82.159},{"id":"lawson","position":9,"seconds":82.652},{"id":"hamilton","position":10,"seconds":82.731},{"id":"perez","position":11,"seconds":82.978},{"id":"bearman","position":12,"seconds":82.51},{"id":"bottas","position":13,"seconds":83.335},{"id":"alonso","position":14,"seconds":83.619},{"id":"zhou","position":15,"seconds":83.126}]},{"number":67,"timings":[{"id":"max_verstappen","position":1,"seconds":80.472},{"id":"ocon","position":2,"seconds":82.108},{"id":"gasly","position":3,"seconds":81.783},{"id":"russell","position":4,"seconds":81.886},{"id":"leclerc","position":5,"seconds":81.973},{"id":"norris","position":6,"seconds":81.517},{"id":"piastri","position":7,"seconds":82.286},{"id":"tsunoda","position":8,"seconds":81.894},{"id":"lawson","position":9,"seconds":82.123},{"id":"hamilton","position":10,"seconds":82.614},{"id":"perez","position":11,"seconds":82.143},{"id":"bearman","position":12,"seconds":82.533},{"id":"bottas","position":13,"seconds":82.877},{"id":"alonso","position":14,"seconds":84.475},{"id":"zhou","position":15,"seconds":83.991}]},{"number":68,"timings":[{"id":"max_verstappen","position":1,"seconds":81.554},{"id":"ocon","position":2,"seconds":81.964},{"id":"gasly","position":3,"seconds":81.877},{"id":"russell","position":4,"seconds":81.803},{"id":"leclerc","position":5,"seconds":81.664},{"id":"norris","position":6,"seconds":81.886},{"id":"piastri","position":7,"seconds":81.719},{"id":"tsunoda","position":8,"seconds":81.979},{"id":"lawson","position":9,"seconds":82.291},{"id":"hamilton","position":10,"seconds":82.104},{"id":"perez","position":11,"seconds":82.263},{"id":"bearman","position":12,"seconds":82.67},{"id":"bottas","position":13,"seconds":83.034},{"id":"alonso","position":14,"seconds":83.717},{"id":"zhou","position":15,"seconds":83.33}]},{"number":69,"timings":[{"id":"max_verstappen","position":1,"seconds":81.322},{"id":"ocon","position":2,"seconds":82.264},{"id":"gasly","position":3,"seconds":82.436},{"id":"russell","position":4,"seconds":82.189},{"id":"leclerc","position":5,"seconds":82.994},{"id":"norris","position":6,"seconds":81.819},{"id":"piastri","position":7,"seconds":81.532},{"id":"tsunoda","position":8,"seconds":81.828},{"id":"lawson","position":9,"seconds":82.533},{"id":"hamilton","position":10,"seconds":82.041},{"id":"perez","position":11,"seconds":82.222},{"id":"bearman","position":12,"seconds":82.839},{"id":"bottas","position":13,"seconds":83.003},{"id":"alonso","position":14,"seconds":83.919},{"id":"zhou","position":15,"seconds":83.701}]}],"pitStops":[{"id":"leclerc","lap":24,"seconds":24.573},{"id":"hulkenberg","lap":25,"seconds":25.369},{"id":"colapinto","lap":26,"seconds":25.287},{"id":"piastri","lap":27,"seconds":26.05},{"id":"alonso","lap":27,"seconds":25.107},{"id":"bearman","lap":27,"seconds":35.755},{"id":"hamilton","lap":27,"seconds":25.205},{"id":"sainz","lap":27,"seconds":25.92},{"id":"perez","lap":27,"seconds":26.692},{"id":"zhou","lap":27,"seconds":25.248},{"id":"russell","lap":28,"seconds":25.698},{"id":"norris","lap":28,"seconds":24.764},{"id":"tsunoda","lap":28,"seconds":25.431},{"id":"lawson","lap":28,"seconds":28.167},{"id":"hulkenberg","lap":27,"seconds":58.984},{"id":"zhou","lap":28,"seconds":27.067},{"id":"colapinto","lap":30,"seconds":25.239},{"id":"hulkenberg","lap":29,"seconds":28.59},{"id":"ocon","lap":32,"seconds":1408.695},{"id":"max_verstappen","lap":32,"seconds":1409.303},{"id":"zhou","lap":31,"seconds":1283.154},{"id":"gasly","lap":32,"seconds":1407.586},{"id":"norris","lap":32,"seconds":1415.363},{"id":"russell","lap":32,"seconds":1415.838},{"id":"tsunoda","lap":32,"seconds":1419.03},{"id":"leclerc","lap":32,"seconds":1420.26},{"id":"piastri","lap":32,"seconds":1420.27},{"id":"alonso","lap":32,"seconds":1422.872},{"id":"lawson","lap":32,"seconds":1422.722},{"id":"hamilton","lap":32,"seconds":1425.703},{"id":"bottas","lap":32,"seconds":1422.094},{"id":"sainz","lap":32,"seconds":1424.659},{"id":"perez","lap":32,"seconds":1425.215},{"id":"bearman","lap":32,"seconds":1424.737},{"id":"zhou","lap":32,"seconds":68.542}],"events":[{"lap":1,"kind":"start","title":"A race before the race","description":"An aborted start delays lights out on a wet Interlagos grid."},{"lap":28,"kind":"vsc","title":"Virtual safety car","description":"A VSC changes the pit-stop calculation as the rain intensifies."},{"lap":30,"kind":"safety","title":"Safety car deployed","description":"The field bunches up in difficult conditions."},{"lap":32,"kind":"red","title":"Red flag","description":"The race stops, and the order is reset for a rolling restart."},{"lap":33,"kind":"restart","title":"Rolling restart","description":"The field gets going again behind Ocon."},{"lap":39,"kind":"safety","title":"Safety car returns","description":"Another interruption bunches the field."},{"lap":42,"kind":"restart","title":"Safety car in","description":"The race returns to green-flag running."},{"lap":69,"kind":"finish","title":"Chequered flag","description":"Verstappen wins from P17; Alpine finishes second and third."}],"track":[[332.4,497.3],[334.7,506.4],[337.0,515.6],[340.2,527.7],[343.1,538.9],[346.7,552.1],[348.0,557.2],[352.9,575.3],[362.7,611.1],[366.7,625.1],[369.1,633.0],[371.0,639.0],[372.7,643.9],[376.4,653.6],[380.0,661.4],[384.1,668.7],[387.4,673.4],[392.9,679.2],[396.1,681.5],[400.7,683.8],[405.9,685.0],[409.7,685.0],[414.2,684.1],[417.7,682.6],[420.6,680.8],[424.6,678.0],[427.1,675.9],[430.3,673.0],[432.3,671.3],[434.8,669.1],[438.2,666.3],[443.5,662.4],[452.0,658.7],[457.4,658.2],[463.7,659.2],[471.8,662.2],[477.5,664.6],[481.6,666.2],[484.9,667.6],[490.6,669.7],[499.1,672.4],[506.0,673.9],[513.6,674.9],[518.9,675.2],[529.6,674.5],[536.5,673.6],[541.5,672.6],[557.1,666.9],[561.5,664.7],[567.5,661.1],[572.9,657.5],[586.3,645.6],[589.8,641.5],[600.7,626.0],[603.5,621.0],[607.3,612.8],[609.7,606.6],[615.7,586.2],[620.2,568.4],[623.3,556.3],[626.8,542.2],[630.3,527.9],[633.6,514.2],[637.5,497.5],[639.4,490.1],[641.6,480.7],[644.2,470.4],[647.3,458.1],[651.7,441.0],[653.2,435.2],[656.4,422.7],[658.6,414.0],[660.9,405.3],[664.9,389.8],[667.5,380.1],[669.8,371.6],[674.4,354.1],[676.7,345.4],[680.0,332.9],[683.0,322.0],[687.5,305.5],[689.9,297.2],[691.9,290.1],[694.0,282.2],[695.4,277.2],[697.6,266.3],[698.1,260.3],[698.1,253.2],[697.8,249.7],[696.8,243.7],[695.6,239.3],[694.0,235.3],[692.4,232.4],[690.0,229.0],[683.5,223.0],[680.9,221.2],[677.6,219.3],[670.0,216.0],[664.6,214.4],[661.0,213.5],[656.9,212.6],[654.0,212.0],[644.6,210.4],[634.0,209.8],[628.5,209.8],[622.0,209.8],[611.9,210.8],[605.5,211.9],[597.7,214.0],[589.2,217.4],[578.9,223.1],[567.9,231.2],[563.7,234.7],[559.1,238.9],[548.0,251.4],[543.7,257.4],[536.6,267.7],[529.8,277.3],[523.7,285.8],[519.2,292.3],[515.1,298.1],[503.2,314.9],[497.1,323.9],[492.0,331.5],[487.9,337.7],[475.9,355.5],[471.8,361.6],[466.8,369.0],[461.2,377.3],[453.8,387.9],[450.3,392.2],[446.0,397.2],[438.6,403.9],[430.0,409.6],[425.2,411.8],[419.8,413.7],[417.0,414.4],[410.2,415.8],[405.3,416.4],[399.9,416.6],[394.0,416.3],[387.8,415.4],[382.9,413.9],[375.5,410.9],[371.8,408.9],[367.7,406.4],[363.1,402.8],[358.9,398.7],[354.3,393.4],[350.2,387.6],[347.9,383.7],[344.0,375.6],[341.9,369.7],[340.4,364.7],[339.2,360.1],[337.1,350.3],[336.3,345.8],[335.0,336.7],[334.6,332.8],[334.4,328.0],[334.3,323.6],[334.8,316.7],[335.3,313.6],[336.1,310.1],[337.2,306.5],[339.7,300.7],[342.5,297.0],[344.5,295.3],[347.3,294.0],[351.8,293.2],[357.1,293.6],[360.8,294.7],[363.8,296.0],[367.2,297.6],[369.7,298.9],[375.1,301.8],[379.2,303.8],[384.4,305.9],[390.5,307.6],[396.1,308.1],[399.1,308.0],[400.5,307.8],[405.2,306.5],[408.5,304.7],[411.0,302.8],[413.2,300.6],[414.2,299.5],[416.0,296.4],[417.7,291.6],[418.3,287.6],[418.3,283.3],[417.8,279.5],[416.7,275.3],[415.0,270.7],[414.6,270.1],[411.0,263.2],[407.9,258.8],[404.7,254.7],[403.1,252.8],[396.7,245.9],[394.4,243.4],[389.5,238.2],[386.7,235.1],[382.6,230.4],[376.9,222.9],[373.7,218.4],[368.0,209.3],[365.2,203.9],[362.8,198.5],[360.9,193.8],[358.6,186.6],[357.4,180.5],[357.1,171.2],[357.7,165.4],[357.8,165.1],[359.3,160.6],[361.4,157.3],[363.3,155.4],[365.7,153.9],[368.4,153.2],[371.7,153.0],[373.9,153.2],[377.0,154.1],[379.0,154.9],[379.5,155.2],[382.2,156.5],[384.8,158.2],[386.6,159.6],[388.4,161.1],[389.6,162.2],[391.7,164.1],[395.3,168.0],[398.6,171.9],[401.0,175.0],[403.4,178.0],[405.9,181.4],[408.4,184.6],[411.4,188.5],[414.5,192.0],[420.0,197.8],[423.9,201.4],[431.6,206.3],[436.9,208.4],[443.7,210.4],[449.5,211.5],[454.2,212.1],[461.3,212.6],[467.9,212.3],[477.6,210.8],[480.6,210.0],[486.6,208.0],[495.4,204.1],[500.4,201.1],[506.8,196.4],[513.8,189.8],[518.5,184.4],[523.6,177.3],[529.3,168.4],[535.6,157.6],[541.4,147.8],[545.3,141.2],[547.9,136.7],[551.4,130.9],[554.9,124.7],[556.8,121.2],[560.0,115.4],[560.6,114.2],[562.9,109.3],[564.5,105.2],[565.4,102.4],[566.0,99.1],[566.3,92.1],[564.6,85.7],[563.0,82.2],[560.9,79.1],[558.8,76.6],[556.5,74.6],[554.4,73.0],[550.2,70.3],[548.7,69.5],[544.1,67.2],[541.1,65.8],[538.0,64.6],[534.4,63.2],[532.1,62.4],[522.7,59.4],[518.7,58.3],[510.0,56.4],[506.3,55.8],[499.1,55.2],[489.7,55.0],[482.2,55.2],[478.9,55.5],[462.4,57.8],[456.9,58.9],[450.6,60.4],[439.1,63.5],[433.4,65.3],[425.4,68.1],[416.7,71.7],[408.7,75.3],[401.4,79.0],[394.2,83.2],[388.6,86.7],[381.7,91.6],[369.9,101.6],[362.6,109.0],[350.9,123.4],[342.2,137.1],[336.0,149.3],[333.0,156.0],[328.9,166.3],[325.3,176.8],[321.8,188.6],[317.7,205.4],[315.2,216.7],[314.0,222.4],[310.2,239.6],[307.2,255.1],[305.7,264.9],[303.4,285.8],[302.7,293.9],[302.3,301.2],[301.9,312.3],[302.0,330.6],[303.0,351.8],[304.6,367.2],[306.3,379.4],[309.1,396.8],[310.8,406.1],[312.6,415.3],[315.5,428.6],[317.6,437.8],[319.5,445.9],[322.5,458.2],[325.8,471.4],[328.4,481.6],[329.4,485.7],[332.2,496.9],[332.4,497.3]],"corners":[{"number":1,"x":407.7,"y":685.1},{"number":2,"x":458.8,"y":658.7},{"number":3,"x":566.3,"y":661.7},{"number":4,"x":693.3,"y":233.7},{"number":5,"x":602.3,"y":212.5},{"number":6,"x":430.0,"y":409.3},{"number":7,"x":355.1,"y":394.2},{"number":8,"x":339.2,"y":301.8},{"number":9,"x":408.3,"y":304.9},{"number":10,"x":366.3,"y":153.6},{"number":11,"x":469.7,"y":212.0},{"number":12,"x":561.1,"y":79.3},{"number":13,"x":499.8,"y":55.3},{"number":14,"x":359.4,"y":112.5},{"number":15,"x":302.5,"y":343.5}],"drsZones":[{"number":1,"track":[[580.4,650.9],[582.6,648.9],[584.8,646.9],[587.0,644.8],[588.9,642.5],[590.7,640.2],[592.4,637.7],[594.2,635.3],[595.9,632.9],[597.6,630.4],[599.3,628.0],[601.0,625.5],[602.4,622.9],[603.8,620.3],[605.1,617.6],[606.3,614.9],[607.6,612.1],[608.6,609.4],[609.7,606.6],[610.6,603.7],[611.4,600.8],[612.2,598.0],[613.1,595.1],[613.9,592.3],[614.8,589.4],[615.6,586.5],[616.3,583.6],[617.1,580.8],[617.8,577.9],[618.5,575.0],[619.3,572.1],[620.0,569.2],[620.7,566.3],[621.5,563.4],[622.2,560.5],[623.0,557.6],[623.7,554.7],[624.4,551.8],[625.1,548.9],[625.8,546.0],[626.6,543.1],[627.3,540.2],[628.0,537.3],[628.7,534.4],[629.4,531.6],[630.1,528.7],[630.8,525.8],[631.5,522.9],[632.2,520.0],[632.9,517.1],[633.6,514.2],[634.3,511.2],[635.0,508.3],[635.6,505.4],[636.3,502.5],[637.0,499.6],[637.7,496.7],[638.4,493.8],[639.2,490.9],[639.9,488.0],[640.6,485.1],[641.2,482.2],[641.9,479.3],[642.7,476.4],[643.4,473.5],[644.1,470.7],[644.9,467.8],[645.6,464.9],[646.3,462.0],[647.1,459.1],[647.8,456.2],[648.5,453.3],[649.3,450.4],[650.0,447.5],[650.8,444.6],[651.5,441.7],[652.3,438.9],[653.0,436.0],[653.7,433.1],[654.5,430.2],[655.2,427.3],[656.0,424.4],[656.7,421.5],[657.4,418.6],[658.2,415.7],[658.9,412.8],[659.7,410.0],[660.4,407.1],[661.2,404.2],[661.9,401.3],[662.7,398.4],[663.4,395.5],[664.2,392.6],[664.9,389.7],[665.7,386.9],[666.5,384.0],[667.2,381.1],[668.0,378.2],[668.8,375.3],[669.6,372.5],[670.3,369.6],[671.1,366.7],[671.9,363.8],[672.6,360.9],[673.4,358.0],[674.1,355.1],[674.9,352.3],[675.6,349.4],[676.4,346.5],[677.2,343.6],[677.9,340.7],[678.7,337.8],[679.5,335.0],[680.2,332.1],[681.0,329.2],[681.8,326.3],[682.6,323.4],[683.4,320.6],[684.2,317.7],[685.0,314.8],[685.7,311.9],[686.5,309.0],[687.3,306.2],[688.1,303.3],[689.0,300.4],[689.8,297.6],[690.6,294.7],[691.4,291.8],[692.2,288.9],[693.0,286.1],[693.7,283.2],[694.5,280.3],[695.3,277.4],[695.9,274.5],[696.5,271.6],[697.1,268.7],[697.6,265.7],[697.9,262.8],[698.1,259.8],[698.1,256.8],[698.1,253.8],[697.9,250.8]]},{"number":2,"track":[[308.4,248.8],[307.8,251.7],[307.3,254.7],[306.8,257.6],[306.4,260.6],[305.9,263.6],[305.5,266.5],[305.2,269.5],[304.9,272.5],[304.5,275.5],[304.2,278.5],[303.9,281.4],[303.6,284.4],[303.3,287.4],[303.0,290.4],[302.7,293.4],[302.6,296.4],[302.4,299.4],[302.3,302.4],[302.2,305.3],[302.0,308.3],[301.9,311.3],[301.9,314.3],[301.9,317.3],[301.9,320.3],[302.0,323.3],[302.0,326.3],[302.0,329.3],[302.1,332.3],[302.2,335.3],[302.4,338.3],[302.5,341.3],[302.6,344.3],[302.8,347.3],[302.9,350.3],[303.2,353.3],[303.5,356.2],[303.8,359.2],[304.1,362.2],[304.4,365.2],[304.7,368.2],[305.1,371.1],[305.6,374.1],[306.0,377.1],[306.4,380.0],[306.9,383.0],[307.4,386.0],[307.8,388.9],[308.3,391.9],[308.8,394.8],[309.3,397.8],[309.8,400.7],[310.4,403.7],[310.9,406.6],[311.5,409.6],[312.1,412.5],[312.6,415.5],[313.3,418.4],[313.9,421.3],[314.6,424.2],[315.2,427.2],[315.8,430.1],[316.5,433.0],[317.2,435.9],[317.8,438.9],[318.5,441.8],[319.2,444.7],[319.9,447.6],[320.6,450.5],[321.3,453.4],[322.0,456.3],[322.8,459.3],[323.5,462.2],[324.2,465.1],[324.9,468.0],[325.7,470.9],[326.4,473.8],[327.2,476.7],[327.9,479.6],[328.6,482.5],[329.3,485.4],[330.1,488.3],[330.8,491.2],[331.5,494.1],[332.3,497.0],[333.1,499.9],[333.8,502.8],[334.5,505.7],[335.3,508.6],[336.0,511.6],[336.7,514.5],[337.5,517.4],[338.2,520.3],[339.0,523.2],[339.8,526.1],[340.5,529.0],[341.3,531.9],[342.0,534.8],[342.8,537.7],[343.6,540.6],[344.3,543.4],[345.1,546.3],[345.9,549.2],[346.7,552.1],[347.4,555.0],[348.2,557.9],[349.0,560.8],[349.8,563.7],[350.5,566.6],[351.3,569.5],[352.1,572.4],[352.9,575.3],[353.7,578.2],[354.5,581.1],[355.3,584.0],[356.1,586.8],[356.9,589.7],[357.6,592.6],[358.4,595.5],[359.2,598.4],[360.0,601.3],[360.8,604.2],[361.6,607.1],[362.4,610.0],[363.2,612.9],[364.0,615.7],[364.8,618.6],[365.7,621.5],[366.5,624.4],[367.4,627.3],[368.2,630.1],[369.1,633.0],[370.0,635.9],[370.9,638.7],[371.9,641.5],[372.9,644.4],[373.9,647.2],[375.0,650.0],[376.1,652.8],[377.3,655.5],[378.5,658.2],[379.8,661.0],[381.2,663.6],[382.7,666.2],[384.2,668.8],[385.9,671.3],[387.7,673.7],[389.7,675.9],[391.8,678.0]]}],"sources":{"laps":"https://api.jolpi.ca/ergast/f1/2024/21/laps/","results":"https://api.jolpi.ca/ergast/f1/2024/21/results/","pitStops":"https://api.jolpi.ca/ergast/f1/2024/21/pitstops/","raceControl":"https://api.openf1.org/v1/race_control?session_key=9636","track":"https://api.openf1.org/v1/location?session_key=9636&driver_number=1","corners":"https://api.multiviewer.app/api/v1/circuits/14/2024","circuitMap":"https://www.fia.com/sites/default/files/decision-document/2024%20S%C3%A3o%20Paulo%20Grand%20Prix%20-%20Event%20Notes%20-%20Circuit%20Map,%20Pit%20Lane%20Drawing%20and%20Red%20Zones.pdf"}} diff --git a/src/lib/site-navigation.ts b/src/lib/site-navigation.ts index 51867ea..bcfc8a6 100644 --- a/src/lib/site-navigation.ts +++ b/src/lib/site-navigation.ts @@ -20,6 +20,7 @@ export const siteGroups = [ label: "Beyond work", items: [ { href: "/coffee", label: "Coffee map" }, + { href: "/race-replay", label: "Race Replay" }, { href: "/travel", label: "Trips & interests" }, { href: "/october", label: "October, forever" }, ], diff --git a/src/styles/globals.css b/src/styles/globals.css index ff07eb0..6d42916 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -489,17 +489,6 @@ button { height: 100% !important; object-fit: cover; } -.studio-product-logo { - position: absolute; - inset: 0; - display: grid; - place-items: center; - background: #0004; -} -.studio-product-logo img { - width: 43%; - height: auto; -} .studio-project-title { display: flex; align-items: center; @@ -1002,33 +991,119 @@ button { position: absolute; inset: 0; overflow: hidden; + padding: 4.5%; + background: radial-gradient(circle at 85% 10%, #313d65 0, #1d2339 42%, #151923 100%); } -.manyangles-preview > img { - object-fit: cover; - object-position: 50% 40%; -} -.manyangles-lockup { - position: absolute; - inset: 0; +.manyangles-demo { display: flex; - align-items: flex-end; - gap: 10px; - padding: 6%; - color: white; - background: linear-gradient(transparent 45%, #101e33b3); + flex-direction: column; + height: 100%; + overflow: hidden; + border: 1px solid #ffffff22; + border-radius: 10px; + background: #1b1e2a; + box-shadow: 0 14px 35px #05091460; + color: #f5f5f7; + font-family: Arial, Helvetica, sans-serif; } -.manyangles-lockup svg { - width: 9%; - max-width: 42px; - flex-shrink: 0; +.manyangles-demo-bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-height: 13%; + padding: 0 4%; + border-bottom: 1px solid #ffffff1a; + font-size: clamp(8px, .9vw, 12px); } -.manyangles-lockup span { +.manyangles-demo-bar-title, +.manyangles-demo-share, +.manyangles-demo-bottom > span:first-child { + display: inline-flex; + align-items: center; + gap: 7px; +} +.manyangles-demo-bar-title { font-weight: 700; } +.manyangles-demo-share { color: #b8c6ff; } +.manyangles-demo-content { + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; + padding: 5% 6% 4%; +} +.manyangles-demo-intro > span { + display: block; + margin-bottom: 2.5%; + color: #a7b7f3; + font-size: clamp(7px, .7vw, 10px); font-weight: 700; - font-variation-settings: "SHRP" 70; - font-size: clamp(24px, 3vw, 36px); - letter-spacing: -0.04em; + letter-spacing: .18em; +} +.manyangles-demo-intro strong { + display: block; + font-size: clamp(17px, 2.1vw, 31px); + letter-spacing: -.035em; line-height: 1.1; } +.manyangles-demo-intro p { + margin: 2.2% 0 0; + color: #b9bdca; + font-size: clamp(8px, .9vw, 12px); + line-height: 1.3; +} +.manyangles-demo-photos { + display: grid; + flex: 1; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 2%; + min-height: 0; + margin-top: 5%; +} +.manyangles-demo-photo { + position: relative; + overflow: hidden; + border-radius: 5px; + background: linear-gradient(165deg, var(--tile-sky), var(--tile-horizon)); +} +.manyangles-demo-photo::before, +.manyangles-demo-photo::after { + position: absolute; + content: ""; + inset: auto -20% -12%; + height: 65%; + border-radius: 48% 52% 0 0; + background: var(--tile-hill); + transform: rotate(-11deg); +} +.manyangles-demo-photo::after { + inset: auto -25% -35%; + height: 70%; + background: var(--tile-foreground); + transform: rotate(14deg); +} +.manyangles-demo-photo span { + position: absolute; + top: 18%; + right: 19%; + width: 17%; + aspect-ratio: 1; + border-radius: 50%; + background: var(--tile-sun); + box-shadow: 0 0 20px 5px var(--tile-sun); +} +.manyangles-demo-photo--one { --tile-sky: #f0baa3; --tile-horizon: #e5d8c6; --tile-hill: #769b99; --tile-foreground: #3d646f; --tile-sun: #fff0b8; } +.manyangles-demo-photo--two { --tile-sky: #a7bbd9; --tile-horizon: #d8d2bb; --tile-hill: #81948c; --tile-foreground: #576d6d; --tile-sun: #f8e0ae; } +.manyangles-demo-photo--three { --tile-sky: #d9b5be; --tile-horizon: #e8cdb6; --tile-hill: #9f998c; --tile-foreground: #6b797a; --tile-sun: #fff2ce; } +.manyangles-demo-photo--four { --tile-sky: #9eb6cf; --tile-horizon: #cddbd5; --tile-hill: #6c958c; --tile-foreground: #3f6c70; --tile-sun: #f6e9bf; } +.manyangles-demo-bottom { + display: flex; + justify-content: space-between; + gap: 10px; + margin-top: 4%; + color: #b9bdca; + font-size: clamp(7px, .75vw, 10px); +} .type-study-controls { padding: 22px 0; @@ -2745,17 +2820,6 @@ body { } } -/* Shared product branding over the existing product imagery. */ -.studio-product-logo { - background: radial-gradient(ellipse at center, #071c2660, #071c261c 75%); -} -.studio-product-logo img { - filter: drop-shadow(0 2px 3px #0009) drop-shadow(0 8px 18px #0008); -} -.beenvoice-logo img { - width: 55%; - filter: brightness(0) invert(1) drop-shadow(0 2px 3px #0009) drop-shadow(0 8px 18px #0008); -} .mobile-menu-label, .primary-links .header-dropdown .mobile-menu-link { display: none; @@ -2794,18 +2858,6 @@ body { .nav-inner, .primary-links { gap: 8px; } } -/* Brand marks share a centered, shadowed treatment. */ -.manyangles-lockup { - inset: 0; - justify-content: center; - align-items: center; - padding: 0; - background: radial-gradient(ellipse at center, #071c2660, #071c261c 75%); -} -.manyangles-lockup svg, -.manyangles-lockup span { - filter: drop-shadow(0 2px 3px #0009) drop-shadow(0 8px 18px #0008); -} .hadlock-brand img { width: min(100%, 340px); height: auto; } /* Personal postcards: photographs, editorial captions, a playful calendar. */ @@ -2942,28 +2994,17 @@ body { --accent-foreground: 199 47% 20%; } -/* One proportional logo height, independent of each wordmark's width. */ -.studio-project-image { - container-type: inline-size; - --project-logo-height: 8cqw; -} -.studio-product-logo img, -.beenvoice-logo img { - width: auto; - height: var(--project-logo-height); - max-width: 84%; - object-fit: contain; -} -.manyangles-lockup { gap: 2cqw; } -.manyangles-lockup svg { - width: var(--project-logo-height); - height: var(--project-logo-height); - max-width: none; -} -.manyangles-lockup span { - /* Match visible capital height to the SVG wordmarks. */ - font-size: 10.5cqw; - line-height: 1; +/* Brand identity sits in the caption below each unobstructed project image. */ +.project-brand { display: inline-flex; align-items: center; gap: 8px; min-height: 30px; max-width: 100%; line-height: 1; } +.project-brand > img { display: block; flex: none; width: auto; height: 27px; max-width: min(45vw, 190px); object-fit: contain; } +.project-brand--racetix > img { filter: brightness(0); } +.project-brand--beenvoice > img { height: 25px; filter: brightness(0); } +.project-brand--puter > img { width: 30px; height: 30px; border-radius: 7px; } +.project-brand--manyangles { font-size: 24px; font-weight: 700; letter-spacing: -.04em; } +.project-brand--manyangles svg { flex: none; width: 29px; height: 29px; } +@media (prefers-color-scheme: dark) { + .project-brand--racetix > img, + .project-brand--beenvoice > img { filter: brightness(0) invert(1); } } /* Contact and footer use a single shared backdrop. */ @@ -2977,6 +3018,56 @@ body { .archive-image > img { object-fit: contain; object-position: center; } .archive-image--photo > img { object-fit: cover; } +/* Puter's real macOS captures, paired with its compiled app icon. */ +.puter .studio-project-image { background: #d7e8df; } +.puter-preview { + position: absolute; + inset: 0; + overflow: hidden; + background: radial-gradient(circle at 80% 5%, #eff9f1, #c6dfd0 85%); +} +.puter-preview--dark { background: radial-gradient(circle at 80% 5%, #31574a, #11332a 85%); } +.puter-preview .puter-preview-screen, +.archive-image .puter-preview .puter-preview-screen { + position: absolute !important; + inset: 7% 5% auto !important; + width: 90% !important; + height: 86% !important; + margin: 0 !important; + border: 1px solid #28564222; + border-radius: 9px; + object-fit: cover !important; + object-position: top center; + box-shadow: 0 15px 28px #15352830; +} +.puter-preview--interactive .puter-preview-screen, +.archive-image .puter-preview--interactive .puter-preview-screen { height: 76% !important; } +.puter-preview-controls { + position: absolute; + right: 4%; + bottom: 4%; + left: 4%; + display: flex; + justify-content: center; + gap: 5px; + overflow-x: auto; + scrollbar-width: none; +} +.puter-preview-controls button { + flex: none; + padding: 4px 8px; + border: 1px solid #52756755; + border-radius: 999px; + background: #ffffffb8; + color: #174634; + font-size: 10px; + line-height: 1.2; + cursor: pointer; +} +.puter-preview-controls button[aria-pressed="true"] { background: #0c5d40; border-color: #0c5d40; color: #fff; } +.puter-preview--dark .puter-preview-controls button { background: #173a2f; color: #effcf3; } +.puter-preview--dark .puter-preview-controls button[aria-pressed="true"] { background: #c0efd3; color: #0a3021; } + /* September 10's full-width mobile header panel. */ .restored-header-menu { width: 100vw;