BitFire style guide · v0.1
last updated 2025
Design System

Calm tools for noisy data.

BitFire surfaces traffic that matters — blocked attacks, suspicious bots, geo anomalies — without drowning operators in chartjunk. The interface is dense, monospaced, and quiet. Color is reserved for status. Motion is reserved for life.

01 · Foundations

Principles

Five rules that everything else descends from.

Density first
Operators read this dashboard for hours. Vertical rhythm should let them scan 30+ requests without scrolling. Padding is measured in 4px increments, not 8px.
Color encodes state, never decoration
Red means blocked. Amber means rate limited. Green means allowed. If something is colored, it means something. Don't use accent for hierarchy.
Mono for facts, sans for labels
URLs, IPs, codes, timestamps — JetBrains Mono. Section titles, pill labels, button text — Inter. The split makes scannability automatic.
One pulse, one accent
Only the live indicator pulses. Only the brand ember (oklch ~50°) is the accent. Adding more living things or more accent colors flattens the hierarchy.
Don't gradient surfaces
Surfaces stack with hairline borders, not gradients. No glassmorphism on data rows. Background tints belong to the status system (blocked rows, selected rows) — that's it.
Don't replace text with icons
Icons are scanning aids alongside text, never replacements. A "blocked" pill says "blocked" — even if it's red. Iconography is reserved for the brand mark, the live dot, and the result-rail.
02 · Foundations

Typography

Two families. Inter for prose and labels, JetBrains Mono for everything that's data: URLs, IPs, codes, fingerprints, timestamps.

Display & headings

Reserved for marketing, docs, and section heads in admin views. The product proper rarely goes above 14px.

Display
56 / 1.05 / -0.025em / 600
Calm tools.
H1
32 / 1.15 / -0.018em / 600
Section heading
H2
20 / 1.3 / -0.01em / 600
Subsection heading
H3
14 / 1.35 / 600
Inline heading

Body & UI

13px is the workhorse. Most data rows live here. Below 11px is reserved for labels and meta.

Body
13 / 1.4 / 400
An incoming GET to /api/v1/login from 198.51.100.42 was blocked under rule 601 (SQL injection attempt). The request had been challenged twice in the prior 90 seconds.
Body small
11.5 / 1.4 / 400
Secondary information — IPs, timestamps, secondary cell content. Always one rung dimmer than primary.
Meta / caption
10.5 / 1.45 / mono / 0.04em
RULE 601 · HTTP 403 · 14:22:17
Label / overline
10 / 1 / 600 / 0.1em / uppercase
Origin

Mono

JetBrains Mono with font-feature-settings: "ss01", "zero", "tnum". The slashed zero and tabular numbers are non-negotiable — operators scan IP addresses by column.

192.168.42.10 → /api/v1/users/42
fp_a8f30c · sig_3402 · 14:22:17.481
RULE 601 · HTTP 403 · oklch(0.52 0.18 25)
03 · Foundations

Color

Every color is defined in oklch() for perceptual uniformity. Each status color has a triplet — text, background, border — so any pill works on any surface without contrast hacks.

Surfaces

A four-rung stack. Page is the lowest, raised is the highest. Selected rows use the accent-tinted variant.

Foreground

Four levels of ink, never pure black or pure white. Use --fg-dim for secondary content, --fg-dimmer for meta, --fg-muted for placeholders.

Status

Five families. Each has --{name}, --{name}-bg, and --{name}-border. The brand accent (ember) is also a "status" — it marks the live, the now, the user's anchor.

Status semantics

ColorMeaningUsed on
redBlocked, denied, malicious, dangerousWAF rules, IP rep, blocked rows, DELETE method, exclude chips
amberRate limited, throttled, datacenter origin, tools (curl, requests)Rate-limit pill, mobile origin, hosting origin
greenAllowed, ok, healthy, residentialOK pill, env "live" indicator
blueInformational, neutral signal, good bots3xx redirect, POST method, CDN origin, include chips, googlebot
violetDatacenter, infrastructure, PUT/PATCHDatacenter origin pill, PUT method chip
emberBrand, accent, user anchorBrand mark, live count dot, focus rings, "you are here"
04 · Foundations

Spacing & shape

A 4px grid for component padding, 8px for layout gaps. Radii are subtle: 3px for chips, 5–6px for cards, 999px for pills.

Spacing scale

scale: 4 · 8 · 12 · 16 · 22 · 32 · 56

Radii

3px · chips
5px · cards
6px · panels
999px · pills

Borders

All separation is hairline. We never use shadows for boundaries — only elevation. --border is the default, --border-strong for hover states, --border-subtle for nested or alternating rows.

05 · Library

Components

The vocabulary. Each piece has a single purpose and a single semantic color rule.

Result pill

The status of a single request. Always rounded, always paired with a colored dot. The dot is decorative — the label carries meaning.

ok redirect not found blocked rate limited error
class: .result-pill.{ok|redirect|missing|blocked|ratelimit|error}

Method chip

HTTP method, inline before the URL. Default (GET, HEAD, OPTIONS) is neutral; verbs that mutate state get color.

GET POST PUT DELETE HEAD OPTIONS
class: .method-chip[data-method=".."]

Origin pill

Where the IP came from. Five canonical buckets. Color tells you risk profile at a glance — datacenter (violet) and proxy (red) are inherently more suspicious than residential.

residential mobile cdn datacenter proxy
class: .origin-pill.origin-{residential|mobile|cdn|datacenter|proxy}

Who tag

User-agent classification. browser is the only neutral; everything else is signal.

BROWSER TOOL GOOD BOT BAD BOT FAKE UA
class: .who-tag.{browser|tool|goodbot|badbot|fake}

Filter chip

A live filter. Include chips are blue, exclude chips are red. The leading button toggles AND/OR; the second toggles include/exclude; the trailing × removes.

url = /api/v1/login
country = CN
class: .chip.chip-{include|exclude}

Env pill & live dot

The "everything is fine" indicator. The dot pulses on a 2.5s breath. Only one of these per page — it's the user's anchor that the system is responsive.

live · demo.bitfire.app paused
class: .env-pill / .env-dot.live

Button: ghost

The default action. No fill until hover. Used for everything in the chrome — toolbar, filter bar, drawer header.

class: .btn-ghost

Theme toggle

Sun/moon icon in a pill. The icon swaps based on the active theme — moon when in day (click to go dark), sun when in dark (click to go light).

class: .theme-toggle
06 · Library

Patterns

How components compose into functional pieces of UI.

Log row

The atomic unit of the dashboard. Four cells: request, where, who, result. Each cell has a primary line (mono) and a secondary line (smaller, dimmer). A 3px left rail color-codes the row's status — invisible for OK rows, red for blocked, blue for redirect, amber for missing.

GET /api/v1/users/42
14:22:17 · 198.51.100.42
🇩🇪Frankfurt, DE
residential
?
Chrome 124
BROWSERWindows 11
ok
HTTP 200
POST /api/v1/search?q=' OR 1=1--
14:22:18 · 45.142.10.18
🇷🇺Moscow, RU
datacenter
?
curl/8.6
TOOLCLI
blocked
SQL injection attempt
rule 601 · HTTP 403
class: .lite-row[data-result] · .lite-row.is-blocked

Result rail

A 3px colored stripe down the left edge of each row. Lets you scan vertical patterns — a column of red is an attack burst, a column of blue is a redirect chain. Invisible for OK to keep the rail meaningful: only signal, never noise.

GET /healthz
14:22:01
ok
POST /api/v1/search?q=...
14:22:02
blocked
POST /api/v1/search?q=...
14:22:03
blocked
GET /old-link
14:22:04
redirect
GET /favicon.ico
14:22:05
not found
visual: .lite-row[data-result] ::before · 3px width

Reason hierarchy

For blocked / rate-limited rows the result cell stacks three lines, in descending priority: the pill (status), the reason (human, sans, tinted), and the sub (technical, mono, dim). Operators read top-down; juniors get plain English first, seniors can drop to the rule code.

blocked
Cross-site scripting attempt
rule 602 · HTTP 403
order: pill → reason (sans, tinted) → sub (mono, muted)
07 · Themes

Light & dark

One layout, two skins. The day theme is the source of truth; bitfire-lite-dark.css is a thin overlay that flips token values under body.theme-dark and adds a few dark-native flourishes (glow on the live dot, alternating row stripe, inset filter input).

Day defaults
Paper-white surfaces, hairline borders for separation, low-chroma status colors. Designed for sustained reading. The default for new users.
Dark for noisy environments
Deep neutral page, slightly warmer accent ember, glowing live indicator. Designed for SOC walls and night shifts. Color saturation is bumped so status reads against dark surfaces.

Toggle behavior

Both stylesheets load. Day owns layout; dark only flips tokens via body.theme-dark. Switching is instant — no FOUC, no layout shift. Preference is persisted to localStorage[bitfire.theme].

08 · Reference

Block reasons

Every blocked request carries a numeric rule code. The first digit groups by category. The UI translates these to one-line human-readable strings via BLOCK_REASONS in bitfire-lite.js.

CodeGroupReasonResult
1011xx · RateRate limit exceededrate limited
1021xx · RateBurst limit exceededrate limited
2202xx · IntegrityInvalid request signatureblocked
2212xx · IntegrityTampered headersblocked
3103xx · GeoCountry blocked by policyblocked
3113xx · GeoRegion blocked by policyblocked
4204xx · ChallengeFailed JS challengeblocked
4214xx · ChallengeFailed CAPTCHAblocked
5105xx · BehaviorBot score above thresholdblocked
5115xx · BehaviorKnown bot, no allow-listblocked
6016xx · WAFSQL injection attemptblocked
6026xx · WAFCross-site scripting attemptblocked
6036xx · WAFPath traversal attemptblocked
6046xx · WAFCommand injection attemptblocked
7017xx · IP repIP reputation: maliciousblocked
7027xx · IP repIP on threat feedblocked
8108xx · ManualManual block by operatorblocked