Skip to main content

Home & Sports — Figma-Exact Correction — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Pixel-correct the authenticated /home and /sports screens in strykr-fe to match the two Figma frames, reusing the components PR #868 already shipped (visual reconciliation, not a rebuild).

Architecture: Frontend-only. Edit five existing components + globals.css (one new design token) + one small shared util. No backend change — grounding proved the carousel payload already ships exchange size (BestOddsEntry.backVolume/layVolume, populated in marginService.buildEntry:511-515, already mirrored in src/types/betting.ts:71-72). The spec's planned extractBestOdds enrichment, carousel-route plumbing, and financial-guardian gate are therefore removed. Dark is the shipped theme; every change must also hold in light mode via tokens.

Tech Stack: Next.js 16 App Router, React 19, Tailwind v4 (@theme inline tokens in globals.css), @base-ui/react + cva primitives, Figma MCP for design intent.

Global Constraints

Every task's requirements implicitly include this section.

  • Worktree only. All edits happen in .claude/worktrees/home-sports-figma-correction (branch feat/home-sports-figma-correction, off dev). Never edit the legacy top-level frontend/ tree — the app lives in strykr-fe/.
  • Design system is law (strykr-design-system skill). NEVER write a raw hex / rgb() / rgba() in a component — every color is a token or a remapped palette class. Map Figma hex → token; the only place literal colors live is globals.css. Reuse src/components/ui/* primitives; new variants go in a cva map, not one-off call-site strings.
  • Figma is intent, not code. Pull each frame with the Figma MCP (get_design_context / get_screenshot) at implementation time and MAP values to tokens — do not transcribe hex. Figma nodes: Home 4202:91031, Sports 4160:81461 (forsyt.io master file).
  • Keep "Soccer" naming even where Figma labels it "Football".
  • No new icons — sport glyphs come from the existing SPORT_ICON_MAP.
  • Financial / domain integrity (financial-security.md). Odds size is display-only liquidity, not a money field: an absent size renders nothing (or -) — never a fabricated number, never ?? 0 / || 0. homeScore / awayScore null means unknown — never default to 0.
  • New token protocol. A new token is added to both :root, .dark and .light in globals.css, then npm run gen:design-system is run, and globals.css and the regenerated strykr-fe/.claude/skills/strykr-design-system/SKILL.md are committed together (CI check:design-system fails otherwise).
  • Never autonomously merge the PR — human-only, hook-enforced.

Verification reality (read before starting)

The local machine cannot run the full app (per project setup) — verification is typecheck + lint + design-system check, plus visual mapping against the Figma frame at implementation time, plus a frontend code-review subagent, plus user visual sign-off (on the PR, or on dev.strykr.io after merge-to-dev auto-deploys). No task below claims "I ran the screen and it looks right" — the visual gate is Figma-comparison + reviewer + user, not a local render.

Per-task verification commands (run from strykr-fe/):

npx tsc --noEmit           # types (both themes are token-driven, so this + design-system check covers theming)
npm run lint # eslint (scope to touched files in the message to the reviewer)
npm run check:design-system # only required for the token task; catches a stale SKILL.md

File Structure

FileResponsibilityTask
src/app/globals.css (+ regenerated SKILL.md)Add --watchlist-gold token (dark + light) + @theme inline mapping1
src/lib/formatSize.ts (new)Shared exchange-size formatter, extracted from FixtureCard2
src/components/betting/CarouselFixtureCard.tsxGold-wash → --watchlist-gold, watchlist star box, odds-size line3
src/app/home/page.tsxReplace the two inline banners with Figma gradient/illustration banners4
public/*.png (new assets)Banner illustrations + tiled patterns from Figma4
src/app/sports/page.tsxStraight 2px --cta tab underline (retire WaveUnderline); filter-chip reconcile5
src/components/sports/TournamentFixtures.tsxAdd AI-sparkle box to the group header; fix the one raw-rgba on the edited row6
src/components/betting/FixtureCard.tsxFull-width row visual reconcile; consume shared formatSize7
Dead-code + full verification + carousel-order regression + review8

Task 1: --watchlist-gold design token

Files:

  • Modify: src/app/globals.css (dark block after :24, light block after :265, @theme inline after :492)
  • Regenerate: strykr-fe/.claude/skills/strykr-design-system/SKILL.md (via npm run gen:design-system)

Interfaces:

  • Produces: the utility classes text-watchlist-gold / bg-watchlist-gold / border-watchlist-gold (consumed by Task 3).

Design decision (surface to user before commit): token name --watchlist-gold and light-mode value #e0a413 are the spec's recommended default (dark #f9d131 = Figma Watchlist_Yellow/500; darkened for legibility on white light-mode card fills). These are the sign-off items in the spec's "Open items". Proceed with them as the default; call them out in the PR description for final sign-off.

  • Step 1: Add the dark-mode token. In src/app/globals.css, immediately after line 125 (--signal-warn-strong: #edbb3a; …), add:
  --watchlist-gold: #f9d131; /* Watchlist / hero star (Figma Watchlist_Yellow/500) */
  • Step 2: Add the light-mode token. In the .light block, immediately after line 353 (--signal-warn-strong: #edbb3a; …; if .light has no -strong line, anchor after line 352 --signal-warn: #e5b93c;), add:
  --watchlist-gold: #e0a413; /* Watchlist / hero star — darkened for legibility on white */
  • Step 3: Expose the utility. In @theme inline, immediately after line 536 (--color-warn: var(--signal-warn);), add:
  --color-watchlist-gold: var(--watchlist-gold);
  • Step 4: Regenerate the design-system skill.

Run (from strykr-fe/): npm run gen:design-system Expected: SKILL.md token table now lists a watchlist-gold row with Dark #f9d131 / Light #e0a413.

  • Step 5: Verify the check passes.

Run: npm run check:design-system Expected: PASS (no "SKILL.md is stale" error).

  • Step 6: Commit globals.css and SKILL.md together.
git add src/app/globals.css .claude/skills/strykr-design-system/SKILL.md
git commit -m "feat(design-system): add --watchlist-gold token (dark #f9d131 / light #e0a413)"

Task 2: Shared formatSize util

Files:

  • Create: src/lib/formatSize.ts
  • Create: src/lib/formatSize.test.mjs
  • Modify: src/components/betting/FixtureCard.tsx:233-239 (delete the local function formatSize, import the shared one)

Test convention (verified): strykr-fe has NO vitest/jest. It uses Node's built-in runner: test files are src/lib/<name>.test.mjs, import test from 'node:test' + import assert from 'node:assert/strict', importing the source directly from ./formatSize.ts (Node ≥22.18 strips types). Run with node --test src/lib/formatSize.test.mjs. Match the existing src/lib/formatAmt.test.mjs pattern exactly — do NOT add vitest.

Interfaces:

  • Produces: export function formatSize(size: number | undefined): string — same contract as the current FixtureCard local (returns '' for 0/undefined; K/M suffixes at 1e3/1e6, else toFixed(2)). Consumed by Task 3 and Task 7.

Note: formatSize is display formatting for exchange liquidity volume (provider currency), not FP or a money-movement field — the existing if (!size) return '' is acceptable here (empty size drives no arithmetic). The no-default guard on whether to render at all lives at the call site (Task 3), not in this formatter.

  • Step 1: Write the failing test.
// src/lib/formatSize.test.mjs
import assert from 'node:assert/strict';
import test from 'node:test';
import { formatSize } from './formatSize.ts';

test('formatSize: returns empty string for undefined or zero', () => {
assert.equal(formatSize(undefined), '');
assert.equal(formatSize(0), '');
});

test('formatSize: formats plain values to 2dp', () => {
assert.equal(formatSize(42), '42.00');
assert.equal(formatSize(999.5), '999.50');
});

test('formatSize: formats thousands with K', () => {
assert.equal(formatSize(1_500), '1.50K');
});

test('formatSize: formats millions with M', () => {
assert.equal(formatSize(2_400_000), '2.40M');
});
  • Step 2: Run test to verify it fails.

Run: node --test src/lib/formatSize.test.mjs Expected: FAIL — Cannot find module './formatSize.ts' (util not created yet).

  • Step 3: Create the util (verbatim from the current FixtureCard local).
// src/lib/formatSize.ts
// Format exchange size for display — volume is exchange liquidity (provider currency), not FP.
export function formatSize(size: number | undefined): string {
if (!size) return '';
if (size >= 1_000_000) return `${(size / 1_000_000).toFixed(2)}M`;
if (size >= 1_000) return `${(size / 1_000).toFixed(2)}K`;
return size.toFixed(2);
}
  • Step 4: Run test to verify it passes.

Run: node --test src/lib/formatSize.test.mjs Expected: PASS (4 tests, 0 fail).

  • Step 5: Point FixtureCard at the shared util. In src/components/betting/FixtureCard.tsx, delete the local function formatSize(...) at lines 233-239 and add an import at the top with the other @/lib imports:
import { formatSize } from '@/lib/formatSize';
  • Step 6: Verify no behavior change.

Run: npx tsc --noEmit Expected: PASS — FixtureCard still resolves formatSize (now from @/lib), no other call site touched.

  • Step 7: Commit.
git add src/lib/formatSize.ts src/lib/formatSize.test.mjs src/components/betting/FixtureCard.tsx
git commit -m "refactor(fe): hoist formatSize to shared util (no behavior change)"

Task 3: CarouselFixtureCard — gold wash, watchlist star, odds-size line

Files:

  • Modify: src/components/betting/CarouselFixtureCard.tsx
    • OddsChip (39-55) — add optional size + size line
    • OutcomeRow (57-94) — thread backSize/laySize into the two OddsChip
    • call sites (~177-201) — pass backSize={entry.backVolume} / laySize={entry.layVolume}
    • featured wash (134-141) — swap warn token → --watchlist-gold
    • header (147-…) — add the watchlist star box

Interfaces:

  • Consumes: formatSize from @/lib/formatSize (Task 2); bestOdds.{home,draw,away}.backVolume|layVolume (already typed BestOddsEntry, src/types/betting.ts:71-72); WatchlistToggleButton (@/components/betting/WatchlistToggleButton, variant="star").

Pre-pull: get_design_context + get_screenshot on Sports node 4160:81461 (the carousel card) — confirm gold-wash intensity, star placement, and size-line typography before editing. Map every hex to a token (rule: no raw color in a component).

  • Step 1: Add the size line to OddsChip. Replace lines 39-55 with:
function OddsChip({ odds, size, kind }: { odds: number | undefined; size?: number; kind: 'back' | 'lay' }) {
const hasOdds = typeof odds === 'number' && odds > 1;
// Display-only liquidity: render the size line ONLY when a real positive size exists.
// Absent/zero size renders nothing — never a fabricated 0 (financial-security: no default).
const sizeLabel = typeof size === 'number' && size > 0 ? formatSize(size) : null;
return (
<div
className={cn(
'min-w-[40px] px-[6px] py-[5px] rounded-[8px] border flex flex-col items-center justify-center',
kind === 'back'
? 'bg-back/[0.12] border-back/[0.24]'
: 'bg-lay/[0.12] border-lay/[0.24]',
)}
>
<span className={cn('text-[13px] font-semibold tabular-nums', hasOdds ? 'text-fg' : 'text-fg/40')}>
{hasOdds ? formatOdds(odds!) : '-'}
</span>
{sizeLabel && <span className="text-[10px] text-fg/50 tabular-nums leading-none">{sizeLabel}</span>}
</div>
);
}

Add the import at the top of the file (with the other @/lib imports): import { formatSize } from '@/lib/formatSize';

  • Step 2: Thread size through OutcomeRow. In the OutcomeRow prop type (65-72) add backSize?: number; and laySize?: number;, destructure them (58-64), and pass them to the chips (89-90):
        <OddsChip odds={backOdds} size={backSize} kind="back" />
<OddsChip odds={layOdds} size={laySize} kind="lay" />
  • Step 3: Pass the volumes at each call site. At the home/draw/away OutcomeRow render sites (~177-201), add backSize/laySize from the corresponding entry, e.g. for home:
        backSize={bestOdds?.home?.backVolume}
laySize={bestOdds?.home?.layVolume}

Repeat with bestOdds?.draw?.* and bestOdds?.away?.*. (Scores are already correctly conditional — homeScore/awayScore computed at 110-113 with a null-guard — do not touch that logic.)

  • Step 4: Swap the featured wash to --watchlist-gold. Pull the exact gold from Figma node 4160:81461; the featured overlay (currently from-warn/[0.22] via-warn/[0.06], 136-141) becomes the watchlist gold. If Figma's decorative radial has no token match, it is a legit inline one-off gradient (decorative, non-theme-flipping shape) — but the flat tint/border that reads as "gold" uses the token, e.g.:
        className="pointer-events-none absolute inset-0 bg-gradient-to-b from-watchlist-gold/[0.22] via-watchlist-gold/[0.06] to-transparent"

and the card border for featured (127) border-warn/[0.45]border-watchlist-gold/[0.45]. Confirm against the Figma screenshot which surfaces are gold.

  • Step 5: Add the watchlist star box. In the card header (near the LIVE badge / top-right, ~147-160), add a star that reuses the existing primitive and does not turn the whole card-Link into a nested-interactive violation (the card is a Link; the star must stopPropagation):
        <WatchlistToggleButton
itemType="fixture"
itemId={fixture.id}
itemName={`${fixture.homeTeam} v ${fixture.awayTeam}`}
sportId={fixture.sportId}
variant="star"
size="sm"
className="text-watchlist-gold"
/>

Verify WatchlistToggleButton's itemType/itemId contract against its current signature at the pull step (the tournament header uses itemType="tournament" keyed on providerTournamentId; the fixture star keys on fixture.id — confirm the fixture variant exists, else match whatever the card already has access to). If the star's gold must differ from --cta/--warn, use text-watchlist-gold (the Task 1 token).

  • Step 6: Verify types.

Run: npx tsc --noEmit Expected: PASS — OddsChip/OutcomeRow new optional props type-check; backVolume/layVolume resolve on BestOddsEntry.

  • Step 7: Visual acceptance (Figma-compare, not local render). Compare the edited component's classes against get_screenshot of node 4160:81461: gold wash on the featured (index 0) card, star top-right, size line under each price, Row-1 cards (no score) shorter than Row-2 (score) cards, 6 chips for soccer / 4 for cricket. Record deltas for the Task 8 reviewer.

  • Step 8: Commit.

git add src/components/betting/CarouselFixtureCard.tsx
git commit -m "feat(carousel-card): watchlist-gold wash + star + odds-size line"

Task 4: home/page.tsx — Figma banners

Files:

  • Modify: src/app/home/page.tsx:64-83 (both inline banner blocks)
  • Add: public/allsports_dice.png, public/casino_chips.png, public/banner_green_pattern.png, public/banner_red_pattern.png (from Figma download_assets)

Interfaces:

  • Consumes: next/image (Image), next/link (Link) — both already imported in the file.

Pre-pull: get_design_context on Home node 4202:91031 for the two banners; download_assets for the dice/chips illustrations and tiled patterns. Map the green/red gradients to tokens (green → --brand family; red → --loss/--danger family) — the gradient stops may be an inline one-off shape, but each color stop must still resolve to a token var, never a raw hex.

  • Step 1: Replace the green "All Sports" banner. Lines 65-71 (currently bg-brand flat Link/sports) become the Figma banner: 72px tall, rounded-[20px], p-[12px], gradient background (token stops) + allsports_dice.png illustration + banner_green_pattern.png tiled pattern. Stays a Link to /sports. Keep "All Sports" copy.

  • Step 2: Replace the red "Casino" banner. Lines 75-83 (currently a bg-loss/15 div with a "Soon" pill) become the Figma red banner: same geometry, red gradient (token stops) + casino_chips.png + banner_red_pattern.png + the Figma subtitle. Non-interactive — plain <div> (keep aria-disabled="true"), no Link, no "Soon" pill (Figma shows a full banner with a subtitle, not a muted placeholder). Match the exact subtitle copy from Figma.

  • Step 3: Verify types + assets resolve.

Run: npx tsc --noEmit Expected: PASS. Confirm the four public/*.png paths exist and next/image width/height/alt are set on each illustration.

  • Step 4: Visual acceptance. Compare against get_screenshot of node 4202:91031: two full banners, green clickable, red inert-but-full, correct illustrations/patterns, 72px height, dark + light both legible (token-driven). Record deltas.

  • Step 5: Commit.

git add src/app/home/page.tsx public/allsports_dice.png public/casino_chips.png public/banner_green_pattern.png public/banner_red_pattern.png
git commit -m "feat(home): Figma gradient banners (All Sports link + Casino non-interactive)"

Task 5: sports/page.tsx — straight tab underline + filter chips

Files:

  • Modify: src/app/sports/page.tsx:636-638 (swap WaveUnderline → straight underline), :29 (drop the now-unused WaveUnderline import), :650-687 (filter-chip reconcile)

Interfaces:

  • Produces: WaveUnderline becomes unused app-wide after this task (its only caller is line 637) — flagged for Task 8 dead-code review (do not delete the definition in this task).

Pre-pull: get_design_context + get_screenshot on Sports node 4160:81461 — the active-tab indicator and the filter-chip row.

  • Step 1: Replace the active-tab indicator. Lines 636-638 (the WaveUnderline render, gated on isActive) become a straight 2px underline in --cta, matching Figma. Example:
                    {isActive && (
<span className="absolute -bottom-[2px] left-1/2 -translate-x-1/2 h-[2px] w-[24px] rounded-full bg-cta" />
)}

Confirm the underline width/offset against the Figma screenshot (the w-[24px] is a placeholder for the Figma value — pull it).

  • Step 2: Drop the unused import. On line 29 change import { SPORT_ICON_MAP, WaveUnderline } from '@/components/ui/SportIcons'; to import { SPORT_ICON_MAP } from '@/components/ui/SportIcons';.

  • Step 3: Reconcile the filter chips. Lines 650-687 — align the mobile time-filter pills (All / live / today / tomorrow / next7days, active = bg-fg text-bg, live has a bg-cta dot) with the Figma chip row (Live active w/ red dot, Upcoming, tournament-name chips). Keep the existing setUrlParams/toggleTimeFilter handlers and timeFilterOptions data; change only the visual treatment + which chips show, to match Figma. Any "active red dot" uses bg-live or bg-cta per the Figma color (map, don't hardcode).

  • Step 4: Verify types.

Run: npx tsc --noEmit Expected: PASS — no dangling WaveUnderline reference.

  • Step 5: Visual acceptance. Compare tab strip + chip row against get_screenshot of 4160:81461. Record deltas.

  • Step 6: Commit.

git add src/app/sports/page.tsx
git commit -m "feat(sports): straight cta tab underline + Figma filter chips"

Task 6: TournamentFixtures.tsx — AI-sparkle box in the group header

Files:

  • Modify: src/components/sports/TournamentFixtures.tsx:352-396 (group header row)

Interfaces:

  • Consumes: the same click-isolation pattern the watchlist star already uses (sibling of the collapse <button>, own onClick with stopPropagation — never nested inside the button).

Pre-pull: get_design_context on Sports node 4160:81461 for the group-header AI-sparkle affordance (icon, color, box treatment).

  • Step 1: Fix the raw-rgba on the row I'm editing. Line 356 currently has hover:bg-[rgba(255,255,255,0.07)] (a design-system violation). Since this task edits that exact row, replace it with the token form consistent with the same element's bg-fg/[0.04]:
            <div className="w-full flex items-center bg-fg/[0.04] hover:bg-fg/[0.07] transition-colors">
  • Step 2: Add the AI-sparkle box. Insert it as a flex sibling of the collapse <button> (between </button> on line 379 and the watchlist-star conditional on 385), so the trailing affordances read left→right as: collapse toggle + sport icon + tournament name … AI-sparkle box (new) + watchlist star. It needs its own click handler with stopPropagation so tapping it never toggles the collapse. Use the existing sparkle icon from the icon set (no new icons) and the Figma color mapped to a token. Wire its onClick to whatever the AI entry point is (match how FixtureCard's AIFixtureButton is invoked — pull that at implementation; if there is no group-level AI action yet, the box opens the same AI surface scoped to the tournament).

  • Step 3: Verify types.

Run: npx tsc --noEmit Expected: PASS.

  • Step 4: Visual acceptance. Compare the header row against get_screenshot of 4160:81461: collapse + sport icon + name + AI-sparkle + star, all three trailing affordances present, star still tappable without toggling collapse. Record deltas.

  • Step 5: Commit.

git add src/components/sports/TournamentFixtures.tsx
git commit -m "feat(tournament-header): AI-sparkle box + tokenize hover bg"

Task 7: FixtureCard.tsx — full-width row visual reconcile

Files:

  • Modify: src/components/betting/FixtureCard.tsx (mobile full-width row, ~568-704)

Interfaces:

  • Consumes: formatSize from @/lib/formatSize (already re-imported in Task 2); the row already renders real size via ExchangeOddsButton (formatSize(displaySize) at ~401).

Pre-pull: get_design_context + get_screenshot on Sports node 4160:81461 for the full-width match row.

  • Step 1: Visual reconcile only. Adjust spacing / typography / borders of the mobile row (577-667) to match Figma. This row already renders real exchange size — do not change the data path. Any color you touch must be a token.

  • Step 2: Leave the pre-existing rgba violations untouched (flag only). ExchangeOddsButton/BestOddsButton carry raw rgba(5,154,72,…) / rgba(234,59,82,…) at lines 213-214, 361-362, 379-383 (the numeric equivalents of --bet-back/--bet-lay). These are out of scope (spec: "full-width row visual reconcile only") — do not rewrite them in this task unless a specific Figma delta forces you into that exact string. Note them in the Task 8 report as pre-existing debt.

  • Step 3: Verify types.

Run: npx tsc --noEmit Expected: PASS.

  • Step 4: Visual acceptance. Compare the row against get_screenshot of 4160:81461. Record deltas.

  • Step 5: Commit.

git add src/components/betting/FixtureCard.tsx
git commit -m "style(fixture-card): full-width row visual reconcile to Figma"

Task 8: Dead-code, full verification, and review

Files:

  • Potentially remove (ask first): src/components/ui/SportIcons.tsx WaveUnderline export; src/components/sports/BannerCarousel.tsx; src/components/betting/TournamentRow.tsx

  • Step 1: Confirm dead code. Grounding found: after Task 5, WaveUnderline has no callers; BannerCarousel.tsx has zero importers; betting/TournamentRow.tsx has zero importers (the admin tournaments-markets/TournamentRow.tsx is live — do not touch it). Re-grep to confirm post-edit:

cd strykr-fe && grep -rn "WaveUnderline\|BannerCarousel\|from '@/components/betting/TournamentRow'" src --include=*.tsx --include=*.ts

Expected: WaveUnderline only in its own definition; BannerCarousel/betting/TournamentRow no import hits.

  • Step 2: List dead code and ASK before deleting (Dead Code Hygiene — do not delete unilaterally). Present the three candidates to the user; delete only what they approve, in a separate commit.

  • Step 3: Full typecheck + lint + design-system check.

cd strykr-fe && npx tsc --noEmit && npm run lint && npm run check:design-system

Expected: all PASS.

  • Step 4: Carousel-order regression check. Confirm no change to carouselRank ordering — the carousel still renders in the backend-ranked order (featured={index === 0} unchanged in FixtureCarousel.tsx; no client reordering introduced). Verify by reading the diff, not by re-implementing.

  • Step 5: Frontend code review (orchestrated). Dispatch a pr-review-toolkit:code-reviewer (or feature-dev:code-reviewer) over the full branch diff, with focus: no raw hex/rgb introduced in components, tokens used correctly, size render has no default/fabrication, scores never defaulted to 0, "Soccer" naming preserved, no legacy frontend/ edits, nested-interactive (Link + star/sparkle) handled with stopPropagation.

  • Step 6: BLAST RADIUS report (per impact-analysis.md). Run graphify affected on each changed symbol (CarouselFixtureCard, formatSize, TournamentFixtures, FixtureCard, the sports/home pages) and report the dependent set grouped by system, with a verdict.

  • Step 7: Open the PR (do NOT merge). Draft PR body: scope, the "backend unchanged — size already shipped" correction, the --watchlist-gold name/light-value sign-off request, dead-code removal list, and the visual-sign-off ask (reviewer + user check on dev after merge).


Self-Review (against the spec)

Spec coverage: gold-wash featured state → T3; watchlist star → T3; score rendering → confirmed already correct, T3 note; odds-size line → T3 (backend proven unnecessary); banners → T4; sport-tab straight underline → T5; filter chips → T5; AI-sparkle header → T6; full-width row reconcile → T7; --watchlist-gold token → T1; formatSize hoist → T2; dead code → T8; carousel-order regression → T8; BLAST RADIUS → T8. Removed from spec with evidence: marginService.extractBestOdds enrichment + carousel-route plumbing + financial-guardian gate (size already in payload: marginService.ts:58-59,511-515; src/types/betting.ts:71-72).

Placeholder scan: the only deliberately-deferred values are Figma pixel numbers (underline width, gradient stops, exact subtitle copy) and the AI-sparkle onClick target — each is marked "pull at implementation" because the design-system skill requires mapping live Figma intent rather than pre-writing pixel JSX. All code-complete logic (token, formatSize, OddsChip, size guard, import swaps) is written verbatim.

Type consistency: formatSize(size: number | undefined): string used identically in T2/T3/T7; OddsChip gains size?: number; OutcomeRow gains backSize?/laySize?; backVolume/layVolume are the real field names on BestOddsEntry (src/types/betting.ts:71-72) — matched, not backSize/laySize (the spec's guess).

Open items (carry to PR)

  • Final sign-off on --watchlist-gold name + light value #e0a413 (proceeding with these as default).
  • Dead-code deletion approval for WaveUnderline / BannerCarousel.tsx / betting/TournamentRow.tsx.
  • Visual pixel sign-off (reviewer + user) — the local machine can't render the app.