Dark by default, light by attribute
The dark theme is declared on :root and repeated for [data-theme="dark"], so that attribute on any element pins its subtree to dark whatever the page shows. The light theme is :root[data-theme="light"]: the same custom properties, redefined — never a second copy of the app. Only the surface, text and border scaffolding flips; the accent and every data-semantic hue read identically in both.
--bg-page—--surface-panel—--text-strong—Toggle the theme (top right) and these change. The choice is kept in local storage, as the demo's ThemeContext keeps it.
This card is pinned to the dark theme whatever the page shows — /design's Final section pins two cards this way. Inside it --surface-panel is —.
Each theme block sets color-scheme, and select, option inherit it, so dropdowns and date inputs render in the page's scheme instead of forcing dark.
Wordmark, accent family, type
The one thing that is exact and never substituted: --blue-500. Everything else sits quietly around it.
The full word, both leading S's in the accent, and an accent asterisk floating above each i (drawn over a dotless ı) — the one deliberate glyph-level brand mark (founder logo 2026-07-21). Flat accent colour, no gradient, no drop shadow. The compact mark is the eight-spoke asterisk from the favicon, for the collapsed left nav; it draws in currentColor so a parent can retint it.
--blue-300—--blue-400—= accent-bright--blue-500—Exact hex, never substituted.--blue-600—= accent-hover--blue-700—--accent—--accent-hover—--accent-bright—--accent-soft—--accent-tint—rgba fill, e.g. the Active badge.--accent-tint-strong—The leverage picker's selected band.One typeface — Inter — carries every word AND every number. Numerals get Inter's tabular figures via font-num, so prices and countdowns align in a column without a second webfont. A real monospace survives only for literal code (font-code) — raw wire frames and the token names on this page. font-mono still works and resolves to the same stack; it is a legacy alias and no longer means monospace.
--font-display—--font-num—--font-mono—--font-code—Hierarchy by tone, with a hairline edge
Every background below is one step lighter than the last in dark mode. Tone still does most of the separating; the difference from round 3's "border massacre" is that a card now also carries --border to define its edge. Light mode inverts the stack: the page is the tinted step and cards are pure white, so a card still reads as the raised object.
--bg-page—Base the whole app sits on — the darkest step in dark theme, the lightest in light.--surface-panel—Cards, modals, the calendar's own Card wrapper.--surface-row—Table / list zebra tone, one step up from panel.--surface-hover—Default interactive hover; the topic-row-driven day highlight.--surface-hover-light—A directly hovered day cell — one step lighter than hover.--surface-raised—Popovers and modals float above everything else.--surface-inset—Recessed wells, e.g. the sample strips inside a palette card.Round 12 removed the Swiss-grid overlay that used to sit on the page tone — a bordered card on a flat page reads as a discrete object, and the grid was competing with its edge. --grid-line is kept for components that draw their own rules. The scrollbar thumb is its own pair so the light theme can lighten it: --scrollbar-thumb —, --scrollbar-thumb-hover —.
Emphasis follows data, not position
Five weights of grey, one job each — used consistently whether the surface underneath is page or panel. Hierarchy comes from weight and colour, not from a size jump or a second family, which is why the working range of the whole UI is 11–16px.
--text-strong—Strong — settlement calendarTitles, event-day numerals, emphasis.--text-body—Body — settlement calendarDefault running copy.--text-muted—Secondary copy, taglines.--text-faint—Faint — a section markerChrome labels, eyebrows, empty-day numerals.--text-dim—Dim — settledPast / settled state, lowest-priority meta.--text-onaccent—--text-on-fill-dark—--text-on-fill-light—A day cell's numeral weight is driven entirely by whether it HAS events — not by weekday, column or row. Event days render strong; empty days recede to faint; past days dim further regardless of events; today is a solid YELLOW block (founder 2026-08-16 round 2 — accent stays reserved for interactive / selection state) with near-black text and a pulsing dot.
Three border tones, one ring
A card's edge is its 1px --border; the quiet rule between day cells is the tertiary tone. The only outline in the system is *:focus-visible — hover is a background step, never a ring.
--border—A card's edge.--border-strong—An unchecked category checkbox.--border-tertiary—Calendar grid line — the quiet rule between day cells.--grid-line—For components that draw their own rules.--focus-ring—— solid, offset —.The class-order trap: a day cell's background must resolve to exactly ONE mutually exclusive class — never two conditional utilities layered side by side. Tailwind orders rules by declaration order, not by how cn() concatenates class names, so whichever rule the stylesheet emits LAST silently wins.
Colour = data semantics, not decoration
Every non-neutral colour in the system answers a specific question. None of them are interchangeable.
--up—--up-dim—Candle wick tone.--down—--down-dim—Candle wick tone.--viz-settle—The payoff at settlement — a categorical pair, not a market one.--viz-now—The payoff today; stepped for the white card in light.Rule: up / down is reserved for MARKET DIRECTION only — PnL sign, 24h change, candle colour, In-Zone status. Never repurposed as a generic success / error pair.
--up aliases the brand's sage, but --down is a separate muted red rather than the brand's oxblood — chosen so the breaking-news alarm can be louder than price direction. Both values on the overview's drift list.
--accent—--accent-hover—--accent-bright—--focus-ring—Rule: accent marks INTERACTIVE state and selection — the segmented control's thumb, a selected row, a selected date. Never market direction or category. The calendar's TODAY block moved off the accent to the yellow below (founder 2026-08-16 round 2, item 9), so accent now means exactly one thing.
--today-highlight—Every calendar's TODAY block. Text: --text-on-today —.--spot-highlight—The order books' inline spot bar — a signal yellow used at block scale nowhere else. Text: --text-on-spot —.--star—The watchlist star's fill. Deeper on the light theme: the dark yellow measures about 1.6:1 on white as a small glyph.The demo declares the same yellow three separate times with no shared variable, even though /design calls them one "signal-yellow family"; the foundation's --signal-yellow — is that family, and today and spot alias it.
lib/mood.ts maps each portfolio mood to a representative action score (0 / 15 / 45 / 65 / 95) and resolves it through the ACTIVE action palette rather than inventing a second colour system. Those scores are number tokens, not CSS; the chips read the active palette's buckets.
A palette readout, not a component: /design sets each label in the mood's own colour over a 13% wash of it, which measures about 2:1 for the yellow and orange buckets on the light theme; here the label is strong text and the dot carries the colour. The shipped MoodPanel uses the colour only for its bar.
Six categories, one source of truth
lib/categories.ts is the only place a category colour is assigned — every chip, pill and dot reads through it, never a hard-coded hex. BREAKING (founder 2026-08-16) is the urgent-news alarm tag, a hot signal red deliberately louder than the muted market --down; it sorts first and always wins as an event's primary tag. The text on a solid fill is computed per fill by WCAG luminance, not eyeballed: crypto's cobalt and breaking's red take white, the other four take near-black.
--cat-breaking—
--cat-breaking-tint—
text-on: light → --text-on-fill-light —
--cat-crypto—
--cat-crypto-tint—
text-on: light → --text-on-fill-light —
--cat-macro—
--cat-macro-tint—
text-on: dark → --text-on-fill-dark —
--cat-political—
--cat-political-tint—
text-on: dark → --text-on-fill-dark —
--cat-banking—
--cat-banking-tint—
text-on: dark → --text-on-fill-dark —
--cat-equities—
--cat-equities-tint—
text-on: dark → --text-on-fill-dark —
Crypto reuses --blue-400 and equities reuses --up exactly; political reuses --amber-500. Macro is a near-duplicate of the foundation's teal-400 that is not the same value (foundation › teal).
Severity reuses the market and status hues
High is the market red, medium the amber status hue, low the dim text tone — no new palette. The topic row's 3px severity bar draws it; the bar is hidden in the demo today ("may return"), so the recipe makes it opt-in by data-severity.
--impact-high—→ down--impact-med—→ amber-500--impact-low—→ text-dimFour palettes for one 0–100 urgency number
Bucketed low 0–25 / mid 26–50 / high 51–75 / critical 76–100. Every palette renders the same sample scores so they can be compared directly; the active one is what the portfolio uses (founders' palette review, 2026-07-10: "action score carries the traffic-light semantics"). A few buckets carry a light-theme override where the dark hex washes out on white — toggle the theme to see them step.
The switch is CSS only: the strip reads the chosen palette's tokens through the radio group. Without :has() it shows the active palette. The score is drawn as ActionPositionCard ships it — --fs-3xl bold on the bucket colour mixed 14% into --surface-row — so the strip measures the real pair.
At the card's 28px the pair needs 3:1 as large text. The dark theme clears it except Verdant→Crimson's low and critical (2.8 and 2.9:1); the light theme does not for the mid and high buckets of Signal, Vital and Verdant→Crimson, or Vital's low (1.9–2.6:1). The demo's values, kept exactly; toggle the theme to see them.
Signal signal
Clinical grey-to-red ramp — quiet until it isn't, no colour spent on "fine."
--action-signal-low—Light: deepened so "low" still registers on white.--action-signal-mid—--action-signal-high—--action-signal-critical—Cobalt Heat cobalt-heat
Brand cobalt warming through violet into magenta — the house accent turned into an urgency gauge.
--action-cobalt-heat-low—Light: the brand's own deep cobalt keeps "low" legible without changing its hue.--action-cobalt-heat-mid—--action-cobalt-heat-high—--action-cobalt-heat-critical—Vital vital
Teal-yellow-orange-red — a vitals-monitor read, healthy to flatlining.
--action-vital-low—--action-vital-mid—Light: the yellow is too pale on white; deepened toward gold.--action-vital-high—--action-vital-critical—Verdant→Crimson verdant-crimson
ActiveGreen through amber into crimson — the classic semantic traffic light, ported from the DTE ramp per the founders' palette review (2026-07-10): the action system carries the loudest semantics because it matters more.
--action-verdant-crimson-low—Light: the deep verdant reads muddy on white — brightened one step.--action-verdant-crimson-mid—--action-verdant-crimson-high—--action-verdant-crimson-critical—The score itself is a deterministic pseudo-random stand-in seeded off the position id (lib/actionScore.ts's DEMO LOGIC NOTICE) — the buckets, palettes and colour resolution are meant to outlive it. The /design specimen sets the score in the code face; a score is a number, so the system sets it in the figure face (Rule 2: "numbers use font-num").
Three 7-step ramps, deeper the closer
Calendar-day brackets for a position's settlement — today, 0–2, 3–7, 7–14, 14–21, 21–30, >30 — each whole day in exactly one (a shared boundary day resolves to the farther bucket). Days farther out than 60 get no DTE colour at all. The far end of a 7-step ramp is tuned to barely register on the dark page; on white that same hex reads as a blotch, so the far brackets carry light-theme overrides. The cells' text is whichever of the two on-fill inks measures the higher contrast against the painted fill; /design picks by a luminance threshold instead, and the line under each week says where the two differ.
—
/design's contrastTextFor() inks a fill white below a luminance of 0.4 and near-black above it. The two inks cross at about 0.19, so on the saturated ambers, oranges and reds of these ramps — most of Ember in both themes, the near brackets of Verdant→Crimson — a fill between 0.19 and 0.40 takes white at 2.3–4.3:1 where --text-on-fill-dark measures higher, and in the audit's cases (Ember's 7–14d on the light theme, Verdant→Crimson's 7–14d and 14–21d) would clear 4.5. The cells here take whichever of --text-on-fill-light and --text-on-fill-dark measures higher against the painted fill, and the line under each week names every cell where the app's pick is the other one, with both ratios (hover a cell for its own). The threshold itself is the demo's to change.
Ember ember
ActiveHot red today cooling through orange into warm sand — saturation fades with distance, but every in-window day stays brighter than a plain day. (Founder 2026-08-16: the old ramp's far end smouldered darker than the plain-day grey; urgency = saturation, never dimness.)
--dte-ember-gt30—--dte-ember-d21-30—--dte-ember-d14-21—--dte-ember-d7-14—--dte-ember-d3-7—--dte-ember-d0-2—--dte-ember-today——
Abyss abyss
Cobalt / indigo ramp — today lands exactly on the brand accent, everything else recedes from it. The four far brackets go pale on the light theme.
--dte-abyss-gt30—--dte-abyss-d21-30—--dte-abyss-d14-21—--dte-abyss-d7-14—--dte-abyss-d3-7—--dte-abyss-d0-2—--dte-abyss-today——
Verdant→Crimson verdant-crimson
Green through amber into red — the classic semantic-urgency traffic light. The three far brackets go pale on the light theme.
--dte-verdant-crimson-gt30—--dte-verdant-crimson-d21-30—--dte-verdant-crimson-d14-21—--dte-verdant-crimson-d7-14—--dte-verdant-crimson-d3-7—--dte-verdant-crimson-d0-2—--dte-verdant-crimson-today——
Eight assets, two colours each
data/underlyings.ts is the single source of truth for asset identity. The theme colour is the portfolio heatmap tile's identity hue, used as a low-alpha tile background and a tile-top rule — never for on-tile text, because BTC / GOLD / NVDA measure about 7:1 on the dark row surface but about 2:1 on the light one. The glyph colour is the inline SVG mark's own fill, a legibility recolour in several cases rather than a brand colour. The glyphs below are the demo's own paths — bare at 16 / 24 / 40px, as /design shows them, and in the 20px page-tone disc UnderlyingGlyphs puts a 13px glyph in beside an event, ringed in --border-strong; every fill is read from its --underlying-*-glyph token, so SPACEX, AAPL and the S&P text-mark follow the theme.
--underlying-btc—
--underlying-btc-glyph—
simple-icons siBitcoin — the exact brand orange, glyph and tile alike.
--underlying-eth—
--underlying-eth-glyph—
The glyph is recoloured to slate (the literal near-black brand hex disappears on ink surfaces); the tile keeps Ethereum's blue.
--underlying-gold—
--underlying-gold-glyph-face—
--underlying-gold-glyph-top—
--underlying-gold-glyph-edge—
No official mark — a custom flat-fill ingot, two solid tones standing in for a bevel (no gradients).
--underlying-nvda—
--underlying-nvda-glyph—
simple-icons siNvidia — the exact brand green.
--underlying-aapl—
--underlying-aapl-glyph—
The glyph follows --text-strong: the literal mark ships pure black, and a fixed inverse would vanish on the light theme. The tile is a warm grey, since black cannot be a tile colour.
--underlying-crcl—
--underlying-crcl-glyph—
Recoloured to Circle's USDC blue — the literal simple-icons violet reads as neither Circle nor legible at glyph size.
--underlying-spacex—
--underlying-spacex-glyph—
The glyph follows --text-strong, near-white on dark and near-black on light.
--underlying-sp500—
--underlying-sp500-glyph—
No brand mark — a bold S&P / 500 text-mark in currentColor, so it inherits ambient tint like any other chrome icon (the dot shows this page's text colour).
/design's UNDERLYING_NOTES still describes the SpaceX glyph as inverted to a fixed off-white literal for dark surfaces, while data/underlyings.ts has since switched it to the --text-strong token (its own comment: the literal dark-surface recolour "was invisible on the light theme"). The reference page's note is stale; the token follows the data file.
A true 4px grid, cards on a quiet page
The root is 16px — that is the whole fix. The previous 14px root (a density knob) landed Tailwind's spacing on 7px, 14px, 35px and the type scale on sizes like 12.25px. With a 16px root, gap-2 is exactly 8px, h-10 exactly 40px. Prefer the plain utilities; the tokens exist so inline styles and arbitrary values can't drift off the grid either. Each bar is drawn at its token and the readout resolves the same token.
--root-font-size—--container-max—--gutter—16 → 24 → 32 at 768 and 1280px; resize to see it step..sx-container centres document-shaped pages (this one) at 1280px. The workspace pages — event, trade, portfolio — stay full-bleed on purpose: a left rail plus a fluid grid, and centring them would waste the half of a wide monitor those layouts exist to fill.
--control-h-sm—--control-h-md—--control-h-lg—--btn-px-sm—--btn-px-md—--btn-px-lg—--chip-h-filter—--chip-h-tag—--chip-px-filter—--chip-px-tag—--segmented-pad—--segmented-height—--segmented-option-padding-x—--rail-w-collapsed—--rail-w-expanded—--rail-header-h—--rail-row-h—--icon-xs—--icon-sm—--icon-md—--icon-lg—--card-radius—--card-pad—--card-pad-roomy—--popover-gap—--popover-side-gap—--popover-side-width—--modal-max-width—--day-size—--focus-ring-width—--focus-ring-offset—The segmented option's 14px inset (px-3.5) and the popover's 10px side gap are the demo's values and sit off the 4px grid; they are kept as the source has them.
Whole pixels on a 16px root
10 · 11 · 12 · 13 · 14 · 16 · 20 · 24 · 32, then a display ramp of 28 · 32 · 40 · 48 · 64 for the calendar's giant day numerals and the splash. Deliberately compressed at the top: a title is separated from its body by weight and colour, not a 2× size jump. Tracking goes negative as size grows (Inter is drawn tight); the all-caps label dropped from 0.14em to 0.06em. The px column is resolved at runtime — a hard-coded value silently goes stale the moment the scale moves.
--fs-3xs—Aa — eyebrow chromeMicro tag / eyebrow chrome only--fs-2xs—Aa — chip labelChip / tag labels, countdown tokens--fs-xs—Aa — secondary UISecondary UI chrome--fs-sm—Aa — dense contentDense content — event bars, meta lines--fs-base—Aa — body copyBODY DEFAULT — prose, titles, day numbers--fs-md—Aa — card titleCard titles, emphasised readouts--fs-lg—Aa — headingPanel headings--fs-xl—Aa — headlinePage headings--fs-2xl—Aa — hero numeralHero numerals only--fs-3xl—14text-3xl · 1.1--fs-4xl—14text-4xl · 1.1 — meets --fs-2xl at 32px, where the content ramp hands over--fs-5xl—14text-5xl · 1.05--fs-6xl—14text-6xl · 1--fs-7xl—14text-7xl · 1--tracking-label—Was 0.14em; the landing's still is.--tracking-snug—--tracking-tight—Tailwind reads the same scale as text-3xs … text-7xl through dist/css/app.tailwind.css, with line-heights that loosen as size drops (1.4 at the small end, 1 at the top) and the snug / tight tracking from 16px up.
Distinctly rounder than a terminal
A card is 12px, a control 8px, a chip a full pill. Roundness is most of what separates a consumer market UI from a trading terminal, so this is where the brand shift is most visible.
--radius-xs—Micro tags, inline markers--radius-sm—Small chrome, swatches--radius-md—Buttons, inputs, nav rows--radius-lg—Cards, panels, modals--radius-xl—Hero cards--radius-2xl—Oversized feature surfaces--radius-full—Chips, tags, segmented controls (rounded-full)Two of the demo's own drawings sit off this scale: the event pill rounds at 3px, below --radius-xs, and the /design modal mock rounds at 8px where the brief puts "cards, panels, modals" at --radius-lg. The recipes follow the brief for the modal and keep the pill as EventPill.tsx has it.
For things that float
A card's edge is its 1px border; the shadow only separates things that FLOAT — popovers, modals, drag targets. Cards get a shadow barely past nothing so they don't look stamped on. The light theme's are softer and tighter: a near-black shadow at dark-theme opacity reads as a harsh smudge on white.
--shadow-card—--shadow-raised—--shadow-popover—Popovers, modals, the rail while it overlays the page.One easing curve, two motion languages
Zoom (scale + opacity, 0.32s) means the VIEW changed — Day, Week, Month, Year. Slide (y + opacity, 0.3s) means you moved WITHIN the same view — previous / next month. Same ease curve, different transform. CSS transitions read the tokens; framer-motion needs raw seconds, so those are number tokens that are not emitted as CSS. A second curve rides along unnamed in the demo — Tailwind's default transition timing function, which every bare transition-colors inherits — and the system names it --ease-ui so a recipe's hover moves the way the demo's does.
--dur-fast—--dur-base—The segmented thumb's slide.--dur-slow—The landing's is 360ms.--dur-micro—Every transition-colors: a hover, a pressed chip, a row's tone.--ease-out—What the source names: the flip, the pulses, the rail's width.--ease-ui—Unnamed in the source — what Button, Chip, the segmented options, the rail's rows and TopicRow ease their colours on.The global reduced-motion rule turns every animation and transition in the app off at once, leaving each instant but intact; the segmented control's thumb and the rail's width change are covered by it.
The recipes, live
Every specimen below is drawn by dist/css/app.css alone — the framework-neutral .sx-* equivalents of the demo's Tailwind primitives — on this page's chrome. Toggle the theme and they follow.
Panel tone, a hairline border, a 12px radius and a shadow barely past nothing — the base surface every panel and list sits on. 16px of padding.
The same card with the roomy 20px inset every specimen card on /design uses.
--surface-panel—
--surface-row—
--radius-md—
--ease-out—
0.5s · app.motion.framer.flip
The recipe is three classes and no look of its own: .sx-flip holds the perspective — on the OUTER element, so every card in a grid shares one vanishing point — .sx-flip-inner keeps the 3D context and turns 180° over 0.5s on --ease-out when data-flipped="true", and the two .sx-flip-faces are stacked absolutely with their backs hidden, the back pre-turned, so the card keeps one height whichever side is up. What is on the faces is the caller's: here StrategyCard's — a panel-tone front that is ONE button (the venue symbol, the side as a chip, the row's Λ beside its payoff, the asset and the structure), and a row-tone back of ordinary text under a title bar with a small Back, its list scrolling and its Buy pinned below. The face turned away is aria-hidden and inert, and focus follows the turn to the arriving face's button. The second card is pinned to its back.
Under reduced motion the global kill switch drops the transition, which leaves the flip instant — still a flip, no motion. The card's 16rem is STRATEGY_CARD_HEIGHT, arithmetic rather than taste: the back's title bar, padding and pinned footer cost 86px and the list keeps 170.
Still a solid block — a category colour fills the whole chip with contrast text when active, lowest-tone dim block when off, no border and no leading colour dot. Round 12 changed only the SHAPE (a full pill) and the LABEL (caps at 0.06em).
"In zone" is drawn as the demo draws it: OpenPositionsTable passes --up with no text colour, so Chip falls back to white — 3.46:1 on the sage, under AA at 11px — while the equities category, the same sage, declares the dark ink (5.58:1). The specimen follows the source; the pair belongs in the demo.
The active option is a SOLID cobalt thumb on the sunken track; round 12 rounds both to full pills — a segmented control is the same shape family as a chip, not a miniature set of buttons. Today is an ACTION, never a selectable option — it never renders as active inside the control. The browser owns the exclusive choice and the arrow keys; the thumb slides with :has() and falls back to a solid label without it.
--accent-bright—
--accent—
--surface-row—
--surface-hover-light—
--radius-xs—
--focus-ring—
Two native <input type="range"> stacked on one track, as BandSlider.tsx composes them: the recipe hides each input's own track and gives only the THUMB pointer events, so both ends stay grabbable when the band is narrow and the control stays keyboard-steppable, labelled and touch-sized. The track (row tone, 4px), the halo (--surface-hover-light, 8px — what the chain offers, drawn TALLER than the selection so a band cutting into empty space reads at a glance) and the cobalt selection are the component's own elements under the inputs. The thumb is a 16px square at --radius-xs in --accent-bright, deepening to --accent while dragged, with the system's ring on the thumb and none on the input. Drag either end: the band follows, clamped so the two can never cross — "a crossed band is not a state the backend will accept".
The thumb on the halo it usually sits inside measures 2.20:1 on the dark theme (3.19:1 light), under the 3:1 non-text line; it is a 16px square and the cobalt selection beside it is what keeps it findable. The thumb reads --slider-thumb-size; the flip's 1200px perspective is the recipe's one untokenised literal.
--border-strong—
--text-on-fill-light—
--text-on-fill-dark—
--accent—
--accent-bright—
Drawn as /design's specimen and the calendar's FiltersBar draw it, by this page's chrome: components/app/ has no .sx-checkbox yet, because the 14px box and its 2px corner would need two tokens first. Each toggle is a <button aria-pressed> holding a box and its word. Off, the box is a --border-strong outline on nothing and the word is --text-faint — neutral, no hint of the category; on, the box fills with categoryColor() and its CheckIcon — the one glyph at stroke 2, 9px — takes categoryTextOn()'s ink, white on breaking and crypto, near-black on the other four, and the word goes --text-strong. The fill and ink arrive as --chip-fill / --chip-ink, the two properties the chip recipe already reads, and are painted only while pressed: the fill IS the state. The All box is the accent's — solid with a white check when every category is on, an accent outline with an --accent-bright minus when some are.
The off outline measures 1.59:1 on the dark panel and 1.56:1 on white, under 3:1 for a component's boundary in both themes; the word's tone is what tells an off box from an on one. The 14px box and its 2px corner sit off the grid and under --radius-xs, and the word is set at --tracking-label where the demo writes tracking-wide.
Panel tone, overflow hidden, the popover shadow, — wide at most, a row-tone header under a hairline. The real modals are animated portals with the same anatomy.
--surface-raised—--shadow-popover— at rest, — open — pump.fun proportions. An unpinned rail keeps its collapsed width in LAYOUT and the open panel overlays the page (widening the layout on hover would reflow a chart and a grid of live cards); a pinned rail takes its width, because that is a deliberate, persistent choice. Rows are sentence case at semibold; the active row is row tone with a 2px cobalt bar on its left edge. Hover the collapsed Trade icon for its flyout, and the others for their tooltips.
The /design specimen (GalleryStat) sets the figure in the code face and tracks its label with Tailwind's tracking-wide; the brief says "numbers use font-num" and puts labels at --tracking-label, which is what the recipe does.
A — square at --radius-md, the numeral at --fs-2xl in 800. Today's dot pulses on the shared todayPulse keyframes.
Body tone once 7+ days out; strong once inside a week; dim once settled. Lowercase bold. Inside a topic row it is one step smaller.
A full solid category-coloured block with contrast text — no outline, no dot-as-colour. Purely decorative since 2026-07-23: no hover, no click; the pointer falls through to the day cell.
Thirty-nine strokes on a 16-unit grid
Every export of components/icons.tsx, inline, drawn as the demo draws them: a 0 0 16 16 viewBox, fill="none", stroke="currentColor" at width 1.6 with round caps and joins, and paths kept inside a 1.5–2 unit margin so the caps never clip. The weight was chosen for 16–18px rendering; at — it is 1.8 device px. Three glyphs keep a different rule on purpose — CheckIcon is stroke 2 so a 9px check inside a checkbox still reads, GripIcon is six filled dots and no stroke, and StarIcon fills with currentColor once watched; HelpIcon's dot is a 2-unit stroke and EyeOffIcon's top arc sits at opacity 0.55. The greeks are stroked letterforms, not <text>: the app's Inter build is a latin subset, so a real Greek codepoint would fall out to whatever the OS happens to have and stop matching the rest of the set. There is no icon colour. Every glyph is currentColor and takes the text tone of whatever it sits in — the cells below inherit this page's --text-body; the first card shows the tones the app actually hands them. The four sizes are the four tokens: --icon-xs — (the rail's sub-rows and its hover flyout, where the glyph is secondary to its label — off the 4px grid, as the source has it), --icon-sm —, --icon-md —, --icon-lg —.
--text-faint—--text-body—--text-strong—--star—--up—--down—An icon beside a label is aria-hidden; an icon-only control carries the label. At 16–18px the stroke holds up at 200% zoom, and the inherited tone is the control's: an idle row's glyph sits at --text-faint and steps to --text-body on hover with its label.
Each Trade sub-page is named for the exposure it sells, so the rail draws the letter itself: Δ direction (the first derivative on price), ν volatility (a curved descender into a pointed foot, then a straight riser — a symmetrical curve reads as a plain "u" at rail size), θ yield (the time decay a yield seller collects).
CheckIcon is the one glyph at stroke 2: it renders at 9px inside a 14px checkbox (the filter checkbox, below) and would vanish at 1.6. The sun shows while in dark mode (click switches to light), the moon while in light. The gear is a rim and then the teeth — the hub-and-spokes it replaced was pixel for pixel the sun.
The trends carry --up / --down where they mean direction (the Direction toggle). The star is outlined until watched, then filled in --star; the wallet's dot is the set's one filled detail inside a stroked glyph.
Reset selection, reset the time range, zones panel visible / hidden (the icon carries the current state; the struck eye's top arc sits at opacity 0.55), and the six-dot grab handle — the one glyph with no stroke at all.
Laws of the terminal
Harvested from the codebase's own comments (/design §12) — every one is enforced by convention, not by a linter. Rendered as /design lists them; four have drifted from the round-12 brief and are flagged under their entry.
- 01Mono for numerals only.IBM Plex Mono renders prices, scores, countdowns, timestamps — never a heading, label, or button.Stale since round 12: BRAND.md replaced IBM Plex Mono with Inter's tabular figures (font-num); "a real monospace survives only for literal code". The law's intent — numerals in a fixed-width figure, never as chrome — still holds.
- 02No gradients, no emoji.Flat fills everywhere, including the gold-ingot glyph's two-tone bevel standing in for a gradient.
- 0314px is the content floor.--fs-sm (14px) is the minimum size for real content; only micro chip/tag/eyebrow chrome goes under it.Stale: on the round-12 scale
--fs-smis — and--fs-baseis the 14px body default; globals.css now puts the content floor at "13–14px". - 04Hierarchy by tone, not borders.The page → panel → row → hover tone scale is the only separator mechanism for cells, rows, and panels.Softened by round 12: a card now also carries a hairline border — "tone for hierarchy, border for edges" (BRAND.md Rule 3, Card.tsx).
- 05Color = data semantics, not decoration.up/down is market direction only; accent is interactive/today/selection only; category colors are assigned once and never invented ad hoc."today" no longer belongs in the accent's list: the TODAY block moved to the signal yellow (founder 2026-08-16 round 2, item 9), as /design's own Semantic section says two sections earlier.
- 06Hover = background only.No hover rings or outlines — a background-tone step is the entire affordance. The only outline in the system is *:focus-visible.
- 07Emphasis follows events, not position.A calendar day's numeral weight is driven by whether it has events — not by weekday, column, or row.
- 08One mutually-exclusive background class per element.Never layer two conditional bg-* utilities — Tailwind's stylesheet order, not cn()'s concatenation order, decides which wins.
- 09The accent is exact.— — never substitute a close-enough periwinkle or blue.
- 10Solid blocks over outlines.Category/status chips are solid fills with computed contrast text — no leading color dot, no outline-only chip.