For most of this game's life, every list page leaned on the same crutch: a jQuery DataTable with horizontal scroll, fixed left columns, and a tiny pagination footer. It worked fine on a 1920×1080 monitor. On a phone it was a nightmare. Twenty-seven columns of player stats turn into a swipe-and-squint mini-game when the viewport is 380 px wide.
Over the last two weeks we deleted that pattern. Not minor restyling — actually deleted it, across fourteen pages, and replaced it with a unified card system designed mobile-first. This post is the story of how the pieces fit together and what we learned along the way.
The problem with tables
A table answers one question well: how does this row compare to that row on every column? Spreadsheet thinking. When you actually use the player search on a phone, you don't want to compare 50 players across 27 columns. You want to see one player at a time, with the data that matters, in a layout that reads top-to-bottom.
The 27-column DataTable was solving a problem only desktop users had — and it was solving it poorly there too, because horizontal scroll inside a vertical scroll page is a usability footgun.
One chrome to rule them all
The first decision was visual. Every card across the entire game now shares the same outer shell:
- 2 px
#c8d0dd border — solid enough to separate cards visually without competing with content
- Soft
0 3px 10px rgba(0,0,0,0.09) shadow — depth without drama
- Hover lift —
translateY(-1px) + a stronger shadow on hover, instant feedback that this is clickable
- User-team highlight via a single CSS variable pattern:
border-color: var(--row-bg) + inset shadow. Set --row-bg on the article once and the whole card recolors
Player cards, schedule cards, standings cards, draft picks, trade history, achievements, news events, watchlist entries, coins history, ticket sales — they all read as one family now. The eye learns the language once and applies it everywhere.
The sticky chrome stack
Tables had a giant tradeoff: scroll past the header and you forget what column is what. We replaced that with a three-layer sticky stack that follows you down the page:
- The team-color tab strip at the very top (Offense / Defense / Special, or seasonal phases on schedule.php)
- A position-filter row (QB · RB · WR · TE · OL · DL · LB · CB · S · K · P pills) below the tabs
- The sort bar below that, with the active sort highlighted gold
Each layer reads the height of the layer above through CSS variables: top: calc(var(--team-sticky-top) + var(--t5-tabs-h) + var(--pcard-filter-h)). A small JavaScript IIFE measures the page chrome (header + news ticker + notification bar) and writes the root variable. No magic numbers, no broken stacks when the news ticker collapses, no jitter on tab switches.
The inline mini-pager
The full pager at the bottom of a paginated list is fine when you reach the bottom. The problem was getting there: 50 player cards is a lot of scroll. So the sort bar now carries a tiny ‹ X / Y › mini-pager on the right edge — same prev/next handlers as the full pager, but always in your thumb's reach because the sort bar is sticky.
This is the kind of detail you don't notice when it's present and immediately miss when it's gone. Two-finger swipe to scroll down used to mean a long swipe to the bottom and back. Now it's one tap on the sticky chrome.
Mobile-first compaction
The card chrome looks the same on desktop and phone, but everything inside shrinks below 640 px:
- Player photo: 44 px → 34 px
- Position badge: 0.85 rem → 0.72 rem
- Character icons: 19 px → 16 px
- Toolbar padding: 0.65/0.85 rem → 0.35/0.45 rem
- Sort pills: 0.32/0.7 rem padding → 0.2/0.55 rem
End result: a fully populated player card with OVR, POT, age, value, salary, team and a CTA drops from ~200 px tall to ~135 px. You see two and a half cards in a viewport instead of one and a half. That's a 65% jump in information density without resorting to a horizontal scrollbar.
The team tab strip got the same treatment — Offense / Defense / Special / Practice / Stats no longer dominate half the phone screen on its own.
Shared renderers, not copy-paste
The temptation when building 14 different card pages is to copy the markup and tweak per page. We resisted, and the codebase is better for it:
players_pcard.php renders one player card and is included by players_cards_ajax.php, freeagents_cards_ajax.php, draft_cards_ajax.php, players_stats_cards_ajax.php, and the synchronous server-render on players_watchlist.php
_player_leaders_card.php exports both a render function and a standalone CSS emitter — so sister card families that reuse the .pll-* classes (the MVP card, for instance) can guarantee the styles are on the page without rendering a leader-card themselves
_game_mvp_card.php was extracted when we realized halloffame and player_stats_leaders were drifting. Same helper, same chrome — the only difference is an optional saison pill that only renders on the multi-saison Hall of Fame view
_schedule_cards.php handles every game-list grid: last week, this week, team schedule, full schedule, the trade-history cards even borrow its week pill
When a visual tweak lands (border thickness, hover behavior, mobile compaction), it lands once and propagates everywhere. The alternative — hunting through 14 pages for a copy-pasted style — was a maintenance trap we'd already fallen into twice. Never again.
Special cases that earned their own treatment
- Power Ranking opens with a podium of the league's top three teams, then every team's card surfaces a prominent power-rank badge top-left and an expandable breakdown of every factor (11 position chips + weights) feeding the formula
- Trade History mimics the simresult overlay's "Top Trade" card: team-color gradient, ⇄ separator that rotates 90° on mobile so the two sides stack vertically but still read as a comparison, gold "Total Value" pill that drives the sort
- News events get a four-pixel colored top border per category (player_pos green, player_neg red, finance amber, etc.) so a quick scroll tells you what kind of story is on each card before you read a word
- Coins History uses a sign-coded left border — green for income, red for spend — for ledger-style glanceability
- Tickets & Merchandise color-codes home vs. away (green/blue left border) and gold-tints the side that earned your revenue
What we kept
Not everything became a card. The cap-summary table on the Contracts page, the per-team season-stats matrix on team.php, the Hall of Fame trophy wall — these are still tabular because tabular is genuinely the right answer for "compare these N rows on every column." The point was never "no tables ever," it was "stop using a table when a card-per-entity is what the user actually wants."
The phone build is finally usable. The desktop build kept all its information density. Filter, sort, and pagination behave the same on every page in the game. And the next time we ship a new list view, the answer to "how should it look" is already on disk — include pcard_styles.php, drop in players_pcard.php, add a sort bar, you're done.
If you've been playing on mobile, this is the update where the experience stops being a compromise.