📐 Sibyl Dashboard, Canonical Component Styleguide

The single source of truth for the dashboard's reusable component vocabulary. This catalog is Sibyl-owned and hand-maintained, it is no longer a Prometheus port (the sync script is retired). When applying the styleguide to the app or building a new component, this page is authoritative: the app should change to match this, not vice-versa.

How to read each section: a guidance block (what it is · when / when-not to use · sizing & spacing · variants · do/don't · which inconsistent in-app patterns it supersedes), then the canonical markup, then a live rendered example. Icons: emoji for wayfinding (sidebar/section/breadcrumb), flat Lucide SVG for all buttons.

Action toolbar · Avatar / user chip · Batch action bar · Breadcrumb page header · Buttons · Cards · Charts (Chart.js) · Code & markdown surfaces · Collapsible content sections · Color tokens · Confirmation gate · Expandable table rows · Forms · Inline divider · Layout grids · Loading & empty states · Modal · Mode switch · Nav count badge · Numeric formatting · Pill-filter row · Pills & status badges · Progress bars · Search + dropdown filter bar · Segmented stat cards · Side panel / drawer · Sidebar navigation, 4-level expandable header system · Sparklines · Stat grid · Status dot · Sticky apply bar · Tables · Tabs · Toast · Typography

Action toolbar - CANONICAL page-level action bar

The horizontal action bar at the top of a page, Back on the left, a vertical divider, then per-record actions grouped by kind, with an optional utility affordance on the right.

What
A single .card row of outline buttons with a flat single-color Lucide SVG icon on each. Layout: Back (blue outline, ←) → a .button-toolbar-divider (1px vertical line) → action buttons grouped by kind in fixed order (lifecycle → note → investigation → read) → optional right-side affordance (Generate summary, Export, etc.). The Back divider is the only inter-button separator; kind groups inside the action span carry no visible divider.
When
Top of a page that operates on a single record (item detail, schedule detail, dig detail).
Placement
Default: directly below the page title / breadcrumb header. If the page has tabs, the toolbar lives under the tab row instead, it becomes the first thing the operator sees after switching tabs. That way, each tab can swap in its own action set (a Notes tab surfaces note actions, a Digs tab surfaces investigation actions, etc.) without making the toolbar above the tabs choose a one-size-fits-all action list.
When not
List/index pages → no toolbar (those use header-row buttons via the breadcrumb header's right-side action slot). Modals → use a .button-row.spread footer. Per-row table actions → see Tables → Row actions.
Sizing
card padding 10px 14px · margin-bottom 12px · flex, align-items center, flex-wrap wrap, gap 6px · each button padding 6px 14px · radius 4px · 12px font · transparent background, 1px colored border, color matches border (outline-only). Icons use .icon (14px, flat single-color Lucide SVG, stroke="currentColor" so they inherit the button's kind color). The Back divider is .button-toolbar-divider (1px × 22px, var(--border)).
Variants
Pick whatever color reads as appropriate for the action, both the role-based palette (.primary blue, .danger red, .ghost muted) and raw color tokens (var(--blue)/--green/--yellow/--red/--muted/--text) are fair game. One useful default scheme, used in the example below and in item-detail.js, keys color by action kind:
  • lifecyclevar(--blue), Back, Triage, Start, Block, Unblock, Defer, Close
  • notevar(--text), Add note, Mark notes seen, Mark notes processed
  • investigationvar(--green), Spawn dig, Synthesize, Re-triage, Replay probe
  • readvar(--muted), Copy link, Open in CLI, Dependent items, Probe target
A right-side affordance (Generate summary, Regenerate summary…) typically carries the muted-border style of a disabled-ish read button when inactive, and a stronger color when active. In-flight states render disabled with cursor:not-allowed and a "doing X…" label.
Do
✓ Always lead with Back followed by a .button-toolbar-divider · use a flat Lucide SVG on every button · use outline style (transparent bg + colored border + colored text) for every button in the bar
Don't
✗ Use emoji icons (⛔ ✅ 📝 🔬 🔗 etc.) instead of flat SVG icons
Canonical, supersedes: ad-hoc per-page top-of-detail action rows; emoji-prefixed action buttons. Live in sibyl-reviews/src/public/pages/item-detail.js:_idRenderActionBarCard (the Back / Block / Defer / Close / Add note / … / Generate summary bar at the top of every item-detail page).
<div class="card" style="padding:10px 14px;margin-bottom:12px;display:flex;gap:6px;align-items:center;flex-wrap:wrap">
  <button style="…outline blue…"><svg class="icon" …>…arrow-left…</svg> Back</button>
  <span class="button-toolbar-divider"></span>
  <!-- lifecycle group (blue) -->
  <button style="…outline blue…"><svg class="icon" …>…ban…</svg> Block</button>
  <button style="…outline blue…"><svg class="icon" …>…pause…</svg> Defer</button>
  <button style="…outline blue…"><svg class="icon" …>…check…</svg> Close</button>
  <!-- note group (text color) -->
  <button style="…outline text…"><svg class="icon" …>…file-plus…</svg> Add note</button>
  <!-- investigation group (green) -->
  <button style="…outline green…"><svg class="icon" …>…flask…</svg> Spawn dig</button>
  <!-- read group (muted) -->
  <button style="…outline muted…"><svg class="icon" …>…link…</svg> Copy link</button>
  <!-- right-side affordance -->
  <button style="…outline muted…">Generate summary</button>
</div>

Back sits alone on the left, separated by a 1px .button-toolbar-divider. The right-side Generate summary affordance uses the muted-border treatment until a summary exists.

Avatar / user chip

28px circular identity chip, photo if avatar_path is set, otherwise an initial on var(--blue).

What
28px border-radius:50% chip. Photo variant: <img> with object-fit:cover. Initial fallback: uppercase first letter on a solid var(--blue) background, white text, centered.
Seen at
sidebar user block index.html:212; Users admin users.js:121–124, users.js:214.
J A Jjdog@sibyl.llc

Batch action bar

Header strip above a table that appears only when one or more checkbox-selected rows exist, shows a contextual action button.

What
Flex row, initially display:none; flips to display:flex when selection count > 0. Carries action button(s) labeled with the count ("Investigate Selected (3)").
Seen at
whale-watcher.js:599, whale-watcher.js:766, whale-watcher.js:1469.
Distinct from
The action toolbar (page-level, always present); this bar is table-local and selection-driven.
3 selected

Buttons - role-based + Lucide SVG

Outline-only, semantic color roles, flat single-color SVG icon on the left.

What
Buttons grouped in .button-row (or .button-toolbar for detail-page action bars). Color = meaning.
When
green submit/.success = commit (Save/Run) · blue .primary = forward/additive (Add/Open) · default = secondary (Refresh) · red .danger = destructive (Delete/Cancel) · ghost = reversible tertiary (Archive).
When not
Never solid-fill buttons; never inline-styled buttons; never the old .btn-action or per-module button DSLs.
Sizing
padding 4px 11px · radius 5px · 12px/500 · icon 14px inheriting currentColor · 8px gap in a row.
Variants
.button-row.spread (secondary left / primary right) · .button-row.right · .button-toolbar + .button-toolbar-divider.
Do
✓ Icon left via renderIcon(); pick role by intent
Don't
background:var(--blue);color:#fff solid buttons (7 files do this)
Canonical, supersedes: ~33 files of inline-styled buttons, the undefined .btn/.btn-sm usage, and the 7 private _wwBtn* constants in whale-watcher.js:219.
<div class="button-row">
  <button type="submit">${renderIcon('save')} Save</button>
  <button class="primary">${renderIcon('plus')} Add</button>
  <button class="danger">${renderIcon('trash')} Delete</button>
  <button class="ghost">${renderIcon('archive')} Archive</button>
</div>

Cards

General-purpose container for grouped content.

What
.card, surface panel with border + radius + padding. Optional .card-header / .card-footer slots.
When
Group related content where a full-width table is overkill; KPI tiles via the .label/.value/.sub children.
When not
Full-width tabular data → use .table-wrap. A grid of metrics → use the .stats-grid.
Sizing
padding 16px · radius 8px · border 1px var(--border) · bg var(--surface). In a grid use minmax(240px,1fr) columns, 12px gap.
Variants
.card-header (flex title/action row) · .card-footer (top-bordered action row) · .label/.value/.sub KPI children.
Do
✓ Use .card for any bordered surface panel
Don't
✗ Hand-roll style="background:var(--surface);border:1px solid var(--border);border-radius:8px"
Canonical, supersedes: ~15 ad-hoc inline panel divs (whale-watcher.js:891, users.js:687, platform-config.js:218, backtest.js:614).
<div class="card">
  <div class="card-header"><h3>Title</h3><span class="pill yes">active</span></div>
  <div class="label">Net P&L</div><div class="value">$12,480</div>
</div>

Bare card, surface, border, padding.

With header

active
Net P&L
$12,480
last 24h

With footer

Body content.

Charts (Chart.js)

Chart.js conventions: dark theme, disabled legend by default, custom tooltips, fixed-height container. Heavy usage across 20+ pages but no canonical spec yet.

Types in use
line, bar (stacked + grouped), scatter / bubble, doughnut.
Conventions
Dark canvas; grid lines = var(--border); ticks = var(--muted); legend usually disabled; tooltip callbacks.label overridden for unit-aware formatting; canvas wrapped in a fixed-height position:relative div.
Seen at
line advanced-analytics.js:415; bar advanced-analytics.js:446; doughnut advanced-analytics.js:543; bubble advanced-analytics.js:795, signal-quality.js:271.
[Chart.js canvas placeholder, fixed-height container, dark theme, disabled legend]

Live preview omitted; the styleguide page doesn't bundle Chart.js. Add a working preview once we settle on the canonical chart-options blob.

Code & markdown surfaces

Inline <code>, monospace <pre> blocks, and the .tc-md wrapper used to render LLM markdown.

Inline code
<code> for config keys, parameter names, short literal values. Seen at strategy.js:49, backtest.js:899.
Pre block
Multi-line output (raw JSON, smoke-test logs), dark bordered surface, Courier New monospace, sometimes with a copy-to-clipboard affordance. Seen at smoke-test.js:309, strategy-analyst.js:1161.
Markdown wrapper
.tc-md = marked.parse() output with scoped overrides for blockquote, code, pre, details/summary, and heading sizes. Used for LLM-produced content. Seen at triage-console.js:244–277, item-detail.js:3236, claude-sessions.js:68.

Inline: tune HOUR_WEIGHT_PROFILE via the COE.

{
  "change_kind": "synthesis_decision",
  "confidence_band": "high",
  "scope": "SHARED"
}

Collapsible content sections

Native <details>/<summary> blocks used outside the sidebar, for collapsible logs, hidden detail panels, expert-mode toggles.

What
Native HTML disclosure block. Sometimes styled with a chevron and inner CSS scope; sometimes bare. The sidebar nav already uses this internally, but standalone content usage is a separate pattern.
Seen at
crypto-probe.js:1443, tuning-center.js:419, whale-watcher.js:975, triage-console.js:1757.
Expert-only · raw decision brief JSON
Click the summary to expand/collapse this content.

Color tokens

The full palette. Always use the CSS variable, never a raw hex.

What
The dashboard's design tokens, defined on :root. Values here are aligned 1:1 with the live app shell.
When
Every color reference in CSS or inline styles.
When not
Never hardcode hex (e.g. #3fb950), it breaks theming and diverges over time.
Variants
--surface/--blue/--green/--yellow/--red are the canonical names; --panel/--accent/--ok/--warn/--err are legacy aliases (same values) kept only for ported CSS.
Do
color: var(--green)
Don't
color: #3fb950
Canonical, supersedes: ~40 hardcoded hex values in whale-watcher.js (#3fb950/#f85149/#d29922/#f0883e) and scattered inline colors.
--bg #0d1117 --surface #161b22 --text #e6edf3 --muted #8b949e --blue #58a6ff --green #3fb950 --yellow #d29922 --red #f85149

Confirmation gate

Native confirm('…') guard used uniformly before destructive operations, delete, promote, rollback, mass-approve.

What
One-line guard: if (!confirm('Delete user X? This cannot be undone.')) return;. Message phrasing pattern: action verb + object + consequence note.
Seen at
platform-config.js:390, whale-watcher.js:1889, users.js:604, regime-tuner.js:431 (15+ call sites total).
Distinct from
The custom modal (used for multi-step interactions); native confirm is the canonical lower-fi guard for "are you sure?" before destructive verbs.

Example call sites (not rendered, confirm() is a browser dialog):

if (!confirm('Delete user ' + name + '? This cannot be undone.')) return;
if (!confirm('Promote whale candidate ' + id + ' to live tracking?')) return;
if (!confirm('Roll back to revision ' + rev + '? Newer changes will be lost.')) return;

Expandable table rows

Click a row to insert a full-width colspan detail row below it; click again to collapse. Toggle state tracked in a JS Set.

What
Row gets a chevron/caret cell; click toggles insertion of an immediately-following <tr> with one <td colspan=N> holding the detail panel.
Seen at
advanced-analytics.js:1215, overview.js:196, optimization-lab.js:1034, calibration.js:519.
Distinct from
The drilldown-into-detail-page navigation (which jumps to a new route); expandable rows keep the operator on the same page.
MarketEdgeStatus
BTC > $80k EOD4.1%open
Detail panel, last 5 fills, slippage stats, source attribution. Closes when the parent row's caret is clicked again.
ETH > $4k EOD2.3%SIM
Fed cut March-1.0%skipped

Forms

One canonical input/select treatment, surface bg, border, 4px radius. Gallery below shows every common control so we can iterate on what stays / changes.

What
Text-class inputs (text, number, search, email, date…), <textarea>, <select>, plus the native widgets (checkbox, radio, range, file, color). Text/select fields share one treatment: var(--surface) background, var(--border) 1px border, 4px radius, 12px text. Native widgets keep their browser-default chrome (checkbox/radio dots, range track, file button) and inherit the page font.
When
All form controls. One token only for field backgrounds, var(--surface).
When not
Don't mix --bg/--bg2 backgrounds (today three different tokens are used interchangeably).
Sizing
text/select padding 4px 8px · radius 4px · 12px text · label 11px muted uppercase above the field with 4px gap · helper/error text 11px below the field with 4px gap. Textareas: same field treatment + min-height: 80px and resize:vertical only.
States
:disabled drops opacity to ~.55 and switches cursor to not-allowed; readonly keeps full opacity but removes the focus ring. aria-invalid="true" swaps the border to var(--red). Required fields lead the label with an asterisk styled color:var(--red).
Do
background:var(--surface);border:1px solid var(--border) for every text and select field · keep labels on their own line above the field (not inline) · use the helper-text slot below the field for unit hints / format examples
Don't
--bg/--bg2/--fg field tokens · floating labels · inline placeholder-as-label (placeholders are examples, not labels)
Canonical, supersedes: 3 select background tokens used interchangeably (--surface vs --bg vs --bg2); the backtest-scoped .bt-form-input leaked into users.js.
Text input
Text input, filled
Required field Asterisk in the label · validate on submit (not on blur).
Number input
Search input type="search" · browser clear-X · debounce in the handler.
Email input
URL input
Password input
Date input
Time input
Prefix · USD
$
Prefix · units
min
Textarea
Select, single
Select, with optgroups
Select, multiple
Checkbox group
Radio group
Toggle switch (off)
Toggle switch (on)
Segmented (3-way)
Range slider, 75%
File input
Color picker
Disabled input
Read-only input
Error state Enter a valid email address.
With helper text Threshold in percent (0–100). Defaults to 2.5%.

Gallery is a sandbox, every variant is a candidate, not a commitment. Call out which look right, which to change, and which to drop.

Inline divider

1px vertical rule for separating groups of inline controls, used in filter bars, toolbars, header strips.

What
<span style="width:1px;height:16px;background:var(--border);margin:0 8px">, or the named .button-toolbar-divider already in style.css for the action toolbar.
Seen at
whale-watcher.js:581, whale-watcher.js:588, whale-watcher.js:595; .button-toolbar-divider usage in the action toolbar.
Open question
Two parallel implementations (inline span vs .button-toolbar-divider class), pick one canonical form during review.
7d · 30d SIM · LIVE All instances

Layout grids

.two-col / .three-col / .cards-N, the responsive grid wrappers that size cards across a page.

What
.two-col is a 1fr 1fr grid (gap 12px) that collapses to 1 column on narrow viewports; .three-col is 1fr 1fr 1fr; .cards-2 through .cards-6 are column-count modifiers on the .cards grid.
Seen at
strategy.js:151, backtest.js:603, kpi.js:211, whale-watcher.js:2697 (two-col); advanced-analytics.js:971 (three-col).
Win Rate
61.4%
Edge
3.2%

Col 1

Col 2

Col 3

Loading & empty states

One loading pattern, one empty pattern, no more 3-way split.

What
Loading: .loading-state + .spinner. Empty: .empty.
When
Loading while a page/section fetches; empty when a fetch returns no rows.
When not
Never use .empty as a loading state; never raw inline padding loaders.
Sizing
spinner 22px · loading-state column, 14px gap, 80px/40px padding, muted 13px · empty 24px padding centered muted.
Do
.loading-state+.spinner for every loader
Don't
class="loading", .empty-state, or inline padding:40px loaders
Canonical, supersedes: .loading plain text (6 files), .empty misused as loader (ab-testing.js:53, regime-tuner.js:602), undefined .empty-state (whale-watcher.js:680), inline loaders (settings.js:184).
Loading…
No results for this filter.

Mode switch

Binary or 2–3-way pill toggle for an inline state choice, distinct from tabs (page navigation) and pill-filter rows (category narrowing).

What
Button group inside a bordered rounded container; the active button gets background:var(--blue) and white text.
Seen at
overview.js:542, kpi.js:151, settlement.js:112, whale-watcher.js:2104, positions.js:33.
Distinct from
Tabs switch views; mode-switch flips a single parameter (e.g. ROI vs P&L, SIM vs LIVE, 7d vs 30d).

Numeric formatting

Cross-page conventions for displaying numbers, relative time, and signed deltas. Bundles three closely-related patterns the audit flagged.

Monospace value
font-family:'Courier New',monospace on headline numbers (KPI tiles, table-cell amounts) so digits align across rows.
Time-relative labels
Standardized "Xs ago" / "Xm ago" / "Xh ago" / "Xd Xh ago" via global helpers in app.js:106–131 (fmtAge, fmtAgo, fmtDuration). Reviews has a parallel local variant at item-detail.js:1069.
Signed-delta coloring
Positive = leading + + var(--green); negative = var(--red); neutral = var(--muted). Applied to ROI / P&L / win-rate deltas. Seen at kpi.js:1264, strategy-lab.js:591–601, backtest.js:648–651.
$12,480.55 3m ago +4.1% -2.3% ±0.0%

Pill-filter row

Clickable pill row for one-axis filtering, via renderCategoryBar() in _shared.js.

What
A row of .pill-filter toggles; .active marks the current selection.
When
Category/segment filtering above a table or chart. Prefer the shared renderCategoryBar() helper for category fields.
When not
Non-interactive status → .pill. Many options / free text → a <select>.
Sizing
padding 3px 10px · radius 12px · 11px · 6px gap between pills, 12px margin-bottom on the row.
Do
✓ Reuse renderCategoryBar() output shape
Don't
✗ Re-implement a pill-filter row inline per page
Canonical, supersedes: 6+ hand-rolled pill-filter rows (calibration.js:400, stock-probe.js:182, profile-attribution.js:108).
All Crypto Weather Politics Sports

Pills & status badges

Compact inline status chips with semantic modifier classes.

What
.pill + a semantic modifier (.yes/.no/.sim/.live/.off and the .bt-* bet-type set).
When
Inline state/flags in tables, headers, list rows.
When not
Clickable filters → .pill-filter (next section), not .pill.
Sizing
padding 1px 7px · radius 10px · 11px/600. Tinted bg at ~15% alpha, solid token text color.
Variants
.yes green · .no red · .sim yellow · .live red · .off muted · .bt-* bet types.
Do
✓ Pick the modifier by meaning, not color
Don't
✗ Inline border-radius chip spans or a local _catPill copy
Canonical, supersedes: 41 inline chip spans (whale-watcher.js:115, users.js:139), the duplicated _catPill in kpi.js:116.
active failed SIM LIVE disabled

Progress bars

Two related patterns: a plain linear fill bar, and a composite label-+-bar-+-value score row.

Linear bar
6–8px tall, var(--border) background, 3–4px radius, colored fill width:%. Used for score bars, coverage, AB-test progress, readiness scores. Seen at strategy.js:140, whale-watcher.js:207, advanced-analytics.js:1418, backtest.js:274.
Score-bar composite
Three-part flex row: fixed-width muted label → flex fill track + colored fill → right-aligned monospace numeric value. Seen at strategy.js:138–144, whale-watcher.js:207–225, advanced-analytics.js:1000.
Distinct from
The segmented bar inside segmented stat cards is a different pattern (multi-segment proportional).
Coverage · 72%
Regime confidence
88%
Signal quality
42%

Search + dropdown filter bar - CANONICAL multi-axis table filter

Horizontal toolbar combining free-text search with one or more single-select dropdowns, multi-axis filtering without consuming vertical real estate.

What
A .card row with: a type="search" input on the left (debounced, drives a q= query param), then one or more single-select <select> dropdowns whose default option is an icon-prefixed "All …" sentinel ("🏷️ All kinds", "🎯 All priorities").
When
Filtering a table where the operator wants to combine quick-typed text refinement (title / id / description) with structured dropdown narrowing (kind / priority / component / system). Each filter contributes one query parameter to the server-paged table below.
When not
Few mutually exclusive categories that should be one click → pill-filter row. No text dimension and only one or two booleans → an inline mode-switch.
Sizing
row card padding 12px · min-height 56px · 8px gap · flex-wrap · align-items center · search input 240px wide, 6px 10px padding · selects 6px padding · 12px font on every control · 4px radius · 1px border var(--border) · bg var(--bg) (NOT --surface) so controls read as inputs against the card.
Variants
Default sentinel options always lead with a category icon ("🏷️ All kinds", "🎯 All priorities", "📦 All components", "📋 All systems"). Per-option labels carry their own icon ("🔴 HIGH", "🐛 finding"). A boolean refinement always becomes its own two-option dropdown ("All / Only X"), never a trailing checkbox + label, which eats horizontal real estate for one on/off bit.
Do
✓ Use type="search" (gets the browser clear-X for free) · debounce the input handler · icon-prefix every dropdown sentinel · keep all controls on one wrapping row
Don't
✗ Stack the search above the dropdowns · drop a "Filter" submit button (filtering should be live) · use surface-colored controls (they blend into the card) · add a trailing checkbox + caption for a boolean filter (promote it to a 2-option dropdown instead)
Canonical, supersedes: ad-hoc per-page filter rows that mix label-above-input layouts with bare un-iconed selects. Live in sibyl-reviews/src/public/pages/triage-console.js (decisions / items / revisits sub-tabs all share this exact bar, see _tcRenderInboxDecisionsView).
<div class="card" style="padding:12px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;min-height:56px">
  <input type="search" placeholder="Search title, rationale, recommendation…"
         style="padding:6px 10px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;width:240px;font-size:12px">
  <select style="padding:6px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;font-size:12px" title="Item kind">
    <option>🏷️ All kinds</option>
    <option>🐛 finding</option>
    <option>📋 spec</option>
    <option>📡 probe</option>
  </select>
  <!-- additional dropdowns: priority, component, system… -->
</div>

Segmented stat cards - CANONICAL drill-through header row

Compact card showing a total, a proportional segmented bar, and a clickable bubble row that deep-links to a filtered table.

What
A .card with four stacked rows: title row (label on the left, total right-justified) → one-line muted caption8px segmented bar where each segment's width = its share of the total → wrap row of color-coded badge bubbles, one per segment, each clickable to jump to a pre-filtered view of the linked table.
When
Showing a population (open backlog, 24h activity, errors by class) AND letting the operator drill into any slice in one click. Two to four cards make a "command-center" header above a table.
When not
Single headline number → bare .card with .label/.value. 4–8 unrelated KPIs → the stat grid. Time-series → chart.
Sizing
card flex-basis 200–240px · padding 8px 12px · 6px gap between rows · label 10px uppercase muted with .04em tracking · total 12px/700 right-justified · caption 10px muted with margin-top:-4px · bar 8px tall, 4px radius, 1px border, var(--bg) fill behind segments · bubbles 2px 8px · 10px/600 · 10px radius · 6px gap.
Variants
Bar segment + bubble share one canonical palette per dimension (priority, kind, activity bucket). Bubbles carry a count prefix ("12 high") and an onclick that filters the linked table; zero-count bubbles stay clickable and jump to an empty filtered view (the affordance is the point).
Do
✓ Right-justify the total on the label line · use one shared palette across bar + bubbles · keep the bar at exactly 8px tall
Don't
✗ Pile the total on its own line · use different colors in the bar than in the bubbles · invent a third row of mini-numbers below the bubbles
Canonical, supersedes: 12-tile .stats-grid headers that flatten priority/kind into equal-weight boxes. Live in sibyl-reviews/src/public/pages/triage-console.js:_tcRenderInboxStats (above the Decisions table).
Open Items 87 total
not closed · excludes outcome checks
4 high 12 medium 51 normal 14 low 6 info
Open by Kind 142 total
actionable backlog + outcome tracking
68 find 14 spec 5 probe 55 outcome

Side panel / drawer

Right-edge full-height fixed panel for conversational / contextual content. Distinct from modal (centered overlay).

What
position:fixed;right:0;top:0;bottom:0 panel, 360–500px wide, border-left:1px solid var(--border), drop shadow. Slides in over main content; dismissed by clicking backdrop or a close button.
Seen at
chat panel at triage-console.js:1022–1038, triage-console.js:1765.
Distinct from
The modal (centered, modal-blocking); the drawer is edge-anchored and full-height for ongoing context.
Main content area…
Chat session
Conversation messages…

Sparklines

Inline SVG mini-charts (~60×14) for time-series data in compact cards, no Chart.js dependency.

What
Tiny normalized <svg><polyline> rendered by a local helper. Distinct from full Chart.js canvas because they sit inline with text and don't need axes or legends.
Seen at
system-health.js:1518–1529, strategy.js:96, whale-watcher.js:2606.
Latency 42ms Errors 17

Stat grid

The canonical "row of headline numbers", via renderParamsGrid() in _shared.js.

What
.stats-grid of .stat-box tiles, each .s-label + .s-value (+ optional .s-sub).
When
3–8 related metrics at the top of a page/section. Prefer the shared renderParamsGrid() helper.
When not
A single number → a .card with .label/.value. Tabular data → table.
Sizing
grid repeat(4,1fr), gap 12px · tile padding 14px · value 18px/700 mono.
Do
✓ Route through renderParamsGrid()
Don't
✗ Use the legacy .label/.value/.sub-in-card pattern for metric rows
Canonical, supersedes: 4 rival conventions, ad-hoc .stat-box (alpha-hunter.js:184), .label/.value/.sub tiles in 18 files, inline font-size:32px numbers (system-health.js:170).
Win Rate
61.4%
312 settled
Net P&L
$2,140
+4.1%
Open Risk
$780
6 positions
Edge
3.2%
avg

Status dot

8px solid color-coded health indicator, sits next to a label as a textless cue.

What
.dot class with .fresh (green) / .stale (yellow) / .dead (red) modifiers; 8px circle, flex-shrink:0. Also appears with inline backgrounds for per-instance colored dots.
Seen at
definition at style.css + index.html:386–389; usage at orchestrator.js:131, platform-config.js:453, instance-selector.js:76.
Distinct from pills
Pills carry text; dots are pure-color cues that sit before a separate label.
fresh stale dead Instance 17

Sticky apply bar

A position:sticky;top:0 strip that appears above content when there are unsaved settings changes, count + Save + Revert.

What
Sticky bar with pending-change count, a primary "Save" button, and (on platform-config) a revert affordance. Hidden when zero changes pending.
Seen at
settings.js:221–246, platform-config.js:210.
Distinct from
The action toolbar (always present on detail pages); this bar appears/disappears based on dirty state.
3 unsaved changes

Tables

Data grid, always wrapped in .table-wrap and wired via makeTableInteractive().

What
.table-wrap > <table> with <th class="sortable" data-col> headers; makeTableInteractive(id, …) adds sort + pagination + sticky header.
When
Every tabular dataset, even read-only ones (still wrap + register so scroll/paging is uniform).
When not
A handful of metrics → .stats-grid. Key/value pairs → a .card.
Sizing
12px cells · uppercase 11px muted headers · radius 8px on the wrap · horizontal scroll on overflow.
Variants
Sort via createSortState() · server paging via the remote option · noPagination for tiny tables.
Row actions
Per-row buttons (Edit, Delete, Investigate…) live in a single far-right "Actions" column, never scattered inline with data cells. The column is shrunk to fit the buttons via width:1% on the <th> (table-auto layout treats this as "as narrow as content allows" and gives leftover width to the data columns); cells inherit the table's white-space:nowrap so the button row never wraps. With the column shrunk, text-align:center on the header centers "Actions" directly over the button row. Wrap the buttons in a plain display:inline-flex;flex-wrap:nowrap;gap:6px;justify-content:flex-end container, not .button-row (its default flex-wrap:wrap would stack the buttons two-deep once the column tightens). Row buttons are sized one notch tighter than the canonical button (padding:2px 8px · font-size:11px · gap:4px · 12px icon) so they don't dominate the row visually. Every button carries a flat Lucide SVG icon (.icon, stroke="currentColor") so the glyph inherits the button's role color. Non-sortable header (no .sortable, no .sort-caret).
Do
✓ Give every table an id and pass it through the helper · put row actions in one far-right Actions column with right-justified buttons and a centered header
Don't
✗ Anonymous .table-wrap with no helper, or custom pager buttons · scatter buttons across multiple data columns or left-align the action cell (drifts away from the table edge as the column widens)
Canonical, supersedes: 20+ anonymous .table-wrap tables bypassing the helper (advanced-analytics.js:210, strategy-lab.js:141); custom pagers (ab-testing.js:843, settings.js:531); per-cell inline action buttons (users.js:412, whale-watcher.js:687) that the eye has to hunt for.
per page Page 1 of 1 0 results
Market Edge Status Actions
BTC > $80k EOD4.1%open
ETH > $4k EOD2.3%SIM
Fed cut March-1.0%skipped
NYC rain Friday3.7%open
SOL > $2001.2%open
NFL Sunday over0.8%SIM
Senate confirm-0.5%skipped
CPI > 3.2%2.9%open
SPX close > 53001.7%open
BTC > $100k EOY5.4%SIM
AVAX > $400.3%skipped
LA wildfire ext-1.4%skipped
SCOTUS ruling2.1%open
UK election early1.9%SIM

14 rows · default 10/page. Switch to 25 or All to land on "Page 1 of 1" with all four nav arrows disabled, the canonical page-bound state. The far-right Actions column shows the canonical row-button placement: centered header, right-justified buttons.

Tabs

In-page section switcher.

What
.tabs container of .tab items, each with an emoji .tab-icon + label; .tab.active is the current one (blue underline). The .tabs row always lives inside a horizontal-scroll wrapper (overflow-x:auto;white-space:nowrap;flex-wrap:nowrap) so tabs never wrap to a second line on narrow viewports, they scroll horizontally instead.
When
Switching views within one page (sub-pages of a hub, facets of a dataset). Every tab carries an emoji icon, tabs are wayfinding, so they use the emoji language (like the sidebar/breadcrumb), not button SVGs.
When not
Cross-page navigation → the sidebar. Binary mode → .mode-switch.
Sizing
tab padding 8px 16px · 13px · 6px icon gap · icon 14px · bottom-border 2px accent when active. Row has its own bottom border + 16px margin-bottom (don't override with margin-top:0). Scroll wrapper has no extra padding, the tab bar's own padding/border still aligns with the page gutter.
Do
✓ Always include an emoji .tab-icon; use the default margins · always wrap the .tabs row in a horizontal-scroll container so the bar scrolls instead of wrapping
Don't
✗ Icon-less tabs, SVG icons in tabs, or style="margin-top:0" per instance (15+ files do this today) · let tabs wrap to a second line (they become unreadable and break the active-underline anchor)
Canonical, supersedes: the near-universal inline .tabs margin-top:0 override; .tabs used as a toolbar via inline flex (platform-config.js:146); icon-less tabs across most pages; any tab row that wraps on narrow viewports.
<div style="overflow-x:auto;white-space:nowrap">
  <div class="tabs" style="flex-wrap:nowrap">
    <div class="tab active"><span class="tab-icon">🏠</span>Overview</div>
    <div class="tab"><span class="tab-icon">📋</span>Detail</div>
    <div class="tab"><span class="tab-icon">🕐</span>History</div>
  </div>
</div>
🏠Overview
📋Detail
🕐History
⚙️Settings
📊Metrics
🧪Experiments
🔔Alerts
👥Team

Eight tabs, shrink the window or zoom in until the bar overflows; it scrolls horizontally instead of wrapping.

Toast

Transient feedback, supersedes the 3 copy-pasted showToast copies and bare alert().

What
A .toast pinned bottom-right; left-border color = severity (.ok green / default blue / .err red). Auto-dismisses.
When
Success/info/non-blocking error feedback after an action.
When not
Decisions/confirmations → modal. Persistent inline validation → near the field.
Sizing
bottom/right 24px · padding 10px 16px · radius 8px · 13px/600 · z-index 1100 · 3px severity left-border.
Do
✓ One shared showToast() for all transient feedback
Don't
alert()/confirm() (~118 calls) or per-page toast copies
Canonical, supersedes: triply-duplicated showToast (alpha-hunter.js:430, governance.js:258, strategy.js:530) and ~118 raw alert()/confirm() calls across 16 files.
Info toast Saved. Request failed.

Typography

Base font is 14px system sans; numeric/monospace data uses Courier New.

What
Body text, headings, and the monospace numeric treatment for data values.
When
Numeric KPIs/metrics use the monospace 'value' treatment so digits align in tables and tiles.
Sizing
Body 14px/1.5 · section h2 14px/600 · stat value 18–24px/700 mono · labels 11px uppercase muted.
Do
✓ Use .value/.s-value for headline numbers
Don't
✗ Invent ad-hoc font-size:32px inline numbers
Body text, 14px / 1.5 line-height, var(--text).
Label, 11px uppercase muted
$12,480.55