Staging Real data, but not the live site — MFL writes, push and chat posts are switched off here.

Editorial Design System

The living reference for our typography, layout, and component patterns. Every section below shows a real pattern from the codebase — not abstract swatches.

Preview theme — does not change your setting

Typography

Type Scale

HERO TITLE Roster Analysis 1.35rem / 700
SECTION TITLE Contract Details 0.75rem / 700 / uppercase
BODY 3 years, $4.2M per year with 10% escalation 0.875rem / 400–500
META Kansas City Chiefs · QB · #15 0.875rem / 500 / gray-600
DETAIL LABEL COLLEGE 0.75rem / 600 / uppercase / gray-400
MICRO LABEL PER GAME 0.6875rem / 600 / uppercase
TABLE HEADER SALARY 0.625rem / 600 / uppercase / gray-400

Section Titles

Left-Border Accent (Signature Pattern)

The most recognizable editorial element. Used for every content section heading across all pages.

Contract Details

Season Results

Roster Breakdown

font-size: var(--font-size-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; border-left: 2px solid var(--color-primary);

Hero Banner

Text Only (No Image)

Eyebrow kicker + date, large editorial title, body copy, and CTA actions. Used when there's no screenshot to show.

New Feature Mar 2, 2026

The League Has an App Now

That's right. A nineteen-year-old fantasy football league has its own app. Install it to your home screen and try to act surprised.

variant="editorial" — no image prop

With Image (Split Layout)

Same editorial hero, but with a browser-framed screenshot on the right. Image floats right at ≥768px, stacks below on mobile.

New Feature Feb 26, 2026

The League Has an App Now

That's right. A nineteen-year-old fantasy football league has its own app. Install it to your home screen and try to act surprised.

theleague.us/whats-new
The League PWA installed on a home screen
variant="editorial" image="screenshot.webp" imageAlt="..."

Key Metrics

3-Column Metrics Grid

Hero-level stats in a tight grid. Large tabular-nums values with micro uppercase labels.

$38.2M
Cap Used
24
Rostered
$6.8M
Cap Space

Variant: 4-Column

287.4
Total Pts
18.0
Per Game
3
Yrs Left
$4.2M
Salary

Detail Rows

Label + Value Flex Rows

Fixed-width right-aligned uppercase labels with flexible values. Separated by subtle gray-50 borders. Not a table — flexbox.

Player Details

Status Active Roster
College Texas Tech
Draft 2017 Round 1, Pick 10
Build 6'2" / 225 lbs
Age 30

Contract

Type Veteran
Years 3 years (2024–2026)
Base $4,200,000
Current $5,082,000

Badges & Pills

Status Indicators

Pills (Neutral)

Age 27 3 YRS QB Round 1

Semantic Badges

Starter Veteran Questionable IR

Type Badges

Veteran Rookie Extension

Contract Status Designations

Compact chips displayed inline with salary & years. RC and TO are persistent — they follow a player across seasons. F is a decision result applied by the owner and resets each season.

RC TO F
In Context
RC Anthony Richardson $1.2M · 1 yr — Rookie Contract (non-1st round)
TO C.J. Stroud $2.5M · 2 yrs — 1st round, team option eligible
F Saquon Barkley $12.5M · 1 yr — Franchise tagged

Player Lockup

Default Size (40px avatar)

The standard player display. Circular headshot, bold name, NFL logo + position meta row. Uses PlayerCell.astro.

Patrick Mahomes headshot
Patrick Mahomes
QB
Ja'Marr Chase headshot
Ja'Marr Chase
WR
Justin Jefferson headshot
Justin Jefferson
WR
Travis Kelce headshot
Travis Kelce
TE
Christian McCaffrey headshot
Christian McCaffrey
RB

Compact Size (32px avatar)

For denser layouts like tables. Same pattern, smaller scale.

Patrick Mahomes headshot
Patrick Mahomes
QB
Ja'Marr Chase headshot
Ja'Marr Chase
WR
Justin Jefferson headshot
Justin Jefferson
WR

DEF Handling (Auto)

Team defenses automatically swap the headshot for the NFL team logo. No special caller logic needed.

BUF logo
Bills, Buffalo
DEF

Tables

Editorial Table

Sticky uppercase headers, hover rows, tabular-nums, and color-coded values. Player lockups in the first column.

Player Pos Salary Pts/G Yrs Status
Patrick Mahomes headshot
Patrick Mahomes
QB
QB $5,082,000 24.3 3 S
Ja'Marr Chase headshot
Ja'Marr Chase
WR
WR $3,850,000 19.7 2 S
Justin Jefferson headshot
Justin Jefferson
WR
WR $4,100,000 18.1 4 S
Travis Kelce headshot
Travis Kelce
TE
TE $2,200,000 12.4 1 Q
Christian McCaffrey headshot
Christian McCaffrey
RB
RB $3,600,000 0.0 2 IR
Total (5 players) $18,832,000 14.9

Interactive States

Selection Pattern

Interactive cards/options use left-border accent + subtle gradient to indicate selection.

1 Year
$4,200,000
2 Years
$4,620,000 avg
3 Years
$5,082,000 avg

Color Palette

Text Hierarchy

gray-900 Primary text
gray-700 Secondary text
gray-600 Tertiary / meta
gray-400 Labels / hints

Brand & Semantic

primary Accent, links, borders
success-dark Positive values
warning Caution states
error Negative / IR

Surfaces & Borders

white Card backgrounds
gray-50 Subtle bg, hover, light borders
gray-100 Pill bg, close button bg
content-border Card borders, dividers

Shadows

Elevation Scale

Layered shadows with blue-tinted color. Each level doubles offset/blur for realistic depth.

shadow-sm Buttons, inputs
shadow-md Cards, dropdowns
shadow-lg Popovers, nav
shadow-xl Modals, dialogs

Spacing Scale

Spacing Tokens

xs
4px
sm
8px
md
16px
lg
24px
xl
32px
2xl
48px

Border Radius

Radius Tokens

sm (4px)
md (8px)
lg (16px)
full (pill)

Buttons & CTAs

Every call-to-action on the site is one shared pattern, defined once in src/styles/cta.css and loaded by every layout. Three shapes: cta cta--primary, cta cta--ghost and cta-link. They work identically on <button> (in-page actions: submit, open a modal, run script) and <a> (anything that navigates). Never hand-roll a __cta / __btn look — add the shared class and keep your own BEM class only for layout (padding, width, margin). tests/cta-pattern.test.ts fails on a CTA-shaped link without it.

Primary — cta cta--primary

The standard filled CTA for page-level actions, forms and modals.

Link CTA Disabled link
Markup <a class="cta cta--primary" href="…">Change your vote</a>
Base --cta-bg → var(--btn-primary-bg)
Hover --cta-bg-hover → var(--btn-primary-bg-hover), translateY(-1px)
Text --cta-ink → var(--btn-primary-text) — never changes, never underlined
Shape padding 0.625rem 1.25rem · var(--radius-md) · var(--font-size-sm) · bold
Disabled :disabled or aria-disabled="true" → opacity 0.45, no hover

Ghost — cta cta--ghost

Outlined, transparent ground, link-coloured ink. The secondary action sitting beside a primary: "Back", "Cancel", "Maybe later".

Cancel Continue

Arrow link — cta-link

A text link that goes somewhere: "see more", cross-links between pages. The CSS draws the trailing arrow, so the label never types its own › or →.

Hover Convention

Filled background steps to its hover token + 1px lift; text colour never changes, never underlines
Arrow link ink steps to the link-hover token, the arrow nudges right, no underline
Why the state rules sit at (0,2,0) and beat every layout's global a:hover (red + underline)

Customising — the --cta-* variables

The base rules carry zero specificity, so your own class wins for layout. Colours are custom properties — set them on your class, never background / color directly (the hover rule reads the variables and would replace a hard-coded background). Filled and ghost: --cta-bg, --cta-bg-hover, --cta-ink, --cta-ink-hover, --cta-border, --cta-border-hover, --cta-focus-ring. Arrow link: --cta-link-ink, --cta-link-ink-hover.

Example — the green affirmative CTA ("Submit Bid"), reserved for actions where green signals "go". Not a general-purpose CTA; default to primary.

View Results
Inline style="--cta-bg: var(--btn-secondary-bg); --cta-bg-hover: var(--btn-secondary-bg-hover); --cta-ink: var(--btn-secondary-text); --cta-border: var(--btn-secondary-bg)"
Class .hero__cta { --cta-bg: #fff; --cta-bg-hover: color-mix(in srgb, #fff 88%, black); --cta-ink: var(--league-accent); --cta-border: #fff; }

Composite Example

Player Profile Card

A realistic composition showing how all editorial patterns combine into a cohesive layout.

Patrick Mahomes headshot
Patrick Mahomes Age 30
Kansas City · QB · #15
287.4
Total Pts
18.0
Per Game
$5.1M
Salary

Details

College Texas Tech
Draft 2017 Rd 1, Pick 10
Build 6'2" / 225 lbs
Status Starter

Contract

Type Veteran
Term 3 years (2024–2026)
Base $4,200,000
Current $5,082,000

Animation & Motion

Slide Animations

Entrance animations for modals, panels, sidebars, and floating UI. All use ease-out timing for smooth deceleration and forwards fill-mode to hold the end state.

Card Enter Desktop modals & tutorial cards. Subtle scale-up with vertical lift.
@keyframes cardEnter {
  from { opacity: 0; transform: scale(0.96) translateY(12px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}
DURATION 0.32s TIMING ease-out USAGE Modals, tutorial overlays
Card
Slide Up Mobile bottom-sheet modals. Full translateY(100%) to resting position.
@keyframes slideUp {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}
DURATION 0.3s TIMING ease-out USAGE Mobile modals, action sheets
Sheet
Panel In Step-to-step transitions within a modal. Short horizontal slide with fade.
@keyframes panelIn {
  from { opacity: 0; transform: translateX(16px); }
  to   { opacity: 1; transform: translateX(0); }
}
DURATION 0.3s TIMING ease-out USAGE Wizard steps, tab panels
Step 2
Trigger Enter Floating buttons anchored to viewport edge. Slides in from off-screen with delayed start.
@keyframes triggerEnter {
  from { transform: translateX(100%); opacity: 0; }
  to   { transform: translateX(0); opacity: 1; }
}
DURATION 0.5s DELAY 1.5s USAGE Floating CTAs, edge triggers
CTA

Motion Guidelines

Timing Always ease-out for entrances — elements decelerate into place
Duration 0.2s–0.35s for UI elements; 0.5s max for dramatic reveals
Fill Mode forwards when the element starts hidden (opacity:0)
Delays Use for staggered lists (0.05s increments) or deferred UI (1–2s)
Mobile Prefer slide-up (bottom sheet) over scale-enter on small screens
Transitions 0.3s cubic-bezier(0.4, 0, 0.2, 1) for interactive state changes (hover, focus)

Empty & Error States

Empty state

EmptyState.astro (React: states-react.tsx; scripts: buildEmptyStateHTML). Say what is missing, why that is fine or when it fills, and offer the next step. Action labels are verb + object.

No brackets yet

Brackets appear once playoff seeds are set after Week 14.

Error state

ErrorState.astro. Only for "we could not read it" — never for a well-formed empty answer. Say what failed, that the data is missing rather than zero, and how to recover.

Compact

Inside a card or a table cell, pass compact: no dashed panel (no card within a card), left-aligned with its neighbours.

No MVP votes yet.

Loading States

The Duration Ladder

Loading indicators are chosen by how long the user actually waits, not by page. The full contract lives in docs/claude/loading-standards.md; the shared primitives are src/components/shared/loading/* + src/styles/loading.css, with string builders in src/utils/loading-html.ts and the React hook useLoadingState() driving tier escalation via resolveLoadingTier().

< 0.3s Nothing — a loader that flashes reads as a glitch
0.3–1s Optimistic — show the result, reconcile in background
1–10s Skeleton for content, inline button spinner for discrete actions
10s+ Branded "on the wire" moment with narration (AI endpoints)

Spinner (tier 2/3)

Accent from var(--league-accent) — skins per league with no code branch. Sizes: compact / default / large.

Button Loading (tier 3)

Add loading-btn + is-loading to a button: spinner replaces the label and the button disables — double-submit prevention comes free. Use loading-btn--on-accent on solid accent fills.

Skeleton (tier 4)

Gray shimmer blocks mirroring the final layout. Brand-neutral by design. Variants: title / text / block / circle; count renders a stacked list.

Thinking Dots (tier 5, AI)

Extracted from the Schefter typing indicator. For AI waits under the branded threshold.

Roger is checking the constitution

Branded Moment (tier 5, 10s+)

The one tier that carries league character. Narration cycles via aria-label updates; reduced-motion collapses the pulse to a static ring. Fires only above the 10s threshold — never on routine loads.

Roger is deep in the constitution

Flipping to the relevant bylaws…

Rules

Reduced motion Every loader collapses to a static-but-present state with the same ARIA label — the guard ships in loading.css
Tokens Zero hex in loader code — accent is --league-accent, fills are --color-gray-100/200
ARIA role="status" + aria-busy + polite live region on every surface; button spinners add disabled