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(branchfeat/home-sports-figma-correction, offdev). Never edit the legacy top-levelfrontend/tree — the app lives instrykr-fe/. - Design system is law (
strykr-design-systemskill). 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 isglobals.css. Reusesrc/components/ui/*primitives; new variants go in acvamap, 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: Home4202:91031, Sports4160: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). Oddssizeis display-only liquidity, not a money field: an absent size renders nothing (or-) — never a fabricated number, never?? 0/|| 0.homeScore/awayScorenullmeans unknown — never default to0. - New token protocol. A new token is added to both
:root, .darkand.lightinglobals.css, thennpm run gen:design-systemis run, andglobals.cssand the regeneratedstrykr-fe/.claude/skills/strykr-design-system/SKILL.mdare committed together (CIcheck:design-systemfails 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
| File | Responsibility | Task |
|---|---|---|
src/app/globals.css (+ regenerated SKILL.md) | Add --watchlist-gold token (dark + light) + @theme inline mapping | 1 |
src/lib/formatSize.ts (new) | Shared exchange-size formatter, extracted from FixtureCard | 2 |
src/components/betting/CarouselFixtureCard.tsx | Gold-wash → --watchlist-gold, watchlist star box, odds-size line | 3 |
src/app/home/page.tsx | Replace the two inline banners with Figma gradient/illustration banners | 4 |
public/*.png (new assets) | Banner illustrations + tiled patterns from Figma | 4 |
src/app/sports/page.tsx | Straight 2px --cta tab underline (retire WaveUnderline); filter-chip reconcile | 5 |
src/components/sports/TournamentFixtures.tsx | Add AI-sparkle box to the group header; fix the one raw-rgba on the edited row | 6 |
src/components/betting/FixtureCard.tsx | Full-width row visual reconcile; consume shared formatSize | 7 |
| — | Dead-code + full verification + carousel-order regression + review | 8 |
Task 1: --watchlist-gold design token
Files:
- Modify:
src/app/globals.css(dark block after:24, light block after:265,@theme inlineafter:492) - Regenerate:
strykr-fe/.claude/skills/strykr-design-system/SKILL.md(vianpm 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-goldand light-mode value#e0a413are the spec's recommended default (dark#f9d131= FigmaWatchlist_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
.lightblock, immediately after line 353 (--signal-warn-strong: #edbb3a; …; if.lighthas no-strongline, 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.cssandSKILL.mdtogether.
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 localfunction formatSize, import the shared one)
Test convention (verified):
strykr-fehas NO vitest/jest. It uses Node's built-in runner: test files aresrc/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 withnode --test src/lib/formatSize.test.mjs. Match the existingsrc/lib/formatAmt.test.mjspattern exactly — do NOT add vitest.
Interfaces:
- Produces:
export function formatSize(size: number | undefined): string— same contract as the currentFixtureCardlocal (returns''for0/undefined;K/Msuffixes at 1e3/1e6, elsetoFixed(2)). Consumed by Task 3 and Task 7.
Note:
formatSizeis display formatting for exchange liquidity volume (provider currency), not FP or a money-movement field — the existingif (!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
FixtureCardlocal).
// 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
FixtureCardat the shared util. Insrc/components/betting/FixtureCard.tsx, delete the localfunction formatSize(...)at lines 233-239 and add an import at the top with the other@/libimports:
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.tsxOddsChip(39-55) — add optionalsize+ size lineOutcomeRow(57-94) — threadbackSize/laySizeinto the twoOddsChip- call sites (~177-201) — pass
backSize={entry.backVolume}/laySize={entry.layVolume} - featured wash (134-141) — swap
warntoken →--watchlist-gold - header (147-…) — add the watchlist star box
Interfaces:
- Consumes:
formatSizefrom@/lib/formatSize(Task 2);bestOdds.{home,draw,away}.backVolume|layVolume(already typedBestOddsEntry,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 theOutcomeRowprop type (65-72) addbackSize?: number;andlaySize?: 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
OutcomeRowrender sites (~177-201), addbackSize/laySizefrom 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 node4160:81461; the featured overlay (currentlyfrom-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-
Linkinto a nested-interactive violation (the card is aLink; the star muststopPropagation):
<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_screenshotof node4160: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 Figmadownload_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-brandflatLink→/sports) become the Figma banner: 72px tall,rounded-[20px],p-[12px], gradient background (token stops) +allsports_dice.pngillustration +banner_green_pattern.pngtiled pattern. Stays aLinkto/sports. Keep "All Sports" copy. -
Step 2: Replace the red "Casino" banner. Lines 75-83 (currently a
bg-loss/15div 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>(keeparia-disabled="true"), noLink, 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_screenshotof node4202: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(swapWaveUnderline→ straight underline),:29(drop the now-unusedWaveUnderlineimport),:650-687(filter-chip reconcile)
Interfaces:
- Produces:
WaveUnderlinebecomes 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
WaveUnderlinerender, gated onisActive) 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';toimport { 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,livehas abg-ctadot) with the Figma chip row (Live active w/ red dot, Upcoming, tournament-name chips). Keep the existingsetUrlParams/toggleTimeFilterhandlers andtimeFilterOptionsdata; change only the visual treatment + which chips show, to match Figma. Any "active red dot" usesbg-liveorbg-ctaper 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_screenshotof4160: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>, ownonClickwithstopPropagation— 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'sbg-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 withstopPropagationso 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 howFixtureCard'sAIFixtureButtonis 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_screenshotof4160: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:
formatSizefrom@/lib/formatSize(already re-imported in Task 2); the row already renders real size viaExchangeOddsButton(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/BestOddsButtoncarry rawrgba(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_screenshotof4160: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.tsxWaveUnderlineexport;src/components/sports/BannerCarousel.tsx;src/components/betting/TournamentRow.tsx -
Step 1: Confirm dead code. Grounding found: after Task 5,
WaveUnderlinehas no callers;BannerCarousel.tsxhas zero importers;betting/TournamentRow.tsxhas zero importers (the admintournaments-markets/TournamentRow.tsxis 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
carouselRankordering — the carousel still renders in the backend-ranked order (featured={index === 0}unchanged inFixtureCarousel.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(orfeature-dev:code-reviewer) over the full branch diff, with focus: no raw hex/rgb introduced in components, tokens used correctly,sizerender has no default/fabrication, scores never defaulted to 0, "Soccer" naming preserved, no legacyfrontend/edits, nested-interactive (Link+ star/sparkle) handled withstopPropagation. -
Step 6: BLAST RADIUS report (per
impact-analysis.md). Rungraphify affectedon 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-goldname/light-value sign-off request, dead-code removal list, and the visual-sign-off ask (reviewer + user check ondevafter 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-goldname + 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.