Most AI systems forget what they learn.
This one writes it down.
A structured corpus of hard-won knowledge accumulated across every development session. API quirks discovered the painful way. Design patterns refined through iteration. Architecture decisions documented before the rationale evaporated. Loaded into context at the start of every task so the machine doesn't have to rediscover the same pothole twice.
The System
How accumulated knowledge becomes a development accelerant
Every development session produces discoveries. A third-party API that silently returns
stale data during the offseason. A CSS token that renders fine on the design system
demo but breaks in every real component. A drag-and-drop library that will not work
if you wrap its items in a <div> instead of a
React.Fragment. Left undocumented, these discoveries evaporate — and
the next session rediscovers them from scratch.
The AI Insights System is a structured corpus organized into files by domain
(design-system.md, mfl-api.md, frontend.md)
and by feature (nav-redesign.md, custom-rankings.md).
Before any development task begins, relevant files are loaded into the AI's context.
After each task completes, new learnings are written back. The knowledge compounds.
"The AI Insights System is itself a demonstration of the same principles applied to the product: structured context, explicit formats, and a maintained feedback loop. The development workflow is the feature."
Design System
Tokens, visual patterns, and the editorial language derived from the PlayerDetailsModal
The design system grew bottom-up from a single modal component. The PlayerDetailsModal established a typographic and spatial language so coherent that it became the canonical reference for every page that followed. Most of what's documented here is "what the modal taught us" — and what had to be written down to survive the next refactor.
Editorial Design Standard
The left-border accent on uppercase section titles is the most recognizable editorial pattern in the codebase. Every new page must use it. The modal-derived typographic hierarchy — five distinct roles from 0.625rem table headers to 1.35rem hero titles — is now the site standard.
.section-title { border-left: 2px solid var(--color-primary); padding-left: 0.625rem; } Button & CTA Hierarchy (Official)
Dark blue (#1c497c) is the primary CTA. Green (#2e8743) is
reserved only for affirmative/go actions like submitting a bid or confirming
a roster change — not general-purpose CTAs. Ghost buttons pair with primary buttons,
never solo.
<a href> for navigation, <button> for
in-page actions. Same CSS classes, different elements. Never use <a>
without an href.
Broadcast Diagonal Cut (Flair Pattern)
ESPN/FOX-inspired parallelogram clip-path for hero sections that need sports-forward energy. Both edges use an identical 20% horizontal slope — creating true parallel lines that convey motion. Primary-blue accent stripes run along each diagonal edge via pseudo-elements.
players.astro).
Negative State: Subtle Accents, Not Color Floods
Negative/warning states use a red left-border accent (border-left: 3px solid var(--color-error))
and red text on key values. Never use large colored backgrounds or gradient fills.
The gray-50 card background stays neutral — the accent does all the signaling.
/* ❌ Not this */ background: var(--color-error-light);
/* ✓ This */ border-left: 3px solid var(--color-error); WCAG Contrast Correction: gray-400 vs gray-500
--color-gray-400 (#9ca3af, ~2.86:1 contrast) fails WCAG AA for text.
Labels, hints, and any readable text must use --color-gray-500
(#6b7280, ~4.63:1) or darker. Reserve gray-400 for decorative borders and
disabled controls only.
Nav Tokens Must Live in tokens.css
Nav CSS variables defined only in _nav-tokens.scss fail on pages that
import tokens.css but not the full compiled SCSS bundle.
All --nav-* tokens must be defined in both files — tokens.css
is the canonical source, SCSS is a mirror.
var(--nav-team-logo-size, 40px).
Modal Shell Spec (for new modals)
Overlay: rgba(15, 23, 42, 0.45) + backdrop-filter: blur(2px).
Modal: max-width: 580px, entry animation 0.32s ease-out with
scale(0.96→1) + translateY. Mobile: bottom-sheet slide-up with top-only radius.
Close button: 32px circle, absolute top-right.
Frontend Patterns
Component architecture, player display, headshots, and the gotchas that cost real time
Frontend work in this codebase has a consistent source of friction: third-party data that doesn't quite match what you expect, and small normalization gaps that produce broken logos or missing headshots for specific teams. The Player Lockup standard exists because every component that skipped it eventually had broken Kansas City Chiefs logos.
Player Lockup Standard (and why it exists)
Multiple components had slightly different player display patterns — some missed
DEF handling, some skipped normalizeTeamCode(). The result: broken
logos for KCC (file is KC.svg), JAC, NEP, NOS, and DEF players showing a generic
silhouette instead of the team logo.
PlayerCell.astro or buildPlayerCellHTML().
Test with Kansas City, Jacksonville, New England, and New Orleans. DEF handling
is automatic — don't special-case it.
ESPN Headshot URLs + College Fallback Chain
ESPN uses different athlete IDs for college and NFL — a player's college athlete ID
is NOT the same as their NFL player ID. MFL stores ESPN NFL IDs as espn_id
but 2026 draft prospects have none. College headshots require a separate mapping file
(data/theleague/espn-college-ids.json).
ESPN NFL → ESPN College → MFL headshot → placeholder Franchise History for Name/Logo Changes
Heavy Chevy rebranded to Dead Cap Walking for 2026. Historical views need to show
"Heavy Chevy" for 2024 data. A history[] array in
theleague.config.json with yearStart/yearEnd
plus all identity fields solves this via getTeamIdentityForYear().
getTeamIdentityForYear(team, year)
from src/utils/team-names.ts. Future rebrands only need a new
history entry in config.
Dark Mode: Both Media Query AND .dark Class
All new components must support dark mode via two mechanisms:
@media (prefers-color-scheme: dark) for OS-level preference,
and .dark class on :root for a manual toggle.
Use :global(.dark) in Astro scoped styles to target the class.
Clean URL Rewrites Break on Prerendered Pages
Host-level clean URL rewrites (/trade-builder →
/theleague/trade-builder) depend on runtime route resolution in
middleware.ts. Pages with prerender = true only exist as
static output and don't register as runtime route entries — the middleware rewrite
cannot resolve them.
prerender = false
while clean URL rewriting is active. Affected pages: calendar, league-summary,
trade-builder, import-rankings.
MFL API Quirks
Third-party API behavior that is not documented, not obvious, and not forgiving
MyFantasyLeague's API is mature but idiosyncratic. Most of what's documented here
is behavior that only becomes visible under specific conditions — the pre-rollover
window, a serverless execution environment, a league with a non-standard
draftPlayerPool setting. The notes exist so these conditions don't
produce mysterious failures that take hours to trace.
Login Authentication: XML, Not JSON
The MFL login endpoint does NOT support JSON=1 — it returns an
empty body. Use XML=1 instead. The response is XML:
<status MFL_USER_ID="base64cookie"/> on success,
<error>Invalid Password</error> on failure.
The standalone /myleagues endpoint always returns HTML from
server-side fetch — use export?TYPE=myleagues&JSON=1 instead.
Step 1: POST /login?XML=1 → parse XML for MFL_USER_ID
Step 2: GET /export?TYPE=myleagues&JSON=1 with Cookie: MFL_USER_ID=…
Rosters API Staleness During Pre-Rollover Window
Between season end and the Feb 14 league year rollover, the rosters
endpoint can return stale data for recent drops — confirmed 31+ hours after
drops occurred. The transactions and freeAgents endpoints
correctly reflected changes. The rosters endpoint is not the sole
source of truth during the offseason.
rosters with recent
TRANS_TYPE=FREE_AGENT transactions. Filter out players in
drop-only transactions (|{id}, format). Transaction endpoint requires
following HTTP 302 redirects — use -L or a redirect-following client.
myWatchList vs myDraftList
myDraftList is ordered but destructive (complete overwrite) and
possibly restricted to draftPlayerPool players.
myWatchList supports incremental ADD/REMOVE and has no documented
player restrictions — but is unordered (a set, not a ranked list).
myWatchList as the MFL-synced "flagged"
set. Store ranking order in Upstash Redis or local storage.
Use playerRanks (FantasySharks) as the default seed.
pointsAllowed: Full Season Only, Strings Not Numbers
The pointsAllowed DVP endpoint accepts a W (week) param
but completely ignores it — all requests return identical full-season cumulative
totals. Point values are strings — always parseFloat() before math.
MFL uses non-standard team codes (KCC, JAC, NEP) — always normalizeTeamCode().
nflSchedule. For season-long
averages, divide by 17. Filter out entries with name: "".
No Dedicated Franchise History API
MFL has no endpoint for franchise name/logo history. The year-specific
league export returns names as they were that year.
Pre-2016 seasons used different league IDs (e.g., 2007: 76273
on www42) — all mapped in the history.league array of the
current year's league data.
data/theleague/mfl-feeds/{YEAR}/league.json
is cached for 2011–2025. No additional API calls needed for historical names.
Free Agent Status "locked" in Offseason
Players dropped during the offseason appear in freeAgents with
status: "locked" — they cannot be claimed until the new league
year begins or waivers open. This is distinct from status: "available"
which means claims are open.
status: "locked" players with a visual indicator
(grayed out or lock icon) to signal they're not claimable.
Feature Architecture
What building each major feature taught us about the next one
Feature development is where abstract architectural decisions meet concrete implementation reality. The nav redesign taught us about the limits of prerendering and the importance of cookie-first server rendering. Custom rankings exposed edge cases in drag-and-drop library assumptions. Each feature's learnings file is a post-mortem for the next engineer.
@dnd-kit: Three Silent Killers
Drag-and-drop simply stops working if: (1) the sortable container has
overflow: hidden — dragged elements appear not to move;
(2) restrictToParentElement modifier is used — constrains
drag too aggressively; (3) sortable items are wrapped in extra
<div>s instead of React.Fragment — confuses
position tracking. All three issues can be present simultaneously and interact.
restrictToVerticalAxis modifier. Never add
overflow: hidden to the sortable container. Use
React.Fragment for item wrappers. Check
ManageImportsSection.tsx for a working reference pattern.
Upstash Redis for Cross-Device KV
For per-user state (<10KB) that needs cross-device sync without a full
database, Upstash Redis via @upstash/redis is the answer.
Auto-reads env vars (UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN).
Each request is a standalone HTTP call — no connection management.
key pattern: {feature}:{franchiseId}e.g. cr:0003 for franchise 0003's custom rankings Edit Mode Prevents Accidental Drag
@dnd-kit supports empty useSensors() which completely disables drag
without unmounting the context. Passing sensors={isEditing ? editSensors : noSensors}
gates drag to an explicit edit mode, preventing accidental reorders while browsing
the list.
:active.
Composite Hash for Staleness Detection
When rankings derive from user-configured composite sources, a hash of the
source config (sorted member IDs + player count) reliably detects when the
source data changed. On mismatch, mergeWithOverrides() re-seeds
from the new composite while preserving manual customizations.
MutationObserver Beats Custom Events for State Sync
The page wrapper needs to respond to drawer state changes (open/close) by
adjusting its margin-right. Using MutationObserver to watch for
class changes on the drawer element is more robust than custom events — it
catches all state changes regardless of how they were triggered, including
programmatic changes from window.navDrawer API calls.
Playwright Surfaced Critical Spacing Issues
E2E testing revealed nav links were 26.59px tall instead of the 40px minimum
touch target. SVG icons were rendering at browser default size (hundreds of pixels)
because Astro scoped CSS wasn't targeting nested SVGs — required
:global(.nav-links__icon svg) and explicit width/height
attributes on the SVG element itself.
width and height attributes to
SVGs used as nav icons.
Patterns That Compound
The meta-observations that emerge after enough individual insights accumulate
After enough individual insights accumulate, second-order patterns start to emerge. These aren't observations about specific API endpoints or CSS tokens — they're observations about the shape of problems in this codebase.
MFL's rosters endpoint returns stale data in the pre-rollover window.
ESPN's headshot CDN serves 404s for players without photos instead of
a fallback. pointsAllowed ignores the week parameter silently.
None of these are bugs in the traditional sense — they're edge conditions
that exist at the boundaries between systems. The documentation for these
conditions is the codebase.
A missing normalizeTeamCode() call doesn't fail loudly —
it silently serves a 404 for a logo file. Skipping DEF handling in a
player component doesn't crash — it shows a generic avatar instead of
the team logo. These failures are invisible until someone looks at the
specific player or team that triggers them. The Player Lockup standard
exists because every shortcut eventually produces a broken logo.
The clean URL rewrite system requires pages to be server-rendered.
The token system requires tokens.css as the canonical source.
The franchise history system requires a history[] array in config
rather than dynamic API calls. Each of these decisions has a straightforward
rationale that is obvious in hindsight — and expensive to retrofit when
discovered late. The insights corpus exists to make them obvious upfront.
"The best time to document a quirk is immediately after discovering it, before the workaround becomes load-bearing and the original problem is forgotten. The second best time is now."
The File Structure
The corpus lives in docs/claude/insights/. Domain files cover
cross-cutting concerns: design-system.md, frontend.md,
mfl-api.md, accessibility.md. Feature files cover
the learnings specific to each major feature: nav-redesign.md,
custom-rankings.md, and growing.
Each entry follows a consistent format: date, context, insight, evidence, recommendation. New entries are appended, not edited — the history of how understanding evolved is part of the value.
Message