DESIGN.md
Rama main · réplica de lectura del repositorio original.
---
version: alpha
name: Rembric
description: >-
Dark monochrome zinc design system with the Rembric lime as the single accent
for the self-hosted MCP memory dashboard. A Next.js App Router + React 19
surface on Tailwind v4 + shadcn/Radix, with a `--radius` token scale. The
canonical identity doc is `.agents/skills/rembric-dashboard-ui/SKILL.md`; the
formal OpenSpec spec change is pending.
colors:
primary: '#c6f24e'
on-primary: '#09090b'
background: '#09090b'
card: '#101012'
raised: '#18181b'
accent: '#1c1c1f'
border: '#1f1f23'
input: '#27272a'
ring: '#3f3f46'
foreground: '#fafafa'
foreground-secondary: '#a1a1aa'
foreground-muted: '#71717a'
warn: '#ff8c00'
destructive: 'oklch(0.704 0.191 22.216)'
typography:
display:
fontFamily: Geist
fontSize: 3rem
fontWeight: 700
lineHeight: 0.95
letterSpacing: -0.025em
display-lg:
fontFamily: Geist
fontSize: 5rem
fontWeight: 700
lineHeight: 0.9
letterSpacing: -0.035em
h1:
fontFamily: Geist
fontSize: 2.4rem
fontWeight: 700
lineHeight: 1
letterSpacing: -0.02em
h2:
fontFamily: Geist Mono
fontSize: 0.78rem
fontWeight: 600
lineHeight: 1.2
letterSpacing: 0.14em
h3:
fontFamily: Geist Mono
fontSize: 0.72rem
fontWeight: 600
lineHeight: 1.2
letterSpacing: 0.12em
body-md:
fontFamily: Geist
fontSize: 0.92rem
fontWeight: 400
lineHeight: 1.55
body-sm:
fontFamily: Geist
fontSize: 0.82rem
fontWeight: 400
lineHeight: 1.5
label-md:
fontFamily: Geist Mono
fontSize: 0.72rem
fontWeight: 500
lineHeight: 1
letterSpacing: 0.12em
label-sm:
fontFamily: Geist Mono
fontSize: 0.66rem
fontWeight: 500
lineHeight: 1
letterSpacing: 0.14em
mono-md:
fontFamily: Geist Mono
fontSize: 0.78rem
fontWeight: 400
lineHeight: 1.4
mono-sm:
fontFamily: Geist Mono
fontSize: 0.7rem
fontWeight: 400
lineHeight: 1.4
spacing:
xs: 4px
sm: 8px
md: 12px
base: 16px
lg: 24px
xl: 32px
2xl: 48px
3xl: 64px
rounded:
button: 10px
input: 12px
card: 16px
components:
button-primary:
backgroundColor: '{colors.primary}'
textColor: '{colors.on-primary}'
typography: '{typography.label-md}'
rounded: '{rounded.button}'
padding: '16px'
height: '44px'
button-primary-hover:
backgroundColor: '{colors.primary}'
textColor: '{colors.on-primary}'
button-secondary:
backgroundColor: '{colors.raised}'
textColor: '{colors.foreground}'
rounded: '{rounded.button}'
padding: '16px'
height: '44px'
button-warn:
backgroundColor: 'transparent'
textColor: '{colors.warn}'
rounded: '{rounded.button}'
padding: '16px'
height: '44px'
button-warn-hover:
backgroundColor: '{colors.warn}'
textColor: '{colors.on-primary}'
button-danger:
backgroundColor: 'transparent'
textColor: '{colors.destructive}'
rounded: '{rounded.button}'
padding: '16px'
height: '44px'
button-danger-hover:
backgroundColor: '{colors.destructive}'
textColor: '{colors.on-primary}'
button-sm:
typography: '{typography.label-sm}'
padding: '10px'
height: '28px'
input:
backgroundColor: '{colors.input}'
textColor: '{colors.foreground}'
typography: '{typography.body-sm}'
rounded: '{rounded.input}'
padding: '12px'
height: '44px'
status-pill:
backgroundColor: '{colors.raised}'
textColor: '{colors.foreground}'
typography: '{typography.label-sm}'
rounded: '9999px'
padding: '8px'
stat-card:
backgroundColor: '{colors.card}'
textColor: '{colors.foreground}'
rounded: '{rounded.card}'
padding: '24px'
card:
backgroundColor: '{colors.card}'
textColor: '{colors.foreground}'
rounded: '{rounded.card}'
modal:
backgroundColor: '{colors.raised}'
textColor: '{colors.foreground}'
rounded: '{rounded.card}'
padding: '0px'
row-hover:
backgroundColor: '{colors.accent}'
caption:
textColor: '{colors.foreground-muted}'
typography: '{typography.label-sm}'
divider:
backgroundColor: '{colors.border}'
height: '1px'
---
# Rembric — Dashboard Design System
Single source of truth for the **current** visual identity and UI patterns
served at `/dashboard/*`. The YAML frontmatter above carries the
machine-readable design tokens; the prose below tells you **why** those values
exist and how to apply them.
The canonical, always-current identity doc is
`.agents/skills/rembric-dashboard-ui/SKILL.md` (v1.1). This file is the
repo-root overview that defers to it and to the live implementations. The
identity was **redesigned (2026-09, branch `feat/dashboard-identity-redesign`)**;
the previous identity is preserved as history at the end of this file.
If you are building or extending a dashboard page, read the whole file before
touching code. The recipes at the bottom show the canonical patterns — match
them rather than inventing new ones.
## Overview
Rembric's identity is **dark monochrome zinc with a single lime accent**. The
product is a single-tenant, self-hosted memory + sessions + dashboard for AI
coding agents; it should feel operational, terse, and unambiguous — a control
room, not a SaaS landing page.
Visual choices:
- **Dark canvas, one accent.** Near-black zinc surfaces (`#09090b`) carried by
a layered surface ladder (`#101012` cards, `#18181b` raised/popover/muted) and
1-px `#1f1f23` borders. Lime (`#c6f24e`) is the **only** accent — it marks
primary actions, active state, and the chart's save series, never wallpaper.
Amber (`#ff8c00`) and destructive stay for warn / danger tones.
- **Geist, self-hosted.** Geist drives UI and display output; Geist Mono owns
labels, meta, counters and tabular data. Both are self-hosted woff2 — no CDN
at runtime, no second display face.
- **Depth without heavy chrome.** Soft shadows only (`shadow-lg shadow-black/20`
– `shadow-black/40`) plus **radial lime glows**; hierarchy comes from the
surface ladder and 1-px lines, not from hard borders.
- **Rounded, restrained geometry.** `--radius: 1rem`; cards `rounded-2xl` (16),
inputs `rounded-xl` (12), buttons `rounded-lg` (10), status pills
`rounded-full`. Shape is a token decision, never a hard-coded zero.
- **Floating shell, no sidebar.** A floating centered command bar replaces the
old fixed rail; login and OAuth consent render chrome-free.
- **One product, one operator.** No theming, no light mode, no per-user
preferences. The surface looks the same for every operator.
Emotional response: precise, sober, in-control. The interface should read as a
control room — fast to scan, hard to misread.
## Colors
The palette is a zinc surface ladder plus a single lime accent, two alert tones,
and a zinc chart ladder that uses lime as `chart-1`.
- **Primary (#c6f24e — Lime)**: the only "happy" colour. Reserved for primary
actions, active nav items, active-state pills, and the chart's save series.
- **On-primary (#09090b)**: text colour on lime surfaces, so primary buttons
read as lime fill + near-black ink.
- **Background (#09090b)**: the canvas — every page and the floating command
bar share it.
- **Card (#101012)**: the base contained surface for cards, panels, and stat
tiles.
- **Raised (#18181b)**: raised / popover / secondary / muted surfaces that lift
above a card (dropdowns, sheets, secondary buttons, empty pills).
- **Accent (#1c1c1f)**: hover and subtle-selected background (row-hover, active
menu items).
- **Border (#1f1f23)**: 1-px borders on cards, tables, and chrome.
- **Input (#27272a)**: form-field background; **Ring (#3f3f46)**: focus rings.
- **Foreground (#fafafa)**: primary text. **Foreground-secondary (#a1a1aa)**:
secondary copy. **Foreground-muted (#71717a)**: labels, captions, meta.
- **Warn (#ff8c00 — Amber)**: reversible-but-cautious actions (abandon, archive,
supersede) and the chart's consolidation-ops series.
- **Destructive**: irreversible / impactful actions (delete session, revoke
token). Kept as the shadcn `--destructive` token.
- **Charts (`--chart-1`…`--chart-5`)**: `#c6f24e` → `#a1a1aa` → `#52525b` →
`#3f3f46` → `#27272a`; the zinc ladder with lime reserved for the primary
series.
### The chrome-only rule
On **light** surfaces lime is **chrome only** — fills, borders, dots, and bars.
**Never lime text on a light background.** On the dark dashboard lime text is
legitimate (active nav item, active-state pill, positive metric), but it stays
rationed: one lime decision per screen.
### Implementation
Tokens are defined as CSS custom properties in
`apps/web/src/app/globals.css` and mapped into Tailwind v4 via `@theme inline`,
so utilities such as `bg-background`, `bg-card`, `bg-popover`,
`text-foreground`, `text-muted-foreground`, `border-border`, `bg-primary`,
`text-warn`, and `text-destructive` resolve to the tokens. Use the utilities and
variables, never raw hex.
| Token | CSS variable | Tailwind utility |
| ---------------------- | --------------------------- | ----------------------------- |
| `primary` | `var(--primary)` | `bg-primary` / `text-primary` |
| `on-primary` | `var(--primary-foreground)` | `text-primary-foreground` |
| `background` | `var(--background)` | `bg-background` |
| `card` | `var(--card)` | `bg-card` |
| `raised` | `var(--popover)` | `bg-popover` / `bg-secondary` |
| `accent` | `var(--accent)` | `bg-accent` |
| `border` | `var(--border)` | `border-border` |
| `input` | `var(--input)` | `bg-input` |
| `ring` | `var(--ring)` | `ring-ring` |
| `foreground` | `var(--foreground)` | `text-foreground` |
| `foreground-secondary` | (zinc step) | `text-zinc-400` |
| `foreground-muted` | `var(--muted-foreground)` | `text-muted-foreground` |
| `warn` | `var(--warn)` | `text-warn` |
| `destructive` | `var(--destructive)` | `text-destructive` |
## Typography
Two families, each with a deliberate role:
- **Geist** drives UI and display output — titles, body copy, stat values. It is
wired to `--font-sans` and `--font-display`, giving the product a single clean
voice instead of a three-face stack.
- **Geist Mono** owns labels, counters, meta, and tabular data — table numbers,
durations, IDs, code snippets, section labels. Its tabular cadence is what
makes the dashboard read as an operator console.
Both are vendored as woff2 under `apps/web/src/app/fonts/` and loaded in
`apps/web/src/app/layout.tsx` with `next/font/local`. Use the CSS variables,
never the family name directly:
| Stack | CSS variable |
| ------- | --------------------------------------------- |
| UI | `var(--font-geist)` → `var(--font-sans)` |
| Display | `var(--font-geist)` → `var(--font-display)` |
| Mono | `var(--font-geist-mono)` → `var(--font-mono)` |
No CDN fonts and no second display face.
## Layout
The dashboard is laid out by a **floating command bar over a fluid content
column**. There is **no sidebar rail** — the old fixed 196 px rail is gone.
### Shell
`apps/web/src/components/dashboard/command-bar.tsx` renders `CommandFrame`:
a floating, centered header (`rounded-2xl border bg-card/90 backdrop-blur`) with
the primary nav, a "More" dropdown for secondary entries, badge counters, and a
mobile `Sheet` drawer. `apps/web/src/app/dashboard/layout.tsx` wraps every
authenticated route in it. Nav data lives in `apps/web/src/lib/nav.ts`
(`NAV` / `NAV_GROUPS`); add entries there and both the desktop bar and the mobile
sheet pick them up.
### Chrome-free screens
`/dashboard/login` and `/dashboard/oauth-consent` render **without** the frame.
They are listed in `CHROME_FREE_PATHS` in `apps/web/src/lib/nav.ts`, and
`CommandFrame` short-circuits on `isChromeFreePath()` — a route renders
standalone as soon as it is added to that list.
### Spacing
A strict **8-px scale** governs spacing:
| Token | Value | Use |
| ------ | ----- | ------------------------------ |
| `xs` | 4px | inner gaps in compact rows |
| `sm` | 8px | between siblings, button gaps |
| `md` | 12px | small block padding |
| `base` | 16px | default block padding |
| `lg` | 24px | section padding, card body |
| `xl` | 32px | main vertical rhythm |
| `2xl` | 48px | wide-screen horizontal padding |
| `3xl` | 64px | page bottom padding |
### Responsive
Below **768 px** the command bar collapses to a compact bar with a `Sheet`
drawer, page content stacks to a single column, and tables stay scrollable
inside their panel rather than pushing page width. Minimum supported viewport:
**320 px** with no page-level horizontal scroll. Touch targets stay **≥44 × 44
px** on touch layouts.
## Elevation & Depth
Depth is conveyed by **tonal layers, soft shadows, and glows** rather than heavy
borders:
- **Tonal layers**: canvas `#09090b`; cards `#101012`; raised surfaces
`#18181b`; hover accent `#1c1c1f`. Each step is small — felt, not seen.
- **Soft shadows only**: `shadow-lg` with `shadow-black/20`–`shadow-black/40`.
There is no hard drop-shadow vocabulary and no stacked shadow decoration.
- **Radial lime glows**: a faint radial gradient anchors key surfaces, e.g.
`bg-[radial-gradient(640px_260px_at_50%_-60px,rgba(198,242,78,0.09),transparent_70%)]`.
This is the signature "lit from within" depth cue from the redesign mockups.
- **1-px borders** in `#1f1f23` separate chrome and panels where a line is
clearer than a tonal step.
Hovers shift tone (surface or lime), they do not enlarge or float the element.
## Shapes
The shape language is driven by the `--radius` token scale (`--radius: 1rem`)
and expressed in Tailwind utilities:
| Role | Radius | Utility |
| -------------- | ------ | -------------- |
| Cards / panels | 16 px | `rounded-2xl` |
| Inputs | 12 px | `rounded-xl` |
| Buttons | 10 px | `rounded-lg` |
| Status pills | full | `rounded-full` |
Keep the scale restrained. Character comes from the monochrome palette, the lime
accent, the typography, and 1-px lines — not from heavy rounding or mixed
radii. Don't hard-code a corner radius; use the scale so shape stays consistent
across primitives.
## Components
Server components compose the new identity directly. Shared primitives live in
`apps/web/src/components/dashboard/` and `apps/web/src/components/ui/`.
### Buttons
`Button` (shadcn) drives tone, and tone semantics are **load-bearing**:
| Variant | Tone | When to use |
| --------------- | ---------- | --------------------------------------------------------------------------------- |
| `default` | Lime fill | Confirm / save / sign-in / filter — the affirmative action |
| `secondary` | Raised | Cancel, dismissive, navigation |
| `outline`/ghost | Borderless | Secondary or low-emphasis controls |
| `destructive` | Red | Irreversible (delete session, revoke token, bulk remove) |
| warn-toned | Amber | Reversible-but-cautious (abandon session, archive memory, mark relation orphaned) |
Primary buttons are **lime fill + near-black text**. Every destructive button
(warn/danger) MUST live inside a `<ConfirmSubmit>` inside its `<ActionForm>` so
the confirmation dialog opens before submit.
### StatusPill
`StatusPill` (`apps/web/src/components/dashboard/ui.tsx`) is the shared,
app-wide status pill: a `rounded-full` container with a dot plus a tinted
background/border per tone (`TONE_PILL`), driven by the `STATUS_TONE` map
(active/judged → lime, abandoned/superseded → amber, archived/ended/pending →
dim, orphaned/deleted → danger). Use it for every status; never hand-roll a
second badge.
### Spectrum data-table (list views)
Every list/table page goes through `DataTable` from
`@/components/spectrumui/data-table` with `variant="panel"`. The shared engine
provides:
- **quick-filter pills** with live counts,
- **client search**,
- **selectable rows** with a rising **bulk-action bar**,
- a contextual **`⋯` row menu** (`rowActions`),
- a **detail disclosure** (`renderDetail`),
- and `pageSize` for client-side pagination.
`apps/web/src/components/dashboard/sessions-table.tsx` is the **canonical
implementation** to copy. Siblings in the same pattern: `judgments-table.tsx`,
`prompts-table.tsx`, `memories-table.tsx`, `entities-table.tsx`,
`tokens-table.tsx`.
### Forms & mutations
Every mutation is a **server action through `ActionForm`**
(`@/components/dashboard/action-form`). Put a `<CsrfField form={FORM_NAME} />`
(`csrf-field.tsx`) in it and start the action with
`guardAction(formData, FORM_NAME)` / `guardFailure(...)`
(`@/lib/actions/guard`), which enforces session + admin + CSRF. The action
returns `ActionState = { error: string | null }`.
Inputs use the shadcn primitives bound to the tokens (`bg-input`,
`border-border`, `rounded-xl`). There is no second styling system.
### ConfirmSubmit
`ConfirmSubmit` (`confirm-submit.tsx`) wires a Radix `AlertDialog` to the
enclosing `ActionForm` via `useActionFormId`, so it only works **inside** an
`<ActionForm>`, wrapping a `<Button type="button">` trigger. Pass
`tone="danger"` for irreversible actions, `"warn"` for reversible ones.
### Time
`<Time>` (`time.tsx`) emits `<time dateTime data-rembric-ts>` with a UTC
fallback and re-renders in the viewer's timezone after mount. Never hand-write
`toISOString` / `toLocaleString` in views.
### Nav
Nav entries live in the `NAV` array (`lib/nav.ts`): group, `num`, label, `href`,
lucide `icon`, optional `badgeKey`. The command bar and its mobile sheet both
read it. Chrome-free routes go in `CHROME_FREE_PATHS`.
### Charts
`activity-chart.tsx` is the reference for lime/zinc bar visuals: lime for saves,
amber for superseded/archived ops, rounded caps, hover tooltips on `bg-popover`
with soft `shadow-black/40`.
## Do's and Don'ts
**Do**
- Use the Tailwind utilities bound to the tokens in `globals.css` for every
colour, spacing, and type reference. Never raw hex.
- Use `StatusPill` for status and the Spectrum `DataTable` for list views —
copy `sessions-table.tsx` rather than inventing a table.
- Route every mutation through `ActionForm` + `CsrfField` + `guardAction`.
- Wrap every destructive action in `ConfirmSubmit` and pick the right tone
(`warn` vs `danger`) — that's a semantic decision, not aesthetic.
- Add new nav entries to `NAV` (and chrome-free routes to `CHROME_FREE_PATHS`)
so the command bar and mobile sheet stay in sync.
- Put the ⋯ menu's "View details" as a `next/link`; there is no `data-href` /
`ROW_LINK` script anymore.
- Send timestamps through `<Time>`.
- Maintain WCAG AA contrast (4.5:1 for body text). The palette satisfies this;
don't add colours that don't.
- Keep touch targets ≥44 × 44 px on touch layouts.
**Don't**
- Don't introduce new CSS files or inline `<style>` blocks — style with Tailwind
utilities bound to `globals.css`.
- Don't add a second client framework or a competing styling system on top of
React + Tailwind v4.
- Don't use lime text on a light surface, and don't spend more than one lime
element per decision unit.
- Don't hard-code a corner radius; use the 16 / 12 / 10 scale.
- Don't layer decorative `box-shadow`s. Depth comes from tonal layers, soft
shadows, and radial lime glows.
- Don't add a CDN at runtime — fonts are self-hosted; the dashboard must work
offline.
- Don't mix tone semantics. `Abandon` is warn, `Delete` is danger. Don't
downgrade a delete to warn to "soften" it — the colour is the warning.
- Don't reach for the legacy primitives still exported from `ui.tsx` /
`filters.tsx` (see **Pending items**) when building or restyling a page.
---
## Recipe: add a new dashboard page
1. **Route**: create `apps/web/src/app/dashboard/<name>/page.tsx` as an async
server component with `export const dynamic = 'force-dynamic'`. `searchParams`
is a `Promise` in this Next.js version — `const params = await searchParams`
before reading. Data comes from `getServices()` (`@/lib/services`).
2. **Compose** the page from the new identity: a Spectrum `DataTable` for lists,
`StatusPill` for status, `<Time>` for timestamps, `ActionForm` +
`ConfirmSubmit` for mutations.
3. **Nav**: add the entry to the `NAV` array in `apps/web/src/lib/nav.ts`; the
command bar and mobile sheet read it. Add a chrome-free route to
`CHROME_FREE_PATHS` if it must render standalone.
4. **Table**: if the page lists rows, create
`apps/web/src/components/dashboard/<view>-table.tsx` by copying
`sessions-table.tsx` and swapping the columns.
5. **Tests**: add a co-located page test under
`apps/web/src/test/dashboard/<name>.test.tsx`.
## Recipe: a destructive action
A server action with `guardAction`, a matching `<CsrfField>`, and a
`<ConfirmSubmit>` wrapping the trigger inside the same `<ActionForm>`:
```tsx
<ActionForm action={revokeToken}>
<CsrfField form={FORM} />
<ConfirmSubmit
tone="danger"
title="Revoke token"
description="Revoke this token? This is IRREVERSIBLE. Any agent using it loses access immediately."
confirmLabel="Revoke token"
>
<Button type="button" variant="destructive">
Revoke
</Button>
</ConfirmSubmit>
</ActionForm>
```
Skip confirmation for undelete, unarchive, rename, and create (benign or
trivially reversible).
## Recipe: row navigation
There is no `data-href` / `ROW_LINK` script anymore. Row navigation lives in the
`⋯` menu as a `next/link`:
```tsx
rowActions={(row) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" aria-label="Row actions">
<MoreHorizontal className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<Link href={`/dashboard/sessions/${row.id}`}>View details</Link>
</DropdownMenuItem>
{/* destructive items: confirm via ConfirmSubmit inside the ActionForm */}
</DropdownMenuContent>
</DropdownMenu>
)}
```
Bulk selection exposes the same actions in the table's rising bulk-action bar.
## Client components
Interactivity is React client components, not inline scripts. The shared ones:
| Component | Purpose |
| --------------------------- | ---------------------------------------------------------- |
| `command-bar.tsx` | Floating command bar, More dropdown, mobile `Sheet` |
| `time.tsx` | Locale-renders `<time data-rembric-ts>` after mount |
| `action-form.tsx` | `ActionForm` + `useActionFormId` for server-action forms |
| `confirm-submit.tsx` | Radix `AlertDialog` bound to the enclosing form |
| `activity-chart.tsx` | Lime/amber bar chart with hover tooltips |
| `spectrumui/data-table.tsx` | List-view engine: search, quick filters, selection, detail |
## Reference files
```
apps/web/src/app/dashboard/layout.tsx CommandFrame wrapper
apps/web/src/app/dashboard/**/page.tsx one async server component per route
apps/web/src/components/dashboard/command-bar.tsx floating command bar + shell
apps/web/src/components/dashboard/sessions-table.tsx canonical Spectrum data-table
apps/web/src/components/dashboard/ui.tsx StatusPill (live) + legacy primitives
apps/web/src/components/spectrumui/data-table.tsx shared list-view table engine
apps/web/src/components/dashboard/action-form.tsx ActionForm + useActionFormId
apps/web/src/components/dashboard/csrf-field.tsx CsrfField
apps/web/src/components/dashboard/confirm-submit.tsx ConfirmSubmit
apps/web/src/components/dashboard/time.tsx Time
apps/web/src/components/dashboard/activity-chart.tsx lime/zinc bar chart
apps/web/src/components/dashboard/support.ts PAGE_SIZE, queryWithPage, …
apps/web/src/lib/nav.ts NAV / NAV_GROUPS / CHROME_FREE_PATHS
apps/web/src/app/globals.css Tailwind v4 theme + design tokens
apps/web/src/app/layout.tsx Geist / Geist Mono localFont wiring
apps/web/src/test/dashboard/*.test.tsx per-page tests
.agents/skills/rembric-dashboard-ui/SKILL.md canonical identity doc (v1.1)
odd/tasks/dashboard-identity-redesign.md redesign task file (pending items)
```
---
## Superseded identity — brutalist editorial (2026-09 redesign)
**Historical record. Superseded — do not use for new work.**
The identity described in this section governed the Rembric dashboard until the
**2026-09 redesign (branch `feat/dashboard-identity-redesign`)**. It is kept here
so the record of why the surface once looked the way it did is not erased. Every
token, font, and pattern below is **superseded** by the zinc/lime/Geist system
documented above.
The dashboard began as a **brutalist editorial** design system: a near-black
canvas (`#0a0a0a`) with coal cards (`#141414`), a coal-with-lime-tint row hover
(`#15170d`), and iron borders (`#2a2a2a`); hero titles in Space Grotesk, body
copy in Inter, and labels in JetBrains Mono; a `--radius` scale that leaned to
hard 0-radii; and a fixed **196 px sidebar rail** (`196 px`, collapsing to
`56 px`). It was framed as "operational, terse, unambiguous — like an
oscilloscope, not a SaaS landing page," with the `hl-lime` lime-block title
(`REMBRIC <PAGE>.`) as its signature stamp.
**Why it was replaced.** The fixed sidebar rail, the hard-edged zero-radius
primitives, the three-face type stack, and the flat tonal palette read as
heavier and less scannable than the current floating command bar over a layered
zinc surface, and the old surface ladder was too compressed to carry depth
without borders. The redesign (owner-approved, explored in
OpenPencil) moved to a two-face Geist stack, a wider surface ladder with soft
shadows and radial lime glows, a floating shell, and the Spectrum UI
data-table listing model.
Superseded artifacts, retained only as history:
| Superseded token | Old value | Status in the new identity |
| ------------------ | --------- | --------------------------------- |
| `primary` | `#c6f24e` | retained (still the only accent) |
| `on-primary` | `#0a0a0a` | replaced by `#09090b` |
| `neutral` | `#0a0a0a` | replaced by the background token |
| `surface` | `#141414` | replaced by `#101012` (card) |
| `surface-hover` | `#15170d` | replaced by `#1c1c1f` (accent) |
| `on-surface` | `#f2f2f2` | replaced by `#fafafa` |
| `on-surface-dim` | `#9a9a9a` | replaced by `#71717a` (muted) |
| `on-surface-faint` | `#2a2a2a` | replaced by `#1f1f23` (border) |
| `warn` | `#ff8c00` | retained (amber) |
| `danger` | `#ff3344` | replaced by the destructive token |
- **Type**: Space Grotesk (display), Inter (body), JetBrains Mono (labels) —
all replaced by Geist / Geist Mono.
- **Shell & scripts**: the `.sb` sidebar, `renderPage()`, `shell()`, HTMX,
`data-href` / `ROW_LINK`, and the inline `TS_UPGRADER` / `MOB_TOGGLE` /
`SB_COLLAPSE` / `CONFIRM` scripts — the stack was Hono + HTMX + SSR helpers
under `src/dashboard/`, which no longer exists.
- **Primitives**: `viewHead`, `statCard`, `pager`, the hard-bordered `Pill`,
`FilterForm` / `Pager`, and the `app-sidebar.tsx` rail — now **LEGACY**
(see below).
## Pending items
- **Legacy primitives not yet restyled.** The consolidation and maintenance
pages (plus several detail pages) still carry the pre-redesign primitives
exported as LEGACY from `apps/web/src/components/dashboard/ui.tsx`
(`ViewHead`, `SectionBar`, `StatCard`/`StatGrid`, `Kv`/`KvGrid`,
`Panel`/`PanelHead`, `DataTable`, `Pill`/`ReviewPill`/`Chip`/`Tag`, `Bar`,
`Notice`, `Flash`) and from `filters.tsx` (`FilterForm`/`Pager`). When those
pages are restyled, replace the legacy primitives with the new identity
(`StatusPill`, the Spectrum `DataTable`), not with another legacy primitive.
- **Formal OpenSpec change pending.** The token unlock has not yet been
formalized. Until the `dashboard` OpenSpec spec is updated and merged, the
**live reference is the branch code** — `sessions-table.tsx`,
`command-bar.tsx`, `activity-chart.tsx`, `ui.tsx`, and `globals.css` — and the
canonical identity doc is `.agents/skills/rembric-dashboard-ui/SKILL.md`.
Tracked as task 9 in `odd/tasks/dashboard-identity-redesign.md`. Do not edit
`openspec/specs/dashboard/spec.md` or archived `design.md` files from UI work;
that is a separate task.