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.
“A design language for human decency — with respect for heritage and fiscal responsibility for the lineage of original peoples.”
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.
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.
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.
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.
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.
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.
Two-tier sticky nav, MARKET / REGISTRY / LEDGER tabs with text-color active state, theme persistence under shared 'theme' key.
MADE CX Wallet (live Stripe Connect balance), Personal Valuation Card visual at ISO 7810 ID-1 ratio, funding-speed picker, connect-method tiles.
CPRS scoring (5-dim mean-of-valid), tier classification (1–5), TCPMV render conventions, confidence thresholds, the dim-bar fingerprint primitive.
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.
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.
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.
No rounded corners, no blur effects. All edges are crisp and defined. border-radius: 0 everywhere.
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.
Clear labels, not emoji decorations. Every element is labeled with text. No emoji in production UI.
Pure black/white with strategic accent color. No gradients anywhere. Solid colors only.
2px borders define space, not shadows. Consistent border treatment. No drop shadows.
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.
| Don't | Do Instead |
|---|---|
| Use rounded corners (border-radius) | Keep all edges sharp (0px radius) |
| Fill backgrounds with green | Use green only for arrows and accents |
| Add drop shadows | Use 2px borders consistently |
| Use emoji in production UI | Use text labels or simple SVG icons |
| Use gradients anywhere | Use solid colors only |
| Use 1px borders | Use 2px borders for all structural elements |
| Add blur or glassmorphism effects | Keep contrast high and crisp |
Brand & Logo
The MADE CX wordmark: solid "MADE" represents established culture, outlined "CX" in green represents exchange and future potential.
| Property | Value | CSS |
|---|---|---|
| Font | Inter, 900 | font-weight: 900 |
| "MADE" Color (Light) | #000000 | color: var(--text) |
| "MADE" Color (Dark) | #FFFFFF | color: var(--text) |
| "CX" Fill | Transparent | color: transparent |
| "CX" Stroke | #00C805, 2.5px | -webkit-text-stroke: 2.5px var(--green) |
| Letter Spacing | -0.01em | letter-spacing: -0.01em |
| Gap | 8px | margin-left: 8px |
.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.
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.
| Forbidden | Use 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-elements | JS-injected SVG into the container |
Emoji prefixes in console.log & debug output | Bracketed prefix: [init] / [auth] / [error] |
Standard typographic characters are not pictographic icons and are encouraged where they improve readability:
| Character | Name | Use For |
|---|---|---|
| — | Em-dash | Date ranges (Submitted Jan 1 — Approved Jan 3), eyebrow separators, parenthetical phrases |
| – | En-dash | Numeric ranges (8–12 minutes, $50K–$100K) |
| · | Middle dot | Inline separators (Tier 2 · Verified Acquirer) |
| … | Horizontal ellipsis | Continuation, truncation, loading state |
| “ ” ‘ ’ | Smart quotes | Quotations, emphasis (never use straight quotes for prose) |
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 |
<!-- 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.
| Correct Usage | Incorrect Usage |
|---|---|
| Logo "CX" outline stroke | Button fills or backgrounds |
| Arrow symbols in buttons | Badge or card backgrounds |
| Success/verified checkmarks | Text 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 |
| Token | Light | Dark | Usage |
|---|---|---|---|
--bg | #FFFFFF | #000000 | Page background |
--bg-alt | #F5F5F5 | #171717 | Section/card backgrounds |
--bg-tertiary | #FAFAFA | #262626 | Nested backgrounds |
--text | #000000 | #FFFFFF | Primary text, buttons |
--text-secondary | #525252 | #A3A3A3 | Body text |
--text-muted | #737373 | #525252 | Captions, labels |
--border | #E5E5E5 | #262626 | All 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.
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.
| Property | Value |
|---|---|
| Border width | 2px (all variants) |
| Font weight | 800 |
| Text transform | uppercase |
| Letter spacing | 0.1em |
| Arrow icon color | var(--green) — only green allowed on buttons (activation indicator) |
| Arrow hover | translateX(4px) with 200ms transition — the button leans toward action |
| Primary bg | var(--text) — black/white per theme |
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.
| State | Visual change | Timing | Why |
|---|---|---|---|
| Default | Base treatment per variant. Arrow at translateX(0) | — | Resting state — discoverable, not loud |
| Hover | Primary: bg inverts (text→bg, bg→text). Ghost: border-color → var(--text). Arrow translates +4px on the X-axis | 200ms 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 |
| Disabled | opacity: 0.4, cursor: not-allowed, all hover/active transitions suppressed | — | Reads 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 |
.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); }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.
| State | Background | Border | Text color |
|---|---|---|---|
| Default (unselected) | var(--bg) | 2px solid var(--border) | var(--text) |
| Hover | var(--bg) | 2px solid var(--border-hover) | var(--text) |
| Selected (active) | var(--text) | 2px solid var(--text) | var(--bg) |
| Disabled | var(--bg-alt) | 2px solid var(--border) | var(--text-muted) |
- 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-pressedon 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.
A tiny circular badge for unread counts on header icons (messages, notifications). Green background, white number. Hidden when count is 0.
| Element | Spec |
|---|---|
| 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 container | 1px solid var(--text) (default) or semantic color, padding 4px 10px, sharp corners |
| Pill text | Mono 9px / 800 / 0.12em uppercase |
| Badge count | min-width 16px, height 16px, border-radius:50%, mono 9px / 800, green background |
- 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.
| Element | Spec |
|---|---|
| Container | Padding 12px 14px 12px 18px, border:2px solid var(--border), sharp corners |
| Background | var(--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 text | Inter 13px / 1.5 / var(--text) |
- No green left bar by default. Reserve green for the explicit
.successvariant. - 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.
Image Standards
Professional photography with clear subjects. No text overlays. High contrast for white backgrounds.
| Property | Value |
|---|---|
| Max file size | 2MB |
| Object fit | object-fit: cover |
| Object position | object-position: center |
| Border on images | none |
| Resolution | 2x for retina |
Images must work on white backgrounds
Main subject clearly identifiable
Text separate from image layer
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.
When I Get Home
Solange Knowles
Beat Production Pack
ATL Sound Labs
| Element | Style |
|---|---|
| Card border | 2px solid var(--border) |
| Hover | border-color: var(--black) |
| Image ratio | aspect-ratio: 4/3 |
| Category badge | Top-left 16px, 2px solid #000, transparent bg |
| Asset stamp | Bottom-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 type | Green #00C805 (only green on card) |
| Content padding | 20px |
List View & Financial Accordion
Table-style layout with expandable financial charts. Click chevron to reveal price history, projections, and stats.
Beat Production Pack
ATL Sound Labs
| Element | Style |
|---|---|
| Chart height | 180px, border: 2px solid var(--border) |
| SVG viewBox | 0 0 700 156 |
| Price line (positive) | stroke: #00C805, width: 2.5 |
| Price line (negative) | stroke: #EF4444 |
| Projection line | stroke: #737373, dasharray: 4,3 |
| Volume bars | opacity: 0.3, green/red fill |
| TODAY label | Green 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.
| Dimension | Product Detail (Public) | IP Asset Detail (Trading) |
|---|---|---|
| Audience | Casual visitors, supporters, museum-goers, anyone discovering culture | Verified buyers, institutional licensees, secondary-market traders |
| Primary intent | Encounter / appreciate / consider supporting | Evaluate / price / transact |
| Composition | Tile-based, asymmetric, with masonry "Museum Exhibition" section | Two-column grid: 1.5fr main / 1fr sticky side |
| Tone | Exhibition floor — dignified, slow-read, generous whitespace | Trading desk — dense, scannable, sticky controls |
| CTAs | Support This Creator (donation, heart icon) + License This Property (document icon) | License Tier picker (Primary) · Buy IP Outright · Trade Certificates · Options · Loans |
| Modal flow | Donation Modal — 3 steps: Amount → Payment Method → Details | License Purchase Modal — 3 steps: Order Form → Processing → Success + Ledger link |
| Stats / chart | Lightweight stats container — "this exists, here's its scope" | Full stats grid (Price, Cap, Volume, ATH) + 90-day projection chart |
| Financial instruments | None — donation goes 100% to creator | Fidelity options chain · Charles Schwab IP-backed loans |
| Auth gate | Public — viewable by anyone in non-auth mode. Same auth posture as the Registry. License button click → Section 19 verification flow | Verified buyers only — page-level gate. requireVerifiedBuyer() fires on page load. Anonymous and unverified-buyer sessions redirect to verification before the page renders |
| Reads from | cultural_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."
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.
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.
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.
| Card | Label | Width | Body |
|---|---|---|---|
| Description | About | Wide (2 cols) | Title + full-paragraph rich text describing the cultural property |
| Cultural Information | Heritage | Standard | Country of Origin · Cultural Significance · Context paragraph |
| Business Information | Creator | Standard | Business Name · Website · Location (full-width row) |
| Ethical Commitments | Standards | Standard | Feature list with green checkmark per item: Fair Trade Practices, etc. |
| Blockchain Verification | Provenance | Wide (2 cols) | Registration ID + chain hash + verification timestamp |
| Metadata | Technical | Standard | File format · Resolution · Tags · Indexed date |
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).
Preset tiles ($5, $10, $25, $100) + custom input. Active tile inverts to text-bg.
Stripe Card · Apple Pay · Google Pay. Tiles populated by JS based on browser capability.
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.
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.
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.
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.
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.
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).
| Section | Fields |
|---|---|
| Creator Information | Creator Name · Creator Email · Company · Industry |
| Cultural Context | Origin · Language · Genre/Style · Target Audience · Cultural Significance · Historical Period |
| Registration Details | Registration Type · Registration ID · Blockchain ID · Status · Registered Date · Item Category · Item Format · Usage Rights · Territory · License Duration · Exclusivity · Derivative Works · Attribution Required |
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.
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.
Purchase usage rights directly from creator. Creator retains 100% IP ownership. Creator can sell many licenses.
Creator sells complete ownership. Full copyright transfer. One-time transaction.
Trade license certificates between holders. Market-driven pricing. Creator earns royalties on every trade.
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().
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.
Use case, term length, exclusivity selection, payment method (Stripe). Validate before enabling Continue.
Stripe payment intent confirmed. Width-preserving spinner. No layout shift. Cancel still available.
Receipt + license certificate ID + ledger link. Single forward action: View in Ledger.
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.
| Instrument | Partner | Eligibility | Mechanic |
|---|---|---|---|
| Options chain | Fidelity | Approved options agreement, min balance | Calls + puts on registered assets with sufficient market depth. Settles in physical license certificates |
| IP-backed loan | Charles Schwab | KYC verified, ≥$10K portfolio | Pledged Asset Line, revolving credit, LTV ≤ 50%, interest on drawn amounts only. 3–5 day disbursement |
| Element | Spec |
|---|---|
| Top header | Inherits 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 grid | display: grid; grid-template-columns: 1.5fr 1fr; gap: 32px at desktop. Stacks at ≤960px |
| Side column sticky | position: sticky; top: 96px — license picker stays visible while user scrolls main column |
| Image watermark | CX outline, position: absolute; top: 16px; right: 16px, opacity 0.4. Same primitive as card stamp (Section 08) |
| Section toggles | toggleSection(id) — rotates chevron 180° via class, no animation library required |
| Stat values | color: var(--text). Change indicators may carry green for positive direction |
| Auth gating | Page-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 |
| Modals | License purchase modal + white-glove modal harness (wgAlert, wgConfirm) — see Section 12 modal patterns. Native dialogs banned |
| Message drawer | Right-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.
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.
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.
| Element | Style |
|---|---|
| Container | 2px solid var(--border), padding 24px, no border-radius |
| Wallet label | Inter 900 16px, "MADE" + green-stroke "CX" + "Wallet" suffix at weight 600 |
| Status pill | 1px green border, 9px mono uppercase 0.12em, dot 6×6 green |
| Balance amount | Space Grotesk 32px / 900 / line-height 1 |
| Pending amount | Same scale as Available, color var(--text-secondary) |
| Balance label | Mono 10px / 700 / 0.12em uppercase / var(--text-muted) |
| Next-payout strip | Background var(--bg-alt), mono 11px, clock icon 14×14 |
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.
| Element | Style |
|---|---|
| Aspect ratio | 1.586 / 1 (ISO/IEC 7810 ID-1 credit-card) |
| Background | Two 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 |
| Border | 2px 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 brackets | 14×14, 2px rgba(255,255,255,0.6), absolute-positioned at 6px from each corner |
| Status bar | Top edge, mono 8px / 700 / 0.18em uppercase, 6×6 green dot (activation indicator), 1px rgba(255,255,255,0.15) hairline divider |
| EMV chip | 36×28 gold gradient (#C9A66B → #A8884A → #8C6E36) |
| Contactless icon | Three concentric arcs, stroke rgba(255,255,255,0.85) |
| Ticker symbol | Space Grotesk 34px / 900 / line-height 1, "$" prefix at 0.4 alpha |
| Card number (PAN) | Mono 13px / 0.18em, masked dots + last-4 |
| Cardholder | Mono 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 |
| Padding | 30px 28px 22px |
| Max width | 480px (responsive width 100%) |
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).
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.
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.
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.
| Selector | Property | Required Value |
|---|---|---|
| .header | background | var(--bg-primary) or var(--bg) |
| .header | border-bottom | 2px solid var(--border-color) or var(--border) |
| .logo-made | color | var(--text-primary) or var(--text) |
| .nav-link, .nav a | color | var(--text-primary) — never white |
| .auth-btn (filled) | background & color | background: var(--text-primary); color: var(--bg-primary) (inverse of bg) |
| .auth-btn (outline) | border & color | border: 2px solid var(--border-color); color: var(--text-primary) |
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.
| Class | Element | Notes |
|---|---|---|
.header-icon-wrapper | Container around bell + dropdown | position: relative |
.header-icon-btn | Bell button | 40×40, 2px outline border |
.notification-badge | Unread count | 18px, mono font, bg=text/text-on-bg inverse, top-right corner |
.dropdown-panel | Dropdown container | 360px wide, 480px max-height, hidden by default; .active shows it |
.dropdown-header | Title row | Holds .dropdown-title + .dropdown-action (e.g., "Clear all") |
.dropdown-list | Scrollable item list | 360px max-height, overflow-y auto |
.dropdown-item | Single notification | 40px avatar (SVG icon) + content (title row + 2-line message). Add .unread for unread state |
.dropdown-item-avatar | Icon container | 40×40 with bg-secondary, holds an inline SVG; SVG color is var(--text-muted) |
.dropdown-footer | Bottom action row | Holds .dropdown-footer-link ("View All") |
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_type | Use For |
|---|---|
license | License issuance, expiry |
trade | Marketplace trades |
payment | Payment events |
approval | Verification approval, account-level approvals |
revision_request | Reviewer needs changes / additional documents |
system | Generic system messages, status changes |
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.
| Property | Value |
|---|---|
| position | fixed |
| top / left (closed) | 0 / -100% |
| left (open) | 0 (set via .open class) |
| max-width | 400px |
| height | 100vh |
| border-right | 2px solid var(--border) |
| transition | left 0.4s cubic-bezier(0.4, 0, 0.2, 1) |
| drawer-header | Sticky logo + close, position: sticky; top: 0 |
| drawer-body | 24px 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.
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:
| Outcome | Action |
|---|---|
'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. |
null | No prior state. Render form fresh. |
| Status | URL Has ?upgrade=N | Action |
|---|---|---|
| (no row) | any | Show form fresh |
draft | any | Restore draft into form, jump to last step |
expired | any | Show form (renewal flow) |
submitted · under_review · pending_documents | no | Redirect to review page |
verified · rejected · suspended | no | Redirect to review page |
| any in-progress | yes | Allow form (legitimate upgrade flow) |
Three rules to prevent user confusion when redirected:
- Hide the form during the check. Set
visibility: hiddenon 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.
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.
| Element | Spec |
|---|---|
| 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. |
| Avatar | 36×36 square. 2px border, NO border-radius (border-radius: 0). Mono font, 13px/700, uppercase initial. Border color matches the card's accent stripe. |
| Card | max-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 header | flex, 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. |
| Body | padding 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. |
| Element | Class | Style |
|---|---|---|
| Sigil | .message-prompt-sigil | Literal $ character. Font-weight 700. Color: green for own/admin, text-secondary for incoming/user. |
| Hostname | .message-prompt-host or .message-author-label | text-primary, font-weight 600. Content rules below. |
| Separator | .message-prompt-sep | Literal · middle-dot. text-secondary at opacity 0.6. |
| Timestamp | .message-prompt-time or .message-time | text-secondary, font-weight 500, font-feature-settings: 'tnum' for tabular numerals. Margin-left auto pushes it to the far edge. |
| Field | Own / Admin | Incoming / User |
|---|---|---|
| Hostname | cx-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 initial | M (MADE) | First letter of sender_name, then first letter of sender_email, then U. Always uppercased. |
| Timestamp format | YYYY-MM-DD HH:MM:SS (24h, zero-padded). Use the included formatLogTimestamp(date) helper or equivalent. | |
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.
<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>
- Round avatars. The 50% border-radius circle is the chat-app default and breaks the terminal aesthetic. Always square.
- Rounded bubble corners. No
border-radiuson 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-ishYYYY-MM-DD HH:MM:SSfor 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.
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.
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.
| State | Border | Fill | Button label | Notes |
|---|---|---|---|---|
| Default | 2px 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) |
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.
| Spec | Value |
|---|---|
| Padding | 9px 16px |
| Font | Inter 800 / 11px / 0.10em uppercase |
| Border | 2px solid var(--text) (no green) |
| Default | bg var(--bg), color var(--text) |
| Hover | bg var(--text), color var(--bg) (inversion) |
| Class | .allowance-bar-action |
| Handler | onclick="openPricingModal()" |
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
×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.
Enables off-session charges when credits hit zero. Two states:
- No payment method: Shows a single button —
LINK MY SUBSCRIPTION CARD. Callsbilling?action=link-subscription-cardwhich self-heals: looks up the user's Stripe customer (searching by email ifstripe_customer_idcolumn is NULL), grabs the default payment method, saves it tocreator_payment_methodviasave_payment_methodRPC. - 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 Top-Up
Selected option uses inversion (filled var(--text) bg, var(--bg) text) — NOT green tint.
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:
| Step | Lookup | On miss |
|---|---|---|
| 1 | user_profiles.stripe_customer_id | Fall through to 1b |
| 1b | Search Stripe by auth.users.email | Return 400 no_subscription |
| 2 | customer.default_payment_method | Fall through to 3 |
| 3 | subscription.default_payment_method | Fall through to 4 |
| 4 | List PMs on customer, pick first card | Return 400 no_payment_method |
| 5 | Save to creator_payment_method | Write 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.
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).
<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>
.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; }
}
Notice: Tile A (120px) and Tile B (160px) have different heights. The columns pack independently — no awkward whitespace.
- 2×2 layouts where tile heights naturally differ
- Account/settings pages with grouped controls
- Dashboards with paired data widgets
- 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).
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.
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.
| Element | Spec |
|---|---|
| Drawer trigger | Message icon in header, badge count shows unread total |
| Drawer position | Right-side slide-in, fixed width 380px |
| Pinned indicator | Mono 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 format | 12-hour clock, e.g. “2:47 PM” |
| Cost indicator | Per-message: “1 credit” (or “1 of monthly allowance”) |
| Backend | get_creator_agent_context_v2() RPC provides context, agent-chat Edge Function handles inference |
| Models | gpt-4o-mini primary, Claude Haiku 4.5 fallback |
UI Patterns
Common patterns: section headers, pull quotes, stats, layer diagrams, form elements, badges, and icons.
The MADE Foundation
Reclaiming $15 Trillion by 2050
Culture is the world's most powerful operating system. MADE CX is the ledger that protects it.
Broadcasting, licensing, owning value
Ledger / Compliance / Valuation
Brands / Platforms / Institutions
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 purchase
| Region | Spec |
|---|---|
| Backdrop | position: 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 card | Centered 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 |
| Header | Padding 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 |
| Body | Padding 24px. Body copy 14px / 1.6 line-height. Bold the variables (amount, asset name) so they read at a glance |
| Actions row | Padding 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) |
| Animation | Backdrop 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 management | On open: focus first interactive element (close button or first input). Trap Tab within modal. On close: return focus to trigger |
| Esc key | Closes modal (unless destructive — same rule as backdrop click) |
| Variant | Use case | Action row |
|---|---|---|
| Info | "Here's what happened" — license issued, payout sent, transaction confirmed | Single primary button: "Got it" or "View receipt" |
| Confirm | Reversible decisions — discard draft, save changes, switch tiers | Ghost Cancel + Primary Confirm |
| Destructive | Irreversible — delete asset, revoke license, deactivate account. Backdrop click and Esc are disabled — user must click Cancel or Confirm | Ghost Cancel + Primary Confirm with color:var(--red) on the icon |
| Form | Inline data entry — verification doc upload, edit profile field, set payout method | Ghost Cancel + Primary Submit (disabled until valid) |
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.
| Toast variant | Border-left color | Icon | Use case |
|---|---|---|---|
| Success | var(--green) — activation indicator (permitted) | Check (stroke green) | Confirmations: saved, sent, paid, license issued |
| Info | var(--text) | Info circle | Status updates: submitted, in review, queued |
| Error | var(--red) | X circle | Failures: payment declined, validation error, network |
| Property | Value |
|---|---|
| Position | position: fixed; bottom: 24px; right: 24px; z-index: 1200. Stack vertically with 12px gap, newest on top |
| Container | Min-width 320px, max-width 480px. background: var(--bg), border: 2px solid var(--text), border-left: 4px solid {variant} |
| Padding | 14px 18px |
| Title | Space Grotesk 14px / 800, color var(--text) |
| Body | Inter 12px / regular, color var(--text-secondary) |
| Auto-dismiss | 4000ms. Hover pauses the timer; mouse-leave resumes |
| Animation | Slide 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 |
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 replacement | Why |
|---|---|---|
alert("Saved!") | wgSuccess("Saved") → toast | Confirmations don't need to block. Toast slides in, dismisses itself |
alert("Error: ...") | wgError(title, body) → modal or toast based on severity | Errors 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 modal | Native prompt is the worst of the three — looks like a virus alert on Windows. Always replace |
window.confirm on form submit | Inline validation + modal on dirty navigation | "Leave site? Changes you made may not be saved." Replace with a proper unsaved-changes modal |
// 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.
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.
| Method | Returns | Use 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) |
Every alert/confirm call passes a variant string that styles the icon and border. Default is 'info'.
| Variant | Icon | Border accent | Common usage |
|---|---|---|---|
info | i circle | var(--text) | Default info messages, neutral confirmations |
success | checkmark | var(--success) | Save successful, card linked, payout completed |
warning | triangle | var(--warning) | Low balance, near limits, non-blocking issues |
error | ! square | var(--red) | API failures, auth errors, validation issues |
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
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.
| Element | Style |
|---|---|
| Overlay | Fixed full-screen, rgba(0,0,0,0.6), click-outside dismisses (alert only) |
| Card border | 2px solid (color varies by variant) |
| Card padding | 24-28px |
| Title | Space Grotesk 900 / 18-20px / -0.01em |
| Message | Inter 14-15px / 1.55 / var(--text-secondary) |
| Detail line | Mono 11px / var(--text-muted), below message |
| OK / Confirm button | .btn-primary styling (filled inverted) |
| Cancel button | .btn-secondary (outlined) |
| Destructive confirm | Confirm button border & bg = var(--red) |
| Escape key | Closes 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.
- Find the auth token in localStorage. Supabase stores it under a key matching
sb-{ref}-auth-token. IteratelocalStorage.key(i), match the pattern, parse JSON, extractaccess_token. - Fetch with plain
fetch(). No supabase client. Two headers:apikey(anon key, public) andAuthorization: Bearer {access_token}(auth, only when needed). - 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.
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);
}
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”.
- 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)
- 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.
| Context | Value |
|---|---|
| Card padding | 20px |
| Section spacing | 40px |
| Element gaps | 16px |
| Inline spacing | 8px |
| 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.
User signup, status change, agreement signed
Supabase trigger detects change, queues email
Processes queue every 5 min, delivers via Resend API
| # | Template | Trigger | Audience |
|---|---|---|---|
| 01 | Brand Welcome | Brand completes onboarding | Brands |
| 02 | Registration Approved | Status → approved | Creators |
| 03 | Needs More Info | Status → needs_info | Creators |
| 04 | Royalties Enabled | royalties_enabled = true | Creators |
| 05 | CCA Confirmed | Agreement inserted | Creators |
| 06 | Under Review | Registration inserted (pending) | Creators |
| 07 | Cultural Lien Filed | Lien record inserted | Creators |
| 08 | Brand Registration | Brand status → approved | Brands |
| 09 | License Approved | License status → approved | Brands |
Table-based layout, centered on #F5F5F5 background. All content within a single 600px container.
No external stylesheets. Every style is inline for Gmail, Outlook, and Apple Mail compatibility.
Use text badges (REGISTRATION APPROVED, ACTION REQUIRED) instead of emoji. Checkmarks use "✓" character.
Logo CX stroke and arrow symbols (→) are the only green elements. Everything else is black/white/gray.
| 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.
|
| Element | Style |
|---|---|
| Outer background | #F5F5F5 with 20px padding |
| Container | 600px max-width, #FFFFFF background |
| Header | #000000 bg, 32px 40px padding, border-bottom: 3px solid #00C805 |
| Logo in header | 28px, weight 900. "MADE" white, "CX" stroke 3px green |
| Hero section | 56px 40px padding, border-bottom: 1px solid #E5E5E5 |
| Status badge | border: 2px solid #000, 11px mono, uppercase, 0.15em spacing |
| Headline | 36px, weight 900, -0.02em, line-height 1.1 |
| Body text | 15px Inter, line-height 1.8, #171717 |
| Info box | border: 2px solid #000, bg #FAFAFA, 32px padding |
| Accent box | border-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 logo | 18px, "CX" stroke 2.5px green |
[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
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
);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();-- 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
// 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`);
});supabase functions deploy process-email-queue
React Component Library
Production-ready TSX components for the React platform team. Copy these files into your project or download the complete package below.
# 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'/* 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); }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>
);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>
);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>
);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>
);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>
);export { Logo } from './Logo';
export { Button } from './Button';
export { ProductCard } from './ProductCard';
export { Badge } from './Badge';
export { SectionLabel } from './SectionLabel';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]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.
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.
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.
| Dim | Name | What it measures |
|---|---|---|
d1 | Origin | Provenance — where did this culture come from, who originated it, is the lineage clear and uncontested |
d2 | Commercial | Commercial viability and licensing potential — addressable market, brand fit, monetization paths |
d3 | Reach | Geographic + demographic spread — how far has this cultural footprint already extended |
d4 | Velocity | Rate of change — is attention rising, stable, or fading; trajectory over the trailing window |
d5 | Return | Historical return on cultural exploitation — revenue per unit of attention captured to date |
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.
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 Score | Tier | Label |
|---|---|---|
| ≥ 80 | 1 | Premium Cultural Asset |
| ≥ 60 | 2 | Commercial Cultural Asset |
| ≥ 40 | 3 | Emerging Cultural Asset |
| ≥ 20 | 4 | Early-Stage Cultural Signal |
| < 20 | 5 | Unscored |
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';
}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.
| Property | Convention |
|---|---|
| Storage column | tcpmv_numeric — bigint, stored in cents (USD) |
| Display column | tcpmv_display — text, pre-formatted for surface use; null when unscored |
| Null state | Render 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 rule | Registered + verified: var(--green). Registered but unverified: var(--text). Unregistered: var(--text-muted) + outline-only container |
| Type scale | Detail page: Space Grotesk 36px / 900. Registry card: 24px / 900. Grid cell: 16px / 800. Inline mention: same size as surrounding body |
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.
| Confidence | Treatment |
|---|---|
| ≥ 0.85 | No decoration — score renders normally |
| 0.60–0.84 | Mono 0.74 next to score in var(--text-muted), no warning |
| < 0.60 | Score grayed to var(--text-muted); small warning glyph; "LOW CONFIDENCE" label on detail surfaces |
| Tier-5 / unscored | Confidence not displayed at all |
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.
| Element | Style |
|---|---|
| Container width | Fixed 200px on detail surfaces; flex: 1 in list-view rows |
| Bar height | 2px (compact rows) or 4px (detail). Never above 6px. |
| Track | background: 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 segments | 2px (compact) or 3px (detail) |
.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 */function dimClass(val) {
if (val >= 60) return 'high';
if (val >= 30) return 'mid';
return 'low';
}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.
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.
cprsScore()cprsTier()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.
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.
| Tier | CPRS | Label | Typical TCPMV | License band | Commercial implication |
|---|---|---|---|---|---|
| 1 | ≥ 80 | Premium | $5M+ | $50K+ | Heritage assets. Exclusive deals, multi-year terms, brand-defining placements. Buyer almost always negotiated. |
| 2 | 60–79 | Commercial | $1M–$5M | $10K–$50K | Established cultural property with proven commercial fit. Standard licensing, defined-use terms. |
| 3 | 40–59 | Emerging | $200K–$1M | $2K–$10K | Growing relevance, narrower market fit. Self-serve licensing common. Often Tier-3 → Tier-2 within 18 months. |
| 4 | 20–39 | Early-Stage | $10K–$200K | $500–$2K | Pre-commercial or niche-only. Available at low price but commercial signal still developing. |
| 5 | < 20 | Unscored | — | Quote on request | Insufficient 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.
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.
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.
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.
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.
- 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)
- 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.
: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);
}