Skip to main content

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):

FileChange
src/app/home/page.tsxreplace the two hand-rolled banners; section spacing
src/app/sports/page.tsxsport-tab active indicator; filter-chip reconcile
src/components/betting/CarouselFixtureCard.tsxgold-wash featured state, watchlist star, score rendering, odds-size line
src/components/sports/TournamentFixtures.tsxadd AI-sparkle box to the group header
src/components/betting/FixtureCard.tsxfull-width row visual reconcile only
src/app/globals.css + regenone new token --watchlist-gold (see Tokens)
backend/.../marginService.ts + carousel routeBestOddsEntry.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 throwaway OddsCell/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 elementExisting component (file:anchor)Change
Top 2-row column-major carouselFixtureCarousel rows={2} (betting/FixtureCarousel.tsx)nonegrid-flow-col auto-cols-max already correct
Match/score cardCarouselFixtureCard (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" bannerinline block home/page.tsx L65-71 (bg-brand flat)Figma green gradient + dice/ball asset + pattern
Red "Casino" bannerinline block home/page.tsx L75-83 (bg-loss/15 "Soon")Figma red gradient + chips asset + subtitle; non-interactive
Per-sport railSportRail + FixtureCarousel rows={1}none
Sport tabssports/page.tsx L601-648swap WaveUnderline → straight 2px underline per Figma
Filter chipssports/page.tsx L650-687reconcile with Figma tournament-name chips
Tournament group headerTournamentFixtures.tsx L348-396 (inline)add AI-sparkle box next to existing collapse toggle + watchlist star
Full-width match rowFixtureCard.tsx mobile render L568+visual reconcile only — already renders real size
TeamLogo / SportIcons / HorizontalScroll / Badgeui/*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/awayScore when present (Row-2 cards); omit when null (Row-1 cards) — height is content-driven, matching Figma. Never default a missing score to 0 (DOMAIN_INTEGRITY).
  • Odds size: OddsChip gains a small size number under formatOdds(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--live token (#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 --cta underline under the active tab (Figma), replacing WaveUnderline. WaveUnderline stays 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 sizeFixtureCard.formatSize() reading Outcome.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* sends includeMarkets=true).
  • Plan: enrich BestOddsEntry with backSize?/laySize? inside marginService.extractBestOdds (it already sees the depth used to pick best odds), plumb through the /fixtures/carousel + /fixtures/carousel/grouped responses, and read it in OddsChip. Reuse FixtureCard.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,.dark and .light in globals.css, then npm run gen:design-system; commit globals.css and the regenerated SKILL.md together (CI check:design-system fails otherwise). (Name/light value are the recommended default — final sign-off at PR time.)

Existing-token mappings (no new tokens):

Figma valueToken
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 radialinline 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/laySize in marginService.extractBestOdds
    • carousel route response shape. Display-only; no money path. Route to financial-guardian for the extractBestOdds touch even though it's non-monetary.
  • services / infra: none.

Testing / verification

  • npx tsc --noEmit + lint on touched files.
  • Compare /home and /sports to the Figma frames in both dark and light mode: gold wash + --watchlist-gold star, 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-gold name + light value (default proposed above).
  • financial-guardian review of the extractBestOdds size enrichment before merge.