Quantbot Design System

Dark-first, blue-on-near-black system for an algorithmic-trading product family: a Tailwind v4 marketing site with an 8-bit pixel wordmark, and — the focus of this spec — a companion analytics dashboard portal built as a widget grid (stat tiles, equity/returns charts, progress trackers) on a shadcn-style component vocabulary. This page is the visual regression test for the JSON spec: every component and every meaningful state below is rendered as a live specimen on the actual design-system surfaces.

Read Signature moves first — those 6 moves are what separate this system from generic AI UI. Get them right before anything else. Then resolve Tokens, Type ramp, Motion, Elevation, Iconography; those are the atoms. For a dashboard build: Layout & grid + widget-container + the widget components + Data viz + the Account overview composition recipe.

At a glance

Platform
Tailwind v4 (oklch token base) + React 19 + Vercel
Marketing uses Inter; dashboard portal uses the ui-sans-serif system stack. Recommended consumer stack: Next.js 16 / React 19 / Tailwind v4 + react-grid-layout.
Primary surface
app (dashboard portal)
The login page renders the dashboard inside a tilted MacBook as its marketing visual.
App page
#020617 — slate-950
Cool near-black. Cards float on this with a 1px rim — no shadow.
Marketing page
#09090b — zinc-950
Warmer/neutral near-black; deliberately different from the app page.
Card surface
#18181b — zinc-900
The single most-used surface. surface-hover is one zinc step up (#27272a).
Accent
#2563eb — blue-600
The entire accent system. Nothing else is brand-colored; chart series are data.
Type
Inter (marketing) / ui-sans-serif (app)
Press Start 2P for the 8-bit wordmark ONLY.
Radii
6 / 8 / 12 / 16 / pill
Dashboard widget radius 12px (10px in source portal). Buttons 8px.
Elevation
rim — inset 0 0 0 1px rgba(255,255,255,.06)
Rims, not drop shadows. Modals/popovers are the only exception.
Dashboard grid
12 columns · 16px gap · 32px container padding
stat-tile span-4 · area-chart span-8 · bar-chart span-6 · tracker span-12 · status-card span-4.
Languages
es (source copy is Spanish)
Sentence case headings/buttons. No exclamation marks, no hype words.
Theme
Dark only
Two cool near-blacks: deep slate page under zinc cards.

Signature moves

The six moves that make this system itself. Violating any one of these breaks the system's identity even if individual screens "look fine".

1 · Rim, not shadow

Cards/widgets/inputs/modals are bounded by a 1px white-at-6% inset border on a surface exactly one step darker than its background — no drop shadow. What breaks if you skip it: add box-shadows to cards and the dashboard reads like a default Material/Bootstrap admin template — the #1 tell of generic AI UI here.

right — rim
Equity
$12,701.19
wrong — drop shadow
Equity
$12,701.19

2 · Top-right period-selector pill

A small dark pill (border-rim bg-white/3 px-3 py-1.5 text-[11px] + a trailing ChevronDown) pinned top-right of every chart/metric/tracker widget header. Skip it and widgets stop rhyming — the grid looks assembled from a parts bin.

Monthly returns
Net return per month

3 · Two cool near-blacks

The dashboard page is slate-950 #020617 (faint blue cast); cards are zinc-900 #18181b (neutral). Two distinct near-blacks, not one — and neither is pure #000. Skip it and the rim disappears into the background; use #000 and it looks like an OLED test card.

page #020617
surface #18181b

4 · Skeleton, never spinner

Loading states keep the widget's header and rim intact and replace the body with pulsing zinc-800 placeholder blocks. No centered spinners, no "Loading…" text. Skip it and a centered spinner inside a dark card screams "unfinished admin tool" and causes layout jump.

right — skeleton
Recent trades
wrong — spinner
Recent trades

5 · Flat — no scale-on-hover

Interactive surfaces respond to hover with a background-color lift only (surface → surface-hover, 120ms). No scale(), no translateY on widgets/rows/buttons. (Marketing service cards are the lone, subtle exception: a 2px lift.) Skip it and bouncy hover transforms read as "demo", not "platform".

default
Running
hover — bg lift only
Running

6 · One saturated royal-blue

#2563eb (blue-600) is the entire accent system — buttons, links-as-action, active nav/tab, focus rings, progress fill, primary chart line, eyebrow accents. Multi-series charts rotate through 6 hues but those are data, never reused as UI chrome. Skip it — introduce a second accent and the hierarchy collapses.

Saber más

Tokens

Two cool near-blacks are deliberate: the dashboard page is slate-950 (#020617, faint blue cast) and cards are zinc-900 (#18181b, neutral). The marketing site uses warmer zinc-950 (#09090b) as page. surface-hover is always one zinc step up from the surface it lifts from. The rim is the elevation primitive — a 1px white-at-6% inset border — used instead of drop shadows. Every value below is a resolved Tailwind v4 canonical token; oklch is the framework's canonical form, hex/rgb are the resolved equivalents.

Color roles — page & surfaces

page
slate-950
#020617 · oklch(12.9% 0.042 264.7)
Dashboard / app page background — the deepest layer; cards float on this.
Coolest near-black; the faint blue cast ties it to the brand without being colored.
app
page-marketing
zinc-950
#09090b · oklch(14.1% 0.005 285.8)
Marketing-site page background.
Warmer/neutral near-black. Marketing reads neutral, app reads cool — on purpose.
website
page-sunken
#050507
Bottom of the marketing hero radial gradient; the darkest point on screen.
page-marketing minus a hair of lightness.
website
surface
zinc-900
#18181b · oklch(21% 0.006 285.9)
Every widget / card / panel body in the dashboard; also marketing card surface.
The single most-used surface. Sits on page (#020617) with a rim.
appwebsite
surface-2
#101012
Marketing card / modal body — a half-step darker than surface.
Between page-marketing and surface.
website
surface-3
#131313
VPS-landing card surface and comparison-table body on marketing.
≈ surface-2 + a hair.
website
surface-hover
zinc-800
#27272a · oklch(27.4% 0.006 286)
Hover state for any surface that lifts (widgets, list rows, nav items, dropdown items).
Exactly one zinc step above surface. Re-derive on any base as "surface +1 step".
appwebsite
surface-overlay
rgba(255,255,255,0.05)
Inset fields / wells: form inputs in the dashboard, inner panels, the "sunken" look.
white @ 5% over whatever surface it's on.
app
scrim
rgba(0,0,0,0.7)
Modal / lead-capture backdrop.
black @ 70%.
appwebsite

Rims & dividers

rim
rgba(255,255,255,0.06)
THE elevation primitive — a 1px border (often as inset 0 0 0 1px) on every card, widget, input, dropdown, modal. Replaces drop shadows.
white @ 6%.
appwebsite
rim-hover
rgba(255,255,255,0.10)
Card/input border on hover (marketing) and default input border.
white @ 10%.
appwebsite
rim-strong
rgba(255,255,255,0.20)
Marketing card border on hover/active; the most prominent hairline.
white @ 20%.
website
divider
slate-800 / gray-800
#1f2937 · oklch(27.8% 0.033 256.8)
Solid hairlines in the dashboard: table row dividers, tab-pill track, list separators, input borders in the portal.
A cool dark gray; reads as a line, not a fill. Distinct from the white-alpha rim.
app
divider-soft
rgba(255,255,255,0.05)
Faintest hairline — between marketing cards, comparison-table rows, FAQ rows.
white @ 5%.
website

Ink scale (text)

ink
gray-50
#f9fafb · oklch(98.5% 0.002 247.8)
Dashboard high-emphasis text: widget titles, metric values, page titles.
Near-white with a faint cool cast. Marketing uses zinc-50 (#fafafa) — interchangeable.
appwebsite
ink-2
zinc-200
#e4e4e7 · oklch(92% 0.004 286.3)
Secondary copy that still reads as body text: nav links, table cell values, paragraph copy on dark.
One zinc step below ink. USE THIS, not ink-muted, for body copy below ~16px.
appwebsite
ink-muted
zinc-400
#a1a1aa · oklch(70.5% 0.015 286.1)
Labels, captions, axis ticks, widget subtitles, footer links, period-pill text.
The default "quiet" text color. Borderline for AA at small sizes — fine for labels/ticks/large text.
appwebsite
ink-faint
zinc-500
#71717a · oklch(55.2% 0.016 285.9)
De-emphasized chrome: sidebar section labels, modal close icon, disabled-adjacent hints.
appwebsite
ink-disabled
zinc-600
#52525b · oklch(44.2% 0.017 285.8)
Placeholder text in inputs; disabled control labels.
appwebsite

Brand family

brand
blue-600
#2563eb · oklch(54.6% 0.245 262.9)
The single accent. Primary buttons, active tab/nav, focus rings, links-as-action, progress fill, chart primary line, eyebrow accent, modal CTA.
Tailwind blue-600 exactly. Nothing else is brand-colored — chart series are data.
appwebsite
brand-hover
blue-700
#1d4ed8 · oklch(48.8% 0.243 264.4)
Hover state of any brand-filled element.
brand minus ~6% lightness.
appwebsite
brand-pressed
blue-800
#1e40af · oklch(42.4% 0.199 265.6)
Active/pressed state of a brand-filled element.
brand-hover minus another step.
appwebsite
brand-soft
rgba(37,99,235,0.18)
Subtle brand wash: icon tiles, active-nav background tint, hero glow blocks, badge backgrounds.
brand @ 18%. Also @ 8% for the faintest active-row tint; 0.45 for the hero globe glow center.
appwebsite
link
blue-500
#3b82f6 · oklch(62.3% 0.214 259.8)
Inline text links and link-hover in the dashboard portal; also the certificate-progress fill in some charts. A touch lighter than brand for legibility as text.
brand + one step lighter.
appwebsite

Status

ok
green-500
#22c55e · oklch(72.3% 0.219 149.6)
Positive deltas (+%), completed progress, comparison-table checks, "online" status dot, trend-up icons.
As text: large/icon use only — for small positive numbers keep ≥14px medium.
ok-soft
rgba(34,197,94,0.12)
Success badge / pill background.
warn
yellow-400
#facc15 · oklch(85.2% 0.199 91.9)
Caution: "medium / high risk" badge on algorithm cards, 5-star rating fill, attention banners.
Tailwind yellow-400 (400, not 500 — slightly brighter).
warn-soft
rgba(250,204,21,0.15)
Warning badge background.
danger
red-500
#ef4444 · oklch(63.7% 0.237 25.3)
Negative deltas (-%), drawdown numbers, comparison-table crosses, destructive actions, error states, trend-down icons.
As text on dark: large/icon use; for body errors pair with an icon or use a lighter red.
danger-soft
rgba(239,68,68,0.15)
Error/destructive badge background; error-state body tint.
info
blue-500
#3b82f6
Informational badges/dots. Same value as link — the system reuses the lighter blue for "info". There is no separate info hue.
input-border
rgba(255,255,255,0.15)
Marketing form-input border (resting).
website
input-border-app
rgba(255,255,255,0.08)
Dashboard form-input border (resting).
app

Chart-series rotation

Ordered rotation for multi-series bar/line charts (e.g. monthly-returns bars). First series is brand/link blue; the rest are evenly-spaced hues. These are data colors, never brand — never restyle a button or nav with a chart-series color. Single series → #3b82f6 line/bars; area charts use the brand@18% → transparent gradient under a 2px #3b82f6 line.

1 · blue
#3b82f6
2 · green
#22c55e
3 · orange
#f97316
4 · purple
#a855f7
5 · pink
#ec4899
6 · yellow
#facc15
area-fill
brand@18%→0

Radii

Three structural stops + a pill. The source dashboard portal uses 10px for cards (shadcn's --radius default territory); the recommended canonical value for new builds is 12px (lg / widget). Don't "split the difference" to arbitrary values — pick from this set.

6
sm — inputs, badges, small chips, active tab in a track
8
md — buttons (all sizes), marketing cards, dropdown panels, tab track
12
widget / lg — dashboard widget · metric tile · chart card · modals · comparison table (10px matches the live portal)
16
xl — sticky CTA banner, largest container blocks
pill
pill — tag chips, risk badges, progress tracks/fills, status dots-with-label, avatar

Spacing scale

Tailwind 4px base unit. Internal widget padding lives at 16–20px (px-5 / py-4..py-5). The dashboard grid gap is 16px; container padding 32px. Marketing section padding ramps 48 → 80 → 96px (mobile → tablet → desktop). widgetHeader: title→subtitle 2px, header→body 16px (gap-4). nav-link gap 28px; icon→text 8px.

1
4
1.5
6
2
8
3
12
4
16
5
20
6
24
8
32
12
48
16
64
20
80
24
96

Border widths · z-index · breakpoints

Border widths

Tokenpx
hairline1

Everything is 1px. The focus "ring" is a 2px box-shadow ring (ring-2), not a border-width change.

Breakpoints (Tailwind defaults)

NamepxEffect
sm640below: single-col dashboard grid
md768dashboard becomes a 6-col layout; sidebar → off-canvas drawer
lg1024full 12-col dashboard grid; spans as in layout.dashboard.grid.spans.desktop
xl1280marketing content max-width

Z-index layers

Layerz
base content0
sticky table header / sidebar10
dropdown / popover / tooltip30
lead-capture modal (marketing) / app modal40
sticky top nav50
toast region60
design-system page toolbar (this doc only)1000

Type ramp

~26 type roles. Marketing uses Inter; the dashboard portal uses the ui-sans-serif system stack. Press Start 2P (alt VT323) is used for the 8-bit brand wordmark only — never for UI text. Specimens below are rendered at the exact size/weight/leading/tracking for each role.

SpecimenRolepx / weight / line / trackingColor roleContextUse
Gestión de capitalhero-display48 / 700 / 1.1 / -0.02eminkwebsiteMarketing hero H1.
QUANTBOTdisplay-pixel-wordmark20 / 400 / 1.4 / 0.4em · uppercase · 'Press Start 2P'inkbothThe 8-bit brand wordmark ONLY. Never UI text.
Account overviewsection-h136 / 700 / 1.15 / -0.02eminkbothMarketing section H1; dashboard page title (portal renders ~30px).
Account overviewpage-title-app30 / 700 / 1.2 · ui-sans-serifinkappDashboard page heading ("Account overview", "Settings").
Gestión de cuenta realsection-h230 / 700 / 1.2 / -0.01eminkwebsiteService-card titles, large sub-headings.
Card sub-headingsection-h324 / 600 / 1.3inkbothCard sub-headings, sticky CTA headline, modal title (large).
Equity curvewidget-title17 / 600 / 1.35 · ui-sans-serifinkappTitle in a widget/card header (16–18px OK; 17 recommended).
Payment status overview for last 6 monthswidget-subtitle12 / 400 / 1.4 · ui-sans-serifink-mutedappOptional one-line caption under a widget title.
+$2,156.92metric-value26 / 700 / 1.1 · tabular figuresinkappBig number in a stat tile (22–28px; 26 recommended).
+12% from last monthmetric-delta-chip11 / 500 / 1.3ok | danger | ink-mutedapp"+12% from last month" under the value. Green/red/neutral by sign.
Symboltable-header12 / 600 / 1.3 / 0.05em · uppercaseink-mutedappColumn headers in a data table / list card.
EUR/USD · long · 0.50 lottable-cell14 / 400 / 1.5 · tabular for numericink-2appBody cells in a table / list row. Numeric cells right-aligned + tabular.
Jan · Feb · Maraxis-tick11 / 400 / 1ink-mutedappChart axis labels (month names, y-axis values).
Dashboardnav-label-app14 / 500 / 1.4ink-2 (ink/brand when active)appSidebar nav item label.
Overviewsidebar-section-label10 / 600 / 1.3 / 0.08em · uppercaseink-faintappGroup heading inside the sidebar ("OVERVIEW", "ACCOUNTS").
Serviciosnav-link-marketing14 / 500 / 1.4ink-2 (ink on hover)websiteTop-nav links on the marketing site.
Iniciar sesiónbutton-md14 / 500 / 1white on brand / ink on ghostbothDefault button label (10/20px padding, 40px min-height).
Contratar ahorabutton-lg16 / 600 / 1white on brandbothLarge CTA (14/28px padding, 48px min-height).
tucorreo@ejemplo.cominput-text14 / 400 / 1.4ink (placeholder: ink-disabled)bothText typed into form fields (12/16px padding, 44px min-height).
Emaillabel14 / 500 / 1.4ink-2bothForm field label ("Email", "Contraseña").
Riesgo altobadge12 / 500 / 1matches badge tonebothPill badge text (risk tags, status pills).
Hero subtitle paragraphbody-lg18 / 400 / 1.55ink-2websiteHero subtitle, primary supporting paragraph.
Default body copy on dark surfacesbody16 / 400 / 1.5ink-2bothDefault body copy.
Footer copy, fine printsmall14 / 400 / 1.5ink-mutedbothFooter copy, fine print, secondary table copy.
Beneficios destacadoscaption / eyebrow12 / 500 / 1.4 / 0.025emink-muted (or brand for accent eyebrows)bothEyebrow labels above section titles; meta captions.
Last 6 monthsperiod-pill-text11 / 500 / 1ink-mutedappText inside the period-selector pill.

Motion

standard easing (cubic-bezier(0.4,0,0.2,1), Tailwind's default ease) for ~everything; emphasized (cubic-bezier(0.2,0,0,1)) only for entrances like the lead-capture modal.

Interaction classDurationEasingPropertiesNotes
hover / press feedback120msstandardbackground-color, border-color, color, opacityWidgets, list rows, nav items, buttons. NO transform — see signatures.
color / background transition200msstandardbackground-color, color, border-color, box-shadowTailwind transition-colors default. The general-purpose transition.
panel / drawer / accordion320msstandardmax-height, transform, opacitySidebar drawer (mobile); FAQ accordion expand (chevron rotates 180°); off-canvas panels.
modal entrance320msemphasizedopacity, transformLead-capture modal fades in + slides down ~10px. Exit is faster (~200ms, standard).
button press120msstandardbackground-color, box-shadowBrand buttons darken to brand-pressed + gain an inset top highlight inset 0 1px 0 rgba(255,255,255,0.08).

Hover the chips — each runs its transition tier on a background-color lift.

120ms quick 200ms base 320ms slow 320ms emphasized (entrances)
Reduced motion. Honor prefers-reduced-motion: reduce: drop all transform/translate motion (modal slide, card lift), keep color/opacity transitions but cap them at ≤120ms, replace accordion max-height animation with an instant toggle, and never auto-animate charts on mount.

Elevation — rims, not shadows

This system uses rims, not drop shadows. A card is defined by a 1px white-at-6% inset border on a one-step-darker-than-its-neighbour surface — not by a shadow. Adding box-shadows to cards is the single fastest way to make this system look wrong. (Modals and dropdown panels are the only things that get a real shadow.)

rim — the elevation primitive
inset 0 0 0 1px rgba(255,255,255,.06)
or border: 1px solid rgba(255,255,255,.06) — equivalent
rim-hover (10%) — marketing card hover / default app input border
1px solid rgba(255,255,255,.10)
rim-strong (20%) — marketing card hover/active
1px solid rgba(255,255,255,.20)
WRONG — drop shadow on a card
box-shadow: 0 12px 32px rgba(0,0,0,.5)
Don't do this — reads as a generic admin template.

Exception shadows (the only real shadows in the system)

modal — shadow + rim
0 24px 64px rgba(0,0,0,.6),
0 0 0 1px rgba(255,255,255,.06)
macbook-mockup — login product shot
0 32px 64px rgba(0,0,0,.6)
focus-glow — focused/active hero CTA (use sparingly)
0 0 32px rgba(37,99,235,.35)
globe-glow — hero illustration glow
radial-gradient(ellipse 50% 50% at 50% 50%,
rgba(37,99,235,.45) 0%, rgba(37,99,235,0) 70%)

card-rim-gradient (optional top-lit refinement on hero cards): linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0)).

Iconography

Set: Lucide (outline) — alt Heroicons (outline), equivalent. Stroke width 1.5. Sizes 16 & 20px (16 = inline-with-text, table cells, chips, small buttons, period-pill chevron; 20 = nav items, larger buttons, card-header actions, status dots). Color inherits currentColor — defaults to ink-muted; goes ink or brand when active/hover; semantic colors (ok/warn/danger) when the icon carries status. Glyphs below are inline-SVG stand-ins; the canonical Lucide name is in the table.

GlyphMeaningLucide name + color
done / completeCheck or CheckCircle, colored ok #22c55e
failed / not-includedX or XCircle, colored danger #ef4444
warning / riskAlertTriangle, colored warn #facc15
infoInfo, colored info #3b82f6
external linkArrowUpRight (inline) or ExternalLink
expand / collapseChevronDown — rotates 180° on open (200ms standard)
period / date rangeChevronDown trailing the label inside the period-selector pill (Calendar optional leading)
trend upTrendingUp, colored ok
trend downTrendingDown, colored danger
more actionsMoreHorizontal (kebab is MoreVertical) — top-right of a widget when there's no period selector
closeX, colored ink-faint, 16px, top-right
searchSearch, leading inside a search input
settingsSettings (gear)
user / accountUser or CircleUser
navigate / link-arrowliteral '→' character used in marketing "Saber más →" affordances (not an icon font)

Layout & grid

The dashboard shell + the 12-col / 16px-gap widget grid math, and the marketing layout rhythm.

Dashboard shell

sidebar
width 240px (240–260) · bg page #020617 (or #0c0c0e) · right border 1px rgba(255,255,255,.06) · padding 28px 0
Wordmark/logo at top with a bottom hairline divider → groups of nav items, each preceded by a sidebar-section-label. Below md: off-canvas drawer (slide in from left, 320ms standard, scrim behind); hamburger in the top bar. Thin scrollbar, rgba(255,255,255,.18) thumb.
top-bar
height 64px · bg page (optionally page/85 + backdrop-blur-md if overlapping scroll) · bottom border 1px rgba(255,255,255,.05) · padding 0 32px · sticky · z-50
Left: page title / breadcrumb (or hamburger + title below md). Right: contextual actions — a ghost pill ("Contact support"), notification bell, account avatar/menu.
content
padding 32px (16px mobile) · max-width none — the widget grid fills available width · background page #020617

Grid math

PropertyValue
columns12
gap16px
container padding32px
row heightauto / content-driven (CSS grid auto rows). Chart cards enforce their own min-height (220–260px body); everything else is content height.
Widget typedesktop (lg+)tablet (md)mobile (sm-)Note
stat-tile43123-up at desktop; 2-up at tablet; stacked on mobile. Content height.
area-chart-card8 (alt 6)612Hero chart often span-8 next to a span-4 status card; or span-6 in a 2-up chart row. Fixed min-height.
bar-chart-card6612Fixed min-height (body 220–260px).
list-table-card6 (alt 12)612span-6 alongside another card, or span-12 for a primary table. Content height.
progress-tracker-card12612Almost always full-width — it's a list of labelled progress rows. Content height.
status-card4312Content height.

Collapse rules: ≥1024px (lg) → 12-col grid, spans as above. 768–1023px (md) → effectively a 6-col grid: stat-tiles 3-up become 2-up, chart cards go full-width-of-row, trackers/tables full width. <768px (sm and below) → single column, every widget full width in source order, charts keep their min-height; tables may switch to a stacked-row layout.

Drag/resize note. If you add a drag/resize layer (react-grid-layout), the widget's card chrome — rim, radius, header layout, padding, the period-selector pill — MUST be byte-identical between the static render and the editable render. The grid library only owns position/size; it must never restyle the card. Snap to the same 12-col / 16px-gap math.

12-column grid diagram

Row 1 — three stat-tiles @ span-4

stat-tile · span-4
stat-tile · span-4
stat-tile · span-4

Row 2 — primary chart @ span-8 + status @ span-4

area-chart-card · span-8
status-card · span-4

Row 3 — two cards @ span-6

bar-chart-card · span-6
list-table-card · span-6

Row 4 — full-width tracker @ span-12

progress-tracker-card · span-12

Marketing layout

container
max-width 1280px · padding-x 16 / 32 / 48px (mobile / tablet / desktop)
section rhythm
padding-y 48 / 80 / 96px (mobile / tablet / desktop)
nav
Sticky, height 64px, bg-page/85 backdrop-blur-md, 1px bottom hairline
Pixel wordmark left, link cluster center/right (28px gap), primary blue "client area" button far right.
grids
3-col card grids → 2-col at md → 1-col below sm
Comparison table is a 3-col grid (label | highlighted product | generic). Footer is 4 columns → stacks on mobile.
hero pattern
Centered: eyebrow chip → 48px headline → 18px subtitle → blue CTA
A persistent lead-capture modal absolutely positioned over the hero and a glowing globe/illustration behind.

Components

Every component from the JSON, rendered as a live specimen with its parts, dimensions, a demo of every state in its matrix (each state shown as its own labeled instance — you can't hover a static screenshot), and its codeAnchor (React + Tailwind v4). The dashboard widgets come first (the focus of this spec), then the shared controls, then the website components.

widget-container app

The universal dashboard primitive. Every chart/metric/tracker/list widget is this shell + a body. Cohesion across the grid comes from this being identical everywhere.

SlotToken
rootbg surface #18181b · rim 1px rgba(255,255,255,.06) · radius widget 12px (10px in source) · padding 16–20px · CSS grid item, gets grid-column: span N
headerflex row, items-start justify-between, gap 12px → titleBlock + controlSlot
header.titletypeRamp widget-title (17/600), color ink
header.subtitle (optional)typeRamp widget-subtitle (12/400), color ink-muted, margin-top 2–4px
header.controlSlotright-aligned; holds the period-selector-pill (default), a dropdown filter (list cards), or a kebab — pick exactly one. Prefer a context-label pill over an empty slot — the slot is load-bearing.
bodymargin-top 16px (gap-4 from header); the chart / metric / table / progress rows / empty-state / skeleton goes here
footer (optional)right-aligned link-arrow ("View all →") in brand; margin-top 16px; only on list/summary widgets

Dimensions: border-radius 12px (10px source) · border 1px rgba(255,255,255,.06) · bg #18181b · minHeight content-driven; chart variants 220–260px body min-height. Default grid span 6. Carries control: period-selector-pill.

default — bg #18181b · rim 6% · no shadow
Widget title
Optional one-line subtitle
hover — bg → surface-hover (only when whole widget is clickable; static widgets don't hover)
Widget title
focus-within — ring-2 brand/20 + border → brand (a control inside is focused)
Widget title
active / selected — border → brand + brand/8 fill
Widget title
loading — header intact, body → 2–4 animate-pulse zinc-800 blocks. No spinner.
Widget title
empty — header stays, body → centered empty-state. Never blank.
Widget title
No data yet
Populated once activity starts.
error — chrome UNCHANGED, body → danger-soft inline block + Retry. Do not turn the card red.
Widget title
Couldn't load this · Retry
disabled — opacity 0.5, pointer-events none (rare; widget gated behind a plan)
Widget title
function Widget({ title, subtitle, control, span = 6, footer, children }) {
  return (
    <section style={{ gridColumn: `span ${span} / span ${span}` }}
      className="rounded-[12px] border border-white/[0.06] bg-[#18181b] p-5
                 transition-colors duration-[120ms] ease-[cubic-bezier(.4,0,.2,1)]
                 focus-within:border-[#2563eb] focus-within:ring-2 focus-within:ring-[#2563eb]/20">
      <header className="flex items-start justify-between gap-3">
        <div>
          <h3 className="text-[17px] font-semibold leading-tight text-[#f9fafb]">{title}</h3>
          {subtitle && <p className="mt-0.5 text-[12px] text-[#a1a1aa]">{subtitle}</p>}
        </div>
        {control}
      </header>
      <div className="mt-4">{children}</div>
      {footer && <div className="mt-4 text-right">{footer}</div>}
    </section>
  );
}

period-selector-pill app

The signature affordance — a small dark dropdown trigger pinned top-right of data widgets. Often the only thing that makes the grid feel unified.

SlotToken
triggerinline-flex, items-center, gap 6px · border 1px rim · bg white/3 (rgba(255,255,255,.03)) · radius md 8px · padding 6px 12px (px-3 py-1.5) · text period-pill-text 11/500 ink-muted
labelthe current range ("Last 6 months", "Last 30 days", "YTD")
chevronChevronDown, 14px, ink-muted — rotates 180° when the menu is open (200ms)
menu→ dropdown-panel: opens below, right-aligned, ~160–200px wide

Dimensions: height ≈28px · min-width fit-content.

default — border rim 6% · text ink-muted
hover — border → rim-hover · text → ink-2 · 120ms
focus-visible — + ring-2 brand/20
open — chevron rotated 180° · border → rim-hover · menu visible (active item brand text)
disabled — opacity 0.5, no interaction (widget has a single fixed range)
<button type="button"
  className="inline-flex items-center gap-1.5 rounded-md border border-white/[0.06] bg-white/[0.03]
             px-3 py-1.5 text-[11px] font-medium text-[#a1a1aa] transition-colors duration-[120ms]
             hover:border-white/15 hover:text-[#e4e4e7] focus-visible:ring-2 focus-visible:ring-[#2563eb]/20">
  Last 6 months
  <ChevronDown className="size-3.5" />
</button>

stat-tile app

Compact KPI widget — a label, a big number, a signed delta chip. The atom of the metrics row. Uses widget-container chrome; usually no header title (the label IS the content); often carries no control or a tiny context label.

SlotToken
icon (optional)Lucide 16–20px in a 28–32px brand-soft rounded square (radius sm 6px), top-left or inline before the label
label12/500, ink-muted ("Total profit/loss", "Equity", "Monthly performance")
valuemetric-value 26/700 ink, tabular figures, margin-top 4px ("+$2,156.92", "$12,701.19", "+6.4%")
deltametric-delta-chip 11/500, color by sign — ok if ≥0, danger if <0, ink-muted if flat; margin-top 4px; "+12% from last month"
sparkline (optional)tiny inline area/line, ~32px tall, 1.5px link line, no axes — bottom of the tile

Dimensions: padding 16px 20px · grid span 4.

default — positive delta
Total profit / loss
+$2,156.92
+12% from last month
default — with icon, negative delta
Equity
$12,701.19
-2% from last month
default — with sparkline
Monthly performance
+6.4%
+5% from last month
hover — only if the tile is a link/drill; bg → surface-hover
Total profit / loss
+$2,156.92
+12% from last month
loading — value → w-24 h-7 pulse · delta → w-32 h-3 pulse
empty — value → "—" in ink-muted; delta hidden; optional caption
Total profit / loss
No data for this period
error — value → "—"; small danger AlertTriangle + "Couldn't load" where the delta would be
Total profit / loss
Couldn't load
<Widget title="" span={4} control={<PeriodPill label="This month" />}>
  <div className="text-[12px] font-medium text-[#a1a1aa]">Total profit / loss</div>
  <div className="mt-1 text-[26px] font-bold tabular-nums text-[#f9fafb]">+$2,156.92</div>
  <div className={`mt-1 text-[11px] font-medium ${delta >= 0 ? 'text-[#22c55e]' : 'text-[#ef4444]'}`}>
    {delta >= 0 ? '+' : ''}{delta}% from last month
  </div>
</Widget>

area-chart-card app

Single-series trend widget (equity curve, balance over time). The signature data viz: blue gradient fill under a 2px blue line.

SlotToken
root→ widget-container; header carries the period-selector pill; padding 20/20
bodyRecharts/visx AreaChart, min-height 220–260px, fills width
areafill: url(#brandGradient) — linear-gradient(180deg, rgba(37,99,235,0.18) → rgba(37,99,235,0)); stroke: #3b82f6; strokeWidth: 2; type: monotone
dotsnone by default; active dot on hover only — r:4, fill #3b82f6, stroke #18181b strokeWidth 2
gridnone, or horizontal-only at rgba(255,255,255,0.04), no vertical lines
axesx: month/period labels, axis-tick 11/ink-muted, no axis line, no tick line. y: optional, same treatment. Hide one axis if the chart is small.
tooltip→ dataViz.tooltip

Dimensions: grid span 8 (alt 6) · body min-height 240px.

default — equity curve, period pill "Last 6 months"
Equity curve
NovDecJanFebMarApr
hover — vertical reference line at cursor (rgba(255,255,255,.10), 1px) + active dot + tooltip
Equity curve
Mar
Equity$11,402
loading — header stays; body → single h-[220px] animate-pulse block
Equity curve
empty — body → empty-state: faint line-chart icon + "No activity in this period" + "Change range"
Equity curve
No activity in this period
Change range
error — body → danger-soft block: AlertTriangle + "Chart unavailable" + "Retry" link
Equity curve
Chart unavailable · Retry
<Widget title="Equity curve" span={8} control={<PeriodPill label="Last 6 months" />}>
  <div className="h-[240px]">
    <ResponsiveContainer>
      <AreaChart data={data} margin={{ top: 8, right: 8, bottom: 0, left: 0 }}>
        <defs>
          <linearGradient id="brandGradient" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor="#2563eb" stopOpacity={0.18} />
            <stop offset="100%" stopColor="#2563eb" stopOpacity={0} />
          </linearGradient>
        </defs>
        <XAxis dataKey="month" tick={{ fill: '#a1a1aa', fontSize: 11 }} axisLine={false} tickLine={false} />
        <YAxis hide />
        <Tooltip cursor={{ stroke: 'rgba(255,255,255,0.1)' }} content={<DarkTooltip />} />
        <Area type="monotone" dataKey="equity" stroke="#3b82f6" strokeWidth={2} fill="url(#brandGradient)" dot={false}
              activeDot={{ r: 4, fill: '#3b82f6', stroke: '#18181b', strokeWidth: 2 }} />
      </AreaChart>
    </ResponsiveContainer>
  </div>
</Widget>

bar-chart-card app

Categorical/periodic widget — e.g. monthly returns. Multi-color bars rotating through the chart-series palette (blue → green → orange → purple → pink → yellow, then wrap). Bars radius:[4,4,0,0] (rounded top only), category gap ~24–32%.

SlotToken
root→ widget-container; header carries title + (often) a subtitle + period pill
bodyRecharts BarChart, min-height 220–260px
barsvertical bars; each takes the next color in chartSeries.order; radius [4,4,0,0]; category gap ~24–32%
axesx: category labels (Jan, Feb…) axis-tick 11/ink-muted, no line. y: value labels with %/units, same treatment, no line; faint horizontal gridlines rgba(255,255,255,0.04) optional
tooltip→ dataViz.tooltip
legend (optional)only if categories aren't self-explanatory; top-right or below, 11px, colored dot + label

Dimensions: grid span 6 · body min-height 240px.

default — monthly returns, 6-color bar rotation
Monthly returns
Net return per month
NovDecJanFebMarApr
hover — hovered bar +12% lightness / 1px ink/20 stroke; tooltip; others unchanged
Monthly returns
Net return per month
Feb
Return+9.2%
loading — body → 6 animate-pulse columns of varied heights with rounded tops
Monthly returns
empty — body → empty-state: faint bar-chart icon + "Nothing to show yet"
Monthly returns
Nothing to show yet
error — body → danger-soft block + Retry link
Monthly returns
Couldn't load · Retry
<Widget title="Monthly returns" subtitle="Net return per month" span={6} control={<PeriodPill label="Last 6 months" />}>
  <div className="h-[240px]">
    <ResponsiveContainer>
      <BarChart data={data}>
        <CartesianGrid stroke="rgba(255,255,255,0.04)" vertical={false} />
        <XAxis dataKey="month" tick={{ fill: '#a1a1aa', fontSize: 11 }} axisLine={false} tickLine={false} />
        <YAxis tick={{ fill: '#a1a1aa', fontSize: 11 }} axisLine={false} tickLine={false} unit="%" />
        <Tooltip cursor={{ fill: 'rgba(255,255,255,0.04)' }} content={<DarkTooltip />} />
        <Bar dataKey="return" radius={[4, 4, 0, 0]}>
          {data.map((_, i) => <Cell key={i} fill={['#3b82f6','#22c55e','#f97316','#a855f7','#ec4899','#facc15'][i % 6]} />)}
        </Bar>
      </BarChart>
    </ResponsiveContainer>
  </div>
</Widget>

progress-tracker-card app

Full-width list of labelled progress rows (e.g. "Certificate progress tracker": Phase 1 target 100%, Phase 2 target 75%). Each row: label, bar, %, status glyph. span-12 typically.

SlotToken
root→ widget-container; header has title + subtitle + period pill
rowgrid/flex row, items-center, gap 16px, padding-y 12px, optional divider-soft hairline between rows
row.label14/500 ink-2 ("Phase 1 target (10%)")
row.barprogress bar — track surface-hover #27272a, height 6px, radius pill; fill brand #2563eb (or link #3b82f6); becomes ok #22c55e at 100%; the track flexes to fill available width
row.valueright-aligned, 12/500 ink ("100%", "75%"), tabular figures
row.statustrailing glyph — ok CheckCircle when complete; a brand/link badge with a small check when in progress; ink-faint Circle when not started

Dimensions: grid span 12 · row height ≈40px · bar height 6px.

default
Certificate progress tracker
Payment status overview for the last 6 months
Phase 1 target (10%)
100%
Phase 2 target (5%)
75%
Phase 3 target (5%)
20%
hover (row) — row bg → white/3, 120ms (only if rows are clickable)
Certificate progress tracker
Phase 1 target (10%)
100%
Phase 2 target (5%)
75%
loading — header stays; body → 3 rows of pulse (short label + long bar each)
Certificate progress tracker
empty — body → empty-state: "No milestones yet" + a one-liner
Certificate progress tracker
No milestones yet
Targets appear here once your plan is set up.
error — body → danger-soft block + Retry
Certificate progress tracker
Couldn't load · Retry
<Widget title="Certificate progress tracker" subtitle="Payment status overview for the last 6 months" span={12} control={<PeriodPill label="Last 6 months" />}>
  <ul className="divide-y divide-white/5">
    {rows.map(r => (
      <li key={r.id} className="flex items-center gap-4 py-3">
        <span className="w-48 shrink-0 text-[14px] font-medium text-[#e4e4e7]">{r.label}</span>
        <div className="h-1.5 flex-1 rounded-full bg-[#27272a]">
          <div className="h-full rounded-full" style={{ width: `${r.pct}%`, background: r.pct >= 100 ? '#22c55e' : '#2563eb' }} />
        </div>
        <span className="w-12 shrink-0 text-right text-[12px] font-medium tabular-nums text-[#f9fafb]">{r.pct}%</span>
        {r.pct >= 100 ? <CheckCircle className="size-4 text-[#22c55e]" /> : <Circle className="size-4 text-[#71717a]" />}
      </li>
    ))}
  </ul>
</Widget>

list-table-card app

Tabular widget — recent trades, open positions, transactions. Header row + data rows with subtle dividers; hover lift on rows; status pills inline. Control slot often a filter dropdown instead of a period pill; optional footer link "View all →".

SlotToken
header-rowtable-header 12/600 uppercase ink-muted, padding-y 8px, bottom border divider #1f2937
data-rowtable-cell 14/400 ink-2, padding-y 12px, bottom border divider-soft; numeric columns right-aligned + tabular figures; status column uses badge-pill
row-hoverbg → surface-hover #27272a, 120ms
empty-rowsingle full-width cell → empty-state inline

Dimensions: grid span 6 (alt 12) · row height ≈44px.

default + hover row + selected row + status pills + footer link
Recent trades
SymbolTypeSizeP/LStatus
EUR/USDLong0.50+$182.40Closed
GBP/JPYShort0.25-$54.10Open
XAU/USDLong0.10+$612.00Open
USD/CADShort0.40+$98.75Closed
loading — header row stays; 5 data rows → animate-pulse cells
Recent trades
SymbolTypeSizeP/LStatus
empty — header row stays; body → empty-state ("No trades yet" + "They'll appear here once your bots execute.")
Recent trades
SymbolTypeSizeP/LStatus
No trades yet
They'll appear here once your bots execute.
error — body → danger-soft block + Retry
Recent trades
Couldn't load · Retry
<Widget title="Recent trades" span={6} control={<FilterDropdown />} footer={<a className="text-[14px] font-medium text-[#2563eb] hover:text-[#3b82f6]">View all →</a>}>
  <table className="w-full text-left">
    <thead>
      <tr className="border-b border-[#1f2937]">
        {['Symbol','Type','Size','P/L','Status'].map(h =>
          <th key={h} className="py-2 text-[12px] font-semibold uppercase tracking-wider text-[#a1a1aa]">{h}</th>)}
      </tr>
    </thead>
    <tbody>
      {rows.map(r => (
        <tr key={r.id} className="border-b border-white/5 transition-colors duration-[120ms] hover:bg-[#27272a]">
          <td className="py-3 text-[14px] text-[#e4e4e7]">{r.symbol}</td>
          <td className="py-3 text-[14px] text-[#e4e4e7]">{r.side}</td>
          <td className="py-3 text-right text-[14px] tabular-nums text-[#e4e4e7]">{r.size}</td>
          <td className={`py-3 text-right text-[14px] tabular-nums ${r.pnl >= 0 ? 'text-[#22c55e]' : 'text-[#ef4444]'}`}>{r.pnl}</td>
          <td className="py-3"><Badge tone={r.status === 'open' ? 'info' : 'ok'}>{r.status}</Badge></td>
        </tr>
      ))}
    </tbody>
  </table>
</Widget>

status-card app

Single-fact widget — the state of one thing (bot running / paused / error; broker connected). A big colored dot + a label + a supporting line. span-4; usually no control, maybe a kebab.

SlotToken
indicator8–10px dot, ok (running/online) / warn (paused/degraded) / danger (error/offline) / ink-faint (idle); optionally with a soft halo <tone>-soft
status-textwidget-title 17/600 ink ("Running", "Paused")
supportingsmall 14/400 ink-muted ("3 bots active · last trade 4m ago")
action (optional)a small ghost button or link-arrow ("Pause", "Reconnect")

Dimensions: grid span 4 · padding 16px 20px.

default — running (ok dot + halo)
Running

3 bots active · last trade 4m ago

paused (warn dot)
Paused

Resume to keep trading Resume

error / offline (danger dot)
Broker disconnected

Last seen 12m ago Reconnect

idle (ink-faint dot)
Idle

No active strategies

hover — if clickable → bg surface-hover
Running

3 bots active · last trade 4m ago

loading — dot → ink-faint; status-text → w-20 h-4 pulse; supporting → w-32 h-3 pulse
error (the data, not the state) — dot danger; status-text "Status unknown"; supporting "Couldn't reach the service" + Retry link
Status unknown

Couldn't reach the service · Retry

<Widget title="" span={4}>
  <div className="flex items-center gap-2">
    <span className="size-2.5 rounded-full bg-[#22c55e] shadow-[0_0_0_4px_rgba(34,197,94,0.12)]" />
    <span className="text-[17px] font-semibold text-[#f9fafb]">Running</span>
  </div>
  <p className="mt-1 text-[14px] text-[#a1a1aa]">3 bots active · last trade 4m ago</p>
</Widget>

sidebar app

Fixed left navigation column for the dashboard. Wordmark + grouped nav items under uppercase section labels.

SlotToken
rootfixed, width 240px, height 100vh, bg page (#020617) or #0c0c0e, right border 1px rim, padding 28px 0, overflow-y auto
logopadding 0 22px 22px, bottom hairline rim; the pixel wordmark in display-pixel-wordmark type (uppercase, 0.4em tracking), optionally a tiny ink-muted subtitle below
groupvertical stack; preceded by a sidebar-section-label (10/600 uppercase ink-faint, padding 16px 22px 6px)
nav-item→ sidebar-nav-item

States: default; collapsed (responsive < md) → translateX(-100%) off-canvas, opens as a drawer over a scrim, 320ms standard, hamburger toggle; scrolled → internal scroll only, logo block can stay pinned.

sidebar — default + an active item + a hover item + an item with a count badge

Overview
Dashboard Accounts Performance
Trading
Bots 3 Trades
Settings
Settings
<aside className="fixed inset-y-0 left-0 w-60 overflow-y-auto border-r border-white/[0.06] bg-[#020617] py-7">
  <div className="border-b border-white/[0.06] px-[22px] pb-[22px]">
    <span className="font-[var(--font-display)] text-base uppercase tracking-[0.4em] text-[#f9fafb]">QUANTBOT</span>
  </div>
  <nav className="mt-4">
    <div className="px-[22px] pb-1.5 pt-4 text-[10px] font-semibold uppercase tracking-[0.08em] text-[#71717a]">Overview</div>
    <SidebarItem icon={LayoutDashboard} active>Dashboard</SidebarItem>
    <SidebarItem icon={Wallet}>Accounts</SidebarItem>
  </nav>
</aside>

sidebar-nav-item app

Block, padding 7px 22px, left 2px transparent border, text nav-label-app 14/500; icon Lucide 20px (or 16px), ink-muted (→ ink/brand when active/hover), 8px from label; optional trailing count pill in brand-soft/brand text; optional indented sub-item (padding-left 38px, 12px, ink-faint).

StateDelta
defaulttext rgba(255,255,255,.72) (ink-2-ish); icon ink-muted; transparent left border; transparent bg
hovertext → ink; bg → white/4 (rgba(255,255,255,.04)); 120ms
active / currenttext → ink; left border → 2px brand; bg → brand/8 (rgba(37,99,235,.08)); icon → brand (or stays ink)
focus-visible+ ring-2 brand/20 inset
default Trades hover — text → ink, bg → white/4 Trades active — left 2px brand border, brand/8 fill, brand icon Trades focus-visible — + ring-2 brand/20 inset Trades with count badge / with sub-item Bots 3 Scalper EU Swing US
<a href="..." className={`flex items-center gap-2 border-l-2 px-[22px] py-[7px] text-[14px] font-medium transition-colors duration-[120ms]
  ${active ? 'border-[#2563eb] bg-[#2563eb]/[0.08] text-[#f9fafb]' : 'border-transparent text-white/70 hover:bg-white/[0.04] hover:text-[#f9fafb]'}`}>
  <Icon className={`size-5 ${active ? 'text-[#2563eb]' : 'text-[#a1a1aa]'}`} />
  {children}
</a>

top-bar app

sticky top-0 z-50, height 64px, bg page (optionally page/85 + backdrop-blur-md), bottom border 1px divider-soft, padding 0 32px, flex items-center justify-between. Left: hamburger (< md only) + page title / breadcrumb. Right: a ghost pill button ("Contact support"), an icon button (Bell), an avatar/account menu trigger.

Dimensions: height 64 · padding-x 32.

default
Account overview
scrolled-over-content — bg → page/85 + backdrop-blur-md; bottom hairline a touch more visible
Account overview
<header className="sticky top-0 z-50 flex h-16 items-center justify-between border-b border-white/5 bg-[#020617] px-8">
  <h1 className="text-[18px] font-semibold text-[#f9fafb]">Account overview</h1>
  <div className="flex items-center gap-3">
    <button className="rounded-md border border-white/10 px-3 py-1.5 text-[13px] font-medium text-[#e4e4e7] hover:bg-white/5">Contact support</button>
    <button className="rounded-md p-2 text-[#a1a1aa] hover:bg-white/5"><Bell className="size-5" /></button>
    <button className="size-8 rounded-full bg-[#27272a]" />
  </div>
</header>

tab-pill-switcher app

Segmented control — a track holding 2+ tabs; the active tab is a filled brand pill inside the track. Used for the login/register switcher and for in-page view toggles. Track: bg divider #1f2937 · radius md 8px · padding 4px. Tab: padding 8px 16px · radius sm 6px · 14px. Active: bg brand #2563eb · text white · weight 600. Inactive: transparent · text ink-muted (→ ink on hover) · weight 500.

default — left tab active
inactive:hover — text → ink + faint white/4 bg
focus-visible — ring-2 brand/40 on the focused tab
disabled — tab opacity 0.4, no interaction
3-tab in-page view toggle
<div className="grid grid-cols-2 rounded-md bg-[#1f2937] p-1">
  <button className="rounded-sm bg-[#2563eb] px-4 py-2 text-[14px] font-semibold text-white">Iniciar sesión</button>
  <button className="rounded-sm px-4 py-2 text-[14px] font-medium text-[#a1a1aa] hover:text-[#f9fafb]">Crear cuenta</button>
</div>

dropdown-panel app website

The floating menu surface used by the period-selector pill, filter dropdowns, account menu, kebab menus. Panel: bg surface #18181b · border 1px rim · radius md 8px · padding 4px · shadow 0 24px 64px rgba(0,0,0,.6) (the modal-grade shadow — popovers get it too) · min-width 160px. Item: flex items-center gap 8px · padding 8px 12px · radius sm 6px · 14px ink-2 · optional leading icon 16px ink-muted. Active-item: text → brand (or a brand/10 fill row); optional trailing Check brand. Separator: 1px divider-soft, margin 4px 0. Section-label: 10/600 uppercase ink-faint.

open — default + a hovered item + a selected/checked item
Period
account menu — with leading icons + separator
<div className="min-w-40 rounded-md border border-white/[0.06] bg-[#18181b] p-1 shadow-[0_24px_64px_rgba(0,0,0,0.6)]">
  {options.map(o => (
    <button key={o.id} className={`flex w-full items-center justify-between rounded-sm px-3 py-2 text-[14px] hover:bg-[#27272a]
      ${o.id === value ? 'text-[#2563eb]' : 'text-[#e4e4e7]'}`}>
      {o.label}{o.id === value && <Check className="size-4 text-[#2563eb]" />}
    </button>
  ))}
</div>

modal app website

Centered dialog over a scrim. Scrim: fixed inset-0, bg rgba(0,0,0,.7), z-40. Dialog: centered, bg surface-2/surface (#101012 / #18181b) · border 1px rim · radius lg 12px · padding 24px · shadow 0 24px 64px rgba(0,0,0,.6), 0 0 0 1px rgba(255,255,255,.06) · width ~320–480px. Close: top-right X, 16px, ink-faint, hover ink. Title: 16/500 (or section-h3 24/600 for larger). Actions: right-aligned row — a ghost (cancel) + a primary/primary-lg (confirm); destructive confirm uses destructive. Entering: scrim fades 0→0.7; dialog opacity 0→1 + translateY +10px→0, 320ms emphasized. Exiting: reverse, faster ~200ms standard. Focus trapped; Esc closes; body scroll locked.

open — confirmation modal (ghost cancel + primary confirm)
Pause all bots?
Open positions stay open. You can resume any time.
open — destructive (ghost-style danger confirm naming the consequence)
Delete account
This permanently removes your data and bots.
marketing lead-capture special case — centered card, full-width primary CTA (see lead-capture-modal)
Solicítalos ya
Recibe los resultados verificados en tu correo.
<div className="fixed inset-0 z-40 grid place-items-center bg-black/70">
  <div className="relative w-80 rounded-[12px] border border-white/8 bg-[#101012] p-6 text-center shadow-[0_24px_64px_rgba(0,0,0,0.6)]">
    <button className="absolute right-4 top-4 text-[#71717a] hover:text-[#f9fafb]"><X className="size-4" /></button>
    <h2 className="text-[16px] font-medium text-[#f9fafb]">Solicítalos ya</h2>
    <button className="mt-4 w-full rounded-md bg-[#2563eb] py-3 text-[16px] font-semibold text-white hover:bg-[#1d4ed8]">Solicitar</button>
  </div>
</div>

toast app website

Transient notification — bottom-right (or top-right), auto-dismiss, dark with a status-colored accent. Container: fixed, z-60, stacking from bottom-right (gap 12px). Toast: bg surface #18181b · border 1px rim · radius md 8px · padding 12px 16px · shadow 0 24px 64px rgba(0,0,0,.6) · max-width ~360px · flex gap 12px. Icon: 16–20px status icon. Text: title body 14/600 ink + optional description 13/400 ink-muted. Accent: a 3px left border in the status color, OR an accent on the icon only. Entering: slide in from the right + fade, 200ms standard. Shown 4–6s; pauses on hover. Stacked: older toasts shift up; max ~3 visible.

ok (CheckCircle)
Settings saved
Your bot configuration was updated.
danger (XCircle)
Couldn't reconnect broker
Check your API credentials and try again.
warn (AlertTriangle)
High-risk algorithm enabled
Drawdown limits are looser on this strategy.
info (Info) — icon-accent only, no description
New monthly report available
<div className="flex max-w-sm items-start gap-3 rounded-md border border-white/[0.06] bg-[#18181b] p-3 pr-4 shadow-[0_24px_64px_rgba(0,0,0,0.6)]">
  <CheckCircle className="mt-0.5 size-5 text-[#22c55e]" />
  <div>
    <p className="text-[14px] font-semibold text-[#f9fafb]">Settings saved</p>
    <p className="text-[13px] text-[#a1a1aa]">Your bot configuration was updated.</p>
  </div>
</div>

button app website

One filled brand variant (two sizes), one ghost variant, one link-arrow, one icon button, one destructive. Radius 8px (md) on all. Min touch target 44px on mobile, 36–40px desktop. The full variant × state matrix below.

VariantSpec
primarybg brand #2563eb · text white · padding 10px 20px · radius md 8px · type button-md 14/500 · min-height 40px (44 mobile). hover → brand-hover #1d4ed8 (200ms); active → brand-pressed #1e40af + inset top highlight; focus-visible → outline 2px brand offset 2px (or ring-2 brand/40); disabled → brand@40% (or #27272a), text ink-disabled; loading → 16px spinner (the ONE place a small inline spinner is allowed), width preserved, disabled
primary-lgas primary but padding 14px 28px · type button-lg 16/600 · min-height 48px. Use: modal CTA, VPS "Contratar ahora", hero CTA
ghostbg transparent · text ink · border 1px input-border (rgba(255,255,255,.15)) · padding 10px 20px · radius md 8px. hover → border rim-strong 20–30% + bg white/4; active → bg white/8; focus-visible → ring-2 brand/40; disabled → opacity 0.4. (App variant uses a 10% border + ink-2 text.)
link-arrowno bg/border · text brand #2563eb (→ link #3b82f6 on hover) · 14/500 · inline-flex gap 4px · trailing literal "→". hover → text link/blue-400; the "→" nudges 2px right (translateX, 120ms) — the one tiny transform allowed
iconsquare, 36–40px · radius md 8px · transparent bg · ink-muted icon at 18–20px. hover → bg white/5 + icon ink; active → bg white/8; focus-visible → ring-2 brand/40; disabled → opacity 0.4; active-toggle → bg brand/15 + icon brand
destructivebg danger #ef4444 · text white · otherwise identical to primary; OR ghost-style with text/border danger for low-emphasis. hover → ~red-600 #dc2626; active → red-700 #b91c1c; focus-visible → ring-2 danger/40; disabled → opacity 0.4
primary
primary-lg
ghost
ghost (app)
link-arrowSaber más hover — "→" nudges 2px right
icon
destructive
// primary
<button className="inline-flex h-10 items-center justify-center rounded-md bg-[#2563eb] px-5 text-[14px] font-medium text-white transition-colors duration-200 hover:bg-[#1d4ed8] active:bg-[#1e40af] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] disabled:opacity-40">Iniciar sesión</button>
// ghost
<button className="inline-flex h-10 items-center justify-center rounded-md border border-white/15 px-5 text-[14px] font-medium text-[#f9fafb] transition-colors hover:border-white/30 hover:bg-white/5">Cancel</button>
// link-arrow
<a className="group inline-flex items-center gap-1 text-[14px] font-medium text-[#2563eb] hover:text-[#3b82f6]">Saber más <span className="transition-transform duration-[120ms] group-hover:translate-x-0.5">→</span></a>

input app website

Text field. Marketing variant: surface-2 bg, 15%-white border. Dashboard variant: surface-overlay (5% white) bg, 8%-white border. Both: 8px radius, 12/16 padding, 44px min-height, brand focus border + soft ring. Label 14/500 ink-2 + 8px margin-bottom; placeholder ink-disabled; optional leading icon (Lucide 16px ink-muted, search inputs); optional trailing affordance (password eye / clear X); hint/error 12px (hint ink-muted, error danger) + 6px margin-top.

StateDelta
defaultborder 15%/8% white, bg as above
hoverborder → +5% (e.g. 15% → 20% white)
focusborder-color → brand #2563eb + box-shadow 0 0 0 3px rgba(37,99,235,.18) (soft brand ring); 200ms
filledtext ink; placeholder gone
errorborder-color → danger #ef4444; ring rgba(239,68,68,.18); hint slot shows danger message + optional AlertTriangle
disabledopacity 0.5, bg slightly flatter, no caret, cursor not-allowed
readonlyborder softened to divider, no focus ring, text ink-2
Mínimo 8 caracteres.
Te enviaremos los resultados aquí.
// dashboard input
<label className="block text-[14px] font-medium text-[#e4e4e7]">Email</label>
<input type="email" placeholder="tucorreo@ejemplo.com"
  className="mt-2 h-11 w-full rounded-md border border-white/8 bg-white/5 px-4 text-[14px] text-[#f9fafb] placeholder:text-[#52525b]
             outline-none transition-colors focus:border-[#2563eb] focus:ring-[3px] focus:ring-[#2563eb]/20" />

badge-pill app website

Small status/category pill. Soft-tinted background + matching text color, pill radius. inline-flex items-center gap 4px · padding 4px 10px · radius pill 9999px · type badge 12/500. States: static (default — badges are usually non-interactive); removable (+ trailing X 12px; X hover → text darkens); as-filter selected (solid brand bg + white text when used as a toggle chip).

tones
Riesgo alto Riesgo medio Riesgo bajo Open Borrador Recomendado
removable / as-filter selected
EUR/USD Scalping Todos Forex Cripto
<span className="inline-flex items-center gap-1 rounded-full bg-[#ef4444]/15 px-2.5 py-1 text-[12px] font-medium text-[#ef4444]">Riesgo alto</span>

skeleton app website

Loading placeholder. A pulsing zinc-800 block shaped like the eventual content. NEVER a spinner (except the tiny inline one inside a loading button). Block: bg surface-hover #27272a · radius matching the real element (text → sm or full pill, chart → md) · animate-pulse (opacity 1↔0.5, ~1.5s) · explicit width/height matching the target. Resolved: skeleton unmounts, real content fades in over ≤120ms; layout must not shift. Reduced-motion → static 0.6 opacity, no pulse.

stat-tile — label w-24 h-3 · value w-32 h-7 · delta w-36 h-3
chart — one w-full h-[220px] block
table — header real; 5 rows of [w-20 h-3 · w-16 h-3 · w-12 h-3 · w-12 h-3 · w-16 h-5] cells
SymbolTypeSizeP/LStatus
list/progress — 3 rows of [w-40 h-3 label + w-full h-1.5 bar]
text line — w-3/4 h-3 (last line of a paragraph w-1/2)
the ONLY spinner — 16px inline, inside a loading button
<div className="space-y-2" aria-hidden>
  <div className="h-3 w-24 animate-pulse rounded bg-[#27272a]" />
  <div className="h-7 w-32 animate-pulse rounded bg-[#27272a]" />
  <div className="h-3 w-36 animate-pulse rounded bg-[#27272a]" />
</div>

empty-state app website

What a widget/page shows when there's genuinely no data. Always designed, never blank, never raw "null". Wrap: centered column, padding ~32px, max-width ~280px, text-center. Icon: a relevant Lucide glyph at ~32px, ink-faint, often inside a surface-hover circle. Title: body 14–16/600 ink-2 — what's missing. Supporting: small 13–14/400 ink-muted — ONE line on what happens next. CTA (optional): a ghost small button or a link-arrow.

first-run — leans on the CTA ("Get started")
No trades yet
They'll appear here once your bots execute their first orders.
Connect a broker
filtered-empty — leans on "Change range" / "Clear filters", quieter icon
Nothing in this range
Try a wider period.
Change range
no-CTA variant — just icon + title + supporting
No data yet
This widget populates as activity starts.
<div className="mx-auto flex max-w-[280px] flex-col items-center gap-2 py-8 text-center">
  <div className="grid size-12 place-items-center rounded-full bg-[#27272a]"><Inbox className="size-5 text-[#71717a]" /></div>
  <p className="text-[14px] font-semibold text-[#e4e4e7]">No trades yet</p>
  <p className="text-[13px] text-[#a1a1aa]">They'll appear here once your bots execute their first orders.</p>
  <a className="mt-2 text-[14px] font-medium text-[#2563eb] hover:text-[#3b82f6]">Connect a broker →</a>
</div>

top-nav-marketing website

sticky top-0 z-50, height 64px, bg page-marketing/85 + backdrop-blur-md saturate-140%, bottom border 1px divider-soft, padding 0 32px, flex items-center. Logo: the pixel wordmark — uppercase, display-pixel-wordmark (0.4em tracking), ~16px height, ink. Links: nav-link-marketing 14/500 ink-2 (→ ink hover), 28px gap. CTA: primary blue button "Área del cliente", far right, navCTA size (8px/16px padding, 13px). Mobile (< md): links collapse into a hamburger → a full-height drawer; the CTA may stay visible.

default — translucent blurred bar (a link shown hovered: text → ink)
Servicios Algoritmos VPS Preguntas
mobile (< md) — links → hamburger; CTA stays
<nav className="sticky top-0 z-50 flex h-16 items-center gap-7 border-b border-white/5 bg-[#09090b]/85 px-8 backdrop-blur-md">
  <span className="font-[var(--font-display)] text-base uppercase tracking-[0.4em] text-[#fafafa]">QUANTBOT</span>
  <a className="text-[14px] font-medium text-[#e4e4e7] hover:text-white">Servicios</a>
  <a className="ml-auto rounded-md bg-[#2563eb] px-4 py-2 text-[13px] font-medium text-white hover:bg-[#1d4ed8]">Área del cliente</a>
</nav>

service-card website

bg surface-2 #101012 · border 1px rim · radius lg 12px · padding 32px. icon-tile: 48px square · bg brand-soft rgba(37,99,235,.18) · radius md 8px · centered Lucide icon brand/ink. title: section-h2/section-h3 24–30/600–700 ink, margin-top 20px. body: body 16/400 ink-2, margin-top 12px. link: link-arrow "Saber más →" in brand, margin-top 20px. States: default (rim 6%, no shadow); hover (border → rim-strong 20% white; translateY -2px — the rare allowed lift; 200ms — restrained, not bouncy); focus-within (ring-2 brand/20).

default

Gestión de cuenta real

Operamos tu cuenta con estrategias algorítmicas verificadas y control de riesgo continuo.

Saber más
hover — border → rim-strong 20% + translateY -2px (200ms, restrained)

Bots automatizados

Despliega estrategias preconfiguradas en minutos, con backtests y métricas transparentes.

Saber más
focus-within — ring-2 brand/20

Infraestructura VPS

Servidores de baja latencia para que tus algoritmos operen sin interrupciones.

Saber más
<article className="rounded-[12px] border border-white/[0.06] bg-[#101012] p-8 transition-colors hover:border-white/20">
  <div className="grid size-12 place-items-center rounded-md bg-[#2563eb]/[0.18]"><LineChart className="size-6 text-[#3b82f6]" /></div>
  <h3 className="mt-5 text-[24px] font-semibold text-[#fafafa]">Gestión de cuenta real</h3>
  <p className="mt-3 text-[16px] text-[#e4e4e7]">…</p>
  <a className="mt-5 inline-flex items-center gap-1 text-[14px] font-medium text-[#2563eb]">Saber más →</a>
</article>

comparison-table website

Two-product comparison (this product vs a generic provider). 3-col grid: feature label | highlighted column (brand-tinted, green checks) | neutral column (muted, red crosses). Root: bg surface-3 #131313 · radius lg 12px · overflow hidden. Row: grid grid-cols-3 · padding 16px 24px · bottom border 1px divider-soft. label-cell: left column, weight 600 ink. highlighted-cell: bg brand/8 rgba(37,99,235,.08) · a ok Check (or value text). neutral-cell: text ink-muted · a danger X (or "—"). header-row: the two product names as column headers; the highlighted product gets the brand tint + maybe a "Recomendado" badge. Optional row:hover → entire row bg lifts to white/2.

Característica
QuantbotRecomendado
Proveedor genérico
Latencia < 10 ms
Backtests verificados
Soporte 24/7
Comisión mensual
40 USD/mes
75 USD/mes
<div className="overflow-hidden rounded-[12px] bg-[#131313]">
  {rows.map(r => (
    <div key={r.id} className="grid grid-cols-3 border-b border-white/5 px-6 py-4 text-[14px]">
      <span className="font-semibold text-[#fafafa]">{r.label}</span>
      <span className="bg-[#2563eb]/[0.08] -mx-6 px-6 flex items-center text-[#22c55e]"><Check className="size-4" /></span>
      <span className="text-[#a1a1aa] flex items-center"><X className="size-4 text-[#ef4444]" /></span>
    </div>
  ))}
</div>

faq-accordion website

Row: full-width · padding 20px 24px · bottom border 1px rim. Question: flex justify-between · body 16/500 ink · trailing ChevronDown ink-muted. Answer: small 14/400 ink-muted line-height 1.6 · padding-top 12px · revealed on open. States: collapsed (answer hidden, max-height 0; chevron points down); hover (row bg → white/3); expanded (answer expands via max-height transition, 200ms standard; chevron rotates 180°); focus-visible (ring-2 brand/20 on the question button).

¿Qué necesito para empezar?
Solo una cuenta de bróker compatible (MT4, MT5 o cTrader) y el capital que quieras destinar. La configuración inicial se hace en minutos desde el área del cliente.
¿Cómo se calculan los resultados?
Los resultados son netos, después de comisiones, y verificados con el bróker.
¿Puedo cancelar en cualquier momento?
Sí. No hay permanencia.
¿Hay riesgo de pérdidas?
Sí — operar conlleva riesgo. Cada estrategia muestra su nivel de riesgo y su drawdown histórico.
<div className="border-b border-white/[0.06]">
  <button className="flex w-full items-center justify-between px-6 py-5 text-left text-[16px] font-medium text-[#fafafa] hover:bg-white/[0.03]">
    ¿Qué necesito para empezar? <ChevronDown className="size-5 text-[#a1a1aa] transition-transform data-[open]:rotate-180" />
  </button>
  <div className="px-6 pb-5 text-[14px] leading-relaxed text-[#a1a1aa]">…</div>
</div>

testimonial-card website

bg surface-2 #101012 · border 1px rim · radius lg 12px · padding 32px. stars: 5× star, warn #facc15, ~16px. quote: section-h3-ish 20/500 ink, margin-top 16px. author: small 14/400 ink-muted, margin-top 16px. States: default (static; optional very subtle hover border brighten in a carousel).

default
Los informes mensuales son claros y los resultados, consistentes. Es lo más cerca que he estado de "configúralo y olvídate".
— Nombre Apellido
carousel hover — subtle border brighten
La latencia del VPS marca la diferencia. Mis estrategias entran cuando deben.
— Nombre Apellido
<figure className="rounded-[12px] border border-white/[0.06] bg-[#101012] p-8">
  <div className="flex gap-0.5 text-[#facc15]">{Array.from({length:5}).map((_,i)=><Star key={i} className="size-4 fill-current"/>)}</div>
  <blockquote className="mt-4 text-[20px] font-medium text-[#fafafa]">…</blockquote>
  <figcaption className="mt-4 text-[14px] text-[#a1a1aa]">— Nombre Apellido</figcaption>
</figure>

sticky-cta-banner website

bg surface-2 #101012 · border 1px rim · radius xl 16px · padding 24px 32px · flex (column on mobile, row on desktop) justify-between items-center gap 16px. text: title section-h3 24/600 ink + description small ink-muted. actions: a ghost + a primary/primary-lg blue button. States: default (static block above the footer).

¿Listo para empezar?

Reserva una llamada de 15 minutos y te explicamos cómo funciona.

<div className="flex flex-col items-start justify-between gap-4 rounded-[16px] border border-white/[0.06] bg-[#101012] p-6 lg:flex-row lg:items-center lg:p-8">
  <div><h3 className="text-[24px] font-semibold text-[#fafafa]">¿Listo para empezar?</h3><p className="mt-1 text-[14px] text-[#a1a1aa]">…</p></div>
  <div className="flex gap-3">
    <button className="rounded-md border border-white/15 px-5 py-2.5 text-[14px] font-medium text-[#fafafa] hover:bg-white/5">Saber más</button>
    <button className="rounded-md bg-[#2563eb] px-7 py-3 text-[16px] font-semibold text-white hover:bg-[#1d4ed8]">Reservar llamada</button>
  </div>
</div>

lead-capture-modal website

Persistent (non-blocking) dark popup absolutely positioned over the hero on every marketing page — captures a results-data email lead. Not a true modal (no scrim, page is usable). Card: width 320px · bg surface-2 #101012 · border 1px rim (8% white) · radius lg 12px · padding 24px · shadow 0 24px 64px rgba(0,0,0,.6) · text-center. close: top-right X 16px ink-faint. title: 16/500 centered ink. preview: an inline mini dashboard-chart card (dark) as a teaser. field+cta: email input + full-width primary blue "Solicitar" / "Solicítalos ya" button (8px radius). States: shown (fades in + slides down ~10px, 320ms emphasized, ~1–2s after load); dismissed (fades out; remembers dismissal so it doesn't re-nag).

Desde 2023

Gestión algorítmica de capital, sin complicaciones.

Resultados verificados, control de riesgo y un panel claro. Tú decides cuánto destinar.

Solicítalos ya
Equity (últimos 6 meses)
// see `modal` codeAnchor — same card, but rendered absolutely over the hero with no scrim and a dismissable state
<div className="absolute right-8 bottom-8 z-40 w-80 rounded-[12px] border border-white/8 bg-[#101012] p-6 text-center shadow-[0_24px_64px_rgba(0,0,0,0.6)]">…</div>

Composition recipes

Five full-page layouts as data. The "Account overview" recipe is also rendered as a miniature-but-real assembled dashboard — the centerpiece showing what the system looks like put together.

1 · Account overview (dashboard home) app

Shell: sidebar (240px) + top-bar ("Account overview" + "Contact support" ghost pill + bell + avatar) + content (padding 32px) holding a 12-col / 16px-gap grid. Row 1 — three stat-tiles @ span-4. Row 2 — area-chart-card @ span-8 + status-card @ span-4. Row 3 — bar-chart-card @ span-6 + list-table-card @ span-6 (filter dropdown, footer "View all →"). Row 4 — progress-tracker-card @ span-12. Responsive: md → stat-tiles 2-up then 1, charts full-width-of-row, status-card full width, tracker full width. sm → single column, source order, charts keep min-height.

Account overview
Total profit / loss
+$2,156.92
+12% from last month
Equity
$12,701.19
-2% from last month
Monthly performance
+6.4%
+5% from last month
Equity curve
NovDecJanFebMarApr
Running

3 bots active · last trade 4m ago

Bot status
Monthly returns
Net return per month
NovDecJanFebMarApr
Recent trades
SymbolTypeP/LStatus
EUR/USDLong+$182.40Closed
GBP/JPYShort-$54.10Open
XAU/USDLong+$612.00Open
Certificate progress tracker
Payment status overview for the last 6 months
Phase 1 target (10%)
100%
Phase 2 target (5%)
75%

2 · Settings page app

Shell: sidebar + top-bar ("Settings") + content with a max-width ~720px reading column (NOT the full-bleed grid). Section "Profile" — a card (widget chrome) with label+input pairs (Name, Email), a Save primary button bottom-right. Section "Notifications" — a card with rows of label + description + toggle (use a real switch in brand when on, not the tab-pill styling). Section "Danger zone" — a card with a 1px danger/30 border, a "Delete account" destructive (ghost-style) button, and a confirmation modal. Responsive: always single column; cards stack with 24px gap.

Profile
Notifications
Resumen diario
Un email cada mañana con tu equity y operaciones.
Alertas de drawdown
Aviso si una estrategia supera su límite.
Danger zone
Eliminar tu cuenta borra permanentemente tus datos y bots.

3 · Login / register (the source's hero) app

Shell: no sidebar — full-page grid grid-cols-1 lg:grid-cols-2 bg-page text-ink min-h-screen. Left half: a tilted MacBook product shot (frame #3f3f46, screen-shadow 0 32px 64px rgba(0,0,0,.6)) showing the dashboard overview — i.e. the dashboard IS the marketing visual. Right half: a centered ~440px form column — pixel wordmark → 30px "Inicia sesión en tu cuenta" → supporting line → tab-pill-switcher (Iniciar sesión / Crear cuenta) → label+input (Email) → label+input (Contraseña, with eye toggle) → "Mantener sesión iniciada" checkbox + "¿Olvidaste tu contraseña?" link → full-width primary "Iniciar sesión" → "OR" divider → full-width ghost "Iniciar con Google".

P/L
+$2,156
Equity
$12,701
Mes
+6.4%
Equity curve
Running
QUANTBOT
Inicia sesión en tu cuenta
Accede a tu panel para ver tu equity y operaciones.
¿Olvidaste tu contraseña?
O

4 · Marketing home website

top-nav-marketing (sticky, blurred) → Hero (eyebrow chip "Desde 2023" → 48px headline → 18px subtitle → primary-lg blue CTA → glowing 3D globe behind → lead-capture-modal floating over it) → Trust strip (broker partner logos on dark, low contrast) → Services (3× service-card @ ~1/3 width) → Globe / global-reach section (full-bleed dark, glowing globe, "Alcance global" headline, a country count) → Platform compatibility (MT4 / MT5 / cTrader logo grid + a dashboard preview on a blurred dark surface) → Testimonials (a row of testimonial-cards) → sticky-cta-banner ("Reservar llamada" prompt) → footer (4 columns of links + a newsletter input+button group in the last column). Responsive: card grids 3→2→1; footer columns stack; nav links → hamburger drawer.

top-nav-marketing — sticky, blurred · pixel wordmark · link cluster · "Área del cliente" CTA
Hero — eyebrow chip · 48px headline · 18px subtitle · primary-lg CTA · glowing globe behind · lead-capture-modal floating over
Trust strip — broker partner logos, low contrast
service-card
service-card
service-card
Global-reach — full-bleed dark · glowing globe · "Alcance global" · country count
Platform compatibility — MT4 / MT5 / cTrader logo grid + dashboard preview on blurred dark surface
testimonial-card
testimonial-card
testimonial-card
sticky-cta-banner — "Reservar llamada" (ghost + primary)
footer col
footer col
footer col
newsletter input + button

5 · VPS landing (single-offer page) website

Hero (centered "40 USD/mes" price headline + a primary-lg "Contratar ahora" CTA + supporting copy) → "Beneficios destacados" (3-col grid of vps-feature tiles, bg surface-3 #131313, blue Lucide icons) → "Características técnicas" (2-col bulleted spec lists with blue check glyphs) → comparison-table ("this VPS" vs "generic provider" — highlighted column brand-tinted with green checks, neutral column muted with red crosses) → sticky-cta-banner + footer. Responsive: grids collapse 3→1; comparison table stays 3-col but tightens padding.

Hero — centered "40 USD/mes" headline · primary-lg "Contratar ahora" · supporting copy
vps-feature tile (surface-3 · blue icon)
vps-feature tile
vps-feature tile
Características técnicas — bulleted spec list (blue checks)
Características técnicas — bulleted spec list (blue checks)
comparison-table — label | highlighted (brand tint, green checks) | neutral (muted, red crosses)
sticky-cta-banner
footer

Data viz

Library: Recharts (SVG, themeable) — or visx for more control. Whatever it is, it must render flat (no default drop-shadows, no default gridline soup) and theme to the tokens. Charts live inside a widget-container — the card chrome (rim, header, period pill) is not the chart library's job. One series → brand blue; multiple → the chartSeries.order rotation (data colors, never reused as UI chrome). Minimal axes: no axis lines, no tick lines, ticks in axis-tick type (11px ink-muted). Hide an axis entirely on small charts. Gridlines: none, or horizontal-only at rgba(255,255,255,0.04). Never vertical gridlines. No data dots at rest; active dot on hover only.

Specs

areaChart
fill: linearGradient 180deg — #2563eb @ 0.18 → #2563eb @ 0 · line stroke #3b82f6 strokeWidth 2 type monotone · activeDot r:4 fill #3b82f6 stroke #18181b strokeWidth 2 · cursor: vertical reference line rgba(255,255,255,.10) 1px · minHeight 220–260px
barChart
bars: vertical, radius:[4,4,0,0] (rounded top only), category gap ~24–32% · color: each bar/series takes the next entry in chartSeries.order; single-series may all be #3b82f6 · cursor: translucent rgba(255,255,255,.04) highlight behind hovered category · minHeight 220–260px
progressBar
track #27272a (surface-hover), height 6px, radius pill · fill #2563eb (brand) — or #3b82f6 (link); becomes #22c55e (ok) at 100% · label right-aligned, 12/500 ink, tabular figures; optional trailing status glyph
sparkline
inside stat tiles: a tiny line/area, ~32px tall, 1.5px link stroke, no axes, no labels, optional faint brand/8 fill
donut / radial
if used (e.g. allocation): same series rotation, 8–12px ring thickness, center label in metric-value type; no shadow
legend
only when >1 series and categories aren't self-evident. Top-right of the widget header area or directly below. 11px, a colored dot (8px) + label in ink-muted. Clickable to toggle is fine; keep it quiet

Tooltip + chart states

tooltip — bg surface · rim · radius md · padding 8px 12px · shadow popover; label = x value in 12/600 ink; each series: colored 8px dot + name in ink-muted 12px + value in ink 12/600 (tabular), right-aligned; follows the cursor; never clipped
March 2026
Equity$11,402.18
Return+6.4%
loading — single tall pulse block; header/period-pill stay
Equity curve
empty — faint chart-type icon + "No activity in this period" + optional "Change range"
Equity curve
No activity in this period
Change range
error — danger-soft tinted block + AlertTriangle + "Chart unavailable" + "Retry"; card chrome unchanged
Equity curve
Chart unavailable · Retry

Voice & copy

Plain, confident, low-jargon. The source is Spanish; copy is short, direct, slightly formal-friendly ("Inicia sesión", "Reservar llamada"). No exclamation marks, no hype words. Numbers and outcomes do the talking.

AspectRule
headingsSentence case ("Inicia sesión en tu cuenta", "Gestión de cuenta real").
buttonsSentence case ("Iniciar sesión", "Reservar llamada", "Solicitar", "Suscribirse"). Never ALL CAPS, never Title Case.
labels / form fieldsSentence case ("Email", "Contraseña" — single-word labels capitalized).
nav itemsSentence case.
section eyebrows / sidebar section labelsShort; the sidebar section labels render UPPERCASE via CSS but are authored sentence case. Eyebrows ≤3 words ("Beneficios destacados").
table headersRender UPPERCASE via CSS; authored normally.
currency$12,701.19 — symbol prefix, comma thousands, 2 decimals. (Pricing also uses "USD" suffix: "40 USD/mes".)
percentPerformance figures show 1 decimal ("+6.4%"); round percentages show 0 ("75%", "100%"). Deltas ALWAYS carry an explicit sign: "+12%", "-2%".
deltaChipPattern: "<sign><value>% from last month". NB: the source literally renders "form last month" — that is a typo on the source site; use the correct "from last month" in new builds.
dates / rangesPrefer relative ranges in selectors and captions: "Last 6 months", "Last 30 days", "YTD". Absolute dates short: "Apr 12", "Apr 12, 2026".
largeNumbersComma-grouped; no "k"/"M" abbreviation inside stat tiles (full number reads as more credible). Abbreviation OK in dense table cells if space-constrained.
tabularFiguresUse tabular/monospaced figures (tabular-nums) anywhere numbers stack or align — metric values, table numeric columns, axis ticks, progress percentages.
delta-chip pattern — note the source typo "form" vs the correction "from"
Total profit / loss
+$2,156.92
+12% from last month
"+12% form last month"  →  "+12% from last month"
empty-state copy — name what's missing + one line on what populates it
No trades yet
They'll appear here once your bots execute their first orders.
Nothing in this range
Try a wider period.

Never show "undefined", "null", "NaN", "0" where there should be a dash — use "—" in ink-muted. Loading states use skeletons, not the word "Loading…". Buttons say what they do ("Iniciar sesión", not "Submit"/"OK"). Links-as-action use a trailing "→". Destructive confirmations name the consequence: "Delete account" / "This permanently removes your data and bots."

Anti-patterns

The do-not list. Violating any of these breaks the system's identity even if individual screens "look fine".

Don't add drop shadows to cards/widgets

Depth = a 1px white-at-6% rim on a one-step-darker surface. (Modals and dropdown panels are the only things that get a real shadow.)

wrong
Equity
right
Equity

Don't scale() / translateY on hover (dashboard)

Hover = a background-color lift only (surface → surface-hover, 120ms). The marketing service-card's subtle -2px lift is the lone exception — don't generalize it.

wrong — scale-on-hover
Recent trades
right — bg lift only
Recent trades

Don't use a centered spinner inside a card/page region

Use skeletons. The only allowed spinner is a 16px inline one inside a loading button.

wrong
right

Don't use pure black #000 for any surface

The dashboard page is #020617; cards are #18181b; the marketing page is #09090b.

wrong — #000
right — #020617

Don't introduce a second accent hue

#2563eb (blue-600) is the entire accent system. A "success-green button", a "purple secondary CTA", a "teal highlight" all break it. Chart series colors are data only — never restyle a button/nav/badge with them.

wrong — second accent
right — single accent

Don't invent radii / split the difference to 10px arbitrarily

Use 6 / 8 / 12 / 16 / pill. (10px is allowed only when matching the source portal exactly — then be consistent about it.) Don't create hierarchy by bolding body text — use the type ramp + the ink/ink-2/ink-muted color steps. Don't put a border AND a shadow on the same surface (other than the modal, which is intentionally shadow + rim). Don't use Tailwind's default blue-500 for primary actions — primary is blue-600 #2563eb (blue-500 is reserved for inline text links and "info"). Don't use Inter for the dashboard portal body text — that surface uses the ui-sans-serif system stack.

Also: Don't drop the period-selector pill (or a context-label pill) from a data widget's header just because there's nothing to filter — the slot is load-bearing for the grid's visual rhythm. Don't mix numeric alignment — numeric table columns and progress percentages are right-aligned with tabular figures; metric deltas are left-aligned directly under the value. Don't let charts render with library defaults: no axis lines, no tick lines, no vertical gridlines, no data dots at rest, no soft drop-shadow on areas/bars. Don't blow up the dashboard grid math: 12 columns, 16px gap, 32px container padding; a drag/resize layer must keep card chrome byte-identical to the static render.

Re-skin contract

This system re-skins by swapping exactly two things: the brand accent and the display (wordmark) font. Everything structural — the cool-near-black surface ramp, radii, motion, rims, the chart-series rotation — stays put. After swapping, re-check the contrast pairs below.

Swappable

brand (5 tokens: brand, brand-hover, brand-pressed, brand-soft, link/info)
Pick a hue, then derive: brand-hover = brand −1 lightness step, brand-pressed = −2 steps, brand-soft = brand @ 18% (also @ 8% for faint tints), link/info = brand +1 step lighter (for text legibility). Everything that was blue-600 becomes the new hue; everything that pointed at link/info follows. Default: #2563eb (blue-600) / hover #1d4ed8 / pressed #1e40af / soft rgba(37,99,235,0.18) / link #3b82f6.
displayFont (font-display — the pixel wordmark face)
Swap freely — it only affects the wordmark, never UI text. Body stays Inter (marketing) / ui-sans-serif (app). Default: 'Press Start 2P' (alt 'VT323') — reserve strictly for the brand mark.

Do NOT swap

  • The surface ramp (page #020617 / #09090b, surface #18181b, surface-hover #27272a, the white-alpha rims) — tuned for dark-first rim contrast; changing it breaks the no-shadow depth model.
  • Radii set (6 / 8 / 12 / 16 / pill).
  • Motion (easing + the 120/200/320ms tiers).
  • Chart-series rotation order (blue → green → orange → purple → pink → yellow).
  • Status colors (ok #22c55e / warn #facc15 / danger #ef4444) — they carry universal meaning.

Swap instructions: (1) Replace the five brand-family tokens per the rule. (2) Optionally replace font-display. (3) Re-run the white-on-brand and new-brand-as-text (link) checks. (4) Leave surfaces, radii, motion, rims, status colors, chart series untouched. (5) If the new brand is light/low-contrast, darken brand-pressed more aggressively and keep button text white.

Known-AA contrast table

PairRatioVerdictLive sample
ink #f9fafb on page #020617≈ 19:1AAA — any sizeThe quick brown fox
ink #f9fafb on surface #18181b≈ 16:1AAA — any sizeThe quick brown fox
ink-2 #e4e4e7 on surface #18181b≈ 13:1AAA — use for dense body copyThe quick brown fox
ink-muted #a1a1aa on surface #18181b≈ 6.4:1AA for normal text, AAA for large — fine for labels/captions/ticks; for paragraphs below ~16px prefer ink-2The quick brown fox
ink-faint #71717a on surface #18181b≈ 3.4:1Large text / non-text only (10px uppercase section labels borderline — acceptable as decoration-grade labels, not body)SECTION LABEL
ink-disabled #52525b on surface-overlay< 3:1Placeholder/disabled only — never load-bearing textplaceholder text
white #ffffff on brand #2563eb≈ 5.2:1AA — prefer ≥14px medium for button labels; AAA for ≥18px boldIniciar sesión
ok #22c55e text on surface #18181b≈ 7.6:1AA at normal size — still saturated; reserve for short labels/icons, not paragraphs+12% from last month
danger #ef4444 text on surface #18181b≈ 4.4:1Just under AA for normal text — for body errors pair with an icon or bump to ≥18px / use a lighter red; fine for icons + short labels-2% from last month
link #3b82f6 text on surface #18181b≈ 5.0:1AA — good for inline linksSaber más →

Stack mapping

Translations for the three target stacks so you skip a translation pass.

Tailwind v4 — @theme { … } block

App-scoped values shown; for the marketing surface swap --color-page to #09090b and use Inter. Tailwind v4's default palette already covers the zinc/blue scales — these are the brand-aware aliases.

@theme {
  --color-page: #020617;            /* slate-950 */
  --color-page-marketing: #09090b;  /* zinc-950 */
  --color-surface: #18181b;         /* zinc-900 */
  --color-surface-2: #101012;
  --color-surface-3: #131313;
  --color-surface-hover: #27272a;   /* zinc-800 */
  --color-surface-overlay: rgba(255,255,255,0.05);
  --color-rim: rgba(255,255,255,0.06);
  --color-rim-hover: rgba(255,255,255,0.10);
  --color-rim-strong: rgba(255,255,255,0.20);
  --color-divider: #1f2937;
  --color-divider-soft: rgba(255,255,255,0.05);
  --color-ink: #f9fafb;             /* gray-50 */
  --color-ink-2: #e4e4e7;
  --color-ink-muted: #a1a1aa;
  --color-ink-faint: #71717a;
  --color-ink-disabled: #52525b;
  --color-brand: #2563eb;           /* blue-600 */
  --color-brand-hover: #1d4ed8;
  --color-brand-pressed: #1e40af;
  --color-brand-soft: rgba(37,99,235,0.18);
  --color-link: #3b82f6;
  --color-ok: #22c55e;
  --color-warn: #facc15;
  --color-danger: #ef4444;
  --color-info: #3b82f6;
  --radius-sm: 6px;
  --radius-md: 8px;
  --radius-widget: 12px;
  --radius-lg: 12px;
  --radius-xl: 16px;
  --font-sans: ui-sans-serif, system-ui, sans-serif;
  --font-sans-marketing: 'Inter', system-ui, -apple-system, sans-serif;
  --font-display: 'Press Start 2P', 'VT323', ui-monospace, monospace;
  --ease-standard: cubic-bezier(0.4,0,0.2,1);
  --ease-emphasized: cubic-bezier(0.2,0,0,1);
  --duration-quick: 120ms;
  --duration-base: 200ms;
  --duration-slow: 320ms;
  --shadow-modal: 0 24px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.06);
  --shadow-rim: inset 0 0 0 1px rgba(255,255,255,0.06);
  --shadow-blue-glow: 0 0 32px rgba(37,99,235,0.35);
}

Framework-neutral CSS custom properties

Same names as the Tailwind block above — put them under :root (and the marketing overrides under a [data-surface="marketing"] scope if you host both in one app). Reference as var(--color-surface) etc. Identical key/value pairs to the @theme block.

SwiftUI — for a native dashboard

Rims become .overlay(RoundedRectangle(cornerRadius: 12).strokeBorder(Color.white.opacity(0.06))); never use .shadow() on cards (only on sheets/popovers). Hover-equivalents (selection/press) use .animation(.easeInOut(duration: 0.12), value:). Honor @Environment(\.accessibilityReduceMotion).

static let page = Color(red: 0.008, green: 0.024, blue: 0.090)        // #020617
static let pageMarketing = Color(red: 0.035, green: 0.035, blue: 0.043) // #09090b
static let surface = Color(red: 0.094, green: 0.094, blue: 0.106)     // #18181b
static let surfaceHover = Color(red: 0.153, green: 0.153, blue: 0.165) // #27272a
static let divider = Color(red: 0.122, green: 0.161, blue: 0.216)     // #1f2937
static let ink = Color(red: 0.976, green: 0.980, blue: 0.984)         // #f9fafb
static let ink2 = Color(red: 0.894, green: 0.894, blue: 0.906)        // #e4e4e7
static let inkMuted = Color(red: 0.631, green: 0.631, blue: 0.667)    // #a1a1aa
static let inkFaint = Color(red: 0.443, green: 0.443, blue: 0.478)    // #71717a
static let brand = Color(red: 0.145, green: 0.388, blue: 0.922)       // #2563eb
static let brandHover = Color(red: 0.114, green: 0.306, blue: 0.847)  // #1d4ed8
static let link = Color(red: 0.231, green: 0.510, blue: 0.965)        // #3b82f6
static let ok = Color(red: 0.133, green: 0.773, blue: 0.369)          // #22c55e
static let warn = Color(red: 0.980, green: 0.800, blue: 0.082)        // #facc15
static let danger = Color(red: 0.937, green: 0.267, blue: 0.267)      // #ef4444

// Body / UI: Font.system(.body). widget-title → .system(size: 17, weight: .semibold).
// metric-value → .system(size: 26, weight: .bold).monospacedDigit(). table-cell → .system(size: 14). axis-tick / caption → .system(size: 11).
// Wordmark: a bundled pixel font (Press Start 2P / VT323) at ~20pt with wide tracking — wordmark only.
extension View { func rim(_ r: CGFloat = 12) -> some View { self.overlay(RoundedRectangle(cornerRadius: r).strokeBorder(Color.white.opacity(0.06))) } }
// card helper: surface bg + .cornerRadius(12) + .rim(12) — no .shadow().

react-grid-layout — for the editable dashboard

cols={{ lg: 12, md: 12, sm: 1 }}, rowHeight ~ 8px with margins, margin={[16,16]}, containerPadding={[32,32]}. Each item's child is a <Widget> (the component above) — react-grid-layout owns x/y/w/h only and must never inject styles onto the card. Default w per widget type: stat-tile w=4, area-chart w=8, bar-chart w=6, list-table w=6, progress-tracker w=12, status-card w=4. h: charts ~32 rows-of-8px (~256px), tiles ~14, tracker by content.

Tailwind reference

How to consume the exported tailwind block from the JSON. There are two scoped configs — app (dashboard portal, the top-level tailwind block) and website (marketing). Both share the brand + status + rim tokens; they differ on the page color (slate-950 vs zinc-950) and the body font (ui-sans-serif vs Inter). Drop the theme.extend values into your Tailwind config (or the @theme vars from Stack mapping above for v4), then build components from the componentRecipes map rather than reinventing class strings.

App config — theme.extend colors

TokenValueMaps to
brand / brand-hover / brand-pressed#2563eb / #1d4ed8 / #1e40afPrimary actions, hover, pressed
brand-softrgba(37,99,235,0.18)Icon tiles, active-nav tint, badge bg
link / info#3b82f6Inline text links, info dots
page#020617Dashboard page background (slate-950)
surface / surface-2 / surface-hover#18181b / #101012 / #27272aCards · modal body · hover lift
surface-overlayrgba(255,255,255,0.05)Inset form fields / wells
rim / rim-hoverrgba(255,255,255,0.06) / 0.10Card edges (the elevation primitive)
divider / divider-soft#1f2937 / rgba(255,255,255,0.05)Table rows, tab track · faintest hairline
ink / ink-2 / ink-muted / ink-faint / ink-disabled#f9fafb / #e4e4e7 / #a1a1aa / #71717a / #52525bText scale (high → placeholder)
ok / warn / danger#22c55e / #facc15 / #ef4444Positive / caution / negative & destructive
chart-1chart-6#3b82f6 / #22c55e / #f97316 / #a855f7 / #ec4899 / #facc15Multi-series chart rotation (DATA only)

Other theme.extend keys: fontFamily.sans = ui-sans-serif/system-ui/sans-serif · fontFamily.display = 'Press Start 2P'/VT323/ui-monospace · fontSize page-title 30/700/1.2 · widget-title 17/600/1.35 · metric 26/700/1.1 · table 14/1.5 · label 14/500/1.4 · caption 12/1.4 · tick 11/1 · borderRadius sm 6 / md 8 / lg 10 / widget 12 / xl 16 · boxShadow rim · popover (0 24 64 /.6) · modal (popover + rim) · blue-glow (0 0 32 /.35) · status-halo (0 0 0 4px rgba(34,197,94,.12)) · transitionDuration quick 120 / base 200 / slow 320 · transitionTimingFunction standard (cubic-bezier(.4,0,.2,1)) / emphasized (cubic-bezier(.2,0,0,1)) · screens sm 640 / md 768 / lg 1024 / xl 1280.

App config — componentRecipes

RecipeClasses
appShellmin-h-screen bg-page text-ink-2
sidebarfixed inset-y-0 left-0 w-60 overflow-y-auto border-r border-rim bg-page py-7
sidebarItem / sidebarItemActiveflex items-center gap-2 border-l-2 border-transparent px-[22px] py-[7px] text-[14px] font-medium text-white/70 transition-colors duration-quick hover:bg-white/[0.04] hover:text-ink  ·  …border-l-2 border-brand bg-brand/[0.08] …text-ink
topBarsticky top-0 z-50 flex h-16 items-center justify-between border-b border-white/5 bg-page px-8
contentGridgrid grid-cols-1 gap-4 p-8 md:grid-cols-2 lg:grid-cols-12
widgetrounded-[12px] border border-rim bg-surface p-5 transition-colors duration-quick focus-within:border-brand focus-within:ring-2 focus-within:ring-brand/20
widgetHeader / widgetTitle / widgetSubtitle / widgetBodyflex items-start justify-between gap-3  ·  text-[17px] font-semibold leading-tight text-ink  ·  mt-0.5 text-[12px] text-ink-muted  ·  mt-4
periodPillinline-flex items-center gap-1.5 rounded-md border border-rim bg-white/[0.03] px-3 py-1.5 text-[11px] font-medium text-ink-muted transition-colors duration-quick hover:border-white/15 hover:text-ink-2 focus-visible:ring-2 focus-visible:ring-brand/20
statValue / statDeltaPositive / statDeltaNegative / statDeltaFlatmt-1 text-[26px] font-bold tabular-nums text-ink  ·  mt-1 text-[11px] font-medium text-ok / text-danger / text-ink-muted
tableHeader / tableRow / tableCell / tableCellNumpy-2 text-left text-[12px] font-semibold uppercase tracking-wider text-ink-muted  ·  border-b border-white/5 transition-colors duration-quick hover:bg-surface-hover  ·  py-3 text-[14px] text-ink-2  ·  py-3 text-right text-[14px] tabular-nums text-ink-2
progressTrack / progressFill / progressFillDoneh-1.5 flex-1 rounded-full bg-surface-hover  ·  h-full rounded-full bg-brand  ·  h-full rounded-full bg-ok
statusDotsize-2.5 rounded-full bg-ok shadow-status-halo
tabTrack / tabActive / tabInactivegrid grid-cols-2 rounded-md bg-divider p-1  ·  rounded-sm bg-brand px-4 py-2 text-[14px] font-semibold text-white  ·  rounded-sm px-4 py-2 text-[14px] font-medium text-ink-muted hover:text-ink
inputApph-11 w-full rounded-md border border-white/8 bg-white/5 px-4 text-[14px] text-ink placeholder:text-ink-disabled outline-none transition-colors focus:border-brand focus:ring-[3px] focus:ring-brand/20
btnPrimary / btnPrimaryFull / btnGhost / googleBtninline-flex h-10 items-center justify-center rounded-md bg-brand px-5 text-[14px] font-medium text-white transition-colors duration-base hover:bg-brand-hover active:bg-brand-pressed focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand disabled:opacity-40  ·  w-full rounded-md bg-brand py-3 text-[14px] font-semibold text-white hover:bg-brand-hover  ·  …border border-white/10 …text-ink-2 hover:bg-white/5  ·  inline-flex w-full items-center justify-center gap-2 rounded-md border border-white/10 bg-transparent py-3 text-[14px] font-medium text-ink-2 hover:bg-white/5
dropdownPanel / dropdownItem / dropdownItemActivemin-w-40 rounded-md border border-rim bg-surface p-1 shadow-popover  ·  flex w-full items-center justify-between rounded-sm px-3 py-2 text-[14px] text-ink-2 transition-colors duration-quick hover:bg-surface-hover  ·  …text-brand
badge / skeletonBlock / emptyState / toastinline-flex items-center gap-1 rounded-full px-2.5 py-1 text-[12px] font-medium  ·  animate-pulse rounded bg-surface-hover  ·  mx-auto flex max-w-[280px] flex-col items-center gap-2 py-8 text-center  ·  flex max-w-sm items-start gap-3 rounded-md border border-rim bg-surface p-3 pr-4 shadow-popover

Website config — theme.extend + componentRecipes

Same brand/status/rim tokens; differs on page (#09090b zinc-950), surface (#101012 / #131313), font (Inter sans), and adds rim-strong (rgba(255,255,255,0.20)) plus marketing-only type sizes (display 48 / h1 36 / h2 30 / h3 24 / body-lg 18 / body 16 / small 14 / caption 12) and spacing (section-y 96 / -tablet 80 / -mobile 48 / nav 64).

RecipeClasses
navTopsticky top-0 z-50 flex h-16 items-center gap-7 border-b border-white/5 bg-page/85 px-8 backdrop-blur-md saturate-150 text-ink
logoPixelfont-display text-base uppercase tracking-[0.4em] text-ink
navLink / navCTAtext-[14px] font-medium text-ink-2 transition-colors hover:text-ink  ·  rounded-md bg-brand px-4 py-2 text-[13px] font-medium text-white transition-colors hover:bg-brand-hover
btnPrimary / btnPrimaryLg / btnGhost / linkArrowinline-flex h-10 items-center justify-center rounded-md bg-brand px-5 text-[14px] font-medium text-white transition-colors duration-base hover:bg-brand-hover active:bg-brand-pressed focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand disabled:opacity-40  ·  …h-12 …px-7 text-[16px] font-semibold…  ·  …border border-white/15 …text-ink hover:border-white/30 hover:bg-white/5  ·  group inline-flex items-center gap-1 text-[14px] font-medium text-brand hover:text-link
cardService / iconTilerounded-lg border border-rim bg-surface p-8 transition-colors hover:border-rim-strong  ·  grid size-12 place-items-center rounded-md bg-brand-soft
input / modalh-11 w-full rounded-md border border-white/15 bg-surface px-4 text-[14px] text-ink placeholder:text-ink-disabled outline-none transition-colors focus:border-brand focus:ring-[3px] focus:ring-brand/20  ·  fixed left-1/2 top-1/2 z-40 w-80 -translate-x-1/2 -translate-y-1/2 rounded-lg border border-white/8 bg-surface p-6 text-center shadow-modal
faqRow / faqQuestionborder-b border-rim  ·  flex w-full items-center justify-between px-6 py-5 text-left text-[16px] font-medium text-ink hover:bg-white/3
comparisonRow / comparisonHighlightCell / comparisonNeutralCellgrid grid-cols-3 border-b border-white/5 px-6 py-4 text-[14px]  ·  bg-brand/[0.08] flex items-center text-ok  ·  flex items-center text-ink-muted
badge / stickyBanner / footerinline-flex items-center gap-1 rounded-full px-2.5 py-1 text-[12px] font-medium  ·  flex flex-col items-start justify-between gap-4 rounded-xl border border-rim bg-surface p-6 lg:flex-row lg:items-center lg:p-8  ·  border-t border-white/5 bg-page py-16 text-[14px] text-ink-muted