+ Need at least two drivers in the standings to compare a rivalry.
+
+ )
+ }
+
+ const colorA = teamColor(a.team_colour)
+ const colorB = teamColor(b.team_colour)
+ const sameTeam = a.team_name === b.team_name
+
+ const cumA = a.cumulative ?? []
+ const cumB = b.cumulative ?? []
+ const rounds = Math.min(cumA.length, cumB.length)
+ const gaps = gapSeries(cumA, cumB)
+ const lastGap = gaps[gaps.length - 1] ?? 0
+ const strip = tally ? lastRounds(tally, 5) : []
+
+ const pickers = (
+
+ {pickers}
+
+
+
+
+ {a.name_acronym}
+
+
+ {tally ? `${tally.a}–${tally.b}` : '—'}
+
+
+ {b.name_acronym}
+
+
+
+ Race head-to-head · {tally?.rounds.length ?? 0} round{(tally?.rounds.length ?? 0) === 1 ? '' : 's'} counted
+ {tally && tally.skipped > 0 ? ` · ${tally.skipped} skipped` : ''}
+
+ {strip.length > 0 && (
+
+ Last {strip.length}
+ {strip.map((r) => (
+
+ {r.label}
+
+ {r.winner === 'a' ? a.name_acronym : b.name_acronym}
+
+
+ ))}
+
+ )}
+
+
+
+
+
+ Points race — {a.name_acronym} vs {b.name_acronym}
+
+
+ Rounds 1–{rounds} · {hub.season}
+
+
+
+
+
+
+
+
+
+
+ Gap over season — {a.name_acronym} − {b.name_acronym}
+
+ {gapCaption}
+
+
+
+
+
+ Above the zero line: {a.name_acronym} ahead. Below: {b.name_acronym} ahead.
+
+
+
+ )
+}
diff --git a/frontend/src/lib/rivalry.ts b/frontend/src/lib/rivalry.ts
new file mode 100644
index 0000000..f5b4092
--- /dev/null
+++ b/frontend/src/lib/rivalry.ts
@@ -0,0 +1,61 @@
+// Pure helpers for the championship rivalry compare view.
+
+/** One round that counted toward the head-to-head tally. */
+export interface H2HRound {
+ round: number // 1-based season round
+ label: string
+ posA: number
+ posB: number
+ winner: 'a' | 'b'
+}
+
+export interface H2HTally {
+ a: number
+ b: number
+ /** Rounds counted in the tally, in season order. */
+ rounds: H2HRound[]
+ /** Rounds skipped because either driver had no finishing position. */
+ skipped: number
+}
+
+/**
+ * Per-round points gap (a − b) across the rounds both cumulative series
+ * cover. Positive values mean driver A is ahead.
+ */
+export function gapSeries(a: number[], b: number[]): number[] {
+ const n = Math.min(a.length, b.length)
+ const out: number[] = []
+ for (let i = 0; i < n; i++) out.push(a[i] - b[i])
+ return out
+}
+
+/**
+ * Race head-to-head: per round, the lower finishing position wins. Rounds
+ * where either driver has no position (0 / missing) are skipped from the
+ * tally, as are equal positions (defensive — races can't tie).
+ */
+export function h2hTally(posA: number[], posB: number[], labels: string[]): H2HTally {
+ const n = Math.max(posA.length, posB.length)
+ const rounds: H2HRound[] = []
+ let a = 0
+ let b = 0
+ let skipped = 0
+ for (let i = 0; i < n; i++) {
+ const pa = posA[i] ?? 0
+ const pb = posB[i] ?? 0
+ if (pa <= 0 || pb <= 0 || pa === pb) {
+ skipped++
+ continue
+ }
+ const winner = pa < pb ? 'a' : 'b'
+ if (winner === 'a') a++
+ else b++
+ rounds.push({ round: i + 1, label: labels[i] ?? `R${i + 1}`, posA: pa, posB: pb, winner })
+ }
+ return { a, b, rounds, skipped }
+}
+
+/** Last n counted rounds of a tally, for the mini strip. */
+export function lastRounds(tally: H2HTally, n: number): H2HRound[] {
+ return tally.rounds.slice(-n)
+}
diff --git a/frontend/src/pages/ChampionshipPage.tsx b/frontend/src/pages/ChampionshipPage.tsx
index b922202..f26253b 100644
--- a/frontend/src/pages/ChampionshipPage.tsx
+++ b/frontend/src/pages/ChampionshipPage.tsx
@@ -5,10 +5,11 @@ import { fetchChampionshipHub, fetchSeasons } from '../api'
import { teamColor } from '../utils'
import type { ChampHubDriver, ChampionshipHub } from '../types'
import { ChampionshipSimulator } from '../components/ChampionshipSimulator'
+import { RivalryCompare } from '../components/RivalryCompare'
import { Meaning } from '../components/Meaning'
import { pointsGapMeaning } from '../lib/meaning'
-type View = 'drivers' | 'constructors' | 'progression' | 'simulator'
+type View = 'drivers' | 'constructors' | 'progression' | 'rivalry' | 'simulator'
const GOLD = '#ffd700'
const SILVER = '#c0c0c0'
@@ -214,6 +215,14 @@ function ChampionshipBody({ hub, view, setView }: BodyProps) {
>
Progression
+