Moodboard and token proposal
For dusty's review at gate G1, alongside the wireframes (F3). This document proposes the tokens packages/ui ships in F1: two real palettes (dark, light) and the reasoning behind them, the type system, motion/sound/cursor principles, how the legal and devnet notices look, and the four open questions this gate resolves.
Direction
The master plan's brief: "Bloomberg terminal possessed by SPX6900" — a real index's gravitas (dense data, monospace numerals, methodology language) delivered with meme-market energy (the 69 motif, celebratory burn animations). That still holds. Three things changed for v2:
- Permanence over speed. V1's brief leaned on weekly churn — the site the plan describes is calmer: seasons, not weeks; a slot lost for good, not a status that flickers. The design should feel like a ledger, not a leaderboard that resets.
- Amber lost its old job. It used to flag a token about to lose its slot. It no longer means that — nothing in this design shows a verdict before the reveal, so amber only ever means "pending" (awaiting graduation, LP lock pending, a season that ended and hasn't rebalanced yet). It never touches a constituent's survival state.
- The star, not the flag. A season survived earns a permanent ★. The 69 Grid is built around this: mostly quiet, briefly celebratory at admission and at season close, otherwise just a record of who's still there.
Nothing here is copied from Stonk.fun's or pump.fun's branding, layout, or logos. The interaction model (a curve, a buy/sell panel, a bonding progress bar) is the same familiar shape traders already know; the visual language is INDEX's own.
Palettes
Both themes are shipped as CSS custom properties in packages/ui/src/tokens.css (dark is the default; a third, wire, greyscale theme exists only for the wireframes and isn't part of this proposal). Nothing below is a mockup color picked by eye — every value is what's actually checked into tokens.css today, and the contrast table further down is computed from that same file, not redrawn from memory.
Dark (default)
| Token | Value | Role |
|---|---|---|
--color-ground | #0A0B0E | Page background — near-black, the terminal floor |
--color-panel | #12141A | Card/panel surface, one step up from ground |
--color-text | #F5F6F8 | Primary text |
--color-muted | #8B93A3 | Secondary text, labels |
--color-brand | #FF7A00 | INDEX brand: burn accents, primary actions, large counters |
--color-positive | #00E17B | Up-movement, the OPEN state |
--color-alert | #FF3B4E | Down-movement, permanent removal |
--status-pending | #FFC24B | Awaiting graduation / LP lock pending / season ended — never a survival verdict |
--star | #FFD166 | The ★ a survived season earns — deliberately a different gold from --status-pending, so a star never reads as a pending flag |
--state-full | #FFB020 | The 69 Grid at FULL |
--focus | #5AC8FA | Keyboard focus ring |
--banner-devnet / --banner-devnet-fg | #8C74FF / #0A0B0E | The devnet/unaudited banner — its own hue, so "this is a test network" never reads as a warning about the protocol |
Light
| Token | Value | Role |
|---|---|---|
--color-ground | #F4F5F7 | Page background |
--color-panel | #FFFFFF | Card/panel surface |
--color-text | #12141A | Primary text |
--color-muted | #5B6472 | Secondary text, labels |
--color-brand | #C75A00 | INDEX brand (darkened from dark-theme orange to hold contrast on a light ground) |
--color-positive | #048A54 | Up-movement, the OPEN state |
--color-alert | #B91C3C | Down-movement, permanent removal |
--status-pending | #9C6500 | Awaiting graduation / LP lock pending / season ended |
--star | #9C7209 | The survivor ★ |
--state-full | #AD6600 | The 69 Grid at FULL |
--focus | #0A63B5 | Keyboard focus ring |
--banner-devnet / --banner-devnet-fg | #5B34E6 / #FFFFFF | The devnet/unaudited banner |
Every color that carries meaning in the dark theme keeps the same *role* in light — only the value moves, enough to hold WCAG contrast on the lighter ground. Nothing swaps roles between themes.
Contrast
Every pair above is checked against WCAG AA (4.5:1 for body text; 3:1 for large text and non-text UI, per WCAG 1.4.11) by packages/ui/test/contrast-helper.ts, which reads tokens.css directly rather than a copy of the palette — so this table can't quietly drift from what ships. packages/ui/test/contrast.test.ts asserts every row below passes; running it is part of pnpm --filter @index/ui test.
Reproduce this table: pnpm --filter @index/ui contrast-report
| Theme | Pair | Foreground on background | Ratio | Needs | Result |
|---|---|---|---|---|---|
| dark | body text on the page | --color-text on --color-ground | 18.20:1 | 4.5:1 (normal text) | pass |
| dark | body text on a panel | --color-text on --color-panel | 17.02:1 | 4.5:1 (normal text) | pass |
| dark | muted text on the page | --color-muted on --color-ground | 6.37:1 | 4.5:1 (normal text) | pass |
| dark | brand counters / headlines | --color-brand on --color-ground | 7.53:1 | 3:1 (large text / UI) | pass |
| dark | positive indicator | --color-positive on --color-ground | 11.30:1 | 3:1 (large text / UI) | pass |
| dark | alert indicator | --color-alert on --color-ground | 5.61:1 | 3:1 (large text / UI) | pass |
| dark | pending status | --status-pending on --color-ground | 12.25:1 | 3:1 (large text / UI) | pass |
| dark | survivor star glyph | --star on --color-ground | 13.65:1 | 3:1 (large text / UI) | pass |
| dark | FULL state indicator | --state-full on --color-ground | 10.76:1 | 3:1 (large text / UI) | pass |
| dark | focus ring (non-text, WCAG 1.4.11) | --focus on --color-ground | 10.38:1 | 3:1 (large text / UI) | pass |
| dark | devnet/unaudited banner text | --banner-devnet-fg on --banner-devnet | 5.68:1 | 4.5:1 (normal text) | pass |
| light | body text on the page | --color-text on --color-ground | 16.88:1 | 4.5:1 (normal text) | pass |
| light | body text on a panel | --color-text on --color-panel | 18.41:1 | 4.5:1 (normal text) | pass |
| light | muted text on the page | --color-muted on --color-ground | 5.48:1 | 4.5:1 (normal text) | pass |
| light | brand counters / headlines | --color-brand on --color-ground | 3.94:1 | 3:1 (large text / UI) | pass |
| light | positive indicator | --color-positive on --color-ground | 4.04:1 | 3:1 (large text / UI) | pass |
| light | alert indicator | --color-alert on --color-ground | 5.84:1 | 3:1 (large text / UI) | pass |
| light | pending status | --status-pending on --color-ground | 4.50:1 | 3:1 (large text / UI) | pass |
| light | survivor star glyph | --star on --color-ground | 3.99:1 | 3:1 (large text / UI) | pass |
| light | FULL state indicator | --state-full on --color-ground | 4.12:1 | 3:1 (large text / UI) | pass |
| light | focus ring (non-text, WCAG 1.4.11) | --focus on --color-ground | 5.56:1 | 3:1 (large text / UI) | pass |
| light | devnet/unaudited banner text | --banner-devnet-fg on --banner-devnet | 6.82:1 | 4.5:1 (normal text) | pass |
Every pair passes today, in both themes, with margin — the tightest is light's pending status at 4.50:1 against a 3:1 floor.
Type
Three families, all SIL OFL 1.1 and self-hosted rather than loaded from Google Fonts or any third-party CDN (OD4):
- Archivo Black — display: the hero counters (
NN / 69, burn totals) and headlines. - JetBrains Mono,
font-variant-numeric: tabular-nums— data: every figure that can change, so a tick never reflows its neighbours. - Inter — body: everything else.
lightweight-charts' own TradingView attribution is kept as its license requires.
Specimen
| Role | Family | 1440px | 390px |
|---|---|---|---|
Hero counter (NN / 69) | Archivo Black | 96px / 1.0 | 48px / 1.0 |
| Section headline | Archivo Black | 48px / 1.1 | 28px / 1.15 |
| Table data, large (a token's price) | JetBrains Mono | 20px / 1.3, tabular-nums | 16px / 1.3, tabular-nums |
| Table data, body (a trade row) | JetBrains Mono | 16px / 1.4, tabular-nums | 14px / 1.4, tabular-nums |
| Body copy | Inter | 16px / 1.5 | 15px / 1.5 |
| Muted label | Inter | 13px / 1.4, uppercase | 12px / 1.4, uppercase |
F3 renders this specimen live (/moodboard), against the real fonts.
Motion, sound, cursor
Motion — numbers tick, never fade. Counters count up or down (odometer-style), they don't cross-fade. Beyond that, one deliberate, celebratory moment per event class, and everything else instant:
- the graduation tape banner (kept from V1);
- an INDEX burn pulse on the burn counter;
- a position-claim flip when a token takes a slot;
- a star stamp at season close;
- a quiet removal — deliberately the calmest animation in the set, because removal is permanent and shouldn't be dressed up;
- a season rollover.
Two motion tokens exist today (--motion-duration-fast, --motion-duration-celebratory, --motion-ease); the keyframes themselves are each surface's own job as they're built.
Sound — opt-in, off by default. If enabled, sound plays only on the same celebratory moments motion marks above — never as UI feedback (no click, hover or error sounds). A visitor who never opts in hears nothing, ever.
Cursor — a crosshair on data surfaces. The 69 Grid, charts, and trade/holder tables get a crosshair cursor, the one deliberately "terminal" affordance in an otherwise ordinary interface. Everywhere else, the ordinary pointer.
Legal and risk notice styles
Two components carry every legal/risk surface (packages/ui/src/primitives/Banner.tsx and the acknowledgment dialog, F10a):
- The devnet/unaudited banner. A full-width bar, top of every page, using
--banner-devnet/--banner-devnet-fg— its own hue (a purple-violet), not--color-alertor--status-pending, so a network notice never reads as a protocol warning. Plain text, no icon-led scare styling. Persistent, not a toast. - The acknowledgment dialog. Shown once per wallet before a first launch and before a first trade (two separate acknowledgments,
kind: 'launch' | 'trade'). Plain language, a single checkbox and a single confirm button — not a wall of legal text. It blocks only the specific action it gates. Its record ({ wallet, termsVersion, kind }) lives inlocalStorage; whether it also needs a server-side record is OD15, below. - `/risk`'s placeholder sections. Each carries "Placeholder — pending counsel (OD15). Not legal advice." until counsel drafts real text.
Questions for G1
Three open decisions this gate is asked to resolve or confirm. Each has a safe default already built in, named below, that ships if nothing is said.
- OD11 — "no trading terminal." The token page is a chart plus one simple buy/sell panel, not a multi-pane trading terminal — confirming that's still the right call for launch, not a heavier interface. Default: chart + simple panel, as built.
- OD12 — themes, tagline, brand mark and colour, sound, crosshair, pair emphasis. Confirming seven small defaults together: dark and light both shipped (dark default); no tagline until you give one, so the wordmark stands alone; a wordmark rather than a pictorial logo; orange (
#FF7A00on dark,#C75A00on light) as the v0 brand colour; sound off by default; the crosshair cursor on data surfaces; SOL shown first among trading pairs. Say now if any of these should change, and send the tagline if there is one. Default: all seven, as described above. - OD15's placement (text itself is a separate, later decision with counsel). Where the legal and risk notices sit is fixed by this proposal — top-of-page banner, gated acknowledgment dialogs, a placeholder
/riskpage, footer links on every page. Confirming that placement now means counsel's later text drops into a structure you've already seen, rather than reopening layout once legal text exists.
Anything raised here that changes a page's composition can reopen F5, F6, F10 or F13 (recorded against whichever task it reopens, per the plan's G1 section) — this is expected, not a sign something was missed.