Every hero puts a real player on screen.
The face always means something.
The homepage hero isn't decoration. A rules engine decides which player's face appears on any given day, and a small image pipeline decides how it’s rendered — a free ESPN cutout composited over his NFL team's colors, personalized to the signed-in owner, and stable for a full Pacific-Time day. This is how it works.
FeatureCompositeHero New this week
Dynamic top-player spotlight on Free Agents
A player appears only if the entry names one: an ESPN cutout over league blue, his NFL club’s crest behind him. An announcement belongs to nobody, so that glow is the only team colour here.
Read the updateFeatureCompositeHero · enhancement Improved this week
Trade Builder now shows live cap impact
“Same guy, leveled up” — an enhancement casts a rostered player in his first five NFL seasons. Strictly someone an owner invested in; the caption reads his year.
See what changedRecapCompositeHero Pacific Pigskins Tuesday · Week 12 recap
The week’s highest score has an owner.
Tuesday’s card is about a franchise, not a player — so the glow and the crest behind him are the rostering club’s, not Minnesota’s. The caption still names his NFL team.
Read the recapAuctionCompositeHero · countdown Auction opens Friday
The money window is coming.
Before bidding opens the card counts down and keeps its wordmark. Best available by dynasty ADP models the window — the pick rotates daily so one face doesn’t own the whole auction.
Prep your boardAuctionCompositeHero · live Bidding open
The auction is live.
Once bidding opens the ghost wordmark drops entirely — the live board needs the width, and decoration behind live money reads as clutter. The crest stays; it was never the noisy layer.
Go to the auctionCutWatchCompositeHero · planning Da Dangsters Cut watch · 44 days left
Roster planning ahead.
Outside the final 30 days the hero is calm blue and the copy is planning, not panic. Your club’s crest sits behind your own bubble player.
See who’s on the bubbleCutWatchCompositeHero · urgent Cowboy Up Cut watch · 6 days left
Roster deadline approaching.
Inside the final 30 days a tone — not a second accent — flips the surface and pill to red over the same roster palette. Signed in, the CTA becomes “Plan your cuts.”
Plan your cutsCutWatchCompositeHero · all clear Dead Cap Walking Cut watch · 6 days left
Every roster is legal.
The state most hero systems forget: nobody is over the limit. Rather than hide the hero, the metrics become days-to-deadline and the cap, and it still wears your club’s crest.
Review your rosterPreseasonCompositeHero Season opener
Football is back Thursday night.
The best projected starter in the week’s earliest game — and if you roster a player in that opener, you see your own. Kickoff belongs to the league, so the card stays league-coloured.
See the openerUdfaCompositeHero · CompositePanelBoard UDFA window · Best on the board
The draft is over. The bargains aren’t.
TaggedShowcaseCompositeHero · CompositePanelBoard Gridiron Geeks Franchise tags · 2026
Four players just got the franchise tag
Every composite state TheLeague ships, at true homepage scale — real players, real NFL team colours on the league-scoped cards and real franchise colours on the team-scoped ones, and the exact gradient / radial glow / ghost-wordmark / frosted-caption CSS from the live components. The badge on each card says which half of the colour rule it is on. Toggle your theme: the gradient, glow and tokens re-resolve for dark mode.
The Premise
Why there's a real player on the hero at all — and the one rule that governs it
Identifiable NFL action photography is rights-managed — Getty, AP, Imagn. A fantasy site can't ship it. The easy fallback is a gradient with a headline slapped on top, which is exactly how most fantasy homepages look: fine, forgettable, decorative.
The approach here is different. Every NFL player has a free, transparent headshot on ESPN's CDN. Layer that cutout over his team's colors with a little typography and glow, and you get a branded, on-topic hero — automatically, for any player, with zero licensing. The constraint became the design language.
That unlocks the actual rule, which is the whole point of the system:
Casting Rule — Brandon, binding“Every hero casts a semantically relevant player — the actual player in a breaking story, the rookie behind a new feature, a bubble player from your roster on cut watch. Never a random star for decoration.”
The Image Pipeline
How a name becomes a composite — and the gate that keeps it from breaking
WHAT’S NEW, AUCTION, CUT WATCH — bled off the top edge.hexToRgba(primary, 0.38) in dark, 0.2 in light — the player’s team hue bleeds in from the corner behind him.loading="eager" (above the fold); the reproductions above lazy-load. Always decoding="async", decorative alt="".backdrop-filter: blur(2px) pill bottom-right; kicker, title, summary, CTA bottom-left with an explicit text color so global heading rules can’t restyle it./** Composites need a transparent ESPN cutout —
MFL JPGs have baked backgrounds. */
function isCompositable(player: PlayerIdentity): boolean {
return player.position !== 'DEF'
&& isEspnCdnUrl(player.headshot);
} Two failure modes get gated out before anything renders. Transparency is load-bearing: the ESPN headshot has an alpha channel; MFL's fallback JPG (player_photos_big_2014/…_thumb.jpg) has a baked-in background that ruins the composite. So the gate requires an ESPN CDN URL — matched on the parsed host, never a substring, so a lookalike like espncdn.com.evil.com can’t slip a foreign image into an <img src>. And a team defense isn’t a person — position === ‘DEF’ is excluded, since a "DEF player" is a logo, not a face.
Even a valid ESPN id can 404 (deep free agents sometimes have no photo). The cutout carries an inline onerror that hides itself and its caption, and the hero swaps to the league-logo silhouette — a gradient with an empty flank reads as broken, so it never ships one.
Branding & Color
The player's NFL team paints the hero; his fantasy team rides alongside
Color comes from one source of truth: a 32-team NFL brand-color map (NFL_TEAM_COLORS, keyed by ESPN codes) with a hand-picked primary and secondary per team — secondary deliberately skips near-white "official" colors that would be useless on a dark composite. The player’s NFL team drives the gradient anchor and the glow tint. Unknown or free-agent codes fall back to TheLeague blue, #1c497c.
The hero is about a fantasy team, so franchise identity rides alongside the NFL color — the owner’s crest and short name as a chip, a “You +N” marker when it’s your own player. Cut watch and the tagged-player showcase are the roster-branded surfaces.
No fantasy team to brand to, so the hero leans on NFL identity — the team-color gradient and glow, the league logo as backdrop art on the live auction. Auction and UDFA are the free-agent surfaces; deep FAs fall back to league blue.
#241773 #fb4f14 #0b162a #a71930 #e31837 #1c497c Personalization Without Randomness
Signed-in owners see their own player; guests see a deterministic public pick
Same layout, different casting. The roster-action and kickoff heroes take an optional userFranchiseId. When it’s set and the owner has a relevant candidate, the pool narrows to their own players — your bubble cut, your kickoff starter, your suggested tag. When it’s empty (a guest, or an owner with no candidate), the hero falls back to a league-wide pick.
The catch: this is server-rendered, and SSR can’t call Math.random() at request time — two loads of the same page would disagree, and the cache would be useless. So the pick is deterministic per Pacific-Time calendar day: an FNV-1a hash of the PT day-key plus a per-hero seed indexes into the sorted candidate pool. Stable across every request that day, then it rotates at midnight PT so one face doesn’t own the window.
It’s testable, too — ?testDate=YYYY-MM-DD drives both the phase and the rotation seed, so any day’s hero can be previewed on demand.
// PT calendar day (YYYY-MM-DD) — the rotation seed
function ptDayKey(date) {
return new Intl.DateTimeFormat('en-CA', {
timeZone: 'America/Los_Angeles', …
}).format(date);
}
function dailyPick(pool, date, seedKey, keyOf) {
if (pool.length === 0) return null;
const sorted = [...pool].sort(
(a, b) => keyOf(a).localeCompare(keyOf(b)));
const i = hashSeed(`${ptDayKey(date)}:${seedKey}`)
% sorted.length;
return sorted[i]; // same all day, rotates at PT midnight
} franchiseId === userFranchiseIdThe Casting Rules, by Category
What's New heroes show the feature itself — a player appears only when the entry names one
Eight Casting Functions
Each surface has a purpose-built picker in hero-casting.ts — all deterministic, all reusable
| Function | Surface | Who it picks |
|---|---|---|
castFeaturedModel | What’s New (entry names a player) | Exactly the player the entry features — otherwise the hero shows the feature’s own screenshot. |
castRookieModel | Season reset (AFL new-season) | Newest draft class in the pool, rostered-first, daily rotation. |
castTopFreeAgentModel | Auction preview / live | Top-5 unrostered free agents by dynasty ADP, rotated daily. |
castClosingAuctionModel | Auction, live bids | The player whose bid clock runs out first (oldest anchor + MFL’s 36h rule). |
castRookiesOnBoard | UDFA window | Best current-class rookies still unrostered — the board is the board (no rotation). |
castBestScoredModel | Preseason / kickoff | Highest-projected starter in the earliest game — your player if you roster one. |
castRandomStarterModel | Preseason / kickoff / game day | Rotates among YOUR likely starters (top-8/team by projection) in the first game you play — your opener player if you have one. Guests get the opener, league-wide. |
castRosterModel | Roster actions (cuts, tags) | A candidate from YOUR franchise; guests get a league-wide daily pick. |
The Calendar Decides
14 core season phases resolve by date — plus a trade-deadline override and an offseason fallback; the two live phases subdivide into 6 daily slots
Which hero shows isn’t a toggle — it’s resolved from the calendar. The hero-resolver maps the current Pacific-Time date to a season phase. Fourteen are real windows on the league year; the SeasonPhase union below adds two more — a 24-hour trade-deadline override and an offseason-fallback catch-all for any gap (16 states in all). During the two live phases the resolver drops into a second axis — six daily slots keyed on day-of-week and game windows — so a Monday standings hero gives way to a Tuesday recap gives way to a waiver-wire push. A priority ladder (P0++ → P5) breaks ties.
Phases with a shipped composite hero today
What's Shipped
Five composite heroes live today; each owns its gradient, wordmark, glow, and casting
The What’s New hero. The feature’s own screenshot in a browser frame; the entry’s featured player composites over the gradient when one is named. Can force the dark treatment when the dark card is the story.
Amber money accent. Best-available-by-ADP in preview; once bids are live, it flips to the player whose clock runs out first and adds a high-bid line.
The first personalized hero — your own bubble player, “You +N” chip. Blue planning tier flips red inside the final 30 days.
The best (or a rotating) likely starter in the week’s earliest game — your player if you roster one in the opener.
Four team-gradient panels of the best rookies still on the board. Casts eight, renders the four whose photos actually load — a 404 never costs a slot.
All of the above now render through two shared components. Seven forks became one spotlight shell and one panel board — every dimension that differed is a custom property, every palette a named accent. 3,028 lines became 800, verified pixel-identical.
The playoff hero is a bracket-summary table, not a player composite. Feed-card composites were built and deliberately reverted.
The Bottom Line
The interesting part isn’t the gradient — it’s that a licensing constraint turned into a rule (“every hero means something”), the rule turned into eight small deterministic pickers, and those plug into a calendar-driven state machine that already knew what day it was. Personalization fell out for free, because the casting functions already took a franchise id.
It renders on the server, stays stable for a Pacific-Time day, degrades gracefully when a photo 404s, and looks right in light and dark. No stock photos. No licensed action shots. Just the right player, rendered the right way, every day.








Message