Skip to content

Commit 2d35f67

Browse files
valvesssclaude
andcommitted
feat(odds): model vs market — observer-invariance of the skew law (Box A/B)
New /odds page + market overlay on the World Cup ledger. For each knockout favourite bet we now have three readings: our odds-free Elo p, a real bookmaker's de-vigged p (frozen pre-match line), and the realised outcome. - Box A (descriptive): de-vig real FanDuel 3-way lines (Shin) → market p for the SAME bet; apply the law (1-2p)/sqrt(p(1-p)) to both. Both land on one curve — agreement (rank corr 0.998) is the headline, not edge. The /odds page plots the law with model (open) and market (filled) points per match. - Box B (calibration): model vs book Brier/log-loss (0.272/0.740 vs 0.300/0.813; n=3, two penalty lotteries — noise, real at n=16). - Box C (value/edge): the p_model - p_book gap is shown as a diagnostic only, deliberately NOT staked — keeps the study descriptive, not an alpha claim. Book odds hand-recorded + sourced + frozen in study/wc_book_odds.json (no API). skewlib/worldcup.py: load_book_odds + book_pfav (de-vig + favourite column). predict_worldcup.py freezes a per-entry book block (never rewritten) + emits triangulation + model_vs_book into wc_live.json. Docs in WORLDCUP.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent fc62469 commit 2d35f67

10 files changed

Lines changed: 479 additions & 1 deletion

File tree

site/src/components/Nav.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const links = [
1010
['Reproduce', '#reproduce'],
1111
['World Cup', '/worldcup'],
1212
['Goals', '/goals'],
13+
['Odds', '/odds'],
1314
];
1415
---
1516
<nav class="nav">

site/src/data/wc_live.json

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,83 @@
361361
"resolved_p_fav": 0.5534,
362362
"resolved_hit_rate": 0.3333
363363
},
364+
"triangulation": [
365+
{
366+
"home": "Brazil",
367+
"away": "Japan",
368+
"home_flag": "br",
369+
"away_flag": "jp",
370+
"fav_team": "Brazil",
371+
"fav_pick": "H",
372+
"p_model": 0.556,
373+
"p_book": 0.5625,
374+
"skew_model": -0.2255,
375+
"skew_book": -0.252,
376+
"o_model": 1.7985,
377+
"o_book": 1.7143,
378+
"overround": 0.0534,
379+
"edge": -0.0065,
380+
"book": "FanDuel",
381+
"fav_won": true,
382+
"score": "2-1",
383+
"pens": false,
384+
"advanced": "Brazil"
385+
},
386+
{
387+
"home": "Germany",
388+
"away": "Paraguay",
389+
"home_flag": "de",
390+
"away_flag": "py",
391+
"fav_team": "Germany",
392+
"fav_pick": "H",
393+
"p_model": 0.6233,
394+
"p_book": 0.7423,
395+
"skew_model": -0.5087,
396+
"skew_book": -1.108,
397+
"o_model": 1.6045,
398+
"o_book": 1.3125,
399+
"overround": 0.0451,
400+
"edge": -0.119,
401+
"book": "FanDuel",
402+
"fav_won": false,
403+
"score": "1-1",
404+
"pens": true,
405+
"advanced": "Paraguay"
406+
},
407+
{
408+
"home": "Netherlands",
409+
"away": "Morocco",
410+
"home_flag": "nl",
411+
"away_flag": "ma",
412+
"fav_team": "Netherlands",
413+
"fav_pick": "H",
414+
"p_model": 0.4809,
415+
"p_book": 0.3981,
416+
"skew_model": 0.0764,
417+
"skew_book": 0.4162,
418+
"o_model": 2.0794,
419+
"o_book": 2.4,
420+
"overround": 0.053,
421+
"edge": 0.0828,
422+
"book": "FanDuel",
423+
"fav_won": false,
424+
"score": "1-1",
425+
"pens": true,
426+
"advanced": "Morocco"
427+
}
428+
],
429+
"model_vs_book": {
430+
"n": 3,
431+
"p_model_mean": 0.5534,
432+
"p_book_mean": 0.5676,
433+
"hit_rate": 0.3333,
434+
"brier_model": 0.2723,
435+
"brier_book": 0.3003,
436+
"logloss_model": 0.7397,
437+
"logloss_book": 0.813,
438+
"agree_corr": 0.9984,
439+
"mean_abs_edge": 0.0694
440+
},
364441
"resolved_detail": [
365442
{
366443
"home": "Brazil",

site/src/data/wc_predictions.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@
2727
"note": "Martinelli 96', won in regulation",
2828
"source": "ESPN/FIFA/CBS",
2929
"settled_asof": "2026-06-28"
30+
},
31+
"book": {
32+
"p_fav": 0.5625,
33+
"o_fav": 1.7143,
34+
"skew": -0.252,
35+
"overround": 0.0534,
36+
"edge": -0.0065,
37+
"book": "FanDuel",
38+
"source": "CBS Sports / ESPN (pre-match line, 2026-06-29)",
39+
"asof": "2026-06-29"
3040
}
3141
},
3242
{
@@ -53,6 +63,16 @@
5363
"note": "1-1 after 120'; Paraguay won 4-3 on penalties — Germany's first-ever WC shootout loss",
5464
"source": "ESPN/CBS/VAVEL",
5565
"settled_asof": "2026-06-28"
66+
},
67+
"book": {
68+
"p_fav": 0.7423,
69+
"o_fav": 1.3125,
70+
"skew": -1.108,
71+
"overround": 0.0451,
72+
"edge": -0.119,
73+
"book": "FanDuel",
74+
"source": "CBS Sports / FanDuel Sportsbook (pre-match line, 2026-06-29)",
75+
"asof": "2026-06-29"
5676
}
5777
},
5878
{
@@ -79,6 +99,16 @@
7999
"note": "1-1 after 120'; Morocco won 3-2 on penalties (Bounou save, Saibari decisive)",
80100
"source": "ESPN/Yahoo/Outlook",
81101
"settled_asof": "2026-06-28"
102+
},
103+
"book": {
104+
"p_fav": 0.3981,
105+
"o_fav": 2.4,
106+
"skew": 0.4162,
107+
"overround": 0.053,
108+
"edge": 0.0828,
109+
"book": "FanDuel",
110+
"source": "CBS Sports / FanDuel Sportsbook (pre-match line, 2026-06-29)",
111+
"asof": "2026-06-29"
82112
}
83113
},
84114
{

site/src/pages/odds.astro

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
---
2+
import Base from '../layouts/Base.astro';
3+
import Footer from '../components/Footer.astro';
4+
import wcl from '../data/wc_live.json';
5+
6+
const T = wcl.triangulation || [];
7+
const M = wcl.model_vs_book || null;
8+
const pct = (x) => Math.round(x * 100) + '%';
9+
const sgn = (x) => (x >= 0 ? '+' : '') + x.toFixed(2);
10+
11+
// ── the structural law, plotted: skew = (1−2p)/√(p(1−p)) ──
12+
const law = (p) => (1 - 2 * p) / Math.sqrt(p * (1 - p));
13+
const P0 = 0.33, P1 = 0.95, S0 = 0.7, S1 = -1.25; // axis domains
14+
const L = 64, Rr = 688, Tp = 34, Bt = 320; // plot box (viewBox 720×372)
15+
const X = (p) => L + ((p - P0) / (P1 - P0)) * (Rr - L);
16+
const Y = (s) => Tp + ((S0 - s) / (S0 - S1)) * (Bt - Tp);
17+
// law polyline
18+
const curve = Array.from({ length: 125 }, (_, i) => {
19+
const p = P0 + (i / 124) * (P1 - P0);
20+
return `${X(p).toFixed(1)},${Y(law(p)).toFixed(1)}`;
21+
}).join(' ');
22+
// axis ticks
23+
const pTicks = [0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95];
24+
const sTicks = [0.5, 0, -0.5, -1.0];
25+
---
26+
<Base title="Model vs Market — the law is observer-invariant | skew-dynamic"
27+
description="Our odds-free Elo and a real bookmaker price the same World Cup bets. Two probabilities, one law: both land on (1−2p)/√(p(1−p)). And whose p was better calibrated."
28+
path="/odds">
29+
<header class="o-top">
30+
<div class="container o-top-inner">
31+
<a class="brand" href="/"><span class="brand-mark"></span>skew<span class="brand-dim">-dynamic</span></a>
32+
<div class="o-top-links">
33+
<a href="/worldcup">World Cup</a><a href="/goals">Goals</a><a href="/retro">Retro</a>
34+
<a href="https://github.com/cold-code-labs/skew-dynamic" target="_blank" rel="noopener">GitHub ↗</a>
35+
</div>
36+
</div>
37+
</header>
38+
39+
<article class="container o-wrap">
40+
<div class="eyebrow reveal">● Model vs Market · same bet, two prices, one law</div>
41+
<h1 class="reveal">Our odds, the bookmaker's odds, and the law that holds for both.</h1>
42+
<p class="o-lede reveal">For these knockout games we have <em>three</em> readings of each favourite bet:
43+
our <b>odds-free Elo</b> probability (results only — no market), the <b>bookmaker's</b> de-vigged
44+
probability (a real published line, frozen pre-match), and the <b>realised</b> outcome. The structural
45+
claim is not that we beat the market. It is sharper and stranger: <strong>whatever probability you
46+
believe, the skewness of the bet is forced</strong> — both our number and the market's land on the same
47+
curve <span class="mono">(1−2p)/√(p(1−p))</span>. They disagree on <em>where</em> on the curve a game sits,
48+
never on the curve itself.</p>
49+
50+
{M && <div class="o-scoreboard reveal">
51+
<div class="o-sb-card">
52+
<span class="o-sb-k">Agreement (rank corr)</span>
53+
<b class="o-sb-v pos">{M.agree_corr.toFixed(3)}</b>
54+
<span class="o-sb-d">our p vs the market's p — near-identical ordering</span>
55+
</div>
56+
<div class="o-sb-card">
57+
<span class="o-sb-k">Mean disagreement</span>
58+
<b class="o-sb-v">{pct(M.mean_abs_edge)}</b>
59+
<span class="o-sb-d">average gap in level (|p<sub>model</sub> − p<sub>book</sub>|)</span>
60+
</div>
61+
<div class="o-sb-card">
62+
<span class="o-sb-k">Brier — model vs book</span>
63+
<b class="o-sb-v"><span class={M.brier_model<=M.brier_book?'pos':''}>{M.brier_model.toFixed(3)}</span> <i>/</i> {M.brier_book.toFixed(3)}</b>
64+
<span class="o-sb-d">lower is better · n = {M.n} (far too few to conclude)</span>
65+
</div>
66+
<div class="o-sb-card">
67+
<span class="o-sb-k">Log-loss — model vs book</span>
68+
<b class="o-sb-v"><span class={M.logloss_model<=M.logloss_book?'pos':''}>{M.logloss_model.toFixed(3)}</span> <i>/</i> {M.logloss_book.toFixed(3)}</b>
69+
<span class="o-sb-d">same direction — our humility on the upsets paid</span>
70+
</div>
71+
</div>}
72+
73+
<!-- ── the law curve with model (○) and market (●) points ── -->
74+
<figure class="o-fig reveal">
75+
<svg viewBox="0 0 720 372" role="img" aria-label="Skewness law with model and bookmaker points">
76+
<!-- grid -->
77+
{sTicks.map(s => <g>
78+
<line x1={L} y1={Y(s)} x2={Rr} y2={Y(s)} class={s===0?'ax0':'grid'} />
79+
<text x={L-10} y={Y(s)+4} class="tk" text-anchor="end">{sgn(s)}</text>
80+
</g>)}
81+
{pTicks.map(p => <g>
82+
<line x1={X(p)} y1={Tp} x2={X(p)} y2={Bt} class={Math.abs(p-0.5)<1e-9?'ax0':'grid'} />
83+
<text x={X(p)} y={Bt+18} class="tk" text-anchor="middle">{p.toFixed(2)}</text>
84+
</g>)}
85+
<text x={(L+Rr)/2} y={362} class="axlab" text-anchor="middle">favourite probability p</text>
86+
<text x={16} y={(Tp+Bt)/2} class="axlab" text-anchor="middle" transform={`rotate(-90 16 ${(Tp+Bt)/2})`}>return skewness</text>
87+
<!-- the law -->
88+
<polyline points={curve} class="law" />
89+
<text x={X(0.9)} y={Y(law(0.9))-10} class="lawlab" text-anchor="end">(1−2p)/√(p(1−p))</text>
90+
<!-- per-match: connect model ○ and book ● (both on the curve) -->
91+
{T.map(t => <g>
92+
<line x1={X(t.p_model)} y1={Y(t.skew_model)} x2={X(t.p_book)} y2={Y(t.skew_book)}
93+
class={`conn ${t.fav_won?'ok':'no'}`} />
94+
<circle cx={X(t.p_model)} cy={Y(t.skew_model)} r="6" class={`pt model ${t.fav_won?'ok':'no'}`} />
95+
<circle cx={X(t.p_book)} cy={Y(t.skew_book)} r="6" class={`pt book ${t.fav_won?'ok':'no'}`} />
96+
<text x={X(t.p_book)} y={Y(t.skew_book)+ (t.skew_book < t.skew_model ? 20 : -12)} class="ptlab" text-anchor="middle">{t.fav_team}</text>
97+
</g>)}
98+
</svg>
99+
<figcaption>
100+
<span class="lg"><span class="dot model"></span> our Elo (results-only)</span>
101+
<span class="lg"><span class="dot book"></span> {T[0]?.book || 'bookmaker'} (de-vigged)</span>
102+
<span class="lg"><span class="bar ok"></span> favourite advanced</span>
103+
<span class="lg"><span class="bar no"></span> favourite out</span>
104+
</figcaption>
105+
</figure>
106+
107+
<!-- ── triangulation table ── -->
108+
<div class="o-tbl reveal">
109+
<div class="o-tr o-th">
110+
<span>Match</span><span>p model</span><span>p market</span><span>skew model</span><span>skew market</span><span>outcome</span>
111+
</div>
112+
{T.map(t => <div class={`o-tr ${t.fav_won?'ok':'no'}`}>
113+
<span class="o-m">
114+
{t.home_flag && <img src={`https://flagcdn.com/w20/${t.home_flag}.png`} alt="" width="18" height="12" />}
115+
{t.fav_team}<i> fav</i>
116+
</span>
117+
<span class="mono">{t.p_model.toFixed(3)}</span>
118+
<span class="mono">{t.p_book.toFixed(3)} <em class={t.edge>=0?'up':'dn'}>{t.edge>=0?'+':''}{(t.edge*100).toFixed(1)}</em></span>
119+
<span class="mono">{sgn(t.skew_model)}</span>
120+
<span class="mono">{sgn(t.skew_book)}</span>
121+
<span class="o-o">{t.fav_won ? '✓ won' : (t.pens ? '✗ pens' : '✗ upset')} <i>{t.score}</i></span>
122+
</div>)}
123+
</div>
124+
125+
<section class="o-body reveal">
126+
<h2>What the three games say</h2>
127+
<p><b>Brazil — the market and the model are the same bet.</b> Our p = 0.556, the book's de-vigged
128+
p = 0.562: a six-thousandths gap. Both put Brazil just past a coin-flip, both imply a mildly
129+
left-skewed bet (−0.23 / −0.25), and the favourite won. When everyone agrees, the law is invisible
130+
— it just sits there being satisfied.</p>
131+
<p><b>Germany — the market was the over-confident one.</b> The book priced Germany at <b>0.742</b>;
132+
our results-only Elo, at <b>0.623</b>, was far more humble. That gap is the whole story: a higher p
133+
means a <em>more</em> left-skewed bet — skew <b>−1.11</b> on the market's number vs −0.51 on ours, i.e.
134+
"wins small, rarely loses <b>catastrophically</b>." Germany lost on penalties. The catastrophic tail
135+
the market's own skew was screaming about is exactly the one that hit — and our lower p ate a smaller
136+
loss in the scorecard.</p>
137+
<p><b>Netherlands — both called the toss-up, the market more loudly.</b> Here we were the bolder one
138+
(0.481) and the book the cautious one (0.398, skew <b>+0.42</b> — a <em>positively</em> skewed,
139+
longshot-shaped favourite). Both readings said "do not trust this favourite"; it went to penalties,
140+
Morocco through. Different p, same warning, same curve.</p>
141+
142+
<h2>The point — and the line we are not crossing</h2>
143+
<p>This is a test of <b>agreement and shape</b>, not of edge. The rank correlation between our p and the
144+
market's is <b>{M ? M.agree_corr.toFixed(3) : ''}</b>: the odds-free engine reconstructs what a real
145+
sportsbook priced, from results alone. That is the same result as the World Cup annex's
146+
<span class="mono"> +0.998</span> on realised skew, now against a <em>second observer</em> instead of
147+
against frequency.</p>
148+
<blockquote>The difference <span class="mono">p<sub>model</sub> − p<sub>book</sub></span> is, literally,
149+
a betting signal. We compute it, we show it — and we stop there. Turning that gap into staked "value"
150+
would convert this study from a structural law into an alpha claim, with a completely different burden
151+
of proof (market efficiency, transaction costs, years of out-of-sample). The whole credibility of the
152+
thesis rests on it being <em>descriptive history</em>, not a tip sheet. So the edge column is a
153+
diagnostic, never an instruction.</blockquote>
154+
<p>And the calibration scoreboard — model Brier {M ? M.brier_model.toFixed(3) : ''} vs market
155+
{M ? ' ' + M.brier_book.toFixed(3) : ''} — is <b>not</b> evidence we beat the market. With
156+
<b> n = {M ? M.n : 0}</b> and two penalty lotteries it is noise. It becomes a real number at n = 16,
157+
the full Round of 32, and it is reported here only so the comparison is honest in both directions.</p>
158+
<p class="o-foot">Book odds: real pre-match lines, hand-recorded and frozen in
159+
<a href="https://github.com/cold-code-labs/skew-dynamic/blob/main/study/wc_book_odds.json" target="_blank" rel="noopener">wc_book_odds.json</a>
160+
(FanDuel via CBS Sports / ESPN, 2026-06-29) · de-vig: Shin · method in
161+
<a href="/worldcup">the World Cup annex</a> · this page updates as more games resolve.</p>
162+
</section>
163+
</article>
164+
165+
<Footer />
166+
</Base>
167+
168+
<style>
169+
.o-top { position: sticky; top: 0; z-index: 40; backdrop-filter: saturate(160%) blur(12px); background: rgba(252,253,255,.82); border-bottom: 1px solid var(--line-soft); }
170+
.o-top-inner { display: flex; align-items: center; height: 60px; }
171+
.o-top-links { margin-left: auto; display: flex; gap: 18px; }
172+
.o-top-links a { color: var(--ink-soft); font-size: .9rem; font-weight: 500; }
173+
.o-top-links a:hover { color: var(--accent); text-decoration: none; }
174+
.brand { font-weight: 650; letter-spacing: -.02em; color: var(--ink); display: flex; align-items: center; gap: 9px; }
175+
.brand:hover { text-decoration: none; } .brand-dim { color: var(--ink-faint); font-weight: 500; }
176+
.brand-mark { width: 16px; height: 16px; border-radius: 5px; background: linear-gradient(135deg, var(--accent), var(--navy)); box-shadow: 0 0 0 3px var(--accent-soft); }
177+
178+
.o-wrap { max-width: 820px; padding-top: 56px; padding-bottom: 40px; }
179+
.o-wrap h1 { font-size: clamp(2rem, 5vw, 2.8rem); letter-spacing: -.03em; line-height: 1.06; margin: 14px 0 18px; }
180+
.o-lede { font-size: 1.12rem; color: var(--ink-soft); line-height: 1.6; }
181+
.mono { font-family: var(--mono); }
182+
183+
.o-scoreboard { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin: 30px 0; }
184+
@media (min-width: 720px) { .o-scoreboard { grid-template-columns: repeat(4, 1fr); } }
185+
.o-sb-card { border: 1px solid var(--line-soft); border-radius: 12px; padding: 14px 16px; background: var(--surface, #fff); }
186+
.o-sb-k { font-size: .72rem; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .04em; }
187+
.o-sb-v { font-family: var(--mono); font-size: 1.5rem; font-weight: 650; color: var(--ink); display: block; margin: 4px 0 2px; }
188+
.o-sb-v i { color: var(--ink-faint); font-style: normal; font-weight: 400; }
189+
.o-sb-v .pos, .pos { color: var(--pos); }
190+
.o-sb-d { font-size: .74rem; color: var(--ink-faint); line-height: 1.3; }
191+
192+
.o-fig { margin: 32px 0 8px; }
193+
.o-fig svg { width: 100%; height: auto; display: block; }
194+
.grid { stroke: var(--line-soft); stroke-width: 1; }
195+
.ax0 { stroke: var(--line); stroke-width: 1.3; }
196+
.tk { fill: var(--ink-faint); font-size: 11px; font-family: var(--mono); }
197+
.axlab { fill: var(--ink-soft); font-size: 12px; font-weight: 500; }
198+
.law { fill: none; stroke: var(--accent); stroke-width: 2.4; opacity: .9; }
199+
.lawlab { fill: var(--accent); font-size: 12px; font-family: var(--mono); font-weight: 600; }
200+
.conn { stroke-width: 2; stroke-dasharray: 3 3; opacity: .55; }
201+
.conn.ok { stroke: var(--pos); } .conn.no { stroke: var(--neg); }
202+
.pt { stroke-width: 2; }
203+
.pt.model { fill: #fff; } .pt.book { fill: currentColor; }
204+
.pt.ok { color: var(--pos); stroke: var(--pos); } .pt.no { color: var(--neg); stroke: var(--neg); }
205+
.ptlab { fill: var(--ink-soft); font-size: 11px; font-weight: 600; }
206+
.o-fig figcaption { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 12px; font-size: .8rem; color: var(--ink-soft); justify-content: center; }
207+
.lg { display: inline-flex; align-items: center; gap: 6px; }
208+
.dot { width: 11px; height: 11px; border-radius: 50%; border: 2px solid var(--ink-soft); }
209+
.dot.model { background: #fff; } .dot.book { background: var(--ink-soft); }
210+
.bar { width: 14px; height: 3px; border-radius: 2px; } .bar.ok { background: var(--pos); } .bar.no { background: var(--neg); }
211+
212+
.o-tbl { margin: 26px 0; border: 1px solid var(--line-soft); border-radius: 12px; overflow: hidden; }
213+
.o-tr { display: grid; grid-template-columns: 1.6fr 1fr 1.2fr 1fr 1fr 1.1fr; align-items: center; gap: 8px; padding: 11px 16px; font-size: .86rem; border-top: 1px solid var(--line-soft); }
214+
.o-tr:first-child { border-top: none; }
215+
.o-th { background: var(--accent-soft); color: var(--ink-soft); font-size: .72rem; text-transform: uppercase; letter-spacing: .03em; font-weight: 600; }
216+
.o-tr.ok { border-left: 3px solid var(--pos); } .o-tr.no { border-left: 3px solid var(--neg); }
217+
.o-m { display: flex; align-items: center; gap: 7px; font-weight: 600; color: var(--ink); }
218+
.o-m i { color: var(--ink-faint); font-style: normal; font-weight: 400; font-size: .78rem; }
219+
.o-m img { border-radius: 2px; }
220+
.o-tr em { font-family: var(--mono); font-size: .72rem; font-style: normal; } .o-tr .up { color: var(--pos); } .o-tr .dn { color: var(--neg); }
221+
.o-o { font-weight: 600; } .o-o i { font-style: normal; font-weight: 400; color: var(--ink-faint); font-family: var(--mono); }
222+
223+
.o-body h2 { font-size: 1.4rem; letter-spacing: -.02em; margin: 36px 0 12px; }
224+
.o-body p { color: var(--ink-soft); line-height: 1.7; margin-bottom: 14px; }
225+
.o-body b { color: var(--ink); }
226+
.o-body blockquote { margin: 20px 0; padding: 14px 20px; border-left: 3px solid var(--accent); background: var(--accent-soft); border-radius: 0 8px 8px 0; font-size: 1.04rem; color: var(--ink); line-height: 1.6; }
227+
.o-foot { font-size: .82rem; color: var(--ink-faint); margin-top: 28px; padding-top: 16px; border-top: 1px solid var(--line-soft); }
228+
</style>

site/src/pages/retro.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const pct = (x) => Math.round(x * 100) + '%';
2222
<div class="container r-top-inner">
2323
<a class="brand" href="/"><span class="brand-mark"></span>skew<span class="brand-dim">-dynamic</span></a>
2424
<div class="r-top-links">
25-
<a href="/worldcup">World Cup</a><a href="/goals">Goals</a>
25+
<a href="/worldcup">World Cup</a><a href="/goals">Goals</a><a href="/odds">Odds</a>
2626
<a href="https://github.com/cold-code-labs/skew-dynamic" target="_blank" rel="noopener">GitHub ↗</a>
2727
</div>
2828
</div>

site/src/pages/worldcup.astro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const pct = (x) => (x * 100).toFixed(0) + '%';
2929
<a class="brand" href="/"><span class="brand-mark"></span>skew<span class="brand-dim">-dynamic</span></a>
3030
<div class="wc-top-links">
3131
<a href="/">← The study</a>
32+
<a href="/odds">Model vs Market</a>
3233
<a href="https://github.com/cold-code-labs/skew-dynamic" target="_blank" rel="noopener">GitHub ↗</a>
3334
</div>
3435
</div>

0 commit comments

Comments
 (0)