Every component, rendered from ds/registry.json v3 (2026-07-10). Registry-first: consult before building, extend variants over forking, one-offs are findings.
Swaps CSS vars on the previews only - the app stays lab
TOKENS
Raw values + semantic roles - the only home for literals.04 ENTRIES
a11y · brand theme: accent text <32px must use BRAND_ACCENT.text #00D0DE, never #00F0FF (OLED bloom rule); same split principle applies to lab volt when text-heavy
rule · class strings must be complete literals (Tailwind v4 JIT scanning)
rule · canvas/gradient call sites use accentHex(theme, accent), never a local hex map
rule · domain narrowing helpers (chromeAccent, vip bone) stay in their domains and consume this map
rule · MIGRATION refit item 7 (app accent sweep) LANDED 2026-06-13 (Wave 2b): the 4 app-local hex maps are gone — vip-accents deleted, landing/identity-theme trimmed to HERO_CTA, identity-accents trimmed to the chromeAccent/archetypeGlyph helpers; every accent consumer now reads accentHex(lab,·)/ACCENT_CLASSES from here (pinned to lab for byte-parity). No NEW app code may add another local accent map
rule · brand-skin cyan slot is deep azure #0EA5E9 (shifted from lab #29E6FF, which collides with the brand volt-slot #00F0FF ladder — Tim-ratified 2026-06-13)
used by · ds-internal: 23+ files across primitives/motion/patterns/igaming (SectionTag, MagneticButton, ContourField, GlowWash, NudgeShell, LedgerRow, OddsButton, MicroMarketCard, TierRung, …) · app-direct (Wave 2b accent sweep — local hex/class maps deleted, consumers now import accentHex(lab,·) + ACCENT_CLASSES straight from this api): identity (IdentityShiftScene), you (SkillCard, IdentityHeader, WarArts, VipBand, QuickStats, QuickNavCards), vip (VipHero, TierUpMoment), chrome (PlayerMenu, MobileMenu, MobileMenuFooter), join (SceneYours, SceneArrival), community (CrewCard, RaceBoard), ds/igaming (EngagementDock, OrbitalBar)
layout-tokens
draftds/tokens/semantic.ts
PAGE_CONTAINER / PAGE_MAX_W / PAGE_GUTTER_X — the ONE page-content gutter, byte-aligned to the header nav row (max-w-[1500px] px-5 md:px-8) so the SWOOBZ logo's left edge lines up with page content at every breakpoint. Replaces the drifted vertical/leaf containers (max-w-[1400px] px-5 md:px-10, ~58px narrower).
rule · class strings are complete literals (Tailwind v4 JIT scanning); compose page-specific spacing via cn(PAGE_CONTAINER, '...')
rule · PAGE_CONTAINER carries mx-auto w-full max-w-[1500px] px-5 md:px-8 — do not re-add mx-auto/w-full at the call site
rule · rails whose gutter lives on inner bleed-scroll children (LaneRail) use PAGE_MAX_W + md:px-8 on the container and keep px-5 md:px-0 on the children
rule · the VerticalHeroScene containerClassName default IS PAGE_CONTAINER; only pass an override for a deliberately non-standard gutter
used by · ds: VerticalHeroScene (default containerClassName) · app: app/sportsbook/page.tsx, components/sportsbook/{SportsbookHeader,LaneRail,LeagueChips,LiveRail,detail/MatchHero,detail/MatchDetail}, components/predictions/PredictionsHeader
motion-tokens
draftds/tokens/motion.ts
EASE, SPRING families (cursor/snap/standard/sheet/slowFill/pop/burst), DURATION, DWELL_MS, STAGGER, PULL_CAP, Z ladder
EASE (out-expo)
SPRING.cursor
SPRING.cursorWrap
SPRING.cursorFollow
SPRING.snap
SPRING.standard
SPRING.sheet
SPRING.slowFill
SPRING.pop
SPRING.burst
rule · components import EASE/SPRING from here — no local ease arrays or spring literals after refit
rule · animate transform + opacity only (verified motion discipline)
rule · z-[NN] literals must match the Z ladder; MobileSlip scrim moves 64 → Z.sheet (74) in refit
used by · all motion call sites after refit
density-tokens
draftds/tokens/density.ts
Admin density scale (comfortable/compact/ultra-compact) + densityToCssVars; flat --density-* namespace driving the [data-density] cascade. comfortable mirrors today's player geometry (SSR-safe default).
comfortablecompactultra-compact
comfortable
compact
ultra-compact
rule · drift-tested against globals.css via density-css.test.ts — every default + both override blocks + both @custom-variant lines must match densityToCssVars()
rule · comfortable is the SSR-safe default (mirrors today's player geometry); admin never reskins the player
rule · Exposure-Red is the ONLY sanctioned [data-domain][data-density] compound selector — in ultra-compact admin --color-hero swaps volt→blood so KPI/exposure surfaces consuming var(--color-hero) inherit it for free; no other compound data-attr selector may be added
rule · --color-hero is a NEW semantic alias added to BOTH lab+brand themes (defaults to the primary accent so it exists outside admin)
used by · ds/admin/DensityBoundary.tsx · app/globals.css ([data-density] blocks)
PRIMITIVES
Buttons, inputs, meters - the atoms every surface consumes.20 ENTRIES
glyph
stableds/primitives/Glyph.tsx
THE icon system — 29 stroke glyphs (added logout for the admin top-bar sign-out control; activity/beaker/layers/ticket/users/link/sliders/chart/arrow landed for the admin scaffold + cockpit wave), currentColor, weight prop. Glyph-only icons platform-wide
a11y · radiogroup semantics; selection carried by solid accent fill + aria-checked, never color alone; arrow keys step with wrap and move focus (roving tabindex); FOCUS_RING token
rule · step/wrap logic lives in chip-selection.ts (tested)
rule · chip rows keep individual chips <= 256px (cursor WRAP_MAX_W) so the wrap-ring treatment is deterministic at every viewport
a11y · FOCUS_RING token on interactive pills; interactive pills are buttons, badges are spans (never focusable); type from MICRO_LABEL.label
rule · CTAs stay magnetic-button; TradeButtons stays its own igaming pair
particle-burst
draftds/primitives/ParticleBurst.tsx
Radial particle burst with palette prop
prop
type
default
palette
readonly string[]
BURST_PALETTES.gold
count
number
22
reach
number (min scatter px; band to reach*2.4)
90
originClassName
string (complete literal)
"left-1/2 top-1/2"
className
string
—
motion · SPRING.burst scatter + DURATION.burst opacity fade; cleans up on parent unmount
a11y · aria-hidden; purely decorative
rule · LOSS states never burst — celebration is for wins above wager only (anti-LDW)
rule · scatter math lives in burst-geometry.ts (tested, injectable rand)
rule · refit landed: founder/GoldBurst deleted and LevelUpOverlay's local Particles removed — both consume this
used by · ClaimSuccess · LevelUpOverlay
tab-strip
stableds/primitives/TabStrip.tsx
The single sliding-pill segmented control: one hairline track with a volt layoutId pill behind labeled segments. Active = solid fill + aria-selected/checked + label (never color alone); arrows step with wrap via chip-selection and move focus via roving tabindex; segment|strip variants; tablist|radiogroup semantics. The trailing count (TabOption.count) renders as a SUBORDINATE de-tracked tabular tally (dimmer than the label, no caps tracking) so a counted strip reads 'label first, count second' — not the glued 'SLOTS 164' token. strip wrap defaults true; wrap={false} keeps one line for an overflow-x-auto scroll parent (a flex-nowrap className cannot override the base flex-wrap — cn is clsx, not twMerge).
motion · layoutId pill slides SPRING.snap; entrance is opacity/travel-free — the pill must NOT fly from a scaling ancestor (verified-facts no-scaling-ancestor rule)
a11y · tablist|radiogroup + roving tabindex; selection carried by solid fill + aria-selected/checked + label, never color alone; arrows step+wrap via chip-selection
rule · id is the layoutId scope — must be unique per mounted strip so pills never share an origin
rule · no scaling ancestor may sit above the strip (layoutId measurement — verified-facts)
rule · player-facing immersive register (motion-forward sliding pill) — do NOT fold the denser/quieter admin Tabs kit into this; separate register
rule · pure logic (role mapping + roving tabindex) lives in tab-selection.ts; keyboard step reuses chip-selection.ts
rule · draft→stable 2026-06-13 (Wave 2b consolidation): shipped across /history, predictions, casino + community segmented rows and design-qa'd for byte-parity; the gated sportsbook ScopeSegment + LaneRail stay hand-rolled (filmed no-scroll-jump filter gate + layoutId measurement) and are deferred, NOT migrated
used by · components/history/HistoryFilters.tsx · components/predictions/CategoryFilter.tsx · components/community/Boards.tsx · components/casino/filter/LobbyFilterBar.tsx · components/casino/filter/parts/FilterPanel.tsx · components/casino/filter/parts/ThemeStrip.tsx
status-dot
draftds/primitives/StatusDot.tsx
Pulsing status/live dot. Decorative by default (aria-hidden); pass label when the dot signals LIVE/active so the meaning reaches assistive tech via sr-only text — status is never color/animation-only (triple-channel rule). Shared across player + admin registers.
LivebloodvoltmagentagoldSTATIC XS
prop
type
default
accent
AccentName
"blood"
size
"xs" | "sm"
"sm"
pulse
boolean
true
label
string
—
className
string
—
a11y · decorative => aria-hidden; meaningful (LIVE) => role=status + sr-only label so status is never color/animation-only (triple-channel rule)
rule · purely presentational — no admin variant needed; consumed by both player + admin registers
rule · meaning must come from the label (or an adjacent label) — the dot alone never carries status
tooltip
draftds/primitives/Tooltip.tsx
Hover + focus-visible tooltip. The trigger owns focus and gets aria-describedby; the bubble is role=tooltip, non-focusable, mounted/unmounted via AnimatePresence (no persistent aria-hidden toggle), Escape dismisses. Rides Z.tooltip (above modal/menu/playerMenu) so a trigger inside any overlay still surfaces. bg-coal/90 glass surface + ACCENT_CLASSES.borderSoft hairline + MICRO_LABEL.label type. Shows on hover AND focus (keyboard parity — never hover-only); over-limit triggers use aria-disabled (not native disabled) so they stay in the tab order. Supplementary text only — never the sole channel (triple-channel rule).
hiddenopen
prop
type
default
children
ReactNode (the focusable trigger)
—
content
ReactNode
—
placement
"top" | "bottom"
"top"
accent
AccentName
"bone"
className
string
—
motion · AnimatePresence opacity + 4px travel via DURATION.crossfade (0.2) + EASE; reduced-motion → opacity only
a11y · trigger gets aria-describedby when open; bubble is role=tooltip + non-focusable, AnimatePresence mount/unmount (no aria-hidden toggle); Escape dismisses; supplementary text only (never the sole channel)
rule · supplementary text only — never the sole channel (triple-channel)
rule · shows on hover AND focus-visible (keyboard parity, never hover-only)
rule · a disabled trigger must use aria-disabled + stay focusable (not native `disabled`, which leaves the tab order and makes the bubble hover-only) — the over-limit deposit presets follow this
rule · bubble rides Z.tooltip (82, above modal/menu/playerMenu) via style={{ zIndex }} — never a bare z-50 literal off the ladder
used by · components/join/SceneFuel.tsx · components/wallet/DepositTab.tsx
avatar
draftds/primitives/Avatar.tsx
Identity disc — initials fallback or a fading image, ringed in the accent. Sizes xs(h-6)/sm(h-8, default)/md(h-11); accent drives the border ring + initials tint (default volt); optional corner status reuses ds/primitives/StatusDot (composition). Image path uses alt={name}; initials path is role=img + aria-label with the initials span aria-hidden; status carries a StatusDot label so online/live is never color-only. Image opacity crossfade on load (DURATION.crossfade), onError → initials.
used by · components/community/cosweat/CoSweatSeat.tsx (Wave 4 — first real app consumer; seat faces)
swoobz-wordmark
draftds/primitives/SwoobzWordmark.tsx
The real SWOOBZ wordmark as inlined SVG paths — replaces the plain-text "SWOOBZ" that was standing in for the logo in the admin top bar. Single currentColor fill (the source asset's warm #fd4e07 gradient is DROPPED — cyan-only accent brand rule forbids injecting a second hue into the mark), so it inherits the header text color and has zero external-asset/CSP dependency.
LIVE PREVIEW NEEDS APP CONTEXT · SEE ds: AdminTopBar (default brand slot, replacing the plain-text SWOOBZ span)
prop
type
default
className
string
—
title
string (flips to role=img + <title>; omit for decorative aria-hidden)
—
a11y · decorative by default (aria-hidden, no role); pass title to make it an accessible role=img landmark (the admin top bar passes title="SWOOBZ")
rule · no path may carry its own fill/class — every path renders on the single currentColor fill so no warm hue can smuggle back in from the source asset
rule · aspect ratio is fixed by the source viewBox — scale via height only (h-* + w-auto), never stretch
used by · ds: AdminTopBar (default brand slot, replacing the plain-text SWOOBZ span)
switch
draftds/primitives/Switch.tsx
Player-register toggle — a real <button role=switch aria-checked>; h-7 w-12 rounded-full track, h-[18px] knob, transition-all duration-300 (the player pace), data-cursor=hover (the player cursor ring), accent glow when on (SELECTED_GLOW.volt for volt else ACCENT_CLASSES.glow, gated off when disabled). FOCUS_RING; Space/Enter toggle natively; disabled dims to opacity-40 and drops the cursor ring. Triple-channel: knob POSITION + accent fill + label all signal state, never color alone.
offondisabled
prop
type
default
checked
boolean
—
onChange
(next: boolean) => void
—
label
ReactNode
—
accent
AccentName
"volt"
disabled
boolean
false
ariaLabel
string (required when no visible label)
—
className
string
—
motion · knob + track transition-all duration-300 (the player pace); reduced-motion collapses harmlessly via the OS
a11y · role=switch + aria-checked; Space/Enter toggle natively; FOCUS_RING; ariaLabel required when no visible label; triple-channel (knob POSITION + accent fill + label)
rule · PLAYER REGISTER — carries the cursor ring + 300ms pace + accent glow; the calm ds/admin Switch (100ms peer-checkbox, no ring) stays separate, mirroring the StatusDot player/admin split
rule · accent glow is gated off when disabled
used by · components/settings/PreferencesSection.tsx
skeleton
draftds/primitives/Skeleton.tsx
Shimmer placeholder bar carrying the house animate-shimmer sweep (SHIMMER_SWEEP recipe, WHITE_ALPHA.sweep midpoint) — no new gradient literal. width/height are Tailwind literals from the caller (defaults w-full/h-3). aria-hidden — the loading announcement belongs to Spinner / the table role. Register-neutral placeholder shared by player + admin loading surfaces.
prop
type
default
width
string (Tailwind literal)
"w-full"
height
string (Tailwind literal)
"h-3"
className
string
—
a11y · aria-hidden (loading announcement belongs to Spinner/table role)
rule · reuses the house animate-shimmer utility + SHIMMER_SWEEP recipe (WHITE_ALPHA.sweep midpoint) — NO new gradient literal
rule · register-neutral — a separate player Skeleton would be a byte-identical fork (forbidden duplication); player + admin loading surfaces share this primitive
used by · data-table (loading rows)
mini-trend
draftds/primitives/MiniTrend.tsx
Compact STATIC KPI sparkline — the admin-register twin of the igaming Sparkline (no motion, no gradient id, so it renders in a server component). Min..max-scaled polyline, volt rising / blood falling via var(--color-*), fog tone for skeleton ghosts. Under two points draws an honest dashed resting baseline, never a fabricated flat line. Path math is the node-tested mini-trend-path helper.
Rising
Falling
Resting
prop
type
default
series
readonly number[]
—
tone
"auto" | "fog"
"auto"
className
string
—
a11y · aria-hidden — direction MUST also be carried by an adjacent signed-delta string, never the tint alone
rule · static (no motion) — the admin calm budget
rule · no gradient id / no hooks — renders inside a server component
rule · < 2 points => honest dashed resting baseline, never a fake flat line
rule · tints via accentVar (var(--color-*)) — theme-reactive, no raw hex
used by · scaffold-preview · dashboard-card trend slot
MOTION
Entrances, rails, overlays - immersion as the standard.16 ENTRIES
reveal
stableds/motion/Reveal.tsx
Scroll entrance wrapper — rise + fade + unblur
RISES + FADES + UNBLURS INTO VIEW
prop
type
default
delay
number
0
y
number
44
once
boolean
true
motion · DURATION.reveal with EASE
used by · 44 files
scramble-text
stableds/motion/ScrambleText.tsx
Decode-in headline text
EVERY ACTION IS A MARKET
prop
type
default
text
string
—
speed
number
28
delay
number
0
a11y · aria-label keeps the real text during scramble
used by · 7 headers
marquee
stableds/motion/Marquee.tsx
Seamless infinite strip — dual-render loop
VOLT ◆MAGENTA ◆CYAN ◆GOLD ◆BLOOD ◆
VOLT ◆MAGENTA ◆CYAN ◆GOLD ◆BLOOD ◆
prop
type
default
duration
number
MARQUEE_S.base
reverse
boolean
false
pauseOnHover
boolean
false
used by · GamesFilmstrip · LiveTicker
carousel
stableds/motion/Carousel.tsx
Drag-momentum rail with click-guard, edge mask, arrows
CARD 1
CARD 2
CARD 3
CARD 4
CARD 5
CARD 6
prop
type
default
accentBar
string (Tailwind bg-* class for the progress fill)
The dot is the pointer - opt in with data-cursor; the live layer is already active on this page.
motion · SPRING.cursor ring chase; Z.cursorRing/Z.cursorDot
rule · data-cursor / data-cursor-color is the opt-in contract
rule · the dot IS the pointer — it never disappears (failed-attempts 2026-06-12)
rule · volt + ink stroke come from theme tokens, not literals
rule · WRAP_MAX_W=256 / WRAP_MAX_H=64 gate the wrap-ring; wider targets get the free ring + visible dot — size demo containers so interactive targets stay under the gate
rule · every ds overlay (sheet, modal, celebration) registers through this so FounderNudge's overlay-hold gate keeps working
rule · pure decisions (drag-dismiss thresholds, focus wrap) live in ds/motion/overlay-logic.ts with node tests
use-pointer-parallax
stableds/motion/use-pointer-parallax.ts
Pointer parallax springs (±14px) for hero focal layers
±14px pointer parallax springs for hero focal layers. Active on vertical-hero-scene's focal slot.
used by · VerticalHeroScene · BossHero
use-breakpoint
stableds/motion/use-breakpoint.ts
Media-query hook for desktop/mobile branching
(min-width:640px) → MOBILELive value on this viewport - re-renders on resize.
used by · MicroMarketDock · GamesFilmstrip · TriggerLab · WalletSurface
command-palette
draftcomponents/search/CommandPalette.tsx
Cmd/Ctrl+K global command palette — fuzzy subsequence search across nav/games/markets/fixtures/actions; top-anchored command bar with arrow+Enter+Esc, role=combobox/listbox + aria-activedescendant, focus-trap via use-overlay, Z.modal scrim. Enter navigates (router.push) or fires the wallet-open action; opacity/travel entrance (no scale — verified-facts). Presentation-only: navigation + the deposit action, never money.
closedopenno-results
LIVE PREVIEW NEEDS APP CONTEXT · SEE components/chrome/TopNav.tsx (CHROME-MAKER mounts SearchTrigger) · app/layout.tsx (CHROME-MAKER mounts CommandPalette + CommandKListener)
prop
type
default
(CommandPalette)
no props — reads useCommand open state
—
(SearchTrigger)
{ className?: string } — the TopNav opener button
—
(CommandKListener)
no props — global ⌘K keydown, null render
—
motion · AnimatePresence opacity scrim (DURATION.crossfade) + panel opacity/travel (DURATION.expand, EASE) — never scale on the ancestor (verified-facts layoutId rule)
used by · components/chrome/TopNav.tsx (CHROME-MAKER mounts SearchTrigger) · app/layout.tsx (CHROME-MAKER mounts CommandPalette + CommandKListener)
PATTERNS
Panels, stat grids, ledgers, shells - composed surfaces.17 ENTRIES
panel
draftds/patterns/Panel.tsx
Surface panel: surface glass|coal|well
glasscoalwell
glass
coal
well
prop
type
default
surface
"glass" | "coal" | "well"
"coal"
rounded
"none" | "xl" | "2xl"
"2xl"
rule · padding stays with the consumer; dashed empty-state wells add border-dashed via className
rule · PANEL_SURFACE class map is exported so sibling patterns (StatTile) wear a surface without nesting
used by · StatTile
spotlight-coach
draftds/patterns/SpotlightCoach.tsx
Guided first-run coachmark: a dimming scrim with a transparent cutout ring around a live target (data-guide-anchor selector) + an anchored card (eyebrow / title / body / Back / Next / Skip + step dots). NON-BLOCKING (portal pointer-events:none except the card; the cutout is a shadow, so the highlighted element stays tappable), resumable (controlled stepIndex or defaultStepIndex), dismissible (Skip / Escape / scrim), graceful-degrade to a centered card when no target resolves. Has NO round-awareness: the consumer gates open on the settle beat (useNudgeBeatReady) so a beat never paints over a live round. Portals to document.body. Pure geometry + state hooks in spotlight-coach-geometry.ts (banned-hook split).
voltmagentacyangoldbloodfogbone
LIVE PREVIEW NEEDS APP CONTEXT · SEE app: SurfaceGuideCoach (components/onboarding)
prop
type
default
steps
ReadonlyArray<SpotlightCoachStep>
—
stepIndex
number (controlled resume cursor)
—
open
boolean (consumer gates first-run + the settle beat)
callbacks (onStepPrimary returns true = handled, do not advance)
—
rule · the coach is NOT its own gate: the consumer MUST gate open on useNudgeBeatReady so a beat never paints over a live/unsettled round (round-guard law)
rule · target anchors are data-guide-anchor selectors on the real UI; a missing anchor degrades to a centered card (never a dead-end)
rule · content lives as DATA in a per-surface registry (app: lib/onboarding/surface-guides.ts), never scattered JSX
rule · one voice at a time: while a coach is active the AmbientNudgeSlot + GuideToast stand down (activeSurface signal)
used by · app: SurfaceGuideCoach (components/onboarding)
pagination
draftds/patterns/Pagination.tsx
Player Pagination — discrete page Pills (solid=active, ghost=others+arrows) with first/last + current ± siblings and ellipsis gaps; window math in pagination-range.ts (tested). nav[aria-label]; active page aria-current=page; prev/next real buttons with sr-only labels + disabled at bounds; ellipsis aria-hidden. No layoutId (discrete buttons, not a sliding segment — TabStrip owns that register). The caret is a decorative CSS chevron (rotated border edges, aria-hidden — the sanctioned ds/admin/Select pattern), NOT a Glyph. No money math — page math only.
first-pagelast-pageper-page-active
prop
type
default
page
number (1-based current page)
—
pageCount
number (>= 1)
—
onChange
(page: number) => void
—
accent
AccentName
"volt"
siblingCount
number (pages either side of current)
1
ariaLabel
string
"Pagination"
className
string
—
a11y · nav[aria-label]; active page aria-current=page; prev/next real buttons with sr-only labels + disabled at bounds; ellipsis aria-hidden
rule · player register — the OPERATOR/admin pager is a separate ds/admin entry (long-tail, task #32); do not fold them
rule · no money math — page math only; window math lives in pagination-range.ts (tested)
rule · no layoutId (discrete buttons, not a sliding segment — TabStrip owns that register)
rule · caret is a decorative CSS chevron (ds/admin/Select pattern), not a Glyph; if a chevron Glyph is later added it can adopt it
accordion
draftds/patterns/Accordion.tsx
Click-to-disclose content accordion (greenfield for completeness). Each item is a coal Panel; the header is button[aria-expanded][aria-controls]; the panel is role=region[aria-labelledby] mounted via AnimatePresence height-expand (DURATION.expand/EASE) with a chevron rotating 180°, reduced-motion → instant. Single-open default + allowMultiple; roving Arrow Up/Down + Home/End move focus between triggers; FOCUS_RING on triggers. The chevron is a decorative CSS caret (ds/admin/Select pattern), not a Glyph.
singlemultiplecollapsedexpanded
HOW PAYOUTS WORK
Settled plays credit your balance the moment a market resolves. Server-formatted display strings only - the floor never does money math client-side.
DEPOSIT LIMITS
Set a per-session ceiling in settings. Presets above your limit dim out and explain why on hover - the limit is honored before any intent is built.
RESPONSIBLE PLAY
Reduced-motion follows your OS. Self-exclusion and cool-off tools live in settings and apply instantly across every surface.
a11y · header button[aria-expanded][aria-controls]; panel role=region[aria-labelledby]; roving Arrow Up/Down + Home/End move focus between triggers; FOCUS_RING on triggers
rule · greenfield for completeness — this is the click-to-disclose content pattern for future FAQ/detail-meta surfaces
rule · the FILED disclosures (FilmstripCard hover-expand + nav toggles) are NOT accordions and stay out of scope
rule · chevron is a decorative CSS caret (ds/admin/Select pattern), not a Glyph
rule · roving-focus + open/close math live in accordion-nav.ts (tested)
used by · app/faq/FaqAccordion.tsx · app/help/HelpAccordion.tsx
breadcrumb
stableds/patterns/Breadcrumb.tsx
Multi-segment breadcrumb trail (greenfield for completeness — the multi-segment trail for nested detail routes). nav[aria-label=Breadcrumb] > ol > li; the last item is aria-current=page rendered as text (not a link); separators are aria-hidden; links wear accent text + FOCUS_RING. Optional Link prop (next/link or TransitionLink) — defaults to a plain <a> so Breadcrumb keeps no hard dep.
linkcurrent
prop
type
default
items
readonly BreadcrumbItem[] {label, href?}
—
accent
AccentName
"fog"
separator
ReactNode (rendered aria-hidden)
"/"
Link
ComponentType<{href, className?, children}> (next/link or TransitionLink)
plain <a>
className
string
—
motion · none beyond link transition-colors
a11y · nav[aria-label=Breadcrumb] > ol > li; last item aria-current=page rendered as text (not a link); separators aria-hidden; links wear accent text + FOCUS_RING
rule · landed a live consumer (page-crumb) in the wayfinding wave — graduated draft→stable; the one-level ← BACK TO X links stay single-segment back-links and out of scope
rule · optional Link prop defaults to a plain <a> so Breadcrumb keeps no hard dep; the page-crumb wrapper passes next/link for client-side + prefetch
rule · API needs zero changes to serve page-crumb — the trail/aria-current/separator contract is already complete
used by · page-crumb
page-crumb
draftds/patterns/PageCrumb.tsx
Placement shell over the Breadcrumb primitive — the ONE source of truth for where a hub-page breadcrumb sits. Wraps Breadcrumb in the shared rail gutter `mx-auto max-w-7xl px-5 sm:px-8` + `pt-24` (clears the fixed h-16 nav) + `mb-6` before content, and passes `next/link` as Link so trail links are client-side + prefetched. Trail is HOME / parent / current; the personal hubs route their parent through /you (the command center). Hard Link to the parent (never router.back()) — deterministic on the way back regardless of entry path, matching the shipped detail back-links.
prop
type
default
items
readonly BreadcrumbItem[] {label, href?}
—
accent
AccentName
Breadcrumb default ("fog")
className
string
—
a11y · inherits Breadcrumb: nav[aria-label=Breadcrumb] > ol > li; last item aria-current=page rendered as text (not a link); separators aria-hidden; links wear accent text + FOCUS_RING. Crumb links come first in DOM (under nav, before the page title) = correct tab/reading order
rule · thin wrapper — placement only; never re-implement the trail markup (that is Breadcrumb's contract). PageCrumb owns the gutter/top-pad/Link-binding, nothing else
rule · next/link is the only Link binding — client-side + prefetch on hub trails
rule · hero pages mount PageCrumb as the FIRST child before the hero (hero keeps its own top padding); no-hero pages REMOVE their root pt-24 (PageCrumb supplies it)
rule · presentation only — no money math, no money invariant surface touched
used by · app/bonuses/page.tsx · app/history/page.tsx · app/vip/page.tsx · app/settings/page.tsx · app/you/page.tsx · app/community/page.tsx · app/season/page.tsx
content-page
draftds/patterns/ContentPage.tsx
The shared shell for all static content/legal/SEO pages. PageCrumb (nav clearance) + an Anton hero with mono eyebrow + lede + a pure anchor-link TOC (no JS), then an anchored <main> column of ContentSections. Server component — markup only, zero store reads, no money math (SSG by construction). Interactive children (the FAQ Accordion) are client islands passed into a section body. content-prose.ts is the single source of prose class literals + the pure tested sectionAnchor slug helper; ContentSection renders one anchored <section id> with scroll-mt-24 nav clearance.
EXAMPLE
CONTENT PAGE
The shared shell for every static content / legal page - Anton hero, mono eyebrow, anchored prose.
■ 01 / OVERVIEW
WHAT THIS IS
One server component the content / legal pages compose - pure presentation, no store reads, no money. Real OP stakes, no cash wagering yet.
■ 02 / STRUCTURE
HOW IT COMPOSES
PageCrumb for nav clearance + wayfinding.
An anchored column of ContentSections.
Interactive bodies (Accordion, DefinitionList) passed in.
LAST UPDATED · JUNE 2026
prop
type
default
crumbs
readonly BreadcrumbItem[] {label, href?}
—
eyebrow
string (mono kicker)
—
title
ReactNode (Anton title; one word may wear text-stroke-<accent>)
—
lede
ReactNode (one-line summary; mirrors meta description)
—
accent
AccentName (legal set = fog; trust set = vertical accents)
a11y · PageCrumb landmark first in DOM (correct reading order); TOC is nav[aria-label='On this page'] of pure #anchor links; each ContentSection is <section aria-labelledby> with scroll-mt-24 nav clearance; FOCUS_RING on TOC links
rule · server component — no "use client", no store reads, no bus.emit, no money math (SSG + money invariant by construction)
rule · all prose styling flows from content-prose.ts (single source of complete Tailwind v4 literals) — never inline ad-hoc prose classes
rule · honest-claims law: pages composing this shell state demo + play-money + no-real-money where relevant; fairness never claims a real RNG cert / licence / real money
rule · DefinitionList + Accordion are the reusable bodies — a <dl> or disclosure block is a ds element, never inline app markup (no-inline-UI rule)
used by · app/legal/terms/page.tsx · app/legal/privacy/page.tsx · app/legal/cookies/page.tsx · app/responsible-play/page.tsx · app/how-it-works/page.tsx · app/fairness/page.tsx · app/about/page.tsx · app/faq/page.tsx · app/glossary/page.tsx
definition-list
draftds/patterns/DefinitionList.tsx
The glossary <dl> term/definition primitive. Each pair is <dt> (Anton, text-bone) + <dd> (content-prose body); an accent tints an optional editorial leading rule reusing the shipped gold-stripe idiom (border-<accent>/30 pl-4). Server component, pure presentation — no store reads, no money.
FLUID iGAMING
Every action can spawn its own behavior micro-market.
◈
Ownership Points - real stakes in the floor's economy. Not cash, not withdrawable yet.
DETERMINISTIC SIM
The same seed produces the same outcome sequence.
prop
type
default
items
readonly Definition[] {term, def}
—
accent
AccentName (tints the optional leading rule)
—
className
string
—
rule · server component — pure presentation, no store reads, no money
rule · the <dl> is a ds element, never inline app markup (no-inline-UI rule)
rule · accent rule literals are complete per-accent strings (Tailwind v4 JIT), no interpolation
used by · app/glossary/page.tsx
account-rail
stableds/patterns/AccountRail.tsx
Persistent YOU-group side menu (StyleguideNav lineage). Desktop lg+: a sticky left rail listing the 5 account destinations (YOU/BONUSES/VIP/SETTINGS/HISTORY) with an active highlight (usePathname + the pure accountRailActiveHref helper); collapses to a sticky horizontal chip row on mobile (mask edge-fade + active chip centered on mount). Wraps {children} as the 2-col grid content column so account pages share one wayfinding spine.
account page content
a11y · nav landmark (aria-label='Account sections'); aria-current=page on the active destination; native next/link keyboard nav + FOCUS_RING.
rule · single source of truth for the YOU-group link set lives in ds/patterns/account-rail-links.ts (ACCOUNT_RAIL_LINKS + the pure tested accountRailActiveHref)
rule · do NOT wrap a page that already has its own in-page nav rail (e.g. /you's command-center zone-jumper) — stacking both is a double-menu regression (failed-attempts 2026-06-13)
rule · mobile chip row sits at z-40, under the chrome ladder (60+); client island, host pages stay server components / SSG
used by · app/bonuses · app/vip · app/settings · app/history
stat-tile
draftds/patterns/StatTile.tsx
Label/value/foot stat tile on the MICRO_LABEL scale; adds admin KPI delta?/isLive? variants (pre-formatted sign-in-text delta with accent by direction up→volt/down→blood/flat→fog, never caller-set; isLive pulses an inlined cyan dot — the patterns layer takes no upward import from ds/admin)
a11y · values use tabular-nums; label/value association via dl semantics (renders a dt/dd group — must live inside StatGrid's dl)
rule · non-interactive by design — wrap the grid cell, not the tile, for click targets
rule · delta accent derives from direction via DELTA_ACCENT (up→volt, down→blood, flat→fog) — never caller-set
rule · isLive is the ONLY cyan entry in the admin KPI register (the cyan budget gate); the live dot is INLINED so patterns takes no upward import from ds/admin/StatusDot
rule · delta.value carries its own sign — no money math in the DS
stat-grid
draftds/patterns/StatGrid.tsx
Responsive grid of StatTiles, hairline-gap variant included (composition contract — tiles arrive as children, not imports)
gaphairline
LEVEL
7
XP
2,340
QUESTS
3
STREAK
W2
prop
type
default
columns
2 | 3 | 4 | 5
2
variant
"gap" | "hairline"
"gap"
a11y · renders the <dl> that StatTile's dt/dd groups require
rule · hairline tiles need an opaque surface — the 1px line grid shows through gap-px seams only
ledger-row
stableds/patterns/LedgerRow.tsx
Mono transaction/position row: label, amount, timestamp, status accent
SLIP WON · ARSENAL TO WIN+120 ◈14:02
QUICK PLAY · NEON DICE−50 ◈13:48
DEPOSIT+500 ◈PENDING13:30
prop
type
default
label
ReactNode
—
leading
ReactNode (direction badge / live dot / glyph)
—
amount
string — PRE-FORMATTED, sign included
—
amountAccent
AccentName
"bone"
status
{ label, accent: AccentName, pulse? }
—
timestamp
string (hidden under sm)
—
a11y · amounts tabular-nums; gains/losses carried by sign + text, never color alone (triple-channel)
rule · amounts arrive pre-formatted with an explicit sign — this row never does money math
rule · renders an <li>; the consumer owns the <ul> (divide-y divide-line for the classic ledger)
rule · draft→stable 2026-06-13 (Wave 2b registry hygiene): graduated — shipped across history/wallet/you/notifications/admin (6 real surfaces) + styleguide preview; the empty usedBy was stale, now populated with the actual import edges
used by · components/history/HistoryList.tsx · components/history/history-rows.ts · components/wallet/WalletHistory.tsx · components/you/TransactionList.tsx · components/notifications/NotificationRow.tsx · ds/admin/DataTable.tsx · components/styleguide/previews-patterns.tsx
nudge-shell
draftds/patterns/NudgeShell.tsx
Toast/nudge shell with the cap + snooze + overlay-hold gates built in
rule · static forever: never animates, never tracks the pointer (one glow layer per hero)
used by · VerticalHeroScene
empty-state
draftds/patterns/EmptyState.tsx
Empty/error state block, two density registers: `page` (giant 404/not-found headline) and `compact` (disc-framed glyph + mono kicker + foot, for in-chrome empties like the notification feed). The compact register replaces the inline empty block that used to live in NotificationFeed.
GlyphId (disc-framed mark — compact register only)
—
rule · CTA and ambient are SLOTS — the shell imports no stores, canvas or buttons (error boundaries stay dependency-light)
rule · compact density drops the giant headline + ambient — disc glyph + kicker + foot only, so it fits a popover or right Sheet without blowing the frame
rule · consumers own vertical centering via className/wrapper — cn() is plain clsx, so never pass a conflicting display utility onto the register root
used by · app/not-found · app/error · notification feed (compact)
IGAMING
Odds, markets, HUD, celebrations - the vertical recipes.33 ENTRIES
odds-value
stableds/igaming/OddsValue.tsx
Odds number that flashes on change — up volt / down blood for DWELL_MS.oddsFlash
steadyflash-upflash-down
2.45Flashes volt/blood + signed delta on live change
prop
type
default
value
number
—
className
string
—
a11y · triple-channel (WCAG 1.4.1): direction carried by signed delta text + flash, NEVER color alone; tabular-nums
used by · QuickBetControls · OddsButton · SlipBody
odds-button
stableds/igaming/OddsButton.tsx
Canonical odds control wired to the slip store; selected = volt fill + glow. `selection` (OddsSelectionButton) is the derived-market variant: slot-id per market group + responsive shortLabel.
smlgselectionidleselected
Taps write the slip store - presentation only, no money.
prop
type
default
matchId/home/away/pick/pickLabel/odds
selection identity
—
size
"sm" | "lg"
"sm"
motion · SPRING.snap select
a11y · selected carried by fill + aria-pressed + label, never color alone; focus-visible ring
rule · slip mutations via slip-store only; money via bus events only
rule · selected glow comes from SELECTED_GLOW (semantic.ts) — no inline rgba
used by · LiveMatchCard · MatchBoard · detail/MarketsBoard
trade-buttons
stableds/igaming/TradeButtons.tsx
YES/NO trade pair — cyan/magenta, price-aware
prop
type
default
yesPrice
number
—
activeSide
"yes" | "no" | null
—
onPick
(side: "yes" | "no") => void
—
disabled
boolean
false
size
"sm" | "lg"
"sm"
a11y · side carried by YES/NO text + price, never color alone (triple-channel)
used by · MarketCard · TradeDesk · PredictionsHeroModule
probability-bar
stableds/igaming/ProbabilityBar.tsx
Cyan spring probability fill (0..1) — becomes a ProgressBar skin during refit (design-qa first: spring physics + cyan glow must not silently change)
62%
used by · MarketCard · TapeStats
context-strip
draftds/igaming/ContextStrip.tsx
DISPLAY-ONLY model context strip: model 1X2 lead + form + h2h home strength as compact labeled cyan mini-bars. Fail-closed null → renders nothing. NEVER a settlement/money input.
MODEL48%ARSENAL
FORM65%ARSENAL
H2H60%ARSENAL
FORM60%CHELSEA
prop
type
default
context
WireMarketContext | null (display ints 0..100)
—
homeTeam
string
—
awayTeam
string
—
className
string
—
a11y · decorative model suggestion; the percent + side label carry meaning, never the fill tint alone
rule · DISPLAY-ONLY - never a settlement or OP input; the market settles on the final score
rule · fail-closed: a null context or a context with no surviving reading renders nothing (no empty strip)
rule · the cyan fill is bg-cyan; small accent percents use text-cyan/90 (OLED halation rule)
used by · RichMarketCardShell
sparkline
stableds/igaming/Sparkline.tsx
Price sparkline — rising volt / falling blood tint, spring dot. `detail` is the hero size: fixed 0..100¢ scale, gridlines, DURATION.drawFast.
defaultdetail
25¢50¢75¢
0 real ticks - awaiting first move, not a fabricated flat line
Same resting state at the detail (hero) size
prop
type
default
series
number[] (detail expects 0..1 prices)
—
variant
"default" | "detail"
"default"
className
string
—
motion · DURATION.draw path draw
a11y · direction also carried by signed change text near the chart, never tint alone
rule · refit still owes: hand-rolled ring → progress-ring, stake row → chip-selector
rule · STATUS HONESTY: resolved-lost still renders the shipped blood treatment — flipping to the NEUTRAL loss contract (a11y note) is a refit design-qa change, not a silent M4 edit
rule · compact stake chips intentionally NOT refit to chip-selector (compact geometry differs) — revisit only with a design-qa pass
used by · MicroMarketDock · MicroMarketSheet
micro-market-dock
stableds/igaming/MicroMarketDock.tsx
Desktop micro-market stack (max 4 + "+N MORE") at Z.microMarkets; steps left of the open chat dock
spawned by your win streak
DEMO
16
HOT HAND DETECTED
You're on a 3-win run. Does the next round hit ≥2×?
Desktop stacks ≤4 of these + "+N MORE"; runs fixed in chrome at every route.
used by · app/layout
micro-market-sheet
stableds/igaming/MicroMarketSheet.tsx
Mobile pill → sheet at Z.sheet; one auto-expand per session; suppressed while chat sheet is open
Mobile pill ⇄ bottom sheet; one auto-expand per session - the fixed pill lives in chrome on mobile. RUNNING AT /casino.
rule · panel re-base onto the Sheet primitive stays refit (ds/motion/Sheet was draft-unbuilt when M4 landed); springs snapped to SPRING.sheet/standard/pop
used by · MicroMarketDock
slip-dock
draftds/igaming/SlipDock.tsx
Persistent global betslip dock (mounted once in app/layout beside MicroMarketDock so it follows the navigation-surviving slip). HYBRID: desktop floating SlipDockTab (bottom-4 right-4 z-[70]=Z.microMarkets, below the mm stack, chatDockOpen step-aside) appears only when the in-flow SlipColumn scrolls out of view (IntersectionObserver → useSlipDockUi), opening SlipBody verbatim in a right-side Sheet; mobile = the existing MobileSlip re-homed
Tap any odds to build a slip. Stack legs for a parlay.
Real OP wager · results settle from the real match · track it in ACTIVE BETS.
Expanded surface the dock opens in a right-side Sheet. The floating tab appears bottom-right only when this column scrolls out of view + BUMPS on every leg add - pure presentation, no money moves here.
motion · count + combined-odds BUMP key={count} scale [1,1.08,1] (0.3s) + SPRING.pop pulse-dot ping on every leg add; tab enter/exit y24 SPRING.standard via AnimatePresence; right→ step-aside transition-[right] 500ms on chatDockOpen
a11y · tab is a button aria-label="Open bet slip"; pulse-dot + REVIEW → arrow are aria-hidden; Sheet carries role=dialog aria-modal + focus trap + ESC via use-overlay (ariaLabel="Bet slip"); status/leg copy announced as text, never color-only
rule · PURE PRESENTATION — reads useSlip selectors (selections/status/legResults) + opens a Sheet; NO bus.emit, NO useEngagement, NO second bet:placed emitter; placement still flows through slip-engine.placeSlip via SlipBody
rule · money invariant untouched: slip-store/slip-engine/SlipBody/MobileSlip are CONSUMED ONLY, never modified (the module-level navigation-surviving settleTimer + SWEAT-THE-SLIP bus spawn stay intact)
rule · z-[70] is the spec value mapped to Z.microMarkets — sits spatially BELOW the mm desktop stack (bottom-44 up) so they never overlap; adopts the mm chatDockOpen lg:right-[376px] step-aside
rule · self-suppresses entirely (returns null) when count===0 && status===idle via dockHasContent — no chrome while the slip is EMPTY; a LIVE slip (any leg, or a pending/won/lost flight) follows across surfaces by design (spec dd 2 "follow the live slip across board↔detail↔beyond" + the HERO opt-in surfaces a draft in the dock from the landing page), so the dock is intentionally NOT route-gated; the "tap any odds" empty copy stays in SlipBody's empty state (single home, no second empty surface)
rule · no fly-to / layoutId across containers — the bump is an in-place key={count} pulse (avoids the cross-container layoutId trap, docs/MEMORY-TRAPS.md)
rule · pure derivation lives in slip-dock-logic.ts (node-testable: dockHasContent / dockTabVisible / dockTabLabel / dockOddsChip / dockTint); the shell stays thin
rule · useSlipDockUi declared create-if-missing, EXACTLY {columnInView, setColumnInView} — defaults columnInView:true so the tab stays quiet until a SlipColumn mounts; carries one boolean, zero money/ledger field
rule · draft until the refit lands it in the live flow (M-B wires app/layout + swaps both sticky SlipBody columns to SlipColumn + removes the double page-local MobileSlip) + a design-qa pass
used by · app/layout
engagement-dock
stableds/igaming/EngagementDock.tsx
XP HUD: quests strip + XP bar + level diamond + "+XP" chip; hidden on /you
XP bar + level diamond + quests chip, docked bottom-right (hidden <sm, null on /you). Reads the ledger XP - presentation HUD. RUNNING AT /casino.
motion · SPRING.slowFill bar; shimmer sweep from token recipe
rule · refit: bar → progress-bar(shimmer) still owed; sweep gradient → WHITE_ALPHA.sweep LANDED (registry-lint 2026-06-12)
used by · app/layout
quest-drawer
stableds/igaming/QuestDrawer.tsx
Right-side quest drawer with claim flow at Z.menu
Right-side quest + boss-HP drawer - reads the ledger read-only (zero-state: 0 claimable, full boss HP). The drawer's ✕ closes it.
rule · refit: re-base on Sheet(side: right); bars → progress-bar
used by · app/layout
level-up-overlay
stableds/igaming/LevelUpOverlay.tsx
Level-up celebration at Z.levelUp — join-grace sequencing, DWELL_MS.levelUp dwell
Fires on level-up (ledger-driven) - the same burst composition is live in celebration-shell above. RUNNING AT /casino.
rule · refit: particle-burst re-base LANDED; celebration-shell re-base still owed
rule · wins only — never fires on a loss (anti-LDW)
used by · app/layout
orbital-nav
stableds/igaming/orbital/OrbitalNav.tsx
Mobile thumb nav system: XP top-edge line (OrbitalBar), FAB + arc (OrbitalArc) at Z.orbital*
keyedguestbadgedarc-open
Mobile thumb nav: XP top-edge line + FAB arc (fixed, <sm only). RUNNING AT /casino on mobile.
a11y · arc buttons carry aria-label from their ORBITAL_ACTIONS entry (failed-attempts 2026-06-12 gap CLOSED in M4 — keep labels when extending actions); FAB carries aria-expanded
rule · XP line animates from explicit initial width, never auto
rule · FAB fill is opaque — translucent FAB over the progress line reads broken
Every threshold is published. Every benefit is listed. Nothing unlocks behind a curtain - when your wagered total crosses a line, the rung is yours, automatically.
rule · a LINK, never a modal, never gates play (frozen engagement spec §0)
rule · refit landed: all three hero modules re-pointed here (ModuleRibbon ×2 + sportsbook AccountRibbon deleted); casino passes idle=null, predictions passes its season-intel link
rule · flip to stable after design-qa on the refit build (status honesty)
used by · CasinoMomentModule · PredictionsHeroModule · SportsbookMoment
rule · reads personalization profile only — never writes; reasonLabel chip always visible
used by · 6 detail/end pages
slip-status-banner
stableds/igaming/SlipStatusBanner.tsx
Slip placed/won/lost banner — re-bases on nudge-shell during refit
Presentation slip status - no money settles here.
a11y · loss carried by the SLIP DIED text — no icon, no celebration; the full NEUTRAL loss contract (zero color, no shake) is the refit target (triple-channel loss rule)
rule · STATUS HONESTY: lost still renders the shipped blood treatment + shake — flipping to the NEUTRAL loss contract (a11y note) is a refit design-qa change, not a silent edit (same rule as micro-market-card)
used by · SlipBody
lane-rail
draftcomponents/sportsbook/LaneRail.tsx
Sticky sport-lane filter bar: ALL + 6 sport chips (volt layoutId pill, pulsing blood live dot, padded fixture count, behavior-ranked order from lane-prefs), scope segment right / second row on mobile, contextual league chips fading in underneath — never a league axis at lane=ALL.
motion · layoutId sportsbook-lane-pill slides SPRING.snap; league strip fades DURATION.crossfade; no scaling ancestor may sit above the bar (layoutId measurement — verified-facts)
a11y · radiogroup + roving tabindex, arrows step via ds chip-selection; active carried by solid volt fill + aria-checked, never color alone; floated chips add sr-only 'ranked by your bets'
rule · sticky top-16 z-50 — deliberately below Z.nav (60), NOT a new ladder rung
rule · no filter drawers/sheets — inline pills only (a drawer hides state)
rule · URL sync reads window.location.search ONCE on mount via history.replaceState — never useSearchParams (keeps /sportsbook SSG)
rule · filter state lives in lane-store (§3.1) — MatchBoard and LiveRail consume the same store, so the rail filters with the board
rule · app-side recipe (store-coupled) — registered for registry-first discovery; a ds/ fold is a later-wave call
rule · league chips render INSIDE the sticky bar — picking a lane mid-scroll reveals the league axis pinned under the rail, never off-screen at the board top
rule · /ds preview = context-route fallback (RUNNING AT /sportsbook) — mounting the rail on /ds would run use-lane-url-sync and rewrite the /ds URL; ratified convention for full-bleed store-coupled chrome (verified-facts)
used by · app/sportsbook/page.tsx
scope-segment
draftcomponents/sportsbook/ScopeSegment.tsx
ALL / TODAY / LIVE segmented control — three segments in one hairline capsule, the active segment carried by the sliding volt pill.
activeinactive
Live lane-store control - /sportsbook reseeds scope from its URL on mount
a11y · radiogroup + roving tabindex, arrows step via ds chip-selection; active carried by solid volt fill + aria-checked, never color alone
rule · scope definitions live in lane-store (§3.1): live = !!match.live; today = live OR startsInMin <= 1440; all = everything
used by · LaneRail
board-section
draftcomponents/sportsbook/BoardSection.tsx
Grouped-board section header — the SAME anatomy whether the group is a sport (lane=ALL) or a league (lane=sport): Glyph + name + count + live dot, mono 10px, hairline underline, scroll-mt-20 anchor.
STANLEY CUP FINAL· 0201 LIVE
prop
type
default
anchor
string (scroll-anchor id)
—
sport
Sport (Glyph via SPORT_ICONS)
—
label
string
—
count
number (padStart(2, "0"))
—
liveCount
number (blood dot + NN LIVE when > 0)
—
rule · rendered as a popLayout SIBLING of its rows, never their parent — fixture row keys stay stable across grouping changes (the stable-container/no-remount gate)
used by · MatchBoard
shared-slip-card
draftcomponents/slip/SharedSlip.tsx
Shared-slip view: matchArt hero painted from the first leg's fixture (continuity with the book), STAGGER.base leg cascade, honest repricing — current odds always shown, shared snapshot struck through + ODDS MOVED chip (volt up / blood down) when |Δ| ≥ 0.05 — and the single LOAD INTO YOUR SLIP CTA.
okdead-linklegs-expiredbusy
ARSENAL TO WIN
Arsenal v Liverpool · PICK 1
ODDS MOVED
2.45
2.51
UNDER 5.5 · PANTHERS V OILERS
Panthers v Oilers · PICK 2
1.84
Leg anatomy - struck shared price + ODDS MOVED is the only sanctioned strike-through; full card runs at /slip/[code]
motion · hero + legs enter SPRING.standard, legs cascade STAGGER.base y 14→0
a11y · odds movement carried by the ODDS MOVED chip text + struck price, never color alone; DEMO label on the hero
rule · loading NEVER places — LOAD only writes useSlip when status === 'idle' (else FINISH YOUR SWEAT FIRST); placement always flows through placeSlip() with the balance check; no deposit prompt
rule · always repriced to current data-derived odds — the struck shared price is the sportsbook's ONLY sanctioned strike-through (anti-dark-pattern by construction)
rule · fail-closed: any codec rejection renders the empty-state error, never a crash; unresolvable legs drop with a count notice
used by · app/slip/[code]/page.tsx
slip-share-button
draftcomponents/sportsbook/SlipShareButton.tsx
Share entry point — ghost SHARE SLIP while building, gold SHARE THE CASH on a settled win; the share chain prefers the richest channel: canvas card file share > URL share > clipboard copy (+ card download), confirming inline with LINK COPIED.
idle-ghostwon-goldpreparingcopiedfailedhidden
Renders null on an empty slip - never a dead share affordance
rule · renders null with an empty slip — never a dead share affordance
rule · the 1200×630 canvas card + share chain live in lib/slip-share/share-card.ts; the code comes from lib/slip-share/codec.ts (labels never encoded — rebuilt from static data)
rule · share is presentation only — zero bus events, zero store writes
rule · click flips to disabled PREPARING… synchronously BEFORE the canvas render (fonts.ready + 1200×630 draw + toBlob take seconds) — the CTA must never read dead or accept double-clicks
rule · inline confirm on the button (AccountSection precedent) is the ratified deviation from §5.3's guide-toast wording — verified-facts
Bell + activity feed — a presentation-only zustand store aggregates bus events into read/unread notifications (ring buffer 30); Sheet on mobile / glass popover on desktop; NEVER emits money events or touches the ledger. Maps ONLY real emitters (bet:won/lost, market:resolved, bonus:granted/offered, quest:completed, level:up, wallet:deposited/withdrawn, founder:claimed, account:created) — no tier/season events (not in the typed bus).
emptyunreadall-read
LIVE PREVIEW NEEDS APP CONTEXT · SEE components/chrome/TopNav.tsx (CHROME-MAKER mounts NotificationBell) · app/layout.tsx (CHROME-MAKER mounts NotificationsProvider)
motion · popover opacity+y travel (EASE) · mobile Sheet side=right (SPRING.sheet) · no scale on entrance
a11y · bell aria-label is dynamic ('Notifications — N unread'); rows are buttons with mono text + relative time; popover follows the WAI-ARIA menu pattern via use-menu-popover (ESC, outside-click, arrow roving)
rule · store READS the bus only — no bus.emit, no useEngagement; money invariant by construction (store.test.ts asserts emit never called + rows carry no numeric money field)
rule · ring buffer cap 30 (NOTIFICATIONS_CAP) — newest first
rule · maps ONLY events the typed bus actually emits (noise events drop to null in map.ts)
rule · mark-read on CLOSE, not open — the unread highlight persists while the feed is up
rule · Bell owns the Feed state internally; chrome mounts only <NotificationBell/> + <NotificationsProvider/>
used by · components/chrome/TopNav.tsx (CHROME-MAKER mounts NotificationBell) · app/layout.tsx (CHROME-MAKER mounts NotificationsProvider)
Named co-sweat 'play together' shells — each card is a TransitionLink into a real detail page (same art string both sides). The seat rail shows REAL sim members (memberFor) through ds/primitives/Avatar accented by archetype, with honest OPEN chairs (dashed nodes Glyph) so the shell reads as joinable, never an always-full social-proof wall. Header + a per-card SIMULATED Pill state it plainly: simulated co-presence on demo credits, zero fake social proof, no real-multiplayer claim. CHALLENGE lifts the lead seated member to the parent (M-WIRE), which drops a callout draft into the chat composer — the EXISTING challenge seam, unforked. Presentation-only: NEVER bus.emit / useEngagement / money.
ssr-empty (pre-mount, empty chairs)seated (filled + open chairs)challenge-armed
PULL UP A SEAT
NAMED SHELLS · SIMULATED CO-PRESENCE · OWNERSHIP POINTS
Named co-sweat shells - real sim faces (Avatar) + honest open chairs, labelled SIMULATED. CHALLENGE is a no-op here (no parent seam) - never a fake send.
a11y · filled seats: Avatar role=img/alt={handle} + sr-only '{handle}, {archetype}, simulated' so the simulated nature reaches assistive tech; open chairs: role=img aria-label='Open seat'; SIMULATED badge is text (never color-only); CHALLENGE is a real button; seat-count label is mono text, never a bare dot
rule · HONEST simulated co-presence — every face is a real sim member (memberFor + SIM_NAMES), seats deterministic on a 45s window (no Math.random); never invents handles
rule · always >= 1 open chair and >= 2 seated — the rail never reads as a closed crowd, never inflates occupancy (cosweat-data.test asserts the bounds)
rule · labelled SIMULATED / DEMO CREDITS — zero fake social proof, no real-multiplayer claim (keeps the TableCard honesty contract)
rule · presentation-only — reads memberFor + seeded sim; NEVER bus.emit, NEVER useEngagement; CHALLENGE reuses the chat composer-prefill seam through the parent, never moves a credit
rule · Glyph-only icons (bolt CTA, nodes open-chair), no emoji, no raw hex/EASE/z literals (ds/tokens)
rule · M-WIRE mounts <CoSweatTables onChallenge=…/> in app/community/page.tsx via the CoSweatSection client wrapper (the server page can't hold the useChatUi handler) and passes the composer-prefill handler; onChallenge is optional (honest no-op default) so the section stands alone
used by · components/community/cosweat/CoSweatSection.tsx (M-WIRE client mount wrapper — owns the requestPrefill(challengeLine(member)) seam) · app/community/page.tsx (M-WIRE mounts <CoSweatSection /> at slot 03, between LiveTables and FloorTalks)
SPORTSBOOK
Board chrome - store-coupled scan strips and lane filters.01 ENTRIES
sportsbook-sport-meta-strip
draftcomponents/sportsbook/SportMetaStrip.tsx
Per-sport scan strip surfacing live posture (N LIVE / M), league spread (N LEAGUES) and next kickoff (NEXT · home—away · 1H). Renders inside the pinned LeagueChips slot, above the league chips, only when a lane is committed (lane !== 'all'). Three pure facts from board-groups' sportMeta() selector; presentation-only, emits nothing.
Client wrapper stamping data-density (+data-domain) so the var cascade re-skins descendants; publishes density-context for portals that escape the DOM cascade.
compact
ultra-compact
prop
type
default
density
Density
—
domain
"admin" | null
null
className
string
—
children
ReactNode
—
a11y · presentation wrapper only — no role; geometry change is visual + var-driven
used by · data-table · page-header · admin previews
admin-badge
draftds/admin/Badge.tsx
Admin status badge — status vocabulary → accent + label; non-focusable <span>. Status is conveyed by LABEL text + accent, never color alone, never a left-stripe. Re-exports StatusDot.
Label + control + hint/error wrapper; wires id (label htmlFor), aria-describedby (to hint/error), and pushes invalid into the control on error. Error message is mono + blood.
defaulterror
Hex address, no prefix
Must be a positive number
prop
type
default
label
ReactNode
—
hint
ReactNode
—
error
string
—
children
control element
—
a11y · label htmlFor + aria-describedby to the hint/error; error message mono+blood, control border blood via its invalid prop
rule · error replaces the hint and turns it blood
rule · composes any density-driven control by slot
Token-skinned checkbox; volt fill on checked with a pure-CSS tick. Native input drives state (peer); FOCUS_RING via peer-focus-visible.
uncheckedcheckedfocus-visibledisabled
prop
type
default
label
ReactNode
—
...native
InputHTMLAttributes (forwardRef)
—
a11y · native input drives state; FOCUS_RING via peer-focus-visible
rule · native input drives state (peer)
rule · pure-CSS tick (no Glyph)
rule · ≤100ms transition
used by · admin forms
admin-switch
draftds/admin/Switch.tsx
Token-skinned toggle (native checkbox role=switch) skinned as track + knob; track fills volt on checked, knob slides via translate.
offonfocus-visibledisabled
prop
type
default
label
ReactNode
—
...native
InputHTMLAttributes (forwardRef)
—
a11y · role=switch; FOCUS_RING via peer-focus-visible
rule · track fills volt on checked, knob slides via translate
rule · no glow
used by · admin forms
admin-search-input
draftds/admin/SearchInput.tsx
Search field — Input geometry + leading pure-CSS magnifier + a real focusable clear button. Height var(--density-input-h).
prop
type
default
value
string
—
onValueChange
(value: string) => void
—
clearLabel
string
"Clear search"
...native
InputHTMLAttributes
—
a11y · clear is a real focusable button with aria-label; FOCUS_RING
rule · leading magnifier is a pure-CSS mark (NOT a Glyph — Glyph.tsx locked, no search glyph)
rule · clear is a real focusable button
rule · no glow
used by · admin tables/toolbars
admin-modal-parts
draftds/admin/ModalParts.tsx
Compound Modal (root + .Header / .Body / .Footer) over ds/motion/Modal — composes the existing overlay (opacity+travel entrance, scrim tiers, useOverlay ESC/scroll-lock/focus); does NOT re-implement it. Close control is a CSS '×'.
rule · composes the EXISTING ds/motion/Modal — does NOT re-implement the overlay
rule · close control is a CSS '×' (Glyph locked)
used by · reveal-with-reason · admin dialogs
reveal-with-reason
draftds/admin/RevealWithReason.tsx
Masked-by-default value with a reason-required reveal → onReveal(reason). Owns only modal-open + reason-input state; the revealed value comes from the parent after onReveal resolves (never derived client-side).
maskedreason-modal-openrevealedreveal-error
•••.•••.•••.••
prop
type
default
maskedDisplay
ReactNode
—
revealedDisplay
ReactNode
—
isRevealed
boolean
—
reasonRequired
boolean
true
reasonMinLen
number
8
reasonPlaceholder
string
—
onReveal
(reason: string) => Promise<void>
—
onError
(err: unknown) => void
—
onRemask
() => void
—
eventTypeDisplay
string
—
fieldLabelDisplay
string
—
size
"sm" | "md"
—
className
string
—
data-testid
string
—
a11y · reason input is a native <textarea> (Input has no multiline variant); confirm gated on reason length
rule · owns ONLY modal-open + reason-input state
rule · revealed value comes from the parent after onReveal resolves — NEVER derived client-side
rule · confirm gated on reason.trim().length >= reasonMinLen
rule · rejection keeps modal open, surfaced via onError
rule · MONEY INVARIANT — onReveal is caller-owned, this component NEVER emits a bus event
used by · masked-pii-cell
masked-pii-cell
draftds/admin/MaskedPiiCell.tsx
Masked PII table cell — kind → fieldLabel + audit-event via pii-contract; composes RevealWithReason. Both masked AND revealed forms are server-supplied props (no masking arithmetic).
+1 ••• ••• ••42
prop
type
default
kind
"ip" | "phone" | "id-number" | "kyc-doc"
—
maskedDisplay
ReactNode
—
revealedDisplay
ReactNode
—
isRevealed
boolean
—
reasonMinLen
number
8
onReveal
(reason: string) => Promise<void>
—
onError
(err: unknown) => void
—
onRemask
() => void
—
fieldLabelOverride
string
—
eventTypeOverride
string
—
size
"sm" | "md"
—
className
string
—
a11y · inherits RevealWithReason reason ceremony
rule · kind ONLY drives field-label + audit-event default — NO masking arithmetic
rule · both masked AND revealed forms are server-supplied props
rule · Badge is passed by caller via the cell render-prop, not imported by DataTable
rule · search/filter narrow ONLY the rows already loaded — never a server call (data-table-filter.ts)
rule · truncate uses the native `title` attribute (not the DS Tooltip primitive) — a non-interactive span carrying tabIndex fails lint/a11y/noNoninteractiveTabindex, and CSS truncation never removes the full text from the a11y tree anyway
rule · truncate is a no-op for non-string cell content — never puts arbitrary JSX in a title attribute
used by · admin lists/triage
page-header
draftds/admin/PageHeader.tsx
Admin page header: optional breadcrumb slot + mono eyebrow + Anton title + one-line purpose + subtitle + actions slot + KPI stats slot + up to 3 related-surface links. Composes AdminButton by SLOT (imports no button — stays presentational); actions pin right on desktop, stack on mobile. v2 slots (breadcrumb/purpose/related, Wave 1 item 6) are additive and opt-in.
PLAYER OPERATIONS
Accounts
Search, flag and triage player accounts.
ACTIVE12,408+3.1%
FLAGGED42−6
LIABILITY€1.24M
prop
type
default
breadcrumb
ReactNode (pre-rendered, e.g. <Breadcrumb items={breadcrumbTrailFor(...)} />)
PageHeaderRelatedLink[] {label, href} — capped at 3, extra entries dropped
—
Link
ComponentType<{href, className?, children}> — client-routed nav for related links
plain <a>
className
string
—
a11y · title is the page heading; eyebrow is mono context; related links render inside nav[aria-label="Related pages"]
rule · composes AdminButton by SLOT (imports no button — stays presentational)
rule · actions pin right on desktop, stack mobile
rule · stats slot hosts a KpiStrip / DashboardCard row
rule · breadcrumb is a pre-rendered slot — PageHeader never derives a trail itself (use breadcrumbTrailFor)
rule · AdminPageScaffold consumers keep using its own top-level breadcrumbs prop (rendered above PageHeader) — the breadcrumb slot here is for the ~13 direct PageHeader consumers
used by · admin pages
admin-page-scaffold
draftds/admin/AdminPageScaffold.tsx
The one page-shell composite: Breadcrumb (only when a non-empty trail is given) → PageHeader (eyebrow/title/subtitle/actions/stats, forwarded unchanged) → children, all inside `flex flex-col gap-6 p-6` — formalizes the wrapper ~50 admin page.tsx/*Client.tsx files already hand-copy (e.g. partner-applications/page.tsx:25, cashier/CashierHubClient.tsx:86). Purely additive; touches neither Breadcrumb nor PageHeader, zero existing PageHeader consumers regressed.
LIVE PREVIEW NEEDS APP CONTEXT · SEE ds/admin/AdminPageScaffold.tsx
prop
type
default
breadcrumbs
readonly BreadcrumbItem[] (optional — omit or pass [] on a group's top-level landing page, §5.2)
—
eyebrow
ReactNode (forwarded to PageHeader)
—
title
ReactNode (forwarded to PageHeader, required)
—
purpose
ReactNode (forwarded to PageHeader — one-line job-language description, above subtitle)
—
subtitle
ReactNode (forwarded to PageHeader)
—
actions
ReactNode (forwarded to PageHeader)
—
stats
ReactNode (forwarded to PageHeader — KpiStrip / DashboardCard row)
—
related
PageHeaderRelatedLink[] (forwarded to PageHeader — capped at 3)
—
relatedLink
ComponentType<{href, className?, children}> (forwarded to PageHeader's Link prop)
plain <a>
children
ReactNode (page content)
—
className
string
—
a11y · inherits Breadcrumb (nav[aria-label=Breadcrumb] > ol > li, last item aria-current=page) and PageHeader (title is the page heading) unchanged
rule · composes the EXISTING canonical Breadcrumb + PageHeader only — never re-implements either's markup
rule · breadcrumbs omitted or empty → no Breadcrumb rendered (top-level group-landing pages per §5.2)
rule · the flex flex-col gap-6 p-6 wrapper is the ONE source of the page-content gutter convention — pages adopting this composite drop their own hand-copied wrapper div
gated-surface-notice
draftds/admin/GatedSurfaceNotice.tsx
The one 'this surface is off' register every ALLOW_*-gated admin route renders on a 503/gate-disabled response, instead of each page inventing its own failure copy — or worse, mislabeling a deploy-gate response as a different failure (the content/registration-form/[id] 'FORM NOT FOUND' bug this fixes). Composes InlineState only; kind=empty — a gated surface is neutral, not an error to retry.
LIVE PREVIEW NEEDS APP CONTEXT · SEE ds/admin/GatedSurfaceNotice.tsx
prop
type
default
envVar
string (the ALLOW_* deploy-gate env var, rendered verbatim so the operator knows exactly which flag to flip)
—
surface
string (optional — what's disabled, e.g. "Announcements"; falls back to a generic phrase)
—
className
string
—
a11y · inherits InlineState unchanged
rule · kind is always empty, never error — a gate-off surface is not a failure state
rule · render this INSTEAD OF the page's normal data/error UI when the API response is 503 gate-disabled, not alongside it
rule · never used for genuine 404/500 responses — those keep their own accurate InlineState kicker
dashboard-card
draftds/admin/DashboardCard.tsx
Admin KPI card: mono eyebrow + big mono tabular numeric + delta row; exposure tone routes the value through text-hero (var(--color-hero)) so the one sanctioned Exposure-Red selector swaps it volt→blood in ultra-compact admin for free. isLive adds the cyan StatusDot pulse. Wave 1 item 7 — the KPI provenance contract: `scope` is now REQUIRED so a client-side page-filtered count can never present visually identical to a real server total (`loaded-page` renders a small "PAGE" tag beside the eyebrow).
defaultexposuredefaultkpi-deltakpi-trendkpi-live
DEPOSITS / 24H€312,880+8.2%
WITHDRAWALS€97,1400.0%
EXPOSURE€1.24M+€44k
prop
type
default
eyebrow
ReactNode
—
value
ReactNode (pre-formatted string)
—
scope
"server-aggregate" | "loaded-page" (REQUIRED — verify the true provenance at the call site, never assume)
—
delta
StatTileDelta {value; direction}
—
trend
ReactNode (pass a MiniTrend)
—
freshness
ReactNode (pre-formatted "as-of" string, e.g. "2m ago" — combines with foot on one caption line)
—
tone
"default" | "exposure"
"default"
isLive
boolean
false
href
string — click-through into the surface this KPI summarizes
—
Link
ComponentType<{href, className?, children}> — client-routed nav, defaults to a plain <a>
plain <a>
className
string
—
a11y · numeric is mono/tabular; isLive dot is aria-hidden, meaning carried by the eyebrow
rule · numeric is mono/tabular (admin doubles down on mono, §5)
rule · tone=exposure routes the value through text-hero so the ONE sanctioned Exposure-Red compound selector swaps it volt→blood in ultra-compact admin for free (never a raw accent)
rule · density-padded via var(--density-pad-card); no money math (pre-formatted strings)
rule · scope is required — do not default to server-aggregate without verifying the source query is a true COUNT(*)/aggregate, not a capped/paginated client count
used by · kpi-strip · page-header stats slot
kpi-strip
draftds/admin/KpiStrip.tsx
Horizontal KPI row: density-gap responsive grid composing DashboardCards / StatTiles. Pure layout — rides var(--density-gap) so spacing tightens with density.
GGR€212k+4.0%
NGR€188k+2.7%
MARGIN6.4%−0.2pp
LIVE TABLESPAGE24
prop
type
default
children
ReactNode (DashboardCard/StatTile cells)
—
columns
2 | 3 | 4 (mobile always 2-up)
4
className
string
—
rule · pure layout — rides var(--density-gap) so spacing tightens with density
rule · the cyan / Exposure-Red rules live on the cards, the strip only arranges them
used by · page-header stats slot · admin dashboards
inline-state
draftds/admin/InlineState.tsx
Inline empty/error register: compact, table-frame-preserving (distinct from the page-register ds/patterns/EmptyState giant-404 headline). error tints kicker blood + hosts retry; empty keeps it fog; action by slot.
emptyerror
NO RESULTS
No accounts match the current filters.
COULD NOT LOAD
Upstream timed out (504).
prop
type
default
kind
"empty" | "error"
"empty"
kicker
ReactNode
—
body
ReactNode
—
action
ReactNode
—
className
string
—
a11y · kicker carries the state label as text; action is a slotted control
rule · COMPACT register distinct from the page-register ds/patterns/EmptyState (giant 404 headline)
rule · error tints kicker blood + hosts retry, empty keeps it fog
rule · action by slot (imports no button — stays dependency-light inside tables)
rule · vertical padding from the consumer
used by · data-table (error/empty rows)
admin-spinner
draftds/admin/Spinner.tsx
Token-stroked rotating loading ring (calm admin register, no glow). currentColor drives the stroke — tint via text-*; role=status + sr-only label.
rule · animate-spin at admin pace (≤ house durations)
rule · currentColor drives stroke — tint via text-*
rule · no accent shout, no neon glow
used by · admin loading states
admin-shell
draftds/admin/AdminShell/AdminShell.tsx
Compound operator shell on ds tokens: sticky top bar (brand + section context + Cmd-K trigger + operator identity), collapsible section sidebar with active-rail, command palette (Cmd/Ctrl+K, arrow-key nav over the flattened nav), and mobile chrome (left drawer + bottom quick-nav). Presentational + slot-driven — the app owns nav config, resolved operator identity, and the detail-rail / audit-bar contents.
LIVE PREVIEW NEEDS APP CONTEXT · SEE app/admin (AdminConsoleShell — the whole console chrome)
prop
type
default
sections
AdminNavSection[]
—
activePath
string (app passes usePathname())
—
operator
AdminOperator | null
—
detailRail / detailRailCollapsed / footer
ReactNode / boolean / ReactNode (slots)
—
a11y · mobile-first hard gate — sidebar folds to a drawer + bottom nav under md, every nav target >=44px, no horizontal overflow at 390px; skip-link first in DOM; active route carries aria-current; palette is a labelled aria-modal dialog; drawer closes on Escape/scrim
rule · presentational — no claims/session logic; the app claim-filters sections BEFORE passing them
rule · cyan economy: active-nav rail + Cmd-K key hint + highlighted palette row only
rule · navigation is plain <a href> (dependency-light, matches the console pattern) — no next/navigation coupling
rule · detail-rail + audit-bar are slots the app fills (keeps the legacy rail contract working during migration)
used by · app/admin (AdminConsoleShell — the whole console chrome)
section-card
draftds/admin/SectionCard.tsx
The titled dashboard-section container — a coal panel with a hairline-separated header (eyebrow + title + optional live cyan dot + right action slot) over a body. Turns loose stacked panels into a scannable cockpit. flush hands the body edge-to-edge for a DataTable / divided ActivityRow feed.
RECENT ACTIVITYSettlements
Market 0x91F2 settledoracle · 4 outcomes
12:04
House pool rebalancedrevenue-router
11:58
Withdrawal held for reviewcashier · manual
11:41
prop
type
default
eyebrow
ReactNode
—
title
ReactNode
—
action
ReactNode (link / Badge / AdminButton by slot)
—
isLive
boolean
false
flush
boolean
false
className
string
—
children
ReactNode
—
a11y · <section> landmark; isLive dot is aria-hidden, meaning carried by the title
rule · header composes AdminButton / Badge by SLOT — imports none of them
rule · flush drops body padding so a table / feed seats against the border
used by · seam scaffolds · cockpit landings
activity-row
draftds/admin/ActivityRow.tsx
One dense row of an activity / audit feed — a leading accent StatusDot, a two-line body (event + mono meta), and a trailing mono cell (time or value). The dot is decorative; the meaning rides the text, so the row survives forced-colors and never leans on colour alone. Stack inside a SectionCard flush body with divide-y for the feed rhythm.
KYC approved0x4B7E11 · support
+2m
Bet placedlive · €120
now
Session ended0xC0FFEE
18m
prop
type
default
accent
AccentName
"fog"
pulse
boolean
false
title
ReactNode
—
meta
ReactNode
—
trailing
ReactNode (mono time / value)
—
className
string
—
a11y · leading dot aria-hidden; the event + meta text carry the meaning
rule · meaning rides the text, never colour alone (forced-colors safe)
rule · trailing cell is mono/tabular
rule · stack in a SectionCard flush body with divide-y divide-line/60
used by · cockpit recent-activity feeds
quick-action
draftds/admin/QuickAction.tsx
A navigable quick-action tile — a glyph disc, a label + hint, and a trailing chevron that leans forward on hover (transform-only, the admin motion budget). Renders a real anchor (keyboard-reachable, works without JS). primary is the single volt-emphasised affordance a surface leads with; default the calm siblings. The suggested-next affordance the ScaffoldState seams and cockpit landings compose.
rule · primary is the ONE volt-emphasised affordance; default is calm line
rule · external adds rel=noreferrer
used by · scaffold-state suggested actions · cockpit quick-action grids
scaffold-preview
draftds/admin/ScaffoldPreview.tsx
The dimmed ghost of the interface a scaffolded surface WILL render once its data producer is wired — a skeleton table / KPI cards / activity feed / chart / form. Exists so an operator sees the STRUCTURE that is coming, not a blank void. Every part is aria-hidden (the real state is carried by the ScaffoldState copy layered over it) and fully static / deterministic (no Math.random).
tablecardslistchartform
30D
prop
type
default
kind
"table" | "cards" | "list" | "chart" | "form"
—
a11y · entirely aria-hidden — announces nothing; the ScaffoldState copy carries state
rule · static + deterministic ghost (no Math.random — Domain C fixture contract)
rule · skeleton bars via the Skeleton primitive; card/chart ghosts host a fog MiniTrend
used by · scaffold-state
scaffold-state
draftds/admin/ScaffoldState.tsx
THE designed 'not wired yet' surface — the replacement for the bare bordered void box the honest-seam pages shipped. NEVER an empty frame: a glyph disc, a pulsing cyan status pill, a human headline, a plain-language explanation, an honest technical seam kicker, a suggested next action, AND a dimmed ScaffoldPreview of the interface this surface will grow into. Server-safe (no hooks / no motion): the seam pages render it directly. The ghost sits inset-0 under a radial + bottom scrim (tokenised via var(--color-coal)); the preview is aria-hidden, the copy is what announces.
WIRED · AWAITING DATA
Churn cohorts land when the detector persists
packages/churn-detector runs in-memory only today, with no persisted cohort table, so the API returns a stub rather than real cohort counts.
a11y · the copy (headline/status/body/kicker) is the announced content; the ghost preview is aria-hidden
rule · NEVER a bare void — always a glyph, headline, explanation, next action AND a skeleton preview of the coming UI
rule · server-safe (no hooks / no motion) — honest-seam pages render it directly
rule · scrims tokenised via var(--color-coal); status pill is the ONLY cyan (cyan budget gate)
rule · forwards data-testid so the pages' placeholder-* selectors survive
used by · app/admin honest-seam pages (churn, decision-engine, experiments, observability, cms, lotteries, operators, seo, settings, stats, tournaments)
api-error-state
draftds/admin/ApiErrorState.tsx
Surfaces the backend human-readable message body VERBATIM (never a bare "HTTP 503") and classifies the failure into operator language via kind: permission (names the missing claim, gold), disabled (deploy switch off, calm fog register matching GatedSurfaceNotice), error (a genuine failure, alarming blood register + retry). data-kind carries the classification as a real DOM attribute. Optional collapsed-by-default detail expander (native <details>, no JS state) for the raw payload.
string (the backend raw message body, surfaced verbatim)
—
claim
string (kind="permission" — the missing claim/scope)
—
envVar
string (kind="disabled" — the ALLOW_* deploy-gate env var, rendered verbatim)
—
surface
string (what is affected, e.g. "Bridge operations")
—
onRetry
() => void
—
detail
unknown (raw payload for the collapsed detail expander)
—
className
string
—
data-testid
string
—
a11y · kicker + message are the announced content; the detail expander is a native <details>/<summary> (keyboard + screen-reader native disclosure)
rule · ALWAYS surfaces the backend message body verbatim — never collapses to a bare HTTP status
rule · kind="disabled" never renders the alarming blood accent — a gated surface is not a failure
rule · kind="error" is the only alarming (blood) register and the one that expects onRetry
rule · server-safe (no hooks) — the detail expander is a native <details>, collapsed by default
rule · this is the general contract every page-level data-fetch failure should render through instead of hand-rolling InlineState branching
used by · app/admin bridge hub + stuck + reorg-alerts (gated/error banners) · app/admin /audit-log (error + CORE_DATABASE_URL-not-configured states)
seam-state
draftds/admin/SeamState.tsx
Every zero/blank admin surface declares WHICH kind of nothing it is: verified-empty (real data source, zero rows — says what "none" means + the next step), awaiting-data (feature real, producer not shipped — delegates to ScaffoldState), disabled-by-flag (deploy switch off — delegates to ApiErrorState kind="disabled", VISIBLY DISTINCT from verified-empty), error (delegates to ApiErrorState). data-seam-kind carries the classification as a real DOM attribute. Does NOT replace app/admin SeamScaffold (kept working unchanged) — this is the newer general contract for pages that must tell all four kinds of nothing apart, closing the Wave-0 bug where bridge-reorg's gated-off state rendered identically to "no alerts".
rule · a discriminated union on kind — each branch only accepts that branch's props
rule · disabled-by-flag MUST stay visually + copy distinct from verified-empty (the Wave-0 bridge-reorg identical-empty bug)
rule · does not replace app/admin/SeamScaffold.tsx — that stays working unchanged for existing honest-seam pages
rule · data-seam-kind is a mechanical, real DOM attribute — tests assert on it, not on styled text
used by · app/admin bridge stuck + reorg-alerts (mobile fallback state) · app/admin /kyc queue (verified-empty copy fix)
donut-chart
draftds/admin/DonutChart.tsx
Ring breakdown of flat single-accent arcs (never a two-accent blend on one arc) + a label/value/share legend. Segments carry pre-resolved basis points (Domain A: buildDonutGeometry does only arc-length arithmetic on already-server-computed bps, no client float division of a financial value). Honest empty state — an all-zero/absent segment set renders a dim resting ring + InlineState instead of a fabricated full circle.
populatedemptydefaultempty
LIVE PREVIEW NEEDS APP CONTEXT · SEE app/admin GlassBoxDashboard (revenue-router bucket breakdown)
a11y · the ring SVG is aria-hidden; the legend's visible text (label + share % + value) carries the meaning, never the arc color alone
rule · each arc is ONE flat ACCENT_CLASSES color — never two accents blended on the same arc/segment
rule · valueBps is basis points already resolved server-side (floor division); this component performs no financial math, only arc-length geometry via donut-geometry.ts
rule · empty/zero segments never draw — buildDonutGeometry drops <=0 bps and returns [] when the whole set is zero, which renders the honest InlineState register instead of a full ring
used by · app/admin GlassBoxDashboard (revenue-router bucket breakdown)
bar-chart
draftds/admin/BarChart.tsx
Horizontal bar list — mono label + pre-formatted value row over a track/fill bar, scaled against the largest bar (or an explicit max). Native title attribute carries the value on hover (the visible valueDisplay text is the real a11y channel, the tooltip is a convenience). Honest empty state — an empty or all-zero bar set renders InlineState instead of a fabricated full bar.
populatedemptydefaultempty
LIVE PREVIEW NEEDS APP CONTEXT · SEE app/admin GlassBoxDashboard (markets-by-status posture)
a11y · each row's visible label + valueDisplay text carries the meaning; bar width is decorative reinforcement, never the only signal
rule · value is a plain count/magnitude used ONLY to size the bar — financial figures still arrive as pre-formatted valueDisplay strings (no money math in this component)
rule · an empty bars array or an all-zero value set renders the InlineState empty register, never a fabricated full-width bar
rule · transition is transform/width only, capped at 300ms (admin motion budget)
used by · app/admin GlassBoxDashboard (markets-by-status posture)
admin-profile-block
draftds/admin/AdminShell/AdminProfileBlock.tsx
Operator identity block pinned to the foot of the console sidebar (and composable into the mobile drawer): an Avatar disc (initials/photo), display name + role in the mono register, and a trailing sign-out affordance. Collapsed rail shows the disc alone (title carries the name).
expandedcollapseddefaultno-operator
LIVE PREVIEW NEEDS APP CONTEXT · SEE ds/admin/AdminShell/AdminSidebar (desktop foot block)
prop
type
default
operator
AdminOperator | null
—
collapsed
boolean
false
onSignOut
() => void
—
className
string
—
a11y · the sign-out control carries aria-label + title "Sign out"; the Avatar's initials fallback is role=img + aria-label per the Avatar contract
rule · composes the Avatar primitive — never a bespoke identity disc
rule · display-only: no session/auth logic lives here, the shell supplies the resolved identity + sign-out handler
used by · ds/admin/AdminShell/AdminSidebar (desktop foot block)
admin-icon-action
draftds/admin/AdminShell/AdminIconAction.tsx
A square glyph icon-link for the top-bar action cluster. Anchor-based (a real navigation, never a fake notification bell) — the label is the accessible name (sr-only span + native title). No unread badge; this affordance only ever carries an honest, claim-gated destination the caller resolves.
defaultdefaulthoverfocus-visible
LIVE PREVIEW NEEDS APP CONTEXT · SEE app/admin AdminChrome (settings, audit-log top-bar shortcuts)
rule · NEVER a fabricated unread/notification badge — this is a plain navigation affordance
rule · the composing app claim-gates which actions render (mirrors the nav item's own gate) so an icon never outlives its authorization
used by · app/admin AdminChrome (settings, audit-log top-bar shortcuts)
trend-chart
draftds/admin/TrendChart.tsx
Single-series area+line HERO chart with a minimal Y-axis (3 gridline ticks) and a bounded, evenly-spaced set of X-axis labels (never one per point). The row-spanning chart a dashboard leads with — distinct from MiniTrend (a KPI-card sparkline, no axes). Honest empty state: fewer than two points renders InlineState instead of a fabricated line, matching the BarChart/DonutChart/MiniTrend convention across the admin chart family.
populatedemptydefaultempty
LIVE PREVIEW NEEDS APP CONTEXT · SEE app/admin GlassBoxDashboard (signups-over-time main chart)
a11y · the SVG carries role="img" + aria-label=series.label; x-axis tick labels are aria-hidden decoration since the same values are already legible text, not the sole channel
rule · accent is 'volt' by default; pass 'cyan' ONLY for the one designated hero metric on a page (the admin cyan budget) — never two cyan hero charts on one screen
rule · y-axis ticks are ALWAYS exactly 3 (min/mid/max), x-axis ticks capped at 6 regardless of series length via trend-chart-path.ts (pure, node-tested geometry)
rule · fewer than two points renders the InlineState empty register, never a fabricated flat/interpolated line
rule · formatTick is the caller's formatter — no money math or locale logic lives in this component itself
used by · app/admin GlassBoxDashboard (signups-over-time main chart)
activity-rail
draftds/admin/ActivityRail.tsx
Compact activity feed list — leading mark (StatusDot by default, or a custom leading slot for a Glyph/avatar) · primary text · muted meta line · optional trailing pre-formatted value, one row per item, hairline-divided. The dashboard-sidebar composite (newest signups, pending approvals, latest XP awards) — thin and scannable, no table chrome. Honest empty state matches the admin chart family.
populatedemptydefaultempty
LIVE PREVIEW NEEDS APP CONTEXT · SEE app/admin GlassBoxDashboard (activity rail: newest signups, pending approvals, latest XP)
a11y · the leading StatusDot is aria-hidden; the primary text is the real signal, meta/trailing are supplementary — never color-only meaning
rule · the rail is a thin list, not a table — no column alignment, no headers; each row stays scannable at a glance
rule · an empty items array renders the InlineState empty register, never a fabricated placeholder row
rule · trailing/meta are pre-formatted strings from the caller — no money math or date formatting in this component
used by · app/admin GlassBoxDashboard (activity rail: newest signups, pending approvals, latest XP)
confirm-danger-modal
draftds/admin/ConfirmDangerModal.tsx
The uniform confirm contract for every irreversible/money-mutating admin action: an action-summary sentence, a labeled who/amount/destination restatement (arbitrary rows), and a confirm tier — 'confirm' (a deliberate click) or 'typed' (the operator must type an exact phrase before the confirm button enables). Cancel (ESC, backdrop, header ×, Cancel button) is always the safe default and never fires the action; onConfirm fires ONLY from the confirm button. Owns busy state and surfaces a rejected onConfirm inline without closing.
a11y · the underlying Modal owns ESC/scroll-lock/focus-trap via useOverlay; the error line is role="alert"
rule · onConfirm is wired ONLY to the confirm button — no other interaction (cancel, backdrop, ESC) may call it
rule · tier='typed' throws a dev-time error if typedPhrase is missing or empty — fail-closed on misconfiguration, not a silent no-gate
rule · this component never re-derives or previews the action's own money math — the caller supplies pre-computed `rows` (render amounts via MoneyDisplay)
rule · built on the existing ds/admin/ModalParts (Modal.Header/Body/Footer) — does not fork the overlay
Renders a monetary wire value (bigint/string USDC micro-units, or an integer BPS string) through the house lexical formatter — the ONE place a raw wire integer becomes operator-facing text. unit="usdc" renders grouped truncated dollars ("$1,234.56"); unit="bps" renders a multiplier ("1.5x"). Never prints the bare integer. Pure display (Domain C) — performs no financial math, only string formatting via ds/admin/money-conversion.
a11y · tabular-nums text; carries no color-only meaning
rule · never render a raw micro-unit/BPS integer directly — always through this component or its money-conversion.ts formatters
rule · unparseable input falls back to the raw string rather than throwing (house convention, matches eye-format.ts)
used by · app/admin bonus-codes BonusCodesClient + CampaignCard (replaces local formatUsdDisplay/formatBpsDisplay) · app/admin cashier/adjustments AdjustmentsClient (applied amount + new-balance restatement) · app/admin token-ops RevenueRouterMonitor + BuybackConsole (ConfirmDangerModal restatement rows)
money-input
draftds/admin/MoneyInput.tsx
The operator-facing money input — the ONE place a dollar (mode="usdc") or percent (mode="bps", 1 bps = 0.01%) string is typed. Conversion to the wire format (bigint micro-units / integer BPS as a STRING) happens at this display boundary via pure string math (ds/admin/money-conversion.ts) — never float multiplication, never BigInt in the browser. onChange emits both the raw typed decimal AND the parsed wire value (null while unparseable — fail-closed; caller treats null as not-ready). Preserves an in-progress keystroke (e.g. a trailing ".") across a controlled round trip by only re-seeding its text when the incoming value differs from what it itself last emitted.
usdcbpsdefaultinvalid
LIVE PREVIEW NEEDS APP CONTEXT · SEE app/admin bonus-codes BonusCodesClient create-campaign form (usdMasterAmountLamports, depositRequirementLamports, wageringMultiplierBps, rewardValueBps) · app/admin cashier/adjustments AdjustmentsClient (amount field, allowNegative) · app/admin token-ops BuybackConsole (TWAP total USDC) + RevenueRouterMonitor (fixed costs)
a11y · invalid state sets aria-invalid via the underlying ds/admin/Input; the $/% affordance glyphs are aria-hidden
rule · never computes/emits a value via Number()/parseFloat/BigInt — string slicing/padding only (ds/admin/money-conversion.ts)
rule · rejects (wire: null, invalid styling) more than the mode's max decimals, a disallowed negative sign, or non-numeric junk — fail-closed, never a best-effort guess
rule · the `value` prop is the wire format (micro-units for usdc, integer bps for bps) — a call site whose own API expects decimal dollars (e.g. cashier manual-adjust) converts locally with the exported decimalToMicroUnits/microUnitsToDecimalInput helpers rather than storing wire-format state
used by · app/admin bonus-codes BonusCodesClient create-campaign form (usdMasterAmountLamports, depositRequirementLamports, wageringMultiplierBps, rewardValueBps) · app/admin cashier/adjustments AdjustmentsClient (amount field, allowNegative) · app/admin token-ops BuybackConsole (TWAP total USDC) + RevenueRouterMonitor (fixed costs)