Home & Sports Pages — Figma-Exact Correction — Design Spec
Date: 2026-07-01
Status: Approach approved (verify items resolved); token names pending final PR-time sign-off
Figma: Home node 4202:91031, Sports node 4160:81461 (forsyt.io master file)
Supersedes visual scope of: PR #868 (merged 2026-07-01) — shipped a Home/Sports redesign that does not match Figma.
Goal
Pixel-exact correction of the authenticated /home and /sports screens to the two
Figma frames above. Reuse the components PR #868 already created — this is a visual
reconciliation, not a rebuild. Dark is the shipped theme; must also hold in light mode
via the design system. Keep "Soccer" naming even where Figma labels it "Football".
No new icons — sport glyphs come from the existing SPORT_ICON_MAP.
Scope
In scope (files the correction edits):
| File | Change |
|---|---|
src/app/home/page.tsx | replace the two hand-rolled banners; section spacing |
src/app/sports/page.tsx | sport-tab active indicator; filter-chip reconcile |
src/components/betting/CarouselFixtureCard.tsx | gold-wash featured state, watchlist star, score rendering, odds-size line |
src/components/sports/TournamentFixtures.tsx | add AI-sparkle box to the group header |
src/components/betting/FixtureCard.tsx | full-width row visual reconcile only |
src/app/globals.css + regen | one new token --watchlist-gold (see Tokens) |
backend/.../marginService.ts + carousel route | BestOddsEntry.backSize/laySize (see Odds-size) |
Out of scope (shell-owned or unrelated — do NOT touch):
src/components/layout/Header.tsx— logo, balance pill, avatar (shared shell).src/components/layout/BottomNav.tsx— 4-tab nav (shared shell).src/components/layout/MainLayout.tsx— page grid shell.src/app/page.tsx— logged-out marketing landing (has its own throwawayOddsCell/LiveMatchCard; NOT the redesign target).src/components/sports/BannerCarousel.tsx— orphaned, not imported.src/components/betting/TournamentRow.tsx— likely dead; grep for usages before any reuse.- Notion carousel-ordering logic — already faithful in backend
carouselService.carouselRank.
Element → existing-primitive map
| Figma element | Existing component (file:anchor) | Change |
|---|---|---|
| Top 2-row column-major carousel | FixtureCarousel rows={2} (betting/FixtureCarousel.tsx) | none — grid-flow-col auto-cols-max already correct |
| Match/score card | CarouselFixtureCard (betting/CarouselFixtureCard.tsx) | gold wash + star + scores + odds size |
| Odds cell (in card) | CarouselFixtureCard.OddsChip (L39-55) | add size line under price |
| Green "All Sports" banner | inline block home/page.tsx L65-71 (bg-brand flat) | Figma green gradient + dice/ball asset + pattern |
| Red "Casino" banner | inline block home/page.tsx L75-83 (bg-loss/15 "Soon") | Figma red gradient + chips asset + subtitle; non-interactive |
| Per-sport rail | SportRail + FixtureCarousel rows={1} | none |
| Sport tabs | sports/page.tsx L601-648 | swap WaveUnderline → straight 2px underline per Figma |
| Filter chips | sports/page.tsx L650-687 | reconcile with Figma tournament-name chips |
| Tournament group header | TournamentFixtures.tsx L348-396 (inline) | add AI-sparkle box next to existing collapse toggle + watchlist star |
| Full-width match row | FixtureCard.tsx mobile render L568+ | visual reconcile only — already renders real size |
| TeamLogo / SportIcons / HorizontalScroll / Badge | ui/* | reuse as-is |
Per-component deltas
CarouselFixtureCard.tsx
- Featured/hero state = gold radial wash behind the card
(
radial-gradient(...rgb(255,211,35)...), decorative — inline gradient is a legit one-off, NOT a color token) + a watchlist star box using the new--watchlist-gold. - Scores: render
homeScore/awayScorewhen present (Row-2 cards); omit whennull(Row-1 cards) — height is content-driven, matching Figma. Never default a missing score to0(DOMAIN_INTEGRITY). - Odds size:
OddsChipgains a small size number underformatOdds(odds)— see Odds-size section. - Cell count: 3-outcome (soccer, home/draw/away) → 6 cells (back+lay each);
2-outcome (cricket/basketball) → 4 cells. Already driven by presence of
bestOdds.draw; verify the back/lay split renders both. - LIVE badge →
--livetoken (#cc2e2b).
home/page.tsx banners
- Replace both inline blocks with a gradient + illustration + tiled-pattern banner
(72px tall, rounded-20, p-12). Green routes to
/sports; Casino stays non-interactive (no link, no "Soon" pill — Figma shows a full red banner with a subtitle, not a muted placeholder). - Assets →
public/(see Assets).
sports/page.tsx
- Sport-tab indicator: straight 2px
--ctaunderline under the active tab (Figma), replacingWaveUnderline.WaveUnderlinestays exported for any other caller — grep before removing (Dead Code Hygiene). - Filter chips: match Figma's chip row (Live active w/ red dot, Upcoming, tournament-name chips). Reconcile the existing time-filter pills with the Figma set.
TournamentFixtures.tsx group header
- Header row = collapse toggle (existing) + sport icon + tournament name + AI-sparkle box (new) + watchlist star box (existing). Confirmed with user: all three trailing affordances present.
Odds-size design (revises PR #868 "add a size field" plan — smaller than thought)
- The Sports full-width rows already render real exchange size —
FixtureCard.formatSize()readingOutcome.backDepth[0].size/layDepth[0].size. No change needed there beyond visuals. - Only the carousel card omits size. The depth data is already in the carousel
payload (
/fixtures/carousel*sendsincludeMarkets=true). - Plan: enrich
BestOddsEntrywithbackSize?/laySize?insidemarginService.extractBestOdds(it already sees the depth used to pick best odds), plumb through the/fixtures/carousel+/fixtures/carousel/groupedresponses, and read it inOddsChip. ReuseFixtureCard.formatSize()(hoist to a shared util). - Financial-integrity: size is display-only liquidity, not a money-movement field.
Absent size → render nothing (or
-), never a fabricated number. No default.
Tokens
One new token. Everything else maps to existing tokens.
--watchlist-gold— the watchlist/hero star (Watchlist_Yellow/500 #f9d131). Dark#f9d131, light#e0a413(darkened so the star stays legible on white card fills in light mode). Added to both:root,.darkand.lightinglobals.css, thennpm run gen:design-system; commitglobals.cssand the regeneratedSKILL.mdtogether (CIcheck:design-systemfails otherwise). (Name/light value are the recommended default — final sign-off at PR time.)
Existing-token mappings (no new tokens):
| Figma value | Token |
|---|---|
| LIVE badge red | --live #cc2e2b (≈1 shade off Figma's rendered #b52926) |
| banner subtitle 80% white | --fg-muted (90%) |
border_light 4% | --line-subtle (6%) |
border_darkest 12% | --line-default (8%) |
gold hero wash #ffd323 radial | inline decorative gradient (one-off, non-token) |
| back/lay odds tints | --bet-back / --bet-lay |
| active nav/tab orange | --cta |
Assets
Downloaded from Figma, place under public/ following existing conventions:
allsports_dice.png, casino_chips.png, banner_green_pattern.png,
banner_red_pattern.png. (Currently staged in scratchpad for the review artifacts.)
Dead code (list, do not delete without confirmation)
WaveUnderline— if no other caller after the tab swap.BannerCarousel.tsx,TournamentRow.tsx— orphaned candidates.
Itemize + confirm removal after wiring (Dead Code Hygiene).
Cross-system impact
- frontend: banners, carousel card, sport tabs, filter chips, group header, 1 new token.
- backend: small —
BestOddsEntry.backSize/laySizeinmarginService.extractBestOdds- carousel route response shape. Display-only; no money path. Route to
financial-guardianfor theextractBestOddstouch even though it's non-monetary.
- carousel route response shape. Display-only; no money path. Route to
- services / infra: none.
Testing / verification
npx tsc --noEmit+ lint on touched files.- Compare
/homeand/sportsto the Figma frames in both dark and light mode: gold wash +--watchlist-goldstar, Row-1 (no score) vs Row-2 (score) card heights, 6-vs-4 odds cells, carousel odds-size renders from real depth (not hardcoded), banners with real assets, straight sport-tab underline, group header showing collapse + AI-sparkle + watchlist star, full-width rows unchanged. - Verify carousel order still matches
carouselRank(no regression).
Open items
- Final sign-off on
--watchlist-goldname + light value (default proposed above). financial-guardianreview of theextractBestOddssize enrichment before merge.