Culture Exchange Standard v3.2

MADE CX Design System

A design language for human decency — with respect for heritage and fiscal responsibility for the lineage of original peoples.

Every score traces to a formula. Every component anchors to a canonical implementation. Sharp edges, functional color, 2px structural borders. Labels and numbers are black or white — never decorative green.

Mission

“A design language for human decency — with respect for heritage and fiscal responsibility for the lineage of original peoples.”

01 · Human Decency

The platform handles transactions tied to people's identities, traditions, and economic futures. Decency means: white-glove modal patterns instead of native browser dialogs; honest confidence ratings instead of fake certainty; pricing bands derived from formulas, not invented per-deal. The UI never pretends to know more than it does, and never less.

02 · Respect for Heritage

Every cultural asset surfaces with documented provenance, chain of custody, and dimensional context. Heritage isn't a brand asset — it's a real lineage of creators, traditions, and communities. The Registry, the dim bars, the verified status indicators all exist to keep that lineage visible on every screen, not buried in a footnote.

03 · Fiscal Responsibility

Every license traceable to its formula. Every payout reproducible from raw signals. Every TCPMV stored in cents and rendered with confidence. 4% Cultural Reinvestment on every transaction returns capital to the communities of origin — documented in the Patterns section, enforced at the ledger layer, surfaced as a public number on every transaction receipt.

04 · Lineage of Original Peoples

The d1 Origin dimension exists for this. The verified-status checkmark exists for this. The Personal Valuation Card with its Ticker identity exists for this. Cultural property has originated somewhere — from real people, in real traditions, with real descendants. The system's job is to make that lineage economically honored, not merely acknowledged.

For LLMs and devs reading this

When you make a design or implementation decision on this platform, run it against the four commitments above. Does this dialog respect the user's intelligence (decency)? Does this card surface origin (heritage)? Does this transaction reproduce from formula (fiscal responsibility)? Does this flow honor the creator's lineage (original peoples)? If you can't answer yes to all four, the choice isn't yet a MADE CX choice — keep working.

What's new in v3.2

A measurable cultural exchange — not a moodboard.

v3.2 extends the executable spec with the live agentic operations surface (§11e), the in-place MADE CX Agent thread (§11g), the engineering pattern for self-contained modals (§12d), and three component primitives — Selection Groups (§05b), Status Pills (§05c), and Form Messages (§05d). v3.0 turned the system from a visual language into a working specification of the platform itself; v3.2 documents the surfaces that ship per-call billing, in-place agent access, and the editorial creator marketplace.

06 · Components
Header Navigation

Two-tier sticky nav, MARKET / REGISTRY / LEDGER tabs with text-color active state, theme persistence under shared 'theme' key.

11 · Templates
Account & Wallet

MADE CX Wallet (live Stripe Connect balance), Personal Valuation Card visual at ISO 7810 ID-1 ratio, funding-speed picker, connect-method tiles.

16 · Foundation
Cultural Valuation Math

CPRS scoring (5-dim mean-of-valid), tier classification (1–5), TCPMV render conventions, confidence thresholds, the dim-bar fingerprint primitive.

Tightened

Labels and numbers are black or white. Never green. Section labels (01 — Foundation), sidebar numbers, technical labels — all neutral. The 40px decorative line accent before each section label keeps its green — it's structural decoration, not a label itself. Green also stays for principle 03's functional uses: arrows, the logo CX stroke, verified checkmarks, chart lines, stat values that are the data.

Added

Sticky left section index. Visible on desktop ≥1024px. Sections grouped by category (Foundation / Components / Templates / Patterns / Email / Code). Active link tracks the section in the viewport via IntersectionObserver. Mobile falls back to the existing top-bar drawer.

Anchored

Every new section names its canonical file. Header Navigation → /culture-market-data.html. Account & Wallet → /account-settings.html. Valuation Math → /culture-market-data.html (cprsScore / cprsTier / dimClass). The DS is no longer a parallel document — it's a reference into running code.

Core Design Principles

Every element reinforces the platform's role as the authoritative ledger for cultural property rights. These five rules are non-negotiable.

Sharp, Not Soft

No rounded corners, no blur effects. All edges are crisp and defined. border-radius: 0 everywhere.

Green is Functional

The only permitted green stroke in the system is the CX logo outline — wherever the brand mark appears. Beyond that, green is reserved for decorative accents (the line bar before section labels, the +/− toggle in the drawer) and activations (button arrows, live status dots, success state borders, chart data, directional change indicators). No liberal usage. No green borders, no green fills on UI chrome, no green stat values.

Text Over Icons

Clear labels, not emoji decorations. Every element is labeled with text. No emoji in production UI.

Contrast Over Gradients

Pure black/white with strategic accent color. No gradients anywhere. Solid colors only.

Structure Over Decoration

2px borders define space, not shadows. Consistent border treatment. No drop shadows.

Heritage in Every Interaction

A design language for human decency — with respect for heritage and fiscal responsibility for the lineage of original peoples. Heritage is preserved whether the interaction is superficial — a hover state, a tooltip, a 200ms transition — or profound: a license acquisition, a royalty distribution, a transfer of cultural property. Every animation, every microinteraction, every state change carries the weight of the lineages the platform protects: not abstract "culture," but the specific peoples and traditions from which it descends. Nothing is incidental. Nothing is decorative-for-decoration's-sake. The system is custodianship made visible.

Anti-Patterns
Don'tDo Instead
Use rounded corners (border-radius)Keep all edges sharp (0px radius)
Fill backgrounds with greenUse green only for arrows and accents
Add drop shadowsUse 2px borders consistently
Use emoji in production UIUse text labels or simple SVG icons
Use gradients anywhereUse solid colors only
Use 1px bordersUse 2px borders for all structural elements
Add blur or glassmorphism effectsKeep contrast high and crisp

Brand & Logo

The MADE CX wordmark: solid "MADE" represents established culture, outlined "CX" in green represents exchange and future potential.

Primary Logo
MADECX
MADECX
Logo Construction
PropertyValueCSS
FontInter, 900font-weight: 900
"MADE" Color (Light)#000000color: var(--text)
"MADE" Color (Dark)#FFFFFFcolor: var(--text)
"CX" FillTransparentcolor: transparent
"CX" Stroke#00C805, 2.5px-webkit-text-stroke: 2.5px var(--green)
Letter Spacing-0.01emletter-spacing: -0.01em
Gap8pxmargin-left: 8px
Downloadable Logo Files
MADECX
SVG — Light
MADECX
SVG — Dark
MADECX
PNG — Light (2x)
MADECX
PNG — Dark (2x)
HTML + CSS
.logo-text {
    display: flex;
    align-items: baseline;
    font-family: var(--font-primary); /* Inter */
    font-size: 22px;
    font-weight: 900;
    letter-spacing: -0.01em;
}
.logo-made { color: var(--text); }
.logo-cx {
    color: transparent;
    -webkit-text-stroke: 2.5px var(--green);
    margin-left: 8px;
}

Iconography & Glyphs

All pictographic icons across MADE CX surfaces must be inline SVG. Emojis and Unicode-icon characters are not used anywhere on the platform — not in UI strings, not in toast messages, not in console output, not in placeholder labels.

Rule 1 — Inline SVG Only

Every visual icon on the platform is rendered as an inline <svg> element with stroke="currentColor" so it inherits its container's color. No icon fonts, no image sprites for UI icons, no Unicode pictographs.

ForbiddenUse Instead
Emojis (🚀 ⚡ ✨ ✅ 🎉 ❤️ 🔐 etc.)Inline SVG with currentColor stroke
Unicode check & cross (✓ ✗ ✔ ✘)SVG <polyline points="20 6 9 17 4 12"/> or <path d="M18 6L6 18M6 6l12 12"/>
Unicode arrows (→ ← ↑ ↓ ↕)SVG arrow / chevron icons
Unicode stars / hearts / sparkles (★ ❤ ✨)SVG equivalent or remove entirely
CSS content: '✓' pseudo-elementsJS-injected SVG into the container
Emoji prefixes in console.log & debug outputBracketed prefix: [init] / [auth] / [error]
Rule 2 — Acceptable Typography

Standard typographic characters are not pictographic icons and are encouraged where they improve readability:

CharacterNameUse For
Em-dashDate ranges (Submitted Jan 1 — Approved Jan 3), eyebrow separators, parenthetical phrases
En-dashNumeric ranges (8–12 minutes, $50K–$100K)
·Middle dotInline separators (Tier 2 · Verified Acquirer)
Horizontal ellipsisContinuation, truncation, loading state
“ ” ‘ ’Smart quotesQuotations, emphasis (never use straight quotes for prose)
Rule 3 — Green Accent Discipline

Green (var(--green) / #00C805) is the platform's only accent color. It signals exchange, verification, and forward motion. To preserve its meaning, green is reserved for very small accents only — never as a fill, never as a state.

✓ Allowed (small accents only)✗ Forbidden (would dilute the accent)
CX glyph stroke (the brand mark itself) Pill backgrounds & borders
Verified-pill checkmarks (≤14px SVG inside the pill) Button fills
Status dots (6–8px, outline only) Progress bars & stepper indicators
Accent dashes (2px tall, ≤22px wide) Sort arrows & hover states
Toast.success border-left strip (4px) Focus rings & ::selection backgrounds
Stepper completed checkmarks (14px SVG) Heavy filled icons or shapes
SVG Icon Pattern
<!-- Standard checkmark icon -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
    <polyline points="20 6 9 17 4 12"/>
</svg>

<!-- Standard arrow icon -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
    <path d="M5 12h14M12 5l7 7-7 7"/>
</svg>

<!-- Sort chevron (replaces ↕) -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
    <path d="M7 10l5-5 5 5M7 14l5 5 5-5"/>
</svg>

Color System

Pure black and white with strategic green accent. Green is functional, never decorative. Red for negative/error states only.

Primary Palette
Black
#000000
Primary text, borders, backgrounds
White
#FFFFFF
Backgrounds, text on dark
Green (Accent)
#00C805
Arrows, logo, success, charts
Red (Error)
#EF4444
Negative charts, errors, declines
Gray Scale
50
100
200
300
400
500
600
700
800
900
Green Usage Rules
Correct UsageIncorrect Usage
Logo "CX" outline strokeButton fills or backgrounds
Arrow symbols in buttonsBadge or card backgrounds
Success/verified checkmarksText highlights or section fills
Chart lines (positive growth)Icon fills
Price change indicators with directional sign (+2.5%, −1.4%)Navigation elements
Decorative line accent before section labels (40×2 bar)Statistics, large numerical figures, hero stats
Dimensional bar fills above 60% (per Section 16)Sidebar section numbers or active-link borders
Theme Variables
TokenLightDarkUsage
--bg#FFFFFF#000000Page background
--bg-alt#F5F5F5#171717Section/card backgrounds
--bg-tertiary#FAFAFA#262626Nested backgrounds
--text#000000#FFFFFFPrimary text, buttons
--text-secondary#525252#A3A3A3Body text
--text-muted#737373#525252Captions, labels
--border#E5E5E5#262626All borders (always 2px)

Typography

Three font families with strict roles: Space Grotesk for display/headlines, Inter for body/UI, IBM Plex Mono for technical/labels.

Font Families
Space Grotesk
ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789
var(--font-display) — Headlines, Titles, Display, Card Titles
Aa 400
Aa 500
Aa 600
Aa 700
Aa 800
IBM Plex Mono
ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789
var(--font-mono) — Labels, Code, Technical, Badges, Section Labels
Aa 400
Aa 700
Aa 800
Type Scale
Hero Title
The Manifesto
Space Groteskclamp(40px, 6vw, 72px)900-0.03em
Section Title
Why MADE CX Exists
Space Groteskclamp(32px, 5vw, 48px)900-0.02em
Card Title
When I Get Home
Space Grotesk20px900
Section Label
Community Impact
IBM Plex Mono11px8000.15em
Body Text
Black creativity has always built global markets — yet no system has ever existed to protect its origin.
Inter16px400-500line-height: 1.6
Button Text
Start Registration →
Inter14px8000.1em uppercase

Buttons

Buttons use var(--text) as background (black in light, white in dark). Green is ONLY for the arrow icon accent. All borders 2px. Font-weight 800.

Button Variants
Button Sizes
CTA Button (Header)
Button Specs
PropertyValue
Border width2px (all variants)
Font weight800
Text transformuppercase
Letter spacing0.1em
Arrow icon colorvar(--green) — only green allowed on buttons (activation indicator)
Arrow hovertranslateX(4px) with 200ms transition — the button leans toward action
Primary bgvar(--text) — black/white per theme
Interactions & States — Canonical Microinteractions

Every button has five states. Default, hover, active, focus, disabled. Each state is structurally distinct so a user always knows what's possible. The animations are deliberate, not decorative — a button leaning forward on hover (4px arrow translate) signals "I'm ready"; the 1px inset on active signals "I received your input"; the focus ring signals "you're here, keyboard works." This is custodial UI: the system never leaves the user guessing what state they're in.

Try these — hover, click, tab in
Primary
Ghost
Disabled
State Specification Table
StateVisual changeTimingWhy
DefaultBase treatment per variant. Arrow at translateX(0)Resting state — discoverable, not loud
HoverPrimary: bg inverts (text→bg, bg→text). Ghost: border-color → var(--text). Arrow translates +4px on the X-axis200ms cubic-bezier(.4,0,.2,1)The button leans toward action. Arrow movement signals forward motion before the click
Focus (keyboard)2px outline at outline-offset: 3px, color var(--text). No outline replacement — the offset gap reads as a "focus halo"0ms (instant)Keyboard users need unambiguous "you are here" feedback. Sharp halo, no glow
Active (pressing)transform: translateY(1px). Arrow snaps to translateX(2px) (mid-transition)0ms (instant)Tactile acknowledgment. The button visibly receives the input
Disabledopacity: 0.4, cursor: not-allowed, all hover/active transitions suppressedReads as "unavailable" without the heavy hand of grayed-out fills. Pointer change reinforces it
Loading (async)Text replaced by ellipsis or spinner-dot row. Click suppressed. Width preserved (no layout shift)For acquire / submit / pay actions where async completion matters. Width-preserving so the page doesn't reflow during latency
Canonical CSS — Transitions & States
Button base + states
.btn {
  /* Base treatment */
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 12px 24px;
  font-family: var(--font-primary);
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  border: 2px solid var(--text);
  background: transparent;
  color: var(--text);
  cursor: pointer;
  /* Heritage Principle: every transition carries weight */
  transition: background 200ms cubic-bezier(.4,0,.2,1),
              color 200ms cubic-bezier(.4,0,.2,1),
              border-color 200ms cubic-bezier(.4,0,.2,1),
              transform 100ms ease-out;
}

.btn svg {
  width: 16px;
  height: 16px;
  color: var(--green);            /* arrow is the activation indicator */
  transition: transform 200ms cubic-bezier(.4,0,.2,1);
}

.btn-primary { background: var(--text); color: var(--bg); }

/* Hover — the button leans forward */
.btn:hover:not(:disabled) {
  background: var(--text);
  color: var(--bg);
}
.btn-primary:hover:not(:disabled) {
  background: transparent;
  color: var(--text);
}
.btn:hover:not(:disabled) svg { transform: translateX(4px); }

/* Focus — keyboard halo, no glow */
.btn:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 3px;
}

/* Active — tactile press */
.btn:active:not(:disabled) {
  transform: translateY(1px);
}
.btn:active:not(:disabled) svg { transform: translateX(2px); }

/* Disabled — unavailable, not absent */
.btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.btn:disabled svg { color: var(--text-muted); }
Why Microinteractions Matter Here

This is a platform that handles cultural property. A click that says "Acquire License" might trigger a five-figure transaction and create a permanent on-chain record. The button needs to feel commensurate with that — confident, intentional, never accidental. The 200ms transition is slow enough that a user sees what's happening; the 4px arrow nudge is visible enough that the button reads as "ready"; the 1px press translate confirms receipt without bouncing or oversteering.

For LLMs and future devs reading this: when you build a new button variant, copy these timings exactly. The transition curve cubic-bezier(.4,0,.2,1) is Material's "standard easing" — it accelerates fast and decelerates gently, which reads as "decisive but not rushed." Do not substitute ease-in-out (too symmetrical, too "designed-feeling") or linear (mechanical). The 4px arrow translate is the canonical activation distance — not 8px (too theatrical), not 2px (too subtle to register).

Selection Groups

Radio-like option pickers where the user chooses exactly one value from 2–5 mutually exclusive options. Used for the Funding Speed picker on the Wallet, the Withdraw modal speed selector, and the Auto Top-Up “Max charges per billing period” control. The selected option uses inversion — never a green tint — per DS-canon.

Live example — Max charges per billing period
Live example — Funding Speed (2-option layout)
Standard
1–3 days · Free
Instant
~30 min · +1%
Specs
StateBackgroundBorderText color
Default (unselected)var(--bg)2px solid var(--border)var(--text)
Hovervar(--bg)2px solid var(--border-hover)var(--text)
Selected (active)var(--text)2px solid var(--text)var(--bg)
Disabledvar(--bg-alt)2px solid var(--border)var(--text-muted)
Usage rules
  • 2–5 options per group. More than 5 → use a dropdown.
  • Always sharp corners (border-radius:0).
  • Equal column widths via CSS Grid (grid-template-columns:repeat(N,1fr)).
  • Single source of truth via aria-pressed on the active button.
  • Never tint the active state green — that violates DS-canon.

Status Pills & Dots

Small accent components that communicate state without dominating the layout. This is where MADE-green legitimately appears: in 6–8px dots, in 12–16px icons inside pills, and in mono uppercase status text. These are the canonical “green as punctuation” surfaces.

Status Dot — Live indicator
Live
Status Pill — Active subscription
Active Connected Auto-payouts On
Status Pill — Warning & Error variants
Pending Failed Inactive
Badge Count — Notification accent

A tiny circular badge for unread counts on header icons (messages, notifications). Green background, white number. Hidden when count is 0.

3
Specs
ElementSpec
Status dot (live)8×8 px, border-radius:50%, optional animation:pulse 2s ease-in-out infinite
Status dot (in pill)6×6 px, border-radius:50%, color matches pill semantic
Pill container1px solid var(--text) (default) or semantic color, padding 4px 10px, sharp corners
Pill textMono 9px / 800 / 0.12em uppercase
Badge countmin-width 16px, height 16px, border-radius:50%, mono 9px / 800, green background
Anti-patterns
  • Don't make pills larger than ~24px tall — they become buttons.
  • Don't fill the pill background with green; the dot/icon does that job.
  • Don't use red/amber pills for general status — reserve for warning/error.

Form Messages

Inline alert banners under (or alongside) a form field. Pattern: full-width box with a 4px left bar in the semantic color. Default state has a neutral left bar — never green by default. Success states are the exception where the bar is green, because the entire bar is communicating state.

Info / Default
Note Your subscription auto-renews monthly. Cancel anytime — access continues through the current billing period.
Success
Saved Your auto top-up settings have been updated.
Warning
Low Balance You have 8 agentic operations remaining this month. Auto top-up is off — consider enabling it from your account settings.
Error
Card declined Your payment method couldn't be charged. Update your card in account settings to resume auto top-up.
Specs
ElementSpec
ContainerPadding 12px 14px 12px 18px, border:2px solid var(--border), sharp corners
Backgroundvar(--bg-alt) — faint contrast against page
Left bar (info / default)border-left:4px solid var(--text)
Left bar (success)border-left:4px solid var(--success)
Left bar (warning)border-left:4px solid var(--warning)
Left bar (error)border-left:4px solid var(--red)
Title (optional)Mono 11px / 800 / 0.14em uppercase, color matches semantic
Body textInter 13px / 1.5 / var(--text)
Rules
  • No green left bar by default. Reserve green for the explicit .success variant.
  • Keep messages under 2 sentences. Long content belongs in a modal.
  • One form message per field. Stacking multiple is a sign the form needs restructuring.
  • For transient messages (saved confirmations, etc.), auto-dismiss after ~4 seconds.

Header Navigation

Two-tier sticky header used across every authenticated and public page. Tier 1 holds the logo, the primary tab cluster (MARKET / REGISTRY / LEDGER), and the tools cluster (search, sort, view, theme, live, volume). Tier 2 holds page-specific tools (filters, view-toggles, sort buttons) when a page needs them. Theme persists across navigation via shared localStorage.

Canonical Reference
/culture-market-data.html

Master implementation of the two-tier nav. The same structure is mirrored on /brand-dashboard.html, /ip-asset-detail.html, and /ledger.html. Class names (.nav, .nav-top, .main-nav, .main-nav-tab, .btn-group, .btn-icon) are stable. Changes happen there first, then propagate.

Live Preview — Tier 1
MADECX
30D VOLUME
$1.2M
LIVE
Tier 1 Element Anatomy

The nav decomposes into four canonical regions: logo (flush-left), main-nav (primary tabs at margin-left: 24px), tools cluster (right side, margin-left: auto), and the optional Tier 2 page-tools strip (separated by a 1px hairline). Each element below is rendered live — what you see is the spec.

A · Container .nav sticky · z-900 · backdrop-blur(12px) · 2px border-bottom
flex · justify-between · padding 12 / 20
MADECX
[ tools cluster ]
B · Logo .logo Inter 900 · 22px · -0.01em
MADECX
Brand mark composition

"MADE" — solid var(--text). The wordmark anchor.

"CX"color: transparent + -webkit-text-stroke: 2.5px var(--green). The exchange's signature outline. This is the only canonical green stroke in the system.

Gap between MADE and CX: margin-left: 8px on the CX span. Don't use word-spacing — the gap is structural, not typographic.

C · Main Nav Tab .main-nav-tab Mono 11 / 700 / 0.1em UC · padding 6 / 16
Default
MARKET
color: var(--text-muted)
Hover
MARKET
color: var(--text-secondary)
Active
MARKET
color: var(--text) · border-bottom: 2px var(--text)
Active state pin: the underline is text-color, never green. Green underlines previously read as "selected = approved" — a redundant signal that bled into the activation reservation. The text-color underline reads as structural state.
Tools Cluster Elements

Right-aligned at margin-left: auto. Order matters — left-to-right reads as data → query → control → state. Each tool is rendered live below at production scale.

30D Volume Display
30D Volume
$1.2M
Mono 8 label · Space Grotesk 13 / 700 / green value
Search Wrap
180px → 220px on focus · clear-X right when query present
Button Group .btn-group
Active inverts to var(--text) bg · shared 1px borders
Theme Toggle .btn-icon
Sun (light) ↔ Moon (dark) · 1px var(--border)
Live Status
LIVE
7×7 dot · box-shadow 4px green @ 0.4 · pulses on data refresh
Tier 2 — Page Tools Strip

Optional second row when a page has its own filter / sort / view tools. Used by brand-dashboard (grid/list/map view-toggle), ledger (positions/orders/payment tabs), ip-asset-detail (tier-picker chrome). Separated from Tier 1 by a 1px (not 2px) border so it reads as continuous chrome, not a second nav.

[ Tier 1 — logo · main-nav · tools cluster ]
View
Filter 247 assets
flex · padding 8 / 20 · 1px border-top · gap 14 · flex-wrap with row-gap 8
Auth State Toggle

The right cluster swaps between two states depending on session. Anonymous shows SIGN IN + SIGN UP; authenticated shows the user-menu (avatar + name + role). Toggle is driven by applyBuyerContextToHeader(ctx) after resolveBuyerContext() resolves the session. Sign-in URLs preserve ?return=<deep-link> so anon visitors who click an action land back on the same page after auth.

Anonymous ctx.state === 'anonymous'
Authed · Verified Buyer ctx.state === 'authed_brand_verified'
A
Angela Davis Verified Buyer
Context stateHeader renderRole label in user-menu
anonymousSIGN IN + SIGN UP visible
authed_brand_verifiedUser-menu visibleVERIFIED BUYER
authed_brand_unverifiedUser-menu visibleUNVERIFIED
authed_brand_pendingUser-menu visibleONBOARDING
authed_creatorUser-menu visibleCREATOR
Theme Persistence

Theme state is stored under a shared key 'theme' in localStorage, not per-page. This is the contract that makes toggle persistence work across navigation — flipping to dark on Market keeps it dark on Registry, Ledger, Detail. Any new page must use the same key. A previous bug stored ledger's theme under 'made-theme'; that's deprecated and must not return.

To prevent a flash of light theme on dark-mode reload, an early synchronous script in <head> reads the persisted value and applies data-theme="dark" on <html> before the CSS renders. This script runs before <link rel="stylesheet"> resolves, so the first paint is already in the correct theme.

HTML — Required head script (insert before any <link rel="stylesheet">)
<script>
  // Apply persisted theme before CSS renders (prevents flash on reload).
  // Shared key 'theme' across all MADE CX pages so toggle persists on nav.
  (function () {
    try {
      var t = localStorage.getItem('theme') || 'light';
      if (t === 'dark') document.documentElement.setAttribute('data-theme', 'dark');
    } catch (e) { /* localStorage blocked — fall back to light */ }
  })();
</script>
JavaScript — Toggle handler (writes to shared key)
function toggleTheme() {
  var html = document.documentElement;
  var current = html.getAttribute('data-theme');
  var next = current === 'dark' ? 'light' : 'dark';
  html.setAttribute('data-theme', next === 'dark' ? 'dark' : '');
  try { localStorage.setItem('theme', next); } catch {}
  applyThemeIcon();
}
Search Preview (Dropdown, Not Stage Filter)

Search reveals matches without redacting context. The canonical pattern is a dropdown panel positioned beneath the search input, populated as the user types. Selecting a result navigates to it. The main stage — the visible content the user was reading or browsing — stays unchanged the entire time. This is the opposite of "filter as you type", where keystrokes immediately collapse what the user was looking at into a narrower view.

Why dropdown over filter: filter-as-you-type breaks scanning and back-tracking. A user reading section 12 who searches for "valuation" loses 12 from view as the page collapses to a single result. The dropdown pattern preserves their position — they can scan suggestions, decide whether to navigate, or dismiss the dropdown and keep reading. This DS uses the dropdown pattern in its own header — try typing in the search field at the top right.

ElementSpec
Search wrapPosition relative, mono input with 14×14 magnifier glyph at left: 8px, clear-button at right: 6px (visible only when query present)
Input fieldMono 11px / 0.04em, padding 6px 24px 6px 28px, 1px var(--border), focus border-color → var(--text-secondary), focus expands width 200 → 280px
Dropdown panelAbsolute, top: calc(100% + 6px), anchored right, min-width 340px, max-width 420px, max-height 60vh with scroll, 2px var(--border), z-index 1001 (above sticky header at 1000)
Dropdown header stripMono 9px / 800 / 0.16em uppercase, color var(--text-muted), background var(--bg-alt), padding 8px 14px, 1px border-bottom — shows result count
Result rowPadding 10px 14px, 1px border-bottom (none on last), hover/highlighted background var(--bg-alt), cursor pointer
Result thumb36×36 square, 1px var(--border), var(--bg-alt) fill, mono 10px / 800 — typically the section number, or an icon/glyph for visual content
Result titleSpace Grotesk 14px / 800, line-height 1.2, ellipsis overflow
Result metaMono 9px / 700 / 0.12em uppercase, color var(--text-muted) — category or context
Empty stateMono 11px, var(--text-muted), centered, padding 20px 14px
Behavior Contract
ActionResult
Type into inputDropdown opens, populated with matches against title + category + section number + id. Main stage unchanged.
Empty inputDropdown closes. Stage still unchanged.
↓ Arrow DownHighlights next result (wraps at end)
↑ Arrow UpHighlights previous result (wraps at start)
EnterNavigates to highlighted result (or first if none highlighted). Smooth-scroll to anchor, hash updates, dropdown closes, input clears
Click a resultSame as Enter on that row
EscCloses dropdown, clears input, blurs field. Stage still where it was.
Click outside the wrapCloses dropdown. Input retains its value if user wants to come back.
Where to Use This Pattern

The search-preview-dropdown is the canonical search pattern across the platform. It applies anywhere a user might want to find something specific without losing their place:

  • Registry asset search — type "Drake" → dropdown shows matching assets with thumbnails + creator names. Clicking navigates to detail page. The grid behind stays as it was.
  • Ticker lookup — type "$ANG" → dropdown shows matching creators with ticker badges. Stage stays put.
  • Ledger transaction history — type a registration ID → dropdown shows matching txns. The current ledger view doesn't collapse.
  • Admin search — type a user email → dropdown shows matching profiles. Admin's current view stays visible.

The deprecated pattern (do NOT use): typing in a search field and having the underlying grid/list immediately collapse to only matching items. That is filter-as-you-type — appropriate for refining an active query view (e.g., a dedicated search results page where filtering IS the purpose), but never for the global header search where the user is mid-task on a different stage.

Mobile Responsive

At ≤900px the nav-top padding shrinks to 10px 14px and flex-wrap: wrap lets the main-nav row drop below the logo when there's not enough horizontal room. Below 768px, the canonical pattern is to hide the inline desktop-controls + main-nav and surface a hamburger button that opens a left drawer (mirroring the .mobile-drawer from /culture-market-data.html).

BreakpointBehavior
> 900pxFull Tier 1 + Tier 2, all controls inline
≤ 900pxPadding shrinks, main-nav wraps to row 2 if needed
≤ 768pxHamburger button appears; main-nav + desktop-controls hidden; drawer becomes primary nav
Drawer Navigation (Hamburger Expanded)

The drawer is what the hamburger reveals. Below ~768px the inline tab cluster gives way to a hamburger button; tapping it slides in a left-anchored drawer that takes over navigation. The drawer follows the same philosophy as the desktop nav: structural borders, mono uppercase group labels, no decorative chrome — but trades inline horizontal compression for vertical scannability with collapsible accordion groups.

Why grouped accordion over flat list: a flat list of 17 nav links scrolls forever and gives no shape to the platform's information architecture. Grouping by category (DISCOVER / FOR CREATORS / FOR BRANDS / RESOURCES on the marketing site, FOUNDATION / COMPONENTS / TEMPLATES / CODE in the DS) lets a user jump directly to the right neighborhood, then expand only the group they need. Other groups stay collapsed so the drawer remains scannable at a glance.

Anatomy
RegionSpec
ContainerFixed left-anchored, width 320px (max 85vw on narrow phones), full viewport height, border-right: 2px solid var(--border), z-index: 1200 (above page, above sticky header). Slide-in via transform: translateX(-100%) → 0, transition 0.3s cubic-bezier(.4,0,.2,1)
OverlayFull-viewport black at 0.5 opacity, z-index: 1100. Tap anywhere outside drawer → close. Fade-in/out matches drawer slide
HeaderLogo flush-left at 18px (smaller than nav 22px), close button (X) flush-right. Padding 20px 24px. border-bottom: 2px solid var(--border)
Close button40×40 box, 2px var(--border), transparent bg. Hover: bg → var(--text), color → var(--bg), border → var(--text) (full inverse)
Group summary rowPadding 16px 24px, mono 11px / 700 / 0.16em uppercase label in var(--text-muted); turns var(--text) when group is open. +/− toggle floats right
+/− toggle indicator14×14 cross drawn with two CSS pseudos in var(--green). Open state hides the vertical bar via transform: scaleY(0) leaving just the minus. Permitted green — decorative accent, not a label or number
Group itemsInter 15px / 700 / -0.005em (sentence case). Padding 14px 24px. border-top: 1px solid var(--border) between items. Hover bumps padding-left → 32px (4px nudge) and bg → var(--bg-alt)
Group separatorborder-bottom: 1px solid var(--border) on each .drawer-group — a single hairline divides one collapsed group from the next
FooterMono 10px / 700 / 0.1em uppercase muted, centered, padding 24px. border-top: 2px solid var(--border). Optional CTA button above (Start Registration, Sign In, etc.) — full-width primary or inverted depending on context
Color Rules (Drawer-Specific)
ElementColor
Group label (mono, "FOUNDATION", etc.)var(--text-muted) when collapsed → var(--text) when open
Item link textvar(--text) always
+/− togglevar(--green) (decorative accent — permitted)
Hairline dividersvar(--border)
Hover backgroundvar(--bg-alt)
Container backgroundvar(--bg) — same as page
Behavior Contract
ActionResult
Tap hamburgerDrawer slides in from left; overlay fades in; body scroll locks
Tap close (X)Drawer slides out; overlay fades out; body scroll restored
Tap outside drawer (overlay)Same as close
Tap group summaryGroup accordion toggles open/closed. Other groups stay as they are (multi-open allowed)
Tap an itemNavigates to target, drawer auto-closes
First group default stateOpen. Subsequent groups collapsed. Sets a clear "where you are" anchor on first reveal
Esc keyCloses drawer (when focus is anywhere within)
Hamburger Icon Philosophy

The hamburger button is a state primitive, not a logo or accent. Three 2px horizontal lines, evenly spaced, in a 40×40 square with 2px var(--border). Hover bumps border to var(--text). No animation transformation into an X — the X is a separate icon inside the drawer header, not the hamburger morphing. Treat them as two distinct UI states (closed = hamburger visible, open = drawer with X visible) rather than a continuous icon transformation. This matches the structural-over-decorative principle: borders define state, not motion.

Common Anti-Patterns
Don'tWhy
Active tab in var(--green)Violates principle 03 — green is functional, not decorative. Active tab is structural state, not a success indicator. Use var(--text)
Center main-nav with grid 1fr auto 1frForces fake symmetry that breaks at narrow widths. The canonical is flex space-between with margin-left: 24px on .main-nav
Per-page localStorage keys ('made-theme', 'site-theme', etc.)Breaks toggle persistence across navigation. Single shared key 'theme' only
Inline style="display: none" or "display: flex" on the nav containerInline styles override the responsive @media rules. Let CSS classes drive display state
Hiding .main-nav at <1200px without a hamburger replacementCreates "ghost nav" — no way to navigate on tablets. Either keep it visible (with flex-wrap) or provide drawer fallback
Filter-as-you-type — header search collapses the underlying stageBreaks scanning and back-tracking. User reading section 12 who types "valuation" loses 12 from view. Use the search-preview-dropdown pattern instead: dropdown reveals matches, stage stays put until user explicitly navigates
Flat scrolling drawer with 15+ links and no groupingLoses the platform's information architecture. Drawer becomes a soup. Use the accordion-grouped pattern: 4–6 category groups, first one expanded by default, rest collapsed
Shield iconography to denote "verified" or "secure"Off-brand. The shield carries military/insurance connotations that don't fit MADE CX. Use the green CX outline mark — that's the canonical brand-as-trust primitive
Statistics rendered in var(--green)Numbers are labels in disguise — they identify quantities, not communicate +/− direction. Render in var(--text). Green is reserved for directional change indicators (+2.5%, −1.4%) where the sign carries meaning

Image Standards

Professional photography with clear subjects. No text overlays. High contrast for white backgrounds.

Image Dimensions
4:3
Grid View Cards
400 x 300px
1:1
List Thumbnails
80 x 80px
1:1
Detail Hero
600 x 600px
Requirements
PropertyValue
Max file size2MB
Object fitobject-fit: cover
Object positionobject-position: center
Border on imagesnone
Resolution2x for retina
High Contrast

Images must work on white backgrounds

Clear Subject

Main subject clearly identifiable

No Text Overlays

Text separate from image layer

Professional Quality

No phone snapshots, proper lighting

Product Cards

Grid view cards with 4:3 image ratio. Category badge top-left, asset icon bottom-right. 2px border, hover to black.

Product
CX
Product

When I Get Home

Solange Knowles

Uncategorized
Service
CX
Service

Beat Production Pack

ATL Sound Labs

Music
Card Specs
ElementStyle
Card border2px solid var(--border)
Hoverborder-color: var(--black)
Image ratioaspect-ratio: 4/3
Category badgeTop-left 16px, 2px solid #000, transparent bg
Asset stampBottom-right 16px, 36×36 SVG floating on alpha — no background box, no border. Renders the canonical CX outline (stroke: var(--green), 1.5px) at 0.85 opacity so it reads as a brand watermark, not a UI label. Same treatment as the detail-page .detail-logo-overlay. Replaces previous registration-type-specific iconography (shield for Product, lightning for Service) and the previous black-stamp container, both of which read as decorative chrome rather than brand provenance
Asset typeGreen #00C805 (only green on card)
Content padding20px

List View & Financial Accordion

Table-style layout with expandable financial charts. Click chevron to reveal price history, projections, and stats.

NameCategoryChartPriceValuation
Product

When I Get Home

Solange Knowles

When I Get Home
Solange Knowles
Uncategorized
$1,000
+2.5%
$10K
Market Cap
$1,000 $1,200$1,000$800$400 1Y6MTODAY+3M
View Full Details
Price
$1,000
Projected
$1,150 (+15%)
Mkt Cap
$10,000
Service

Beat Production Pack

ATL Sound Labs

Beat Production Pack
ATL Sound Labs
Music
$8,750
-1.4%
$450K
Market Cap
$8,750 $12K$9K$6K 1YTODAY+3M
View Full Details
Price
$8,750
Projected
$8,200 (-6.3%)
Mkt Cap
$450K
Chart Specs
ElementStyle
Chart height180px, border: 2px solid var(--border)
SVG viewBox0 0 700 156
Price line (positive)stroke: #00C805, width: 2.5
Price line (negative)stroke: #EF4444
Projection linestroke: #737373, dasharray: 4,3
Volume barsopacity: 0.3, green/red fill
TODAY labelGreen for positive, red for negative

Asset Detail Surfaces

Every cultural asset on the platform has two distinct detail surfaces — the Product Detail (public listing, exhibition tone) and the IP Asset Detail (trading view, market structure). They render the same underlying row from cultural_assets, but for fundamentally different audiences with fundamentally different intents — and behind different auth gates. Product Detail is public, viewable by anyone in non-auth mode (same posture as the Registry index). IP Asset Detail is verified-buyers-only, gated at page load. Same asset. Two lenses. Two flows. Two auth boundaries.

Canonical References
Surface A · Public
/product-detail.html
Listing-page surface. Tile composition + museum-exhibition masonry. Two CTAs: Support + License.
Surface B · Trading
/ip-asset-detail.html
Trading-desk surface. Two-column grid + three-market structure + financial instruments + license tier picker.
The Two Surfaces, Side by Side
DimensionProduct Detail (Public)IP Asset Detail (Trading)
AudienceCasual visitors, supporters, museum-goers, anyone discovering cultureVerified buyers, institutional licensees, secondary-market traders
Primary intentEncounter / appreciate / consider supportingEvaluate / price / transact
CompositionTile-based, asymmetric, with masonry "Museum Exhibition" sectionTwo-column grid: 1.5fr main / 1fr sticky side
ToneExhibition floor — dignified, slow-read, generous whitespaceTrading desk — dense, scannable, sticky controls
CTAsSupport This Creator (donation, heart icon) + License This Property (document icon)License Tier picker (Primary) · Buy IP Outright · Trade Certificates · Options · Loans
Modal flowDonation Modal — 3 steps: Amount → Payment Method → DetailsLicense Purchase Modal — 3 steps: Order Form → Processing → Success + Ledger link
Stats / chartLightweight stats container — "this exists, here's its scope"Full stats grid (Price, Cap, Volume, ATH) + 90-day projection chart
Financial instrumentsNone — donation goes 100% to creatorFidelity options chain · Charles Schwab IP-backed loans
Auth gatePublic — viewable by anyone in non-auth mode. Same auth posture as the Registry. License button click → Section 19 verification flowVerified buyers only — page-level gate. requireVerifiedBuyer() fires on page load. Anonymous and unverified-buyer sessions redirect to verification before the page renders
Reads fromcultural_assets (same row)cultural_assets (same row) + market history + license tiers

The architectural insight: the same asset can be both a venerated artifact and a tradable property without the platform having to pick one or the other. Product Detail honors the cultural dimension (heritage, ethical commitments, the act of supporting); IP Asset Detail honors the commercial dimension (tiers, market caps, options). A creator's lineage isn't compromised by being licensable; a buyer's commercial decision isn't reduced to its price tag. Both surfaces are first-class. Neither is the "primary."

Surface A — Product Detail (Public Listing)

The exhibition surface. A casual visitor's first encounter with a piece of cultural property. Composed as stacked tiles — Header, Product Image, Product Info, Creator Card, Action Buttons, Museum Exhibition (masonry detail cards), Stats. Treats the asset as something to be considered, not just transacted on. The two CTAs (Support and License) sit side-by-side as equally weighted paths, signaling that supporting the creator is as legitimate an outcome as licensing the work.

Tile Composition
Header tile — Page title "Listing Details" + breadcrumbs above
Product Image tile — Hero image + thumbnails strip
Product Info tile — Type · Name · Business · Description · Meta (date, registration ID)
Creator Card tile — Avatar · Name · Location · "View profile" link
Action Buttons tile — Support This Creator + License This Property (the two CTAs)
Museum Exhibition tile — "Property Details" masonry of 6 detail cards
Stats container — Lightweight (right column / row 4)
The Two CTAs — Support & License

Side-by-side equally-weighted action buttons. Support This Creator opens the donation modal; 100% of the donation routes to the creator with no platform cut. License This Property opens the license flow (which routes to verification if the user isn't a verified buyer). Both buttons are full-width within the tile, flexed equal, with explanatory micro-copy below.

• Support: Make a direct donation to the creator (100% goes to them).
• License: Obtain commercial rights to use this cultural IP in your brand.

The Support button uses a filled heart icon (fill="currentColor") at currentColor — gentle, warm. The License button uses a stroked document icon — formal, transactional. The icons themselves carry the tone: gift vs. contract.

Museum Exhibition — Masonry Detail Cards

The "Property Details" section below the action buttons renders as a museum-style masonry grid. Each card carries an icon + small mono category label in its header, a title, and structured body content. The cards are not all the same size — wide cards span 2 columns, standard cards span 1. This irregularity is intentional: it reads as exhibition layout, not data table.

CardLabelWidthBody
DescriptionAboutWide (2 cols)Title + full-paragraph rich text describing the cultural property
Cultural InformationHeritageStandardCountry of Origin · Cultural Significance · Context paragraph
Business InformationCreatorStandardBusiness Name · Website · Location (full-width row)
Ethical CommitmentsStandardsStandardFeature list with green checkmark per item: Fair Trade Practices, etc.
Blockchain VerificationProvenanceWide (2 cols)Registration ID + chain hash + verification timestamp
MetadataTechnicalStandardFile format · Resolution · Tags · Indexed date
Donation Modal — 3-Step Flow

Clicking Support This Creator opens the donation modal. Three steps in the same modal frame, just like the license purchase modal — but the steps reflect the donation intent, not commercial transaction. Step 1: Select Amount (preset tiles like $5/$10/$25/$100 + custom input). Step 2: Select Payment Method (Stripe, Apple Pay, Google Pay — populated by JS). Step 3: Enter Details (Stripe Card Element if selected, otherwise the chosen method's form).

Step 1 of 3
Select Amount

Preset tiles ($5, $10, $25, $100) + custom input. Active tile inverts to text-bg.

Step 2 of 3
Payment Method

Stripe Card · Apple Pay · Google Pay. Tiles populated by JS based on browser capability.

Step 3 of 3
Enter Details + Confirm

Stripe Card Element (or chosen method form). Confirm → 100% routes to creator.

The 100% rule: donations route entirely to the creator. The platform takes no fee on Support transactions. This is the structural distinction from the License path — Support is generosity, License is commerce. The same modal mechanic, two different economic flows, one transparent rule.

Surface B — IP Asset Detail (Trading View)

The trading-desk surface. A verified buyer's full evaluation surface. Page-level auth gate — only verified buyers can view this surface; anonymous and unverified sessions are redirected to the verification flow before the page renders. Two-column grid: main column carries the gallery + asset metadata + collapsible registration sections + stats grid + price projection chart; side column carries the three-market structure (Primary / IP Sale / Secondary) + license tier picker + financial instruments. Sticky side column keeps the picker visible while the buyer reads the asset details.

Two-Column Layout
Main Column · 1.5fr
Image gallery + watermark + lightbox
Blockchain ID strip
Asset title + meta (creator, date) + description
▾ Creator Information (collapsible)
▾ Cultural Context (collapsible)
▾ Registration Details (collapsible)
Stats grid (Price · Cap · Volume · ATH)
90-day price projection chart
Side Column · 1fr
Primary: License IP (tiers + execute)
IP Asset Sale (full ownership transfer)
Secondary Trading (license certificates)
Financial Instruments (Options + Loans)
▾ Three Ways to Acquire IP Rights

Grid: grid-template-columns: 1.5fr 1fr; gap: 32px at desktop. Stacks at ≤960px (main column above, side column below). Sticky position on side column at desktop so the license picker stays visible while reading the asset detail.

Image Gallery + Watermark

Square aspect-ratio container. CX brand watermark floats top-right on alpha (matches the card stamp pattern). Multi-image registrations show prev/next nav arrows + image-count badge. Click expand-icon → lightbox modal. Below: thumbnails strip (one row, horizontal scroll on overflow) and an optional image-specs panel showing total / current / format.

CX
MADE-2026-FCE4E

When I Get Home

Solange Knowles

A studio album exploring identity, place, and Black womanhood through immersive sonic architecture. Cultural property documented and indexed at the time of release.

Registration IDMADE-2026-FCE4E
CategoryMusic · Album
Statusverified
Created By
Solange Knowles
Greenbelt
Collapsible Info Sections

Three collapsible regions stack below the asset header, each scoped to one type of metadata. Toggled via toggleSection(id). Section title row uses 2px border-bottom on hover; chevron rotates 180° when expanded. Inside each section, a responsive .data-grid renders Label/Value pairs at 2 columns (desktop) or 1 column (mobile).

SectionFields
Creator InformationCreator Name · Creator Email · Company · Industry
Cultural ContextOrigin · Language · Genre/Style · Target Audience · Cultural Significance · Historical Period
Registration DetailsRegistration Type · Registration ID · Blockchain ID · Status · Registered Date · Item Category · Item Format · Usage Rights · Territory · License Duration · Exclusivity · Derivative Works · Attribution Required
Stats Grid

Four stat cards in a responsive grid. Each card carries a label + value + change indicator. Stat values render in var(--text) (NOT green); change indicators carry the directional sign and may be green for positive, neutral text for null states. The All-Time High card omits the change indicator and shows the date instead.

Current Price
$24,500
▲ +4.2%
Market Cap
$2.4M
Est. Valuation
24H Volume
147
12 trades
All-Time High
$28,900
Mar 14, 2026
Three Markets, One Asset — The Side Column

Every cultural asset can transact in three distinct markets. The side column makes this explicit so the buyer always understands which market they're entering. The Primary market (License IP) is foregrounded as the most common path; IP Asset Sale and Secondary Trading are visible but tertiary. This is intentional — most buyers want a license, not full ownership.

Primary
License IP

Purchase usage rights directly from creator. Creator retains 100% IP ownership. Creator can sell many licenses.

Most common path · Recorded to your Ledger
IP Asset Sale
Buy IP Outright

Creator sells complete ownership. Full copyright transfer. One-time transaction.

Rare · Heritage acquisitions
Secondary
Trade Certificates

Trade license certificates between holders. Market-driven pricing. Creator earns royalties on every trade.

Liquidity layer · Royalty backflow
License Tier Picker (Primary Market)

Inside the Primary section, license tiers render as selectable rows. Each tier shows price, scope summary, and an inline expand for full terms. Selection updates the Total Price strip below; the Execute button stays disabled until a tier is selected, then activates and routes to handleLicensePurchase().

Tier 2 — Commercial
$24,500
Standard commercial use · 2-year term · Non-exclusive
Tier 3 — Editorial
$8,200
Editorial / non-commercial · 1-year term · Single-use
Total $24,500
License is recorded to your account · Visible in Ledger
License Purchase Modal — 3-Step Flow

Clicking Execute License Purchase opens a 3-step modal. Step 1 collects the order form (use case, term, exclusivity, payment). Step 2 shows processing state with the Stripe payment intent confirmation. Step 3 shows success with receipt + ledger link. Each step replaces the previous in the same modal frame — no second modal stack. Cancel is allowed in steps 1 and 2; step 3's only action is a "View in Ledger" forward navigation.

Step 1 of 3
Order Form

Use case, term length, exclusivity selection, payment method (Stripe). Validate before enabling Continue.

Step 2 of 3
Processing

Stripe payment intent confirmed. Width-preserving spinner. No layout shift. Cancel still available.

Step 3 of 3 · Success
License Recorded

Receipt + license certificate ID + ledger link. Single forward action: View in Ledger.

Financial Instruments — Options & Asset-Backed Loans

Below the three markets, the side column surfaces partner-routed financial products. Fidelity handles options trading on cultural IP (covered calls, protective puts) for verified investors with approved options agreements. Charles Schwab handles asset-backed loans against verified IP portfolios (Pledged Asset Line, up to 50% LTV). Both buttons open the white-glove modal harness with full disclosure copy — no native browser dialogs. Disclaimer footer below references investment risk, broker-dealer relationships, and forced-liquidation possibility.

InstrumentPartnerEligibilityMechanic
Options chainFidelityApproved options agreement, min balanceCalls + puts on registered assets with sufficient market depth. Settles in physical license certificates
IP-backed loanCharles SchwabKYC verified, ≥$10K portfolioPledged Asset Line, revolving credit, LTV ≤ 50%, interest on drawn amounts only. 3–5 day disbursement
Page Composition Notes
ElementSpec
Top headerInherits canonical 2-tier nav (Section 06). Tier 2 on this page shows Volume + Refresh, left-justified — no sort/view tools. auth_state drives SIGN IN/UP vs user-menu
Detail layout griddisplay: grid; grid-template-columns: 1.5fr 1fr; gap: 32px at desktop. Stacks at ≤960px
Side column stickyposition: sticky; top: 96px — license picker stays visible while user scrolls main column
Image watermarkCX outline, position: absolute; top: 16px; right: 16px, opacity 0.4. Same primitive as card stamp (Section 08)
Section togglestoggleSection(id) — rotates chevron 180° via class, no animation library required
Stat valuescolor: var(--text). Change indicators may carry green for positive direction
Auth gatingPage-level gate. requireVerifiedBuyer() fires on page load — anonymous and unverified-buyer sessions redirect to the verification flow before the page renders. Distinct from the Registry and Product Detail surfaces, which are public-readable in non-auth mode
ModalsLicense purchase modal + white-glove modal harness (wgAlert, wgConfirm) — see Section 12 modal patterns. Native dialogs banned
Message drawerRight-anchored slide-in for creator/buyer messaging. Overlay click closes. Independent z-index from license modal

Account Settings & Wallet

The account-settings template is the canonical home for the MADE CX Wallet, the Personal Valuation Card, payout funding controls, and connect-method tiles. All wallet-and-card patterns shown elsewhere in the platform are implemented here first and referenced by ID/class.

Canonical Reference
/account-settings.html

This is where the wallet, the Personal Valuation Card, and all connect-method tiles are implemented. Treat the markup + CSS in that file as the source of truth — changes to these patterns happen in account-settings.html first, then propagate elsewhere. Class names (.tile-wallet, .card-visual, .wallet-balance-block, .funding-option) are stable and re-used wherever wallet/card UI appears.

MADE CX Wallet

A two-column live balance (Available / Pending) sourced from Stripe Connect via the get-wallet-balance Edge Function. Status pill shows auto-payout state. Funding-speed picker writes to payout_preference via set-payout-schedule.

MADECXWallet Auto-payouts active
Available
$2,847.12
Ready to withdraw
Pending
$890.00
Settling from sales
Next payout: Mon, May 12
Wallet Specs
ElementStyle
Container2px solid var(--border), padding 24px, no border-radius
Wallet labelInter 900 16px, "MADE" + green-stroke "CX" + "Wallet" suffix at weight 600
Status pill1px green border, 9px mono uppercase 0.12em, dot 6×6 green
Balance amountSpace Grotesk 32px / 900 / line-height 1
Pending amountSame scale as Available, color var(--text-secondary)
Balance labelMono 10px / 700 / 0.12em uppercase / var(--text-muted)
Next-payout stripBackground var(--bg-alt), mono 11px, clock icon 14×14
Personal Valuation Card

Credit-card-shaped visual (1.586:1 ISO/IEC 7810 ID-1 ratio) representing the buyer's MADE CX account in physical form. Black gradient background, green accent border, four hairline corner brackets, top status bar, EMV chip + contactless icon, ticker symbol, masked PAN, cardholder name, network logo, and footer brand mark.

MADE CX // PERSONAL VALUATION CARD // VOL. I SERIES A
— TICKER SYMBOL
$ANGELA
••••  ••••  ••••  0000
— CARDHOLDER
ANGELA DAVIS
PARTNER PENDING
MADECX EXCHANGE // MADE CX
Card Visual Specs
ElementStyle
Aspect ratio1.586 / 1 (ISO/IEC 7810 ID-1 credit-card)
BackgroundTwo radial gradients (white-tint @ 18%/30% and 82%/75%, both at 4–6% alpha) over linear-gradient(135deg, #050505, #1A1A1A 50%, #050505) — purely tonal, no green wash
Border2px solid rgba(255,255,255,0.4) — white-tinted to read on the dark card. No green: green strokes are reserved for activations, never UI chrome (v3.0 rule)
Corner brackets14×14, 2px rgba(255,255,255,0.6), absolute-positioned at 6px from each corner
Status barTop edge, mono 8px / 700 / 0.18em uppercase, 6×6 green dot (activation indicator), 1px rgba(255,255,255,0.15) hairline divider
EMV chip36×28 gold gradient (#C9A66B → #A8884A → #8C6E36)
Contactless iconThree concentric arcs, stroke rgba(255,255,255,0.85)
Ticker symbolSpace Grotesk 34px / 900 / line-height 1, "$" prefix at 0.4 alpha
Card number (PAN)Mono 13px / 0.18em, masked dots + last-4
CardholderMono 11px / 700 / 0.12em uppercase
Footer brand mark"MADE" in solid white + "CX" with color: transparent; -webkit-text-stroke: 1.5px var(--green). Inter 900 13px. The CX outline is the one canonical green stroke permitted in the system — it appears wherever the brand mark renders, including inside UI surfaces like this card
Padding30px 28px 22px
Max width480px (responsive width 100%)
Funding Speed Picker

Two-tile radio group. Selection writes to payout_preference via set-payout-schedule and triggers re-fetch of next-payout ETA. Standard = free 1–3 day ACH; Instant = +1% Stripe Instant Payouts (~30 min).

Connect Methods

External-service tiles for connecting third-party accounts (Stripe Connect, bank ACH, exchange APIs, social verification). Each tile is a uniform 2px-bordered cell with a leading mono service name, status copy, and a right-aligned action button (CONNECT for unlinked, MANAGE for linked, with green check). The status pattern is reused across all integration surfaces.

Stripe Connect
Connected · acct_1TT6lY4WP50zjH55
Bank Account (ACH)
Not connected
Page Composition

The account-settings template stacks tiles in a single column on a centered max-width:1100px page body, each tile separated by 32px vertical gap. Tile order (top→bottom): identity → wallet → personal valuation card → connected methods → notification preferences → security → danger zone. The same tile primitive (.tile = 2px border + 24px padding + var(--card-bg)) is the structural unit; each variant adds its own modifier class (.tile-wallet, .tile-card-promo, etc.) for content-specific layout.

Notifications, Drawers & Header Adaptation

Three platform-wide rules for header components: a single canonical notification taxonomy, a single drawer pattern, and the rule that all headers must adapt to the active theme.

Theme-Adaptive Headers (Required Everywhere)

Every header on the platform — including marketing pages (pricing, licensing, about, etc.) — must use the theme tokens for background, text, and borders. Hardcoded background: #000000 and color: white are not allowed in the header CSS block. Use the variables the page already defines.

SelectorPropertyRequired Value
.headerbackgroundvar(--bg-primary) or var(--bg)
.headerborder-bottom2px solid var(--border-color) or var(--border)
.logo-madecolorvar(--text-primary) or var(--text)
.nav-link, .nav acolorvar(--text-primary) — never white
.auth-btn (filled)background & colorbackground: var(--text-primary); color: var(--bg-primary) (inverse of bg)
.auth-btn (outline)border & colorborder: 2px solid var(--border-color); color: var(--text-primary)
Notification Bell + Dropdown Taxonomy

Used on brand-dashboard.html and account-settings-buyer.html. The class names below are stable and shared — do not invent parallel .notif-* classes. The dropdown shows/hides via the .active class, not the [hidden] attribute.

ClassElementNotes
.header-icon-wrapperContainer around bell + dropdownposition: relative
.header-icon-btnBell button40×40, 2px outline border
.notification-badgeUnread count18px, mono font, bg=text/text-on-bg inverse, top-right corner
.dropdown-panelDropdown container360px wide, 480px max-height, hidden by default; .active shows it
.dropdown-headerTitle rowHolds .dropdown-title + .dropdown-action (e.g., "Clear all")
.dropdown-listScrollable item list360px max-height, overflow-y auto
.dropdown-itemSingle notification40px avatar (SVG icon) + content (title row + 2-line message). Add .unread for unread state
.dropdown-item-avatarIcon container40×40 with bg-secondary, holds an inline SVG; SVG color is var(--text-muted)
.dropdown-footerBottom action rowHolds .dropdown-footer-link ("View All")
Data Layer — user_notifications Table

The single canonical notifications table. All bell components query this table joined by user_email. The icon for each row maps from the icon_type column (or type, fallback) via getNotifIcon():

icon_typeUse For
licenseLicense issuance, expiry
tradeMarketplace trades
paymentPayment events
approvalVerification approval, account-level approvals
revision_requestReviewer needs changes / additional documents
systemGeneric system messages, status changes
Drawer (Mobile + Tablet)

Always slides in from the left. Header is sticky inside the drawer. Use visibility: hidden, never display: none, while loading state is being decided so the layout doesn't shift.

PropertyValue
positionfixed
top / left (closed)0 / -100%
left (open)0 (set via .open class)
max-width400px
height100vh
border-right2px solid var(--border)
transitionleft 0.4s cubic-bezier(0.4, 0, 0.2, 1)
drawer-headerSticky logo + close, position: sticky; top: 0
drawer-body24px padding, scrollable

State-Aware Flow Routing

Multi-step submission flows must check for existing user state before rendering the form. A buyer with an in-flight verification submission should never see the new-application form — they should be redirected to the read-only review page that surfaces their actual status.

The Pattern

Every gated multi-step flow runs a resolve<Entity>State() check immediately after the auth gate clears, before any form rendering. The check returns one of three outcomes:

OutcomeAction
'redirected'Function called window.location.replace(...). Caller stops init.
{ kind: 'draft', row, lastStep }Caller proceeds with init, then calls restoreDraftInto() after event listeners are wired.
nullNo prior state. Render form fresh.
Status Routing Matrix — Verification Example
StatusURL Has ?upgrade=NAction
(no row)anyShow form fresh
draftanyRestore draft into form, jump to last step
expiredanyShow form (renewal flow)
submitted · under_review · pending_documentsnoRedirect to review page
verified · rejected · suspendednoRedirect to review page
any in-progressyesAllow form (legitimate upgrade flow)
Redirect UX

Three rules to prevent user confusion when redirected:

  • Hide the form during the check. Set visibility: hidden on the main layout while the DB query resolves. If we're not redirecting, reveal it. Prevents flicker.
  • Use window.location.replace(), not .href. The back button shouldn't loop the buyer back into a redirect cycle.
  • Stash a sessionStorage hint. The destination page reads the hint and shows a brief 5-second toast acknowledging the redirect (e.g., "Your application is already in review. Track its status below."). Hint must include a timestamp and be cleared on read — ignore stale hints older than 5 seconds.

Threaded Messaging — Terminal Style

Every message thread surface on MADE CX uses the same visual pattern: sharp-edged panels with mono shell-prompt headers and square avatars. The aesthetic is dev-tool, not chat-app. No rounded bubbles, no circular avatars. The pattern lives on three pages today: admin.html (admin thread view), contact.html (customer thread view, anon and authed), and dashboard.html (creator messaging). Any future messaging surface adopts this same pattern verbatim.

Anatomy

Each message is a flex row containing two children: a 36×36 square avatar and a max-width message card. The card has two stacked sections — a terminal prompt header and a mono body. Side alignment + accent stripe + prompt color together communicate sender role without needing a separate badge.

ElementSpec
Row wrapper.message or .message-bubble. flex, width 100%, gap 12px, margin-bottom 16px. flex-direction: row-reverse for own/admin messages so the avatar visually trails.
Avatar36×36 square. 2px border, NO border-radius (border-radius: 0). Mono font, 13px/700, uppercase initial. Border color matches the card's accent stripe.
Cardmax-width: calc(78% - 48px) (leaves room for avatar + gap). min-width 280px so short messages don't shrink to nothing. 2px border, sharp corners. 4px accent stripe on the side opposite the alignment (green right-stripe for own/admin, gray left-stripe for incoming/user).
Prompt headerflex, gap 8px, padding 10px 14px. Background = var(--bg-secondary) for incoming, rgba(0, 200, 5, 0.06) for own. 1px bottom border. Mono 11px/500, letter-spacing 0.03em.
Bodypadding 14px 18px. Mono 13.5px, line-height 1.65. white-space: pre-wrap so multi-line messages render correctly. word-break: break-word to handle long URLs.
Prompt Header Tokens
ElementClassStyle
Sigil.message-prompt-sigilLiteral $ character. Font-weight 700. Color: green for own/admin, text-secondary for incoming/user.
Hostname.message-prompt-host or .message-author-labeltext-primary, font-weight 600. Content rules below.
Separator.message-prompt-sepLiteral · middle-dot. text-secondary at opacity 0.6.
Timestamp.message-prompt-time or .message-timetext-secondary, font-weight 500, font-feature-settings: 'tnum' for tabular numerals. Margin-left auto pushes it to the far edge.
Content Conventions
FieldOwn / AdminIncoming / User
Hostnamecx-support (or you in creator dashboard)Local-part of sender's email (e.g., jay from jay@drgx.co). Fallbacks: sender_name lowercased, then customer.
Avatar initialM (MADE)First letter of sender_name, then first letter of sender_email, then U. Always uppercased.
Timestamp formatYYYY-MM-DD HH:MM:SS (24h, zero-padded). Use the included formatLogTimestamp(date) helper or equivalent.
Direction & Role Determination

Whether a message renders as own/admin (right) or incoming/user (left) is determined by sender_role, the canonical column added in migration 2026-05-10e. The role is resilient to admin email renames; never use sender_email === currentUser.email comparisons. Falls back to email match only for legacy pre-migration rows that lack sender_role.

Required HTML Skeleton
<div class="message [is-admin|is-user|own]">
  <div class="message-avatar">M</div>
  <div class="message-card">
    <div class="message-meta">
      <span class="message-prompt-sigil">$</span>
      <span class="message-author-label">cx-support</span>
      <span class="message-prompt-sep">·</span>
      <span class="message-time">2026-05-10 13:26:03</span>
    </div>
    <div class="message-body">message text here</div>
  </div>
</div>
Anti-Patterns
  • Round avatars. The 50% border-radius circle is the chat-app default and breaks the terminal aesthetic. Always square.
  • Rounded bubble corners. No border-radius on cards. Sharp 90° corners everywhere.
  • Sender labels like "MADE CX Support" or "You". Use the terminal hostname convention (cx-support, jay, you) so the prompt header reads like a real shell.
  • Localized timestamps (5/10/2026, 1:26:03 PM). Use ISO-ish YYYY-MM-DD HH:MM:SS for log feel and tnum alignment.
  • Mixing prose font with mono. Both header AND body use mono. Don't switch fonts.
  • Side alignment without color cue. Always pair side alignment with the colored accent stripe + prompt sigil color. Side alone isn't enough for scanning.

Agentic Operations & Credits

The MADE CX Agent runs on a metered allowance system. Subscribed creators receive a monthly bundle of operations (100/mo at the Verified tier). Operations beyond that draw from paid credits, sold in three packs (25, 100, 500). Creators can enable auto top-up to charge a saved card off-session whenever the balance hits zero. Every surface in this section is a creator-facing billing UI — they live primarily in account-settings.html and licensing-opportunities.html, and route through the billing Edge Function.

Canonical Reference
billing/index.ts & account-settings.html

All billing flows route through the billing Edge Function actions: catalog, overview, purchase, charge-saved-card, autotopup, link-subscription-card. The UI never embeds Stripe.js card capture directly — subscription cards are reused via the link endpoint instead.

Allowance Bar

The progress strip showing “X of N agentic operations used · Resets {date}”. Lives at the top of any page that consumes agent operations (licensing-opportunities, dashboard agent surface). Border is fully neutral — no green left-edge accent. State changes via full-frame border-color: amber for low, red for exhausted.

Agentic Operations
100 of 100 remaining · Resets May 31
25 credits
Paid balance
Allowance Bar States
StateBorderFillButton labelNotes
Default2px solid var(--border)var(--text)“Manage”Standard meter
Low (<20%)2px solid var(--warning)var(--warning)“Manage”Amber full-frame
Exhausted (=0)2px solid var(--red)var(--red)“Buy Credits”Faint red bg tint rgba(220,38,38,0.04)
Manage Agentic Button

The action button at the right edge of the allowance bar. Opens the Agentic Operations Pricing modal. Pure structural styling — black border, white background, black text. Hover inverts.

SpecValue
Padding9px 16px
FontInter 800 / 11px / 0.10em uppercase
Border2px solid var(--text) (no green)
Defaultbg var(--bg), color var(--text)
Hoverbg var(--text), color var(--bg) (inversion)
Class.allowance-bar-action
Handleronclick="openPricingModal()"
Pricing Modal

Triggered by the Manage button. Shows tier cards (Free / Verified / Pro) above paid credit packs (25 / 100 / 500). Catalog is fetched from billing?action=catalog (public, apikey only), then per-user usage from billing?action=overview (best-effort with auth token). Modal opens whether or not overview succeeds — the catalog always renders.

Agentic Operations Pricing

×
Free
$0
10 ops/mo
Verified · current
$49/mo
100 ops/mo + 10 IDs
Pro
$199/mo
500 ops/mo
Or buy credits
$5
25 credits
$45
100 credits
$60
500 credits · best value
Buy More Credits Modal

Standalone modal for one-shot pack purchases from the Subscription tile. Self-contained (does not instantiate Supabase client) to avoid GoTrueClient hangs from multi-tab auth coordination. Reads access_token directly from localStorage using the sb-{ref}-auth-token key pattern. POSTs to billing?action=purchase and redirects to Stripe Checkout.

Auto Top-Up Modal

Enables off-session charges when credits hit zero. Two states:

  • No payment method: Shows a single button — LINK MY SUBSCRIPTION CARD. Calls billing?action=link-subscription-card which self-heals: looks up the user's Stripe customer (searching by email if stripe_customer_id column is NULL), grabs the default payment method, saves it to creator_payment_method via save_payment_method RPC.
  • Card linked: Shows a banner Charging VISA ····4242, an enable toggle, pack dropdown (default 25-Pack), and a max-charges button group (1 / 2 / 3 / 5 / 10) with inverted-active styling.
// Auto refill

Auto Top-Up

Charging VISA ····4242
Max charges per billing period

Selected option uses inversion (filled var(--text) bg, var(--bg) text) — NOT green tint.

Subscription Card Self-Heal Flow

A graceful resolution path when user_profiles.stripe_customer_id is NULL despite the user having an active subscription. The link-subscription-card endpoint cascades through four lookup strategies:

StepLookupOn miss
1user_profiles.stripe_customer_idFall through to 1b
1bSearch Stripe by auth.users.emailReturn 400 no_subscription
2customer.default_payment_methodFall through to 3
3subscription.default_payment_methodFall through to 4
4List PMs on customer, pick first cardReturn 400 no_payment_method
5Save to creator_payment_methodWrite customer_id back to user_profiles

Tile-Pair Masonry Layout

A 2-column layout pattern where the left and right columns each pack their tiles flush, with no row-height matching between columns. Used canonically on account-settings.html to stack Subscription + Valuation Card on the left and Wallet + Stripe Connect on the right. Avoids the awkward whitespace that occurs in symmetric grids when tiles have unequal heights.

Canonical Reference
account-settings.html (lines ~4200–4500)

The pattern is implemented via DOM column wrappers (not CSS Grid). Each column is a flex-column container that owns its own children. Mobile collapses to a single column at @media (max-width: 900px).

Markup pattern
<div class="tile-pair">
  <div class="tile-pair-left">
    <div class="tile">Subscription</div>
    <div class="tile">Personal Valuation Card</div>
  </div>
  <div class="tile-pair-right">
    <div class="tile">Wallet</div>
    <div class="tile">Stripe Connect</div>
  </div>
</div>
CSS
.tile-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
  max-width: 1400px;
  margin: 0 auto;
}
.tile-pair-left,
.tile-pair-right {
  display: flex;
  flex-direction: column;
  gap: 24px;
}
@media (max-width: 900px) {
  .tile-pair { grid-template-columns: 1fr; }
}
Visual schematic
Tile A
Subscription
Tile C
Personal Valuation Card
Tile B
Wallet
Tile D
Stripe Connect

Notice: Tile A (120px) and Tile B (160px) have different heights. The columns pack independently — no awkward whitespace.

When to use
  • 2×2 layouts where tile heights naturally differ
  • Account/settings pages with grouped controls
  • Dashboards with paired data widgets
When NOT to use
  • Symmetric layouts where rows must align (use CSS Grid auto-rows instead)
  • 3+ column layouts (use a true masonry library)
  • When tile order matters across columns (this layout reads top-to-bottom per column, not left-to-right)

MADE CX Agent Thread

The MADE CX Agent is the creator's autonomous representative inside the platform. It lives as a pinned thread at the top of the messaging drawer — accessible from the messages icon in every creator-facing page header. Each message exchange consumes one agentic operation from the creator's monthly allowance (or one paid credit if the allowance is exhausted).

Surface Location
Messaging Drawer · Pinned Thread

The agent thread appears at the top of the drawer above all human conversations. It cannot be archived or deleted. The drawer opens from the message icon in the page header, present on every authenticated creator page.

Thread preview — pinned position
Thread — terminal-style messages

When opened, the agent thread uses terminal-style message blocks: mono typography, sharp borders, timestamps on every entry. 12-hour clock format. Each agent response is preceded by a small “thinking” indicator while the LLM call is in flight.

You · 2:45 PM
What's my best move on the DRGX deal?
MADE CX Agent · 2:47 PM · 1 credit
Two licensed prints at $999 each suggests they'd accept a 3-year exclusive for 6× that. Their last 3 partnerships averaged $24K. I'd open at $30K with a 5-year option clause.
Specs
ElementSpec
Drawer triggerMessage icon in header, badge count shows unread total
Drawer positionRight-side slide-in, fixed width 380px
Pinned indicatorMono 8px “· Pinned” in green, before timestamp
Thread title“MADE CX Agent” with brand-stroke CX
Message block (you)Left border 2px solid var(--border), 14px padding-left
Message block (agent)Left border 2px solid var(--text), distinguishes voice
Timestamp format12-hour clock, e.g. “2:47 PM”
Cost indicatorPer-message: “1 credit” (or “1 of monthly allowance”)
Backendget_creator_agent_context_v2() RPC provides context, agent-chat Edge Function handles inference
Modelsgpt-4o-mini primary, Claude Haiku 4.5 fallback

UI Patterns

Common patterns: section headers, pull quotes, stats, layer diagrams, form elements, badges, and icons.

Section Header

The MADE Foundation

Reclaiming $15 Trillion by 2050

Pull Quote

Culture is the world's most powerful operating system. MADE CX is the ledger that protects it.

Statistics
$15T
Target by 2050
4%
Community Reinvestment
100%
Transparency
Layer Diagram
Top
Creators

Broadcasting, licensing, owning value

Middle
MADE CX

Ledger / Compliance / Valuation

Bottom
Marketplace

Brands / Platforms / Institutions

Form Elements
Badges
Default BadgeOutline Badge
Filter Chips
AllMusicFashionArtDance
Icons
Arrow
External
CXCX Mark
Check
Menu
Close
Expand
Grid
Modals — White-Glove Pattern

Modals are conversations, not interruptions. The platform handles licensing decisions worth thousands of dollars; modal interactions need to feel commensurate with that. White-glove means: titled, considered, dismissable on the user's terms, never auto-closed mid-read, never ambient-lit by browser default chrome. Every modal has a backdrop, a centered card with sharp 2px border, an unambiguous title, body content, and a deliberate action row. Nothing more, nothing less.

Confirm Modal — anatomy preview (not a real modal)
License Acquisition

Confirm purchase

You're about to acquire a Tier-2 Commercial license for "In My Mind" by Heritage at $24,500. This will create a permanent record on the cultural ledger and trigger a 4% community reinvestment.
Modal Anatomy
RegionSpec
Backdropposition: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1300. Click backdrop → close (unless modal is destructive — then require explicit Cancel)
Modal cardCentered with flexbox. Max-width 480–680px depending on density. border: 2px solid var(--text). Background var(--bg). No box-shadow, no border-radius — sharp edges, contrast over glow
HeaderPadding 20px 24px, border-bottom: 2px solid var(--border). Mono 9px / 0.16em uppercase context label above Space Grotesk 22px / 900 title. Close X (32×32 box, 1px border) flush-right
BodyPadding 24px. Body copy 14px / 1.6 line-height. Bold the variables (amount, asset name) so they read at a glance
Actions rowPadding 16px 24px, border-top: 2px solid var(--border), justify-content: flex-end. Cancel left of Confirm. Primary button on the right (matches reading-order: scan left → click right)
AnimationBackdrop fades in 200ms. Card slides up 12px + fades in over 240ms cubic-bezier(.4,0,.2,1). On close: reverse, 180ms (slightly faster — exits should feel decisive)
Focus managementOn open: focus first interactive element (close button or first input). Trap Tab within modal. On close: return focus to trigger
Esc keyCloses modal (unless destructive — same rule as backdrop click)
Modal Variants
VariantUse caseAction row
Info"Here's what happened" — license issued, payout sent, transaction confirmedSingle primary button: "Got it" or "View receipt"
ConfirmReversible decisions — discard draft, save changes, switch tiersGhost Cancel + Primary Confirm
DestructiveIrreversible — delete asset, revoke license, deactivate account. Backdrop click and Esc are disabled — user must click Cancel or ConfirmGhost Cancel + Primary Confirm with color:var(--red) on the icon
FormInline data entry — verification doc upload, edit profile field, set payout methodGhost Cancel + Primary Submit (disabled until valid)
Notifications — Toast Pattern

Toasts confirm without blocking. Used for ambient confirmations: "Saved." "Payment received." "Verification submitted." A toast slides in from the bottom-right, sits for 4 seconds, then exits unless the user dismisses it. Never used for critical info — if a user must see it, use a modal.

License confirmed
"In My Mind" — Tier 2 Commercial
Verification submitted
Review in 1–2 business days
Payment declined
Card on file expired — update method
Toast variantBorder-left colorIconUse case
Successvar(--green) — activation indicator (permitted)Check (stroke green)Confirmations: saved, sent, paid, license issued
Infovar(--text)Info circleStatus updates: submitted, in review, queued
Errorvar(--red)X circleFailures: payment declined, validation error, network
Toast Specs
PropertyValue
Positionposition: fixed; bottom: 24px; right: 24px; z-index: 1200. Stack vertically with 12px gap, newest on top
ContainerMin-width 320px, max-width 480px. background: var(--bg), border: 2px solid var(--text), border-left: 4px solid {variant}
Padding14px 18px
TitleSpace Grotesk 14px / 800, color var(--text)
BodyInter 12px / regular, color var(--text-secondary)
Auto-dismiss4000ms. Hover pauses the timer; mouse-leave resumes
AnimationSlide in from right 16px + fade over 280ms cubic-bezier(.4,0,.2,1). Exit: slide out + fade over 200ms
Mobile (≤640px)Full-width minus 24px gutters, anchored bottom-center. Stack still applies
Anti-Browser-Dialog Policy
Forbidden APIs

Native browser dialogs are banned in production. No alert(), no confirm(), no prompt(). Browser dialogs are visually inconsistent across operating systems, ignore the platform's typography and color system, and break the white-glove tone the platform requires. They also can't be styled, can't be themed (light/dark), can't be tested in Playwright reliably, and create accessibility regressions on mobile.

Use the canonical modal API instead: wgAlert(title, body), wgConfirm(title, body, { onConfirm, destructive }), wgPrompt(title, body, { placeholder, onSubmit }), wgSuccess(message). Implementation lives in /account-settings.html and is portable to any page.

Native API (banned)Canonical replacementWhy
alert("Saved!")wgSuccess("Saved") → toastConfirmations don't need to block. Toast slides in, dismisses itself
alert("Error: ...")wgError(title, body) → modal or toast based on severityErrors deserve a real modal when the user must read; toast for ambient failures
confirm("Are you sure?")wgConfirm(title, body, { onConfirm })Yes/No dialogs are the most common offender. Custom confirm respects theme + lets you mark destructive variants
prompt("Enter name:")wgPrompt(title, body, { placeholder, onSubmit }) or a real form modalNative prompt is the worst of the three — looks like a virus alert on Windows. Always replace
window.confirm on form submitInline validation + modal on dirty navigation"Leave site? Changes you made may not be saved." Replace with a proper unsaved-changes modal
Canonical API Sketch
JavaScript — wg* modal/notification helpers (Promise-based)
// Promise-based — await the user's decision instead of a callback pyramid
async function wgConfirm(title, body, opts = {}) {
  return new Promise((resolve) => {
    const modal = buildModal({
      label: opts.label || 'Confirm',
      title,
      body,
      destructive: !!opts.destructive,
      actions: [
        { kind: 'ghost', text: 'Cancel', onClick: () => { close(); resolve(false); } },
        { kind: 'primary', text: opts.confirmText || 'Confirm',
          onClick: () => { close(); resolve(true); } }
      ]
    });
    open(modal);
  });
}

// Usage — replaces window.confirm, theme-respectful, testable
const ok = await wgConfirm(
  'Confirm acquisition',
  'You\'re about to acquire a Tier-2 Commercial license for $24,500.',
  { confirmText: 'Confirm Acquisition' }
);
if (ok) await acquireLicense(asset.id);

// Toast for ambient confirmations — never blocks
wgSuccess('License confirmed', '"In My Mind" — Tier 2 Commercial');
wgError('Payment declined', 'Card on file expired — update method');
wgInfo('Verification submitted', 'Review in 1–2 business days');

For LLMs and devs: if you find yourself reaching for alert, confirm, or prompt while writing platform code, stop and use the wg* helpers. If they don't yet exist on the page you're working on, copy them from /account-settings.html. There is no situation in this platform where a native browser dialog is the right answer.

White-Glove Dialogs — MADEDialog

Native browser dialogs (alert(), confirm(), prompt()) are banned across the MADE CX product. They break the editorial aesthetic with browser-chrome styling, can't be themed for dark mode, and feel cheap. Every confirmation, error, and prompt routes through the MADEDialog module: a sharp-corner, mono-typeset, framed modal that matches the rest of the product.

Mandatory replacement
Never write alert() — always MADEDialog.alert()

If you find yourself writing alert('Failed to save'), stop. Replace with MADEDialog.alert({ variant:'error', title:'Save failed', message:'...' }). The MADEDialog code lives at the top of account-settings.html (line ~5091) and can be copy-pasted into any page that needs it.

API Surface
MethodReturnsUse for
MADEDialog.alert(opts)Promise<void>Errors, success notices, info messages with a single OK
MADEDialog.confirm(opts)Promise<boolean>Destructive actions (cancel sub, delete work, etc.) needing yes/no
MADEDialog.loading(opts){ update, success, error, close }Long-running operations (Stripe redirects, file uploads)
Variants

Every alert/confirm call passes a variant string that styles the icon and border. Default is 'info'.

VariantIconBorder accentCommon usage
infoi circlevar(--text)Default info messages, neutral confirmations
successcheckmarkvar(--success)Save successful, card linked, payout completed
warningtrianglevar(--warning)Low balance, near limits, non-blocking issues
error! squarevar(--red)API failures, auth errors, validation issues
Live Example — Error Variant
!

Could not link card

No active Stripe subscription found on your account. If you have a Verified subscription, please contact support — your Stripe data may not be linked.

no_subscription

Friendly Error Mapping

Edge functions return machine-readable error codes (no_subscription, no_payment_method, no_email, etc.). The UI maps these to human-readable messages before showing the dialog — never expose raw error codes as the primary message. Raw codes belong in the detail field.

Specs
ElementStyle
OverlayFixed full-screen, rgba(0,0,0,0.6), click-outside dismisses (alert only)
Card border2px solid (color varies by variant)
Card padding24-28px
TitleSpace Grotesk 900 / 18-20px / -0.01em
MessageInter 14-15px / 1.55 / var(--text-secondary)
Detail lineMono 11px / var(--text-muted), below message
OK / Confirm button.btn-primary styling (filled inverted)
Cancel button.btn-secondary (outlined)
Destructive confirmConfirm button border & bg = var(--red)
Escape keyCloses alert / cancels confirm (resolves false)

Anti-Patterns — Never Do These

A concise reference of design and engineering decisions that violate the MADE CX standard. If you spot any of these in code review, flag them — they're either holdovers from prototype phases or shortcuts that compound into incoherence. Each one has been deliberated and rejected; do not relitigate without a strong case.

!

Don't use rounded corners

Every box has border-radius: 0. Buttons, cards, inputs, modals, tiles — all sharp. The only exception is the 6×6 status dot (border-radius:50%) and the badge-count circle. If you import a third-party component that ships rounded, override the radius.

!

Don't use green as a structural border

Green (#00C805) is punctuation, not structure. Acceptable: 4px green bar on .form-message.success (state indicator), the brand logo CX stroke, small icons (≤16px) inside buttons. Not acceptable: green border-left on tiles, green border on connected platform cards, green left-accent on the allowance bar, green outline on focus rings.

!

Don't color body text or titles green

Section eyebrows, work-type labels, drawer titles, and similar mono uppercase text are var(--text-muted) — never green. Hero titles are var(--text). If you want a green accent on an eyebrow, use a 6×6 dot or a 22×2 bar before the text, not the text color itself.

!

Don't tint selected states green

Selected options in radio-like pickers (Funding Speed, Withdraw modal speed, Auto Top-Up max charges) use inversion — filled var(--text) background with var(--bg) text. The unselected state has a neutral border. Green tinting violates the rule because it implies brand-positive sentiment on a neutral choice.

!

Don't use native browser dialogs

alert(), confirm(), and prompt() are banned. They break the editorial aesthetic and can't be themed. Every error, confirmation, and notice routes through MADEDialog. See section 12b for the API surface.

!

Don't put auth tokens in URLs

Access tokens, API keys, and similar credentials only appear in Authorization: Bearer headers — never as URL query params. URL params leak through browser history, referrer headers, and server logs.

!

Don't collect cards outside Stripe Elements

Credit card numbers, CVCs, and expiry dates only enter the system through Stripe Elements iframes. Never roll a custom card form. For auto top-up, reuse the subscription card via the link-subscription-card Edge Function action — don't collect again.

!

Don't instantiate multiple Supabase clients per page

Multiple createClient() calls within one page produce GoTrueClient instance conflicts and silent auth/query hangs. For pop-up modals that need data on demand, use a self-contained fetch fallback: direct fetch() to the Edge Function or PostgREST with apikey + access_token headers, reading the token from localStorage. Pattern is documented in account-settings.html Buy More Credits modal (v23).

Self-Contained Modal Pattern

An engineering pattern for modals that must work even when the page's primary Supabase client is hanging or compromised. Used in Buy More Credits, Auto Top-Up, and the Agentic Operations Pricing modal. The modal bypasses supabase.createClient() entirely and reads auth tokens directly from localStorage, hitting Edge Functions via plain fetch().

!

Why this pattern exists

Multiple supabase.createClient() calls within one page produce GoTrueClient instance conflicts. Symptoms: await supabase.from(…).select() hangs forever, never resolving or rejecting. The page UI is stuck in a loading state with no error.

This pattern sidesteps the problem by avoiding the supabase client entirely for these modals.

The three-step pattern
  1. Find the auth token in localStorage. Supabase stores it under a key matching sb-{ref}-auth-token. Iterate localStorage.key(i), match the pattern, parse JSON, extract access_token.
  2. Fetch with plain fetch(). No supabase client. Two headers: apikey (anon key, public) and Authorization: Bearer {access_token} (auth, only when needed).
  3. Graceful degradation. Public catalog data fetches first with apikey only — always succeeds. User-specific overview fetches second with auth — best-effort. If overview fails, the modal still renders with the catalog.
Reference implementation
async function openSelfContainedModal() {
  const cfg = window.MADE_CONFIG || window.APP_CONFIG || {};
  if (!cfg.SUPABASE_URL || !cfg.SUPABASE_ANON_KEY) {
    body.innerHTML = '<div class="pricing-error">Config missing</div>';
    return;
  }

  // Step 1: Find auth token in localStorage
  let accessToken = null;
  for (let i = 0; i < localStorage.length; i++) {
    const k = localStorage.key(i);
    if (k && k.startsWith('sb-') && k.includes('-auth-token')) {
      try {
        const stored = JSON.parse(localStorage.getItem(k));
        accessToken = stored && stored.access_token;
        if (accessToken) break;
      } catch (_) {}
    }
  }

  // Step 2: Public fetch (always works)
  const catRes = await fetch(
    cfg.SUPABASE_URL + '/functions/v1/billing?action=catalog',
    { headers: { apikey: cfg.SUPABASE_ANON_KEY } }
  );
  const catalog = await catRes.json();

  // Step 3: Auth'd fetch (best-effort)
  let usage = {};
  if (accessToken) {
    try {
      const ovRes = await fetch(
        cfg.SUPABASE_URL + '/functions/v1/billing?action=overview',
        {
          headers: {
            apikey: cfg.SUPABASE_ANON_KEY,
            Authorization: 'Bearer ' + accessToken,
          }
        }
      );
      const overview = await ovRes.json();
      if (ovRes.ok && overview.usage) usage = overview.usage;
    } catch (_) { /* fall through with empty usage */ }
  }

  renderModal(catalog, usage);
}
Multi-script-scope helpers

When a page has multiple <script> blocks (e.g. one type="module" and one classic script), helper functions don't cross scope. Modals that live in the bottom script must re-declare their own local esc(), fmtDate(), etc. — copying the implementation is acceptable here; the symptom of forgetting is a silent ReferenceError caught by the modal's try/catch and rendered as “Could not load … : esc is not defined”.

When to use this pattern
  • Modals triggered after the page has been open for a while (when GoTrueClient may have drifted)
  • Modals that need to work in incognito / multi-tab scenarios
  • Any modal where a loading hang is unrecoverable (no retry UX)
When NOT to use this pattern
  • Page-load data fetches — those should use the normal supabase client
  • Real-time subscriptions — those require the client's websocket infrastructure
  • Anything that needs RLS policies to be enforced via the client (they're enforced server-side here anyway)

Spacing System

4px base unit. Structure over decoration.

4px
8px
16px
24px
32px
40px
48px
64px
ContextValue
Card padding20px
Section spacing40px
Element gaps16px
Inline spacing8px
Container (mobile)16px
Container (tablet)24px
Container (desktop)40px

Email Templates

9 production-ready transactional emails triggered by database events. All follow the Culture Exchange Standard: table-based layout, 600px max-width, inline CSS, cross-client compatible.

Architecture
01
Platform Action

User signup, status change, agreement signed

02
Database Trigger

Supabase trigger detects change, queues email

03
Edge Function + Resend

Processes queue every 5 min, delivers via Resend API

Email Template Index
#TemplateTriggerAudience
01Brand WelcomeBrand completes onboardingBrands
02Registration ApprovedStatus → approvedCreators
03Needs More InfoStatus → needs_infoCreators
04Royalties Enabledroyalties_enabled = trueCreators
05CCA ConfirmedAgreement insertedCreators
06Under ReviewRegistration inserted (pending)Creators
07Cultural Lien FiledLien record insertedCreators
08Brand RegistrationBrand status → approvedBrands
09License ApprovedLicense status → approvedBrands
Email Design Standards
600px Max Width

Table-based layout, centered on #F5F5F5 background. All content within a single 600px container.

Inline CSS Only

No external stylesheets. Every style is inline for Gmail, Outlook, and Apple Mail compatibility.

No Emoji in Prod

Use text badges (REGISTRATION APPROVED, ACTION REQUIRED) instead of emoji. Checkmarks use "✓" character.

Green = Arrows Only

Logo CX stroke and arrow symbols (→) are the only green elements. Everything else is black/white/gray.

Email Anatomy
MADECX
Template Badge
Email Headline Goes Here
Supporting description text for the email.
Hi [First Name], body content with dynamic variables replaced by the trigger function.
Content Section
Structured content: registration details, checklists, distribution breakdowns, etc.
Accent Section
Used for "What This Unlocks", "Why Verification Matters", etc.
Call to Action
MADECX
Verify the Culture. Reinvest the Future.
Email Component Specs
ElementStyle
Outer background#F5F5F5 with 20px padding
Container600px max-width, #FFFFFF background
Header#000000 bg, 32px 40px padding, border-bottom: 3px solid #00C805
Logo in header28px, weight 900. "MADE" white, "CX" stroke 3px green
Hero section56px 40px padding, border-bottom: 1px solid #E5E5E5
Status badgeborder: 2px solid #000, 11px mono, uppercase, 0.15em spacing
Headline36px, weight 900, -0.02em, line-height 1.1
Body text15px Inter, line-height 1.8, #171717
Info boxborder: 2px solid #000, bg #FAFAFA, 32px padding
Accent boxborder-left: 3px solid #00C805, bg #FAFAFA
Warning box (needs-info)border-left: 4px solid #FF6B00, bg #FFFAF5
CTA button#000 bg, 18px 40px padding, 13px uppercase, arrow #00C805
Footer#000000 bg, border-top: 3px solid #00C805, 40px padding
Footer logo18px, "CX" stroke 2.5px green
Template Variables
Dynamic Variables
[First Name]           — User's first name
[PROPERTY_NAME]        — Creative property title
[BCID_NUMBER]          — Blackchain Creative ID
[SUBMISSION_ID]        — Registration submission ID
[CCA_ID]               — Creator Creditor Agreement ID
[LIEN_ID]              — Cultural Lien reference number
[LICENSE_ID]           — Cultural Use License number
[BRAND_NAME]           — Brand/company name
[Brand Contact Name]   — Brand contact person
[DATE_TIME]            — Formatted timestamp
[REVIEWER_NOTES_HERE]  — Admin notes for rejections
[RESUBMIT_URL]         — Link to edit submission
[SUBMISSION_URL]       — New submission link
[LEDGER_URL]           — Public ledger entry
[PROPERTY_URL]         — Product detail page
[CERTIFICATE_PDF_URL]  — Certificate download
[DASHBOARD_URL]        — Dashboard link
[PORTAL_URL]           — Creator portal link
[SHARE_IG]             — Instagram share
[SHARE_TIKTOK]         — TikTok share
[SHARE_X]              — X/Twitter share
[SHARE_LINKEDIN]       — LinkedIn share
Royalty Distribution (Email 04)
80%
Creator
16%
Custodian / Enforcement
4%
Cultural Reinvestment
Trigger Functions (Supabase)
SQL — Email Queue Table
CREATE TABLE IF NOT EXISTS email_queue (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    to_email TEXT NOT NULL,
    to_name TEXT,
    subject TEXT NOT NULL,
    html_content TEXT NOT NULL,
    email_type TEXT NOT NULL,
    user_id UUID REFERENCES auth.users(id),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    sent_at TIMESTAMPTZ,
    error_message TEXT
);
SQL — Example Trigger (Registration Approved)
CREATE OR REPLACE FUNCTION queue_registration_approved_email()
RETURNS TRIGGER AS $
BEGIN
    IF NEW.status IN ('approved', 'verified')
       AND (OLD.status IS NULL
            OR OLD.status NOT IN ('approved', 'verified'))
    THEN
        INSERT INTO email_queue (
            to_email, to_name, subject,
            html_content, email_type, user_id
        )
        SELECT
            NEW.email,
            NEW.business_name,
            'Your Creative Property Is Now Protected',
            replace(
              replace(
                replace(template_html,
                  '[First Name]', COALESCE(NEW.first_name, 'there')),
                '[PROPERTY_NAME]', NEW.item_name),
              '[BCID_NUMBER]', NEW.registration_id),
            'registration_approved',
            NEW.auth_user_id;
    END IF;
    RETURN NEW;
END;
$ LANGUAGE plpgsql;

-- Attach trigger
CREATE TRIGGER trigger_registration_approved_email
    AFTER UPDATE OF status ON registrations
    FOR EACH ROW
    EXECUTE FUNCTION queue_registration_approved_email();
SQL — All 9 Trigger Summary
-- TRIGGER MAP: Action → Function → Table
--
-- 01  Brand onboarding complete    → queue_brand_welcome_email()
--     ON users AFTER UPDATE OF onboarding_completed
--
-- 02  Registration approved        → queue_registration_approved_email()
--     ON registrations AFTER UPDATE OF status
--
-- 03  Registration needs info      → queue_registration_needs_info_email()
--     ON registrations AFTER UPDATE OF status
--
-- 04  Royalties enabled            → queue_royalties_enabled_email()
--     ON registrations AFTER UPDATE OF royalties_enabled
--
-- 05  CCA signed                   → queue_cca_confirmed_email()
--     ON creator_creditor_agreements AFTER INSERT
--
-- 06  Registration submitted       → queue_registration_under_review_email()
--     ON registrations AFTER INSERT
--
-- 07  Cultural lien filed          → queue_cultural_lien_filed_email()
--     ON cultural_liens AFTER INSERT
--
-- 08  Brand registration approved  → queue_brand_registration_confirmed_email()
--     ON registrations AFTER UPDATE OF status (user_type='brand')
--
-- 09  Brand license approved       → queue_brand_license_approved_email()
--     ON cultural_use_licenses AFTER UPDATE OF status
Queue Processing (Edge Function)
TypeScript — Supabase Edge Function
// supabase/functions/process-email-queue/index.ts
import { serve } from 'https://deno.land/std/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js';

const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY')!;
const SUPABASE_URL = Deno.env.get('SUPABASE_URL')!;
const SUPABASE_KEY = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;

serve(async () => {
  const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);

  // Fetch pending emails
  const { data: emails } = await supabase
    .from('email_queue')
    .select('*')
    .is('sent_at', null)
    .is('error_message', null)
    .order('created_at')
    .limit(10);

  if (!emails?.length) return new Response('No emails to send');

  for (const email of emails) {
    try {
      const res = await fetch('https://api.resend.com/emails', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${RESEND_API_KEY}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          from: 'MADECX <[email protected]>',
          to: email.to_email,
          subject: email.subject,
          html: email.html_content,
        }),
      });

      if (res.ok) {
        await supabase.from('email_queue')
          .update({ sent_at: new Date().toISOString() })
          .eq('id', email.id);
      } else {
        const err = await res.text();
        await supabase.from('email_queue')
          .update({ error_message: err })
          .eq('id', email.id);
      }
    } catch (e) {
      await supabase.from('email_queue')
        .update({ error_message: e.message })
        .eq('id', email.id);
    }
  }

  return new Response(`Processed ${emails.length} emails`);
});
Deployment Checklist
Upload 9 HTML templates to Supabase Storage
Run all 9 trigger SQL scripts in Supabase SQL Editor
Deploy Edge Function: supabase functions deploy process-email-queue
Set RESEND_API_KEY in Edge Function secrets
Configure cron job (every 5 min) to process queue
Test each email type with manual SQL updates
Verify rendering in Gmail, Outlook, Apple Mail

React Component Library

Production-ready TSX components for the React platform team. Copy these files into your project or download the complete package below.

Quick Start
Terminal
# 1. Copy the design-system/ folder into your project
# 2. Import tokens in your root layout:
import '@/design-system/tokens.css'

# 3. Import components:
import { Button, Card, Badge, Logo } from '@/design-system/components'
tokens.css
design-system/tokens.css
/* MADE CX Design Tokens — v3.2 */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Space+Grotesk:wght@400;500;600;700;800;900&family=IBM+Plex+Mono:wght@400;500;600;700;800&display=swap');

:root {
  --black: #000000;
  --white: #FFFFFF;
  --gray-50: #FAFAFA; --gray-100: #F5F5F5; --gray-200: #E5E5E5;
  --gray-300: #D4D4D4; --gray-400: #A3A3A3; --gray-500: #737373;
  --gray-600: #525252; --gray-700: #404040; --gray-800: #262626;
  --gray-900: #171717;
  --green: #00C805; --green-light: #00E806; --green-dark: #00A804;
  --red: #EF4444;
  --bg: var(--white); --bg-alt: var(--gray-100); --bg-tertiary: var(--gray-50);
  --text: var(--black); --text-secondary: var(--gray-600); --text-muted: var(--gray-500);
  --border: var(--gray-200); --card-bg: var(--white);
  --font-primary: 'Inter', -apple-system, sans-serif;
  --font-display: 'Space Grotesk', sans-serif;
  --font-mono: 'IBM Plex Mono', 'Monaco', monospace;
}
[data-theme="dark"] {
  --bg: var(--black); --bg-alt: var(--gray-900); --bg-tertiary: var(--gray-800);
  --text: var(--white); --text-secondary: var(--gray-400); --text-muted: var(--gray-600);
  --border: var(--gray-800); --card-bg: var(--gray-900);
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
::selection { background: var(--green); color: var(--black); }
Logo Component
design-system/components/Logo.tsx
import React from 'react';

interface LogoProps {
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

const sizes = { sm: '18px', md: '22px', lg: '40px' };
const strokes = { sm: '2px', md: '2.5px', lg: '3px' };

export const Logo: React.FC<LogoProps> = ({ size = 'md', className }) => (
  <span
    className={className}
    style={{
      display: 'flex',
      alignItems: 'baseline',
      fontSize: sizes[size],
      fontWeight: 900,
      letterSpacing: '-0.01em',
      lineHeight: 1,
      fontFamily: 'var(--font-primary)',
    }}
  >
    <span style={{ color: 'var(--text)' }}>MADE</span>
    <span
      style={{
        color: 'transparent',
        WebkitTextStroke: `${strokes[size]} var(--green)`,
        marginLeft: '8px',
      }}
    >
      CX
    </span>
  </span>
);
Button Component
design-system/components/Button.tsx
import React from 'react';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'default' | 'lg';
  showArrow?: boolean;
  children: React.ReactNode;
}

const Arrow = () => (
  <svg width="16" height="16" viewBox="0 0 24 24"
    fill="none" stroke="currentColor" strokeWidth="2"
    style={{ color: 'var(--green)', transition: 'transform 0.2s' }}>
    <path d="M5 12h14M12 5l7 7-7 7" />
  </svg>
);

const sizeStyles = {
  sm:      { padding: '8px 16px',  fontSize: '12px' },
  default: { padding: '16px 32px', fontSize: '14px' },
  lg:      { padding: '20px 40px', fontSize: '16px' },
};

const variantStyles = {
  primary:   { background: 'var(--text)', color: 'var(--bg)', borderColor: 'var(--text)' },
  secondary: { background: 'transparent', color: 'var(--text)', borderColor: 'var(--text)' },
  ghost:     { background: 'transparent', color: 'var(--text)', borderColor: 'transparent' },
};

export const Button: React.FC<ButtonProps> = ({
  variant = 'primary', size = 'default', showArrow = true,
  children, style, ...props
}) => (
  <button
    {...props}
    style={{
      display: 'inline-flex', alignItems: 'center', gap: '12px',
      fontFamily: 'var(--font-primary)', fontWeight: 800,
      textTransform: 'uppercase', letterSpacing: '0.1em',
      border: '2px solid', cursor: 'pointer', transition: 'all 0.2s',
      textDecoration: 'none',
      ...sizeStyles[size], ...variantStyles[variant], ...style,
    }}
  >
    {children}
    {showArrow && <Arrow />}
  </button>
);
ProductCard Component
design-system/components/ProductCard.tsx
import React from 'react';

interface ProductCardProps {
  image?: string;
  category: string;
  assetType: string;
  name: string;
  creator: string;
  tag?: string;
  verifiedDate?: string;
  onClick?: () => void;
}

export const ProductCard: React.FC<ProductCardProps> = ({
  image, category, assetType, name, creator, tag, verifiedDate, onClick,
}) => (
  <div onClick={onClick} style={{
    background: 'var(--card-bg)', border: '2px solid var(--border)',
    cursor: 'pointer', transition: 'border-color 0.2s',
    display: 'flex', flexDirection: 'column',
  }}>
    {/* Image */}
    <div style={{ position: 'relative', width: '100%', aspectRatio: '4/3',
      overflow: 'hidden', background: 'var(--bg-alt)' }}>
      {image
        ? <img src={image} alt={name}
            style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
        : <div style={{ width: '100%', height: '100%', display: 'flex',
            alignItems: 'center', justifyContent: 'center' }}>
            <PlaceholderIcon />
          </div>
      }
      <span style={{
        position: 'absolute', top: 16, left: 16, padding: '6px 12px',
        border: '2px solid var(--black)', fontFamily: 'var(--font-mono)',
        fontSize: '11px', fontWeight: 800, textTransform: 'uppercase',
        letterSpacing: '0.15em',
      }}>{category}</span>
    </div>

    {/* Content */}
    <div style={{ padding: 20 }}>
      <div style={{ fontFamily: 'var(--font-mono)', fontSize: '11px',
        fontWeight: 800, textTransform: 'uppercase', letterSpacing: '0.15em',
        color: 'var(--green)', marginBottom: 8 }}>{assetType}</div>
      <h3 style={{ fontFamily: 'var(--font-display)', fontSize: '20px',
        fontWeight: 900, margin: '0 0 8px', lineHeight: 1.2 }}>{name}</h3>
      <p style={{ fontSize: '14px', color: 'var(--text-muted)',
        margin: '0 0 16px' }}>{creator}</p>
      {tag && <span style={{ display: 'inline-block', padding: '6px 12px',
        border: '1px solid var(--border)', fontFamily: 'var(--font-mono)',
        fontSize: '10px', fontWeight: 700, textTransform: 'uppercase',
        letterSpacing: '0.1em', color: 'var(--text-secondary)' }}>{tag}</span>}
      {verifiedDate && (
        <div style={{ display: 'flex', justifyContent: 'space-between',
          marginTop: 20, paddingTop: 16,
          borderTop: '1px solid var(--border)' }}>
          <span style={{ fontFamily: 'var(--font-mono)', fontSize: '11px',
            fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.1em',
            color: 'var(--text-muted)' }}>{verifiedDate}</span>
          <span style={{ fontFamily: 'var(--font-mono)', fontSize: '11px',
            fontWeight: 800, textTransform: 'uppercase', letterSpacing: '0.1em',
            display: 'flex', alignItems: 'center', gap: 6 }}>
            <CheckIcon /> Asset
          </span>
        </div>
      )}
    </div>
  </div>
);

const PlaceholderIcon = () => (
  <svg width="48" height="48" viewBox="0 0 24 24" fill="none"
    stroke="currentColor" strokeWidth="1.5" style={{ opacity: 0.4 }}>
    <rect x="3" y="3" width="18" height="18" />
    <circle cx="8.5" cy="8.5" r="1.5" />
    <path d="M21 15l-5-5L5 21" />
  </svg>
);

const CheckIcon = () => (
  <svg width="12" height="12" viewBox="0 0 24 24"
    fill="var(--green)" stroke="none">
    <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
  </svg>
);
Badge Component
design-system/components/Badge.tsx
import React from 'react';

interface BadgeProps {
  variant?: 'default' | 'outline';
  children: React.ReactNode;
}

export const Badge: React.FC<BadgeProps> = ({ variant = 'default', children }) => (
  <span style={{
    display: 'inline-flex', alignItems: 'center', gap: '8px',
    padding: '8px 16px', fontFamily: 'var(--font-mono)',
    fontSize: '11px', fontWeight: 800, textTransform: 'uppercase',
    letterSpacing: '0.15em',
    background: variant === 'outline' ? 'transparent' : 'var(--bg-alt)',
    border: `2px solid ${variant === 'outline' ? 'var(--text)' : 'var(--border)'}`,
  }}>
    {children}
  </span>
);
SectionLabel Component
design-system/components/SectionLabel.tsx
import React from 'react';

interface SectionLabelProps {
  children: React.ReactNode;
  centered?: boolean;
}

export const SectionLabel: React.FC<SectionLabelProps> = ({
  children, centered = false,
}) => (
  <div style={{
    fontFamily: 'var(--font-mono)', fontSize: '11px', fontWeight: 800,
    textTransform: 'uppercase', letterSpacing: '0.15em', color: 'var(--green)',
    display: 'flex', alignItems: 'center', gap: '12px',
    justifyContent: centered ? 'center' : 'flex-start',
    marginBottom: '12px',
  }}>
    <span style={{ width: 40, height: 2, background: 'var(--green)' }} />
    {children}
    {centered &&
      <span style={{ width: 40, height: 2, background: 'var(--green)' }} />}
  </div>
);
Barrel Export
design-system/components/index.ts
export { Logo } from './Logo';
export { Button } from './Button';
export { ProductCard } from './ProductCard';
export { Badge } from './Badge';
export { SectionLabel } from './SectionLabel';
File Structure
Directory
design-system/
├── tokens.css                  # CSS custom properties + fonts
├── components/
│   ├── index.ts                # Barrel export
│   ├── Logo.tsx                # Logo wordmark
│   ├── Button.tsx              # Primary / Secondary / Ghost
│   ├── ProductCard.tsx         # Grid view product card
│   ├── Badge.tsx               # Default / Outline badges
│   └── SectionLabel.tsx        # Green accent label
└── assets/
    ├── madecx-logo-light.svg   # Logo on white
    ├── madecx-logo-dark.svg    # Logo on black
    ├── [email protected]
    └── [email protected]
Usage Example
App.tsx — Example
import '@/design-system/tokens.css';
import { Logo, Button, ProductCard, Badge, SectionLabel } from '@/design-system/components';

export default function RegistryPage() {
  return (
    <main>
      <header style={{ display: 'flex', justifyContent: 'space-between',
        padding: '0 40px', height: 72, alignItems: 'center',
        borderBottom: '2px solid var(--border)' }}>
        <Logo size="md" />
        <Button variant="primary" size="default">Register Asset</Button>
      </header>

      <section style={{ padding: '80px 40px', maxWidth: 1200, margin: '0 auto' }}>
        <SectionLabel>Cultural Registry</SectionLabel>
        <h2 style={{ fontFamily: 'var(--font-display)', fontSize: 48,
          fontWeight: 900, letterSpacing: '-0.02em' }}>
          Browse Assets
        </h2>

        <div style={{ display: 'flex', gap: 12, margin: '32px 0' }}>
          <Badge>All</Badge>
          <Badge variant="outline">Music</Badge>
          <Badge variant="outline">Fashion</Badge>
        </div>

        <div style={{ display: 'grid',
          gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))',
          gap: 24 }}>
          <ProductCard
            category="Product"
            assetType="Product"
            name="When I Get Home"
            creator="Solange Knowles"
            tag="Uncategorized"
            verifiedDate="Verified Jan 2026"
          />
          <ProductCard
            category="Service"
            assetType="Service"
            name="Beat Production Pack"
            creator="ATL Sound Labs"
            tag="Music"
            verifiedDate="Verified Dec 2025"
          />
        </div>
      </section>
    </main>
  );
}

Cultural Valuation Math

The mathematical layer underneath the Culture Market Grid, the registry detail page, and the eventual ticker pricing surfaces. Every cultural signal carries a CPRS score (0–100), a tier classification (1–5), a TCPMV dollar value, and a confidence rating (0.0–1.0). This section documents the formulas, conventions, and visual primitives used to render these values. Auditability is the goal: anyone reading a score on a card should be able to trace it back to the formula here.

Canonical Reference
/culture-market-data.html

The CPRS scoring functions (cprsScore, cprsTier, dimClass) live in this file. Upstream, the data pipeline writes d1_origin through d5_return, tcpmv_numeric, and confidence to the source rows; the frontend reads them. Any change to the formula happens in the pipeline + this file together.

CPRS Score — Cultural Provenance & Reach (0–100)

Five-dimensional composite. Each dimension is independently scored 0–100 by upstream signal classifiers; the displayed CPRS is the mean of populated dimensions. The five-dim shape is intentional — it lets a viewer read a score's fingerprint at a glance, not just its magnitude.

DimNameWhat it measures
d1OriginProvenance — where did this culture come from, who originated it, is the lineage clear and uncontested
d2CommercialCommercial viability and licensing potential — addressable market, brand fit, monetization paths
d3ReachGeographic + demographic spread — how far has this cultural footprint already extended
d4VelocityRate of change — is attention rising, stable, or fading; trajectory over the trailing window
d5ReturnHistorical return on cultural exploitation — revenue per unit of attention captured to date
JavaScript — CPRS Score
function cprsScore(dims) {
  // dims is [d1, d2, d3, d4, d5] — values 0..100, or 0 for "not yet scored"
  const valid = dims.filter(d => d > 0);
  if (valid.length === 0) return 0;
  return parseFloat((valid.reduce((s, d) => s + d, 0) / valid.length).toFixed(1));
}

Why mean-of-valid rather than weighted sum: early signals often have only 2–3 dimensions populated. Averaging only the populated dimensions lets a partial-data signal still produce a meaningful score without zero-padding distortion. As more dimensions fill in, the score self-stabilizes. A weighted formula was rejected because dimension importance varies by signal type — a transaction signal weights d2 (commercial) heavily; a heritage piece weights d1 (origin). Tier-1 thresholds work better against the mean.

Tier Classification

CPRS scores collapse to tier labels for plain-language surfacing. Tiers drive UI affordances (Tier-1 unlocks priority listings, Tier-4 routes to the early-stage feed, Tier-5 is hidden from buyer-facing surfaces).

CPRS ScoreTierLabel
≥ 801Premium Cultural Asset
≥ 602Commercial Cultural Asset
≥ 403Emerging Cultural Asset
≥ 204Early-Stage Cultural Signal
< 205Unscored
JavaScript — Tier Classification
function cprsTier(score) {
  if (score >= 80) return 'TIER 1 PREMIUM CULTURAL ASSET';
  if (score >= 60) return 'TIER 2 COMMERCIAL CULTURAL ASSET';
  if (score >= 40) return 'TIER 3 EMERGING CULTURAL ASSET';
  if (score >= 20) return 'TIER 4 EARLY-STAGE CULTURAL SIGNAL';
  return 'TIER 5 UNSCORED';
}
TCPMV — Total Cultural Property Market Value

The dollar value of a cultural property when fully exploited across primary + secondary markets. Surfaces as the headline number on registry detail pages, the green stat on registered registry cards, and the running tally in the Culture Market Grid stat bar. The actual computation lives upstream (data pipeline / valuation models); the frontend renders the stored result. This section documents only the render conventions.

PropertyConvention
Storage columntcpmv_numeric — bigint, stored in cents (USD)
Display columntcpmv_display — text, pre-formatted for surface use; null when unscored
Null stateRender literal "VALUATION PENDING" in var(--text-muted) at the same type scale as the dollar value
Format breakpoints<$1,000: $847.50 · <$1M: $45,200 · <$1B: $2.4M · ≥$1B: $1.8B (one decimal at M/B suffixes)
Color ruleRegistered + verified: var(--green). Registered but unverified: var(--text). Unregistered: var(--text-muted) + outline-only container
Type scaleDetail page: Space Grotesk 36px / 900. Registry card: 24px / 900. Grid cell: 16px / 800. Inline mention: same size as surrounding body
Confidence (0.0–1.0)

Reliability of the CPRS score itself, not the underlying signal. A 0.45 confidence on an 82 score means "we're computing 82 from limited data — treat with care." Surfaces as a small mono indicator next to the score on list/detail surfaces; hidden on space-constrained grid cards.

ConfidenceTreatment
≥ 0.85No decoration — score renders normally
0.60–0.84Mono 0.74 next to score in var(--text-muted), no warning
< 0.60Score grayed to var(--text-muted); small warning glyph; "LOW CONFIDENCE" label on detail surfaces
Tier-5 / unscoredConfidence not displayed at all
5-Dim Bar — Visual Fingerprint Primitive

Five thin segments rendered side-by-side, each filled to a percentage matching its dimension's score. Becomes a visual fingerprint — at-a-glance you can tell whether a signal is "high-origin, low-velocity" (heritage piece), "low-origin, high-velocity" (viral moment without lineage), or fully balanced (Tier-1 candidate).

Permitted green usage: the dim bar fill uses var(--green) for high-tier dimensions. This is an explicit exception to the "green is functional only" principle (section 03) — the dim bar is a verified-status visualization (per-dimension score validity), which the principle permits. Future contributors should not "fix" this back to neutral colors.

Tier-1 Premium · CPRS 87.4
Solange Knowles × Nike + Jacquemus
ORIGIN · COMM · REACH · VELOCITY · RETURN
Tier-2 Commercial · CPRS 65.0 · viral fingerprint
Metro Boomin surprise beat tape
ORIGIN · COMM · REACH · VELOCITY · RETURN
Tier-4 Early-Stage · CPRS 32.0 · low confidence 0.51
Emerging fashion designer · TBD
ORIGIN · COMM · REACH · VELOCITY · RETURN · two dims unpopulated
5-Dim Bar Specs
ElementStyle
Container widthFixed 200px on detail surfaces; flex: 1 in list-view rows
Bar height2px (compact rows) or 4px (detail). Never above 6px.
Trackbackground: var(--bg-alt)
Fill (high, ≥60)background: var(--green) — permitted exception per principle 03
Fill (mid, 30–59)background: var(--text)
Fill (low, <30)background: var(--text-muted)
Fill (zero / unpopulated)Empty track — no fill at all
Gap between segments2px (compact) or 3px (detail)
CSS — Compact list-view variant
.list-dims { display: flex; gap: 2px; margin-top: 3px; }
.list-dims .list-dim { flex: 1; height: 2px; background: var(--bg-alt); position: relative; }
.list-dims .list-dim-fill { position: absolute; top: 0; left: 0; height: 100%; }
.list-dim-fill.high { background: var(--green); }    /* ≥ 60 */
.list-dim-fill.mid  { background: var(--text); }     /* 30..59 */
.list-dim-fill.low  { background: var(--text-muted); } /* < 30 */
JavaScript — Dim class helper
function dimClass(val) {
  if (val >= 60) return 'high';
  if (val >= 30) return 'mid';
  return 'low';
}
Teaching Module · Instructional Design

From Cultural Signal to Commercial Value

CPRS, TCPMV, and dim bars aren't decorative — they're the literal pricing engine for cultural property licensing. This module walks through how a piece of culture becomes a commercial asset with a defensible price tag, in five teaching units. Read in order; each unit builds on the last.

A. The Pipeline
B. Worked Example
C. Tier → Price
D. Fingerprints
E. Confidence
A · The Valuation Pipeline

Five stages turn a raw cultural signal into a commercial license price. Each stage is computed by a different layer of the platform and persisted to the source row. By the time the score reaches a buyer's screen, every value upstream is auditable.

Stage 1
Signal
Streams, citations, social mentions, sample usage, geo-pings — captured by the data pipeline
Stage 2
5 Dims
Classifiers score Origin, Commercial, Reach, Velocity, Return on 0–100 each
Stage 3
CPRS
Mean of populated dims → single 0–100 composite. cprsScore()
Stage 4
Tier
CPRS bucketed into 1–5. Determines license access band. cprsTier()
Stage 5
$ Price
TCPMV stored in cents → tier-banded license price the buyer sees on screen
Each stage's output is persisted; the license price you click on is reproducible from raw signals at any time
B · Worked Example — "When I Get Home" by Solange Knowles

Walking the same album through every stage. This is the canonical instructional asset — used everywhere in the DS to demonstrate the math. The numbers below are illustrative but typed to a real-feeling cultural property at the upper edge of Tier 1.

Asset
When I Get Home
Creator
Solange Knowles
Class
Music · Album
Tier
1 · Premium
Step 1 · Score each dimension (0–100)
d1 · Origin
88
Verified IP, single creator, clean chain of custody from Saint Records
d2 · Commercial
82
Strong sync history, brand fits ranging luxury → indie editorial
d3 · Reach
79
Global streaming, critical canon entries, museum + academic citations
d4 · Velocity
71
Steady catalog appreciation, not a viral spike — durable not trending
d5 · Return
76
Strong unit economics — historical $/attention ratio in the top quartile
Step 2 · Aggregate to CPRS
cprsScore([88, 82, 79, 71, 76]) = mean(88, 82, 79, 71, 76)
79.2
CPRS
→ rounds to 79 displayed
Step 3 · Classify into a tier
cprsTier(79) → score is in the 60–79 band → returns Tier 2 · Commercial
Note: a CPRS of 79.2 sits at the upper edge of Tier 2; one more dim point pushes it to Tier 1. The tier boundary is a hard threshold — no half-tiers.
Step 4 · TCPMV + license price band
TCPMV
$2.4M
Total Cultural Property Market Value (stored as 240,000,000¢)
Tier 2 license band
$10K–$50K
Commercial license price range — exact quote depends on use, term, exclusivity
C · Tier → Commercial Value Translation

What each tier actually means in commercial terms. The tier number is the buyer's first read on access, pricing, and exclusivity. The TCPMV is the upstream economic estimate; the license band is the typical buyer-facing price range. Both are anchored to the CPRS score, not invented per-deal.

TierCPRSLabelTypical TCPMVLicense bandCommercial implication
1≥ 80Premium$5M+$50K+Heritage assets. Exclusive deals, multi-year terms, brand-defining placements. Buyer almost always negotiated.
260–79Commercial$1M–$5M$10K–$50KEstablished cultural property with proven commercial fit. Standard licensing, defined-use terms.
340–59Emerging$200K–$1M$2K–$10KGrowing relevance, narrower market fit. Self-serve licensing common. Often Tier-3 → Tier-2 within 18 months.
420–39Early-Stage$10K–$200K$500–$2KPre-commercial or niche-only. Available at low price but commercial signal still developing.
5< 20UnscoredQuote on requestInsufficient data to price. May be brand-new entries or assets with unresolved provenance. Must be reviewed by ops before licensing.

For LLMs and devs: when surfacing a license price on screen, never compute it client-side from CPRS. Read the tier band from tcpmv_numeric + tier on the source row. The mapping above is illustrative — the real bands live in /culture-market-data.html and may shift as the market matures.

D · Fingerprint Reading — Shape Tells a Story Score Doesn't

Two assets with the same CPRS can have wildly different commercial profiles. The 5-dim fingerprint reveals what kind of asset you're looking at — not just how good it scores. A balanced asset and a spike-shaped asset behave differently in licensing, even at the same tier. This is why the dim bars exist at all: the magnitude alone is incomplete.

Solange · "When I Get Home"
79
Balanced fingerprint · Tier 2
d1 Origin
88
d2 Comm.
82
d3 Reach
79
d4 Vel.
71
d5 Return
76
Reads as: heritage asset with proven track record across all five axes. Buyer can license confidently; price is anchored. This is a buy-and-hold cultural property.
Hypothetical · Viral Track
62
Spike fingerprint · Tier 2
d1 Origin
42
d2 Comm.
68
d3 Reach
55
d4 Vel.
96
d5 Return
48
Reads as: a moment, not a heritage asset. d4 spike (96) drives the score; d1 origin is shaky (42). Price now, but exclusivity windows should be short — the velocity is mean-reverting.

Same tier, very different commercial conversation. The CPRS score tells the buyer "what level," the fingerprint tells them "what kind." Designs that hide the fingerprint behind a single number flatten this — every surface that shows CPRS should show the dim bars too, or link to them.

E · Confidence as Due-Diligence Signal

Confidence is independent of CPRS magnitude. A score of 79 with confidence 0.40 is not the same product as a score of 79 with confidence 0.92, even though both render the same number. Confidence answers "how much classifier evidence underpins this score?" — and it directly affects how aggressively a buyer should act on the price.

Confidence ≥ 0.85
79
High confidence
Render the score plainly. Score is reliable. Buyer can transact at the listed band without further due diligence.
Confidence 0.60–0.84
79
·67
Mid confidence — annotated
Score plus a mono confidence suffix. Provisional. Buyer should review the supporting signals tab before high-value commitments.
Confidence < 0.60
79
Low confidence
Score grayed, explicit "LOW CONFIDENCE" badge. Treat as a placeholder. Pricing must be quote-on-request — never auto-honor the band.

The principle: a score is information; a score with confidence is information with epistemics. Designs that surface CPRS without rendering confidence treatment are quietly lying to the buyer about the score's reliability — and exposing the platform to mispriced licensing.

Module Recap · What you should now be able to do
  • Trace any displayed CPRS score back through the 5 dims, the mean-of-valid aggregation, and the tier threshold
  • Read a fingerprint shape and tell a heritage asset apart from a viral spike, even at the same CPRS magnitude
  • Translate a tier number into a commercial conversation — pricing band, license type, exclusivity expectation
  • Recognize when a confidence rating demotes a score from "transactable" to "needs human review"
  • Render any of these primitives correctly per the visual specs above (5-dim bars, confidence treatments, tier badges, TCPMV formatting)
Where these values surface
  • Culture Market Grid (section 17, forthcoming) — every cell shows CPRS score, dim bars, TCPMV, confidence, tier badge
  • Registry detail page (section 09) — TCPMV is the headline number; CPRS + dim bars on the right column
  • Registry cards (section 07) — TCPMV in the verified row when present; full CPRS hidden by default
  • Personal Valuation Card (section 10) — eventual surface for creator-level CPRS rollups (post-ticker launch)
  • Ticker pages (section 18, forthcoming) — per-creator CPRS aggregate computed across all owned/licensed registrations
  • Admin signals dashboard — full dim breakdown + raw classifier outputs for ops review

Design Tokens

Complete CSS custom properties. Copy into any project to implement the MADE CX design system.

CSS Variables — Consolidated v3.2
:root {
    /* ── Primary Colors ── */
    --black: #000000;
    --white: #FFFFFF;

    /* ── Gray Scale ── */
    --gray-50:  #FAFAFA;
    --gray-100: #F5F5F5;
    --gray-200: #E5E5E5;
    --gray-300: #D4D4D4;
    --gray-400: #A3A3A3;
    --gray-500: #737373;
    --gray-600: #525252;
    --gray-700: #404040;
    --gray-800: #262626;
    --gray-900: #171717;

    /* ── Accent — FUNCTIONAL ONLY ── */
    --green: #00C805;
    --green-light: #00E806;
    --green-dark: #00A804;
    --red: #EF4444;

    /* ── Theme (Light) ── */
    --bg: var(--white);
    --bg-alt: var(--gray-100);
    --bg-tertiary: var(--gray-50);
    --text: var(--black);
    --text-secondary: var(--gray-600);
    --text-muted: var(--gray-500);
    --border: var(--gray-200);
    --card-bg: var(--white);

    /* ── Typography ── */
    --font-primary: 'Inter', -apple-system, sans-serif;
    --font-display: 'Space Grotesk', sans-serif;
    --font-mono: 'IBM Plex Mono', 'Monaco', monospace;

    /* ── Spacing (4px base) ── */
    --space-1: 4px;   --space-2: 8px;
    --space-3: 12px;  --space-4: 16px;
    --space-6: 24px;  --space-8: 32px;
    --space-10: 40px; --space-12: 48px;
    --space-16: 64px; --space-20: 80px;

    /* ── Borders — Always 2px, sharp ── */
    --border-width: 2px;
    --border-color: var(--gray-200);
}

/* ── Dark Mode ── */
[data-theme="dark"] {
    --bg: var(--black);
    --bg-alt: var(--gray-900);
    --bg-tertiary: var(--gray-800);
    --text: var(--white);
    --text-secondary: var(--gray-400);
    --text-muted: var(--gray-600);
    --border: var(--gray-800);
    --card-bg: var(--gray-900);
    --border-color: var(--gray-800);
}